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