Repository navigation
feat(wallet): derive masternode operator keys from the wallet mnemonic #8002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
daba63c
993ca19
e76fc85
29db58d
98ea896
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| Wallet | ||
| ------ | ||
|
|
||
| - New wallet RPC `bls operatorkey` issues masternode operator keys derived from | ||
| the wallet mnemonic at `m/9'/coin'/3'/3'/index`, the keys the DashPay mobile | ||
| wallets derive from the same recovery phrase, so that phrase is the only | ||
| backup an operator needs. Each call issues the next key; `bls operatorkey | ||
| <index>` returns a key issued earlier. A wallet restored from the phrase | ||
| skips indexes whose key is in the masternode list at the chain tip, and a | ||
| rescan moves past every key it sees assigned by a ProRegTx or ProUpRegTx, | ||
| so keys rotated away earlier are not issued again. Rescans driven by block | ||
| filters cannot see operator keys, and a pruned node cannot rescan at all, | ||
| so issuing on a pruned node returns a warning. Requires an unlocked | ||
|
Comment on lines
+10
to
+13
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 Nitpick: Describe the implemented rescan and pruning behavior The statement that a pruned node cannot rescan at all is incorrect: it can scan retained blocks, and fails to complete recovery when required blocks are unavailable. Also clarify that wallets with an initialized operator window deliberately bypass fast block filtering and inspect blocks, rather than presenting filter-based omission as an unavoidable limitation of their recovery. The recognized assignment types include ProUpSharedRegTx as well as the two listed types. Update the note to describe those behaviors and retain the warning about unavailable pruned history. source: |
||
| descriptor wallet with a mnemonic. (#8002) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,6 +54,7 @@ using wallet::DEFAULT_DISABLE_WALLET; | |
| using wallet::GetWalletForJSONRPCRequest; | ||
| using wallet::HELP_REQUIRING_PASSPHRASE; | ||
| using wallet::isminetype; | ||
| using wallet::PlatformKeyStatus; | ||
| #endif // ENABLE_WALLET | ||
|
|
||
| // Defined here rather than with the other ToJson() in evo/core_write.cpp: dash-tx never prints | ||
|
|
@@ -2462,13 +2463,125 @@ static RPCHelpMan bls_fromsecret() | |
| }; | ||
| } | ||
|
|
||
| #ifdef ENABLE_WALLET | ||
| [[noreturn]] static void ThrowOperatorKeyError(PlatformKeyStatus status, uint32_t index) | ||
| { | ||
| switch (status) { | ||
| case PlatformKeyStatus::WALLET_LOCKED: | ||
| throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, | ||
| "Error: Please enter the wallet passphrase with walletpassphrase first."); | ||
| case PlatformKeyStatus::NOT_SUPPORTED: | ||
| throw JSONRPCError(RPC_WALLET_ERROR, "Operator keys need a descriptor wallet with private keys and a mnemonic"); | ||
| case PlatformKeyStatus::AMBIGUOUS_SOURCE: | ||
| throw JSONRPCError(RPC_WALLET_ERROR, "Active descriptors of this wallet come from different root keys"); | ||
| case PlatformKeyStatus::INVALID_SOURCE: | ||
| throw JSONRPCError(RPC_WALLET_ERROR, "Wallet mnemonic does not match the descriptor root key"); | ||
| case PlatformKeyStatus::INVALID_ARGUMENT: | ||
| throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("operator key %d has not been issued by this wallet", index)); | ||
| case PlatformKeyStatus::DATABASE_ERROR: | ||
| throw JSONRPCError(RPC_WALLET_ERROR, "Operator key records could not be read or written"); | ||
| case PlatformKeyStatus::DERIVATION_ERROR: | ||
| throw JSONRPCError(RPC_WALLET_ERROR, "Operator key derivation failed"); | ||
| case PlatformKeyStatus::SUCCESS: | ||
| case PlatformKeyStatus::IMPORT_ERROR: | ||
| break; | ||
| } | ||
| NONFATAL_UNREACHABLE(); | ||
| } | ||
|
|
||
| static RPCHelpMan bls_operatorkey() | ||
| { | ||
| return RPCHelpMan{ | ||
| "bls operatorkey", | ||
| "\nIssues the next masternode operator key derived from the wallet mnemonic, or returns a key issued earlier.\n" | ||
| "Keys follow m/9'/coin'/3'/3'/index with the BLS HD scheme of the DashPay mobile wallets, so restoring\n" | ||
| "the mnemonic anywhere reproduces the same keys in the same order. Indexes whose key is in the masternode\n" | ||
| "list at the chain tip are skipped.\n" + | ||
| HELP_REQUIRING_PASSPHRASE, | ||
| { | ||
| {"index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, | ||
| "Return the key issued earlier at this index instead of issuing a new one"}, | ||
|
Comment on lines
+2502
to
+2503
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🔴 Blocking: Register the operator index for CLI parameter conversion The numeric index has no conversion entry in src/rpc/client.cpp. RPCConvertValues therefore leaves the documented source:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved (re-reviewed at |
||
| }, | ||
| RPCResult{RPCResult::Type::OBJ, | ||
| "", | ||
| "", | ||
| { | ||
| {RPCResult::Type::NUM, "index", "Derivation index of the key"}, | ||
| {RPCResult::Type::STR_HEX, "secret", "BLS secret key"}, | ||
| {RPCResult::Type::STR_HEX, "public", "BLS public key"}, | ||
| {RPCResult::Type::STR, "scheme", "BLS scheme, always basic (use bls fromsecret for legacy)"}, | ||
| {RPCResult::Type::ARR, | ||
| "warnings", | ||
| /*optional=*/true, | ||
| "Why the key may have been used before", | ||
| { | ||
| {RPCResult::Type::STR, "", ""}, | ||
| }}, | ||
| }}, | ||
| RPCExamples{HelpExampleCli("bls operatorkey", "") + HelpExampleCli("bls operatorkey", "0")}, | ||
| [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { | ||
| std::shared_ptr<CWallet> const wallet{GetWalletForJSONRPCRequest(request)}; | ||
| if (!wallet) return UniValue::VNULL; | ||
| // Make sure the results are valid at least up to the most recent block | ||
| // the user could have gotten from another RPC command prior to now | ||
| wallet->BlockUntilSyncedToCurrentChain(); | ||
| EnsureWalletIsUnlocked(*wallet); | ||
|
|
||
| uint32_t index{0}; | ||
| CBLSSecretKey secret; | ||
| PlatformKeyStatus status; | ||
| if (request.params[0].isNull()) { | ||
| const NodeContext& node{EnsureAnyNodeContext(request.context)}; | ||
| std::set<std::vector<uint8_t>> operator_keys; | ||
| CHECK_NONFATAL(node.dmnman)->GetListAtChainTip().ForEachMN(/*onlyValid=*/false, [&](const auto& dmn) { | ||
| operator_keys.insert(dmn.pdmnState->pubKeyOperator.Get().ToByteVector(/*specificLegacyScheme=*/false)); | ||
| }); | ||
| LOCK(wallet->cs_wallet); | ||
| status = wallet->IssueOperatorKey( | ||
| [&operator_keys](const CBLSPublicKey& pubkey) { | ||
| return operator_keys.count(pubkey.ToByteVector(/*specificLegacyScheme=*/false)) != 0; | ||
| }, | ||
| index, secret); | ||
|
Comment on lines
+2533
to
+2544
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🔴 Blocking: Synchronize wallet notifications before choosing an operator index Issuance combines the current masternode list with a counter updated by asynchronous wallet notifications, but this RPC never calls BlockUntilSyncedToCurrentChain. If connected blocks assign a derived key and then rotate it away while wallet callbacks remain queued, the tip list already excludes the key while the wallet counter still considers it unissued. The RPC can acquire cs_wallet before those callbacks and return the previously used key. The wallet mutex serializes running callbacks but does not drain queued ones. Use the existing synchronization barrier outside cs_wallet before taking the list snapshot and making the issuance decision. source:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved (re-reviewed at |
||
| } else { | ||
| const int64_t requested{request.params[0].getInt<int64_t>()}; | ||
| if (requested < 0 || requested >> 31) { | ||
| throw JSONRPCError(RPC_INVALID_PARAMETER, "Operator key index out of range"); | ||
| } | ||
| index = requested; | ||
| LOCK(wallet->cs_wallet); | ||
| status = wallet->GetOperatorKey(index, secret); | ||
| } | ||
| if (status != PlatformKeyStatus::SUCCESS) ThrowOperatorKeyError(status, index); | ||
|
|
||
| UniValue ret(UniValue::VOBJ); | ||
| ret.pushKV("index", index); | ||
| ret.pushKV("secret", secret.ToString()); | ||
| ret.pushKV("public", secret.GetPublicKey().ToString(/*specificLegacyScheme=*/false)); | ||
| ret.pushKV("scheme", "basic"); | ||
| if (request.params[0].isNull() && wallet->chain().havePruned()) { | ||
| UniValue warnings(UniValue::VARR); | ||
| warnings.push_back( | ||
| "This node is pruned, so provider transactions in pruned blocks were not scanned: a key " | ||
| "rotated away before this wallet was restored may be issued again. Issue the key on a " | ||
| "node with full blocks or rotate the operator key after registering."); | ||
| PushWarnings(warnings, ret); | ||
| } | ||
| return ret; | ||
| }, | ||
| }; | ||
| } | ||
| #endif // ENABLE_WALLET | ||
|
|
||
| static RPCHelpMan bls_help() | ||
| { | ||
| return RPCHelpMan{"bls", | ||
| "Set of commands to execute BLS related actions.\n" | ||
| "To get help on individual commands, use \"help bls command\".\n" | ||
| "\nAvailable commands:\n" | ||
| " generate - Create a BLS secret/public key pair\n" | ||
| #ifdef ENABLE_WALLET | ||
| " operatorkey - Issue or return a masternode operator key derived from the wallet mnemonic\n" | ||
| #endif | ||
| " fromsecret - Parse a BLS secret key and return the secret/public key pair\n", | ||
| { | ||
| {"command", RPCArg::Type::STR, RPCArg::Optional::NO, "The command to execute"}, | ||
|
|
@@ -2507,6 +2620,7 @@ Span<const CRPCCommand> GetWalletEvoRPCCommands() | |
| {"evo", &protx_shared_dissolve_prepare}, | ||
| {"evo", &protx_shared_update_share}, | ||
| {"evo", &protx_shared_update_registrar_prepare}, | ||
| {"evo", &bls_operatorkey}, | ||
| {"hidden", &protx_register_legacy}, | ||
| {"hidden", &protx_register_fund_legacy}, | ||
| {"hidden", &protx_register_prepare_legacy}, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -431,6 +431,12 @@ static RPCHelpMan upgradetohd() | |
|
|
||
| if (pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { | ||
| pwallet->SetupDescriptorScriptPubKeyMans(mnemonic, mnemonic_passphrase); | ||
| // A supplied mnemonic may already have operator keys on-chain; the | ||
| // chain record lets the rescan below recognise them and must exist | ||
| // before encryption locks the seed away. | ||
| if (!generate_mnemonic && pwallet->SetupOperatorKeyChain() != PlatformKeyStatus::SUCCESS) { | ||
| throw JSONRPCError(RPC_WALLET_ERROR, "Failed to store the operator key chain"); | ||
| } | ||
|
Comment on lines
+437
to
+439
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } else { | ||
| auto spk_man = pwallet->GetLegacyScriptPubKeyMan(); | ||
| if (!spk_man) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: dashpay/dash
Length of output: 5744
Script executed:
Repository: dashpay/dash
Length of output: 11870
Script executed:
Repository: dashpay/dash
Length of output: 10935
Correct the release note: a pruned node can rescan the blocks it retains.
The note says "a pruned node cannot rescan at all." The wallet scan code rescans blocks that are still on disk and fails only when a required block was pruned. Replace the sentence so users know a pruned node can recover the retained range. Keep the warning about pruned blocks, which the code also emits.
📝 Suggested fix