Skip to content

Commit a1da5e6

Browse files
authored
dash(s8): WonBlockRelay embedded-P2P won-block relay framer + KATs (#432)
The framing half of the embedded-P2P broadcast leg of the dual-path block-viability gate (the dashd-submitblock arm is already proven via the launcher producer). Frames the standard Dash block-relay handshake the DashBroadcaster fans out on a won block: inv(MSG_BLOCK,hash) announce -> peer getdata -> full block message, with a pending-block book keyed by hash. Pure + socket-free: the block hash is SUPPLIED by the consensus/producer layer (the X11 header hash on the won-block path), not recomputed here, so this leaf carries no consensus value. The decision to actually transmit a real won block onto the live net (on_block_found -> announce -> live sockets) stays in the operator-gated broadcaster_full keystone. Header-only, single dash tree. 5/5 ctest green on Linux x86_64 (non-hollow). Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
1 parent 6c95f02 commit a1da5e6

4 files changed

Lines changed: 245 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 \
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 \
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 \
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 \
209209
test_hash_link test_decay_pplns \
210210
test_pplns_consensus \
211211
test_v36_script_sorting test_v36_cross_impl_refhash \

src/impl/dash/block_relay.hpp

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#pragma once
2+
3+
// Phase S8 — WonBlockRelay: embedded-P2P won-block relay framer (LEAF).
4+
//
5+
// This is the framing half of the embedded-P2P broadcast leg of the dual-path
6+
// block-viability gate. The OTHER arm (won block -> dashd submitblock RPC) is
7+
// already proven (the launcher producer, regtest crossing). When DASH wins a
8+
// block, the embedded path must also fan it out to the parent-chain (dashd)
9+
// peers the DashBroadcaster pool holds. Dash/Bitcoin block relay is a two-step
10+
// handshake, not a raw push:
11+
//
12+
// [we win a block]
13+
// -> announce : send `inv(MSG_BLOCK, hash)` to every live peer
14+
// [peer] -> getdata(MSG_BLOCK, hash)
15+
// -> on_getdata_block : answer with the full `block` message
16+
//
17+
// This LEAF is the PURE, socket-free framer + pending-block book for exactly
18+
// that handshake:
19+
//
20+
// * announce(hash, block) -> the `inv` RawMessage to fan out, and
21+
// records the block under `hash` so a later
22+
// getdata can be answered.
23+
// * on_getdata_block(hash) -> the full `block` RawMessage if known, else
24+
// nullptr (we never serve a block we did not
25+
// announce).
26+
// * knows / pending / forget / clear — the pending-block bookkeeping.
27+
//
28+
// SCOPE / NON-CONSENSUS: the block hash is SUPPLIED by the consensus/producer
29+
// layer (the X11 header hash already computed on the won-block path) — it is
30+
// NOT recomputed here, so this leaf carries no consensus value and cannot
31+
// silently diverge a hash. The DECISION to actually transmit a REAL won block
32+
// onto the live network — wiring on_block_found -> announce -> the live slots'
33+
// sockets — stays in the operator-gated broadcaster_full keystone. This leaf
34+
// only builds the wire frames and answers getdata deterministically, so it is
35+
// unit-testable with zero sockets and zero live dashd, like its sibling leaves
36+
// (broadcaster.hpp, p2p_messages.hpp). Header-only, single dash tree.
37+
38+
#include "coin/p2p_messages.hpp"
39+
#include "coin/block.hpp"
40+
41+
#include <core/message.hpp>
42+
#include <core/uint256.hpp>
43+
44+
#include <map>
45+
#include <memory>
46+
#include <vector>
47+
48+
namespace dash
49+
{
50+
51+
// Won-block relay framer + pending-block book. Non-consensus, socket-free.
52+
class WonBlockRelay
53+
{
54+
public:
55+
using inventory_type = dash::coin::p2p::inventory_type;
56+
using BlockType = dash::coin::BlockType;
57+
58+
// Record a won block under its (consensus-computed) block hash and return
59+
// the `inv(MSG_BLOCK, hash)` RawMessage to fan out to the live peers.
60+
// Idempotent: re-announcing a known hash refreshes the stored block.
61+
std::unique_ptr<RawMessage> announce(const uint256& hash, BlockType block)
62+
{
63+
m_pending[hash] = std::move(block);
64+
std::vector<inventory_type> invs{
65+
inventory_type(inventory_type::block, hash)};
66+
return dash::coin::p2p::message_inv::make_raw(invs);
67+
}
68+
69+
// Answer a peer's getdata(MSG_BLOCK, hash): the full `block` RawMessage if
70+
// the hash was previously announced, else nullptr. We never serve a block
71+
// we did not announce.
72+
std::unique_ptr<RawMessage> on_getdata_block(const uint256& hash) const
73+
{
74+
auto it = m_pending.find(hash);
75+
if (it == m_pending.end())
76+
return nullptr;
77+
return dash::coin::p2p::message_block::make_raw(it->second);
78+
}
79+
80+
bool knows(const uint256& hash) const { return m_pending.count(hash) != 0; }
81+
size_t pending() const { return m_pending.size(); }
82+
void forget(const uint256& hash) { m_pending.erase(hash); }
83+
void clear() { m_pending.clear(); }
84+
85+
private:
86+
std::map<uint256, BlockType> m_pending;
87+
};
88+
89+
} // namespace dash

test/CMakeLists.txt

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

455+
add_executable(test_dash_block_relay test_dash_block_relay.cpp)
456+
target_link_libraries(test_dash_block_relay PRIVATE
457+
GTest::gtest_main GTest::gtest
458+
dash_x11 core
459+
nlohmann_json::nlohmann_json
460+
${Boost_LIBRARIES}
461+
)
462+
target_link_libraries(test_dash_block_relay PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39)
463+
gtest_add_tests(test_dash_block_relay "" AUTO)
464+
455465
add_executable(test_compact_blocks test_compact_blocks.cpp)
456466
target_link_libraries(test_compact_blocks PRIVATE
457467
GTest::gtest_main GTest::gtest

test/test_dash_block_relay.cpp

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/// Phase S8 — WonBlockRelay (embedded-P2P won-block relay framer) KATs.
2+
///
3+
/// Exercises src/impl/dash/block_relay.hpp — the framing half of the
4+
/// embedded-P2P broadcast leg of the dual-path block-viability gate. Pins the
5+
/// `inv(MSG_BLOCK,hash)` announce -> getdata -> full `block` handshake that the
6+
/// DashBroadcaster pool fans out when DASH wins a block, WITHOUT any socket or
7+
/// live dashd:
8+
///
9+
/// (a) announce — returns an `inv` RawMessage carrying exactly one
10+
/// inventory_type, type == MSG_BLOCK (2), hash == the
11+
/// supplied (consensus-computed) hash; layout PINNED by
12+
/// independent reparse, not a self round-trip.
13+
/// (b) on_getdata — a known hash returns the full `block` message whose
14+
/// header bytes reconstruct the announced block; an
15+
/// UNKNOWN hash returns nullptr (never serve unannounced).
16+
/// (c) bookkeeping — knows / pending / forget / clear; re-announce is
17+
/// idempotent (refreshes, no duplicate slot).
18+
///
19+
/// SCOPE NOTE (honest): pure framer + pending-block book. The block hash is an
20+
/// INPUT (the consensus layer's X11 header hash) — not recomputed here, so no
21+
/// consensus value is asserted. The live on_block_found -> announce -> socket
22+
/// fan-out is the operator-gated broadcaster_full concern and is not claimed.
23+
24+
#include <gtest/gtest.h>
25+
26+
#include <impl/dash/block_relay.hpp>
27+
#include <impl/dash/coin/p2p_messages.hpp>
28+
29+
#include <core/uint256.hpp>
30+
#include <core/pack.hpp>
31+
32+
#include <array>
33+
#include <cstdint>
34+
#include <cstring>
35+
#include <vector>
36+
37+
using dash::WonBlockRelay;
38+
using dash::coin::BlockType;
39+
using dash::coin::p2p::message_inv;
40+
using dash::coin::p2p::message_block;
41+
using dash::coin::p2p::inventory_type;
42+
43+
namespace {
44+
45+
uint256 hash_seq(uint8_t base) {
46+
std::array<uint8_t, 32> p{};
47+
for (size_t i = 0; i < 32; ++i) p[i] = static_cast<uint8_t>(base + i);
48+
uint256 h; std::memcpy(h.data(), p.data(), 32); return h;
49+
}
50+
51+
BlockType make_block(uint8_t seed) {
52+
BlockType b;
53+
b.m_version = 0x20000000u | seed;
54+
b.m_previous_block = hash_seq(0x10 + seed);
55+
b.m_merkle_root = hash_seq(0x50 + seed);
56+
b.m_timestamp = 0x5f5e1000u + seed;
57+
b.m_bits = 0x1d00ffffu;
58+
b.m_nonce = 0xdeadbeefu - seed;
59+
return b;
60+
}
61+
62+
} // namespace
63+
64+
// (a) announce frames a single MSG_BLOCK inv for the supplied hash ────────────
65+
TEST(DashWonBlockRelay, Announce_FramesSingleBlockInv) {
66+
WonBlockRelay relay;
67+
const uint256 h = hash_seq(0x77);
68+
69+
auto rmsg = relay.announce(h, make_block(1));
70+
ASSERT_NE(rmsg, nullptr);
71+
EXPECT_EQ(rmsg->m_command, "inv");
72+
73+
// Reparse the inv payload independently (layout-sensitive, not self-rt).
74+
auto parsed = message_inv::make(rmsg->m_data);
75+
ASSERT_EQ(parsed->m_invs.size(), 1u);
76+
EXPECT_EQ(static_cast<uint32_t>(parsed->m_invs[0].m_type),
77+
static_cast<uint32_t>(inventory_type::block)); // MSG_BLOCK == 2
78+
EXPECT_EQ(static_cast<uint32_t>(inventory_type::block), 2u);
79+
EXPECT_EQ(parsed->m_invs[0].m_hash, h);
80+
}
81+
82+
// (b) getdata on a known hash yields the full announced block ─────────────────
83+
TEST(DashWonBlockRelay, GetData_KnownHash_ReturnsFullBlock) {
84+
WonBlockRelay relay;
85+
const uint256 h = hash_seq(0x20);
86+
const BlockType blk = make_block(4);
87+
relay.announce(h, blk);
88+
89+
auto rmsg = relay.on_getdata_block(h);
90+
ASSERT_NE(rmsg, nullptr);
91+
EXPECT_EQ(rmsg->m_command, "block");
92+
93+
auto parsed = message_block::make(rmsg->m_data);
94+
EXPECT_EQ(parsed->m_block.m_version, blk.m_version);
95+
EXPECT_EQ(parsed->m_block.m_previous_block, blk.m_previous_block);
96+
EXPECT_EQ(parsed->m_block.m_merkle_root, blk.m_merkle_root);
97+
EXPECT_EQ(parsed->m_block.m_timestamp, blk.m_timestamp);
98+
EXPECT_EQ(parsed->m_block.m_bits, blk.m_bits);
99+
EXPECT_EQ(parsed->m_block.m_nonce, blk.m_nonce);
100+
}
101+
102+
// (b') never serve a block we did not announce ────────────────────────────────
103+
TEST(DashWonBlockRelay, GetData_UnknownHash_ReturnsNull) {
104+
WonBlockRelay relay;
105+
relay.announce(hash_seq(0x01), make_block(1));
106+
EXPECT_EQ(relay.on_getdata_block(hash_seq(0xfe)), nullptr);
107+
EXPECT_FALSE(relay.knows(hash_seq(0xfe)));
108+
}
109+
110+
// (c) bookkeeping: knows / pending / forget / clear ───────────────────────────
111+
TEST(DashWonBlockRelay, Bookkeeping_KnowsPendingForgetClear) {
112+
WonBlockRelay relay;
113+
EXPECT_EQ(relay.pending(), 0u);
114+
115+
const uint256 a = hash_seq(0x10), b = hash_seq(0x40);
116+
relay.announce(a, make_block(1));
117+
relay.announce(b, make_block(2));
118+
EXPECT_EQ(relay.pending(), 2u);
119+
EXPECT_TRUE(relay.knows(a));
120+
EXPECT_TRUE(relay.knows(b));
121+
122+
relay.forget(a);
123+
EXPECT_FALSE(relay.knows(a));
124+
EXPECT_EQ(relay.pending(), 1u);
125+
EXPECT_EQ(relay.on_getdata_block(a), nullptr);
126+
127+
relay.clear();
128+
EXPECT_EQ(relay.pending(), 0u);
129+
}
130+
131+
// (c') re-announce of a known hash is idempotent (refresh, no dup) ────────────
132+
TEST(DashWonBlockRelay, ReAnnounce_SameHash_IsIdempotentRefresh) {
133+
WonBlockRelay relay;
134+
const uint256 h = hash_seq(0x33);
135+
136+
relay.announce(h, make_block(1));
137+
relay.announce(h, make_block(9)); // refresh under same hash
138+
EXPECT_EQ(relay.pending(), 1u);
139+
140+
auto rmsg = relay.on_getdata_block(h);
141+
ASSERT_NE(rmsg, nullptr);
142+
auto parsed = message_block::make(rmsg->m_data);
143+
EXPECT_EQ(parsed->m_block.m_nonce, make_block(9).m_nonce); // latest wins
144+
}

0 commit comments

Comments
 (0)