From f1903eb647359947652e801844a5ba8e9e73e2c7 Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 15 Aug 2026 15:33:07 -0500 Subject: [PATCH 1/6] feat(evo): expose whether an operator key is in use at the chain tip Add an interfaces::EVO query predicate answering whether an operator BLS public key is assigned to any masternode in the deterministic list at the current chain tip, probing both BLS scheme encodings. This is a per-key chainstate query for callers that select fresh operator keys; it is a UX guard rather than a safety mechanism, so an unready node answers false. Co-Authored-By: Claude Fable 5 --- src/interfaces/node.h | 10 +++++++ src/node/interfaces.cpp | 19 +++++++++++++ src/test/evo_deterministicmns_tests.cpp | 38 +++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/src/interfaces/node.h b/src/interfaces/node.h index f3362cefba6b..59a3be12e44a 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -30,6 +30,7 @@ class BanMan; class CBlockIndex; +class CBLSPublicKey; class CDeterministicMNList; class CFeeRate; class CGovernanceObject; @@ -151,6 +152,15 @@ class EVO Wallet& wallet, const ProviderUpdateRegistrarRequest& request) = 0; virtual ProviderTxResult revokeMasternode(Wallet& wallet, const ProviderRevokeRequest& request) = 0; + /** + * Whether an operator public key is assigned to any masternode in the + * deterministic list at the current chain tip, under either BLS scheme + * encoding. This is a UX guard for skipping keys that would be rejected + * by DIP3 duplicate-key checks, not a safety mechanism: when the node is + * not ready to answer (no tip or no masternode manager yet), it returns + * false. Keys used only historically also return false. + */ + virtual bool isMasternodeOperatorKeyInUse(const CBLSPublicKey& public_key) = 0; virtual void setContext(node::NodeContext* context) {} }; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index c57159887945..9bb5cf644a8a 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -270,6 +270,25 @@ class EVOImpl : public EVO { return evo::provider::Revoke(context(), wallet, request); } + bool isMasternodeOperatorKeyInUse(const CBLSPublicKey& public_key) override + { + if (!m_context || !m_context->chainman || !m_context->dmnman) return false; + const CBlockIndex* tip{WITH_LOCK(::cs_main, return chainman().ActiveChain().Tip())}; + if (!tip) return false; + CDeterministicMNList mn_list; + try { + mn_list = m_context->dmnman->GetListForBlock(tip); + } catch (const BlockDataUnavailableError& e) { + // Expected while a snapshot's background chainstate is still + // catching up; this predicate fails open by design. Any other + // exception means local EvoDB/list corruption and must not be + // hidden, so it deliberately stays unhandled. + LogPrintf("%s -- masternode list unavailable: %s\n", __func__, e.what()); + return false; + } + if (mn_list.GetBlockHash().IsNull()) return false; + return mn_list.HasOperatorKeyUnderAnyScheme(public_key, /*self=*/uint256()); + } void setContext(NodeContext* context) override { m_context = context; diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index 8ca40a2d2909..0875847c35a0 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -1720,6 +1721,43 @@ BOOST_AUTO_TEST_CASE(v19_activation_legacy) FuncV19Activation(setup); } +BOOST_AUTO_TEST_CASE(operator_key_in_use_follows_current_list) +{ + TestMNChainSetup setup(DIP3_ACTIVATION_HEIGHT - 2, {"-dip3params=109:500"}); + setup.ProcessBlock(); // The next block may contain DIP3 transactions. + + auto node{interfaces::MakeNode(setup.m_node)}; + auto in_use = [&](const CBLSSecretKey& key) { return node->evo().isMasternodeOperatorKeyInUse(key.GetPublicKey()); }; + + BOOST_CHECK(!node->evo().isMasternodeOperatorKeyInUse(CBLSPublicKey{})); + + CKey owner_key; + CBLSSecretKey registered_key; + auto tx_reg{CreateProRegTx(setup.chainman, setup.utxos, 19999, GenerateRandomAddress(), setup.coinbaseKey, + owner_key, registered_key)}; + BOOST_CHECK(!in_use(registered_key)); + setup.ProcessBlock({tx_reg}); + BOOST_CHECK(in_use(registered_key)); + + // Rotating the operator key makes the old key immediately reusable: the + // predicate answers for the current list, not for historical assignments. + CBLSSecretKey rotated_key; + rotated_key.MakeNewKey(); + auto tx_upreg{CreateProUpRegTx(setup.chainman, setup.utxos, tx_reg.GetHash(), owner_key, + rotated_key.GetPublicKey(), owner_key.GetPubKey().GetID(), GenerateRandomAddress(), + setup.coinbaseKey)}; + setup.ProcessBlock({tx_upreg}); + BOOST_CHECK(!in_use(registered_key)); + BOOST_CHECK(in_use(rotated_key)); + + // Revocation clears the operator key in the list while the masternode entry remains. + auto tx_revoke{CreateProUpRevTx(setup.chainman, setup.utxos, tx_reg.GetHash(), rotated_key, setup.coinbaseKey)}; + setup.ProcessBlock({tx_revoke}); + BOOST_REQUIRE(setup.dmnman.GetListAtChainTip().HasMN(tx_reg.GetHash())); + BOOST_CHECK(!in_use(registered_key)); + BOOST_CHECK(!in_use(rotated_key)); +} + // The invariant this whole change rests on: a stored operator key never advertises a scheme its own // state version contradicts, so the live list and the same list reloaded from disk agree — including // mnUniquePropertyMap, which IsEqual() compares directly. From 198a9e2eb9325bf4e4c5eaab9cab58393d6d75b0 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 18 Aug 2026 11:09:37 -0500 Subject: [PATCH 2/6] feat(wallet): derive masternode operator keys from seed Descriptor wallets backed by a single mnemonic can derive DashSync-compatible operator BLS keys at m/9'/coin'/3'/3'/index. Consumption is lowest-index-first and permanent, tracked by a watermark persisted before any secret is exposed. Issuance scans candidates against the current masternode list gap-limit style and a persisted lookahead of derived public keys lets sync/rescan advance the watermark opportunistically without the seed. Reuse protection is best-effort by design: it avoids accidental reuse without guaranteeing historical uniqueness, which the network's current-list uniqueness rules make harmless. Legacy wallets are intentionally unsupported: legacy is deprecated and DashPay is descriptor-only, so a new feature should not entrench it. They keep bls generate plus a separate key backup. --- doc/release-notes-7594.md | 8 + src/Makefile.am | 2 + src/Makefile.test.include | 1 + src/bls/bls.h | 7 + src/interfaces/masternode_operator.h | 43 ++ src/interfaces/wallet.h | 18 + src/wallet/interfaces.cpp | 18 + src/wallet/masternode_operator.h | 90 +++ src/wallet/rpc/wallet.cpp | 17 + src/wallet/salvage.cpp | 2 +- src/wallet/scriptpubkeyman.cpp | 97 +++ src/wallet/scriptpubkeyman.h | 31 + src/wallet/test/masternode_operator_tests.cpp | 634 ++++++++++++++++++ src/wallet/wallet.cpp | 376 +++++++++++ src/wallet/wallet.h | 56 ++ src/wallet/walletdb.cpp | 45 ++ src/wallet/walletdb.h | 11 + test/util/data/non-backported.txt | 3 + 18 files changed, 1458 insertions(+), 1 deletion(-) create mode 100644 doc/release-notes-7594.md create mode 100644 src/interfaces/masternode_operator.h create mode 100644 src/wallet/masternode_operator.h create mode 100644 src/wallet/test/masternode_operator_tests.cpp diff --git a/doc/release-notes-7594.md b/doc/release-notes-7594.md new file mode 100644 index 000000000000..c830898ea4b0 --- /dev/null +++ b/doc/release-notes-7594.md @@ -0,0 +1,8 @@ +Wallet +------ + +- Mnemonic-backed descriptor wallets can now derive DashSync-compatible + masternode operator BLS keys from the wallet seed, so the recovery phrase + also backs up operator keys. Restored wallets avoid keys that are currently + registered, but may reuse a key that was retired in the past. Other wallet + types remain unchanged and can continue using `bls generate`. (#7594) diff --git a/src/Makefile.am b/src/Makefile.am index 0cf8ce802fdb..132e6c7e34b5 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -281,6 +281,7 @@ BITCOIN_CORE_H = \ interfaces/handler.h \ interfaces/init.h \ interfaces/ipc.h \ + interfaces/masternode_operator.h \ interfaces/node.h \ interfaces/providertx.h \ interfaces/wallet.h \ @@ -478,6 +479,7 @@ BITCOIN_CORE_H = \ wallet/hdchain.h \ wallet/ismine.h \ wallet/load.h \ + wallet/masternode_operator.h \ wallet/receive.h \ wallet/rpc/util.h \ wallet/rpc/wallet.h \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 805083ffeb5f..2b4be209884c 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -224,6 +224,7 @@ if ENABLE_WALLET BITCOIN_TESTS += \ wallet/test/bip39_tests.cpp \ wallet/test/coinjoin_tests.cpp \ + wallet/test/masternode_operator_tests.cpp \ wallet/test/psbt_wallet_tests.cpp \ wallet/test/spend_tests.cpp \ wallet/test/wallet_tests.cpp \ diff --git a/src/bls/bls.h b/src/bls/bls.h index ae2124cd38e1..f08f448a914e 100644 --- a/src/bls/bls.h +++ b/src/bls/bls.h @@ -283,6 +283,13 @@ class CBLSSecretKey : public CBLSWrapper bytes) const + { + if (!IsValid() || bytes.size() != SerSize) return false; + impl.Serialize(bytes.data()); + return true; + } + void AggregateInsecure(const CBLSSecretKey& o); static CBLSSecretKey AggregateInsecure(Span sks); diff --git a/src/interfaces/masternode_operator.h b/src/interfaces/masternode_operator.h new file mode 100644 index 000000000000..6bbfeadb285a --- /dev/null +++ b/src/interfaces/masternode_operator.h @@ -0,0 +1,43 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_INTERFACES_MASTERNODE_OPERATOR_H +#define BITCOIN_INTERFACES_MASTERNODE_OPERATOR_H + +#include + +#include +#include +#include + +namespace interfaces { + +//! Result of a deterministic masternode operator-key operation. +enum class MasternodeOperatorKeyStatus : uint8_t { + SUCCESS, + NOT_SUPPORTED, + WALLET_LOCKED, + EXHAUSTED, + INVALID_KEY, + NOT_FOUND, + DATABASE_ERROR, + DERIVATION_ERROR, +}; + +//! A deterministic masternode operator key returned by the wallet. The public +//! key uses the canonical basic-scheme serialization. +struct MasternodeOperatorKey { + SecureVector secret_key; + std::vector public_key; + std::string path; +}; + +struct MasternodeOperatorKeyResult { + MasternodeOperatorKeyStatus status{MasternodeOperatorKeyStatus::DERIVATION_ERROR}; + MasternodeOperatorKey key; +}; + +} // namespace interfaces + +#endif // BITCOIN_INTERFACES_MASTERNODE_OPERATOR_H diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 9fc779906135..a78a20e4ff99 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -8,6 +8,7 @@ #include // For CAmount #include #include // For ChainClient +#include #include // For CKeyID and CScriptID (definitions needed in CTxDestination instantiation) #include