Skip to content

Commit 79970c7

Browse files
committed
dgb: wire embedded P2P header feed into HeaderChain (live validate_and_append)
wire_header_ingest (coin/header_ingest.hpp) subscribes a HeaderChain to dgb::interfaces::Node::new_headers -- the feed coin/p2p_node.hpp fires from ADD_P2P_HANDLER(headers) -- and routes every announced BlockHeaderType through the make_header_sample SSOT (#227) into HeaderChain::validate_and_append. This is what turns the chain from a unit-test fixture into one that advances on LIVE wire headers, lighting up tip_hash() -> previousblockhash for the embedded work template. The connector adds NO policy: disposition (Scrypt validate / continuity / reject) stays in validate_and_append, the single SSOT. Feeding real-shaped headers surfaced a latent SIGFPE in work_from_target: it divides UINT64_MAX by target.low64(), but a real-difficulty target has its significant bits high in the 256-bit word (genesis target ~2^224 -> low64()==0), an integer div-by-zero. Prior tests only used small uint64-range targets, so it never fired. Guarded: an unrepresentable-in-uint64 target credits 0 proxy work instead of crashing. cumulative_work is unconsumed by any V36 consensus path (retarget demoted to no-op; PPLNS scores shares); true 2^256/(target+1) work lands at the embedded-daemon work-accounting boundary alongside scrypt->pow_hash. dgb_header_ingest_test 5/5 (routing, batch arrival-order, driver-only, reject-delegation, empty-tip); header_chain_test 35/35 (no regression). Test wired into BOTH build.yml --target allowlists (#143 NOT_BUILT trap). Fenced to src/impl/dgb/; no shared-base touch.
1 parent a6c2785 commit 79970c7

5 files changed

Lines changed: 262 additions & 3 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
test_phase4_embedded \
6868
test_mweb_builder \
6969
test_address_resolution test_compute_share_target \
70-
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test \
70+
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test \
7171
dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
7272
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test \
7373
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
@@ -200,7 +200,7 @@ jobs:
200200
test_phase4_embedded \
201201
test_mweb_builder \
202202
test_address_resolution test_compute_share_target \
203-
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test \
203+
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test \
204204
dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
205205
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test \
206206
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \

src/impl/dgb/coin/header_chain.hpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,20 @@ class HeaderChain {
416416
// work() (2^256 / (target+1)) over the same Scrypt-only credit path.
417417
static uint64_t work_from_target(const u256& target)
418418
{
419-
return target.is_zero() ? 0 : (UINT64_MAX / target.low64());
419+
// Crude uint64 work proxy: UINT64_MAX / low64(target). DELIBERATELY a
420+
// proxy -- cumulative_work is internal bookkeeping NOT consumed by any
421+
// V36 consensus path (the parent-difficulty retarget gate is demoted to
422+
// a no-op; PPLNS scores shares, not header work). A REAL difficulty
423+
// header has its significant bits high in the 256-bit word, so its low
424+
// 64 bits are ZERO (e.g. genesis target ~2^224, low64()==0) -- dividing
425+
// by that is an integer div-by-zero (SIGFPE). Guard it: an
426+
// unrepresentable-in-uint64 target credits 0 proxy work rather than
427+
// crashing the live ingest path. The true 2^256/(target+1) work
428+
// computation lands at the embedded-daemon work-accounting boundary
429+
// alongside the scrypt(header)->pow_hash fill (same V37 deferral).
430+
if (target.is_zero() || target.low64() == 0)
431+
return 0;
432+
return UINT64_MAX / target.low64();
420433
}
421434

422435
DigiShieldParams m_ds_params{}; // retarget gate params
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#pragma once
2+
// ===========================================================================
3+
// c2pool::dgb::wire_header_ingest -- connect the embedded P2P header-download
4+
// feed to the HeaderChain so validate_and_append runs on LIVE headers.
5+
//
6+
// The embedded coin P2P layer (coin/p2p_node.hpp, ADD_P2P_HANDLER(headers))
7+
// parses each received `headers` batch into BlockHeaderType records and fires
8+
// dgb::interfaces::Node::new_headers (coin/node_interface.hpp). Until this
9+
// slice nothing consumed that event for the HeaderChain, so validate_and_append
10+
// never ran on live headers: the chain stayed empty and EmbeddedCoinNode
11+
// reported is_synced()==false / tip_hash()==nullopt regardless of P2P traffic.
12+
//
13+
// wire_header_ingest subscribes the chain to that feed. Every announced header
14+
// is converted through the make_header_sample SSOT (coin/header_sample_build.hpp
15+
// -- the SAME pure builder the ingest scaffold pins: block_hash =
16+
// sha256d(80-byte header), target = SetCompact(nBits), pow_hash left 0 for the
17+
// daemon-port scrypt boundary) and handed to HeaderChain::validate_and_append.
18+
//
19+
// CONSENSUS DISCIPLINE: this connector adds NO policy of its own. Disposition
20+
// (VALIDATED_SCRYPT / ACCEPTED_CONTINUITY / REJECTED), the Scrypt-only PoW gate
21+
// and the work-neutral continuity accounting all live inside
22+
// validate_and_append -- the single validation SSOT. Routing the live path
23+
// through it is what makes "the live header feed cannot bypass the Scrypt-only
24+
// gate" concrete rather than theoretical. The batch is ingested in arrival
25+
// order, exactly as the wire delivered it.
26+
//
27+
// LIFETIME: the handler captures `chain` by reference, so `chain` MUST outlive
28+
// `node`. The returned EventDisposable lets a caller tear the subscription down
29+
// explicitly; while it (and the node) live, every new_headers batch is ingested.
30+
// ===========================================================================
31+
32+
#include <memory>
33+
#include <vector>
34+
35+
#include <core/events.hpp>
36+
37+
#include "node_interface.hpp" // dgb::interfaces::Node (new_headers feed)
38+
#include "header_chain.hpp" // c2pool::dgb::HeaderChain / HeaderSample
39+
#include "header_sample_build.hpp" // c2pool::dgb::make_header_sample SSOT
40+
41+
namespace c2pool::dgb
42+
{
43+
44+
// Subscribe `chain` to `node.new_headers`. Returns the subscription handle so
45+
// the caller controls teardown; the subscription persists for the node's life
46+
// if the handle is dropped (EventDisposable does not auto-dispose on destruction).
47+
inline std::shared_ptr<EventDisposable>
48+
wire_header_ingest(::dgb::interfaces::Node& node, HeaderChain& chain)
49+
{
50+
return node.new_headers.subscribe(
51+
[&chain](const std::vector<::dgb::coin::BlockHeaderType>& headers)
52+
{
53+
for (const auto& h : headers)
54+
chain.validate_and_append(make_header_sample(h));
55+
});
56+
}
57+
58+
} // namespace c2pool::dgb

src/impl/dgb/test/CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ if (BUILD_TESTING AND GTest_FOUND)
4848
dgb_coin pool sharechain)
4949
gtest_add_tests(dgb_header_sample_build_test "" AUTO)
5050

51+
# --- embedded P2P header-download ingest connector ------------------------
52+
# Pins coin/header_ingest.hpp: wire_header_ingest subscribes a HeaderChain
53+
# to dgb::interfaces::Node::new_headers (the feed coin/p2p_node.hpp fires)
54+
# and routes each announced header through make_header_sample ->
55+
# validate_and_append. Proves the LIVE wire path advances the chain (and no
56+
# other path does) without adding consensus policy of its own. Links the dgb
57+
# OBJECT lib like dgb_header_sample_build_test because it pulls block.hpp
58+
# (BlockHeaderType codec via node_interface.hpp) + core Hash/pack. MUST
59+
# appear in BOTH this registration AND the build.yml --target allowlist.
60+
add_executable(dgb_header_ingest_test header_ingest_test.cpp)
61+
target_link_libraries(dgb_header_ingest_test PRIVATE
62+
GTest::gtest_main GTest::gtest
63+
core dgb
64+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
65+
dgb_coin pool sharechain)
66+
gtest_add_tests(dgb_header_ingest_test "" AUTO)
67+
5168
# --- #82: won-block reconstructor BODY (as_block composition) ----------
5269
# Pins coin/reconstruct_won_block.hpp: the single composition the dispatch
5370
# handler injects as its WonBlockReconstructor (resolve_other_tx_hashes ->
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// ---------------------------------------------------------------------------
2+
// dgb_header_ingest_test -- guards c2pool::dgb::wire_header_ingest, the
3+
// connector that feeds the embedded P2P header-download feed
4+
// (dgb::interfaces::Node::new_headers, fired by coin/p2p_node.hpp's
5+
// ADD_P2P_HANDLER(headers)) into HeaderChain::validate_and_append through the
6+
// make_header_sample SSOT. This is what turns the HeaderChain from a unit-test
7+
// fixture into a chain that advances on LIVE wire headers (lighting up
8+
// tip_hash() -> previousblockhash for the embedded work template).
9+
//
10+
// What it pins:
11+
// 1. Routing -- a header announced on new_headers lands in the chain via
12+
// make_header_sample (block_hash = sha256d(header)) + validate_and_append.
13+
// 2. Batch fidelity -- a multi-header batch is ingested in arrival order; the
14+
// tip is the LAST header and the chain grew by the full batch length.
15+
// 3. The connector is the driver -- an un-wired node ingests nothing
16+
// (no hidden side path appends headers).
17+
// 4. Disposition is DELEGATED, not overridden -- a header validate_and_append
18+
// REJECTS (unknown algo bits) never reaches the chain; the connector adds
19+
// no policy of its own and never force-appends.
20+
//
21+
// Pulls dgb::interfaces::Node (block.hpp codec) + header_chain.hpp + the
22+
// make_header_sample SSOT, so it links the proven dgb OBJECT-lib SCC set like
23+
// dgb_header_sample_build_test. MUST also appear in BOTH build.yml --target
24+
// allowlists (#143 NOT_BUILT trap).
25+
// ---------------------------------------------------------------------------
26+
27+
#include <cstdint>
28+
#include <vector>
29+
30+
#include <gtest/gtest.h>
31+
32+
#include <impl/dgb/coin/header_ingest.hpp>
33+
#include <impl/dgb/coin/node_interface.hpp>
34+
#include <impl/dgb/coin/header_chain.hpp>
35+
#include <impl/dgb/coin/header_sample_build.hpp>
36+
#include <impl/dgb/coin/hash_format.hpp>
37+
#include <impl/dgb/coin/dgb_block_algo.hpp>
38+
39+
using c2pool::dgb::HeaderChain;
40+
using c2pool::dgb::make_header_sample;
41+
using c2pool::dgb::wire_header_ingest;
42+
using dgb::coin::BlockHeaderType;
43+
using dgb::coin::u256_be_display_hex;
44+
using dgb::coin::DGB_BLOCK_VERSION_ALGO;
45+
46+
namespace {
47+
48+
// Canonical Bitcoin genesis header -- same 80-byte serialization DGB uses, and
49+
// a Scrypt header (version 1 -> algo nibble 0 == SCRYPT), so it walks the
50+
// VALIDATE_SCRYPT path. pow_hash is left 0 by make_header_sample (trivially
51+
// satisfies any target), and a default-ctor HeaderChain leaves pow_limit /
52+
// target_timespan 0 so the ceiling + DigiShield gates are no-ops -- exactly the
53+
// bootstrap posture the embedded port starts from.
54+
BlockHeaderType genesis_header()
55+
{
56+
BlockHeaderType h;
57+
h.m_version = 1;
58+
h.m_previous_block.SetNull();
59+
h.m_merkle_root.SetHex(
60+
"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b");
61+
h.m_timestamp = 1231006505u;
62+
h.m_bits = 0x1d00ffffu;
63+
h.m_nonce = 2083236893u;
64+
return h;
65+
}
66+
67+
// A second Scrypt header whose nTime is strictly greater than genesis' (the MTP
68+
// monotonicity gate requires nTime > median-of-ancestors), with a distinct
69+
// nonce so its sha256d block id differs from genesis'.
70+
BlockHeaderType second_scrypt_header()
71+
{
72+
BlockHeaderType h = genesis_header();
73+
h.m_timestamp = 1231006506u; // genesis + 1 -> passes MTP over [genesis]
74+
h.m_nonce = 12345u; // distinct id
75+
return h;
76+
}
77+
78+
// An unknown-algo header: algo nibble 1 (0x0100) maps to no DigiByte algo, so
79+
// dgb_header_disposition() -> REJECT. validate_and_append must drop it.
80+
BlockHeaderType unknown_algo_header()
81+
{
82+
BlockHeaderType h = genesis_header();
83+
h.m_version = 1 | 0x0100; // nibble 1 == ALGO_UNKNOWN
84+
return h;
85+
}
86+
87+
const std::string GENESIS_ID =
88+
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
89+
90+
} // namespace
91+
92+
// A fresh HeaderChain is empty -- no tip height, no tip hash.
93+
TEST(HeaderIngest, EmptyChainHasNoTip)
94+
{
95+
HeaderChain chain;
96+
EXPECT_FALSE(chain.tip_height().has_value());
97+
EXPECT_FALSE(chain.tip_hash().has_value());
98+
}
99+
100+
// 1. A Scrypt header announced on new_headers is ingested through
101+
// make_header_sample + validate_and_append: the chain grows and tip_hash()
102+
// is the header's sha256d block id (the well-known genesis hash).
103+
TEST(HeaderIngest, AnnouncedScryptHeaderIsIngested)
104+
{
105+
HeaderChain chain;
106+
dgb::interfaces::Node node;
107+
auto sub = wire_header_ingest(node, chain);
108+
109+
node.new_headers.happened(std::vector<BlockHeaderType>{ genesis_header() });
110+
111+
ASSERT_TRUE(chain.tip_height().has_value());
112+
ASSERT_TRUE(chain.tip_hash().has_value());
113+
EXPECT_EQ(u256_be_display_hex(*chain.tip_hash()), GENESIS_ID);
114+
}
115+
116+
// 2. A multi-header batch is ingested in arrival order: the tip is the LAST
117+
// header, and the chain grew by the full batch length (height delta == 1
118+
// versus a single-header chain).
119+
TEST(HeaderIngest, BatchIngestedInArrivalOrder)
120+
{
121+
HeaderChain one;
122+
dgb::interfaces::Node node_one;
123+
auto sub_one = wire_header_ingest(node_one, one);
124+
node_one.new_headers.happened(std::vector<BlockHeaderType>{ genesis_header() });
125+
126+
HeaderChain two;
127+
dgb::interfaces::Node node_two;
128+
auto sub_two = wire_header_ingest(node_two, two);
129+
node_two.new_headers.happened(
130+
std::vector<BlockHeaderType>{ genesis_header(), second_scrypt_header() });
131+
132+
ASSERT_TRUE(one.tip_height().has_value());
133+
ASSERT_TRUE(two.tip_height().has_value());
134+
// The two-header chain is exactly one block taller than the one-header chain.
135+
EXPECT_EQ(*two.tip_height(), *one.tip_height() + 1u);
136+
// Tip is the LAST header of the batch, not the first.
137+
ASSERT_TRUE(two.tip_hash().has_value());
138+
EXPECT_EQ(u256_be_display_hex(*two.tip_hash()),
139+
u256_be_display_hex(make_header_sample(second_scrypt_header()).block_hash));
140+
EXPECT_NE(u256_be_display_hex(*two.tip_hash()), GENESIS_ID);
141+
}
142+
143+
// 3. The connector is the driver: a node with NO ingest subscription appends
144+
// nothing when headers are announced.
145+
TEST(HeaderIngest, UnwiredNodeIngestsNothing)
146+
{
147+
HeaderChain chain;
148+
dgb::interfaces::Node node; // deliberately NOT wired
149+
150+
node.new_headers.happened(std::vector<BlockHeaderType>{ genesis_header() });
151+
152+
EXPECT_FALSE(chain.tip_height().has_value());
153+
EXPECT_FALSE(chain.tip_hash().has_value());
154+
}
155+
156+
// 4. Disposition is delegated to validate_and_append, not overridden by the
157+
// connector: an unknown-algo header (REJECT) never reaches the chain.
158+
TEST(HeaderIngest, RejectedHeaderIsNotAppended)
159+
{
160+
HeaderChain chain;
161+
dgb::interfaces::Node node;
162+
auto sub = wire_header_ingest(node, chain);
163+
164+
// Sanity: this header really is on the unknown-algo (reject) path.
165+
ASSERT_EQ(unknown_algo_header().m_version & DGB_BLOCK_VERSION_ALGO, 0x0100);
166+
167+
node.new_headers.happened(std::vector<BlockHeaderType>{ unknown_algo_header() });
168+
169+
EXPECT_FALSE(chain.tip_height().has_value());
170+
EXPECT_FALSE(chain.tip_hash().has_value());
171+
}

0 commit comments

Comments
 (0)