1use 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
19pub 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
33pub 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 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 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 }
207
208impl From<ReplayEngineError> for ReplayFuzzError {
209 fn from(err: ReplayEngineError) -> Self {
210 ReplayFuzzError::LocalExecError { err }
211 }
212}