diff --git a/.github/workflows/code_sanitizers.yml b/.github/workflows/code_sanitizers.yml index 9e9e7d6cd..00ef52af3 100644 --- a/.github/workflows/code_sanitizers.yml +++ b/.github/workflows/code_sanitizers.yml @@ -21,7 +21,7 @@ jobs: # Bug when running with TSAN: "ThreadSanitizer: CHECK failed: sanitizer_deadlock_detector" - BACKEND: rocksdb SANITIZER: { name: TSAN } - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: COMPILER: ${{ matrix.COMPILER }} BACKEND: ${{ matrix.BACKEND }} diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 12799e345..6b48f00a6 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -56,7 +56,7 @@ jobs: COMPILER: [gcc, clang] RELEASE: - ${{ startsWith(github.ref, 'refs/tags/') }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: COMPILER: ${{ matrix.COMPILER }} BACKEND: ${{ matrix.BACKEND }} diff --git a/CMakeLists.txt b/CMakeLists.txt index b04357cff..d3807ccf7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -526,11 +526,9 @@ target_link_libraries(boost_property_tree INTERFACE Boost::multi_index) # RocksDB include_directories(submodules/rocksdb/include) -if(WIN32) - set(FAIL_ON_WARNINGS - OFF - CACHE BOOL "") -endif() +set(FAIL_ON_WARNINGS + OFF + CACHE BOOL "") set(USE_RTTI ON CACHE BOOL "") diff --git a/ci/prepare/linux/prepare-clang.sh b/ci/prepare/linux/prepare-clang.sh index 4220a325e..fbd07063a 100755 --- a/ci/prepare/linux/prepare-clang.sh +++ b/ci/prepare/linux/prepare-clang.sh @@ -4,10 +4,7 @@ set -euox pipefail # Clang installer dependencies DEBIAN_FRONTEND=noninteractive apt-get install -yqq lsb-release software-properties-common gnupg -CLANG_VERSION=16 - -# TODO: Verify integrity (at this time, the clang build is not used for any production artifacts) -curl -O https://apt.llvm.org/llvm.sh && chmod +x llvm.sh && ./llvm.sh $CLANG_VERSION +CLANG_VERSION=18 update-alternatives --install /usr/bin/cc cc /usr/bin/clang-$CLANG_VERSION 100 update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-$CLANG_VERSION 100 diff --git a/nano/benchmarks/ledger.cpp b/nano/benchmarks/ledger.cpp index 34ad0c94f..9c27bb45c 100644 --- a/nano/benchmarks/ledger.cpp +++ b/nano/benchmarks/ledger.cpp @@ -23,7 +23,7 @@ static void BM_ledger_iterate_accounts (benchmark::State & state) auto store_impl{ nano::make_store (logger, application_path, network_params.ledger) }; auto & store{ *store_impl }; - auto ledger_impl{ std::make_unique (store, network_params.ledger, stats, logger, nano::generate_cache_flags::all_disabled ()) }; + auto ledger_impl{ std::make_unique (store, network_params, stats, logger, nano::generate_cache_flags::all_disabled ()) }; auto & ledger{ *ledger_impl }; auto transaction = ledger.tx_begin_read (); diff --git a/nano/core_test/CMakeLists.txt b/nano/core_test/CMakeLists.txt index 89e080e23..d2e9c62d3 100644 --- a/nano/core_test/CMakeLists.txt +++ b/nano/core_test/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable( fakes/websocket_client.hpp fakes/work_peer.hpp active_elections.cpp + active_elections_index.cpp assert.cpp async.cpp backlog.cpp @@ -49,6 +50,7 @@ add_executable( random.cpp random_pool.cpp rate_limiting.cpp + recently_cache.cpp rep_crawler.cpp receivable.cpp peer_history.cpp diff --git a/nano/core_test/active_elections.cpp b/nano/core_test/active_elections.cpp index ac6e95ba3..bf319a828 100644 --- a/nano/core_test/active_elections.cpp +++ b/nano/core_test/active_elections.cpp @@ -20,7 +20,9 @@ #include +#include #include +#include using namespace std::chrono_literals; @@ -147,17 +149,18 @@ TEST (active_elections, confirm_frontier) // start node2 later so that we do not get the gossip traffic auto & node2 = *system.add_node (node_config2, node_flags2); - // Add representative to disabled rep crawler - auto peers (node2.network.random_set (1)); - ASSERT_FALSE (peers.empty ()); - node2.rep_crawler.force_add_rep (nano::dev::genesis_key.pub, *peers.begin ()); - ASSERT_EQ (nano::block_status::progress, node2.process (send)); ASSERT_TIMELY (5s, !node2.active.empty ()); // Save election to check request count afterwards std::shared_ptr election2; ASSERT_TIMELY (5s, election2 = node2.active.election (send->qualified_root ())); + + // Add representative to disabled rep crawler + auto peers (node2.network.random_set (1)); + ASSERT_FALSE (peers.empty ()); + node2.rep_crawler.force_add_rep (nano::dev::genesis_key.pub, *peers.begin ()); + ASSERT_TIMELY (5s, nano::test::confirmed (node2, { send })); ASSERT_TIMELY_EQ (5s, node2.ledger.cemented_count (), 2); ASSERT_TIMELY (5s, node2.active.empty ()); @@ -336,7 +339,7 @@ TEST (active_elections, DISABLED_keep_local) // ASSERT_EQ (1, node.scheduler.size ()); } -TEST (inactive_votes_cache, basic) +TEST (active_elections, cached_vote_basic) { nano::test::system system (1); auto & node = *system.nodes[0]; @@ -360,7 +363,7 @@ TEST (inactive_votes_cache, basic) /** * This test case confirms that a non final vote cannot cause an election to become confirmed */ -TEST (inactive_votes_cache, non_final) +TEST (active_elections, cached_vote_non_final) { nano::test::system system (1); auto & node = *system.nodes[0]; @@ -386,7 +389,7 @@ TEST (inactive_votes_cache, non_final) ASSERT_FALSE (election->confirmed ()); } -TEST (inactive_votes_cache, fork) +TEST (active_elections, cached_vote_fork) { nano::test::system system{ 1 }; auto & node = *system.nodes[0]; @@ -426,7 +429,7 @@ TEST (inactive_votes_cache, fork) ASSERT_EQ (1, node.stats.count (nano::stat::type::election_vote, nano::stat::detail::cache)); } -TEST (inactive_votes_cache, existing_vote) +TEST (active_elections, cached_vote_existing) { nano::test::system system; nano::node_config node_config = system.default_config (); @@ -480,7 +483,7 @@ TEST (inactive_votes_cache, existing_vote) ASSERT_EQ (0, node.stats.count (nano::stat::type::election_vote, nano::stat::detail::cache)); } -TEST (inactive_votes_cache, multiple_votes) +TEST (active_elections, cached_vote_multiple) { nano::test::system system; nano::node_config node_config = system.default_config (); @@ -533,7 +536,7 @@ TEST (inactive_votes_cache, multiple_votes) ASSERT_EQ (2, node.stats.count (nano::stat::type::election_vote, nano::stat::detail::cache)); } -TEST (inactive_votes_cache, election_start) +TEST (active_elections, cached_vote_election_start) { nano::test::system system; nano::node_config node_config = system.default_config (); @@ -658,7 +661,6 @@ TEST (active_elections, vote_replays) .sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub) .work (*system.work.generate (nano::dev::genesis->hash ())) .build (); - ASSERT_NE (nullptr, send1); // create open block for key receing Knano_ratio raw auto open1 = builder.make_block () @@ -670,11 +672,9 @@ TEST (active_elections, vote_replays) .sign (key.prv, key.pub) .work (*system.work.generate (key.pub)) .build (); - ASSERT_NE (nullptr, open1); // wait for elections objects to appear in the AEC - node.process_active (send1); - node.process_active (open1); + nano::test::process (node, { send1, open1 }); ASSERT_TRUE (nano::test::start_elections (system, node, { send1, open1 })); ASSERT_EQ (2, node.active.size ()); @@ -694,7 +694,6 @@ TEST (active_elections, vote_replays) // Open new account auto vote_open1 = nano::test::make_final_vote (nano::dev::genesis_key, { open1 }); ASSERT_EQ (nano::vote_code::vote, node.vote_router.vote (vote_open1).at (open1->hash ())); - ASSERT_EQ (nano::vote_code::replay, node.vote_router.vote (vote_open1).at (open1->hash ())); ASSERT_TIMELY (5s, node.active.empty ()); ASSERT_EQ (nano::vote_code::late, node.vote_router.vote (vote_open1).at (open1->hash ())); ASSERT_EQ (nano::Knano_ratio, node.ledger.weight (key.pub)); @@ -709,8 +708,7 @@ TEST (active_elections, vote_replays) .sign (key.prv, key.pub) .work (*system.work.generate (open1->hash ())) .build (); - ASSERT_NE (nullptr, send2); - node.process_active (send2); + nano::test::process (node, { send2 }); ASSERT_TRUE (nano::test::start_elections (system, node, { send2 })); ASSERT_EQ (1, node.active.size ()); @@ -1582,13 +1580,13 @@ TEST (active_elections, broadcast_block_on_activation) ASSERT_TIMELY (5s, node2->block_or_pruned_exists (send1->hash ())); } -TEST (active_elections, bootstrap_stale) +TEST (active_elections, stale_election) { nano::test::system system; // Configure node with short stale threshold for testing nano::node_config node_config = system.default_config (); - node_config.active_elections.bootstrap_stale_threshold = 2s; // Short threshold for faster testing + node_config.active_elections.stale_threshold = 2s; // Short threshold for faster testing auto & node = *system.add_node (node_config); @@ -1605,6 +1603,12 @@ TEST (active_elections, bootstrap_stale) .work (*system.work.generate (nano::dev::genesis->hash ())) .build (); + std::atomic stale_detected{ false }; + node.active.election_stale.add ([&] (auto const & election) { + EXPECT_EQ (send->qualified_root (), election->qualified_root); + stale_detected = true; + }); + // Process the block and start an election node.process_active (send); @@ -1613,8 +1617,263 @@ TEST (active_elections, bootstrap_stale) ASSERT_TIMELY (5s, (election = node.active.election (send->qualified_root ())) != nullptr); // Check initial state - ASSERT_EQ (0, node.stats.count (nano::stat::type::active_elections, nano::stat::detail::bootstrap_stale)); + ASSERT_EQ (0, node.stats.count (nano::stat::type::active_elections, nano::stat::detail::stale)); - // Wait for bootstrap_stale_threshold to pass and the statistic to be incremented - ASSERT_TIMELY (5s, node.stats.count (nano::stat::type::active_elections, nano::stat::detail::bootstrap_stale) > 0); + // Wait for stale_threshold to pass and stats to be incremented + ASSERT_TIMELY (5s, stale_detected); + ASSERT_TIMELY (5s, node.stats.count (nano::stat::type::active_elections, nano::stat::detail::stale) > 0); +} + +TEST (active_elections, stale_election_multiple) +{ + nano::test::system system; + + // Configure node with short stale threshold for testing + nano::node_config node_config = system.default_config (); + node_config.active_elections.stale_threshold = 2s; // Short threshold for faster testing + + auto & node = *system.add_node (node_config); + + // Create 10 independent blocks that will each have their own election + auto blocks = nano::test::setup_independent_blocks (system, node, 10); + + // Track which elections had stale events fired + nano::locked> stale_detected; + + node.active.election_stale.add ([&] (auto const & election) { + stale_detected.lock ()->insert (election->qualified_root); + }); + + // Start elections for all blocks + ASSERT_TRUE (nano::test::start_elections (system, node, blocks)); + + // Ensure all elections are active + ASSERT_TIMELY_EQ (5s, node.active.size (), blocks.size ()); + for (auto const & block : blocks) + { + ASSERT_TRUE (node.active.active (block->qualified_root ())); + } + + // Check initial state + ASSERT_EQ (0, node.stats.count (nano::stat::type::active_elections, nano::stat::detail::stale)); + + // Wait for stale_threshold to pass (2s) plus some buffer + // The stale event should fire for ALL elections that are stale + ASSERT_TIMELY (5s, node.stats.count (nano::stat::type::active_elections, nano::stat::detail::stale) >= blocks.size ()); + + // Check that all elections had their stale event fired + ASSERT_TIMELY_EQ (5s, blocks.size (), stale_detected.lock ()->size ()); + + // Verify each block's election was marked as stale + for (auto const & block : blocks) + { + ASSERT_TRUE (stale_detected.lock ()->count (block->qualified_root ()) > 0) + << "Election for block " << block->hash ().to_string () << " was not marked as stale"; + } +} + +TEST (active_elections, transition_optimistic_to_priority) +{ + nano::test::system system; + auto & node = *system.add_node (); + + // Create a block for optimistic election + nano::state_block_builder builder; + auto block = builder + .account (nano::dev::genesis_key.pub) + .previous (nano::dev::genesis->hash ()) + .representative (nano::dev::genesis_key.pub) + .balance (nano::dev::constants.genesis_amount - 100) + .link (nano::keypair{}.pub) + .sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub) + .work (*system.work.generate (nano::dev::genesis->hash ())) + .build (); + + ASSERT_TRUE (nano::test::process (node, { block })); + + // Start optimistic election + auto result = node.active.insert (block, nano::election_behavior::optimistic); + ASSERT_TRUE (result.inserted); + auto election = result.election; + ASSERT_EQ (nano::election_behavior::optimistic, election->behavior ()); + + // Verify initial sizes + ASSERT_EQ (1, node.active.size (nano::election_behavior::optimistic)); + ASSERT_EQ (0, node.active.size (nano::election_behavior::priority)); + + // Transition to priority + auto transition_result = node.active.insert (block, nano::election_behavior::priority); + ASSERT_FALSE (transition_result.inserted); + ASSERT_EQ (election, transition_result.election); + + // Verify transition + ASSERT_EQ (nano::election_behavior::priority, election->behavior ()); + ASSERT_EQ (1, node.stats.count (nano::stat::type::active_elections, nano::stat::detail::transition_priority)); + ASSERT_EQ (0, node.active.size (nano::election_behavior::optimistic)); + ASSERT_EQ (1, node.active.size (nano::election_behavior::priority)); +} + +TEST (active_elections, transition_hinted_to_priority) +{ + nano::test::system system; + auto & node = *system.add_node (); + + // Create a block for hinted election + nano::state_block_builder builder; + auto block = builder + .account (nano::dev::genesis_key.pub) + .previous (nano::dev::genesis->hash ()) + .representative (nano::dev::genesis_key.pub) + .balance (nano::dev::constants.genesis_amount - 100) + .link (nano::keypair{}.pub) + .sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub) + .work (*system.work.generate (nano::dev::genesis->hash ())) + .build (); + + ASSERT_TRUE (nano::test::process (node, { block })); + + // Start hinted election + auto result = node.active.insert (block, nano::election_behavior::hinted); + ASSERT_TRUE (result.inserted); + auto election = result.election; + ASSERT_EQ (nano::election_behavior::hinted, election->behavior ()); + + // Verify initial sizes + ASSERT_EQ (1, node.active.size (nano::election_behavior::hinted)); + ASSERT_EQ (0, node.active.size (nano::election_behavior::priority)); + + // Transition to priority + auto transition_result = node.active.insert (block, nano::election_behavior::priority); + ASSERT_FALSE (transition_result.inserted); + ASSERT_EQ (election, transition_result.election); + + // Verify transition + ASSERT_EQ (nano::election_behavior::priority, election->behavior ()); + ASSERT_EQ (1, node.stats.count (nano::stat::type::active_elections, nano::stat::detail::transition_priority)); + ASSERT_EQ (0, node.active.size (nano::election_behavior::hinted)); + ASSERT_EQ (1, node.active.size (nano::election_behavior::priority)); +} + +TEST (active_elections, cancel_cemented_races) +{ + nano::test::system system; + nano::node_config config = system.default_config (); + + // Disable schedulers and backlog scan to have full control + config.backlog_scan.enable = false; + config.priority_scheduler.enable = false; + config.hinted_scheduler.enable = false; + config.optimistic_scheduler.enable = false; + + auto & node = *system.add_node (config); + + // Create many chains with many blocks + const int chain_count = 10; + const int blocks_per_chain = 20; + const auto duration = 2s; + + auto const chains = nano::test::setup_chains (system, node, chain_count, + blocks_per_chain, + nano::dev::genesis_key, + false); // Don't auto-confirm + + // Collect all blocks for random access + std::vector> all_blocks; + for (auto & [account, blocks] : chains) + { + all_blocks.insert (all_blocks.end (), blocks.begin (), blocks.end ()); + } + + std::atomic stop_tasks{ false }; + + // Cement chain heads + auto cementing_task = std::async (std::launch::async, [&] () { + for (auto & [account, blocks] : chains) + { + // Cement using the cementing set so that notifications are properly handled + node.cementing_set.add (blocks.back ()->hash ()); + std::this_thread::sleep_for (10ms); + } + }); + + // Insert elections continuously + auto insertion_task = std::async (std::launch::async, [&] () { + std::random_device rd; + std::mt19937 gen (rd ()); + std::uniform_int_distribution<> dis (0, all_blocks.size () - 1); + + while (!stop_tasks) + { + auto block = all_blocks[dis (gen)]; + node.active.insert (block, nano::election_behavior::priority); + std::this_thread::yield (); + } + }); + + // Let tasks run for 2 seconds + WAIT (2s); + + // Signal tasks to stop + stop_tasks = true; + + // Wait for all async tasks to complete + cementing_task.wait (); + insertion_task.wait (); + + // Wait for all cementing operations to complete + ASSERT_TIMELY (5s, node.cementing_set.size () == 0); + + // Verify all blocks were cemented + auto transaction = node.ledger.tx_begin_read (); + for (auto & [account, blocks] : chains) + { + for (auto & block : blocks) + { + ASSERT_TRUE (node.ledger.confirmed.block_exists (transaction, block->hash ())); + } + } + + // Critical assertion: No elections should remain active + ASSERT_TIMELY (5s, node.active.empty ()); +} + +TEST (active_elections, cancel_already_cemented) +{ + nano::test::system system; + + nano::node_config config; + // Configure checkup interval for faster test execution + config.active_elections.checkup_interval = 100ms; + + auto & node = *system.add_node (config); + + // Create a chain of blocks + auto const chain_count = 1; + auto const blocks_per_chain = 5; + auto const chains = nano::test::setup_chains (system, node, chain_count, blocks_per_chain, nano::dev::genesis_key, false); + + auto & [account, blocks] = *chains.begin (); + auto last_block = blocks.back (); + + // First, cement the block through direct ledger confirmation process, skips callbacks + { + auto transaction = node.ledger.tx_begin_write (); + node.ledger.confirm (transaction, last_block->hash ()); + } + + // Verify the block is actually cemented + ASSERT_TRUE (node.ledger.confirmed.block_exists (node.ledger.tx_begin_read (), last_block->hash ())); + + // Now start an election for the already cemented block + auto election = node.active.insert (last_block, nano::election_behavior::priority); + ASSERT_NE (nullptr, election.election); + + // Wait for the cleanup thread to detect and cancel the election + // The cleanup thread runs every checkup_interval (100ms) and only cancels elections + // that have been running for at least 3 * aec_loop_interval (3 * 50ms = 150ms by default) + ASSERT_TIMELY (5s, node.active.empty ()); + + // Verify that the election was cancelled by the checkup thread + ASSERT_GT (node.stats.count (nano::stat::type::active_elections, nano::stat::detail::cancel_checkup), 0) + << "Expected election to be cancelled by checkup thread"; } \ No newline at end of file diff --git a/nano/core_test/active_elections_index.cpp b/nano/core_test/active_elections_index.cpp new file mode 100644 index 000000000..7c4cb4a5f --- /dev/null +++ b/nano/core_test/active_elections_index.cpp @@ -0,0 +1,462 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +using namespace std::chrono_literals; + +namespace +{ +// Full node is necessary to create elections +class test_context final +{ +public: + nano::test::system system; + nano::node & node; + std::deque> blocks; + + explicit test_context (size_t count = 10) : + node{ *system.add_node () } + { + auto chain = nano::test::setup_chain (system, node, count); + blocks.insert (blocks.end (), chain.begin (), chain.end ()); + } + + std::shared_ptr next_block () + { + debug_assert (!blocks.empty ()); + auto block = blocks.front (); + blocks.pop_front (); + return block; + } + + std::shared_ptr random_election (nano::election_behavior behavior = nano::election_behavior::priority) + { + return std::make_shared (node, next_block (), behavior); + } +}; +} + +TEST (active_elections_index, insert) +{ + test_context context{ 10 }; + nano::active_elections_index index; + + // Create elections with different behaviors and buckets + auto election1 = context.random_election (nano::election_behavior::priority); + auto election2 = context.random_election (nano::election_behavior::hinted); + auto election3 = context.random_election (nano::election_behavior::optimistic); + + // Test initial state + ASSERT_EQ (index.size (), 0); + ASSERT_FALSE (index.exists (election1)); + + // Insert first election + index.insert (election1, nano::election_behavior::priority, 1, 100); + ASSERT_EQ (index.size (), 1); + ASSERT_TRUE (index.exists (election1)); + ASSERT_TRUE (index.exists (election1->qualified_root)); + ASSERT_EQ (index.size (nano::election_behavior::priority), 1); + ASSERT_EQ (index.size (nano::election_behavior::priority, 1), 1); + + // Insert second election with different behavior + index.insert (election2, nano::election_behavior::hinted, 2, 200); + ASSERT_EQ (index.size (), 2); + ASSERT_TRUE (index.exists (election2)); + ASSERT_EQ (index.size (nano::election_behavior::hinted), 1); + ASSERT_EQ (index.size (nano::election_behavior::hinted, 2), 1); + + // Insert third election + index.insert (election3, nano::election_behavior::optimistic, 1, 50); + ASSERT_EQ (index.size (), 3); + ASSERT_EQ (index.size (nano::election_behavior::optimistic), 1); + ASSERT_EQ (index.size (nano::election_behavior::optimistic, 1), 1); +} + +TEST (active_elections_index, erase) +{ + test_context context{ 5 }; + nano::active_elections_index index; + + auto election1 = context.random_election (); + auto election2 = context.random_election (); + auto election3 = context.random_election (); + + // Insert elections + index.insert (election1, nano::election_behavior::priority, 1, 100); + index.insert (election2, nano::election_behavior::priority, 1, 200); + index.insert (election3, nano::election_behavior::hinted, 2, 300); + + ASSERT_EQ (index.size (), 3); + ASSERT_EQ (index.size (nano::election_behavior::priority), 2); + ASSERT_EQ (index.size (nano::election_behavior::priority, 1), 2); + + // Erase existing election + ASSERT_TRUE (index.erase (election1)); + ASSERT_EQ (index.size (), 2); + ASSERT_FALSE (index.exists (election1)); + ASSERT_FALSE (index.exists (election1->qualified_root)); + ASSERT_EQ (index.size (nano::election_behavior::priority), 1); + ASSERT_EQ (index.size (nano::election_behavior::priority, 1), 1); + + // Try to erase non-existent election + ASSERT_FALSE (index.erase (election1)); + ASSERT_EQ (index.size (), 2); + + // Erase remaining elections + ASSERT_TRUE (index.erase (election2)); + ASSERT_TRUE (index.erase (election3)); + ASSERT_EQ (index.size (), 0); + ASSERT_EQ (index.size (nano::election_behavior::priority), 0); + ASSERT_EQ (index.size (nano::election_behavior::hinted), 0); +} + +TEST (active_elections_index, update) +{ + test_context context{ 5 }; + nano::active_elections_index index; + + auto election1 = context.random_election (); + auto election2 = context.random_election (); + + // Insert elections + index.insert (election1, nano::election_behavior::priority, 1, 100); + index.insert (election2, nano::election_behavior::hinted, 2, 200); + + ASSERT_EQ (index.size (nano::election_behavior::priority), 1); + ASSERT_EQ (index.size (nano::election_behavior::hinted), 1); + ASSERT_EQ (index.size (nano::election_behavior::priority, 1), 1); + ASSERT_EQ (index.size (nano::election_behavior::hinted, 2), 1); + + // Update election1 behavior from priority to optimistic + index.update (election1, nano::election_behavior::optimistic); + + ASSERT_EQ (index.size (), 2); + ASSERT_EQ (index.size (nano::election_behavior::priority), 0); + ASSERT_EQ (index.size (nano::election_behavior::optimistic), 1); + ASSERT_EQ (index.size (nano::election_behavior::hinted), 1); + ASSERT_EQ (index.size (nano::election_behavior::priority, 1), 0); + ASSERT_EQ (index.size (nano::election_behavior::optimistic, 1), 1); + + // Verify election still exists + ASSERT_TRUE (index.exists (election1)); + + // Update with same behavior (no change) + index.update (election2, nano::election_behavior::hinted); + ASSERT_EQ (index.size (nano::election_behavior::hinted), 1); + ASSERT_EQ (index.size (nano::election_behavior::hinted, 2), 1); +} + +TEST (active_elections_index, exists) +{ + test_context context{ 3 }; + nano::active_elections_index index; + + auto election1 = context.random_election (); + auto election2 = context.random_election (); + auto election3 = context.random_election (); + + // Test non-existent elections + ASSERT_FALSE (index.exists (election1)); + ASSERT_FALSE (index.exists (election1->qualified_root)); + + // Insert election1 + index.insert (election1, nano::election_behavior::priority, 1, 100); + + // Test exists with election pointer + ASSERT_TRUE (index.exists (election1)); + ASSERT_FALSE (index.exists (election2)); + ASSERT_FALSE (index.exists (election3)); + + // Test exists with qualified_root + ASSERT_TRUE (index.exists (election1->qualified_root)); + ASSERT_FALSE (index.exists (election2->qualified_root)); + ASSERT_FALSE (index.exists (election3->qualified_root)); + + // Add more elections and test + index.insert (election2, nano::election_behavior::hinted, 2, 200); + ASSERT_TRUE (index.exists (election2)); + ASSERT_TRUE (index.exists (election2->qualified_root)); +} + +TEST (active_elections_index, election_lookup) +{ + test_context context{ 3 }; + nano::active_elections_index index; + + auto election1 = context.random_election (); + auto election2 = context.random_election (); + + // Test lookup on empty index + ASSERT_EQ (index.election (election1->qualified_root), nullptr); + + // Insert elections + index.insert (election1, nano::election_behavior::priority, 1, 100); + index.insert (election2, nano::election_behavior::hinted, 2, 200); + + // Test successful lookup + ASSERT_EQ (index.election (election1->qualified_root), election1); + ASSERT_EQ (index.election (election2->qualified_root), election2); + + // Test lookup for non-existent root + auto election3 = context.random_election (); + ASSERT_EQ (index.election (election3->qualified_root), nullptr); + + // Test info method + auto info1 = index.info (election1); + ASSERT_TRUE (info1.has_value ()); + ASSERT_EQ (info1->election, election1); + ASSERT_EQ (info1->behavior, nano::election_behavior::priority); + ASSERT_EQ (info1->bucket, 1); + ASSERT_EQ (info1->priority, 100); + + auto info3 = index.info (election3); + ASSERT_FALSE (info3.has_value ()); +} + +TEST (active_elections_index, size_operations) +{ + test_context context{ 10 }; + nano::active_elections_index index; + + // Test empty index + ASSERT_EQ (index.size (), 0); + ASSERT_EQ (index.size (nano::election_behavior::priority), 0); + ASSERT_EQ (index.size (nano::election_behavior::priority, 1), 0); + + // Add elections with different behaviors and buckets + auto e1 = context.random_election (); + auto e2 = context.random_election (); + auto e3 = context.random_election (); + auto e4 = context.random_election (); + auto e5 = context.random_election (); + + index.insert (e1, nano::election_behavior::priority, 1, 100); + index.insert (e2, nano::election_behavior::priority, 1, 200); + index.insert (e3, nano::election_behavior::priority, 2, 300); + index.insert (e4, nano::election_behavior::hinted, 1, 400); + index.insert (e5, nano::election_behavior::optimistic, 3, 500); + + // Test total size + ASSERT_EQ (index.size (), 5); + + // Test size by behavior + ASSERT_EQ (index.size (nano::election_behavior::priority), 3); + ASSERT_EQ (index.size (nano::election_behavior::hinted), 1); + ASSERT_EQ (index.size (nano::election_behavior::optimistic), 1); + ASSERT_EQ (index.size (nano::election_behavior::manual), 0); + + // Test size by behavior and bucket + ASSERT_EQ (index.size (nano::election_behavior::priority, 1), 2); + ASSERT_EQ (index.size (nano::election_behavior::priority, 2), 1); + ASSERT_EQ (index.size (nano::election_behavior::priority, 3), 0); + ASSERT_EQ (index.size (nano::election_behavior::hinted, 1), 1); + ASSERT_EQ (index.size (nano::election_behavior::optimistic, 3), 1); +} + +TEST (active_elections_index, last) +{ + test_context context{ 10 }; + nano::active_elections_index index; + + // Test empty index + auto [empty_election, empty_priority] = index.last (nano::election_behavior::priority, 1); + ASSERT_EQ (empty_election, nullptr); + ASSERT_EQ (empty_priority, std::numeric_limits::max ()); + + // Add elections with different priorities + auto e1 = context.random_election (); + auto e2 = context.random_election (); + auto e3 = context.random_election (); + auto e4 = context.random_election (); + + index.insert (e1, nano::election_behavior::priority, 1, 300); // Highest priority value in bucket 1 + index.insert (e2, nano::election_behavior::priority, 1, 100); + index.insert (e3, nano::election_behavior::priority, 1, 200); + index.insert (e4, nano::election_behavior::priority, 2, 50); // Only election in bucket 2 + + // Test last for bucket 1 (should return e1 with priority 300 - highest value) + auto [top1_election, top1_priority] = index.last (nano::election_behavior::priority, 1); + ASSERT_EQ (top1_election, e1); + ASSERT_EQ (top1_priority, 300); + + // Test last for bucket 2 (should return e4 with priority 50) + auto [top2_election, top2_priority] = index.last (nano::election_behavior::priority, 2); + ASSERT_EQ (top2_election, e4); + ASSERT_EQ (top2_priority, 50); + + // Test last for empty bucket + auto [top3_election, top3_priority] = index.last (nano::election_behavior::priority, 3); + ASSERT_EQ (top3_election, nullptr); + ASSERT_EQ (top3_priority, std::numeric_limits::max ()); + + // Test last for different behavior + auto e5 = context.random_election (); + index.insert (e5, nano::election_behavior::hinted, 1, 75); + auto [top4_election, top4_priority] = index.last (nano::election_behavior::hinted, 1); + ASSERT_EQ (top4_election, e5); + ASSERT_EQ (top4_priority, 75); +} + +TEST (active_elections_index, list_with_cutoff) +{ + test_context context{ 5 }; + nano::active_elections_index index; + + auto e1 = context.random_election (); + auto e2 = context.random_election (); + auto e3 = context.random_election (); + + // Insert elections (they start with timestamp at epoch) + index.insert (e1, nano::election_behavior::priority, 1, 100); + index.insert (e2, nano::election_behavior::priority, 2, 200); + index.insert (e3, nano::election_behavior::hinted, 1, 300); + + auto initial_time = std::chrono::steady_clock::now (); + auto cutoff = initial_time + 1s; // All elections should be before this cutoff + + // Process all elections and update their timestamps to initial_time + auto elections_list = index.list (cutoff, initial_time); + + // All elections should have been processed + ASSERT_EQ (elections_list.size (), 3); + std::set> processed (elections_list.begin (), elections_list.end ()); + ASSERT_TRUE (processed.count (e1)); + ASSERT_TRUE (processed.count (e2)); + ASSERT_TRUE (processed.count (e3)); + + // Now test with earlier cutoff - no elections should be processed + // because their timestamps were updated to initial_time in the previous list call + auto earlier_cutoff = initial_time - 1s; + auto elections_list2 = index.list (earlier_cutoff); + + ASSERT_EQ (elections_list2.size (), 0); + ASSERT_TRUE (elections_list2.empty ()); + + // Test with future time - should process all elections again + auto future_time = initial_time + 2s; + auto future_cutoff = future_time - 1s; // Between initial_time and future_time + auto elections_list3 = index.list (future_cutoff, future_time); + + // All elections should be processed since cutoff is after their timestamp + ASSERT_EQ (elections_list3.size (), 3); +} + +TEST (active_elections_index, trigger) +{ + test_context context{ 3 }; + nano::active_elections_index index; + + auto e1 = context.random_election (); + auto e2 = context.random_election (); + + // Insert elections (they start with timestamp at epoch) + index.insert (e1, nano::election_behavior::priority, 1, 100); + index.insert (e2, nano::election_behavior::priority, 2, 200); + + // Process elections to update their timestamps to a future time + auto future_time = std::chrono::steady_clock::now () + 1h; + auto cutoff = future_time + 1s; + index.list (cutoff, future_time); + + // Trigger e1 to reset its timestamp + index.trigger (e1); + + // Check that e1 has been triggered (timestamp reset to epoch) + // We can verify this by using list with a very early cutoff + auto very_early_cutoff = std::chrono::steady_clock::time_point{} + 1ms; + auto triggered_elections = index.list (very_early_cutoff); + + // Only e1 should have been processed (because it was triggered) + ASSERT_EQ (triggered_elections.size (), 1); + ASSERT_EQ (triggered_elections[0], e1); +} + +TEST (active_elections_index, any) +{ + test_context context{ 3 }; + nano::active_elections_index index; + + // Test empty index + auto cutoff = std::chrono::steady_clock::now (); + ASSERT_FALSE (index.any (cutoff)); + + auto e1 = context.random_election (); + auto e2 = context.random_election (); + + // Insert elections (they will have timestamp at epoch initially) + index.insert (e1, nano::election_behavior::priority, 1, 100); + index.insert (e2, nano::election_behavior::priority, 2, 200); + + // Check with cutoff in the future - should find elections + auto future_cutoff = std::chrono::steady_clock::now () + 1s; + ASSERT_TRUE (index.any (future_cutoff)); + + // Update timestamps by processing elections + auto now = std::chrono::steady_clock::now (); + index.list (future_cutoff, now); + + // Check with cutoff in the past - should not find elections + auto past_cutoff = now - 1s; + ASSERT_FALSE (index.any (past_cutoff)); + + // Trigger one election to reset its timestamp + index.trigger (e1); + + // Now should find the triggered election even with past cutoff + ASSERT_TRUE (index.any (past_cutoff)); +} + +TEST (active_elections_index, clear) +{ + test_context context{ 5 }; + nano::active_elections_index index; + + // Add multiple elections + auto e1 = context.random_election (); + auto e2 = context.random_election (); + auto e3 = context.random_election (); + auto e4 = context.random_election (); + + index.insert (e1, nano::election_behavior::priority, 1, 100); + index.insert (e2, nano::election_behavior::priority, 2, 200); + index.insert (e3, nano::election_behavior::hinted, 1, 300); + index.insert (e4, nano::election_behavior::optimistic, 3, 400); + + // Verify index is populated + ASSERT_EQ (index.size (), 4); + ASSERT_EQ (index.size (nano::election_behavior::priority), 2); + ASSERT_EQ (index.size (nano::election_behavior::hinted), 1); + ASSERT_EQ (index.size (nano::election_behavior::optimistic), 1); + ASSERT_TRUE (index.exists (e1)); + ASSERT_TRUE (index.exists (e2->qualified_root)); + + // Clear the index + index.clear (); + + // Verify everything is cleared + ASSERT_EQ (index.size (), 0); + ASSERT_EQ (index.size (nano::election_behavior::priority), 0); + ASSERT_EQ (index.size (nano::election_behavior::hinted), 0); + ASSERT_EQ (index.size (nano::election_behavior::optimistic), 0); + ASSERT_EQ (index.size (nano::election_behavior::manual), 0); + ASSERT_FALSE (index.exists (e1)); + ASSERT_FALSE (index.exists (e2)); + ASSERT_FALSE (index.exists (e3)); + ASSERT_FALSE (index.exists (e4)); + ASSERT_FALSE (index.exists (e1->qualified_root)); + + // Test that we can still use the index after clearing + index.insert (e1, nano::election_behavior::manual, 0, 50); + ASSERT_EQ (index.size (), 1); + ASSERT_TRUE (index.exists (e1)); + ASSERT_EQ (index.size (nano::election_behavior::manual), 1); +} \ No newline at end of file diff --git a/nano/core_test/block_store.cpp b/nano/core_test/block_store.cpp index 636001405..1b1a679d6 100644 --- a/nano/core_test/block_store.cpp +++ b/nano/core_test/block_store.cpp @@ -627,7 +627,7 @@ TEST (mdb_block_store, supported_version_upgrades) { nano::store::lmdb::component store (logger, path, nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (store, nano::dev::constants, stats, logger); + nano::ledger ledger (store, nano::dev::network_params, stats, logger); auto transaction (store.tx_begin_write ()); // Lower the database to the max version unsupported for upgrades store.version.put (transaction, store.version_minimum - 1); @@ -643,7 +643,7 @@ TEST (mdb_block_store, supported_version_upgrades) { nano::store::lmdb::component store (logger, path1, nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (store, nano::dev::constants, stats, logger); + nano::ledger ledger (store, nano::dev::network_params, stats, logger); auto transaction (store.tx_begin_write ()); // Lower the database version to the minimum version supported for upgrade. store.version.put (transaction, store.version_minimum); @@ -875,7 +875,7 @@ TEST (block_store, cemented_count_cache) nano::logger logger; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); ASSERT_EQ (1, ledger.cemented_count ()); } @@ -960,7 +960,7 @@ TEST (mdb_block_store, sideband_height) nano::store::lmdb::component store (logger, nano::unique_path () / "data.ldb", nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (store, nano::dev::constants, stats, logger); + nano::ledger ledger (store, nano::dev::network_params, stats, logger); nano::block_builder builder; auto transaction = ledger.tx_begin_write (); nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; diff --git a/nano/core_test/bootstrap.cpp b/nano/core_test/bootstrap.cpp index 4356b2cd9..e99b17fae 100644 --- a/nano/core_test/bootstrap.cpp +++ b/nano/core_test/bootstrap.cpp @@ -309,11 +309,10 @@ TEST (bootstrap, account_inductive) .sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub) .work (*system.work.generate (send1->hash ())) .build (); - // std::cerr << "Genesis: " << nano::dev::genesis->hash ().to_string () << std::endl; - // std::cerr << "Send1: " << send1->hash ().to_string () << std::endl; - // std::cerr << "Send2: " << send2->hash ().to_string () << std::endl; + ASSERT_EQ (nano::block_status::progress, node0.process (send1)); ASSERT_EQ (nano::block_status::progress, node0.process (send2)); + auto & node1 = *system.add_node (flags); ASSERT_TIMELY (50s, node1.block (send2->hash ()) != nullptr); } @@ -352,7 +351,7 @@ TEST (bootstrap, trace_base) ASSERT_EQ (nano::block_status::progress, node0.process (send1)); ASSERT_EQ (nano::block_status::progress, node0.process (receive1)); - ASSERT_EQ (node1.ledger.any.receivable_end (), node1.ledger.any.receivable_upper_bound (node1.ledger.tx_begin_read (), key.pub, 0)); + ASSERT_TIMELY (10s, node1.ledger.any.receivable_upper_bound (node1.ledger.tx_begin_read (), key.pub, 0) != node1.ledger.any.receivable_end ()); ASSERT_TIMELY (10s, node1.block (receive1->hash ()) != nullptr); } diff --git a/nano/core_test/bootstrap_server.cpp b/nano/core_test/bootstrap_server.cpp index 45e493807..391c90eb1 100644 --- a/nano/core_test/bootstrap_server.cpp +++ b/nano/core_test/bootstrap_server.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include diff --git a/nano/core_test/cementing_set.cpp b/nano/core_test/cementing_set.cpp index 8d2d39bdd..0b2c6f192 100644 --- a/nano/core_test/cementing_set.cpp +++ b/nano/core_test/cementing_set.cpp @@ -180,7 +180,7 @@ TEST (confirmation_callback, confirmed_history) // Confirm send1 election->force_confirm (); ASSERT_TIMELY_EQ (10s, node->active.size (), 0); - ASSERT_EQ (0, node->active.recently_cemented.list ().size ()); + ASSERT_EQ (0, node->active.recently_cemented.size ()); ASSERT_TRUE (node->active.empty ()); auto transaction = node->ledger.tx_begin_read (); @@ -200,7 +200,7 @@ TEST (confirmation_callback, confirmed_history) ASSERT_TIMELY_EQ (10s, node->stats.count (nano::stat::type::confirmation_observer, nano::stat::detail::active_quorum, nano::stat::dir::out), 1); // Each block that's confirmed is in the recently_cemented history - ASSERT_EQ (2, node->active.recently_cemented.list ().size ()); + ASSERT_EQ (2, node->active.recently_cemented.size ()); ASSERT_TRUE (node->active.empty ()); // Confirm the callback is not called under this circumstance diff --git a/nano/core_test/confirmation_solicitor.cpp b/nano/core_test/confirmation_solicitor.cpp index ca48a2ee8..096dc1474 100644 --- a/nano/core_test/confirmation_solicitor.cpp +++ b/nano/core_test/confirmation_solicitor.cpp @@ -46,11 +46,11 @@ TEST (confirmation_solicitor, batches) nano::lock_guard guard (node2.active.mutex); for (size_t i (0); i < nano::network::confirm_req_hashes_max; ++i) { - auto election (std::make_shared (node2, send, nullptr, nullptr, nano::election_behavior::priority)); + auto election (std::make_shared (node2, send, nano::election_behavior::priority)); ASSERT_FALSE (solicitor.add (*election)); } // Reached the maximum amount of requests for the channel - auto election (std::make_shared (node2, send, nullptr, nullptr, nano::election_behavior::priority)); + auto election (std::make_shared (node2, send, nano::election_behavior::priority)); // Broadcasting should be immediate ASSERT_EQ (0, node2.stats.count (nano::stat::type::message, nano::stat::detail::publish, nano::stat::dir::out)); ASSERT_FALSE (solicitor.broadcast (*election)); @@ -92,7 +92,7 @@ TEST (confirmation_solicitor, different_hash) .work (*system.work.generate (nano::dev::genesis->hash ())) .build (); send->sideband_set ({}); - auto election (std::make_shared (node2, send, nullptr, nullptr, nano::election_behavior::priority)); + auto election (std::make_shared (node2, send, nano::election_behavior::priority)); // Add a vote for something else, not the winner election->last_votes[representative.account] = { std::chrono::steady_clock::now (), 1, 1 }; // Ensure the request and broadcast goes through @@ -136,7 +136,7 @@ TEST (confirmation_solicitor, bypass_max_requests_cap) .work (*system.work.generate (nano::dev::genesis->hash ())) .build (); send->sideband_set ({}); - auto election (std::make_shared (node2, send, nullptr, nullptr, nano::election_behavior::priority)); + auto election (std::make_shared (node2, send, nano::election_behavior::priority)); // Add a vote for something else, not the winner for (auto const & rep : representatives) { @@ -149,7 +149,7 @@ TEST (confirmation_solicitor, bypass_max_requests_cap) ASSERT_TIMELY_EQ (6s, max_representatives + 1, node2.stats.count (nano::stat::type::message, nano::stat::detail::confirm_req, nano::stat::dir::out)); solicitor.prepare (representatives); - auto election2 (std::make_shared (node2, send, nullptr, nullptr, nano::election_behavior::priority)); + auto election2 (std::make_shared (node2, send, nano::election_behavior::priority)); ASSERT_FALSE (solicitor.add (*election2)); ASSERT_FALSE (solicitor.broadcast (*election2)); diff --git a/nano/core_test/difficulty.cpp b/nano/core_test/difficulty.cpp index 77fbc418d..2e46bd940 100644 --- a/nano/core_test/difficulty.cpp +++ b/nano/core_test/difficulty.cpp @@ -10,11 +10,8 @@ #include -TEST (difficultyDeathTest, multipliers) +TEST (difficulty, multipliers) { - // For ASSERT_DEATH_IF_SUPPORTED - testing::FLAGS_gtest_death_test_style = "threadsafe"; - { uint64_t base = 0xff00000000000000; uint64_t difficulty = 0xfff27e7a57c285cd; @@ -51,19 +48,14 @@ TEST (difficultyDeathTest, multipliers) ASSERT_EQ (difficulty, nano::difficulty::from_multiplier (expected_multiplier, base)); } - // The death checks don't fail on a release config, so guard against them -#ifndef NDEBUG - // Causes valgrind to be noisy - if (!nano::running_within_valgrind ()) { uint64_t base = 0xffffffc000000000; uint64_t difficulty_nil = 0; double multiplier_nil = 0.; - ASSERT_DEATH_IF_SUPPORTED (nano::difficulty::to_multiplier (difficulty_nil, base), ""); - ASSERT_DEATH_IF_SUPPORTED (nano::difficulty::from_multiplier (multiplier_nil, base), ""); + ASSERT_EQ (0., nano::difficulty::to_multiplier (difficulty_nil, base)); + ASSERT_EQ (0, nano::difficulty::from_multiplier (multiplier_nil, base)); } -#endif } TEST (difficulty, overflow) diff --git a/nano/core_test/election.cpp b/nano/core_test/election.cpp index 8cabb88cd..280cbb74e 100644 --- a/nano/core_test/election.cpp +++ b/nano/core_test/election.cpp @@ -19,7 +19,7 @@ TEST (election, construction) nano::test::system system (1); auto & node = *system.nodes[0]; auto election = std::make_shared ( - node, nano::dev::genesis, [] (auto const &) {}, [] (auto const &) {}, nano::election_behavior::priority); + node, nano::dev::genesis, nano::election_behavior::priority, [] (auto const &) {}, [] (auto const &) {}, [] (auto const &) {}); } TEST (election, behavior) @@ -152,10 +152,12 @@ TEST (election, quorum_minimum_confirm_success) .sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub) .build (); node1.work_generate_blocking (*send1); - node1.process_active (send1); + + nano::test::process (node1, { send1 }); auto election = nano::test::start_election (system, node1, send1->hash ()); ASSERT_NE (nullptr, election); ASSERT_EQ (1, election->blocks ().size ()); + auto vote = nano::test::make_final_vote (nano::dev::genesis_key, { send1->hash () }); ASSERT_EQ (nano::vote_code::vote, node1.vote_router.vote (vote).at (send1->hash ())); ASSERT_NE (nullptr, node1.block (send1->hash ())); @@ -182,7 +184,7 @@ TEST (election, quorum_minimum_confirm_fail) .sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub) .build (); - node1.process_active (send1); + nano::test::process (node1, { send1 }); auto election = nano::test::start_election (system, node1, send1->hash ()); ASSERT_NE (nullptr, election); ASSERT_EQ (1, election->blocks ().size ()); diff --git a/nano/core_test/fork_cache.cpp b/nano/core_test/fork_cache.cpp index b11591f2f..91c7d33f6 100644 --- a/nano/core_test/fork_cache.cpp +++ b/nano/core_test/fork_cache.cpp @@ -1,4 +1,5 @@ #include +#include #include #include diff --git a/nano/core_test/ledger.cpp b/nano/core_test/ledger.cpp index 3e0a93222..c3537d5f1 100644 --- a/nano/core_test/ledger.cpp +++ b/nano/core_test/ledger.cpp @@ -876,7 +876,7 @@ TEST (ledger, double_open) nano::logger logger; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); auto transaction = ledger.tx_begin_write (); nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; nano::keypair key2; @@ -4810,7 +4810,7 @@ TEST (ledger, dependents_confirmed_pruning) nano::logger logger; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); ledger.pruning = true; auto transaction = ledger.tx_begin_write (); nano::block_builder builder; @@ -4908,7 +4908,7 @@ TEST (ledger, cache) }; cache_check (ledger); - cache_check (nano::ledger (store, nano::dev::constants, stats, logger)); + cache_check (nano::ledger (store, nano::dev::network_params, stats, logger)); nano::keypair key; auto const latest = ledger.any.account_head (ledger.tx_begin_read (), nano::dev::genesis_key.pub); @@ -4938,7 +4938,7 @@ TEST (ledger, cache) ++block_count; --genesis_weight; cache_check (ledger); - cache_check (nano::ledger (store, nano::dev::constants, stats, logger)); + cache_check (nano::ledger (store, nano::dev::network_params, stats, logger)); { auto transaction = ledger.tx_begin_write (); @@ -4948,7 +4948,7 @@ TEST (ledger, cache) ++block_count; ++account_count; cache_check (ledger); - cache_check (nano::ledger (store, nano::dev::constants, stats, logger)); + cache_check (nano::ledger (store, nano::dev::network_params, stats, logger)); { auto transaction = ledger.tx_begin_write (); @@ -4958,7 +4958,7 @@ TEST (ledger, cache) ++cemented_count; cache_check (ledger); - cache_check (nano::ledger (store, nano::dev::constants, stats, logger)); + cache_check (nano::ledger (store, nano::dev::network_params, stats, logger)); { auto transaction = ledger.tx_begin_write (); @@ -4968,7 +4968,7 @@ TEST (ledger, cache) ++cemented_count; cache_check (ledger); - cache_check (nano::ledger (store, nano::dev::constants, stats, logger)); + cache_check (nano::ledger (store, nano::dev::network_params, stats, logger)); { auto transaction = ledger.tx_begin_write (); @@ -4976,7 +4976,7 @@ TEST (ledger, cache) } ++pruned_count; cache_check (ledger); - cache_check (nano::ledger (store, nano::dev::constants, stats, logger)); + cache_check (nano::ledger (store, nano::dev::network_params, stats, logger)); } } @@ -4985,7 +4985,7 @@ TEST (ledger, pruning_action) nano::logger logger; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); ledger.pruning = true; auto transaction = ledger.tx_begin_write (); nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; @@ -5069,7 +5069,7 @@ TEST (ledger, pruning_large_chain) nano::logger logger; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); ledger.pruning = true; auto transaction = ledger.tx_begin_write (); nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; @@ -5123,7 +5123,7 @@ TEST (ledger, pruning_source_rollback) nano::logger logger; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); ledger.pruning = true; auto transaction = ledger.tx_begin_write (); nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; @@ -5210,7 +5210,7 @@ TEST (ledger, pruning_source_rollback_legacy) nano::logger logger; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); ledger.pruning = true; auto transaction = ledger.tx_begin_write (); nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; @@ -5322,7 +5322,7 @@ TEST (ledger, pruning_legacy_blocks) nano::logger logger; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); ledger.pruning = true; nano::keypair key1; auto transaction = ledger.tx_begin_write (); @@ -5407,7 +5407,7 @@ TEST (ledger, pruning_safe_functions) nano::logger logger; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); ledger.pruning = true; auto transaction = ledger.tx_begin_write (); nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; @@ -5457,7 +5457,7 @@ TEST (ledger, random_blocks) nano::logger logger; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); ledger.pruning = true; auto transaction = ledger.tx_begin_write (); nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; @@ -5544,7 +5544,7 @@ TEST (ledger, migrate_lmdb_to_rocksdb) boost::asio::ip::address_v6 address (boost::asio::ip::make_address_v6 ("::ffff:127.0.0.1")); uint16_t port = 100; nano::store::lmdb::component store{ logger, path / "data.ldb", nano::dev::constants }; - nano::ledger ledger{ store, nano::dev::constants, system.stats, system.logger }; + nano::ledger ledger{ store, nano::dev::network_params, system.stats, system.logger }; nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; std::shared_ptr send = nano::state_block_builder () diff --git a/nano/core_test/ledger_confirm.cpp b/nano/core_test/ledger_confirm.cpp index 692fe15ae..2e4ca59c9 100644 --- a/nano/core_test/ledger_confirm.cpp +++ b/nano/core_test/ledger_confirm.cpp @@ -760,7 +760,7 @@ TEST (ledger_confirm, pruned_source) auto path (nano::unique_path ()); auto store = nano::make_store (system.logger, path, nano::dev::constants); - nano::ledger ledger (*store, nano::dev::constants, system.stats, system.logger); + nano::ledger ledger (*store, nano::dev::network_params, system.stats, system.logger); ledger.pruning = true; nano::store::write_queue write_queue; nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; @@ -844,7 +844,7 @@ TEST (ledger_confirmDeathTest, rollback_added_block) auto path (nano::unique_path ()); auto store = nano::make_store (system.logger, path, nano::dev::constants); - nano::ledger ledger (*store, nano::dev::constants, system.stats, system.logger); + nano::ledger ledger (*store, nano::dev::network_params, system.stats, system.logger); nano::store::write_queue write_queue; nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; nano::keypair key1; diff --git a/nano/core_test/message.cpp b/nano/core_test/message.cpp index 12ff989af..3faa468da 100644 --- a/nano/core_test/message.cpp +++ b/nano/core_test/message.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include diff --git a/nano/core_test/message_deserializer.cpp b/nano/core_test/message_deserializer.cpp index 7d65d6528..86cfadc87 100644 --- a/nano/core_test/message_deserializer.cpp +++ b/nano/core_test/message_deserializer.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include diff --git a/nano/core_test/node.cpp b/nano/core_test/node.cpp index 3f0c49336..40d69689c 100644 --- a/nano/core_test/node.cpp +++ b/nano/core_test/node.cpp @@ -1605,7 +1605,7 @@ TEST (node, block_confirm) ASSERT_TIMELY (5s, election = node2.active.election (send1_copy->qualified_root ())); // Make node2 genesis representative so it can vote system.wallet (1)->insert_adhoc (nano::dev::genesis_key.prv); - ASSERT_TIMELY_EQ (10s, node1.active.recently_cemented.list ().size (), 1); + ASSERT_TIMELY_EQ (10s, node1.active.recently_cemented.size (), 1); } TEST (node, confirm_quorum) diff --git a/nano/core_test/optimistic_scheduler.cpp b/nano/core_test/optimistic_scheduler.cpp index 7466584b6..542c758d0 100644 --- a/nano/core_test/optimistic_scheduler.cpp +++ b/nano/core_test/optimistic_scheduler.cpp @@ -17,8 +17,11 @@ using namespace std::chrono_literals; */ TEST (optimistic_scheduler, activate_one) { - nano::test::system system{}; - auto & node = *system.add_node (); + nano::test::system system; + + nano::node_config config; + config.priority_scheduler.enable = false; // Disable priority scheduler to avoid interference + auto & node = *system.add_node (config); // Needs to be greater than optimistic scheduler `gap_threshold` const int howmany_blocks = 64; @@ -41,8 +44,11 @@ TEST (optimistic_scheduler, activate_one) */ TEST (optimistic_scheduler, activate_one_zero_conf) { - nano::test::system system{}; - auto & node = *system.add_node (); + nano::test::system system; + + nano::node_config config; + config.priority_scheduler.enable = false; // Disable priority scheduler to avoid interference + auto & node = *system.add_node (config); // Can be smaller than optimistic scheduler `gap_threshold` // This is meant to activate short account chains (eg. binary tree spam leaf accounts) @@ -63,8 +69,11 @@ TEST (optimistic_scheduler, activate_one_zero_conf) */ TEST (optimistic_scheduler, activate_many) { - nano::test::system system{}; - auto & node = *system.add_node (); + nano::test::system system; + + nano::node_config config; + config.priority_scheduler.enable = false; // Disable priority scheduler to avoid interference + auto & node = *system.add_node (config); // Needs to be greater than optimistic scheduler `gap_threshold` const int howmany_blocks = 64; @@ -72,8 +81,8 @@ TEST (optimistic_scheduler, activate_many) auto chains = nano::test::setup_chains (system, node, howmany_chains, howmany_blocks, nano::dev::genesis_key, /* do not confirm */ false); - // Ensure all unconfirmed accounts head block gets activated - ASSERT_TIMELY (5s, std::all_of (chains.begin (), chains.end (), [&] (auto const & entry) { + // Ensure all unconfirmed account head blocks get activated + ASSERT_TIMELY (15s, std::all_of (chains.begin (), chains.end (), [&] (auto const & entry) { auto const & [account, blocks] = entry; auto const & block = blocks.back (); auto election = node.active.election (block->qualified_root ()); @@ -86,7 +95,8 @@ TEST (optimistic_scheduler, activate_many) */ TEST (optimistic_scheduler, under_gap_threshold) { - nano::test::system system{}; + nano::test::system system; + nano::node_config config = system.default_config (); config.backlog_scan.enable = false; auto & node = *system.add_node (config); diff --git a/nano/core_test/processor_service.cpp b/nano/core_test/processor_service.cpp index 284bfffdd..df5c263b5 100644 --- a/nano/core_test/processor_service.cpp +++ b/nano/core_test/processor_service.cpp @@ -17,7 +17,7 @@ TEST (processor_service, bad_send_signature) nano::test::system system; auto store = nano::make_store (system.logger, nano::unique_path (), nano::dev::constants); - nano::ledger ledger (*store, nano::dev::constants, system.stats, system.logger); + nano::ledger ledger (*store, nano::dev::network_params, system.stats, system.logger); auto transaction = ledger.tx_begin_write (); nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; auto info1 = ledger.any.account_get (transaction, nano::dev::genesis_key.pub); @@ -41,7 +41,7 @@ TEST (processor_service, bad_receive_signature) nano::test::system system; auto store = nano::make_store (system.logger, nano::unique_path (), nano::dev::constants); - nano::ledger ledger (*store, nano::dev::constants, system.stats, system.logger); + nano::ledger ledger (*store, nano::dev::network_params, system.stats, system.logger); auto transaction = ledger.tx_begin_write (); nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; auto info1 = ledger.any.account_get (transaction, nano::dev::genesis_key.pub); diff --git a/nano/core_test/recently_cache.cpp b/nano/core_test/recently_cache.cpp new file mode 100644 index 000000000..14beedb1c --- /dev/null +++ b/nano/core_test/recently_cache.cpp @@ -0,0 +1,210 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ +std::shared_ptr make_test_block () +{ + nano::block_builder builder; + return builder.state () + .account (nano::dev::genesis_key.pub) + .previous (nano::test::random_hash ()) + .representative (nano::dev::genesis_key.pub) + .balance (nano::dev::constants.genesis_amount - 1) + .link (nano::dev::genesis_key.pub) + .sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub) + .work (0) + .build (); +} + +nano::election_status make_test_election_status () +{ + auto block = make_test_block (); + nano::election_status status; + status.winner = block; + status.type = nano::election_status_type::active_confirmed_quorum; + status.election_end = std::chrono::system_clock::now (); + status.election_duration = std::chrono::milliseconds (100); + return status; +} +} + +/* + * recently_confirmed_cache + */ + +TEST (recently_confirmed_cache, construction) +{ + nano::recently_confirmed_cache cache (10); + ASSERT_EQ (0, cache.size ()); +} + +TEST (recently_confirmed_cache, put) +{ + nano::recently_confirmed_cache cache (3); + std::vector roots; + std::vector hashes; + + // Add entries and test size limit + for (int i = 0; i < 5; ++i) + { + auto root = nano::test::random_qualified_root (); + auto hash = nano::test::random_hash (); + roots.push_back (root); + hashes.push_back (hash); + cache.put (root, hash); + } + + ASSERT_EQ (3, cache.size ()); + + // Test duplicate filtering + cache.put (roots[4], hashes[4]); + ASSERT_EQ (3, cache.size ()); + + // First entries should have been evicted (LRU) + ASSERT_FALSE (cache.contains (roots[0])); + ASSERT_FALSE (cache.contains (roots[1])); + + // Last entries should still be present + ASSERT_TRUE (cache.contains (roots[2])); + ASSERT_TRUE (cache.contains (roots[3])); + ASSERT_TRUE (cache.contains (roots[4])); +} + +TEST (recently_confirmed_cache, erase) +{ + nano::recently_confirmed_cache cache (10); + auto root = nano::test::random_qualified_root (); + auto hash = nano::test::random_hash (); + + cache.put (root, hash); + ASSERT_TRUE (cache.contains (hash)); + ASSERT_EQ (1, cache.size ()); + + cache.erase (hash); + ASSERT_FALSE (cache.contains (hash)); + ASSERT_FALSE (cache.contains (root)); + ASSERT_EQ (0, cache.size ()); +} + +TEST (recently_confirmed_cache, clear) +{ + nano::recently_confirmed_cache cache (10); + for (int i = 0; i < 5; ++i) + { + auto root = nano::test::random_qualified_root (); + auto hash = nano::test::random_hash (); + cache.put (root, hash); + } + + ASSERT_EQ (5, cache.size ()); + cache.clear (); + ASSERT_EQ (0, cache.size ()); +} + +/* + * recently_cemented_cache + */ + +TEST (recently_cemented_cache, construction) +{ + nano::recently_cemented_cache cache (10); + ASSERT_EQ (0, cache.size ()); +} + +TEST (recently_cemented_cache, put) +{ + nano::recently_cemented_cache cache (3); + std::vector statuses; + + // Add entries and test size limit + for (int i = 0; i < 5; ++i) + { + auto status = make_test_election_status (); + statuses.push_back (status); + cache.put (status); + } + + ASSERT_EQ (3, cache.size ()); + + // Test duplicate filtering + cache.put (statuses[4]); + ASSERT_EQ (3, cache.size ()); + + // First entries should have been evicted (LRU) + ASSERT_FALSE (cache.contains (statuses[0].winner->qualified_root ())); + ASSERT_FALSE (cache.contains (statuses[1].winner->qualified_root ())); + + // Last entries should still be present + ASSERT_TRUE (cache.contains (statuses[2].winner->qualified_root ())); + ASSERT_TRUE (cache.contains (statuses[3].winner->qualified_root ())); + ASSERT_TRUE (cache.contains (statuses[4].winner->qualified_root ())); +} + +TEST (recently_cemented_cache, erase) +{ + nano::recently_cemented_cache cache (10); + auto status = make_test_election_status (); + + cache.put (status); + ASSERT_TRUE (cache.contains (status.winner->hash ())); + ASSERT_EQ (1, cache.size ()); + + cache.erase (status.winner->hash ()); + ASSERT_FALSE (cache.contains (status.winner->hash ())); + ASSERT_FALSE (cache.contains (status.winner->qualified_root ())); + ASSERT_EQ (0, cache.size ()); +} + +TEST (recently_cemented_cache, clear) +{ + nano::recently_cemented_cache cache (10); + for (int i = 0; i < 5; ++i) + { + auto status = make_test_election_status (); + cache.put (status); + } + + ASSERT_EQ (5, cache.size ()); + cache.clear (); + ASSERT_EQ (0, cache.size ()); +} + +TEST (recently_cemented_cache, list) +{ + nano::recently_cemented_cache cache (10); + + // Test empty list + auto list = cache.list (); + ASSERT_TRUE (list.empty ()); + + // Add entries and test list functionality + std::vector statuses; + for (int i = 0; i < 5; ++i) + { + auto status = make_test_election_status (); + statuses.push_back (status); + cache.put (status); + } + + // Test full list + list = cache.list (); + ASSERT_EQ (5, list.size ()); + + // List should be in reverse order (most recent first) + for (size_t i = 0; i < list.size (); ++i) + { + ASSERT_EQ (statuses[4 - i].winner->hash (), list[i].winner->hash ()); + } + + // Test list with limit + auto limited_list = cache.list (3); + ASSERT_EQ (3, limited_list.size ()); +} \ No newline at end of file diff --git a/nano/core_test/stats.cpp b/nano/core_test/stats.cpp index 6ee083d27..77b5e1615 100644 --- a/nano/core_test/stats.cpp +++ b/nano/core_test/stats.cpp @@ -11,6 +11,11 @@ TEST (stats, counters) nano::test::system system; auto & node = *system.add_node (); + // Initial state + ASSERT_EQ (0, node.stats.count (nano::stat::type::ledger, nano::stat::dir::in)); + ASSERT_EQ (0, node.stats.count (nano::stat::type::ledger, nano::stat::detail::test, nano::stat::dir::in)); + ASSERT_EQ (0, node.stats.count (nano::stat::type::ledger, nano::stat::detail::send, nano::stat::dir::out)); + node.stats.add (nano::stat::type::ledger, nano::stat::detail::test, nano::stat::dir::in, 1); node.stats.add (nano::stat::type::ledger, nano::stat::detail::test, nano::stat::dir::in, 5); node.stats.inc (nano::stat::type::ledger, nano::stat::detail::test, nano::stat::dir::in); diff --git a/nano/core_test/toml.cpp b/nano/core_test/toml.cpp index 986869ae3..6192abcae 100644 --- a/nano/core_test/toml.cpp +++ b/nano/core_test/toml.cpp @@ -308,7 +308,7 @@ TEST (toml_config, daemon_config_deserialize_defaults) ASSERT_EQ (conf.node.signature_checker_threads, defaults.node.signature_checker_threads); ASSERT_EQ (conf.node.unchecked_cutoff_time, defaults.node.unchecked_cutoff_time); ASSERT_EQ (conf.node.use_memory_pools, defaults.node.use_memory_pools); - ASSERT_EQ (conf.node.vote_generator_delay, defaults.node.vote_generator_delay); + ASSERT_EQ (conf.node.vote_generator.delay, defaults.node.vote_generator.delay); ASSERT_EQ (conf.node.vote_minimum, defaults.node.vote_minimum); ASSERT_EQ (conf.node.work_peers, defaults.node.work_peers); ASSERT_EQ (conf.node.work_threads, defaults.node.work_threads); @@ -473,7 +473,6 @@ TEST (toml_config, daemon_config_deserialize_no_defaults) signature_checker_threads = 999 unchecked_cutoff_time = 999 use_memory_pools = false - vote_generator_delay = 999 vote_minimum = "999" work_peers = ["dev.org:999"] work_threads = 999 @@ -670,6 +669,9 @@ TEST (toml_config, daemon_config_deserialize_no_defaults) duplicate_filter_cutoff = 999 minimum_fanout = 99 + [node.vote_generator] + delay = 999 + [opencl] device = 999 enable = true @@ -740,7 +742,7 @@ TEST (toml_config, daemon_config_deserialize_no_defaults) ASSERT_NE (conf.node.signature_checker_threads, defaults.node.signature_checker_threads); ASSERT_NE (conf.node.unchecked_cutoff_time, defaults.node.unchecked_cutoff_time); ASSERT_NE (conf.node.use_memory_pools, defaults.node.use_memory_pools); - ASSERT_NE (conf.node.vote_generator_delay, defaults.node.vote_generator_delay); + ASSERT_NE (conf.node.vote_generator.delay, defaults.node.vote_generator.delay); ASSERT_NE (conf.node.vote_minimum, defaults.node.vote_minimum); ASSERT_NE (conf.node.work_peers, defaults.node.work_peers); ASSERT_NE (conf.node.work_threads, defaults.node.work_threads); diff --git a/nano/core_test/unchecked_map.cpp b/nano/core_test/unchecked_map.cpp index ea19b8583..51af28c8f 100644 --- a/nano/core_test/unchecked_map.cpp +++ b/nano/core_test/unchecked_map.cpp @@ -129,11 +129,6 @@ TEST (unchecked, simple) TEST (unchecked, multiple) { nano::test::system system{}; - if (nano::rocksdb_config::using_rocksdb_in_tests ()) - { - // Don't test this in rocksdb mode - GTEST_SKIP (); - } nano::unchecked_map unchecked{ max_unchecked_blocks, system.stats, false }; nano::block_builder builder; auto block = builder diff --git a/nano/core_test/vote_cache.cpp b/nano/core_test/vote_cache.cpp index 916179b6e..348a5b809 100644 --- a/nano/core_test/vote_cache.cpp +++ b/nano/core_test/vote_cache.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include diff --git a/nano/lib/config.cpp b/nano/lib/config.cpp index 83915e0be..c48ca1b25 100644 --- a/nano/lib/config.cpp +++ b/nano/lib/config.cpp @@ -63,27 +63,27 @@ bool slow_instrumentation () std::string get_node_toml_config_path (std::filesystem::path const & data_path) { - return (data_path / "config-node.toml").string (); + return (data_path / node_config_filename).string (); } std::string get_rpc_toml_config_path (std::filesystem::path const & data_path) { - return (data_path / "config-rpc.toml").string (); + return (data_path / rpc_config_filename).string (); } std::string get_qtwallet_toml_config_path (std::filesystem::path const & data_path) { - return (data_path / "config-qtwallet.toml").string (); + return (data_path / qtwallet_config_filename).string (); } std::string get_access_toml_config_path (std::filesystem::path const & data_path) { - return (data_path / "config-access.toml").string (); + return (data_path / access_config_filename).string (); } std::string get_tls_toml_config_path (std::filesystem::path const & data_path) { - return (data_path / "config-tls.toml").string (); + return (data_path / tls_config_filename).string (); } } diff --git a/nano/lib/config.hpp b/nano/lib/config.hpp index 9e1f5f597..3d31e442d 100644 --- a/nano/lib/config.hpp +++ b/nano/lib/config.hpp @@ -5,12 +5,12 @@ #include #include -#include #include #include #include #include #include +#include using namespace std::chrono_literals; @@ -86,6 +86,14 @@ uint16_t test_websocket_port (); std::array test_magic_number (); uint32_t test_scan_wallet_reps_delay (); // How often to scan for representatives in local wallet, in milliseconds +// Configuration file names +constexpr std::string_view node_config_filename{ "config-node.toml" }; +constexpr std::string_view rpc_config_filename{ "config-rpc.toml" }; +constexpr std::string_view log_config_filename{ "config-log.toml" }; +constexpr std::string_view access_config_filename{ "config-access.toml" }; +constexpr std::string_view qtwallet_config_filename{ "config-qtwallet.toml" }; +constexpr std::string_view tls_config_filename{ "config-tls.toml" }; + std::string get_node_toml_config_path (std::filesystem::path const & data_path); std::string get_rpc_toml_config_path (std::filesystem::path const & data_path); std::string get_access_toml_config_path (std::filesystem::path const & data_path); @@ -139,4 +147,10 @@ T load_config_file (T fallback, const std::filesystem::path & config_filename, c } return config; } + +template +T load_config_file (const std::filesystem::path & config_filename, const std::filesystem::path & data_path, const std::vector & config_overrides) +{ + return load_config_file (T{}, config_filename, data_path, config_overrides); +} } diff --git a/nano/lib/constants.cpp b/nano/lib/constants.cpp index f32bbafd7..bb359138d 100644 --- a/nano/lib/constants.cpp +++ b/nano/lib/constants.cpp @@ -35,6 +35,10 @@ struct HexTo }; } +/* + * Work thresholds + */ + nano::work_thresholds const nano::work_thresholds::publish_full ( 0xffffffc000000000, 0xfffffff800000000, // 8x higher than epoch_1 @@ -59,6 +63,10 @@ nano::env::get> ("NANO_TEST_EPOCH_2").value_or (0xfffffff8000000 nano::env::get> ("NANO_TEST_EPOCH_2_RECV").value_or (0xfffffe0000000000) // 8x lower than epoch_1 ); +/* + * + */ + uint64_t nano::work_thresholds::threshold_entry (nano::work_version const version_a, nano::block_type const type_a) const { uint64_t result{ std::numeric_limits::max () }; @@ -168,7 +176,6 @@ double nano::work_thresholds::denormalized_multiplier (double const multiplier_a if (threshold_a == epoch_1 || threshold_a == epoch_2_receive) { auto ratio (nano::difficulty::to_multiplier (epoch_2, threshold_a)); - debug_assert (ratio >= 1); multiplier = multiplier * ratio + 1.0 - ratio; debug_assert (multiplier >= 1); } diff --git a/nano/lib/constants.hpp b/nano/lib/constants.hpp index cea24ba65..eab5ad52e 100644 --- a/nano/lib/constants.hpp +++ b/nano/lib/constants.hpp @@ -29,16 +29,17 @@ std::string_view to_string (nano::networks); class work_thresholds { public: - uint64_t const epoch_1; - uint64_t const epoch_2; - uint64_t const epoch_2_receive; + uint64_t epoch_1; + uint64_t epoch_2; + uint64_t epoch_2_receive; // Automatically calculated. The base threshold is the maximum of all thresholds and is used for all work multiplier calculations - uint64_t const base; + uint64_t base; // Automatically calculated. The entry threshold is the minimum of all thresholds and defines the required work to enter the node, but does not guarantee a block is processed - uint64_t const entry; + uint64_t entry; +public: constexpr work_thresholds (uint64_t epoch_1_a, uint64_t epoch_2_a, uint64_t epoch_2_receive_a) : epoch_1 (epoch_1_a), epoch_2 (epoch_2_a), epoch_2_receive (epoch_2_receive_a), base (std::max ({ epoch_1, epoch_2, epoch_2_receive })), @@ -46,25 +47,21 @@ public: { } work_thresholds () = delete; - work_thresholds operator= (nano::work_thresholds const & other_a) - { - return other_a; - } - uint64_t threshold_entry (nano::work_version const, nano::block_type const) const; + uint64_t threshold_entry (nano::work_version, nano::block_type) const; uint64_t threshold (nano::block_details const &) const; // Ledger threshold - uint64_t threshold (nano::work_version const, nano::block_details const) const; - uint64_t threshold_base (nano::work_version const) const; - uint64_t value (nano::root const & root_a, uint64_t work_a) const; - double normalized_multiplier (double const, uint64_t const) const; - double denormalized_multiplier (double const, uint64_t const) const; - uint64_t difficulty (nano::work_version const, nano::root const &, uint64_t const) const; - uint64_t difficulty (nano::block const & block_a) const; - bool validate_entry (nano::work_version const, nano::root const &, uint64_t const) const; - bool validate_entry (nano::block const &) const; + uint64_t threshold (nano::work_version, nano::block_details) const; + uint64_t threshold_base (nano::work_version) const; + uint64_t value (nano::root const & root, uint64_t work) const; + double normalized_multiplier (double multiplier, uint64_t threshold) const; + double denormalized_multiplier (double multiplier, uint64_t threshold) const; + uint64_t difficulty (nano::work_version, nano::root const & root, uint64_t work) const; + uint64_t difficulty (nano::block const & block) const; + bool validate_entry (nano::work_version, nano::root const & root, uint64_t work) const; + bool validate_entry (nano::block const & block) const; - /** Network work thresholds. Define these inline as constexpr when moving to cpp17. */ +public: // Network work thresholds static nano::work_thresholds const publish_full; static nano::work_thresholds const publish_beta; static nano::work_thresholds const publish_dev; @@ -73,22 +70,20 @@ public: class network_constants { - static constexpr std::chrono::seconds default_cleanup_period = std::chrono::seconds (60); - public: - network_constants (nano::work_thresholds const & work_, nano::networks network_a) : + network_constants (nano::work_thresholds const & work_a, nano::networks network_a) : current_network (network_a), - work (work_), + work (work_a), principal_weight_factor (1000), // 0.1% A representative is classified as principal based on its weight and this factor default_node_port (44000), default_rpc_port (45000), default_ipc_port (46000), default_websocket_port (47000), aec_loop_interval (300ms), // Update AEC ~3 times per second - cleanup_period (default_cleanup_period), + cleanup_period (60s), merge_period (std::chrono::milliseconds (250)), keepalive_period (std::chrono::seconds (15)), - idle_timeout (default_cleanup_period * 2), + idle_timeout (120s), silent_connection_tolerance_time (std::chrono::seconds (120)), syn_cookie_cutoff (std::chrono::seconds (5)), bootstrap_interval (std::chrono::seconds (15 * 60)), @@ -132,7 +127,6 @@ public: telemetry_cache_cutoff = 2000ms; telemetry_request_interval = 500ms; telemetry_broadcast_interval = 500ms; - optimistic_activation_delay = 2s; rep_crawler_normal_interval = 500ms; rep_crawler_warmup_interval = 500ms; } @@ -183,9 +177,6 @@ public: /** Telemetry data older than this value is considered stale */ std::chrono::milliseconds telemetry_cache_cutoff{ 1000 * 130 }; // 2 * `telemetry_broadcast_interval` + some margin - /** How much to delay activation of optimistic elections to avoid interfering with election scheduler */ - std::chrono::seconds optimistic_activation_delay{ 30 }; - std::chrono::milliseconds rep_crawler_normal_interval{ 1000 * 7 }; std::chrono::milliseconds rep_crawler_warmup_interval{ 1000 * 3 }; diff --git a/nano/lib/logging.cpp b/nano/lib/logging.cpp index 8e0d305d8..c548a85ab 100644 --- a/nano/lib/logging.cpp +++ b/nano/lib/logging.cpp @@ -312,10 +312,10 @@ spdlog::level::level_enum nano::logger::to_spdlog_level (nano::log::level level) * logging config presets */ -nano::log_config nano::log_config::cli_default () +nano::log_config nano::log_config::cli_default (nano::log::level default_level) { log_config config{}; - config.default_level = nano::log::level::critical; + config.default_level = default_level; config.console.colors = false; config.console.to_cerr = true; // Use cerr to avoid interference with CLI output that goes to stdout config.file.enable = false; @@ -474,10 +474,9 @@ std::map nano::log_config::default_level // Using std::cerr here, since logging may not be initialized yet nano::log_config nano::load_log_config (nano::log_config fallback, const std::filesystem::path & data_path, const std::vector & config_overrides) { - const std::string config_filename = "config-log.toml"; try { - auto config = nano::load_config_file (fallback, config_filename, data_path, config_overrides); + auto config = nano::load_config_file (fallback, log_config_filename, data_path, config_overrides); // Parse default log level from environment variable, e.g. "NANO_LOG=debug" auto env_level = nano::env::get ("NANO_LOG"); diff --git a/nano/lib/logging.hpp b/nano/lib/logging.hpp index a7296f88e..688edf891 100644 --- a/nano/lib/logging.hpp +++ b/nano/lib/logging.hpp @@ -134,7 +134,7 @@ public: nano::log::tracing_format tracing_format{ nano::log::tracing_format::standard }; public: // Predefined defaults - static log_config cli_default (); + static log_config cli_default (nano::log::level default_level = nano::log::level::critical); static log_config daemon_default (); static log_config tests_default (); static log_config dummy_default (); // For empty logger diff --git a/nano/lib/numbers.cpp b/nano/lib/numbers.cpp index af667f7c3..8908bb37c 100644 --- a/nano/lib/numbers.cpp +++ b/nano/lib/numbers.cpp @@ -809,7 +809,10 @@ std::ostream & nano::operator<< (std::ostream & os, const nano::account & val) uint64_t nano::difficulty::from_multiplier (double const multiplier_a, uint64_t const base_difficulty_a) { - debug_assert (multiplier_a > 0.); + if (multiplier_a <= 0.) + { + return 0; + } nano::uint128_t reverse_difficulty ((-base_difficulty_a) / multiplier_a); if (reverse_difficulty > std::numeric_limits::max ()) { @@ -827,7 +830,10 @@ uint64_t nano::difficulty::from_multiplier (double const multiplier_a, uint64_t double nano::difficulty::to_multiplier (uint64_t const difficulty_a, uint64_t const base_difficulty_a) { - debug_assert (difficulty_a > 0); + if (difficulty_a == 0) + { + return 0; + } return static_cast (-base_difficulty_a) / (-difficulty_a); } diff --git a/nano/lib/numbers.hpp b/nano/lib/numbers.hpp index 94c7fba55..f8e08fce0 100644 --- a/nano/lib/numbers.hpp +++ b/nano/lib/numbers.hpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include diff --git a/nano/lib/stats.cpp b/nano/lib/stats.cpp index fa3bd1314..ab9f8ec7a 100644 --- a/nano/lib/stats.cpp +++ b/nano/lib/stats.cpp @@ -15,8 +15,19 @@ #include #include +#include + using namespace std::chrono_literals; +// Static assertions to ensure our predefined array sizes are sufficient for the enums +// We check the _last value's integer value, which should be the highest valid index we need +static_assert (magic_enum::enum_integer (nano::stat::type::_last) <= nano::stats::types_count, +"stat::type enum has grown beyond the predefined array size. Increase types_count in stats.hpp"); +static_assert (magic_enum::enum_integer (nano::stat::detail::_last) <= nano::stats::details_count, +"stat::detail enum has grown beyond the predefined array size. Increase details_count in stats.hpp"); +static_assert (magic_enum::enum_integer (nano::stat::dir::_last) <= nano::stats::dirs_count, +"stat::dir enum has grown beyond the predefined array size. Increase dirs_count in stats.hpp"); + /* * stat_log_sink */ @@ -31,6 +42,8 @@ std::string nano::stat_log_sink::tm_to_string (tm & tm) */ nano::stats::stats (nano::logger & logger_a, nano::stats_config config_a) : + counters_impl{ std::make_unique () }, + counters{ *counters_impl }, config{ std::move (config_a) }, logger{ logger_a }, enable_logging{ is_stat_logging_enabled () } @@ -71,21 +84,50 @@ void nano::stats::stop () void nano::stats::clear () { - std::lock_guard guard{ mutex }; - counters.clear (); - samplers.clear (); - timestamp = std::chrono::steady_clock::now (); + // Clear all counters + for (auto & counter : counters) + { + counter.store (0, std::memory_order_relaxed); + } + + // Clear samplers (still needs mutex) + { + std::lock_guard guard{ mutex }; + samplers.clear (); + timestamp = std::chrono::steady_clock::now (); + } +} + +size_t nano::stats::idx (stat::type type, stat::detail detail, stat::dir dir) +{ + // Dir is the slowest changing dimension, so it goes first for better cache locality + auto type_idx = magic_enum::enum_integer (type); + auto detail_idx = magic_enum::enum_integer (detail); + auto dir_idx = magic_enum::enum_integer (dir); + return (dir_idx * types_count * details_count) + (type_idx * details_count) + detail_idx; +} + +std::atomic & nano::stats::counter_ref (stat::type type, stat::detail detail, stat::dir dir) +{ + auto index = idx (type, detail, dir); + debug_assert (index < counters.size ()); + return counters[index]; +} + +std::atomic const & nano::stats::counter_ref (stat::type type, stat::detail detail, stat::dir dir) const +{ + auto index = idx (type, detail, dir); + debug_assert (index < counters.size ()); + return counters[index]; } void nano::stats::add (stat::type type, stat::detail detail, stat::dir dir, counter_value_t value, bool aggregate_all) { debug_assert (type != stat::type::_invalid); + debug_assert (type != stat::type::_last); debug_assert (detail != stat::detail::_invalid); - - if (value == 0) - { - return; - } + debug_assert (detail != stat::detail::_last); + debug_assert (dir != stat::dir::_last); if (enable_logging) { @@ -96,71 +138,30 @@ void nano::stats::add (stat::type type, stat::detail detail, stat::dir dir, coun value); } - // Updates need to happen while holding the mutex - auto update_counter = [this, aggregate_all] (nano::stats::counter_key key, auto && updater) { - counter_key all_key{ key.type, stat::detail::all, key.dir }; + counter_ref (type, detail, dir).fetch_add (value, std::memory_order_relaxed); - // This is a two-step process to avoid exclusively locking the mutex in the common case - { - std::shared_lock lock{ mutex }; - - if (auto it = counters.find (key); it != counters.end ()) - { - updater (*it->second); - - if (aggregate_all && key != all_key) - { - auto it_all = counters.find (all_key); - release_assert (it_all != counters.end ()); // The `all` counter should always be created together - updater (*it_all->second); // Also update the `all` counter - } - - return; - } - } - // Not found, create a new entry - { - std::unique_lock lock{ mutex }; - - // Insertions will be ignored if the key already exists - auto [it, inserted] = counters.emplace (key, std::make_unique ()); - updater (*it->second); - - if (aggregate_all && key != all_key) - { - auto [it_all, inserted_all] = counters.emplace (all_key, std::make_unique ()); - updater (*it_all->second); // Also update the `all` counter - } - } - }; - - update_counter (counter_key{ type, detail, dir }, [value] (counter_entry & counter) { - counter.value += value; - }); + if (aggregate_all && detail != stat::detail::all) + { + counter_ref (type, stat::detail::all, dir).fetch_add (value, std::memory_order_relaxed); + } } nano::stats::counter_value_t nano::stats::count (stat::type type, stat::detail detail, stat::dir dir) const { - std::shared_lock lock{ mutex }; - if (auto it = counters.find (counter_key{ type, detail, dir }); it != counters.end ()) - { - return it->second->value; - } - return 0; + return counter_ref (type, detail, dir).load (std::memory_order_relaxed); } nano::stats::counter_value_t nano::stats::count (stat::type type, stat::dir dir) const { - std::shared_lock lock{ mutex }; counter_value_t result = 0; - auto it = counters.lower_bound (counter_key{ type, stat::detail::all, dir }); - while (it != counters.end () && it->first.type == type) + + // Sum all detail counters for this type and direction (except the 'all' detail) + for (auto detail : magic_enum::enum_values ()) { - if (it->first.dir == dir && it->first.detail != stat::detail::all) + if (detail != stat::detail::all && detail != stat::detail::_invalid && detail != stat::detail::_last) { - result += it->second->value; + result += counter_ref (type, detail, dir).load (std::memory_order_relaxed); } - ++it; } return result; } @@ -225,6 +226,7 @@ void nano::stats::log_counters (stat_log_sink & sink) void nano::stats::log_counters_impl (stat_log_sink & sink, tm & tm) { sink.begin (); + if (sink.entries () >= config.log_rotation_count) { sink.rotate (); @@ -236,14 +238,35 @@ void nano::stats::log_counters_impl (stat_log_sink & sink, tm & tm) sink.write_header ("counters", walltime); } - for (auto const & [key, entry] : counters) + for (auto dir : magic_enum::enum_values ()) { - std::string type{ to_string (key.type) }; - std::string detail{ to_string (key.detail) }; - std::string dir{ to_string (key.dir) }; + if (dir == stat::dir::_last) + continue; - sink.write_counter_entry (tm, type, detail, dir, entry->value); + for (auto type : magic_enum::enum_values ()) + { + if (type == stat::type::_invalid || type == stat::type::_last) + continue; + + for (auto detail : magic_enum::enum_values ()) + { + if (detail == stat::detail::_invalid || detail == stat::detail::_last) + continue; + + auto value = counter_ref (type, detail, dir).load (std::memory_order_relaxed); + + if (value > 0) // Only log non-zero counters + { + std::string type_str{ to_string (type) }; + std::string detail_str{ to_string (detail) }; + std::string dir_str{ to_string (dir) }; + + sink.write_counter_entry (tm, type_str, detail_str, dir_str, value); + } + } + } } + sink.entries ()++; sink.finalize (); } @@ -301,7 +324,9 @@ void nano::stats::run () std::unique_lock lock{ mutex }; while (!stopped) { - condition.wait_for (lock, 1s); + condition.wait_for (lock, 1s, [this] { + return stopped; + }); if (!stopped) { run_one (lock); @@ -446,4 +471,4 @@ nano::error nano::stats_config::deserialize_toml (nano::tomlconfig & toml) } return toml.get_error (); -} \ No newline at end of file +} diff --git a/nano/lib/stats.hpp b/nano/lib/stats.hpp index 8f20ada72..2d976d520 100644 --- a/nano/lib/stats.hpp +++ b/nano/lib/stats.hpp @@ -7,6 +7,8 @@ #include +#include +#include #include #include #include @@ -71,6 +73,11 @@ public: using counter_value_t = uint64_t; using sampler_value_t = int64_t; +public: // Array dimensions - must match the enum sizes in stats_enums.hpp + static constexpr size_t types_count = 256; // Enough for stat::type enum range + static constexpr size_t details_count = 1024; // Enough for stat::detail enum range + static constexpr size_t dirs_count = 2; // Enough for stat::dir enum range + public: explicit stats (nano::logger &, nano::stats_config = {}); ~stats (); @@ -132,15 +139,6 @@ public: std::string dump (category category = category::counters); private: - struct counter_key - { - stat::type type; - stat::detail detail; - stat::dir dir; - - auto operator<=> (const counter_key &) const = default; - }; - struct sampler_key { stat::sample sample; @@ -149,18 +147,6 @@ private: }; private: - class counter_entry - { - public: - // Prevent copying - counter_entry () = default; - counter_entry (counter_entry const &) = delete; - counter_entry & operator= (counter_entry const &) = delete; - - public: - std::atomic value{ 0 }; - }; - class sampler_entry { public: @@ -183,9 +169,13 @@ private: mutable nano::mutex mutex; }; - // Wrap in unique_ptrs because mutex/atomic members are not movable - // TODO: Compare performance of map vs unordered_map - std::map> counters; + // Flat array for direct-indexed counter access + // Allocate on the heap to avoid stack overflows when intantiating the stats class in tests + using counters_array_t = std::array, types_count * details_count * dirs_count>; + std::unique_ptr counters_impl; + counters_array_t & counters; + + // Keep samplers as map since they have different behavior and lower frequency std::map> samplers; private: @@ -201,6 +191,12 @@ private: static bool is_stat_logging_enabled (); + std::atomic & counter_ref (stat::type type, stat::detail detail, stat::dir dir); + std::atomic const & counter_ref (stat::type type, stat::detail detail, stat::dir dir) const; + + // Helper function to calculate flat array index + static size_t idx (stat::type type, stat::detail detail, stat::dir dir); + private: nano::stats_config const config; nano::logger & logger; diff --git a/nano/lib/stats_enums.hpp b/nano/lib/stats_enums.hpp index c37b421ff..1542205c3 100644 --- a/nano/lib/stats_enums.hpp +++ b/nano/lib/stats_enums.hpp @@ -144,6 +144,7 @@ enum class detail total, loop, loop_cleanup, + loop_checkup, process, processed, ignored, @@ -515,7 +516,10 @@ enum class detail started, stopped, confirm_dependent, + cancel_dependent, + cancel_checkup, forks_cached, + stale, bootstrap_stale, // unchecked diff --git a/nano/lib/thread_roles.cpp b/nano/lib/thread_roles.cpp index 0f4d0f976..1679f5f7f 100644 --- a/nano/lib/thread_roles.cpp +++ b/nano/lib/thread_roles.cpp @@ -49,6 +49,9 @@ std::string nano::thread_role::get_string (nano::thread_role::name role) case nano::thread_role::name::aec_loop: thread_role_name_string = "AEC"; break; + case nano::thread_role::name::aec_checkup: + thread_role_name_string = "AEC checkup"; + break; case nano::thread_role::name::aec_notifications: thread_role_name_string = "AEC notif"; break; diff --git a/nano/lib/thread_roles.hpp b/nano/lib/thread_roles.hpp index 37e4d50a3..041f842c2 100644 --- a/nano/lib/thread_roles.hpp +++ b/nano/lib/thread_roles.hpp @@ -21,6 +21,7 @@ enum class name block_processing, ledger_notifications, aec_loop, + aec_checkup, aec_notifications, wallet_actions, bootstrap_initiator, diff --git a/nano/lib/timer.hpp b/nano/lib/timer.hpp index f9b6580d9..8b458397e 100644 --- a/nano/lib/timer.hpp +++ b/nano/lib/timer.hpp @@ -100,29 +100,29 @@ private: using millis_t = uint64_t; -inline millis_t milliseconds_since_epoch () +inline millis_t milliseconds_since_epoch (std::chrono::system_clock::time_point tp = std::chrono::system_clock::now ()) { - return std::chrono::duration_cast (std::chrono::system_clock::now ().time_since_epoch ()).count (); + return std::chrono::duration_cast (tp.time_since_epoch ()).count (); } -inline std::chrono::time_point from_milliseconds_since_epoch (nano::millis_t millis) +inline std::chrono::system_clock::time_point from_milliseconds_since_epoch (nano::millis_t millis) { - return std::chrono::time_point (std::chrono::milliseconds{ millis }); + return std::chrono::system_clock::time_point (std::chrono::milliseconds{ millis }); } using seconds_t = uint64_t; -inline seconds_t seconds_since_epoch () +inline seconds_t seconds_since_epoch (std::chrono::system_clock::time_point tp = std::chrono::system_clock::now ()) { - return std::chrono::duration_cast (std::chrono::system_clock::now ().time_since_epoch ()).count (); + return std::chrono::duration_cast (tp.time_since_epoch ()).count (); } -inline std::chrono::time_point from_seconds_since_epoch (nano::seconds_t seconds) +inline std::chrono::system_clock::time_point from_seconds_since_epoch (nano::seconds_t seconds) { - return std::chrono::time_point (std::chrono::seconds{ seconds }); + return std::chrono::system_clock::time_point (std::chrono::seconds{ seconds }); } -inline nano::millis_t time_difference (nano::millis_t start, nano::millis_t end) +inline millis_t time_difference (millis_t start, millis_t end) { return end > start ? (end - start) : 0; } diff --git a/nano/nano_node/CMakeLists.txt b/nano/nano_node/CMakeLists.txt index 48d42d358..47d582eba 100644 --- a/nano/nano_node/CMakeLists.txt +++ b/nano/nano_node/CMakeLists.txt @@ -1,4 +1,14 @@ -add_executable(nano_node daemon.cpp daemon.hpp entry.cpp) +add_executable( + nano_node + benchmarks/benchmarks.cpp + benchmarks/benchmarks.hpp + benchmarks/benchmark_block_processing.cpp + benchmarks/benchmark_cementing.cpp + benchmarks/benchmark_elections.cpp + benchmarks/benchmark_pipeline.cpp + daemon.cpp + daemon.hpp + entry.cpp) target_link_libraries(nano_node node Boost::process ${PLATFORM_LIBS}) diff --git a/nano/nano_node/benchmarks/benchmark_block_processing.cpp b/nano/nano_node/benchmarks/benchmark_block_processing.cpp new file mode 100644 index 000000000..da5cb5aba --- /dev/null +++ b/nano/nano_node/benchmarks/benchmark_block_processing.cpp @@ -0,0 +1,253 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace nano::cli +{ +/* + * Block Processing Benchmark + * + * Measures the performance of the block processor - the component responsible for validating + * and inserting blocks into the ledger. This benchmark tests raw block processing throughput + * without elections or confirmation. + * + * How it works: + * 1. Setup: Creates a node with unlimited queue sizes and disabled work requirements + * 2. Generate: Creates random transfer transactions (send/receive pairs) between accounts + * 3. Submit: Adds all blocks to the block processor queue via block_processor.add() + * 4. Measure: Tracks time from submission until all blocks are processed into the ledger + * 5. Report: Calculates blocks/sec throughput and final account states + * + * What is tested: + * - Block validation speed (signature verification, balance checks, etc.) + * - Ledger write performance (database insertion) + * - Block processor queue management + * - Unchecked block handling for out-of-order blocks + * + * What is NOT tested: + * - Elections or voting (blocks are not confirmed) + * - Cementing (blocks remain unconfirmed) + * - Network communication (local-only testing) + */ +class block_processing_benchmark : public benchmark_base +{ +private: + // Blocks currently being processed + nano::locked> current_blocks; + + // Metrics + std::atomic processed_blocks_count{ 0 }; + std::atomic failed_blocks_count{ 0 }; + std::atomic old_blocks_count{ 0 }; + std::atomic gap_previous_count{ 0 }; + std::atomic gap_source_count{ 0 }; + +public: + block_processing_benchmark (std::shared_ptr node_a, benchmark_config const & config_a); + + void run (); + void run_iteration (std::deque> & blocks); + void print_statistics (); +}; + +void run_block_processing_benchmark (boost::program_options::variables_map const & vm, std::filesystem::path const & data_path) +{ + auto config = benchmark_config::parse (vm); + + std::cout << "=== BENCHMARK: Block Processing ===\n"; + std::cout << "Configuration:\n"; + std::cout << fmt::format (" Accounts: {}\n", config.num_accounts); + std::cout << fmt::format (" Iterations: {}\n", config.num_iterations); + std::cout << fmt::format (" Batch size: {}\n", config.batch_size); + + // Setup node directly in run method + nano::network_constants::set_active_network ("dev"); + nano::logger::initialize (nano::log_config::cli_default (nano::log::level::warn)); + + nano::node_flags node_flags; + nano::update_flags (node_flags, vm); + + auto io_ctx = std::make_shared (); + nano::work_pool work_pool{ nano::dev::network_params.network, std::numeric_limits::max () }; + + // Load configuration from current working directory (if exists) and cli config overrides + auto daemon_config = nano::load_config_file (nano::node_config_filename, {}, node_flags.config_overrides); + auto node_config = daemon_config.node; + node_config.network_params.work = nano::work_thresholds{ 0, 0, 0 }; + node_config.peering_port = 0; // Use random available port + node_config.max_backlog = 0; // Disable bounded backlog + node_config.block_processor.max_system_queue = std::numeric_limits::max (); // Unlimited queue size + node_config.max_unchecked_blocks = 1024 * 1024; // Large unchecked blocks cache to avoid dropping blocks + + auto node = std::make_shared (io_ctx, nano::unique_path (), node_config, work_pool, node_flags); + node->start (); + nano::thread_runner runner (io_ctx, nano::default_logger (), node->config.io_threads); + + std::cout << "\nSystem Info:\n"; + std::cout << fmt::format (" Backend: {}\n", node->store.vendor_get ()); + std::cout << fmt::format (" Block processor threads: {}\n", 1); // TODO: Log number of block processor threads when upstreamed + std::cout << fmt::format (" Block processor batch size: {}\n", node->config.block_processor.batch_size); + std::cout << "\n"; + + // Wait for node to be ready + std::this_thread::sleep_for (500ms); + + // Run benchmark + block_processing_benchmark benchmark{ node, config }; + benchmark.run (); + + node->stop (); +} + +block_processing_benchmark::block_processing_benchmark (std::shared_ptr node_a, benchmark_config const & config_a) : + benchmark_base (node_a, config_a) +{ + // Register notification handler to track block processing results + node->ledger_notifications.blocks_processed.add ([this] (std::deque> const & batch) { + auto current_l = current_blocks.lock (); + for (auto const & [status, context] : batch) + { + if (status == nano::block_status::progress) + { + current_l->erase (context.block->hash ()); + processed_blocks_count++; + } + else + { + switch (status) + { + case nano::block_status::old: + // Block already exists in ledger + old_blocks_count++; + break; + case nano::block_status::gap_previous: + // Missing previous block, should be handled by unchecked map + gap_previous_count++; + break; + case nano::block_status::gap_source: + // Missing source block, should be handled by unchecked map + gap_source_count++; + break; + default: + std::cout << fmt::format ("Block processing failed: {} for block {}\n", to_string (status), context.block->hash ().to_string ()); + failed_blocks_count++; + break; + } + } + } + }); +} + +void block_processing_benchmark::run () +{ + // Create account pool and distribute genesis funds to a random account + std::cout << fmt::format ("Generating {} accounts...\n", config.num_accounts); + pool.generate_accounts (config.num_accounts); + + setup_genesis_distribution (); + + // Run multiple iterations to measure consistent performance + for (size_t iteration = 0; iteration < config.num_iterations; ++iteration) + { + std::cout << fmt::format ("\n--- Iteration {}/{} --------------------------------------------------------------\n", iteration + 1, config.num_iterations); + std::cout << fmt::format ("Generating {} random transfers...\n", config.batch_size / 2); + auto blocks = generate_random_transfers (); + + std::cout << fmt::format ("Processing {} blocks...\n", blocks.size ()); + run_iteration (blocks); + } + + print_statistics (); +} + +void block_processing_benchmark::run_iteration (std::deque> & blocks) +{ + auto const total_blocks = blocks.size (); + + // Add all blocks to tracking set + { + auto current_l = current_blocks.lock (); + for (auto const & block : blocks) + { + current_l->insert (block->hash ()); + } + } + + auto const time_begin = std::chrono::high_resolution_clock::now (); + + // Process all blocks + while (!blocks.empty ()) + { + auto block = blocks.front (); + blocks.pop_front (); + + bool added = node->block_processor.add (block, nano::block_source::test); + release_assert (added, "failed to add block to processor"); + } + + // Wait for processing to complete + nano::interval progress_interval; + while (true) + { + { + auto current_l = current_blocks.lock (); + if (current_l->empty () || progress_interval.elapse (3s)) + { + std::cout << fmt::format ("Blocks remaining: {:>9} (block processor: {:>9} | unchecked: {:>5})\n", + current_l->size (), + node->block_processor.size (), + node->unchecked.count ()); + } + if (current_l->empty ()) + { + break; + } + } + + std::this_thread::sleep_for (1ms); + } + + auto const time_end = std::chrono::high_resolution_clock::now (); + auto const time_us = std::chrono::duration_cast (time_end - time_begin).count (); + + std::cout << fmt::format ("\nPerformance: {} blocks/sec [{:.2f}s] {} blocks processed\n", + total_blocks * 1000000 / time_us, time_us / 1000000.0, total_blocks); + std::cout << "─────────────────────────────────────────────────────────────────\n"; + + node->stats.clear (); +} + +void block_processing_benchmark::print_statistics () +{ + std::cout << "\n--- SUMMARY ---------------------------------------------------------------------\n\n"; + std::cout << fmt::format ("Blocks processed: {:>10}\n", processed_blocks_count.load ()); + std::cout << fmt::format ("Blocks failed: {:>10}\n", failed_blocks_count.load ()); + std::cout << fmt::format ("Blocks old: {:>10}\n", old_blocks_count.load ()); + std::cout << fmt::format ("Blocks gap_previous: {:>10}\n", gap_previous_count.load ()); + std::cout << fmt::format ("Blocks gap_source: {:>10}\n", gap_source_count.load ()); + std::cout << fmt::format ("\n"); + std::cout << fmt::format ("Accounts total: {:>10}\n", pool.total_accounts ()); + std::cout << fmt::format ("Accounts with balance: {:>10} ({:.1f}%)\n", + pool.accounts_with_balance_count (), + 100.0 * pool.accounts_with_balance_count () / pool.total_accounts ()); +} +} \ No newline at end of file diff --git a/nano/nano_node/benchmarks/benchmark_cementing.cpp b/nano/nano_node/benchmarks/benchmark_cementing.cpp new file mode 100644 index 000000000..4e5a66ee5 --- /dev/null +++ b/nano/nano_node/benchmarks/benchmark_cementing.cpp @@ -0,0 +1,285 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace nano::cli +{ +/* + * Cementing Benchmark + * + * Measures the performance of the cementing subsystem - the component that marks blocks + * as confirmed/immutable in the ledger. + * + * How it works: + * 1. Setup: Creates a node and generates random transfer blocks + * 2. Process: Inserts blocks directly into ledger (bypassing block processor) + * 3. Submit: Adds blocks to cementing set for confirmation + * 4. Measure: Tracks time from submission until all blocks are cemented + * 5. Report: Calculates cementing throughput in blocks/sec + * + * Two modes: + * - Sequential mode: Each block is submitted to cementing set individually + * - Root mode: Only the final block is submitted, which triggers cascading cementing + * of all dependent blocks (tests dependency resolution performance) + * + * What is tested: + * - Cementing set processing speed + * - Database write performance for confirmation marks + * - Dependency resolution (root mode only) + * + * What is NOT tested: + * - Block processing (blocks inserted directly into ledger) + * - Elections or voting (blocks pre-confirmed) + * - Network communication + */ +class cementing_benchmark : public benchmark_base +{ +private: + // Track blocks waiting to be cemented + nano::locked> pending_cementing; + + // Metrics + std::atomic processed_blocks_count{ 0 }; + std::atomic cemented_blocks_count{ 0 }; + +public: + cementing_benchmark (std::shared_ptr node_a, benchmark_config const & config_a); + + void run (); + void run_iteration (std::deque> & blocks); + void print_statistics (); +}; + +void run_cementing_benchmark (boost::program_options::variables_map const & vm, std::filesystem::path const & data_path) +{ + auto config = benchmark_config::parse (vm); + + std::cout << "=== BENCHMARK: Cementing ===\n"; + std::cout << "Configuration:\n"; + std::cout << fmt::format (" Mode: {}\n", config.cementing_mode == cementing_mode::root ? "root" : "sequential"); + std::cout << fmt::format (" Accounts: {}\n", config.num_accounts); + std::cout << fmt::format (" Iterations: {}\n", config.num_iterations); + std::cout << fmt::format (" Batch size: {}\n", config.batch_size); + + // Setup node directly in run method + nano::network_constants::set_active_network ("dev"); + nano::logger::initialize (nano::log_config::cli_default (nano::log::level::warn)); + + nano::node_flags node_flags; + nano::update_flags (node_flags, vm); + + auto io_ctx = std::make_shared (); + nano::work_pool work_pool{ nano::dev::network_params.network, std::numeric_limits::max () }; + + // Load configuration from current working directory (if exists) and cli config overrides + auto daemon_config = nano::load_config_file (nano::node_config_filename, {}, node_flags.config_overrides); + auto node_config = daemon_config.node; + node_config.network_params.work = nano::work_thresholds{ 0, 0, 0 }; + node_config.peering_port = 0; // Use random available port + node_config.max_backlog = 0; // Disable bounded backlog + node_config.block_processor.max_system_queue = std::numeric_limits::max (); // Unlimited queue size + node_config.max_unchecked_blocks = 1024 * 1024; // Large unchecked blocks cache to avoid dropping blocks + + auto node = std::make_shared (io_ctx, nano::unique_path (), node_config, work_pool, node_flags); + node->start (); + nano::thread_runner runner (io_ctx, nano::default_logger (), node->config.io_threads); + + std::cout << "\nSystem Info:\n"; + std::cout << fmt::format (" Backend: {}\n", node->store.vendor_get ()); + std::cout << "\n"; + + // Wait for node to be ready + std::this_thread::sleep_for (500ms); + + // Run benchmark + cementing_benchmark benchmark{ node, config }; + benchmark.run (); + + node->stop (); +} + +cementing_benchmark::cementing_benchmark (std::shared_ptr node_a, benchmark_config const & config_a) : + benchmark_base (node_a, config_a) +{ + // Track when blocks get processed + node->ledger_notifications.blocks_processed.add ([this] (std::deque> const & batch) { + for (auto const & [status, context] : batch) + { + if (status == nano::block_status::progress) + { + processed_blocks_count++; + } + } + }); + + // Track when blocks get cemented + node->cementing_set.batch_cemented.add ([this] (auto const & hashes) { + auto pending_l = pending_cementing.lock (); + for (auto const & ctx : hashes) + { + pending_l->erase (ctx.block->hash ()); + cemented_blocks_count++; + } + }); +} + +void cementing_benchmark::run () +{ + std::cout << fmt::format ("Generating {} accounts...\n", config.num_accounts); + pool.generate_accounts (config.num_accounts); + + setup_genesis_distribution (); + + std::cout << fmt::format ("Cementing mode: {}\n", config.cementing_mode == cementing_mode::root ? "root" : "sequential"); + + for (size_t iteration = 0; iteration < config.num_iterations; ++iteration) + { + std::cout << fmt::format ("\n--- Iteration {}/{} --------------------------------------------------------------\n", iteration + 1, config.num_iterations); + + std::deque> blocks; + if (config.cementing_mode == cementing_mode::root) + { + std::cout << fmt::format ("Generating dependent chain topology...\n"); + blocks = generate_dependent_chain (); + } + else + { + std::cout << fmt::format ("Generating {} random transfers...\n", config.batch_size / 2); + blocks = generate_random_transfers (); + } + + std::cout << fmt::format ("Cementing {} blocks...\n", blocks.size ()); + run_iteration (blocks); + } + + print_statistics (); +} + +void cementing_benchmark::run_iteration (std::deque> & blocks) +{ + auto const total_blocks = blocks.size (); + + // Add all blocks to tracking set + { + auto now = std::chrono::steady_clock::now (); + auto pending_l = pending_cementing.lock (); + for (auto const & block : blocks) + { + pending_l->emplace (block->hash (), now); + } + } + + std::cout << fmt::format ("Processing {} blocks directly into the ledger...\n", blocks.size ()); + + // Process all blocks directly into the ledger + { + auto transaction = node->ledger.tx_begin_write (); + for (auto const & block : blocks) + { + auto result = node->ledger.process (transaction, block); + release_assert (result == nano::block_status::progress, to_string (result)); + } + } + + std::cout << "All blocks processed, starting cementing...\n"; + + auto const time_begin = std::chrono::high_resolution_clock::now (); + + // Mode-specific cementing + size_t blocks_submitted = 0; + if (config.cementing_mode == cementing_mode::root) + { + // In root mode, only submit the final block which depends on all others + if (!blocks.empty ()) + { + auto final_block = blocks.back (); + bool added = node->cementing_set.add (final_block->hash ()); + release_assert (added, "failed to add final block to cementing set"); + blocks_submitted = 1; + + std::cout << fmt::format ("Submitted 1 root block to cement {} dependent blocks\n", + total_blocks); + } + } + else + { + // Sequential mode - submit each block separately + while (!blocks.empty ()) + { + auto block = blocks.front (); + blocks.pop_front (); + + bool added = node->cementing_set.add (block->hash ()); + release_assert (added, "failed to add block to cementing set"); + blocks_submitted++; + } + + std::cout << fmt::format ("Submitted {} blocks to cementing set\n", + blocks_submitted); + } + + // Wait for cementing to complete + nano::interval progress_interval; + while (true) + { + { + auto pending_l = pending_cementing.lock (); + if (pending_l->empty () || progress_interval.elapse (3s)) + { + std::cout << fmt::format ("Blocks remaining: {:>9} (cementing set: {:>5} | deferred: {:>5})\n", + pending_l->size (), + node->cementing_set.size (), + node->cementing_set.deferred_size ()); + } + if (pending_l->empty ()) + { + break; + } + } + + std::this_thread::sleep_for (1ms); + } + + auto const time_end = std::chrono::high_resolution_clock::now (); + auto const time_us = std::chrono::duration_cast (time_end - time_begin).count (); + + std::cout << fmt::format ("\nPerformance: {} blocks/sec [{:.2f}s] {} blocks processed\n", + total_blocks * 1000000 / time_us, time_us / 1000000.0, total_blocks); + std::cout << "─────────────────────────────────────────────────────────────────\n"; + + node->stats.clear (); +} + +void cementing_benchmark::print_statistics () +{ + std::cout << "\n--- SUMMARY ---------------------------------------------------------------------\n\n"; + std::cout << fmt::format ("Mode: {:>10}\n", config.cementing_mode == cementing_mode::root ? "root" : "sequential"); + std::cout << fmt::format ("Blocks processed: {:>10}\n", processed_blocks_count.load ()); + std::cout << fmt::format ("Blocks cemented: {:>10}\n", cemented_blocks_count.load ()); + std::cout << fmt::format ("\n"); + std::cout << fmt::format ("Accounts total: {:>10}\n", pool.total_accounts ()); + std::cout << fmt::format ("Accounts with balance: {:>10} ({:.1f}%)\n", + pool.accounts_with_balance_count (), + 100.0 * pool.accounts_with_balance_count () / pool.total_accounts ()); +} +} \ No newline at end of file diff --git a/nano/nano_node/benchmarks/benchmark_elections.cpp b/nano/nano_node/benchmarks/benchmark_elections.cpp new file mode 100644 index 000000000..988590b94 --- /dev/null +++ b/nano/nano_node/benchmarks/benchmark_elections.cpp @@ -0,0 +1,344 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +namespace nano::cli +{ +/* + * Elections Benchmark + * + * Measures the performance of the election subsystem - the component that runs voting + * consensus to cement blocks. Tests how quickly the node can start elections, collect + * votes, reach quorum, and cement blocks. + * + * How it works: + * 1. Setup: Creates a node with genesis representative key for voting + * 2. Prepare: Generates independent open blocks (send blocks are pre-cemented) + * 3. Process: Inserts open blocks directly into ledger (bypassing block processor) + * 4. Start: Manually triggers elections for all open blocks + * 5. Measure: Tracks time from election start until blocks are confirmed and cemented + * 6. Report: Calculates election throughput and timing statistics + * + * What is tested: + * - Election startup performance + * - Vote generation and processing speed (with one local rep running on the same node) + * - Quorum detection and confirmation logic + * - Cementing after confirmation + * - Concurrent election handling + * + * What is NOT tested: + * - Block processing (blocks inserted directly) + * - Network vote propagation (local voting only) + * - Election schedulers (elections started manually) + */ +class elections_benchmark : public benchmark_base +{ +private: + struct block_timing + { + std::chrono::steady_clock::time_point submitted; + std::chrono::steady_clock::time_point election_started; + std::chrono::steady_clock::time_point election_stopped; + std::chrono::steady_clock::time_point cemented; + }; + + // Track timing for each block through the election pipeline + nano::locked> block_timings; + + nano::locked> pending_confirmation; + nano::locked> pending_cementing; + + // Metrics + std::atomic elections_started{ 0 }; + std::atomic elections_stopped{ 0 }; + std::atomic elections_confirmed{ 0 }; + std::atomic blocks_cemented{ 0 }; + +public: + elections_benchmark (std::shared_ptr node_a, benchmark_config const & config_a); + + void run (); + void run_iteration (std::deque> & sends, std::deque> & opens); + void print_statistics (); +}; + +void run_elections_benchmark (boost::program_options::variables_map const & vm, std::filesystem::path const & data_path) +{ + auto config = benchmark_config::parse (vm); + + std::cout << "=== BENCHMARK: Elections ===\n"; + std::cout << "Configuration:\n"; + std::cout << fmt::format (" Accounts: {}\n", config.num_accounts); + std::cout << fmt::format (" Iterations: {}\n", config.num_iterations); + std::cout << fmt::format (" Batch size: {}\n", config.batch_size); + + // Setup node directly in run method + nano::network_constants::set_active_network ("dev"); + nano::logger::initialize (nano::log_config::cli_default (nano::log::level::warn)); + + nano::node_flags node_flags; + nano::update_flags (node_flags, vm); + + auto io_ctx = std::make_shared (); + nano::work_pool work_pool{ nano::dev::network_params.network, std::numeric_limits::max () }; + + // Load configuration from current working directory (if exists) and cli config overrides + auto daemon_config = nano::load_config_file (nano::node_config_filename, {}, node_flags.config_overrides); + auto node_config = daemon_config.node; + node_config.network_params.work = nano::work_thresholds{ 0, 0, 0 }; + node_config.peering_port = 0; // Use random available port + node_config.max_backlog = 0; // Disable bounded backlog + + // Disable election schedulers and backlog scanning + node_config.hinted_scheduler.enable = false; + node_config.optimistic_scheduler.enable = false; + node_config.priority_scheduler.enable = false; + node_config.backlog_scan.enable = false; + + node_config.block_processor.max_peer_queue = std::numeric_limits::max (); // Unlimited queue size + node_config.block_processor.max_system_queue = std::numeric_limits::max (); // Unlimited queue size + node_config.max_unchecked_blocks = 1024 * 1024; // Large unchecked blocks cache to avoid dropping blocks + node_config.vote_processor.max_pr_queue = std::numeric_limits::max (); // Unlimited vote processing queue + + auto node = std::make_shared (io_ctx, nano::unique_path (), node_config, work_pool, node_flags); + node->start (); + nano::thread_runner runner (io_ctx, nano::default_logger (), node->config.io_threads); + + std::cout << "\nSystem Info:\n"; + std::cout << fmt::format (" Backend: {}\n", node->store.vendor_get ()); + std::cout << "\n"; + + // Insert dev genesis representative key for voting + auto wallet = node->wallets.create (nano::random_wallet_id ()); + wallet->insert_adhoc (nano::dev::genesis_key.prv); + + // Wait for node to be ready + std::this_thread::sleep_for (500ms); + + // Run benchmark + elections_benchmark benchmark{ node, config }; + benchmark.run (); + + node->stop (); +} + +elections_benchmark::elections_benchmark (std::shared_ptr node_a, benchmark_config const & config_a) : + benchmark_base (node_a, config_a) +{ + // Track when elections start + node->active.election_started.add ([this] (std::shared_ptr const & election, nano::bucket_index const & bucket, nano::priority_timestamp const & priority) { + auto now = std::chrono::steady_clock::now (); + auto hash = election->winner ()->hash (); + auto timings_l = block_timings.lock (); + + if (auto it = timings_l->find (hash); it != timings_l->end ()) + { + it->second.election_started = now; + } + + elections_started++; + }); + + // Track when elections stop (regardless of confirmation) + node->active.election_erased.add ([this] (std::shared_ptr const & election) { + auto now = std::chrono::steady_clock::now (); + auto hash = election->winner ()->hash (); + auto timings_l = block_timings.lock (); + auto pending_confirmation_l = pending_confirmation.lock (); + + if (auto it = timings_l->find (hash); it != timings_l->end ()) + { + it->second.election_stopped = now; + } + pending_confirmation_l->erase (hash); + + elections_stopped++; + elections_confirmed += election->confirmed () ? 1 : 0; + }); + + // Track when blocks get cemented + node->cementing_set.batch_cemented.add ([this] (auto const & hashes) { + auto now = std::chrono::steady_clock::now (); + auto pending_l = pending_cementing.lock (); + auto timings_l = block_timings.lock (); + + for (auto const & ctx : hashes) + { + auto hash = ctx.block->hash (); + + if (auto it = timings_l->find (hash); it != timings_l->end ()) + { + it->second.cemented = now; + } + pending_l->erase (hash); + + blocks_cemented++; + } + }); +} + +void elections_benchmark::run () +{ + std::cout << fmt::format ("Generating {} accounts...\n", config.num_accounts); + pool.generate_accounts (config.num_accounts); + + setup_genesis_distribution (0.1); // Only distribute 10%, keep 90% for voting weight + + for (size_t iteration = 0; iteration < config.num_iterations; ++iteration) + { + std::cout << fmt::format ("\n--- Iteration {}/{} --------------------------------------------------------------\n", iteration + 1, config.num_iterations); + std::cout << fmt::format ("Generating independent blocks...\n"); + auto [sends, opens] = generate_independent_blocks (); + + std::cout << fmt::format ("Measuring elections performance for {} opens...\n", opens.size ()); + run_iteration (sends, opens); + } + + print_statistics (); +} + +void elections_benchmark::run_iteration (std::deque> & sends, std::deque> & opens) +{ + auto const total_opens = opens.size (); + + // Process and cement all send blocks directly + std::cout << fmt::format ("Processing and cementing {} send blocks...\n", sends.size ()); + { + auto transaction = node->ledger.tx_begin_write (); + for (auto const & send : sends) + { + auto result = node->ledger.process (transaction, send); + release_assert (result == nano::block_status::progress, to_string (result)); + + // Add to cementing set for direct cementing + auto cemented = node->ledger.confirm (transaction, send->hash ()); + release_assert (!cemented.empty () && cemented.back ()->hash () == send->hash ()); + } + } + + // Process open blocks into ledger without confirming + std::cout << fmt::format ("Processing {} open blocks into ledger...\n", opens.size ()); + { + auto transaction = node->ledger.tx_begin_write (); + for (auto const & open : opens) + { + auto result = node->ledger.process (transaction, open); + release_assert (result == nano::block_status::progress, to_string (result)); + } + } + + // Initialize timing entries for open blocks only + { + auto now = std::chrono::steady_clock::now (); + auto timings_l = block_timings.lock (); + auto pending_cementing_l = pending_cementing.lock (); + auto pending_confirmation_l = pending_confirmation.lock (); + for (auto const & open : opens) + { + pending_cementing_l->emplace (open->hash ()); + pending_confirmation_l->emplace (open->hash ()); + timings_l->emplace (open->hash (), block_timing{ now }); + } + } + + auto const time_begin = std::chrono::high_resolution_clock::now (); + + // Manually start elections for open blocks only + std::cout << fmt::format ("Starting elections manually for {} open blocks...\n", opens.size ()); + for (auto const & open : opens) + { + // Use manual scheduler to start election + node->scheduler.manual.push (open); + } + + // Wait for all elections to complete and blocks to be cemented + nano::interval progress_interval; + while (true) + { + { + auto pending_cementing_l = pending_cementing.lock (); + auto pending_confirmation_l = pending_confirmation.lock (); + + if ((pending_cementing_l->empty () && pending_confirmation_l->empty ()) || progress_interval.elapse (3s)) + { + std::cout << fmt::format ("Confirming elections: {:>9} remaining | cementing: {:>9} remaining (active: {:>5} | cementing: {:>5} | deferred: {:>5})\n", + pending_confirmation_l->size (), + pending_cementing_l->size (), + node->active.size (), + node->cementing_set.size (), + node->cementing_set.deferred_size ()); + } + if (pending_cementing_l->empty () && pending_confirmation_l->empty ()) + { + break; + } + } + + std::this_thread::sleep_for (1ms); + } + + auto const time_end = std::chrono::high_resolution_clock::now (); + auto const time_us = std::chrono::duration_cast (time_end - time_begin).count (); + + std::cout << fmt::format ("\nPerformance: {} blocks/sec [{:.2f}s] {} blocks processed\n", + total_opens * 1000000 / time_us, time_us / 1000000.0, total_opens); + std::cout << "─────────────────────────────────────────────────────────────────\n"; + + node->stats.clear (); +} + +void elections_benchmark::print_statistics () +{ + std::cout << "\n--- SUMMARY ---------------------------------------------------------------------\n\n"; + std::cout << fmt::format ("Elections started: {:>10}\n", elections_started.load ()); + std::cout << fmt::format ("Elections stopped: {:>10}\n", elections_stopped.load ()); + std::cout << fmt::format ("Elections confirmed: {:>10}\n", elections_confirmed.load ()); + std::cout << fmt::format ("\n"); + + // Calculate timing statistics from raw data + auto timings_l = block_timings.lock (); + + uint64_t total_election_time = 0; + uint64_t total_confirmation_time = 0; + size_t election_count = 0; + size_t confirmed_count = 0; + + for (auto const & [hash, timing] : *timings_l) + { + release_assert (timing.election_started != std::chrono::steady_clock::time_point{}); + release_assert (timing.election_stopped != std::chrono::steady_clock::time_point{}); + release_assert (timing.cemented != std::chrono::steady_clock::time_point{}); + + total_election_time += std::chrono::duration_cast (timing.election_stopped - timing.election_started).count (); + election_count++; + + total_confirmation_time += std::chrono::duration_cast (timing.cemented - timing.election_started).count (); + confirmed_count++; + } + + std::cout << "\n"; + + std::cout << fmt::format ("Election time (activated > confirmed): {:>8.2f} ms/block avg\n", total_election_time / (election_count * 1000.0)); + std::cout << fmt::format ("Total time (activated > cemented): {:>8.2f} ms/block avg\n", total_confirmation_time / (confirmed_count * 1000.0)); +} +} \ No newline at end of file diff --git a/nano/nano_node/benchmarks/benchmark_pipeline.cpp b/nano/nano_node/benchmarks/benchmark_pipeline.cpp new file mode 100644 index 000000000..f0022d233 --- /dev/null +++ b/nano/nano_node/benchmarks/benchmark_pipeline.cpp @@ -0,0 +1,359 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +namespace nano::cli +{ +/* + * Full Pipeline Benchmark + * + * Measures the complete block confirmation pipeline from submission through processing, + * elections, and cementing. Tests all stages together including inter-component coordination. + * + * How it works: + * 1. Setup: Creates a node with genesis representative key for voting + * 2. Generate: Creates random transfer transactions (send/receive pairs) + * 3. Submit: Adds blocks via process_active() which triggers the full pipeline + * 4. Measure: Tracks time from submission through processing, election, and cementing + * 5. Report: Calculates overall throughput and timing breakdown for each stage + * + * Pipeline stages measured: + * - Block processing: submission -> ledger insertion + * - Election activation: ledger insertion -> election start + * - Election confirmation: election start -> block cemented + * - Total pipeline: submission -> cemented + * + * What is tested: + * - Block processor throughput + * - Election startup and scheduling + * - Vote generation and processing (with one local rep) + * - Quorum detection and confirmation + * - Cementing performance + * - Inter-component coordination and queueing + * + * What is NOT tested: + * - Network communication (local-only) + * - Multiple remote representatives + */ +class pipeline_benchmark : public benchmark_base +{ +private: + struct block_timing + { + std::chrono::steady_clock::time_point submitted; + std::chrono::steady_clock::time_point processed; + std::chrono::steady_clock::time_point election_started; + std::chrono::steady_clock::time_point election_stopped; + std::chrono::steady_clock::time_point confirmed; + std::chrono::steady_clock::time_point cemented; + }; + + // Track timing for each block through the pipeline + nano::locked> block_timings; + + // Track blocks waiting to be cemented + nano::locked> pending_cementing; + + // Metrics + std::atomic elections_started{ 0 }; + std::atomic elections_stopped{ 0 }; + std::atomic elections_confirmed{ 0 }; + std::atomic blocks_cemented{ 0 }; + +public: + pipeline_benchmark (std::shared_ptr node_a, benchmark_config const & config_a); + + void run (); + void run_iteration (std::deque> & blocks); + void print_statistics (); +}; + +void run_pipeline_benchmark (boost::program_options::variables_map const & vm, std::filesystem::path const & data_path) +{ + auto config = benchmark_config::parse (vm); + + std::cout << "=== BENCHMARK: Full Pipeline ===\n"; + std::cout << "Configuration:\n"; + std::cout << fmt::format (" Accounts: {}\n", config.num_accounts); + std::cout << fmt::format (" Iterations: {}\n", config.num_iterations); + std::cout << fmt::format (" Batch size: {}\n", config.batch_size); + + // Setup node directly in run method + nano::network_constants::set_active_network ("dev"); + nano::logger::initialize (nano::log_config::cli_default (nano::log::level::warn)); + + nano::node_flags node_flags; + nano::update_flags (node_flags, vm); + + auto io_ctx = std::make_shared (); + nano::work_pool work_pool{ nano::dev::network_params.network, std::numeric_limits::max () }; + + // Load configuration from current working directory (if exists) and cli config overrides + auto daemon_config = nano::load_config_file (nano::node_config_filename, {}, node_flags.config_overrides); + auto node_config = daemon_config.node; + node_config.network_params.work = nano::work_thresholds{ 0, 0, 0 }; + node_config.peering_port = 0; // Use random available port + node_config.max_backlog = 0; // Disable bounded backlog + node_config.block_processor.max_peer_queue = std::numeric_limits::max (); // Unlimited queue size + node_config.block_processor.max_system_queue = std::numeric_limits::max (); // Unlimited queue size + node_config.max_unchecked_blocks = 1024 * 1024; // Large unchecked blocks cache to avoid dropping blocks + node_config.vote_processor.max_pr_queue = std::numeric_limits::max (); // Unlimited vote processing queue + + node_config.priority_bucket.max_blocks = std::numeric_limits::max (); // Unlimited priority bucket + node_config.priority_bucket.max_elections = std::numeric_limits::max (); // Unlimited bucket elections + node_config.priority_bucket.reserved_elections = std::numeric_limits::max (); // Unlimited bucket elections + + auto node = std::make_shared (io_ctx, nano::unique_path (), node_config, work_pool, node_flags); + node->start (); + nano::thread_runner runner (io_ctx, nano::default_logger (), node->config.io_threads); + + std::cout << "\nSystem Info:\n"; + std::cout << fmt::format (" Backend: {}\n", node->store.vendor_get ()); + std::cout << fmt::format (" Block processor threads: {}\n", 1); // TODO: Log number of block processor threads when upstreamed + std::cout << fmt::format (" Vote processor threads: {}\n", node->config.vote_processor.threads); + std::cout << fmt::format (" Active elections limit: {}\n", node->config.active_elections.size); + std::cout << fmt::format (" Priority bucket max blocks: {}\n", node->config.priority_bucket.max_blocks); + std::cout << fmt::format (" Priority bucket max elections: {}\n", node->config.priority_bucket.max_elections); + std::cout << fmt::format (" Block processor max peer queue: {}\n", node->config.block_processor.max_peer_queue); + std::cout << fmt::format (" Block processor max system queue: {}\n", node->config.block_processor.max_system_queue); + std::cout << fmt::format (" Vote processor max pr queue: {}\n", node->config.vote_processor.max_pr_queue); + std::cout << fmt::format (" Max unchecked blocks: {}\n", node->config.max_unchecked_blocks); + std::cout << "\n"; + + // Insert dev genesis representative key for voting + auto wallet = node->wallets.create (nano::random_wallet_id ()); + wallet->insert_adhoc (nano::dev::genesis_key.prv); + + // Wait for node to be ready + std::this_thread::sleep_for (500ms); + + // Run benchmark + pipeline_benchmark benchmark{ node, config }; + benchmark.run (); + + node->stop (); +} + +pipeline_benchmark::pipeline_benchmark (std::shared_ptr node_a, benchmark_config const & config_a) : + benchmark_base (node_a, config_a) +{ + // Track when blocks get processed + node->ledger_notifications.blocks_processed.add ([this] (std::deque> const & batch) { + auto now = std::chrono::steady_clock::now (); + auto timings_l = block_timings.lock (); + + for (auto const & [status, context] : batch) + { + if (status == nano::block_status::progress) + { + if (auto it = timings_l->find (context.block->hash ()); it != timings_l->end ()) + { + it->second.processed = now; + } + processed_blocks_count++; + } + } + }); + + // Track when elections start + node->active.election_started.add ([this] (std::shared_ptr const & election, nano::bucket_index const & bucket, nano::priority_timestamp const & priority) { + auto now = std::chrono::steady_clock::now (); + auto hash = election->winner ()->hash (); + auto timings_l = block_timings.lock (); + + if (auto it = timings_l->find (hash); it != timings_l->end ()) + { + it->second.election_started = now; + } + + elections_started++; + }); + + // Track when elections stop (regardless of confirmation) + node->active.election_erased.add ([this] (std::shared_ptr const & election) { + auto now = std::chrono::steady_clock::now (); + auto hash = election->winner ()->hash (); + auto timings_l = block_timings.lock (); + + if (auto it = timings_l->find (hash); it != timings_l->end ()) + { + it->second.election_stopped = now; + } + + elections_stopped++; + elections_confirmed += election->confirmed () ? 1 : 0; + }); + + // Track when blocks get cemented + node->cementing_set.batch_cemented.add ([this] (auto const & hashes) { + auto now = std::chrono::steady_clock::now (); + auto pending_l = pending_cementing.lock (); + auto timings_l = block_timings.lock (); + + for (auto const & ctx : hashes) + { + auto hash = ctx.block->hash (); + + if (auto it = timings_l->find (hash); it != timings_l->end ()) + { + it->second.cemented = now; + } + pending_l->erase (hash); + + blocks_cemented++; + } + }); +} + +void pipeline_benchmark::run () +{ + std::cout << fmt::format ("Generating {} accounts...\n", config.num_accounts); + pool.generate_accounts (config.num_accounts); + + setup_genesis_distribution (0.1); // Only distribute 10%, keep 90% for voting weight + + for (size_t iteration = 0; iteration < config.num_iterations; ++iteration) + { + std::cout << fmt::format ("\n--- Iteration {}/{} --------------------------------------------------------------\n", iteration + 1, config.num_iterations); + std::cout << fmt::format ("Generating {} random transfers...\n", config.batch_size / 2); + auto blocks = generate_random_transfers (); + + std::cout << fmt::format ("Measuring full confirmation pipeline for {} blocks...\n", blocks.size ()); + run_iteration (blocks); + } + + print_statistics (); +} + +void pipeline_benchmark::run_iteration (std::deque> & blocks) +{ + auto const total_blocks = blocks.size (); + + // Initialize timing entries for all blocks + { + auto now = std::chrono::steady_clock::now (); + auto timings_l = block_timings.lock (); + auto pending_l = pending_cementing.lock (); + for (auto const & block : blocks) + { + timings_l->emplace (block->hash (), block_timing{ now }); + pending_l->emplace (block->hash (), now); + } + } + + auto const time_begin = std::chrono::high_resolution_clock::now (); + + // Submit all blocks through the full pipeline + while (!blocks.empty ()) + { + auto block = blocks.front (); + blocks.pop_front (); + + // Process block through full confirmation pipeline + node->process_active (block); + } + + // Wait for all blocks to be confirmed and cemented + nano::interval progress_interval; + while (true) + { + { + auto pending_l = pending_cementing.lock (); + if (pending_l->empty () || progress_interval.elapse (3s)) + { + std::cout << fmt::format ("Blocks remaining: {:>9} (block processor: {:>9} | active: {:>5} | cementing: {:>5} | pool: {:>5})\n", + pending_l->size (), + node->block_processor.size (), + node->active.size (), + node->cementing_set.size (), + node->scheduler.priority.size ()); + } + if (pending_l->empty ()) + { + break; + } + } + + std::this_thread::sleep_for (1ms); + } + + auto const time_end = std::chrono::high_resolution_clock::now (); + auto const time_us = std::chrono::duration_cast (time_end - time_begin).count (); + + std::cout << fmt::format ("\nPerformance: {} blocks/sec [{:.2f}s] {} blocks processed\n", + total_blocks * 1000000 / time_us, time_us / 1000000.0, total_blocks); + std::cout << "─────────────────────────────────────────────────────────────────\n"; + + node->stats.clear (); +} + +void pipeline_benchmark::print_statistics () +{ + std::cout << "\n--- SUMMARY ---------------------------------------------------------------------\n\n"; + std::cout << fmt::format ("Blocks processed: {:>10}\n", processed_blocks_count.load ()); + std::cout << fmt::format ("Elections started: {:>10}\n", elections_started.load ()); + std::cout << fmt::format ("Elections stopped: {:>10}\n", elections_stopped.load ()); + std::cout << fmt::format ("Elections confirmed: {:>10}\n", elections_confirmed.load ()); + std::cout << fmt::format ("\n"); + std::cout << fmt::format ("Accounts total: {:>10}\n", pool.total_accounts ()); + std::cout << fmt::format ("Accounts with balance: {:>10} ({:.1f}%)\n", + pool.accounts_with_balance_count (), + 100.0 * pool.accounts_with_balance_count () / pool.total_accounts ()); + + // Calculate timing statistics from raw data + auto timings_l = block_timings.lock (); + + uint64_t total_processing_time = 0; + uint64_t total_activation_time = 0; + uint64_t total_election_time = 0; + uint64_t total_cementing_time = 0; + size_t processed_count = 0; + size_t activation_count = 0; + size_t election_count = 0; + size_t cemented_count = 0; + + for (auto const & [hash, timing] : *timings_l) + { + release_assert (timing.submitted != std::chrono::steady_clock::time_point{}); + release_assert (timing.election_started != std::chrono::steady_clock::time_point{}); + release_assert (timing.election_stopped != std::chrono::steady_clock::time_point{}); + release_assert (timing.cemented != std::chrono::steady_clock::time_point{}); + + total_processing_time += std::chrono::duration_cast (timing.processed - timing.submitted).count (); + processed_count++; + + total_activation_time += std::chrono::duration_cast (timing.election_started - timing.processed).count (); + activation_count++; + + total_election_time += std::chrono::duration_cast (timing.cemented - timing.election_started).count (); + election_count++; + + total_cementing_time += std::chrono::duration_cast (timing.cemented - timing.submitted).count (); + cemented_count++; + } + + std::cout << "\n"; + std::cout << fmt::format ("Block processing (submitted > processed): {:>8.2f} ms/block avg\n", total_processing_time / (processed_count * 1000.0)); + std::cout << fmt::format ("Election activation (processed > activated): {:>8.2f} ms/block avg\n", total_activation_time / (activation_count * 1000.0)); + std::cout << fmt::format ("Election time (activated > confirmed): {:>8.2f} ms/block avg\n", total_election_time / (election_count * 1000.0)); + std::cout << fmt::format ("Total pipeline (submitted > cemented): {:>8.2f} ms/block avg\n", total_cementing_time / (cemented_count * 1000.0)); +} +} \ No newline at end of file diff --git a/nano/nano_node/benchmarks/benchmarks.cpp b/nano/nano_node/benchmarks/benchmarks.cpp new file mode 100644 index 000000000..d3f2866f1 --- /dev/null +++ b/nano/nano_node/benchmarks/benchmarks.cpp @@ -0,0 +1,616 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace nano::cli +{ +account_pool::account_pool () : + gen (rd ()) +{ +} + +void account_pool::generate_accounts (size_t count) +{ + keys.clear (); + keys.reserve (count); + account_to_keypair.clear (); + balances.clear (); + accounts_with_balance.clear (); + balance_lookup.clear (); + frontiers.clear (); + + for (size_t i = 0; i < count; ++i) + { + keys.emplace_back (); + account_to_keypair[keys[i].pub] = keys[i]; + balances[keys[i].pub] = 0; + } +} + +nano::account account_pool::get_random_account_with_balance () +{ + debug_assert (!accounts_with_balance.empty ()); + std::uniform_int_distribution dist (0, accounts_with_balance.size () - 1); + return accounts_with_balance[dist (gen)]; +} + +nano::account account_pool::get_random_account () +{ + debug_assert (!keys.empty ()); + std::uniform_int_distribution dist (0, keys.size () - 1); + return keys[dist (gen)].pub; +} + +nano::keypair const & account_pool::get_keypair (nano::account const & account) +{ + auto it = account_to_keypair.find (account); + debug_assert (it != account_to_keypair.end ()); + return it->second; +} + +void account_pool::update_balance (nano::account const & account, nano::uint128_t new_balance) +{ + auto old_balance = balances[account]; + balances[account] = new_balance; + + bool had_balance = balance_lookup.count (account) > 0; + bool has_balance_now = new_balance > 0; + + if (!had_balance && has_balance_now) + { + // Account gained balance + accounts_with_balance.push_back (account); + balance_lookup.insert (account); + } + else if (had_balance && !has_balance_now) + { + // Account lost balance + auto it = std::find (accounts_with_balance.begin (), accounts_with_balance.end (), account); + if (it != accounts_with_balance.end ()) + { + accounts_with_balance.erase (it); + } + balance_lookup.erase (account); + } +} + +nano::uint128_t account_pool::get_balance (nano::account const & account) +{ + auto it = balances.find (account); + return (it != balances.end ()) ? it->second : 0; +} + +bool account_pool::has_balance (nano::account const & account) +{ + return balance_lookup.count (account) > 0; +} + +size_t account_pool::accounts_with_balance_count () const +{ + return accounts_with_balance.size (); +} + +size_t account_pool::total_accounts () const +{ + return keys.size (); +} + +std::vector account_pool::get_accounts_with_balance () const +{ + return accounts_with_balance; +} + +void account_pool::set_initial_balance (nano::account const & account, nano::uint128_t balance) +{ + balances[account] = balance; + if (balance > 0) + { + if (balance_lookup.count (account) == 0) + { + accounts_with_balance.push_back (account); + balance_lookup.insert (account); + } + } +} + +void account_pool::set_frontier (nano::account const & account, nano::block_hash const & frontier) +{ + frontiers[account] = frontier; +} + +nano::block_hash account_pool::get_frontier (nano::account const & account) const +{ + auto it = frontiers.find (account); + return (it != frontiers.end ()) ? it->second : nano::block_hash (0); +} + +/* + * + */ + +benchmark_config benchmark_config::parse (boost::program_options::variables_map const & vm) +{ + benchmark_config config; + + if (vm.count ("accounts")) + { + config.num_accounts = std::stoull (vm["accounts"].as ()); + } + if (vm.count ("iterations")) + { + config.num_iterations = std::stoull (vm["iterations"].as ()); + } + if (vm.count ("batch_size")) + { + config.batch_size = std::stoull (vm["batch_size"].as ()); + } + if (vm.count ("cementing_mode")) + { + auto mode_str = vm["cementing_mode"].as (); + if (mode_str == "root") + { + config.cementing_mode = cementing_mode::root; + } + else if (mode_str == "sequential") + { + config.cementing_mode = cementing_mode::sequential; + } + else + { + std::cerr << "Invalid cementing mode: " << mode_str << ". Using default (sequential).\n"; + } + } + return config; +} + +benchmark_base::benchmark_base (std::shared_ptr node_a, benchmark_config const & config_a) : + node (node_a), config (config_a) +{ +} + +/* + * Prepares the ledger for benchmarking by transferring all genesis funds to a single random account. + * This creates a clean starting state where: + * - One account holds all the balance (simulating a funded account) + * - All other accounts start with zero balance + * - The funded account can then distribute funds to other accounts during the benchmark + * + * Algorithm: + * 1. Select a random account from the pool to be the initial holder + * 2. Create a send block from genesis account sending all balance + * 3. Create an open block for the selected account to receive all funds + * 4. Process both blocks to establish the initial state + */ +void benchmark_base::setup_genesis_distribution (double distribution_percentage) +{ + std::cout << "Setting up genesis distribution...\n"; + + // Get genesis balance and latest block + nano::block_hash genesis_latest (node->latest (nano::dev::genesis_key.pub)); + nano::uint128_t genesis_balance (std::numeric_limits::max ()); + + // Calculate amount to send using 256-bit arithmetic to avoid precision loss + nano::uint256_t genesis_balance_256 = genesis_balance; + nano::uint256_t multiplier = static_cast (distribution_percentage * 1000000); + nano::uint256_t send_amount_256 = (genesis_balance_256 * multiplier) / 1000000; + release_assert (send_amount_256 <= std::numeric_limits::max (), "send amount overflows uint128_t"); + nano::uint128_t send_amount = static_cast (send_amount_256); + nano::uint128_t remaining_balance = genesis_balance - send_amount; + + // Select random account to receive genesis funds + nano::account target_account = pool.get_random_account (); + auto & target_keypair = pool.get_keypair (target_account); + + // Create send block from genesis to target account + nano::block_builder builder; + auto send = builder.state () + .account (nano::dev::genesis_key.pub) + .previous (genesis_latest) + .representative (nano::dev::genesis_key.pub) + .balance (remaining_balance) + .link (target_account) + .sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub) + .work (0) + .build (); + + // Create open block for target account + auto open = builder.state () + .account (target_account) + .previous (0) + .representative (target_account) + .balance (send_amount) + .link (send->hash ()) + .sign (target_keypair.prv, target_keypair.pub) + .work (0) + .build (); + + // Process blocks + auto result1 = node->process (send); + release_assert (result1 == nano::block_status::progress, to_string (result1)); + auto result2 = node->process (open); + release_assert (result2 == nano::block_status::progress, to_string (result2)); + + // Update pool balance tracking + pool.set_initial_balance (target_account, send_amount); + + // Initialize frontier for target account + pool.set_frontier (target_account, open->hash ()); + + std::cout << fmt::format ("Genesis distribution complete: {:.1f}% distributed, {:.1f}% retained for voting\n", + distribution_percentage * 100.0, (1.0 - distribution_percentage) * 100.0); +} + +/* + * Generates random transfer transactions between accounts with no specific dependency pattern. + * This simulates typical network activity with independent transactions. + * + * Algorithm: + * 1. For each transfer (batch_size/2 transfers, since each creates 2 blocks): + * a. Select a random sender account that has balance + * b. Select a random receiver account (can be any account) + * c. Generate a random transfer amount (up to sender's balance) + * d. Create a send block from sender + * e. Create a receive/open block for receiver + * 2. Update account balances and frontiers after each transfer + * 3. Continue until batch_size blocks are generated or no accounts have balance + * + * The resulting blocks have no intentional dependency structure beyond the natural + * send->receive pairs, making this suitable for testing sequential block processing. + */ +std::deque> benchmark_base::generate_random_transfers () +{ + std::deque> blocks; + std::random_device rd; + std::mt19937 gen (rd ()); + + // Generate batch_size number of transfer pairs (send + receive = 2 blocks each) + size_t transfers_generated = 0; + nano::block_builder builder; + + while (transfers_generated < config.batch_size / 2) // Divide by 2 since each transfer creates 2 blocks + { + if (pool.accounts_with_balance_count () == 0) + { + std::cout << "No accounts with balance remaining, stopping...\n"; + break; + } + + // Get random sender with balance + nano::account sender = pool.get_random_account_with_balance (); + auto & sender_keypair = pool.get_keypair (sender); + nano::uint128_t sender_balance = pool.get_balance (sender); + + if (sender_balance == 0) + continue; + + // Get random receiver + nano::account receiver = pool.get_random_account (); + auto & receiver_keypair = pool.get_keypair (receiver); + + // Random transfer amount (but not more than sender balance) + std::uniform_int_distribution amount_dist (1, sender_balance.convert_to ()); + nano::uint128_t transfer_amount = std::min (static_cast (amount_dist (gen)), sender_balance); + + // Get or initialize sender frontier + nano::block_hash sender_frontier = pool.get_frontier (sender); + nano::root work_root; + if (sender_frontier != 0) + { + work_root = sender_frontier; + } + else + { + sender_frontier = 0; // First block for this account + work_root = sender; // Use account address for first block work + } + + // Create send block + nano::uint128_t new_sender_balance = sender_balance - transfer_amount; + auto send = builder.state () + .account (sender) + .previous (sender_frontier) + .representative (sender) + .balance (new_sender_balance) + .link (receiver) + .sign (sender_keypair.prv, sender_keypair.pub) + .work (0) + .build (); + + blocks.push_back (send); + pool.set_frontier (sender, send->hash ()); + pool.update_balance (sender, new_sender_balance); + + // Create receive block + nano::uint128_t receiver_balance = pool.get_balance (receiver); + nano::uint128_t new_receiver_balance = receiver_balance + transfer_amount; + + nano::block_hash receiver_frontier = pool.get_frontier (receiver); + nano::root receiver_work_root; + if (receiver_frontier != 0) + { + receiver_work_root = receiver_frontier; + } + else + { + receiver_frontier = 0; // First block for this account (open block) + receiver_work_root = receiver; // Use account address for first block work + } + + auto receive = builder.state () + .account (receiver) + .previous (receiver_frontier) + .representative (receiver) + .balance (new_receiver_balance) + .link (send->hash ()) + .sign (receiver_keypair.prv, receiver_keypair.pub) + .work (0) + .build (); + + blocks.push_back (receive); + pool.set_frontier (receiver, receive->hash ()); + pool.update_balance (receiver, new_receiver_balance); + + transfers_generated++; + } + + std::cout << fmt::format ("Generated {} blocks\n", blocks.size ()); + + return blocks; +} + +/* + * Generates blocks in a dependency tree structure optimized for root mode cementing. + * All blocks are organized so they become dependencies of a single root block. + * + * Algorithm: + * 1. Random transfer phase (80% of blocks): + * - Generate random transfers between accounts (same as generate_random_transfers) + * - Creates a natural web of dependencies through send/receive pairs + * 2. Convergence phase (20% of blocks): + * - All accounts with balance send their entire balance to a collector account + * - The collector receives all these sends in sequence + * - The final receive block becomes the root that depends on all previous blocks + * + * The last block in the returned deque is the ultimate root that depends on all others. + * Cementing this single block will cascade and cement all blocks in the tree. + */ +std::deque> benchmark_base::generate_dependent_chain () +{ + std::deque> blocks; + std::random_device rd; + std::mt19937 gen (rd ()); + nano::block_builder builder; + + // Phase 1: Random transfers (80% of blocks) + size_t random_transfer_blocks = config.batch_size * 0.8; + size_t transfers_to_generate = random_transfer_blocks / 2; // Each transfer creates 2 blocks + + std::cout << fmt::format ("Generating dependent chain: {} random transfers, then convergence\n", + transfers_to_generate); + + // Phase 1: Generate random transfers (same logic as generate_random_transfers) + size_t transfers_generated = 0; + while (transfers_generated < transfers_to_generate && pool.accounts_with_balance_count () > 0) + { + // Get random sender with balance + nano::account sender = pool.get_random_account_with_balance (); + auto & sender_keypair = pool.get_keypair (sender); + nano::uint128_t sender_balance = pool.get_balance (sender); + + if (sender_balance == 0) + continue; + + // Get random receiver + nano::account receiver = pool.get_random_account (); + auto & receiver_keypair = pool.get_keypair (receiver); + + // Random transfer amount (but not more than sender balance) + std::uniform_int_distribution amount_dist (1, sender_balance.convert_to ()); + nano::uint128_t transfer_amount = std::min (static_cast (amount_dist (gen)), sender_balance); + + // Get or initialize sender frontier + nano::block_hash sender_frontier = pool.get_frontier (sender); + + // Create send block + nano::uint128_t new_sender_balance = sender_balance - transfer_amount; + auto send = builder.state () + .account (sender) + .previous (sender_frontier) + .representative (sender) + .balance (new_sender_balance) + .link (receiver) + .sign (sender_keypair.prv, sender_keypair.pub) + .work (0) + .build (); + + blocks.push_back (send); + pool.set_frontier (sender, send->hash ()); + pool.update_balance (sender, new_sender_balance); + + // Create receive block + nano::uint128_t receiver_balance = pool.get_balance (receiver); + nano::uint128_t new_receiver_balance = receiver_balance + transfer_amount; + nano::block_hash receiver_frontier = pool.get_frontier (receiver); + + auto receive = builder.state () + .account (receiver) + .previous (receiver_frontier) + .representative (receiver) + .balance (new_receiver_balance) + .link (send->hash ()) + .sign (receiver_keypair.prv, receiver_keypair.pub) + .work (0) + .build (); + + blocks.push_back (receive); + pool.set_frontier (receiver, receive->hash ()); + pool.update_balance (receiver, new_receiver_balance); + + transfers_generated++; + } + + // Phase 2: Convergence - all accounts with balance send to a collector + std::cout << fmt::format ("Converging {} accounts to collector account\n", + pool.accounts_with_balance_count ()); + + // Select a collector account (can be new or existing) + nano::account collector = pool.get_random_account (); + auto & collector_keypair = pool.get_keypair (collector); + nano::block_hash collector_frontier = pool.get_frontier (collector); + nano::uint128_t collector_balance = pool.get_balance (collector); + + // Collect all accounts with balance (except collector) + std::vector> accounts_to_drain; + auto accounts_with_balance = pool.get_accounts_with_balance (); + for (auto const & account : accounts_with_balance) + { + if (account != collector) + { + nano::uint128_t balance = pool.get_balance (account); + accounts_to_drain.push_back ({ account, balance }); + } + } + + // All accounts send a random amount to collector + std::vector> convergence_sends; + for (auto const & [account, balance] : accounts_to_drain) + { + auto & account_keypair = pool.get_keypair (account); + nano::block_hash account_frontier = pool.get_frontier (account); + + // Send random amount to collector (between 1 and full balance) + std::uniform_int_distribution amount_dist (1, balance.convert_to ()); + nano::uint128_t send_amount = static_cast (amount_dist (gen)); + nano::uint128_t remaining_balance = balance - send_amount; + + auto send = builder.state () + .account (account) + .previous (account_frontier) + .representative (account) + .balance (remaining_balance) + .link (collector) + .sign (account_keypair.prv, account_keypair.pub) + .work (0) + .build (); + + blocks.push_back (send); + convergence_sends.push_back ({ send->hash (), send_amount }); + pool.set_frontier (account, send->hash ()); + pool.update_balance (account, remaining_balance); + } + + // Collector receives all sends (these become the root blocks) + for (auto const & [send_hash, amount] : convergence_sends) + { + collector_balance += amount; + auto receive = builder.state () + .account (collector) + .previous (collector_frontier) + .representative (collector) + .balance (collector_balance) + .link (send_hash) + .sign (collector_keypair.prv, collector_keypair.pub) + .work (0) + .build (); + + blocks.push_back (receive); + collector_frontier = receive->hash (); + } + + // Update collector state + pool.set_frontier (collector, collector_frontier); + pool.update_balance (collector, collector_balance); + + std::cout << fmt::format ("Generated {} blocks in dependent chain topology\n", blocks.size ()); + + return blocks; +} + +/* + * Generates independent blocks - one block per account with no dependencies. + * Returns sends and opens separately so sends can be confirmed first, then opens processed for elections. + */ +std::pair>, std::deque>> benchmark_base::generate_independent_blocks () +{ + std::deque> sends; + std::deque> opens; + nano::block_builder builder; + + // Find accounts with balance to send from + auto accounts_with_balance = pool.get_accounts_with_balance (); + if (accounts_with_balance.empty ()) + { + std::cout << "No accounts with balance available\n"; + return { sends, opens }; + } + + // Generate independent blocks up to batch_size + for (size_t i = 0; i < config.batch_size && !accounts_with_balance.empty (); ++i) + { + // Pick a sender with balance + nano::account sender = accounts_with_balance[i % accounts_with_balance.size ()]; + auto & sender_keypair = pool.get_keypair (sender); + nano::uint128_t sender_balance = pool.get_balance (sender); + + if (sender_balance == 0) + continue; + + // Create a brand new receiver account + nano::keypair receiver_keypair; + nano::account receiver = receiver_keypair.pub; + + // Send a small amount to the new account + nano::uint128_t transfer_amount = std::min (sender_balance, nano::uint128_t (1000000)); // Small fixed amount + nano::block_hash sender_frontier = pool.get_frontier (sender); + nano::uint128_t new_sender_balance = sender_balance - transfer_amount; + + // Create send block + auto send = builder.state () + .account (sender) + .previous (sender_frontier) + .representative (sender) + .balance (new_sender_balance) + .link (receiver) + .sign (sender_keypair.prv, sender_keypair.pub) + .work (0) + .build (); + + // Create open block for new receiver (this is the independent block) + auto open = builder.state () + .account (receiver) + .previous (0) // First block for this account + .representative (receiver) + .balance (transfer_amount) + .link (send->hash ()) + .sign (receiver_keypair.prv, receiver_keypair.pub) + .work (0) + .build (); + + // Separate sends and opens + sends.push_back (send); + opens.push_back (open); + + // Update pool state for sender only (receiver is new account not tracked) + pool.set_frontier (sender, send->hash ()); + pool.update_balance (sender, new_sender_balance); + } + + std::cout << fmt::format ("Generated {} sends and {} opens\n", sends.size (), opens.size ()); + + return { sends, opens }; +} +} \ No newline at end of file diff --git a/nano/nano_node/benchmarks/benchmarks.hpp b/nano/nano_node/benchmarks/benchmarks.hpp new file mode 100644 index 000000000..8565d6020 --- /dev/null +++ b/nano/nano_node/benchmarks/benchmarks.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace nano::cli +{ +enum class cementing_mode +{ + sequential, + root +}; + +class account_pool +{ +private: + std::vector keys; + std::unordered_map account_to_keypair; + std::unordered_map balances; + std::vector accounts_with_balance; + std::unordered_set balance_lookup; + std::unordered_map frontiers; + std::random_device rd; + std::mt19937 gen; + +public: + account_pool (); + + void generate_accounts (size_t count); + nano::account get_random_account_with_balance (); + nano::account get_random_account (); + nano::keypair const & get_keypair (nano::account const & account); + void update_balance (nano::account const & account, nano::uint128_t new_balance); + nano::uint128_t get_balance (nano::account const & account); + bool has_balance (nano::account const & account); + size_t accounts_with_balance_count () const; + size_t total_accounts () const; + std::vector get_accounts_with_balance () const; + void set_initial_balance (nano::account const & account, nano::uint128_t balance); + void set_frontier (nano::account const & account, nano::block_hash const & frontier); + nano::block_hash get_frontier (nano::account const & account) const; +}; + +struct benchmark_config +{ + size_t num_accounts{ 150000 }; + size_t num_iterations{ 5 }; + size_t batch_size{ 250000 }; + nano::cli::cementing_mode cementing_mode{ nano::cli::cementing_mode::sequential }; + + static benchmark_config parse (boost::program_options::variables_map const & vm); +}; + +class benchmark_base +{ +protected: + account_pool pool; + std::shared_ptr node; + benchmark_config config; + + // Common metrics + std::atomic processed_blocks_count{ 0 }; + +public: + benchmark_base (std::shared_ptr node_a, benchmark_config const & config_a); + virtual ~benchmark_base () = default; + + // Transfers genesis balance to a random account to prepare for benchmarking + void setup_genesis_distribution (double distribution_percentage = 1.0); + + // Generates random transfer pairs between accounts with no specific dependency structure + std::deque> generate_random_transfers (); + + // Generates blocks that are dependencies of a single root block (last in deque) + std::deque> generate_dependent_chain (); + + // Generates independent blocks - returns sends and opens separately + std::pair>, std::deque>> generate_independent_blocks (); +}; + +// Benchmark entry points - individual implementations are in separate cpp files +void run_block_processing_benchmark (boost::program_options::variables_map const & vm, std::filesystem::path const & data_path); +void run_cementing_benchmark (boost::program_options::variables_map const & vm, std::filesystem::path const & data_path); +void run_elections_benchmark (boost::program_options::variables_map const & vm, std::filesystem::path const & data_path); +void run_pipeline_benchmark (boost::program_options::variables_map const & vm, std::filesystem::path const & data_path); +} \ No newline at end of file diff --git a/nano/nano_node/entry.cpp b/nano/nano_node/entry.cpp index e27e551cf..e7fbf38f2 100644 --- a/nano/nano_node/entry.cpp +++ b/nano/nano_node/entry.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -132,6 +133,10 @@ int main (int argc, char * const * argv) ("debug_profile_bootstrap", "Profile bootstrap style blocks processing (at least 10GB of free storage space required)") ("debug_profile_sign", "Profile signature generation") ("debug_profile_process", "Profile active blocks processing (only for nano_dev_network)") + ("benchmark_block_processing", "Run block processing throughput benchmark") + ("benchmark_cementing", "Run cementing throughput benchmark") + ("benchmark_elections", "Run elections confirmation and cementing benchmark") + ("benchmark_pipeline", "Run full confirmation pipeline benchmark") ("debug_profile_votes", "Profile votes processing (only for nano_dev_network)") ("debug_profile_frontiers_confirmation", "Profile frontiers confirmation speed (only for nano_dev_network)") ("debug_random_feed", "Generates output to RNG test suites") @@ -149,6 +154,10 @@ int main (int argc, char * const * argv) ("difficulty", boost::program_options::value (), "Defines for OpenCL command, HEX") ("multiplier", boost::program_options::value (), "Defines for work generation. Overrides ") ("count", boost::program_options::value (), "Defines for various commands") + ("accounts", boost::program_options::value (), "Defines for throughput benchmark (default 500000)") + ("iterations", boost::program_options::value (), "Defines for throughput benchmark (default 10)") + ("batch_size", boost::program_options::value (), "Defines for throughput benchmark (default 250000)") + ("cementing_mode", boost::program_options::value (), "Defines cementing mode for benchmark: 'sequential' or 'root' (default sequential)") ("pow_sleep_interval", boost::program_options::value (), "Defines the amount to sleep inbetween each pow calculation attempt") ("address_column", boost::program_options::value (), "Defines which column the addresses are located, 0 indexed (check --debug_output_last_backtrace_dump output)") ("silent", "Silent command execution") @@ -196,12 +205,7 @@ int main (int argc, char * const * argv) { nano::daemon daemon; nano::node_flags flags; - auto flags_ec = nano::update_flags (flags, vm); - if (flags_ec) - { - std::cerr << flags_ec.message () << std::endl; - std::exit (1); - } + nano::update_flags (flags, vm); daemon.run (data_path, flags); } else if (vm.count ("compare_rep_weights")) @@ -1052,6 +1056,22 @@ int main (int argc, char * const * argv) std::cout << boost::str (boost::format ("%|1$ 12d| us \n%2% blocks per second\n") % time % (max_blocks * 1000000 / time)); release_assert (node->ledger.block_count () == max_blocks + 1); } + else if (vm.count ("benchmark_block_processing")) + { + nano::cli::run_block_processing_benchmark (vm, data_path); + } + else if (vm.count ("benchmark_cementing")) + { + nano::cli::run_cementing_benchmark (vm, data_path); + } + else if (vm.count ("benchmark_elections")) + { + nano::cli::run_elections_benchmark (vm, data_path); + } + else if (vm.count ("benchmark_pipeline")) + { + nano::cli::run_pipeline_benchmark (vm, data_path); + } else if (vm.count ("debug_profile_votes")) { nano::block_builder builder; diff --git a/nano/nano_wallet/entry.cpp b/nano/nano_wallet/entry.cpp index b32ccb687..61b6fa0bc 100644 --- a/nano/nano_wallet/entry.cpp +++ b/nano/nano_wallet/entry.cpp @@ -319,11 +319,7 @@ int main (int argc, char * const * argv) data_path = nano::working_path (); } nano::node_flags flags; - auto flags_ec = nano::update_flags (flags, vm); - if (flags_ec) - { - throw std::runtime_error (flags_ec.message ()); - } + nano::update_flags (flags, vm); result = daemon.run_wallet (application, argc, argv, data_path, flags); } catch (std::exception const & e) diff --git a/nano/node/CMakeLists.txt b/nano/node/CMakeLists.txt index 84768428b..2f90c6998 100644 --- a/nano/node/CMakeLists.txt +++ b/nano/node/CMakeLists.txt @@ -16,6 +16,8 @@ add_library( ${platform_sources} active_elections.hpp active_elections.cpp + active_elections_index.hpp + active_elections_index.cpp backlog_scan.hpp backlog_scan.cpp bandwidth_limiter.hpp diff --git a/nano/node/active_elections.cpp b/nano/node/active_elections.cpp index 93ebb0957..e55e7bfac 100644 --- a/nano/node/active_elections.cpp +++ b/nano/node/active_elections.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -17,6 +16,7 @@ #include #include #include +#include #include #include @@ -29,11 +29,9 @@ nano::active_elections::active_elections (nano::node & node_a, nano::ledger_noti ledger_notifications{ ledger_notifications_a }, cementing_set{ cementing_set_a }, recently_confirmed{ config.confirmation_cache }, - recently_cemented{ config.confirmation_history_size }, + recently_cemented{ config.confirmation_cache }, workers{ 1, nano::thread_role::name::aec_notifications } { - count_by_behavior.fill (0); // Zero initialize array - // Cementing blocks might implicitly confirm dependent elections cementing_set.batch_cemented.add ([this] (auto const & cemented) { std::deque results; @@ -55,9 +53,20 @@ nano::active_elections::active_elections (nano::node & node_a, nano::ledger_noti // Notify observers about cemented blocks on a background thread workers.post ([this, results = std::move (results)] () { auto transaction = node.ledger.tx_begin_read (); - for (auto const & [status, votes] : results) + for (auto const & [election, status, votes] : results) { transaction.refresh_if_needed (); + + // Dependent elections are cancelled when their block is cemented + if (election) + { + bool cancelled = election->cancel (); + if (cancelled) + { + node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::cancel_dependent); + } + } + notify_observers (transaction, status, votes); } }); @@ -107,6 +116,11 @@ void nano::active_elections::start () nano::thread_role::set (nano::thread_role::name::aec_loop); run (); }); + + checkup_thread = std::thread ([this] () { + nano::thread_role::set (nano::thread_role::name::aec_checkup); + run_checkup (); + }); } void nano::active_elections::stop () @@ -116,35 +130,273 @@ void nano::active_elections::stop () stopped = true; } condition.notify_all (); - if (thread.joinable ()) - { - thread.join (); - } + join_or_pass (thread); + join_or_pass (checkup_thread); workers.stop (); clear (); } -void nano::active_elections::run () +auto nano::active_elections::insert (std::shared_ptr const & block, nano::election_behavior behavior, nano::bucket_index bucket, nano::priority_timestamp priority, erased_callback_t erased_callback) -> insert_result +{ + release_assert (block); + release_assert (block->has_sideband ()); + + nano::unique_lock lock{ mutex }; + + insert_result result{ nullptr, false }; + + if (stopped) + { + return result; + } + + auto const root = block->qualified_root (); + auto const hash = block->hash (); + + if (!index.exists (root)) + { + if (!recently_confirmed.contains (root) && !recently_cemented.contains (root)) + { + result.inserted = true; + + // Passing this callback into the election is important + // We need to observe and update the online voting weight *before* election quorum is checked + auto observe_rep_action = [&node = node] (auto const & rep) { + node.online_reps.observe (rep); + }; + + // On any election state update, schedule a call to tick it immediately + auto update_action = [this] (auto const & root) { + trigger (root); + }; + + result.election = std::make_shared (node, block, behavior, nullptr, observe_rep_action, update_action); + + // Store erased callback if provided + if (erased_callback) + { + erased_callbacks[root] = std::move (erased_callback); + } + + // Insert the election into index + index.insert (result.election, behavior, bucket, priority); + + node.vote_router.connect (hash, result.election); + + node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::started); + node.stats.inc (nano::stat::type::active_elections_started, to_stat_detail (behavior)); + + node.logger.trace (nano::log::type::active_elections, nano::log::detail::active_started, + nano::log::arg{ "behavior", behavior }, + nano::log::arg{ "election", result.election }); + + node.logger.debug (nano::log::type::active_elections, "Started new election for root: {} with blocks: {} (behavior: {})", + root, + fmt::join (result.election->blocks_hashes (), ", "), // TODO: Lazy eval + to_string (behavior)); + + // Notify observers that a new election started (while still holding the lock to avoid races) + election_started.notify (result.election, bucket, priority); + } + else + { + // Result is not set + } + } + else + { + result.election = index.election (root); + + // Upgrade to priority election to enable immediate vote broadcasting. + auto previous_behavior = result.election->behavior (); + if (behavior == nano::election_behavior::priority && previous_behavior != nano::election_behavior::priority) + { + bool transitioned = result.election->transition_priority (); + if (transitioned) + { + index.update (result.election, behavior); + node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::transition_priority); + + // Notify observers that this election is now priority (same as election_started) + election_started.notify (result.election, bucket, priority); + } + else + { + node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::transition_priority_failed); + } + } + } + + lock.unlock (); + + if (result.inserted) + { + release_assert (result.election); + + auto should_activate_immediately = [&] () { + // Skip passive phase for blocks without cached votes to avoid bootstrap delays + if (!node.vote_cache.contains (hash)) + { + return true; + } + return false; + }; + + // Transition to active (broadcasting votes, sending confirm reqs) state if needed + bool activate_immediately = should_activate_immediately (); + if (activate_immediately) + { + node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::activate_immediately); + result.election->transition_active (); + } + + // Notifications + node.observers.active_started.notify (hash); + vacancy_updated.notify (); + + // Let the election know about already observed votes + node.vote_cache_processor.trigger (hash); + + // Let the election know about already observed forks + auto forks = node.fork_cache.get (root); + node.stats.add (nano::stat::type::active_elections, nano::stat::detail::forks_cached, forks.size ()); + for (auto const & fork : forks) + { + publish (fork); + } + } + + // Votes are generated for inserted or ongoing elections + if (result.election) + { + result.election->broadcast_vote (); + } + + return result; +} + +bool nano::active_elections::publish (std::shared_ptr const & block) { nano::unique_lock lock{ mutex }; - while (!stopped) + + if (auto election = index.election (block->qualified_root ())) { - auto const stamp = std::chrono::steady_clock::now (); + lock.unlock (); - node.stats.inc (nano::stat::type::active, nano::stat::detail::loop); + bool result = election->publish (block); // false => new block was added + if (!result) + { + node.vote_router.connect (block->hash (), election); + node.vote_cache_processor.trigger (block->hash ()); - tick_elections (lock); - debug_assert (!lock.owns_lock ()); - lock.lock (); + node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::fork); - auto const min_sleep = node.network_params.network.aec_loop_interval / 2; - auto const wakeup = std::max (stamp + node.network_params.network.aec_loop_interval, std::chrono::steady_clock::now () + min_sleep); + node.logger.debug (nano::log::type::active_elections, "Block was added to an existing election: {} with root: {} (behavior: {}, state: {}, voters: {}, blocks: {}, duration: {}ms)", + block->hash (), + election->qualified_root, + to_string (election->behavior ()), + to_string (election->state ()), + election->voter_count (), + election->block_count (), + election->duration ().count ()); - condition.wait_until (lock, wakeup, [this, wakeup] { - return stopped || std::chrono::steady_clock::now () >= wakeup; - }); + return false; // Added + } } + + return true; // Not added +} + +void nano::active_elections::erase_election (nano::unique_lock & lock, std::shared_ptr election) +{ + debug_assert (!mutex.try_lock ()); + debug_assert (lock.owns_lock ()); + debug_assert (!election->confirmed () || recently_confirmed.contains (election->qualified_root)); + + auto blocks_l = election->blocks (); + node.vote_router.disconnect (*election); + + // Erase from index + bool erased = index.erase (election); + release_assert (erased); + + // Get and remove the erased callback + auto callback_it = erased_callbacks.find (election->qualified_root); + erased_callback_t erased_callback; + if (callback_it != erased_callbacks.end ()) + { + erased_callback = std::move (callback_it->second); + erased_callbacks.erase (callback_it); + } + + node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::stopped); + node.stats.inc (nano::stat::type::active_elections, election->confirmed () ? nano::stat::detail::confirmed : nano::stat::detail::unconfirmed); + node.stats.inc (nano::stat::type::active_elections_stopped, to_stat_detail (election->state ())); + node.stats.inc (to_stat_type (election->state ()), to_stat_detail (election->behavior ())); + + node.logger.trace (nano::log::type::active_elections, nano::log::detail::active_stopped, nano::log::arg{ "election", election }); + + node.logger.debug (nano::log::type::active_elections, "Erased election for root: {} with blocks: {} (behavior: {}, state: {}, voters: {}, blocks: {}, duration: {}ms)", + election->qualified_root, + fmt::join (election->blocks_hashes (), ", "), // TODO: Lazy eval + to_string (election->behavior ()), + to_string (election->state ()), + election->voter_count (), + election->block_count (), + election->duration ().count ()); + + lock.unlock (); + + // Track election duration + node.stats.sample (nano::stat::sample::active_election_duration, election->duration ().count (), { 0, 1000 * 60 * 10 /* 0-10 minutes range */ }); + + // Notify observers without holding the lock + if (erased_callback) + { + erased_callback (election); + } + + // Notify observers that the election was erased + election_erased.notify (election); + + vacancy_updated.notify (); + + for (auto const & [hash, block] : blocks_l) + { + // Notify observers about dropped elections & blocks lost confirmed elections + if (!election->confirmed () || hash != election->winner ()->hash ()) + { + node.observers.active_stopped.notify (hash); + } + + if (!election->confirmed ()) + { + // Clear from publish filter + node.network.filter.clear (block); + } + } +} + +bool nano::active_elections::erase (nano::qualified_root const & root) +{ + nano::unique_lock lock{ mutex }; + + if (auto election = index.election (root)) + { + release_assert (election->qualified_root == root); + erase_election (lock, election); + return true; + } + else + { + return false; + } +} + +bool nano::active_elections::erase (nano::block const & block) +{ + return erase (block.qualified_root ()); } auto nano::active_elections::block_cemented (std::shared_ptr const & block, nano::block_hash const & confirmation_root, std::shared_ptr const & source_election) -> block_cemented_result @@ -153,12 +405,7 @@ auto nano::active_elections::block_cemented (std::shared_ptr const debug_assert (node.block_confirmed (block->hash ())); // Dependent elections are implicitly confirmed when their block is cemented - auto dependend_election = election_impl (block->qualified_root ()); - if (dependend_election) - { - node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::confirm_dependent); - dependend_election->try_confirm (block->hash ()); // TODO: This should either confirm or cancel the election - } + auto election = election_impl (block->qualified_root ()); nano::election_status status; std::vector votes; @@ -172,7 +419,7 @@ auto nano::active_elections::block_cemented (std::shared_ptr const votes = source_election->votes_with_weight (); status.type = nano::election_status_type::active_confirmed_quorum; } - else if (dependend_election) + else if (election) { status.type = nano::election_status_type::active_confirmation_height; } @@ -186,12 +433,17 @@ auto nano::active_elections::block_cemented (std::shared_ptr const node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::cemented); node.stats.inc (nano::stat::type::active_elections_cemented, to_stat_detail (status.type)); + node.logger.debug (nano::log::type::active_elections, "Cemented root: {} with block: {} (status: {})", + block->qualified_root (), + block->hash (), + to_string (status.type)); + node.logger.trace (nano::log::type::active_elections, nano::log::detail::active_cemented, nano::log::arg{ "block", block }, nano::log::arg{ "confirmation_root", confirmation_root }, nano::log::arg{ "source_election", source_election }); - return { status, votes }; + return { election, status, votes }; } void nano::active_elections::notify_observers (nano::secure::transaction const & transaction, nano::election_status const & status, std::vector const & votes) const @@ -232,6 +484,182 @@ void nano::active_elections::notify_observers (nano::secure::transaction const & } } +bool nano::active_elections::trigger (nano::qualified_root const & root) +{ + bool triggered = false; + { + nano::lock_guard guard{ mutex }; + if (auto election = index.election (root)) + { + triggered = index.trigger (election); + } + } + if (triggered) + { + condition.notify_all (); + } + return triggered; +} + +void nano::active_elections::tick_elections (nano::unique_lock & lock) +{ + debug_assert (lock.owns_lock ()); + + auto const now = std::chrono::steady_clock::now (); + auto const cutoff = now - node.network_params.network.aec_loop_interval; + auto const election_list = index.list (cutoff, now); + debug_assert (!election_list.empty ()); // Shouldn't be called if there are no elections to process + + lock.unlock (); + + nano::confirmation_solicitor solicitor (node.network, node.config); + solicitor.prepare (node.rep_crawler.principal_representatives (std::numeric_limits::max ())); + + for (auto const & election : election_list) + { + bool tick_result = election->tick (solicitor); + if (tick_result) + { + erase (election->qualified_root); + } + } + + solicitor.flush (); +} + +bool nano::active_elections::predicate () const +{ + debug_assert (!mutex.try_lock ()); + + auto cutoff = std::chrono::steady_clock::now () - node.network_params.network.aec_loop_interval; + return index.any (cutoff); +} + +void nano::active_elections::run () +{ + nano::unique_lock lock{ mutex }; + while (!stopped) + { + if (predicate ()) + { + node.stats.inc (nano::stat::type::active, nano::stat::detail::loop); + + tick_elections (lock); + debug_assert (!lock.owns_lock ()); + lock.lock (); + } + else + { + condition.wait_for (lock, node.network_params.network.aec_loop_interval / 2, [this] { + return stopped || predicate (); + }); + } + } +} + +void nano::active_elections::checkup_elections (nano::unique_lock & lock) +{ + auto all_elections = index.list (); + + lock.unlock (); + + auto transaction = node.ledger.tx_begin_read (); + + auto should_cancel = [&] (std::shared_ptr const & election) { + auto const target = node.ledger.any.block_successor (transaction, election->qualified_root); + if (target) + { + // Cancel if the election's block is already cemented + return node.ledger.confirmed.block_exists (transaction, *target); + } + else + { + // No successor means the block is not in the ledger, rather unexpected + return true; // Cancel the election + } + }; + + auto const now = std::chrono::steady_clock::now (); + auto const min_duration = node.network_params.network.aec_loop_interval * 3; + + std::deque> stale_elections; + + for (auto const & election : all_elections) + { + // Only cancel elections if they have been running for a minimum duration of time + // Usually the normal cemented callback will handle the cleanup + if ((now - election->get_state_start ()) > min_duration && should_cancel (election)) + { + bool cancelled = election->cancel (); + if (cancelled) + { + node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::cancel_checkup); + node.logger.debug (nano::log::type::active_elections, "Checkup cancelled election for root: {} with blocks: {} (behavior: {}, state: {}, voters: {}, blocks: {}, duration: {}ms)", + election->qualified_root, + fmt::join (election->blocks_hashes (), ", "), // TODO: Lazy eval + to_string (election->behavior ()), + to_string (election->state ()), + election->voter_count (), + election->block_count (), + election->duration ().count ()); + } + } + else if (election->duration () > config.stale_threshold) + { + stale_elections.push_back (election); + } + } + + node.logger.debug (nano::log::type::active_elections, "Checkup found {} stale elections", stale_elections.size ()); + + // Notify about stale elections at most once per half stale threshold, avoid too frequent notifications + if (stale_interval.elapse (config.stale_threshold / 2)) + { + node.stats.add (nano::stat::type::active_elections, nano::stat::detail::stale, stale_elections.size ()); + + for (auto const & election : stale_elections) + { + node.logger.debug (nano::log::type::active_elections, "Stale election for account: {} with root: {} blocks: {} (behavior: {}, state: {}, voters: {}, blocks: {}, duration: {}ms)", + election->account, + election->qualified_root, + fmt::join (election->blocks_hashes (), ", "), // TODO: Lazy eval + to_string (election->behavior ()), + to_string (election->state ()), + election->voter_count (), + election->block_count (), + election->duration ().count ()); + + election_stale.notify (election); + } + } +} + +void nano::active_elections::run_checkup () +{ + nano::unique_lock lock{ mutex }; + while (!stopped) + { + // Ignore predicate in condition, this loop should be woken up periodically + condition.wait_for (lock, config.checkup_interval, [this] { + return stopped; + }); + + if (stopped) + { + return; + } + + if (!index.empty ()) + { + node.stats.inc (nano::stat::type::active, nano::stat::detail::loop_checkup); + + checkup_elections (lock); + debug_assert (!lock.owns_lock ()); + lock.lock (); + } + } +} + int64_t nano::active_elections::limit (nano::election_behavior behavior) const { switch (behavior) @@ -269,10 +697,10 @@ int64_t nano::active_elections::vacancy (nano::election_behavior behavior) const case nano::election_behavior::manual: return std::numeric_limits::max (); case nano::election_behavior::priority: - return limit (nano::election_behavior::priority) - static_cast (roots.size ()); + return limit (nano::election_behavior::priority) - static_cast (index.size ()); case nano::election_behavior::hinted: case nano::election_behavior::optimistic: - return limit (behavior) - count_by_behavior[behavior]; + return limit (behavior) - static_cast (index.size (behavior)); } debug_assert (false); // Unknown enum return 0; @@ -285,119 +713,6 @@ int64_t nano::active_elections::vacancy (nano::election_behavior behavior) const return std::min (election_vacancy (behavior), election_winners_vacancy ()); } -void nano::active_elections::tick_elections (nano::unique_lock & lock) -{ - debug_assert (lock.owns_lock ()); - - auto const election_list = list_active_impl (); - - lock.unlock (); - - nano::confirmation_solicitor solicitor (node.network, node.config); - solicitor.prepare (node.rep_crawler.principal_representatives (std::numeric_limits::max ())); - - nano::timer elapsed (nano::timer_state::started); - - std::deque> stale_elections; - for (auto const & election : election_list) - { - if (election->transition_time (solicitor)) - { - erase (election->qualified_root); - } - else if (election->duration () > config.bootstrap_stale_threshold) - { - stale_elections.push_back (election); - } - } - - solicitor.flush (); - - if (bootstrap_stale_interval.elapse (config.bootstrap_stale_threshold / 2)) - { - node.stats.add (nano::stat::type::active_elections, nano::stat::detail::bootstrap_stale, stale_elections.size ()); - - for (auto const & election : stale_elections) - { - node.logger.debug (nano::log::type::active_elections, "Bootstrapping account: {} with stale election with root: {}, blocks: {} (behavior: {}, state: {}, voters: {}, blocks: {}, duration: {}ms)", - election->account, - election->qualified_root, - fmt::join (election->blocks_hashes (), ", "), // TODO: Lazy eval - to_string (election->behavior ()), - to_string (election->state ()), - election->voter_count (), - election->block_count (), - election->duration ().count ()); - - node.bootstrap.prioritize (election->account); - } - } -} - -void nano::active_elections::cleanup_election (nano::unique_lock & lock_a, std::shared_ptr election) -{ - debug_assert (!mutex.try_lock ()); - debug_assert (lock_a.owns_lock ()); - debug_assert (!election->confirmed () || recently_confirmed.exists (election->qualified_root)); - - // Keep track of election count by election type - release_assert (count_by_behavior[election->behavior ()] > 0); - count_by_behavior[election->behavior ()]--; - - auto blocks_l = election->blocks (); - node.vote_router.disconnect (*election); - - // Erase root info - auto it = roots.get ().find (election->qualified_root); - release_assert (it != roots.get ().end ()); - entry entry = *it; - roots.get ().erase (it); - - node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::stopped); - node.stats.inc (nano::stat::type::active_elections, election->confirmed () ? nano::stat::detail::confirmed : nano::stat::detail::unconfirmed); - node.stats.inc (nano::stat::type::active_elections_stopped, to_stat_detail (election->state ())); - node.stats.inc (to_stat_type (election->state ()), to_stat_detail (election->behavior ())); - - node.logger.trace (nano::log::type::active_elections, nano::log::detail::active_stopped, nano::log::arg{ "election", election }); - - node.logger.debug (nano::log::type::active_elections, "Erased election for root: {} with blocks: {} (behavior: {}, state: {}, voters: {}, blocks: {}, duration: {}ms)", - election->qualified_root, - fmt::join (election->blocks_hashes (), ", "), // TODO: Lazy eval - to_string (election->behavior ()), - to_string (election->state ()), - election->voter_count (), - election->block_count (), - election->duration ().count ()); - - lock_a.unlock (); - - // Track election duration - node.stats.sample (nano::stat::sample::active_election_duration, election->duration ().count (), { 0, 1000 * 60 * 10 /* 0-10 minutes range */ }); - - // Notify observers without holding the lock - if (entry.erased_callback) - { - entry.erased_callback (election); - } - - vacancy_updated.notify (); - - for (auto const & [hash, block] : blocks_l) - { - // Notify observers about dropped elections & blocks lost confirmed elections - if (!election->confirmed () || hash != election->winner ()->hash ()) - { - node.observers.active_stopped.notify (hash); - } - - if (!election->confirmed ()) - { - // Clear from publish filter - node.network.filter.clear (block); - } - } -} - std::vector> nano::active_elections::list_active (std::size_t max_count) { nano::lock_guard guard{ mutex }; @@ -407,149 +722,29 @@ std::vector> nano::active_elections::list_active std::vector> nano::active_elections::list_active_impl (std::size_t max_count) const { std::vector> result_l; - result_l.reserve (std::min (max_count, roots.size ())); + auto entries = index.list (); + result_l.reserve (std::min (max_count, entries.size ())); + for (auto const & entry : entries) { - auto & sorted_roots_l (roots.get ()); - for (auto i = sorted_roots_l.begin (), n = sorted_roots_l.end (); i != n && result_l.size () < max_count; ++i) + if (result_l.size () >= max_count) { - result_l.push_back (i->election); + break; } + result_l.push_back (entry); } return result_l; } -nano::election_insertion_result nano::active_elections::insert (std::shared_ptr const & block_a, nano::election_behavior election_behavior_a, erased_callback_t erased_callback_a) -{ - release_assert (block_a); - release_assert (block_a->has_sideband ()); - - nano::unique_lock lock{ mutex }; - - nano::election_insertion_result result; - - if (stopped) - { - return result; - } - - auto const root = block_a->qualified_root (); - auto const hash = block_a->hash (); - - if (auto existing = roots.get ().find (root); existing == roots.get ().end ()) - { - if (!recently_confirmed.exists (root)) - { - result.inserted = true; - - // Passing this callback into the election is important - // We need to observe and update the online voting weight *before* election quorum is checked - auto observe_rep_callback = [&node = node] (auto const & rep_a) { - node.online_reps.observe (rep_a); - }; - result.election = nano::make_shared (node, block_a, nullptr, observe_rep_callback, election_behavior_a); - - roots.get ().emplace (entry{ root, result.election, std::move (erased_callback_a) }); - node.vote_router.connect (hash, result.election); - - // Keep track of election count by election type - release_assert (count_by_behavior[result.election->behavior ()] >= 0); - count_by_behavior[result.election->behavior ()]++; - - // Skip passive phase for blocks without cached votes to avoid bootstrap delays - bool activate_immediately = false; - if (!node.vote_cache.contains (hash)) - { - activate_immediately = true; - } - - if (activate_immediately) - { - node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::activate_immediately); - result.election->transition_active (); - } - - node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::started); - node.stats.inc (nano::stat::type::active_elections_started, to_stat_detail (election_behavior_a)); - - node.logger.trace (nano::log::type::active_elections, nano::log::detail::active_started, - nano::log::arg{ "behavior", election_behavior_a }, - nano::log::arg{ "election", result.election }); - - node.logger.debug (nano::log::type::active_elections, "Started new election for root: {} with blocks: {} (behavior: {}, active immediately: {})", - root, - fmt::join (result.election->blocks_hashes (), ", "), // TODO: Lazy eval - to_string (election_behavior_a), - activate_immediately); - } - else - { - // result is not set - } - } - else - { - result.election = existing->election; - - // Upgrade to priority election to enable immediate vote broadcasting. - auto previous_behavior = result.election->behavior (); - if (election_behavior_a == nano::election_behavior::priority && previous_behavior != nano::election_behavior::priority) - { - bool transitioned = result.election->transition_priority (); - if (transitioned) - { - count_by_behavior[previous_behavior]--; - count_by_behavior[election_behavior_a]++; - node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::transition_priority); - } - else - { - node.stats.inc (nano::stat::type::active_elections, nano::stat::detail::transition_priority_failed); - } - } - } - - lock.unlock (); - - if (result.inserted) - { - release_assert (result.election); - - // Notifications - node.observers.active_started.notify (hash); - vacancy_updated.notify (); - - // Let the election know about already observed votes - node.vote_cache_processor.trigger (hash); - - // Let the election know about already observed forks - auto forks = node.fork_cache.get (root); - node.stats.add (nano::stat::type::active_elections, nano::stat::detail::forks_cached, forks.size ()); - - for (auto const & fork : forks) - { - publish (fork); - } - } - - // Votes are generated for inserted or ongoing elections - if (result.election) - { - result.election->broadcast_vote (); - } - - return result; -} - bool nano::active_elections::active (nano::qualified_root const & root_a) const { nano::lock_guard lock{ mutex }; - return roots.get ().find (root_a) != roots.get ().end (); + return index.exists (root_a); } bool nano::active_elections::active (nano::block const & block_a) const { nano::lock_guard guard{ mutex }; - return roots.get ().find (block_a.qualified_root ()) != roots.get ().end (); + return index.exists (block_a.qualified_root ()); } std::shared_ptr nano::active_elections::election (nano::qualified_root const & root) const @@ -561,84 +756,31 @@ std::shared_ptr nano::active_elections::election (nano::qualifie std::shared_ptr nano::active_elections::election_impl (nano::qualified_root const & root) const { debug_assert (!mutex.try_lock ()); - std::shared_ptr result; - auto existing = roots.get ().find (root); - if (existing != roots.get ().end ()) - { - result = existing->election; - } - return result; -} - -bool nano::active_elections::erase (nano::block const & block_a) -{ - return erase (block_a.qualified_root ()); -} - -bool nano::active_elections::erase (nano::qualified_root const & root_a) -{ - nano::unique_lock lock{ mutex }; - auto root_it (roots.get ().find (root_a)); - if (root_it != roots.get ().end ()) - { - release_assert (root_it->election->qualified_root == root_a); - cleanup_election (lock, root_it->election); - return true; - } - return false; + return index.election (root); } bool nano::active_elections::empty () const { nano::lock_guard lock{ mutex }; - return roots.empty (); + return index.size () == 0; } std::size_t nano::active_elections::size () const { nano::lock_guard lock{ mutex }; - return roots.size (); + return index.size (); } std::size_t nano::active_elections::size (nano::election_behavior behavior) const { nano::lock_guard lock{ mutex }; - auto count = count_by_behavior[behavior]; - debug_assert (count >= 0); - return static_cast (count); + return index.size (behavior); } -bool nano::active_elections::publish (std::shared_ptr const & block_a) +std::size_t nano::active_elections::size (nano::election_behavior behavior, nano::bucket_index bucket) const { - nano::unique_lock lock{ mutex }; - auto existing (roots.get ().find (block_a->qualified_root ())); - auto result (true); - if (existing != roots.get ().end ()) - { - auto election (existing->election); - lock.unlock (); - result = election->publish (block_a); - if (!result) - { - lock.lock (); - node.vote_router.connect (block_a->hash (), election); - lock.unlock (); - - node.vote_cache_processor.trigger (block_a->hash ()); - - node.stats.inc (nano::stat::type::active, nano::stat::detail::election_block_conflict); - - node.logger.debug (nano::log::type::active_elections, "Block was added to an existing election: {} with root: {} (behavior: {}, state: {}, voters: {}, blocks: {}, duration: {}ms)", - block_a->hash (), - election->qualified_root, - to_string (election->behavior ()), - to_string (election->state ()), - election->voter_count (), - election->block_count (), - election->duration ().count ()); - } - } - return result; + nano::lock_guard lock{ mutex }; + return index.size (behavior, bucket); } void nano::active_elections::clear () @@ -646,7 +788,8 @@ void nano::active_elections::clear () // TODO: Call erased_callback for each election { nano::lock_guard guard{ mutex }; - roots.clear (); + index.clear (); + erased_callbacks.clear (); } vacancy_updated.notify (); } @@ -656,10 +799,10 @@ nano::container_info nano::active_elections::container_info () const nano::lock_guard guard{ mutex }; nano::container_info info; - info.put ("roots", roots.size ()); - info.put ("normal", static_cast (count_by_behavior[nano::election_behavior::priority])); - info.put ("hinted", static_cast (count_by_behavior[nano::election_behavior::hinted])); - info.put ("optimistic", static_cast (count_by_behavior[nano::election_behavior::optimistic])); + info.put ("roots", index.size ()); + info.put ("normal", index.size (nano::election_behavior::priority)); + info.put ("hinted", index.size (nano::election_behavior::hinted)); + info.put ("optimistic", index.size (nano::election_behavior::optimistic)); info.add ("recently_confirmed", recently_confirmed.container_info ()); info.add ("recently_cemented", recently_cemented.container_info ()); @@ -681,10 +824,9 @@ nano::error nano::active_elections_config::serialize (nano::tomlconfig & toml) c toml.put ("size", size, "Number of active elections. Elections beyond this limit have limited survival time.\nWarning: modifying this value may result in a lower confirmation rate. \ntype:uint64,[250..]"); toml.put ("hinted_limit_percentage", hinted_limit_percentage, "Limit of hinted elections as percentage of `active_elections_size` \ntype:uint64"); toml.put ("optimistic_limit_percentage", optimistic_limit_percentage, "Limit of optimistic elections as percentage of `active_elections_size`. \ntype:uint64"); - toml.put ("confirmation_history_size", confirmation_history_size, "Maximum confirmation history size. If tracking the rate of block confirmations, the websocket feature is recommended instead. \ntype:uint64"); toml.put ("confirmation_cache", confirmation_cache, "Maximum number of confirmed elections kept in cache to prevent restarting an election. \ntype:uint64"); toml.put ("max_election_winners", max_election_winners, "Maximum size of election winner details set. \ntype:uint64"); - toml.put ("bootstrap_stale_threshold", bootstrap_stale_threshold.count (), "Time after which additional bootstrap attempts are made to find missing blocks for an election. \ntype:seconds"); + toml.put ("stale_threshold", stale_threshold.count (), "Time after which additional bootstrap attempts are made to find missing blocks for an election. \ntype:seconds"); return toml.get_error (); } @@ -693,10 +835,9 @@ nano::error nano::active_elections_config::deserialize (nano::tomlconfig & toml) toml.get ("size", size); toml.get ("hinted_limit_percentage", hinted_limit_percentage); toml.get ("optimistic_limit_percentage", optimistic_limit_percentage); - toml.get ("confirmation_history_size", confirmation_history_size); toml.get ("confirmation_cache", confirmation_cache); toml.get ("max_election_winners", max_election_winners); - toml.get_duration ("bootstrap_stale_threshold", bootstrap_stale_threshold); + toml.get_duration ("stale_threshold", stale_threshold); return toml.get_error (); } @@ -728,6 +869,11 @@ nano::stat::type nano::to_stat_type (nano::election_state state) return {}; } +std::string_view nano::to_string (nano::election_status_type type) +{ + return nano::enum_util::name (type); +} + nano::stat::detail nano::to_stat_detail (nano::election_status_type type) { return nano::enum_util::cast (type); diff --git a/nano/node/active_elections.hpp b/nano/node/active_elections.hpp index e8416bdd1..526c2e772 100644 --- a/nano/node/active_elections.hpp +++ b/nano/node/active_elections.hpp @@ -1,12 +1,11 @@ #pragma once -#include #include #include #include #include +#include #include -#include #include #include #include @@ -15,20 +14,12 @@ #include #include -#include -#include -#include -#include -#include - #include #include #include #include #include -namespace mi = boost::multi_index; - namespace nano { class active_elections_config final @@ -46,14 +37,13 @@ public: std::size_t hinted_limit_percentage{ 20 }; // Limit of optimistic elections as percentage of `active_elections_size` std::size_t optimistic_limit_percentage{ 10 }; - // Maximum confirmation history size - std::size_t confirmation_history_size{ 2048 }; // Maximum cache size for recently_confirmed - std::size_t confirmation_cache{ 65536 }; + std::size_t confirmation_cache{ 1024 * 64 }; // Maximum size of election winner details set std::size_t max_election_winners{ 1024 * 16 }; - std::chrono::seconds bootstrap_stale_threshold{ 60s }; + std::chrono::milliseconds checkup_interval{ 1s }; + std::chrono::seconds stale_threshold{ nano::is_dev_run () ? 1s : 60s }; }; /** @@ -65,34 +55,6 @@ class active_elections final public: using erased_callback_t = std::function)>; -private: // Elections - class entry final - { - public: - nano::qualified_root root; - std::shared_ptr election; - erased_callback_t erased_callback; - }; - - friend class nano::election; - - // clang-format off - class tag_account {}; - class tag_root {}; - class tag_sequenced {}; - class tag_uncemented {}; - class tag_arrival {}; - class tag_hash {}; - - using ordered_roots = boost::multi_index_container>, - mi::hashed_unique, - mi::member> - >>; - // clang-format on - ordered_roots roots; - public: active_elections (nano::node &, nano::ledger_notifications &, nano::cementing_set &); ~active_elections (); @@ -100,46 +62,76 @@ public: void start (); void stop (); - /** - * Starts new election with a specified behavior type - */ - nano::election_insertion_result insert (std::shared_ptr const &, nano::election_behavior = nano::election_behavior::priority, erased_callback_t = nullptr); - // Is the root of this block in the roots container - bool active (nano::block const &) const; - bool active (nano::qualified_root const &) const; - std::shared_ptr election (nano::qualified_root const &) const; - // Returns a list of elections sorted by difficulty - std::vector> list_active (std::size_t max_count = std::numeric_limits::max ()); - bool erase (nano::block const &); - bool erase (nano::qualified_root const &); - bool empty () const; - std::size_t size () const; - std::size_t size (nano::election_behavior) const; + struct insert_result + { + std::shared_ptr election; + bool inserted; + }; + + /// Starts new election + insert_result insert ( + std::shared_ptr const &, + nano::election_behavior = nano::election_behavior::priority, + nano::bucket_index bucket = 0, + nano::priority_timestamp priority = 0, + erased_callback_t = nullptr); + + // Notify this container about a new block (potential fork) bool publish (std::shared_ptr const &); - /** - * Maximum number of elections that should be present in this container - * NOTE: This is only a soft limit, it is possible for this container to exceed this count - */ + // Trigger an immediate election update (e.g. after it is confirmed) + bool trigger (nano::qualified_root const &); + + /// Is the root of this block in the roots container + bool active (nano::block const &) const; + bool active (nano::qualified_root const &) const; + + std::shared_ptr election (nano::qualified_root const &) const; + + /// Returns a list of elections sorted by difficulty + std::vector> list_active (std::size_t max_count = std::numeric_limits::max ()); + + bool erase (nano::block const &); + bool erase (nano::qualified_root const &); + + bool empty () const; + + size_t size () const; + size_t size (nano::election_behavior) const; + size_t size (nano::election_behavior, nano::bucket_index) const; + + /// Maximum number of elections that should be present in this container + /// NOTE: This is only a soft limit, it is possible for this container to exceed this count int64_t limit (nano::election_behavior behavior) const; - /** - * How many election slots are available for specified election type - */ + + /// How many election slots are available for specified election type int64_t vacancy (nano::election_behavior behavior) const; nano::container_info container_info () const; public: // Events nano::observer_set<> vacancy_updated; + nano::observer_set, nano::bucket_index, nano::priority_timestamp> election_started; + nano::observer_set> election_erased; + nano::observer_set> election_stale; private: + bool predicate () const; void run (); + void run_checkup (); void tick_elections (nano::unique_lock &); + void checkup_elections (nano::unique_lock &); // Erase all blocks from active and, if not confirmed, clear digests from network filters - void cleanup_election (nano::unique_lock & lock_a, std::shared_ptr); + void erase_election (nano::unique_lock & lock_a, std::shared_ptr); + + struct block_cemented_result + { + std::shared_ptr election; + nano::election_status status; + std::vector votes; + }; - using block_cemented_result = std::pair>; block_cemented_result block_cemented (std::shared_ptr const & block, nano::block_hash const & confirmation_root, std::shared_ptr const & source_election); void notify_observers (nano::secure::transaction const &, nano::election_status const & status, std::vector const & votes) const; @@ -153,6 +145,10 @@ private: // Dependencies nano::cementing_set & cementing_set; public: + nano::active_elections_index index; + + std::unordered_map erased_callbacks; + nano::recently_confirmed_cache recently_confirmed; nano::recently_cemented_cache recently_cemented; @@ -161,34 +157,18 @@ public: mutable nano::mutex mutex{ mutex_identifier (mutexes::active) }; private: - /** Keeps track of number of elections by election behavior (normal, hinted, optimistic) */ - nano::enum_array count_by_behavior{}; - nano::condition_variable condition; bool stopped{ false }; std::thread thread; + std::thread checkup_thread; nano::thread_pool workers; - nano::interval bootstrap_stale_interval; + nano::interval stale_interval; nano::interval warning_interval; - friend class election; - public: // Tests void clear (); - - friend class node_fork_storm_Test; - friend class system_block_sequence_Test; - friend class node_mass_block_new_Test; - friend class active_elections_vote_replays_Test; - friend class frontiers_confirmation_prioritize_frontiers_Test; - friend class frontiers_confirmation_prioritize_frontiers_max_optimistic_elections_Test; - friend class confirmation_height_prioritize_frontiers_overwrite_Test; - friend class active_elections_confirmation_consistency_Test; - friend class node_deferred_dependent_elections_Test; - friend class active_elections_pessimistic_elections_Test; - friend class frontiers_confirmation_expired_optimistic_elections_removal_Test; }; nano::stat::type to_stat_type (nano::election_state); diff --git a/nano/node/active_elections_index.cpp b/nano/node/active_elections_index.cpp new file mode 100644 index 000000000..e919bc004 --- /dev/null +++ b/nano/node/active_elections_index.cpp @@ -0,0 +1,196 @@ +#include +#include + +#include + +void nano::active_elections_index::insert (std::shared_ptr const & election, nano::election_behavior behavior, nano::bucket_index bucket, nano::priority_timestamp priority) +{ + debug_assert (!entries.get ().contains (election)); + debug_assert (!entries.get ().contains (election->qualified_root)); + + auto [it, inserted] = entries.emplace_back (entry{ election, election->qualified_root, behavior, bucket, priority }); + debug_assert (inserted); + + // Update cached size + size_by_behavior[{ behavior }]++; + size_by_bucket[{ behavior, bucket }]++; +} + +bool nano::active_elections_index::erase (std::shared_ptr const & election) +{ + auto maybe_entry = info (election); + if (!maybe_entry) + { + return false; // Not found + } + auto entry = *maybe_entry; + + auto & index = entries.get (); + auto erased = index.erase (election); + release_assert (erased == 1); + + // Update cached size + size_by_behavior[{ entry.behavior }]--; + size_by_bucket[{ entry.behavior, entry.bucket }]--; + + return true; // Erased +} + +void nano::active_elections_index::update (std::shared_ptr const & election, nano::election_behavior behavior) +{ + auto & index = entries.get (); + auto existing = index.find (election); + if (existing != index.end ()) + { + auto old_behavior = existing->behavior; + auto bucket = existing->bucket; + + if (old_behavior != behavior) + { + // Update cached sizes + size_by_behavior[{ old_behavior }]--; + size_by_bucket[{ old_behavior, bucket }]--; + size_by_behavior[{ behavior }]++; + size_by_bucket[{ behavior, bucket }]++; + + // Update the entry + index.modify (existing, [behavior] (entry & e) { + e.behavior = behavior; + }); + } + } + else + { + debug_assert (false, "election not found in index"); + } +} + +bool nano::active_elections_index::exists (nano::qualified_root const & root) const +{ + return entries.get ().contains (root); +} + +bool nano::active_elections_index::exists (std::shared_ptr const & election) const +{ + return entries.get ().contains (election); +} + +std::shared_ptr nano::active_elections_index::election (nano::qualified_root const & root) const +{ + if (auto existing = entries.get ().find (root); existing != entries.get ().end ()) + { + return existing->election; + } + return nullptr; +} + +auto nano::active_elections_index::info (std::shared_ptr const & election) const -> std::optional +{ + if (auto existing = entries.get ().find (election); existing != entries.get ().end ()) + { + return *existing; + } + return std::nullopt; +} + +auto nano::active_elections_index::list () const -> std::deque> +{ + auto r = entries.get () | std::views::transform ([] (auto const & entry) { return entry.election; }); + return { r.begin (), r.end () }; +} + +bool nano::active_elections_index::empty () const +{ + return entries.empty (); +} + +size_t nano::active_elections_index::size () const +{ + return entries.size (); +} + +size_t nano::active_elections_index::size (nano::election_behavior behavior) const +{ + if (auto existing = size_by_behavior.find ({ behavior }); existing != size_by_behavior.end ()) + { + return existing->second; + } + return 0; +} + +size_t nano::active_elections_index::size (nano::election_behavior behavior, nano::bucket_index bucket) const +{ + if (auto existing = size_by_bucket.find ({ behavior, bucket }); existing != size_by_bucket.end ()) + { + return existing->second; + } + return 0; +} + +auto nano::active_elections_index::last (nano::election_behavior behavior, nano::bucket_index bucket) const -> priority_result +{ + auto & index = entries.get (); + + // Find the range of entries with matching behavior and bucket + auto range = index.equal_range (std::make_tuple (behavior, bucket)); + if (range.first != range.second) + { + // Since the index is ordered, the last element has the highest priority (largest value) + auto last = std::prev (range.second); + return { last->election, last->priority }; + } + + return { nullptr, std::numeric_limits::max () }; +} + +auto nano::active_elections_index::list (std::chrono::steady_clock::time_point cutoff, std::chrono::steady_clock::time_point now) -> std::deque> +{ + auto & index = entries.get (); + + // Collect entries to process first to avoid iterator invalidation issues + std::deque to_process; + auto end = index.upper_bound (cutoff); + for (auto it = index.begin (); it != end; ++it) + { + to_process.push_back (it); + } + + // Process and update timestamps + for (auto it : to_process) + { + // Update timestamp to 'now' for processed entries + index.modify (it, [now] (entry & e) { + e.timestamp = now; + }); + } + + auto r = to_process | std::views::transform ([] (auto const & it) { return it->election; }); + return { r.begin (), r.end () }; +} + +bool nano::active_elections_index::trigger (std::shared_ptr const & election) +{ + auto & index = entries.get (); + if (auto existing = index.find (election); existing != index.end ()) + { + index.modify (existing, [] (entry & e) { + e.timestamp = {}; + }); + return true; + } + return false; // Not found +} + +bool nano::active_elections_index::any (std::chrono::steady_clock::time_point cutoff) const +{ + auto & index = entries.get (); + auto it = index.begin (); + return it != index.end () && it->timestamp <= cutoff; +} + +void nano::active_elections_index::clear () +{ + entries.clear (); + size_by_behavior.clear (); + size_by_bucket.clear (); +} \ No newline at end of file diff --git a/nano/node/active_elections_index.hpp b/nano/node/active_elections_index.hpp new file mode 100644 index 000000000..80e9dbfb3 --- /dev/null +++ b/nano/node/active_elections_index.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace mi = boost::multi_index; + +namespace nano +{ +class active_elections_index +{ +public: + struct entry + { + std::shared_ptr election; + + nano::qualified_root root; + nano::election_behavior behavior; + nano::bucket_index bucket; + nano::priority_timestamp priority; + + std::chrono::steady_clock::time_point timestamp{}; + }; + +public: + void insert (std::shared_ptr const &, nano::election_behavior, nano::bucket_index, nano::priority_timestamp); + bool erase (std::shared_ptr const &); + void update (std::shared_ptr const &, nano::election_behavior); + + bool exists (nano::qualified_root const &) const; + bool exists (std::shared_ptr const &) const; + + std::shared_ptr election (nano::qualified_root const &) const; + std::optional info (std::shared_ptr const &) const; + + size_t size () const; + size_t size (nano::election_behavior) const; + size_t size (nano::election_behavior, nano::bucket_index) const; + bool empty () const; + + // Returns election with the highest priority value. NOTE: Lower "priority" is better + using priority_result = std::pair, nano::priority_timestamp>; + priority_result last (nano::election_behavior, nano::bucket_index) const; + + std::deque> list () const; + + // Return list of elections with a timestamp before the specified cutoff time + std::deque> list ( + std::chrono::steady_clock::time_point cutoff, + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now ()); + + // Mark an election for update (reset its timestamp) + bool trigger (std::shared_ptr const &); + + // Are there any elections with a timestamp before the specified cutoff time + bool any (std::chrono::steady_clock::time_point cutoff) const; + + void clear (); + +private: + // clang-format off + class tag_sequenced {}; + class tag_root {}; + class tag_ptr {}; + class tag_key {}; + class tag_timestamp {}; + + using ordered_entries = boost::multi_index_container>, + mi::hashed_unique, + mi::member>, + mi::hashed_unique, + mi::member, &entry::election>>, + mi::ordered_non_unique, + mi::composite_key, + mi::member, + mi::member>>, + mi::ordered_non_unique, + mi::member> + >>; + // clang-format on + ordered_entries entries; + + // Keep track of the total number of elections to provide constant time lookups + using behavior_key_t = nano::election_behavior; + std::map size_by_behavior; + + using bucket_key_t = std::pair; + std::map size_by_bucket; +}; +} diff --git a/nano/node/block_processor.cpp b/nano/node/block_processor.cpp index 30df420f6..4385a2ae4 100644 --- a/nano/node/block_processor.cpp +++ b/nano/node/block_processor.cpp @@ -49,7 +49,6 @@ nano::block_processor::block_processor (nano::node_config const & node_config_a, return config.priority_live; case nano::block_source::bootstrap: case nano::block_source::bootstrap_legacy: - case nano::block_source::unchecked: return config.priority_bootstrap; case nano::block_source::local: return config.priority_local; @@ -278,7 +277,7 @@ void nano::block_processor::run () // It's possible that ledger processing happens faster than the notifications can be processed by other components, cooldown here ledger_notifications.wait ([this] { stats.inc (nano::stat::type::block_processor, nano::stat::detail::cooldown); - if (log_cooldown_interval.elapse (15s)) + if (log_cooldown_interval.elapse (nano::is_dev_run () ? 1s : 15s)) { logger.warn (nano::log::type::block_processor, "Cooldown in block processing, waiting for remaining ledger notifications to be processed"); } diff --git a/nano/node/block_source.hpp b/nano/node/block_source.hpp index 5fe23e5d0..95acdb209 100644 --- a/nano/node/block_source.hpp +++ b/nano/node/block_source.hpp @@ -17,6 +17,7 @@ enum class block_source local, forced, election, + test, }; std::string_view to_string (block_source); diff --git a/nano/node/bounded_backlog.cpp b/nano/node/bounded_backlog.cpp index b50ffd26b..61fac47b0 100644 --- a/nano/node/bounded_backlog.cpp +++ b/nano/node/bounded_backlog.cpp @@ -274,7 +274,7 @@ bool nano::bounded_backlog::should_rollback (nano::block_hash const & hash) cons { return false; } - if (node.active.recently_confirmed.exists (hash)) + if (node.active.recently_confirmed.contains (hash)) { return false; } diff --git a/nano/node/cementing_set.cpp b/nano/node/cementing_set.cpp index fe49da854..a3eda2fb4 100644 --- a/nano/node/cementing_set.cpp +++ b/nano/node/cementing_set.cpp @@ -54,7 +54,7 @@ nano::cementing_set::~cementing_set () debug_assert (!thread.joinable ()); } -void nano::cementing_set::add (nano::block_hash const & hash, std::shared_ptr const & election) +bool nano::cementing_set::add (nano::block_hash const & hash, std::shared_ptr const & election) { bool added = false; { @@ -71,6 +71,7 @@ void nano::cementing_set::add (nano::block_hash const & hash, std::shared_ptr const & election = nullptr); + bool add (nano::block_hash const & hash, std::shared_ptr const & election = nullptr); + // Added blocks will remain in this set until after ledger has them marked as confirmed. bool contains (nano::block_hash const & hash) const; std::size_t size () const; + std::size_t deferred_size () const; nano::container_info container_info () const; @@ -119,6 +121,7 @@ private: ordered_entries set; // Blocks that could not be cemented immediately (e.g. waiting for rollbacks to complete) ordered_entries deferred; + // Blocks that are being cemented in the current batch std::unordered_set current; diff --git a/nano/node/cli.cpp b/nano/node/cli.cpp index 8c22c044c..7981bb619 100644 --- a/nano/node/cli.cpp +++ b/nano/node/cli.cpp @@ -132,9 +132,8 @@ void nano::add_node_flag_options (boost::program_options::options_description & // clang-format on } -std::error_code nano::update_flags (nano::node_flags & flags_a, boost::program_options::variables_map const & vm) +void nano::update_flags (nano::node_flags & flags_a, boost::program_options::variables_map const & vm) { - std::error_code ec; flags_a.disable_add_initial_peers = (vm.count ("disable_add_initial_peers") > 0); flags_a.disable_max_peers_per_ip = (vm.count ("disable_max_peers_per_ip") > 0); flags_a.disable_max_peers_per_subnetwork = (vm.count ("disable_max_peers_per_subnetwork") > 0); @@ -209,7 +208,6 @@ std::error_code nano::update_flags (nano::node_flags & flags_a, boost::program_o { flags_a.rpc_config_overrides = nano::config_overrides (rpcconfig->second.as> ()); } - return ec; } std::error_code nano::flags_config_conflicts (nano::node_flags const & flags_a, nano::node_config const & config_a) diff --git a/nano/node/cli.hpp b/nano/node/cli.hpp index 8f124206a..c2f6eeaab 100644 --- a/nano/node/cli.hpp +++ b/nano/node/cli.hpp @@ -21,7 +21,7 @@ enum class error_cli void add_node_options (boost::program_options::options_description &); void add_node_flag_options (boost::program_options::options_description &); -std::error_code update_flags (nano::node_flags &, boost::program_options::variables_map const &); +void update_flags (nano::node_flags &, boost::program_options::variables_map const &); std::error_code flags_config_conflicts (nano::node_flags const &, nano::node_config const &); std::error_code handle_node_options (boost::program_options::variables_map const &); } diff --git a/nano/node/election.cpp b/nano/node/election.cpp index c20739c75..8516b6f19 100644 --- a/nano/node/election.cpp +++ b/nano/node/election.cpp @@ -23,9 +23,10 @@ std::chrono::milliseconds nano::election::base_latency () const * election */ -nano::election::election (nano::node & node_a, std::shared_ptr const & block_a, std::function const &)> const & confirmation_action_a, std::function const & vote_action_a, nano::election_behavior election_behavior_a) : - confirmation_action (confirmation_action_a), - vote_action (vote_action_a), +nano::election::election (nano::node & node_a, std::shared_ptr const & block_a, nano::election_behavior election_behavior_a, std::function const &)> confirmation_action_a, std::function vote_action_a, std::function update_action_a) : + confirmation_action (std::move (confirmation_action_a)), + vote_action (std::move (vote_action_a)), + update_action (std::move (update_action_a)), node (node_a), behavior_m (election_behavior_a), status (block_a), @@ -45,10 +46,11 @@ void nano::election::confirm_once (nano::unique_lock & lock) bool just_confirmed = state_m != nano::election_state::confirmed; state_m = nano::election_state::confirmed; + state_start = std::chrono::steady_clock::now (); if (just_confirmed) { - status.election_end = std::chrono::duration_cast (std::chrono::system_clock::now ().time_since_epoch ()); + status.election_end = std::chrono::system_clock::now (); // Timestamp as system time status.election_duration = std::chrono::duration_cast (std::chrono::steady_clock::now () - election_start); status.confirmation_request_count = confirmation_request_count; status.block_count = nano::narrow_cast (last_blocks.size ()); @@ -78,12 +80,19 @@ void nano::election::confirm_once (nano::unique_lock & lock) lock.unlock (); - node.election_workers.post ([status_l, confirmation_action_l = confirmation_action] () { - if (confirmation_action_l) - { + if (update_action) + { + node.election_workers.post ([qualified_root_l = qualified_root, update_action_l = update_action] () { + update_action_l (qualified_root_l); + }); + } + + if (confirmation_action) + { + node.election_workers.post ([status_l, confirmation_action_l = confirmation_action] () { confirmation_action_l (status_l.winner); - } - }); + }); + } } else { @@ -145,8 +154,15 @@ bool nano::election::state_change (nano::election_state expected_a, nano::electi if (state_m == expected_a) { state_m = desired_a; - state_start = std::chrono::steady_clock::now ().time_since_epoch (); + state_start = std::chrono::steady_clock::now (); result = false; + + if (update_action) + { + node.election_workers.post ([qualified_root_l = qualified_root, update_action_l = update_action] () { + update_action_l (qualified_root_l); + }); + } } } return result; @@ -191,12 +207,6 @@ void nano::election::send_confirm_req (nano::confirmation_solicitor & solicitor_ } } -void nano::election::transition_active () -{ - nano::lock_guard guard{ mutex }; - state_change (nano::election_state::passive, nano::election_state::active); -} - bool nano::election::transition_priority () { nano::lock_guard guard{ mutex }; @@ -214,13 +224,26 @@ bool nano::election::transition_priority () qualified_root, duration ().count ()); + if (update_action) + { + node.election_workers.post ([qualified_root_l = qualified_root, update_action_l = update_action] () { + update_action_l (qualified_root_l); + }); + } + return true; } -void nano::election::cancel () +bool nano::election::transition_active () { nano::lock_guard guard{ mutex }; - state_change (state_m, nano::election_state::cancelled); + return !state_change (nano::election_state::passive, nano::election_state::active); // Invert since false => success +} + +bool nano::election::cancel () +{ + nano::lock_guard guard{ mutex }; + return !state_change (state_m, nano::election_state::cancelled); // Invert since false => success } bool nano::election::confirmed_locked () const @@ -306,26 +329,26 @@ nano::election_status nano::election::get_status () const return status; } -bool nano::election::transition_time (nano::confirmation_solicitor & solicitor_a) +bool nano::election::tick (nano::confirmation_solicitor & solicitor) { nano::unique_lock lock{ mutex }; bool result = false; switch (state_m) { case nano::election_state::passive: - if (base_latency () * passive_duration_factor < std::chrono::steady_clock::now ().time_since_epoch () - state_start) + if (base_latency () * passive_duration_factor < std::chrono::steady_clock::now () - state_start) { state_change (nano::election_state::passive, nano::election_state::active); } break; case nano::election_state::active: broadcast_vote_locked (lock); - broadcast_block (solicitor_a); - send_confirm_req (solicitor_a); + broadcast_block (solicitor); + send_confirm_req (solicitor); break; case nano::election_state::confirmed: result = true; // Return true to indicate this election should be cleaned up - broadcast_block (solicitor_a); // Ensure election winner is broadcasted + broadcast_block (solicitor); // Ensure election winner is broadcasted state_change (nano::election_state::confirmed, nano::election_state::expired_confirmed); break; case nano::election_state::expired_unconfirmed: @@ -351,6 +374,7 @@ bool nano::election::transition_time (nano::confirmation_solicitor & solicitor_a status.type = nano::election_status_type::stopped; } } + return result; } diff --git a/nano/node/election.hpp b/nano/node/election.hpp index 9f3881cd8..a46086309 100644 --- a/nano/node/election.hpp +++ b/nano/node/election.hpp @@ -70,13 +70,14 @@ private: // Callbacks std::function const &)> confirmation_action; std::function vote_action; + std::function update_action; private: // State management static unsigned constexpr passive_duration_factor = 5; static unsigned constexpr active_request_count_min = 2; nano::election_state state_m{ election_state::passive }; - std::chrono::steady_clock::duration state_start{ std::chrono::steady_clock::now ().time_since_epoch () }; + std::chrono::steady_clock::time_point state_start{ std::chrono::steady_clock::now () }; // These are modified while not holding the mutex from transition_time only std::chrono::steady_clock::time_point last_block{}; @@ -89,10 +90,12 @@ private: // State management bool state_change (nano::election_state, nano::election_state); public: // State transitions - bool transition_time (nano::confirmation_solicitor &); - void transition_active (); + // Returns true if the election should be cleaned up + bool tick (nano::confirmation_solicitor &); + + bool transition_active (); bool transition_priority (); - void cancel (); + bool cancel (); public: // Status bool confirmed () const; @@ -100,6 +103,7 @@ public: // Status nano::election_extended_status current_status () const; std::shared_ptr winner () const; std::chrono::milliseconds duration () const; + std::atomic confirmation_request_count{ 0 }; std::atomic vote_broadcast_count{ 0 }; @@ -110,7 +114,13 @@ public: // Status nano::election_status status; public: // Interface - election (nano::node &, std::shared_ptr const & block, std::function const &)> const & confirmation_action, std::function const & vote_action, nano::election_behavior behavior); + election ( + nano::node &, + std::shared_ptr const & block, + nano::election_behavior behavior, + std::function const &)> confirmation_action = nullptr, + std::function vote_action = nullptr, + std::function update_action = nullptr); std::shared_ptr find (nano::block_hash const &) const; /* @@ -131,10 +141,16 @@ public: // Interface nano::vote_info get_last_vote (nano::account const & account); void set_last_vote (nano::account const & account, nano::vote_info vote_info); nano::election_status get_status () const; + std::chrono::steady_clock::time_point get_election_start () const { return election_start; } + std::chrono::steady_clock::time_point get_state_start () const + { + nano::lock_guard guard{ mutex }; + return state_start; + } private: // Dependencies nano::node & node; diff --git a/nano/node/election_status.hpp b/nano/node/election_status.hpp index d75b7e3a9..99e27d617 100644 --- a/nano/node/election_status.hpp +++ b/nano/node/election_status.hpp @@ -23,6 +23,7 @@ enum class election_status_type : uint8_t stopped = 5 }; +std::string_view to_string (election_status_type); nano::stat::detail to_stat_detail (election_status_type); /* Holds a summary of an election */ @@ -32,8 +33,8 @@ public: std::shared_ptr winner; nano::amount tally{ 0 }; nano::amount final_tally{ 0 }; - std::chrono::milliseconds election_end{ std::chrono::duration_cast (std::chrono::system_clock::now ().time_since_epoch ()) }; - std::chrono::milliseconds election_duration{ std::chrono::duration_values::zero () }; + std::chrono::system_clock::time_point election_end{}; + std::chrono::milliseconds election_duration{}; unsigned confirmation_request_count{ 0 }; unsigned vote_broadcast_count{ 0 }; unsigned block_count{ 0 }; diff --git a/nano/node/ipc/ipc_broker.cpp b/nano/node/ipc/ipc_broker.cpp index 15ba718cc..2650db1eb 100644 --- a/nano/node/ipc/ipc_broker.cpp +++ b/nano/node/ipc/ipc_broker.cpp @@ -54,7 +54,7 @@ void nano::ipc::broker::start () confirmation->block = nano::ipc::flatbuffers_builder::block_to_union (*status_a.winner, amount_a, is_state_send_a, is_state_epoch_a); confirmation->election_info = std::make_unique (); confirmation->election_info->duration = status_a.election_duration.count (); - confirmation->election_info->time = status_a.election_end.count (); + confirmation->election_info->time = milliseconds_since_epoch (status_a.election_end); confirmation->election_info->tally = status_a.tally.to_string_dec (); confirmation->election_info->block_count = status_a.block_count; confirmation->election_info->voter_count = status_a.voter_count; diff --git a/nano/node/ipc/ipc_config.cpp b/nano/node/ipc/ipc_config.cpp index 7672615bb..1a9e19304 100644 --- a/nano/node/ipc/ipc_config.cpp +++ b/nano/node/ipc/ipc_config.cpp @@ -3,7 +3,7 @@ #include #include -nano::ipc::ipc_config_tcp_socket::ipc_config_tcp_socket (nano::network_constants & network_constants) : +nano::ipc::ipc_config_tcp_socket::ipc_config_tcp_socket (nano::network_constants const & network_constants) : network_constants{ network_constants }, port{ network_constants.default_ipc_port } { diff --git a/nano/node/ipc/ipc_config.hpp b/nano/node/ipc/ipc_config.hpp index 3cdb73bef..8243b2cf9 100644 --- a/nano/node/ipc/ipc_config.hpp +++ b/nano/node/ipc/ipc_config.hpp @@ -15,6 +15,7 @@ namespace ipc { public: virtual ~ipc_config_transport () = default; + bool enabled{ false }; bool allow_unsafe{ false }; std::size_t io_timeout{ 15 }; @@ -46,8 +47,9 @@ namespace ipc class ipc_config_tcp_socket : public ipc_config_transport { public: - ipc_config_tcp_socket (nano::network_constants & network_constants); - nano::network_constants & network_constants; + ipc_config_tcp_socket (nano::network_constants const &); + + nano::network_constants const & network_constants; /** Listening port */ uint16_t port; }; @@ -56,12 +58,14 @@ namespace ipc class ipc_config { public: - ipc_config (nano::network_constants & network_constants) : + ipc_config (nano::network_constants const & network_constants) : transport_tcp{ network_constants } { } + nano::error deserialize_toml (nano::tomlconfig & toml_a); nano::error serialize_toml (nano::tomlconfig & toml) const; + ipc_config_domain_socket transport_domain; ipc_config_tcp_socket transport_tcp; ipc_config_flatbuffers flatbuffers; diff --git a/nano/node/json_handler.cpp b/nano/node/json_handler.cpp index 02efe1b7e..50a08e870 100644 --- a/nano/node/json_handler.cpp +++ b/nano/node/json_handler.cpp @@ -2062,14 +2062,16 @@ void nano::json_handler::confirmation_history () } if (!ec) { - for (auto const & status : node.active.recently_cemented.list ()) + // TODO: Allow passing a count parameter to limit the number of results + // Default to 2000 for now since it was the previous limit + for (auto const & status : node.active.recently_cemented.list (2000)) { if (hash.is_zero () || status.winner->hash () == hash) { boost::property_tree::ptree election; election.put ("hash", status.winner->hash ().to_string ()); election.put ("duration", status.election_duration.count ()); - election.put ("time", status.election_end.count ()); + election.put ("time", milliseconds_since_epoch (status.election_end)); election.put ("tally", status.tally.to_string_dec ()); election.add ("final", status.final_tally.to_string_dec ()); election.put ("blocks", std::to_string (status.block_count)); diff --git a/nano/node/node.cpp b/nano/node/node.cpp index d0616746c..5d1cf054a 100644 --- a/nano/node/node.cpp +++ b/nano/node/node.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -102,7 +103,7 @@ nano::node::node (std::shared_ptr io_ctx_a, std::filesy wallets_store{ *wallets_store_impl }, wallets_impl{ std::make_unique (false, *this) }, wallets{ *wallets_impl }, - ledger_impl{ std::make_unique (store, network_params.ledger, stats, logger, flags_a.generate_cache, config.representative_vote_weight_minimum.number (), config.max_backlog) }, + ledger_impl{ std::make_unique (store, network_params, stats, logger, flags_a.generate_cache, config.representative_vote_weight_minimum.number (), config.max_backlog) }, ledger{ *ledger_impl }, runner_impl{ std::make_unique (io_ctx_shared, logger, config.io_threads) }, runner{ *runner_impl }, @@ -176,9 +177,9 @@ nano::node::node (std::shared_ptr io_ctx_a, std::filesy vote_processor{ *vote_processor_impl }, vote_cache_processor_impl{ std::make_unique (config.vote_processor, vote_router, vote_cache, stats, logger) }, vote_cache_processor{ *vote_cache_processor_impl }, - generator_impl{ std::make_unique (config, *this, ledger, wallets, vote_processor, history, network, stats, logger, /* non-final */ false, loopback_channel) }, + generator_impl{ std::make_unique (config.vote_generator, *this, ledger, wallets, vote_processor, history, network, stats, logger, /* non-final */ false, loopback_channel) }, generator{ *generator_impl }, - final_generator_impl{ std::make_unique (config, *this, ledger, wallets, vote_processor, history, network, stats, logger, /* final */ true, loopback_channel) }, + final_generator_impl{ std::make_unique (config.vote_generator, *this, ledger, wallets, vote_processor, history, network, stats, logger, /* final */ true, loopback_channel) }, final_generator{ *final_generator_impl }, scheduler_impl{ std::make_unique (config, *this, ledger, ledger_notifications, bucketing, active, online_reps, vote_cache, cementing_set, stats, logger) }, scheduler{ *scheduler_impl }, @@ -217,6 +218,11 @@ nano::node::node (std::shared_ptr io_ctx_a, std::filesy return ledger.weight (rep); }; + // Prioritize bootstrapping accounts with stale elections to find alternative forks + active.election_stale.add ([this] (auto const & election) { + bootstrap.prioritize (election->account); + }); + // TODO: Hook this direclty in the schedulers backlog_scan.batch_activated.add ([this] (auto const & batch) { auto transaction = ledger.tx_begin_read (); diff --git a/nano/node/nodeconfig.cpp b/nano/node/nodeconfig.cpp index 410c14805..4c98c1451 100644 --- a/nano/node/nodeconfig.cpp +++ b/nano/node/nodeconfig.cpp @@ -22,12 +22,7 @@ std::string const default_beta_peer_network = nano::env::get ("NANO_DEFAULT_PEER std::string const default_test_peer_network = nano::env::get ("NANO_DEFAULT_PEER").value_or ("peering-test.nano.org"); } -nano::node_config::node_config (nano::network_params & network_params) : - node_config (std::nullopt, network_params) -{ -} - -nano::node_config::node_config (const std::optional & peering_port_a, nano::network_params & network_params) : +nano::node_config::node_config (std::optional peering_port_a, nano::network_params const & network_params) : network_params{ network_params }, peering_port{ peering_port_a }, hinted_scheduler{ network_params.network }, @@ -92,6 +87,11 @@ nano::node_config::node_config (const std::optional & peering_port_a, } } +nano::node_config::node_config (nano::network_params const & network_params) : + node_config{ std::nullopt, network_params } +{ +} + nano::node_config::~node_config () { // Keep the node_config destructor definition here to avoid incomplete type issues @@ -123,7 +123,6 @@ nano::error nano::node_config::serialize_toml (nano::tomlconfig & toml) const toml.put ("block_processor_batch_max_time", block_processor_batch_max_time.count (), "The maximum time the block processor can continuously process blocks for.\ntype:milliseconds"); toml.put ("allow_local_peers", allow_local_peers, "Enable or disable local host peering.\ntype:bool"); toml.put ("vote_minimum", vote_minimum.to_string_dec (), "Local representatives do not vote if the delegated weight is under this threshold. Saves on system resources.\ntype:string,amount,raw"); - toml.put ("vote_generator_delay", vote_generator_delay.count (), "Delay before votes are sent to allow for efficient bundling of hashes in votes.\ntype:milliseconds"); toml.put ("unchecked_cutoff_time", unchecked_cutoff_time.count (), "Number of seconds before deleting an unchecked entry.\nWarning: lower values (e.g., 3600 seconds, or 1 hour) may result in unsuccessful bootstraps, especially a bootstrap from scratch.\ntype:seconds"); toml.put ("pow_sleep_interval", pow_sleep_interval.count (), "Time to sleep between batch work generation attempts. Reduces max CPU usage at the expense of a longer generation time.\ntype:nanoseconds"); toml.put ("external_address", external_address, "The external address of this node (NAT). If not set, the node will request this information via UPnP.\ntype:string,ip"); @@ -249,6 +248,10 @@ nano::error nano::node_config::serialize_toml (nano::tomlconfig & toml) const block_processor.serialize (block_processor_l); toml.put_child ("block_processor", block_processor_l); + nano::tomlconfig vote_generator_l; + vote_generator.serialize (vote_generator_l); + toml.put_child ("vote_generator", vote_generator_l); + nano::tomlconfig vote_processor_l; vote_processor.serialize (vote_processor_l); toml.put_child ("vote_processor", vote_processor_l); @@ -410,6 +413,12 @@ nano::error nano::node_config::deserialize_toml (nano::tomlconfig & toml) block_processor.deserialize (config_l); } + if (toml.has_key ("vote_generator")) + { + auto config_l = toml.get_required_child ("vote_generator"); + vote_generator.deserialize (config_l); + } + if (toml.has_key ("vote_processor")) { auto config_l = toml.get_required_child ("vote_processor"); @@ -566,10 +575,6 @@ nano::error nano::node_config::deserialize_toml (nano::tomlconfig & toml) toml.get_error ().set ("vote_minimum contains an invalid decimal amount"); } - auto delay_l = vote_generator_delay.count (); - toml.get ("vote_generator_delay", delay_l); - vote_generator_delay = std::chrono::milliseconds (delay_l); - auto block_processor_batch_max_time_l = block_processor_batch_max_time.count (); toml.get ("block_processor_batch_max_time", block_processor_batch_max_time_l); block_processor_batch_max_time = std::chrono::milliseconds (block_processor_batch_max_time_l); diff --git a/nano/node/nodeconfig.hpp b/nano/node/nodeconfig.hpp index c7ae5ccd9..4a7d3be99 100644 --- a/nano/node/nodeconfig.hpp +++ b/nano/node/nodeconfig.hpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -60,9 +61,8 @@ std::optional parse_database_backend (std::string const &); class node_config { public: - // TODO: Users of this class rely on the default copy consturctor. This prevents using unique_ptrs with forward declared types. - node_config (nano::network_params & network_params = nano::dev::network_params); - node_config (const std::optional &, nano::network_params & network_params = nano::dev::network_params); + node_config (std::optional peering_port, nano::network_params const & = nano::dev::network_params); + node_config (nano::network_params const & = nano::dev::network_params); ~node_config (); nano::error serialize_toml (nano::tomlconfig &) const; @@ -85,7 +85,6 @@ public: nano::amount receive_minimum{ nano::nano_ratio / 1000 / 1000 }; // 0.000001 nano nano::amount vote_minimum{ nano::nano_ratio * 42 }; // 42 nano nano::amount rep_crawler_weight_minimum{ "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" }; - std::chrono::milliseconds vote_generator_delay{ std::chrono::milliseconds (100) }; nano::amount online_weight_minimum{ nano::nano_ratio * 42 }; // TODO increase later as we get nodes /* * The minimum vote weight that a representative must have for its vote to be counted. @@ -150,6 +149,7 @@ public: nano::rep_crawler_config rep_crawler; nano::block_processor_config block_processor; nano::active_elections_config active_elections; + nano::vote_generator_config vote_generator; nano::vote_processor_config vote_processor; nano::peer_history_config peer_history; nano::transport::tcp_config tcp; diff --git a/nano/node/recently_cemented_cache.cpp b/nano/node/recently_cemented_cache.cpp index 13d84ac23..5208417f4 100644 --- a/nano/node/recently_cemented_cache.cpp +++ b/nano/node/recently_cemented_cache.cpp @@ -1,9 +1,8 @@ +#include #include #include -/* - * class recently_cemented - */ +#include nano::recently_cemented_cache::recently_cemented_cache (std::size_t max_size_a) : max_size{ max_size_a } @@ -13,23 +12,53 @@ nano::recently_cemented_cache::recently_cemented_cache (std::size_t max_size_a) void nano::recently_cemented_cache::put (const nano::election_status & status) { nano::lock_guard guard{ mutex }; - cemented.push_back (status); - if (cemented.size () > max_size) + entries.emplace_back (entry{ status.winner->qualified_root (), status.winner->hash (), status }); + if (entries.size () > max_size) { - cemented.pop_front (); + entries.pop_front (); // Remove oldest } } -nano::recently_cemented_cache::queue_t nano::recently_cemented_cache::list () const +void nano::recently_cemented_cache::erase (const nano::block_hash & hash) { nano::lock_guard guard{ mutex }; - return cemented; + entries.get ().erase (hash); +} + +void nano::recently_cemented_cache::clear () +{ + nano::lock_guard guard{ mutex }; + entries.clear (); +} + +auto nano::recently_cemented_cache::list (size_t max_count) const -> std::deque +{ + nano::lock_guard guard{ mutex }; + std::deque result; + auto it = entries.rbegin (); + for (size_t i = 0; i < max_count && it != entries.rend (); ++i, ++it) + { + result.push_back (it->status); + } + return result; } std::size_t nano::recently_cemented_cache::size () const { nano::lock_guard guard{ mutex }; - return cemented.size (); + return entries.size (); +} + +bool nano::recently_cemented_cache::contains (const nano::qualified_root & root) const +{ + nano::lock_guard guard{ mutex }; + return entries.get ().contains (root); +} + +bool nano::recently_cemented_cache::contains (const nano::block_hash & hash) const +{ + nano::lock_guard guard{ mutex }; + return entries.get ().contains (hash); } nano::container_info nano::recently_cemented_cache::container_info () const @@ -37,6 +66,6 @@ nano::container_info nano::recently_cemented_cache::container_info () const nano::lock_guard guard{ mutex }; nano::container_info info; - info.put ("cemented", cemented); + info.put ("entries", entries); return info; } diff --git a/nano/node/recently_cemented_cache.hpp b/nano/node/recently_cemented_cache.hpp index b18a645e7..cb221b112 100644 --- a/nano/node/recently_cemented_cache.hpp +++ b/nano/node/recently_cemented_cache.hpp @@ -1,14 +1,20 @@ #pragma once #include +#include +#include #include +#include + +#include +#include +#include +#include +#include #include -namespace nano -{ -class container_info_component; -} +namespace mi = boost::multi_index; namespace nano { @@ -18,18 +24,44 @@ namespace nano class recently_cemented_cache final { public: - using queue_t = std::deque; - - explicit recently_cemented_cache (std::size_t max_size); + explicit recently_cemented_cache (size_t max_size); void put (nano::election_status const &); - queue_t list () const; + void erase (nano::block_hash const &); + void clear (); std::size_t size () const; + // Returns up to max_count most recent entries + std::deque list (size_t max_count = std::numeric_limits::max ()) const; + + bool contains (nano::qualified_root const &) const; + bool contains (nano::block_hash const &) const; + nano::container_info container_info () const; private: - queue_t cemented; + struct entry + { + nano::qualified_root root; + nano::block_hash hash; + nano::election_status status; + }; + + // clang-format off + class tag_hash {}; + class tag_root {}; + class tag_sequenced {}; + + using ordered_entries = boost::multi_index_container>, + mi::hashed_unique, + mi::member>, + mi::hashed_unique, + mi::member>>>; + // clang-format on + ordered_entries entries; + std::size_t const max_size; mutable nano::mutex mutex; diff --git a/nano/node/recently_confirmed_cache.cpp b/nano/node/recently_confirmed_cache.cpp index 6f1205b52..6b8e8e910 100644 --- a/nano/node/recently_confirmed_cache.cpp +++ b/nano/node/recently_confirmed_cache.cpp @@ -1,10 +1,6 @@ #include #include -/* - * class recently_confirmed - */ - nano::recently_confirmed_cache::recently_confirmed_cache (std::size_t max_size_a) : max_size{ max_size_a } { @@ -13,47 +9,47 @@ nano::recently_confirmed_cache::recently_confirmed_cache (std::size_t max_size_a void nano::recently_confirmed_cache::put (const nano::qualified_root & root, const nano::block_hash & hash) { nano::lock_guard guard{ mutex }; - confirmed.get ().emplace_back (root, hash); - if (confirmed.size () > max_size) + entries.emplace_back (root, hash); + if (entries.size () > max_size) { - confirmed.get ().pop_front (); + entries.pop_front (); } } void nano::recently_confirmed_cache::erase (const nano::block_hash & hash) { nano::lock_guard guard{ mutex }; - confirmed.get ().erase (hash); + entries.get ().erase (hash); } void nano::recently_confirmed_cache::clear () { nano::lock_guard guard{ mutex }; - confirmed.clear (); + entries.clear (); } -bool nano::recently_confirmed_cache::exists (const nano::block_hash & hash) const +bool nano::recently_confirmed_cache::contains (const nano::block_hash & hash) const { nano::lock_guard guard{ mutex }; - return confirmed.get ().find (hash) != confirmed.get ().end (); + return entries.get ().contains (hash); } -bool nano::recently_confirmed_cache::exists (const nano::qualified_root & root) const +bool nano::recently_confirmed_cache::contains (const nano::qualified_root & root) const { nano::lock_guard guard{ mutex }; - return confirmed.get ().find (root) != confirmed.get ().end (); + return entries.get ().contains (root); } std::size_t nano::recently_confirmed_cache::size () const { nano::lock_guard guard{ mutex }; - return confirmed.size (); + return entries.size (); } nano::recently_confirmed_cache::entry_t nano::recently_confirmed_cache::back () const { nano::lock_guard guard{ mutex }; - return confirmed.back (); + return entries.back (); } nano::container_info nano::recently_confirmed_cache::container_info () const @@ -61,6 +57,6 @@ nano::container_info nano::recently_confirmed_cache::container_info () const nano::lock_guard guard{ mutex }; nano::container_info info; - info.put ("confirmed", confirmed); + info.put ("entries", entries); return info; } diff --git a/nano/node/recently_confirmed_cache.hpp b/nano/node/recently_confirmed_cache.hpp index bdfc95611..e374f5eca 100644 --- a/nano/node/recently_confirmed_cache.hpp +++ b/nano/node/recently_confirmed_cache.hpp @@ -13,11 +13,6 @@ namespace mi = boost::multi_index; -namespace nano -{ -class container_info_component; -} - namespace nano { class recently_confirmed_cache final @@ -32,8 +27,8 @@ public: void clear (); std::size_t size () const; - bool exists (nano::qualified_root const &) const; - bool exists (nano::block_hash const &) const; + bool contains (nano::qualified_root const &) const; + bool contains (nano::block_hash const &) const; nano::container_info container_info () const; @@ -44,17 +39,17 @@ private: // clang-format off class tag_hash {}; class tag_root {}; - class tag_sequence {}; + class tag_sequenced {}; - using ordered_recent_confirmations = boost::multi_index_container>, + mi::sequenced>, mi::hashed_unique, mi::member>, mi::hashed_unique, mi::member>>>; // clang-format on - ordered_recent_confirmations confirmed; + ordered_entries entries; std::size_t const max_size; diff --git a/nano/node/repcrawler.cpp b/nano/node/repcrawler.cpp index 499fef276..b0ebc3eab 100644 --- a/nano/node/repcrawler.cpp +++ b/nano/node/repcrawler.cpp @@ -301,7 +301,7 @@ auto nano::rep_crawler::prepare_query_target () const -> hash_root_t for (auto const & block : random_blocks) { // Avoid blocks that could still have live votes coming in - if (active.recently_confirmed.exists (block->hash ())) + if (active.recently_confirmed.contains (block->hash ())) { continue; } diff --git a/nano/node/scheduler/bucket.cpp b/nano/node/scheduler/bucket.cpp index de9083b34..06976cda2 100644 --- a/nano/node/scheduler/bucket.cpp +++ b/nano/node/scheduler/bucket.cpp @@ -91,7 +91,7 @@ bool nano::scheduler::bucket::activate () elections.get ().erase (election->qualified_root); }; - auto result = active.insert (block, nano::election_behavior::priority, erase_callback); + auto result = active.insert (block, nano::election_behavior::priority, index, priority, erase_callback); if (result.inserted) { release_assert (result.election); diff --git a/nano/node/scheduler/component.hpp b/nano/node/scheduler/component.hpp index a8491ec13..8eeabc856 100644 --- a/nano/node/scheduler/component.hpp +++ b/nano/node/scheduler/component.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/nano/node/scheduler/hinted.cpp b/nano/node/scheduler/hinted.cpp index c727ec6f6..7338e6725 100644 --- a/nano/node/scheduler/hinted.cpp +++ b/nano/node/scheduler/hinted.cpp @@ -49,8 +49,8 @@ void nano::scheduler::hinted::stop () nano::lock_guard lock{ mutex }; stopped = true; } - notify (); - nano::join_or_pass (thread); + condition.notify_all (); + join_or_pass (thread); } void nano::scheduler::hinted::notify () diff --git a/nano/node/scheduler/hinted.hpp b/nano/node/scheduler/hinted.hpp index 097647249..9d7b45fb6 100644 --- a/nano/node/scheduler/hinted.hpp +++ b/nano/node/scheduler/hinted.hpp @@ -64,6 +64,7 @@ private: nano::uint128_t final_tally_threshold () const; private: // Dependencies + hinted_config const & config; nano::node & node; nano::vote_cache & vote_cache; nano::active_elections & active; @@ -71,8 +72,6 @@ private: // Dependencies nano::stats & stats; private: - hinted_config const & config; - std::atomic stopped{ false }; nano::condition_variable condition; mutable nano::mutex mutex; diff --git a/nano/node/scheduler/manual.cpp b/nano/node/scheduler/manual.cpp index f2fa3d1e2..b2ac593b2 100644 --- a/nano/node/scheduler/manual.cpp +++ b/nano/node/scheduler/manual.cpp @@ -30,7 +30,7 @@ void nano::scheduler::manual::stop () nano::lock_guard lock{ mutex }; stopped = true; } - notify (); + condition.notify_all (); nano::join_or_pass (thread); } @@ -39,23 +39,43 @@ void nano::scheduler::manual::notify () condition.notify_all (); } -void nano::scheduler::manual::push (std::shared_ptr const & block_a, boost::optional const & previous_balance_a) +auto nano::scheduler::manual::push (std::shared_ptr const & block) -> std::future> { nano::lock_guard lock{ mutex }; - queue.push_back (std::make_tuple (block_a, previous_balance_a, nano::election_behavior::manual)); - notify (); + + // Check if block already exists + auto & hash_index = queue.get (); + + if (hash_index.contains (block->hash ())) + { + // Block already exists, return future that immediately resolves to nullptr + std::promise> promise; + auto future = promise.get_future (); + promise.set_value (nullptr); + return future; + } + + // Create entry and get future before inserting + entry new_entry{ block }; + auto future = new_entry.promise.get_future (); + + auto [it, inserted] = queue.push_back (std::move (new_entry)); + debug_assert (inserted); + + condition.notify_all (); + return future; } bool nano::scheduler::manual::contains (nano::block_hash const & hash) const { nano::lock_guard lock{ mutex }; - return std::any_of (queue.cbegin (), queue.cend (), [&hash] (auto const & item) { - return std::get<0> (item)->hash () == hash; - }); + auto & hash_index = queue.get (); + return hash_index.contains (hash); } bool nano::scheduler::manual::predicate () const { + debug_assert (!mutex.try_lock ()); return !queue.empty (); } @@ -67,28 +87,37 @@ void nano::scheduler::manual::run () condition.wait (lock, [this] () { return stopped || predicate (); }); - debug_assert ((std::this_thread::yield (), true)); // Introduce some random delay in debug builds - if (!stopped) + + if (stopped) + { + return; + } + + debug_assert ((std::this_thread::yield (), true)); + + if (predicate ()) { node.stats.inc (nano::stat::type::election_scheduler, nano::stat::detail::loop); - if (predicate ()) + auto promise = std::move (queue.front ().promise); + auto block = queue.front ().block; + queue.pop_front (); + + lock.unlock (); + + auto result = node.active.insert (block, nano::election_behavior::manual); + if (result.inserted) { - auto const [block, previous_balance, election_behavior] = queue.front (); - queue.pop_front (); - lock.unlock (); node.stats.inc (nano::stat::type::election_scheduler, nano::stat::detail::insert_manual); - auto result = node.active.insert (block, election_behavior); - if (result.election != nullptr) - { - result.election->transition_active (); - } } - else + if (result.election != nullptr) { - lock.unlock (); + result.election->transition_active (); } - notify (); + + // Fulfill the promise + promise.set_value (result.election); + lock.lock (); } } diff --git a/nano/node/scheduler/manual.hpp b/nano/node/scheduler/manual.hpp index c62f215bd..9290edd06 100644 --- a/nano/node/scheduler/manual.hpp +++ b/nano/node/scheduler/manual.hpp @@ -4,41 +4,73 @@ #include #include -#include +#include +#include +#include +#include +#include -#include +#include +#include #include -#include +#include + +namespace mi = boost::multi_index; namespace nano::scheduler { -class buckets; - class manual final { - std::deque, boost::optional, nano::election_behavior>> queue; - nano::node & node; - mutable nano::mutex mutex; - nano::condition_variable condition; - bool stopped{ false }; - std::thread thread; - void notify (); - bool predicate () const; - void run (); - public: - explicit manual (nano::node & node); + explicit manual (nano::node &); ~manual (); void start (); void stop (); - // Manually start an election for a block - // Call action with confirmed block, may be different than what we started with - void push (std::shared_ptr const &, boost::optional const & = boost::none); + std::future> push (std::shared_ptr const & block); bool contains (nano::block_hash const &) const; nano::container_info container_info () const; + +private: + bool predicate () const; + void notify (); + void run (); + +private: // Dependencies + nano::node & node; + +private: + struct entry + { + std::shared_ptr block; + mutable std::promise> promise; + + nano::block_hash hash () const + { + return block->hash (); + } + }; + + // clang-format off + class tag_sequenced {}; + class tag_hash {}; + + using ordered_queue = boost::multi_index_container>, + mi::hashed_unique, + mi::const_mem_fun> + >>; + // clang-format on + + ordered_queue queue; + + bool stopped{ false }; + nano::condition_variable condition; + mutable nano::mutex mutex; + std::thread thread; }; } diff --git a/nano/node/scheduler/optimistic.cpp b/nano/node/scheduler/optimistic.cpp index caab43c70..4f261987e 100644 --- a/nano/node/scheduler/optimistic.cpp +++ b/nano/node/scheduler/optimistic.cpp @@ -46,13 +46,17 @@ void nano::scheduler::optimistic::stop () nano::lock_guard guard{ mutex }; stopped = true; } - notify (); - nano::join_or_pass (thread); + condition.notify_all (); + join_or_pass (thread); } void nano::scheduler::optimistic::notify () { - condition.notify_all (); + // Only wake up the thread if there is space inside AEC for optimistic elections + if (active.vacancy (nano::election_behavior::optimistic) > 0) + { + condition.notify_all (); + } } bool nano::scheduler::optimistic::activate_predicate (const nano::account_info & account_info, const nano::confirmation_height_info & conf_info) const @@ -72,33 +76,36 @@ bool nano::scheduler::optimistic::activate_predicate (const nano::account_info & bool nano::scheduler::optimistic::activate (const nano::account & account, const nano::account_info & account_info, const nano::confirmation_height_info & conf_info) { + debug_assert (account_info.block_count >= conf_info.height); + if (!config.enable) { return false; } - debug_assert (account_info.block_count >= conf_info.height); if (activate_predicate (account_info, conf_info)) { + nano::lock_guard lock{ mutex }; + + // Assume gap_threshold for accounts with nothing confirmed + auto const unconfirmed_height = std::max (account_info.block_count - conf_info.height, config.gap_threshold); + + auto [it, inserted] = candidates.push_back ({ account, unconfirmed_height, std::chrono::steady_clock::now () }); + + stats.inc (nano::stat::type::optimistic_scheduler, inserted ? nano::stat::detail::activated : nano::stat::detail::duplicate); + + // Limit candidates container size + if (candidates.size () > config.max_size) { - nano::lock_guard lock{ mutex }; - - // Prevent duplicate candidate accounts - if (candidates.get ().contains (account)) - { - return false; // Not activated - } - // Limit candidates container size - if (candidates.size () >= config.max_size) - { - return false; // Not activated - } - - stats.inc (nano::stat::type::optimistic_scheduler, nano::stat::detail::activated); - candidates.push_back ({ account, nano::clock::now () }); + // Remove oldest candidate + candidates.pop_front (); } - return true; // Activated + + // Not notifying the thread immediately here, since we need to wait for activation_delay to elapse + + return inserted; } + return false; // Not activated } @@ -106,17 +113,25 @@ bool nano::scheduler::optimistic::predicate () const { debug_assert (!mutex.try_lock ()); - if (active.vacancy (nano::election_behavior::optimistic) <= 0) + // Check if there is space inside AEC for a new optimistic election + return !candidates.empty () && active.vacancy (nano::election_behavior::optimistic) > 0; +} + +auto nano::scheduler::optimistic::snapshot (size_t max_count) const -> std::deque +{ + auto const now = std::chrono::steady_clock::now (); + + std::deque result; + + auto & height_index = candidates.get (); + for (auto it = height_index.begin (); it != height_index.end () && result.size () < max_count; ++it) { - return false; - } - if (candidates.empty ()) - { - return false; + if (elapsed (it->timestamp, config.activation_delay, now)) + { + result.push_back (*it); + } } - auto candidate = candidates.front (); - bool result = nano::elapsed (candidate.timestamp, network_constants.optimistic_activation_delay); return result; } @@ -125,33 +140,60 @@ void nano::scheduler::optimistic::run () nano::unique_lock lock{ mutex }; while (!stopped) { - stats.inc (nano::stat::type::optimistic_scheduler, nano::stat::detail::loop); + // Ignore predicate in condition, we always want to wait for activation_delay to elapse before next wake up + condition.wait_for (lock, config.activation_delay, [this] () { + return stopped.load (); + }); + + if (stopped) + { + return; + } if (predicate ()) { - auto transaction = ledger.tx_begin_read (); + stats.inc (nano::stat::type::optimistic_scheduler, nano::stat::detail::loop); - while (predicate ()) - { - debug_assert (!candidates.empty ()); - auto candidate = candidates.front (); - candidates.pop_front (); - - lock.unlock (); - - run_one (transaction, candidate); - - lock.lock (); - } + run_iterative (lock); + debug_assert (!lock.owns_lock ()); + lock.lock (); } - - condition.wait_for (lock, network_constants.optimistic_activation_delay / 2, [this] () { - return stopped || predicate (); - }); } } -void nano::scheduler::optimistic::run_one (secure::transaction const & transaction, entry const & candidate) +void nano::scheduler::optimistic::run_iterative (nano::unique_lock & lock) +{ + debug_assert (lock.owns_lock ()); + debug_assert (!mutex.try_lock ()); + + auto tops = snapshot (active.limit (nano::election_behavior::optimistic)); + + lock.unlock (); + + auto transaction = ledger.tx_begin_read (); + + for (auto const & candidate : tops) + { + if (stopped) + { + return; + } + + transaction.refresh_if_needed (); + + bool good = run_one (transaction, candidate); + if (!good) + { + // Remove no longer valid candidate + nano::lock_guard guard{ mutex }; + auto & account_index = candidates.get (); + account_index.erase (candidate.account); + stats.inc (nano::stat::type::optimistic_scheduler, nano::stat::detail::erased); + } + } +} + +bool nano::scheduler::optimistic::run_one (secure::transaction const & transaction, entry const & candidate) { auto block = ledger.any.block_get (transaction, ledger.any.account_head (transaction, candidate.account)); if (block) @@ -160,12 +202,13 @@ void nano::scheduler::optimistic::run_one (secure::transaction const & transacti if (!node.block_confirmed_or_being_confirmed (transaction, block->hash ())) { // Try to insert it into AEC - // We check for AEC vacancy inside our predicate auto result = node.active.insert (block, nano::election_behavior::optimistic); - stats.inc (nano::stat::type::optimistic_scheduler, result.inserted ? nano::stat::detail::insert : nano::stat::detail::insert_failed); + + return true; // Activation attempted } } + return false; // Activation not attempted, block not found or already confirmed, should be erased from candidates } nano::container_info nano::scheduler::optimistic::container_info () const @@ -186,6 +229,7 @@ nano::error nano::scheduler::optimistic_config::deserialize (nano::tomlconfig & toml.get ("enable", enable); toml.get ("gap_threshold", gap_threshold); toml.get ("max_size", max_size); + toml.get_duration ("activation_delay", activation_delay); return toml.get_error (); } @@ -195,6 +239,7 @@ nano::error nano::scheduler::optimistic_config::serialize (nano::tomlconfig & to toml.put ("enable", enable, "Enable or disable optimistic elections\ntype:bool"); toml.put ("gap_threshold", gap_threshold, "Minimum difference between confirmation frontier and account frontier to become a candidate for optimistic confirmation\ntype:uint64"); toml.put ("max_size", max_size, "Maximum number of candidates stored in memory\ntype:uint64"); + toml.put ("activation_delay", activation_delay.count (), "How much to delay activation of optimistic elections to avoid interfering with election scheduler\ntype:milliseconds"); return toml.get_error (); } diff --git a/nano/node/scheduler/optimistic.hpp b/nano/node/scheduler/optimistic.hpp index 24edd1e73..0028b6a80 100644 --- a/nano/node/scheduler/optimistic.hpp +++ b/nano/node/scheduler/optimistic.hpp @@ -33,10 +33,13 @@ public: bool enable{ true }; /** Minimum difference between confirmation frontier and account frontier to become a candidate for optimistic confirmation */ - std::size_t gap_threshold{ 32 }; + uint64_t gap_threshold{ 16 }; /** Maximum number of candidates stored in memory */ - std::size_t max_size{ 1024 * 64 }; + std::size_t max_size{ 1024 * 4 }; + + /** How much to delay activation of optimistic elections to avoid interfering with election scheduler */ + std::chrono::milliseconds activation_delay{ 1s }; }; class optimistic final @@ -67,7 +70,10 @@ private: bool predicate () const; void run (); - void run_one (secure::transaction const &, entry const & candidate); + void run_iterative (nano::unique_lock &); + bool run_one (secure::transaction const &, entry const & candidate); + + std::deque snapshot (size_t max_count) const; private: // Dependencies optimistic_config const & config; @@ -81,25 +87,29 @@ private: struct entry { nano::account account; - nano::clock::time_point timestamp; + uint64_t unconfirmed_height; + std::chrono::steady_clock::time_point timestamp; }; // clang-format off class tag_sequenced {}; class tag_account {}; + class tag_unconfirmed_height {}; using ordered_candidates = boost::multi_index_container>, mi::hashed_unique, - mi::member> + mi::member>, + mi::ordered_non_unique, + mi::member, std::greater<>> // Descending >>; // clang-format on /** Accounts eligible for optimistic scheduling */ ordered_candidates candidates; - bool stopped{ false }; + std::atomic stopped{ false }; nano::condition_variable condition; mutable nano::mutex mutex; std::thread thread; diff --git a/nano/node/vote_generator.cpp b/nano/node/vote_generator.cpp index 5c34d8130..70bcecc6f 100644 --- a/nano/node/vote_generator.cpp +++ b/nano/node/vote_generator.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -16,21 +17,21 @@ #include -nano::vote_generator::vote_generator (nano::node_config const & config_a, nano::node & node_a, nano::ledger & ledger_a, nano::wallets & wallets_a, nano::vote_processor & vote_processor_a, nano::local_vote_history & history_a, nano::network & network_a, nano::stats & stats_a, nano::logger & logger_a, bool is_final_a, std::shared_ptr inproc_channel_a) : +nano::vote_generator::vote_generator (vote_generator_config const & config_a, nano::node & node_a, nano::ledger & ledger_a, nano::wallets & wallets_a, nano::vote_processor & vote_processor_a, nano::local_vote_history & history_a, nano::network & network_a, nano::stats & stats_a, nano::logger & logger_a, bool is_final_a, std::shared_ptr inproc_channel_a) : config (config_a), node (node_a), ledger (ledger_a), wallets (wallets_a), vote_processor (vote_processor_a), history (history_a), - spacing_impl{ std::make_unique (config_a.network_params.voting.delay) }, + spacing_impl{ std::make_unique (node_a.network_params.voting.delay) }, spacing{ *spacing_impl }, network (network_a), stats (stats_a), logger (logger_a), is_final (is_final_a), inproc_channel{ inproc_channel_a }, - vote_generation_queue{ stats, nano::stat::type::vote_generator, is_final ? nano::thread_role::name::voting_final : nano::thread_role::name::voting, /* single threaded */ 1, /* max queue size */ 1024 * 32, /* max batch size */ 256 } + vote_generation_queue{ stats, nano::stat::type::vote_generator, is_final ? nano::thread_role::name::voting_final : nano::thread_role::name::voting, /* single threaded */ 1, config.max_queue, config.batch_size } { vote_generation_queue.process_batch = [this] (auto & batch) { process_batch (batch); @@ -246,7 +247,7 @@ void nano::vote_generator::reply (nano::unique_lock & lock_a, reque stats.add (nano::stat::type::requests, nano::stat::detail::requests_generated_hashes, stat::dir::in, hashes.size ()); vote (hashes, roots, [this, channel = request_a.second] (std::shared_ptr const & vote_a) { - nano::confirm_ack confirm{ config.network_params.network, vote_a }; + nano::confirm_ack confirm{ node.network_params.network, vote_a }; channel->send (confirm, nano::transport::traffic_type::vote_reply); stats.inc (nano::stat::type::requests, nano::stat::detail::requests_generated_votes, stat::dir::in); }); @@ -292,8 +293,8 @@ void nano::vote_generator::run () nano::unique_lock lock{ mutex }; while (!stopped) { - // Wait for at most vote_generator_delay in case no further notification is received - condition.wait_for (lock, config.vote_generator_delay, [this] () { + // Wait for at most delay in case no further notification is received + condition.wait_for (lock, config.delay, [this] () { return stopped || broadcast_predicate () || !requests.empty (); }); @@ -315,7 +316,7 @@ void nano::vote_generator::run () if (broadcast_predicate ()) { broadcast (lock); - next_broadcast = std::chrono::steady_clock::now () + config.vote_generator_delay; + next_broadcast = std::chrono::steady_clock::now () + config.delay; } if (!requests.empty ()) @@ -362,4 +363,26 @@ nano::stat::type nano::vote_generator::stat_type () const nano::log::type nano::vote_generator::log_type () const { return is_final ? nano::log::type::vote_generator_final : nano::log::type::vote_generator; +} + +/* + * vote_generator_config + */ + +nano::error nano::vote_generator_config::serialize (nano::tomlconfig & toml) const +{ + toml.put ("max_queue", max_queue, "Maximum number of entries in the vote generation queue. \ntype:uint64"); + toml.put ("batch_size", batch_size, "Maximum number of entries to process in a single batch. \ntype:uint64"); + toml.put ("delay", delay.count (), "Delay before votes are sent to allow for efficient bundling of hashes in votes. \ntype:milliseconds"); + + return toml.get_error (); +} + +nano::error nano::vote_generator_config::deserialize (nano::tomlconfig & toml) +{ + toml.get ("max_queue", max_queue); + toml.get ("batch_size", batch_size); + toml.get_duration ("delay", delay); + + return toml.get_error (); } \ No newline at end of file diff --git a/nano/node/vote_generator.hpp b/nano/node/vote_generator.hpp index 0233bf907..f8edc4d3e 100644 --- a/nano/node/vote_generator.hpp +++ b/nano/node/vote_generator.hpp @@ -25,6 +25,18 @@ namespace mi = boost::multi_index; namespace nano { +class vote_generator_config final +{ +public: + nano::error serialize (nano::tomlconfig & toml) const; + nano::error deserialize (nano::tomlconfig & toml); + +public: + size_t max_queue{ 1024 * 32 }; + size_t batch_size{ 256 }; + std::chrono::milliseconds delay{ 100ms }; +}; + class vote_generator final { private: @@ -34,7 +46,7 @@ private: std::chrono::steady_clock::time_point next_broadcast = { std::chrono::steady_clock::now () }; public: - vote_generator (nano::node_config const &, nano::node &, nano::ledger &, nano::wallets &, nano::vote_processor &, nano::local_vote_history &, nano::network &, nano::stats &, nano::logger &, bool is_final, std::shared_ptr inproc_channel); + vote_generator (vote_generator_config const &, nano::node &, nano::ledger &, nano::wallets &, nano::vote_processor &, nano::local_vote_history &, nano::network &, nano::stats &, nano::logger &, bool is_final, std::shared_ptr inproc_channel); ~vote_generator (); /** Queue items for vote generation, or broadcast votes already in cache */ @@ -63,7 +75,7 @@ private: nano::log::type log_type () const; private: // Dependencies - nano::node_config const & config; + vote_generator_config const & config; nano::node & node; nano::ledger & ledger; nano::wallets & wallets; diff --git a/nano/node/vote_router.cpp b/nano/node/vote_router.cpp index 7589c6a9c..5241ce4ba 100644 --- a/nano/node/vote_router.cpp +++ b/nano/node/vote_router.cpp @@ -86,7 +86,7 @@ std::unordered_map nano::vote_router::vote (s } else { - if (recently_confirmed.exists (hash)) + if (recently_confirmed.contains (hash)) { results[hash] = nano::vote_code::late; } diff --git a/nano/node/wallet.cpp b/nano/node/wallet.cpp index 8563457fc..e6a735c00 100644 --- a/nano/node/wallet.cpp +++ b/nano/node/wallet.cpp @@ -901,7 +901,7 @@ std::shared_ptr nano::wallet::receive_action (nano::block_hash cons nano::raw_key prv; if (!store.fetch (transaction, account_a, prv)) { - logger.info (nano::log::type::wallet, "Receiving block {} from account {}, amount: {}", + logger.info (nano::log::type::wallet, "Receiving block: {} from account: {}, amount: {}", send_hash_a.to_string (), account_a.to_account (), pending_info->amount.number ().convert_to ()); @@ -924,7 +924,7 @@ std::shared_ptr nano::wallet::receive_action (nano::block_hash cons } else { - logger.warn (nano::log::type::wallet, "Unable to receive, wallet locked, block {} to account: {}", + logger.warn (nano::log::type::wallet, "Unable to receive, wallet locked, block: {} to account: {}", send_hash_a.to_string (), account_a.to_account ()); } @@ -932,19 +932,19 @@ std::shared_ptr nano::wallet::receive_action (nano::block_hash cons else { // Ledger doesn't have this marked as available to receive anymore - logger.warn (nano::log::type::wallet, "Not receiving block {}, block already received", send_hash_a.to_string ()); + logger.warn (nano::log::type::wallet, "Not receiving block: {}, block already received", send_hash_a.to_string ()); } } else { // Ledger doesn't have this block anymore. - logger.warn (nano::log::type::wallet, "Not receiving block {}, block no longer exists or pruned", send_hash_a.to_string ()); + logger.warn (nano::log::type::wallet, "Not receiving block: {}, block no longer exists or pruned", send_hash_a.to_string ()); } } else { // Someone sent us something below the threshold of receiving - logger.warn (nano::log::type::wallet, "Not receiving block {} due to minimum receive threshold", send_hash_a.to_string ()); + logger.warn (nano::log::type::wallet, "Not receiving block: {} due to minimum receive threshold", send_hash_a.to_string ()); } if (block != nullptr) { @@ -969,7 +969,7 @@ std::shared_ptr nano::wallet::change_action (nano::account const & auto existing (store.find (transaction, source_a)); if (existing != store.end (transaction) && !wallets.node.ledger.any.account_head (block_transaction, source_a).is_zero ()) { - logger.info (nano::log::type::wallet, "Changing representative for account {} to {}", + logger.info (nano::log::type::wallet, "Changing representative for account: {} to: {}", source_a.to_account (), representative_a.to_account ()); @@ -988,13 +988,13 @@ std::shared_ptr nano::wallet::change_action (nano::account const & } else { - logger.warn (nano::log::type::wallet, "Changing representative for account {} failed, wallet locked or account not found", + logger.warn (nano::log::type::wallet, "Changing representative for account: {} failed, wallet locked or account not found", source_a.to_account ()); } } else { - logger.warn (nano::log::type::wallet, "Changing representative for account {} failed, wallet locked", + logger.warn (nano::log::type::wallet, "Changing representative for account: {} failed, wallet locked", source_a.to_account ()); } } @@ -1148,7 +1148,7 @@ bool nano::wallet::action_complete (std::shared_ptr const & block_a auto required_difficulty{ wallets.node.network_params.work.threshold (block_a->work_version (), details_a) }; if (wallets.node.network_params.work.difficulty (*block_a) < required_difficulty) { - logger.info (nano::log::type::wallet, "Cached or provided work for block {} account {} is invalid, regenerating...", + logger.info (nano::log::type::wallet, "Cached or provided work for block: {}, account {}: is invalid, regenerating...", block_a->hash ().to_string (), account_a.to_account ()); @@ -1292,9 +1292,15 @@ bool nano::wallet::search_receivable (store::transaction const & wallet_transact auto amount (pending.amount.number ()); if (wallets.node.config.receive_minimum.number () <= amount) { - logger.info (nano::log::type::wallet, "Found a receivable block {} for account {}", hash.to_string (), pending.source.to_account ()); + bool const confirmed = wallets.node.ledger.confirmed.block_exists_or_pruned (block_transaction, hash); - if (wallets.node.ledger.confirmed.block_exists_or_pruned (block_transaction, hash)) + logger.info (nano::log::type::wallet, "Found a receivable block: {} ({}) for account: {} from: {}", + hash.to_string (), + confirmed ? "confirmed" : "unconfirmed", + key.account.to_account (), + pending.source.to_account ()); + + if (confirmed) { auto representative = store.representative (wallet_transaction_a); // Receive confirmed block @@ -1371,7 +1377,7 @@ nano::public_key nano::wallet::change_seed (store::transaction const & transacti if (count == 0) { count = deterministic_check (transaction_a, 0); - logger.info (nano::log::type::wallet, "Auto-detected {} accounts to generate", count); + logger.info (nano::log::type::wallet, "Auto-detected {} accounts to generate from seed", count); } for (uint32_t i (0); i < count; ++i) { @@ -1416,7 +1422,7 @@ void nano::wallet::work_cache_blocking (nano::account const & account_a, nano::r } else if (!wallets.node.stopped) { - logger.warn (nano::log::type::wallet, "Could not precache work for root {} due to work generation failure", root_a.to_string ()); + logger.warn (nano::log::type::wallet, "Could not precache work for root: {} due to work generation failure", root_a.to_string ()); } } } diff --git a/nano/node/websocket.cpp b/nano/node/websocket.cpp index 9fc79b928..35362d339 100644 --- a/nano/node/websocket.cpp +++ b/nano/node/websocket.cpp @@ -772,7 +772,7 @@ nano::websocket::message nano::websocket::message_builder::block_confirmed (std: { boost::property_tree::ptree election_node_l; election_node_l.add ("duration", election_status.election_duration.count ()); - election_node_l.add ("time", election_status.election_end.count ()); + election_node_l.add ("time", milliseconds_since_epoch (election_status.election_end)); election_node_l.add ("tally", election_status.tally.to_string_dec ()); election_node_l.add ("final", election_status.final_tally.to_string_dec ()); election_node_l.add ("blocks", std::to_string (election_status.block_count)); diff --git a/nano/node/websocketconfig.cpp b/nano/node/websocketconfig.cpp index 5e050f82e..2eccaff1a 100644 --- a/nano/node/websocketconfig.cpp +++ b/nano/node/websocketconfig.cpp @@ -2,7 +2,7 @@ #include #include -nano::websocket::config::config (nano::network_constants & network_constants) : +nano::websocket::config::config (nano::network_constants const & network_constants) : network_constants{ network_constants }, port{ network_constants.default_websocket_port }, address{ boost::asio::ip::address_v6::loopback ().to_string () } diff --git a/nano/node/websocketconfig.hpp b/nano/node/websocketconfig.hpp index 9b44e4446..c12b0d53a 100644 --- a/nano/node/websocketconfig.hpp +++ b/nano/node/websocketconfig.hpp @@ -14,10 +14,12 @@ namespace websocket class config final { public: - config (nano::network_constants & network_constants); - nano::error deserialize_toml (nano::tomlconfig & toml_a); - nano::error serialize_toml (nano::tomlconfig & toml) const; - nano::network_constants & network_constants; + config (nano::network_constants const &); + + nano::error deserialize_toml (nano::tomlconfig &); + nano::error serialize_toml (nano::tomlconfig &) const; + + nano::network_constants const & network_constants; bool enabled{ false }; uint16_t port; std::string address; diff --git a/nano/qt_test/qt.cpp b/nano/qt_test/qt.cpp index 4a1f68796..6d1aa482e 100644 --- a/nano/qt_test/qt.cpp +++ b/nano/qt_test/qt.cpp @@ -520,7 +520,7 @@ TEST (history, short_text) nano::logger logger; nano::stats stats{ logger }; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); { auto transaction (ledger.tx_begin_write ()); nano::keypair key; @@ -557,7 +557,7 @@ TEST (history, pruned_source) nano::logger logger; nano::stats stats{ logger }; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); ledger.pruning = true; nano::block_hash next_pruning; // Basic pruning for legacy blocks. Previous block is pruned, source is pruned @@ -833,32 +833,48 @@ TEST (wallet, import) TEST (wallet, republish) { nano_qt::eventloop_processor processor; - nano::test::system system (2); + + // Configure nodes to disable bootstrap, election schedulers, and other background processes for test isolation + nano::node_config node_config; + node_config.bootstrap.enable = false; + node_config.priority_scheduler.enable = false; + node_config.optimistic_scheduler.enable = false; + node_config.hinted_scheduler.enable = false; + + nano::test::system system; + auto & node1 = *system.add_node (node_config); + auto & node2 = *system.add_node (node_config); + system.wallet (0)->insert_adhoc (nano::dev::genesis_key.prv); nano::keypair key; nano::block_hash hash; { - auto transaction = system.nodes[0]->ledger.tx_begin_write (); - auto latest (system.nodes[0]->ledger.any.account_head (transaction, nano::dev::genesis_key.pub)); + auto transaction = node1.ledger.tx_begin_write (); + auto latest (node1.ledger.any.account_head (transaction, nano::dev::genesis_key.pub)); auto block = std::make_shared (latest, key.pub, 0, nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, *system.work.generate (latest)); hash = block->hash (); - ASSERT_EQ (nano::block_status::progress, system.nodes[0]->ledger.process (transaction, block)); + ASSERT_EQ (nano::block_status::progress, node1.ledger.process (transaction, block)); } + + // Ensure node2 does not have the block initially + ASSERT_FALSE (node2.ledger.any.block_exists (node2.ledger.tx_begin_read (), hash)); + auto account (nano::dev::genesis_key.pub); - auto wallet (std::make_shared (*test_application, processor, *system.nodes[0], system.wallet (0), account)); + auto wallet (std::make_shared (*test_application, processor, node1, system.wallet (0), account)); wallet->start (); QTest::mouseClick (wallet->show_advanced, Qt::LeftButton); ASSERT_EQ (wallet->advanced.window, wallet->main_stack->currentWidget ()); QTest::mouseClick (wallet->advanced.block_viewer, Qt::LeftButton); ASSERT_EQ (wallet->block_viewer.window, wallet->main_stack->currentWidget ()); QTest::keyClicks (wallet->block_viewer.hash, hash.to_string ().c_str ()); + + // Verify the block exists in node1 before rebroadcast + ASSERT_TRUE (node1.ledger.any.block_exists (node1.ledger.tx_begin_read (), hash)); + QTest::mouseClick (wallet->block_viewer.rebroadcast, Qt::LeftButton); - ASSERT_FALSE (system.nodes[1]->balance (nano::dev::genesis_key.pub).is_zero ()); - system.deadline_set (10s); - while (system.nodes[1]->balance (nano::dev::genesis_key.pub).is_zero ()) - { - ASSERT_NO_ERROR (system.poll ()); - } + + // Now verify that the block was received by node2 due to the rebroadcast + ASSERT_TIMELY (10s, node2.ledger.any.block_exists (node2.ledger.tx_begin_read (), hash)); } TEST (wallet, ignore_empty_adhoc) diff --git a/nano/rpc_test/rpc.cpp b/nano/rpc_test/rpc.cpp index b53f10448..603135eab 100644 --- a/nano/rpc_test/rpc.cpp +++ b/nano/rpc_test/rpc.cpp @@ -6820,8 +6820,7 @@ TEST (rpc, confirmation_active) .sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub) .work (*system.work.generate (send1->hash ())) .build (); - node1->process_active (send1); - node1->process_active (send2); + nano::test::process (*node1, { send1, send2 }); ASSERT_TRUE (nano::test::start_elections (system, *node1, { send1, send2 })); ASSERT_EQ (2, node1->active.size ()); auto election (node1->active.election (send1->qualified_root ())); diff --git a/nano/secure/common.cpp b/nano/secure/common.cpp index 6c2e89294..9778352bb 100644 --- a/nano/secure/common.cpp +++ b/nano/secure/common.cpp @@ -90,7 +90,7 @@ std::shared_ptr & nano::dev::genesis = nano::dev::constants.genesis * */ -nano::work_thresholds const & nano::work_thresholds_for_network (nano::networks network_type) +nano::work_thresholds nano::work_thresholds_for_network (nano::networks network_type) { switch (network_type) { @@ -110,7 +110,7 @@ nano::work_thresholds const & nano::work_thresholds_for_network (nano::networks nano::network_params::network_params (nano::networks network_type) : work{ work_thresholds_for_network (network_type) }, network{ work, network_type }, - ledger{ work, network_type }, + ledger{ network_type }, voting{ network }, node{ network }, portmapping{ network }, @@ -125,8 +125,7 @@ nano::network_params::network_params (nano::networks network_type) : * */ -nano::ledger_constants::ledger_constants (nano::work_thresholds & work, nano::networks network_type) : - work{ work }, +nano::ledger_constants::ledger_constants (nano::networks network_type) : zero_key{ "0" }, nano_beta_account{ beta_public_key_data }, nano_live_account{ live_public_key_data }, @@ -249,7 +248,7 @@ nano::hardened_constants::hardened_constants () : * */ -nano::node_constants::node_constants (nano::network_constants & network_constants) +nano::node_constants::node_constants (nano::network_constants const & network_constants) { backup_interval = std::chrono::minutes (5); search_pending_interval = network_constants.is_dev_network () ? std::chrono::seconds (1) : std::chrono::seconds (5 * 60); @@ -263,7 +262,7 @@ nano::node_constants::node_constants (nano::network_constants & network_constant * */ -nano::voting_constants::voting_constants (nano::network_constants & network_constants) : +nano::voting_constants::voting_constants (nano::network_constants const & network_constants) : max_cache{ network_constants.is_dev_network () ? 256U : 128U * 1024 }, delay{ network_constants.is_dev_network () ? 1 : 15 } { @@ -273,7 +272,7 @@ nano::voting_constants::voting_constants (nano::network_constants & network_cons * */ -nano::portmapping_constants::portmapping_constants (nano::network_constants & network_constants) +nano::portmapping_constants::portmapping_constants (nano::network_constants const & network_constants) { lease_duration = std::chrono::seconds (1787); // ~30 minutes health_check_period = std::chrono::seconds (53); @@ -283,7 +282,7 @@ nano::portmapping_constants::portmapping_constants (nano::network_constants & ne * */ -nano::bootstrap_constants::bootstrap_constants (nano::network_constants & network_constants) +nano::bootstrap_constants::bootstrap_constants (nano::network_constants const & network_constants) { lazy_max_pull_blocks = network_constants.is_dev_network () ? 2 : 512; lazy_min_pull_blocks = network_constants.is_dev_network () ? 1 : 32; diff --git a/nano/secure/common.hpp b/nano/secure/common.hpp index 1c375e16a..7a75659bf 100644 --- a/nano/secure/common.hpp +++ b/nano/secure/common.hpp @@ -166,8 +166,8 @@ class network_params; class ledger_constants { public: - ledger_constants (nano::work_thresholds &, nano::networks); - nano::work_thresholds & work; + ledger_constants (nano::networks); + nano::keypair zero_key; nano::account nano_beta_account; nano::account nano_live_account; @@ -207,7 +207,8 @@ private: class node_constants { public: - node_constants (nano::network_constants & network_constants); + explicit node_constants (nano::network_constants const &); + std::chrono::minutes backup_interval; std::chrono::seconds search_pending_interval; std::chrono::minutes unchecked_cleaning_interval; @@ -223,7 +224,8 @@ public: class voting_constants { public: - voting_constants (nano::network_constants & network_constants); + explicit voting_constants (nano::network_constants const &); + size_t const max_cache; std::chrono::seconds const delay; }; @@ -232,7 +234,8 @@ public: class portmapping_constants { public: - portmapping_constants (nano::network_constants & network_constants); + explicit portmapping_constants (nano::network_constants const &); + // Timeouts are primes so they infrequently happen at the same time std::chrono::seconds lease_duration; std::chrono::seconds health_check_period; @@ -242,7 +245,8 @@ public: class bootstrap_constants { public: - bootstrap_constants (nano::network_constants & network_constants); + explicit bootstrap_constants (nano::network_constants const &); + uint32_t lazy_max_pull_blocks; uint32_t lazy_min_pull_blocks; unsigned frontier_retry_limit; @@ -252,7 +256,7 @@ public: uint32_t default_frontiers_age_seconds; }; -nano::work_thresholds const & work_thresholds_for_network (nano::networks); +nano::work_thresholds work_thresholds_for_network (nano::networks); /** Constants whose value depends on the active network */ class network_params diff --git a/nano/secure/ledger.cpp b/nano/secure/ledger.cpp index bd05c3bf6..93934675b 100644 --- a/nano/secure/ledger.cpp +++ b/nano/secure/ledger.cpp @@ -30,9 +30,10 @@ #include -nano::ledger::ledger (nano::store::component & store_a, nano::ledger_constants & constants_a, nano::stats & stats_a, nano::logger & logger_a, nano::generate_cache_flags generate_cache_flags_a, nano::uint128_t min_rep_weight_a, uint64_t max_backlog_a) : +nano::ledger::ledger (nano::store::component & store_a, nano::network_params const & params_a, nano::stats & stats_a, nano::logger & logger_a, nano::generate_cache_flags generate_cache_flags_a, nano::uint128_t min_rep_weight_a, uint64_t max_backlog_a) : + constants{ params_a.ledger }, + work{ params_a.work }, store{ store_a }, - constants{ constants_a }, stats{ stats_a }, logger{ logger_a }, rep_weights{ store_a.rep_weight, min_rep_weight_a }, @@ -356,11 +357,12 @@ void nano::ledger::confirm_one (secure::write_transaction & transaction, nano::b stats.inc (nano::stat::type::confirmation_height, nano::stat::detail::blocks_confirmed); } -nano::block_status nano::ledger::process (secure::write_transaction const & transaction_a, std::shared_ptr block_a) +nano::block_status nano::ledger::process (secure::write_transaction const & transaction, std::shared_ptr block) { - debug_assert (!constants.work.validate_entry (*block_a) || constants.genesis == nano::dev::genesis); - ledger_processor processor (transaction_a, *this); - block_a->visit (processor); + debug_assert (!work.validate_entry (*block) || constants.genesis == nano::dev::genesis); + + ledger_processor processor (transaction, *this); + block->visit (processor); if (processor.result == nano::block_status::progress) { ++cache.block_count; diff --git a/nano/secure/ledger.hpp b/nano/secure/ledger.hpp index 40dc2d4e8..c539936ff 100644 --- a/nano/secure/ledger.hpp +++ b/nano/secure/ledger.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -37,7 +38,7 @@ class ledger final friend class receivable_iterator; public: - ledger (nano::store::component &, nano::ledger_constants &, nano::stats &, nano::logger &, nano::generate_cache_flags = {}, nano::uint128_t min_rep_weight = 0, uint64_t max_backlog = 0); + ledger (nano::store::component &, nano::network_params const &, nano::stats &, nano::logger &, nano::generate_cache_flags = {}, nano::uint128_t min_rep_weight = 0, uint64_t max_backlog = 0); ~ledger (); /** Start read-write transaction */ @@ -101,8 +102,9 @@ public: public: static nano::uint128_t const unit; + nano::ledger_constants const & constants; + nano::work_thresholds const & work; nano::store::component & store; - nano::ledger_constants & constants; nano::stats & stats; nano::logger & logger; diff --git a/nano/secure/ledger_processor.cpp b/nano/secure/ledger_processor.cpp index 2947bb6a5..a68757960 100644 --- a/nano/secure/ledger_processor.cpp +++ b/nano/secure/ledger_processor.cpp @@ -43,7 +43,7 @@ void nano::ledger_processor::send_block (nano::send_block & block_a) if (result == nano::block_status::progress) { nano::block_details block_details (nano::epoch::epoch_0, false /* unused */, false /* unused */, false /* unused */); - result = ledger.constants.work.difficulty (block_a) >= ledger.constants.work.threshold (block_a.work_version (), block_details) ? nano::block_status::progress : nano::block_status::insufficient_work; // Does this block have sufficient work? (Malformed) + result = ledger.work.difficulty (block_a) >= ledger.work.threshold (block_a.work_version (), block_details) ? nano::block_status::progress : nano::block_status::insufficient_work; // Does this block have sufficient work? (Malformed) if (result == nano::block_status::progress) { debug_assert (!validate_message (account, hash, block_a.signature)); @@ -107,7 +107,7 @@ void nano::ledger_processor::receive_block (nano::receive_block & block_a) if (result == nano::block_status::progress) { nano::block_details block_details (nano::epoch::epoch_0, false /* unused */, false /* unused */, false /* unused */); - result = ledger.constants.work.difficulty (block_a) >= ledger.constants.work.threshold (block_a.work_version (), block_details) ? nano::block_status::progress : nano::block_status::insufficient_work; // Does this block have sufficient work? (Malformed) + result = ledger.work.difficulty (block_a) >= ledger.work.threshold (block_a.work_version (), block_details) ? nano::block_status::progress : nano::block_status::insufficient_work; // Does this block have sufficient work? (Malformed) if (result == nano::block_status::progress) { auto new_balance (info->balance.number () + pending.value ().amount.number ()); @@ -160,7 +160,7 @@ void nano::ledger_processor::open_block (nano::open_block & block_a) if (result == nano::block_status::progress) { nano::block_details block_details (nano::epoch::epoch_0, false /* unused */, false /* unused */, false /* unused */); - result = ledger.constants.work.difficulty (block_a) >= ledger.constants.work.threshold (block_a.work_version (), block_details) ? nano::block_status::progress : nano::block_status::insufficient_work; // Does this block have sufficient work? (Malformed) + result = ledger.work.difficulty (block_a) >= ledger.work.threshold (block_a.work_version (), block_details) ? nano::block_status::progress : nano::block_status::insufficient_work; // Does this block have sufficient work? (Malformed) if (result == nano::block_status::progress) { ledger.store.pending.del (transaction, key); @@ -205,7 +205,7 @@ void nano::ledger_processor::change_block (nano::change_block & block_a) if (result == nano::block_status::progress) { nano::block_details block_details (nano::epoch::epoch_0, false /* unused */, false /* unused */, false /* unused */); - result = ledger.constants.work.difficulty (block_a) >= ledger.constants.work.threshold (block_a.work_version (), block_details) ? nano::block_status::progress : nano::block_status::insufficient_work; // Does this block have sufficient work? (Malformed) + result = ledger.work.difficulty (block_a) >= ledger.work.threshold (block_a.work_version (), block_details) ? nano::block_status::progress : nano::block_status::insufficient_work; // Does this block have sufficient work? (Malformed) if (result == nano::block_status::progress) { debug_assert (!validate_message (account, hash, block_a.signature)); @@ -325,7 +325,7 @@ void nano::ledger_processor::state_block_impl (nano::state_block & block_a) if (result == nano::block_status::progress) { nano::block_details block_details (epoch, is_send, is_receive, false); - result = ledger.constants.work.difficulty (block_a) >= ledger.constants.work.threshold (block_a.work_version (), block_details) ? nano::block_status::progress : nano::block_status::insufficient_work; // Does this block have sufficient work? (Malformed) + result = ledger.work.difficulty (block_a) >= ledger.work.threshold (block_a.work_version (), block_details) ? nano::block_status::progress : nano::block_status::insufficient_work; // Does this block have sufficient work? (Malformed) if (result == nano::block_status::progress) { ledger.stats.inc (nano::stat::type::ledger, nano::stat::detail::state_block); @@ -415,7 +415,7 @@ void nano::ledger_processor::epoch_block_impl (nano::state_block & block_a) if (result == nano::block_status::progress) { nano::block_details block_details (epoch, false, false, true); - result = ledger.constants.work.difficulty (block_a) >= ledger.constants.work.threshold (block_a.work_version (), block_details) ? nano::block_status::progress : nano::block_status::insufficient_work; // Does this block have sufficient work? (Malformed) + result = ledger.work.difficulty (block_a) >= ledger.work.threshold (block_a.work_version (), block_details) ? nano::block_status::progress : nano::block_status::insufficient_work; // Does this block have sufficient work? (Malformed) if (result == nano::block_status::progress) { ledger.stats.inc (nano::stat::type::ledger, nano::stat::detail::epoch_block); diff --git a/nano/slow_test/node.cpp b/nano/slow_test/node.cpp index c42a51428..d933dd65b 100644 --- a/nano/slow_test/node.cpp +++ b/nano/slow_test/node.cpp @@ -130,7 +130,7 @@ TEST (ledger, deep_account_compute) nano::logger logger; auto store = nano::make_store (logger, nano::unique_path (), nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); auto transaction = ledger.tx_begin_write (); nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; nano::keypair key; @@ -323,7 +323,7 @@ TEST (node, fork_storm) else { nano::unique_lock lock{ node_a->active.mutex }; - auto election = node_a->active.roots.begin ()->election; + auto election = *node_a->active.list_active ().begin (); lock.unlock (); if (election->votes ().size () == 1) { @@ -1131,7 +1131,7 @@ TEST (confirmation_height, many_accounts_send_receive_self_no_elections) auto store = nano::make_store (logger, path, nano::dev::constants); nano::stats stats{ logger }; - nano::ledger ledger (*store, nano::dev::constants, stats, logger); + nano::ledger ledger (*store, nano::dev::network_params, stats, logger); nano::store::write_queue write_database_queue; nano::work_pool pool{ nano::dev::network_params.network, std::numeric_limits::max () }; std::atomic stopped{ false }; @@ -1794,7 +1794,7 @@ TEST (node, mass_block_new) // Clear all active { nano::lock_guard guard{ node.active.mutex }; - node.active.roots.clear (); + node.active.clear (); } }; @@ -2159,12 +2159,12 @@ TEST (system, block_sequence) { message += boost::str (boost::format ("N:%1% b:%2% c:%3% a:%4% s:%5% p:%6%\n") % std::to_string (i->network.port) % std::to_string (i->ledger.block_count ()) % std::to_string (i->ledger.cemented_count ()) % std::to_string (i->active.size ()) % std::to_string (i->scheduler.priority.size ()) % std::to_string (i->network.size ())); nano::lock_guard lock{ i->active.mutex }; - for (auto const & j : i->active.roots) + for (auto const & j : i->active.list_active ()) { - auto election = j.election; + auto election = j; if (election->confirmation_request_count > 10) { - message += boost::str (boost::format ("\t r:%1% i:%2%\n") % j.root.to_string () % std::to_string (election->confirmation_request_count)); + message += boost::str (boost::format ("\t r:%1% i:%2%\n") % j->root.to_string () % std::to_string (election->confirmation_request_count)); for (auto const & k : election->votes ()) { message += boost::str (boost::format ("\t\t r:%1% t:%2%\n") % k.first.to_account () % std::to_string (k.second.timestamp)); diff --git a/nano/store/component.cpp b/nano/store/component.cpp index 0dda015fd..df292ebb3 100644 --- a/nano/store/component.cpp +++ b/nano/store/component.cpp @@ -28,7 +28,7 @@ nano::store::component::component (nano::store::block & block_store_a, nano::sto * If using a different store version than the latest then you may need * to modify some of the objects in the store to be appropriate for the version before an upgrade. */ -void nano::store::component::initialize (store::write_transaction const & transaction, nano::ledger_constants & constants) +void nano::store::component::initialize (store::write_transaction const & transaction, nano::ledger_constants const & constants) { release_assert (constants.genesis->has_sideband ()); release_assert (account.begin (transaction) == account.end (transaction)); diff --git a/nano/store/component.hpp b/nano/store/component.hpp index 7a5b79b6d..f8ff35c25 100644 --- a/nano/store/component.hpp +++ b/nano/store/component.hpp @@ -50,7 +50,7 @@ namespace store virtual ~component () = default; - void initialize (store::write_transaction const &, nano::ledger_constants &); + void initialize (store::write_transaction const &, nano::ledger_constants const &); virtual uint64_t count (store::transaction const & transaction_a, tables table_a) const = 0; virtual int drop (store::write_transaction const & transaction_a, tables table_a) = 0; diff --git a/nano/test_common/CMakeLists.txt b/nano/test_common/CMakeLists.txt index 56d6a53a4..35d655cb7 100644 --- a/nano/test_common/CMakeLists.txt +++ b/nano/test_common/CMakeLists.txt @@ -8,6 +8,8 @@ add_library( make_store.cpp network.hpp network.cpp + random.hpp + random.cpp rate_observer.cpp rate_observer.hpp system.hpp diff --git a/nano/test_common/ledger_context.cpp b/nano/test_common/ledger_context.cpp index 5a4b6b91a..d98415e6a 100644 --- a/nano/test_common/ledger_context.cpp +++ b/nano/test_common/ledger_context.cpp @@ -5,7 +5,7 @@ nano::test::ledger_context::ledger_context (std::deque> && blocks) : store_m{ nano::make_store (logger_m, nano::unique_path (), nano::dev::constants) }, - ledger_m{ *store_m, nano::dev::constants, stats_m, logger_m }, + ledger_m{ *store_m, nano::dev::network_params, stats_m, logger_m }, blocks_m{ blocks }, pool_m{ nano::dev::network_params.network, 1 } { diff --git a/nano/test_common/random.cpp b/nano/test_common/random.cpp new file mode 100644 index 000000000..38b1ef856 --- /dev/null +++ b/nano/test_common/random.cpp @@ -0,0 +1,46 @@ +#include +#include + +nano::hash_or_account nano::test::random_hash_or_account () +{ + nano::hash_or_account random_hash; + nano::random_pool::generate_block (random_hash.bytes.data (), random_hash.bytes.size ()); + return random_hash; +} + +nano::block_hash nano::test::random_hash () +{ + return nano::test::random_hash_or_account ().as_block_hash (); +} + +nano::account nano::test::random_account () +{ + return nano::test::random_hash_or_account ().as_account (); +} + +nano::qualified_root nano::test::random_qualified_root () +{ + return { nano::test::random_hash (), nano::test::random_hash () }; +} + +nano::amount nano::test::random_amount () +{ + nano::amount result; + nano::random_pool::generate_block (result.bytes.data (), result.bytes.size ()); + return result; +} + +std::shared_ptr nano::test::random_block () +{ + nano::keypair key; + auto block = std::make_shared ( + nano::test::random_account (), + nano::test::random_hash (), + nano::test::random_account (), + nano::test::random_amount (), + nano::test::random_hash (), + key.prv, + key.pub, + 0); + return block; +} \ No newline at end of file diff --git a/nano/test_common/random.hpp b/nano/test_common/random.hpp new file mode 100644 index 000000000..a1d41c8ee --- /dev/null +++ b/nano/test_common/random.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +namespace nano::test +{ +/* + * Random generators + */ +nano::hash_or_account random_hash_or_account (); +nano::block_hash random_hash (); +nano::account random_account (); +nano::qualified_root random_qualified_root (); +nano::amount random_amount (); +std::shared_ptr random_block (); +} \ No newline at end of file diff --git a/nano/test_common/system.cpp b/nano/test_common/system.cpp index 5a947ac92..3bdc8a485 100644 --- a/nano/test_common/system.cpp +++ b/nano/test_common/system.cpp @@ -337,6 +337,8 @@ std::error_code nano::test::system::poll (std::chrono::nanoseconds const & wait_ } } + std::this_thread::yield (); + std::error_code ec; if (std::chrono::steady_clock::now () > deadline) { diff --git a/nano/test_common/testutil.cpp b/nano/test_common/testutil.cpp index 9dbe4210d..fd476b627 100644 --- a/nano/test_common/testutil.cpp +++ b/nano/test_common/testutil.cpp @@ -50,23 +50,6 @@ void nano::test::wait_peer_connections (nano::test::system & system_a) wait_peer_count (false); } -nano::hash_or_account nano::test::random_hash_or_account () -{ - nano::hash_or_account random_hash; - nano::random_pool::generate_block (random_hash.bytes.data (), random_hash.bytes.size ()); - return random_hash; -} - -nano::block_hash nano::test::random_hash () -{ - return nano::test::random_hash_or_account ().as_block_hash (); -} - -nano::account nano::test::random_account () -{ - return nano::test::random_hash_or_account ().as_account (); -} - bool nano::test::process (nano::node & node, std::vector> blocks) { auto const transaction = node.ledger.tx_begin_write (); @@ -266,48 +249,34 @@ std::shared_ptr nano::test::test_channel (nano::n return channel; } -std::shared_ptr nano::test::start_election (nano::test::system & system_a, nano::node & node_a, const nano::block_hash & hash_a) +std::shared_ptr nano::test::start_election (nano::test::system & system, nano::node & node, const nano::block_hash & hash) { - system_a.deadline_set (5s); + system.deadline_set (5s); - // wait until and ensure that the block is in the ledger - auto block_l = node_a.block (hash_a); - while (!block_l) - { - if (system_a.poll ()) - { - return nullptr; - } - block_l = node_a.block (hash_a); - } + // Wait until and ensure that the block is in the ledger + auto block_l = node.block (hash); + debug_assert (block_l); - node_a.scheduler.manual.push (block_l); + auto fut = node.scheduler.manual.push (block_l); - // wait for the election to appear - std::shared_ptr election = node_a.active.election (block_l->qualified_root ()); - while (!election) - { - if (system_a.poll ()) - { - return nullptr; - } - election = node_a.active.election (block_l->qualified_root ()); - } + // Wait for the block to be scheduled + auto status = fut.wait_for (5s); + debug_assert (status == std::future_status::ready); - election->transition_active (); + auto election = fut.get (); return election; } -bool nano::test::start_elections (nano::test::system & system_a, nano::node & node_a, std::vector const & hashes_a, bool const forced_a) +bool nano::test::start_elections (nano::test::system & system, nano::node & node, std::vector const & hashes, bool const forced) { - for (auto const & hash_l : hashes_a) + for (auto const & hash_l : hashes) { - auto election = nano::test::start_election (system_a, node_a, hash_l); + auto election = start_election (system, node, hash_l); if (!election) { return false; } - if (forced_a) + if (forced) { election->force_confirm (); } @@ -315,9 +284,9 @@ bool nano::test::start_elections (nano::test::system & system_a, nano::node & no return true; } -bool nano::test::start_elections (nano::test::system & system_a, nano::node & node_a, std::vector> const & blocks_a, bool const forced_a) +bool nano::test::start_elections (nano::test::system & system, nano::node & node, std::vector> const & blocks, bool const forced) { - return nano::test::start_elections (system_a, node_a, blocks_to_hashes (blocks_a), forced_a); + return start_elections (system, node, blocks_to_hashes (blocks), forced); } nano::account_info nano::test::account_info (nano::node const & node, nano::account const & acc) diff --git a/nano/test_common/testutil.hpp b/nano/test_common/testutil.hpp index b0e162f59..ddf3e162f 100644 --- a/nano/test_common/testutil.hpp +++ b/nano/test_common/testutil.hpp @@ -57,25 +57,30 @@ } /** Expects that the condition becomes true within the deadline */ -#define EXPECT_TIMELY(time, condition) \ - system.deadline_set (time); \ - { \ - std::error_code _ec; \ - while (!(condition) && !(_ec = system.poll ())) \ - { \ - } \ - EXPECT_NO_ERROR (_ec); \ +#define EXPECT_TIMELY(time, condition) \ + system.deadline_set (time); \ + { \ + std::error_code _ec; \ + while (!_ec && !(condition)) \ + { \ + _ec = system.poll (); \ + } \ + EXPECT_NO_ERROR (_ec); \ } /* * Asserts that the `val1 == val2` condition becomes true within the deadline * Condition must hold for at least 2 consecutive reads */ -#define ASSERT_TIMELY_EQ(time, val1, val2) \ - system.deadline_set (time); \ - while (!((val1) == (val2)) && !system.poll ()) \ - { \ - } \ +#define ASSERT_TIMELY_EQ(time, val1, val2) \ + system.deadline_set (time); \ + { \ + std::error_code _ec; \ + while (!_ec && !((val1) == (val2))) \ + { \ + _ec = system.poll (); \ + } \ + } \ ASSERT_EQ (val1, val2); /* @@ -265,19 +270,6 @@ namespace test void wait_peer_connections (nano::test::system &); - /** - * Generate a random block hash - */ - nano::hash_or_account random_hash_or_account (); - /** - * Generate a random block hash - */ - nano::block_hash random_hash (); - /** - * Generate a random block hash - */ - nano::account random_account (); - /** Convenience function to call `node::process` function for multiple blocks at once. @return true if all blocks were successfully processed and inserted into ledger