Skip to content
Merged
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 @@ -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)
Expand Down Expand Up @@ -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 \
Expand Down
102 changes: 102 additions & 0 deletions src/impl/dgb/coin/other_tx_resolver.hpp
Original file line number Diff line number Diff line change
@@ -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 <functional>
#include <stdexcept>
#include <vector>

#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<uint256>
resolve_other_tx_hashes(
const uint256& won_share_hash,
const std::vector<TxHashRefs>& refs,
const std::function<uint256(const uint256&, uint64_t)>& nth_parent_fn,
const std::function<const std::vector<uint256>&(const uint256&)>& new_tx_hashes_fn)
{
std::vector<uint256> 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<uint256>& 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
16 changes: 16 additions & 0 deletions src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
149 changes: 149 additions & 0 deletions src/impl/dgb/test/other_tx_resolver_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <gtest/gtest.h>

#include <functional>
#include <map>
#include <stdexcept>
#include <vector>

#include <core/uint256.hpp>

#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<uint256, uint256> parent; // child -> parent
std::map<uint256, std::vector<uint256>> 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<uint256>& new_tx_hashes(const uint256& h) const
{
return nths.at(h);
}
};

std::function<uint256(const uint256&, uint64_t)> walk_of(const Ancestry& a)
{
return [&a](const uint256& s, uint64_t n) { return a.nth_parent(s, n); };
}
std::function<const std::vector<uint256>&(const uint256&)> nths_of(const Ancestry& a)
{
return [&a](const uint256& h) -> const std::vector<uint256>& { 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<TxHashRefs> 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<TxHashRefs> 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<TxHashRefs> 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<TxHashRefs> 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<TxHashRefs> 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);
}