From dc83f91c8e4044aa75efac31ab8009f6ae4d7721 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 25 Sep 2026 20:22:45 -0400 Subject: [PATCH 1/4] Skip stored package txs, reject forward references, rate new txs only. --- .../node/chasers/chaser_transaction.hpp | 4 +- include/bitcoin/node/error.hpp | 1 + src/chasers/chaser_transaction.cpp | 62 +++++++++++-------- src/error.cpp | 1 + src/protocols/protocol_transaction_in_106.cpp | 4 +- 5 files changed, 43 insertions(+), 29 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_transaction.hpp b/include/bitcoin/node/chasers/chaser_transaction.hpp index d9d0ab80..c29ffdfa 100644 --- a/include/bitcoin/node/chasers/chaser_transaction.hpp +++ b/include/bitcoin/node/chasers/chaser_transaction.hpp @@ -55,8 +55,8 @@ class BCN_API chaser_transaction private: static size_t to_rate(const system::chain::transaction& tx) NOEXCEPT; - code validate(size_t& index, - const system::chain::transaction_cptrs& txs) NOEXCEPT; + code validate(size_t& index, const system::chain::transaction_cptrs& txs, + const std::vector& stored) NOEXCEPT; code validate(const system::chain::transaction& tx) NOEXCEPT; // These are protected by strand. diff --git a/include/bitcoin/node/error.hpp b/include/bitcoin/node/error.hpp index b96c62d5..8c90f0b5 100644 --- a/include/bitcoin/node/error.hpp +++ b/include/bitcoin/node/error.hpp @@ -64,6 +64,7 @@ enum error_t : uint8_t pooling_disabled, insufficient_fee, empty_package, + duplicate_transaction, excessive_backlog, /// fee estimation diff --git a/src/chasers/chaser_transaction.cpp b/src/chasers/chaser_transaction.cpp index 2a179595..d055e601 100644 --- a/src/chasers/chaser_transaction.cpp +++ b/src/chasers/chaser_transaction.cpp @@ -137,8 +137,15 @@ void chaser_transaction::do_submit(const transactions_cptr& txs, bool test, return; } + auto& query = archive(); + std::vector stored(txs->size()); + std::ranges::transform(*txs, stored.begin(), [&](const auto& tx) NOEXCEPT + { + return !tx->is_coinbase() && query.is_tx(tx->get_hash(false)); + }); + size_t index{}; - if (const auto ec = validate(index, *txs)) + if (const auto ec = validate(index, *txs, stored)) { handler(ec, index); return; @@ -150,10 +157,12 @@ void chaser_transaction::do_submit(const transactions_cptr& txs, bool test, return; } - auto& query = archive(); - database::tx_links fresh(txs->size(), database::tx_link::terminal); + // Parents precede children, so a parent is archived before it is resolved. for (index = zero; index < txs->size(); ++index) { + if (stored.at(index)) + continue; + bool pooled{}; database::tx_link link{}; const auto& tx = *txs->at(index); @@ -165,23 +174,14 @@ void chaser_transaction::do_submit(const transactions_cptr& txs, bool test, return; } - if (!pooled) - fresh.at(index) = link; - - fire(events::tx_archived, to_rate(tx)); - notify(error::success, chases::transaction{ link }); - } - - // Package parents are resolved by hash, so the whole package precedes. - for (index = zero; index < txs->size(); ++index) - { - const database::tx_link link{ fresh.at(index) }; - if (!link.is_terminal() && !query.set_pooled(link, - *txs->at(index), pool_)) + if (!pooled && !query.set_pooled(link, tx, pool_)) { handler(fault(error::transaction2), index); return; } + + fire(events::tx_archived, to_rate(tx)); + notify(error::success, chases::transaction{ link }); } handler(error::success, {}); @@ -206,31 +206,41 @@ size_t chaser_transaction::to_rate(const chain::transaction& tx) NOEXCEPT // validation // ---------------------------------------------------------------------------- -code chaser_transaction::validate(size_t& index, - const transaction_cptrs& txs) NOEXCEPT +code chaser_transaction::validate(size_t& index, const transaction_cptrs& txs, + const std::vector& stored) NOEXCEPT { if (txs.empty()) return error::empty_package; + if (std::ranges::all_of(stored, std::identity{})) + return error::duplicate_transaction; + + if (block::is_forward_reference(txs, false)) + return system::error::forward_reference; + if (block::is_internal_double_spend(txs, false)) return system::error::block_internal_double_spend; if (const auto ec = block::populate(txs, pool_, false)) return ec; - for(const auto& tx: txs) - if (const auto ec = validate(*tx)) - return ec; - - uint64_t fee{}, size{}; - for (const auto& tx: txs) + for (index = zero; index < txs.size(); ++index) { - fee = ceilinged_add(fee, tx->fee()); + if (stored.at(index)) + continue; + + const auto& tx = *txs.at(index); + if (const auto ec = validate(tx)) + return ec; + + fee = ceilinged_add(fee, tx.fee()); size = ceilinged_add(size, possible_wide_cast( - tx->virtual_size())); + tx.virtual_size())); } + index = zero; + // Compared in satoshis per virtual kilobyte. const auto rate = node_settings().minimum_fee_rate_(); return ceilinged_multiply(fee, 1'000_u64) < ceilinged_multiply(rate, size) ? diff --git a/src/error.cpp b/src/error.cpp index eefe49b7..7f08a138 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -54,6 +54,7 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { pooling_disabled, "pooling disabled" }, { insufficient_fee, "insufficient fee" }, { empty_package, "empty package" }, + { duplicate_transaction, "duplicate transaction" }, { excessive_backlog, "excessive transaction backlog" }, // fee estimation diff --git a/src/protocols/protocol_transaction_in_106.cpp b/src/protocols/protocol_transaction_in_106.cpp index 3b331f70..901aab1f 100644 --- a/src/protocols/protocol_transaction_in_106.cpp +++ b/src/protocols/protocol_transaction_in_106.cpp @@ -171,7 +171,8 @@ void protocol_transaction_in_106::do_handle_submit(const code& ec, return; // A conflict with an unconfirmed tx is not misbehavior, as the peer - // cannot know what is held here. An unknown prevout is not misbehavior, + // cannot know what is held here, and a requested tx may be archived from + // another peer before it arrives. An unknown prevout is not misbehavior, // as announcement order is not constrained and orphans are not pooled. // A locked tx is final to a peer one block ahead, as locks are evaluated // against the presumed next block, so neither lock is misbehavior. @@ -180,6 +181,7 @@ void protocol_transaction_in_106::do_handle_submit(const code& ec, if (ec && (ec != error::pooling_disabled) && (ec != error::insufficient_fee) && + (ec != error::duplicate_transaction) && (ec != system::error::double_spend) && (ec != system::error::absolute_time_locked) && (ec != system::error::relative_time_locked) && From 32907653c6be2c4909270b3144b8437e7453d4cf Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 25 Sep 2026 20:22:51 -0400 Subject: [PATCH 2/4] Add validate_usecs block validation timing event. --- include/bitcoin/node/events.hpp | 1 + src/chasers/chaser_validate_parallel.cpp | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/include/bitcoin/node/events.hpp b/include/bitcoin/node/events.hpp index f3be8a53..53bcdd4d 100644 --- a/include/bitcoin/node/events.hpp +++ b/include/bitcoin/node/events.hpp @@ -65,6 +65,7 @@ enum events : uint8_t ecdsa_secs, // ecdsa batch verify timespan in seconds. schnorr_secs, // schnorr batch verify timespan in seconds. silent_secs, // silent payment scan timespan in seconds. + validate_usecs, // block validation timespan in microseconds. unknown }; diff --git a/src/chasers/chaser_validate_parallel.cpp b/src/chasers/chaser_validate_parallel.cpp index 279df056..42d73a38 100644 --- a/src/chasers/chaser_validate_parallel.cpp +++ b/src/chasers/chaser_validate_parallel.cpp @@ -26,6 +26,7 @@ namespace node { using namespace system; using namespace database; +using namespace std::chrono; // Parallel execution path (concurrent by block). // ---------------------------------------------------------------------------- @@ -40,6 +41,7 @@ void chaser_validate::validate_block(const header_link& link, chain::context ctx{}; bool batched{}, capturing{}; auto& query = archive(); + const auto start = network::logger::now(); // TODO: implement allocator parameter resulting in full allocation to // shared_ptr, to optimize deallocate (12% of milestone/filter). @@ -65,6 +67,13 @@ void chaser_validate::validate_block(const header_link& link, ec = error::validate5; } + if (!ec && !bypass) + { + const auto elapsed = network::logger::now() - start; + fire(events::validate_usecs, + duration_cast(elapsed).count()); + } + --validate_backlog_; complete_block(ec, link, ctx.height, bypass, batched, capturing); } From ae2fa3544682c4c41f12cb9aa8fd909b60a7825d Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 25 Sep 2026 20:23:21 -0400 Subject: [PATCH 3/4] Populate block tx metadata from the pool when sufficiently pooled. --- .../bitcoin/node/chasers/chaser_validate.hpp | 4 ++- src/chasers/chaser_validate_parallel.cpp | 32 +++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/include/bitcoin/node/chasers/chaser_validate.hpp b/include/bitcoin/node/chasers/chaser_validate.hpp index 786f98f2..b168a174 100644 --- a/include/bitcoin/node/chasers/chaser_validate.hpp +++ b/include/bitcoin/node/chasers/chaser_validate.hpp @@ -69,7 +69,9 @@ class BCN_API chaser_validate const system::chain::block& block, const header_link& link, const system::chain::context& ctx) NOEXCEPT; virtual code populate(bool bypass, const system::chain::block& block, - const system::chain::context& ctx) NOEXCEPT; + const header_link& link, const system::chain::context& ctx) NOEXCEPT; + virtual bool populate_metadata(const system::chain::block& block, + const header_link& link, const system::chain::context& ctx) NOEXCEPT; virtual void complete_block(const code& ec, const header_link& link, size_t height, bool bypass, bool batched=false, bool capturing=false) NOEXCEPT; diff --git a/src/chasers/chaser_validate_parallel.cpp b/src/chasers/chaser_validate_parallel.cpp index 42d73a38..34ad79fe 100644 --- a/src/chasers/chaser_validate_parallel.cpp +++ b/src/chasers/chaser_validate_parallel.cpp @@ -55,7 +55,7 @@ void chaser_validate::validate_block(const header_link& link, { ec = error::validate3; } - else if ((ec = populate(bypass, *block, ctx))) + else if ((ec = populate(bypass, *block, link, ctx))) { if (!query.set_block_unconfirmable(link)) ec = error::validate4; @@ -82,7 +82,7 @@ void chaser_validate::validate_block(const header_link& link, // ---------------------------------------------------------------------------- code chaser_validate::populate(bool bypass, const chain::block& block, - const chain::context& ctx) NOEXCEPT + const header_link& link, const chain::context& ctx) NOEXCEPT { const auto& query = archive(); @@ -101,13 +101,39 @@ code chaser_validate::populate(bool bypass, const chain::block& block, return ec; // Metadata identifies internal spends allowing confirmation bypass. - if (!query.populate_with_metadata(block)) + if (!populate_metadata(block, link, ctx)) return system::error::missing_previous_output; } return error::success; } +// A tx pooled under a sufficient context takes its metadata from the pool. +bool chaser_validate::populate_metadata(const chain::block& block, + const header_link& link, const chain::context& ctx) NOEXCEPT +{ + const auto& query = archive(); + const auto& txs = *block.transactions_ptr(); + const auto links = query.to_transactions(link); + if (links.size() != txs.size()) + return query.populate_with_metadata(block); + + pooled_tx pooled{}; + const auto pool = database::context::from(ctx); + for (auto index = one; index < txs.size(); ++index) + { + const auto& tx = *txs.at(index); + const auto populated = query.populate_pooled(pooled, tx, + links.at(index), pool) ? query.populate_with_metadata(tx) : + query.populate_without_metadata(tx); + + if (!populated) + return false; + } + + return true; +} + code chaser_validate::validate(bool& batched, bool& capturing, bool bypass, const chain::block& block, const header_link& link, const chain::context& ctx) NOEXCEPT From 5cd9885a6fdbfb126e87a90009912a8729ed5d1f Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 25 Sep 2026 21:00:21 -0400 Subject: [PATCH 4/4] Use std::transform for vector output (libc++ bit iterator). --- src/chasers/chaser_transaction.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/chasers/chaser_transaction.cpp b/src/chasers/chaser_transaction.cpp index d055e601..0de002e4 100644 --- a/src/chasers/chaser_transaction.cpp +++ b/src/chasers/chaser_transaction.cpp @@ -139,10 +139,11 @@ void chaser_transaction::do_submit(const transactions_cptr& txs, bool test, auto& query = archive(); std::vector stored(txs->size()); - std::ranges::transform(*txs, stored.begin(), [&](const auto& tx) NOEXCEPT - { - return !tx->is_coinbase() && query.is_tx(tx->get_hash(false)); - }); + std::transform(txs->cbegin(), txs->cend(), stored.begin(), + [&](const auto& tx) NOEXCEPT + { + return !tx->is_coinbase() && query.is_tx(tx->get_hash(false)); + }); size_t index{}; if (const auto ec = validate(index, *txs, stored))