Skip to content

Commit d5ffc23

Browse files
committed
dash(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 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.
1 parent 62d3096 commit d5ffc23

3 files changed

Lines changed: 259 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272
test_threading test_weights \
7373
test_header_chain test_mempool test_template_builder \
7474
test_doge_chain test_compact_blocks test_dash_x11_kat \
75-
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_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer \
75+
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_share_hash_link test_dash_block_relay test_dash_embedded_relay_e2e test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer \
7676
test_multiaddress_pplns test_pplns_stress \
7777
test_hash_link test_decay_pplns \
7878
test_pplns_consensus \
@@ -205,7 +205,7 @@ jobs:
205205
test_threading test_weights \
206206
test_header_chain test_mempool test_template_builder \
207207
test_doge_chain test_compact_blocks test_dash_x11_kat \
208-
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_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer \
208+
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_share_hash_link test_dash_block_relay test_dash_embedded_relay_e2e test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer \
209209
test_hash_link test_decay_pplns \
210210
test_pplns_consensus \
211211
test_v36_script_sorting test_v36_cross_impl_refhash \

test/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,16 @@ if (BUILD_TESTING AND GTest_FOUND)
488488
target_link_libraries(test_dash_block_relay PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39)
489489
gtest_add_tests(test_dash_block_relay "" AUTO)
490490

491+
add_executable(test_dash_embedded_relay_e2e test_dash_embedded_relay_e2e.cpp)
492+
target_link_libraries(test_dash_embedded_relay_e2e PRIVATE
493+
GTest::gtest_main GTest::gtest
494+
dash_x11 core
495+
nlohmann_json::nlohmann_json
496+
${Boost_LIBRARIES}
497+
)
498+
target_link_libraries(test_dash_embedded_relay_e2e PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39)
499+
gtest_add_tests(test_dash_embedded_relay_e2e "" AUTO)
500+
491501
# DASH block PRODUCER KAT (launcher slice 5): merkle fold + compact-bits
492502
# target + full-block serialize + X11 nonce search (block_producer.hpp).
493503
# Network-free; builds a real coinbase via coinbase_builder over the
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/// test_dash_embedded_relay_e2e.cpp
2+
///
3+
/// Phase S8 — EMBEDDED won-block relay END-TO-END KAT (won-block-reaches-network).
4+
///
5+
/// G3b proved the dashd `submitblock` RPC FALLBACK arm of the dual-path
6+
/// block-viability gate end-to-end on regtest (c2pool-dash won block 272,
7+
/// ACCEPTED, mempool drained). This KAT proves the OTHER arm — the EMBEDDED P2P
8+
/// relay — at the wire-framing level, which G3b did NOT exercise:
9+
///
10+
/// DashBroadcaster (leaf, #405) --fan-out hook--> per-slot submit seam
11+
/// |
12+
/// WonBlockRelay (#432/#441) framer
13+
/// inv(MSG_BLOCK,h) -> getdata -> block
14+
/// |
15+
/// [peer reconstructs block]
16+
///
17+
/// Both leaves are on master but had only ISOLATED unit coverage; the embedded
18+
/// arm is the COMPOSITION the (operator-gated) broadcaster_full.hpp keystone
19+
/// drives onto a live socket. Here we drive that composition socket-free and
20+
/// assert every LIVE peer reconstructs the EXACT won block (byte-parity vs the
21+
/// canonical serialization), and that a COLD pool reaches zero peers — which is
22+
/// precisely why the dashd-RPC fallback arm (proven in G3b) is never removed.
23+
///
24+
/// Single dash tree, non-consensus, zero sockets. Block hash is producer-
25+
/// supplied (not recomputed), so this carries no consensus value.
26+
27+
#include <gtest/gtest.h>
28+
29+
#include <impl/dash/broadcaster.hpp>
30+
#include <impl/dash/block_relay.hpp>
31+
#include <impl/dash/config.hpp>
32+
#include <impl/dash/coin/p2p_messages.hpp>
33+
34+
#include <core/netaddress.hpp>
35+
#include <core/uint256.hpp>
36+
#include <core/pack.hpp>
37+
38+
#include <boost/asio.hpp>
39+
40+
#include <array>
41+
#include <cstring>
42+
#include <memory>
43+
#include <span>
44+
#include <string>
45+
#include <vector>
46+
47+
using namespace dash;
48+
using dash::coin::BlockType;
49+
using dash::coin::p2p::message_inv;
50+
using dash::coin::p2p::message_block;
51+
using dash::coin::p2p::inventory_type;
52+
using nlohmann::json;
53+
54+
namespace {
55+
56+
constexpr uint16_t kCanonicalPort = 19999;
57+
constexpr char kPrimaryHost[] = "10.9.9.9";
58+
59+
uint256 hash_seq(uint8_t base) {
60+
std::array<uint8_t, 32> p{};
61+
for (size_t i = 0; i < 32; ++i) p[i] = static_cast<uint8_t>(base + i);
62+
uint256 h; std::memcpy(h.data(), p.data(), 32); return h;
63+
}
64+
65+
BlockType make_block(uint8_t seed) {
66+
BlockType b;
67+
b.m_version = 0x20000000u | seed;
68+
b.m_previous_block = hash_seq(0x10 + seed);
69+
b.m_merkle_root = hash_seq(0x50 + seed);
70+
b.m_timestamp = 0x5f5e1000u + seed;
71+
b.m_bits = 0x1d00ffffu;
72+
b.m_nonce = 0xdeadbeefu - seed;
73+
return b;
74+
}
75+
76+
// Non-destructive byte view of a PackStream (does not advance the read cursor).
77+
std::vector<unsigned char> bytes_of(PackStream& ps) {
78+
auto sp = ps.get_span();
79+
auto* p = reinterpret_cast<const unsigned char*>(sp.data());
80+
return std::vector<unsigned char>(p, p + sp.size());
81+
}
82+
83+
std::unique_ptr<Config> make_config() {
84+
auto cfg = std::make_unique<Config>("dash-s8-embedded-relay-e2e-kat");
85+
cfg->coin()->m_p2p.address =
86+
NetService{std::string{"127.0.0.1"}, std::to_string(kCanonicalPort)};
87+
return cfg;
88+
}
89+
90+
json peer(const std::string& addr) {
91+
json p = json::object();
92+
p["addr"] = addr;
93+
return p;
94+
}
95+
96+
// Leaf broadcaster with a bare-NodeP2P factory + injected liveness, populated
97+
// with N live slots via a discovery pass (mirrors test_dash_broadcaster_full).
98+
struct PoolEnv {
99+
boost::asio::io_context ioc;
100+
std::unique_ptr<Config> cfg{make_config()};
101+
bool all_live{true};
102+
std::unique_ptr<DashBroadcaster> pool;
103+
104+
explicit PoolEnv(size_t max_peers = 16) {
105+
pool = std::make_unique<DashBroadcaster>(
106+
&ioc, cfg.get(),
107+
NetService{std::string{kPrimaryHost}, std::to_string(kCanonicalPort)},
108+
max_peers);
109+
pool->set_slot_factory([this](const NetService&) {
110+
return std::make_unique<dash::coin::p2p::NodeP2P>(&ioc);
111+
});
112+
pool->set_live_predicate(
113+
[this](const dash::coin::p2p::NodeP2P&) { return all_live; });
114+
}
115+
116+
void dial(size_t n) {
117+
json peers = json::array();
118+
for (size_t i = 0; i < n; ++i)
119+
peers.push_back(peer("10.0.0." + std::to_string(i + 1) + ":19999"));
120+
size_t got = pool->discover(peers);
121+
ASSERT_EQ(got, n);
122+
}
123+
};
124+
125+
// Wire the leaf's fan-out seam to the WonBlockRelay handshake: each LIVE peer
126+
// the fan-out reaches runs getdata(h) against the relay and reconstructs the
127+
// served block. Records one reconstructed block per peer reached.
128+
struct EmbeddedRelayHarness {
129+
WonBlockRelay relay;
130+
BlockType won;
131+
uint256 won_hash;
132+
std::vector<unsigned char> won_wire; // canonical serialized block
133+
std::vector<BlockType> delivered; // one per reached peer
134+
std::vector<std::vector<unsigned char>> fanned; // bytes the seam handed each peer
135+
136+
EmbeddedRelayHarness(uint8_t seed, uint8_t hashbase)
137+
: won(make_block(seed)), won_hash(hash_seq(hashbase))
138+
{
139+
won_wire = bytes_of(message_block::make_raw(won)->m_data);
140+
}
141+
142+
// Announce once (records the won block + builds the inv to fan out), wire the
143+
// leaf seam to the per-peer getdata->block handshake, then drive the won
144+
// block through submit_block_raw_all. Returns the live-peer fan-out count.
145+
size_t broadcast(DashBroadcaster& pool) {
146+
auto inv = relay.announce(won_hash, won);
147+
// The inv that fans to every peer carries exactly our MSG_BLOCK hash.
148+
EXPECT_EQ(inv->m_command, "inv");
149+
auto pinv = message_inv::make(inv->m_data);
150+
EXPECT_EQ(pinv->m_invs.size(), 1u);
151+
EXPECT_EQ(static_cast<uint32_t>(pinv->m_invs[0].m_type),
152+
static_cast<uint32_t>(inventory_type::block));
153+
EXPECT_EQ(pinv->m_invs[0].m_hash, won_hash);
154+
155+
pool.set_fan_out_hook([this](dash::coin::p2p::NodeP2P&,
156+
std::span<const unsigned char> b) {
157+
fanned.emplace_back(b.begin(), b.end());
158+
// Peer side: getdata(MSG_BLOCK, h) -> full `block` message -> parse.
159+
auto blkmsg = relay.on_getdata_block(won_hash);
160+
ASSERT_NE(blkmsg, nullptr);
161+
EXPECT_EQ(blkmsg->m_command, "block");
162+
auto parsed = message_block::make(blkmsg->m_data);
163+
delivered.push_back(parsed->m_block);
164+
});
165+
return pool.submit_block_raw_all(won_wire);
166+
}
167+
};
168+
169+
void expect_block_eq(const BlockType& a, const BlockType& b) {
170+
EXPECT_EQ(a.m_version, b.m_version);
171+
EXPECT_EQ(a.m_previous_block, b.m_previous_block);
172+
EXPECT_EQ(a.m_merkle_root, b.m_merkle_root);
173+
EXPECT_EQ(a.m_timestamp, b.m_timestamp);
174+
EXPECT_EQ(a.m_bits, b.m_bits);
175+
EXPECT_EQ(a.m_nonce, b.m_nonce);
176+
}
177+
178+
} // namespace
179+
180+
// (1) Embedded arm delivers the EXACT won block to EVERY live peer ────────────
181+
TEST(DashEmbeddedRelayE2E, DeliversExactWonBlockToEveryLivePeer) {
182+
PoolEnv env;
183+
env.dial(3);
184+
EmbeddedRelayHarness h{7, 0xA0};
185+
186+
size_t reached = h.broadcast(*env.pool);
187+
188+
EXPECT_EQ(reached, 3u); // leaf fanned to all live slots
189+
ASSERT_EQ(h.delivered.size(), 3u); // 3 peers completed the handshake
190+
ASSERT_EQ(h.fanned.size(), 3u);
191+
for (size_t i = 0; i < h.delivered.size(); ++i) {
192+
expect_block_eq(h.delivered[i], h.won); // reconstructed == won block
193+
// byte-parity: relay-served bytes == canonical won-block wire bytes, and
194+
// the bytes the seam handed the peer are the same won-block bytes.
195+
auto served = bytes_of(message_block::make_raw(h.delivered[i])->m_data);
196+
EXPECT_EQ(served, h.won_wire);
197+
EXPECT_EQ(h.fanned[i], h.won_wire);
198+
}
199+
}
200+
201+
// (2) COLD pool: zero live peers -> embedded arm reaches NO peer ──────────────
202+
// The embedded arm alone can silently drop a won block — exactly why the
203+
// authoritative dashd-RPC fallback arm (proven end-to-end in G3b) is kept.
204+
TEST(DashEmbeddedRelayE2E, ColdPoolReachesNoPeer) {
205+
PoolEnv env;
206+
env.dial(3);
207+
env.all_live = false; // every slot now dead
208+
EmbeddedRelayHarness h{4, 0x30};
209+
210+
size_t reached = h.broadcast(*env.pool);
211+
212+
EXPECT_EQ(reached, 0u);
213+
EXPECT_TRUE(h.delivered.empty()); // no peer reconstructed a block
214+
EXPECT_TRUE(h.fanned.empty());
215+
}
216+
217+
// (3) The embedded arm never serves a block the producer did not announce ─────
218+
TEST(DashEmbeddedRelayE2E, NeverServesUnannouncedBlock) {
219+
WonBlockRelay relay;
220+
const uint256 announced = hash_seq(0x11);
221+
relay.announce(announced, make_block(2));
222+
223+
// getdata for a hash we never announced as won -> nothing on the wire.
224+
EXPECT_EQ(relay.on_getdata_block(hash_seq(0x99)), nullptr);
225+
226+
// The announced won block is served intact.
227+
auto msg = relay.on_getdata_block(announced);
228+
ASSERT_NE(msg, nullptr);
229+
auto parsed = message_block::make(msg->m_data);
230+
expect_block_eq(parsed->m_block, make_block(2));
231+
}
232+
233+
// (4) Byte-parity is identical across ALL peers — no per-peer fan-out drift ───
234+
TEST(DashEmbeddedRelayE2E, ByteParityIdenticalAcrossAllPeers) {
235+
PoolEnv env;
236+
env.dial(5);
237+
EmbeddedRelayHarness h{9, 0xC0};
238+
239+
size_t reached = h.broadcast(*env.pool);
240+
241+
ASSERT_EQ(reached, 5u);
242+
ASSERT_EQ(h.delivered.size(), 5u);
243+
for (const auto& d : h.delivered) {
244+
auto served = bytes_of(message_block::make_raw(d)->m_data);
245+
EXPECT_EQ(served, h.won_wire); // every peer got identical bytes
246+
}
247+
}

0 commit comments

Comments
 (0)