diff --git a/Makefile.am b/Makefile.am
index d4bffa1b..8aba8035 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -39,6 +39,7 @@ src_libbitcoin_node_la_SOURCES = \
src/block_memory.cpp \
src/configuration.cpp \
src/error.cpp \
+ src/estimator.cpp \
src/full_node.cpp \
src/settings.cpp \
src/channels/channel_peer.cpp \
@@ -46,6 +47,7 @@ src_libbitcoin_node_la_SOURCES = \
src/chasers/chaser_block.cpp \
src/chasers/chaser_check.cpp \
src/chasers/chaser_confirm.cpp \
+ src/chasers/chaser_estimate.cpp \
src/chasers/chaser_header.cpp \
src/chasers/chaser_snapshot.cpp \
src/chasers/chaser_storage.cpp \
@@ -89,6 +91,7 @@ test_libbitcoin_node_test_SOURCES = \
test/channel_peer.cpp \
test/configuration.cpp \
test/error.cpp \
+ test/estimator.cpp \
test/full_node.cpp \
test/main.cpp \
test/settings.cpp \
@@ -98,6 +101,7 @@ test_libbitcoin_node_test_SOURCES = \
test/chasers/chaser_block.cpp \
test/chasers/chaser_check.cpp \
test/chasers/chaser_confirm.cpp \
+ test/chasers/chaser_estimate.cpp \
test/chasers/chaser_header.cpp \
test/chasers/chaser_template.cpp \
test/chasers/chaser_transaction.cpp \
@@ -121,6 +125,7 @@ include_bitcoin_node_HEADERS = \
include/bitcoin/node/configuration.hpp \
include/bitcoin/node/define.hpp \
include/bitcoin/node/error.hpp \
+ include/bitcoin/node/estimator.hpp \
include/bitcoin/node/events.hpp \
include/bitcoin/node/full_node.hpp \
include/bitcoin/node/settings.hpp \
@@ -138,6 +143,7 @@ include_bitcoin_node_chasers_HEADERS = \
include/bitcoin/node/chasers/chaser_block.hpp \
include/bitcoin/node/chasers/chaser_check.hpp \
include/bitcoin/node/chasers/chaser_confirm.hpp \
+ include/bitcoin/node/chasers/chaser_estimate.hpp \
include/bitcoin/node/chasers/chaser_header.hpp \
include/bitcoin/node/chasers/chaser_organize.hpp \
include/bitcoin/node/chasers/chaser_snapshot.hpp \
diff --git a/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj b/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj
index 9e00a736..1442f090 100644
--- a/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj
@@ -125,12 +125,14 @@
+
+
diff --git a/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj.filters
index f792e48a..3e9441ba 100644
--- a/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj.filters
+++ b/builds/msvc/vs2022/libbitcoin-node-test/libbitcoin-node-test.vcxproj.filters
@@ -42,6 +42,9 @@
src\chasers
+
+ src\chasers
+
src\chasers
@@ -60,6 +63,9 @@
src
+
+ src
+
src
diff --git a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj
index 6d595fe3..98d88eed 100644
--- a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj
@@ -128,6 +128,7 @@
+
@@ -136,6 +137,7 @@
+
@@ -172,6 +174,7 @@
+
@@ -183,6 +186,7 @@
+
diff --git a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters
index 6b4100d0..5b8a5dde 100644
--- a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters
+++ b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters
@@ -84,6 +84,9 @@
src\chasers
+
+ src\chasers
+
src\chasers
@@ -108,6 +111,9 @@
src
+
+ src
+
src
@@ -212,6 +218,9 @@
include\bitcoin\node\chasers
+
+ include\bitcoin\node\chasers
+
include\bitcoin\node\chasers
@@ -245,6 +254,9 @@
include\bitcoin\node
+
+ include\bitcoin\node
+
include\bitcoin\node
diff --git a/builds/msvc/vs2026/libbitcoin-node-test/libbitcoin-node-test.vcxproj b/builds/msvc/vs2026/libbitcoin-node-test/libbitcoin-node-test.vcxproj
index 961f605d..584293ee 100644
--- a/builds/msvc/vs2026/libbitcoin-node-test/libbitcoin-node-test.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-node-test/libbitcoin-node-test.vcxproj
@@ -125,12 +125,14 @@
+
+
diff --git a/builds/msvc/vs2026/libbitcoin-node-test/libbitcoin-node-test.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-node-test/libbitcoin-node-test.vcxproj.filters
index f792e48a..3e9441ba 100644
--- a/builds/msvc/vs2026/libbitcoin-node-test/libbitcoin-node-test.vcxproj.filters
+++ b/builds/msvc/vs2026/libbitcoin-node-test/libbitcoin-node-test.vcxproj.filters
@@ -42,6 +42,9 @@
src\chasers
+
+ src\chasers
+
src\chasers
@@ -60,6 +63,9 @@
src
+
+ src
+
src
diff --git a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj
index 277bb8e9..d6bb716e 100644
--- a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj
@@ -128,6 +128,7 @@
+
@@ -136,6 +137,7 @@
+
@@ -172,6 +174,7 @@
+
@@ -183,6 +186,7 @@
+
diff --git a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters
index 6b4100d0..5b8a5dde 100644
--- a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters
+++ b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters
@@ -84,6 +84,9 @@
src\chasers
+
+ src\chasers
+
src\chasers
@@ -108,6 +111,9 @@
src
+
+ src
+
src
@@ -212,6 +218,9 @@
include\bitcoin\node\chasers
+
+ include\bitcoin\node\chasers
+
include\bitcoin\node\chasers
@@ -245,6 +254,9 @@
include\bitcoin\node
+
+ include\bitcoin\node
+
include\bitcoin\node
diff --git a/include/bitcoin/node.hpp b/include/bitcoin/node.hpp
index cef48d1b..2befa366 100644
--- a/include/bitcoin/node.hpp
+++ b/include/bitcoin/node.hpp
@@ -22,6 +22,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -33,6 +34,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/include/bitcoin/node/chasers/chaser_estimate.hpp b/include/bitcoin/node/chasers/chaser_estimate.hpp
new file mode 100644
index 00000000..52fb6a83
--- /dev/null
+++ b/include/bitcoin/node/chasers/chaser_estimate.hpp
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS)
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#ifndef LIBBITCOIN_NODE_CHASERS_CHASER_ESTIMATE_HPP
+#define LIBBITCOIN_NODE_CHASERS_CHASER_ESTIMATE_HPP
+
+#include
+#include
+
+namespace libbitcoin {
+namespace node {
+
+class full_node;
+
+/// Maintain a running fee estimate cache.
+class BCN_API chaser_estimate
+ : public chaser
+{
+public:
+ DELETE_COPY_MOVE_DESTRUCT(chaser_estimate);
+
+ chaser_estimate(full_node& node) NOEXCEPT;
+
+ code start() NOEXCEPT override;
+
+protected:
+ virtual bool handle_chase(const code& ec, chase event_,
+ event_value value) NOEXCEPT;
+
+ virtual void do_organized(header_t value) NOEXCEPT;
+ virtual void do_reorganized(header_t value) NOEXCEPT;
+};
+
+} // namespace node
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/node/chasers/chasers.hpp b/include/bitcoin/node/chasers/chasers.hpp
index a3187493..4ecc20d0 100644
--- a/include/bitcoin/node/chasers/chasers.hpp
+++ b/include/bitcoin/node/chasers/chasers.hpp
@@ -23,6 +23,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/include/bitcoin/node/define.hpp b/include/bitcoin/node/define.hpp
index ac433205..59229b45 100644
--- a/include/bitcoin/node/define.hpp
+++ b/include/bitcoin/node/define.hpp
@@ -116,6 +116,7 @@ using type_id = network::messages::peer::inventory_item::type_id;
// Other directory common includes are not internally chained.
// Each header includes only its required common headers.
+// estimator : define
// settings : define
// configuration : define settings
// parser : define configuration
diff --git a/include/bitcoin/node/estimator.hpp b/include/bitcoin/node/estimator.hpp
new file mode 100644
index 00000000..c1d8a453
--- /dev/null
+++ b/include/bitcoin/node/estimator.hpp
@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS)
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#ifndef LIBBITCOIN_NODE_ESTIMATOR_HPP
+#define LIBBITCOIN_NODE_ESTIMATOR_HPP
+
+#include
+#include
+
+namespace libbitcoin {
+namespace node {
+
+/// Fee estimator with contained accumulator.
+/// Accumulator is typically too large for stack creation.
+/// Thread safe, blocking calls to estimate() during updates.
+/// Initialize on chain current coalesce after snapshot/prune.
+/// If chain falls > 1008/2 (?) blocks behind, reset and wait for coalesce.
+/// When validating still use query-based fee rate population (compact blocks).
+class BCN_API estimator
+{
+public:
+ typedef std::shared_ptr ptr;
+ static constexpr size_t maximum_horizon = 1008;
+
+ DELETE_COPY_MOVE_DESTRUCT(estimator);
+
+ /// Estimation modes.
+ enum class mode
+ {
+ basic,
+ geometric,
+ economical,
+ conservative
+ };
+
+ /// Construct (use heap allocation).
+ estimator() NOEXCEPT {};
+
+ /// Fee estimation in satoshis / transaction virtual size.
+ /// Pass zero to target next block for confirmation, range:0..1007.
+ uint64_t estimate(size_t target, mode mode) const NOEXCEPT;
+
+ /// Populate accumulator with count blocks up to the top confirmed block.
+ bool initialize(std::atomic_bool& cancel, const query& query,
+ size_t count=maximum_horizon) NOEXCEPT;
+
+ /// Update accumulator.
+ bool push(const query& query) NOEXCEPT;
+ bool pop(const query& query) NOEXCEPT;
+
+ /// Top height of accumulator.
+ size_t top_height() const NOEXCEPT;
+
+protected:
+ using rates = database::fee_rates;
+ using rate_sets = database::fee_rate_sets;
+
+ /// Bucket depth sizing parameters (number of blocks).
+ enum horizon : size_t
+ {
+ /// 2 hrs × 60 mins/hr / 10 mins/block = 12 blocks.
+ small = 12,
+
+ /// 8 hrs × 60 mins/hr / 10 mins/block = 48 blocks.
+ medium = 48,
+
+ /// 7 days * 24 hrs/day × 60 mins/hr / 10 mins/block = 1008 blocks.
+ large = maximum_horizon
+ };
+
+ /// Bucket count sizing parameters.
+ struct sizing
+ {
+ static constexpr double min = 0.1;
+ static constexpr double max = 100'000.0;
+ static constexpr double step = 1.05;
+
+ /// Derived from min/max/step above.
+ static constexpr size_t count = 283;
+ };
+
+ /// Estimation confidences.
+ struct confidence
+ {
+ static constexpr double low = 0.60;
+ static constexpr double mid = 0.85;
+ static constexpr double high = 0.95;
+ };
+
+ /// Accumulator (persistent, decay-weighted counters).
+ struct accumulator
+ {
+ template
+ struct bucket
+ {
+ /// Total scaled txs in bucket.
+ size_t total{};
+
+ /// confirmed[n]: scaled txs confirmed in > n blocks.
+ std::array confirmed;
+ };
+
+ /// Current block height of accumulated state.
+ size_t top_height{};
+
+ /// Accumulated scaled fee in decayed buckets by horizon.
+ /// Array count is the half life of the decay it implies.
+ std::array, sizing::count> small{};
+ std::array, sizing::count> medium{};
+ std::array, sizing::count> large{};
+ };
+
+ // C++23: make consteval.
+ static inline double decay_rate() NOEXCEPT
+ {
+ static const auto rate = std::pow(0.5, 1.0 / sizing::count);
+ return rate;
+ }
+
+ // C++23: make constexpr.
+ static inline double to_scale_term(size_t age) NOEXCEPT
+ {
+ return system::power(decay_rate(), age);
+ }
+
+ // C++23: make constexpr.
+ static inline double to_scale_factor(bool push) NOEXCEPT
+ {
+ return std::pow(decay_rate(), push ? +1.0 : -1.0);
+ }
+
+ accumulator& history() NOEXCEPT;
+ const accumulator& history() const NOEXCEPT;
+ bool initialize(const rate_sets& blocks) NOEXCEPT;
+ bool push(const rates& block) NOEXCEPT;
+ bool pop(const rates& block) NOEXCEPT;
+ uint64_t compute(size_t target, double confidence,
+ bool geometric=false) const NOEXCEPT;
+
+private:
+ bool update(const rates& block, size_t height, bool push) NOEXCEPT;
+ void decay(auto& buckets, double factor) NOEXCEPT;
+ void decay(bool push) NOEXCEPT;
+
+ accumulator fees_{};
+};
+
+} // namespace node
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/node/full_node.hpp b/include/bitcoin/node/full_node.hpp
index 9ad408aa..54464d25 100644
--- a/include/bitcoin/node/full_node.hpp
+++ b/include/bitcoin/node/full_node.hpp
@@ -193,6 +193,7 @@ class BCN_API full_node
chaser_confirm chaser_confirm_;
chaser_transaction chaser_transaction_;
chaser_template chaser_template_;
+ chaser_estimate chaser_estimate_;
chaser_snapshot chaser_snapshot_;
chaser_storage chaser_storage_;
event_subscriber event_subscriber_{};
diff --git a/src/chasers/chaser_estimate.cpp b/src/chasers/chaser_estimate.cpp
new file mode 100644
index 00000000..89b9f480
--- /dev/null
+++ b/src/chasers/chaser_estimate.cpp
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS)
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#include
+
+#include
+#include
+#include
+
+namespace libbitcoin {
+namespace node {
+
+#define CLASS chaser_estimate
+
+using namespace system;
+using namespace std::placeholders;
+
+BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT)
+
+chaser_estimate::chaser_estimate(full_node& node) NOEXCEPT
+ : chaser(node)
+{
+}
+
+// start
+// ----------------------------------------------------------------------------
+
+code chaser_estimate::start() NOEXCEPT
+{
+ SUBSCRIBE_CHASE(handle_chase, _1, _2, _3);
+ return error::success;
+}
+
+// event handlers
+// ----------------------------------------------------------------------------
+
+bool chaser_estimate::handle_chase(const code&, chase event_,
+ event_value value) NOEXCEPT
+{
+ if (closed())
+ return false;
+
+ // Keep updating the fee accumulator (blocks continue organizing).
+ ////if (suspended())
+ //// return true;
+
+ switch (event_)
+ {
+ case chase::organized:
+ {
+ BC_ASSERT(std::holds_alternative(value));
+ POST(do_organized, std::get(value));
+ break;
+ }
+ case chase::reorganized:
+ {
+ BC_ASSERT(std::holds_alternative(value));
+ POST(do_reorganized, std::get(value));
+ break;
+ }
+ case chase::stop:
+ {
+ return false;
+ }
+ default:
+ {
+ break;
+ }
+ }
+
+ return true;
+}
+
+void chaser_estimate::do_organized(header_t) NOEXCEPT
+{
+ BC_ASSERT(stranded());
+}
+
+void chaser_estimate::do_reorganized(header_t) NOEXCEPT
+{
+ BC_ASSERT(stranded());
+}
+
+BC_POP_WARNING()
+
+} // namespace node
+} // namespace libbitcoin
diff --git a/src/estimator.cpp b/src/estimator.cpp
new file mode 100644
index 00000000..0623f38f
--- /dev/null
+++ b/src/estimator.cpp
@@ -0,0 +1,304 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS)
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+namespace libbitcoin {
+namespace node {
+
+using namespace system;
+
+// public
+// ----------------------------------------------------------------------------
+
+uint64_t estimator::estimate(size_t target, mode mode) const NOEXCEPT
+{
+ // max_uint64 is failure sentinel (and unachievable/invalid as a fee).
+ auto estimate = max_uint64;
+ constexpr size_t large = horizon::large;
+ if (target >= large)
+ return estimate;
+
+ // Valid results are effectively limited to at least 1 sat/vb.
+ // threshold_fee is thread safe but values are affected during update.
+ switch (mode)
+ {
+ case mode::basic:
+ {
+ estimate = compute(target, confidence::high);
+ break;
+ }
+ case mode::geometric:
+ {
+ estimate = compute(target, confidence::high, true);
+ break;
+ }
+ case mode::economical:
+ {
+ const auto target1 = to_half(target);
+ const auto target2 = std::min(one, target);
+ const auto target3 = std::min(large, two * target);
+ const auto fee1 = compute(target1, confidence::low);
+ const auto fee2 = compute(target2, confidence::mid);
+ const auto fee3 = compute(target3, confidence::high);
+ estimate = std::max({ fee1, fee2, fee3 });
+ break;
+ }
+ case mode::conservative:
+ {
+ const auto target1 = to_half(target);
+ const auto target2 = std::min(one, target);
+ const auto target3 = std::min(large, two * target);
+ const auto fee1 = compute(target1, confidence::low);
+ const auto fee2 = compute(target2, confidence::mid);
+ const auto fee3 = compute(target3, confidence::high);
+ estimate = std::max({ fee1, fee2, fee3 });
+ break;
+ }
+ }
+
+ return estimate;
+}
+
+bool estimator::initialize(std::atomic_bool& cancel, const query& query,
+ size_t count) NOEXCEPT
+{
+ if (is_zero(count))
+ return true;
+
+ const auto top = query.get_top_confirmed();
+ if (sub1(count) > top)
+ return false;
+
+ rate_sets blocks{};
+ const auto start = top - sub1(count);
+ return query.get_branch_fees(cancel, blocks, start, count) &&
+ initialize(blocks);
+}
+
+bool estimator::push(const query& query) NOEXCEPT
+{
+ if (is_add_overflow(top_height(), one))
+ return false;
+
+ rates block{};
+ const auto link = query.to_confirmed(add1(top_height()));
+ return query.get_block_fees(block, link) && push(block);
+}
+
+bool estimator::pop(const query& query) 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);
+}
+
+size_t estimator::top_height() const NOEXCEPT
+{
+ return fees_.top_height;
+}
+
+// protected
+// ----------------------------------------------------------------------------
+
+estimator::accumulator& estimator::history() NOEXCEPT
+{
+ return fees_;
+}
+
+const estimator::accumulator& estimator::history() const NOEXCEPT
+{
+ return fees_;
+}
+
+bool estimator::initialize(const rate_sets& blocks) NOEXCEPT
+{
+ const auto count = blocks.size();
+ if (is_zero(count))
+ return true;
+
+ if (system::is_add_overflow(fees_.top_height, sub1(count)))
+ return false;
+
+ auto height = fees_.top_height;
+ fees_.top_height += sub1(count);
+
+ // 3-4 secs slower when parallel at 1008 blocks.
+ for (const auto& block: blocks)
+ if (!update(block, height++, true))
+ return false;
+
+ return true;
+}
+
+// Blocks must be pushed in order (but independent of chain index).
+bool estimator::push(const rates& block) NOEXCEPT
+{
+ decay(true);
+ return update(block, ++fees_.top_height, true);
+}
+
+// Blocks must be pushed in order (but independent of chain index).
+bool estimator::pop(const rates& block) NOEXCEPT
+{
+ const auto result = update(block, fees_.top_height, false);
+ decay(false);
+ --fees_.top_height;
+ return result;
+}
+
+uint64_t estimator::compute(size_t target, double confidence,
+ bool geometric) const NOEXCEPT
+{
+ const auto threshold = [](double part, double total, size_t) NOEXCEPT
+ {
+ return part / total;
+ };
+
+ // Geometric distribution approximation, not a full Markov process.
+ const auto markov = [](double part, double total, size_t target) NOEXCEPT
+ {
+ return power(part / total, target);
+ };
+
+ const auto call = [&](const auto& buckets) NOEXCEPT
+ {
+ BC_PUSH_WARNING(NO_UNGUARDED_POINTERS)
+ const auto& contribution = geometric ? markov : threshold;
+ BC_POP_WARNING()
+
+ constexpr auto magic_number = 2u;
+ const auto at_least_four = magic_number * add1(target);
+ double total{}, part{};
+ auto index = buckets.size();
+ auto found = index;
+ for (const auto& bucket: std::views::reverse(buckets))
+ {
+ --index;
+ total += to_floating(bucket.total);
+ part += to_floating(bucket.confirmed.at(target));
+ if (total < at_least_four)
+ continue;
+
+ if (contribution(part, total, target) > (1.0 - confidence))
+ break;
+
+ found = index;
+ }
+
+ if (found == buckets.size())
+ return max_uint64;
+
+ const auto minimum = sizing::min * std::pow(sizing::step, found);
+ return to_ceilinged_integer(minimum);
+ };
+
+ if (target < horizon::small) return call(fees_.small);
+ if (target < horizon::medium) return call(fees_.medium);
+ if (target < horizon::large) return call(fees_.large);
+ return max_uint64;
+}
+
+// private
+// ----------------------------------------------------------------------------
+
+void estimator::decay(bool push) NOEXCEPT
+{
+ const auto factor = to_scale_factor(push);
+ decay(fees_.large, factor);
+ decay(fees_.medium, factor);
+ decay(fees_.small, factor);
+}
+
+void estimator::decay(auto& buckets, double factor) NOEXCEPT
+{
+ for (auto& bucket: buckets)
+ {
+ bucket.total = to_floored_integer(bucket.total * factor);
+ for (auto& count: bucket.confirmed)
+ count = to_floored_integer(count * factor);
+ }
+}
+
+bool estimator::update(const rates& block, size_t height, bool push) NOEXCEPT
+{
+ // std::log (replace static with constexpr in c++26).
+ static const auto growth = std::log(sizing::step);
+ std::array counts{};
+
+ for (const auto& tx: block)
+ {
+ if (is_zero(tx.bytes))
+ return false;
+
+ if (is_zero(tx.fee))
+ continue;
+
+ const auto rate = to_floating(tx.fee) / tx.bytes;
+ if (rate < sizing::min)
+ continue;
+
+ // Clamp overflow to last bin.
+ const auto bin = std::log(rate / sizing::min) / growth;
+ ++counts.at(std::min(to_floored_integer(bin), sub1(sizing::count)));
+ }
+
+ // At age zero scale term is one.
+ const auto age = top_height() - height;
+ const auto scale = to_scale_term(age);
+ const auto call = [&](auto& buckets) NOEXCEPT
+ {
+ // The array count of the buckets element type.
+ const auto horizon = buckets.front().confirmed.size();
+
+ size_t bin{};
+ for (const auto count: counts)
+ {
+ if (is_zero(count))
+ {
+ ++bin;
+ continue;
+ }
+
+ auto& bucket = buckets.at(bin++);
+ const auto scaled = to_floored_integer(count * scale);
+ const auto signed_term = push ? scaled : twos_complement(scaled);
+
+ bucket.total += signed_term;
+ for (auto target = age; target < horizon; ++target)
+ bucket.confirmed.at(target) += signed_term;
+ }
+ };
+
+ call(fees_.large);
+ call(fees_.medium);
+ call(fees_.small);
+ return true;
+}
+
+} // namespace node
+} // namespace libbitcoin
diff --git a/src/full_node.cpp b/src/full_node.cpp
index bd9d36a1..2cc19d3a 100644
--- a/src/full_node.cpp
+++ b/src/full_node.cpp
@@ -47,6 +47,7 @@ full_node::full_node(query& query, const configuration& configuration,
chaser_confirm_(*this),
chaser_transaction_(*this),
chaser_template_(*this),
+ chaser_estimate_(*this),
chaser_snapshot_(*this),
chaser_storage_(*this)
{
@@ -84,6 +85,7 @@ void full_node::do_start(const result_handler& handler) NOEXCEPT
((ec = chaser_confirm_.start())) ||
((ec = chaser_transaction_.start())) ||
((ec = chaser_template_.start())) ||
+ ((ec = chaser_estimate_.start())) ||
((ec = chaser_snapshot_.start())) ||
((ec = chaser_storage_.start())))
{
@@ -131,6 +133,7 @@ void full_node::close() NOEXCEPT
chaser_confirm_.stop();
chaser_transaction_.stop();
chaser_template_.stop();
+ chaser_estimate_.stop();
chaser_snapshot_.stop();
chaser_storage_.stop();
}
@@ -148,6 +151,7 @@ void full_node::do_close() NOEXCEPT
chaser_confirm_.stopping(network::error::service_stopped);
chaser_transaction_.stopping(network::error::service_stopped);
chaser_template_.stopping(network::error::service_stopped);
+ chaser_estimate_.stopping(network::error::service_stopped);
chaser_snapshot_.stopping(network::error::service_stopped);
chaser_storage_.stopping(network::error::service_stopped);
diff --git a/test/chasers/chaser_estimate.cpp b/test/chasers/chaser_estimate.cpp
new file mode 100644
index 00000000..a6747d3a
--- /dev/null
+++ b/test/chasers/chaser_estimate.cpp
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS)
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#include "../test.hpp"
+
+BOOST_AUTO_TEST_SUITE(chaser_estimate_tests)
+
+BOOST_AUTO_TEST_CASE(chaser_estimate_test)
+{
+ BOOST_REQUIRE(true);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/estimator.cpp b/test/estimator.cpp
new file mode 100644
index 00000000..161b3dec
--- /dev/null
+++ b/test/estimator.cpp
@@ -0,0 +1,419 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS)
+ *
+ * This file is part of libbitcoin.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+#include "test.hpp"
+
+BOOST_AUTO_TEST_SUITE(estimator_tests)
+
+using namespace system;
+
+struct acessor
+ : node::estimator
+{
+ typedef std::shared_ptr ptr;
+
+ static acessor::ptr create() NOEXCEPT
+ {
+ return { new acessor(),[](acessor* ptr) NOEXCEPT { delete ptr; } };
+ }
+
+ using rates = estimator::rates;
+ using rate_sets = estimator::rate_sets;
+ using confidence = estimator::confidence;
+ using horizon = estimator::horizon;
+ using sizing = estimator::sizing;
+ using estimator::decay_rate;
+ using estimator::to_scale_term;
+ using estimator::to_scale_factor;
+ using estimator::history;
+ using estimator::initialize;
+ using estimator::push;
+ using estimator::pop;
+ using estimator::compute;
+};
+
+// decay_rate
+
+BOOST_AUTO_TEST_CASE(estimator__decay_rate__invoke__expected)
+{
+ const auto expected = std::pow(0.5, 1.0 / acessor::sizing::count);
+ BOOST_REQUIRE_CLOSE(acessor::decay_rate(), expected, 0.000001);
+}
+
+// to_scale_term
+
+BOOST_AUTO_TEST_CASE(estimator__to_scale_term__zero__one)
+{
+ BOOST_REQUIRE_EQUAL(acessor::to_scale_term(0u), 1.0);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__to_scale_term__non_zero__expected)
+{
+ const auto rate = acessor::decay_rate();
+ constexpr auto age = 42u;
+ const auto expected = std::pow(rate, age);
+ BOOST_REQUIRE_CLOSE(acessor::to_scale_term(age), expected, 0.000001);
+}
+
+// to_scale_factor
+
+BOOST_AUTO_TEST_CASE(estimator__to_scale_factor__push_true__decay_rate)
+{
+ const auto rate = acessor::decay_rate();
+ const auto expected = std::pow(rate, +1.0);
+ BOOST_REQUIRE_CLOSE(acessor::to_scale_factor(true), expected, 0.000001);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__to_scale_factor__push_false__inverse_decay_rate)
+{
+ const auto rate = acessor::decay_rate();
+ const auto expected = std::pow(rate, -1.0);
+ BOOST_REQUIRE_CLOSE(acessor::to_scale_factor(false), expected, 0.000001);
+}
+
+// top_height
+
+BOOST_AUTO_TEST_CASE(estimator__top_height__default__zero)
+{
+ const auto instance = acessor::create();
+ BOOST_REQUIRE_EQUAL(instance->top_height(), 0u);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__top_height__non_default__expected)
+{
+ const auto instance = acessor::create();
+ instance->history().top_height = 42u;
+ BOOST_REQUIRE_EQUAL(instance->top_height(), 42u);
+}
+
+// initialize
+
+BOOST_AUTO_TEST_CASE(estimator__initialize__empty__true_height_unchanged)
+{
+ const auto instance = acessor::create();
+ const acessor::rate_sets empty{};
+ BOOST_REQUIRE(instance->initialize(empty));
+ BOOST_REQUIRE_EQUAL(instance->top_height(), 0u);
+ BOOST_REQUIRE_EQUAL(instance->history().small[0].total, 0u);
+
+
+}BOOST_AUTO_TEST_CASE(estimator__initialize__overflow__false_height_unchanged)
+{
+ const auto instance = acessor::create();
+ instance->history().top_height = sub1(max_size_t);
+ acessor::rate_sets blocks(3);
+ BOOST_REQUIRE(!instance->initialize(blocks));
+ BOOST_REQUIRE_EQUAL(instance->top_height(), sub1(max_size_t));
+}
+
+BOOST_AUTO_TEST_CASE(estimator__initialize__two_blocks__true_height_updated)
+{
+ const auto instance = acessor::create();
+ acessor::rate_sets blocks(2);
+ BOOST_REQUIRE(instance->initialize(blocks));
+ BOOST_REQUIRE_EQUAL(instance->top_height(), 1u);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__initialize__single_block__populates_expected)
+{
+ const auto instance = acessor::create();
+
+ // rate of 1/10 (0.1) in bin 0.
+ const acessor::rates block{ { 10u, 1u } };
+ const acessor::rate_sets blocks{ block };
+ BOOST_REQUIRE(instance->initialize(blocks));
+
+ constexpr size_t age{};
+ const auto scale = acessor::to_scale_term(age);
+ const auto scaled = to_floored_integer(1u * scale);
+ const auto& small0 = instance->history().small.at(0);
+
+ BOOST_REQUIRE_EQUAL(small0.total, scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[0], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[1], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[2], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[3], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[4], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[5], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[6], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[7], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[8], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[9], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[10], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[11], scaled);
+
+ const auto& medium0 = instance->history().medium.at(0);
+ BOOST_REQUIRE_EQUAL(medium0.total, scaled);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[0], scaled);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[1], scaled);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[2], scaled);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[45], scaled);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[46], scaled);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[47], scaled);
+
+ const auto& large0 = instance->history().large.at(0);
+ BOOST_REQUIRE_EQUAL(large0.total, scaled);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[0], scaled);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[1], scaled);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[2], scaled);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[1005], scaled);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[1006], scaled);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[1007], scaled);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__initialize__two_blocks_with_data__expected)
+{
+ // 1 tx, rate=0.1, bin=0
+ // 2 tx, rate=0.1, bin=0
+ // Expected total: floor(1 * decay_rate) + floor(2 * 1.0) = 0 + 2 = 2.
+ const auto instance = acessor::create();
+ const acessor::rates oldest{ { 10u, 1u } };
+ const acessor::rates newest{ { 10u, 1u }, { 10u, 1u } };
+ acessor::rate_sets blocks{ oldest, newest };
+ BOOST_REQUIRE(instance->initialize(blocks));
+ BOOST_REQUIRE_EQUAL(instance->top_height(), 1u);
+ BOOST_REQUIRE_EQUAL(instance->history().small.at(0).total, 2u);
+}
+
+// push
+
+BOOST_AUTO_TEST_CASE(estimator__push__empty_block__decays_and_increments)
+{
+ const auto instance = acessor::create();
+ constexpr auto initial = 100u;
+ instance->history().small[0].total = initial;
+ const auto factor = acessor::to_scale_factor(true);
+ const auto expected = to_floored_integer(initial * factor);
+ const acessor::rates empty{};
+ BOOST_REQUIRE(instance->push(empty));
+ BOOST_REQUIRE_EQUAL(instance->top_height(), 1u);
+ BOOST_REQUIRE_EQUAL(instance->history().small[0].total, expected);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__push__single_tx__populates_expected)
+{
+ const auto instance = acessor::create();
+
+ // rate of 1/10 (0.1) in bin 0.
+ const acessor::rates block{ { 10u, 1u } };
+ BOOST_REQUIRE(instance->push(block));
+ BOOST_REQUIRE_EQUAL(instance->top_height(), 1u);
+
+ constexpr size_t age{};
+ const auto scale = acessor::to_scale_term(age);
+ const auto scaled = to_floored_integer(1u * scale);
+ const auto& small0 = instance->history().small.at(0);
+
+ BOOST_REQUIRE_EQUAL(small0.total, scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[0], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[1], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[2], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[3], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[4], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[5], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[6], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[7], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[8], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[9], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[10], scaled);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[11], scaled);
+
+ const auto& medium0 = instance->history().medium.at(0);
+ BOOST_REQUIRE_EQUAL(medium0.total, scaled);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[0], scaled);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[1], scaled);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[2], scaled);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[45], scaled);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[46], scaled);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[47], scaled);
+
+ const auto& large0 = instance->history().large.at(0);
+ BOOST_REQUIRE_EQUAL(large0.total, scaled);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[0], scaled);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[1], scaled);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[2], scaled);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[1005], scaled);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[1006], scaled);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[1007], scaled);
+}
+
+// pop
+
+BOOST_AUTO_TEST_CASE(estimator__pop__empty_block__decays_and_decrements)
+{
+ const auto instance = acessor::create();
+ instance->history().top_height = 1u;
+ constexpr auto initial = 100u;
+ instance->history().small[0].total = initial;
+ const auto factor = acessor::to_scale_factor(false);
+ const auto expected = to_floored_integer(initial * factor);
+ const acessor::rates empty{};
+ BOOST_REQUIRE(instance->pop(empty));
+ BOOST_REQUIRE_EQUAL(instance->top_height(), 0u);
+ BOOST_REQUIRE_EQUAL(instance->history().small[0].total, expected);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__pop__reverses_push__restores_state)
+{
+ const auto instance = acessor::create();
+
+ // rate of 1/10 (0.1) in bin 0.
+ const acessor::rates block{ { 10u, 1u } };
+ BOOST_REQUIRE(instance->push(block));
+ BOOST_REQUIRE(instance->pop(block));
+ BOOST_REQUIRE_EQUAL(instance->top_height(), 0u);
+
+ const auto& small0 = instance->history().small.at(0);
+
+ BOOST_REQUIRE_EQUAL(small0.total, 0u);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[0], 0u);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[1], 0u);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[2], 0u);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[3], 0u);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[4], 0u);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[5], 0u);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[6], 0u);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[7], 0u);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[8], 0u);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[9], 0u);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[10], 0u);
+ BOOST_REQUIRE_EQUAL(small0.confirmed[11], 0u);
+
+ const auto& medium0 = instance->history().medium.at(0);
+ BOOST_REQUIRE_EQUAL(medium0.total, 0u);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[0], 0u);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[1], 0u);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[2], 0u);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[45], 0u);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[46], 0u);
+ BOOST_REQUIRE_EQUAL(medium0.confirmed[47], 0u);
+
+ const auto& large0 = instance->history().large.at(0);
+ BOOST_REQUIRE_EQUAL(large0.total, 0u);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[0], 0u);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[1], 0u);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[2], 0u);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[1005], 0u);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[1006], 0u);
+ BOOST_REQUIRE_EQUAL(large0.confirmed[1007], 0u);
+}
+
+// compute
+
+BOOST_AUTO_TEST_CASE(estimator__compute__default_state__max_uint64)
+{
+ const auto instance = acessor::create();
+ BOOST_REQUIRE_EQUAL(instance->compute(0, acessor::confidence::high), max_uint64);
+ BOOST_REQUIRE_EQUAL(instance->compute(1, acessor::confidence::mid, true), max_uint64);
+ BOOST_REQUIRE_EQUAL(instance->compute(50, acessor::confidence::low), max_uint64);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__compute__insufficient_total__max_uint64)
+{
+ const auto instance = acessor::create();
+ constexpr auto bin = 0u;
+
+ // < at_least_four=2 for target=0.
+ constexpr auto value = 1u;
+ instance->history().small[bin].total = value;
+ instance->history().small[bin].confirmed[0] = value;
+ BOOST_REQUIRE_EQUAL(instance->compute(0, acessor::confidence::high), max_uint64);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__compute__low_failure_basic__expected_fee)
+{
+ const auto instance = acessor::create();
+ constexpr auto bin = 0u;
+ constexpr auto total = 10u;
+
+ // 0/10 = 0 <= 0.05.
+ constexpr auto failure = 0u;
+ instance->history().small[bin].total = total;
+ instance->history().small[bin].confirmed[0] = failure;
+ const auto fee = to_ceilinged_integer(acessor::sizing::min * std::pow(acessor::sizing::step, bin));
+ BOOST_REQUIRE_EQUAL(instance->compute(0, acessor::confidence::high), fee);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__compute__high_failure_basic__max_uint64)
+{
+ const auto instance = acessor::create();
+ constexpr auto bin = 0u;
+ constexpr auto total = 10u;
+
+ // 1/10 = 0.1 > 0.05.
+ constexpr auto failure = 1u;
+ instance->history().small[bin].total = total;
+ instance->history().small[bin].confirmed[0] = failure;
+ BOOST_REQUIRE_EQUAL(instance->compute(0, acessor::confidence::high), max_uint64);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__compute__multi_bin_basic__expected_fee)
+{
+ const auto instance = acessor::create();
+ constexpr auto low_bin = 0u;
+ constexpr auto high_bin = 1u;
+ constexpr auto total = 10u;
+
+ // high failure in low bin.
+ constexpr auto low_failure = 10u;
+
+ // low failure in high bin.
+ constexpr auto high_failure = 0u;
+ instance->history().small[low_bin].total = total;
+ instance->history().small[low_bin].confirmed[0] = low_failure;
+ instance->history().small[high_bin].total = total;
+ instance->history().small[high_bin].confirmed[0] = high_failure;
+
+ // Cumulative at high_bin: 0/10 = 0 <= 0.05, then at low_bin: 10/20 = 0.5 > 0.05, found=1.
+ const auto fee = to_ceilinged_integer(acessor::sizing::min * std::pow(acessor::sizing::step, high_bin));
+ BOOST_REQUIRE_EQUAL(instance->compute(0, acessor::confidence::high), fee);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__compute__geometric_target_one__matches_basic)
+{
+ const auto instance = acessor::create();
+ constexpr auto bin = 0u;
+ constexpr auto total = 10u;
+ constexpr auto failure = 0u;
+ instance->history().small[bin].total = total;
+ instance->history().small[bin].confirmed[1] = failure;
+ const auto fee = to_ceilinged_integer(acessor::sizing::min * std::pow(acessor::sizing::step, bin));
+ const auto basic = instance->compute(1, acessor::confidence::high, false);
+ const auto geometric = instance->compute(1, acessor::confidence::high, true);
+ BOOST_REQUIRE_EQUAL(basic, fee);
+ BOOST_REQUIRE_EQUAL(geometric, fee);
+}
+
+BOOST_AUTO_TEST_CASE(estimator__compute__geometric_high_target__expected)
+{
+ const auto instance = acessor::create();
+ constexpr auto bin = 0u;
+ constexpr auto total = 10u;
+
+ // p=0.1, pow(0.1,2)=0.01 < 0.05, so found=0.
+ constexpr auto failure = 1u;
+ instance->history().small[bin].total = total;
+ instance->history().small[bin].confirmed[2] = failure;
+ const auto fee = to_ceilinged_integer(acessor::sizing::min * std::pow(acessor::sizing::step, bin));
+ BOOST_REQUIRE_EQUAL(instance->compute(2, acessor::confidence::high, true), fee);
+
+ // Contrast with basic: 0.1 > 0.05, would be max_uint64.
+ BOOST_REQUIRE_EQUAL(instance->compute(2, acessor::confidence::high, false), max_uint64);
+}
+
+BOOST_AUTO_TEST_SUITE_END()