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