iota_replay/
fuzz.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use iota_config::node::ExpensiveSafetyCheckConfig;
6use iota_types::{
7    digests::TransactionDigest, execution_status::ExecutionFailureStatus,
8    transaction::TransactionKind,
9};
10use thiserror::Error;
11use tracing::{error, info};
12
13use crate::{
14    replay::{ExecutionSandboxState, LocalExec},
15    transaction_provider::{TransactionProvider, TransactionSource},
16    types::ReplayEngineError,
17};
18
19// Step 1: Get a transaction T from the network
20// Step 2: Create the sandbox and verify the TX does not fork locally
21// Step 3: Create desired mutations of T in set S
22// Step 4: For each mutation in S, replay the transaction with the sandbox state
23// from T         and verify no panic or invariant violation
24
25pub struct ReplayFuzzerConfig {
26    pub num_mutations_per_base: u64,
27    pub mutator: Box<dyn TransactionKindMutator + Send + Sync>,
28    pub tx_source: TransactionSource,
29    pub fail_over_on_err: bool,
30    pub expensive_safety_check_config: ExpensiveSafetyCheckConfig,
31}
32
33/// Provides the starting transaction for a fuzz session
34pub struct ReplayFuzzer {
35    pub local_exec: LocalExec,
36    pub sandbox_state: ExecutionSandboxState,
37    pub config: ReplayFuzzerConfig,
38    pub transaction_provider: TransactionProvider,
39}
40
41pub trait TransactionKindMutator {
42    fn mutate(&mut self, transaction_kind: &TransactionKind) -> Option<TransactionKind>;
43
44    fn reset(&mut self, mutations_per_base: u64);
45}
46
47impl ReplayFuzzer {
48    pub async fn new(rpc_url: String, config: ReplayFuzzerConfig) -> Result<Self, anyhow::Error> {
49        let local_exec = LocalExec::new_from_fn_url(&rpc_url)
50            .await?
51            .init_for_execution()
52            .await?;
53
54        let mut tx_provider = TransactionProvider::new(&rpc_url, config.tx_source.clone()).await?;
55
56        Self::new_with_local_executor(local_exec, config, &mut tx_provider).await
57    }
58
59    pub async fn new_with_local_executor(
60        mut local_exec: LocalExec,
61        config: ReplayFuzzerConfig,
62        transaction_provider: &mut TransactionProvider,
63    ) -> Result<Self, anyhow::Error> {
64        // Seed with the first transaction
65        let base_transaction = transaction_provider.next().await?.unwrap_or_else(|| {
66            panic!(
67                "No transactions found at source: {:?}",
68                transaction_provider.source
69            )
70        });
71        let sandbox_state = local_exec
72            .execute_transaction(
73                &base_transaction,
74                config.expensive_safety_check_config.clone(),
75                false,
76                None,
77                None,
78                None,
79                None,
80            )
81            .await?;
82
83        Ok(Self {
84            local_exec,
85            sandbox_state,
86            config,
87            transaction_provider: transaction_provider.clone(),
88        })
89    }
90
91    pub async fn re_init(mut self) -> Result<Self, anyhow::Error> {
92        let local_executor = self
93            .local_exec
94            .reset_for_new_execution_with_client()
95            .await?;
96        self.config
97            .mutator
98            .reset(self.config.num_mutations_per_base);
99        Self::new_with_local_executor(local_executor, self.config, &mut self.transaction_provider)
100            .await
101    }
102
103    pub async fn execute_tx(
104        &mut self,
105        transaction_kind: &TransactionKind,
106    ) -> Result<ExecutionSandboxState, ReplayEngineError> {
107        self.local_exec
108            .execution_engine_execute_with_tx_info_impl(
109                &self.sandbox_state.transaction_info,
110                Some(transaction_kind.clone()),
111                ExpensiveSafetyCheckConfig::new_enable_all(),
112            )
113            .await
114    }
115
116    pub async fn execute_tx_and_check_status(
117        &mut self,
118        transaction_kind: &TransactionKind,
119    ) -> Result<ExecutionSandboxState, ReplayFuzzError> {
120        let sandbox_state = self.execute_tx(transaction_kind).await?;
121        if let Some(Err(e)) = &sandbox_state.local_exec_status {
122            let stat = e.to_execution_status().0;
123            match &stat {
124                ExecutionFailureStatus::InvariantViolation
125                | ExecutionFailureStatus::VMInvariantViolation => {
126                    return Err(ReplayFuzzError::InvariantViolation {
127                        tx_digest: sandbox_state.transaction_info.tx_digest,
128                        kind: transaction_kind.clone(),
129                        exec_status: stat,
130                    });
131                }
132                _ => (),
133            }
134        }
135        Ok(sandbox_state)
136    }
137
138    // Simple command and arg shuffle mutation
139    // TODO: do more complicated mutations
140    pub fn next_mutation(&mut self, transaction_kind: &TransactionKind) -> Option<TransactionKind> {
141        self.config.mutator.mutate(transaction_kind)
142    }
143
144    pub async fn run(mut self, mut num_base_tx: u64) -> Result<(), ReplayFuzzError> {
145        while num_base_tx > 0 {
146            let mut tx_kind = self.sandbox_state.transaction_info.kind.clone();
147
148            info!(
149                "Starting fuzz with new base TX {}, with at most {} mutations",
150                self.sandbox_state.transaction_info.tx_digest, self.config.num_mutations_per_base
151            );
152            while let Some(mutation) = self.next_mutation(&tx_kind) {
153                info!(
154                    "Executing mutation: base tx {}, mutation {:?}",
155                    self.sandbox_state.transaction_info.tx_digest, mutation
156                );
157                match self.execute_tx_and_check_status(&mutation).await {
158                    Ok(v) => tx_kind = v.transaction_info.kind.clone(),
159                    Err(e) => {
160                        error!(
161                            "Error executing transaction: base tx: {}, mutation: {:?} with error{:?}",
162                            self.sandbox_state.transaction_info.tx_digest, mutation, e
163                        );
164                        if self.config.fail_over_on_err {
165                            return Err(e);
166                        }
167                    }
168                }
169            }
170            info!(
171                "Ended fuzz with for base TX {}\n",
172                self.sandbox_state.transaction_info.tx_digest
173            );
174            self = self
175                .re_init()
176                .await
177                .map_err(ReplayEngineError::from)
178                .map_err(ReplayFuzzError::from)?;
179            num_base_tx -= 1;
180        }
181
182        Ok(())
183    }
184}
185
186#[expect(clippy::large_enum_variant)]
187#[derive(Debug, Error, Clone)]
188pub enum ReplayFuzzError {
189    #[error(
190        "InvariantViolation: digest: {tx_digest}, kind: {kind}, status: {:?}",
191        exec_status
192    )]
193    InvariantViolation {
194        tx_digest: TransactionDigest,
195        kind: TransactionKind,
196        exec_status: ExecutionFailureStatus,
197    },
198
199    #[error(
200        "LocalExecError: exec system error which may/not be related to fuzzing: {:?}.",
201        err
202    )]
203    LocalExecError { err: ReplayEngineError },
204    // TODO: how exactly do we catch this?
205    // Panic(TransactionDigest, TransactionKind),
206}
207
208impl From<ReplayEngineError> for ReplayFuzzError {
209    fn from(err: ReplayEngineError) -> Self {
210        ReplayFuzzError::LocalExecError { err }
211    }
212}