Skip to content

Commit d9510b0

Browse files
committed
Merge remote-tracking branch 'origin/master' into bch/m5-embedded-body-wip
2 parents 4913c68 + 345e88a commit d9510b0

5 files changed

Lines changed: 181 additions & 0 deletions

File tree

.github/workflows/build.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
test_mweb_builder \
6969
test_address_resolution test_compute_share_target \
7070
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test dgb_gentx_coinbase_test \
71+
nmc_auxpow_merkle_test \
7172
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
7273
v37_test \
7374
-j$(nproc)
@@ -198,6 +199,7 @@ jobs:
198199
test_mweb_builder \
199200
test_address_resolution test_compute_share_target \
200201
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test dgb_gentx_coinbase_test \
202+
nmc_auxpow_merkle_test \
201203
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
202204
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
203205
v37_test \
@@ -451,6 +453,11 @@ jobs:
451453
qt-webengine:
452454
name: Qt WebEngine leak gate
453455
runs-on: ubuntu-24.04
456+
# Hard ceiling: orphaned QtWebEngine/Chromium helper children survive
457+
# the inner `timeout 180`, holding the xvfb pipe open and hanging the
458+
# job to the 360-min runner default. Cap it so a hung leak gate is
459+
# force-killed (with its whole process tree) in minutes, not hours.
460+
timeout-minutes: 20
454461
steps:
455462
- uses: actions/checkout@v6
456463

src/impl/nmc/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
# NMC (embedded merge-mined Namecoin) — P0 structural leaf.
22
# Only the coin tree exists at P0. node/stratum/share layers come later.
33
add_subdirectory(coin)
4+
5+
# P1: NMC module tests (AuxPow merkle-walk KAT). Gated on BUILD_TESTING.
6+
if (BUILD_TESTING)
7+
add_subdirectory(test)
8+
endif()

src/impl/nmc/coin/header_chain.hpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
#include <optional>
4949
#include <string>
5050
#include <unordered_map>
51+
#include <span>
52+
#include <stdexcept>
5153
#include <vector>
5254

5355
namespace nmc {
@@ -77,6 +79,44 @@ inline uint256 pow_hash(const BlockHeaderType& header) {
7779
return Hash(packed.get_span());
7880
}
7981

82+
// ─── Merkle-branch walk (P1: AuxPow merkle-proof primitive) ────────────────
83+
84+
/// Walk a merkle BRANCH up to its root, starting from `leaf`.
85+
///
86+
/// The core primitive of AuxPow verification: BOTH legs of the proof are
87+
/// merkle-branch walks — the chain-merkle leg (aux block hash → merged-mining
88+
/// root, AuxPow::check_proof step 1) and the parent-coinbase leg (coinbase
89+
/// txid → parent block tx-merkle-root, step 3). At level i, bit i of `index`
90+
/// selects the side: if set, branch[i] is the LEFT sibling (running hash on the
91+
/// right); else branch[i] is on the right. Each pair is SHA256d'd (the
92+
/// Bitcoin/Namecoin merkle convention).
93+
///
94+
/// Byte-faithful port of legacy libcoind/data.cpp check_merkle_link() — the
95+
/// same SSOT btc/ltc use — kept NMC-LOCAL per the coin fence (no btc/ltc
96+
/// include; only core/* primitives: Hash, PackStream).
97+
inline uint256 aux_merkle_root(const uint256& leaf,
98+
const std::vector<uint256>& branch,
99+
uint32_t index) {
100+
if (!branch.empty() && index >= (1u << branch.size()))
101+
throw std::invalid_argument("aux_merkle_root: index too large for branch depth");
102+
103+
uint256 cur = leaf;
104+
for (size_t i = 0; i < branch.size(); ++i) {
105+
PackStream ps;
106+
if ((index >> i) & 1u) {
107+
ps << branch[i]; // sibling on the left
108+
ps << cur;
109+
} else {
110+
ps << cur;
111+
ps << branch[i]; // sibling on the right
112+
}
113+
auto sp = std::span<const unsigned char>(
114+
reinterpret_cast<const unsigned char*>(ps.data()), ps.size());
115+
cur = Hash(sp); // SHA256d of the 64-byte concatenation
116+
}
117+
return cur;
118+
}
119+
80120
// ─── AuxPow (merge-mining proof) ─────────────────────────────────────────────
81121

82122
/// One aux-chain slot inside a parent coinbase's merged-mining merkle tree.

src/impl/nmc/test/CMakeLists.txt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# NMC module tests (P1). Mirrors the dgb header-only guard registration in
2+
# src/impl/dgb/test/CMakeLists.txt: gated on BUILD_TESTING + GTest, registered
3+
# via gtest_add_tests for the CI --target allowlist.
4+
#
5+
# nmc_auxpow_merkle_test pins the aux_merkle_root walk in coin/header_chain.hpp
6+
# (the AuxPow merge-mining merkle-branch walk). It links core because the walk
7+
# folds nodes with core::Hash / PackStream; it does NOT link any node/pool/
8+
# share OBJECT lib (none exist for NMC yet -- P0/P1 is coin-tree only).
9+
#
10+
# Each target MUST appear in BOTH this registration AND the build.yml
11+
# --target allowlist, or it becomes a NOT_BUILT sentinel that reds master.
12+
if (BUILD_TESTING AND GTest_FOUND)
13+
add_executable(nmc_auxpow_merkle_test auxpow_merkle_test.cpp)
14+
target_link_libraries(nmc_auxpow_merkle_test PRIVATE
15+
GTest::gtest_main GTest::gtest
16+
core nlohmann_json::nlohmann_json)
17+
# core pulls stratum_server.cpp (hashrate refs); link the same OBJECT-lib
18+
# SCC the sibling dgb merkle test links so core resolves. No node/pool/
19+
# share/coin libs are needed -- this KAT only folds with core::Hash.
20+
target_link_libraries(nmc_auxpow_merkle_test PRIVATE
21+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage)
22+
23+
include(GoogleTest)
24+
include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR})
25+
gtest_add_tests(nmc_auxpow_merkle_test "" AUTO)
26+
endif()
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// ---------------------------------------------------------------------------
2+
// nmc::coin::aux_merkle_root KAT (P1 — AuxPow merkle-proof walk).
3+
//
4+
// Pins the merge-mining merkle-branch walk that AuxPow::check_proof() (still a
5+
// P-DEFER stub) will consume for step-1 (chain-merkle-root) and step-3 (parent
6+
// tx-merkle-root). The walk is a byte-faithful port of legacy
7+
// libcoind/data.cpp check_merkle_link() — the same SSOT btc uses — so these
8+
// KATs lock the consensus-relevant ordering:
9+
// * empty branch => root == leaf (identity);
10+
// * index bit i selects whether branch[i] is the LEFT or RIGHT sibling at
11+
// depth i, folded with double-SHA256 of the 64-byte concatenation;
12+
// * an index that does not fit in branch.size() bits is rejected.
13+
//
14+
// Expected roots are derived INDEPENDENTLY here via core::Hash on the explicit
15+
// concatenation (not by calling aux_merkle_root), so a side-swap or fold bug in
16+
// the walk is caught rather than mirrored. Self-derived => no fixture file.
17+
//
18+
// Per-coin isolation: src/impl/nmc/ only; btc tree consumed READ-ONLY (this is
19+
// an independent NMC-local re-derivation, fence #4). MUST appear in BOTH
20+
// test/CMakeLists.txt AND the build.yml --target allowlist or it becomes a
21+
// NOT_BUILT sentinel that reds master.
22+
// ---------------------------------------------------------------------------
23+
24+
#include <gtest/gtest.h>
25+
26+
#include <stdexcept>
27+
#include <vector>
28+
29+
#include <core/hash.hpp>
30+
#include <core/pack.hpp>
31+
#include <core/uint256.hpp>
32+
33+
#include "../coin/header_chain.hpp"
34+
35+
namespace {
36+
37+
using nmc::coin::aux_merkle_root;
38+
39+
// Build a uint256 whose first byte is `b` (rest zero) — distinct, legible leaves.
40+
static uint256 leaf_of(unsigned char b)
41+
{
42+
uint256 u;
43+
u.SetNull();
44+
*(u.begin()) = b;
45+
return u;
46+
}
47+
48+
// Independent reference combine: double-SHA256 of l||r, mirroring the legacy
49+
// merkle node hash but computed here WITHOUT touching aux_merkle_root.
50+
static uint256 combine(const uint256& l, const uint256& r)
51+
{
52+
PackStream ps;
53+
ps << l;
54+
ps << r;
55+
auto sp = std::span<const unsigned char>(
56+
reinterpret_cast<const unsigned char*>(ps.data()), ps.size());
57+
return Hash(sp);
58+
}
59+
60+
TEST(NmcAuxMerkle, EmptyBranchIsIdentity)
61+
{
62+
uint256 leaf = leaf_of(0x11);
63+
EXPECT_EQ(aux_merkle_root(leaf, {}, 0), leaf);
64+
// index is ignored when there is no branch to walk.
65+
EXPECT_EQ(aux_merkle_root(leaf, {}, 12345u), leaf);
66+
}
67+
68+
TEST(NmcAuxMerkle, SingleStepIndexZeroLeafOnLeft)
69+
{
70+
uint256 leaf = leaf_of(0x11);
71+
uint256 sib = leaf_of(0x22);
72+
// index bit 0 == 0 => leaf is LEFT, sibling RIGHT.
73+
EXPECT_EQ(aux_merkle_root(leaf, {sib}, 0), combine(leaf, sib));
74+
}
75+
76+
TEST(NmcAuxMerkle, SingleStepIndexOneLeafOnRight)
77+
{
78+
uint256 leaf = leaf_of(0x11);
79+
uint256 sib = leaf_of(0x22);
80+
// index bit 0 == 1 => sibling LEFT, leaf RIGHT.
81+
EXPECT_EQ(aux_merkle_root(leaf, {sib}, 1), combine(sib, leaf));
82+
}
83+
84+
TEST(NmcAuxMerkle, TwoStepFoldRespectsPerLevelIndexBits)
85+
{
86+
uint256 leaf = leaf_of(0x11);
87+
uint256 b0 = leaf_of(0x22);
88+
uint256 b1 = leaf_of(0x33);
89+
// index = 0b10: level0 bit=0 (leaf left), level1 bit=1 (accum right).
90+
uint256 lvl0 = combine(leaf, b0);
91+
uint256 want = combine(b1, lvl0);
92+
EXPECT_EQ(aux_merkle_root(leaf, {b0, b1}, 0b10u), want);
93+
}
94+
95+
TEST(NmcAuxMerkle, IndexTooLargeForBranchThrows)
96+
{
97+
uint256 leaf = leaf_of(0x11);
98+
uint256 sib = leaf_of(0x22);
99+
// one-element branch admits indices 0..1; 2 overflows the implied tree.
100+
EXPECT_THROW(aux_merkle_root(leaf, {sib}, 2u), std::invalid_argument);
101+
}
102+
103+
} // namespace

0 commit comments

Comments
 (0)