From 5403ef253eb7747c77c0e909e92237cd9a8d4714 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Mon, 26 Jan 2026 22:04:07 -0500 Subject: [PATCH 1/6] Log style. --- include/bitcoin/network/impl/channels/channel_rpc.ipp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/bitcoin/network/impl/channels/channel_rpc.ipp b/include/bitcoin/network/impl/channels/channel_rpc.ipp index 255f77ee0..57918e6f5 100644 --- a/include/bitcoin/network/impl/channels/channel_rpc.ipp +++ b/include/bitcoin/network/impl/channels/channel_rpc.ipp @@ -107,7 +107,7 @@ inline void CLASS::handle_receive(const code& ec, size_t bytes, identity_ = request->message.id; version_ = request->message.jsonrpc; - LOGA("Rpc request: (" << bytes << ") bytes from [" << endpoint() << "] " + LOGA("Rpc request : (" << bytes << ") bytes [" << endpoint() << "] " << request->message.method << "(...)."); reading_ = false; @@ -187,7 +187,7 @@ inline void CLASS::handle_send(const code& ec, size_t bytes, // Typically a noop, but handshake may pause channel here. handler(ec); - LOGA("Rpc response: (" << bytes << ") bytes to [" << endpoint() << "] " + LOGA("Rpc response: (" << bytes << ") bytes [" << endpoint() << "] " << response->message.error.value_or(rpc::result_t{}).message); // Continue read loop (does not unpause or restart channel). From b55c327ea06eda4d8a2029c7a284c278ded4b637 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 27 Jan 2026 03:22:09 -0500 Subject: [PATCH 2/6] Define jsonrpc error codes and map thru boost_code. --- include/bitcoin/network/error.hpp | 17 ++++- .../network/impl/messages/json_body.ipp | 1 + src/error.cpp | 36 +++++++-- src/messages/rpc/body.cpp | 23 ++---- src/net/socket_rpc.cpp | 8 +- test/error.cpp | 74 +++++++++++++++++++ 6 files changed, 133 insertions(+), 26 deletions(-) diff --git a/include/bitcoin/network/error.hpp b/include/bitcoin/network/error.hpp index 479ac31be..74ff149a6 100644 --- a/include/bitcoin/network/error.hpp +++ b/include/bitcoin/network/error.hpp @@ -301,7 +301,7 @@ enum error_t : uint8_t exhausted_variants, unknown_name, - // rpc error + // query string parse error message_overflow, undefined_type, unexpected_method, @@ -310,7 +310,17 @@ enum error_t : uint8_t extra_named, missing_array, missing_object, - missing_parameter + missing_parameter, + + // json-rpc error + jsonrpc_requires_method, + jsonrpc_requires_params, + jsonrpc_v1_requires_array_params, + jsonrpc_v1_requires_id, + jsonrpc_reader_bad_buffer, + jsonrpc_reader_stall, + jsonrpc_reader_exception, + jsonrpc_writer_exception }; // No current need for error_code equivalence mapping. @@ -364,6 +374,9 @@ BCT_API code ws_to_error_code(const boost_code& ec) NOEXCEPT; /// 1:1 mapping of boost::json::error to network (or error::unknown). BCT_API code json_to_error_code(const boost_code& ec) NOEXCEPT; +/// 1:1 mapping of wrapped json-rpc codes back to network (or error::unknown). +BCT_API code rpc_to_error_code(const boost_code& ec) NOEXCEPT; + } // namespace error } // namespace network } // namespace libbitcoin diff --git a/include/bitcoin/network/impl/messages/json_body.ipp b/include/bitcoin/network/impl/messages/json_body.ipp index d0f1ce9d2..52cde6ece 100644 --- a/include/bitcoin/network/impl/messages/json_body.ipp +++ b/include/bitcoin/network/impl/messages/json_body.ipp @@ -114,6 +114,7 @@ void CLASS::reader::finish(boost_code& ec) NOEXCEPT catch (...) { // As a catch-all we blame alloc. + using namespace network::error; ec = to_http_code(http_error_t::bad_alloc); } diff --git a/src/error.cpp b/src/error.cpp index 4380395bc..4bf261bbb 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -272,7 +272,7 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { exhausted_variants, "exhausted variants" }, { unknown_name, "unknown name" }, - // rpc error + // query string parse error { message_overflow, "message overflow" }, { undefined_type, "undefined type" }, { unexpected_method, "unexpected method" }, @@ -281,7 +281,17 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { extra_named, "extra named" }, { missing_array, "missing array" }, { missing_object, "missing object" }, - { missing_parameter, "missing parameter" } + { missing_parameter, "missing parameter" }, + + // json-rpc error + { jsonrpc_requires_method, "jsonrpc requires method" }, + { jsonrpc_requires_params, "jsonrpc requires params" }, + { jsonrpc_v1_requires_array_params, "jsonrpc v1 requires array params" }, + { jsonrpc_v1_requires_id, "jsonrpc v1 requires id" }, + { jsonrpc_reader_bad_buffer, "jsonrpc reader bad buffer " }, + { jsonrpc_reader_stall, "jsonrpc reader stall " }, + { jsonrpc_reader_exception, "jsonrpc reader exception " }, + { jsonrpc_writer_exception, "jsonrpc writer exception " } }; DEFINE_ERROR_T_CATEGORY(error, "network", "network code") @@ -294,6 +304,7 @@ bool asio_is_canceled(const boost_code& ec) NOEXCEPT || ec == boost::asio::error::operation_aborted; } +// TODO: change to cast model (like others). // The success and operation_canceled codes are the only expected in normal // operation, so these are first, to optimize the case where asio_is_canceled // is not used. boost_code overloads the `==` operator to include category. @@ -440,15 +451,30 @@ code ssl_to_error_code(const boost_code& ec) NOEXCEPT switch (static_cast(ec.value())) { - case asio_ssl_stream_error_t::stream_truncated: return error::tls_stream_truncated; - case asio_ssl_stream_error_t::unspecified_system_error: return error::tls_unspecified_system_error; - case asio_ssl_stream_error_t::unexpected_result: return error::tls_unexpected_result; + case asio_ssl_stream_error_t::stream_truncated: + return error::tls_stream_truncated; + case asio_ssl_stream_error_t::unspecified_system_error: + return error::tls_unspecified_system_error; + case asio_ssl_stream_error_t::unexpected_result: + return error::tls_unexpected_result; // TODO: return ssl category generic error. default: return error::unknown; } } +// includes http codes +code rpc_to_error_code(const boost_code& ec) NOEXCEPT +{ + if (!ec) + return error::success; + + if (ec.category() == network::error::error_category::singleton) + return { static_cast(ec.value()) }; + + return http_to_error_code(ec); +} + // includes json codes code http_to_error_code(const boost_code& ec) NOEXCEPT { diff --git a/src/messages/rpc/body.cpp b/src/messages/rpc/body.cpp index c955d7771..81b859168 100644 --- a/src/messages/rpc/body.cpp +++ b/src/messages/rpc/body.cpp @@ -51,7 +51,7 @@ put(const buffer_type& buffer, boost_code& ec) NOEXCEPT if (is_null(buffer.data())) { - ec = to_system_code(boost_error_t::bad_address); + ec = code{ error::jsonrpc_reader_bad_buffer }; return {}; } @@ -67,7 +67,7 @@ put(const buffer_type& buffer, boost_code& ec) NOEXCEPT // There is no terminator (terminal). if (is_zero(parsed)) { - ec = to_http_code(http_error_t::end_of_stream); + ec = code{ error::jsonrpc_reader_stall }; return parsed; } @@ -115,8 +115,7 @@ finish(boost_code& ec) NOEXCEPT } catch (...) { - // As a catch-all we blame alloc. - ec = to_http_code(http_error_t::bad_alloc); + ec = code{ error::jsonrpc_reader_exception }; } // Post-parse semantic validation. @@ -124,24 +123,19 @@ finish(boost_code& ec) NOEXCEPT if (value_.message.jsonrpc == version::undefined) value_.message.jsonrpc = version::v1; - if (value_.message.method.empty() || - !value_.message.params.has_value()) + if (value_.message.method.empty()) { - ec = to_system_code(boost_error_t::bad_message); + ec = code{ error::jsonrpc_requires_method }; return; } if (value_.message.jsonrpc == version::v1) { if (!value_.message.id.has_value()) - ec = to_system_code(boost_error_t::bad_message); + ec = code{ error::jsonrpc_v1_requires_id }; else if (!std::holds_alternative( value_.message.params.value())) - ec = to_system_code(boost_error_t::bad_message); - - // TODO: v1 batch is not allowed. - ////else if (value_.message.is_batch()) - //// ec = to_system_code(boost_error_t::bad_message); + ec = code{ error::jsonrpc_v1_requires_array_params }; } } @@ -190,8 +184,7 @@ init(boost_code& ec) NOEXCEPT } catch (...) { - // As a catch-all we blame alloc. - ec = to_http_code(http_error_t::bad_alloc); + ec = code{ error::jsonrpc_writer_exception }; return; } diff --git a/src/net/socket_rpc.cpp b/src/net/socket_rpc.cpp index 48dd473e6..bb7e93530 100644 --- a/src/net/socket_rpc.cpp +++ b/src/net/socket_rpc.cpp @@ -60,8 +60,8 @@ void socket::do_rpc_read(boost_code ec, size_t total, const read_rpc::ptr& in, if (ec) { - // Json parser emits http and json codes. - const auto code = error::http_to_error_code(ec); + // Json parser emits rpc, http and json codes. + const auto code = error::rpc_to_error_code(ec); if (code == error::unknown) logx("rpc-read", ec); handler(code, total); return; @@ -138,8 +138,8 @@ void socket::do_rpc_write(boost_code ec, size_t total, const auto buffer = ec ? write_rpc::out_buffer{} : out->writer.get(ec); if (ec) { - // Json serializer emits http and json codes. - const auto code = error::http_to_error_code(ec); + // Json serializer emits rpc, http and json codes. + const auto code = error::rpc_to_error_code(ec); if (code == error::unknown) logx("rpc-write", ec); handler(code, total); return; diff --git a/test/error.cpp b/test/error.cpp index 16283039a..f66db2b49 100644 --- a/test/error.cpp +++ b/test/error.cpp @@ -2037,4 +2037,78 @@ BOOST_AUTO_TEST_CASE(error_t__code__missing_parameter__true_expected_message) BOOST_REQUIRE_EQUAL(ec.message(), "missing parameter"); } +// json-rpc error + +BOOST_AUTO_TEST_CASE(error_t__code__jsonrpc_requires_method__true_expected_message) +{ + constexpr auto value = error::jsonrpc_requires_method; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "jsonrpc requires method"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__jsonrpc_requires_params__true_expected_message) +{ + constexpr auto value = error::jsonrpc_requires_params; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "jsonrpc requires params"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__jsonrpc_v1_requires_array_params__true_expected_message) +{ + constexpr auto value = error::jsonrpc_v1_requires_array_params; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "jsonrpc v1 requires array params"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__jsonrpc_v1_requires_id__true_expected_message) +{ + constexpr auto value = error::jsonrpc_v1_requires_id; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "jsonrpc v1 requires id"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__jsonrpc_reader_bad_buffer__true_expected_message) +{ + constexpr auto value = error::jsonrpc_reader_bad_buffer; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "jsonrpc reader bad buffer"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__jsonrpc_reader_stall__true_expected_message) +{ + constexpr auto value = error::jsonrpc_reader_stall; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "jsonrpc reader stall"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__jsonrpc_reader_exception__true_expected_message) +{ + constexpr auto value = error::jsonrpc_reader_exception; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "jsonrpc reader exception"); +} + +BOOST_AUTO_TEST_CASE(error_t__code__jsonrpc_writer_exception__true_expected_message) +{ + constexpr auto value = error::jsonrpc_writer_exception; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "jsonrpc writer exception"); +} + BOOST_AUTO_TEST_SUITE_END() From f2329214c95c85d7357509ccf4fcb00bd1ef9e79 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 27 Jan 2026 03:23:18 -0500 Subject: [PATCH 3/6] Fix lack of json-rpc buffer reuse. --- include/bitcoin/network/net/socket.hpp | 5 +++-- src/net/socket_rpc.cpp | 11 +++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/bitcoin/network/net/socket.hpp b/include/bitcoin/network/net/socket.hpp index 40fa1b031..d40585751 100644 --- a/include/bitcoin/network/net/socket.hpp +++ b/include/bitcoin/network/net/socket.hpp @@ -224,13 +224,14 @@ class BCT_API socket { typedef std::shared_ptr ptr; - read_rpc(rpc::request& request) NOEXCEPT - : value{ request }, reader{ value } + read_rpc(rpc::request& request, http::flat_buffer& buffer) NOEXCEPT + : value{ request }, reader{ value }, buffer{ buffer } { } rpc::request& value; rpc::reader reader; + http::flat_buffer& buffer; }; struct write_rpc diff --git a/src/net/socket_rpc.cpp b/src/net/socket_rpc.cpp index bb7e93530..c904f3d5b 100644 --- a/src/net/socket_rpc.cpp +++ b/src/net/socket_rpc.cpp @@ -43,8 +43,7 @@ void socket::rpc_read(http::flat_buffer& buffer, rpc::request& request, count_handler&& handler) NOEXCEPT { boost_code ec{}; - const auto in = emplace_shared(request); - in->value.buffer = emplace_shared(buffer); + const auto in = emplace_shared(request, buffer); in->reader.init({}, ec); boost::asio::dispatch(strand_, @@ -68,7 +67,7 @@ void socket::do_rpc_read(boost_code ec, size_t total, const read_rpc::ptr& in, } VARIANT_DISPATCH_METHOD(get_tcp(), - async_read_some(in->value.buffer->prepare(size), + async_read_some(in->buffer.prepare(size), std::bind(&socket::handle_rpc_read, shared_from_this(), _1, _2, total, in, handler))); } @@ -93,12 +92,12 @@ void socket::handle_rpc_read(boost_code ec, size_t size, size_t total, if (!ec) { - in->value.buffer->commit(size); - const auto data = in->value.buffer->data(); + in->buffer.commit(size); + const auto data = in->buffer.data(); const auto parsed = in->reader.put(data, ec); if (!ec) { - in->value.buffer->consume(parsed); + in->buffer.consume(parsed); if (in->reader.done()) { in->reader.finish(ec); From a89559bd3bf05528cb8778547595731b18a32804 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 27 Jan 2026 03:24:18 -0500 Subject: [PATCH 4/6] Never call parser_.finish() when parser.done() (basically dead code). --- .../network/impl/messages/json_body.ipp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/include/bitcoin/network/impl/messages/json_body.ipp b/include/bitcoin/network/impl/messages/json_body.ipp index 52cde6ece..614103840 100644 --- a/include/bitcoin/network/impl/messages/json_body.ipp +++ b/include/bitcoin/network/impl/messages/json_body.ipp @@ -89,18 +89,17 @@ size_t CLASS::reader::put(const buffer_type& buffer, boost_code& ec) NOEXCEPT TEMPLATE void CLASS::reader::finish(boost_code& ec) NOEXCEPT { - using namespace network::error; - if (!done()) - { - ec = to_http_code(http_error_t::partial_message); - return; - } + ec.clear(); - BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) - parser_.finish(ec); - BC_POP_WARNING() + // Calling parser.finish() when parser.done() results in error::incomplete. + if (!parser_.done()) + { + BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + parser_.finish(ec); + BC_POP_WARNING() - if (ec) return; + if (ec) return; + } try { From 2e79ca82162fe9ce164641ed8d163d04bddc0ef1 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 27 Jan 2026 03:35:26 -0500 Subject: [PATCH 5/6] Limit jsonrpc params required to v1. --- include/bitcoin/network/error.hpp | 2 +- src/error.cpp | 2 +- src/messages/rpc/body.cpp | 12 ++++++------ test/error.cpp | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/include/bitcoin/network/error.hpp b/include/bitcoin/network/error.hpp index 74ff149a6..c996eb2cd 100644 --- a/include/bitcoin/network/error.hpp +++ b/include/bitcoin/network/error.hpp @@ -314,7 +314,7 @@ enum error_t : uint8_t // json-rpc error jsonrpc_requires_method, - jsonrpc_requires_params, + jsonrpc_v1_requires_params, jsonrpc_v1_requires_array_params, jsonrpc_v1_requires_id, jsonrpc_reader_bad_buffer, diff --git a/src/error.cpp b/src/error.cpp index 4bf261bbb..593f475b6 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -285,7 +285,7 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) // json-rpc error { jsonrpc_requires_method, "jsonrpc requires method" }, - { jsonrpc_requires_params, "jsonrpc requires params" }, + { jsonrpc_v1_requires_params, "jsonrpc v1 requires params" }, { jsonrpc_v1_requires_array_params, "jsonrpc v1 requires array params" }, { jsonrpc_v1_requires_id, "jsonrpc v1 requires id" }, { jsonrpc_reader_bad_buffer, "jsonrpc reader bad buffer " }, diff --git a/src/messages/rpc/body.cpp b/src/messages/rpc/body.cpp index 81b859168..8bba7b4fc 100644 --- a/src/messages/rpc/body.cpp +++ b/src/messages/rpc/body.cpp @@ -118,20 +118,20 @@ finish(boost_code& ec) NOEXCEPT ec = code{ error::jsonrpc_reader_exception }; } - // Post-parse semantic validation. - + // Set version default. if (value_.message.jsonrpc == version::undefined) value_.message.jsonrpc = version::v1; + // Post-parse semantic validation. if (value_.message.method.empty()) { ec = code{ error::jsonrpc_requires_method }; - return; } - - if (value_.message.jsonrpc == version::v1) + else if (value_.message.jsonrpc == version::v1) { - if (!value_.message.id.has_value()) + if (!value_.message.params.has_value()) + ec = code{ error::jsonrpc_v1_requires_params }; + else if (!value_.message.id.has_value()) ec = code{ error::jsonrpc_v1_requires_id }; else if (!std::holds_alternative( value_.message.params.value())) diff --git a/test/error.cpp b/test/error.cpp index f66db2b49..c9960b8dc 100644 --- a/test/error.cpp +++ b/test/error.cpp @@ -1954,7 +1954,7 @@ BOOST_AUTO_TEST_CASE(error_t__code__unknown_name__true_expected_message) BOOST_REQUIRE_EQUAL(ec.message(), "unknown name"); } -// rpc error +// query string parse error BOOST_AUTO_TEST_CASE(error_t__code__message_overflow__true_expected_message) { @@ -2048,13 +2048,13 @@ BOOST_AUTO_TEST_CASE(error_t__code__jsonrpc_requires_method__true_expected_messa BOOST_REQUIRE_EQUAL(ec.message(), "jsonrpc requires method"); } -BOOST_AUTO_TEST_CASE(error_t__code__jsonrpc_requires_params__true_expected_message) +BOOST_AUTO_TEST_CASE(error_t__code__jsonrpc_v1_requires_params__true_expected_message) { - constexpr auto value = error::jsonrpc_requires_params; + constexpr auto value = error::jsonrpc_v1_requires_params; const auto ec = code(value); BOOST_REQUIRE(ec); BOOST_REQUIRE(ec == value); - BOOST_REQUIRE_EQUAL(ec.message(), "jsonrpc requires params"); + BOOST_REQUIRE_EQUAL(ec.message(), "jsonrpc v1 requires params"); } BOOST_AUTO_TEST_CASE(error_t__code__jsonrpc_v1_requires_array_params__true_expected_message) From 549cda7f1f650ce2bc5d1b7770a9b16612c05be5 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Tue, 27 Jan 2026 04:05:26 -0500 Subject: [PATCH 6/6] Fix trailing whitespace break in jsonrpc error code text. --- src/error.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/error.cpp b/src/error.cpp index 593f475b6..b8b98ff99 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -288,10 +288,10 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { jsonrpc_v1_requires_params, "jsonrpc v1 requires params" }, { jsonrpc_v1_requires_array_params, "jsonrpc v1 requires array params" }, { jsonrpc_v1_requires_id, "jsonrpc v1 requires id" }, - { jsonrpc_reader_bad_buffer, "jsonrpc reader bad buffer " }, - { jsonrpc_reader_stall, "jsonrpc reader stall " }, - { jsonrpc_reader_exception, "jsonrpc reader exception " }, - { jsonrpc_writer_exception, "jsonrpc writer exception " } + { jsonrpc_reader_bad_buffer, "jsonrpc reader bad buffer" }, + { jsonrpc_reader_stall, "jsonrpc reader stall" }, + { jsonrpc_reader_exception, "jsonrpc reader exception" }, + { jsonrpc_writer_exception, "jsonrpc writer exception" } }; DEFINE_ERROR_T_CATEGORY(error, "network", "network code")