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_decode — dash/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_decode — dash/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_decode — dash/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::tests — include_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:
- 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.
- 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.
- 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
dash-spv/dashcore: SML masternode-list-entry decoder fails on Dash Core 23.1.x (
ExtNetInfo/ extended core-P2P addresses) — "Invalid MasternodeType variant" desyncSummary
dash-spv's masternode-list sync (viaMnListDiff/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 thedashcoreSML 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 lateru16is interpreted as theEntryMasternodeTypetag, tripping:…which the SPV peer reports as a framing/decode error, drops the buffer ("stream desync: dropping N bytes (no magic found)"), retries
QRInfo3×, then gives up withNo masternode lists available. Masternode-list sync never completes against a Core-23.1 network.Cross-version evidence (brackets the regression precisely)
The same
dash-spvbuild decodes one network and not the other, and the boundary is exactly the Core 23.0 → 23.1 protocol bump:dash-spveb889afdecoderInvalid MasternodeType variant 779170240is 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) serializesExtNetInfo, which the decoder does not understand. This is a forward-compat gap that will reach testnet, then mainnet, as 23.1 rolls out.Environment
devHEADeb889af13f667ed39c35e8e8a0830eeedf523476(v0.43.0). Decode sites below are unchanged across recent revs.getnetworkinfo:version 230102,protocolversion 70240), 13 HP/Evo masternodes.version 230000,protocolversion 70238).QRInfo) content decode.Observed symptom (live logs, paloma)
Root cause (verified against
eb889af)The SML masternode-list-entry decoder has no extended-address (
ExtNetInfo) branch.1.
MasternodeListEntry::consensus_decode—dash/src/sml/masternode_list_entry/mod.rs:136-163:2.
SocketAddr::consensus_decode—dash/src/sml/address.rs. Fixed 16-byte IP + 2-byte (byte-swapped) port; no length prefix, no netinfo version, no multi-address handling:3.
EntryMasternodeType::consensus_decode—dash/src/sml/masternode_list_entry/mod.rs:50-68. The guard that surfaces the failure:Dash Core side:
CSimplifiedMNListEntryserialization gates the service-address representation on entry version (src/evo/simplifiedmns.h); 23.1.0 writes the address asExtNetInfo(src/evo/netinfo.h) — a versioned, compact-size-prefixed collection of type-tagged service entries — rather than the legacy fixed-sizeCService(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 asoperator_public_key/key_id_voting/is_valid/mn_type, and theu16mn_typeread 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— loadstests/data/test_DML_diffs/DML_0_2221605.hexanddeserializes aMnListDiff.dash/src/sml/masternode_list_engine/mod.rs::tests—include_bytes!themn_list_diff_*.binfixtures +load_qrinfo_2240504_fixture()which decodes aQRInfo.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
qrinfofixture is attached below — drop it underdash/tests/data/test_DML_diffs/and add a test mirroringload_qrinfo_2240504_fixture:Run:
cargo test -p dashcore --lib sml. The new test fails atdash/src/sml/masternode_list_entry/mod.rs:64; the existing pre-23.1qrinfo_2240504/mn_list_diff_*fixtures stay green — isolating the regression to the extended-address format. Aserialize-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-byteqrinfoframe; sha2562811689d4bca44b17a881fc799a563d9ac78c3b7a4b8395a67bb0d857eb61eec.curl -sL https://gist.githubusercontent.com/Claudius-Maginificent/2d94ced328dc4b2bfe8f6035057c607b/raw/qrinfo-20064.hex -o dash/tests/data/test_DML_diffs/qrinfo_core231_paloma.hexdashcore::consensus::deserialize::<QRInfo>on these bytes returnsErr(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-spvclient (eb889af) at paloma and let masternode-list sync run after headers reach tip:paloma uses default LLMQ routing (
llmqchainlocks=llmq_devnet,llmqinstantsenddip0024=llmq_devnet_dip0024,llmqplatform=llmq_devnet_platform) — which already matchesNetworkLLMQExt's devnet defaults — and no-llmqdevnetparams, so no routing overrides are needed to reproduce.C. Via the platform e2e suite (real-world repro)
This commit already wires the required
DevnetConfigintotests/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):RUST_LOG=info,dash_spv=info cargo test -p platform-wallet --test e2e --features e2e \ -- --nocapture --test-threads=1 pa_002_partial_fund_changeSetup reaches SPV start → headers/filters sync to tip →
wait_for_mn_list_syncedfails with theInvalid MasternodeType variant 7791decode error above (the test body never runs). The same.envagainst a testnet bank + endpoints (Core 23.0.0) syncs the mn-list fine.What this is NOT (ruled out)
LLMQTypedecoder gap flagged in PR feat(dash-spv): devnet LLMQ routing overrides (chainlocks/isd24/platform) #786:eb889afalready maps107 => LlmqtypeDevnetPlatform(dash/src/sml/llmq_type/mod.rs:579).NetworkLLMQExtdevnet defaults; no-llmqdevnetparams.PROTOCOL_VERSIONis still70237atdash/src/network/constants.rs:51vs the node's70240— see Secondary.)Impact
dash-spvcannot complete masternode-list sync against Dash Core 23.1.x networks (currently devnets; testnet and mainnet as 23.1 rolls out).Proposed fix
Make the SML masternode-list-entry deserializer version-aware:
src/evo/netinfo.hCExtNetInfo: a netinfo version byte + compact-size-prefixed, type-tagged address entries) instead of the fixed-sizeCService. Keep the legacy fixed path for older entries.CSimplifiedMNListEntryserialization (src/evo/simplifiedmns.h) — in particular whether the inline EvoplatformHTTPPortis still present or now lives inside the extended netinfo, soEntryMasternodeType::HighPerformancedecoding matches.MnListDiff/QRInfofixture containing at least one extended-address HPMN entry.Maintainer guidance on the exact
ExtNetInfowire layout would help — the structure above is reconstructed from the Core 23.1.0 source/release notes; we could not capture a rawprotx diff(RPC firewalled on the probed nodes).Secondary (optional, separable)
PROTOCOL_VERSIONindash/src/network/constants.rs:51is70237; Dash Core 23.1.0 is70240. 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
dash/src/sml/masternode_list_entry/mod.rs:{50-68,136-163},dash/src/sml/address.rs.dashpay/dashsrc/evo/{simplifiedmns.h,providertx.h,netinfo.h}; Dash Core 23.1.0 release notes (extended core-P2P addresses).DevnetConfigstruct #788 (devnet LLMQ routing & config), dash-spv: ClientConfig has no way to supply a devnet genesis block hash #789 (devnet genesis hash).