/ node / bft / tests / bft_e2e.rs
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  }