Merge pull request #4829 from pwojcikdev/frontier-scan-tests
Tests for `frontier_scan` class
This commit is contained in:
commit
5e5905f3c4
5 changed files with 248 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
242
nano/core_test/bootstrap_frontier_scan.cpp
Normal file
242
nano/core_test/bootstrap_frontier_scan.cpp
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
#include <nano/lib/blocks.hpp>
|
||||
#include <nano/lib/logging.hpp>
|
||||
#include <nano/lib/stats.hpp>
|
||||
#include <nano/lib/tomlconfig.hpp>
|
||||
#include <nano/node/bootstrap/bootstrap_service.hpp>
|
||||
#include <nano/node/bootstrap/frontier_scan.hpp>
|
||||
#include <nano/test_common/system.hpp>
|
||||
#include <nano/test_common/testutil.hpp>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
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<std::pair<nano::account, nano::block_hash>> 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<std::pair<nano::account, nano::block_hash>> response;
|
||||
response.push_back ({ nano::account{ std::numeric_limits<nano::uint256_t>::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<std::pair<nano::account, nano::block_hash>> response1;
|
||||
for (int i = 0; i <= 9; i += 3)
|
||||
{
|
||||
response1.push_back ({ nano::account{ start.number () + i }, nano::block_hash{ static_cast<uint64_t> (i) } });
|
||||
}
|
||||
ASSERT_FALSE (frontier_scan.process (start, response1));
|
||||
|
||||
// Response contains: 1, 3, 5, 7, 9
|
||||
std::deque<std::pair<nano::account, nano::block_hash>> response2;
|
||||
for (int i = 0; i <= 8; i += 2)
|
||||
{
|
||||
response2.push_back ({ nano::account{ start.number () + i }, nano::block_hash{ static_cast<uint64_t> (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<nano::account> 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<std::pair<nano::account, nano::block_hash>> 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<std::pair<nano::account, nano::block_hash>> 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<std::pair<nano::account, nano::block_hash>> 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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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<nano::uint256_t>::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 } });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue