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
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
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_other_tx_resolver_test \
dgb_other_tx_assembler_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 @@ -200,6 +201,7 @@ jobs:
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_other_tx_resolver_test \
dgb_other_tx_assembler_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
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
15 changes: 15 additions & 0 deletions src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,21 @@ if (BUILD_TESTING AND GTest_FOUND)
dgb_coin pool sharechain)
gtest_add_tests(dgb_other_tx_resolver_test "" AUTO)

# --- #82 sub-slice 2: other_tx hashes -> MutableTransaction other_txs ---
# coin/other_tx_assembler.hpp is the won-block reconstructor`s bridge from
# the resolved other_tx hash list (sub-slice 1) to the deserialized txs that
# assemble_won_block frames (faithful as_block known_txs[h] lookup). Links
# the dgb OBJECT lib like dgb_block_assembly_test because the end-to-end KAT
# frames through dgb::check_merkle_link + the live BlockType codec. MUST also
# appear in the build.yml --target allowlist (#143 NOT_BUILT trap).
add_executable(dgb_other_tx_assembler_test other_tx_assembler_test.cpp)
target_link_libraries(dgb_other_tx_assembler_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_assembler_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
254 changes: 254 additions & 0 deletions src/impl/dgb/test/other_tx_assembler_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// ---------------------------------------------------------------------------
// dgb_other_tx_assembler_test -- pins coin/other_tx_assembler.hpp, the won-block
// reconstructor's (#82 sub-slice 2) bridge from the ordered other_tx hash list
// (resolve_other_tx_hashes, sub-slice 1) to the MutableTransaction vector that
// assemble_won_block frames as txs = [gentx] ++ other_txs.
//
// Faithful to p2pool data.py as_block: other_txs = [known_txs[h] for h in
// transaction_hashes]. Proves:
// * the known_txs lookup PRESERVES ref/hash order (= block other_txs order,
// which fixes the merkle root) and is the SAME order assemble_won_block frames;
// * a hash absent from known_txs THROWS std::out_of_range (loud failure: a
// dropped other_tx => wrong merkle root => daemon-rejected block);
// * empty hash list => empty other_txs;
// * END-TO-END: resolve_other_tx_hashes -> assemble_other_txs -> assemble_won_block
// lands the resolved txs in the block in order, after the coinbase;
// * WITNESS reachability via the other_tx path: a witness-bearing other_tx
// flips the assembled block to the TX_WITH_WITNESS codec (DGB is
// segwit-active), proving witness framing is not gentx-only dead code.
//
// Links the dgb OBJECT lib (assemble_won_block reuses dgb::check_merkle_link +
// the live BlockType submitblock codec) like dgb_block_assembly_test. 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/pack.hpp>
#include <core/uint256.hpp>

#include "../coin/other_tx_assembler.hpp"
#include "../coin/other_tx_resolver.hpp"
#include "../coin/block_assembly.hpp"

namespace {

using dgb::coin::MutableTransaction;
using dgb::coin::BlockType;
using dgb::coin::SmallBlockHeaderType;
using dgb::coin::assemble_other_txs;
using dgb::coin::assemble_won_block;
using dgb::coin::resolve_other_tx_hashes;
using dgb::TxHashRefs;

uint256 H(const char* two)
{
std::string s;
for (int i = 0; i < 32; ++i) s += two;
uint256 h; h.SetHex(s);
return h;
}

// A tx whose output value tags it, so we can assert identity/order after the
// lookup and after the block round-trip.
MutableTransaction tagged_tx(int64_t value)
{
MutableTransaction tx;
tx.version = 1;
tx.locktime = 0;
dgb::coin::TxIn in;
in.prevout.hash.SetNull();
in.prevout.index = 0;
in.sequence = 0xffffffff;
tx.vin.push_back(in);
dgb::coin::TxOut out;
out.value = value;
tx.vout.push_back(out);
return tx;
}

MutableTransaction witness_tx(int64_t value)
{
auto tx = tagged_tx(value);
tx.vin[0].scriptWitness.stack.assign(1, std::vector<unsigned char>(4, 0xab));
return tx;
}

MutableTransaction coinbase_gentx()
{
MutableTransaction tx;
tx.version = 1;
tx.locktime = 0;
dgb::coin::TxIn in;
in.prevout.hash.SetNull();
in.prevout.index = 0xffffffff;
in.sequence = 0xffffffff;
tx.vin.push_back(in);
dgb::coin::TxOut out;
out.value = 5000000000LL;
tx.vout.push_back(out);
return tx;
}

SmallBlockHeaderType small_header()
{
SmallBlockHeaderType h;
h.m_version = 0x20000000;
h.m_previous_block.SetHex("00000000000000000000000000000000000000000000000000000000deadbeef");
h.m_timestamp = 1718700000;
h.m_bits = 0x1a0fffff;
h.m_nonce = 0x12345678;
return h;
}

// A synthetic known_txs store keyed by hash, with a lookup matching the
// assembler's injected (hash)->const MutableTransaction* contract.
struct KnownTxs
{
std::map<uint256, MutableTransaction> by_hash;
const MutableTransaction* lookup(const uint256& h) const
{
auto it = by_hash.find(h);
return it == by_hash.end() ? nullptr : &it->second;
}
std::function<const MutableTransaction*(const uint256&)> fn() const
{
return [this](const uint256& h) { return lookup(h); };
}
};

// --- Test 1: lookup preserves order ----------------------------------------
TEST(DgbOtherTxAssembler, PreservesHashOrder)
{
KnownTxs k;
k.by_hash[H("c0")] = tagged_tx(10);
k.by_hash[H("c1")] = tagged_tx(20);
k.by_hash[H("c2")] = tagged_tx(30);

// hashes deliberately NOT in map-sorted order -- output must follow input.
std::vector<uint256> hashes = { H("c2"), H("c0"), H("c1") };
auto txs = assemble_other_txs(hashes, k.fn());

ASSERT_EQ(txs.size(), 3u);
EXPECT_EQ(txs[0].vout[0].value, 30);
EXPECT_EQ(txs[1].vout[0].value, 10);
EXPECT_EQ(txs[2].vout[0].value, 20);
}

// --- Test 2: missing hash throws (loud failure, no partial block) ----------
TEST(DgbOtherTxAssembler, MissingHashThrows)
{
KnownTxs k;
k.by_hash[H("c0")] = tagged_tx(10);

std::vector<uint256> hashes = { H("c0"), H("ff") }; // ff is unknown
EXPECT_THROW(assemble_other_txs(hashes, k.fn()), std::out_of_range);
}

// --- Test 3: empty hash list => empty other_txs ----------------------------
TEST(DgbOtherTxAssembler, EmptyHashesEmptyTxs)
{
KnownTxs k;
std::vector<uint256> none;
auto txs = assemble_other_txs(none, k.fn());
EXPECT_TRUE(txs.empty());
}

// --- Test 4: end-to-end resolve -> assemble -> won-block --------------------
// Walks the full reconstructor bridge: a 2-share ancestry's refs resolve to
// hashes (sub-slice 1), the hashes resolve to txs (sub-slice 2), and the txs
// frame into the block after the coinbase, in order.
TEST(DgbOtherTxAssembler, EndToEndResolveAssembleFrame)
{
// ancestry: won <- p1 ; won.nths=[c0,c1], p1.nths=[d0]
const uint256 won = H("a0"), p1 = H("a1");
std::map<uint256, uint256> parent = { {won, p1} };
std::map<uint256, std::vector<uint256>> nths = {
{won, { H("c0"), H("c1") }},
{p1, { H("d0") }},
};
auto nth_parent_fn = [&](const uint256& start, uint64_t n) -> uint256 {
uint256 cur = start;
for (uint64_t i = 0; i < n; ++i) {
auto it = parent.find(cur);
if (it == parent.end()) { uint256 z; z.SetNull(); return z; }
cur = it->second;
}
return cur;
};
auto new_tx_hashes_fn = [&](const uint256& h) -> const std::vector<uint256>& {
return nths.at(h);
};

// refs: (0,1)=won.nths[1]=c1 ; (1,0)=p1.nths[0]=d0 ; (0,0)=won.nths[0]=c0
std::vector<TxHashRefs> refs = { {0,1}, {1,0}, {0,0} };
auto hashes = resolve_other_tx_hashes(won, refs, nth_parent_fn, new_tx_hashes_fn);
ASSERT_EQ(hashes.size(), 3u);
EXPECT_EQ(hashes[0], H("c1"));
EXPECT_EQ(hashes[1], H("d0"));
EXPECT_EQ(hashes[2], H("c0"));

KnownTxs k;
k.by_hash[H("c0")] = tagged_tx(100);
k.by_hash[H("c1")] = tagged_tx(200);
k.by_hash[H("d0")] = tagged_tx(300);
auto other = assemble_other_txs(hashes, k.fn());

auto gentx = coinbase_gentx();
uint256 gtx_hash; gtx_hash.SetHex("1111111111111111111111111111111111111111111111111111111111111111");
::dgb::MerkleLink link; // empty branch => root == gtx_hash
auto [bytes, hex] = assemble_won_block(small_header(), gentx, gtx_hash, link, other);

PackStream ps(bytes);
BlockType blk;
ps >> blk;
// [gentx] ++ other_txs, in resolved (ref) order: 200 (c1), 300 (d0), 100 (c0)
ASSERT_EQ(blk.m_txs.size(), 4u);
EXPECT_EQ(blk.m_txs[0].vin[0].prevout.index, 0xffffffffu); // coinbase first
EXPECT_EQ(blk.m_txs[1].vout[0].value, 200);
EXPECT_EQ(blk.m_txs[2].vout[0].value, 300);
EXPECT_EQ(blk.m_txs[3].vout[0].value, 100);
}

// --- Test 5: witness-bearing other_tx flips block to TX_WITH_WITNESS --------
// Proves the segwit-active path is reachable via the other_tx leg (not just the
// gentx): an other_tx carrying a witness stack makes the conditional codec emit
// a witness block, and the witness round-trips through BlockType.
TEST(DgbOtherTxAssembler, WitnessOtherTxEmitsWitnessBlock)
{
KnownTxs k;
k.by_hash[H("c0")] = witness_tx(42);
std::vector<uint256> hashes = { H("c0") };
auto other = assemble_other_txs(hashes, k.fn());
ASSERT_EQ(other.size(), 1u);
ASSERT_TRUE(other[0].HasWitness());

auto gentx = coinbase_gentx(); // legacy coinbase
ASSERT_FALSE(gentx.HasWitness());
uint256 gtx_hash; gtx_hash.SetHex("1111111111111111111111111111111111111111111111111111111111111111");
::dgb::MerkleLink link;

auto [wbytes, whex] = assemble_won_block(small_header(), gentx, gtx_hash, link, other);
// same block but with a legacy other_tx => strictly smaller (no witness).
KnownTxs kl; kl.by_hash[H("c0")] = tagged_tx(42);
auto legacy_other = assemble_other_txs(hashes, kl.fn());
auto [lbytes, lhex] = assemble_won_block(small_header(), gentx, gtx_hash, link, legacy_other);
EXPECT_GT(wbytes.size(), lbytes.size());

PackStream ps(wbytes);
BlockType blk;
ps >> blk;
ASSERT_EQ(blk.m_txs.size(), 2u);
EXPECT_FALSE(blk.m_txs[0].HasWitness()); // coinbase legacy
EXPECT_TRUE(blk.m_txs[1].HasWitness()); // other_tx carries witness
ASSERT_EQ(blk.m_txs[1].vin[0].scriptWitness.stack.size(), 1u);
EXPECT_EQ(blk.m_txs[1].vin[0].scriptWitness.stack[0],
std::vector<unsigned char>(4, 0xab));
}

} // namespace