Skip to content
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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion crates/rpc/rpc-api/src/debug.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use alloy_primitives::{Address, Bytes, B256};
use alloy_primitives::{Address, BlockHash, Bytes, B256};
use alloy_rpc_types::{Block, Bundle, StateContext};
use alloy_rpc_types_debug::ExecutionWitness;
use alloy_rpc_types_engine::PayloadAttributes;
use alloy_rpc_types_eth::transaction::TransactionRequest;
use alloy_rpc_types_trace::geth::{
BlockTraceResult, GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, TraceResult,
Expand Down Expand Up @@ -143,6 +144,21 @@ pub trait DebugApi {
async fn debug_execution_witness(&self, block: BlockNumberOrTag)
-> RpcResult<ExecutionWitness>;

/// The `debug_executePayload` method allows for re-execution of a group of transactions 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.
///
/// The first argument is the block number or block hash. The second argument is the payload
/// attributes for the new block. The third argument is a list of transactions to be included.
#[method(name = "executePayload")]
async fn debug_execute_payload(
&self,
parent_block_hash: BlockHash,
attributes: PayloadAttributes,
transactions: Vec<Bytes>,
) -> RpcResult<ExecutionWitness>;

/// Sets the logging backtrace location. When a backtrace location is set and a log message is
/// emitted at that location, the stack of the goroutine executing the log statement will
/// be printed to stderr.
Expand Down
140 changes: 134 additions & 6 deletions crates/rpc/rpc/src/debug.rs
Copy link
Collaborator

@clabby clabby Nov 1, 2024

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:

  1. Block $A$ is sent to the engine, has a user-space tx that has an invalid signature.
  2. Engine rejects block $A$
  3. Rollup node trims off user-space transactions, sends deposit-only payload back to the engine (block $A\prime$).
  4. Engine accepts block $A\prime$.

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.

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;
Expand All @@ -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,
Expand All @@ -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};

Expand Down Expand Up @@ -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() {
Copy link
Collaborator

@clabby clabby Nov 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the first draft of the debug_executionWitness endpoint, we do need a bit more here. This endpoint also needs to apply the system transactions (4788) and OP Stack system transactions (Canyon's create2 deployer call, etc.)

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 debug API.

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
Expand Down Expand Up @@ -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(())
}
Expand Down