From cf231f3ef8c7dcd77778a4b62b94c339492e487c Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 3 Aug 2026 00:28:13 -0400 Subject: [PATCH 1/4] Implement DoS protect send throttle. --- include/bitcoin/network/net/proxy.hpp | 27 ++++++++- include/bitcoin/network/settings.hpp | 6 +- src/channels/channel.cpp | 6 +- src/net/proxy.cpp | 21 ++++++- src/net/proxy_actions.cpp | 31 +++++----- src/net/proxy_queue.cpp | 84 ++++++++++++++++++++++++++- test/net/proxy.cpp | 81 +++++++++++++++++++++++++- test/settings.cpp | 2 +- 8 files changed, 233 insertions(+), 25 deletions(-) diff --git a/include/bitcoin/network/net/proxy.hpp b/include/bitcoin/network/net/proxy.hpp index 1e1a411fb..c8cd9b361 100644 --- a/include/bitcoin/network/net/proxy.hpp +++ b/include/bitcoin/network/net/proxy.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include namespace libbitcoin { @@ -40,6 +41,11 @@ namespace network { /// Completion handler is invoked once write is complete, at which point the /// next queued write is invoked. When a channel stops with pending writes the /// write queue is purged without invoke of the purged handlers. +/// Each send is allocated (bytes/rate_limit) of time, and its completion is +/// deferred by whatever portion of that allocation the write did not consume. +/// Since nothing is produced until the completion handler is invoked, this +/// throttles the channel without queueing. Reads are not metered, as they are +/// bounded by protocol correctness. Zero rate_limit disables the throttle. class BCT_API proxy : public enable_shared_from_base, public reporter { @@ -111,7 +117,7 @@ class BCT_API proxy const config::endpoint& endpoint() const NOEXCEPT; protected: - proxy(const socket::ptr& socket) NOEXCEPT; + proxy(const socket::ptr& socket, uint32_t rate_limit) NOEXCEPT; /// Stranded event, allows timer reset. virtual void reading() NOEXCEPT; @@ -122,6 +128,15 @@ class BCT_API proxy /// Subscribe to stop notification (requires strand). void subscribe_stop(result_handler&& handler) NOEXCEPT; + /// Throttle. + /// ----------------------------------------------------------------------- + + /// Unconsumed portion of the byte allocation, by which a send completing + /// now is deferred. Zero if unlimited, stopped, or fully consumed by the + /// transmission (requires strand). + steady_clock::duration unconsumed(size_t bytes, + const steady_clock::time_point& start) const NOEXCEPT; + /// Wait. /// ----------------------------------------------------------------------- @@ -243,15 +258,25 @@ class BCT_API proxy void handle_write(const code& ec, size_t bytes, const count_handler& handler) NOEXCEPT; + // Meter sent bytes and defer the completion by the unconsumed allocation. + count_handler metered(count_handler&& handler) NOEXCEPT; + void handle_metered(const code& ec, size_t bytes, + const steady_clock::time_point& start, + const count_handler& handler) NOEXCEPT; + void handle_charge(const code&, const code& ec, size_t bytes, + const count_handler& handler) NOEXCEPT; + // Invoke reading() on strand. void do_reading() NOEXCEPT; // These are thread safe. std::atomic_bool paused_{ true }; std::atomic total_{}; + const uint32_t rate_limit_; socket::ptr socket_; // These are protected by strand. + deadline::ptr throttle_; stop_subscriber stop_subscriber_{}; socket::http_parser_ptr parser_{}; queue deferred_{}; diff --git a/include/bitcoin/network/settings.hpp b/include/bitcoin/network/settings.hpp index 16eafed96..79e1777a9 100644 --- a/include/bitcoin/network/settings.hpp +++ b/include/bitcoin/network/settings.hpp @@ -297,7 +297,11 @@ struct BCT_API settings uint32_t handshake_timeout_seconds{ 15 }; uint32_t channel_heartbeat_minutes{ 5 }; uint32_t maximum_skew_minutes{ 120 }; - uint32_t rate_limit{ 1024 }; + + /// Bytes/second allocated to each channel for sending, zero is unlimited. + /// A send is deferred by the unconsumed portion of its byte allocation, + /// which the next send of the channel cannot start until it expires. + uint32_t rate_limit{ 0 }; std::string user_agent{ BC_USER_AGENT }; std::filesystem::path path{}; config::authorities blacklists{}; diff --git a/src/channels/channel.cpp b/src/channels/channel.cpp index e4ea0cb18..eb35b6f60 100644 --- a/src/channels/channel.cpp +++ b/src/channels/channel.cpp @@ -46,7 +46,7 @@ inline deadline::ptr make_timer(const logger& log, asio::strand& strand, channel::channel(const logger& log, const socket::ptr& socket, uint64_t identifier, const settings_t& settings, const options_t& options) NOEXCEPT - : proxy(socket), + : proxy(socket, settings.rate_limit), options_(options), settings_(settings), identifier_(identifier), @@ -112,7 +112,9 @@ void channel::handle_monitor(const code& ec) NOEXCEPT // Timers. // ---------------------------------------------------------------------------- -// TODO: build DoS protection around rate_limit_, total(), and time. +// Send throttling (settings.rate_limit) is implemented by the proxy. A channel +// whose accrued deferral exceeds inactivity is dropped by that timer, which is +// the intended outcome (the throttle degrades to disconnection under abuse). // A restarted timer invokes completion handler with error::operation_canceled. // Called from start or strand. diff --git a/src/net/proxy.cpp b/src/net/proxy.cpp index 5af17ecde..bd0ec1601 100644 --- a/src/net/proxy.cpp +++ b/src/net/proxy.cpp @@ -28,11 +28,21 @@ namespace network { BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) +// Factory for variable deadline timer pointer construction (or null). +inline deadline::ptr make_throttle(const logger& log, asio::strand& strand, + uint32_t rate_limit) NOEXCEPT +{ + return to_bool(rate_limit) ? + system::emplace_shared(log, strand) : nullptr; +} + // This is created in a started state and must be stopped, as the subscribers // assert if not stopped. Subscribers may hold protocols even if the service // is not started. -proxy::proxy(const socket::ptr& socket) NOEXCEPT - : socket_(socket), +proxy::proxy(const socket::ptr& socket, uint32_t rate_limit) NOEXCEPT + : rate_limit_(rate_limit), + socket_(socket), + throttle_(make_throttle(socket->log, socket->strand(), rate_limit)), reporter(socket->log) { } @@ -75,6 +85,10 @@ void proxy::do_stop(const code& ec) NOEXCEPT BC_ASSERT(stranded()); using namespace std::placeholders; + // The socket is not yet stopped, so a deferred send still holds the queue. + // Release it here so that the close part is not delayed by the throttle. + if (throttle_) throttle_->stop(); + batched_ = false; parted_ = false; @@ -138,6 +152,9 @@ void proxy::stopping(const code& ec) NOEXCEPT { BC_ASSERT(stranded()); + // Release any deferred send (fires pending charge with canceled). + if (throttle_) throttle_->stop(); + // Release any http message parse in progress. parser_.reset(); diff --git a/src/net/proxy_actions.cpp b/src/net/proxy_actions.cpp index 6946c288a..df90feba5 100644 --- a/src/net/proxy_actions.cpp +++ b/src/net/proxy_actions.cpp @@ -73,8 +73,8 @@ void proxy::do_ws_write(const asio::const_buffer& payload, bool binary, const count_handler& handler) NOEXCEPT { socket_->ws_write({ payload.data(), payload.size() }, binary, - std::bind(&proxy::handle_write, - shared_from_this(), _1, _2, handler)); + metered(std::bind(&proxy::handle_write, + shared_from_this(), _1, _2, handler))); } // TCP (generic, fixed size). @@ -103,8 +103,8 @@ void proxy::do_tcp_write(const asio::const_buffer& payload, const count_handler& handler) NOEXCEPT { socket_->tcp_write({ payload.data(), payload.size() }, - std::bind(&proxy::handle_write, - shared_from_this(), _1, _2, handler)); + metered(std::bind(&proxy::handle_write, + shared_from_this(), _1, _2, handler))); } // RPC (TCP: electrum/stratum_v1, WS: btcd). @@ -256,8 +256,8 @@ void proxy::do_response_write(const rpc::response_ptr& response, } socket_->rpc_write(std::move(*response), - std::bind(&proxy::handle_write, - shared_from_this(), _1, _2, handler)); + metered(std::bind(&proxy::handle_write, + shared_from_this(), _1, _2, handler))); } // private @@ -265,8 +265,8 @@ void proxy::do_notification_write(const rpc::request_ptr& notification, const count_handler& handler) NOEXCEPT { socket_->rpc_notify(std::move(*notification), - std::bind(&proxy::handle_write, - shared_from_this(), _1, _2, handler)); + metered(std::bind(&proxy::handle_write, + shared_from_this(), _1, _2, handler))); } // HTTP/WS (generic/rpc). @@ -466,7 +466,8 @@ void proxy::write(http::response&& response, if (parted_) { - socket_->rpc_write_chunk(std::move(part), std::move(handler)); + socket_->rpc_write_chunk(std::move(part), + metered(std::move(handler))); return; } @@ -477,13 +478,13 @@ void proxy::write(http::response&& response, const auto out = move_shared(std::move(part)); socket_->http_write_header(std::move(response), - std::bind(&proxy::handle_http_header_write, - shared_from_this(), _1, _2, out, std::move(handler))); + metered(std::bind(&proxy::handle_http_header_write, + shared_from_this(), _1, _2, out, std::move(handler)))); return; } // http is half duplex so there is no interleave risk. - socket_->http_write(std::move(response), std::move(handler)); + socket_->http_write(std::move(response), metered(std::move(handler))); } // private @@ -498,7 +499,7 @@ void proxy::handle_http_header_write(const code& ec, size_t bytes, return; } - socket_->rpc_write_chunk(std::move(*part), move_copy(handler)); + socket_->rpc_write_chunk(std::move(*part), metered(move_copy(handler))); } // private @@ -506,8 +507,8 @@ void proxy::do_http_write(const http::response_ptr& response, const count_handler& handler) NOEXCEPT { socket_->http_write(std::move(*response), - std::bind(&proxy::handle_write, - shared_from_this(), _1, _2, handler)); + metered(std::bind(&proxy::handle_write, + shared_from_this(), _1, _2, handler))); } BC_POP_WARNING() diff --git a/src/net/proxy_queue.cpp b/src/net/proxy_queue.cpp index 32e899584..6f3438723 100644 --- a/src/net/proxy_queue.cpp +++ b/src/net/proxy_queue.cpp @@ -27,6 +27,13 @@ namespace libbitcoin { namespace network { +// Shared pointers required in handler parameters so closures control lifetime. +BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) +BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +using namespace std::placeholders; + // Send cycle (send continues until queue is empty). // ---------------------------------------------------------------------------- // private @@ -67,13 +74,88 @@ void proxy::handle_write(const code& ec, size_t bytes, if (queue_.empty()) return; + // Handler precedes pop so that a handler send does not start a second + // write loop (a non-empty queue defers the start to the pop below). handler(ec, bytes); queue_.pop_front(); - total_ = system::ceilinged_add(total_.load(), bytes); // All handlers must be invoked unless stopped, so continue despite code. write(); } +// Throttle (sent bytes are allocated time at the configured rate). +// ---------------------------------------------------------------------------- +// private +// Applied to every send, queued or not, so that the deferral is imposed +// without imposing the queue (http is half duplex, so it is not queued). + +// Nanoseconds allocated to the transmission of bytes at the given rate. +inline steady_clock::duration to_allocation(size_t bytes, + uint32_t rate) NOEXCEPT +{ + using namespace system; + constexpr auto nanos = 1'000'000'000_u64; + + // Overflow implies an unusable rate/size, saturated at the type maximum. + const auto span = ceilinged_multiply(bytes, nanos) / rate; + return nanoseconds{ limit(span) }; +} + +count_handler proxy::metered(count_handler&& handler) NOEXCEPT +{ + // Stamped at issue, so that only transmission time is credited against + // the allocation. Time spent idle between sends earns nothing. + return std::bind(&proxy::handle_metered, + shared_from_this(), _1, _2, steady_clock::now(), std::move(handler)); +} + +steady_clock::duration proxy::unconsumed(size_t bytes, + const steady_clock::time_point& start) const NOEXCEPT +{ + BC_ASSERT(stranded()); + + // Stop is never deferred, and a null throttle implies no rate limit. + if (!throttle_ || stopped()) + return {}; + + const auto allocated = to_allocation(bytes, rate_limit_); + const auto consumed = steady_clock::now() - start; + return consumed < allocated ? allocated - consumed : + steady_clock::duration{}; +} + +void proxy::handle_metered(const code& ec, size_t bytes, + const steady_clock::time_point& start, + const count_handler& handler) NOEXCEPT +{ + BC_ASSERT(stranded()); + total_ = system::ceilinged_add(total_.load(), bytes); + + // A send that consumed its full allocation is not deferred. + const auto delay = unconsumed(bytes, start); + if (is_zero(delay.count())) + { + handler(ec, bytes); + return; + } + + // Handler is posted to the strand, and fired by stop (canceled). + throttle_->start(std::bind(&proxy::handle_charge, + shared_from_this(), _1, ec, bytes, handler), delay); +} + +void proxy::handle_charge(const code&, const code& ec, size_t bytes, + const count_handler& handler) NOEXCEPT +{ + BC_ASSERT(stranded()); + + // The timer code is discarded, as the send result is what is reported. + handler(ec, bytes); +} + +BC_POP_WARNING() +BC_POP_WARNING() +BC_POP_WARNING() + } // namespace network } // namespace libbitcoin diff --git a/test/net/proxy.cpp b/test/net/proxy.cpp index d012a8370..1798f14b2 100644 --- a/test/net/proxy.cpp +++ b/test/net/proxy.cpp @@ -30,13 +30,90 @@ class mock_proxy proxy::subscribe_stop(std::move(handler)); } + // Call must be stranded. + steady_clock::duration unconsumed1(size_t bytes, + const steady_clock::time_point& start) const NOEXCEPT + { + return proxy::unconsumed(bytes, start); + } + // Access protected constructor. - mock_proxy(const socket::ptr& socket) NOEXCEPT - : proxy(socket) + mock_proxy(const socket::ptr& socket, uint32_t rate_limit=0) NOEXCEPT + : proxy(socket, rate_limit) { } }; +// Obtain the deferral for a send of bytes that started at the given offset. +static milliseconds get_unconsumed(uint32_t rate_limit, size_t bytes, + const steady_clock::duration& elapsed) NOEXCEPT +{ + const logger log{}; + threadpool pool(1); + socket::parameters params{ .maximum_request = 42 }; + auto socket_ptr = std::make_shared(log, pool.service(), std::move(params)); + auto proxy_ptr = std::make_shared(socket_ptr, rate_limit); + + std::promise deferral; + boost::asio::post(proxy_ptr->strand(), [=, &deferral]() NOEXCEPT + { + deferral.set_value(std::chrono::duration_cast( + proxy_ptr->unconsumed1(bytes, steady_clock::now() - elapsed))); + }); + + const auto result = deferral.get_future().get(); + proxy_ptr->stop(error::invalid_magic); + pool.stop(); + return result; +} + +BOOST_AUTO_TEST_CASE(proxy__unconsumed__unlimited__zero) +{ + BOOST_REQUIRE_EQUAL(get_unconsumed(0, 1000, seconds(0)), milliseconds(0)); +} + +BOOST_AUTO_TEST_CASE(proxy__unconsumed__untransmitted__full_allocation) +{ + // 1000 bytes at 1000 bytes/second is allocated one second, unconsumed. + const auto deferral = get_unconsumed(1000, 1000, seconds(0)); + BOOST_REQUIRE_GT(deferral, milliseconds(900)); + BOOST_REQUIRE_LE(deferral, milliseconds(1000)); +} + +BOOST_AUTO_TEST_CASE(proxy__unconsumed__partly_transmitted__remainder) +{ + // Transmission consumed 250ms of the 1000ms allocation. + const auto deferral = get_unconsumed(1000, 1000, milliseconds(250)); + BOOST_REQUIRE_GT(deferral, milliseconds(650)); + BOOST_REQUIRE_LE(deferral, milliseconds(750)); +} + +BOOST_AUTO_TEST_CASE(proxy__unconsumed__fully_transmitted__zero) +{ + // Transmission was slower than the rate limit, so there is nothing to add. + BOOST_REQUIRE_EQUAL(get_unconsumed(1000, 1000, seconds(2)), + milliseconds(0)); +} + +BOOST_AUTO_TEST_CASE(proxy__unconsumed__stopped__zero) +{ + const logger log{}; + threadpool pool(1); + socket::parameters params{ .maximum_request = 42 }; + auto socket_ptr = std::make_shared(log, pool.service(), std::move(params)); + auto proxy_ptr = std::make_shared(socket_ptr, 1000); + proxy_ptr->stop(error::invalid_magic); + + std::promise deferral; + boost::asio::post(proxy_ptr->strand(), [=, &deferral]() NOEXCEPT + { + deferral.set_value( + proxy_ptr->unconsumed1(1000, steady_clock::now()).count()); + }); + + BOOST_REQUIRE_EQUAL(deferral.get_future().get(), 0); +} + BOOST_AUTO_TEST_CASE(proxy__paused__default__true) { const logger log{}; diff --git a/test/settings.cpp b/test/settings.cpp index 05ca1532f..4423322d4 100644 --- a/test/settings.cpp +++ b/test/settings.cpp @@ -53,7 +53,7 @@ BOOST_AUTO_TEST_CASE(settings__construct__default__expected) BOOST_REQUIRE_EQUAL(instance.handshake_timeout_seconds, 15u); BOOST_REQUIRE_EQUAL(instance.channel_heartbeat_minutes, 5u); BOOST_REQUIRE_EQUAL(instance.maximum_skew_minutes, 120u); - BOOST_REQUIRE_EQUAL(instance.rate_limit, 1024u); + BOOST_REQUIRE_EQUAL(instance.rate_limit, 0u); BOOST_REQUIRE_EQUAL(instance.user_agent, BC_USER_AGENT); BOOST_REQUIRE(instance.path.empty()); BOOST_REQUIRE(instance.blacklists.empty()); From aeb4229ce64412728183c3bbe812f335955801b2 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 3 Aug 2026 21:31:59 -0400 Subject: [PATCH 2/4] Regenerate artifacts for secp256k1 0.8.0.0. --- builds/cmake/install-cmake.sh | 2 +- builds/cmake/install-presets.sh | 2 +- builds/gnu/configure.ac | 2 +- builds/gnu/install-gnu.sh | 2 +- .../libbitcoin-network-test/libbitcoin-network-test.vcxproj | 4 ++-- builds/msvc/vs2026/libbitcoin-network-test/packages.config | 2 +- .../msvc/vs2026/libbitcoin-network/libbitcoin-network.vcxproj | 4 ++-- builds/msvc/vs2026/libbitcoin-network/packages.config | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/builds/cmake/install-cmake.sh b/builds/cmake/install-cmake.sh index 3b39995b3..b54c820e1 100755 --- a/builds/cmake/install-cmake.sh +++ b/builds/cmake/install-cmake.sh @@ -71,7 +71,7 @@ if [[ -z ${secp256k1_OWNER} ]]; then secp256k1_OWNER="bitcoin-core" fi if [[ -z ${secp256k1_TAG} ]]; then - secp256k1_TAG="v0.7.0" + secp256k1_TAG="v0.8.0" fi if [[ -z ${UltrafastSecp256k1_OWNER} ]]; then diff --git a/builds/cmake/install-presets.sh b/builds/cmake/install-presets.sh index 33fd3cf56..3cca40c25 100755 --- a/builds/cmake/install-presets.sh +++ b/builds/cmake/install-presets.sh @@ -61,7 +61,7 @@ if [[ -z ${secp256k1_OWNER} ]]; then secp256k1_OWNER="bitcoin-core" fi if [[ -z ${secp256k1_TAG} ]]; then - secp256k1_TAG="v0.7.0" + secp256k1_TAG="v0.8.0" fi if [[ -z ${UltrafastSecp256k1_OWNER} ]]; then diff --git a/builds/gnu/configure.ac b/builds/gnu/configure.ac index 53187daf8..e318269dd 100644 --- a/builds/gnu/configure.ac +++ b/builds/gnu/configure.ac @@ -97,7 +97,7 @@ AC_SUBST([pkgconfigdir],[${with_pkgconfigdir}]) AC_MSG_CHECKING([--enable-ndebug option]) AC_ARG_ENABLE([ndebug], AS_HELP_STRING([--enable-ndebug], - [Compile with NDEBUG assertion. @<:@default=yes@:>@]), + [Compile with NDEBUG (no debug assertions). @<:@default=yes@:>@]), [enable_ndebug=$enableval], [enable_ndebug=yes]) AC_MSG_RESULT([$enable_ndebug]) diff --git a/builds/gnu/install-gnu.sh b/builds/gnu/install-gnu.sh index cec6e58c2..af3f30f15 100755 --- a/builds/gnu/install-gnu.sh +++ b/builds/gnu/install-gnu.sh @@ -71,7 +71,7 @@ if [[ -z ${secp256k1_OWNER} ]]; then secp256k1_OWNER="bitcoin-core" fi if [[ -z ${secp256k1_TAG} ]]; then - secp256k1_TAG="v0.7.0" + secp256k1_TAG="v0.8.0" fi if [[ -z ${UltrafastSecp256k1_OWNER} ]]; then diff --git a/builds/msvc/vs2026/libbitcoin-network-test/libbitcoin-network-test.vcxproj b/builds/msvc/vs2026/libbitcoin-network-test/libbitcoin-network-test.vcxproj index de20c95c6..051c31209 100644 --- a/builds/msvc/vs2026/libbitcoin-network-test/libbitcoin-network-test.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-network-test/libbitcoin-network-test.vcxproj @@ -422,7 +422,7 @@ - + @@ -438,7 +438,7 @@ - + diff --git a/builds/msvc/vs2026/libbitcoin-network-test/packages.config b/builds/msvc/vs2026/libbitcoin-network-test/packages.config index 241bbaedc..2d57cf7b2 100644 --- a/builds/msvc/vs2026/libbitcoin-network-test/packages.config +++ b/builds/msvc/vs2026/libbitcoin-network-test/packages.config @@ -16,5 +16,5 @@ - + diff --git a/builds/msvc/vs2026/libbitcoin-network/libbitcoin-network.vcxproj b/builds/msvc/vs2026/libbitcoin-network/libbitcoin-network.vcxproj index 1e3285e68..4ade976d1 100644 --- a/builds/msvc/vs2026/libbitcoin-network/libbitcoin-network.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-network/libbitcoin-network.vcxproj @@ -598,7 +598,7 @@ - + @@ -613,7 +613,7 @@ - + diff --git a/builds/msvc/vs2026/libbitcoin-network/packages.config b/builds/msvc/vs2026/libbitcoin-network/packages.config index 5efab0603..ae997d1fb 100644 --- a/builds/msvc/vs2026/libbitcoin-network/packages.config +++ b/builds/msvc/vs2026/libbitcoin-network/packages.config @@ -15,5 +15,5 @@ - + From 497f86df31a5504cc96e34e147fd5c4497d463fb Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 3 Aug 2026 23:14:29 -0400 Subject: [PATCH 3/4] Update secp256k1_vc145 to nuget normalized version 0.8.0. Co-Authored-By: Claude Fable 5 --- .../libbitcoin-network-test/libbitcoin-network-test.vcxproj | 4 ++-- builds/msvc/vs2026/libbitcoin-network-test/packages.config | 2 +- .../msvc/vs2026/libbitcoin-network/libbitcoin-network.vcxproj | 4 ++-- builds/msvc/vs2026/libbitcoin-network/packages.config | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/builds/msvc/vs2026/libbitcoin-network-test/libbitcoin-network-test.vcxproj b/builds/msvc/vs2026/libbitcoin-network-test/libbitcoin-network-test.vcxproj index 051c31209..2b7453c0f 100644 --- a/builds/msvc/vs2026/libbitcoin-network-test/libbitcoin-network-test.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-network-test/libbitcoin-network-test.vcxproj @@ -422,7 +422,7 @@ - + @@ -438,7 +438,7 @@ - + diff --git a/builds/msvc/vs2026/libbitcoin-network-test/packages.config b/builds/msvc/vs2026/libbitcoin-network-test/packages.config index 2d57cf7b2..28d417f8c 100644 --- a/builds/msvc/vs2026/libbitcoin-network-test/packages.config +++ b/builds/msvc/vs2026/libbitcoin-network-test/packages.config @@ -16,5 +16,5 @@ - + diff --git a/builds/msvc/vs2026/libbitcoin-network/libbitcoin-network.vcxproj b/builds/msvc/vs2026/libbitcoin-network/libbitcoin-network.vcxproj index 4ade976d1..02af753cd 100644 --- a/builds/msvc/vs2026/libbitcoin-network/libbitcoin-network.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-network/libbitcoin-network.vcxproj @@ -598,7 +598,7 @@ - + @@ -613,7 +613,7 @@ - + diff --git a/builds/msvc/vs2026/libbitcoin-network/packages.config b/builds/msvc/vs2026/libbitcoin-network/packages.config index ae997d1fb..b8c3a66ec 100644 --- a/builds/msvc/vs2026/libbitcoin-network/packages.config +++ b/builds/msvc/vs2026/libbitcoin-network/packages.config @@ -15,5 +15,5 @@ - + From 6135b958cca77e74aec4d855049f2530921fddcf Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 4 Aug 2026 01:43:34 -0400 Subject: [PATCH 4/4] Add per service rate limit config (most restrictive controls). --- include/bitcoin/network/channels/channel.hpp | 6 +++ include/bitcoin/network/settings.hpp | 5 ++ src/channels/channel.cpp | 12 ++++- test/channels/channel.cpp | 48 ++++++++++++++++++++ test/settings.cpp | 1 + 5 files changed, 71 insertions(+), 1 deletion(-) diff --git a/include/bitcoin/network/channels/channel.hpp b/include/bitcoin/network/channels/channel.hpp index 394e8e859..199b466c8 100644 --- a/include/bitcoin/network/channels/channel.hpp +++ b/include/bitcoin/network/channels/channel.hpp @@ -93,6 +93,12 @@ class BCT_API channel const options_t& options() const NOEXCEPT; protected: + /// The network and service rate limits overlap, so the more restrictive + /// applies. Zero is unlimited, and therefore the less restrictive, so the + /// maximum applies if either is zero (zero only if both are zero). + static uint32_t rate_limited(const settings_t& settings, + const options_t& options) NOEXCEPT; + /// Construct a channel to encapsulated and communicate on the socket. channel(const logger& log, const socket::ptr& socket, uint64_t identifier, const settings_t& settings, const options_t& options) NOEXCEPT; diff --git a/include/bitcoin/network/settings.hpp b/include/bitcoin/network/settings.hpp index 79e1777a9..1a7be297b 100644 --- a/include/bitcoin/network/settings.hpp +++ b/include/bitcoin/network/settings.hpp @@ -75,6 +75,10 @@ struct BCT_API settings uint32_t maximum_request{ maximum_request_default }; uint32_t minimum_buffer{ maximum_request_default }; + /// Service send rate limit, overlapping the network rate limit (see + /// settings::rate_limited). Zero is unlimited. + uint32_t rate_limit{ 0 }; + /// Helpers. virtual bool enabled() const NOEXCEPT; virtual steady_clock::duration inactivity() const NOEXCEPT; @@ -301,6 +305,7 @@ struct BCT_API settings /// Bytes/second allocated to each channel for sending, zero is unlimited. /// A send is deferred by the unconsumed portion of its byte allocation, /// which the next send of the channel cannot start until it expires. + /// Overlaps tcp_server::rate_limit (see settings::rate_limited). uint32_t rate_limit{ 0 }; std::string user_agent{ BC_USER_AGENT }; std::filesystem::path path{}; diff --git a/src/channels/channel.cpp b/src/channels/channel.cpp index eb35b6f60..1f2a54edd 100644 --- a/src/channels/channel.cpp +++ b/src/channels/channel.cpp @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -41,12 +42,21 @@ inline deadline::ptr make_timer(const logger& log, asio::strand& strand, emplace_shared(log, strand, span) : nullptr; } +// protected/static +uint32_t channel::rate_limited(const settings_t& settings, + const options_t& options) NOEXCEPT +{ + return to_bool(settings.rate_limit) && to_bool(options.rate_limit) ? + std::min(settings.rate_limit, options.rate_limit) : + std::max(settings.rate_limit, options.rate_limit); +} + // Protocols invoke channel stop for application layer protocol violations. // Channels invoke channel stop for channel timouts and communcation failures. channel::channel(const logger& log, const socket::ptr& socket, uint64_t identifier, const settings_t& settings, const options_t& options) NOEXCEPT - : proxy(socket, settings.rate_limit), + : proxy(socket, rate_limited(settings, options)), options_(options), settings_(settings), identifier_(identifier), diff --git a/test/channels/channel.cpp b/test/channels/channel.cpp index 8d5f3557d..c5eca68ab 100644 --- a/test/channels/channel.cpp +++ b/test/channels/channel.cpp @@ -31,8 +31,56 @@ struct accessor : channel(log, socket, identifier, settings, options) { } + + static uint32_t rate_limited1(const network::settings& settings, + const channel::options_t& options) NOEXCEPT + { + return channel::rate_limited(settings, options); + } }; +// rate_limited + +BOOST_AUTO_TEST_CASE(channel__rate_limited__both_zero__zero) +{ + settings set(bc::system::chain::selection::mainnet); + set.rate_limit = 0; + set.outbound.rate_limit = 0; + BOOST_REQUIRE_EQUAL(accessor::rate_limited1(set, set.outbound), 0u); +} + +BOOST_AUTO_TEST_CASE(channel__rate_limited__network_zero__service) +{ + settings set(bc::system::chain::selection::mainnet); + set.rate_limit = 0; + set.outbound.rate_limit = 42; + BOOST_REQUIRE_EQUAL(accessor::rate_limited1(set, set.outbound), 42u); +} + +BOOST_AUTO_TEST_CASE(channel__rate_limited__service_zero__network) +{ + settings set(bc::system::chain::selection::mainnet); + set.rate_limit = 42; + set.outbound.rate_limit = 0; + BOOST_REQUIRE_EQUAL(accessor::rate_limited1(set, set.outbound), 42u); +} + +BOOST_AUTO_TEST_CASE(channel__rate_limited__network_lesser__network) +{ + settings set(bc::system::chain::selection::mainnet); + set.rate_limit = 24; + set.outbound.rate_limit = 42; + BOOST_REQUIRE_EQUAL(accessor::rate_limited1(set, set.outbound), 24u); +} + +BOOST_AUTO_TEST_CASE(channel__rate_limited__service_lesser__service) +{ + settings set(bc::system::chain::selection::mainnet); + set.rate_limit = 42; + set.outbound.rate_limit = 24; + BOOST_REQUIRE_EQUAL(accessor::rate_limited1(set, set.outbound), 24u); +} + BOOST_AUTO_TEST_CASE(channel__stopped__default__false) { constexpr auto expected = 42u; diff --git a/test/settings.cpp b/test/settings.cpp index 4423322d4..49d7b5935 100644 --- a/test/settings.cpp +++ b/test/settings.cpp @@ -356,6 +356,7 @@ BOOST_AUTO_TEST_CASE(settings__tcp_server__defaults__expected) BOOST_REQUIRE_EQUAL(instance.inactivity_minutes, 10u); BOOST_REQUIRE_EQUAL(instance.expiration_minutes, 60u); BOOST_REQUIRE_EQUAL(instance.maximum_request, maximum_request); + BOOST_REQUIRE_EQUAL(instance.rate_limit, 0u); BOOST_REQUIRE(!instance.enabled()); BOOST_REQUIRE(instance.inactivity() == minutes(10)); BOOST_REQUIRE(instance.expiration() == minutes(60));