From 108244cdf5c0d801b652d8bc744424e47e87f6e3 Mon Sep 17 00:00:00 2001 From: Lee *!* Clagett Date: Fri, 28 Nov 2025 21:20:52 -0500 Subject: [PATCH] Add wire::json_writer, and use in some of ZMQ-PUB functions. --- contrib/epee/include/serialization/wire.h | 9 +- .../epee/include/serialization/wire/json.h | 56 ++++++ .../include/serialization/wire/json/base.h | 93 +++++++++ .../include/serialization/wire/json/fwd.h | 42 ++++ .../include/serialization/wire/json/write.h | 168 ++++++++++++++++ .../serialization/wire/wrapper/range.h | 73 +++++++ .../epee/include/serialization/wire/write.h | 10 +- contrib/epee/src/CMakeLists.txt | 1 + contrib/epee/src/wire/CMakeLists.txt | 34 ++++ contrib/epee/src/wire/error.cpp | 97 +++++++++ contrib/epee/src/wire/json/CMakeLists.txt | 32 +++ contrib/epee/src/wire/json/write.cpp | 154 ++++++++++++++ contrib/epee/src/wire/write.cpp | 31 +++ src/crypto/hash.h | 7 + src/cryptonote_core/cryptonote_tx_utils.cpp | 11 + src/cryptonote_core/cryptonote_tx_utils.h | 2 + src/rpc/CMakeLists.txt | 3 + src/rpc/zmq_pub.cpp | 190 ++++++++++++------ src/rpc/zmq_pub.h | 4 +- src/serialization/json_object.cpp | 21 -- src/serialization/json_object.h | 3 - tests/unit_tests/zmq_rpc.cpp | 90 +++++++++ 22 files changed, 1030 insertions(+), 101 deletions(-) create mode 100644 contrib/epee/include/serialization/wire/json.h create mode 100644 contrib/epee/include/serialization/wire/json/base.h create mode 100644 contrib/epee/include/serialization/wire/json/fwd.h create mode 100644 contrib/epee/include/serialization/wire/json/write.h create mode 100644 contrib/epee/include/serialization/wire/wrapper/range.h create mode 100644 contrib/epee/src/wire/CMakeLists.txt create mode 100644 contrib/epee/src/wire/error.cpp create mode 100644 contrib/epee/src/wire/json/CMakeLists.txt create mode 100644 contrib/epee/src/wire/json/write.cpp create mode 100644 contrib/epee/src/wire/write.cpp diff --git a/contrib/epee/include/serialization/wire.h b/contrib/epee/include/serialization/wire.h index 8aad677fe..8e9d8847a 100644 --- a/contrib/epee/include/serialization/wire.h +++ b/contrib/epee/include/serialization/wire.h @@ -27,24 +27,17 @@ #pragma once +#include "serialization/wire/error.h" #include "serialization/wire/fwd.h" -#include "serialization/wire/read.h" #include "serialization/wire/write.h" //! Define functions that list fields in `type` (using virtual interface) #define WIRE_DEFINE_OBJECT(type, map) \ - void read_bytes(::wire::reader& source, type& dest) \ - { map(source, dest); } \ - \ void write_bytes(::wire::writer& dest, const type& source) \ { map(dest, source); } //! Define `from_bytes` and `to_bytes` for `this`. #define WIRE_DEFINE_CONVERSIONS() \ - template \ - std::error_code from_bytes(T&& source) \ - { return ::wire_read::from_bytes(std::forward(source), *this); } \ - \ template \ std::error_code to_bytes(T& dest) const \ { return ::wire_write::to_bytes(dest, *this); } diff --git a/contrib/epee/include/serialization/wire/json.h b/contrib/epee/include/serialization/wire/json.h new file mode 100644 index 000000000..f3b9483fe --- /dev/null +++ b/contrib/epee/include/serialization/wire/json.h @@ -0,0 +1,56 @@ +// Copyright (c) 2021, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include "serialization/wire/json/base.h" +#include "serialization/wire/json/fwd.h" +#include "serialization/wire/json/write.h" +#include "serialization/wire/write.h" +#include "span.h" + +//! Define functions that list fields in `type` (calls de-virtualized) +#define WIRE_JSON_DEFINE_OBJECT(type, map) \ + void read_bytes(::wire::json_reader& source, type& dest) \ + { map(source, dest); } \ + \ + void write_bytes(::wire::json_writer& dest, const type& source) \ + { map(dest, source); } + +//! Define functions that convert `type` to/from json bytes +#define WIRE_JSON_DEFINE_CONVERSION(type) \ + std::error_code convert_from_json(const ::epee::span source, type& dest) \ + { return ::wire_read::from_bytes<::wire::json_reader>(source, dest); } \ + \ + std::error_code convert_to_json(std::string& dest, const type& source) \ + { return ::wire_write::to_bytes<::wire::json_string_writer>(dest, source); } + +//! Define functions that convert `type::request` and `type::response` to/from json bytes +#define WIRE_JSON_DEFINE_COMMAND(type) \ + WIRE_JSON_DEFINE_CONVERSION(type::request) \ + WIRE_JSON_DEFINE_CONVERSION(type::response) diff --git a/contrib/epee/include/serialization/wire/json/base.h b/contrib/epee/include/serialization/wire/json/base.h new file mode 100644 index 000000000..6415466b4 --- /dev/null +++ b/contrib/epee/include/serialization/wire/json/base.h @@ -0,0 +1,93 @@ +// Copyright (c) 2022, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include "byte_stream.h" +#include "serialization/wire/json/fwd.h" +#include "span.h" + +//! Declare functions that convert `type` to/from json bytes +#define WIRE_JSON_DECLARE_CONVERSION(type) \ + std::error_code convert_from_json(::epee::span, type&); \ + std::error_code convert_to_json(std::string&, const type&) \ + +//! Declare functions that convert `type::request` and `type::response` to/from json bytes +#define WIRE_JSON_DECLARE_COMMAND(type) \ + WIRE_JSON_DECLARE_CONVERSION(type::request); \ + WIRE_JSON_DECLARE_CONVERSION(type::response) + +namespace wire +{ + struct json + { + using input_type = json_reader; + using output_type = json_string_writer; + + //! Only enabled if `T` has templated method `from_bytes`. + template + static auto from_bytes(const epee::span source, T& dest) -> decltype(dest.template from_bytes(source)) + { + return dest.template from_bytes(source); + } + + //! Only enabled if `T` has templated method `to_bytes`. + template + static auto to_bytes(std::string& dest, const T& source) -> decltype(source.template to_bytes(dest)) + { + return source.template to_bytes(dest); + } + + + // Parameters packs have lower precedence; above functions are preferred. + + template + static std::error_code from_bytes(const epee::span source, T&... dest) + { + return convert_from_json(source, dest...); // ADL (searches every associated namespace) + } + + template + static std::error_code to_bytes(std::string& dest, const T&... source) + { + return convert_to_json(dest, source...); // ADL (searches every associated namespace) + } + + template + static std::error_code to_bytes(epee::byte_stream& dest, const T& source) + { + std::string buf{}; + const std::error_code error = to_bytes(buf, source); + if (!error) + dest.write(epee::to_span(buf)); + return error; + } + }; +} + diff --git a/contrib/epee/include/serialization/wire/json/fwd.h b/contrib/epee/include/serialization/wire/json/fwd.h new file mode 100644 index 000000000..5cdab2f22 --- /dev/null +++ b/contrib/epee/include/serialization/wire/json/fwd.h @@ -0,0 +1,42 @@ +// Copyright (c) 2022, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +//! Declare function that list fields `type` (calls de-virtualized) +#define WIRE_JSON_DECLARE_OBJECT(type) \ + void read_bytes(::wire::json_reader&, type&); \ + void write_bytes(::wire::json_writer&, const type&) + +namespace wire +{ + struct json; + class json_reader; + struct json_string_writer; + class json_writer; +} + diff --git a/contrib/epee/include/serialization/wire/json/write.h b/contrib/epee/include/serialization/wire/json/write.h new file mode 100644 index 000000000..1b1483f1a --- /dev/null +++ b/contrib/epee/include/serialization/wire/json/write.h @@ -0,0 +1,168 @@ +// Copyright (c) 2020, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "serialization/wire/write.h" +#include "span.h" + +namespace wire +{ + constexpr const std::size_t uint_to_string_size = + std::numeric_limits::digits10 + 2; + + struct string_stream + { + using Ch = char; + std::string buffer_; + + void Put(const char c) { buffer_.push_back(c); } + static void Flush() noexcept {} + }; + + //! Compatability/optimization for rapidjson. + inline void PutReserve(string_stream& dest, const std::size_t length) + { + dest.buffer_.reserve(length); + } + + //! Compability/optimization for rapidjson. + inline void PutN(string_stream& dest, const std::uint8_t ch, const std::size_t count) + { + dest.buffer_.append(count, ch); + } + + //! Writes JSON tokens one-at-a-time for DOMless output. + class json_writer : public writer + { + string_stream stream_; + rapidjson::Writer formatter_; + bool needs_flush_; + + virtual void do_flush(epee::span); + + //! Flush written bytes to `do_flush(...)` if configured + void check_flush(); + + protected: + json_writer(bool needs_flush) + : writer(), stream_{}, formatter_(stream_), needs_flush_(needs_flush) + {} + + json_writer(std::string&& bytes) + : writer(), stream_{std::move(bytes)}, formatter_(stream_), needs_flush_(false) + {} + + //! \throw std::logic_error if incomplete JSON tree. \return JSON bytes + std::string take_json(); + + //! Flush bytes in local buffer to `do_flush(...)` + void flush() + { + do_flush(epee::to_span(stream_.buffer_)); + stream_.buffer_.clear(); + } + + public: + json_writer(const json_writer&) = delete; + virtual ~json_writer() noexcept; + json_writer& operator=(const json_writer&) = delete; + + //! JSON does not have length field for arrays + static constexpr std::false_type need_array_size() noexcept { return {}; } + + //! \return Null-terminated buffer containing uint as decimal ascii + static std::array to_string(std::uintmax_t) noexcept; + + //! \throw std::logic_error if incomplete JSON tree + void check_complete(); + + void boolean(bool) override final; + + void integer(std::intmax_t) override final; + void unsigned_integer(std::uintmax_t) override final; + + void real(double) override final; + + void string(std::string_view) override final; + void binary(epee::span) override final; + + void start_array(std::size_t) override final; + void end_array() override final; + + void start_object(std::size_t) override final; + void key(std::string_view) override final; + void binary_key(epee::span) override final; + void end_object() override final; + }; + + //! Buffers entire JSON message in memory + struct json_string_writer final : json_writer + { + //! Write json to an empty buffer. + explicit json_string_writer() + : json_string_writer(std::string{}) + {} + + //! Append json to existing `buffer`. + explicit json_string_writer(std::string&& buffer) + : json_writer(std::move(buffer)) + {} + + //! \throw std::logic_error if incomplete JSON tree \return JSON bytes + std::string take_buffer() + { + return json_writer::take_json(); + } + }; + + //! Periodically flushes JSON data to `std::ostream` + class json_stream_writer final : public json_writer + { + std::ostream& dest; + + virtual void do_flush(epee::span) override final; + public: + explicit json_stream_writer(std::ostream& dest) + : json_writer(true), dest(dest) + {} + + //! Flush remaining bytes to stream \throw std::logic_error if incomplete JSON tree + void finish() + { + check_complete(); + flush(); + } + }; +} diff --git a/contrib/epee/include/serialization/wire/wrapper/range.h b/contrib/epee/include/serialization/wire/wrapper/range.h new file mode 100644 index 000000000..228abf742 --- /dev/null +++ b/contrib/epee/include/serialization/wire/wrapper/range.h @@ -0,0 +1,73 @@ +// Copyright (c) 2025, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include + +#include "serialization/wire/field.h" +#include "serialization/wire/traits.h" +#include "serialization/wire.h" + +namespace wire +{ + //! A wrapper that tells `wire::writer`s to write the inner type as an array. + template + struct range_ + { + WIRE_DEFINE_CONVERSIONS() + + using value_type = unwrap_reference_t; + + U value; + + constexpr const value_type& get_value() const noexcept { return value; } + value_type& get_value() noexcept { return value; } + + // concept requirements for `is_optional_on_empty` + bool empty() const { return get_value().empty(); } + }; + + template + void write_bytes(F& format, const range_ self) + { + wire_write::array(format, self.get_value()); + } + + //! Links `value` with `range_`. + template + inline constexpr range_ range(T value) + { + return {std::move(value)}; + } + + template + struct is_optional_on_empty> + : std::true_type + {}; +} // wire diff --git a/contrib/epee/include/serialization/wire/write.h b/contrib/epee/include/serialization/wire/write.h index fd116f1a7..2602dcb4e 100644 --- a/contrib/epee/include/serialization/wire/write.h +++ b/contrib/epee/include/serialization/wire/write.h @@ -27,10 +27,10 @@ #pragma once -#include #include #include #include +#include #include #include @@ -73,14 +73,14 @@ namespace wire virtual void real(double) = 0; - virtual void string(boost::string_ref) = 0; + virtual void string(std::string_view) = 0; virtual void binary(epee::span) = 0; virtual void start_array(std::size_t) = 0; virtual void end_array() = 0; virtual void start_object(std::size_t) = 0; - virtual void key(boost::string_ref) = 0; + virtual void key(std::string_view) = 0; virtual void binary_key(epee::span) = 0; virtual void end_object() = 0; @@ -129,7 +129,7 @@ namespace wire { write_arithmetic(dest, source); } template - inline void write_bytes(W& dest, const boost::string_ref source) + inline void write_bytes(W& dest, const std::string_view source) { dest.string(source); } template @@ -245,7 +245,7 @@ namespace wire_write } template - inline void dynamic_object_key(W& dest, const boost::string_ref source) + inline void dynamic_object_key(W& dest, const std::string_view source) { dest.key(source); } diff --git a/contrib/epee/src/CMakeLists.txt b/contrib/epee/src/CMakeLists.txt index bfde1e2e5..1dc157379 100644 --- a/contrib/epee/src/CMakeLists.txt +++ b/contrib/epee/src/CMakeLists.txt @@ -87,3 +87,4 @@ target_include_directories(epee "${EPEE_INCLUDE_DIR_BASE}" "${OPENSSL_INCLUDE_DIR}") +add_subdirectory(wire) diff --git a/contrib/epee/src/wire/CMakeLists.txt b/contrib/epee/src/wire/CMakeLists.txt new file mode 100644 index 000000000..7078118b7 --- /dev/null +++ b/contrib/epee/src/wire/CMakeLists.txt @@ -0,0 +1,34 @@ +# Copyright (c) 2025, The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +set(wire_sources error.cpp write.cpp) + +add_library(wire ${wire_sources}) +target_link_libraries(wire epee) + +add_subdirectory(json) diff --git a/contrib/epee/src/wire/error.cpp b/contrib/epee/src/wire/error.cpp new file mode 100644 index 000000000..d966df79c --- /dev/null +++ b/contrib/epee/src/wire/error.cpp @@ -0,0 +1,97 @@ +// Copyright (c) 2021, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "serialization/wire/error.h" + +namespace wire +{ + namespace error + { + const char* get_string(const schema value) noexcept + { + switch (value) + { + default: + break; + + case schema::none: + return "No schema errors"; + case schema::array: + return "Schema expected array"; + case schema::array_max_element: + return "Schema expected array size to be smaller"; + case schema::array_min_size: + return "Schema expected minimum wire size per array element to be larger"; + case schema::binary: + return "Schema expected binary value of variable size"; + case schema::boolean: + return "Schema expected boolean value"; + case schema::enumeration: + return "Schema expected a specific of enumeration value(s)"; + case schema::fixed_binary: + return "Schema expected binary of fixed size"; + case schema::integer: + return "Schema expected integer value"; + case schema::invalid_key: + return "Schema does not allow object field key"; + case schema::larger_integer: + return "Schema expected a larger integer value"; + case schema::maximum_depth: + return "Schema hit maximum array+object depth tracking"; + case schema::missing_key: + return "Schema missing required field key"; + case schema::number: + return "Schema expected number (integer or float) value"; + case schema::object: + return "Schema expected object"; + case schema::smaller_integer: + return "Schema expected a smaller integer value"; + case schema::string: + return "Schema expected string"; + } + return "Unknown schema error"; + } + + const std::error_category& schema_category() noexcept + { + struct category final : std::error_category + { + virtual const char* name() const noexcept override final + { + return "wire::error::schema_category()"; + } + + virtual std::string message(int value) const override final + { + return get_string(schema(value)); + } + }; + static const category instance{}; + return instance; + } + } +} diff --git a/contrib/epee/src/wire/json/CMakeLists.txt b/contrib/epee/src/wire/json/CMakeLists.txt new file mode 100644 index 000000000..cb11abeb1 --- /dev/null +++ b/contrib/epee/src/wire/json/CMakeLists.txt @@ -0,0 +1,32 @@ +# Copyright (c) 2025, The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +set(wire-json_sources write.cpp) + +add_library(wire-json ${wire-json_sources}) +target_link_libraries(wire-json wire) diff --git a/contrib/epee/src/wire/json/write.cpp b/contrib/epee/src/wire/json/write.cpp new file mode 100644 index 000000000..a830f456d --- /dev/null +++ b/contrib/epee/src/wire/json/write.cpp @@ -0,0 +1,154 @@ +// Copyright (c) 2020, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "serialization/wire/json/write.h" + +#include +#include + +#include "hex.h" + +namespace +{ + constexpr const unsigned flush_threshold = 100; + constexpr const unsigned max_buffer = 4096; + + boost::container::small_vector json_hex(const epee::span source) + { + boost::container::small_vector buffer; + buffer.resize(source.size() * 2); + if (!epee::to_hex::buffer(epee::to_mut_span(buffer), source)) + throw std::logic_error{"Invalid buffer size for binary->hex conversion in json_writer"}; + return buffer; + } +} + +namespace wire +{ + void json_writer::do_flush(epee::span) + {} + + void json_writer::check_flush() + { + if (needs_flush_ && (max_buffer < stream_.buffer_.size() || stream_.buffer_.capacity() - stream_.buffer_.size() < flush_threshold)) + flush(); + } + + void json_writer::check_complete() + { + if (!formatter_.IsComplete()) + throw std::logic_error{"json_writer::take_json() failed with incomplete JSON tree"}; + } + std::string json_writer::take_json() + { + check_complete(); + std::string out{std::move(stream_.buffer_)}; + stream_.buffer_.clear(); + formatter_.Reset(stream_); + return out; + } + + json_writer::~json_writer() noexcept + {} + + std::array json_writer::to_string(const std::uintmax_t value) noexcept + { + static_assert(std::numeric_limits::max() <= std::numeric_limits::max(), "bad uint conversion"); + std::array buf{{}}; + rapidjson::internal::u64toa(std::uint64_t(value), buf.data()); + return buf; + } + + void json_writer::boolean(const bool source) + { + formatter_.Bool(source); + check_flush(); + } + + void json_writer::integer(const std::intmax_t source) + { + static_assert(std::numeric_limits::min() <= std::numeric_limits::min(), "too small"); + static_assert(std::numeric_limits::max() <= std::numeric_limits::max(), "too large"); + formatter_.Int64(source); + check_flush(); + } + void json_writer::unsigned_integer(const std::uintmax_t source) + { + static_assert(std::numeric_limits::max() <= std::numeric_limits::max(), "too large"); + formatter_.Uint64(source); + check_flush(); + } + void json_writer::real(const double source) + { + formatter_.Double(source); + check_flush(); + } + + void json_writer::string(const std::string_view source) + { + formatter_.String(source.data(), source.size()); + check_flush(); + } + void json_writer::binary(const epee::span source) + { + const auto buffer = json_hex(source); + string({buffer.data(), buffer.size()}); + } + + void json_writer::start_array(std::size_t) + { + formatter_.StartArray(); + } + void json_writer::end_array() + { + formatter_.EndArray(); + } + + void json_writer::start_object(std::size_t) + { + formatter_.StartObject(); + } + void json_writer::key(const std::string_view str) + { + formatter_.Key(str.data(), str.size()); + check_flush(); + } + void json_writer::binary_key(const epee::span source) + { + const auto buffer = json_hex(source); + key({buffer.data(), buffer.size()}); + } + void json_writer::end_object() + { + formatter_.EndObject(); + } + + void json_stream_writer::do_flush(const epee::span bytes) + { + dest.write(bytes.data(), bytes.size()); + } +} diff --git a/contrib/epee/src/wire/write.cpp b/contrib/epee/src/wire/write.cpp new file mode 100644 index 000000000..6977aafa8 --- /dev/null +++ b/contrib/epee/src/wire/write.cpp @@ -0,0 +1,31 @@ +// Copyright (c) 2025, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "serialization/wire/write.h" + +wire::writer::~writer() noexcept +{} diff --git a/src/crypto/hash.h b/src/crypto/hash.h index 3e0c54c6c..b8067f2d7 100644 --- a/src/crypto/hash.h +++ b/src/crypto/hash.h @@ -38,6 +38,7 @@ #include "generic-ops.h" #include "hex.h" #include "span.h" +#include "serialization/wire/traits.h" namespace crypto { @@ -108,3 +109,9 @@ namespace crypto { CRYPTO_MAKE_HASHABLE(hash) CRYPTO_MAKE_COMPARABLE(hash8) + +namespace wire +{ + WIRE_DECLARE_BLOB_NS(crypto::hash); + WIRE_DECLARE_BLOB_NS(crypto::hash8); +} diff --git a/src/cryptonote_core/cryptonote_tx_utils.cpp b/src/cryptonote_core/cryptonote_tx_utils.cpp index 65a0ffc6a..52f05dd82 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.cpp +++ b/src/cryptonote_core/cryptonote_tx_utils.cpp @@ -45,6 +45,7 @@ using namespace epee; #include "crypto/crypto.h" #include "crypto/hash.h" #include "ringct/rctSigs.h" +#include "serialization/wire.h" using namespace crypto; @@ -217,6 +218,16 @@ namespace cryptonote return true; } //--------------------------------------------------------------- + namespace + { + template + void map_backlog_entry(F& format, T& self) + { + wire::object(format, WIRE_FIELD(id), WIRE_FIELD(weight), WIRE_FIELD(fee)); + } + } + WIRE_DEFINE_OBJECT(tx_block_template_backlog_entry, map_backlog_entry); + //--------------------------------------------------------------- crypto::public_key get_destination_view_key_pub(const std::vector &destinations, const boost::optional& change_addr) { account_public_address addr = {null_pkey, null_pkey}; diff --git a/src/cryptonote_core/cryptonote_tx_utils.h b/src/cryptonote_core/cryptonote_tx_utils.h index 582b023df..63b4e6276 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.h +++ b/src/cryptonote_core/cryptonote_tx_utils.h @@ -32,6 +32,7 @@ #include "cryptonote_basic/cryptonote_format_utils.h" #include "ringct/rctOps.h" +#include "serialization/wire/fwd.h" namespace cryptonote { @@ -114,6 +115,7 @@ namespace cryptonote uint64_t weight; uint64_t fee; }; + WIRE_DECLARE_OBJECT(tx_block_template_backlog_entry); //--------------------------------------------------------------- crypto::public_key get_destination_view_key_pub(const std::vector &destinations, const boost::optional& change_addr); diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index 23adb6bba..b7c21b9f4 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -147,7 +147,9 @@ target_link_libraries(rpc_pub epee net cryptonote_basic + cryptonote_core serialization + wire-json ${Boost_THREAD_LIBRARY}) target_link_libraries(daemon_messages @@ -167,6 +169,7 @@ target_link_libraries(daemon_rpc_server version daemon_messages serialization + wire-json ${Boost_REGEX_LIBRARY} ${Boost_SYSTEM_LIBRARY} ${Boost_THREAD_LIBRARY} diff --git a/src/rpc/zmq_pub.cpp b/src/rpc/zmq_pub.cpp index cc9705d35..b045bf130 100644 --- a/src/rpc/zmq_pub.cpp +++ b/src/rpc/zmq_pub.cpp @@ -51,6 +51,10 @@ #include "serialization/json_object.h" #include "ringct/rctTypes.h" #include "cryptonote_core/cryptonote_tx_utils.h" +#include "serialization/wire.h" +#include "serialization/wire/adapted/vector.h" +#include "serialization/wire/json.h" +#include "serialization/wire/wrapper/range.h" #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "net.zmq" @@ -59,9 +63,58 @@ namespace { constexpr const char txpool_signal[] = "tx_signal"; - using chain_writer = void(epee::byte_stream&, std::uint64_t, epee::span); - using miner_writer = void(epee::byte_stream&, uint8_t, uint64_t, const crypto::hash&, const crypto::hash&, cryptonote::difficulty_type, uint64_t, uint64_t, const std::vector&); - using txpool_writer = void(epee::byte_stream&, epee::span); + using chain_writer = std::error_code(epee::byte_stream&, std::uint64_t, epee::span); + using miner_writer = std::error_code(epee::byte_stream&, uint8_t, uint64_t, const crypto::hash&, const crypto::hash&, cryptonote::difficulty_type, uint64_t, uint64_t, const std::vector&); + using txpool_writer = std::error_code(epee::byte_stream&, epee::span); + + namespace error + { + enum class code : int + { + none = 0, //!< Must be zero for `expect<..>` + bad_block_hash //!< Unable to compute block hash + }; + + //! \return Error message string. + const char* get_string(code value) noexcept + { + switch (value) + { + case code::none: + return "no error"; + case code::bad_block_hash: + return "Unable to hash block"; + default: + break; + }; + return "invalid zmq/pub error code"; + } + + //! \return Category for `code`. + const std::error_category& category() noexcept + { + struct category final : std::error_category + { + virtual const char* name() const noexcept override final + { + return "zmq_pub::error_category()"; + } + + virtual std::string message(int value) const override final + { + return get_string(code(value)); + } + }; + static const category instance{}; + return instance; + } + + //! \return Error code with `value` and `schema_category()`. + inline std::error_code make_error_code(const code value) noexcept + { + return std::error_code{int(value), category()}; + } + } template struct context @@ -106,23 +159,48 @@ namespace //! \return `name:...` where `...` is JSON and `name` is directly copied (no quotes - not JSON). template - void json_pub(epee::byte_stream& buf, const T value) + std::error_code json_pub(epee::byte_stream& buf, const T value) { rapidjson::Writer dest{buf}; using cryptonote::json::toJsonValue; toJsonValue(dest, value); + return {}; } //! Object for "minimal" block serialization struct minimal_chain { + WIRE_DEFINE_CONVERSIONS() + const std::uint64_t height; const epee::span blocks; }; + void write_bytes(wire::writer& dest, const minimal_chain& self) + { + namespace adapt = boost::adaptors; + + const auto to_block_id = [](const cryptonote::block& bl) + { + crypto::hash id; + if (!get_block_hash(bl, id)) + WIRE_DLOG_THROW(error::code::bad_block_hash, "ZMQ/Pub failure"); + return id; + }; + + assert(!self.blocks.empty()); // checked in zmq_pub::send_chain_main + + wire::object(dest, + wire::field("first_height", self.height), + wire::field("first_prev_id", std::cref(self.blocks[0].prev_id)), + wire::field("ids", wire::range(self.blocks | adapt::transformed(to_block_id))) + ); + } //! Object for miner data serialization struct miner_data { + WIRE_DEFINE_CONVERSIONS() + uint8_t major_version; uint64_t height; const crypto::hash& prev_id; @@ -132,115 +210,98 @@ namespace uint64_t already_generated_coins; const std::vector& tx_backlog; }; + void write_bytes(wire::writer& dest, const miner_data& self) + { + wire::object(dest, + WIRE_FIELD_COPY(major_version), + WIRE_FIELD_COPY(height), + WIRE_FIELD(prev_id), + WIRE_FIELD(seed_hash), + wire::field("difficulty", cryptonote::hex(self.diff)), + WIRE_FIELD_COPY(median_weight), + WIRE_FIELD_COPY(already_generated_coins), + WIRE_FIELD(tx_backlog) + ); + } //! Object for "minimal" tx serialization struct minimal_txpool { + WIRE_DEFINE_CONVERSIONS() + const cryptonote::transaction& tx; crypto::hash hash; uint64_t blob_size; uint64_t weight; uint64_t fee; }; - - void toJsonValue(rapidjson::Writer& dest, const minimal_chain& self) + void write_bytes(wire::writer& dest, const minimal_txpool& self) { - namespace adapt = boost::adaptors; - - const auto to_block_id = [](const cryptonote::block& bl) - { - crypto::hash id; - if (!get_block_hash(bl, id)) - MERROR("ZMQ/Pub failure: get_block_hash"); - return id; - }; - - assert(!self.blocks.empty()); // checked in zmq_pub::send_chain_main - - dest.StartObject(); - INSERT_INTO_JSON_OBJECT(dest, first_height, self.height); - INSERT_INTO_JSON_OBJECT(dest, first_prev_id, self.blocks[0].prev_id); - INSERT_INTO_JSON_OBJECT(dest, ids, (self.blocks | adapt::transformed(to_block_id))); - dest.EndObject(); + wire::object(dest, + wire::field("id", std::cref(self.hash)), + WIRE_FIELD_COPY(blob_size), + WIRE_FIELD_COPY(weight), + WIRE_FIELD_COPY(fee) + ); } - void toJsonValue(rapidjson::Writer& dest, const miner_data& self) + std::error_code json_full_chain(epee::byte_stream& buf, const std::uint64_t height, const epee::span blocks) { - dest.StartObject(); - INSERT_INTO_JSON_OBJECT(dest, major_version, self.major_version); - INSERT_INTO_JSON_OBJECT(dest, height, self.height); - INSERT_INTO_JSON_OBJECT(dest, prev_id, self.prev_id); - INSERT_INTO_JSON_OBJECT(dest, seed_hash, self.seed_hash); - INSERT_INTO_JSON_OBJECT(dest, difficulty, cryptonote::hex(self.diff)); - INSERT_INTO_JSON_OBJECT(dest, median_weight, self.median_weight); - INSERT_INTO_JSON_OBJECT(dest, already_generated_coins, self.already_generated_coins); - INSERT_INTO_JSON_OBJECT(dest, tx_backlog, self.tx_backlog); - dest.EndObject(); + return json_pub(buf, blocks); } - void toJsonValue(rapidjson::Writer& dest, const minimal_txpool& self) + template + std::error_code minimal_chain_format(epee::byte_stream& buf, const std::uint64_t height, const epee::span blocks) { - dest.StartObject(); - INSERT_INTO_JSON_OBJECT(dest, id, self.hash); - INSERT_INTO_JSON_OBJECT(dest, blob_size, self.blob_size); - INSERT_INTO_JSON_OBJECT(dest, weight, self.weight); - INSERT_INTO_JSON_OBJECT(dest, fee, self.fee); - dest.EndObject(); + return F::to_bytes(buf, minimal_chain{height, blocks}); } - void json_full_chain(epee::byte_stream& buf, const std::uint64_t height, const epee::span blocks) + template + std::error_code miner_data_format(epee::byte_stream& buf, uint8_t major_version, uint64_t height, const crypto::hash& prev_id, const crypto::hash& seed_hash, cryptonote::difficulty_type diff, uint64_t median_weight, uint64_t already_generated_coins, const std::vector& tx_backlog) { - json_pub(buf, blocks); - } - - void json_minimal_chain(epee::byte_stream& buf, const std::uint64_t height, const epee::span blocks) - { - json_pub(buf, minimal_chain{height, blocks}); - } - - void json_miner_data(epee::byte_stream& buf, uint8_t major_version, uint64_t height, const crypto::hash& prev_id, const crypto::hash& seed_hash, cryptonote::difficulty_type diff, uint64_t median_weight, uint64_t already_generated_coins, const std::vector& tx_backlog) - { - json_pub(buf, miner_data{major_version, height, prev_id, seed_hash, diff, median_weight, already_generated_coins, tx_backlog}); + return F::to_bytes(buf, miner_data{major_version, height, prev_id, seed_hash, diff, median_weight, already_generated_coins, tx_backlog}); } // boost::adaptors are in place "views" - no copy/move takes place // moving transactions (via sort, etc.), is expensive! - void json_full_txpool(epee::byte_stream& buf, epee::span txes) + std::error_code json_full_txpool(epee::byte_stream& buf, epee::span txes) { namespace adapt = boost::adaptors; const auto to_full_tx = [](const cryptonote::txpool_event& event) { return event.tx; }; - json_pub(buf, (txes | adapt::filtered(is_valid{}) | adapt::transformed(to_full_tx))); + return json_pub(buf, (txes | adapt::filtered(is_valid{}) | adapt::transformed(to_full_tx))); } - void json_minimal_txpool(epee::byte_stream& buf, epee::span txes) + template + std::error_code minimal_txpool_format(epee::byte_stream& buf, epee::span txes) { namespace adapt = boost::adaptors; const auto to_minimal_tx = [](const cryptonote::txpool_event& event) { return minimal_txpool{event.tx, event.hash, event.blob_size, event.weight, cryptonote::get_tx_fee(event.tx)}; }; - json_pub(buf, (txes | adapt::filtered(is_valid{}) | adapt::transformed(to_minimal_tx))); + + return F::to_bytes(buf, wire::range(txes | adapt::filtered(is_valid{}) | adapt::transformed(to_minimal_tx))); } constexpr const std::array, 2> chain_contexts = {{ {u8"json-full-chain_main", json_full_chain}, - {u8"json-minimal-chain_main", json_minimal_chain} + {u8"json-minimal-chain_main", minimal_chain_format} }}; constexpr const std::array, 1> miner_contexts = {{ - {u8"json-full-miner_data", json_miner_data}, + {u8"json-full-miner_data", miner_data_format} }}; constexpr const std::array, 2> txpool_contexts = {{ {u8"json-full-txpool_add", json_full_txpool}, - {u8"json-minimal-txpool_add", json_minimal_txpool} + {u8"json-minimal-txpool_add", minimal_txpool_format} }}; template @@ -288,20 +349,25 @@ namespace epee::byte_stream buf{}; std::size_t last_offset = 0; + std::array out; std::array offsets{{}}; for (std::size_t i = 0; i < N; ++i) { if (subs[i]) { write_header(buf, contexts[i].name); - contexts[i].generate_pub(buf, std::forward(args)...); + const std::error_code error = contexts[i].generate_pub(buf, std::forward(args)...); + if (error) + { + MERROR("Failed to serialize " << contexts[i].name << ": " << error.message()); + return out; + } offsets[i] = buf.size() - last_offset; last_offset = buf.size(); } } epee::byte_slice bytes{std::move(buf)}; - std::array out; for (std::size_t i = 0; i < N; ++i) out[i] = bytes.take_slice(offsets[i]); diff --git a/src/rpc/zmq_pub.h b/src/rpc/zmq_pub.h index add9481c6..ba730e63b 100644 --- a/src/rpc/zmq_pub.h +++ b/src/rpc/zmq_pub.h @@ -82,8 +82,8 @@ class zmq_pub //! Process a client subscription request (from XPUB sockets). Thread-safe. bool sub_request(const boost::string_ref message); - /*! Forward ZMQ messages sent to `relay` via `send_chain_main` or - `send_txpool_add` to `pub`. Used by `ZmqServer`. */ + /*! Forward ZMQ messages sent to `relay` via `send_chain_main`, + `send_txpool_add`, or `send_miner_data` to `pub`. Used by `ZmqServer`. */ bool relay_to_pub(void* relay, void* pub); /*! Send a `ZMQ_PUB` notification for a change to the main chain. diff --git a/src/serialization/json_object.cpp b/src/serialization/json_object.cpp index 134bf1c69..73ddaeaf2 100644 --- a/src/serialization/json_object.cpp +++ b/src/serialization/json_object.cpp @@ -1532,27 +1532,6 @@ void fromJsonValue(const rapidjson::Value& val, cryptonote::rpc::output_distribu GET_FROM_JSON_OBJECT(val, dist.data.base, base); } -void toJsonValue(rapidjson::Writer& dest, const cryptonote::tx_block_template_backlog_entry& entry) -{ - dest.StartObject(); - INSERT_INTO_JSON_OBJECT(dest, id, entry.id); - INSERT_INTO_JSON_OBJECT(dest, weight, entry.weight); - INSERT_INTO_JSON_OBJECT(dest, fee, entry.fee); - dest.EndObject(); -} - -void fromJsonValue(const rapidjson::Value& val, cryptonote::tx_block_template_backlog_entry& entry) -{ - if (!val.IsObject()) - { - throw WRONG_TYPE("json object"); - } - - GET_FROM_JSON_OBJECT(val, entry.id, id); - GET_FROM_JSON_OBJECT(val, entry.weight, weight); - GET_FROM_JSON_OBJECT(val, entry.fee, fee); -} - } // namespace json } // namespace cryptonote diff --git a/src/serialization/json_object.h b/src/serialization/json_object.h index 5f23cb87c..f2115a9f3 100644 --- a/src/serialization/json_object.h +++ b/src/serialization/json_object.h @@ -311,9 +311,6 @@ void fromJsonValue(const rapidjson::Value& val, cryptonote::rpc::DaemonInfo& inf void toJsonValue(rapidjson::Writer& dest, const cryptonote::rpc::output_distribution& dist); void fromJsonValue(const rapidjson::Value& val, cryptonote::rpc::output_distribution& dist); -void toJsonValue(rapidjson::Writer& dest, const cryptonote::tx_block_template_backlog_entry& entry); -void fromJsonValue(const rapidjson::Value& val, cryptonote::tx_block_template_backlog_entry& entry); - template typename std::enable_if::value, void>::type toJsonValue(rapidjson::Writer& dest, const Map& map); diff --git a/tests/unit_tests/zmq_rpc.cpp b/tests/unit_tests/zmq_rpc.cpp index 2997ef7e1..ec1d4d640 100644 --- a/tests/unit_tests/zmq_rpc.cpp +++ b/tests/unit_tests/zmq_rpc.cpp @@ -30,11 +30,14 @@ #include #include #include +#include +#include #include "cryptonote_basic/account.h" #include "cryptonote_basic/cryptonote_basic.h" #include "cryptonote_basic/events.h" #include "cryptonote_basic/cryptonote_format_utils.h" +#include "cryptonote_core/cryptonote_tx_utils.h" #include "json_serialization.h" #include "net/zmq.h" #include "rpc/message.h" @@ -47,6 +50,17 @@ if (!(__VA_ARGS__)) \ return testing::AssertionFailure() << BOOST_PP_STRINGIZE(__VA_ARGS__) +namespace rapidjson +{ + std::ostream& operator<<(std::ostream& out, const Document& src) + { + OStreamWrapper buffer{out}; + PrettyWriter writer{buffer}; + src.Accept(writer); + return out; + } +} + TEST(ZmqFullMessage, InvalidRequest) { EXPECT_THROW( @@ -138,6 +152,16 @@ namespace return out; } + testing::AssertionResult compare_json(const std::string expected_json, const rapidjson::Document& published) + { + rapidjson::Document expected; + expected.Parse(expected_json.c_str()); + MASSERT(!expected.HasParseError()); + if (expected != published) + return testing::AssertionFailure() << expected << " != " << published; + return testing::AssertionSuccess(); + } + testing::AssertionResult compare_full_txpool(epee::span events, const published_json& pub) { MASSERT(pub.first == "json-full-txpool_add"); @@ -193,6 +217,12 @@ namespace return testing::AssertionSuccess(); } + testing::AssertionResult compare_miner_data(const std::string expected, const published_json& pub) + { + MASSERT(pub.first == "json-full-miner_data"); + return compare_json(expected, pub.second); + } + testing::AssertionResult compare_full_block(const epee::span expected, const published_json& pub) { MASSERT(pub.first == "json-full-chain_main"); @@ -513,6 +543,66 @@ TEST_F(zmq_pub, JsonFullChain) EXPECT_TRUE(compare_full_block(epee::to_span(blocks), pubs.front())); } +TEST_F(zmq_pub, JsonFullMinerData) +{ +/* uint8_t major_version; + uint64_t height; + const crypto::hash& prev_id; + const crypto::hash& seed_hash; + cryptonote::difficulty_type diff; + uint64_t median_weight; + uint64_t already_generated_coins; + const std::vector& tx_backlog ; */ + + static constexpr const char topic[] = "\1json-full-miner_data"; + ASSERT_TRUE(sub_request(topic)); + + //std::size_t send_miner_data(uint8_t major_version, uint64_t height, const crypto::hash& prev_id, const crypto::hash& seed_hash, difficulty_type diff, uint64_t median_weight, uint64_t already_generated_coins, const std::vector& tx_backlog); + + const auto hash = crypto::rand(); + const auto seed = crypto::rand(); + const cryptonote::difficulty_type difficulty = 500; + const std::vector txs = { + {crypto::rand(), 7545, 455}, + {crypto::rand(), 755, 34545} + }; + const std::string expected = + R"({ + "major_version": 100, + "height": 200, + "prev_id": ")" + epee::to_hex::string(epee::as_byte_span(hash)) + R"(", + "seed_hash": ")" + epee::to_hex::string(epee::as_byte_span(seed)) + R"(", + "difficulty": ")" + cryptonote::hex(difficulty) + R"(", + "median_weight": 400, + "already_generated_coins": 10000, + "tx_backlog": [ + { + "id": ")" + epee::to_hex::string(epee::as_byte_span(txs.at(0).id)) + R"(", + "weight": 7545, + "fee": 455 + },{ + "id": ")" + epee::to_hex::string(epee::as_byte_span(txs.at(1).id)) + R"(", + "weight": 755, + "fee": 34545 + } + ] + })"; + EXPECT_EQ(1u, pub->send_miner_data(100, 200, hash, seed, difficulty, 400, 10000, txs)); + EXPECT_TRUE(pub->relay_to_pub(relay.get(), dummy_pub.get())); + + auto pubs = get_published(dummy_client.get()); + ASSERT_EQ(1u, pubs.size()); + EXPECT_TRUE(compare_miner_data(expected, pubs.front())); + + EXPECT_NO_THROW(cryptonote::listener::zmq_pub::miner_data{pub}(100, 200, hash, seed, difficulty, 400, 10000, txs)); + EXPECT_TRUE(pub->relay_to_pub(relay.get(), dummy_pub.get())); + + pubs = get_published(dummy_client.get()); + ASSERT_EQ(1u, pubs.size()); + EXPECT_TRUE(compare_miner_data(expected, pubs.front())); +} + + TEST_F(zmq_pub, JsonMinimalChain) { static constexpr const char topic[] = "\1json-minimal-chain_main";