Summary
SPV historical sync cannot discover masternode special transactions (ProRegTx and the ProUp* updates) that are relevant to the wallet only through their payload keys, not through any normal input or output. A masternode owner who controls just the owner/voting keys — with a third party paying the fee and collateral — will never see their ProRegTx after a compact-filter sync, even though the wallet already knows how to match it.
The payload-matching logic itself is complete. The gap is narrow: it lives in the compact-filter query construction, not in the wallet checker, and it does not require downloading extra blocks.
Background: how Dash Core handles this
Dash Core matches special-transaction payload elements in both of its filter builders, and keeps them in sync:
- Bloom (BIP37):
CBloomFilter::CheckSpecialTransactionMatchesAndUpdate — src/common/bloom.cpp:127
- Compact (BIP157/158):
ExtractSpecialTxFilterElements — src/evo/specialtx_filter.cpp:55, wired into the basic block filter at src/blockfilter.cpp:201
For a ProRegTx both insert: collateralOutpoint, keyIDOwner as bare 20-byte hash160, keyIDVoting as bare 20-byte hash160, and scriptPayout. The BLS pubKeyOperator is deliberately not inserted by either.
The consequence that matters: every Dash Core node already publishes compact filters that contain the owner/voting key hashes. A compact-filter light client only has to query for them.
Note the wallet in Dash Core does not do this — CWallet::IsMine/IsFromMe (src/wallet/wallet.cpp:1596,1618) ignore payloads entirely; "my masternodes" is answered separately by protx list wallet (src/rpc/evo.cpp:1452) walking the deterministic masternode list. Payload matching is a filter-serving feature, and the compact-filter query side is the correct place to mirror it here.
Current state in rust-dashcore
Wallet-side matching already works. key-wallet has first-class provider accounts (owner/voting/operator/platform) and inspects payloads directly:
key-wallet/src/transaction_checking/account_checker.rs:940 matches owner_key_hash, :895 matches voting_key_hash, :980 matches the BLS operator_public_key, plus payout-script matching.
- Any downloaded block runs the full payload-aware scan (
key-wallet-manager/src/process_block.rs:62 -> wallet_checker.rs:57).
So if the transaction reaches the wallet, it is matched. The problem is that a payload-only tx never reaches it during historical sync.
The compact-filter query is scriptPubKey-only. key-wallet-manager/src/matching.rs:39:
let query: FilterQuery = script_pubkeys.iter().map(|s| s.as_bytes()).collect();
fed from wallet.monitored_script_pubkeys_for(wallet_id) (dash-spv/src/sync/filters/manager.rs:748).
The mismatch: the peer inserted keyIDOwner as a bare 20-byte hash, but we test the filter with the full P2PKH script (OP_DUP OP_HASH160 <hash> OP_EQUALVERIFY OP_CHECKSIG). Different byte strings -> no match -> the block is never selected -> never downloaded -> the payload-aware scan never runs on it.
(FilterQuery already supports arbitrary-length elements via its other bucket — dash/src/bip158.rs:377 — so a bare hash160 slots in with no type changes.)
Payout scripts are the exception: they are already wallet scripts in monitored_script_pubkeys, so payout-to-a-wallet-address already matches today. Only the bare key hashes (and, for updates, proTxHash) are missing from the query.
Not a structural BIP158 limitation
Worth stating explicitly to pre-empt the wrong fix: this is not a case for "download all blocks in the masternode range." Our own dash/src/bip158.rs filter builder (add_output_scripts/add_input_scripts) is irrelevant here — during sync we match against filters served by Dash Core peers, and those already carry the payload key hashes (blockfilter.cpp:201). We are simply not querying for them.
Proposed fix
Mirror Dash Core's ExtractSpecialTxFilterElements on the query side.
Phase 1 — close the ProRegTx discovery gap (the core ask):
- Add a wallet-interface method that returns the bare payload elements the wallet wants to match, e.g.
monitored_filter_elements_for(wallet_id) -> Vec<Vec<u8>>, sourced from the provider owner/voting accounts: each owner_key_hash and voting_key_hash as raw 20-byte hash160.
- Extend
check_compact_filters_for_script_pubkeys (or add a sibling) to accept these extra raw elements and fold them into the same FilterQuery alongside the scripts:
pub fn check_compact_filters_for_elements(
input: &HashMap<FilterMatchKey, BlockFilter>,
script_pubkeys: &[ScriptBuf],
extra_elements: &[Vec<u8>], // bare hash160 owner/voting keys, serialized outpoints, proTxHashes
min_height: CoreBlockHeight,
) -> BTreeSet<FilterMatchKey> {
let query: FilterQuery = script_pubkeys
.iter()
.map(|s| s.as_bytes())
.chain(extra_elements.iter().map(|e| e.as_slice()))
.collect();
// ... unchanged match loop ...
}
- Pass the new elements at both call sites in
dash-spv/src/sync/filters/manager.rs:663,801.
This alone lets a payload-only ProRegTx be selected and downloaded, after which the existing account_checker matching does the rest.
Phase 2 — updates and operator-only masternodes (follow-up):
- To catch
ProUpRegTx/ProUpServTx/ProUpRevTx, feed each tracked masternode's proTxHash into extra_elements. Those hashes are learned after discovering the ProRegTx, or from the already-synced masternode list (dash-spv/src/sync/masternodes/).
- The BLS
pubKeyOperator is not in Dash Core's filters at all, so a masternode you only operate (never own) cannot be found via compact filters. Cross-reference the synced masternode list (SML) against the wallet's operator BLS keys to learn those proTxHashes / txids directly. This is also the more robust general discovery path.
Not affected
- Mempool discovery already works: the BIP37 bloom build (
dash-spv/src/sync/mempool/filter.rs:32) already inserts owner/voting hashes as addresses. Only historical/confirmed-block sync has the hole.
- No consensus or wallet-checker change is needed. The payload matching in
account_checker.rs is already correct and reused as-is.
Acceptance
- A wallet holding only the owner (or voting) key of a
ProRegTx — with no wallet-owned input or output in that tx — discovers and records the ProRegTx after a compact-filter historical sync.
- Regression test: a compact filter built by the Dash Core rules (owner/voting hash as bare elements) is matched by the new query; the old scriptPubKey-only query does not match it.
Summary
SPV historical sync cannot discover masternode special transactions (ProRegTx and the ProUp* updates) that are relevant to the wallet only through their payload keys, not through any normal input or output. A masternode owner who controls just the owner/voting keys — with a third party paying the fee and collateral — will never see their
ProRegTxafter a compact-filter sync, even though the wallet already knows how to match it.The payload-matching logic itself is complete. The gap is narrow: it lives in the compact-filter query construction, not in the wallet checker, and it does not require downloading extra blocks.
Background: how Dash Core handles this
Dash Core matches special-transaction payload elements in both of its filter builders, and keeps them in sync:
CBloomFilter::CheckSpecialTransactionMatchesAndUpdate—src/common/bloom.cpp:127ExtractSpecialTxFilterElements—src/evo/specialtx_filter.cpp:55, wired into the basic block filter atsrc/blockfilter.cpp:201For a
ProRegTxboth insert:collateralOutpoint,keyIDOwneras bare 20-byte hash160,keyIDVotingas bare 20-byte hash160, andscriptPayout. The BLSpubKeyOperatoris deliberately not inserted by either.The consequence that matters: every Dash Core node already publishes compact filters that contain the owner/voting key hashes. A compact-filter light client only has to query for them.
Note the wallet in Dash Core does not do this —
CWallet::IsMine/IsFromMe(src/wallet/wallet.cpp:1596,1618) ignore payloads entirely; "my masternodes" is answered separately byprotx list wallet(src/rpc/evo.cpp:1452) walking the deterministic masternode list. Payload matching is a filter-serving feature, and the compact-filter query side is the correct place to mirror it here.Current state in rust-dashcore
Wallet-side matching already works.
key-wallethas first-class provider accounts (owner/voting/operator/platform) and inspects payloads directly:key-wallet/src/transaction_checking/account_checker.rs:940matchesowner_key_hash,:895matchesvoting_key_hash,:980matches the BLSoperator_public_key, plus payout-script matching.key-wallet-manager/src/process_block.rs:62->wallet_checker.rs:57).So if the transaction reaches the wallet, it is matched. The problem is that a payload-only tx never reaches it during historical sync.
The compact-filter query is scriptPubKey-only.
key-wallet-manager/src/matching.rs:39:fed from
wallet.monitored_script_pubkeys_for(wallet_id)(dash-spv/src/sync/filters/manager.rs:748).The mismatch: the peer inserted
keyIDOwneras a bare 20-byte hash, but we test the filter with the full P2PKH script (OP_DUP OP_HASH160 <hash> OP_EQUALVERIFY OP_CHECKSIG). Different byte strings -> no match -> the block is never selected -> never downloaded -> the payload-aware scan never runs on it.(
FilterQueryalready supports arbitrary-length elements via itsotherbucket —dash/src/bip158.rs:377— so a bare hash160 slots in with no type changes.)Payout scripts are the exception: they are already wallet scripts in
monitored_script_pubkeys, so payout-to-a-wallet-address already matches today. Only the bare key hashes (and, for updates,proTxHash) are missing from the query.Not a structural BIP158 limitation
Worth stating explicitly to pre-empt the wrong fix: this is not a case for "download all blocks in the masternode range." Our own
dash/src/bip158.rsfilter builder (add_output_scripts/add_input_scripts) is irrelevant here — during sync we match against filters served by Dash Core peers, and those already carry the payload key hashes (blockfilter.cpp:201). We are simply not querying for them.Proposed fix
Mirror Dash Core's
ExtractSpecialTxFilterElementson the query side.Phase 1 — close the ProRegTx discovery gap (the core ask):
monitored_filter_elements_for(wallet_id) -> Vec<Vec<u8>>, sourced from the provider owner/voting accounts: eachowner_key_hashandvoting_key_hashas raw 20-byte hash160.check_compact_filters_for_script_pubkeys(or add a sibling) to accept these extra raw elements and fold them into the sameFilterQueryalongside the scripts:dash-spv/src/sync/filters/manager.rs:663,801.This alone lets a payload-only ProRegTx be selected and downloaded, after which the existing
account_checkermatching does the rest.Phase 2 — updates and operator-only masternodes (follow-up):
ProUpRegTx/ProUpServTx/ProUpRevTx, feed each tracked masternode'sproTxHashintoextra_elements. Those hashes are learned after discovering the ProRegTx, or from the already-synced masternode list (dash-spv/src/sync/masternodes/).pubKeyOperatoris not in Dash Core's filters at all, so a masternode you only operate (never own) cannot be found via compact filters. Cross-reference the synced masternode list (SML) against the wallet's operator BLS keys to learn thoseproTxHashes / txids directly. This is also the more robust general discovery path.Not affected
dash-spv/src/sync/mempool/filter.rs:32) already inserts owner/voting hashes as addresses. Only historical/confirmed-block sync has the hole.account_checker.rsis already correct and reused as-is.Acceptance
ProRegTx— with no wallet-owned input or output in that tx — discovers and records the ProRegTx after a compact-filter historical sync.