Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/release-notes-7594.md
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -478,6 +479,7 @@ BITCOIN_CORE_H = \
wallet/hdchain.h \
wallet/ismine.h \
wallet/load.h \
wallet/masternode_operator_types.h \
wallet/receive.h \
wallet/rpc/util.h \
wallet/rpc/wallet.h \
Expand Down Expand Up @@ -709,6 +711,7 @@ libbitcoin_wallet_a_SOURCES = \
wallet/hdchain.cpp \
wallet/interfaces.cpp \
wallet/load.cpp \
wallet/masternode_operator.cpp \
wallet/receive.cpp \
wallet/rpc/addresses.cpp \
wallet/rpc/backup.cpp \
Expand Down
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
7 changes: 7 additions & 0 deletions src/bls/bls.h
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,13 @@ class CBLSSecretKey : public CBLSWrapper<bls::PrivateKey, BLS_CURVE_SECKEY_SIZE,
CBLSSecretKey(const CBLSSecretKey&) = default;
CBLSSecretKey& operator=(const CBLSSecretKey&) = default;

bool SerializeTo(Span<unsigned char> bytes) const
{
if (!IsValid() || bytes.size() != SerSize) return false;
impl.Serialize(bytes.data());
return true;
}

void AggregateInsecure(const CBLSSecretKey& o);
static CBLSSecretKey AggregateInsecure(Span<CBLSSecretKey> sks);

Expand Down
43 changes: 43 additions & 0 deletions src/interfaces/masternode_operator.h
Original file line number Diff line number Diff line change
@@ -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 <support/allocators/secure.h>

#include <cstdint>
#include <string>
#include <vector>

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<unsigned char> public_key;
std::string path;
};

struct MasternodeOperatorKeyResult {
MasternodeOperatorKeyStatus status{MasternodeOperatorKeyStatus::DERIVATION_ERROR};
MasternodeOperatorKey key;
};

} // namespace interfaces

#endif // BITCOIN_INTERFACES_MASTERNODE_OPERATOR_H
10 changes: 10 additions & 0 deletions src/interfaces/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

class BanMan;
class CBlockIndex;
class CBLSPublicKey;
class CDeterministicMNList;
class CFeeRate;
class CGovernanceObject;
Expand Down Expand Up @@ -151,6 +152,15 @@ class EVO
Wallet& wallet, const ProviderUpdateRegistrarRequest& request) = 0;
virtual ProviderTxResult<ProviderTxSubmission> 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) {}
};

Expand Down
18 changes: 18 additions & 0 deletions src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <consensus/amount.h> // For CAmount
#include <governance/common.h>
#include <interfaces/chain.h> // For ChainClient
#include <interfaces/masternode_operator.h>
#include <pubkey.h> // For CKeyID and CScriptID (definitions needed in CTxDestination instantiation)
#include <script/standard.h> // For CTxDestination
#include <support/allocators/secure.h> // For SecureString
Expand All @@ -28,6 +29,7 @@
#include <utility>
#include <vector>

class CBLSPublicKey;
class CFeeRate;
class CGovernanceVote;
class CKey;
Expand Down Expand Up @@ -142,6 +144,22 @@ class Wallet
//! Sign special transaction payload
virtual bool signSpecialTxPayload(const uint256& hash, const CKeyID& keyid, std::vector<unsigned char>& vchSig) = 0;

//! Whether this wallet is a descriptor wallet with exactly one
//! mnemonic-backed operator-key source. Legacy wallets are not supported.
virtual bool hasMasternodeOperatorKeySource() = 0;
//! Derive and permanently consume the lowest operator-key index at or
//! above the consumption watermark that is not in use. The watermark is

@knst knst Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is meaning of "watermark" in this context? Is it counter for used indexes in derivation path?

if so, I assume that's a bad naming because I hadn't seen watermark anywhere in wallet's codebase or any DIP / BIP

//! persisted before the secret is returned and never rolled back.
//! is_in_use may be empty; when set it is queried without wallet locks
//! held so the caller can supply interfaces::Node's EVO predicate, and
//! issuance scans gap-limit style past every index it reports in use.
virtual MasternodeOperatorKeyResult getNewMasternodeOperatorKey(
const std::function<bool(const CBLSPublicKey&)>& is_in_use) = 0;
//! Re-derive a previously consumed operator key (an index below the
//! watermark) by its basic-scheme public key. Read-only; keys never
//! exposed are not addressable.
virtual MasternodeOperatorKeyResult getMasternodeOperatorKey(const std::vector<unsigned char>& public_key) = 0;

//! Return whether wallet has private key.
virtual bool isSpendable(const CScript& script) = 0;
virtual bool isSpendable(const CTxDestination& dest) = 0;
Expand Down
19 changes: 19 additions & 0 deletions src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
38 changes: 38 additions & 0 deletions src/test/evo_deterministicmns_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <evo/simplifiedmns.h>
#include <evo/specialtx.h>
#include <evo/specialtxman.h>
#include <interfaces/node.h>
#include <llmq/context.h>
#include <node/mempool_args.h>
#include <messagesigner.h>
Expand Down Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions src/wallet/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <interfaces/wallet.h>

#include <bls/bls.h>
#include <chain.h>
#include <coinjoin/client.h>
#include <consensus/amount.h>
Expand Down Expand Up @@ -239,6 +240,23 @@ class WalletImpl : public Wallet
{
return m_wallet->SignSpecialTxPayload(hash, keyid, vchSig);
}
bool hasMasternodeOperatorKeySource() override { return m_wallet->HasMasternodeOperatorKeySource(); }
interfaces::MasternodeOperatorKeyResult getNewMasternodeOperatorKey(
const std::function<bool(const CBLSPublicKey&)>& is_in_use) override
{
return m_wallet->GetNewMasternodeOperatorKey(is_in_use);
}
interfaces::MasternodeOperatorKeyResult getMasternodeOperatorKey(const std::vector<unsigned char>& public_key) override
{
CBLSPublicKey parsed;
parsed.SetBytes(public_key, /*specificLegacyScheme=*/false);
if (!parsed.IsValid() || parsed.ToByteVector(/*specificLegacyScheme=*/false) != public_key) {
interfaces::MasternodeOperatorKeyResult result;
result.status = interfaces::MasternodeOperatorKeyStatus::INVALID_KEY;
return result;
}
return m_wallet->GetMasternodeOperatorKey(parsed);
}
bool isSpendable(const CScript& script) override
{
LOCK(m_wallet->cs_wallet);
Expand Down
Loading
Loading