mina_node_testing/scenarios/
mod.rs

1//! # Mina Testing Scenarios
2//!
3//! This module contains scenario-based tests for the Mina Rust node implementation.
4//! Scenarios are deterministic, ordered sequences of steps that test complex
5//! multi-node blockchain interactions.
6//!
7//! ## Documentation
8//!
9//! For comprehensive documentation on the testing framework and available
10//! scenarios, see:
11//! - **[Scenario Tests](https://o1-labs.github.io/mina-rust/developers/testing/scenario-tests)** - Main scenario testing documentation
12//! - **[Testing Framework](https://o1-labs.github.io/mina-rust/developers/testing/testing-framework)** - Overall testing architecture
13//! - **[Network Connectivity](https://o1-labs.github.io/mina-rust/developers/testing/network-connectivity)** - Network connectivity testing details
14//!
15//! ## Usage
16//!
17//! List available scenarios:
18//! ```bash
19//! cargo run --release --bin mina-node-testing -- scenarios-list
20//! ```
21//!
22//! Run a specific scenario:
23//! ```bash
24//! cargo run --release --bin mina-node-testing -- scenarios-run --name scenario-name
25//! ```
26//!
27//! ## Test Categories
28//!
29//! ### Directory Structure and CI Integration
30//!
31//! The directory structure in this module maps to specific test execution contexts:
32//!
33//! - **`solo_node/`** - Single node scenarios, often testing OCaml interoperability
34//! - **`multi_node/`** - Multi-node Rust network scenarios
35//! - **`p2p/`** - Low-level P2P networking and protocol tests
36//! - **`record_replay/`** - Scenario recording and replay functionality
37//! - **`simulation/`** - Large-scale network simulations
38//!
39//! In CI environments, these directories determine which specialized test binaries
40//! are used for execution, while locally all scenarios are accessible through the
41//! unified `mina-node-testing` CLI using scenario names.
42//!
43//! ### Basic Connectivity Tests
44//!
45//! * Ensure new nodes can discover peers and establish initial connections.
46//! * Test how nodes handle scenarios when they are overwhelmed with too many
47//!   connections or data requests.
48//!
49//! TODO(vlad9486):
50//! - Reconnection: Validate that nodes can reconnect after both intentional and
51//!   unintentional disconnections.
52//! - Handling Latency: Nodes should remain connected and synchronize even under
53//!   high latency conditions.
54//! - Intermittent Connections: Nodes should be resilient to sporadic network
55//!   dropouts and still maintain synchronization.
56//! - Dynamic IP Handling: Nodes with frequently changing IP addresses should
57//!   maintain stable connections.
58
59pub mod multi_node;
60pub mod record_replay;
61pub mod simulation;
62pub mod solo_node;
63
64pub mod p2p;
65
66mod driver;
67pub use driver::*;
68
69pub use crate::cluster::runner::*;
70
71use mina_core::log::{debug, system_time, warn};
72use strum_macros::{EnumIter, EnumString, IntoStaticStr};
73
74use crate::{
75    cluster::{Cluster, ClusterConfig},
76    scenario::{Scenario, ScenarioId, ScenarioStep},
77};
78
79use self::{
80    multi_node::{
81        basic_connectivity_initial_joining::MultiNodeBasicConnectivityInitialJoining,
82        basic_connectivity_peer_discovery::MultiNodeBasicConnectivityPeerDiscovery,
83        connection_discovery::{
84            OCamlToRust, OCamlToRustViaSeed,
85            RustNodeAsSeed as P2pConnectionDiscoveryRustNodeAsSeed, RustToOCaml,
86            RustToOCamlViaSeed,
87        },
88        pubsub_advanced::MultiNodePubsubPropagateBlock,
89        sync_4_block_producers::MultiNodeSync4BlockProducers,
90        vrf_correct_ledgers::MultiNodeVrfGetCorrectLedgers,
91        vrf_correct_slots::MultiNodeVrfGetCorrectSlots,
92        vrf_epoch_bounds_correct_ledgers::MultiNodeVrfEpochBoundsCorrectLedger,
93        vrf_epoch_bounds_evaluation::MultiNodeVrfEpochBoundsEvaluation,
94    },
95    p2p::{
96        basic_connection_handling::{
97            AllNodesConnectionsAreSymmetric, MaxNumberOfPeersIncoming, MaxNumberOfPeersIs1,
98            SeedConnectionsAreSymmetric, SimultaneousConnections,
99        },
100        basic_incoming_connections::{AcceptIncomingConnection, AcceptMultipleIncomingConnections},
101        basic_outgoing_connections::{
102            ConnectToInitialPeers, ConnectToInitialPeersBecomeReady,
103            ConnectToUnavailableInitialPeers, DontConnectToInitialPeerWithSameId,
104            DontConnectToNodeWithSameId, DontConnectToSelfInitialPeer,
105            MakeMultipleOutgoingConnections, MakeOutgoingConnection,
106        },
107        kademlia::KademliaBootstrap,
108        pubsub::P2pReceiveMessage,
109        signaling::P2pSignaling,
110    },
111    record_replay::{
112        block_production::RecordReplayBlockProduction, bootstrap::RecordReplayBootstrap,
113    },
114    simulation::{small::SimulationSmall, small_forever_real_time::SimulationSmallForeverRealTime},
115    solo_node::{
116        basic_connectivity_accept_incoming::SoloNodeBasicConnectivityAcceptIncoming,
117        basic_connectivity_initial_joining::SoloNodeBasicConnectivityInitialJoining,
118        bootstrap::SoloNodeBootstrap, sync_to_genesis::SoloNodeSyncToGenesis,
119        sync_to_genesis_custom::SoloNodeSyncToGenesisCustom,
120    },
121};
122
123#[derive(EnumIter, EnumString, IntoStaticStr, derive_more::From, Clone, Copy)]
124#[strum(serialize_all = "kebab-case")]
125pub enum Scenarios {
126    SoloNodeSyncToGenesis(SoloNodeSyncToGenesis),
127    SoloNodeBootstrap(SoloNodeBootstrap),
128    SoloNodeSyncToGenesisCustom(SoloNodeSyncToGenesisCustom),
129    SoloNodeBasicConnectivityInitialJoining(SoloNodeBasicConnectivityInitialJoining),
130    SoloNodeBasicConnectivityAcceptIncoming(SoloNodeBasicConnectivityAcceptIncoming),
131    MultiNodeSync4BlockProducers(MultiNodeSync4BlockProducers),
132    MultiNodeVrfGetCorrectLedgers(MultiNodeVrfGetCorrectLedgers),
133    MultiNodeVrfGetCorrectSlots(MultiNodeVrfGetCorrectSlots),
134    MultiNodeVrfEpochBoundsEvaluation(MultiNodeVrfEpochBoundsEvaluation),
135    MultiNodeVrfEpochBoundsCorrectLedger(MultiNodeVrfEpochBoundsCorrectLedger),
136    MultiNodeBasicConnectivityInitialJoining(MultiNodeBasicConnectivityInitialJoining),
137    MultiNodeBasicConnectivityPeerDiscovery(MultiNodeBasicConnectivityPeerDiscovery),
138    SimulationSmall(SimulationSmall),
139    SimulationSmallForeverRealTime(SimulationSmallForeverRealTime),
140    P2pReceiveMessage(P2pReceiveMessage),
141    P2pSignaling(P2pSignaling),
142    P2pConnectionDiscoveryRustNodeAsSeed(P2pConnectionDiscoveryRustNodeAsSeed),
143    MultiNodePubsubPropagateBlock(MultiNodePubsubPropagateBlock),
144    RecordReplayBootstrap(RecordReplayBootstrap),
145    RecordReplayBlockProduction(RecordReplayBlockProduction),
146
147    RustToOCaml(RustToOCaml),
148    OCamlToRust(OCamlToRust),
149    OCamlToRustViaSeed(OCamlToRustViaSeed),
150    RustToOCamlViaSeed(RustToOCamlViaSeed),
151    KademliaBootstrap(KademliaBootstrap),
152    AcceptIncomingConnection(AcceptIncomingConnection),
153    MakeOutgoingConnection(MakeOutgoingConnection),
154    AcceptMultipleIncomingConnections(AcceptMultipleIncomingConnections),
155    MakeMultipleOutgoingConnections(MakeMultipleOutgoingConnections),
156    DontConnectToNodeWithSameId(DontConnectToNodeWithSameId),
157    DontConnectToInitialPeerWithSameId(DontConnectToInitialPeerWithSameId),
158    DontConnectToSelfInitialPeer(DontConnectToSelfInitialPeer),
159    SimultaneousConnections(SimultaneousConnections),
160    ConnectToInitialPeers(ConnectToInitialPeers),
161    ConnectToUnavailableInitialPeers(ConnectToUnavailableInitialPeers),
162    AllNodesConnectionsAreSymmetric(AllNodesConnectionsAreSymmetric),
163    ConnectToInitialPeersBecomeReady(ConnectToInitialPeersBecomeReady),
164    SeedConnectionsAreSymmetric(SeedConnectionsAreSymmetric),
165    MaxNumberOfPeersIncoming(MaxNumberOfPeersIncoming),
166    MaxNumberOfPeersIs1(MaxNumberOfPeersIs1),
167}
168
169impl Scenarios {
170    // Turn off global test
171    pub fn iter() -> impl IntoIterator<Item = Scenarios> {
172        <Self as strum::IntoEnumIterator>::iter().filter(|s| !s.skip())
173    }
174
175    pub fn find_by_name(name: &str) -> Option<Self> {
176        <Self as strum::IntoEnumIterator>::iter().find(|v| v.to_str() == name)
177    }
178
179    fn skip(&self) -> bool {
180        match self {
181            Self::SoloNodeSyncToGenesis(_) => true,
182            Self::SoloNodeSyncToGenesisCustom(_) => true,
183            Self::SoloNodeBasicConnectivityAcceptIncoming(_) => cfg!(feature = "p2p-webrtc"),
184            Self::MultiNodeBasicConnectivityPeerDiscovery(_) => cfg!(feature = "p2p-webrtc"),
185            Self::SimulationSmall(_) => true,
186            Self::SimulationSmallForeverRealTime(_) => true,
187            Self::MultiNodePubsubPropagateBlock(_) => true, // in progress
188            Self::P2pSignaling(_) => !cfg!(feature = "p2p-webrtc"),
189            _ => false,
190        }
191    }
192
193    pub fn id(self) -> ScenarioId {
194        self.into()
195    }
196
197    pub fn to_str(self) -> &'static str {
198        self.into()
199    }
200
201    pub fn parent(self) -> Option<Self> {
202        match self {
203            Self::MultiNodeSync4BlockProducers(_) => Some(SoloNodeSyncToGenesis.into()),
204            Self::MultiNodeVrfGetCorrectLedgers(_) => Some(SoloNodeSyncToGenesisCustom.into()),
205            Self::MultiNodeVrfGetCorrectSlots(_) => Some(SoloNodeSyncToGenesisCustom.into()),
206            Self::MultiNodeVrfEpochBoundsEvaluation(_) => Some(SoloNodeSyncToGenesisCustom.into()),
207            Self::MultiNodeVrfEpochBoundsCorrectLedger(_) => {
208                Some(SoloNodeSyncToGenesisCustom.into())
209            }
210            _ => None,
211        }
212    }
213
214    pub fn parent_id(self) -> Option<ScenarioId> {
215        self.parent().map(Self::id)
216    }
217
218    pub fn description(self) -> &'static str {
219        use documented::Documented;
220        match self {
221            Self::SoloNodeSyncToGenesis(_) => SoloNodeSyncToGenesis::DOCS,
222            Self::SoloNodeBootstrap(_) => SoloNodeBootstrap::DOCS,
223            Self::SoloNodeSyncToGenesisCustom(_) => SoloNodeSyncToGenesis::DOCS,
224            Self::SoloNodeBasicConnectivityInitialJoining(_) => {
225                SoloNodeBasicConnectivityInitialJoining::DOCS
226            }
227            Self::SoloNodeBasicConnectivityAcceptIncoming(_) => {
228                SoloNodeBasicConnectivityAcceptIncoming::DOCS
229            }
230            Self::MultiNodeSync4BlockProducers(_) => MultiNodeSync4BlockProducers::DOCS,
231            Self::MultiNodeVrfGetCorrectLedgers(_) => MultiNodeVrfGetCorrectLedgers::DOCS,
232            Self::MultiNodeVrfGetCorrectSlots(_) => MultiNodeVrfGetCorrectSlots::DOCS,
233            Self::MultiNodeVrfEpochBoundsEvaluation(_) => MultiNodeVrfEpochBoundsEvaluation::DOCS,
234            Self::MultiNodeVrfEpochBoundsCorrectLedger(_) => {
235                MultiNodeVrfEpochBoundsCorrectLedger::DOCS
236            }
237            Self::MultiNodeBasicConnectivityInitialJoining(_) => {
238                MultiNodeBasicConnectivityInitialJoining::DOCS
239            }
240            Self::MultiNodeBasicConnectivityPeerDiscovery(_) => {
241                MultiNodeBasicConnectivityPeerDiscovery::DOCS
242            }
243            Self::SimulationSmall(_) => SimulationSmall::DOCS,
244            Self::SimulationSmallForeverRealTime(_) => SimulationSmallForeverRealTime::DOCS,
245            Self::P2pReceiveMessage(_) => P2pReceiveMessage::DOCS,
246            Self::P2pSignaling(_) => P2pSignaling::DOCS,
247            Self::P2pConnectionDiscoveryRustNodeAsSeed(_) => {
248                P2pConnectionDiscoveryRustNodeAsSeed::DOCS
249            }
250            Self::MultiNodePubsubPropagateBlock(_) => MultiNodePubsubPropagateBlock::DOCS,
251            Self::RecordReplayBootstrap(_) => RecordReplayBootstrap::DOCS,
252            Self::RecordReplayBlockProduction(_) => RecordReplayBlockProduction::DOCS,
253
254            Self::RustToOCaml(_) => RustToOCaml::DOCS,
255            Self::OCamlToRust(_) => OCamlToRust::DOCS,
256            Self::OCamlToRustViaSeed(_) => OCamlToRustViaSeed::DOCS,
257            Self::RustToOCamlViaSeed(_) => RustToOCamlViaSeed::DOCS,
258            Self::KademliaBootstrap(_) => KademliaBootstrap::DOCS,
259            Self::AcceptIncomingConnection(_) => AcceptIncomingConnection::DOCS,
260            Self::MakeOutgoingConnection(_) => MakeOutgoingConnection::DOCS,
261            Self::AcceptMultipleIncomingConnections(_) => AcceptMultipleIncomingConnections::DOCS,
262            Self::MakeMultipleOutgoingConnections(_) => MakeMultipleOutgoingConnections::DOCS,
263            Self::DontConnectToNodeWithSameId(_) => DontConnectToNodeWithSameId::DOCS,
264            Self::DontConnectToInitialPeerWithSameId(_) => DontConnectToInitialPeerWithSameId::DOCS,
265            Self::DontConnectToSelfInitialPeer(_) => DontConnectToSelfInitialPeer::DOCS,
266            Self::SimultaneousConnections(_) => SimultaneousConnections::DOCS,
267            Self::ConnectToInitialPeers(_) => ConnectToInitialPeers::DOCS,
268            Self::ConnectToUnavailableInitialPeers(_) => ConnectToUnavailableInitialPeers::DOCS,
269            Self::AllNodesConnectionsAreSymmetric(_) => AllNodesConnectionsAreSymmetric::DOCS,
270            Self::ConnectToInitialPeersBecomeReady(_) => ConnectToInitialPeersBecomeReady::DOCS,
271            Self::SeedConnectionsAreSymmetric(_) => SeedConnectionsAreSymmetric::DOCS,
272            Self::MaxNumberOfPeersIncoming(_) => MaxNumberOfPeersIncoming::DOCS,
273            Self::MaxNumberOfPeersIs1(_) => MaxNumberOfPeersIs1::DOCS,
274        }
275    }
276
277    pub fn default_cluster_config(self) -> Result<ClusterConfig, anyhow::Error> {
278        let config = ClusterConfig::new(None)
279            .map_err(|err| anyhow::anyhow!("failed to create cluster configuration: {err}"))?;
280
281        match self {
282            Self::P2pSignaling(v) => v.default_cluster_config(config),
283            _ => Ok(config),
284        }
285    }
286
287    pub fn blank_scenario(self) -> Scenario {
288        let mut scenario = Scenario::new(self.id(), self.parent_id());
289        scenario.set_description(self.description().to_owned());
290        scenario.info.nodes = Vec::new();
291
292        scenario
293    }
294
295    async fn run<F>(self, cluster: &mut Cluster, add_step: F)
296    where
297        F: Send + FnMut(&ScenarioStep),
298    {
299        let runner = ClusterRunner::new(cluster, add_step);
300        match self {
301            Self::SoloNodeSyncToGenesis(v) => v.run(runner).await,
302            Self::SoloNodeBootstrap(v) => v.run(runner).await,
303            Self::SoloNodeSyncToGenesisCustom(v) => v.run(runner).await,
304            Self::SoloNodeBasicConnectivityInitialJoining(v) => v.run(runner).await,
305            Self::SoloNodeBasicConnectivityAcceptIncoming(v) => v.run(runner).await,
306            Self::MultiNodeSync4BlockProducers(v) => v.run(runner).await,
307            Self::MultiNodeVrfGetCorrectLedgers(v) => v.run(runner).await,
308            Self::MultiNodeVrfGetCorrectSlots(v) => v.run(runner).await,
309            Self::MultiNodeVrfEpochBoundsEvaluation(v) => v.run(runner).await,
310            Self::MultiNodeVrfEpochBoundsCorrectLedger(v) => v.run(runner).await,
311            Self::MultiNodeBasicConnectivityInitialJoining(v) => v.run(runner).await,
312            Self::MultiNodeBasicConnectivityPeerDiscovery(v) => v.run(runner).await,
313            Self::SimulationSmall(v) => v.run(runner).await,
314            Self::SimulationSmallForeverRealTime(v) => v.run(runner).await,
315            Self::P2pReceiveMessage(v) => v.run(runner).await,
316            Self::P2pSignaling(v) => v.run(runner).await,
317            Self::P2pConnectionDiscoveryRustNodeAsSeed(v) => v.run(runner).await,
318            Self::MultiNodePubsubPropagateBlock(v) => v.run(runner).await,
319            Self::RecordReplayBootstrap(v) => v.run(runner).await,
320            Self::RecordReplayBlockProduction(v) => v.run(runner).await,
321
322            Self::RustToOCaml(v) => v.run(runner).await,
323            Self::OCamlToRust(v) => v.run(runner).await,
324            Self::OCamlToRustViaSeed(v) => v.run(runner).await,
325            Self::RustToOCamlViaSeed(v) => v.run(runner).await,
326            Self::KademliaBootstrap(v) => v.run(runner).await,
327            Self::AcceptIncomingConnection(v) => v.run(runner).await,
328            Self::MakeOutgoingConnection(v) => v.run(runner).await,
329            Self::AcceptMultipleIncomingConnections(v) => v.run(runner).await,
330            Self::MakeMultipleOutgoingConnections(v) => v.run(runner).await,
331            Self::DontConnectToNodeWithSameId(v) => v.run(runner).await,
332            Self::DontConnectToInitialPeerWithSameId(v) => v.run(runner).await,
333            Self::DontConnectToSelfInitialPeer(v) => v.run(runner).await,
334            Self::SimultaneousConnections(v) => v.run(runner).await,
335            Self::ConnectToInitialPeers(v) => v.run(runner).await,
336            Self::ConnectToUnavailableInitialPeers(v) => v.run(runner).await,
337            Self::AllNodesConnectionsAreSymmetric(v) => v.run(runner).await,
338            Self::ConnectToInitialPeersBecomeReady(v) => v.run(runner).await,
339            Self::SeedConnectionsAreSymmetric(v) => v.run(runner).await,
340            Self::MaxNumberOfPeersIncoming(v) => v.run(runner).await,
341            Self::MaxNumberOfPeersIs1(v) => v.run(runner).await,
342        }
343    }
344
345    pub async fn run_and_save(self, cluster: &mut Cluster) {
346        struct ScenarioSaveOnExit(Scenario);
347
348        impl Drop for ScenarioSaveOnExit {
349            fn drop(&mut self) {
350                let info = self.0.info.clone();
351                let steps = std::mem::take(&mut self.0.steps);
352                let scenario = Scenario { info, steps };
353
354                debug!(system_time(); "saving scenario({}) before exit...", scenario.info.id);
355                if let Err(err) = scenario.save_sync() {
356                    warn!(system_time();
357                        "failed to save scenario({})! error: {}",
358                        scenario.info.id, err
359                    );
360                }
361            }
362        }
363
364        debug!(system_time(); "run_and_save: {}", self.to_str());
365        let mut scenario = ScenarioSaveOnExit(self.blank_scenario());
366        self.run(cluster, |step| scenario.0.add_step(step.clone()).unwrap())
367            .await;
368        // drop to save it.
369        let _ = scenario;
370    }
371
372    pub async fn run_only(self, cluster: &mut Cluster) {
373        debug!(system_time(); "run_only: {}", self.to_str());
374        self.run(cluster, |_| {}).await
375    }
376
377    async fn build_cluster_and_run_parents(self, config: ClusterConfig) -> Cluster {
378        let mut parents = std::iter::repeat(())
379            .scan(self.parent(), |parent, _| {
380                let cur_parent = parent.take();
381                *parent = cur_parent.and_then(|p| p.parent());
382                cur_parent
383            })
384            .collect::<Vec<_>>();
385
386        let mut cluster = Cluster::new(config);
387        while let Some(scenario) = parents.pop() {
388            scenario.run_only(&mut cluster).await;
389        }
390
391        cluster
392    }
393
394    pub async fn run_and_save_from_scratch(self, config: ClusterConfig) {
395        let mut cluster = self.build_cluster_and_run_parents(config).await;
396        self.run_and_save(&mut cluster).await;
397    }
398
399    pub async fn run_only_from_scratch(self, config: ClusterConfig) {
400        let mut cluster = self.build_cluster_and_run_parents(config).await;
401        self.run_only(&mut cluster).await;
402    }
403}