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

@Claudius-Maginificent

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
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