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