Skip to content

dash-spv/dashcore: SML masternode-list-entry decoder fails on Dash Core 23.1.x (ExtNetInfo / extended core-P2P addresses) — "Invalid MasternodeType variant" desync #795

Description

@Claudius-Maginificent

dash-spv/dashcore: SML masternode-list-entry decoder fails on Dash Core 23.1.x (ExtNetInfo / extended core-P2P addresses) — "Invalid MasternodeType variant" desync

Summary

dash-spv's masternode-list sync (via MnListDiff / QRInfo) cannot decode Simplified Masternode List (SML) entries produced by Dash Core 23.1.x. Core 23.1.0 introduced an extended, variable-length service-address structure (ExtNetInfo — the "multiple core P2P addresses" feature, protx update_service coreP2PAddrs), but the dashcore SML deserializer reads a fixed 16-byte IP + 2-byte port for every entry regardless of version. On the first such entry the fixed-size read mis-aligns the byte stream; subsequent fields are read from wrong offsets and a later u16 is interpreted as the EntryMasternodeType tag, tripping:

Invalid MasternodeType variant (max: 1, received: 7791)

…which the SPV peer reports as a framing/decode error, drops the buffer ("stream desync: dropping N bytes (no magic found)"), retries QRInfo 3×, then gives up with No masternode lists available. Masternode-list sync never completes against a Core-23.1 network.

Cross-version evidence (brackets the regression precisely)

The same dash-spv build decodes one network and not the other, and the boundary is exactly the Core 23.0 → 23.1 protocol bump:

Network Core version protocol mn-list (QRInfo) decode
dash-spv eb889af decoder (targets ~22.x) advertises 70237
testnet 23.0.0 70238 ✅ decodes — sync completes
paloma devnet 23.1.2 70240 Invalid MasternodeType variant 7791

70240 is the protocol version Core's extended-address (coreP2PAddrs) feature shipped under (23.1.0). testnet on 23.0.0 (70238) still serializes the legacy fixed-size address, so the same decoder handles it; paloma on 23.1.2 (70240) serializes ExtNetInfo, which the decoder does not understand. This is a forward-compat gap that will reach testnet, then mainnet, as 23.1 rolls out.

Environment

  • rust-dashcore: dev HEAD eb889af13f667ed39c35e8e8a0830eeedf523476 (v0.43.0). Decode sites below are unchanged across recent revs.
  • Failing network: "paloma" devnet — Dash Core 23.1.2 (getnetworkinfo: version 230102, protocolversion 70240), 13 HP/Evo masternodes.
  • Working network (control): testnet — Dash Core 23.0.0 (version 230000, protocolversion 70238).
  • SPV version+verack handshake, headers, filter-headers, and filters all sync cleanly to tip on paloma; the failure is strictly masternode-list (QRInfo) content decode.

Observed symptom (live logs, paloma)

WARN dash_spv::network::peer: <mn-ip>:20001: decode error after framing
   (invalid enum value, max: 1 received: 7791 (Invalid MasternodeType variant)), attempting resync
WARN dash_spv::network::peer: <mn-ip>:20001: stream desync: dropping 20068 bytes (no magic found)
WARN dash_spv::sync::masternodes::sync_manager: QRInfo timeout after 3 retries, skipping masternode sync
ERROR dash_spv::sync::sync_manager: Masternode tick error: Masternode sync failed: No masternode lists available

Root cause (verified against eb889af)

The SML masternode-list-entry decoder has no extended-address (ExtNetInfo) branch.

1. MasternodeListEntry::consensus_decodedash/src/sml/masternode_list_entry/mod.rs:136-163:

let version: u16 = Decodable::consensus_decode(reader)?;        // :137
let pro_reg_tx_hash: ProTxHash = ...                            // :138
let confirmed_hash: ConfirmedHash = ...                         // :139
let service_address: SocketAddr = Decodable::consensus_decode(reader)?;  // :145  <-- ALWAYS fixed 18 bytes
let operator_public_key: BLSPublicKey = ...                     // :146
let key_id_voting: PubkeyHash = ...                             // :147
let is_valid: bool = ...                                        // :148
let mn_type: EntryMasternodeType = if version >= 2 {            // :149  (only v2 gating; no extended-address handling)
    Decodable::consensus_decode(reader)?
} else { EntryMasternodeType::Regular };

2. SocketAddr::consensus_decodedash/src/sml/address.rs. Fixed 16-byte IP + 2-byte (byte-swapped) port; no length prefix, no netinfo version, no multi-address handling:

let mut ip_bytes = [0u8; 16];
reader.read_exact(&mut ip_bytes)?;          // fixed 16
let port = u16::consensus_decode(reader)?.swap_bytes();  // fixed 2

3. EntryMasternodeType::consensus_decodedash/src/sml/masternode_list_entry/mod.rs:50-68. The guard that surfaces the failure:

let variant: u16 = Decodable::consensus_decode(reader)?;     // :53
match variant {
    0 => Ok(Regular),
    1 => Ok(HighPerformance { platform_http_port, platform_node_id }),
    received => Err(Error::InvalidEnumValue { max: 1, received, msg: "Invalid MasternodeType variant" }),  // :64
}

Dash Core side: CSimplifiedMNListEntry serialization gates the service-address representation on entry version (src/evo/simplifiedmns.h); 23.1.0 writes the address as ExtNetInfo (src/evo/netinfo.h) — a versioned, compact-size-prefixed collection of type-tagged service entries — rather than the legacy fixed-size CService (MnNetInfo). Because the Rust decoder always consumes exactly 18 bytes for the address, the first extended-address entry desynchronizes the stream; the misaligned bytes are then read as operator_public_key / key_id_voting / is_valid / mn_type, and the u16 mn_type read returns garbage (7791).

Minimal reproduction

A. In-repo unit test (rust-dashcore) — preferred, no external network

The decode path is already covered by fixture-driven unit tests, so a repro can live entirely in this repo:

  • dash/src/network/message_sml.rs::tests::deserialize_mn_list_diff — loads tests/data/test_DML_diffs/DML_0_2221605.hex and deserializes a MnListDiff.
  • dash/src/sml/masternode_list_engine/mod.rs::testsinclude_bytes! the mn_list_diff_*.bin fixtures + load_qrinfo_2240504_fixture() which decodes a QRInfo.

Every existing fixture (DML_0_2221605, mn_list_diff_0_2227096, mn_list_diff_2227096_2241332, qrinfo_2240504, mn_list_diff_testnet_0_1296600, …) is a mainnet/testnet capture from before Core 23.1, so they all decode cleanly — which is exactly why this gap is invisible in CI today. There is no Core-23.1 fixture in the suite.

A captured Core-23.1 qrinfo fixture is attached below — drop it under dash/tests/data/test_DML_diffs/ and add a test mirroring load_qrinfo_2240504_fixture:

use crate::network::message_qrinfo::QRInfo;

#[test]
fn deserialize_qrinfo_core_23_1() {
    // Core 23.1.2 (paloma devnet) qrinfo, captured off the wire.
    let hex = include_str!("../../tests/data/test_DML_diffs/qrinfo_core231_paloma.hex");
    let bytes = hex::decode(hex.trim()).expect("decode hex");
    // FAILS today: InvalidEnumValue { max: 1, received: 7791 } raised in
    // EntryMasternodeType::consensus_decode — the extended-address (ExtNetInfo)
    // service address is mis-read as a fixed 18 bytes, desyncing the stream.
    let _: QRInfo = deserialize(&bytes).expect("deserialize QRInfo");
}

Run: cargo test -p dashcore --lib sml. The new test fails at dash/src/sml/masternode_list_entry/mod.rs:64; the existing pre-23.1 qrinfo_2240504 / mn_list_diff_* fixtures stay green — isolating the regression to the extended-address format. A serialize-round-trip variant then doubles as the fix's acceptance test.

Attached fixture (captured off paloma, Core 23.1.2):

  • qrinfo-20064.hex — a 20064-byte qrinfo frame; sha256 2811689d4bca44b17a881fc799a563d9ac78c3b7a4b8395a67bb0d857eb61eec.
  • https://gist.github.com/Claudius-Maginificent/2d94ced328dc4b2bfe8f6035057c607b
  • Turnkey: curl -sL https://gist.githubusercontent.com/Claudius-Maginificent/2d94ced328dc4b2bfe8f6035057c607b/raw/qrinfo-20064.hex -o dash/tests/data/test_DML_diffs/qrinfo_core231_paloma.hex
  • Verified independently: dashcore::consensus::deserialize::<QRInfo> on these bytes returns Err(InvalidEnumValue { max: 1, received: 7791, msg: "Invalid MasternodeType variant" }). The identical frame was captured from three separate paloma DAPI nodes — deterministic, not corruption.

B. Standalone dash-spv (no monorepo, no secrets)

Point a dash-spv client (eb889af) at paloma and let masternode-list sync run after headers reach tip:

use dash_spv::{ClientConfig, DevnetConfig};
use dashcore::Network;
use std::net::SocketAddr;

let mut cfg = ClientConfig::new(Network::Devnet)
    .with_devnet(DevnetConfig::new("paloma"))          // required on devnet
    .with_storage_path("/tmp/spv-paloma".into())
    .with_start_height(0);

// paloma HP masternodes, Core P2P :20001
for ip in [
    "68.67.122.198","68.67.122.199","68.67.122.86","68.67.122.197","68.67.122.192",
    "68.67.122.85","68.67.122.88","68.67.122.206","68.67.122.193","68.67.122.195",
    "68.67.122.196","68.67.122.87","68.67.122.207",
] {
    cfg.add_peer(SocketAddr::new(ip.parse().unwrap(), 20001));
}
cfg.validate().unwrap();
// build + start the client; headers / filter-headers / filters reach tip,
// then masternode (QRInfo) sync fails with the decode error above.

paloma uses default LLMQ routing (llmqchainlocks=llmq_devnet, llmqinstantsenddip0024=llmq_devnet_dip0024, llmqplatform=llmq_devnet_platform) — which already matches NetworkLLMQExt's devnet defaults — and no -llmqdevnetparams, so no routing overrides are needed to reproduce.

C. Via the platform e2e suite (real-world repro)

git clone https://github.com/dashpay/platform
cd platform
git checkout 76369d64ced7c9a74c3be0bcf00fed14ba474d2e   # feat/rs-platform-wallet-e2e (incl. DevnetConfig wiring)

This commit already wires the required DevnetConfig into tests/e2e/framework/spv.rs::build_client_config (client_config.devnet = Some(DevnetConfig::new("paloma"))), so a plain checkout reproduces — no extra patch needed.

packages/rs-platform-wallet/tests/.env (paloma addresses; fake seed — the failure occurs during SPV mn-list sync, before the bank seed is ever used):

PLATFORM_WALLET_E2E_BANK_MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
PLATFORM_WALLET_E2E_NETWORK=devnet
PLATFORM_WALLET_E2E_DEVNET_NAME=paloma
PLATFORM_WALLET_E2E_CONTEXT_PROVIDER=spv
PLATFORM_WALLET_E2E_P2P_PORT=20001
PLATFORM_WALLET_E2E_DAPI_ADDRESSES="https://68.67.122.198:1443,https://68.67.122.199:1443,https://68.67.122.86:1443,https://68.67.122.197:1443,https://68.67.122.192:1443,https://68.67.122.85:1443,https://68.67.122.88:1443,https://68.67.122.206:1443,https://68.67.122.193:1443,https://68.67.122.195:1443,https://68.67.122.196:1443,https://68.67.122.87:1443,https://68.67.122.207:1443"
PLATFORM_WALLET_E2E_WORKDIR=/tmp/dash-platform-wallet-e2e-paloma
RUST_LOG=info,dash_spv=info cargo test -p platform-wallet --test e2e --features e2e \
  -- --nocapture --test-threads=1 pa_002_partial_fund_change

Setup reaches SPV start → headers/filters sync to tip → wait_for_mn_list_synced fails with the Invalid MasternodeType variant 7791 decode error above (the test body never runs). The same .env against a testnet bank + endpoints (Core 23.0.0) syncs the mn-list fine.

What this is NOT (ruled out)

Impact

  • dash-spv cannot complete masternode-list sync against Dash Core 23.1.x networks (currently devnets; testnet and mainnet as 23.1 rolls out).
  • Downstream: SPV-derived quorum-public-key lookups (proof verification) and SPV ChainLock / InstantSend verification (e.g. asset-lock proofs) are blocked, because both depend on a synced masternode list.

Proposed fix

Make the SML masternode-list-entry deserializer version-aware:

  1. For the extended-address entry version Core 23.1 emits, decode the service address as the extended netinfo structure (src/evo/netinfo.h CExtNetInfo: a netinfo version byte + compact-size-prefixed, type-tagged address entries) instead of the fixed-size CService. Keep the legacy fixed path for older entries.
  2. Re-confirm the field layout end-to-end against Core's CSimplifiedMNListEntry serialization (src/evo/simplifiedmns.h) — in particular whether the inline Evo platformHTTPPort is still present or now lives inside the extended netinfo, so EntryMasternodeType::HighPerformance decoding matches.
  3. Add round-trip encode/decode tests with a captured Core-23.1 MnListDiff / QRInfo fixture containing at least one extended-address HPMN entry.

Maintainer guidance on the exact ExtNetInfo wire layout would help — the structure above is reconstructed from the Core 23.1.0 source/release notes; we could not capture a raw protx diff (RPC firewalled on the probed nodes).

Secondary (optional, separable)

PROTOCOL_VERSION in dash/src/network/constants.rs:51 is 70237; Dash Core 23.1.0 is 70240. The handshake still completed in our run, but advertising the current protocol version is worth a bump to stay aligned with min-peer-protocol enforcement on newer Core.

References

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions