-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add debug_executePayload
RPC to capture arbitrary execution witnesses
#12238
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
use alloy_eips::eip2718::Encodable2718; | ||
use alloy_primitives::{Address, Bytes, B256, U256}; | ||
use alloy_eips::eip2718::{Encodable2718, Decodable2718}; | ||
use alloy_primitives::{Address, BlockHash, Bytes, Keccak256, B256, U256}; | ||
use alloy_rlp::{Decodable, Encodable}; | ||
use alloy_rpc_types::{ | ||
state::EvmOverrides, Block as RpcBlock, BlockError, Bundle, StateContext, TransactionInfo, | ||
engine::PayloadAttributes, state::EvmOverrides, Block as RpcBlock, BlockError, Bundle, | ||
StateContext, TransactionInfo, | ||
}; | ||
use alloy_rpc_types_debug::ExecutionWitness; | ||
use alloy_rpc_types_eth::transaction::TransactionRequest; | ||
|
@@ -17,9 +18,9 @@ use reth_chainspec::EthereumHardforks; | |
use reth_evm::{ | ||
execute::{BlockExecutorProvider, Executor}, | ||
system_calls::SystemCaller, | ||
ConfigureEvmEnv, | ||
ConfigureEvm, ConfigureEvmEnv, NextBlockEnvAttributes, | ||
}; | ||
use reth_primitives::{Block, BlockId, BlockNumberOrTag, TransactionSignedEcRecovered}; | ||
use reth_primitives::{Block, BlockId, BlockNumberOrTag, TransactionSignedEcRecovered, TransactionSigned}; | ||
use reth_provider::{ | ||
BlockReaderIdExt, ChainSpecProvider, HeaderProvider, StateProofProvider, StateProviderFactory, | ||
TransactionVariant, | ||
|
@@ -41,7 +42,7 @@ use revm::{ | |
use revm_inspectors::tracing::{ | ||
FourByteInspector, MuxInspector, TracingInspector, TracingInspectorConfig, TransactionContext, | ||
}; | ||
use revm_primitives::{keccak256, HashMap}; | ||
use revm_primitives::{keccak256, HashMap, ResultAndState, TxEnv}; | ||
use std::sync::Arc; | ||
use tokio::sync::{AcquireError, OwnedSemaphorePermit}; | ||
|
||
|
@@ -733,6 +734,121 @@ where | |
.await | ||
} | ||
|
||
/// The `debug_executePayload` method allows for execution of a block with the purpose of | ||
/// generating an execution witness. The witness comprises of a map of all hashed trie nodes | ||
/// to their preimages that were required during the execution of the block, including during | ||
/// state root recomputation. | ||
pub async fn debug_execute_payload( | ||
&self, | ||
parent_block_hash: BlockHash, | ||
attributes: PayloadAttributes, | ||
txs: Vec<Bytes>, | ||
) -> Result<ExecutionWitness, Eth::Error> { | ||
let transactions = txs | ||
.into_iter() | ||
.map(|b| { TransactionSigned::decode_2718(&mut b.as_ref()) }) | ||
.into_iter() | ||
.collect::<Result<Vec<_>, _>>() | ||
.map_err(|err| EthApiError::InvalidParams(err.to_string()))?; | ||
|
||
let parent_block_header = self | ||
.eth_api() | ||
.block_with_senders(parent_block_hash.into()) | ||
.await | ||
.transpose() | ||
.ok_or(EthApiError::HeaderNotFound(parent_block_hash.into()))??; | ||
|
||
let eth_api = self.eth_api().clone(); | ||
let (cfg, block_env) = eth_api | ||
.evm_config() | ||
.next_cfg_and_block_env( | ||
&parent_block_header.header.clone().unseal(), | ||
NextBlockEnvAttributes { | ||
prev_randao: attributes.prev_randao, | ||
timestamp: attributes.timestamp, | ||
suggested_fee_recipient: attributes.suggested_fee_recipient, | ||
}, | ||
) | ||
.map_err(|err| EthApiError::InvalidParams(err.to_string()))?; | ||
|
||
self.eth_api() | ||
.spawn_with_state_at_block(parent_block_hash.into(), move |state_provider: reth_rpc_eth_types::cache::db::StateProviderTraitObjWrapper<'_>| { | ||
let env = EnvWithHandlerCfg::new_with_cfg_env(cfg, block_env, TxEnv::default()); | ||
let db = StateProviderDatabase::new(&state_provider); | ||
let state_db = | ||
State::builder().with_database(db).with_bundle_update().without_state_clear().build(); | ||
let mut evm = eth_api.evm_config().evm_with_env(state_db, env); | ||
let mut hasher: Keccak256 = Keccak256::new(); | ||
|
||
let mut tx_iter = transactions.into_iter().peekable(); | ||
|
||
while let Some(tx) = tx_iter.next() { | ||
let signer = tx.recover_signer().ok_or( | ||
EthApiError::InvalidTransactionSignature | ||
)?; | ||
|
||
hasher.update(tx.hash()); | ||
eth_api.evm_config().fill_tx_env(evm.tx_mut(), &tx, signer); | ||
|
||
match evm.transact() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the first draft of the Curious the Ithaca team's take here. Originally we didn't want to add this directly into the RPC endpoint implementation because of the duplication. For this we need to be able to access the payload building codepath, which may be a bit more annoying to get into the |
||
Ok(ResultAndState { result: _result, state }) => { | ||
// need to apply the state changes of this call before executing the | ||
// next call | ||
if tx_iter.peek().is_some() { | ||
evm.context.evm.db.commit(state) | ||
} | ||
} | ||
Err(_evm_err) => { | ||
// errors are currently ignored, but stop processing more txs | ||
break | ||
} | ||
} | ||
} | ||
|
||
|
||
let mut hashed_state = HashedPostState::default(); | ||
let mut keys = HashMap::default(); | ||
|
||
let codes = evm.context.evm.db.cache | ||
.contracts | ||
.iter() | ||
.map(|(hash, code)| (*hash, code.original_bytes())) | ||
.collect(); | ||
|
||
for (address, account) in &evm.context.evm.db.cache.accounts { | ||
let hashed_address = keccak256(address); | ||
hashed_state.accounts.insert( | ||
hashed_address, | ||
account.account.as_ref().map(|a| a.info.clone().into()), | ||
); | ||
|
||
let storage = | ||
hashed_state.storages.entry(hashed_address).or_insert_with( | ||
|| HashedStorage::new(account.status.was_destroyed()), | ||
); | ||
|
||
if let Some(account) = &account.account { | ||
keys.insert(hashed_address, address.to_vec().into()); | ||
|
||
for (slot, value) in &account.storage { | ||
let slot = B256::from(*slot); | ||
let hashed_slot = keccak256(slot); | ||
storage.storage.insert(hashed_slot, *value); | ||
|
||
keys.insert(hashed_slot, slot.into()); | ||
} | ||
} | ||
} | ||
// drop evm so db is released. | ||
drop(evm); | ||
|
||
let state = | ||
state_provider.witness(Default::default(), hashed_state).map_err(Into::into)?; | ||
Ok(ExecutionWitness { state: state.into_iter().collect(), codes, keys }) | ||
}) | ||
.await | ||
} | ||
|
||
/// Executes the configured transaction with the environment on the given database. | ||
/// | ||
/// It optionally takes fused inspector ([`TracingInspector::fused`]) to avoid re-creating the | ||
|
@@ -1067,6 +1183,18 @@ where | |
Self::debug_execution_witness(self, block).await.map_err(Into::into) | ||
} | ||
|
||
async fn debug_execute_payload( | ||
&self, | ||
parent_block_hash: BlockHash, | ||
attributes: PayloadAttributes, | ||
transactions: Vec<Bytes>, | ||
) -> RpcResult<ExecutionWitness> { | ||
let _permit = self.acquire_trace_permit().await; | ||
Self::debug_execute_payload(self, parent_block_hash, attributes, transactions) | ||
.await | ||
.map_err(Into::into) | ||
} | ||
|
||
async fn debug_backtrace_at(&self, _location: &str) -> RpcResult<()> { | ||
Ok(()) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A pretty key thing about this endpoint is that it can return witnesses in blocks that fail to entirely execute. With Holocene, there's the following case:
It looks like we're currently ignoring the errors during transaction processing (and short-circuiting if the block fails to fully execute), though will also need to return whether the payload could be cleanly applied on top of the state at the block requested for the consumer.