sui_replay/
fuzz.rs

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