diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13fce7edd..a1eff47d0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -82,7 +82,7 @@ jobs: 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 dgb_header_ingest_test dgb_mempool_ingest_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ - dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test \ + dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test dgb_merkle_link_build_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test v37_test \ -j$(nproc) @@ -214,7 +214,7 @@ jobs: 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 dgb_header_ingest_test dgb_mempool_ingest_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ - dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test \ + dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test dgb_merkle_link_build_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ diff --git a/src/impl/dgb/coin/merkle_link_build.hpp b/src/impl/dgb/coin/merkle_link_build.hpp new file mode 100644 index 000000000..bb65a6c36 --- /dev/null +++ b/src/impl/dgb/coin/merkle_link_build.hpp @@ -0,0 +1,104 @@ +// --------------------------------------------------------------------------- +// merkle_link_build.hpp -- calculate_merkle_link(): the FORWARD merkle-branch +// builder, the exact inverse of share_check.hpp check_merkle_link(). +// +// WHY this exists (the won-block multi-tx gap): +// A reconstructed won block's merkle ROOT is recomputed from the gentx hash +// up the share's m_merkle_link (reconstruct_won_block.hpp:20-21, +// block_assembly.hpp check_merkle_link(gentx_hash, link)); the other_txs only +// populate the block BODY. So the merkle_link MUST encode the branch for the +// exact non-coinbase set the block carries -- a coinbase-only link over a +// multi-tx body hashes to the WRONG root and node B rejects bad-txnmrklroot. +// The live stratum path will carry a real link from its job; the regtest +// forced-won seed synthesizes one coinbase-only, so to drive a tx-bearing +// won-block re-soak its m_merkle_link must be REBUILT over [gentx]++captured +// template txids. check_merkle_link only VERIFIES a branch; this builds one. +// +// CONTRACT (round-trips through the production verifier byte-for-byte): +// check_merkle_link(tx_hashes[index], +// calculate_merkle_link(tx_hashes, index)) +// == BlockMerkleRoot(tx_hashes) // the root node B validates +// For the won-block coinbase, index == 0 (the leftmost leaf). +// +// merkle_combine() below mirrors check_merkle_link's combine EXACTLY +// (share_check.hpp:198-218): PackStream(left||right) -> double-SHA256. The +// tree uses Bitcoin's duplicate-last-on-odd rule, matching the daemon's +// ComputeMerkleRoot, so the rebuilt root agrees with node B's. +// +// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE +// (a build-side helper; the wire merkle_link semantics are unchanged). +// --------------------------------------------------------------------------- +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include // MerkleLink + +namespace dgb +{ +namespace coin +{ + +// double-SHA256 of (left || right) -- byte-identical to check_merkle_link's +// per-level combine (share_check.hpp:198-218). Kept here so this builder and +// the verifier share ONE concatenation/hash convention. +inline uint256 merkle_combine(const uint256& left, const uint256& right) +{ + PackStream ps; + ps << left; + ps << right; + auto sp = std::span( + reinterpret_cast(ps.data()), ps.size()); + return Hash(sp); +} + +// Build the merkle branch from leaf `index` up to the root over `tx_hashes`, +// using Bitcoin's duplicate-last-on-odd rule. Replays through +// check_merkle_link(tx_hashes[index], result) == BlockMerkleRoot(tx_hashes). +// A single-element list yields an empty branch (root == that element). +inline MerkleLink calculate_merkle_link(std::vector level, + uint32_t index = 0) +{ + if (level.empty()) + throw std::invalid_argument("calculate_merkle_link: empty tx list"); + if (index >= level.size()) + throw std::invalid_argument("calculate_merkle_link: index out of range"); + + MerkleLink link; + link.m_index = index; + + uint32_t pos = index; + while (level.size() > 1) + { + // Bitcoin merkle: duplicate the last node when the level is odd, so the + // computed intermediate (and any self-pair on the index path) matches + // the daemon's ComputeMerkleRoot. + if (level.size() & 1u) + level.push_back(level.back()); + + // The sibling combined with the current node at this level. + link.m_branch.push_back(level[pos ^ 1u]); + + std::vector next; + next.reserve(level.size() / 2); + for (std::size_t k = 0; k + 1 < level.size(); k += 2) + next.push_back(merkle_combine(level[k], level[k + 1])); + + level = std::move(next); + pos >>= 1; + } + + return link; +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index d26a490f3..d273ef177 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -451,5 +451,20 @@ if (BUILD_TESTING AND GTest_FOUND) c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage dgb_coin pool sharechain) gtest_add_tests(dgb_aux_parent_coinbase_parity_test "" AUTO) + # --- won-block multi-tx: forward merkle-link builder (coin/merkle_link_build.hpp) + # Pins calculate_merkle_link as the exact inverse of check_merkle_link: the + # rebuilt branch replays (same double-SHA combine, Bitcoin duplicate-last + # rule) to the full-tree BlockMerkleRoot across coinbase-only / even / odd tx + # counts -- the prerequisite for rooting a tx-bearing forced-won block so node + # B does not reject bad-txnmrklroot. Round-trip is asserted through the + # PRODUCTION dgb::check_merkle_link (links the dgb lib), not a re-impl. MUST + # stay in BOTH build.yml --target allowlists (#143 NOT_BUILT trap). + add_executable(dgb_merkle_link_build_test merkle_link_build_test.cpp) + target_link_libraries(dgb_merkle_link_build_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_merkle_link_build_test "" AUTO) endif() diff --git a/src/impl/dgb/test/merkle_link_build_test.cpp b/src/impl/dgb/test/merkle_link_build_test.cpp new file mode 100644 index 000000000..f959af263 --- /dev/null +++ b/src/impl/dgb/test/merkle_link_build_test.cpp @@ -0,0 +1,143 @@ +// --------------------------------------------------------------------------- +// merkle_link_build_test.cpp -- pins coin/merkle_link_build.hpp +// calculate_merkle_link() as the exact inverse of check_merkle_link(). +// +// The won-block multi-tx gap: a reconstructed block's merkle root is recomputed +// from the gentx hash up the share's m_merkle_link, while other_txs fill the +// body. For a tx-bearing won block to ACCEPT at the daemon, the link MUST encode +// the branch for that exact tx set. This KAT proves the builder produces a link +// that replays (via the SAME combine the production check_merkle_link uses) to +// the full-tree BlockMerkleRoot, across coinbase-only, even, and odd +// (duplicate-last) tx counts -- so the rebuilt forced-won block roots correctly. +// --------------------------------------------------------------------------- +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include // dgb::check_merkle_link (the PRODUCTION verifier) +#include + +using dgb::MerkleLink; +using dgb::coin::calculate_merkle_link; +using dgb::coin::merkle_combine; + +namespace +{ + +// A deterministic, distinct uint256 per leaf (no RNG -- byte-stable KAT input). +uint256 leaf(uint32_t i) +{ + uint256 h; + auto* p = reinterpret_cast(h.data()); + p[0] = static_cast(0xA0 + i); + p[1] = static_cast(i * 7 + 1); + p[31] = static_cast(i + 1); + return h; +} + +// Independent reference: Bitcoin's BlockMerkleRoot (duplicate-last on odd), +// over the SAME merkle_combine primitive -- the root node B computes. +uint256 reference_merkle_root(std::vector level) +{ + if (level.empty()) return uint256{}; + while (level.size() > 1) + { + if (level.size() & 1u) level.push_back(level.back()); + std::vector next; + for (std::size_t k = 0; k + 1 < level.size(); k += 2) + next.push_back(merkle_combine(level[k], level[k + 1])); + level = std::move(next); + } + return level.front(); +} + +// The production verifier's walk, re-stated here to keep the KAT self-contained +// (byte-identical to share_check.hpp check_merkle_link's loop). Proves the +// branch the builder emits replays to the root through that exact convention. +uint256 replay_merkle_link(const uint256& tip, const MerkleLink& link) +{ + uint256 cur = tip; + for (std::size_t i = 0; i < link.m_branch.size(); ++i) + cur = ((link.m_index >> i) & 1u) + ? merkle_combine(link.m_branch[i], cur) + : merkle_combine(cur, link.m_branch[i]); + return cur; +} + +std::vector leaves(uint32_t n) +{ + std::vector v; + v.reserve(n); + for (uint32_t i = 0; i < n; ++i) v.push_back(leaf(i)); + return v; +} + +} // namespace + +// Coinbase-only: empty branch, root == the single leaf (a valid coinbase-only +// won block -- the template-capture MISS contract). +TEST(MerkleLinkBuild, SingleLeafEmptyBranch) +{ + auto txs = leaves(1); + auto link = calculate_merkle_link(txs, 0); + EXPECT_TRUE(link.m_branch.empty()); + EXPECT_EQ(link.m_index, 0u); + EXPECT_EQ(replay_merkle_link(txs[0], link), txs[0]); + EXPECT_EQ(replay_merkle_link(txs[0], link), reference_merkle_root(txs)); +} + +// Coinbase index 0 round-trips to the full-tree root across even AND odd +// (duplicate-last) counts -- the multi-tx won-block shape. +TEST(MerkleLinkBuild, CoinbaseIndexRoundTripsRoot) +{ + for (uint32_t n : {2u, 3u, 4u, 5u, 7u, 8u, 13u}) + { + auto txs = leaves(n); + auto link = calculate_merkle_link(txs, 0); + EXPECT_EQ(replay_merkle_link(txs[0], link), reference_merkle_root(txs)) + << "coinbase branch failed for n=" << n; + // ...and through the PRODUCTION verifier the won block is rooted by. + EXPECT_EQ(dgb::check_merkle_link(txs[0], link), reference_merkle_root(txs)) + << "production check_merkle_link disagreed for n=" << n; + // branch depth == ceil(log2(n)) + std::size_t depth = 0, sz = n; + while (sz > 1) { sz = (sz + 1) / 2; ++depth; } + EXPECT_EQ(link.m_branch.size(), depth) << "branch depth wrong for n=" << n; + } +} + +// Non-zero leaf index also round-trips (general inverse, not just index 0): +// every leaf's branch must reproduce the one shared root. +TEST(MerkleLinkBuild, EveryLeafIndexReproducesSameRoot) +{ + for (uint32_t n : {2u, 3u, 6u, 9u}) + { + auto txs = leaves(n); + const uint256 root = reference_merkle_root(txs); + for (uint32_t idx = 0; idx < n; ++idx) + { + auto link = calculate_merkle_link(txs, idx); + EXPECT_EQ(link.m_index, idx); + EXPECT_EQ(replay_merkle_link(txs[idx], link), root) + << "n=" << n << " idx=" << idx; + EXPECT_EQ(dgb::check_merkle_link(txs[idx], link), root) + << "production verifier n=" << n << " idx=" << idx; + } + } +} + +// Fail-closed: empty input and out-of-range index throw rather than emit a +// silently-wrong (daemon-rejected) branch. +TEST(MerkleLinkBuild, FailsClosedOnBadInput) +{ + EXPECT_THROW(calculate_merkle_link({}, 0), std::invalid_argument); + auto txs = leaves(3); + EXPECT_THROW(calculate_merkle_link(txs, 3), std::invalid_argument); +}