Skip to content
Closed
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 @@ -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)
Expand Down Expand Up @@ -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 \
Expand Down
104 changes: 104 additions & 0 deletions src/impl/dgb/coin/merkle_link_build.hpp
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <cstdint>
#include <span>
#include <stdexcept>
#include <utility>
#include <vector>

#include <core/hash.hpp>
#include <core/pack.hpp>
#include <core/uint256.hpp>

#include <impl/dgb/share_types.hpp> // 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<const unsigned char>(
reinterpret_cast<const unsigned char*>(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<uint256> 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<uint256> 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
15 changes: 15 additions & 0 deletions src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
143 changes: 143 additions & 0 deletions src/impl/dgb/test/merkle_link_build_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstdint>
#include <span>
#include <vector>

#include <gtest/gtest.h>

#include <core/hash.hpp>
#include <core/pack.hpp>
#include <core/uint256.hpp>

#include <impl/dgb/coin/merkle_link_build.hpp>
#include <impl/dgb/share_check.hpp> // dgb::check_merkle_link (the PRODUCTION verifier)
#include <impl/dgb/share_types.hpp>

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<unsigned char*>(h.data());
p[0] = static_cast<unsigned char>(0xA0 + i);
p[1] = static_cast<unsigned char>(i * 7 + 1);
p[31] = static_cast<unsigned char>(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<uint256> level)
{
if (level.empty()) return uint256{};
while (level.size() > 1)
{
if (level.size() & 1u) level.push_back(level.back());
std::vector<uint256> 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<uint256> leaves(uint32_t n)
{
std::vector<uint256> 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);
}
Loading