Skip to content

Commit fa93959

Browse files
authored
Merge pull request #179 from frstrtr/dgb/gentx-unpack
dgb(#82): gentx-bytes -> MutableTransaction unpack (inverse of #173)
2 parents 4c9d8d3 + 02049a9 commit fa93959

10 files changed

Lines changed: 1289 additions & 2 deletions

.github/workflows/build.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ 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 \
72+
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test \
7273
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
7374
v37_test \
7475
-j$(nproc)
@@ -199,7 +200,8 @@ jobs:
199200
test_mweb_builder \
200201
test_address_resolution test_compute_share_target \
201202
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 \
203+
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
204+
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test \
203205
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
204206
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
205207
v37_test \

src/impl/dgb/coin/gentx_unpack.hpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#pragma once
2+
// ---------------------------------------------------------------------------
3+
// dgb::coin::unpack_gentx_coinbase -- the INVERSE of #173's GentxCoinbase
4+
// exposure: turn the SSOT non-witness gentx bytes that
5+
// generate_share_transaction surfaces (out_gentx, GentxCoinbase{bytes, txid})
6+
// back into a deserialized MutableTransaction, ready to inject at block tx
7+
// index 0 of the won-block reconstructor (reconstruct_won_block.hpp).
8+
//
9+
// reconstruct_won_block() takes the gentx as an already-deserialized
10+
// MutableTransaction + its gentx_hash; in the run-loop those come from the
11+
// share's own SSOT coinbase (gentx_coinbase.hpp assemble_gentx_coinbase, the
12+
// single wire layout consumed by both emission and verification). This slice
13+
// is the codec step between the two -- deliberately kept OUT of the pure
14+
// composition body (reconstruct_won_block.hpp header note) so the as_block
15+
// ordering + merkle math stay build-verifiable on injected inputs, while the
16+
// byte<->object round-trip is pinned by its own KAT here.
17+
//
18+
// CRITICAL (integrator 2026-06-19): the gentx is a NON-WITNESS coinbase and
19+
// MUST round-trip its non-witness bytes EXACTLY -- the txid (double-SHA256 of
20+
// the non-witness serialization == p2pool gentx_hash) is what the merkle_root
21+
// walk in assemble_won_block consumes, so any witness/serialization drift here
22+
// corrupts the assembled block's merkle root and the daemon rejects it. We
23+
// therefore unpack with TX_NO_WITNESS: the coinbase vin count is a non-zero
24+
// 0x01 (never the 0x00 segwit dummy), so this parses the body unambiguously
25+
// and CANNOT consume a witness marker/flag or attach a witness stack. The
26+
// resulting MutableTransaction has empty witness stacks (HasWitness()==false),
27+
// so re-serializing it -- even inside assemble_won_block's TX_WITH_WITNESS
28+
// BlockType codec -- emits the identical non-witness bytes and a stable txid.
29+
//
30+
// Failure posture mirrors the sibling reconstructor slices: trailing bytes
31+
// after a complete tx mean the input is not a clean gentx serialization; we
32+
// throw std::out_of_range rather than silently accept a truncated/over-long
33+
// blob, since a wrong coinbase hashes to the wrong merkle root.
34+
//
35+
// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE --
36+
// this is the inverse of an already-oracle-pinned serializer; no share format,
37+
// PoW, coinbase commitment, or PPLNS math is touched.
38+
// ---------------------------------------------------------------------------
39+
40+
#include <stdexcept>
41+
#include <utility>
42+
#include <vector>
43+
44+
#include <core/pack.hpp>
45+
#include <core/hash.hpp>
46+
#include <core/uint256.hpp>
47+
48+
#include "transaction.hpp" // dgb::coin::MutableTransaction, TX_NO_WITNESS
49+
50+
namespace dgb
51+
{
52+
namespace coin
53+
{
54+
55+
// The deserialized gentx ready for injection at block tx index 0.
56+
// tx : MutableTransaction reconstructed from the non-witness bytes
57+
// txid : double-SHA256(non-witness serialization) == p2pool gentx_hash
58+
struct UnpackedGentx
59+
{
60+
MutableTransaction tx;
61+
uint256 txid;
62+
};
63+
64+
// Inverse of assemble_gentx_coinbase (#173 exposure): non-witness gentx bytes
65+
// -> {MutableTransaction, txid}. Throws std::out_of_range if the input carries
66+
// trailing bytes past a complete transaction (malformed gentx serialization).
67+
inline UnpackedGentx
68+
unpack_gentx_coinbase(const std::vector<unsigned char>& gentx_bytes)
69+
{
70+
PackStream ps(gentx_bytes);
71+
72+
MutableTransaction tx;
73+
// Non-witness parse: the coinbase vin count is 0x01 (never the segwit
74+
// 0x00 dummy), so this reads version|vin|vout|locktime with no witness
75+
// branch -- guaranteeing the empty-witness, byte-exact round-trip the
76+
// merkle_root walk depends on.
77+
UnserializeTransaction(tx, ps, TX_NO_WITNESS);
78+
79+
if (!ps.empty())
80+
throw std::out_of_range(
81+
"unpack_gentx_coinbase: trailing bytes after gentx -- "
82+
"input is not a clean non-witness coinbase serialization");
83+
84+
UnpackedGentx out;
85+
// txid = SHA256d of the (canonical) non-witness re-serialization; equals
86+
// the gentx_hash assemble_gentx_coinbase computed over the same layout.
87+
out.txid = Hash(pack(TX_NO_WITNESS(tx)).get_span());
88+
out.tx = std::move(tx);
89+
return out;
90+
}
91+
92+
} // namespace coin
93+
} // namespace dgb
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#pragma once
2+
// ---------------------------------------------------------------------------
3+
// dgb::coin::assemble_other_txs -- the won-block reconstructor's (#82) bridge
4+
// between the ORDERED other_tx hash list (resolve_other_tx_hashes, sub-slice 1)
5+
// and the deserialized MutableTransaction vector that assemble_won_block
6+
// (block_assembly.hpp) frames as txs = [gentx] ++ other_txs.
7+
//
8+
// Faithful C++ port of the known_txs lookup inside p2pool data.py
9+
// Share.as_block(tracker, known_txs):
10+
//
11+
// other_txs = [known_txs[h] for h in transaction_hashes]
12+
//
13+
// resolve_other_tx_hashes already produced `transaction_hashes` (the ref-walk
14+
// over ancestor new_transaction_hashes, in block other_txs order); this slice
15+
// is the remaining `known_txs[h]` indexing. Each hash is resolved through an
16+
// INJECTED lookup (same seam-first decomposition as other_tx_resolver.hpp /
17+
// won_block_dispatch.hpp), so the bridge is unit-testable against a synthetic
18+
// known-tx set without standing up a live mempool / ShareTracker. In the
19+
// run-loop known_txs_fn binds to the node's known-transaction store (mempool +
20+
// peer-relayed tx cache), the same set share verification populated.
21+
//
22+
// Failure mode (integrator 2026-06-19, mirroring sub-slice 1): a missing hash
23+
// is a malformed/incomplete-reconstruction condition -- p2pool's known_txs[h]
24+
// raises KeyError. We throw std::out_of_range rather than skip the tx or emit
25+
// a placeholder: a block with a dropped/wrong other_tx has the wrong merkle
26+
// root and is rejected by the daemon, so failing loudly here is strictly safer
27+
// than shipping a malformed block to the network.
28+
//
29+
// Witness framing is NOT decided here. assemble_other_txs returns the txs
30+
// verbatim (whatever witness stacks they carry); the block's witness SHAPE is
31+
// governed downstream by assemble_won_block's TX_WITH_WITNESS conditional
32+
// serializer, which emits the segwit marker/flag iff some tx HasWitness().
33+
// DGB is segwit-active (share_types.hpp SEGWIT_ACTIVATION_VERSION, v36 >= it),
34+
// so that path is LIVE -- a witness-bearing other_tx (or gentx) yields a
35+
// segwit block, exactly as the daemon's submitblock codec expects.
36+
//
37+
// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE --
38+
// this only re-reads transactions already validated/relayed into known_txs; no
39+
// share format, PoW, coinbase commitment, or PPLNS math is touched.
40+
// ---------------------------------------------------------------------------
41+
42+
#include <functional>
43+
#include <stdexcept>
44+
#include <vector>
45+
46+
#include <core/uint256.hpp>
47+
48+
#include "transaction.hpp" // dgb::coin::MutableTransaction
49+
50+
namespace dgb
51+
{
52+
namespace coin
53+
{
54+
55+
// Resolve an ordered other_tx hash list to the deserialized transactions that
56+
// assemble_won_block frames after the gentx.
57+
//
58+
// other_tx_hashes : output of resolve_other_tx_hashes, in block other_txs
59+
// order (= share transaction_hash_refs order)
60+
// known_txs_fn : hash -> pointer to the known MutableTransaction, or
61+
// nullptr if the hash is not in the known-tx set
62+
//
63+
// Returns the transactions in the SAME order as other_tx_hashes (order is
64+
// consensus-relevant: it fixes the block merkle root). Throws std::out_of_range
65+
// if any hash is unknown -- a block with a missing other_tx would hash wrong and
66+
// be rejected, so the reconstruction must fail loudly rather than emit it.
67+
inline std::vector<MutableTransaction>
68+
assemble_other_txs(
69+
const std::vector<uint256>& other_tx_hashes,
70+
const std::function<const MutableTransaction*(const uint256&)>& known_txs_fn)
71+
{
72+
std::vector<MutableTransaction> out;
73+
out.reserve(other_tx_hashes.size());
74+
75+
for (const auto& h : other_tx_hashes)
76+
{
77+
const MutableTransaction* tx = known_txs_fn(h);
78+
if (tx == nullptr)
79+
throw std::out_of_range(
80+
"assemble_other_txs: other_tx hash not present in known_txs -- "
81+
"cannot reconstruct a complete won block");
82+
out.push_back(*tx);
83+
}
84+
85+
return out;
86+
}
87+
88+
} // namespace coin
89+
} // namespace dgb
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

0 commit comments

Comments
 (0)