From eb7eccb067ea62946edf91b45349f69a3f02b8a4 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 25 Jun 2026 07:34:02 +0000 Subject: [PATCH] dash(s8): embedded won-block relay end-to-end KAT (won-block-reaches-network) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G3b proved the dashd submitblock RPC FALLBACK arm of the dual-path block-viability gate end-to-end on regtest (c2pool-dash won block 272, ACCEPTED, mempool drained). This KAT proves the OTHER arm — the EMBEDDED P2P relay — at the wire-framing level, which G3b never exercised. Composes the LANDED master leaves socket-free, so it carries no dependency on the operator-gated broadcaster_full.hpp keystone: DashBroadcaster (#405) fan-out seam --> WonBlockRelay (#432/#441) inv(MSG_BLOCK,h) -> getdata -> block --> peer reconstructs the block 4 KATs (Linux x86_64, ctest 4/4, non-hollow): - DeliversExactWonBlockToEveryLivePeer: every live slot reconstructs the EXACT won block; byte-parity vs canonical block-message wire bytes. - ColdPoolReachesNoPeer: zero live peers -> embedded arm reaches none — precisely why the authoritative dashd-RPC fallback arm is never removed. - NeverServesUnannouncedBlock: relay serves only the announced won block. - ByteParityIdenticalAcrossAllPeers: no per-peer fan-out drift. Single dash tree, non-consensus, zero sockets; block hash is producer- supplied (not recomputed). Additive only: one new test file + build.yml allowlist + CMakeLists target; no src, no master, no other-coin tree. --- .github/workflows/build.yml | 4 +- test/CMakeLists.txt | 10 ++ test/test_dash_embedded_relay_e2e.cpp | 247 ++++++++++++++++++++++++++ 3 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 test/test_dash_embedded_relay_e2e.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d24bfc7e2..5f8aaaaa2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,7 +72,7 @@ jobs: test_threading test_weights \ test_header_chain test_mempool test_template_builder \ test_doge_chain test_compact_blocks test_dash_x11_kat \ - test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_block_relay_plan \ + test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan \ test_multiaddress_pplns test_pplns_stress \ test_hash_link test_decay_pplns \ test_pplns_consensus \ @@ -205,7 +205,7 @@ jobs: test_threading test_weights \ test_header_chain test_mempool test_template_builder \ test_doge_chain test_compact_blocks test_dash_x11_kat \ - test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_block_relay_plan \ + test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan \ test_hash_link test_decay_pplns \ test_pplns_consensus \ test_v36_script_sorting test_v36_cross_impl_refhash \ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ebd26320a..f3b274c51 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -498,6 +498,16 @@ if (BUILD_TESTING AND GTest_FOUND) target_link_libraries(test_dash_block_relay PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) gtest_add_tests(test_dash_block_relay "" AUTO) + add_executable(test_dash_embedded_relay_e2e test_dash_embedded_relay_e2e.cpp) + target_link_libraries(test_dash_embedded_relay_e2e PRIVATE + GTest::gtest_main GTest::gtest + dash_x11 core + nlohmann_json::nlohmann_json + ${Boost_LIBRARIES} + ) + target_link_libraries(test_dash_embedded_relay_e2e PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) + gtest_add_tests(test_dash_embedded_relay_e2e "" AUTO) + # DASH block PRODUCER KAT (launcher slice 5): merkle fold + compact-bits # target + full-block serialize + X11 nonce search (block_producer.hpp). # Network-free; builds a real coinbase via coinbase_builder over the diff --git a/test/test_dash_embedded_relay_e2e.cpp b/test/test_dash_embedded_relay_e2e.cpp new file mode 100644 index 000000000..20d51fad2 --- /dev/null +++ b/test/test_dash_embedded_relay_e2e.cpp @@ -0,0 +1,247 @@ +/// test_dash_embedded_relay_e2e.cpp +/// +/// Phase S8 — EMBEDDED won-block relay END-TO-END KAT (won-block-reaches-network). +/// +/// G3b proved the dashd `submitblock` RPC FALLBACK arm of the dual-path +/// block-viability gate end-to-end on regtest (c2pool-dash won block 272, +/// ACCEPTED, mempool drained). This KAT proves the OTHER arm — the EMBEDDED P2P +/// relay — at the wire-framing level, which G3b did NOT exercise: +/// +/// DashBroadcaster (leaf, #405) --fan-out hook--> per-slot submit seam +/// | +/// WonBlockRelay (#432/#441) framer +/// inv(MSG_BLOCK,h) -> getdata -> block +/// | +/// [peer reconstructs block] +/// +/// Both leaves are on master but had only ISOLATED unit coverage; the embedded +/// arm is the COMPOSITION the (operator-gated) broadcaster_full.hpp keystone +/// drives onto a live socket. Here we drive that composition socket-free and +/// assert every LIVE peer reconstructs the EXACT won block (byte-parity vs the +/// canonical serialization), and that a COLD pool reaches zero peers — which is +/// precisely why the dashd-RPC fallback arm (proven in G3b) is never removed. +/// +/// Single dash tree, non-consensus, zero sockets. Block hash is producer- +/// supplied (not recomputed), so this carries no consensus value. + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +using namespace dash; +using dash::coin::BlockType; +using dash::coin::p2p::message_inv; +using dash::coin::p2p::message_block; +using dash::coin::p2p::inventory_type; +using nlohmann::json; + +namespace { + +constexpr uint16_t kCanonicalPort = 19999; +constexpr char kPrimaryHost[] = "10.9.9.9"; + +uint256 hash_seq(uint8_t base) { + std::array p{}; + for (size_t i = 0; i < 32; ++i) p[i] = static_cast(base + i); + uint256 h; std::memcpy(h.data(), p.data(), 32); return h; +} + +BlockType make_block(uint8_t seed) { + BlockType b; + b.m_version = 0x20000000u | seed; + b.m_previous_block = hash_seq(0x10 + seed); + b.m_merkle_root = hash_seq(0x50 + seed); + b.m_timestamp = 0x5f5e1000u + seed; + b.m_bits = 0x1d00ffffu; + b.m_nonce = 0xdeadbeefu - seed; + return b; +} + +// Non-destructive byte view of a PackStream (does not advance the read cursor). +std::vector bytes_of(PackStream& ps) { + auto sp = ps.get_span(); + auto* p = reinterpret_cast(sp.data()); + return std::vector(p, p + sp.size()); +} + +std::unique_ptr make_config() { + auto cfg = std::make_unique("dash-s8-embedded-relay-e2e-kat"); + cfg->coin()->m_p2p.address = + NetService{std::string{"127.0.0.1"}, std::to_string(kCanonicalPort)}; + return cfg; +} + +json peer(const std::string& addr) { + json p = json::object(); + p["addr"] = addr; + return p; +} + +// Leaf broadcaster with a bare-NodeP2P factory + injected liveness, populated +// with N live slots via a discovery pass (mirrors test_dash_broadcaster_full). +struct PoolEnv { + boost::asio::io_context ioc; + std::unique_ptr cfg{make_config()}; + bool all_live{true}; + std::unique_ptr pool; + + explicit PoolEnv(size_t max_peers = 16) { + pool = std::make_unique( + &ioc, cfg.get(), + NetService{std::string{kPrimaryHost}, std::to_string(kCanonicalPort)}, + max_peers); + pool->set_slot_factory([this](const NetService&) { + return std::make_unique(&ioc); + }); + pool->set_live_predicate( + [this](const dash::coin::p2p::NodeP2P&) { return all_live; }); + } + + void dial(size_t n) { + json peers = json::array(); + for (size_t i = 0; i < n; ++i) + peers.push_back(peer("10.0.0." + std::to_string(i + 1) + ":19999")); + size_t got = pool->discover(peers); + ASSERT_EQ(got, n); + } +}; + +// Wire the leaf's fan-out seam to the WonBlockRelay handshake: each LIVE peer +// the fan-out reaches runs getdata(h) against the relay and reconstructs the +// served block. Records one reconstructed block per peer reached. +struct EmbeddedRelayHarness { + WonBlockRelay relay; + BlockType won; + uint256 won_hash; + std::vector won_wire; // canonical serialized block + std::vector delivered; // one per reached peer + std::vector> fanned; // bytes the seam handed each peer + + EmbeddedRelayHarness(uint8_t seed, uint8_t hashbase) + : won(make_block(seed)), won_hash(hash_seq(hashbase)) + { + won_wire = bytes_of(message_block::make_raw(won)->m_data); + } + + // Announce once (records the won block + builds the inv to fan out), wire the + // leaf seam to the per-peer getdata->block handshake, then drive the won + // block through submit_block_raw_all. Returns the live-peer fan-out count. + size_t broadcast(DashBroadcaster& pool) { + auto inv = relay.announce(won_hash, won); + // The inv that fans to every peer carries exactly our MSG_BLOCK hash. + EXPECT_EQ(inv->m_command, "inv"); + auto pinv = message_inv::make(inv->m_data); + EXPECT_EQ(pinv->m_invs.size(), 1u); + EXPECT_EQ(static_cast(pinv->m_invs[0].m_type), + static_cast(inventory_type::block)); + EXPECT_EQ(pinv->m_invs[0].m_hash, won_hash); + + pool.set_fan_out_hook([this](dash::coin::p2p::NodeP2P&, + std::span b) { + fanned.emplace_back(b.begin(), b.end()); + // Peer side: getdata(MSG_BLOCK, h) -> full `block` message -> parse. + auto blkmsg = relay.on_getdata_block(won_hash); + ASSERT_NE(blkmsg, nullptr); + EXPECT_EQ(blkmsg->m_command, "block"); + auto parsed = message_block::make(blkmsg->m_data); + delivered.push_back(parsed->m_block); + }); + return pool.submit_block_raw_all(won_wire); + } +}; + +void expect_block_eq(const BlockType& a, const BlockType& b) { + EXPECT_EQ(a.m_version, b.m_version); + EXPECT_EQ(a.m_previous_block, b.m_previous_block); + EXPECT_EQ(a.m_merkle_root, b.m_merkle_root); + EXPECT_EQ(a.m_timestamp, b.m_timestamp); + EXPECT_EQ(a.m_bits, b.m_bits); + EXPECT_EQ(a.m_nonce, b.m_nonce); +} + +} // namespace + +// (1) Embedded arm delivers the EXACT won block to EVERY live peer ──────────── +TEST(DashEmbeddedRelayE2E, DeliversExactWonBlockToEveryLivePeer) { + PoolEnv env; + env.dial(3); + EmbeddedRelayHarness h{7, 0xA0}; + + size_t reached = h.broadcast(*env.pool); + + EXPECT_EQ(reached, 3u); // leaf fanned to all live slots + ASSERT_EQ(h.delivered.size(), 3u); // 3 peers completed the handshake + ASSERT_EQ(h.fanned.size(), 3u); + for (size_t i = 0; i < h.delivered.size(); ++i) { + expect_block_eq(h.delivered[i], h.won); // reconstructed == won block + // byte-parity: relay-served bytes == canonical won-block wire bytes, and + // the bytes the seam handed the peer are the same won-block bytes. + auto served = bytes_of(message_block::make_raw(h.delivered[i])->m_data); + EXPECT_EQ(served, h.won_wire); + EXPECT_EQ(h.fanned[i], h.won_wire); + } +} + +// (2) COLD pool: zero live peers -> embedded arm reaches NO peer ────────────── +// The embedded arm alone can silently drop a won block — exactly why the +// authoritative dashd-RPC fallback arm (proven end-to-end in G3b) is kept. +TEST(DashEmbeddedRelayE2E, ColdPoolReachesNoPeer) { + PoolEnv env; + env.dial(3); + env.all_live = false; // every slot now dead + EmbeddedRelayHarness h{4, 0x30}; + + size_t reached = h.broadcast(*env.pool); + + EXPECT_EQ(reached, 0u); + EXPECT_TRUE(h.delivered.empty()); // no peer reconstructed a block + EXPECT_TRUE(h.fanned.empty()); +} + +// (3) The embedded arm never serves a block the producer did not announce ───── +TEST(DashEmbeddedRelayE2E, NeverServesUnannouncedBlock) { + WonBlockRelay relay; + const uint256 announced = hash_seq(0x11); + relay.announce(announced, make_block(2)); + + // getdata for a hash we never announced as won -> nothing on the wire. + EXPECT_EQ(relay.on_getdata_block(hash_seq(0x99)), nullptr); + + // The announced won block is served intact. + auto msg = relay.on_getdata_block(announced); + ASSERT_NE(msg, nullptr); + auto parsed = message_block::make(msg->m_data); + expect_block_eq(parsed->m_block, make_block(2)); +} + +// (4) Byte-parity is identical across ALL peers — no per-peer fan-out drift ─── +TEST(DashEmbeddedRelayE2E, ByteParityIdenticalAcrossAllPeers) { + PoolEnv env; + env.dial(5); + EmbeddedRelayHarness h{9, 0xC0}; + + size_t reached = h.broadcast(*env.pool); + + ASSERT_EQ(reached, 5u); + ASSERT_EQ(h.delivered.size(), 5u); + for (const auto& d : h.delivered) { + auto served = bytes_of(message_block::make_raw(d)->m_data); + EXPECT_EQ(served, h.won_wire); // every peer got identical bytes + } +}