commit
6957afa142
18 changed files with 714 additions and 12 deletions
|
|
@ -25,6 +25,7 @@ add_executable(
|
|||
enums.cpp
|
||||
epochs.cpp
|
||||
fair_queue.cpp
|
||||
fork_cache.cpp
|
||||
ipc.cpp
|
||||
ledger.cpp
|
||||
ledger_confirm.cpp
|
||||
|
|
|
|||
310
nano/core_test/fork_cache.cpp
Normal file
310
nano/core_test/fork_cache.cpp
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
#include <nano/node/fork_cache.hpp>
|
||||
#include <nano/test_common/system.hpp>
|
||||
#include <nano/test_common/testutil.hpp>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <map>
|
||||
|
||||
TEST (fork_cache, construction)
|
||||
{
|
||||
nano::test::system system;
|
||||
nano::fork_cache_config cfg;
|
||||
nano::fork_cache fork_cache{ cfg, system.stats };
|
||||
ASSERT_EQ (0, fork_cache.size ());
|
||||
ASSERT_FALSE (fork_cache.contains (nano::qualified_root{}));
|
||||
}
|
||||
|
||||
/*
|
||||
* Inserts a single block to cache, ensures it can be retrieved
|
||||
*/
|
||||
TEST (fork_cache, one)
|
||||
{
|
||||
nano::test::system system;
|
||||
nano::fork_cache_config cfg;
|
||||
nano::fork_cache fork_cache{ cfg, system.stats };
|
||||
|
||||
auto block = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, nano::dev::genesis->hash (), nano::dev::genesis_key.pub, nano::Knano_ratio, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
nano::qualified_root root = block->qualified_root ();
|
||||
|
||||
fork_cache.put (block);
|
||||
ASSERT_EQ (1, fork_cache.size ());
|
||||
ASSERT_TRUE (fork_cache.contains (root));
|
||||
|
||||
auto blocks = fork_cache.get (root);
|
||||
ASSERT_EQ (1, blocks.size ());
|
||||
ASSERT_EQ (block, blocks.front ());
|
||||
}
|
||||
|
||||
/*
|
||||
* Inserts multiple blocks with same root, ensures all are retrievable
|
||||
*/
|
||||
TEST (fork_cache, multiple_forks)
|
||||
{
|
||||
nano::test::system system;
|
||||
nano::fork_cache_config cfg;
|
||||
nano::fork_cache fork_cache{ cfg, system.stats };
|
||||
|
||||
// Create several blocks with the same qualified root (same previous and account)
|
||||
auto block1 = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, nano::dev::genesis->hash (), nano::dev::genesis_key.pub, nano::Knano_ratio, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
auto block2 = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, nano::dev::genesis->hash (), nano::dev::genesis_key.pub, nano::Knano_ratio * 2, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
auto block3 = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, nano::dev::genesis->hash (), nano::dev::genesis_key.pub, nano::Knano_ratio * 3, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
|
||||
nano::qualified_root root = block1->qualified_root ();
|
||||
ASSERT_EQ (root, block2->qualified_root ());
|
||||
ASSERT_EQ (root, block3->qualified_root ());
|
||||
|
||||
fork_cache.put (block1);
|
||||
fork_cache.put (block2);
|
||||
fork_cache.put (block3);
|
||||
|
||||
ASSERT_EQ (1, fork_cache.size ()); // Only one root in the cache
|
||||
ASSERT_TRUE (fork_cache.contains (root));
|
||||
|
||||
auto blocks = fork_cache.get (root);
|
||||
ASSERT_EQ (3, blocks.size ());
|
||||
|
||||
// Check if all blocks are present
|
||||
ASSERT_TRUE (std::find (blocks.begin (), blocks.end (), block1) != blocks.end ());
|
||||
ASSERT_TRUE (std::find (blocks.begin (), blocks.end (), block2) != blocks.end ());
|
||||
ASSERT_TRUE (std::find (blocks.begin (), blocks.end (), block3) != blocks.end ());
|
||||
}
|
||||
|
||||
/*
|
||||
* Inserts multiple blocks with different roots, ensures all can be retrieved
|
||||
*/
|
||||
TEST (fork_cache, multiple_roots)
|
||||
{
|
||||
nano::test::system system;
|
||||
nano::fork_cache_config cfg;
|
||||
nano::fork_cache fork_cache{ cfg, system.stats };
|
||||
|
||||
// Create blocks with different roots
|
||||
auto block1 = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, nano::test::random_hash (), nano::dev::genesis_key.pub, nano::Knano_ratio, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
|
||||
nano::keypair key2;
|
||||
auto block2 = std::make_shared<nano::state_block> (key2.pub, nano::test::random_hash (), key2.pub, nano::Knano_ratio, nano::test::random_hash (), key2.prv, key2.pub, 0);
|
||||
|
||||
nano::keypair key3;
|
||||
auto block3 = std::make_shared<nano::state_block> (key3.pub, nano::test::random_hash (), key3.pub, nano::Knano_ratio, nano::test::random_hash (), key3.prv, key3.pub, 0);
|
||||
|
||||
nano::qualified_root root1 = block1->qualified_root ();
|
||||
nano::qualified_root root2 = block2->qualified_root ();
|
||||
nano::qualified_root root3 = block3->qualified_root ();
|
||||
|
||||
// Make sure roots are different
|
||||
ASSERT_NE (root1, root2);
|
||||
ASSERT_NE (root1, root3);
|
||||
ASSERT_NE (root2, root3);
|
||||
|
||||
fork_cache.put (block1);
|
||||
fork_cache.put (block2);
|
||||
fork_cache.put (block3);
|
||||
|
||||
ASSERT_EQ (3, fork_cache.size ());
|
||||
ASSERT_TRUE (fork_cache.contains (root1));
|
||||
ASSERT_TRUE (fork_cache.contains (root2));
|
||||
ASSERT_TRUE (fork_cache.contains (root3));
|
||||
|
||||
auto blocks1 = fork_cache.get (root1);
|
||||
ASSERT_EQ (1, blocks1.size ());
|
||||
ASSERT_EQ (block1, blocks1.front ());
|
||||
|
||||
auto blocks2 = fork_cache.get (root2);
|
||||
ASSERT_EQ (1, blocks2.size ());
|
||||
ASSERT_EQ (block2, blocks2.front ());
|
||||
|
||||
auto blocks3 = fork_cache.get (root3);
|
||||
ASSERT_EQ (1, blocks3.size ());
|
||||
ASSERT_EQ (block3, blocks3.front ());
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests duplicate block handling (same block twice)
|
||||
*/
|
||||
TEST (fork_cache, duplicate_block)
|
||||
{
|
||||
nano::test::system system;
|
||||
nano::fork_cache_config cfg;
|
||||
nano::fork_cache fork_cache{ cfg, system.stats };
|
||||
|
||||
auto block = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, nano::dev::genesis->hash (), nano::dev::genesis_key.pub, nano::Knano_ratio, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
nano::qualified_root root = block->qualified_root ();
|
||||
|
||||
// Insert the same block twice
|
||||
fork_cache.put (block);
|
||||
ASSERT_EQ (1, fork_cache.size ());
|
||||
ASSERT_EQ (1, fork_cache.get (root).size ());
|
||||
|
||||
// Check the stats for insert count
|
||||
ASSERT_EQ (1, system.stats.count (nano::stat::type::fork_cache, nano::stat::detail::insert));
|
||||
|
||||
fork_cache.put (block);
|
||||
ASSERT_EQ (1, fork_cache.size ());
|
||||
|
||||
// Block should only be added once to the deque
|
||||
auto blocks = fork_cache.get (root);
|
||||
ASSERT_EQ (1, blocks.size ());
|
||||
ASSERT_EQ (block, blocks.front ());
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests that when max_forks_per_root is exceeded, oldest entries are dropped
|
||||
*/
|
||||
TEST (fork_cache, overfill_per_root)
|
||||
{
|
||||
nano::test::system system;
|
||||
nano::fork_cache_config cfg;
|
||||
cfg.max_forks_per_root = 2;
|
||||
nano::fork_cache fork_cache{ cfg, system.stats };
|
||||
|
||||
// Create several blocks with the same qualified root
|
||||
auto block1 = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, nano::dev::genesis->hash (), nano::dev::genesis_key.pub, nano::Knano_ratio, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
auto block2 = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, nano::dev::genesis->hash (), nano::dev::genesis_key.pub, nano::Knano_ratio * 2, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
auto block3 = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, nano::dev::genesis->hash (), nano::dev::genesis_key.pub, nano::Knano_ratio * 3, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
|
||||
nano::qualified_root root = block1->qualified_root ();
|
||||
|
||||
// Insert all three blocks
|
||||
fork_cache.put (block1);
|
||||
fork_cache.put (block2);
|
||||
fork_cache.put (block3);
|
||||
|
||||
ASSERT_EQ (1, fork_cache.size ());
|
||||
auto blocks = fork_cache.get (root);
|
||||
ASSERT_EQ (2, blocks.size ()); // Only 2 blocks should be kept
|
||||
|
||||
// The oldest block (block1) should have been removed
|
||||
ASSERT_FALSE (std::find (blocks.begin (), blocks.end (), block1) != blocks.end ());
|
||||
ASSERT_TRUE (std::find (blocks.begin (), blocks.end (), block2) != blocks.end ());
|
||||
ASSERT_TRUE (std::find (blocks.begin (), blocks.end (), block3) != blocks.end ());
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests that when max_size is exceeded, oldest root entries are dropped
|
||||
*/
|
||||
TEST (fork_cache, overfill_total)
|
||||
{
|
||||
nano::test::system system;
|
||||
nano::fork_cache_config cfg;
|
||||
cfg.max_size = 2;
|
||||
nano::fork_cache fork_cache{ cfg, system.stats };
|
||||
|
||||
// Create blocks with different roots
|
||||
auto block1 = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, nano::test::random_hash (), nano::dev::genesis_key.pub, nano::Knano_ratio, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
|
||||
nano::keypair key2;
|
||||
auto block2 = std::make_shared<nano::state_block> (key2.pub, nano::test::random_hash (), key2.pub, nano::Knano_ratio, nano::test::random_hash (), key2.prv, key2.pub, 0);
|
||||
|
||||
nano::keypair key3;
|
||||
auto block3 = std::make_shared<nano::state_block> (key3.pub, nano::test::random_hash (), key3.pub, nano::Knano_ratio, nano::test::random_hash (), key3.prv, key3.pub, 0);
|
||||
|
||||
nano::qualified_root root1 = block1->qualified_root ();
|
||||
nano::qualified_root root2 = block2->qualified_root ();
|
||||
nano::qualified_root root3 = block3->qualified_root ();
|
||||
|
||||
// Make sure roots are different
|
||||
ASSERT_NE (root1, root2);
|
||||
ASSERT_NE (root1, root3);
|
||||
ASSERT_NE (root2, root3);
|
||||
|
||||
// Insert all three blocks
|
||||
fork_cache.put (block1);
|
||||
fork_cache.put (block2);
|
||||
fork_cache.put (block3);
|
||||
|
||||
ASSERT_EQ (2, fork_cache.size ()); // Only 2 roots should be kept
|
||||
|
||||
// The oldest root (root1) should have been removed
|
||||
ASSERT_FALSE (fork_cache.contains (root1));
|
||||
ASSERT_TRUE (fork_cache.contains (root2));
|
||||
ASSERT_TRUE (fork_cache.contains (root3));
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests a more complex scenario with multiple roots and multiple forks per root
|
||||
*/
|
||||
TEST (fork_cache, complex_scenario)
|
||||
{
|
||||
nano::test::system system;
|
||||
nano::fork_cache_config cfg;
|
||||
cfg.max_size = 2;
|
||||
cfg.max_forks_per_root = 2;
|
||||
nano::fork_cache fork_cache{ cfg, system.stats };
|
||||
|
||||
// Create multiple blocks for first root
|
||||
auto const previous1 = nano::test::random_hash ();
|
||||
auto block1a = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, previous1, nano::dev::genesis_key.pub, nano::Knano_ratio, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
auto block1b = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, previous1, nano::dev::genesis_key.pub, nano::Knano_ratio * 2, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
auto block1c = std::make_shared<nano::state_block> (nano::dev::genesis_key.pub, previous1, nano::dev::genesis_key.pub, nano::Knano_ratio * 3, nano::test::random_hash (), nano::dev::genesis_key.prv, nano::dev::genesis_key.pub, 0);
|
||||
|
||||
nano::qualified_root root1 = block1a->qualified_root ();
|
||||
|
||||
// Create blocks for second root
|
||||
auto const previous2 = nano::test::random_hash ();
|
||||
nano::keypair key2;
|
||||
auto block2a = std::make_shared<nano::state_block> (key2.pub, previous2, key2.pub, nano::Knano_ratio, nano::test::random_hash (), key2.prv, key2.pub, 0);
|
||||
auto block2b = std::make_shared<nano::state_block> (key2.pub, previous2, key2.pub, nano::Knano_ratio * 2, nano::test::random_hash (), key2.prv, key2.pub, 0);
|
||||
|
||||
nano::qualified_root root2 = block2a->qualified_root ();
|
||||
|
||||
// Create block for third root
|
||||
nano::keypair key3;
|
||||
auto const previous3 = nano::test::random_hash ();
|
||||
auto block3 = std::make_shared<nano::state_block> (key3.pub, previous3, key3.pub, nano::Knano_ratio, nano::test::random_hash (), key3.prv, key3.pub, 0);
|
||||
|
||||
nano::qualified_root root3 = block3->qualified_root ();
|
||||
|
||||
// Make sure roots are different
|
||||
ASSERT_NE (root1, root2);
|
||||
ASSERT_NE (root1, root3);
|
||||
ASSERT_NE (root2, root3);
|
||||
|
||||
// Insert blocks for first root
|
||||
fork_cache.put (block1a);
|
||||
fork_cache.put (block1b);
|
||||
fork_cache.put (block1c);
|
||||
|
||||
// First root should have max_forks_per_root=2 blocks, with the oldest dropped
|
||||
ASSERT_EQ (1, fork_cache.size ());
|
||||
auto blocks1 = fork_cache.get (root1);
|
||||
ASSERT_EQ (2, blocks1.size ());
|
||||
ASSERT_FALSE (std::find (blocks1.begin (), blocks1.end (), block1a) != blocks1.end ()); // Oldest should be dropped
|
||||
ASSERT_TRUE (std::find (blocks1.begin (), blocks1.end (), block1b) != blocks1.end ());
|
||||
ASSERT_TRUE (std::find (blocks1.begin (), blocks1.end (), block1c) != blocks1.end ());
|
||||
|
||||
// Insert blocks for second root
|
||||
fork_cache.put (block2a);
|
||||
fork_cache.put (block2b);
|
||||
|
||||
// Still within max_size=2, so both roots should be present
|
||||
ASSERT_EQ (2, fork_cache.size ());
|
||||
ASSERT_TRUE (fork_cache.contains (root1));
|
||||
ASSERT_TRUE (fork_cache.contains (root2));
|
||||
|
||||
auto blocks2 = fork_cache.get (root2);
|
||||
ASSERT_EQ (2, blocks2.size ());
|
||||
|
||||
// Insert block for third root
|
||||
fork_cache.put (block3);
|
||||
|
||||
// Should exceed max_size, oldest root (root1) should be dropped
|
||||
ASSERT_EQ (2, fork_cache.size ());
|
||||
ASSERT_FALSE (fork_cache.contains (root1)); // Oldest root dropped
|
||||
ASSERT_TRUE (fork_cache.contains (root2));
|
||||
ASSERT_TRUE (fork_cache.contains (root3));
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests getting a non-existent root
|
||||
*/
|
||||
TEST (fork_cache, nonexistent)
|
||||
{
|
||||
nano::test::system system;
|
||||
nano::fork_cache_config cfg;
|
||||
nano::fork_cache fork_cache{ cfg, system.stats };
|
||||
|
||||
nano::qualified_root nonexistent_root;
|
||||
auto blocks = fork_cache.get (nonexistent_root);
|
||||
ASSERT_TRUE (blocks.empty ());
|
||||
}
|
||||
|
|
@ -3232,8 +3232,6 @@ TEST (node, dependency_graph_frontier)
|
|||
ASSERT_TIMELY_EQ (15s, node2.ledger.cemented_count (), node2.ledger.block_count ());
|
||||
}
|
||||
|
||||
namespace nano
|
||||
{
|
||||
TEST (node, deferred_dependent_elections)
|
||||
{
|
||||
nano::test::system system;
|
||||
|
|
@ -3355,7 +3353,6 @@ TEST (node, deferred_dependent_elections)
|
|||
ASSERT_TIMELY (5s, node.block_confirmed (send2->hash ()));
|
||||
ASSERT_TIMELY (5s, node.active.active (receive->qualified_root ()));
|
||||
}
|
||||
}
|
||||
|
||||
// Test that a node configured with `enable_pruning` and `max_pruning_age = 1s` will automatically
|
||||
// prune old confirmed blocks without explicitly saying `node.ledger_pruning` in the unit test
|
||||
|
|
@ -3668,3 +3665,143 @@ TEST (node, bounded_backlog)
|
|||
|
||||
ASSERT_TIMELY_EQ (20s, node.ledger.block_count (), 11); // 10 + genesis
|
||||
}
|
||||
|
||||
// This test checks that a bootstrapping node can resolve a fork when a "poisoned" node
|
||||
// attempts to feed it the incorrect side of a fork.
|
||||
// The scenario involves:
|
||||
// 1. A bootstrapping node (node_boot) - the node being tested
|
||||
// 2. A poisoned node (node_poison) that has bootstrap serving enabled with an incorrect block
|
||||
// 3. A representative node (node_rep) with enough voting weight but bootstrap serving disabled
|
||||
// 4. A non-representative node (node_correct) that serves the correct side of the fork
|
||||
TEST (node, bootstrap_poison)
|
||||
{
|
||||
nano::test::system system;
|
||||
|
||||
// Create the representative node with bootstrap serving disabled
|
||||
nano::node_config rep_config = system.default_config ();
|
||||
rep_config.bootstrap_server.enable = false; // Disable bootstrap serving
|
||||
rep_config.bootstrap.enable = false; // Disable bootstrap from the network
|
||||
// Disable schedulers
|
||||
rep_config.priority_scheduler.enable = false;
|
||||
rep_config.hinted_scheduler.enable = false;
|
||||
rep_config.optimistic_scheduler.enable = false;
|
||||
rep_config.backlog_scan.enable = false;
|
||||
auto & node_rep = *system.add_node (rep_config);
|
||||
|
||||
// Create the poisoned node with bootstrap serving enabled
|
||||
nano::node_config poison_config = system.default_config ();
|
||||
poison_config.bootstrap_server.enable = true; // Enable bootstrap serving
|
||||
poison_config.bootstrap.enable = false; // Disable bootstrap from the network
|
||||
// Disable schedulers
|
||||
poison_config.priority_scheduler.enable = false;
|
||||
poison_config.hinted_scheduler.enable = false;
|
||||
poison_config.optimistic_scheduler.enable = false;
|
||||
poison_config.backlog_scan.enable = false;
|
||||
auto & node_poison = *system.add_node (poison_config);
|
||||
|
||||
// Representative node needs to hold the genesis key to have voting weight
|
||||
system.wallet (0)->insert_adhoc (nano::dev::genesis_key.prv);
|
||||
|
||||
// Create keys for our test accounts
|
||||
nano::keypair key1;
|
||||
|
||||
// Create and process blocks on representative node (the correct chain)
|
||||
nano::block_builder builder;
|
||||
|
||||
// First send from genesis to key1
|
||||
auto send1 = builder.send ()
|
||||
.previous (nano::dev::genesis->hash ())
|
||||
.destination (key1.pub)
|
||||
.balance (nano::dev::constants.genesis_amount - nano::Knano_ratio)
|
||||
.sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub)
|
||||
.work (*system.work.generate (nano::dev::genesis->hash ()))
|
||||
.build ();
|
||||
|
||||
ASSERT_EQ (nano::block_status::progress, node_rep.process (send1));
|
||||
ASSERT_EQ (nano::block_status::progress, node_poison.process (send1));
|
||||
|
||||
// Valid open block for key1 (correct version)
|
||||
auto open_correct = builder.open ()
|
||||
.source (send1->hash ())
|
||||
.representative (key1.pub)
|
||||
.account (key1.pub)
|
||||
.sign (key1.prv, key1.pub)
|
||||
.work (*system.work.generate (key1.pub))
|
||||
.build ();
|
||||
|
||||
// Fork of the open block (incorrect version) - using a different representative
|
||||
nano::keypair bad_rep;
|
||||
auto open_fork = builder.open ()
|
||||
.source (send1->hash ())
|
||||
.representative (bad_rep.pub) // Different representative
|
||||
.account (key1.pub)
|
||||
.sign (key1.prv, key1.pub)
|
||||
.work (*system.work.generate (key1.pub))
|
||||
.build ();
|
||||
|
||||
// Process the correct open block on the representative node
|
||||
ASSERT_EQ (nano::block_status::progress, node_rep.process (open_correct));
|
||||
|
||||
// Process the forked open block on the poisoned node
|
||||
ASSERT_EQ (nano::block_status::progress, node_poison.process (open_fork));
|
||||
|
||||
// Confirm the correct block on the representative node
|
||||
nano::test::confirm (node_rep, { open_correct });
|
||||
|
||||
// Now create a bootstrapping node that will try to sync from both nodes
|
||||
nano::node_config node_config = system.default_config ();
|
||||
node_config.bootstrap.account_sets.cooldown = 100ms; // Short cooldown between requests to speed up the test
|
||||
node_config.bootstrap.request_timeout = 250ms;
|
||||
node_config.bootstrap.frontier_rate_limit = 100;
|
||||
// Disable schedulers
|
||||
node_config.priority_scheduler.enable = false;
|
||||
node_config.hinted_scheduler.enable = false;
|
||||
node_config.optimistic_scheduler.enable = false;
|
||||
node_config.backlog_scan.enable = false;
|
||||
nano::node_flags node_flags;
|
||||
auto & node = *system.add_node (node_config, node_flags);
|
||||
ASSERT_EQ (node.network.size (), 2);
|
||||
|
||||
std::cout << "Main node: " << node.identifier () << std::endl;
|
||||
std::cout << "Waiting for: " << open_fork->hash ().to_string () << std::endl;
|
||||
|
||||
// The node should initially get the incorrect block from the poisoned node
|
||||
ASSERT_TIMELY (15s, node.block (open_fork->hash ()) != nullptr);
|
||||
ASSERT_NEVER (1s, node.stats.count (nano::stat::type::ledger, nano::stat::detail::fork) > 0);
|
||||
|
||||
// Create another non-rep node that will serve the correct side of the fork
|
||||
nano::node_config correct_config = system.default_config ();
|
||||
correct_config.bootstrap_server.enable = true; // Enable bootstrap serving
|
||||
correct_config.bootstrap.enable = false; // Disable bootstrap from the network
|
||||
correct_config.priority_scheduler.enable = false;
|
||||
correct_config.hinted_scheduler.enable = false;
|
||||
correct_config.optimistic_scheduler.enable = false;
|
||||
auto & node_correct = *system.add_node (correct_config);
|
||||
ASSERT_EQ (node.network.size (), 3);
|
||||
|
||||
// Process the correct open block on the non-representative node
|
||||
ASSERT_EQ (nano::block_status::progress, node_correct.process (send1));
|
||||
ASSERT_EQ (nano::block_status::progress, node_correct.process (open_correct));
|
||||
|
||||
// The node should at some point notice that there is a forked block
|
||||
ASSERT_TIMELY (15s, node.stats.count (nano::stat::type::ledger, nano::stat::detail::fork) > 0);
|
||||
|
||||
// Should no longer be needed, fork should be cached
|
||||
node_correct.stop ();
|
||||
|
||||
// We need an election active to force the correct side of the fork
|
||||
ASSERT_TRUE (nano::test::start_election (system, node, open_fork->hash ()));
|
||||
|
||||
// Wait for the node to resolve the fork conflict
|
||||
ASSERT_TIMELY (15s, node.block (open_correct->hash ()) != nullptr);
|
||||
|
||||
// Verify that the node got the correct block and not the fork
|
||||
ASSERT_NE (nullptr, node.block (open_correct->hash ()));
|
||||
ASSERT_EQ (nullptr, node.block (open_fork->hash ()));
|
||||
|
||||
// Verify the account information on the bootstrap node is correct
|
||||
nano::account_info account_info;
|
||||
ASSERT_FALSE (node.store.account.get (node.store.tx_begin_read (), key1.pub, account_info));
|
||||
ASSERT_EQ (account_info.head, open_correct->hash ());
|
||||
ASSERT_EQ (account_info.representative, key1.pub); // Correct representative
|
||||
}
|
||||
|
|
@ -353,7 +353,14 @@ class state_block : public nano::block
|
|||
{
|
||||
public:
|
||||
state_block () = default;
|
||||
state_block (nano::account const &, nano::block_hash const &, nano::account const &, nano::amount const &, nano::link const &, nano::raw_key const &, nano::public_key const &, uint64_t);
|
||||
state_block (nano::account const & account,
|
||||
nano::block_hash const & previous,
|
||||
nano::account const & representative,
|
||||
nano::amount const & balance,
|
||||
nano::link const & link,
|
||||
nano::raw_key const & prv,
|
||||
nano::public_key const & pub,
|
||||
uint64_t work);
|
||||
state_block (bool &, nano::stream &);
|
||||
state_block (bool &, boost::property_tree::ptree const &);
|
||||
virtual ~state_block () = default;
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ enum class type
|
|||
process_confirmed,
|
||||
online_reps,
|
||||
pruning,
|
||||
fork_cache,
|
||||
|
||||
_last // Must be the last enum
|
||||
};
|
||||
|
|
@ -478,18 +479,17 @@ enum class detail
|
|||
activate_full,
|
||||
scanned,
|
||||
|
||||
// active
|
||||
// active_elections
|
||||
insert,
|
||||
insert_failed,
|
||||
transition_priority,
|
||||
transition_priority_failed,
|
||||
election_cleanup,
|
||||
activate_immediately,
|
||||
|
||||
// active_elections
|
||||
started,
|
||||
stopped,
|
||||
confirm_dependent,
|
||||
forks_cached,
|
||||
|
||||
// unchecked
|
||||
put,
|
||||
|
|
@ -677,6 +677,9 @@ enum class detail
|
|||
pruned_count,
|
||||
collect_targets,
|
||||
|
||||
// fork_cache
|
||||
overfill_entry,
|
||||
|
||||
_last // Must be the last enum
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ add_library(
|
|||
epoch_upgrader.hpp
|
||||
epoch_upgrader.cpp
|
||||
fair_queue.hpp
|
||||
fork_cache.hpp
|
||||
fork_cache.cpp
|
||||
fwd.hpp
|
||||
ipc/action_handler.hpp
|
||||
ipc/action_handler.cpp
|
||||
|
|
|
|||
|
|
@ -400,8 +400,8 @@ nano::election_insertion_result nano::active_elections::insert (std::shared_ptr<
|
|||
|
||||
auto const root = block_a->qualified_root ();
|
||||
auto const hash = block_a->hash ();
|
||||
auto const existing = roots.get<tag_root> ().find (root);
|
||||
if (existing == roots.get<tag_root> ().end ())
|
||||
|
||||
if (auto existing = roots.get<tag_root> ().find (root); existing == roots.get<tag_root> ().end ())
|
||||
{
|
||||
if (!recently_confirmed.exists (root))
|
||||
{
|
||||
|
|
@ -479,9 +479,21 @@ nano::election_insertion_result nano::active_elections::insert (std::shared_ptr<
|
|||
{
|
||||
debug_assert (result.election);
|
||||
|
||||
node.vote_cache_processor.trigger (hash);
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ void nano::bootstrap_server::start ()
|
|||
{
|
||||
debug_assert (threads.empty ());
|
||||
|
||||
if (!config.enable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto i = 0u; i < config.threads; ++i)
|
||||
{
|
||||
threads.push_back (std::thread ([this] () {
|
||||
|
|
@ -107,6 +112,11 @@ bool nano::bootstrap_server::verify (const nano::asc_pull_req & message) const
|
|||
|
||||
bool nano::bootstrap_server::request (nano::asc_pull_req const & message, std::shared_ptr<nano::transport::channel> const & channel)
|
||||
{
|
||||
if (!config.enable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!verify (message))
|
||||
{
|
||||
stats.inc (nano::stat::type::bootstrap_server, nano::stat::detail::invalid);
|
||||
|
|
@ -439,6 +449,7 @@ nano::stat::detail nano::to_stat_detail (nano::asc_pull_type type)
|
|||
|
||||
nano::error nano::bootstrap_server_config::serialize (nano::tomlconfig & toml) const
|
||||
{
|
||||
toml.put ("enable", enable, "Enable bootstrap server. \ntype:bool");
|
||||
toml.put ("max_queue", max_queue, "Maximum number of queued requests per peer. \ntype:uint64");
|
||||
toml.put ("threads", threads, "Number of threads to process requests. \ntype:uint64");
|
||||
toml.put ("batch_size", batch_size, "Maximum number of requests to process in a single batch. \ntype:uint64");
|
||||
|
|
@ -449,6 +460,7 @@ nano::error nano::bootstrap_server_config::serialize (nano::tomlconfig & toml) c
|
|||
|
||||
nano::error nano::bootstrap_server_config::deserialize (nano::tomlconfig & toml)
|
||||
{
|
||||
toml.get ("enable", enable);
|
||||
toml.get ("max_queue", max_queue);
|
||||
toml.get ("threads", threads);
|
||||
toml.get ("batch_size", batch_size);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public:
|
|||
nano::error serialize (nano::tomlconfig &) const;
|
||||
|
||||
public:
|
||||
bool enable{ true };
|
||||
size_t max_queue{ 16 };
|
||||
size_t threads{ 1 };
|
||||
size_t batch_size{ 64 };
|
||||
|
|
|
|||
|
|
@ -789,7 +789,7 @@ void nano::bootstrap_service::cleanup_and_sync ()
|
|||
}
|
||||
|
||||
// Reinsert known dependencies into the priority set
|
||||
if (sync_dependencies_interval.elapse (nano::is_dev_run () ? 1s : 60s))
|
||||
if (sync_dependencies_interval.elapse (nano::is_dev_run () ? 500ms : 60s))
|
||||
{
|
||||
stats.inc (nano::stat::type::bootstrap, nano::stat::detail::sync_dependencies);
|
||||
accounts.sync_dependencies ();
|
||||
|
|
@ -803,7 +803,7 @@ void nano::bootstrap_service::run_timeouts ()
|
|||
{
|
||||
stats.inc (nano::stat::type::bootstrap, nano::stat::detail::loop_cleanup);
|
||||
cleanup_and_sync ();
|
||||
condition.wait_for (lock, 5s, [this] () { return stopped; });
|
||||
condition.wait_for (lock, nano::is_dev_run () ? 500ms : 5s, [this] () { return stopped; });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -456,6 +456,17 @@ void nano::election::confirm_if_quorum (nano::unique_lock<nano::mutex> & lock_a)
|
|||
{
|
||||
status.winner = block_l;
|
||||
remove_votes (status_winner_hash_l);
|
||||
|
||||
node.logger.debug (nano::log::type::election, "Winning fork changed from {} to {} for root: {} (behavior: {}, state: {}, voters: {}, blocks: {}, duration: {}ms)",
|
||||
status_winner_hash_l,
|
||||
winner_hash_l,
|
||||
qualified_root,
|
||||
to_string (behavior_m),
|
||||
to_string (state_m),
|
||||
status.voter_count,
|
||||
status.block_count,
|
||||
duration ().count ());
|
||||
|
||||
node.block_processor.force (block_l);
|
||||
}
|
||||
if (have_quorum (tally_l))
|
||||
|
|
|
|||
105
nano/node/fork_cache.cpp
Normal file
105
nano/node/fork_cache.cpp
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
#include <nano/lib/stats.hpp>
|
||||
#include <nano/lib/tomlconfig.hpp>
|
||||
#include <nano/node/fork_cache.hpp>
|
||||
|
||||
#include <boost/range/iterator_range.hpp>
|
||||
|
||||
nano::fork_cache::fork_cache (nano::fork_cache_config const & config_a, nano::stats & stats_a) :
|
||||
config{ config_a },
|
||||
stats{ stats_a }
|
||||
{
|
||||
}
|
||||
|
||||
void nano::fork_cache::put (std::shared_ptr<nano::block> block)
|
||||
{
|
||||
release_assert (block != nullptr);
|
||||
|
||||
std::lock_guard guard{ mutex };
|
||||
|
||||
// Add the new block to the cache, duplicates are prevented by the multi_index container
|
||||
auto [it, added] = roots.push_back ({ block->qualified_root () });
|
||||
release_assert (it != roots.end ());
|
||||
stats.inc (nano::stat::type::fork_cache, added ? nano::stat::detail::insert : nano::stat::detail::duplicate);
|
||||
|
||||
// Check if we already have this hash
|
||||
bool exists = std::find_if (it->forks.begin (), it->forks.end (), [&block] (auto const & fork) {
|
||||
return fork->hash () == block->hash ();
|
||||
})
|
||||
!= it->forks.end ();
|
||||
|
||||
if (exists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
it->forks.push_back (block);
|
||||
|
||||
// Check if we have too many forks for this root
|
||||
if (it->forks.size () > config.max_forks_per_root)
|
||||
{
|
||||
stats.inc (nano::stat::type::fork_cache, nano::stat::detail::overfill_entry);
|
||||
it->forks.pop_front (); // Remove the oldest entry
|
||||
}
|
||||
|
||||
// Check if we're at capacity
|
||||
if (roots.size () > config.max_size)
|
||||
{
|
||||
// Remove oldest entry (first in sequence)
|
||||
stats.inc (nano::stat::type::fork_cache, nano::stat::detail::overfill);
|
||||
roots.pop_front (); // Remove the oldest entry
|
||||
}
|
||||
}
|
||||
|
||||
std::deque<std::shared_ptr<nano::block>> nano::fork_cache::get (nano::qualified_root const & root) const
|
||||
{
|
||||
std::lock_guard guard{ mutex };
|
||||
|
||||
if (auto it = roots.get<tag_root> ().find (root); it != roots.get<tag_root> ().end ())
|
||||
{
|
||||
return it->forks;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t nano::fork_cache::size () const
|
||||
{
|
||||
std::lock_guard guard{ mutex };
|
||||
|
||||
return roots.size ();
|
||||
}
|
||||
|
||||
bool nano::fork_cache::contains (nano::qualified_root const & root) const
|
||||
{
|
||||
std::lock_guard guard{ mutex };
|
||||
|
||||
return roots.get<tag_root> ().count (root) > 0;
|
||||
}
|
||||
|
||||
nano::container_info nano::fork_cache::container_info () const
|
||||
{
|
||||
std::lock_guard guard{ mutex };
|
||||
|
||||
nano::container_info result;
|
||||
result.put ("roots", roots);
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* fork_cache_config
|
||||
*/
|
||||
|
||||
nano::error nano::fork_cache_config::deserialize (nano::tomlconfig & toml)
|
||||
{
|
||||
toml.get ("max_size", max_size);
|
||||
toml.get ("max_forks_per_root", max_forks_per_root);
|
||||
|
||||
return toml.get_error ();
|
||||
}
|
||||
|
||||
nano::error nano::fork_cache_config::serialize (nano::tomlconfig & toml) const
|
||||
{
|
||||
toml.put ("max_size", max_size, "Maximum number of roots in the cache. Each root can have multiple forks. \ntype:uint64");
|
||||
toml.put ("max_forks_per_root", max_forks_per_root, "Maximum number of forks per root. \ntype:uint64");
|
||||
|
||||
return toml.get_error ();
|
||||
}
|
||||
71
nano/node/fork_cache.hpp
Normal file
71
nano/node/fork_cache.hpp
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
|
||||
#include <nano/lib/blocks.hpp>
|
||||
#include <nano/lib/numbers.hpp>
|
||||
#include <nano/lib/numbers_templ.hpp>
|
||||
#include <nano/node/fwd.hpp>
|
||||
|
||||
#include <boost/multi_index/hashed_index.hpp>
|
||||
#include <boost/multi_index/member.hpp>
|
||||
#include <boost/multi_index/sequenced_index.hpp>
|
||||
#include <boost/multi_index_container.hpp>
|
||||
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace mi = boost::multi_index;
|
||||
|
||||
namespace nano
|
||||
{
|
||||
class fork_cache_config final
|
||||
{
|
||||
public:
|
||||
nano::error deserialize (nano::tomlconfig &);
|
||||
nano::error serialize (nano::tomlconfig &) const;
|
||||
|
||||
public:
|
||||
size_t max_size{ 1024 * 16 };
|
||||
size_t max_forks_per_root{ 10 };
|
||||
};
|
||||
|
||||
class fork_cache final
|
||||
{
|
||||
public:
|
||||
fork_cache (fork_cache_config const &, nano::stats &);
|
||||
|
||||
void put (std::shared_ptr<nano::block> fork);
|
||||
std::deque<std::shared_ptr<nano::block>> get (nano::qualified_root const & root) const;
|
||||
|
||||
size_t size () const;
|
||||
bool contains (nano::qualified_root const & root) const;
|
||||
|
||||
nano::container_info container_info () const;
|
||||
|
||||
private:
|
||||
fork_cache_config const & config;
|
||||
nano::stats & stats;
|
||||
|
||||
struct entry
|
||||
{
|
||||
nano::qualified_root root;
|
||||
mutable std::deque<std::shared_ptr<nano::block>> forks;
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
class tag_sequenced {};
|
||||
class tag_root {};
|
||||
|
||||
using ordered_forks = boost::multi_index_container<entry,
|
||||
mi::indexed_by<
|
||||
mi::sequenced<mi::tag<tag_sequenced>>,
|
||||
mi::hashed_unique<mi::tag<tag_root>,
|
||||
mi::member<entry, nano::qualified_root, &entry::root>>
|
||||
>>;
|
||||
// clang-format on
|
||||
|
||||
ordered_forks roots;
|
||||
|
||||
mutable std::mutex mutex;
|
||||
};
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ class bootstrap_service;
|
|||
class confirming_set;
|
||||
class election;
|
||||
class election_status;
|
||||
class fork_cache;
|
||||
class ledger_notifications;
|
||||
class local_block_broadcaster;
|
||||
class local_vote_history;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#include <nano/node/daemonconfig.hpp>
|
||||
#include <nano/node/election_status.hpp>
|
||||
#include <nano/node/endpoint.hpp>
|
||||
#include <nano/node/fork_cache.hpp>
|
||||
#include <nano/node/ledger_notifications.hpp>
|
||||
#include <nano/node/local_block_broadcaster.hpp>
|
||||
#include <nano/node/local_vote_history.hpp>
|
||||
|
|
@ -147,6 +148,8 @@ nano::node::node (std::shared_ptr<boost::asio::io_context> io_ctx_a, std::filesy
|
|||
port_mapping{ *port_mapping_impl },
|
||||
block_processor_impl{ std::make_unique<nano::block_processor> (config, ledger, ledger_notifications, unchecked, stats, logger) },
|
||||
block_processor{ *block_processor_impl },
|
||||
fork_cache_impl{ std::make_unique<nano::fork_cache> (config.fork_cache, stats) },
|
||||
fork_cache{ *fork_cache_impl },
|
||||
confirming_set_impl{ std::make_unique<nano::confirming_set> (config.confirming_set, ledger, ledger_notifications, stats, logger) },
|
||||
confirming_set{ *confirming_set_impl },
|
||||
bucketing_impl{ std::make_unique<nano::bucketing> () },
|
||||
|
|
@ -229,6 +232,17 @@ nano::node::node (std::shared_ptr<boost::asio::io_context> io_ctx_a, std::filesy
|
|||
active.recently_confirmed.erase (hash);
|
||||
});
|
||||
|
||||
// Cache forks
|
||||
ledger_notifications.blocks_processed.add ([this] (auto const & batch) {
|
||||
for (auto const & [result, context] : batch)
|
||||
{
|
||||
if (result == nano::block_status::fork)
|
||||
{
|
||||
fork_cache.put (context.block);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Announce new blocks via websocket
|
||||
ledger_notifications.blocks_processed.add ([this] (auto const & batch) {
|
||||
auto const transaction = ledger.tx_begin_read ();
|
||||
|
|
@ -998,6 +1012,7 @@ nano::container_info nano::node::container_info () const
|
|||
info.add ("http_callbacks", http_callbacks.container_info ());
|
||||
info.add ("pruning", pruning.container_info ());
|
||||
info.add ("vote_rebroadcaster", vote_rebroadcaster.container_info ());
|
||||
info.add ("fork_cache", fork_cache.container_info ());
|
||||
return info;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,6 +147,8 @@ public:
|
|||
nano::port_mapping & port_mapping;
|
||||
std::unique_ptr<nano::block_processor> block_processor_impl;
|
||||
nano::block_processor & block_processor;
|
||||
std::unique_ptr<nano::fork_cache> fork_cache_impl;
|
||||
nano::fork_cache & fork_cache;
|
||||
std::unique_ptr<nano::confirming_set> confirming_set_impl;
|
||||
nano::confirming_set & confirming_set;
|
||||
std::unique_ptr<nano::bucketing> bucketing_impl;
|
||||
|
|
|
|||
|
|
@ -274,6 +274,10 @@ nano::error nano::node_config::serialize_toml (nano::tomlconfig & toml) const
|
|||
bounded_backlog.serialize (bounded_backlog_l);
|
||||
toml.put_child ("bounded_backlog", bounded_backlog_l);
|
||||
|
||||
nano::tomlconfig fork_cache_l;
|
||||
fork_cache.serialize (fork_cache_l);
|
||||
toml.put_child ("fork_cache", fork_cache_l);
|
||||
|
||||
return toml.get_error ();
|
||||
}
|
||||
|
||||
|
|
@ -431,6 +435,12 @@ nano::error nano::node_config::deserialize_toml (nano::tomlconfig & toml)
|
|||
bounded_backlog.deserialize (config_l);
|
||||
}
|
||||
|
||||
if (toml.has_key ("fork_cache"))
|
||||
{
|
||||
auto config_l = toml.get_required_child ("fork_cache");
|
||||
fork_cache.deserialize (config_l);
|
||||
}
|
||||
|
||||
/*
|
||||
* Values
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include <nano/node/bootstrap/bootstrap_server.hpp>
|
||||
#include <nano/node/bounded_backlog.hpp>
|
||||
#include <nano/node/confirming_set.hpp>
|
||||
#include <nano/node/fork_cache.hpp>
|
||||
#include <nano/node/ipc/ipc_config.hpp>
|
||||
#include <nano/node/local_block_broadcaster.hpp>
|
||||
#include <nano/node/message_processor.hpp>
|
||||
|
|
@ -164,6 +165,7 @@ public:
|
|||
nano::backlog_scan_config backlog_scan;
|
||||
nano::bounded_backlog_config bounded_backlog;
|
||||
nano::vote_rebroadcaster_config vote_rebroadcaster;
|
||||
nano::fork_cache_config fork_cache;
|
||||
|
||||
public:
|
||||
/** Entry is ignored if it cannot be parsed as a valid address:port */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue