Skip to content

Commit 27d20be

Browse files
Merge #6116: fix: mitigate crashes associated with some upgradetohd edge cases
69c37f4 rpc: make sure `upgradetohd` always has the passphrase for `UpgradeToHD` (Kittywhiskers Van Gogh) 619b640 wallet: unify HD chain generation in CWallet (Kittywhiskers Van Gogh) 163d318 wallet: unify HD chain generation in LegacyScriptPubKeyMan (Kittywhiskers Van Gogh) Pull request description: ## Motivation When filming demo footage for #6093, I realized that if I tried to create an encrypted blank legacy wallet and run `upgradetohd [mnemonic]`, the client would crash. ``` dash@b9c6631a824d:/src/dash$ ./src/qt/dash-qt QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-dash' dash-qt: wallet/scriptpubkeyman.cpp:399: void LegacyScriptPubKeyMan::GenerateNewCryptedHDChain(const SecureString &, const SecureString &, CKeyingMaterial): Assertion `res' failed. Posix Signal: Aborted No debug information available for stacktrace. You should add debug information and then run: dash-qt -printcrashinfo=bvcgc43iinzgc43ijfxgm3ybaadwiyltnawxc5avkbxxg2lyebjwsz3omfwduicbmjxxe5dfmqaaa=== ``` The expected set of operations when performing privileged operations is to first use `walletpassphrase [passphrase] [time]` to unlock the wallet and then perform the privileged operation. This routine that applies for almost all privileged RPCs doesn't apply here, the unlock state of the wallet has no bearing on constructing an encrypted HD chain as it needs to be encrypted with the master key stored in the wallet, which in turn is encrypted with a key derived from the passphrase (i.e., `upgradetohd` imports **always** need the passphrase, if encrypted). You might have noticed that I used `upgradetohd [mnemonic]` instead of the correct syntax, `upgradetohd [mnemonic] "" [passphrase]` that is supposed to be used when supplying a mnemonic to an encrypted wallet, because when you run the former, you don't get told to enter the passphrase into the RPC command, you're told. ``` Error: Please enter the wallet passphrase with walletpassphrase first. ``` Which tells you to treat it like any other routine privileged operation and follow the routine as mentioned above. This is where insufficient validation starts rearing its head, we only validate the passphrase if we're supplied one even though we should be demanding one if the wallet is encrypted and it isn't supplied. We didn't supply a passphrase because we're following the normal routine, we unlocked the wallet so `EnsureWalletIsUnlocked()` is happy, so now the following happens. ``` upgradetohd() | Insufficient validation has allowed us to supply a blank passphrase | for an encrypted wallet |- CWallet::UpgradeToHD() |- CWallet::GenerateNewHDChainEncrypted() | We get our hands on vMasterKey by generating the key from our passphrase | and using it to unlock vCryptedMasterKey. | | There's one small problem, we don't know if the output of CCrypter::Decrypt | isn't just gibberish. Since we don't have a passphrase, whatever came from | CCrypter::SetKeyFromPassphrase isn't the decryption key, meaning, the | vMasterKey we just got is gibberish |- LegacyScriptPubKeyMan::GenerateNewCryptedHDChain() |- res = LegacyScriptPubKeyMan::EncryptHDChain() | |- EncryptSecret() | |- CCrypter::SetKey() | This is where everything unravels, the gibberish key's size doesn't | match WALLET_CRYPTO_KEY_SIZE, it's no good for encryption. We bail out. |- assert(res) We assume are inputs are safe so there's no real reason we should crash. Except our inputs aren't safe, so we crash. Welp! :c ``` This problem has existed for a while but didn't cause the client to crash, in v20.1.1 (1951298), trying to do the same thing would return you a vague error ``` Failed to generate encrypted HD wallet (code -4) ``` In the process of working on mitigating this crash, another edge case was discovered, where if the wallet was unlocked and an incorrect passphrase was provided to `upgradetohd`, the user would not receive any feedback that they entered the wrong passphrase and the client would similarly crash. ``` upgradetohd() | We've been supplied a passphrase, so we can try and validate it by | trying to unlock the wallet with it. If it fails, we know we got the | wrong passphrase. |- CWallet::Unlock() | | Before we bother unlocking the wallet, we should check if we're | | already unlocked, if we are, we can just say "unlock successful". | |- CWallet::IsLocked() | | Wallet is indeed unlocked. | |- return true; | The validation method we just tried to use has a bail-out mechanism | that we don't account for, the "unlock" succeded so I guess we have the | right passphrase. [...] (continue call chain as mentioned earlier) |- assert(res) Oh... ``` This pull request aims to resolve crashes caused by the above two edge cases. ## Additional Information As this PR was required me to add additional guardrails on `GenerateNewCryptedHDChain()` and `GenerateNewHDChainEncrypted()`, it was taken as an opportunity to resolve a TODO ([source](https://github.com/dashpay/dash/blob/9456d0761d8883cc293dffba11dacded517b5f8f/src/wallet/wallet.cpp#L5028-L5038)). The following mitigations have been implemented. * Validating `vMasterKey` size (any key not of `WALLET_CRYPTO_KEY_SIZE` size cannot be used for encryption and so, cannot be a valid key) * Validating `secureWalletPassphrase`'s presence to catch attempts at passing a blank value (an encrypted wallet cannot have a blank passphrase) * Using `Unlock()` to validate the correctness of `vMasterKey`. (the two other instances of iterating through `mapMasterKeys` use `Unlock()`, see [here](https://github.com/dashpay/dash/blob/1394c41c8d0afb8370726488a2888be30d238148/src/wallet/wallet.cpp#L5498-L5500) and [here](https://github.com/dashpay/dash/blob/1394c41c8d0afb8370726488a2888be30d238148/src/wallet/wallet.cpp#L429-L431)) * `Lock()`'ing the wallet before `Unlock()`'ing the wallet to avoid the `IsLocked()` bail-out condition and then restoring to the previous lock state afterwards. * Add an `IsCrypted()` check to see if `upgradetohd`'s `walletpassphrase` is allowed to be empty. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests - [x] I have made corresponding changes to the documentation **(note: N/A)** - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ ACKs for top commit: knst: utACK 69c37f4 UdjinM6: utACK 69c37f4 PastaPastaPasta: utACK 69c37f4 Tree-SHA512: 4bda1f7155511447d6672bbaa22b909f5e2fc7efd1fd8ae1c61e0cdbbf3f6c28f6e8c1a8fe2a270fdedff7279322c93bf0f8e01890aff556fb17288ef6907b3e
1 parent b0b6ba1 commit 27d20be

File tree

6 files changed

+85
-86
lines changed

6 files changed

+85
-86
lines changed

src/wallet/rpcwallet.cpp

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2779,11 +2779,13 @@ static RPCHelpMan upgradetohd()
27792779
{
27802780
return RPCHelpMan{"upgradetohd",
27812781
"\nUpgrades non-HD wallets to HD.\n"
2782+
"\nIf your wallet is encrypted, the wallet passphrase must be supplied. Supplying an incorrect"
2783+
"\npassphrase may result in your wallet getting locked.\n"
27822784
"\nWarning: You will need to make a new backup of your wallet after setting the HD wallet mnemonic.\n",
27832785
{
27842786
{"mnemonic", RPCArg::Type::STR, /* default */ "", "Mnemonic as defined in BIP39 to use for the new HD wallet. Use an empty string \"\" to generate a new random mnemonic."},
27852787
{"mnemonicpassphrase", RPCArg::Type::STR, /* default */ "", "Optional mnemonic passphrase as defined in BIP39"},
2786-
{"walletpassphrase", RPCArg::Type::STR, /* default */ "", "If your wallet is encrypted you must have your wallet passphrase here. If your wallet is not encrypted specifying wallet passphrase will trigger wallet encryption."},
2788+
{"walletpassphrase", RPCArg::Type::STR, /* default */ "", "If your wallet is encrypted you must have your wallet passphrase here. If your wallet is not encrypted, specifying wallet passphrase will trigger wallet encryption."},
27872789
{"rescan", RPCArg::Type::BOOL, /* default */ "false if mnemonic is empty", "Whether to rescan the blockchain for missing transactions or not"},
27882790
},
27892791
RPCResult{
@@ -2793,6 +2795,7 @@ static RPCHelpMan upgradetohd()
27932795
HelpExampleCli("upgradetohd", "")
27942796
+ HelpExampleCli("upgradetohd", "\"mnemonicword1 ... mnemonicwordN\"")
27952797
+ HelpExampleCli("upgradetohd", "\"mnemonicword1 ... mnemonicwordN\" \"mnemonicpassphrase\"")
2798+
+ HelpExampleCli("upgradetohd", "\"mnemonicword1 ... mnemonicwordN\" \"\" \"walletpassphrase\"")
27962799
+ HelpExampleCli("upgradetohd", "\"mnemonicword1 ... mnemonicwordN\" \"mnemonicpassphrase\" \"walletpassphrase\"")
27972800
},
27982801
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
@@ -2803,17 +2806,17 @@ static RPCHelpMan upgradetohd()
28032806
bool generate_mnemonic = request.params[0].isNull() || request.params[0].get_str().empty();
28042807
SecureString secureWalletPassphrase;
28052808
secureWalletPassphrase.reserve(100);
2806-
// TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string)
2807-
// Alternately, find a way to make request.params[0] mlock()'d to begin with.
2808-
if (!request.params[2].isNull()) {
2809-
secureWalletPassphrase = request.params[2].get_str().c_str();
2810-
if (!pwallet->Unlock(secureWalletPassphrase)) {
2811-
throw JSONRPCError(RPC_WALLET_PASSPHRASE_INCORRECT, "The wallet passphrase entered was incorrect");
2809+
2810+
if (request.params[2].isNull()) {
2811+
if (pwallet->IsCrypted()) {
2812+
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Error: Wallet encrypted but passphrase not supplied to RPC.");
28122813
}
2814+
} else {
2815+
// TODO: get rid of this .c_str() by implementing SecureString::operator=(std::string)
2816+
// Alternately, find a way to make request.params[0] mlock()'d to begin with.
2817+
secureWalletPassphrase = request.params[2].get_str().c_str();
28132818
}
28142819

2815-
EnsureWalletIsUnlocked(pwallet.get());
2816-
28172820
SecureString secureMnemonic;
28182821
secureMnemonic.reserve(256);
28192822
if (!generate_mnemonic) {
@@ -2825,6 +2828,7 @@ static RPCHelpMan upgradetohd()
28252828
if (!request.params[1].isNull()) {
28262829
secureMnemonicPassphrase = request.params[1].get_str().c_str();
28272830
}
2831+
28282832
// TODO: breaking changes kept for v21!
28292833
// instead upgradetohd let's use more straightforward 'sethdseed'
28302834
constexpr bool is_v21 = false;

src/wallet/scriptpubkeyman.cpp

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -377,55 +377,41 @@ void LegacyScriptPubKeyMan::UpgradeKeyMetadata()
377377
}
378378
}
379379

380-
void LegacyScriptPubKeyMan::GenerateNewCryptedHDChain(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase, CKeyingMaterial vMasterKey)
380+
void LegacyScriptPubKeyMan::GenerateNewHDChain(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase, std::optional<CKeyingMaterial> vMasterKeyOpt)
381381
{
382382
assert(!m_storage.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS));
383-
384-
385-
CHDChain hdChainTmp;
383+
CHDChain newHdChain;
386384

387385
// NOTE: an empty mnemonic means "generate a new one for me"
388386
// NOTE: default mnemonic passphrase is an empty string
389-
if (!hdChainTmp.SetMnemonic(secureMnemonic, secureMnemonicPassphrase, true)) {
387+
if (!newHdChain.SetMnemonic(secureMnemonic, secureMnemonicPassphrase, /* fUpdateID = */ true)) {
390388
throw std::runtime_error(std::string(__func__) + ": SetMnemonic failed");
391389
}
392390

393-
// add default account
394-
hdChainTmp.AddAccount();
391+
// Add default account
392+
newHdChain.AddAccount();
395393

396-
// We need to safe chain for validation further
397-
CHDChain hdChainPrev = hdChainTmp;
398-
bool res = EncryptHDChain(vMasterKey, hdChainTmp);
399-
assert(res);
400-
res = LoadHDChain(hdChainTmp);
401-
assert(res);
394+
// Encryption routine if vMasterKey has been supplied
395+
if (vMasterKeyOpt.has_value()) {
396+
auto vMasterKey = vMasterKeyOpt.value();
397+
if (vMasterKey.size() != WALLET_CRYPTO_KEY_SIZE) {
398+
throw std::runtime_error(strprintf("%s : invalid vMasterKey size, got %zd (expected %lld)", __func__, vMasterKey.size(), WALLET_CRYPTO_KEY_SIZE));
399+
}
402400

403-
CHDChain hdChainCrypted;
404-
res = GetHDChain(hdChainCrypted);
405-
assert(res);
401+
// Maintain an unencrypted copy of the chain for sanity checking
402+
CHDChain prevHdChain{newHdChain};
406403

407-
// ids should match, seed hashes should not
408-
assert(hdChainPrev.GetID() == hdChainCrypted.GetID());
409-
assert(hdChainPrev.GetSeedHash() != hdChainCrypted.GetSeedHash());
404+
bool res = EncryptHDChain(vMasterKey, newHdChain);
405+
assert(res);
406+
res = LoadHDChain(newHdChain);
407+
assert(res);
408+
res = GetHDChain(newHdChain);
409+
assert(res);
410410

411-
if (!AddHDChainSingle(hdChainCrypted)) {
412-
throw std::runtime_error(std::string(__func__) + ": AddHDChainSingle failed");
411+
// IDs should match, seed hashes should not
412+
assert(prevHdChain.GetID() == newHdChain.GetID());
413+
assert(prevHdChain.GetSeedHash() != newHdChain.GetSeedHash());
413414
}
414-
}
415-
416-
void LegacyScriptPubKeyMan::GenerateNewHDChain(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase)
417-
{
418-
assert(!m_storage.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS));
419-
CHDChain newHdChain;
420-
421-
// NOTE: an empty mnemonic means "generate a new one for me"
422-
// NOTE: default mnemonic passphrase is an empty string
423-
if (!newHdChain.SetMnemonic(secureMnemonic, secureMnemonicPassphrase, true)) {
424-
throw std::runtime_error(std::string(__func__) + ": SetMnemonic failed");
425-
}
426-
427-
// add default account
428-
newHdChain.AddAccount();
429415

430416
if (!AddHDChainSingle(newHdChain)) {
431417
throw std::runtime_error(std::string(__func__) + ": AddHDChainSingle failed");

src/wallet/scriptpubkeyman.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,7 @@ class LegacyScriptPubKeyMan : public ScriptPubKeyMan, public FillableSigningProv
463463
bool GetDecryptedHDChain(CHDChain& hdChainRet);
464464

465465
/* Generates a new HD chain */
466-
void GenerateNewCryptedHDChain(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase, CKeyingMaterial vMasterKey);
467-
void GenerateNewHDChain(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase);
466+
void GenerateNewHDChain(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase, std::optional<CKeyingMaterial> vMasterKey = std::nullopt);
468467

469468
/**
470469
* Explicitly make the wallet learn the related scripts for outputs to the

src/wallet/wallet.cpp

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ std::shared_ptr<CWallet> CreateWallet(interfaces::Chain& chain, interfaces::Coin
337337
// TODO: drop this condition after removing option to create non-HD wallets
338338
// related backport bitcoin#11250
339339
if (wallet->GetVersion() >= FEATURE_HD) {
340-
if (!wallet->GenerateNewHDChainEncrypted(/*secureMnemonic=*/"", /*secureMnemonicPassphrase=*/"", passphrase)) {
340+
if (!wallet->GenerateNewHDChain(/*secureMnemonic=*/"", /*secureMnemonicPassphrase=*/"", passphrase)) {
341341
error = Untranslated("Error: Failed to generate encrypted HD wallet");
342342
status = DatabaseStatus::FAILED_CREATE;
343343
return nullptr;
@@ -5022,18 +5022,9 @@ bool CWallet::UpgradeToHD(const SecureString& secureMnemonic, const SecureString
50225022
WalletLogPrintf("Upgrading wallet to HD\n");
50235023
SetMinVersion(FEATURE_HD);
50245024

5025-
// TODO: replace to GetLegacyScriptPubKeyMan() when `sethdseed` is backported
5026-
auto spk_man = GetOrCreateLegacyScriptPubKeyMan();
5027-
bool prev_encrypted = IsCrypted();
5028-
// TODO: unify encrypted and plain chains usages here
5029-
if (prev_encrypted) {
5030-
if (!GenerateNewHDChainEncrypted(secureMnemonic, secureMnemonicPassphrase, secureWalletPassphrase)) {
5031-
error = Untranslated("Failed to generate encrypted HD wallet");
5032-
return false;
5033-
}
5034-
Lock();
5035-
} else {
5036-
spk_man->GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase);
5025+
if (!GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase, secureWalletPassphrase)) {
5026+
error = Untranslated("Failed to generate HD wallet");
5027+
return false;
50375028
}
50385029
return true;
50395030
}
@@ -5651,42 +5642,61 @@ void CWallet::ConnectScriptPubKeyManNotifiers()
56515642
}
56525643
}
56535644

5654-
bool CWallet::GenerateNewHDChainEncrypted(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase, const SecureString& secureWalletPassphrase)
5645+
bool CWallet::GenerateNewHDChain(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase, const SecureString& secureWalletPassphrase)
56555646
{
56565647
auto spk_man = GetLegacyScriptPubKeyMan();
56575648
if (!spk_man) {
56585649
throw std::runtime_error(strprintf("%s: spk_man is not available", __func__));
56595650
}
56605651

5661-
if (!HasEncryptionKeys()) {
5662-
return false;
5663-
}
5652+
if (IsCrypted()) {
5653+
if (secureWalletPassphrase.empty()) {
5654+
throw std::runtime_error(strprintf("%s: encrypted but supplied empty wallet passphrase", __func__));
5655+
}
56645656

5665-
CCrypter crypter;
5666-
CKeyingMaterial vMasterKey;
5657+
bool is_locked = IsLocked();
56675658

5668-
LOCK(cs_wallet);
5669-
for (const CWallet::MasterKeyMap::value_type& pMasterKey : mapMasterKeys) {
5670-
if (!crypter.SetKeyFromPassphrase(secureWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod)) {
5671-
return false;
5672-
}
5673-
// get vMasterKey to encrypt new hdChain
5674-
if (crypter.Decrypt(pMasterKey.second.vchCryptedKey, vMasterKey)) {
5675-
break;
5659+
CCrypter crypter;
5660+
CKeyingMaterial vMasterKey;
5661+
5662+
// We are intentionally re-locking the wallet so we can validate vMasterKey
5663+
// by verifying if it can unlock the wallet
5664+
Lock();
5665+
5666+
LOCK(cs_wallet);
5667+
for (const auto& [_, master_key] : mapMasterKeys) {
5668+
CKeyingMaterial _vMasterKey;
5669+
if (!crypter.SetKeyFromPassphrase(secureWalletPassphrase, master_key.vchSalt, master_key.nDeriveIterations, master_key.nDerivationMethod)) {
5670+
return false;
5671+
}
5672+
// Try another key if it cannot be decrypted or the key is incapable of encrypting
5673+
if (!crypter.Decrypt(master_key.vchCryptedKey, _vMasterKey) || _vMasterKey.size() != WALLET_CRYPTO_KEY_SIZE) {
5674+
continue;
5675+
}
5676+
// The likelihood of the plaintext being gibberish but also of the expected size is low but not zero.
5677+
// If it can unlock the wallet, it's a good key.
5678+
if (Unlock(_vMasterKey)) {
5679+
vMasterKey = _vMasterKey;
5680+
break;
5681+
}
56765682
}
56775683

5678-
}
5684+
// We got a gibberish key...
5685+
if (vMasterKey.empty()) {
5686+
// Mimicking the error message of RPC_WALLET_PASSPHRASE_INCORRECT as it's possible
5687+
// that the user may see this error when interacting with the upgradetohd RPC
5688+
throw std::runtime_error("Error: The wallet passphrase entered was incorrect");
5689+
}
56795690

5680-
spk_man->GenerateNewCryptedHDChain(secureMnemonic, secureMnemonicPassphrase, vMasterKey);
5691+
spk_man->GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase, vMasterKey);
56815692

5682-
Lock();
5683-
if (!Unlock(secureWalletPassphrase)) {
5684-
// this should never happen
5685-
throw std::runtime_error(std::string(__func__) + ": Unlock failed");
5686-
}
5687-
if (!spk_man->NewKeyPool()) {
5688-
throw std::runtime_error(std::string(__func__) + ": NewKeyPool failed");
5693+
if (is_locked) {
5694+
Lock();
5695+
}
5696+
} else {
5697+
spk_man->GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase);
56895698
}
5699+
56905700
return true;
56915701
}
56925702

src/wallet/wallet.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1343,7 +1343,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
13431343

13441344
// TODO: move it to scriptpubkeyman
13451345
/* Generates a new HD chain */
1346-
bool GenerateNewHDChainEncrypted(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase, const SecureString& secureWalletPassphrase);
1346+
bool GenerateNewHDChain(const SecureString& secureMnemonic, const SecureString& secureMnemonicPassphrase, const SecureString& secureWalletPassphrase = "");
13471347

13481348
/* Returns true if the wallet can give out new addresses. This means it has keys in the keypool or can generate new keys */
13491349
bool CanGetAddresses(bool internal = false) const;

test/functional/wallet_upgradetohd.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,8 @@ def run_test(self):
190190
node.stop()
191191
node.wait_until_stopped()
192192
self.start_node(0, extra_args=['-rescan'])
193-
assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", node.upgradetohd, mnemonic)
194-
assert_raises_rpc_error(-14, "The wallet passphrase entered was incorrect", node.upgradetohd, mnemonic, "", "wrongpass")
193+
assert_raises_rpc_error(-13, "Error: Wallet encrypted but passphrase not supplied to RPC.", node.upgradetohd, mnemonic)
194+
assert_raises_rpc_error(-1, "Error: The wallet passphrase entered was incorrect", node.upgradetohd, mnemonic, "", "wrongpass")
195195
assert node.upgradetohd(mnemonic, "", walletpass)
196196
assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", node.dumphdinfo)
197197
node.walletpassphrase(walletpass, 100)

0 commit comments

Comments
 (0)