synor/apps/synord/tests/fork_resolution.rs
Gulshan Yadav 5c643af64c fix: resolve all clippy warnings for CI
Fix all Rust clippy warnings that were causing CI failures when built
with RUSTFLAGS=-Dwarnings. Changes include:

- Replace derivable_impls with derive macros for BlockBody, Network, etc.
- Use div_ceil() instead of manual implementation
- Fix should_implement_trait by renaming from_str to parse
- Add type aliases for type_complexity warnings
- Use or_default(), is_some_and(), is_multiple_of() where appropriate
- Remove needless borrows and redundant closures
- Fix manual_strip with strip_prefix()
- Add allow attributes for intentional patterns (too_many_arguments,
  needless_range_loop in cryptographic code, assertions_on_constants)
- Remove unused imports, mut bindings, and dead code in tests
2026-01-08 05:58:22 +05:30

682 lines
21 KiB
Rust

//! Fork resolution and DAG convergence tests.
//!
//! These tests verify:
//! - GHOSTDAG consensus fork resolution
//! - Multiple tips (DAG divergence) handling
//! - Blue/red block classification
//! - Selected parent chain convergence
//! - Reorg and chain reorganization
//! - Network partition recovery
use std::sync::Arc;
use std::time::Duration;
use tempfile::TempDir;
use tokio::time::sleep;
use tracing::info;
use synord::config::NodeConfig;
use synord::node::{NodeState, SynorNode};
/// Test timeout for operations.
#[allow(dead_code)]
const TEST_TIMEOUT: Duration = Duration::from_secs(30);
// ==================== Test Helpers ====================
/// Creates a test node configuration.
fn create_node_config(temp_dir: &TempDir, node_index: u16, seeds: Vec<String>) -> NodeConfig {
let mut config = NodeConfig::for_network("devnet").unwrap();
config.data_dir = temp_dir.path().join(format!("node_{}", node_index));
config.mining.enabled = false;
let port_base = 19000 + (std::process::id() % 500) as u16 * 10 + node_index * 3;
config.p2p.listen_addr = format!("/ip4/127.0.0.1/tcp/{}", port_base);
config.rpc.http_addr = format!("127.0.0.1:{}", port_base + 1);
config.rpc.ws_addr = format!("127.0.0.1:{}", port_base + 2);
config.p2p.seeds = seeds;
config
}
/// Creates a mining-enabled node configuration.
#[allow(dead_code)]
fn create_miner_config(
temp_dir: &TempDir,
node_index: u16,
seeds: Vec<String>,
coinbase_addr: &str,
) -> NodeConfig {
let mut config = create_node_config(temp_dir, node_index, seeds);
config.mining.enabled = true;
config.mining.coinbase_address = Some(coinbase_addr.to_string());
config.mining.threads = 1;
config
}
/// Test network for fork scenarios.
struct ForkTestNetwork {
nodes: Vec<Arc<SynorNode>>,
_temp_dirs: Vec<TempDir>,
}
impl ForkTestNetwork {
/// Creates a network with specified number of mining nodes.
#[allow(dead_code)]
async fn new_with_miners(miner_count: usize) -> anyhow::Result<Self> {
let mut temp_dirs = Vec::new();
let mut nodes = Vec::new();
let first_port = 19000 + (std::process::id() % 500) as u16 * 10;
for i in 0..miner_count {
let temp = TempDir::new()?;
let seeds = if i == 0 {
vec![]
} else {
vec![format!("/ip4/127.0.0.1/tcp/{}", first_port)]
};
let coinbase = format!("tsynor1miner{}...", i);
let config = create_miner_config(&temp, i as u16, seeds, &coinbase);
temp_dirs.push(temp);
let node = Arc::new(SynorNode::new(config).await?);
nodes.push(node);
}
Ok(ForkTestNetwork {
nodes,
_temp_dirs: temp_dirs,
})
}
/// Creates a standard (non-mining) network.
async fn new(node_count: usize) -> anyhow::Result<Self> {
let mut temp_dirs = Vec::new();
let mut nodes = Vec::new();
let first_port = 19000 + (std::process::id() % 500) as u16 * 10;
for i in 0..node_count {
let temp = TempDir::new()?;
let seeds = if i == 0 {
vec![]
} else {
vec![format!("/ip4/127.0.0.1/tcp/{}", first_port)]
};
let config = create_node_config(&temp, i as u16, seeds);
temp_dirs.push(temp);
let node = Arc::new(SynorNode::new(config).await?);
nodes.push(node);
}
Ok(ForkTestNetwork {
nodes,
_temp_dirs: temp_dirs,
})
}
/// Starts all nodes.
async fn start_all(&self) -> anyhow::Result<()> {
for (i, node) in self.nodes.iter().enumerate() {
info!(node = i, "Starting node");
node.start().await?;
}
sleep(Duration::from_millis(500)).await;
Ok(())
}
/// Stops all nodes.
async fn stop_all(&self) -> anyhow::Result<()> {
for node in &self.nodes {
node.stop().await?;
}
Ok(())
}
}
// ==================== DAG Structure Tests ====================
#[tokio::test]
async fn test_dag_tips_tracking() {
let network = ForkTestNetwork::new(2).await.unwrap();
network.start_all().await.unwrap();
// Wait for connection
sleep(Duration::from_secs(2)).await;
// Check tips on each node
for (i, node) in network.nodes.iter().enumerate() {
let consensus = node.consensus();
let tips: Vec<[u8; 32]> = consensus.tips().await;
info!(node = i, tip_count = tips.len(), "DAG tips");
// Initially should have genesis or first block as tip
// Tips list tracks all current DAG leaves
}
network.stop_all().await.unwrap();
}
#[tokio::test]
async fn test_selected_parent_chain() {
let network = ForkTestNetwork::new(2).await.unwrap();
network.start_all().await.unwrap();
sleep(Duration::from_secs(2)).await;
// Get selected chain from each node
for (i, node) in network.nodes.iter().enumerate() {
let consensus = node.consensus();
let chain: Vec<[u8; 32]> = consensus.get_selected_chain(10).await;
info!(
node = i,
chain_length = chain.len(),
"Selected parent chain"
);
// Chain should be consistent across nodes in same network
for (j, block) in chain.iter().enumerate() {
info!(
node = i,
position = j,
block = hex::encode(&block[..8]),
"Chain block"
);
}
}
network.stop_all().await.unwrap();
}
// ==================== GHOSTDAG Configuration Tests ====================
#[tokio::test]
async fn test_ghostdag_k_parameter() {
let temp_dir = TempDir::new().unwrap();
let config = create_node_config(&temp_dir, 0, vec![]);
// Verify GHOSTDAG K is configured
let ghostdag_k = config.consensus.ghostdag_k;
info!(ghostdag_k = ghostdag_k, "GHOSTDAG K parameter");
// K should be a reasonable value (typically 18 for devnet, higher for mainnet)
assert!(ghostdag_k > 0, "GHOSTDAG K should be positive");
assert!(ghostdag_k <= 64, "GHOSTDAG K should be reasonable");
let node = SynorNode::new(config).await.unwrap();
node.start().await.unwrap();
// Verify K affects consensus behavior
let consensus = node.consensus();
// K-cluster determines how many parallel blocks are "blue"
// Higher K = more tolerance for concurrent blocks
let _ = consensus.current_blue_score().await;
node.stop().await.unwrap();
}
// ==================== Blue/Red Classification Tests ====================
#[tokio::test]
async fn test_blue_score_tracking() {
let network = ForkTestNetwork::new(2).await.unwrap();
network.start_all().await.unwrap();
sleep(Duration::from_secs(2)).await;
// Track blue scores across nodes
for (i, node) in network.nodes.iter().enumerate() {
let consensus = node.consensus();
let blue_score = consensus.current_blue_score().await;
let daa_score = consensus.current_daa_score().await;
info!(
node = i,
blue_score = blue_score,
daa_score = daa_score,
"Block scores"
);
// Blue score tracks cumulative "blueness" of chain
// DAA score is used for difficulty adjustment
}
network.stop_all().await.unwrap();
}
#[tokio::test]
async fn test_block_info_blue_red_sets() {
let network = ForkTestNetwork::new(2).await.unwrap();
network.start_all().await.unwrap();
sleep(Duration::from_secs(2)).await;
// Get block info which includes blue/red sets
for (i, node) in network.nodes.iter().enumerate() {
let consensus = node.consensus();
let tips: Vec<[u8; 32]> = consensus.tips().await;
for tip in tips.iter().take(3) {
if let Some(block_info) = consensus.get_block_info(tip).await {
info!(
node = i,
block = hex::encode(&tip[..8]),
blue_score = block_info.blue_score,
blues_count = block_info.blues.len(),
reds_count = block_info.reds.len(),
parents = block_info.parents.len(),
children = block_info.children.len(),
"Block GHOSTDAG info"
);
// Blue set contains blocks in this block's "good" ancestry
// Red set contains blocks that are "parallel" but not in k-cluster
}
}
}
network.stop_all().await.unwrap();
}
// ==================== Fork Scenario Tests ====================
#[tokio::test]
async fn test_concurrent_tips_handling() {
// In GHOSTDAG, multiple tips is normal operation
let network = ForkTestNetwork::new(3).await.unwrap();
network.start_all().await.unwrap();
// Wait for network to form
sleep(Duration::from_secs(3)).await;
// With multiple nodes, we might see multiple tips
let mut all_tips: Vec<Vec<[u8; 32]>> = Vec::new();
for (i, node) in network.nodes.iter().enumerate() {
let consensus = node.consensus();
let tips: Vec<[u8; 32]> = consensus.tips().await;
info!(node = i, tip_count = tips.len(), "Node tips");
all_tips.push(tips);
}
// In a synchronized network, tips should converge
// But during operation, temporary divergence is expected
info!(nodes_checked = all_tips.len(), "Tips collection complete");
network.stop_all().await.unwrap();
}
#[tokio::test]
async fn test_chain_convergence() {
let network = ForkTestNetwork::new(3).await.unwrap();
network.start_all().await.unwrap();
// Let network operate
sleep(Duration::from_secs(3)).await;
// Get virtual selected parent from each node
let mut selected_parents: Vec<Option<[u8; 32]>> = Vec::new();
for (i, node) in network.nodes.iter().enumerate() {
let consensus = node.consensus();
let vsp: Option<[u8; 32]> = consensus.virtual_selected_parent().await;
info!(
node = i,
has_vsp = vsp.is_some(),
vsp = vsp.map(|v| hex::encode(&v[..8])),
"Virtual selected parent"
);
selected_parents.push(vsp);
}
// In a healthy network, selected parents should converge
// (might temporarily differ during block propagation)
info!(
nodes_with_vsp = selected_parents.iter().filter(|p| p.is_some()).count(),
"VSP convergence check"
);
network.stop_all().await.unwrap();
}
// ==================== Block Validation in Fork Context ====================
#[tokio::test]
async fn test_orphan_block_handling() {
let network = ForkTestNetwork::new(2).await.unwrap();
network.start_all().await.unwrap();
sleep(Duration::from_secs(2)).await;
// Test orphan detection (block with unknown parent)
// This test verifies the API for block validation works
// In a full implementation with the consensus types exported,
// we would match on the validation result
let consensus = network.nodes[0].consensus();
// Create a fake block with unknown parent
let fake_block = vec![0u8; 100]; // Invalid block bytes
let validation = consensus.validate_block(&fake_block).await;
info!(validation = ?validation, "Invalid block validation result");
// The validation should indicate the block is invalid or orphan
// We just verify the API doesn't panic
network.stop_all().await.unwrap();
}
#[tokio::test]
async fn test_duplicate_block_rejection() {
let network = ForkTestNetwork::new(2).await.unwrap();
network.start_all().await.unwrap();
sleep(Duration::from_secs(2)).await;
// If we had access to an actual block, submitting it twice should
// return Duplicate. For this test, we verify the API.
{
let consensus = network.nodes[0].consensus();
// First, get a tip (existing block)
let tips: Vec<[u8; 32]> = consensus.tips().await;
if !tips.is_empty() {
info!(
tip = hex::encode(&tips[0][..8]),
"Would test duplicate rejection"
);
// In full implementation, we'd serialize and resubmit
}
}
network.stop_all().await.unwrap();
}
// ==================== Confirmation Depth Tests ====================
#[tokio::test]
async fn test_confirmation_counting() {
let network = ForkTestNetwork::new(2).await.unwrap();
network.start_all().await.unwrap();
sleep(Duration::from_secs(2)).await;
{
let consensus = network.nodes[0].consensus();
let tips: Vec<[u8; 32]> = consensus.tips().await;
for tip in tips.iter().take(3) {
let confirmations = consensus.get_confirmations(tip).await;
info!(
block = hex::encode(&tip[..8]),
confirmations = confirmations,
"Block confirmations"
);
// Recent tip should have 0 confirmations
// Older blocks should have more confirmations
}
}
network.stop_all().await.unwrap();
}
#[tokio::test]
async fn test_is_in_selected_chain() {
let network = ForkTestNetwork::new(2).await.unwrap();
network.start_all().await.unwrap();
sleep(Duration::from_secs(2)).await;
{
let consensus = network.nodes[0].consensus();
let tips: Vec<[u8; 32]> = consensus.tips().await;
let chain: Vec<[u8; 32]> = consensus.get_selected_chain(10).await;
// Check if tips are in selected chain
for tip in tips.iter().take(2) {
let in_chain = consensus.is_in_selected_chain(tip).await;
info!(
block = hex::encode(&tip[..8]),
in_selected_chain = in_chain,
"Selected chain membership"
);
}
// Blocks in the selected chain should return true
for block in chain.iter().take(3) {
let in_chain = consensus.is_in_selected_chain(block).await;
info!(
block = hex::encode(&block[..8]),
in_selected_chain = in_chain,
"Chain block membership"
);
// These should all be true since we got them from get_selected_chain
}
}
network.stop_all().await.unwrap();
}
// ==================== Finality Tests ====================
#[tokio::test]
async fn test_finality_depth_config() {
let temp_dir = TempDir::new().unwrap();
let config = create_node_config(&temp_dir, 0, vec![]);
let finality_depth = config.consensus.finality_depth;
info!(finality_depth = finality_depth, "Finality depth");
// Finality depth determines when blocks are considered final
// In devnet, this is typically lower for faster finality
assert!(finality_depth > 0, "Finality depth should be positive");
let node = SynorNode::new(config).await.unwrap();
node.start().await.unwrap();
// A block with confirmations >= finality_depth is considered final
let consensus = node.consensus();
let tips: Vec<[u8; 32]> = consensus.tips().await;
if !tips.is_empty() {
let confirmations = consensus.get_confirmations(&tips[0]).await;
let is_final = confirmations >= finality_depth;
info!(
confirmations = confirmations,
finality_depth = finality_depth,
is_final = is_final,
"Finality check"
);
}
node.stop().await.unwrap();
}
// ==================== Network Partition Simulation ====================
#[tokio::test]
async fn test_partition_and_recovery() {
// Create 3 nodes
let temp_dirs: Vec<TempDir> = (0..3).map(|_| TempDir::new().unwrap()).collect();
let first_port = 19000 + (std::process::id() % 500) as u16 * 10;
// Node 0: No seeds (seed node)
let config0 = create_node_config(&temp_dirs[0], 0, vec![]);
// Node 1: Connects to node 0
let config1 = create_node_config(
&temp_dirs[1],
1,
vec![format!("/ip4/127.0.0.1/tcp/{}", first_port)],
);
// Node 2: Connects to node 0
let config2 = create_node_config(
&temp_dirs[2],
2,
vec![format!("/ip4/127.0.0.1/tcp/{}", first_port)],
);
let node0 = Arc::new(SynorNode::new(config0).await.unwrap());
let node1 = Arc::new(SynorNode::new(config1).await.unwrap());
let node2 = Arc::new(SynorNode::new(config2).await.unwrap());
// Start all nodes
node0.start().await.unwrap();
node1.start().await.unwrap();
node2.start().await.unwrap();
sleep(Duration::from_secs(2)).await;
info!("Network formed with 3 nodes");
// Simulate partition: Stop node 0 (central node)
info!("Creating partition by stopping node 0");
node0.stop().await.unwrap();
sleep(Duration::from_secs(1)).await;
// Node 1 and 2 are now partitioned (can't reach each other directly)
// They should handle this gracefully
{
let net1 = node1.network();
let peers1 = net1.peer_count().await;
info!(peers = peers1, "Node 1 peers after partition");
}
{
let net2 = node2.network();
let peers2 = net2.peer_count().await;
info!(peers = peers2, "Node 2 peers after partition");
}
// Recovery: Restart node 0
info!("Healing partition by restarting node 0");
// In real test, we'd need fresh config for same ports
// For now, just verify nodes didn't crash
assert_eq!(
node1.state().await,
NodeState::Running,
"Node 1 should survive partition"
);
assert_eq!(
node2.state().await,
NodeState::Running,
"Node 2 should survive partition"
);
node2.stop().await.unwrap();
node1.stop().await.unwrap();
}
// ==================== Reward and Difficulty in Forks ====================
#[tokio::test]
async fn test_reward_calculation() {
let network = ForkTestNetwork::new(2).await.unwrap();
network.start_all().await.unwrap();
sleep(Duration::from_secs(2)).await;
{
let consensus = network.nodes[0].consensus();
let next_reward = consensus.get_next_reward().await;
info!(reward_sompi = next_reward.as_sompi(), "Next block reward");
// Reward should be positive
assert!(
next_reward.as_sompi() > 0,
"Block reward should be positive"
);
}
network.stop_all().await.unwrap();
}
#[tokio::test]
async fn test_difficulty_adjustment() {
let network = ForkTestNetwork::new(2).await.unwrap();
network.start_all().await.unwrap();
sleep(Duration::from_secs(2)).await;
for (i, node) in network.nodes.iter().enumerate() {
let consensus = node.consensus();
let difficulty = consensus.current_difficulty().await;
let _target = consensus.get_current_target().await;
info!(node = i, difficulty_bits = difficulty, "Difficulty info");
// Difficulty should be set
// Target is the hash threshold for valid blocks
}
network.stop_all().await.unwrap();
}
// ==================== Transaction Validation in Fork Context ====================
#[tokio::test]
async fn test_tx_validation_in_fork() {
let network = ForkTestNetwork::new(2).await.unwrap();
network.start_all().await.unwrap();
sleep(Duration::from_secs(2)).await;
// Test transaction validation (would need actual tx)
let consensus = network.nodes[0].consensus();
// Validate a dummy transaction (should fail to parse)
let dummy_tx = vec![0u8; 50];
let validation = consensus.validate_tx(&dummy_tx).await;
info!(validation = ?validation, "Dummy transaction validation result");
// The validation should indicate the transaction is invalid
// Invalid bytes should fail to parse, which is the expected behavior
// We verify the API doesn't panic on invalid input
network.stop_all().await.unwrap();
}
// ==================== Block Subscriber Tests ====================
#[tokio::test]
async fn test_block_accepted_subscription() {
let network = ForkTestNetwork::new(2).await.unwrap();
network.start_all().await.unwrap();
sleep(Duration::from_secs(2)).await;
// Subscribe to block accepted events
{
let consensus = network.nodes[0].consensus();
let mut rx = consensus.subscribe_blocks();
// In production, we'd mine a block and see it here
// For this test, verify subscription API works
info!("Block subscription created");
// Check if any blocks are received (unlikely in test without mining)
match tokio::time::timeout(Duration::from_millis(500), rx.recv()).await {
Ok(Ok(hash)) => {
info!(
block = hex::encode(&hash[..8]),
"Received block notification"
);
}
Ok(Err(_)) => {
info!("Block channel closed");
}
Err(_) => {
info!("No blocks received (expected in test without mining)");
}
}
}
network.stop_all().await.unwrap();
}