diff --git a/nano/core_test/voting.cpp b/nano/core_test/voting.cpp index f0fc36ed17..82bd8a0b98 100644 --- a/nano/core_test/voting.cpp +++ b/nano/core_test/voting.cpp @@ -119,3 +119,117 @@ TEST (vote_generator, session) thread.join (); ASSERT_TIMELY (2s, 1 == node->stats.count (nano::stat::type::vote, nano::stat::detail::vote_indeterminate)); } + +TEST (vote_spacing, basic) +{ + nano::vote_spacing spacing{ std::chrono::milliseconds{ 100 } }; + nano::root root1{ 1 }; + nano::root root2{ 2 }; + nano::block_hash hash3{ 3 }; + nano::block_hash hash4{ 4 }; + nano::block_hash hash5{ 5 }; + ASSERT_EQ (0, spacing.size ()); + ASSERT_TRUE (spacing.votable (root1, hash3)); + spacing.flag (root1, hash3); + ASSERT_EQ (1, spacing.size ()); + ASSERT_TRUE (spacing.votable (root1, hash3)); + ASSERT_FALSE (spacing.votable (root1, hash4)); + spacing.flag (root2, hash5); + ASSERT_EQ (2, spacing.size ()); +} + +TEST (vote_spacing, prune) +{ + auto length = std::chrono::milliseconds{ 100 }; + nano::vote_spacing spacing{ length }; + nano::root root1{ 1 }; + nano::root root2{ 2 }; + nano::block_hash hash3{ 3 }; + nano::block_hash hash4{ 4 }; + spacing.flag (root1, hash3); + ASSERT_EQ (1, spacing.size ()); + std::this_thread::sleep_for (length); + spacing.flag (root2, hash4); + ASSERT_EQ (1, spacing.size ()); +} + +TEST (vote_spacing, vote_generator) +{ + nano::node_config config; + config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled; + nano::system system; + auto & node = *system.add_node (config); + auto & wallet = *system.wallet (0); + wallet.insert_adhoc (nano::dev_genesis_key.prv); + nano::state_block_builder builder; + auto send1 = builder.make_block () + .account (nano::dev_genesis_key.pub) + .previous (nano::genesis_hash) + .representative (nano::dev_genesis_key.pub) + .balance (nano::genesis_amount - nano::Gxrb_ratio) + .link (nano::dev_genesis_key.pub) + .sign (nano::dev_genesis_key.prv, nano::dev_genesis_key.pub) + .work (*system.work.generate (nano::genesis_hash)) + .build_shared (); + auto send2 = builder.make_block () + .account (nano::dev_genesis_key.pub) + .previous (nano::genesis_hash) + .representative (nano::dev_genesis_key.pub) + .balance (nano::genesis_amount - nano::Gxrb_ratio - 1) + .link (nano::dev_genesis_key.pub) + .sign (nano::dev_genesis_key.prv, nano::dev_genesis_key.pub) + .work (*system.work.generate (nano::genesis_hash)) + .build_shared (); + ASSERT_EQ (nano::process_result::progress, node.ledger.process (node.store.tx_begin_write (), *send1).code); + ASSERT_EQ (0, node.stats.count (nano::stat::type::vote_generator, nano::stat::detail::generator_broadcasts)); + node.active.generator.add (nano::genesis_hash, send1->hash ()); + ASSERT_TIMELY (3s, node.stats.count (nano::stat::type::vote_generator, nano::stat::detail::generator_broadcasts) == 1); + ASSERT_FALSE (node.ledger.rollback (node.store.tx_begin_write (), send1->hash ())); + ASSERT_EQ (nano::process_result::progress, node.ledger.process (node.store.tx_begin_write (), *send2).code); + node.active.generator.add (nano::genesis_hash, send2->hash ()); + ASSERT_TIMELY (3s, node.stats.count (nano::stat::type::vote_generator, nano::stat::detail::generator_spacing) == 1); + ASSERT_EQ (1, node.stats.count (nano::stat::type::vote_generator, nano::stat::detail::generator_broadcasts)); + std::this_thread::sleep_for (config.network_params.voting.delay); + node.active.generator.add (nano::genesis_hash, send2->hash ()); + ASSERT_TIMELY (3s, node.stats.count (nano::stat::type::vote_generator, nano::stat::detail::generator_broadcasts) == 2); +} + +TEST (vote_spacing, rapid) +{ + nano::node_config config; + config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled; + nano::system system; + auto & node = *system.add_node (config); + auto & wallet = *system.wallet (0); + wallet.insert_adhoc (nano::dev_genesis_key.prv); + nano::state_block_builder builder; + auto send1 = builder.make_block () + .account (nano::dev_genesis_key.pub) + .previous (nano::genesis_hash) + .representative (nano::dev_genesis_key.pub) + .balance (nano::genesis_amount - nano::Gxrb_ratio) + .link (nano::dev_genesis_key.pub) + .sign (nano::dev_genesis_key.prv, nano::dev_genesis_key.pub) + .work (*system.work.generate (nano::genesis_hash)) + .build_shared (); + auto send2 = builder.make_block () + .account (nano::dev_genesis_key.pub) + .previous (nano::genesis_hash) + .representative (nano::dev_genesis_key.pub) + .balance (nano::genesis_amount - nano::Gxrb_ratio - 1) + .link (nano::dev_genesis_key.pub) + .sign (nano::dev_genesis_key.prv, nano::dev_genesis_key.pub) + .work (*system.work.generate (nano::genesis_hash)) + .build_shared (); + ASSERT_EQ (nano::process_result::progress, node.ledger.process (node.store.tx_begin_write (), *send1).code); + node.active.generator.add (nano::genesis_hash, send1->hash ()); + ASSERT_TIMELY (3s, node.stats.count (nano::stat::type::vote_generator, nano::stat::detail::generator_broadcasts) == 1); + ASSERT_FALSE (node.ledger.rollback (node.store.tx_begin_write (), send1->hash ())); + ASSERT_EQ (nano::process_result::progress, node.ledger.process (node.store.tx_begin_write (), *send2).code); + node.active.generator.add (nano::genesis_hash, send2->hash ()); + ASSERT_TIMELY (3s, node.stats.count (nano::stat::type::vote_generator, nano::stat::detail::generator_spacing) == 1); + ASSERT_TIMELY (3s, 1 == node.stats.count (nano::stat::type::vote_generator, nano::stat::detail::generator_broadcasts)); + std::this_thread::sleep_for (config.network_params.voting.delay); + node.active.generator.add (nano::genesis_hash, send2->hash ()); + ASSERT_TIMELY (3s, node.stats.count (nano::stat::type::vote_generator, nano::stat::detail::generator_broadcasts) == 2); +} diff --git a/nano/lib/stats.cpp b/nano/lib/stats.cpp index 2208c5e4b8..b78b472c1a 100644 --- a/nano/lib/stats.cpp +++ b/nano/lib/stats.cpp @@ -760,6 +760,9 @@ std::string nano::stat::detail_to_string (uint32_t key) case nano::stat::detail::generator_replies_discarded: res = "generator_replies_discarded"; break; + case nano::stat::detail::generator_spacing: + res = "generator_spacing"; + break; } return res; } diff --git a/nano/lib/stats.hpp b/nano/lib/stats.hpp index 6ed0aa820c..9fd47e27bf 100644 --- a/nano/lib/stats.hpp +++ b/nano/lib/stats.hpp @@ -342,7 +342,8 @@ class stat final // vote generator generator_broadcasts, generator_replies, - generator_replies_discarded + generator_replies_discarded, + generator_spacing }; /** Direction of the stat. If the direction is irrelevant, use in */ diff --git a/nano/node/voting.cpp b/nano/node/voting.cpp index 1aab898218..8d64a2a7e1 100644 --- a/nano/node/voting.cpp +++ b/nano/node/voting.cpp @@ -11,6 +11,44 @@ #include +void nano::vote_spacing::trim () +{ + recent.get ().erase (recent.get ().begin (), recent.get ().upper_bound (std::chrono::steady_clock::now () - delay)); +} + +bool nano::vote_spacing::votable (nano::root const & root_a, nano::block_hash const & hash_a) const +{ + bool result = true; + for (auto range = recent.get ().equal_range (root_a); result && range.first != range.second; ++range.first) + { + auto & item = *range.first; + result = hash_a == item.hash || item.time < std::chrono::steady_clock::now () - delay; + } + return result; +} + +void nano::vote_spacing::flag (nano::root const & root_a, nano::block_hash const & hash_a) +{ + trim (); + auto now = std::chrono::steady_clock::now (); + auto existing = recent.get ().find (root_a); + if (existing != recent.end ()) + { + recent.get ().modify (existing, [now] (entry & entry) { + entry.time = now; + }); + } + else + { + recent.insert ({ root_a, now, hash_a }); + } +} + +size_t nano::vote_spacing::size () const +{ + return recent.size (); +} + bool nano::local_vote_history::consistency_check (nano::root const & root_a) const { auto & history_by_root (history.get ()); @@ -117,6 +155,7 @@ ledger (ledger_a), wallets (wallets_a), vote_processor (vote_processor_a), history (history_a), +spacing{ config_a.network_params.voting.delay }, network (network_a), stats (stats_a), thread ([this]() { run (); }) @@ -218,18 +257,27 @@ void nano::vote_generator::broadcast (nano::unique_lock & lock_a) } if (cached_votes.empty () && std::find (roots.begin (), roots.end (), root) == roots.end ()) { - roots.push_back (root); - hashes.push_back (hash); + if (spacing.votable (root, hash)) + { + roots.push_back (root); + hashes.push_back (hash); + } + else + { + stats.inc (nano::stat::type::vote_generator, nano::stat::detail::generator_spacing); + } } candidates.pop_front (); } if (!hashes.empty ()) { lock_a.unlock (); - vote (hashes, roots, [this](auto const & vote_a) { this->broadcast_action (vote_a); }); + vote (hashes, roots, [this](auto const & vote_a) { + this->broadcast_action (vote_a); + this->stats.inc (nano::stat::type::vote_generator, nano::stat::detail::generator_broadcasts); + }); lock_a.lock (); } - stats.inc (nano::stat::type::vote_generator, nano::stat::detail::generator_broadcasts); } void nano::vote_generator::reply (nano::unique_lock & lock_a, request_t && request_a) @@ -260,8 +308,15 @@ void nano::vote_generator::reply (nano::unique_lock & lock_a, reques } if (cached_votes.empty () && std::find (roots.begin (), roots.end (), root) == roots.end ()) { - roots.push_back (i->first); - hashes.push_back (i->second); + if (spacing.votable (root, hash)) + { + roots.push_back (root); + hashes.push_back (hash); + } + else + { + stats.inc (nano::stat::type::vote_generator, nano::stat::detail::generator_spacing); + } } } if (!hashes.empty ()) @@ -289,6 +344,7 @@ void nano::vote_generator::vote (std::vector const & hashes_a, for (size_t i (0), n (hashes_a.size ()); i != n; ++i) { history.add (roots_a[i], hashes_a[i], vote_l); + spacing.flag (roots_a[i], hashes_a[i]); } action_a (vote_l); } diff --git a/nano/node/voting.hpp b/nano/node/voting.hpp index 09502e170f..11bfae6444 100644 --- a/nano/node/voting.hpp +++ b/nano/node/voting.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -31,6 +32,34 @@ namespace transport class channel; } +class vote_spacing final +{ + class entry + { + public: + nano::root root; + std::chrono::steady_clock::time_point time; + nano::block_hash hash; + }; + + boost::multi_index_container, + mi::member>, + mi::ordered_non_unique, + mi::member> + >> + recent; + std::chrono::milliseconds const delay; + void trim (); +public: + vote_spacing (std::chrono::milliseconds const & delay) : + delay{ delay } {} + bool votable (nano::root const & root_a, nano::block_hash const & hash_a) const; + void flag (nano::root const & root_a, nano::block_hash const & hash_a); + size_t size () const; +}; + class local_vote_history final { class local_vote final @@ -77,7 +106,6 @@ class local_vote_history final mutable std::mutex mutex; friend std::unique_ptr collect_container_info (local_vote_history & history, const std::string & name); - friend class local_vote_history_basic_Test; }; @@ -110,6 +138,7 @@ class vote_generator final nano::wallets & wallets; nano::vote_processor & vote_processor; nano::local_vote_history & history; + nano::vote_spacing spacing; nano::network & network; nano::stat & stats; mutable std::mutex mutex;