1use 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
18pub 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
32pub 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 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 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 }
205
206impl From<ReplayEngineError> for ReplayFuzzError {
207 fn from(err: ReplayEngineError) -> Self {
208 ReplayFuzzError::LocalExecError { err }
209 }
210}