diff --git a/nano/core_test/confirmation_height.cpp b/nano/core_test/confirmation_height.cpp index be26896a9d..ce38fda016 100644 --- a/nano/core_test/confirmation_height.cpp +++ b/nano/core_test/confirmation_height.cpp @@ -12,7 +12,7 @@ namespace { void add_callback_stats (nano::node & node, std::vector * observer_order = nullptr, nano::mutex * mutex = nullptr) { - node.observers.blocks.add ([& stats = node.stats, observer_order, mutex](nano::election_status const & status_a, nano::account const &, nano::amount const &, bool) { + node.observers.blocks.add ([& stats = node.stats, observer_order, mutex](nano::election_status const & status_a, std::vector const &, nano::account const &, nano::amount const &, bool) { stats.inc (nano::stat::type::http_callback, nano::stat::detail::http_callback, nano::stat::dir::out); if (mutex) { diff --git a/nano/core_test/node.cpp b/nano/core_test/node.cpp index de657305a8..10371e10e9 100644 --- a/nano/core_test/node.cpp +++ b/nano/core_test/node.cpp @@ -1474,7 +1474,7 @@ TEST (node, coherent_observer) { nano::system system (1); auto & node1 (*system.nodes[0]); - node1.observers.blocks.add ([&node1](nano::election_status const & status_a, nano::account const &, nano::uint128_t const &, bool) { + node1.observers.blocks.add ([&node1](nano::election_status const & status_a, std::vector const &, nano::account const &, nano::uint128_t const &, bool) { auto transaction (node1.store.tx_begin_read ()); ASSERT_TRUE (node1.store.block_exists (transaction, status_a.winner->hash ())); }); diff --git a/nano/core_test/websocket.cpp b/nano/core_test/websocket.cpp index a6039e7f34..9a15dc97dc 100644 --- a/nano/core_test/websocket.cpp +++ b/nano/core_test/websocket.cpp @@ -300,6 +300,8 @@ TEST (websocket, confirmation_options) // Make sure tally and time are non-zero. ASSERT_NE ("0", tally); ASSERT_NE ("0", time); + auto votes_l (election_info.get_child_optional ("votes")); + ASSERT_FALSE (votes_l.is_initialized ()); } catch (std::runtime_error const & ex) { @@ -329,7 +331,84 @@ TEST (websocket, confirmation_options) previous = send->hash (); } + ASSERT_TIMELY (5s, future3.wait_for (0s) == std::future_status::ready); +} + +TEST (websocket, confirmation_options_votes) +{ + nano::system system; + nano::node_config config (nano::get_available_port (), system.logging); + config.websocket_config.enabled = true; + config.websocket_config.port = nano::get_available_port (); + auto node1 (system.add_node (config)); + + std::atomic ack_ready{ false }; + auto task1 = ([&ack_ready, config, &node1]() { + fake_websocket_client client (config.websocket_config.port); + client.send_message (R"json({"action": "subscribe", "topic": "confirmation", "ack": "true", "options": {"confirmation_type": "active_quorum", "include_election_info_with_votes": "true", "include_block": "false"}})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::genesis_amount; + auto send_amount = node1->config.online_weight_minimum.number () + 1; + nano::block_hash previous (node1->latest (nano::dev_genesis_key.pub)); + { + balance -= send_amount; + auto send (std::make_shared (nano::dev_genesis_key.pub, previous, nano::dev_genesis_key.pub, balance, key.pub, nano::dev_genesis_key.prv, nano::dev_genesis_key.pub, *system.work.generate (previous))); + node1->process_active (send); + previous = send->hash (); + } + ASSERT_TIMELY (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 election_info = event.get_child ("message.election_info"); + auto tally (election_info.get ("tally")); + auto time (election_info.get ("time")); + // Duration and request count may be zero on devnet, so we only check that they're present + ASSERT_EQ (1, election_info.count ("duration")); + ASSERT_EQ (1, election_info.count ("request_count")); + ASSERT_EQ (1, election_info.count ("voters")); + ASSERT_GE (1U, election_info.get ("blocks")); + // Make sure tally and time are non-zero. + ASSERT_NE ("0", tally); + ASSERT_NE ("0", time); + auto votes_l (election_info.get_child_optional ("votes")); + ASSERT_TRUE (votes_l.is_initialized ()); + ASSERT_EQ (1, votes_l.get ().size ()); + for (auto & vote : votes_l.get ()) + { + std::string representative (vote.second.get ("representative")); + ASSERT_EQ (nano::dev_genesis_key.pub.to_account (), representative); + std::string timestamp (vote.second.get ("timestamp")); + ASSERT_NE ("0", timestamp); + std::string hash (vote.second.get ("hash")); + ASSERT_EQ (node1->latest (nano::dev_genesis_key.pub).to_string (), hash); + std::string weight (vote.second.get ("weight")); + ASSERT_EQ (node1->balance (nano::dev_genesis_key.pub).convert_to (), weight); + } + } + catch (std::runtime_error const & ex) + { + FAIL () << ex.what (); + } } // Tests updating options of block confirmations diff --git a/nano/node/active_transactions.cpp b/nano/node/active_transactions.cpp index 4b8e8d596b..a762b8085c 100644 --- a/nano/node/active_transactions.cpp +++ b/nano/node/active_transactions.cpp @@ -188,7 +188,7 @@ void nano::active_transactions::block_cemented_callback (std::shared_ptrhash (), account, amount, is_state_send, pending_account); - node.observers.blocks.notify (nano::election_status{ block_a, 0, std::chrono::duration_cast (std::chrono::system_clock::now ().time_since_epoch ()), std::chrono::duration_values::zero (), 0, 1, 0, nano::election_status_type::inactive_confirmation_height }, account, amount, is_state_send); + node.observers.blocks.notify (nano::election_status{ block_a, 0, std::chrono::duration_cast (std::chrono::system_clock::now ().time_since_epoch ()), std::chrono::duration_values::zero (), 0, 1, 0, nano::election_status_type::inactive_confirmation_height }, {}, account, amount, is_state_send); } else { @@ -218,7 +218,8 @@ void nano::active_transactions::block_cemented_callback (std::shared_ptrstatus.confirmation_request_count = election->confirmation_request_count; status_l = election->status; election_lk.unlock (); - node.observers.blocks.notify (status_l, account, amount, is_state_send); + auto votes (election->votes_with_weight ()); + node.observers.blocks.notify (status_l, votes, account, amount, is_state_send); if (amount > 0) { node.observers.account_balance.notify (account, false); diff --git a/nano/node/election.cpp b/nano/node/election.cpp index 8976e31713..cd68f5a00c 100644 --- a/nano/node/election.cpp +++ b/nano/node/election.cpp @@ -625,7 +625,25 @@ std::unordered_map> nano::electio std::unordered_map nano::election::votes () const { - debug_assert (node.network_params.network.is_dev_network ()); nano::lock_guard guard (mutex); return last_votes; } + +std::vector nano::election::votes_with_weight () const +{ + std::multimap> sorted_votes; + std::vector result; + auto votes_l (votes ()); + for (auto const & vote_l : votes_l) + { + if (vote_l.first != node.network_params.random.not_an_account) + { + auto amount (node.ledger.cache.rep_weights.representation_get (vote_l.first)); + nano::vote_with_weight_info vote_info{ vote_l.first, vote_l.second.time, vote_l.second.timestamp, vote_l.second.hash, amount }; + sorted_votes.emplace (std::move (amount), vote_info); + } + } + result.reserve (sorted_votes.size ()); + std::transform (sorted_votes.begin (), sorted_votes.end (), std::back_inserter (result), [](auto const & entry) { return entry.second; }); + return result; +} diff --git a/nano/node/election.hpp b/nano/node/election.hpp index 1c08ace3b1..677eb3d45b 100644 --- a/nano/node/election.hpp +++ b/nano/node/election.hpp @@ -23,6 +23,15 @@ class vote_info final uint64_t timestamp; nano::block_hash hash; }; +class vote_with_weight_info final +{ +public: + nano::account representative; + std::chrono::steady_clock::time_point time; + uint64_t timestamp; + nano::block_hash hash; + nano::uint128_t weight; +}; class election_vote_result final { public: @@ -117,6 +126,7 @@ class election final : public std::enable_shared_from_this uint64_t const height; nano::root const root; nano::qualified_root const qualified_root; + std::vector votes_with_weight () const; private: nano::tally_t tally_impl () const; diff --git a/nano/node/ipc/ipc_broker.cpp b/nano/node/ipc/ipc_broker.cpp index be8e9661d8..628b820f20 100644 --- a/nano/node/ipc/ipc_broker.cpp +++ b/nano/node/ipc/ipc_broker.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -21,7 +22,7 @@ std::shared_ptr nano::ipc::subscriber::get_parser (nano::ip void nano::ipc::broker::start () { - node.observers.blocks.add ([this_l = shared_from_this ()](nano::election_status const & status_a, nano::account const & account_a, nano::amount const & amount_a, bool is_state_send_a) { + node.observers.blocks.add ([this_l = shared_from_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) { debug_assert (status_a.type != nano::election_status_type::ongoing); try diff --git a/nano/node/json_handler.cpp b/nano/node/json_handler.cpp index 477b2c8f28..5fda9dc085 100644 --- a/nano/node/json_handler.cpp +++ b/nano/node/json_handler.cpp @@ -1133,7 +1133,7 @@ void nano::json_handler::block_confirm () is_state_send = node.ledger.is_send (transaction, *state); } } - node.observers.blocks.notify (status, account, amount, is_state_send); + node.observers.blocks.notify (status, {}, account, amount, is_state_send); } response_l.put ("started", "1"); } diff --git a/nano/node/node.cpp b/nano/node/node.cpp index b74b893bab..5b1b8de9a9 100644 --- a/nano/node/node.cpp +++ b/nano/node/node.cpp @@ -151,7 +151,7 @@ node_seq (seq) }; if (!config.callback_address.empty ()) { - observers.blocks.add ([this](nano::election_status const & status_a, nano::account const & account_a, nano::amount const & amount_a, bool is_state_send_a) { + 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) { auto block_a (status_a.winner); if ((status_a.type == nano::election_status_type::active_confirmed_quorum || status_a.type == nano::election_status_type::active_confirmation_height) && this->block_arrival.recent (block_a->hash ())) { @@ -213,7 +213,7 @@ node_seq (seq) } if (websocket_server) { - observers.blocks.add ([this](nano::election_status const & status_a, nano::account const & account_a, nano::amount const & amount_a, bool is_state_send_a) { + 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) { debug_assert (status_a.type != nano::election_status_type::ongoing); if (this->websocket_server->any_subscriber (nano::websocket::topic::confirmation)) @@ -240,7 +240,7 @@ node_seq (seq) } } - this->websocket_server->broadcast_confirmation (block_a, account_a, amount_a, subtype, status_a); + this->websocket_server->broadcast_confirmation (block_a, account_a, amount_a, subtype, status_a, votes_a); } }); @@ -270,7 +270,7 @@ node_seq (seq) }); } // Add block confirmation type stats regardless of http-callback and websocket subscriptions - observers.blocks.add ([this](nano::election_status const & status_a, nano::account const & account_a, nano::amount const & amount_a, bool is_state_send_a) { + 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) { debug_assert (status_a.type != nano::election_status_type::ongoing); switch (status_a.type) { diff --git a/nano/node/node_observers.hpp b/nano/node/node_observers.hpp index 68a6d007e9..8262c85300 100644 --- a/nano/node/node_observers.hpp +++ b/nano/node/node_observers.hpp @@ -11,7 +11,7 @@ class telemetry; class node_observers final { public: - using blocks_t = nano::observer_set; + using blocks_t = nano::observer_set const &, nano::account const &, nano::uint128_t const &, bool>; blocks_t blocks; nano::observer_set wallet; nano::observer_set, std::shared_ptr, nano::vote_code> vote; diff --git a/nano/node/wallet.cpp b/nano/node/wallet.cpp index d4e6d14dd0..bff609f1a0 100644 --- a/nano/node/wallet.cpp +++ b/nano/node/wallet.cpp @@ -1319,7 +1319,7 @@ nano::work_watcher::work_watcher (nano::node & node_a) : node (node_a), stopped (false) { - node.observers.blocks.add ([this](nano::election_status const & status_a, nano::account const & account_a, nano::amount const & amount_a, bool is_state_send_a) { + node.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) { this->remove (*status_a.winner); }); } diff --git a/nano/node/websocket.cpp b/nano/node/websocket.cpp index 7ed84812b6..667884f7e4 100644 --- a/nano/node/websocket.cpp +++ b/nano/node/websocket.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -25,6 +24,7 @@ logger (logger_a) // Non-account filtering options 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); confirmation_types = 0; auto type_l (options_a.get ("confirmation_type", "all")); @@ -612,7 +612,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) +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; @@ -637,11 +637,11 @@ void nano::websocket::listener::broadcast_confirmation (std::shared_ptr const & block_a, nano::account const & account_a, nano::amount const & amount_a, std::string subtype, bool include_block_a, nano::election_status const & election_status_a, nano::websocket::confirmation_options const & options_a) +nano::websocket::message nano::websocket::message_builder::block_confirmed (std::shared_ptr const & block_a, nano::account const & account_a, nano::amount const & amount_a, std::string subtype, bool include_block_a, nano::election_status const & election_status_a, std::vector const & election_votes_a, nano::websocket::confirmation_options const & options_a) { nano::websocket::message message_l (nano::websocket::topic::confirmation); set_common_fields (message_l); @@ -719,7 +719,7 @@ nano::websocket::message nano::websocket::message_builder::block_confirmed (std: }; message_node_l.add ("confirmation_type", confirmation_type); - if (options_a.get_include_election_info ()) + if (options_a.get_include_election_info () || options_a.get_include_election_info_with_votes ()) { boost::property_tree::ptree election_node_l; election_node_l.add ("duration", election_status_a.election_duration.count ()); @@ -728,6 +728,20 @@ nano::websocket::message nano::websocket::message_builder::block_confirmed (std: election_node_l.add ("blocks", std::to_string (election_status_a.block_count)); election_node_l.add ("voters", std::to_string (election_status_a.voter_count)); election_node_l.add ("request_count", std::to_string (election_status_a.confirmation_request_count)); + if (options_a.get_include_election_info_with_votes ()) + { + boost::property_tree::ptree election_votes_l; + for (auto const & vote_l : election_votes_a) + { + boost::property_tree::ptree entry; + entry.put ("representative", vote_l.representative.to_account ()); + entry.put ("timestamp", vote_l.timestamp); + entry.put ("hash", vote_l.hash.to_string ()); + entry.put ("weight", vote_l.weight.convert_to ()); + election_votes_l.push_back (std::make_pair ("", entry)); + } + election_node_l.add_child ("votes", election_votes_l); + } message_node_l.add_child ("election_info", election_node_l); } diff --git a/nano/node/websocket.hpp b/nano/node/websocket.hpp index cb48a6cd71..044ddc99b8 100644 --- a/nano/node/websocket.hpp +++ b/nano/node/websocket.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -91,7 +92,7 @@ namespace websocket class message_builder final { public: - 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, nano::websocket::confirmation_options const & options_a); + 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 stopped_election (nano::block_hash const & hash_a); message vote_received (std::shared_ptr const & vote_a, nano::vote_code code_a); message difficulty_changed (uint64_t publish_threshold_a, uint64_t receive_threshold_a, uint64_t difficulty_active_a); @@ -180,6 +181,12 @@ namespace websocket return include_election_info; } + /** Returns whether or not to include election info with votes */ + bool get_include_election_info_with_votes () const + { + return include_election_info_with_votes; + } + static constexpr const uint8_t type_active_quorum = 1; static constexpr const uint8_t type_active_confirmation_height = 2; static constexpr const uint8_t type_inactive = 4; @@ -192,6 +199,7 @@ namespace websocket nano::wallets & wallets; boost::optional logger; bool include_election_info{ false }; + bool include_election_info_with_votes{ false }; bool include_block{ true }; bool has_account_filtering_options{ false }; bool all_local_accounts{ false }; @@ -292,7 +300,7 @@ namespace websocket void stop (); /** Broadcast block confirmation. The content of the message depends on subscription options (such as "include_block") */ - void 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); + void 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); /** Broadcast \p message to all session subscribing to the message topic. */ void broadcast (nano::websocket::message message_a); diff --git a/nano/qt/qt.cpp b/nano/qt/qt.cpp index ed28bf059a..7be8f1ab34 100644 --- a/nano/qt/qt.cpp +++ b/nano/qt/qt.cpp @@ -1290,7 +1290,7 @@ void nano_qt::wallet::start () this_l->push_main_stack (this_l->send_blocks_window); } }); - node.observers.blocks.add ([this_w](nano::election_status const & status_a, nano::account const & account_a, nano::uint128_t const & amount_a, bool) { + node.observers.blocks.add ([this_w](nano::election_status const & status_a, std::vector const & votes_a, nano::account const & account_a, nano::uint128_t const & amount_a, bool) { if (auto this_l = this_w.lock ()) { this_l->application.postEvent (&this_l->processor, new eventloop_event ([this_w, status_a, account_a]() {