From fe2527abe0b9e3e8b20a7bbe1f38520b4bcabffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Wo=CC=81jcik?= <3044353+pwojcikdev@users.noreply.github.com> Date: Sun, 19 Jan 2025 23:13:03 +0100 Subject: [PATCH 1/2] Fix typo --- nano/core_test/node.cpp | 2 +- nano/node/bootstrap/bootstrap_config.hpp | 2 +- nano/node/bootstrap/frontier_scan.cpp | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nano/core_test/node.cpp b/nano/core_test/node.cpp index 213103785..5fcec4db3 100644 --- a/nano/core_test/node.cpp +++ b/nano/core_test/node.cpp @@ -1387,7 +1387,7 @@ TEST (node, bootstrap_fork_open) nano::test::system system; nano::node_config node_config (system.get_available_port ()); node_config.bootstrap.account_sets.cooldown = 100ms; // Reduce cooldown to speed up fork resolution - node_config.bootstrap.frontier_scan.head_parallelistm = 3; // Make sure we can process the full account number range + node_config.bootstrap.frontier_scan.head_parallelism = 3; // Make sure we can process the full account number range node_config.bootstrap.frontier_rate_limit = 0; // Disable rate limiting to speed up the scan // Disable automatic election activation node_config.backlog_scan.enable = false; diff --git a/nano/node/bootstrap/bootstrap_config.hpp b/nano/node/bootstrap/bootstrap_config.hpp index 289680368..a21bc0a4a 100644 --- a/nano/node/bootstrap/bootstrap_config.hpp +++ b/nano/node/bootstrap/bootstrap_config.hpp @@ -26,7 +26,7 @@ class frontier_scan_config final public: // TODO: Serialize & deserialize - unsigned head_parallelistm{ 128 }; + unsigned head_parallelism{ 128 }; unsigned consideration_count{ 4 }; std::size_t candidates{ 1000 }; std::chrono::milliseconds cooldown{ 1000 * 5 }; diff --git a/nano/node/bootstrap/frontier_scan.cpp b/nano/node/bootstrap/frontier_scan.cpp index a65d64fa5..9d80a4796 100644 --- a/nano/node/bootstrap/frontier_scan.cpp +++ b/nano/node/bootstrap/frontier_scan.cpp @@ -9,13 +9,13 @@ nano::bootstrap::frontier_scan::frontier_scan (frontier_scan_config const & conf { // Divide nano::account numeric range into consecutive and equal ranges nano::uint256_t max_account = std::numeric_limits::max (); - nano::uint256_t range_size = max_account / config.head_parallelistm; + nano::uint256_t range_size = max_account / config.head_parallelism; - for (unsigned i = 0; i < config.head_parallelistm; ++i) + for (unsigned i = 0; i < config.head_parallelism; ++i) { // Start at 1 to avoid the burn account nano::uint256_t start = (i == 0) ? 1 : i * range_size; - nano::uint256_t end = (i == config.head_parallelistm - 1) ? max_account : start + range_size; + nano::uint256_t end = (i == config.head_parallelism - 1) ? max_account : start + range_size; heads.emplace_back (frontier_head{ nano::account{ start }, nano::account{ end } }); } From 62ef69065df2052366cf0c1b9fdefa659a6866fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Wo=CC=81jcik?= <3044353+pwojcikdev@users.noreply.github.com> Date: Sat, 18 Jan 2025 21:07:33 +0100 Subject: [PATCH 2/2] Frontier scan tests --- nano/core_test/CMakeLists.txt | 1 + nano/core_test/bootstrap_frontier_scan.cpp | 242 +++++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 nano/core_test/bootstrap_frontier_scan.cpp diff --git a/nano/core_test/CMakeLists.txt b/nano/core_test/CMakeLists.txt index 28f6f553a..ebb06a69a 100644 --- a/nano/core_test/CMakeLists.txt +++ b/nano/core_test/CMakeLists.txt @@ -11,6 +11,7 @@ add_executable( block_store.cpp block_processor.cpp bootstrap.cpp + bootstrap_frontier_scan.cpp bootstrap_server.cpp bucketing.cpp cli.cpp diff --git a/nano/core_test/bootstrap_frontier_scan.cpp b/nano/core_test/bootstrap_frontier_scan.cpp new file mode 100644 index 000000000..ad78f2c65 --- /dev/null +++ b/nano/core_test/bootstrap_frontier_scan.cpp @@ -0,0 +1,242 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace std::chrono_literals; + +namespace +{ +struct test_context +{ + nano::stats stats; + nano::frontier_scan_config config; + nano::bootstrap::frontier_scan frontier_scan; + + explicit test_context (nano::frontier_scan_config config_a = {}) : + stats{ nano::default_logger () }, + config{ config_a }, + frontier_scan{ config, stats } + { + } +}; +} + +TEST (bootstrap_frontier_scan, construction) +{ + test_context ctx{}; + auto & frontier_scan = ctx.frontier_scan; +} + +TEST (bootstrap_frontier_scan, next_basic) +{ + nano::frontier_scan_config config; + config.head_parallelism = 2; // Two heads for simpler testing + config.consideration_count = 3; + test_context ctx{ config }; + auto & frontier_scan = ctx.frontier_scan; + + // First call should return first head, account number 1 (avoiding burn account 0) + auto first = frontier_scan.next (); + ASSERT_EQ (first.number (), 1); + + // Second call should return second head, account number 0x7FF... (half the range) + auto second = frontier_scan.next (); + ASSERT_EQ (second.number (), nano::account{ "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" }); + + // Third call should return first head again, sequentially iterating through heads + auto third = frontier_scan.next (); + ASSERT_EQ (third.number (), 1); +} + +TEST (bootstrap_frontier_scan, process_basic) +{ + nano::frontier_scan_config config; + config.head_parallelism = 1; // Single head for simpler testing + config.consideration_count = 3; + config.candidates = 5; + test_context ctx{ config }; + auto & frontier_scan = ctx.frontier_scan; + + // Get initial account to scan + auto start = frontier_scan.next (); + ASSERT_EQ (start.number (), 1); + + // Create response with some frontiers + std::deque> response; + response.push_back ({ nano::account{ 2 }, nano::block_hash{ 1 } }); + response.push_back ({ nano::account{ 3 }, nano::block_hash{ 2 } }); + + // Process should not be done until consideration_count is reached + ASSERT_FALSE (frontier_scan.process (start, response)); + ASSERT_FALSE (frontier_scan.process (start, response)); + + // Head should not advance before reaching `consideration_count` responses + ASSERT_EQ (frontier_scan.next (), 1); + + // After consideration_count responses, should be done + ASSERT_TRUE (frontier_scan.process (start, response)); + + // Head should advance to next account and start subsequent scan from there + ASSERT_EQ (frontier_scan.next (), 3); +} + +TEST (bootstrap_frontier_scan, range_wrap_around) +{ + nano::frontier_scan_config config; + config.head_parallelism = 1; + config.consideration_count = 1; + config.candidates = 1; + test_context ctx{ config }; + auto & frontier_scan = ctx.frontier_scan; + + auto start = frontier_scan.next (); + + // Create response that would push next beyond the range end + std::deque> response; + response.push_back ({ nano::account{ std::numeric_limits::max () }, nano::block_hash{ 1 } }); + + // Process should succeed and wrap around + ASSERT_TRUE (frontier_scan.process (start, response)); + + // Next account should be back at start of range + auto next = frontier_scan.next (); + ASSERT_EQ (next.number (), 1); +} + +TEST (bootstrap_frontier_scan, cooldown) +{ + nano::frontier_scan_config config; + config.head_parallelism = 1; + config.consideration_count = 1; + config.cooldown = 250ms; + test_context ctx{ config }; + auto & frontier_scan = ctx.frontier_scan; + + // First call should succeed + auto first = frontier_scan.next (); + ASSERT_NE (first.number (), 0); + + // Immediate second call should fail (return 0) + auto second = frontier_scan.next (); + ASSERT_EQ (second.number (), 0); + + // After cooldown, should succeed again + std::this_thread::sleep_for (500ms); + auto third = frontier_scan.next (); + ASSERT_NE (third.number (), 0); +} + +TEST (bootstrap_frontier_scan, candidate_trimming) +{ + nano::frontier_scan_config config; + config.head_parallelism = 1; + config.consideration_count = 2; + config.candidates = 3; // Only keep the lowest candidates + test_context ctx{ config }; + auto & frontier_scan = ctx.frontier_scan; + + auto start = frontier_scan.next (); + ASSERT_EQ (start.number (), 1); + + // Create response with more candidates than limit + // Response contains: 1, 4, 7, 10 + std::deque> response1; + for (int i = 0; i <= 9; i += 3) + { + response1.push_back ({ nano::account{ start.number () + i }, nano::block_hash{ static_cast (i) } }); + } + ASSERT_FALSE (frontier_scan.process (start, response1)); + + // Response contains: 1, 3, 5, 7, 9 + std::deque> response2; + for (int i = 0; i <= 8; i += 2) + { + response2.push_back ({ nano::account{ start.number () + i }, nano::block_hash{ static_cast (i) } }); + } + ASSERT_TRUE (frontier_scan.process (start, response2)); + + // After processing replies candidates should be ordered and trimmed + auto next = frontier_scan.next (); + ASSERT_EQ (next.number (), 5); +} + +TEST (bootstrap_frontier_scan, heads_distribution) +{ + nano::frontier_scan_config config; + config.head_parallelism = 4; + test_context ctx{ config }; + auto & frontier_scan = ctx.frontier_scan; + + // Collect initial accounts from each head + std::vector initial_accounts; + for (int i = 0; i < 4; i++) + { + initial_accounts.push_back (frontier_scan.next ()); + } + + // Verify accounts are properly distributed across the range + for (size_t i = 1; i < initial_accounts.size (); i++) + { + ASSERT_GT (initial_accounts[i].number (), initial_accounts[i - 1].number ()); + } +} + +TEST (bootstrap_frontier_scan, invalid_response_ordering) +{ + nano::frontier_scan_config config; + config.head_parallelism = 1; + config.consideration_count = 1; + test_context ctx{ config }; + auto & frontier_scan = ctx.frontier_scan; + + auto start = frontier_scan.next (); + + // Create response with out-of-order accounts + std::deque> response; + response.push_back ({ nano::account{ start.number () + 2 }, nano::block_hash{ 1 } }); + response.push_back ({ nano::account{ start.number () + 1 }, nano::block_hash{ 2 } }); // Out of order + + // Should still process successfully + ASSERT_TRUE (frontier_scan.process (start, response)); + ASSERT_EQ (frontier_scan.next (), start.number () + 2); +} + +TEST (bootstrap_frontier_scan, empty_responses) +{ + nano::frontier_scan_config config; + config.head_parallelism = 1; + config.consideration_count = 2; + test_context ctx{ config }; + auto & frontier_scan = ctx.frontier_scan; + + auto start = frontier_scan.next (); + + // Empty response should not advance head even after receiving `consideration_count` responses + std::deque> empty_response; + ASSERT_FALSE (frontier_scan.process (start, empty_response)); + ASSERT_FALSE (frontier_scan.process (start, empty_response)); + ASSERT_EQ (frontier_scan.next (), start); + + // Let the head advance + std::deque> response; + response.push_back ({ nano::account{ start.number () + 1 }, nano::block_hash{ 1 } }); + ASSERT_TRUE (frontier_scan.process (start, response)); + ASSERT_EQ (frontier_scan.next (), start.number () + 1); + + // However, after receiving enough empty responses, head should wrap around to the start + ASSERT_FALSE (frontier_scan.process (start, empty_response)); + ASSERT_FALSE (frontier_scan.process (start, empty_response)); + ASSERT_FALSE (frontier_scan.process (start, empty_response)); + ASSERT_EQ (frontier_scan.next (), start.number () + 1); + ASSERT_TRUE (frontier_scan.process (start, empty_response)); + ASSERT_EQ (frontier_scan.next (), start); // Wraps around +} \ No newline at end of file