bft_e2e.rs
1 // Copyright (c) 2025-2026 ACDC Network 2 // This file is part of the alphaos library. 3 // 4 // Alpha Chain | Delta Chain Protocol 5 // International Monetary Graphite. 6 // 7 // Derived from Aleo (https://aleo.org) and ProvableHQ (https://provable.com). 8 // They built world-class ZK infrastructure. We installed the EASY button. 9 // Their cryptography: elegant. Our modifications: bureaucracy-compatible. 10 // Original brilliance: theirs. Robert's Rules: ours. Bugs: definitely ours. 11 // 12 // Original Aleo/ProvableHQ code subject to Apache 2.0 https://www.apache.org/licenses/LICENSE-2.0 13 // All modifications and new work: CC0 1.0 Universal Public Domain Dedication. 14 // No rights reserved. No permission required. No warranty. No refunds. 15 // 16 // https://creativecommons.org/publicdomain/zero/1.0/ 17 // SPDX-License-Identifier: CC0-1.0 18 19 #[allow(dead_code)] 20 mod common; 21 #[allow(dead_code)] 22 mod components; 23 24 use crate::common::primary::{TestNetwork, TestNetworkConfig}; 25 use alphaos_node_bft::MAX_FETCH_TIMEOUT_IN_MS; 26 use deadline::deadline; 27 use itertools::Itertools; 28 use std::time::Duration; 29 use tokio::time::sleep; 30 31 #[tokio::test(flavor = "multi_thread")] 32 #[ignore = "long-running e2e test"] 33 async fn test_state_coherence() { 34 const N: u16 = 4; 35 const TRANSMISSION_INTERVAL_MS: u64 = 10; 36 37 let mut network = tokio::task::spawn_blocking(|| { 38 TestNetwork::new(TestNetworkConfig { 39 num_nodes: N, 40 bft: true, 41 connect_all: true, 42 fire_transmissions: Some(TRANSMISSION_INTERVAL_MS), 43 // Set this to Some(0..=4) to see the logs. 44 log_level: Some(0), 45 log_connections: true, 46 }) 47 }) 48 .await 49 .unwrap(); 50 51 network.start().await; 52 53 std::future::pending::<()>().await; 54 } 55 56 #[tokio::test(flavor = "multi_thread")] 57 #[ignore = "fails"] 58 async fn test_resync() { 59 // Start N nodes, connect them and start the cannons for each. 60 const N: u16 = 4; 61 const TRANSMISSION_INTERVAL_MS: u64 = 10; 62 let mut network = tokio::task::spawn_blocking(|| { 63 TestNetwork::new(TestNetworkConfig { 64 num_nodes: N, 65 bft: true, 66 connect_all: true, 67 fire_transmissions: Some(TRANSMISSION_INTERVAL_MS), 68 // Set this to Some(0..=4) to see the logs. 69 log_level: Some(0), 70 log_connections: false, 71 }) 72 }) 73 .await 74 .unwrap(); 75 network.start().await; 76 77 // Let the nodes advance through the rounds. 78 const BREAK_ROUND: u64 = 4; 79 let network_clone = network.clone(); 80 deadline!(Duration::from_secs(20), move || { network_clone.is_round_reached(BREAK_ROUND) }); 81 82 network.disconnect(N).await; 83 84 let mut spare_network = TestNetwork::new(TestNetworkConfig { 85 num_nodes: N, 86 bft: true, 87 connect_all: false, 88 fire_transmissions: None, 89 log_level: None, 90 log_connections: false, 91 }); 92 spare_network.start().await; 93 94 for i in 1..N { 95 let spare_validator = spare_network.validators.get(&i).cloned().unwrap(); 96 network.validators.insert(i, spare_validator); 97 } 98 99 network.connect_all().await; 100 101 const RECOVERY_ROUND: u64 = 8; 102 let network_clone = network.clone(); 103 deadline!(Duration::from_secs(20), move || { network_clone.is_round_reached(RECOVERY_ROUND) }); 104 } 105 106 #[tokio::test(flavor = "multi_thread")] 107 async fn test_quorum_threshold() { 108 // Start N nodes but don't connect them. 109 const N: u16 = 4; 110 const TRANSMISSION_INTERVAL_MS: u64 = 10; 111 112 let mut network = tokio::task::spawn_blocking(|| { 113 TestNetwork::new(TestNetworkConfig { 114 num_nodes: N, 115 bft: true, 116 connect_all: false, 117 fire_transmissions: None, 118 // Set this to Some(0..=4) to see the logs. 119 log_level: None, 120 log_connections: true, 121 }) 122 }) 123 .await 124 .unwrap(); 125 network.start().await; 126 127 // Check each node is at round 1 (0 is genesis). 128 for validators in network.validators.values() { 129 assert_eq!(validators.primary.current_round(), 1); 130 } 131 132 // Start the cannons for node 0. 133 network.fire_transmissions_at(0, TRANSMISSION_INTERVAL_MS); 134 135 sleep(Duration::from_millis(MAX_FETCH_TIMEOUT_IN_MS)).await; 136 137 // Check each node is still at round 1. 138 for validator in network.validators.values() { 139 assert_eq!(validator.primary.current_round(), 1); 140 } 141 142 // Connect the first two nodes and start the cannons for node 1. 143 network.connect_validators(0, 1).await; 144 network.fire_transmissions_at(1, TRANSMISSION_INTERVAL_MS); 145 146 sleep(Duration::from_millis(MAX_FETCH_TIMEOUT_IN_MS)).await; 147 148 // Check each node is still at round 1. 149 for validator in network.validators.values() { 150 assert_eq!(validator.primary.current_round(), 1); 151 } 152 153 // Connect the third node and start the cannons for it. 154 network.connect_validators(0, 2).await; 155 network.connect_validators(1, 2).await; 156 network.fire_transmissions_at(2, TRANSMISSION_INTERVAL_MS); 157 158 // Check the nodes reach quorum and advance through the rounds. 159 const TARGET_ROUND: u64 = 4; 160 let net = network.clone(); 161 deadline!(Duration::from_secs(20), move || { net.is_round_reached(TARGET_ROUND) }); 162 } 163 164 #[tokio::test(flavor = "multi_thread")] 165 async fn test_quorum_break() { 166 // Start N nodes, connect them and start the cannons for each. 167 const N: u16 = 4; 168 const TRANSMISSION_INTERVAL_MS: u64 = 10; 169 let mut network = tokio::task::spawn_blocking(|| { 170 TestNetwork::new(TestNetworkConfig { 171 num_nodes: N, 172 bft: true, 173 connect_all: true, 174 fire_transmissions: Some(TRANSMISSION_INTERVAL_MS), 175 // Set this to Some(0..=4) to see the logs. 176 log_level: None, 177 log_connections: true, 178 }) 179 }) 180 .await 181 .unwrap(); 182 network.start().await; 183 184 // Check the nodes have started advancing through the rounds. 185 const TARGET_ROUND: u64 = 4; 186 // Note: cloning the network is fine because the primaries it wraps are `Arc`ed. 187 let network_clone = network.clone(); 188 deadline!(Duration::from_secs(20), move || { network_clone.is_round_reached(TARGET_ROUND) }); 189 190 // Break the quorum by disconnecting two nodes. 191 const NUM_NODES: u16 = 2; 192 network.disconnect(NUM_NODES).await; 193 194 // Check the nodes have stopped advancing through the rounds. 195 assert!(network.is_halted().await); 196 } 197 198 #[tokio::test(flavor = "multi_thread")] 199 async fn test_leader_election_consistency() { 200 // The minimum and maximum rounds to check for leader consistency. 201 // From manual experimentation, the minimum round that works is 4. 202 // Starting at 0 or 2 causes assertion failures. Seems like the committee takes a few rounds to stabilize. 203 const STARTING_ROUND: u64 = 4; 204 const MAX_ROUND: u64 = 20; 205 206 // Start N nodes, connect them and start the cannons for each. 207 const N: u16 = 4; 208 const CANNON_INTERVAL_MS: u64 = 10; 209 let mut network = tokio::task::spawn_blocking(|| { 210 TestNetwork::new(TestNetworkConfig { 211 num_nodes: N, 212 bft: true, 213 connect_all: true, 214 fire_transmissions: Some(CANNON_INTERVAL_MS), 215 // Set this to Some(0..=4) to see the logs. 216 log_level: None, 217 log_connections: true, 218 }) 219 }) 220 .await 221 .unwrap(); 222 network.start().await; 223 224 // Wait for starting round to be reached 225 let cloned_network = network.clone(); 226 deadline!(Duration::from_secs(60), move || { cloned_network.is_round_reached(STARTING_ROUND) }); 227 228 // Check that validators agree about leaders in every even round 229 for target_round in (STARTING_ROUND..=MAX_ROUND).step_by(2) { 230 let cloned_network = network.clone(); 231 deadline!(Duration::from_secs(20), move || { cloned_network.is_round_reached(target_round) }); 232 233 // Get all validators in the network 234 let validators = network.validators.values().collect_vec(); 235 236 // Get leaders of all validators in the current round 237 let mut leaders = Vec::new(); 238 for validator in validators.iter() { 239 if validator.primary.current_round() == target_round { 240 let bft = validator.bft.get().unwrap(); 241 if let Some(leader) = bft.leader() { 242 // Validator is a live object - just because it's 243 // been on the current round above doesn't mean 244 // that's still the case 245 if validator.primary.current_round() == target_round { 246 leaders.push(leader); 247 } 248 } 249 } 250 } 251 252 println!("Found {} validators with a leader ({} out of sync)", leaders.len(), validators.len() - leaders.len()); 253 254 // Assert that all leaders are equal 255 assert!(leaders.iter().all_equal()); 256 } 257 } 258 259 #[tokio::test(flavor = "multi_thread")] 260 #[ignore = "run multiple times to see failure"] 261 async fn test_transient_break() { 262 // Start N nodes, connect them and start the cannons for each. 263 const N: u16 = 4; 264 const TRANSMISSION_INTERVAL_MS: u64 = 10; 265 let mut network = tokio::task::spawn_blocking(|| { 266 TestNetwork::new(TestNetworkConfig { 267 num_nodes: N, 268 bft: true, 269 connect_all: true, 270 fire_transmissions: Some(TRANSMISSION_INTERVAL_MS), 271 // Set this to Some(0..=4) to see the logs. 272 log_level: Some(6), 273 log_connections: false, 274 }) 275 }) 276 .await 277 .unwrap(); 278 network.start().await; 279 280 // Check the nodes have started advancing through the rounds. 281 const FIRST_BREAK_ROUND: u64 = 10; 282 let network_clone = network.clone(); 283 deadline!(Duration::from_secs(60), move || { network_clone.is_round_reached(FIRST_BREAK_ROUND) }); 284 285 // Disconnect the last node. 286 network.disconnect_one(3).await; 287 288 // Check the nodes have started advancing through the rounds. 289 const SECOND_BREAK_ROUND: u64 = 25; 290 let network_clone = network.clone(); 291 deadline!(Duration::from_secs(80), move || { network_clone.is_round_reached(SECOND_BREAK_ROUND) }); 292 293 // Disconnect another node, break quorum. 294 network.disconnect_one(2).await; 295 296 // Check the nodes have stopped advancing through the rounds. 297 assert!(network.is_halted().await); 298 299 // Connect the last node again. 300 network.connect_one(3).await; 301 302 const RECOVERY_ROUND: u64 = 30; 303 let network_clone = network.clone(); 304 deadline!(Duration::from_secs(60), move || { network_clone.is_round_reached(RECOVERY_ROUND) }); 305 }