diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f9bc1437f..d2f1c2f03 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ + dgb_other_tx_assembler_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ v37_test \ -j$(nproc) @@ -200,6 +201,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ + dgb_other_tx_assembler_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ diff --git a/src/impl/dgb/coin/other_tx_assembler.hpp b/src/impl/dgb/coin/other_tx_assembler.hpp new file mode 100644 index 000000000..8eb2d7aae --- /dev/null +++ b/src/impl/dgb/coin/other_tx_assembler.hpp @@ -0,0 +1,89 @@ +#pragma once +// --------------------------------------------------------------------------- +// dgb::coin::assemble_other_txs -- the won-block reconstructor's (#82) bridge +// between the ORDERED other_tx hash list (resolve_other_tx_hashes, sub-slice 1) +// and the deserialized MutableTransaction vector that assemble_won_block +// (block_assembly.hpp) frames as txs = [gentx] ++ other_txs. +// +// Faithful C++ port of the known_txs lookup inside p2pool data.py +// Share.as_block(tracker, known_txs): +// +// other_txs = [known_txs[h] for h in transaction_hashes] +// +// resolve_other_tx_hashes already produced `transaction_hashes` (the ref-walk +// over ancestor new_transaction_hashes, in block other_txs order); this slice +// is the remaining `known_txs[h]` indexing. Each hash is resolved through an +// INJECTED lookup (same seam-first decomposition as other_tx_resolver.hpp / +// won_block_dispatch.hpp), so the bridge is unit-testable against a synthetic +// known-tx set without standing up a live mempool / ShareTracker. In the +// run-loop known_txs_fn binds to the node's known-transaction store (mempool + +// peer-relayed tx cache), the same set share verification populated. +// +// Failure mode (integrator 2026-06-19, mirroring sub-slice 1): a missing hash +// is a malformed/incomplete-reconstruction condition -- p2pool's known_txs[h] +// raises KeyError. We throw std::out_of_range rather than skip the tx or emit +// a placeholder: a block with a dropped/wrong other_tx has the wrong merkle +// root and is rejected by the daemon, so failing loudly here is strictly safer +// than shipping a malformed block to the network. +// +// Witness framing is NOT decided here. assemble_other_txs returns the txs +// verbatim (whatever witness stacks they carry); the block's witness SHAPE is +// governed downstream by assemble_won_block's TX_WITH_WITNESS conditional +// serializer, which emits the segwit marker/flag iff some tx HasWitness(). +// DGB is segwit-active (share_types.hpp SEGWIT_ACTIVATION_VERSION, v36 >= it), +// so that path is LIVE -- a witness-bearing other_tx (or gentx) yields a +// segwit block, exactly as the daemon's submitblock codec expects. +// +// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE -- +// this only re-reads transactions already validated/relayed into known_txs; no +// share format, PoW, coinbase commitment, or PPLNS math is touched. +// --------------------------------------------------------------------------- + +#include +#include +#include + +#include + +#include "transaction.hpp" // dgb::coin::MutableTransaction + +namespace dgb +{ +namespace coin +{ + +// Resolve an ordered other_tx hash list to the deserialized transactions that +// assemble_won_block frames after the gentx. +// +// other_tx_hashes : output of resolve_other_tx_hashes, in block other_txs +// order (= share transaction_hash_refs order) +// known_txs_fn : hash -> pointer to the known MutableTransaction, or +// nullptr if the hash is not in the known-tx set +// +// Returns the transactions in the SAME order as other_tx_hashes (order is +// consensus-relevant: it fixes the block merkle root). Throws std::out_of_range +// if any hash is unknown -- a block with a missing other_tx would hash wrong and +// be rejected, so the reconstruction must fail loudly rather than emit it. +inline std::vector +assemble_other_txs( + const std::vector& other_tx_hashes, + const std::function& known_txs_fn) +{ + std::vector out; + out.reserve(other_tx_hashes.size()); + + for (const auto& h : other_tx_hashes) + { + const MutableTransaction* tx = known_txs_fn(h); + if (tx == nullptr) + throw std::out_of_range( + "assemble_other_txs: other_tx hash not present in known_txs -- " + "cannot reconstruct a complete won block"); + out.push_back(*tx); + } + + return out; +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 66b514928..82ce1a55a 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -84,6 +84,21 @@ if (BUILD_TESTING AND GTest_FOUND) dgb_coin pool sharechain) gtest_add_tests(dgb_other_tx_resolver_test "" AUTO) + # --- #82 sub-slice 2: other_tx hashes -> MutableTransaction other_txs --- + # coin/other_tx_assembler.hpp is the won-block reconstructor`s bridge from + # the resolved other_tx hash list (sub-slice 1) to the deserialized txs that + # assemble_won_block frames (faithful as_block known_txs[h] lookup). Links + # the dgb OBJECT lib like dgb_block_assembly_test because the end-to-end KAT + # frames through dgb::check_merkle_link + the live BlockType codec. MUST also + # appear in the build.yml --target allowlist (#143 NOT_BUILT trap). + add_executable(dgb_other_tx_assembler_test other_tx_assembler_test.cpp) + target_link_libraries(dgb_other_tx_assembler_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_other_tx_assembler_test "" AUTO) + foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test) add_executable(${dgb_guard} ${dgb_guard}.cpp) target_link_libraries(${dgb_guard} PRIVATE diff --git a/src/impl/dgb/test/other_tx_assembler_test.cpp b/src/impl/dgb/test/other_tx_assembler_test.cpp new file mode 100644 index 000000000..e9261500c --- /dev/null +++ b/src/impl/dgb/test/other_tx_assembler_test.cpp @@ -0,0 +1,254 @@ +// --------------------------------------------------------------------------- +// dgb_other_tx_assembler_test -- pins coin/other_tx_assembler.hpp, the won-block +// reconstructor's (#82 sub-slice 2) bridge from the ordered other_tx hash list +// (resolve_other_tx_hashes, sub-slice 1) to the MutableTransaction vector that +// assemble_won_block frames as txs = [gentx] ++ other_txs. +// +// Faithful to p2pool data.py as_block: other_txs = [known_txs[h] for h in +// transaction_hashes]. Proves: +// * the known_txs lookup PRESERVES ref/hash order (= block other_txs order, +// which fixes the merkle root) and is the SAME order assemble_won_block frames; +// * a hash absent from known_txs THROWS std::out_of_range (loud failure: a +// dropped other_tx => wrong merkle root => daemon-rejected block); +// * empty hash list => empty other_txs; +// * END-TO-END: resolve_other_tx_hashes -> assemble_other_txs -> assemble_won_block +// lands the resolved txs in the block in order, after the coinbase; +// * WITNESS reachability via the other_tx path: a witness-bearing other_tx +// flips the assembled block to the TX_WITH_WITNESS codec (DGB is +// segwit-active), proving witness framing is not gentx-only dead code. +// +// Links the dgb OBJECT lib (assemble_won_block reuses dgb::check_merkle_link + +// the live BlockType submitblock codec) like dgb_block_assembly_test. MUST also +// appear in the build.yml --target allowlist (#143 NOT_BUILT trap). +// --------------------------------------------------------------------------- + +#include + +#include +#include +#include +#include + +#include +#include + +#include "../coin/other_tx_assembler.hpp" +#include "../coin/other_tx_resolver.hpp" +#include "../coin/block_assembly.hpp" + +namespace { + +using dgb::coin::MutableTransaction; +using dgb::coin::BlockType; +using dgb::coin::SmallBlockHeaderType; +using dgb::coin::assemble_other_txs; +using dgb::coin::assemble_won_block; +using dgb::coin::resolve_other_tx_hashes; +using dgb::TxHashRefs; + +uint256 H(const char* two) +{ + std::string s; + for (int i = 0; i < 32; ++i) s += two; + uint256 h; h.SetHex(s); + return h; +} + +// A tx whose output value tags it, so we can assert identity/order after the +// lookup and after the block round-trip. +MutableTransaction tagged_tx(int64_t value) +{ + MutableTransaction tx; + tx.version = 1; + tx.locktime = 0; + dgb::coin::TxIn in; + in.prevout.hash.SetNull(); + in.prevout.index = 0; + in.sequence = 0xffffffff; + tx.vin.push_back(in); + dgb::coin::TxOut out; + out.value = value; + tx.vout.push_back(out); + return tx; +} + +MutableTransaction witness_tx(int64_t value) +{ + auto tx = tagged_tx(value); + tx.vin[0].scriptWitness.stack.assign(1, std::vector(4, 0xab)); + return tx; +} + +MutableTransaction coinbase_gentx() +{ + MutableTransaction tx; + tx.version = 1; + tx.locktime = 0; + dgb::coin::TxIn in; + in.prevout.hash.SetNull(); + in.prevout.index = 0xffffffff; + in.sequence = 0xffffffff; + tx.vin.push_back(in); + dgb::coin::TxOut out; + out.value = 5000000000LL; + tx.vout.push_back(out); + return tx; +} + +SmallBlockHeaderType small_header() +{ + SmallBlockHeaderType h; + h.m_version = 0x20000000; + h.m_previous_block.SetHex("00000000000000000000000000000000000000000000000000000000deadbeef"); + h.m_timestamp = 1718700000; + h.m_bits = 0x1a0fffff; + h.m_nonce = 0x12345678; + return h; +} + +// A synthetic known_txs store keyed by hash, with a lookup matching the +// assembler's injected (hash)->const MutableTransaction* contract. +struct KnownTxs +{ + std::map by_hash; + const MutableTransaction* lookup(const uint256& h) const + { + auto it = by_hash.find(h); + return it == by_hash.end() ? nullptr : &it->second; + } + std::function fn() const + { + return [this](const uint256& h) { return lookup(h); }; + } +}; + +// --- Test 1: lookup preserves order ---------------------------------------- +TEST(DgbOtherTxAssembler, PreservesHashOrder) +{ + KnownTxs k; + k.by_hash[H("c0")] = tagged_tx(10); + k.by_hash[H("c1")] = tagged_tx(20); + k.by_hash[H("c2")] = tagged_tx(30); + + // hashes deliberately NOT in map-sorted order -- output must follow input. + std::vector hashes = { H("c2"), H("c0"), H("c1") }; + auto txs = assemble_other_txs(hashes, k.fn()); + + ASSERT_EQ(txs.size(), 3u); + EXPECT_EQ(txs[0].vout[0].value, 30); + EXPECT_EQ(txs[1].vout[0].value, 10); + EXPECT_EQ(txs[2].vout[0].value, 20); +} + +// --- Test 2: missing hash throws (loud failure, no partial block) ---------- +TEST(DgbOtherTxAssembler, MissingHashThrows) +{ + KnownTxs k; + k.by_hash[H("c0")] = tagged_tx(10); + + std::vector hashes = { H("c0"), H("ff") }; // ff is unknown + EXPECT_THROW(assemble_other_txs(hashes, k.fn()), std::out_of_range); +} + +// --- Test 3: empty hash list => empty other_txs ---------------------------- +TEST(DgbOtherTxAssembler, EmptyHashesEmptyTxs) +{ + KnownTxs k; + std::vector none; + auto txs = assemble_other_txs(none, k.fn()); + EXPECT_TRUE(txs.empty()); +} + +// --- Test 4: end-to-end resolve -> assemble -> won-block -------------------- +// Walks the full reconstructor bridge: a 2-share ancestry's refs resolve to +// hashes (sub-slice 1), the hashes resolve to txs (sub-slice 2), and the txs +// frame into the block after the coinbase, in order. +TEST(DgbOtherTxAssembler, EndToEndResolveAssembleFrame) +{ + // ancestry: won <- p1 ; won.nths=[c0,c1], p1.nths=[d0] + const uint256 won = H("a0"), p1 = H("a1"); + std::map parent = { {won, p1} }; + std::map> nths = { + {won, { H("c0"), H("c1") }}, + {p1, { H("d0") }}, + }; + auto nth_parent_fn = [&](const uint256& start, uint64_t n) -> uint256 { + uint256 cur = start; + for (uint64_t i = 0; i < n; ++i) { + auto it = parent.find(cur); + if (it == parent.end()) { uint256 z; z.SetNull(); return z; } + cur = it->second; + } + return cur; + }; + auto new_tx_hashes_fn = [&](const uint256& h) -> const std::vector& { + return nths.at(h); + }; + + // refs: (0,1)=won.nths[1]=c1 ; (1,0)=p1.nths[0]=d0 ; (0,0)=won.nths[0]=c0 + std::vector refs = { {0,1}, {1,0}, {0,0} }; + auto hashes = resolve_other_tx_hashes(won, refs, nth_parent_fn, new_tx_hashes_fn); + ASSERT_EQ(hashes.size(), 3u); + EXPECT_EQ(hashes[0], H("c1")); + EXPECT_EQ(hashes[1], H("d0")); + EXPECT_EQ(hashes[2], H("c0")); + + KnownTxs k; + k.by_hash[H("c0")] = tagged_tx(100); + k.by_hash[H("c1")] = tagged_tx(200); + k.by_hash[H("d0")] = tagged_tx(300); + auto other = assemble_other_txs(hashes, k.fn()); + + auto gentx = coinbase_gentx(); + uint256 gtx_hash; gtx_hash.SetHex("1111111111111111111111111111111111111111111111111111111111111111"); + ::dgb::MerkleLink link; // empty branch => root == gtx_hash + auto [bytes, hex] = assemble_won_block(small_header(), gentx, gtx_hash, link, other); + + PackStream ps(bytes); + BlockType blk; + ps >> blk; + // [gentx] ++ other_txs, in resolved (ref) order: 200 (c1), 300 (d0), 100 (c0) + ASSERT_EQ(blk.m_txs.size(), 4u); + EXPECT_EQ(blk.m_txs[0].vin[0].prevout.index, 0xffffffffu); // coinbase first + EXPECT_EQ(blk.m_txs[1].vout[0].value, 200); + EXPECT_EQ(blk.m_txs[2].vout[0].value, 300); + EXPECT_EQ(blk.m_txs[3].vout[0].value, 100); +} + +// --- Test 5: witness-bearing other_tx flips block to TX_WITH_WITNESS -------- +// Proves the segwit-active path is reachable via the other_tx leg (not just the +// gentx): an other_tx carrying a witness stack makes the conditional codec emit +// a witness block, and the witness round-trips through BlockType. +TEST(DgbOtherTxAssembler, WitnessOtherTxEmitsWitnessBlock) +{ + KnownTxs k; + k.by_hash[H("c0")] = witness_tx(42); + std::vector hashes = { H("c0") }; + auto other = assemble_other_txs(hashes, k.fn()); + ASSERT_EQ(other.size(), 1u); + ASSERT_TRUE(other[0].HasWitness()); + + auto gentx = coinbase_gentx(); // legacy coinbase + ASSERT_FALSE(gentx.HasWitness()); + uint256 gtx_hash; gtx_hash.SetHex("1111111111111111111111111111111111111111111111111111111111111111"); + ::dgb::MerkleLink link; + + auto [wbytes, whex] = assemble_won_block(small_header(), gentx, gtx_hash, link, other); + // same block but with a legacy other_tx => strictly smaller (no witness). + KnownTxs kl; kl.by_hash[H("c0")] = tagged_tx(42); + auto legacy_other = assemble_other_txs(hashes, kl.fn()); + auto [lbytes, lhex] = assemble_won_block(small_header(), gentx, gtx_hash, link, legacy_other); + EXPECT_GT(wbytes.size(), lbytes.size()); + + PackStream ps(wbytes); + BlockType blk; + ps >> blk; + ASSERT_EQ(blk.m_txs.size(), 2u); + EXPECT_FALSE(blk.m_txs[0].HasWitness()); // coinbase legacy + EXPECT_TRUE(blk.m_txs[1].HasWitness()); // other_tx carries witness + ASSERT_EQ(blk.m_txs[1].vin[0].scriptWitness.stack.size(), 1u); + EXPECT_EQ(blk.m_txs[1].vin[0].scriptWitness.stack[0], + std::vector(4, 0xab)); +} + +} // namespace