diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da06458fb..7805fb1fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,7 +81,7 @@ jobs: test_mweb_builder \ test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_redistribute_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \ - dgb_gentx_coinbase_test dgb_connection_coinbase_test dgb_pplns_payout_split_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_conn_pplns_producer_test dgb_other_tx_resolver_test \ + dgb_gentx_coinbase_test dgb_connection_coinbase_test dgb_pplns_payout_split_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test nmc_mempool_name_test dgb_gentx_share_path_test dgb_conn_pplns_producer_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_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test dgb_template_capture_test dgb_aux_doge_db_commitment_bind_test dgb_aux_doge_dc_proof_test dgb_aux_doge_bind_parsers_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 dgb_share_target_genesis_test dgb_pool_msg_wire_test dgb_get_shares_walk_test dgb_download_stops_test dgb_think_p1_walk_bounds_test dgb_think_p1_desired_emit_test dgb_think_p6_desired_cutoff_test dgb_think_p4_head_keys_test dgb_think_p3_best_head_test dgb_g1_oracle_byte_parity_test dgb_think_p2_walk_bounds_test dgb_expected_time_to_block_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test v37_test \ @@ -213,7 +213,7 @@ jobs: test_mweb_builder \ test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_redistribute_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \ - dgb_gentx_coinbase_test dgb_connection_coinbase_test dgb_pplns_payout_split_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_conn_pplns_producer_test dgb_other_tx_resolver_test \ + dgb_gentx_coinbase_test dgb_connection_coinbase_test dgb_pplns_payout_split_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test nmc_mempool_name_test dgb_gentx_share_path_test dgb_conn_pplns_producer_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_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test dgb_template_capture_test dgb_aux_doge_db_commitment_bind_test dgb_aux_doge_dc_proof_test dgb_aux_doge_bind_parsers_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 dgb_share_target_genesis_test dgb_pool_msg_wire_test dgb_get_shares_walk_test dgb_download_stops_test dgb_think_p1_walk_bounds_test dgb_think_p1_desired_emit_test dgb_think_p6_desired_cutoff_test dgb_think_p4_head_keys_test dgb_think_p3_best_head_test dgb_g1_oracle_byte_parity_test dgb_think_p2_walk_bounds_test dgb_expected_time_to_block_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ diff --git a/src/impl/nmc/coin/mempool.hpp b/src/impl/nmc/coin/mempool.hpp index f8fd849d6..fb64cf557 100644 --- a/src/impl/nmc/coin/mempool.hpp +++ b/src/impl/nmc/coin/mempool.hpp @@ -42,6 +42,52 @@ namespace coin { // MAX_MONEY, consensus.h COINBASE_MATURITY. static constexpr core::coin::ChainLimits NMC_LIMITS = {2'100'000'000'000'000LL, 100, 0}; +// ─── Namecoin name operations (PB) ──────────────────────── +// +// Namecoin extends Bitcoin with three name operations encoded as a prefix on an +// output scriptPubKey, gated by a distinguishing tx-version marker. c2pool is a +// merge-miner, NOT a name registry: it RELAYS name txs into the block template +// WITHOUT performing name-db consensus validation (no expiry / no +// name_new->name_firstupdate linkage / no duplicate-name checks). namecoind owns +// name-db consensus. The trust is SPV-trust in the configured daemon backend: +// a name tx is only consensus-TRUSTED when sourced from the trusted +// namecoind-backed mempool/GBT (TxSource::Daemon), NOT from arbitrary peer relay +// (integrator §4 caveat 1). PB records the daemon-vs-relay provenance per entry +// and admits both; binding the trust to the daemon source is a PF KAT item. +// +// Reference: namecoin-core script/names.cpp CNameScript; primitives/transaction.h +// NAMECOIN_TX_VERSION / getNamecoinVersion(). + +// Namecoin name-tx version marker. Namecoin packs the name version into the high +// 16 bits of nVersion; getNamecoinVersion() == NAMECOIN_TX_VERSION (0x7100) +// distinguishes a name tx from a bare Bitcoin tx that merely starts an output +// with OP_1..OP_3 (segwit v1+/taproot, bare multisig). +static constexpr int32_t NAMECOIN_TX_VERSION = 0x7100; +inline int32_t name_version(int32_t nVersion) { return (nVersion >> 16) & 0xffff; } + +// Name-op prefix opcodes (namecoin-core reuses OP_1/OP_2/OP_3). +enum : unsigned char { + OP_NAME_NEW = 0x51, // OP_1 + OP_NAME_FIRSTUPDATE = 0x52, // OP_2 + OP_NAME_UPDATE = 0x53, // OP_3 +}; + +enum class NameOp : uint8_t { None = 0, New, FirstUpdate, Update }; + +// Provenance of an admitted tx. A name tx is consensus-TRUSTED only when +// daemon-sourced (SPV-trust boundary); a PeerRelay name tx is admitted but not +// trusted — PF enforces the binding (integrator §4 caveat 1). +enum class TxSource : uint8_t { Daemon = 0, PeerRelay }; + +inline const char* name_op_str(NameOp op) { + switch (op) { + case NameOp::New: return "name_new"; + case NameOp::FirstUpdate: return "name_firstupdate"; + case NameOp::Update: return "name_update"; + default: return "none"; + } +} + // ─── MempoolEntry ──────────────────────────────────────────────────────────── struct MempoolEntry { @@ -53,6 +99,10 @@ struct MempoolEntry { uint64_t fee{0}; // satoshi (computed from UTXO when available) bool fee_known{false}; // true when fee was computed from UTXO lookups time_t time_added{0}; + NameOp name_op{NameOp::None}; // Namecoin name op class (PB), None for plain tx + TxSource source{TxSource::PeerRelay}; // provenance: daemon-trusted vs peer-relay + + bool is_name() const { return name_op != NameOp::None; } double feerate() const { uint32_t vsize = (weight + 3) / 4; // ceil(weight/4) @@ -83,6 +133,28 @@ inline void compute_tx_weight(const MutableTransaction& tx, weight = base_size * 4 + witness_size; } +/// Classify a transaction as a Namecoin name operation WITHOUT name-db +/// validation. A name tx is identified by the Namecoin version marker AND a +/// leading name opcode on one of its output scripts; the version gate prevents +/// false-positives against bare OP_1..OP_3 outputs (taproot / multisig) in plain +/// Bitcoin txs. Returns the FIRST name op found, else NameOp::None. Full +/// CNameScript structural + name-db validation is namecoind's job (SPV-trust). +inline NameOp classify_name_op(const MutableTransaction& tx) { + if (name_version(tx.version) != NAMECOIN_TX_VERSION) + return NameOp::None; + for (const auto& out : tx.vout) { + const auto& d = out.scriptPubKey.m_data; + if (d.empty()) continue; + switch (d[0]) { + case OP_NAME_NEW: return NameOp::New; + case OP_NAME_FIRSTUPDATE: return NameOp::FirstUpdate; + case OP_NAME_UPDATE: return NameOp::Update; + default: break; + } + } + return NameOp::None; +} + // ─── Mempool ───────────────────────────────────────────────────────────────── class Mempool { @@ -122,7 +194,8 @@ class Mempool { /// When utxo is non-null, computes fee = sum(input_values) - sum(output_values). /// Falls back to fee_known=false if any input is missing from the UTXO set /// or from a parent mempool transaction (chain-of-unconfirmed / CPFP). - bool add_tx(const MutableTransaction& tx, core::coin::UTXOViewCache* utxo) { + bool add_tx(const MutableTransaction& tx, core::coin::UTXOViewCache* utxo, + TxSource source = TxSource::PeerRelay) { uint256 txid = compute_txid(tx); std::lock_guard lock(m_mutex); @@ -137,6 +210,18 @@ class Mempool { compute_tx_weight(tx, entry.base_size, entry.witness_size, entry.weight); entry.time_added = std::time(nullptr); + // PB: classify Namecoin name ops and record provenance. Name txs are + // RELAYED into the template without name-db validation (SPV-trust in the + // daemon source); namecoind owns name consensus. + entry.source = source; + entry.name_op = classify_name_op(tx); + if (entry.name_op != NameOp::None) { + LOG_INFO << "[EMB-NMC] Mempool: admit " << name_op_str(entry.name_op) + << " (relay WITHOUT name-db validation; namecoind owns name consensus)" + << " src=" << (source == TxSource::Daemon ? "daemon" : "peer-relay") + << " txid=" << txid.GetHex().substr(0, 16); + } + // Compute fee from UTXO + mempool parent lookups compute_fee_locked(entry, utxo); @@ -314,6 +399,24 @@ class Mempool { return sum; } + /// Count of admitted Namecoin name-operation txs currently in the pool. + size_t name_op_count() const { + std::lock_guard lock(m_mutex); + size_t n = 0; + for (const auto& [txid, e] : m_pool) if (e.is_name()) ++n; + return n; + } + + /// Count of name-op txs sourced from the trusted daemon backend (the only + /// ones PF will treat as consensus-trusted; SPV-trust boundary). + size_t trusted_name_op_count() const { + std::lock_guard lock(m_mutex); + size_t n = 0; + for (const auto& [txid, e] : m_pool) + if (e.is_name() && e.source == TxSource::Daemon) ++n; + return n; + } + std::optional get_entry(const uint256& txid) const { std::lock_guard lock(m_mutex); auto it = m_pool.find(txid); diff --git a/src/impl/nmc/test/CMakeLists.txt b/src/impl/nmc/test/CMakeLists.txt index 90d86de66..34d2536f0 100644 --- a/src/impl/nmc/test/CMakeLists.txt +++ b/src/impl/nmc/test/CMakeLists.txt @@ -58,10 +58,22 @@ if (BUILD_TESTING AND GTest_FOUND) c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage) target_link_libraries(nmc_reconstruct_won_block_test PRIVATE nmc_coin) + # P1 PB: Namecoin name-operation relay KATs (coin/mempool.hpp name_* path + + # fee flow through template_builder.hpp). Same SCC the sibling tests link + # (core + nmc_coin for the out-of-line MutableTransaction ctor). + add_executable(nmc_mempool_name_test nmc_mempool_name_test.cpp) + target_link_libraries(nmc_mempool_name_test PRIVATE + GTest::gtest_main GTest::gtest + core nlohmann_json::nlohmann_json) + target_link_libraries(nmc_mempool_name_test PRIVATE + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage) + target_link_libraries(nmc_mempool_name_test PRIVATE nmc_coin) + include(GoogleTest) include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR}) gtest_add_tests(nmc_auxpow_merkle_test "" AUTO) gtest_add_tests(nmc_template_builder_test "" AUTO) gtest_add_tests(nmc_auxpow_wire_test "" AUTO) gtest_add_tests(nmc_reconstruct_won_block_test "" AUTO) + gtest_add_tests(nmc_mempool_name_test "" AUTO) endif() diff --git a/src/impl/nmc/test/nmc_mempool_name_test.cpp b/src/impl/nmc/test/nmc_mempool_name_test.cpp new file mode 100644 index 000000000..224e8055d --- /dev/null +++ b/src/impl/nmc/test/nmc_mempool_name_test.cpp @@ -0,0 +1,235 @@ +// --------------------------------------------------------------------------- +// nmc::coin::Mempool — Namecoin name-operation relay KATs (P1 PB). +// +// PB adds the one NMC tx-class with no BTC/DOGE analog: the name operations +// (name_new / name_firstupdate / name_update). c2pool is a merge-miner, not a +// name registry, so it RELAYS name txs into the block template WITHOUT name-db +// consensus validation (SPV-trust in the configured namecoind backend; namecoind +// owns name-db consensus). These KATs pin: +// * classify_name_op() — the version-marker + leading-opcode classifier, and +// its false-positive guard (a bare OP_1 segwit/taproot output in a plain +// Bitcoin-version tx is NOT a name op); +// * Mempool admission — a name tx is admitted (not rejected), counted, and +// carries its daemon-vs-peer-relay provenance (SPV-trust boundary); +// * fee accounting — a name tx's fee flows through get_sorted_txs_with_fees +// into the template coinbasevalue exactly like any other tx (Bitcoin +// subsidy schedule), i.e. name ops are NOT special-cased in payout. +// +// The §4 PF caveats (bind trust to daemon source; v36 short-circuit; aux-slot +// distinctness) are PF KAT items, not exercised here. +// +// Per-coin isolation: src/impl/nmc/ only; btc tree consumed READ-ONLY. MUST +// appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist or it +// becomes a NOT_BUILT sentinel that reds master. +// --------------------------------------------------------------------------- + +#include + +#include +#include +#include +#include + +#include +#include + +#include "../coin/header_chain.hpp" +#include "../coin/mempool.hpp" +#include "../coin/template_builder.hpp" + +namespace { + +using nmc::coin::Mempool; +using nmc::coin::MempoolEntry; +using nmc::coin::MutableTransaction; +using nmc::coin::TxIn; +using nmc::coin::TxOut; +using nmc::coin::NameOp; +using nmc::coin::TxSource; +using nmc::coin::classify_name_op; +using nmc::coin::compute_txid; +using nmc::coin::NAMECOIN_TX_VERSION; +using nmc::coin::HeaderChain; +using nmc::coin::NMCChainParams; +using nmc::coin::BlockHeaderType; +using nmc::coin::TemplateBuilder; +using nmc::coin::block_hash; +using nmc::coin::get_block_subsidy; + +// Full nVersion of a Namecoin name tx: marker in the high 16 bits, base +// version 2 in the low 16 (namecoin-core getNamecoinVersion()/getBaseVersion()). +static constexpr int32_t NMC_NAME_VERSION = + (NAMECOIN_TX_VERSION << 16) | 2; + +// Build a tx with a unique input (so txids differ by `salt`). When +// `name_prefix` is one of the name opcodes, the first output is a structurally +// plausible name script ( OP_2DROP ); when 0x00, the output +// is a plain P2PKH. `extra_first_byte`, if non-zero, prepends a raw leading byte +// to the first output script (used to forge a taproot-shaped output). +static MutableTransaction make_tx(int32_t version, unsigned char name_prefix, + uint32_t salt) +{ + MutableTransaction tx; + tx.version = version; + tx.locktime = 0; + + TxIn in; + in.prevout.hash.SetNull(); + *(in.prevout.hash.begin()) = static_cast(salt & 0xff); + *(in.prevout.hash.begin() + 1) = static_cast((salt >> 8) & 0xff); + in.prevout.index = 0; + in.sequence = 0xffffffff; + tx.vin.push_back(in); + + TxOut out; + out.value = 1000000; // 0.01 NMC + auto& d = out.scriptPubKey.m_data; + if (name_prefix != 0x00) { + d.push_back(name_prefix); // OP_NAME_* + d.push_back(0x14); // push 20 + for (int i = 0; i < 20; ++i) d.push_back(0xab); + d.push_back(0x6d); // OP_2DROP + } + // address part: P2PKH OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG + d.push_back(0x76); d.push_back(0xa9); d.push_back(0x14); + for (int i = 0; i < 20; ++i) d.push_back(0xcd); + d.push_back(0x88); d.push_back(0xac); + tx.vout.push_back(out); + return tx; +} + +static BlockHeaderType plain_header(const uint256& prev, uint32_t bits, + uint32_t nonce, uint32_t ts) +{ + BlockHeaderType h{}; + h.m_version = 1; + h.m_previous_block = prev; + h.m_bits = bits; + h.m_nonce = nonce; + h.m_timestamp = ts; + return h; +} + +static NMCChainParams params_pinned() +{ + NMCChainParams p = NMCChainParams::mainnet(); + p.aux_chain_id = 1; + p.auxpow_activation_height = 19200; // TEST-only pin + return p; +} + +// ── classify_name_op ──────────────────────────────────────────────────────── + +TEST(NmcNameClassify, NameNewFirstUpdateUpdate) +{ + EXPECT_EQ(classify_name_op(make_tx(NMC_NAME_VERSION, 0x51, 1)), NameOp::New); + EXPECT_EQ(classify_name_op(make_tx(NMC_NAME_VERSION, 0x52, 2)), NameOp::FirstUpdate); + EXPECT_EQ(classify_name_op(make_tx(NMC_NAME_VERSION, 0x53, 3)), NameOp::Update); +} + +TEST(NmcNameClassify, PlainTxIsNotName) +{ + // Bitcoin-version tx with a plain P2PKH output -> not a name op. + EXPECT_EQ(classify_name_op(make_tx(2, 0x00, 4)), NameOp::None); +} + +TEST(NmcNameClassify, NameOpcodeButWrongVersionIsNotName) +{ + // Output starts with OP_NAME_NEW(0x51) but the tx carries a plain Bitcoin + // version -> the version gate rejects it (no false-positive). This is the + // taproot/segwit-v1/bare-multisig case: those start with OP_1..OP_16 too. + EXPECT_EQ(classify_name_op(make_tx(2, 0x51, 5)), NameOp::None); +} + +TEST(NmcNameClassify, NameVersionButNoNameScriptIsNotName) +{ + // Name version marker present, but no output begins with a name opcode. + EXPECT_EQ(classify_name_op(make_tx(NMC_NAME_VERSION, 0x00, 6)), NameOp::None); +} + +// ── Mempool admission (relay without name-db validation) ───────────────────── + +TEST(NmcNameRelay, NameTxIsAdmittedCountedAndTagged) +{ + Mempool pool; + auto tx = make_tx(NMC_NAME_VERSION, 0x52, 10); // name_firstupdate + + // Relayed (peer) name tx: admitted but NOT daemon-trusted. + ASSERT_TRUE(pool.add_tx(tx, nullptr, TxSource::PeerRelay)); + EXPECT_EQ(pool.size(), 1u); + EXPECT_EQ(pool.name_op_count(), 1u); + EXPECT_EQ(pool.trusted_name_op_count(), 0u); + + auto entry = pool.get_entry(compute_txid(tx)); + ASSERT_TRUE(entry.has_value()); + EXPECT_TRUE(entry->is_name()); + EXPECT_EQ(entry->name_op, NameOp::FirstUpdate); + EXPECT_EQ(entry->source, TxSource::PeerRelay); +} + +TEST(NmcNameRelay, DaemonSourcedNameTxIsTrusted) +{ + Mempool pool; + auto tx = make_tx(NMC_NAME_VERSION, 0x53, 11); // name_update from daemon + ASSERT_TRUE(pool.add_tx(tx, nullptr, TxSource::Daemon)); + EXPECT_EQ(pool.name_op_count(), 1u); + EXPECT_EQ(pool.trusted_name_op_count(), 1u); +} + +TEST(NmcNameRelay, PlainTxLeavesNameCountsZero) +{ + Mempool pool; + ASSERT_TRUE(pool.add_tx(make_tx(2, 0x00, 12), nullptr, TxSource::PeerRelay)); + EXPECT_EQ(pool.size(), 1u); + EXPECT_EQ(pool.name_op_count(), 0u); +} + +// ── Fee accounting: name-tx fee flows into the template coinbasevalue ───────── + +TEST(NmcNameFee, NameTxFeeIsSelectedIntoTotalFees) +{ + Mempool pool; + auto tx = make_tx(NMC_NAME_VERSION, 0x51, 20); // name_new + ASSERT_TRUE(pool.add_tx(tx, nullptr, TxSource::Daemon)); + + // No UTXO set in this harness; pin the fee directly (as the template-builder + // suite does). 5000 sat fee. + pool.set_tx_fee(compute_txid(tx), 5000); + + auto [selected, total_fees] = pool.get_sorted_txs_with_fees(4'000'000); + EXPECT_EQ(selected.size(), 1u); + EXPECT_EQ(total_fees, 5000u); + EXPECT_EQ(pool.total_fees(), 5000u); +} + +TEST(NmcNameFee, NameTxFeeReachesTemplateCoinbaseValue) +{ + HeaderChain chain(params_pinned()); + Mempool pool; + + // Seed a fresh two-header chain (tip height 1 -> next block height 2). + auto now = static_cast(std::time(nullptr)); + uint256 z; z.SetNull(); + BlockHeaderType g = plain_header(z, 0x1d00ffffu, 1, now - 100); + ASSERT_TRUE(chain.add_header(g)); + BlockHeaderType c = plain_header(block_hash(g), 0x1d00ffffu, 2, now - 50); + ASSERT_TRUE(chain.add_header(c)); + ASSERT_EQ(chain.height(), 1u); + + // Admit a daemon-sourced name_update with a known fee. + auto tx = make_tx(NMC_NAME_VERSION, 0x53, 21); + ASSERT_TRUE(pool.add_tx(tx, nullptr, TxSource::Daemon)); + pool.set_tx_fee(compute_txid(tx), 7000); + + auto wd = TemplateBuilder::build_template(chain, pool, /*is_testnet=*/false); + ASSERT_TRUE(wd.has_value()); + const auto& d = wd->m_data; + + // coinbasevalue = subsidy(2) + the name tx fee. Name ops are NOT special- + // cased: their fee is counted exactly like any other tx. + EXPECT_EQ(d.at("coinbasevalue").get(), + static_cast(get_block_subsidy(2u)) + 7000); + EXPECT_EQ(d.at("transactions").size(), 1u); +} + +} // namespace