diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1330b52df..0525295e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,7 @@ jobs: test_phase4_embedded \ test_mweb_builder \ test_address_resolution test_compute_share_target \ - test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test \ + 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 \ 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 \ 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 \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ @@ -200,7 +200,7 @@ jobs: test_phase4_embedded \ test_mweb_builder \ test_address_resolution test_compute_share_target \ - test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test \ + 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 \ 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 \ 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 \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ diff --git a/src/impl/dgb/coin/header_chain.hpp b/src/impl/dgb/coin/header_chain.hpp index 7804d0a1c..57ad0df41 100644 --- a/src/impl/dgb/coin/header_chain.hpp +++ b/src/impl/dgb/coin/header_chain.hpp @@ -416,7 +416,20 @@ class HeaderChain { // work() (2^256 / (target+1)) over the same Scrypt-only credit path. static uint64_t work_from_target(const u256& target) { - return target.is_zero() ? 0 : (UINT64_MAX / target.low64()); + // Crude uint64 work proxy: UINT64_MAX / low64(target). DELIBERATELY a + // proxy -- cumulative_work is internal bookkeeping NOT consumed by any + // V36 consensus path (the parent-difficulty retarget gate is demoted to + // a no-op; PPLNS scores shares, not header work). A REAL difficulty + // header has its significant bits high in the 256-bit word, so its low + // 64 bits are ZERO (e.g. genesis target ~2^224, low64()==0) -- dividing + // by that is an integer div-by-zero (SIGFPE). Guard it: an + // unrepresentable-in-uint64 target credits 0 proxy work rather than + // crashing the live ingest path. The true 2^256/(target+1) work + // computation lands at the embedded-daemon work-accounting boundary + // alongside the scrypt(header)->pow_hash fill (same V37 deferral). + if (target.is_zero() || target.low64() == 0) + return 0; + return UINT64_MAX / target.low64(); } DigiShieldParams m_ds_params{}; // retarget gate params diff --git a/src/impl/dgb/coin/header_ingest.hpp b/src/impl/dgb/coin/header_ingest.hpp new file mode 100644 index 000000000..80e143803 --- /dev/null +++ b/src/impl/dgb/coin/header_ingest.hpp @@ -0,0 +1,58 @@ +#pragma once +// =========================================================================== +// c2pool::dgb::wire_header_ingest -- connect the embedded P2P header-download +// feed to the HeaderChain so validate_and_append runs on LIVE headers. +// +// The embedded coin P2P layer (coin/p2p_node.hpp, ADD_P2P_HANDLER(headers)) +// parses each received `headers` batch into BlockHeaderType records and fires +// dgb::interfaces::Node::new_headers (coin/node_interface.hpp). Until this +// slice nothing consumed that event for the HeaderChain, so validate_and_append +// never ran on live headers: the chain stayed empty and EmbeddedCoinNode +// reported is_synced()==false / tip_hash()==nullopt regardless of P2P traffic. +// +// wire_header_ingest subscribes the chain to that feed. Every announced header +// is converted through the make_header_sample SSOT (coin/header_sample_build.hpp +// -- the SAME pure builder the ingest scaffold pins: block_hash = +// sha256d(80-byte header), target = SetCompact(nBits), pow_hash left 0 for the +// daemon-port scrypt boundary) and handed to HeaderChain::validate_and_append. +// +// CONSENSUS DISCIPLINE: this connector adds NO policy of its own. Disposition +// (VALIDATED_SCRYPT / ACCEPTED_CONTINUITY / REJECTED), the Scrypt-only PoW gate +// and the work-neutral continuity accounting all live inside +// validate_and_append -- the single validation SSOT. Routing the live path +// through it is what makes "the live header feed cannot bypass the Scrypt-only +// gate" concrete rather than theoretical. The batch is ingested in arrival +// order, exactly as the wire delivered it. +// +// LIFETIME: the handler captures `chain` by reference, so `chain` MUST outlive +// `node`. The returned EventDisposable lets a caller tear the subscription down +// explicitly; while it (and the node) live, every new_headers batch is ingested. +// =========================================================================== + +#include +#include + +#include + +#include "node_interface.hpp" // dgb::interfaces::Node (new_headers feed) +#include "header_chain.hpp" // c2pool::dgb::HeaderChain / HeaderSample +#include "header_sample_build.hpp" // c2pool::dgb::make_header_sample SSOT + +namespace c2pool::dgb +{ + +// Subscribe `chain` to `node.new_headers`. Returns the subscription handle so +// the caller controls teardown; the subscription persists for the node's life +// if the handle is dropped (EventDisposable does not auto-dispose on destruction). +inline std::shared_ptr +wire_header_ingest(::dgb::interfaces::Node& node, HeaderChain& chain) +{ + return node.new_headers.subscribe( + [&chain](const std::vector<::dgb::coin::BlockHeaderType>& headers) + { + for (const auto& h : headers) + chain.validate_and_append(make_header_sample(h)); + }); +} + +} // namespace c2pool::dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 3633cc201..396b99d14 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -48,6 +48,23 @@ if (BUILD_TESTING AND GTest_FOUND) dgb_coin pool sharechain) gtest_add_tests(dgb_header_sample_build_test "" AUTO) + # --- embedded P2P header-download ingest connector ------------------------ + # Pins coin/header_ingest.hpp: wire_header_ingest subscribes a HeaderChain + # to dgb::interfaces::Node::new_headers (the feed coin/p2p_node.hpp fires) + # and routes each announced header through make_header_sample -> + # validate_and_append. Proves the LIVE wire path advances the chain (and no + # other path does) without adding consensus policy of its own. Links the dgb + # OBJECT lib like dgb_header_sample_build_test because it pulls block.hpp + # (BlockHeaderType codec via node_interface.hpp) + core Hash/pack. MUST + # appear in BOTH this registration AND the build.yml --target allowlist. + add_executable(dgb_header_ingest_test header_ingest_test.cpp) + target_link_libraries(dgb_header_ingest_test PRIVATE + GTest::gtest_main GTest::gtest + core dgb + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage + dgb_coin pool sharechain) + gtest_add_tests(dgb_header_ingest_test "" AUTO) + # --- #82: won-block reconstructor BODY (as_block composition) ---------- # Pins coin/reconstruct_won_block.hpp: the single composition the dispatch # handler injects as its WonBlockReconstructor (resolve_other_tx_hashes -> diff --git a/src/impl/dgb/test/header_ingest_test.cpp b/src/impl/dgb/test/header_ingest_test.cpp new file mode 100644 index 000000000..13c81bad2 --- /dev/null +++ b/src/impl/dgb/test/header_ingest_test.cpp @@ -0,0 +1,171 @@ +// --------------------------------------------------------------------------- +// dgb_header_ingest_test -- guards c2pool::dgb::wire_header_ingest, the +// connector that feeds the embedded P2P header-download feed +// (dgb::interfaces::Node::new_headers, fired by coin/p2p_node.hpp's +// ADD_P2P_HANDLER(headers)) into HeaderChain::validate_and_append through the +// make_header_sample SSOT. This is what turns the HeaderChain from a unit-test +// fixture into a chain that advances on LIVE wire headers (lighting up +// tip_hash() -> previousblockhash for the embedded work template). +// +// What it pins: +// 1. Routing -- a header announced on new_headers lands in the chain via +// make_header_sample (block_hash = sha256d(header)) + validate_and_append. +// 2. Batch fidelity -- a multi-header batch is ingested in arrival order; the +// tip is the LAST header and the chain grew by the full batch length. +// 3. The connector is the driver -- an un-wired node ingests nothing +// (no hidden side path appends headers). +// 4. Disposition is DELEGATED, not overridden -- a header validate_and_append +// REJECTS (unknown algo bits) never reaches the chain; the connector adds +// no policy of its own and never force-appends. +// +// Pulls dgb::interfaces::Node (block.hpp codec) + header_chain.hpp + the +// make_header_sample SSOT, so it links the proven dgb OBJECT-lib SCC set like +// dgb_header_sample_build_test. MUST also appear in BOTH build.yml --target +// allowlists (#143 NOT_BUILT trap). +// --------------------------------------------------------------------------- + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +using c2pool::dgb::HeaderChain; +using c2pool::dgb::make_header_sample; +using c2pool::dgb::wire_header_ingest; +using dgb::coin::BlockHeaderType; +using dgb::coin::u256_be_display_hex; +using dgb::coin::DGB_BLOCK_VERSION_ALGO; + +namespace { + +// Canonical Bitcoin genesis header -- same 80-byte serialization DGB uses, and +// a Scrypt header (version 1 -> algo nibble 0 == SCRYPT), so it walks the +// VALIDATE_SCRYPT path. pow_hash is left 0 by make_header_sample (trivially +// satisfies any target), and a default-ctor HeaderChain leaves pow_limit / +// target_timespan 0 so the ceiling + DigiShield gates are no-ops -- exactly the +// bootstrap posture the embedded port starts from. +BlockHeaderType genesis_header() +{ + BlockHeaderType h; + h.m_version = 1; + h.m_previous_block.SetNull(); + h.m_merkle_root.SetHex( + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"); + h.m_timestamp = 1231006505u; + h.m_bits = 0x1d00ffffu; + h.m_nonce = 2083236893u; + return h; +} + +// A second Scrypt header whose nTime is strictly greater than genesis' (the MTP +// monotonicity gate requires nTime > median-of-ancestors), with a distinct +// nonce so its sha256d block id differs from genesis'. +BlockHeaderType second_scrypt_header() +{ + BlockHeaderType h = genesis_header(); + h.m_timestamp = 1231006506u; // genesis + 1 -> passes MTP over [genesis] + h.m_nonce = 12345u; // distinct id + return h; +} + +// An unknown-algo header: algo nibble 1 (0x0100) maps to no DigiByte algo, so +// dgb_header_disposition() -> REJECT. validate_and_append must drop it. +BlockHeaderType unknown_algo_header() +{ + BlockHeaderType h = genesis_header(); + h.m_version = 1 | 0x0100; // nibble 1 == ALGO_UNKNOWN + return h; +} + +const std::string GENESIS_ID = + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; + +} // namespace + +// A fresh HeaderChain is empty -- no tip height, no tip hash. +TEST(HeaderIngest, EmptyChainHasNoTip) +{ + HeaderChain chain; + EXPECT_FALSE(chain.tip_height().has_value()); + EXPECT_FALSE(chain.tip_hash().has_value()); +} + +// 1. A Scrypt header announced on new_headers is ingested through +// make_header_sample + validate_and_append: the chain grows and tip_hash() +// is the header's sha256d block id (the well-known genesis hash). +TEST(HeaderIngest, AnnouncedScryptHeaderIsIngested) +{ + HeaderChain chain; + dgb::interfaces::Node node; + auto sub = wire_header_ingest(node, chain); + + node.new_headers.happened(std::vector{ genesis_header() }); + + ASSERT_TRUE(chain.tip_height().has_value()); + ASSERT_TRUE(chain.tip_hash().has_value()); + EXPECT_EQ(u256_be_display_hex(*chain.tip_hash()), GENESIS_ID); +} + +// 2. A multi-header batch is ingested in arrival order: the tip is the LAST +// header, and the chain grew by the full batch length (height delta == 1 +// versus a single-header chain). +TEST(HeaderIngest, BatchIngestedInArrivalOrder) +{ + HeaderChain one; + dgb::interfaces::Node node_one; + auto sub_one = wire_header_ingest(node_one, one); + node_one.new_headers.happened(std::vector{ genesis_header() }); + + HeaderChain two; + dgb::interfaces::Node node_two; + auto sub_two = wire_header_ingest(node_two, two); + node_two.new_headers.happened( + std::vector{ genesis_header(), second_scrypt_header() }); + + ASSERT_TRUE(one.tip_height().has_value()); + ASSERT_TRUE(two.tip_height().has_value()); + // The two-header chain is exactly one block taller than the one-header chain. + EXPECT_EQ(*two.tip_height(), *one.tip_height() + 1u); + // Tip is the LAST header of the batch, not the first. + ASSERT_TRUE(two.tip_hash().has_value()); + EXPECT_EQ(u256_be_display_hex(*two.tip_hash()), + u256_be_display_hex(make_header_sample(second_scrypt_header()).block_hash)); + EXPECT_NE(u256_be_display_hex(*two.tip_hash()), GENESIS_ID); +} + +// 3. The connector is the driver: a node with NO ingest subscription appends +// nothing when headers are announced. +TEST(HeaderIngest, UnwiredNodeIngestsNothing) +{ + HeaderChain chain; + dgb::interfaces::Node node; // deliberately NOT wired + + node.new_headers.happened(std::vector{ genesis_header() }); + + EXPECT_FALSE(chain.tip_height().has_value()); + EXPECT_FALSE(chain.tip_hash().has_value()); +} + +// 4. Disposition is delegated to validate_and_append, not overridden by the +// connector: an unknown-algo header (REJECT) never reaches the chain. +TEST(HeaderIngest, RejectedHeaderIsNotAppended) +{ + HeaderChain chain; + dgb::interfaces::Node node; + auto sub = wire_header_ingest(node, chain); + + // Sanity: this header really is on the unknown-algo (reject) path. + ASSERT_EQ(unknown_algo_header().m_version & DGB_BLOCK_VERSION_ALGO, 0x0100); + + node.new_headers.happened(std::vector{ unknown_algo_header() }); + + EXPECT_FALSE(chain.tip_height().has_value()); + EXPECT_FALSE(chain.tip_hash().has_value()); +}