diff --git a/nano/core_test/websocket.cpp b/nano/core_test/websocket.cpp index de52c6ab1..826ea179f 100644 --- a/nano/core_test/websocket.cpp +++ b/nano/core_test/websocket.cpp @@ -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 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 ("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 ("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 ("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 ("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 ("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 ("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); diff --git a/nano/node/distributed_work.cpp b/nano/node/distributed_work.cpp index a7cb40598..db2d3ee70 100644 --- a/nano/node/distributed_work.cpp +++ b/nano/node/distributed_work.cpp @@ -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)); diff --git a/nano/node/node.cpp b/nano/node/node.cpp index 114b46b05..1bf210c9b 100644 --- a/nano/node/node.cpp +++ b/nano/node/node.cpp @@ -187,7 +187,7 @@ nano::node::node (std::shared_ptr io_ctx_a, std::filesy bootstrap_server{ *bootstrap_server_impl }, bootstrap_impl{ std::make_unique (config, ledger, ledger_notifications, block_processor, network, stats, logger) }, bootstrap{ *bootstrap_impl }, - websocket_impl{ std::make_unique (config.websocket_config, observers, wallets, ledger, io_ctx, logger) }, + websocket_impl{ std::make_unique (config.websocket_config, *this, observers, wallets, ledger, io_ctx, logger) }, websocket{ *websocket_impl }, epoch_upgrader_impl{ std::make_unique (*this, ledger, store, network_params, logger) }, epoch_upgrader{ *epoch_upgrader_impl }, @@ -236,7 +236,7 @@ nano::node::node (std::shared_ptr 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)); } } } diff --git a/nano/node/websocket.cpp b/nano/node/websocket.cpp index 32a68d2bb..401dbee51 100644 --- a/nano/node/websocket.cpp +++ b/nano/node/websocket.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ nano::websocket::confirmation_options::confirmation_options (boost::property_tre include_block = options_a.get ("include_block", true); include_election_info = options_a.get ("include_election_info", false); include_election_info_with_votes = options_a.get ("include_election_info_with_votes", false); + include_linked_account = options_a.get ("include_linked_account", false); include_sideband_info = options_a.get ("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 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 const & election_votes_a) { - nano::websocket::message_builder builder; + nano::websocket::message_builder builder{ node.ledger }; nano::lock_guard lk (sessions_mutex); boost::optional 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 (logger, wallets, io_ctx, endpoint); + server = std::make_shared (logger, node_a, wallets, io_ctx, endpoint); observers.blocks.add ([this] (nano::election_status const & status_a, std::vector 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 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); } diff --git a/nano/node/websocket.hpp b/nano/node/websocket.hpp index 1a5eb0dbb..2c5475404 100644 --- a/nano/node/websocket.hpp +++ b/nano/node/websocket.hpp @@ -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 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 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 { 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 (); diff --git a/nano/secure/ledger.cpp b/nano/secure/ledger.cpp index a03277613..2619c138e 100644 --- a/nano/secure/ledger.cpp +++ b/nano/secure/ledger.cpp @@ -1170,6 +1170,22 @@ std::shared_ptr nano::ledger::find_receive_block_by_send_hash (secu return result; } +std::optional 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)); diff --git a/nano/secure/ledger.hpp b/nano/secure/ledger.hpp index 91e1b8155..352d466a6 100644 --- a/nano/secure/ledger.hpp +++ b/nano/secure/ledger.hpp @@ -72,6 +72,7 @@ public: bool is_epoch_link (nano::link const &) const; std::array dependent_blocks (secure::transaction const &, nano::block const &) const; std::shared_ptr find_receive_block_by_send_hash (secure::transaction const &, nano::account const & destination, nano::block_hash const & send_block_hash); + std::optional 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;