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
10 changes: 6 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ jobs:
test_phase4_embedded \
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 \
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 dgb_reconstruct_won_block_test dgb_gentx_unpack_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 @@ -198,8 +199,9 @@ jobs:
test_phase4_embedded \
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 \
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 dgb_reconstruct_won_block_test dgb_gentx_unpack_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
93 changes: 93 additions & 0 deletions src/impl/dgb/coin/gentx_unpack.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#pragma once
// ---------------------------------------------------------------------------
// dgb::coin::unpack_gentx_coinbase -- the INVERSE of #173's GentxCoinbase
// exposure: turn the SSOT non-witness gentx bytes that
// generate_share_transaction surfaces (out_gentx, GentxCoinbase{bytes, txid})
// back into a deserialized MutableTransaction, ready to inject at block tx
// index 0 of the won-block reconstructor (reconstruct_won_block.hpp).
//
// reconstruct_won_block() takes the gentx as an already-deserialized
// MutableTransaction + its gentx_hash; in the run-loop those come from the
// share's own SSOT coinbase (gentx_coinbase.hpp assemble_gentx_coinbase, the
// single wire layout consumed by both emission and verification). This slice
// is the codec step between the two -- deliberately kept OUT of the pure
// composition body (reconstruct_won_block.hpp header note) so the as_block
// ordering + merkle math stay build-verifiable on injected inputs, while the
// byte<->object round-trip is pinned by its own KAT here.
//
// CRITICAL (integrator 2026-06-19): the gentx is a NON-WITNESS coinbase and
// MUST round-trip its non-witness bytes EXACTLY -- the txid (double-SHA256 of
// the non-witness serialization == p2pool gentx_hash) is what the merkle_root
// walk in assemble_won_block consumes, so any witness/serialization drift here
// corrupts the assembled block's merkle root and the daemon rejects it. We
// therefore unpack with TX_NO_WITNESS: the coinbase vin count is a non-zero
// 0x01 (never the 0x00 segwit dummy), so this parses the body unambiguously
// and CANNOT consume a witness marker/flag or attach a witness stack. The
// resulting MutableTransaction has empty witness stacks (HasWitness()==false),
// so re-serializing it -- even inside assemble_won_block's TX_WITH_WITNESS
// BlockType codec -- emits the identical non-witness bytes and a stable txid.
//
// Failure posture mirrors the sibling reconstructor slices: trailing bytes
// after a complete tx mean the input is not a clean gentx serialization; we
// throw std::out_of_range rather than silently accept a truncated/over-long
// blob, since a wrong coinbase hashes to the wrong merkle root.
//
// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE --
// this is the inverse of an already-oracle-pinned serializer; no share format,
// PoW, coinbase commitment, or PPLNS math is touched.
// ---------------------------------------------------------------------------

#include <stdexcept>
#include <utility>
#include <vector>

#include <core/pack.hpp>
#include <core/hash.hpp>
#include <core/uint256.hpp>

#include "transaction.hpp" // dgb::coin::MutableTransaction, TX_NO_WITNESS

namespace dgb
{
namespace coin
{

// The deserialized gentx ready for injection at block tx index 0.
// tx : MutableTransaction reconstructed from the non-witness bytes
// txid : double-SHA256(non-witness serialization) == p2pool gentx_hash
struct UnpackedGentx
{
MutableTransaction tx;
uint256 txid;
};

// Inverse of assemble_gentx_coinbase (#173 exposure): non-witness gentx bytes
// -> {MutableTransaction, txid}. Throws std::out_of_range if the input carries
// trailing bytes past a complete transaction (malformed gentx serialization).
inline UnpackedGentx
unpack_gentx_coinbase(const std::vector<unsigned char>& gentx_bytes)
{
PackStream ps(gentx_bytes);

MutableTransaction tx;
// Non-witness parse: the coinbase vin count is 0x01 (never the segwit
// 0x00 dummy), so this reads version|vin|vout|locktime with no witness
// branch -- guaranteeing the empty-witness, byte-exact round-trip the
// merkle_root walk depends on.
UnserializeTransaction(tx, ps, TX_NO_WITNESS);

if (!ps.empty())
throw std::out_of_range(
"unpack_gentx_coinbase: trailing bytes after gentx -- "
"input is not a clean non-witness coinbase serialization");

UnpackedGentx out;
// txid = SHA256d of the (canonical) non-witness re-serialization; equals
// the gentx_hash assemble_gentx_coinbase computed over the same layout.
out.txid = Hash(pack(TX_NO_WITNESS(tx)).get_span());
out.tx = std::move(tx);
return out;
}

} // namespace coin
} // namespace dgb
89 changes: 89 additions & 0 deletions src/impl/dgb/coin/other_tx_assembler.hpp
Original file line number Diff line number Diff line change
@@ -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 <functional>
#include <stdexcept>
#include <vector>

#include <core/uint256.hpp>

#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<MutableTransaction>
assemble_other_txs(
const std::vector<uint256>& other_tx_hashes,
const std::function<const MutableTransaction*(const uint256&)>& known_txs_fn)
{
std::vector<MutableTransaction> 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
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
Loading
Loading