Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down
15 changes: 14 additions & 1 deletion src/impl/dgb/coin/header_chain.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions src/impl/dgb/coin/header_ingest.hpp
Original file line number Diff line number Diff line change
@@ -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 <memory>
#include <vector>

#include <core/events.hpp>

#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<EventDisposable>
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
17 changes: 17 additions & 0 deletions src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down
171 changes: 171 additions & 0 deletions src/impl/dgb/test/header_ingest_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstdint>
#include <vector>

#include <gtest/gtest.h>

#include <impl/dgb/coin/header_ingest.hpp>
#include <impl/dgb/coin/node_interface.hpp>
#include <impl/dgb/coin/header_chain.hpp>
#include <impl/dgb/coin/header_sample_build.hpp>
#include <impl/dgb/coin/hash_format.hpp>
#include <impl/dgb/coin/dgb_block_algo.hpp>

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<BlockHeaderType>{ 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<BlockHeaderType>{ 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<BlockHeaderType>{ 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<BlockHeaderType>{ 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<BlockHeaderType>{ unknown_algo_header() });

EXPECT_FALSE(chain.tip_height().has_value());
EXPECT_FALSE(chain.tip_hash().has_value());
}
Loading