Skip to content

Commit ef81cca

Browse files
committed
refactor contract simulation
1 parent 4c88ceb commit ef81cca

File tree

4 files changed

+267
-337
lines changed

4 files changed

+267
-337
lines changed

crates/op-rbuilder/src/builders/builder_tx.rs

Lines changed: 135 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
use alloy_consensus::{Transaction, TxEip1559};
1+
use alloy_consensus::TxEip1559;
22
use alloy_eips::{Encodable2718, eip7623::TOTAL_COST_FLOOR_PER_TOKEN};
33
use alloy_evm::Database;
44
use alloy_op_evm::OpEvm;
55
use alloy_primitives::{
66
Address, B256, Bytes, TxKind, U256,
77
map::foldhash::{HashMap, HashSet, HashSetExt},
88
};
9+
use alloy_sol_types::{ContractError, Error, Revert, SolCall, SolError, SolInterface};
910
use core::fmt::Debug;
1011
use op_alloy_consensus::OpTypedTransaction;
12+
use op_alloy_rpc_types::OpTransactionRequest;
1113
use op_revm::{OpHaltReason, OpTransactionError};
1214
use reth_evm::{
1315
ConfigureEvm, Evm, EvmError, eth::receipt_builder::ReceiptBuilderCtx,
@@ -18,22 +20,27 @@ use reth_optimism_primitives::OpTransactionSigned;
1820
use reth_primitives::Recovered;
1921
use reth_provider::{ProviderError, StateProvider};
2022
use reth_revm::{State, database::StateProviderDatabase};
23+
use reth_rpc_api::eth::{EthTxEnvError, transaction::TryIntoTxEnv};
2124
use revm::{
22-
DatabaseCommit,
23-
context::result::{EVMError, ExecutionResult, ResultAndState},
25+
DatabaseCommit, DatabaseRef,
26+
context::{
27+
ContextTr,
28+
result::{EVMError, ExecutionResult, ResultAndState},
29+
},
2430
inspector::NoOpInspector,
2531
state::Account,
2632
};
33+
use std::borrow::Cow;
2734
use tracing::warn;
2835

2936
use crate::{
3037
builders::context::OpPayloadBuilderCtx, primitives::reth::ExecutionInfo, tx_signer::Signer,
3138
};
3239

3340
#[derive(Debug, Default)]
34-
pub struct SimulationSuccessResult {
41+
pub struct SimulationSuccessResult<T: SolCall> {
3542
pub gas_used: u64,
36-
pub output: Bytes,
43+
pub output: T::Return,
3744
pub state_changes: HashMap<Address, Account>,
3845
}
3946

@@ -61,8 +68,8 @@ impl BuilderTransactionCtx {
6168

6269
#[derive(Debug, thiserror::Error)]
6370
pub enum InvalidContractDataError {
64-
#[error("did not find expected log {0:?} in emitted logs")]
65-
InvalidLogs(B256),
71+
#[error("did not find expected logs expected {0:?} but got {1:?}")]
72+
InvalidLogs(Vec<B256>, Vec<B256>),
6673
#[error("could not decode output from contract call")]
6774
OutputAbiDecodeError,
6875
}
@@ -80,11 +87,11 @@ pub enum BuilderTransactionError {
8087
#[error("contract {0} may be incorrect, invalid contract data: {1}")]
8188
InvalidContract(Address, InvalidContractDataError),
8289
/// Transaction halted execution
83-
#[error("transaction halted {0:?}")]
84-
TransactionHalted(OpHaltReason),
90+
#[error("transaction to {0} halted {1:?}")]
91+
TransactionHalted(Address, OpHaltReason),
8592
/// Transaction reverted
86-
#[error("transaction reverted {0}")]
87-
TransactionReverted(Box<dyn core::error::Error + Send + Sync>),
93+
#[error("transaction to {0} reverted {1}")]
94+
TransactionReverted(Address, Revert),
8895
/// Invalid tx errors during evm execution.
8996
#[error("invalid transaction error {0}")]
9097
InvalidTransactionError(Box<dyn core::error::Error + Send + Sync>),
@@ -108,6 +115,12 @@ impl From<EVMError<ProviderError, OpTransactionError>> for BuilderTransactionErr
108115
}
109116
}
110117

118+
impl From<EthTxEnvError> for BuilderTransactionError {
119+
fn from(error: EthTxEnvError) -> Self {
120+
BuilderTransactionError::EvmExecutionError(Box::new(error))
121+
}
122+
}
123+
111124
impl From<BuilderTransactionError> for PayloadBuilderError {
112125
fn from(error: BuilderTransactionError) -> Self {
113126
match error {
@@ -130,15 +143,35 @@ impl BuilderTransactionError {
130143
}
131144

132145
pub trait BuilderTransactions<ExtraCtx: Debug + Default = (), Extra: Debug + Default = ()> {
146+
// Simulates and returns the signed builder transactions. The simulation modifies and commit
147+
// changes to the db so call new_simulation_state to simulate on a new copy of the state
133148
fn simulate_builder_txs(
134149
&self,
135150
state_provider: impl StateProvider + Clone,
136151
info: &mut ExecutionInfo<Extra>,
137152
ctx: &OpPayloadBuilderCtx<ExtraCtx>,
138-
db: &mut State<impl Database>,
153+
db: &mut State<impl Database + DatabaseRef>,
139154
top_of_block: bool,
140155
) -> Result<Vec<BuilderTransactionCtx>, BuilderTransactionError>;
141156

157+
fn simulate_builder_txs_with_new_state(
158+
&self,
159+
state_provider: impl StateProvider + Clone,
160+
info: &mut ExecutionInfo<Extra>,
161+
ctx: &OpPayloadBuilderCtx<ExtraCtx>,
162+
db: &State<impl Database>,
163+
top_of_block: bool,
164+
) -> Result<Vec<BuilderTransactionCtx>, BuilderTransactionError> {
165+
let mut simulation_state = self.new_simulation_state(state_provider.clone(), db);
166+
self.simulate_builder_txs(
167+
state_provider,
168+
info,
169+
ctx,
170+
&mut simulation_state,
171+
top_of_block,
172+
)
173+
}
174+
142175
fn add_builder_txs(
143176
&self,
144177
state_provider: impl StateProvider + Clone,
@@ -148,19 +181,20 @@ pub trait BuilderTransactions<ExtraCtx: Debug + Default = (), Extra: Debug + Def
148181
top_of_block: bool,
149182
) -> Result<Vec<BuilderTransactionCtx>, BuilderTransactionError> {
150183
{
184+
let builder_txs = self.simulate_builder_txs_with_new_state(
185+
state_provider,
186+
info,
187+
builder_ctx,
188+
db,
189+
top_of_block,
190+
)?;
191+
151192
let mut evm = builder_ctx
152193
.evm_config
153194
.evm_with_env(&mut *db, builder_ctx.evm_env.clone());
154195

155196
let mut invalid: HashSet<Address> = HashSet::new();
156197

157-
let builder_txs = self.simulate_builder_txs(
158-
state_provider,
159-
info,
160-
builder_ctx,
161-
evm.db_mut(),
162-
top_of_block,
163-
)?;
164198
for builder_tx in builder_txs.iter() {
165199
if builder_tx.is_top_of_block != top_of_block {
166200
// don't commit tx if the buidler tx is not being added in the intended
@@ -211,52 +245,37 @@ pub trait BuilderTransactions<ExtraCtx: Debug + Default = (), Extra: Debug + Def
211245
}
212246
}
213247

214-
fn simulate_builder_txs_state(
248+
// Creates a copy of the state to simulate against
249+
fn new_simulation_state(
215250
&self,
216-
state_provider: impl StateProvider + Clone,
217-
builder_txs: Vec<&BuilderTransactionCtx>,
218-
ctx: &OpPayloadBuilderCtx<ExtraCtx>,
219-
db: &mut State<impl Database>,
220-
) -> Result<State<StateProviderDatabase<impl StateProvider>>, BuilderTransactionError> {
221-
let state = StateProviderDatabase::new(state_provider.clone());
222-
let mut simulation_state = State::builder()
251+
state_provider: impl StateProvider,
252+
db: &State<impl Database>,
253+
) -> State<StateProviderDatabase<impl StateProvider>> {
254+
let state = StateProviderDatabase::new(state_provider);
255+
256+
State::builder()
223257
.with_database(state)
224258
.with_cached_prestate(db.cache.clone())
225259
.with_bundle_update()
226-
.build();
227-
let mut evm = ctx
228-
.evm_config
229-
.evm_with_env(&mut simulation_state, ctx.evm_env.clone());
230-
231-
for builder_tx in builder_txs {
232-
let ResultAndState { state, .. } = evm
233-
.transact(&builder_tx.signed_tx)
234-
.map_err(|err| BuilderTransactionError::EvmExecutionError(Box::new(err)))?;
235-
236-
evm.db_mut().commit(state);
237-
}
238-
239-
Ok(simulation_state)
260+
.build()
240261
}
241262

242263
fn sign_tx(
243264
&self,
244265
to: Address,
245266
from: Signer,
246-
gas_used: Option<u64>,
267+
gas_used: u64,
247268
calldata: Bytes,
248269
ctx: &OpPayloadBuilderCtx<ExtraCtx>,
249-
db: &mut State<impl Database>,
270+
db: impl DatabaseRef,
250271
) -> Result<Recovered<OpTransactionSigned>, BuilderTransactionError> {
251272
let nonce = get_nonce(db, from.address)?;
252273
// Create the EIP-1559 transaction
253274
let tx = OpTypedTransaction::Eip1559(TxEip1559 {
254275
chain_id: ctx.chain_id(),
255276
nonce,
256277
// Due to EIP-150, 63/64 of available gas is forwarded to external calls so need to add a buffer
257-
gas_limit: gas_used
258-
.map(|gas| gas * 64 / 63)
259-
.unwrap_or(ctx.block_gas_limit()),
278+
gas_limit: gas_used * 64 / 63,
260279
max_fee_per_gas: ctx.base_fee().into(),
261280
to: TxKind::Call(to),
262281
input: calldata,
@@ -265,18 +284,32 @@ pub trait BuilderTransactions<ExtraCtx: Debug + Default = (), Extra: Debug + Def
265284
Ok(from.sign_tx(tx)?)
266285
}
267286

268-
fn simulate_call(
287+
fn commit_txs(
288+
&self,
289+
signed_txs: Vec<Recovered<OpTransactionSigned>>,
290+
ctx: &OpPayloadBuilderCtx<ExtraCtx>,
291+
db: &mut State<impl Database>,
292+
) -> Result<(), BuilderTransactionError> {
293+
let mut evm = ctx.evm_config.evm_with_env(&mut *db, ctx.evm_env.clone());
294+
for signed_tx in signed_txs {
295+
let ResultAndState { state, .. } = evm
296+
.transact(&signed_tx)
297+
.map_err(|err| BuilderTransactionError::EvmExecutionError(Box::new(err)))?;
298+
evm.db_mut().commit(state)
299+
}
300+
Ok(())
301+
}
302+
303+
fn simulate_call<T: SolCall, E: SolInterface>(
269304
&self,
270-
signed_tx: Recovered<OpTransactionSigned>,
271-
expected_topic: Option<B256>,
272-
revert_handler: impl FnOnce(Bytes) -> BuilderTransactionError,
273-
evm: &mut OpEvm<
274-
&mut State<StateProviderDatabase<impl StateProvider>>,
275-
NoOpInspector,
276-
PrecompilesMap,
277-
>,
278-
) -> Result<SimulationSuccessResult, BuilderTransactionError> {
279-
let ResultAndState { result, state } = match evm.transact(&signed_tx) {
305+
tx: OpTransactionRequest,
306+
expected_logs: Vec<B256>,
307+
evm: &mut OpEvm<impl Database, NoOpInspector, PrecompilesMap>,
308+
) -> Result<SimulationSuccessResult<T>, BuilderTransactionError> {
309+
let tx_env = tx.try_into_tx_env(evm.cfg(), evm.block())?;
310+
let to = tx_env.base.kind.into_to().unwrap_or_default();
311+
312+
let ResultAndState { result, state } = match evm.transact(tx_env) {
280313
Ok(res) => res,
281314
Err(err) => {
282315
if err.is_invalid_tx_err() {
@@ -291,28 +324,54 @@ pub trait BuilderTransactions<ExtraCtx: Debug + Default = (), Extra: Debug + Def
291324

292325
match result {
293326
ExecutionResult::Success {
294-
logs,
295-
gas_used,
296327
output,
328+
gas_used,
329+
logs,
297330
..
298331
} => {
299-
if let Some(topic) = expected_topic
300-
&& !logs.iter().any(|log| log.topics().first() == Some(&topic))
332+
let topics: HashSet<B256> = logs
333+
.into_iter()
334+
.flat_map(|log| log.topics().to_vec())
335+
.collect();
336+
if !expected_logs
337+
.iter()
338+
.all(|expected_topic| topics.contains(expected_topic))
301339
{
302340
return Err(BuilderTransactionError::InvalidContract(
303-
signed_tx.to().unwrap_or_default(),
304-
InvalidContractDataError::InvalidLogs(topic),
341+
to,
342+
InvalidContractDataError::InvalidLogs(
343+
expected_logs,
344+
topics.into_iter().collect(),
345+
),
305346
));
306347
}
307-
Ok(SimulationSuccessResult {
348+
let return_output = T::abi_decode_returns(&output.into_data()).map_err(|_| {
349+
BuilderTransactionError::InvalidContract(
350+
to,
351+
InvalidContractDataError::OutputAbiDecodeError,
352+
)
353+
})?;
354+
Ok(SimulationSuccessResult::<T> {
308355
gas_used,
309-
output: output.into_data(),
356+
output: return_output,
310357
state_changes: state,
311358
})
312359
}
313-
ExecutionResult::Revert { output, .. } => Err(revert_handler(output)),
360+
ExecutionResult::Revert { output, .. } => {
361+
let revert = ContractError::<E>::abi_decode(&output)
362+
.and_then(|reason| {
363+
reason
364+
.try_into()
365+
.map_err(|_| Error::Other(Cow::Borrowed("failed to convert to revert")))
366+
})
367+
.or_else(|_| Revert::abi_decode(&output))
368+
.unwrap_or_else(|_| {
369+
Revert::from(format!("unknown revert: {}", hex::encode(&output)))
370+
});
371+
Err(BuilderTransactionError::TransactionReverted(to, revert))
372+
}
314373
ExecutionResult::Halt { reason, .. } => Err(BuilderTransactionError::other(
315-
BuilderTransactionError::TransactionHalted(reason),
374+
BuilderTransactionError::TransactionHalted(to, reason),
316375
)),
317376
}
318377
}
@@ -335,7 +394,7 @@ impl<ExtraCtx: Debug + Default> BuilderTxBase<ExtraCtx> {
335394
pub(super) fn simulate_builder_tx(
336395
&self,
337396
ctx: &OpPayloadBuilderCtx<ExtraCtx>,
338-
db: &mut State<impl Database>,
397+
db: impl DatabaseRef,
339398
) -> Result<Option<BuilderTransactionCtx>, BuilderTransactionError> {
340399
match self.signer {
341400
Some(signer) => {
@@ -380,15 +439,12 @@ impl<ExtraCtx: Debug + Default> BuilderTxBase<ExtraCtx> {
380439
fn signed_builder_tx(
381440
&self,
382441
ctx: &OpPayloadBuilderCtx<ExtraCtx>,
383-
db: &mut State<impl Database>,
442+
db: impl DatabaseRef,
384443
signer: Signer,
385444
gas_used: u64,
386445
message: Vec<u8>,
387446
) -> Result<Recovered<OpTransactionSigned>, BuilderTransactionError> {
388-
let nonce = db
389-
.load_cache_account(signer.address)
390-
.map(|acc| acc.account_info().unwrap_or_default().nonce)
391-
.map_err(|_| BuilderTransactionError::AccountLoadFailed(signer.address))?;
447+
let nonce = get_nonce(db, signer.address)?;
392448

393449
// Create the EIP-1559 transaction
394450
let tx = OpTypedTransaction::Eip1559(TxEip1559 {
@@ -411,20 +467,17 @@ impl<ExtraCtx: Debug + Default> BuilderTxBase<ExtraCtx> {
411467
}
412468
}
413469

414-
pub fn get_nonce(
415-
db: &mut State<impl Database>,
416-
address: Address,
417-
) -> Result<u64, BuilderTransactionError> {
418-
db.load_cache_account(address)
419-
.map(|acc| acc.account_info().unwrap_or_default().nonce)
470+
pub fn get_nonce(db: impl DatabaseRef, address: Address) -> Result<u64, BuilderTransactionError> {
471+
db.basic_ref(address)
472+
.map(|acc| acc.unwrap_or_default().nonce)
420473
.map_err(|_| BuilderTransactionError::AccountLoadFailed(address))
421474
}
422475

423476
pub fn get_balance(
424-
db: &mut State<impl Database>,
477+
db: impl DatabaseRef,
425478
address: Address,
426479
) -> Result<U256, BuilderTransactionError> {
427-
db.load_cache_account(address)
428-
.map(|acc| acc.account_info().unwrap_or_default().balance)
480+
db.basic_ref(address)
481+
.map(|acc| acc.unwrap_or_default().balance)
429482
.map_err(|_| BuilderTransactionError::AccountLoadFailed(address))
430483
}

0 commit comments

Comments
 (0)