diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index decda14e7..f9bc1437f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: test_mweb_builder \ 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_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ v37_test \ -j$(nproc) @@ -199,7 +199,7 @@ jobs: test_mweb_builder \ 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_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_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_resolver.hpp b/src/impl/dgb/coin/other_tx_resolver.hpp new file mode 100644 index 000000000..31120b62c --- /dev/null +++ b/src/impl/dgb/coin/other_tx_resolver.hpp @@ -0,0 +1,102 @@ +#pragma once +// --------------------------------------------------------------------------- +// dgb::coin::resolve_other_tx_hashes -- the won-block reconstructor's (#82) +// connecting tissue between a share's transaction_hash_refs and the ordered +// other_tx hash list that assemble_won_block (block_assembly.hpp) needs. +// +// Faithful C++ port of the ancestry resolution inside p2pool data.py +// Tracker.get_other_tx_hashes(share): +// +// parents_needed = max(ref_share_count for ref_share_count, _ in +// share.share_info['transaction_hash_refs']) ... +// other_tx_hashes = [ +// self.items[ self.get_nth_parent_hash(share.hash, ref_share_count) ] +// .share_info['new_transaction_hashes'][ref_tx_count] +// for ref_share_count, ref_tx_count in +// share.share_info['transaction_hash_refs'] +// ] +// +// A transaction_hash_ref is a (share_count, tx_count) pair: walk back +// `share_count` generations from the won share (share_count == 0 is the won +// share ITSELF, matching get_nth_parent_hash(h, 0) == h), then index `tx_count` +// into THAT ancestor's new_transaction_hashes. The result preserves ref order, +// which is the block's other_txs order ([gentx] ++ other_txs in as_block). +// +// Why a ref-walk and NOT a flat known_txs map (integrator 2026-06-19): the +// share format stores ancestry as back-references into ancestor shares' +// new_transaction_hashes (V34+ never embeds txs), so the ONLY faithful +// resolution is to re-walk that ancestry through the tracker. A flat +// known_txs[hash] lookup would resolve the same hashes by accident on the +// happy path but diverges the moment two ancestors announce the same txid or +// a ref points past the locally-known set -- it cannot reproduce how the +// sharechain actually addresses a tx. +// +// The two tracker operations are INJECTED as callables (same seam-first +// decomposition as won_block_dispatch.hpp / WonBlockReconstructor) so the walk +// is unit-testable against a synthetic ancestry without standing up a live +// ShareTracker. In the run-loop these bind to: +// nth_parent_fn = chain.get_nth_parent_via_skip(h, n) (shared skip list) +// new_tx_hashes_fn= chain.get_share(h).invoke([](auto* s){ +// return s->m_tx_info.m_new_transaction_hashes; }) +// +// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE -- +// this only re-reads share_info already validated by share_check; no share +// format, PoW, coinbase commitment, or PPLNS math is touched. +// --------------------------------------------------------------------------- + +#include +#include +#include + +#include "../share_types.hpp" // dgb::TxHashRefs, uint256 + +namespace dgb +{ +namespace coin +{ + +// Resolve a share's transaction_hash_refs to the ordered other_tx hash list. +// +// won_share_hash : hash of the share that found the block (walk origin) +// refs : share.m_tx_info.m_transaction_hash_refs, in order +// nth_parent_fn : (start, n) -> hash of the n-th parent of `start` +// (n == 0 returns `start`); IsNull() if the walk runs off +// the end of the known sharechain +// new_tx_hashes_fn: share_hash -> that share's new_transaction_hashes +// +// Returns the other_tx hashes in ref order. Throws std::out_of_range if an +// ancestor walk-back exceeds the sharechain depth or a tx_count indexes past +// an ancestor's new_transaction_hashes -- both are malformed-share conditions +// that must fail the reconstruction loudly rather than emit a wrong block. +inline std::vector +resolve_other_tx_hashes( + const uint256& won_share_hash, + const std::vector& refs, + const std::function& nth_parent_fn, + const std::function&(const uint256&)>& new_tx_hashes_fn) +{ + std::vector out; + out.reserve(refs.size()); + + for (const auto& ref : refs) + { + const uint256 ancestor = nth_parent_fn(won_share_hash, ref.m_share_count); + if (ancestor.IsNull()) + throw std::out_of_range( + "resolve_other_tx_hashes: transaction_hash_ref share_count " + "walks past the known sharechain"); + + const std::vector& nths = new_tx_hashes_fn(ancestor); + if (ref.m_tx_count >= nths.size()) + throw std::out_of_range( + "resolve_other_tx_hashes: transaction_hash_ref tx_count " + "out of range for ancestor new_transaction_hashes"); + + out.push_back(nths[ref.m_tx_count]); + } + + return out; +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 63a082a16..66b514928 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -68,6 +68,22 @@ if (BUILD_TESTING AND GTest_FOUND) dgb_coin pool sharechain) gtest_add_tests(dgb_gentx_share_path_test "" AUTO) + # --- #82 sub-slice 1: transaction_hash_refs -> other_tx hash walk ------- + # coin/other_tx_resolver.hpp is the won-block reconstructor`s connecting + # tissue (faithful get_other_tx_hashes ancestry resolution). It pulls + # uint256 (SetHex TU) via share_types.hpp, so it links the proven dgb OBJECT-lib SCC set (like dgb_block_assembly_test) + # rather than the header-only guard foreach (linking bare `core` drags in + # web_server.cpp, whose symbols live in the SCC libs). The tracker ops are + # injected, so no dgb pool state is exercised. MUST also appear in the + # build.yml --target allowlist (#143 NOT_BUILT trap). + add_executable(dgb_other_tx_resolver_test other_tx_resolver_test.cpp) + target_link_libraries(dgb_other_tx_resolver_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_resolver_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_resolver_test.cpp b/src/impl/dgb/test/other_tx_resolver_test.cpp new file mode 100644 index 000000000..43f18a748 --- /dev/null +++ b/src/impl/dgb/test/other_tx_resolver_test.cpp @@ -0,0 +1,149 @@ +// --------------------------------------------------------------------------- +// dgb_other_tx_resolver_test -- pins coin/other_tx_resolver.hpp, the won-block +// reconstructor's (#82) transaction_hash_refs -> ordered other_tx hash walk. +// +// Proves the ref-walk semantics against a SYNTHETIC ancestry (no live +// ShareTracker): the two tracker operations (nth_parent / new_transaction_hashes +// lookup) are injected, exactly as resolve_other_tx_hashes takes them, so the +// faithful p2pool get_other_tx_hashes resolution is verified in isolation: +// * share_count == 0 indexes the won share's OWN new_transaction_hashes +// * share_count == N walks back N generations +// * ref ORDER is the output order (= block other_txs order) +// * empty refs => empty list +// * malformed refs (tx_count OOB / walk past chain end) throw, never emit a +// wrong hash +// +// Header-only: links no dgb OBJECT lib, only core (uint256) + GTest. MUST also +// appear in the build.yml --target allowlist (#143 NOT_BUILT trap). +// --------------------------------------------------------------------------- +#include + +#include +#include +#include +#include + +#include + +#include "../coin/other_tx_resolver.hpp" + +using dgb::coin::resolve_other_tx_hashes; +using dgb::TxHashRefs; + +namespace { + +uint256 H(const char* two) // 0x"two" repeated to 64 hex chars +{ + std::string s; + for (int i = 0; i < 32; ++i) s += two; + uint256 h; h.SetHex(s); + return h; +} + +// A synthetic 3-share chain: won <- p1 <- p2. Each share carries its own +// new_transaction_hashes. parent[h] gives the immediate parent; null beyond p2. +struct Ancestry +{ + uint256 won = H("a0"), p1 = H("a1"), p2 = H("a2"); + std::map parent; // child -> parent + std::map> nths; // share -> new_transaction_hashes + + Ancestry() + { + parent[won] = p1; + parent[p1] = p2; + // p2 has no parent -> walking past it returns null. + nths[won] = { H("c0"), H("c1") }; + nths[p1] = { H("d0") }; + nths[p2] = { H("e0"), H("e1"), H("e2") }; + } + + // nth_parent_fn: 0 => start itself; null if walk runs off the chain. + uint256 nth_parent(const uint256& start, uint64_t n) const + { + uint256 cur = start; + for (uint64_t i = 0; i < n; ++i) + { + auto it = parent.find(cur); + if (it == parent.end()) return uint256(); // null + cur = it->second; + } + return cur; + } + + const std::vector& new_tx_hashes(const uint256& h) const + { + return nths.at(h); + } +}; + +std::function walk_of(const Ancestry& a) +{ + return [&a](const uint256& s, uint64_t n) { return a.nth_parent(s, n); }; +} +std::function&(const uint256&)> nths_of(const Ancestry& a) +{ + return [&a](const uint256& h) -> const std::vector& { return a.new_tx_hashes(h); }; +} + +} // namespace + +// share_count == 0 resolves against the won share's own new_transaction_hashes. +TEST(OtherTxResolver, SelfShareRefs) +{ + Ancestry a; + std::vector refs = { {0, 1}, {0, 0} }; + auto out = resolve_other_tx_hashes(a.won, refs, walk_of(a), nths_of(a)); + ASSERT_EQ(out.size(), 2u); + EXPECT_EQ(out[0], H("c1")); // won.new_tx[1] + EXPECT_EQ(out[1], H("c0")); // won.new_tx[0] -- ref ORDER preserved +} + +// share_count == N walks N generations back before indexing. +TEST(OtherTxResolver, AncestorWalkBack) +{ + Ancestry a; + std::vector refs = { {1, 0}, {2, 2} }; + auto out = resolve_other_tx_hashes(a.won, refs, walk_of(a), nths_of(a)); + ASSERT_EQ(out.size(), 2u); + EXPECT_EQ(out[0], H("d0")); // p1.new_tx[0] + EXPECT_EQ(out[1], H("e2")); // p2.new_tx[2] +} + +// Mixed refs across generations preserve emission order = block other_txs order. +TEST(OtherTxResolver, MixedOrderPreserved) +{ + Ancestry a; + std::vector refs = { {2, 0}, {0, 1}, {1, 0} }; + auto out = resolve_other_tx_hashes(a.won, refs, walk_of(a), nths_of(a)); + ASSERT_EQ(out.size(), 3u); + EXPECT_EQ(out[0], H("e0")); + EXPECT_EQ(out[1], H("c1")); + EXPECT_EQ(out[2], H("d0")); +} + +// No refs => empty other_txs (coinbase-only block). +TEST(OtherTxResolver, EmptyRefs) +{ + Ancestry a; + auto out = resolve_other_tx_hashes(a.won, {}, walk_of(a), nths_of(a)); + EXPECT_TRUE(out.empty()); +} + +// tx_count past the ancestor's new_transaction_hashes is a malformed share. +TEST(OtherTxResolver, TxCountOutOfRangeThrows) +{ + Ancestry a; + std::vector refs = { {1, 5} }; // p1 has only 1 new tx + EXPECT_THROW(resolve_other_tx_hashes(a.won, refs, walk_of(a), nths_of(a)), + std::out_of_range); +} + +// share_count past the chain end is a malformed share (null ancestor). +TEST(OtherTxResolver, WalkPastChainEndThrows) +{ + Ancestry a; + std::vector refs = { {3, 0} }; // only won<-p1<-p2 exist + EXPECT_THROW(resolve_other_tx_hashes(a.won, refs, walk_of(a), nths_of(a)), + std::out_of_range); +}