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 @@ -82,7 +82,7 @@ jobs:
test_address_resolution test_compute_share_target \
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \
dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_coinbase_value_parity_test dgb_submit_classify_test \
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test v37_test \
-j$(nproc)
Expand Down Expand Up @@ -214,7 +214,7 @@ jobs:
test_address_resolution test_compute_share_target \
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \
dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_coinbase_value_parity_test dgb_submit_classify_test \
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
v37_test \
Expand Down
108 changes: 108 additions & 0 deletions src/impl/dgb/coin/template_other_txs.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// ---------------------------------------------------------------------------
// template_other_txs.hpp -- the producer bridge between the embedded work
// template's transactions[] (make_mempool_tx_source / embedded_tx_select.cpp)
// and the won-block reconstructor's template_other_txs_fn seam
// (reconstruct_closure.hpp, make_reconstruct_closure_from_template).
//
// The reconstructor frames the broadcast block as [gentx] ++ other_txs, where
// other_txs is the captured-GBT template's non-coinbase set in template order
// (#271). The template carries each tx as a GBT entry {data,txid,hash,fee}; the
// reconstructor consumes them as already-deserialized dgb::coin::Mutable-
// Transaction. This header is the missing deserialize step: the GBT `data`
// (with-witness) hex -> MutableTransaction, in template order. It is the literal
// wire between make_mempool_tx_source (the template's tx SOURCE) and the
// reconstructor (the broadcast tx SINK).
//
// SSOT split:
// * deserialize_template_other_txs(json) -- pure: transactions[] -> txs[].
// Throws on a malformed `data` entry (bad hex / trailing bytes) so the
// closure's broad catch fails the whole won block CLOSED rather than
// broadcasting a half-decoded tx set (mirrors unpack_gentx_coinbase).
// * make_template_other_txs_fn(captured_template_txs_fn) -- adapts a per-share
// captured-transactions[] provider into the template_other_txs_fn signature
// make_reconstruct_closure_from_template installs. The provider MUST return
// the transactions[] of the GBT TEMPLATE the won share was handed at job
// hand-out (the per-job capture seam, #271), NOT the live mempool selection:
// a won share commits to the template it was mined against, which may differ
// from the current mempool. Wiring that per-job capture provider is the
// run-loop integration; this header owns ONLY the deserialize wire.
//
// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE -- the
// transactions[] form is already the conformant GBT shape make_mempool_tx_source
// emits; this only decodes it back into the in-memory tx the block carries.
// ---------------------------------------------------------------------------
#pragma once

#include <functional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

#include <nlohmann/json.hpp>

#include <core/pack.hpp>
#include <core/uint256.hpp>
#include <btclibs/util/strencodings.h> // ParseHex

#include "transaction.hpp" // MutableTransaction, UnserializeTransaction, TX_WITH_WITNESS

namespace dgb
{
namespace coin
{

// Decode one GBT `data` (with-witness) hex string into a MutableTransaction.
// Throws std::out_of_range if the hex carries trailing bytes past a complete
// transaction (a malformed template entry), matching unpack_gentx_coinbase's
// fail-closed contract for the gentx.
inline MutableTransaction
deserialize_template_tx(const std::string& data_hex)
{
PackStream ps(ParseHex(data_hex));
MutableTransaction tx;
// With-witness parse: make_mempool_tx_source emits `data` as the with-
// witness submit bytes (pack(TX_WITH_WITNESS(tx))), so decode symmetrically.
UnserializeTransaction(tx, ps, TX_WITH_WITNESS);
if (!ps.empty())
throw std::out_of_range(
"deserialize_template_tx: trailing bytes after tx -- "
"malformed GBT template `data` entry");
return tx;
}

// Decode the captured GBT template's transactions[] into the won-block other-tx
// vector, in template order (block tx order after the coinbase). Each entry's
// `data` field is the with-witness bytes make_mempool_tx_source emitted. An
// empty / absent array yields an empty vector (a valid coinbase-only block).
inline std::vector<MutableTransaction>
deserialize_template_other_txs(const nlohmann::json& transactions)
{
std::vector<MutableTransaction> out;
if (transactions.is_array())
{
out.reserve(transactions.size());
for (const auto& entry : transactions)
out.push_back(deserialize_template_tx(entry.at("data").get<std::string>()));
}
return out;
}

// Adapt a per-share captured-transactions[] provider into the run-loop's
// template_other_txs_fn (the third argument of make_reconstruct_closure_from_
// template). captured_template_txs_fn(share_hash) MUST return the transactions[]
// of the template the won share was mined against (per-job capture, #271);
// decoded here through the deserialize SSOT. Any decode error propagates so the
// reconstruct closure fails the won block CLOSED.
inline std::function<std::vector<MutableTransaction>(const uint256&)>
make_template_other_txs_fn(
std::function<nlohmann::json(const uint256&)> captured_template_txs_fn)
{
return [captured_template_txs_fn = std::move(captured_template_txs_fn)](
const uint256& share_hash) -> std::vector<MutableTransaction> {
return deserialize_template_other_txs(captured_template_txs_fn(share_hash));
};
}

} // 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 @@ -259,6 +259,21 @@ if (BUILD_TESTING AND GTest_FOUND)
nlohmann_json::nlohmann_json)
gtest_add_tests(dgb_embedded_tx_select_test "" AUTO)

# dgb_template_other_txs_test: pins coin/template_other_txs.hpp, the producer
# bridge decoding the embedded template transactions[] (make_mempool_tx_source
# GBT data) back into the MutableTransaction vector the won-block reconstructor
# frames as [gentx] ++ other_txs. Compiles the tx codec AND the reconstruct
# closure, so it links the embedded_tx_select SCC set + pool/sharechain. MUST
# also be in BOTH build.yml --target allowlists (#143 NOT_BUILT trap).
add_executable(dgb_template_other_txs_test template_other_txs_test.cpp)
target_link_libraries(dgb_template_other_txs_test PRIVATE
GTest::gtest_main GTest::gtest
core dgb dgb_stratum
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
dgb_coin pool sharechain
nlohmann_json::nlohmann_json)
gtest_add_tests(dgb_template_other_txs_test "" AUTO)

# dgb_coinbase_value_parity_test: END-TO-END GBT-parity KAT. Drives a
# populated Mempool through the production make_mempool_tx_source ->
# EmbeddedCoinNode -> build_work_template path and asserts coinbasevalue ==
Expand Down
232 changes: 232 additions & 0 deletions src/impl/dgb/test/template_other_txs_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// ---------------------------------------------------------------------------
// dgb_template_other_txs_test -- pins coin/template_other_txs.hpp, the producer
// bridge that decodes the embedded work template's transactions[] (the GBT data
// the miner is funded with, shaped by make_mempool_tx_source) back into the
// MutableTransaction vector the won-block reconstructor's template_other_txs_fn
// seam frames as [gentx] ++ other_txs.
//
// This closes the loop between the two already-pinned halves:
// * make_mempool_tx_source (embedded_tx_select.cpp) -- Mempool -> GBT txs[]
// * make_reconstruct_closure_from_template (#82) -- [gentx] ++ other_txs
// proving the SAME txs the template hands the miner are the txs that land in the
// reconstructed broadcast block, byte-faithfully and in template order.
//
// Links the full dgb_coin codec (it compiles the tx serialization) and the
// reconstruct closure. MUST be in BOTH build.yml --target allowlists (#143
// NOT_BUILT trap). Per-coin isolation: src/impl/dgb/ only.
// ---------------------------------------------------------------------------
#include <cstdint>
#include <functional>
#include <stdexcept>
#include <string>
#include <vector>

#include <gtest/gtest.h>

#include <impl/dgb/coin/template_other_txs.hpp>
#include <impl/dgb/coin/embedded_tx_select.hpp>
#include <impl/dgb/coin/mempool.hpp>
#include <impl/dgb/coin/transaction.hpp>
#include <impl/dgb/coin/reconstruct_closure.hpp>

#include <core/pack.hpp>
#include <core/uint256.hpp>
#include <btclibs/util/strencodings.h>

using dgb::coin::Mempool;
using dgb::coin::MutableTransaction;
using dgb::coin::TxIn;
using dgb::coin::TxOut;
using dgb::coin::TX_WITH_WITNESS;
using dgb::coin::TX_NO_WITNESS;
using dgb::coin::compute_txid;
using dgb::coin::make_mempool_tx_source;
using dgb::coin::deserialize_template_tx;
using dgb::coin::deserialize_template_other_txs;
using dgb::coin::make_template_other_txs_fn;
using dgb::coin::make_reconstruct_closure_from_template;
using dgb::coin::reconstruct_won_block_from_template;
using dgb::coin::unpack_gentx_coinbase;
using dgb::coin::SmallBlockHeaderType;
using dgb::coin::WonShareInputs;

namespace {

MutableTransaction tagged_tx(int64_t value, uint32_t index)
{
MutableTransaction tx;
tx.version = 1;
tx.locktime = 0;
TxIn in;
in.prevout.hash.SetNull();
in.prevout.index = index;
in.sequence = 0xffffffff;
tx.vin.push_back(in);
TxOut out;
out.value = value;
tx.vout.push_back(out);
return tx;
}

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

std::vector<unsigned char> noseg_bytes(const MutableTransaction& tx)
{
auto packed = pack(TX_NO_WITNESS(tx));
auto sp = packed.get_span();
return std::vector<unsigned char>(
reinterpret_cast<const unsigned char*>(sp.data()),
reinterpret_cast<const unsigned char*>(sp.data()) + sp.size());
}

std::string withwit_hex(const MutableTransaction& tx)
{
return HexStr(pack(TX_WITH_WITNESS(tx)).get_span());
}

SmallBlockHeaderType make_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;
}

uint256 won_hash()
{
uint256 h; h.SetHex("00000000000000000000000000000000000000000000000000000000000000a0");
return h;
}

} // namespace

// --- Test 1: round-trip -- the production template txs decode byte-faithfully -
// Populate a Mempool, shape it through the PRODUCTION make_mempool_tx_source,
// then decode the resulting transactions[] back through the bridge. Each decoded
// tx must re-serialize (with-witness) to the exact `data` the template carried
// and carry the exact `txid`, in template order.
TEST(DgbTemplateOtherTxs, RoundTripsProductionTemplateTxs)
{
Mempool pool;
MutableTransaction a = tagged_tx(10, 0);
MutableTransaction b = tagged_tx(20, 1);
ASSERT_TRUE(pool.add_tx(a));
ASSERT_TRUE(pool.add_tx(b));
pool.set_tx_fee(compute_txid(a), 700);
pool.set_tx_fee(compute_txid(b), 300);

auto source = make_mempool_tx_source(pool, /*max_weight=*/4'000'000);
const auto sel = source();
ASSERT_EQ(sel.transactions.size(), 2u);

const auto txs = deserialize_template_other_txs(sel.transactions);
ASSERT_EQ(txs.size(), sel.transactions.size()); // template order preserved

for (size_t i = 0; i < txs.size(); ++i)
{
// Decoded tx re-serializes to the exact template `data` and `txid`.
EXPECT_EQ(withwit_hex(txs[i]), sel.transactions[i]["data"].get<std::string>());
EXPECT_EQ(compute_txid(txs[i]).GetHex(), sel.transactions[i]["txid"].get<std::string>());
}
}

// --- Test 2: empty / absent transactions[] -> empty vector (coinbase-only) ----
TEST(DgbTemplateOtherTxs, EmptyTransactionsIsEmptyVector)
{
EXPECT_TRUE(deserialize_template_other_txs(nlohmann::json::array()).empty());
EXPECT_TRUE(deserialize_template_other_txs(nlohmann::json(nullptr)).empty());
}

// --- Test 3: malformed `data` (trailing byte) -> throws (fail-closed) ---------
TEST(DgbTemplateOtherTxs, TrailingBytesThrow)
{
std::string good = withwit_hex(tagged_tx(10, 0));
EXPECT_NO_THROW(deserialize_template_tx(good));
EXPECT_THROW(deserialize_template_tx(good + "ff"), std::out_of_range); // junk byte
}

// --- Test 4: end-to-end -- funded template txs land in the reconstructed block -
// The bridge, wired as the reconstructor's template_other_txs_fn via a per-share
// captured-transactions[] provider, reconstructs the SAME block that feeding the
// deserialized txs straight into reconstruct_won_block_from_template produces:
// [gentx] ++ the mempool-funded template txs, byte-identical.
TEST(DgbTemplateOtherTxs, ReconstructsBlockWithFundedTemplateTxs)
{
Mempool pool;
MutableTransaction a = tagged_tx(10, 0);
MutableTransaction b = tagged_tx(20, 1);
ASSERT_TRUE(pool.add_tx(a));
ASSERT_TRUE(pool.add_tx(b));
pool.set_tx_fee(compute_txid(a), 700);
pool.set_tx_fee(compute_txid(b), 300);
const auto sel = make_mempool_tx_source(pool, /*max_weight=*/4'000'000)();
ASSERT_FALSE(sel.transactions.empty());

auto sh = make_small_header();
::dgb::MerkleLink link;
const uint256 won = won_hash();
auto gentx_bytes = noseg_bytes(make_gentx());
auto ug = unpack_gentx_coinbase(gentx_bytes);

// Expected: [gentx] ++ decoded-template-txs straight through the SSOT.
const auto other = deserialize_template_other_txs(sel.transactions);
const auto expected =
reconstruct_won_block_from_template(sh, link, ug.tx, ug.txid, other);

// Actual: the run-loop shape -- closure pulls the per-share captured txs[]
// through the bridge factory.
auto txs_json = sel.transactions;
auto closure = make_reconstruct_closure_from_template(
[won, sh, link](const uint256& h) -> WonShareInputs {
if (h != won) throw std::out_of_range("unknown share");
return WonShareInputs{sh, link};
},
[gentx_bytes](const uint256&) { return gentx_bytes; },
make_template_other_txs_fn(
[txs_json](const uint256&) { return txs_json; }));

auto got = closure(won);
ASSERT_TRUE(got.has_value());
EXPECT_FALSE(got->first.empty());
EXPECT_EQ(got->first, expected.bytes); // funded txs land in the block
EXPECT_EQ(got->second, expected.hex);
}

// --- Test 5: a bad captured provider fails the whole won block CLOSED ----------
TEST(DgbTemplateOtherTxs, BadProviderFailsClosed)
{
auto sh = make_small_header();
::dgb::MerkleLink link;
const uint256 won = won_hash();
auto gentx_bytes = noseg_bytes(make_gentx());

nlohmann::json bad = nlohmann::json::array();
bad.push_back({{"data", withwit_hex(tagged_tx(10, 0)) + "ff"}}); // trailing junk

auto closure = make_reconstruct_closure_from_template(
[won, sh, link](const uint256& h) -> WonShareInputs {
if (h != won) throw std::out_of_range("unknown share");
return WonShareInputs{sh, link};
},
[gentx_bytes](const uint256&) { return gentx_bytes; },
make_template_other_txs_fn([bad](const uint256&) { return bad; }));

EXPECT_FALSE(closure(won).has_value()); // decode throw -> nullopt, no broadcast
}
Loading