diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am
index 2f59415c..a128d1ac 100644
--- a/builds/gnu/Makefile.am
+++ b/builds/gnu/Makefile.am
@@ -89,6 +89,7 @@ src_libbitcoin_node_la_SOURCES = \
${srcdir}/../../src/protocols/protocol_transaction_in_106.cpp \
${srcdir}/../../src/protocols/protocol_transaction_out_106.cpp \
${srcdir}/../../src/protocols/protocol_transaction_out_70001.cpp \
+ ${srcdir}/../../src/protocols/protocol_transaction_out_70013.cpp \
${srcdir}/../../src/sessions/session.cpp \
${srcdir}/../../src/sessions/session_inbound.cpp \
${srcdir}/../../src/sessions/session_manual.cpp \
@@ -175,6 +176,7 @@ include_bitcoin_node_protocols_HEADERS = \
${srcdir}/../../include/bitcoin/node/protocols/protocol_transaction_in_106.hpp \
${srcdir}/../../include/bitcoin/node/protocols/protocol_transaction_out_106.hpp \
${srcdir}/../../include/bitcoin/node/protocols/protocol_transaction_out_70001.hpp \
+ ${srcdir}/../../include/bitcoin/node/protocols/protocol_transaction_out_70013.hpp \
${srcdir}/../../include/bitcoin/node/protocols/protocols.hpp
include_bitcoin_node_sessionsdir = \
diff --git a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj
index 6d92f85f..c8b1f321 100644
--- a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj
+++ b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj
@@ -159,6 +159,7 @@
+
@@ -210,6 +211,7 @@
+
diff --git a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters
index 7bbae4c3..9d62a915 100644
--- a/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters
+++ b/builds/msvc/vs2022/libbitcoin-node/libbitcoin-node.vcxproj.filters
@@ -171,6 +171,9 @@
src\protocols
+
+ src\protocols
+
src\sessions
@@ -320,6 +323,9 @@
include\bitcoin\node\protocols
+
+ include\bitcoin\node\protocols
+
include\bitcoin\node\protocols
diff --git a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj
index 5a3101e5..b6a4db3b 100644
--- a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj
@@ -159,6 +159,7 @@
+
@@ -210,6 +211,7 @@
+
diff --git a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters
index 7bbae4c3..9d62a915 100644
--- a/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters
+++ b/builds/msvc/vs2026/libbitcoin-node/libbitcoin-node.vcxproj.filters
@@ -171,6 +171,9 @@
src\protocols
+
+ src\protocols
+
src\sessions
@@ -320,6 +323,9 @@
include\bitcoin\node\protocols
+
+ include\bitcoin\node\protocols
+
include\bitcoin\node\protocols
diff --git a/include/bitcoin/node.hpp b/include/bitcoin/node.hpp
index 27ee6414..346d143f 100644
--- a/include/bitcoin/node.hpp
+++ b/include/bitcoin/node.hpp
@@ -60,6 +60,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/include/bitcoin/node/error.hpp b/include/bitcoin/node/error.hpp
index 3c2e2e49..0f2ef87d 100644
--- a/include/bitcoin/node/error.hpp
+++ b/include/bitcoin/node/error.hpp
@@ -61,6 +61,7 @@ enum error_t : uint8_t
/// transaction pool
pooling_disabled,
+ insufficient_fee,
empty_package,
/// fee estimation
diff --git a/include/bitcoin/node/impl/sessions/session_peer.ipp b/include/bitcoin/node/impl/sessions/session_peer.ipp
index d766ee94..dc151112 100644
--- a/include/bitcoin/node/impl/sessions/session_peer.ipp
+++ b/include/bitcoin/node/impl/sessions/session_peer.ipp
@@ -154,7 +154,9 @@ inline void CLASS::attach_protocols(const channel_ptr& channel) NOEXCEPT
// Relay is configured, active, and txs are ready (txs in/out).
if (txs_in_out && peer->peer_version()->relay)
{
- if (peer->is_negotiated(level::bip37))
+ if (peer->is_negotiated(level::bip133))
+ channel->attach(self)->start();
+ else if (peer->is_negotiated(level::bip37))
channel->attach(self)->start();
else
channel->attach(self)->start();
diff --git a/include/bitcoin/node/protocols/protocol_transaction_out_70013.hpp b/include/bitcoin/node/protocols/protocol_transaction_out_70013.hpp
new file mode 100644
index 00000000..c63e8426
--- /dev/null
+++ b/include/bitcoin/node/protocols/protocol_transaction_out_70013.hpp
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers
+ *
+ * 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_PROTOCOLS_PROTOCOL_TRANSACTION_OUT_70013_HPP
+#define LIBBITCOIN_NODE_PROTOCOLS_PROTOCOL_TRANSACTION_OUT_70013_HPP
+
+#include
+#include
+
+namespace libbitcoin {
+namespace node {
+
+class BCN_API protocol_transaction_out_70013
+ : public protocol_transaction_out_70001,
+ protected network::tracker
+{
+public:
+ typedef std::shared_ptr ptr;
+
+ protocol_transaction_out_70013(const auto& session,
+ const network::channel::ptr& channel) NOEXCEPT
+ : protocol_transaction_out_70001(session, channel),
+ network::tracker(session->log)
+ {
+ }
+
+ /// Start protocol (strand required).
+ void start() NOEXCEPT override;
+
+protected:
+ /// Capture the peer's minimum fee rate for announcements.
+ virtual bool handle_receive_fee_filter(const code& ec,
+ const network::messages::peer::fee_filter::cptr& message) NOEXCEPT;
+
+ /// Bypasses announcement of a tx below the peer's minimum fee rate.
+ bool do_announce(transaction_t link) NOEXCEPT override;
+
+private:
+ bool insufficient(const database::fee_rate& rate) const NOEXCEPT;
+
+ // This is protected by strand.
+ uint64_t minimum_fee_{};
+};
+
+} // namespace node
+} // namespace libbitcoin
+
+#endif
diff --git a/include/bitcoin/node/protocols/protocols.hpp b/include/bitcoin/node/protocols/protocols.hpp
index 9ff2dc25..ef865577 100644
--- a/include/bitcoin/node/protocols/protocols.hpp
+++ b/include/bitcoin/node/protocols/protocols.hpp
@@ -36,5 +36,6 @@
#include
#include
#include
+#include
#endif
diff --git a/include/bitcoin/node/settings.hpp b/include/bitcoin/node/settings.hpp
index cd0d7682..5c6b3e13 100644
--- a/include/bitcoin/node/settings.hpp
+++ b/include/bitcoin/node/settings.hpp
@@ -66,6 +66,7 @@ class BCN_API settings
virtual size_t maximum_height_() const NOEXCEPT;
virtual size_t maximum_concurrency_() const NOEXCEPT;
virtual size_t fee_estimate_horizon_() const NOEXCEPT;
+ virtual uint64_t minimum_fee_rate_() const NOEXCEPT;
virtual uint64_t services_provided() const NOEXCEPT;
virtual uint64_t services_required() const NOEXCEPT;
virtual bool fee_estimate_enabled() const NOEXCEPT;
diff --git a/src/chasers/chaser_transaction.cpp b/src/chasers/chaser_transaction.cpp
index d59354ec..9fb1b166 100644
--- a/src/chasers/chaser_transaction.cpp
+++ b/src/chasers/chaser_transaction.cpp
@@ -31,6 +31,8 @@ using namespace system;
using namespace system::chain;
using namespace std::placeholders;
+constexpr uint64_t vbytes_per_vkbyte = 1'000;
+
BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT)
BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR)
@@ -179,6 +181,10 @@ code chaser_transaction::validate(size_t& index,
if (txs.empty())
return error::empty_package;
+ // Conflict within the package, as with a block, since it is accepted whole.
+ 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;
@@ -186,6 +192,22 @@ code chaser_transaction::validate(size_t& index,
if (const auto ec = validate(*txs.at(index)))
return ec;
+ // The package is accepted as a whole, so the whole must pay the rate.
+ index = zero;
+ uint64_t fee{};
+ uint64_t size{};
+ for (const auto& tx: txs)
+ {
+ fee = ceilinged_add(fee, tx->fee());
+ size = ceilinged_add(size, possible_wide_cast(
+ tx->virtual_size()));
+ }
+
+ // Compared in satoshis per virtual kilobyte, so exact and undivided.
+ if (ceilinged_multiply(fee, vbytes_per_vkbyte) <
+ ceilinged_multiply(node_settings().minimum_fee_rate_(), size))
+ return error::insufficient_fee;
+
return {};
}
@@ -196,16 +218,18 @@ code chaser_transaction::validate(const chain::transaction& tx) NOEXCEPT
// Ensure tx does not violate tx consensus rules.
if (!ec) ec = tx.check();
if (!ec) ec = tx.check(pool_);
- if (!ec) archive().populate_with_metadata(tx, true);
+ if (!ec) archive().populate_with_metadata(tx, true, true);
if (!ec) ec = tx.accept(pool_);
- if (!ec) ec = tx.confirm(pool_);
- if (!ec) ec = tx.connect(pool_);
// Ensure tx does not violate presumed block consensus rules.
// This is a DoS guard when validating a tx outside of a block.
if (!ec) ec = tx.check_guard();
if (!ec) ec = tx.check_guard(pool_);
if (!ec) ec = tx.accept_guard(pool_);
+ if (!ec) ec = tx.confirm_guard(pool_);
+
+ // Script validation is the most costly, so it follows the guards.
+ if (!ec) ec = tx.connect(pool_);
return ec;
}
diff --git a/src/error.cpp b/src/error.cpp
index 00fca068..e097d614 100644
--- a/src/error.cpp
+++ b/src/error.cpp
@@ -51,6 +51,7 @@ DEFINE_ERROR_T_MESSAGE_MAP(error)
// transaction pool
{ pooling_disabled, "pooling disabled" },
+ { insufficient_fee, "insufficient fee" },
{ empty_package, "empty package" },
// fee estimation
diff --git a/src/protocols/protocol_transaction_out_70013.cpp b/src/protocols/protocol_transaction_out_70013.cpp
new file mode 100644
index 00000000..32814a0d
--- /dev/null
+++ b/src/protocols/protocol_transaction_out_70013.cpp
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2011-2026 libbitcoin developers
+ *
+ * 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
+
+namespace libbitcoin {
+namespace node {
+
+#define CLASS protocol_transaction_out_70013
+
+using namespace system;
+using namespace network::messages::peer;
+using namespace std::placeholders;
+
+// The fee filter is expressed in satoshis per virtual kilobyte (bip133).
+constexpr uint64_t vbytes_per_vkbyte = 1'000;
+
+// Shared pointers required for lifetime in handler parameters.
+BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED)
+BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR)
+
+// start
+// ----------------------------------------------------------------------------
+
+void protocol_transaction_out_70013::start() NOEXCEPT
+{
+ BC_ASSERT(stranded());
+
+ if (started())
+ return;
+
+ SUBSCRIBE_CHANNEL(fee_filter, handle_receive_fee_filter, _1, _2);
+ protocol_transaction_out_70001::start();
+}
+
+// Inbound (feefilter).
+// ----------------------------------------------------------------------------
+
+bool protocol_transaction_out_70013::handle_receive_fee_filter(const code& ec,
+ const fee_filter::cptr& message) NOEXCEPT
+{
+ BC_ASSERT(stranded());
+
+ if (stopped(ec))
+ return false;
+
+ minimum_fee_ = message->minimum_fee;
+ return true;
+}
+
+// Outbound (inv).
+// ----------------------------------------------------------------------------
+
+bool protocol_transaction_out_70013::do_announce(transaction_t link) NOEXCEPT
+{
+ BC_ASSERT(stranded());
+
+ if (stopped())
+ return false;
+
+ // The tx is archived and its prevouts populated, so the rate is known.
+ database::fee_rate rate{};
+ if (!archive().get_tx_fees(rate, link))
+ {
+ fault(database::error::integrity);
+ return false;
+ }
+
+ // bip133: the peer is not sent a tx below the rate it advertised.
+ if (insufficient(rate))
+ return true;
+
+ return protocol_transaction_out_70001::do_announce(link);
+}
+
+// private
+bool protocol_transaction_out_70013::insufficient(
+ const database::fee_rate& rate) const NOEXCEPT
+{
+ return ceilinged_multiply(rate.fee, vbytes_per_vkbyte) <
+ ceilinged_multiply(minimum_fee_,
+ possible_wide_cast(rate.bytes));
+}
+
+BC_POP_WARNING()
+BC_POP_WARNING()
+
+} // namespace node
+} // namespace libbitcoin
diff --git a/src/settings.cpp b/src/settings.cpp
index e0748e5c..b2d77f27 100644
--- a/src/settings.cpp
+++ b/src/settings.cpp
@@ -80,6 +80,14 @@ size_t settings::maximum_concurrency_() const NOEXCEPT
return to_bool(maximum_concurrency) ? maximum_concurrency : max_size_t;
}
+// The configured rate is btc/vkb, as reported to clients (see relayfee).
+uint64_t settings::minimum_fee_rate_() const NOEXCEPT
+{
+ // Ceilinged, as a configured minimum is not rounded down.
+ return to_ceilinged_integer(minimum_fee_rate *
+ chain::satoshi_per_bitcoin);
+}
+
size_t settings::fee_estimate_horizon_() const NOEXCEPT
{
return std::min(fee_estimate_horizon, estimator::maximum_horizon);
diff --git a/test/settings.cpp b/test/settings.cpp
index c4c6a4a0..063c3555 100644
--- a/test/settings.cpp
+++ b/test/settings.cpp
@@ -43,6 +43,7 @@ BOOST_AUTO_TEST_CASE(settings__node__default_context__expected)
BOOST_REQUIRE_EQUAL(node.provide_filters, false);
BOOST_REQUIRE_EQUAL(node.limited_blocks, false);
BOOST_REQUIRE_EQUAL(node.minimum_fee_rate, 0.0);
+ BOOST_REQUIRE_EQUAL(node.minimum_fee_rate_(), 0_u64);
BOOST_REQUIRE_EQUAL(node.minimum_bump_rate, 0.0);
BOOST_REQUIRE_EQUAL(node.allowed_deviation, 1.5);
BOOST_REQUIRE_EQUAL(node.batch_signatures, 0_u64);