From 43aa5abe802be62abb1ae650242623bd5ab3f2ad Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 3 Oct 2026 04:25:13 -0400 Subject: [PATCH 1/4] Pop estimator fees of the reorganized block. --- include/bitcoin/node/estimator.hpp | 2 +- src/chasers/chaser_estimate.cpp | 2 +- src/estimator.cpp | 4 ++-- test/estimator.cpp | 25 ++++++++++++++++++++++++- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/include/bitcoin/node/estimator.hpp b/include/bitcoin/node/estimator.hpp index a24919a4..19116dac 100644 --- a/include/bitcoin/node/estimator.hpp +++ b/include/bitcoin/node/estimator.hpp @@ -64,8 +64,8 @@ class BCN_API estimator /// Update accumulator (not thread safe). bool push(const query& query) NOEXCEPT; - bool pop(const query& query) NOEXCEPT; bool pool(const query& query, const database::tx_link& link) NOEXCEPT; + bool pop(const query& query, const database::header_link& link) NOEXCEPT; /// Top height of accumulator (thread safe). size_t top_height() const NOEXCEPT; diff --git a/src/chasers/chaser_estimate.cpp b/src/chasers/chaser_estimate.cpp index eb67edc4..321d29d0 100644 --- a/src/chasers/chaser_estimate.cpp +++ b/src/chasers/chaser_estimate.cpp @@ -256,7 +256,7 @@ void chaser_estimate::do_reorganized(header_t link) NOEXCEPT if (height.value > estimator_->top_height()) return; - if (!estimator_->pop(query)) + if (!estimator_->pop(query, link)) fault(error::estimates_pop2); } } diff --git a/src/estimator.cpp b/src/estimator.cpp index 6ec49086..5a2ffe0b 100644 --- a/src/estimator.cpp +++ b/src/estimator.cpp @@ -104,13 +104,13 @@ bool estimator::push(const query& query) NOEXCEPT return query.get_block_fees(block, link) && push(block); } -bool estimator::pop(const query& query) NOEXCEPT +bool estimator::pop(const query& query, + const database::header_link& link) NOEXCEPT { if (is_subtract_overflow(top_height(), one)) return false; rates block{}; - const auto link = query.to_confirmed(sub1(top_height())); return query.get_block_fees(block, link) && pop(block); } diff --git a/test/estimator.cpp b/test/estimator.cpp index 252006a0..239793c7 100644 --- a/test/estimator.cpp +++ b/test/estimator.cpp @@ -809,7 +809,7 @@ BOOST_FIXTURE_TEST_CASE(estimator__push__query_maximum_top_height__false, estima BOOST_FIXTURE_TEST_CASE(estimator__pop__query_zero_top_height__false, estimator_query_setup_fixture) { const auto instance = acessor::create(); - BOOST_REQUIRE(!instance->pop(query_)); + BOOST_REQUIRE(!instance->pop(query_, query_.to_confirmed(0))); BOOST_REQUIRE_EQUAL(instance->top_height(), 0u); } @@ -946,4 +946,27 @@ BOOST_FIXTURE_TEST_CASE(estimator__pool__query_unknown_link__false, estimator_qu BOOST_REQUIRE(!instance->pool(query_, 42)); } +BOOST_FIXTURE_TEST_CASE(estimator__pop__query_top_link__removes_top_block_fees, estimator_query_setup_fixture) +{ + constexpr uint64_t value = 5'000'000'000; + const system::settings bitcoin{ chain::selection::mainnet }; + const auto& genesis = bitcoin.genesis_block; + const auto coinbase1 = estimator_coinbase(1, value); + const auto spend = estimator_spend(coinbase1, 10'000u); + const auto block1 = estimator_block(genesis, 1, { coinbase1 }); + const auto block2 = estimator_block(block1, 2, { estimator_coinbase(2, value), spend }); + BOOST_REQUIRE(estimator_confirm(query_, block1, 1)); + BOOST_REQUIRE(estimator_confirm(query_, block2, 2)); + BOOST_REQUIRE(estimator_pool(store_, query_, spend, 1, 10'000u)); + + const auto instance = acessor::create(); + BOOST_REQUIRE(instance->initialize(cancel_, query_, 3)); + BOOST_REQUIRE_EQUAL(instance->top_height(), 2u); + BOOST_REQUIRE_GT(estimator_small_total(*instance), 0u); + + BOOST_REQUIRE(instance->pop(query_, query_.to_header(block2.hash()))); + BOOST_REQUIRE_EQUAL(instance->top_height(), 1u); + BOOST_REQUIRE_EQUAL(estimator_small_total(*instance), 0u); +} + BOOST_AUTO_TEST_SUITE_END() From cb8154a96a942a033e21105d9972240a1de110a5 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 10 Oct 2026 22:09:43 -0400 Subject: [PATCH 2/4] Unpend block conflicts in the fee estimator. --- include/bitcoin/node/estimator.hpp | 1 + src/estimator.cpp | 31 +++++++++-- .../protocol_transaction_out_60002.cpp | 4 +- test/estimator.cpp | 52 +++++++++++++++++++ 4 files changed, 81 insertions(+), 7 deletions(-) diff --git a/include/bitcoin/node/estimator.hpp b/include/bitcoin/node/estimator.hpp index 19116dac..f0fdaec2 100644 --- a/include/bitcoin/node/estimator.hpp +++ b/include/bitcoin/node/estimator.hpp @@ -167,6 +167,7 @@ class BCN_API estimator bool push(const rates& block) NOEXCEPT; bool pop(const rates& block) NOEXCEPT; bool pool(const rate& tx) NOEXCEPT; + void conflict(const rates& txs, bool push) NOEXCEPT; uint64_t compute(size_t target, double confidence, bool geometric=false) const NOEXCEPT; diff --git a/src/estimator.cpp b/src/estimator.cpp index 5a2ffe0b..191853a6 100644 --- a/src/estimator.cpp +++ b/src/estimator.cpp @@ -91,7 +91,7 @@ bool estimator::initialize(const std::atomic_bool& cancel, const query& query, rate_sets blocks{}; const auto start = top - sub1(count); return query.get_branch_fees(cancel, blocks, start, count) && - query.get_pool_fees(pooled) && initialize(blocks, pooled, top); + query.get_confirmable_fees(pooled) && initialize(blocks, pooled, top); } bool estimator::push(const query& query) NOEXCEPT @@ -99,9 +99,14 @@ bool estimator::push(const query& query) NOEXCEPT if (is_add_overflow(top_height(), one)) return false; - rates block{}; + rates block{}, conflicts{}; const auto link = query.to_confirmed(add1(top_height())); - return query.get_block_fees(block, link) && push(block); + if (!query.get_block_fees(block, link) || + !query.get_block_conflict_fees(conflicts, link) || !push(block)) + return false; + + conflict(conflicts, true); + return true; } bool estimator::pop(const query& query, @@ -110,8 +115,13 @@ bool estimator::pop(const query& query, if (is_subtract_overflow(top_height(), one)) return false; - rates block{}; - return query.get_block_fees(block, link) && pop(block); + rates block{}, conflicts{}; + if (!query.get_block_fees(block, link) || + !query.get_block_conflict_fees(conflicts, link)) + return false; + + conflict(conflicts, false); + return pop(block); } bool estimator::pool(const query& query, const database::tx_link& link) NOEXCEPT @@ -213,6 +223,17 @@ bool estimator::pool(const rate& tx) NOEXCEPT return true; } +// Txs conflicted by a block leave pending when it is pushed, reenter on pop. +void estimator::conflict(const rates& txs, bool push) NOEXCEPT +{ + for (const auto& tx: txs) + { + size_t bin{}; + if (tx.height != max_size_t && to_bin(bin, tx)) + pend(bin, tx.height, !push); + } +} + uint64_t estimator::compute(size_t target, double confidence, bool geometric) const NOEXCEPT { diff --git a/src/protocols/protocol_transaction_out_60002.cpp b/src/protocols/protocol_transaction_out_60002.cpp index 2ac4afa9..35f08b08 100644 --- a/src/protocols/protocol_transaction_out_60002.cpp +++ b/src/protocols/protocol_transaction_out_60002.cpp @@ -78,7 +78,6 @@ bool protocol_transaction_out_60002::handle_receive_memory_pool( // Outbound (inv). // ---------------------------------------------------------------------------- -// Each inventory is sent upon completion of the previous. void protocol_transaction_out_60002::send_memory_pool(const code& ec, const database::pool_link& cursor, const database::pool_link& end, const gate_t::ptr& gate) NOEXCEPT @@ -94,7 +93,8 @@ void protocol_transaction_out_60002::send_memory_pool(const code& ec, while (items.size() < max_inventory && next != end) { const auto limit = max_inventory - items.size(); - if (const auto result = query.get_pooled_txs(next, links, end, limit)) + const auto result = query.get_confirmable_txs(next, links, end, limit); + if (result) { stop(result); return; diff --git a/test/estimator.cpp b/test/estimator.cpp index 239793c7..9069eeb5 100644 --- a/test/estimator.cpp +++ b/test/estimator.cpp @@ -47,6 +47,7 @@ struct acessor using estimator::push; using estimator::pop; using estimator::pool; + using estimator::conflict; using estimator::compute; }; @@ -505,6 +506,57 @@ BOOST_AUTO_TEST_CASE(estimator__pop__reverses_push__restores_buckets_and_pending BOOST_REQUIRE_EQUAL(large0.confirmed[1007], 0u); } +// conflict + +BOOST_AUTO_TEST_CASE(estimator__conflict__push__leaves_pending) +{ + const auto instance = acessor::create(); + instance->history().top_height = 4u; + BOOST_REQUIRE(instance->pool({ bin0_bytes, bin0_fee, 2u })); + BOOST_REQUIRE(instance->pool({ bin0_bytes, bin0_fee, 2u })); + + instance->conflict({ { bin0_bytes, bin0_fee, 2u } }, true); + BOOST_REQUIRE_EQUAL(instance->history().pending.at(2).at(0), 1u); + BOOST_REQUIRE_EQUAL(instance->history().small.at(0).total, 0u); +} + +BOOST_AUTO_TEST_CASE(estimator__conflict__pop__reenters_pending) +{ + const auto instance = acessor::create(); + instance->history().top_height = 4u; + BOOST_REQUIRE(instance->pool({ bin0_bytes, bin0_fee, 2u })); + + const acessor::rates conflicts{ { bin0_bytes, bin0_fee, 2u } }; + instance->conflict(conflicts, true); + BOOST_REQUIRE_EQUAL(instance->history().pending.at(2).at(0), 0u); + + instance->conflict(conflicts, false); + BOOST_REQUIRE_EQUAL(instance->history().pending.at(2).at(0), 1u); +} + +BOOST_AUTO_TEST_CASE(estimator__conflict__aged__leaves_aged) +{ + const auto instance = acessor::create(); + constexpr auto large = acessor::horizon::large; + instance->history().top_height = large; + BOOST_REQUIRE(instance->pool({ bin0_bytes, bin0_fee, 1u })); + BOOST_REQUIRE(instance->pool({ bin0_bytes, bin0_fee, add1(large) })); + BOOST_REQUIRE_EQUAL(instance->history().aged.at(0), 1u); + + instance->conflict({ { bin0_bytes, bin0_fee, 1u } }, true); + BOOST_REQUIRE_EQUAL(instance->history().aged.at(0), 0u); + BOOST_REQUIRE_EQUAL(instance->history().pending.at(1).at(0), 1u); +} + +BOOST_AUTO_TEST_CASE(estimator__conflict__unpooled__not_pending) +{ + const auto instance = acessor::create(); + BOOST_REQUIRE(instance->pool({ bin0_bytes, bin0_fee, 2u })); + + instance->conflict({ { bin0_bytes, bin0_fee, max_size_t } }, true); + BOOST_REQUIRE_EQUAL(instance->history().pending.at(2).at(0), 1u); +} + // compute BOOST_AUTO_TEST_CASE(estimator__compute__default_state__max_uint64) From 7b8ef9261dab37a789e5fc35fac971430cf7b7b9 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 10 Oct 2026 17:59:22 -0400 Subject: [PATCH 3/4] Push estimator fees of the organized block. --- include/bitcoin/node/estimator.hpp | 2 +- src/chasers/chaser_estimate.cpp | 2 +- src/estimator.cpp | 4 ++-- test/estimator.cpp | 27 ++++++++++++++++++++++++++- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/include/bitcoin/node/estimator.hpp b/include/bitcoin/node/estimator.hpp index f0fdaec2..84781a93 100644 --- a/include/bitcoin/node/estimator.hpp +++ b/include/bitcoin/node/estimator.hpp @@ -63,7 +63,7 @@ class BCN_API estimator size_t count=maximum_horizon) NOEXCEPT; /// Update accumulator (not thread safe). - bool push(const query& query) NOEXCEPT; + bool push(const query& query, const database::header_link& link) NOEXCEPT; bool pool(const query& query, const database::tx_link& link) NOEXCEPT; bool pop(const query& query, const database::header_link& link) NOEXCEPT; diff --git a/src/chasers/chaser_estimate.cpp b/src/chasers/chaser_estimate.cpp index 321d29d0..e0cdb0a9 100644 --- a/src/chasers/chaser_estimate.cpp +++ b/src/chasers/chaser_estimate.cpp @@ -232,7 +232,7 @@ void chaser_estimate::do_organized(header_t link) NOEXCEPT if (height.value <= estimator_->top_height()) return; - if (!estimator_->push(query)) + if (!estimator_->push(query, link)) fault(error::estimates_push2); } } diff --git a/src/estimator.cpp b/src/estimator.cpp index 191853a6..20c7ea95 100644 --- a/src/estimator.cpp +++ b/src/estimator.cpp @@ -94,13 +94,13 @@ bool estimator::initialize(const std::atomic_bool& cancel, const query& query, query.get_confirmable_fees(pooled) && initialize(blocks, pooled, top); } -bool estimator::push(const query& query) NOEXCEPT +bool estimator::push(const query& query, + const database::header_link& link) NOEXCEPT { if (is_add_overflow(top_height(), one)) return false; rates block{}, conflicts{}; - const auto link = query.to_confirmed(add1(top_height())); if (!query.get_block_fees(block, link) || !query.get_block_conflict_fees(conflicts, link) || !push(block)) return false; diff --git a/test/estimator.cpp b/test/estimator.cpp index 9069eeb5..f0a2209c 100644 --- a/test/estimator.cpp +++ b/test/estimator.cpp @@ -854,7 +854,7 @@ BOOST_FIXTURE_TEST_CASE(estimator__push__query_maximum_top_height__false, estima { const auto instance = acessor::create(); instance->history().top_height = max_size_t; - BOOST_REQUIRE(!instance->push(query_)); + BOOST_REQUIRE(!instance->push(query_, query_.to_confirmed(0))); BOOST_REQUIRE_EQUAL(instance->top_height(), max_size_t); } @@ -1021,4 +1021,29 @@ BOOST_FIXTURE_TEST_CASE(estimator__pop__query_top_link__removes_top_block_fees, BOOST_REQUIRE_EQUAL(estimator_small_total(*instance), 0u); } +BOOST_FIXTURE_TEST_CASE(estimator__push__query_top_link__adds_top_block_fees, estimator_query_setup_fixture) +{ + constexpr uint64_t value = 5'000'000'000; + const system::settings bitcoin{ chain::selection::mainnet }; + const auto& genesis = bitcoin.genesis_block; + const auto coinbase1 = estimator_coinbase(1, value); + const auto spend = estimator_spend(coinbase1, 10'000u); + const auto block1 = estimator_block(genesis, 1, { coinbase1 }); + const auto block2 = estimator_block(block1, 2, { estimator_coinbase(2, value), spend }); + BOOST_REQUIRE(estimator_confirm(query_, block1, 1)); + BOOST_REQUIRE(query_.set(spend)); + BOOST_REQUIRE(estimator_pool(store_, query_, spend, 1, 10'000u)); + + const auto instance = acessor::create(); + BOOST_REQUIRE(instance->initialize(cancel_, query_, 2)); + BOOST_REQUIRE_EQUAL(instance->top_height(), 1u); + BOOST_REQUIRE_EQUAL(estimator_small_total(*instance), 0u); + + store_.set_pooling(); + BOOST_REQUIRE(estimator_confirm(query_, block2, 2)); + BOOST_REQUIRE(instance->push(query_, query_.to_header(block2.hash()))); + BOOST_REQUIRE_EQUAL(instance->top_height(), 2u); + BOOST_REQUIRE_GT(estimator_small_total(*instance), 0u); +} + BOOST_AUTO_TEST_SUITE_END() From 93a33bc8a416e841ba9c410b706843afb28b02c6 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 10 Oct 2026 22:02:34 -0400 Subject: [PATCH 4/4] Pool the unpooled txs of a reorganized block. --- .../node/chasers/chaser_transaction.hpp | 3 ++ include/bitcoin/node/error.hpp | 1 + src/chasers/chaser_transaction.cpp | 49 ++++++++++++++++++- src/error.cpp | 1 + test/chasers/chaser_transaction.cpp | 41 ++++++++++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) diff --git a/include/bitcoin/node/chasers/chaser_transaction.hpp b/include/bitcoin/node/chasers/chaser_transaction.hpp index 549f81cf..e65d7fb3 100644 --- a/include/bitcoin/node/chasers/chaser_transaction.hpp +++ b/include/bitcoin/node/chasers/chaser_transaction.hpp @@ -62,6 +62,9 @@ class BCN_API chaser_transaction /// Recompute the pool context, closing the pool if not current. virtual void do_bump() NOEXCEPT; + /// Pool the unpooled txs of a reorganized block. + virtual void do_reorganized(header_t link) NOEXCEPT; + private: static size_t to_rate(const system::chain::transaction& tx) NOEXCEPT; diff --git a/include/bitcoin/node/error.hpp b/include/bitcoin/node/error.hpp index 8130e509..5ab41158 100644 --- a/include/bitcoin/node/error.hpp +++ b/include/bitcoin/node/error.hpp @@ -121,6 +121,7 @@ enum error_t : uint8_t transaction4, transaction5, transaction6, + transaction7, estimates_initialize, estimates_push1, estimates_push2, diff --git a/src/chasers/chaser_transaction.cpp b/src/chasers/chaser_transaction.cpp index 096b5587..f9e6e708 100644 --- a/src/chasers/chaser_transaction.cpp +++ b/src/chasers/chaser_transaction.cpp @@ -60,11 +60,15 @@ bool chaser_transaction::handle_chase(const code&, event_value value) NOEXCEPT switch (to_chase(value)) { case chase::organized: - case chase::reorganized: { POST(do_bump); break; } + case chase::reorganized: + { + POST(do_reorganized, to_payload(value).link); + break; + } case chase::stop: { return false; @@ -107,6 +111,49 @@ void chaser_transaction::do_bump() NOEXCEPT pooling_ = true; } +// Pooled in block order under the block context, excluding any confirmed (by +// another block). +void chaser_transaction::do_reorganized(header_t link) NOEXCEPT +{ + BC_ASSERT(stranded()); + do_bump(); + if (!pooling_) + return; + + auto& query = archive(); + chain::context ctx{}; + const auto txs = query.to_transactions(link); + if (txs.empty() || !query.get_context(ctx, link)) + { + fault(error::transaction7); + return; + } + + // The coinbase is never pooled. + for (auto it = std::next(txs.cbegin()); it != txs.cend(); ++it) + { + const database::tx_link fk{ *it }; + if (query.is_pooled(fk) || query.is_confirmed_tx(fk)) + continue; + + const auto tx = query.get_transaction(fk, true); + if (!tx || !query.populate_with_metadata(*tx)) + { + fault(error::transaction7); + return; + } + + // Block txs are already silent indexed. + if (!query.set_pooled(fk, *tx, ctx)) + { + fault(error::transaction2); + return; + } + + notify(error::success, chases::transaction{ fk }); + } +} + // methods // ---------------------------------------------------------------------------- diff --git a/src/error.cpp b/src/error.cpp index 661251e2..1b0508c3 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -111,6 +111,7 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { transaction4, "transaction4" }, { transaction5, "transaction5" }, { transaction6, "transaction6" }, + { transaction7, "transaction7" }, { estimates_initialize, "estimates_initialize" }, { estimates_push1, "estimates_push1" }, { estimates_push2, "estimates_push2" }, diff --git a/test/chasers/chaser_transaction.cpp b/test/chasers/chaser_transaction.cpp index 559e7505..c4e7d495 100644 --- a/test/chasers/chaser_transaction.cpp +++ b/test/chasers/chaser_transaction.cpp @@ -188,6 +188,21 @@ struct chaser_transaction_setup_fixture return false; } + bool pooled(const database::tx_link& link) + { + using namespace std::chrono; + const auto deadline = steady_clock::now() + seconds(10); + while (steady_clock::now() < deadline) + { + if (query_.is_pooled(link)) + return true; + + std::this_thread::sleep_for(milliseconds(10)); + } + + return false; + } + // True if the command is received before the pong of a subsequent ping. bool received_before_pong(const std::string& command) { @@ -510,6 +525,32 @@ BOOST_FIXTURE_TEST_CASE(chaser_transaction__organized__invalid_confirmed_top__su BOOST_REQUIRE_EQUAL(submit(package({ spend(parent_value) })).first, node::error::pooling_disabled); } +// do_reorganized + +BOOST_FIXTURE_TEST_CASE(chaser_transaction__reorganized__unpooled_block_tx__pooled, chaser_transaction_pooling_setup_fixture) +{ + const system::settings bitcoin{ chain::selection::mainnet }; + const auto& genesis = bitcoin.genesis_block; + const chain::input null{ chain::point{}, chain::script{}, max_uint32 }; + const chain::transaction coinbase{ 1u, chain::inputs{ null }, chain::outputs{ { 42u, chain::script{} } }, 0u }; + const auto tx = spend(parent_value); + const chain::header header{ 1u, genesis.hash(), null_hash, genesis.header().timestamp() + 1u, genesis.header().bits(), 0u }; + const chain::block block{ header, { coinbase, *tx } }; + BOOST_REQUIRE(query_.set(block, database::context{ 0, 1, 0 }, {}, false, true)); + + const auto link = query_.to_header(block.hash()); + BOOST_REQUIRE(query_.push_candidate(link)); + BOOST_REQUIRE(query_.push_confirmed(link, true)); + BOOST_REQUIRE(query_.pop_confirmed()); + + const auto stored = query_.to_tx(tx->hash(false)); + BOOST_REQUIRE(!query_.is_pooled(stored)); + + node_.notify(node::error::success, chases::reorganized{ link }); + BOOST_REQUIRE(pooled(stored)); + BOOST_REQUIRE(!query_.is_pooled(query_.to_tx(coinbase.hash(false)))); +} + // submit_compact BOOST_FIXTURE_TEST_CASE(chaser_transaction__submit_compact__invalid_tx_link__integrity_suspended, chaser_transaction_header_setup_fixture)