Websocket: Add linked_account to block content of confirmation topic

- Add proper `linked_account` to the block content for send, receive, and open blocks when the `include_linked_account` option is enabled.
- For change and epoch blocks, `linked_account` is set to "0", maintaining consistency with the `source` field in the blocks_info RPC response.

Co-authored-by: clemahieu <clemahieu@gmail.com>
Co-authored-by: gr0vity <gr0vity.dev@gmail.com>
This commit is contained in:
Exxenoz 2025-01-25 00:27:50 +01:00
commit 53fa92954e
7 changed files with 243 additions and 14 deletions

View file

@ -463,6 +463,177 @@ TEST (websocket, confirmation_options_votes)
}
}
TEST (websocket, confirmation_options_linked_account)
{
nano::test::system system;
nano::node_config config = system.default_config ();
config.websocket_config.enabled = true;
config.websocket_config.port = system.get_available_port ();
auto node1 (system.add_node (config));
std::atomic<bool> ack_ready{ false };
auto task1 = ([&ack_ready, config, &node1] () {
fake_websocket_client client (node1->websocket.server->listening_port ());
client.send_message (R"json({"action": "subscribe", "topic": "confirmation", "ack": "true", "options": {"confirmation_type": "active_quorum", "include_block": "true", "include_linked_account": "true"}})json");
client.await_ack ();
ack_ready = true;
EXPECT_EQ (1, node1->websocket.server->subscriber_count (nano::websocket::topic::confirmation));
return client.get_response ();
});
auto future1 = std::async (std::launch::async, task1);
ASSERT_TIMELY (10s, ack_ready);
// Confirm a state block for an in-wallet account
system.wallet (0)->insert_adhoc (nano::dev::genesis_key.prv);
nano::keypair key;
auto balance = nano::dev::constants.genesis_amount;
auto send_amount = node1->config.online_weight_minimum.number () + 1;
nano::block_hash previous (node1->latest (nano::dev::genesis_key.pub));
{
nano::state_block_builder builder;
balance -= send_amount;
auto send = builder
.account (nano::dev::genesis_key.pub)
.previous (previous)
.representative (nano::dev::genesis_key.pub)
.balance (balance)
.link (key.pub)
.sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub)
.work (*system.work.generate (previous))
.build ();
node1->process_active (send);
previous = send->hash ();
}
ASSERT_TIMELY_EQ (5s, future1.wait_for (0s), std::future_status::ready);
auto response1 = future1.get ();
ASSERT_TRUE (response1);
boost::property_tree::ptree event;
std::stringstream stream;
stream << response1.get ();
boost::property_tree::read_json (stream, event);
ASSERT_EQ (event.get<std::string> ("topic"), "confirmation");
try
{
boost::property_tree::ptree block_content = event.get_child ("message.block");
// Check if linked_account is present
ASSERT_EQ (1, block_content.count ("linked_account"));
// Make sure linked_account is non-zero.
ASSERT_NE ("0", block_content.get<std::string> ("linked_account"));
}
catch (std::runtime_error const & ex)
{
FAIL () << ex.what ();
}
ack_ready = false;
auto task2 = ([&ack_ready, config, &node1] () {
fake_websocket_client client (node1->websocket.server->listening_port ());
client.send_message (R"json({"action": "subscribe", "topic": "confirmation", "ack": "true", "options": {"confirmation_type": "active_quorum", "include_block": "true", "include_linked_account": "true"}})json");
client.await_ack ();
ack_ready = true;
EXPECT_EQ (1, node1->websocket.server->subscriber_count (nano::websocket::topic::confirmation));
return client.get_response ();
});
auto future2 = std::async (std::launch::async, task2);
ASSERT_TIMELY (10s, ack_ready);
// Quick-confirm a receive block
{
nano::state_block_builder builder;
balance = send_amount;
auto open = builder
.account (key.pub)
.previous (0)
.representative (nano::dev::genesis_key.pub)
.balance (balance)
.link (previous)
.sign (key.prv, key.pub)
.work (*system.work.generate (key.pub))
.build ();
node1->process_active (open);
previous = open->hash ();
}
ASSERT_TIMELY_EQ (5s, future2.wait_for (0s), std::future_status::ready);
auto response2 = future2.get ();
ASSERT_TRUE (response2);
boost::property_tree::ptree event2;
std::stringstream stream2;
stream2 << response2.get ();
boost::property_tree::read_json (stream2, event2);
ASSERT_EQ (event2.get<std::string> ("topic"), "confirmation");
try
{
boost::property_tree::ptree block_content = event2.get_child ("message.block");
// Check if linked_account is present
ASSERT_EQ (1, block_content.count ("linked_account"));
// Make sure linked_account is non-zero.
ASSERT_NE ("0", block_content.get<std::string> ("linked_account"));
}
catch (std::runtime_error const & ex)
{
FAIL () << ex.what ();
}
ack_ready = false;
auto task3 = ([&ack_ready, config, &node1] () {
fake_websocket_client client (node1->websocket.server->listening_port ());
client.send_message (R"json({"action": "subscribe", "topic": "confirmation", "ack": "true", "options": {"confirmation_type": "active_quorum", "include_block": "true", "include_linked_account": "true"}})json");
client.await_ack ();
ack_ready = true;
EXPECT_EQ (1, node1->websocket.server->subscriber_count (nano::websocket::topic::confirmation));
return client.get_response ();
});
auto future3 = std::async (std::launch::async, task3);
ASSERT_TIMELY (10s, ack_ready);
// Quick-confirm a change block
{
nano::state_block_builder builder;
auto change = builder
.account (key.pub)
.previous (previous)
.representative (key.pub)
.balance (balance)
.link (0)
.sign (key.prv, key.pub)
.work (*system.work.generate (previous))
.build ();
node1->process_active (change);
}
ASSERT_TIMELY_EQ (5s, future3.wait_for (0s), std::future_status::ready);
auto response3 = future3.get ();
ASSERT_TRUE (response3);
boost::property_tree::ptree event3;
std::stringstream stream3;
stream3 << response3.get ();
boost::property_tree::read_json (stream3, event3);
ASSERT_EQ (event3.get<std::string> ("topic"), "confirmation");
try
{
boost::property_tree::ptree block_content = event3.get_child ("message.block");
// Check if linked_account is present
ASSERT_EQ (1, block_content.count ("linked_account"));
// Make sure linked_account is zero.
ASSERT_EQ ("0", block_content.get<std::string> ("linked_account"));
}
catch (std::runtime_error const & ex)
{
FAIL () << ex.what ();
}
}
TEST (websocket, confirmation_options_sideband)
{
nano::test::system system;
@ -681,7 +852,7 @@ TEST (websocket, vote_options_type)
// Custom made votes for simplicity
auto vote = nano::test::make_vote (nano::dev::genesis_key, { nano::dev::genesis }, 0, 0);
nano::websocket::message_builder builder;
nano::websocket::message_builder builder{ node1->ledger };
auto msg (builder.vote_received (vote, nano::vote_code::replay));
node1->websocket.server->broadcast (msg);

View file

@ -41,7 +41,7 @@ nano::distributed_work::~distributed_work ()
{
if (!node_l->stopped && node_l->websocket.server && node_l->websocket.server->any_subscriber (nano::websocket::topic::work))
{
nano::websocket::message_builder builder;
nano::websocket::message_builder builder{ node_l->ledger };
if (status == work_generation_status::success)
{
node_l->websocket.server->broadcast (builder.work_generation (request.version, request.root.as_block_hash (), work_result, request.difficulty, node_l->default_difficulty (request.version), elapsed.value (), winner, bad_peers));

View file

@ -187,7 +187,7 @@ nano::node::node (std::shared_ptr<boost::asio::io_context> io_ctx_a, std::filesy
bootstrap_server{ *bootstrap_server_impl },
bootstrap_impl{ std::make_unique<nano::bootstrap_service> (config, ledger, ledger_notifications, block_processor, network, stats, logger) },
bootstrap{ *bootstrap_impl },
websocket_impl{ std::make_unique<nano::websocket_server> (config.websocket_config, observers, wallets, ledger, io_ctx, logger) },
websocket_impl{ std::make_unique<nano::websocket_server> (config.websocket_config, *this, observers, wallets, ledger, io_ctx, logger) },
websocket{ *websocket_impl },
epoch_upgrader_impl{ std::make_unique<nano::epoch_upgrader> (*this, ledger, store, network_params, logger) },
epoch_upgrader{ *epoch_upgrader_impl },
@ -236,7 +236,7 @@ nano::node::node (std::shared_ptr<boost::asio::io_context> io_ctx_a, std::filesy
{
if (websocket.server && websocket.server->any_subscriber (nano::websocket::topic::new_unconfirmed_block))
{
websocket.server->broadcast (nano::websocket::message_builder ().new_block_arrived (*context.block));
websocket.server->broadcast (nano::websocket::message_builder (ledger).new_block_arrived (*context.block));
}
}
}

View file

@ -7,6 +7,7 @@
#include <nano/lib/logging.hpp>
#include <nano/lib/work.hpp>
#include <nano/node/election_status.hpp>
#include <nano/node/node.hpp>
#include <nano/node/node_observers.hpp>
#include <nano/node/transport/channel.hpp>
#include <nano/node/vote_router.hpp>
@ -35,6 +36,7 @@ nano::websocket::confirmation_options::confirmation_options (boost::property_tre
include_block = options_a.get<bool> ("include_block", true);
include_election_info = options_a.get<bool> ("include_election_info", false);
include_election_info_with_votes = options_a.get<bool> ("include_election_info_with_votes", false);
include_linked_account = options_a.get<bool> ("include_linked_account", false);
include_sideband_info = options_a.get<bool> ("include_sideband_info", false);
confirmation_types = 0;
@ -97,6 +99,14 @@ nano::websocket::confirmation_options::confirmation_options (boost::property_tre
}
}
check_filter_empty ();
if (include_linked_account)
{
if (!include_block)
{
logger.warn (nano::log::type::websocket, "The option \"include_linked_account\" requires \"include_block\" to be set to true, as linked accounts are only retrieved when block content is included");
}
}
}
bool nano::websocket::confirmation_options::should_filter (nano::websocket::message const & message_a) const
@ -580,8 +590,9 @@ void nano::websocket::listener::stop ()
sessions.clear ();
}
nano::websocket::listener::listener (nano::logger & logger_a, nano::wallets & wallets_a, boost::asio::io_context & io_ctx_a, boost::asio::ip::tcp::endpoint endpoint_a) :
nano::websocket::listener::listener (nano::logger & logger_a, nano::node & node_a, nano::wallets & wallets_a, boost::asio::io_context & io_ctx_a, boost::asio::ip::tcp::endpoint endpoint_a) :
logger (logger_a),
node (node_a),
wallets (wallets_a),
acceptor (io_ctx_a),
socket (io_ctx_a)
@ -650,7 +661,7 @@ void nano::websocket::listener::on_accept (boost::system::error_code ec)
void nano::websocket::listener::broadcast_confirmation (std::shared_ptr<nano::block> const & block_a, nano::account const & account_a, nano::amount const & amount_a, std::string const & subtype, nano::election_status const & election_status_a, std::vector<nano::vote_with_weight_info> const & election_votes_a)
{
nano::websocket::message_builder builder;
nano::websocket::message_builder builder{ node.ledger };
nano::lock_guard<nano::mutex> lk (sessions_mutex);
boost::optional<nano::websocket::message> msg_with_block;
@ -711,6 +722,11 @@ void nano::websocket::listener::decrease_subscriber_count (nano::websocket::topi
count -= 1;
}
nano::websocket::message_builder::message_builder (nano::ledger & ledger) :
ledger{ ledger }
{
}
nano::websocket::message nano::websocket::message_builder::started_election (nano::block_hash const & hash_a)
{
nano::websocket::message message_l (nano::websocket::topic::started_election);
@ -794,6 +810,18 @@ nano::websocket::message nano::websocket::message_builder::block_confirmed (std:
{
boost::property_tree::ptree block_node_l;
block_a->serialize_json (block_node_l);
if (options_a.get_include_linked_account ())
{
auto linked_account = ledger.linked_account (ledger.tx_begin_read (), *block_a);
if (linked_account.has_value ())
{
block_node_l.add ("linked_account", linked_account.value ().to_account ());
}
else
{
block_node_l.add ("linked_account", "0");
}
}
if (!subtype.empty ())
{
block_node_l.add ("subtype", subtype);
@ -985,7 +1013,7 @@ std::string nano::websocket::message::to_string () const
* websocket_server
*/
nano::websocket_server::websocket_server (nano::websocket::config & config_a, nano::node_observers & observers_a, nano::wallets & wallets_a, nano::ledger & ledger_a, boost::asio::io_context & io_ctx_a, nano::logger & logger_a) :
nano::websocket_server::websocket_server (nano::websocket::config & config_a, nano::node & node_a, nano::node_observers & observers_a, nano::wallets & wallets_a, nano::ledger & ledger_a, boost::asio::io_context & io_ctx_a, nano::logger & logger_a) :
config{ config_a },
observers{ observers_a },
wallets{ wallets_a },
@ -999,7 +1027,7 @@ nano::websocket_server::websocket_server (nano::websocket::config & config_a, na
}
auto endpoint = nano::tcp_endpoint{ boost::asio::ip::make_address_v6 (config.address), config.port };
server = std::make_shared<nano::websocket::listener> (logger, wallets, io_ctx, endpoint);
server = std::make_shared<nano::websocket::listener> (logger, node_a, wallets, io_ctx, endpoint);
observers.blocks.add ([this] (nano::election_status const & status_a, std::vector<nano::vote_with_weight_info> const & votes_a, nano::account const & account_a, nano::amount const & amount_a, bool is_state_send_a, bool is_state_epoch_a) {
debug_assert (status_a.type != nano::election_status_type::ongoing);
@ -1036,7 +1064,7 @@ nano::websocket_server::websocket_server (nano::websocket::config & config_a, na
observers.active_started.add ([this] (nano::block_hash const & hash_a) {
if (server->any_subscriber (nano::websocket::topic::started_election))
{
nano::websocket::message_builder builder;
nano::websocket::message_builder builder{ ledger };
server->broadcast (builder.started_election (hash_a));
}
});
@ -1044,7 +1072,7 @@ nano::websocket_server::websocket_server (nano::websocket::config & config_a, na
observers.active_stopped.add ([this] (nano::block_hash const & hash_a) {
if (server->any_subscriber (nano::websocket::topic::stopped_election))
{
nano::websocket::message_builder builder;
nano::websocket::message_builder builder{ ledger };
server->broadcast (builder.stopped_election (hash_a));
}
});
@ -1052,7 +1080,7 @@ nano::websocket_server::websocket_server (nano::websocket::config & config_a, na
observers.telemetry.add ([this] (nano::telemetry_data const & telemetry_data, std::shared_ptr<nano::transport::channel> const & channel) {
if (server->any_subscriber (nano::websocket::topic::telemetry))
{
nano::websocket::message_builder builder;
nano::websocket::message_builder builder{ ledger };
server->broadcast (builder.telemetry_received (telemetry_data, channel->get_remote_endpoint ()));
}
});
@ -1061,7 +1089,7 @@ nano::websocket_server::websocket_server (nano::websocket::config & config_a, na
debug_assert (vote_a != nullptr);
if (server->any_subscriber (nano::websocket::topic::vote))
{
nano::websocket::message_builder builder;
nano::websocket::message_builder builder{ ledger };
auto msg{ builder.vote_received (vote_a, code_a) };
server->broadcast (msg);
}

View file

@ -24,6 +24,7 @@ class election_status;
enum class election_status_type : uint8_t;
class ledger;
class logger;
class node;
class node_observers;
class telemetry_data;
class vote;
@ -88,6 +89,8 @@ namespace websocket
class message_builder final
{
public:
message_builder (nano::ledger & ledger);
message block_confirmed (std::shared_ptr<nano::block> const & block_a, nano::account const & account_a, nano::amount const & amount_a, std::string subtype, bool include_block, nano::election_status const & election_status_a, std::vector<nano::vote_with_weight_info> const & election_votes_a, nano::websocket::confirmation_options const & options_a);
message started_election (nano::block_hash const & hash_a);
message stopped_election (nano::block_hash const & hash_a);
@ -103,6 +106,8 @@ namespace websocket
private:
/** Set the common fields for messages: timestamp and topic. */
void set_common_fields (message & message_a);
nano::ledger & ledger;
};
/** Options for subscriptions */
@ -183,6 +188,12 @@ namespace websocket
return include_election_info_with_votes;
}
/** Returns whether or not to include linked accounts */
bool get_include_linked_account () const
{
return include_linked_account;
}
/** Returns whether or not to include sideband info */
bool get_include_sideband_info () const
{
@ -203,6 +214,7 @@ namespace websocket
bool include_election_info{ false };
bool include_election_info_with_votes{ false };
bool include_linked_account{ false };
bool include_sideband_info{ false };
bool include_block{ true };
bool has_account_filtering_options{ false };
@ -302,7 +314,7 @@ namespace websocket
class listener final : public std::enable_shared_from_this<listener>
{
public:
listener (nano::logger &, nano::wallets & wallets_a, boost::asio::io_context & io_ctx_a, boost::asio::ip::tcp::endpoint endpoint_a);
listener (nano::logger &, nano::node &, nano::wallets & wallets_a, boost::asio::io_context & io_ctx_a, boost::asio::ip::tcp::endpoint endpoint_a);
/** Start accepting connections */
void run ();
@ -352,6 +364,7 @@ namespace websocket
void decrease_subscriber_count (nano::websocket::topic const & topic_a);
nano::logger & logger;
nano::node & node;
nano::wallets & wallets;
boost::asio::ip::tcp::acceptor acceptor;
socket_type socket;
@ -368,7 +381,7 @@ namespace websocket
class websocket_server
{
public:
websocket_server (nano::websocket::config &, nano::node_observers &, nano::wallets &, nano::ledger &, boost::asio::io_context &, nano::logger &);
websocket_server (nano::websocket::config &, nano::node &, nano::node_observers &, nano::wallets &, nano::ledger &, boost::asio::io_context &, nano::logger &);
void start ();
void stop ();

View file

@ -1170,6 +1170,22 @@ std::shared_ptr<nano::block> nano::ledger::find_receive_block_by_send_hash (secu
return result;
}
std::optional<nano::account> nano::ledger::linked_account (secure::transaction const & transaction, nano::block const & block)
{
debug_assert (block.has_sideband ());
if (block.sideband ().details.is_send)
{
return block.destination ();
}
else if (block.sideband ().details.is_receive)
{
return any.block_account (transaction, block.source ());
}
return std::nullopt;
}
nano::account const & nano::ledger::epoch_signer (nano::link const & link_a) const
{
return constants.epochs.signer (constants.epochs.epoch (link_a));

View file

@ -72,6 +72,7 @@ public:
bool is_epoch_link (nano::link const &) const;
std::array<nano::block_hash, 2> dependent_blocks (secure::transaction const &, nano::block const &) const;
std::shared_ptr<nano::block> find_receive_block_by_send_hash (secure::transaction const &, nano::account const & destination, nano::block_hash const & send_block_hash);
std::optional<nano::account> linked_account (secure::transaction const &, nano::block const &);
nano::account const & epoch_signer (nano::link const &) const;
nano::link const & epoch_link (nano::epoch) const;
bool migrate_lmdb_to_rocksdb (std::filesystem::path const &) const;