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
682 lines
21 KiB
Rust
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();
|
|
}
|