Add wire::json_writer, and use in some of ZMQ-PUB functions.
What changed, and why it matters
This commit refactors how some Monero ZeroMQ publication messages are serialized to JSON. It introduces a new internal JSON writer library and switches a few ZMQ-PUB message types over to it. The change is primarily a code cleanup and modernization; it does not appear to fix a known security bug or introduce an obvious new vulnerability. One small improvement is that serialization errors are now checked and logged instead of silently producing malformed output.
Treat as a routine refactoring commit. Reviewers should verify that the new JSON writer correctly escapes strings and encodes binary hashes as hex, and that error propagation does not break downstream ZMQ consumers. No urgent security response is indicated from the diff alone.
Security signals we found
Refactor of serialization code for externally-published ZMQ messages
New JSON writer uses RapidJSON with custom string/binary key handling
Serialization errors now returned and logged instead of ignored
No explicit security fix or vulnerability disclosure in commit message or diff
No input parsing path changed; this is output-only serialization
Evidence from the diff
The patch adds wire::json_writer and related helpers under contrib/epee/include/serialization/wire/ and contrib/epee/src/wire/, then converts zmq_pub.cpp to use the new framework for json-minimal-chain_main, json-full-miner_data, and json-minimal-txpool_add messages. It replaces hand-written rapidjson::Writer calls and INSERT_INTO_JSON_OBJECT macros with wire::object/wire::field definitions. The writer signatures change from void to std::error_code, and send_chain_main/send_miner_data/send_txpool_add now propagate serialization failures. The old JSON conversion for tx_block_template_backlog_entry is removed in favor of a wire definition. Tests are updated to cover the new miner-data output format.
Changed components
contrib/epee serialization library (new wire/json writer)src/rpc/zmq_pub.cpp ZMQ-PUB message generationsrc/serialization/json_object.cpp/h (removed tx_block_template_backlog_entry JSON helpers)src/cryptonote_core/cryptonote_tx_utils.cpp/h (added wire serialization for tx_block_template_backlog_entry)tests/unit_tests/zmq_rpc.cppInspect captured patch +1030 / −101
diff --git a/contrib/epee/include/serialization/wire.h b/contrib/epee/include/serialization/wire.h
index 8aad677..8e9d884 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<typename R, typename T> \
- std::error_code from_bytes(T&& source) \
- { return ::wire_read::from_bytes<R>(std::forward<T>(source), *this); } \
- \
template<typename W, typename T> \
std::error_code to_bytes(T& dest) const \
{ return ::wire_write::to_bytes<W>(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 0000000..f3b9483
--- /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 <string>
+#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<const char> 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 0000000..6415466
--- /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 <string>
+#include <system_error>
+#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<const char>, 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<typename T>
+ static auto from_bytes(const epee::span<const char> source, T& dest) -> decltype(dest.template from_bytes<input_type>(source))
+ {
+ return dest.template from_bytes<input_type>(source);
+ }
+
+ //! Only enabled if `T` has templated method `to_bytes`.
+ template<typename T>
+ static auto to_bytes(std::string& dest, const T& source) -> decltype(source.template to_bytes<output_type>(dest))
+ {
+ return source.template to_bytes<output_type>(dest);
+ }
+
+
+ // Parameters packs have lower precedence; above functions are preferred.
+
+ template<typename... T>
+ static std::error_code from_bytes(const epee::span<const char> source, T&... dest)
+ {
+ return convert_from_json(source, dest...); // ADL (searches every associated namespace)
+ }
+
+ template<typename... T>
+ static std::error_code to_bytes(std::string& dest, const T&... source)
+ {
+ return convert_to_json(dest, source...); // ADL (searches every associated namespace)
+ }
+
+ template<typename T>
+ 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 0000000..5cdab2f
--- /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 0000000..1b1483f
--- /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 <array>
+#include <cstdint>
+#include <limits>
+#include <rapidjson/writer.h>
+#include <string>
+#include <string_view>
+
+#include "serialization/wire/write.h"
+#include "span.h"
+
+namespace wire
+{
+ constexpr const std::size_t uint_to_string_size =
+ std::numeric_limits<std::uintmax_t>::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<string_stream> formatter_;
+ bool needs_flush_;
+
+ virtual void do_flush(epee::span<const char>);
+
+ //! 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<char, uint_to_string_size> 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<const std::uint8_t>) 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<const std::uint8_t>) 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<const char>) 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 0000000..228abf7
--- /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 <type_traits>
+#include <utility>
+
+#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<typename U>
+ struct range_
+ {
+ WIRE_DEFINE_CONVERSIONS()
+
+ using value_type = unwrap_reference_t<U>;
+
+ 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<typename F, typename T>
+ void write_bytes(F& format, const range_<T> self)
+ {
+ wire_write::array(format, self.get_value());
+ }
+
+ //! Links `value` with `range_`.
+ template<typename T>
+ inline constexpr range_<T> range(T value)
+ {
+ return {std::move(value)};
+ }
+
+ template<typename T>
+ struct is_optional_on_empty<range_<T>>
+ : std::true_type
+ {};
+} // wire
diff --git a/contrib/epee/include/serialization/wire/write.h b/contrib/epee/include/serialization/wire/write.h
index fd116f1..2602dcb 100644
--- a/contrib/epee/include/serialization/wire/write.h
+++ b/contrib/epee/include/serialization/wire/write.h
@@ -27,10 +27,10 @@
#pragma once
-#include <boost/utility/string_ref.hpp>
#include <boost/range/size.hpp>
#include <cstdint>
#include <iterator>
+#include <string_view>
#include <system_error>
#include <type_traits>
@@ -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<const std::uint8_t>) = 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<const std::uint8_t>) = 0;
virtual void end_object() = 0;
@@ -129,7 +129,7 @@ namespace wire
{ write_arithmetic(dest, source); }
template<typename W>
- 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<typename W, typename T>
@@ -245,7 +245,7 @@ namespace wire_write
}
template<typename W>
- 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 bfde1e2..1dc1573 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 0000000..7078118
--- /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 0000000..d966df7
--- /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 0000000..cb11abe
--- /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 0000000..a830f45
--- /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 <boost/container/small_vector.hpp>
+#include <stdexcept>
+
+#include "hex.h"
+
+namespace
+{
+ constexpr const unsigned flush_threshold = 100;
+ constexpr const unsigned max_buffer = 4096;
+
+ boost::container::small_vector<char, 256> json_hex(const epee::span<const std::uint8_t> source)
+ {
+ boost::container::small_vector<char, 256> 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<const char>)
+ {}
+
+ 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<char, uint_to_string_size> json_writer::to_string(const std::uintmax_t value) noexcept
+ {
+ static_assert(std::numeric_limits<std::uintmax_t>::max() <= std::numeric_limits<std::uint64_t>::max(), "bad uint conversion");
+ std::array<char, uint_to_string_size> 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<std::int64_t>::min() <= std::numeric_limits<std::intmax_t>::min(), "too small");
+ static_assert(std::numeric_limits<std::intmax_t>::max() <= std::numeric_limits<std::int64_t>::max(), "too large");
+ formatter_.Int64(source);
+ check_flush();
+ }
+ void json_writer::unsigned_integer(const std::uintmax_t source)
+ {
+ static_assert(std::numeric_limits<std::uintmax_t>::max() <= std::numeric_limits<std::uint64_t>::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<const std::uint8_t> 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<const std::uint8_t> 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<const char> 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 0000000..6977aaf
--- /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 3e0c54c..b8067f2 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 65a0ffc..52f05dd 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<typename F, typename T>
+ 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<tx_destination_entry> &destinations, const boost::optional<cryptonote::account_public_address>& 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 582b023..63b4e62 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<tx_destination_entry> &destinations, const boost::optional<cryptonote::account_public_address>& change_addr);
diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt
index 23adb6b..b7c21b9 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 cc9705d..b045bf1 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<const cryptonote::block>);
- 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<cryptonote::tx_block_template_backlog_entry>&);
- using txpool_writer = void(epee::byte_stream&, epee::span<const cryptonote::txpool_event>);
+ using chain_writer = std::error_code(epee::byte_stream&, std::uint64_t, epee::span<const cryptonote::block>);
+ 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<cryptonote::tx_block_template_backlog_entry>&);
+ using txpool_writer = std::error_code(epee::byte_stream&, epee::span<const cryptonote::txpool_event>);
+
+ 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<typename F>
struct context
@@ -106,23 +159,48 @@ namespace
//! \return `name:...` where `...` is JSON and `name` is directly copied (no quotes - not JSON).
template<typename T>
- void json_pub(epee::byte_stream& buf, const T value)
+ std::error_code json_pub(epee::byte_stream& buf, const T value)
{
rapidjson::Writer<epee::byte_stream> 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<const cryptonote::block> 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<cryptonote::tx_block_template_backlog_entry>& 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<epee::byte_stream>& 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))
- 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();
- }
-
- void toJsonValue(rapidjson::Writer<epee::byte_stream>& dest, const miner_data& self)
+ void write_bytes(wire::writer& dest, const minimal_txpool& self)
{
- 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();
+ 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<epee::byte_stream>& dest, const minimal_txpool& self)
+ std::error_code json_full_chain(epee::byte_stream& buf, const std::uint64_t height, const epee::span<const cryptonote::block> 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 json_pub(buf, blocks);
}
- void json_full_chain(epee::byte_stream& buf, const std::uint64_t height, const epee::span<const cryptonote::block> blocks)
- {
- json_pub(buf, blocks);
- }
-
- void json_minimal_chain(epee::byte_stream& buf, const std::uint64_t height, const epee::span<const cryptonote::block> blocks)
+ template<typename F>
+ std::error_code minimal_chain_format(epee::byte_stream& buf, const std::uint64_t height, const epee::span<const cryptonote::block> blocks)
{
- json_pub(buf, minimal_chain{height, blocks});
+ return F::to_bytes(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<cryptonote::tx_block_template_backlog_entry>& tx_backlog)
+ template<typename F>
+ 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<cryptonote::tx_block_template_backlog_entry>& 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<const cryptonote::txpool_event> txes)
+ std::error_code json_full_txpool(epee::byte_stream& buf, epee::span<const cryptonote::txpool_event> 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<const cryptonote::txpool_event> txes)
+ template<typename F>
+ std::error_code minimal_txpool_format(epee::byte_stream& buf, epee::span<const cryptonote::txpool_event> 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<context<chain_writer>, 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<wire::json>}
}};
constexpr const std::array<context<miner_writer>, 1> miner_contexts =
{{
- {u8"json-full-miner_data", json_miner_data},
+ {u8"json-full-miner_data", miner_data_format<wire::json>}
}};
constexpr const std::array<context<txpool_writer>, 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<wire::json>}
}};
template<typename T, std::size_t N>
@@ -288,20 +349,25 @@ namespace
epee::byte_stream buf{};
std::size_t last_offset = 0;
+ std::array<epee::byte_slice, N> out;
std::array<std::size_t, N> 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<U>(args)...);
+ const std::error_code error = contexts[i].generate_pub(buf, std::forward<U>(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<epee::byte_slice, N> 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 add9481..ba730e6 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 134bf1c..73ddaea 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<epee::byte_stream>& 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 5f23cb8..f2115a9 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<epee::byte_stream>& dest, const cryptonote::rpc::output_distribution& dist);
void fromJsonValue(const rapidjson::Value& val, cryptonote::rpc::output_distribution& dist);
-void toJsonValue(rapidjson::Writer<epee::byte_stream>& dest, const cryptonote::tx_block_template_backlog_entry& entry);
-void fromJsonValue(const rapidjson::Value& val, cryptonote::tx_block_template_backlog_entry& entry);
-
template <typename Map>
typename std::enable_if<sfinae::is_map_like<Map>::value, void>::type toJsonValue(rapidjson::Writer<epee::byte_stream>& dest, const Map& map);
diff --git a/tests/unit_tests/zmq_rpc.cpp b/tests/unit_tests/zmq_rpc.cpp
index 2997ef7..ec1d4d6 100644
--- a/tests/unit_tests/zmq_rpc.cpp
+++ b/tests/unit_tests/zmq_rpc.cpp
@@ -30,11 +30,14 @@
#include <boost/preprocessor/stringize.hpp>
#include <gtest/gtest.h>
#include <rapidjson/document.h>
+#include <rapidjson/ostreamwrapper.h>
+#include <rapidjson/prettywriter.h>
#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<OStreamWrapper> 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<const cryptonote::txpool_event> 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<const cryptonote::block> 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<cryptonote::tx_block_template_backlog_entry>& 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_block_template_backlog_entry>& tx_backlog);
+
+ const auto hash = crypto::rand<crypto::hash>();
+ const auto seed = crypto::rand<crypto::hash>();
+ const cryptonote::difficulty_type difficulty = 500;
+ const std::vector<cryptonote::tx_block_template_backlog_entry> txs = {
+ {crypto::rand<crypto::hash>(), 7545, 455},
+ {crypto::rand<crypto::hash>(), 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";
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.