Skip to content

Commit ad3c6af

Browse files
authored
Merge pull request #174 from frstrtr/dgb/won-block-other-tx-walk
dgb(#82): resolve transaction_hash_refs -> ordered other_tx hashes (reconstructor sub-slice 1)
2 parents 7b1f57a + 885938d commit ad3c6af

4 files changed

Lines changed: 269 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +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 \
71-
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test \
71+
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
7272
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
7373
v37_test \
7474
-j$(nproc)
@@ -199,7 +199,7 @@ jobs:
199199
test_mweb_builder \
200200
test_address_resolution test_compute_share_target \
201201
test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \
202-
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test \
202+
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
203203
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
204204
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
205205
v37_test \
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#pragma once
2+
// ---------------------------------------------------------------------------
3+
// dgb::coin::resolve_other_tx_hashes -- the won-block reconstructor's (#82)
4+
// connecting tissue between a share's transaction_hash_refs and the ordered
5+
// other_tx hash list that assemble_won_block (block_assembly.hpp) needs.
6+
//
7+
// Faithful C++ port of the ancestry resolution inside p2pool data.py
8+
// Tracker.get_other_tx_hashes(share):
9+
//
10+
// parents_needed = max(ref_share_count for ref_share_count, _ in
11+
// share.share_info['transaction_hash_refs']) ...
12+
// other_tx_hashes = [
13+
// self.items[ self.get_nth_parent_hash(share.hash, ref_share_count) ]
14+
// .share_info['new_transaction_hashes'][ref_tx_count]
15+
// for ref_share_count, ref_tx_count in
16+
// share.share_info['transaction_hash_refs']
17+
// ]
18+
//
19+
// A transaction_hash_ref is a (share_count, tx_count) pair: walk back
20+
// `share_count` generations from the won share (share_count == 0 is the won
21+
// share ITSELF, matching get_nth_parent_hash(h, 0) == h), then index `tx_count`
22+
// into THAT ancestor's new_transaction_hashes. The result preserves ref order,
23+
// which is the block's other_txs order ([gentx] ++ other_txs in as_block).
24+
//
25+
// Why a ref-walk and NOT a flat known_txs map (integrator 2026-06-19): the
26+
// share format stores ancestry as back-references into ancestor shares'
27+
// new_transaction_hashes (V34+ never embeds txs), so the ONLY faithful
28+
// resolution is to re-walk that ancestry through the tracker. A flat
29+
// known_txs[hash] lookup would resolve the same hashes by accident on the
30+
// happy path but diverges the moment two ancestors announce the same txid or
31+
// a ref points past the locally-known set -- it cannot reproduce how the
32+
// sharechain actually addresses a tx.
33+
//
34+
// The two tracker operations are INJECTED as callables (same seam-first
35+
// decomposition as won_block_dispatch.hpp / WonBlockReconstructor) so the walk
36+
// is unit-testable against a synthetic ancestry without standing up a live
37+
// ShareTracker. In the run-loop these bind to:
38+
// nth_parent_fn = chain.get_nth_parent_via_skip(h, n) (shared skip list)
39+
// new_tx_hashes_fn= chain.get_share(h).invoke([](auto* s){
40+
// return s->m_tx_info.m_new_transaction_hashes; })
41+
//
42+
// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE --
43+
// this only re-reads share_info already validated by share_check; no share
44+
// format, PoW, coinbase commitment, or PPLNS math is touched.
45+
// ---------------------------------------------------------------------------
46+
47+
#include <functional>
48+
#include <stdexcept>
49+
#include <vector>
50+
51+
#include "../share_types.hpp" // dgb::TxHashRefs, uint256
52+
53+
namespace dgb
54+
{
55+
namespace coin
56+
{
57+
58+
// Resolve a share's transaction_hash_refs to the ordered other_tx hash list.
59+
//
60+
// won_share_hash : hash of the share that found the block (walk origin)
61+
// refs : share.m_tx_info.m_transaction_hash_refs, in order
62+
// nth_parent_fn : (start, n) -> hash of the n-th parent of `start`
63+
// (n == 0 returns `start`); IsNull() if the walk runs off
64+
// the end of the known sharechain
65+
// new_tx_hashes_fn: share_hash -> that share's new_transaction_hashes
66+
//
67+
// Returns the other_tx hashes in ref order. Throws std::out_of_range if an
68+
// ancestor walk-back exceeds the sharechain depth or a tx_count indexes past
69+
// an ancestor's new_transaction_hashes -- both are malformed-share conditions
70+
// that must fail the reconstruction loudly rather than emit a wrong block.
71+
inline std::vector<uint256>
72+
resolve_other_tx_hashes(
73+
const uint256& won_share_hash,
74+
const std::vector<TxHashRefs>& refs,
75+
const std::function<uint256(const uint256&, uint64_t)>& nth_parent_fn,
76+
const std::function<const std::vector<uint256>&(const uint256&)>& new_tx_hashes_fn)
77+
{
78+
std::vector<uint256> out;
79+
out.reserve(refs.size());
80+
81+
for (const auto& ref : refs)
82+
{
83+
const uint256 ancestor = nth_parent_fn(won_share_hash, ref.m_share_count);
84+
if (ancestor.IsNull())
85+
throw std::out_of_range(
86+
"resolve_other_tx_hashes: transaction_hash_ref share_count "
87+
"walks past the known sharechain");
88+
89+
const std::vector<uint256>& nths = new_tx_hashes_fn(ancestor);
90+
if (ref.m_tx_count >= nths.size())
91+
throw std::out_of_range(
92+
"resolve_other_tx_hashes: transaction_hash_ref tx_count "
93+
"out of range for ancestor new_transaction_hashes");
94+
95+
out.push_back(nths[ref.m_tx_count]);
96+
}
97+
98+
return out;
99+
}
100+
101+
} // namespace coin
102+
} // namespace dgb

src/impl/dgb/test/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@ if (BUILD_TESTING AND GTest_FOUND)
6868
dgb_coin pool sharechain)
6969
gtest_add_tests(dgb_gentx_share_path_test "" AUTO)
7070

71+
# --- #82 sub-slice 1: transaction_hash_refs -> other_tx hash walk -------
72+
# coin/other_tx_resolver.hpp is the won-block reconstructor`s connecting
73+
# tissue (faithful get_other_tx_hashes ancestry resolution). It pulls
74+
# uint256 (SetHex TU) via share_types.hpp, so it links the proven dgb OBJECT-lib SCC set (like dgb_block_assembly_test)
75+
# rather than the header-only guard foreach (linking bare `core` drags in
76+
# web_server.cpp, whose symbols live in the SCC libs). The tracker ops are
77+
# injected, so no dgb pool state is exercised. MUST also appear in the
78+
# build.yml --target allowlist (#143 NOT_BUILT trap).
79+
add_executable(dgb_other_tx_resolver_test other_tx_resolver_test.cpp)
80+
target_link_libraries(dgb_other_tx_resolver_test PRIVATE
81+
GTest::gtest_main GTest::gtest
82+
core dgb
83+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
84+
dgb_coin pool sharechain)
85+
gtest_add_tests(dgb_other_tx_resolver_test "" AUTO)
86+
7187
foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test)
7288
add_executable(${dgb_guard} ${dgb_guard}.cpp)
7389
target_link_libraries(${dgb_guard} PRIVATE
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// ---------------------------------------------------------------------------
2+
// dgb_other_tx_resolver_test -- pins coin/other_tx_resolver.hpp, the won-block
3+
// reconstructor's (#82) transaction_hash_refs -> ordered other_tx hash walk.
4+
//
5+
// Proves the ref-walk semantics against a SYNTHETIC ancestry (no live
6+
// ShareTracker): the two tracker operations (nth_parent / new_transaction_hashes
7+
// lookup) are injected, exactly as resolve_other_tx_hashes takes them, so the
8+
// faithful p2pool get_other_tx_hashes resolution is verified in isolation:
9+
// * share_count == 0 indexes the won share's OWN new_transaction_hashes
10+
// * share_count == N walks back N generations
11+
// * ref ORDER is the output order (= block other_txs order)
12+
// * empty refs => empty list
13+
// * malformed refs (tx_count OOB / walk past chain end) throw, never emit a
14+
// wrong hash
15+
//
16+
// Header-only: links no dgb OBJECT lib, only core (uint256) + GTest. MUST also
17+
// appear in the build.yml --target allowlist (#143 NOT_BUILT trap).
18+
// ---------------------------------------------------------------------------
19+
#include <gtest/gtest.h>
20+
21+
#include <functional>
22+
#include <map>
23+
#include <stdexcept>
24+
#include <vector>
25+
26+
#include <core/uint256.hpp>
27+
28+
#include "../coin/other_tx_resolver.hpp"
29+
30+
using dgb::coin::resolve_other_tx_hashes;
31+
using dgb::TxHashRefs;
32+
33+
namespace {
34+
35+
uint256 H(const char* two) // 0x"two" repeated to 64 hex chars
36+
{
37+
std::string s;
38+
for (int i = 0; i < 32; ++i) s += two;
39+
uint256 h; h.SetHex(s);
40+
return h;
41+
}
42+
43+
// A synthetic 3-share chain: won <- p1 <- p2. Each share carries its own
44+
// new_transaction_hashes. parent[h] gives the immediate parent; null beyond p2.
45+
struct Ancestry
46+
{
47+
uint256 won = H("a0"), p1 = H("a1"), p2 = H("a2");
48+
std::map<uint256, uint256> parent; // child -> parent
49+
std::map<uint256, std::vector<uint256>> nths; // share -> new_transaction_hashes
50+
51+
Ancestry()
52+
{
53+
parent[won] = p1;
54+
parent[p1] = p2;
55+
// p2 has no parent -> walking past it returns null.
56+
nths[won] = { H("c0"), H("c1") };
57+
nths[p1] = { H("d0") };
58+
nths[p2] = { H("e0"), H("e1"), H("e2") };
59+
}
60+
61+
// nth_parent_fn: 0 => start itself; null if walk runs off the chain.
62+
uint256 nth_parent(const uint256& start, uint64_t n) const
63+
{
64+
uint256 cur = start;
65+
for (uint64_t i = 0; i < n; ++i)
66+
{
67+
auto it = parent.find(cur);
68+
if (it == parent.end()) return uint256(); // null
69+
cur = it->second;
70+
}
71+
return cur;
72+
}
73+
74+
const std::vector<uint256>& new_tx_hashes(const uint256& h) const
75+
{
76+
return nths.at(h);
77+
}
78+
};
79+
80+
std::function<uint256(const uint256&, uint64_t)> walk_of(const Ancestry& a)
81+
{
82+
return [&a](const uint256& s, uint64_t n) { return a.nth_parent(s, n); };
83+
}
84+
std::function<const std::vector<uint256>&(const uint256&)> nths_of(const Ancestry& a)
85+
{
86+
return [&a](const uint256& h) -> const std::vector<uint256>& { return a.new_tx_hashes(h); };
87+
}
88+
89+
} // namespace
90+
91+
// share_count == 0 resolves against the won share's own new_transaction_hashes.
92+
TEST(OtherTxResolver, SelfShareRefs)
93+
{
94+
Ancestry a;
95+
std::vector<TxHashRefs> refs = { {0, 1}, {0, 0} };
96+
auto out = resolve_other_tx_hashes(a.won, refs, walk_of(a), nths_of(a));
97+
ASSERT_EQ(out.size(), 2u);
98+
EXPECT_EQ(out[0], H("c1")); // won.new_tx[1]
99+
EXPECT_EQ(out[1], H("c0")); // won.new_tx[0] -- ref ORDER preserved
100+
}
101+
102+
// share_count == N walks N generations back before indexing.
103+
TEST(OtherTxResolver, AncestorWalkBack)
104+
{
105+
Ancestry a;
106+
std::vector<TxHashRefs> refs = { {1, 0}, {2, 2} };
107+
auto out = resolve_other_tx_hashes(a.won, refs, walk_of(a), nths_of(a));
108+
ASSERT_EQ(out.size(), 2u);
109+
EXPECT_EQ(out[0], H("d0")); // p1.new_tx[0]
110+
EXPECT_EQ(out[1], H("e2")); // p2.new_tx[2]
111+
}
112+
113+
// Mixed refs across generations preserve emission order = block other_txs order.
114+
TEST(OtherTxResolver, MixedOrderPreserved)
115+
{
116+
Ancestry a;
117+
std::vector<TxHashRefs> refs = { {2, 0}, {0, 1}, {1, 0} };
118+
auto out = resolve_other_tx_hashes(a.won, refs, walk_of(a), nths_of(a));
119+
ASSERT_EQ(out.size(), 3u);
120+
EXPECT_EQ(out[0], H("e0"));
121+
EXPECT_EQ(out[1], H("c1"));
122+
EXPECT_EQ(out[2], H("d0"));
123+
}
124+
125+
// No refs => empty other_txs (coinbase-only block).
126+
TEST(OtherTxResolver, EmptyRefs)
127+
{
128+
Ancestry a;
129+
auto out = resolve_other_tx_hashes(a.won, {}, walk_of(a), nths_of(a));
130+
EXPECT_TRUE(out.empty());
131+
}
132+
133+
// tx_count past the ancestor's new_transaction_hashes is a malformed share.
134+
TEST(OtherTxResolver, TxCountOutOfRangeThrows)
135+
{
136+
Ancestry a;
137+
std::vector<TxHashRefs> refs = { {1, 5} }; // p1 has only 1 new tx
138+
EXPECT_THROW(resolve_other_tx_hashes(a.won, refs, walk_of(a), nths_of(a)),
139+
std::out_of_range);
140+
}
141+
142+
// share_count past the chain end is a malformed share (null ancestor).
143+
TEST(OtherTxResolver, WalkPastChainEndThrows)
144+
{
145+
Ancestry a;
146+
std::vector<TxHashRefs> refs = { {3, 0} }; // only won<-p1<-p2 exist
147+
EXPECT_THROW(resolve_other_tx_hashes(a.won, refs, walk_of(a), nths_of(a)),
148+
std::out_of_range);
149+
}

0 commit comments

Comments
 (0)