Skip to content

fix(spv): discover masternode special txs matched only by payload keys (owner/voting) during compact-filter sync #861

Description

@xdustinface

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::CheckSpecialTransactionMatchesAndUpdatesrc/common/bloom.cpp:127
  • Compact (BIP157/158): ExtractSpecialTxFilterElementssrc/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):

  1. 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.
  2. 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 ...
}
  1. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions