Make the ethereum node trait object compatible

This commit is contained in:
Omar Abdulla
2025-09-17 06:01:13 +03:00
parent 9fc74aeea0
commit 8a05f8e6e8
5 changed files with 231 additions and 190 deletions
+8 -8
View File
@@ -8,7 +8,7 @@ use alloy::consensus::EMPTY_ROOT_HASH;
use alloy::hex; use alloy::hex;
use alloy::json_abi::JsonAbi; use alloy::json_abi::JsonAbi;
use alloy::network::{Ethereum, TransactionBuilder}; use alloy::network::{Ethereum, TransactionBuilder};
use alloy::primitives::U256; use alloy::primitives::{TxHash, U256};
use alloy::rpc::types::TransactionReceipt; use alloy::rpc::types::TransactionReceipt;
use alloy::rpc::types::trace::geth::{ use alloy::rpc::types::trace::geth::{
CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType, CallFrame, GethDebugBuiltInTracerType, GethDebugTracerConfig, GethDebugTracerType,
@@ -124,14 +124,14 @@ where
.await .await
.context("Failed during transaction execution phase of input handling")?; .context("Failed during transaction execution phase of input handling")?;
let tracing_result = self let tracing_result = self
.handle_input_call_frame_tracing(&execution_receipt, node) .handle_input_call_frame_tracing(execution_receipt.transaction_hash, node)
.await .await
.context("Failed during callframe tracing phase of input handling")?; .context("Failed during callframe tracing phase of input handling")?;
self.handle_input_variable_assignment(input, &tracing_result) self.handle_input_variable_assignment(input, &tracing_result)
.context("Failed to assign variables from callframe output")?; .context("Failed to assign variables from callframe output")?;
let (_, (geth_trace, diff_mode)) = try_join!( let (_, (geth_trace, diff_mode)) = try_join!(
self.handle_input_expectations(input, &execution_receipt, node, &tracing_result), self.handle_input_expectations(input, &execution_receipt, node, &tracing_result),
self.handle_input_diff(&execution_receipt, node) self.handle_input_diff(execution_receipt.transaction_hash, node)
) )
.context("Failed while evaluating expectations and diffs in parallel")?; .context("Failed while evaluating expectations and diffs in parallel")?;
Ok((execution_receipt, geth_trace, diff_mode)) Ok((execution_receipt, geth_trace, diff_mode))
@@ -250,11 +250,11 @@ where
#[instrument(level = "info", skip_all)] #[instrument(level = "info", skip_all)]
async fn handle_input_call_frame_tracing( async fn handle_input_call_frame_tracing(
&self, &self,
execution_receipt: &TransactionReceipt, tx_hash: TxHash,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<CallFrame> { ) -> anyhow::Result<CallFrame> {
node.trace_transaction( node.trace_transaction(
execution_receipt, tx_hash,
GethDebugTracingOptions { GethDebugTracingOptions {
tracer: Some(GethDebugTracerType::BuiltInTracer( tracer: Some(GethDebugTracerType::BuiltInTracer(
GethDebugBuiltInTracerType::CallTracer, GethDebugBuiltInTracerType::CallTracer,
@@ -507,7 +507,7 @@ where
#[instrument(level = "info", skip_all)] #[instrument(level = "info", skip_all)]
async fn handle_input_diff( async fn handle_input_diff(
&self, &self,
execution_receipt: &TransactionReceipt, tx_hash: TxHash,
node: &T::Blockchain, node: &T::Blockchain,
) -> anyhow::Result<(GethTrace, DiffMode)> { ) -> anyhow::Result<(GethTrace, DiffMode)> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
@@ -517,11 +517,11 @@ where
}); });
let trace = node let trace = node
.trace_transaction(execution_receipt, trace_options) .trace_transaction(tx_hash, trace_options)
.await .await
.context("Failed to obtain geth prestate tracer output")?; .context("Failed to obtain geth prestate tracer output")?;
let diff = node let diff = node
.state_diff(execution_receipt) .state_diff(tx_hash)
.await .await
.context("Failed to obtain state diff for transaction")?; .context("Failed to obtain state diff for transaction")?;
+1 -1
View File
@@ -53,5 +53,5 @@ pub trait DynPlatform {
/// Creates a new node for the platform by spawning a new thread, creating the node object, /// Creates a new node for the platform by spawning a new thread, creating the node object,
/// initializing it, spawning it, and waiting for it to start up. /// initializing it, spawning it, and waiting for it to start up.
fn new_node(&self) -> Box<dyn PlatformNode>; fn new_node(&self) -> Box<dyn EthereumNode>;
} }
+11 -8
View File
@@ -1,39 +1,42 @@
//! This crate implements all node interactions. //! This crate implements all node interactions.
use alloy::primitives::{Address, StorageKey, U256}; use std::pin::Pin;
use alloy::primitives::{Address, StorageKey, TxHash, U256};
use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace}; use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace};
use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest}; use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest};
use anyhow::Result; use anyhow::Result;
use revive_dt_format::traits::ResolverApi; use revive_dt_format::traits::ResolverApi;
/// An interface for all interactions with Ethereum compatible nodes. /// An interface for all interactions with Ethereum compatible nodes.
#[allow(clippy::type_complexity)]
pub trait EthereumNode { pub trait EthereumNode {
/// Execute the [TransactionRequest] and return a [TransactionReceipt]. /// Execute the [TransactionRequest] and return a [TransactionReceipt].
fn execute_transaction( fn execute_transaction(
&self, &self,
transaction: TransactionRequest, transaction: TransactionRequest,
) -> impl Future<Output = Result<TransactionReceipt>>; ) -> Pin<Box<dyn Future<Output = Result<TransactionReceipt>> + '_>>;
/// Trace the transaction in the [TransactionReceipt] and return a [GethTrace]. /// Trace the transaction in the [TransactionReceipt] and return a [GethTrace].
fn trace_transaction( fn trace_transaction(
&self, &self,
receipt: &TransactionReceipt, tx_hash: TxHash,
trace_options: GethDebugTracingOptions, trace_options: GethDebugTracingOptions,
) -> impl Future<Output = Result<GethTrace>>; ) -> Pin<Box<dyn Future<Output = Result<GethTrace>> + '_>>;
/// Returns the state diff of the transaction hash in the [TransactionReceipt]. /// Returns the state diff of the transaction hash in the [TransactionReceipt].
fn state_diff(&self, receipt: &TransactionReceipt) -> impl Future<Output = Result<DiffMode>>; fn state_diff(&self, tx_hash: TxHash) -> Pin<Box<dyn Future<Output = Result<DiffMode>> + '_>>;
/// Returns the balance of the provided [`Address`] back. /// Returns the balance of the provided [`Address`] back.
fn balance_of(&self, address: Address) -> impl Future<Output = Result<U256>>; fn balance_of(&self, address: Address) -> Pin<Box<dyn Future<Output = Result<U256>> + '_>>;
/// Returns the latest storage proof of the provided [`Address`] /// Returns the latest storage proof of the provided [`Address`]
fn latest_state_proof( fn latest_state_proof(
&self, &self,
address: Address, address: Address,
keys: Vec<StorageKey>, keys: Vec<StorageKey>,
) -> impl Future<Output = Result<EIP1186AccountProofResponse>>; ) -> Pin<Box<dyn Future<Output = Result<EIP1186AccountProofResponse>> + '_>>;
/// Returns the resolver that is to use with this ethereum node. /// Returns the resolver that is to use with this ethereum node.
fn resolver(&self) -> impl Future<Output = Result<Box<dyn ResolverApi + '_>>>; fn resolver(&self) -> Pin<Box<dyn Future<Output = Result<Box<dyn ResolverApi + '_>>> + '_>>;
} }
+139 -119
View File
@@ -25,7 +25,7 @@ use alloy::{
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
}, },
rpc::types::{ rpc::types::{
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest, EIP1186AccountProofResponse, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
}, },
}; };
@@ -296,152 +296,172 @@ impl EthereumNode for GethNode {
fields(geth_node_id = self.id, connection_string = self.connection_string), fields(geth_node_id = self.id, connection_string = self.connection_string),
err, err,
)] )]
async fn execute_transaction( fn execute_transaction(
&self, &self,
transaction: TransactionRequest, transaction: TransactionRequest,
) -> anyhow::Result<alloy::rpc::types::TransactionReceipt> { ) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::TransactionReceipt>> + '_>>
let provider = self {
.provider() Box::pin(async move {
.await let provider = self
.context("Failed to create provider for transaction submission")?; .provider()
.await
.context("Failed to create provider for transaction submission")?;
let pending_transaction = provider let pending_transaction = provider
.send_transaction(transaction) .send_transaction(transaction)
.await .await
.inspect_err( .inspect_err(
|err| tracing::error!(%err, "Encountered an error when submitting the transaction"), |err| tracing::error!(%err, "Encountered an error when submitting the transaction"),
) )
.context("Failed to submit transaction to geth node")?; .context("Failed to submit transaction to geth node")?;
let transaction_hash = *pending_transaction.tx_hash(); let transaction_hash = *pending_transaction.tx_hash();
// The following is a fix for the "transaction indexing is in progress" error that we used // The following is a fix for the "transaction indexing is in progress" error that we used
// to get. You can find more information on this in the following GH issue in geth // to get. You can find more information on this in the following GH issue in geth
// https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on, // https://github.com/ethereum/go-ethereum/issues/28877. To summarize what's going on,
// before we can get the receipt of the transaction it needs to have been indexed by the // before we can get the receipt of the transaction it needs to have been indexed by the
// node's indexer. Just because the transaction has been confirmed it doesn't mean that it // node's indexer. Just because the transaction has been confirmed it doesn't mean that it
// has been indexed. When we call alloy's `get_receipt` it checks if the transaction was // has been indexed. When we call alloy's `get_receipt` it checks if the transaction was
// confirmed. If it has been, then it will call `eth_getTransactionReceipt` method which // confirmed. If it has been, then it will call `eth_getTransactionReceipt` method which
// _might_ return the above error if the tx has not yet been indexed yet. So, we need to // _might_ return the above error if the tx has not yet been indexed yet. So, we need to
// implement a retry mechanism for the receipt to keep retrying to get it until it // implement a retry mechanism for the receipt to keep retrying to get it until it
// eventually works, but we only do that if the error we get back is the "transaction // eventually works, but we only do that if the error we get back is the "transaction
// indexing is in progress" error or if the receipt is None. // indexing is in progress" error or if the receipt is None.
// //
// Getting the transaction indexed and taking a receipt can take a long time especially when // Getting the transaction indexed and taking a receipt can take a long time especially when
// a lot of transactions are being submitted to the node. Thus, while initially we only // a lot of transactions are being submitted to the node. Thus, while initially we only
// allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for // allowed for 60 seconds of waiting with a 1 second delay in polling, we need to allow for
// a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential // a larger wait time. Therefore, in here we allow for 5 minutes of waiting with exponential
// backoff each time we attempt to get the receipt and find that it's not available. // backoff each time we attempt to get the receipt and find that it's not available.
let provider = Arc::new(provider); let provider = Arc::new(provider);
poll( poll(
Self::RECEIPT_POLLING_DURATION, Self::RECEIPT_POLLING_DURATION,
PollingWaitBehavior::Constant(Duration::from_millis(200)), PollingWaitBehavior::Constant(Duration::from_millis(200)),
move || { move || {
let provider = provider.clone(); let provider = provider.clone();
async move { async move {
match provider.get_transaction_receipt(transaction_hash).await { match provider.get_transaction_receipt(transaction_hash).await {
Ok(Some(receipt)) => Ok(ControlFlow::Break(receipt)), Ok(Some(receipt)) => Ok(ControlFlow::Break(receipt)),
Ok(None) => Ok(ControlFlow::Continue(())), Ok(None) => Ok(ControlFlow::Continue(())),
Err(error) => { Err(error) => {
let error_string = error.to_string(); let error_string = error.to_string();
match error_string.contains(Self::TRANSACTION_INDEXING_ERROR) { match error_string.contains(Self::TRANSACTION_INDEXING_ERROR) {
true => Ok(ControlFlow::Continue(())), true => Ok(ControlFlow::Continue(())),
false => Err(error.into()), false => Err(error.into()),
}
} }
} }
} }
} },
}, )
) .instrument(tracing::info_span!(
.instrument(tracing::info_span!( "Awaiting transaction receipt",
"Awaiting transaction receipt", ?transaction_hash
?transaction_hash ))
)) .await
.await })
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn trace_transaction( fn trace_transaction(
&self, &self,
transaction: &TransactionReceipt, tx_hash: TxHash,
trace_options: GethDebugTracingOptions, trace_options: GethDebugTracingOptions,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> { ) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::trace::geth::GethTrace>> + '_>>
let provider = Arc::new( {
Box::pin(async move {
let provider = Arc::new(
self.provider()
.await
.context("Failed to create provider for tracing")?,
);
poll(
Self::TRACE_POLLING_DURATION,
PollingWaitBehavior::Constant(Duration::from_millis(200)),
move || {
let provider = provider.clone();
let trace_options = trace_options.clone();
async move {
match provider
.debug_trace_transaction(tx_hash, trace_options)
.await
{
Ok(trace) => Ok(ControlFlow::Break(trace)),
Err(error) => {
let error_string = error.to_string();
match error_string.contains(Self::TRANSACTION_TRACING_ERROR) {
true => Ok(ControlFlow::Continue(())),
false => Err(error.into()),
}
}
}
}
},
)
.await
})
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn state_diff(
&self,
tx_hash: TxHash,
) -> Pin<Box<dyn Future<Output = anyhow::Result<DiffMode>> + '_>> {
Box::pin(async move {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
match self
.trace_transaction(tx_hash, trace_options)
.await
.context("Failed to trace transaction for prestate diff")?
.try_into_pre_state_frame()
.context("Failed to convert trace into pre-state frame")?
{
PreStateFrame::Diff(diff) => Ok(diff),
_ => anyhow::bail!("expected a diff mode trace"),
}
})
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn balance_of(
&self,
address: Address,
) -> Pin<Box<dyn Future<Output = anyhow::Result<U256>> + '_>> {
Box::pin(async move {
self.provider() self.provider()
.await .await
.context("Failed to create provider for tracing")?, .context("Failed to get the Geth provider")?
); .get_balance(address)
poll( .await
Self::TRACE_POLLING_DURATION, .map_err(Into::into)
PollingWaitBehavior::Constant(Duration::from_millis(200)), })
move || {
let provider = provider.clone();
let trace_options = trace_options.clone();
async move {
match provider
.debug_trace_transaction(transaction.transaction_hash, trace_options)
.await
{
Ok(trace) => Ok(ControlFlow::Break(trace)),
Err(error) => {
let error_string = error.to_string();
match error_string.contains(Self::TRANSACTION_TRACING_ERROR) {
true => Ok(ControlFlow::Continue(())),
false => Err(error.into()),
}
}
}
}
},
)
.await
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> { fn latest_state_proof(
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
match self
.trace_transaction(transaction, trace_options)
.await
.context("Failed to trace transaction for prestate diff")?
.try_into_pre_state_frame()
.context("Failed to convert trace into pre-state frame")?
{
PreStateFrame::Diff(diff) => Ok(diff),
_ => anyhow::bail!("expected a diff mode trace"),
}
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn balance_of(&self, address: Address) -> anyhow::Result<U256> {
self.provider()
.await
.context("Failed to get the Geth provider")?
.get_balance(address)
.await
.map_err(Into::into)
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
async fn latest_state_proof(
&self, &self,
address: Address, address: Address,
keys: Vec<StorageKey>, keys: Vec<StorageKey>,
) -> anyhow::Result<EIP1186AccountProofResponse> { ) -> Pin<Box<dyn Future<Output = anyhow::Result<EIP1186AccountProofResponse>> + '_>> {
self.provider() Box::pin(async move {
.await self.provider()
.context("Failed to get the Geth provider")? .await
.get_proof(address, keys) .context("Failed to get the Geth provider")?
.latest() .get_proof(address, keys)
.await .latest()
.map_err(Into::into) .await
.map_err(Into::into)
})
} }
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))] // #[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn resolver(&self) -> impl Future<Output = anyhow::Result<Box<dyn ResolverApi + '_>>> { fn resolver(
&self,
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn ResolverApi + '_>>> + '_>> {
Box::pin(async move { Box::pin(async move {
let id = self.id; let id = self.id;
let provider = self.provider().await?; let provider = self.provider().await?;
+72 -54
View File
@@ -412,77 +412,95 @@ impl KitchensinkNode {
} }
impl EthereumNode for KitchensinkNode { impl EthereumNode for KitchensinkNode {
async fn execute_transaction( fn execute_transaction(
&self, &self,
transaction: alloy::rpc::types::TransactionRequest, transaction: alloy::rpc::types::TransactionRequest,
) -> anyhow::Result<TransactionReceipt> { ) -> Pin<Box<dyn Future<Output = anyhow::Result<TransactionReceipt>> + '_>> {
let receipt = self Box::pin(async move {
.provider() let receipt = self
.await .provider()
.context("Failed to create provider for transaction submission")? .await
.send_transaction(transaction) .context("Failed to create provider for transaction submission")?
.await .send_transaction(transaction)
.context("Failed to submit transaction to kitchensink proxy")? .await
.get_receipt() .context("Failed to submit transaction to kitchensink proxy")?
.await .get_receipt()
.context("Failed to fetch transaction receipt from kitchensink proxy")?; .await
Ok(receipt) .context("Failed to fetch transaction receipt from kitchensink proxy")?;
Ok(receipt)
})
} }
async fn trace_transaction( fn trace_transaction(
&self, &self,
transaction: &TransactionReceipt, tx_hash: TxHash,
trace_options: GethDebugTracingOptions, trace_options: GethDebugTracingOptions,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> { ) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::rpc::types::trace::geth::GethTrace>> + '_>>
let tx_hash = transaction.transaction_hash; {
self.provider() Box::pin(async move {
.await self.provider()
.context("Failed to create provider for debug tracing")? .await
.debug_trace_transaction(tx_hash, trace_options) .context("Failed to create provider for debug tracing")?
.await .debug_trace_transaction(tx_hash, trace_options)
.context("Failed to obtain debug trace from kitchensink proxy") .await
.context("Failed to obtain debug trace from kitchensink proxy")
})
} }
async fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> { fn state_diff(
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig { &self,
diff_mode: Some(true), tx_hash: TxHash,
disable_code: None, ) -> Pin<Box<dyn Future<Output = anyhow::Result<DiffMode>> + '_>> {
disable_storage: None, Box::pin(async move {
}); let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
match self diff_mode: Some(true),
.trace_transaction(transaction, trace_options) disable_code: None,
.await? disable_storage: None,
.try_into_pre_state_frame()? });
{ match self
PreStateFrame::Diff(diff) => Ok(diff), .trace_transaction(tx_hash, trace_options)
_ => anyhow::bail!("expected a diff mode trace"), .await?
} .try_into_pre_state_frame()?
{
PreStateFrame::Diff(diff) => Ok(diff),
_ => anyhow::bail!("expected a diff mode trace"),
}
})
} }
async fn balance_of(&self, address: Address) -> anyhow::Result<U256> { fn balance_of(
self.provider() &self,
.await address: Address,
.context("Failed to get the Kitchensink provider")? ) -> Pin<Box<dyn Future<Output = anyhow::Result<U256>> + '_>> {
.get_balance(address) Box::pin(async move {
.await self.provider()
.map_err(Into::into) .await
.context("Failed to get the Kitchensink provider")?
.get_balance(address)
.await
.map_err(Into::into)
})
} }
async fn latest_state_proof( fn latest_state_proof(
&self, &self,
address: Address, address: Address,
keys: Vec<StorageKey>, keys: Vec<StorageKey>,
) -> anyhow::Result<EIP1186AccountProofResponse> { ) -> Pin<Box<dyn Future<Output = anyhow::Result<EIP1186AccountProofResponse>> + '_>> {
self.provider() Box::pin(async move {
.await self.provider()
.context("Failed to get the Kitchensink provider")? .await
.get_proof(address, keys) .context("Failed to get the Kitchensink provider")?
.latest() .get_proof(address, keys)
.await .latest()
.map_err(Into::into) .await
.map_err(Into::into)
})
} }
fn resolver(&self) -> impl Future<Output = anyhow::Result<Box<dyn ResolverApi + '_>>> { fn resolver(
&self,
) -> Pin<Box<dyn Future<Output = anyhow::Result<Box<dyn ResolverApi + '_>>> + '_>> {
Box::pin(async move { Box::pin(async move {
let id = self.id; let id = self.id;
let provider = self.provider().await?; let provider = self.provider().await?;