refactor(rpc-convert): move rpc conversion traits/impls to alloy-evm (#19616)

This commit is contained in:
Mablr
2025-11-14 00:01:43 +01:00
committed by GitHub
parent ba84eeaccd
commit bedbfb83f3
7 changed files with 19 additions and 465 deletions

3
Cargo.lock generated
View File

@@ -10243,6 +10243,7 @@ name = "reth-rpc-convert"
version = "1.9.2"
dependencies = [
"alloy-consensus",
"alloy-evm",
"alloy-json-rpc",
"alloy-network",
"alloy-primitives",
@@ -10254,13 +10255,11 @@ dependencies = [
"op-alloy-consensus",
"op-alloy-network",
"op-alloy-rpc-types",
"op-revm",
"reth-ethereum-primitives",
"reth-evm",
"reth-optimism-primitives",
"reth-primitives-traits",
"reth-storage-api",
"revm-context",
"serde_json",
"thiserror 2.0.17",
]

View File

@@ -80,4 +80,4 @@ portable = [
"op-revm/portable",
"revm/portable",
]
rpc = ["reth-rpc-eth-api", "reth-optimism-primitives/serde", "reth-optimism-primitives/reth-codec"]
rpc = ["reth-rpc-eth-api", "reth-optimism-primitives/serde", "reth-optimism-primitives/reth-codec", "alloy-evm/rpc"]

View File

@@ -25,16 +25,13 @@ alloy-signer.workspace = true
alloy-consensus.workspace = true
alloy-network.workspace = true
alloy-json-rpc.workspace = true
alloy-evm = { workspace = true, features = ["rpc"] }
# optimism
op-alloy-consensus = { workspace = true, optional = true }
op-alloy-rpc-types = { workspace = true, optional = true }
op-alloy-network = { workspace = true, optional = true }
reth-optimism-primitives = { workspace = true, optional = true }
op-revm = { workspace = true, optional = true }
# revm
revm-context.workspace = true
# io
jsonrpsee-types.workspace = true
@@ -56,7 +53,7 @@ op = [
"dep:op-alloy-network",
"dep:reth-optimism-primitives",
"dep:reth-storage-api",
"dep:op-revm",
"reth-evm/op",
"reth-primitives-traits/op",
"alloy-evm/op",
]

View File

@@ -1,281 +0,0 @@
use alloy_primitives::{B256, U256};
use std::cmp::min;
use thiserror::Error;
/// Helper type for representing the fees of a `TransactionRequest`
#[derive(Debug)]
pub struct CallFees {
/// EIP-1559 priority fee
pub max_priority_fee_per_gas: Option<U256>,
/// Unified gas price setting
///
/// Will be the configured `basefee` if unset in the request
///
/// `gasPrice` for legacy,
/// `maxFeePerGas` for EIP-1559
pub gas_price: U256,
/// Max Fee per Blob gas for EIP-4844 transactions
pub max_fee_per_blob_gas: Option<U256>,
}
impl CallFees {
/// Ensures the fields of a `TransactionRequest` are not conflicting.
///
/// # EIP-4844 transactions
///
/// Blob transactions have an additional fee parameter `maxFeePerBlobGas`.
/// If the `maxFeePerBlobGas` or `blobVersionedHashes` are set we treat it as an EIP-4844
/// transaction.
///
/// Note: Due to the `Default` impl of [`BlockEnv`] (Some(0)) this assumes the `block_blob_fee`
/// is always `Some`
///
/// ## Notable design decisions
///
/// For compatibility reasons, this contains several exceptions when fee values are validated:
/// - If both `maxFeePerGas` and `maxPriorityFeePerGas` are set to `0` they are treated as
/// missing values, bypassing fee checks wrt. `baseFeePerGas`.
///
/// This mirrors geth's behaviour when transaction requests are executed: <https://github.com/ethereum/go-ethereum/blob/380688c636a654becc8f114438c2a5d93d2db032/core/state_transition.go#L306-L306>
///
/// [`BlockEnv`]: revm_context::BlockEnv
pub fn ensure_fees(
call_gas_price: Option<U256>,
call_max_fee: Option<U256>,
call_priority_fee: Option<U256>,
block_base_fee: U256,
blob_versioned_hashes: Option<&[B256]>,
max_fee_per_blob_gas: Option<U256>,
block_blob_fee: Option<U256>,
) -> Result<Self, CallFeesError> {
/// Get the effective gas price of a transaction as specfified in EIP-1559 with relevant
/// checks.
fn get_effective_gas_price(
max_fee_per_gas: Option<U256>,
max_priority_fee_per_gas: Option<U256>,
block_base_fee: U256,
) -> Result<U256, CallFeesError> {
match max_fee_per_gas {
Some(max_fee) => {
let max_priority_fee_per_gas = max_priority_fee_per_gas.unwrap_or(U256::ZERO);
// only enforce the fee cap if provided input is not zero
if !(max_fee.is_zero() && max_priority_fee_per_gas.is_zero()) &&
max_fee < block_base_fee
{
// `base_fee_per_gas` is greater than the `max_fee_per_gas`
return Err(CallFeesError::FeeCapTooLow)
}
if max_fee < max_priority_fee_per_gas {
return Err(
// `max_priority_fee_per_gas` is greater than the `max_fee_per_gas`
CallFeesError::TipAboveFeeCap,
)
}
// ref <https://github.com/ethereum/go-ethereum/blob/0dd173a727dd2d2409b8e401b22e85d20c25b71f/internal/ethapi/transaction_args.go#L446-L446>
Ok(min(
max_fee,
block_base_fee
.checked_add(max_priority_fee_per_gas)
.ok_or(CallFeesError::TipVeryHigh)?,
))
}
None => Ok(block_base_fee
.checked_add(max_priority_fee_per_gas.unwrap_or(U256::ZERO))
.ok_or(CallFeesError::TipVeryHigh)?),
}
}
let has_blob_hashes =
blob_versioned_hashes.as_ref().map(|blobs| !blobs.is_empty()).unwrap_or(false);
match (call_gas_price, call_max_fee, call_priority_fee, max_fee_per_blob_gas) {
(gas_price, None, None, None) => {
// either legacy transaction or no fee fields are specified
// when no fields are specified, set gas price to zero
let gas_price = gas_price.unwrap_or(U256::ZERO);
Ok(Self {
gas_price,
max_priority_fee_per_gas: None,
max_fee_per_blob_gas: has_blob_hashes.then_some(block_blob_fee).flatten(),
})
}
(None, max_fee_per_gas, max_priority_fee_per_gas, None) => {
// request for eip-1559 transaction
let effective_gas_price = get_effective_gas_price(
max_fee_per_gas,
max_priority_fee_per_gas,
block_base_fee,
)?;
let max_fee_per_blob_gas = has_blob_hashes.then_some(block_blob_fee).flatten();
Ok(Self {
gas_price: effective_gas_price,
max_priority_fee_per_gas,
max_fee_per_blob_gas,
})
}
(None, max_fee_per_gas, max_priority_fee_per_gas, Some(max_fee_per_blob_gas)) => {
// request for eip-4844 transaction
let effective_gas_price = get_effective_gas_price(
max_fee_per_gas,
max_priority_fee_per_gas,
block_base_fee,
)?;
// Ensure blob_hashes are present
if !has_blob_hashes {
// Blob transaction but no blob hashes
return Err(CallFeesError::BlobTransactionMissingBlobHashes)
}
Ok(Self {
gas_price: effective_gas_price,
max_priority_fee_per_gas,
max_fee_per_blob_gas: Some(max_fee_per_blob_gas),
})
}
_ => {
// this fallback covers incompatible combinations of fields
Err(CallFeesError::ConflictingFeeFieldsInRequest)
}
}
}
}
/// Error coming from decoding and validating transaction request fees.
#[derive(Debug, Error)]
pub enum CallFeesError {
/// Thrown when a call or transaction request (`eth_call`, `eth_estimateGas`,
/// `eth_sendTransaction`) contains conflicting fields (legacy, EIP-1559)
#[error("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")]
ConflictingFeeFieldsInRequest,
/// Thrown post London if the transaction's fee is less than the base fee of the block
#[error("max fee per gas less than block base fee")]
FeeCapTooLow,
/// Thrown to ensure no one is able to specify a transaction with a tip higher than the total
/// fee cap.
#[error("max priority fee per gas higher than max fee per gas")]
TipAboveFeeCap,
/// A sanity error to avoid huge numbers specified in the tip field.
#[error("max priority fee per gas higher than 2^256-1")]
TipVeryHigh,
/// Blob transaction has no versioned hashes
#[error("blob transaction missing blob hashes")]
BlobTransactionMissingBlobHashes,
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_consensus::constants::GWEI_TO_WEI;
#[test]
fn test_ensure_0_fallback() {
let CallFees { gas_price, .. } =
CallFees::ensure_fees(None, None, None, U256::from(99), None, None, Some(U256::ZERO))
.unwrap();
assert!(gas_price.is_zero());
}
#[test]
fn test_ensure_max_fee_0_exception() {
let CallFees { gas_price, .. } =
CallFees::ensure_fees(None, Some(U256::ZERO), None, U256::from(99), None, None, None)
.unwrap();
assert!(gas_price.is_zero());
}
#[test]
fn test_blob_fees() {
let CallFees { gas_price, max_fee_per_blob_gas, .. } =
CallFees::ensure_fees(None, None, None, U256::from(99), None, None, Some(U256::ZERO))
.unwrap();
assert!(gas_price.is_zero());
assert_eq!(max_fee_per_blob_gas, None);
let CallFees { gas_price, max_fee_per_blob_gas, .. } = CallFees::ensure_fees(
None,
None,
None,
U256::from(99),
Some(&[B256::from(U256::ZERO)]),
None,
Some(U256::from(99)),
)
.unwrap();
assert!(gas_price.is_zero());
assert_eq!(max_fee_per_blob_gas, Some(U256::from(99)));
}
#[test]
fn test_eip_1559_fees() {
let CallFees { gas_price, .. } = CallFees::ensure_fees(
None,
Some(U256::from(25 * GWEI_TO_WEI)),
Some(U256::from(15 * GWEI_TO_WEI)),
U256::from(15 * GWEI_TO_WEI),
None,
None,
Some(U256::ZERO),
)
.unwrap();
assert_eq!(gas_price, U256::from(25 * GWEI_TO_WEI));
let CallFees { gas_price, .. } = CallFees::ensure_fees(
None,
Some(U256::from(25 * GWEI_TO_WEI)),
Some(U256::from(5 * GWEI_TO_WEI)),
U256::from(15 * GWEI_TO_WEI),
None,
None,
Some(U256::ZERO),
)
.unwrap();
assert_eq!(gas_price, U256::from(20 * GWEI_TO_WEI));
let CallFees { gas_price, .. } = CallFees::ensure_fees(
None,
Some(U256::from(30 * GWEI_TO_WEI)),
Some(U256::from(30 * GWEI_TO_WEI)),
U256::from(15 * GWEI_TO_WEI),
None,
None,
Some(U256::ZERO),
)
.unwrap();
assert_eq!(gas_price, U256::from(30 * GWEI_TO_WEI));
let call_fees = CallFees::ensure_fees(
None,
Some(U256::from(30 * GWEI_TO_WEI)),
Some(U256::from(31 * GWEI_TO_WEI)),
U256::from(15 * GWEI_TO_WEI),
None,
None,
Some(U256::ZERO),
);
assert!(call_fees.is_err());
let call_fees = CallFees::ensure_fees(
None,
Some(U256::from(5 * GWEI_TO_WEI)),
Some(U256::from(GWEI_TO_WEI)),
U256::from(15 * GWEI_TO_WEI),
None,
None,
Some(U256::ZERO),
);
assert!(call_fees.is_err());
let call_fees = CallFees::ensure_fees(
None,
Some(U256::MAX),
Some(U256::MAX),
U256::from(5 * GWEI_TO_WEI),
None,
None,
Some(U256::ZERO),
);
assert!(call_fees.is_err());
}
}

View File

@@ -11,19 +11,19 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod block;
mod fees;
pub mod receipt;
mod rpc;
pub mod transaction;
pub use block::TryFromBlockResponse;
pub use fees::{CallFees, CallFeesError};
pub use receipt::TryFromReceiptResponse;
pub use rpc::*;
pub use transaction::{
EthTxEnvError, IntoRpcTx, RpcConvert, RpcConverter, TransactionConversionError,
TryFromTransactionResponse, TryIntoSimTx, TxInfoMapper,
RpcConvert, RpcConverter, TransactionConversionError, TryFromTransactionResponse, TryIntoSimTx,
TxInfoMapper,
};
pub use alloy_evm::rpc::{CallFees, CallFeesError, EthTxEnvError, TryIntoTxEnv};
#[cfg(feature = "op")]
pub use transaction::op::*;

View File

@@ -1,28 +1,20 @@
//! Compatibility functions for rpc `Transaction` type.
use crate::{
fees::{CallFees, CallFeesError},
RpcHeader, RpcReceipt, RpcTransaction, RpcTxReq, RpcTypes, SignableTxRequest,
RpcHeader, RpcReceipt, RpcTransaction, RpcTxReq, RpcTypes, SignableTxRequest, TryIntoTxEnv,
};
use alloy_consensus::{
error::ValueError, transaction::Recovered, EthereumTxEnvelope, Sealable, TxEip4844,
};
use alloy_network::Network;
use alloy_primitives::{Address, TxKind, U256};
use alloy_rpc_types_eth::{
request::{TransactionInputError, TransactionRequest},
Transaction, TransactionInfo,
};
use alloy_primitives::{Address, U256};
use alloy_rpc_types_eth::{request::TransactionRequest, Transaction, TransactionInfo};
use core::error;
use dyn_clone::DynClone;
use reth_evm::{
revm::context_interface::{either::Either, Block},
BlockEnvFor, ConfigureEvm, EvmEnvFor, TxEnvFor,
};
use reth_evm::{BlockEnvFor, ConfigureEvm, EvmEnvFor, TxEnvFor};
use reth_primitives_traits::{
BlockTy, HeaderTy, NodePrimitives, SealedBlock, SealedHeader, SealedHeaderFor, TransactionMeta,
TxTy,
};
use revm_context::{BlockEnv, CfgEnv, TxEnv};
use std::{convert::Infallible, error::Error, fmt::Debug, marker::PhantomData};
use thiserror::Error;
@@ -462,7 +454,7 @@ where
tx_req: TxReq,
evm_env: &EvmEnvFor<Evm>,
) -> Result<TxEnvFor<Evm>, Self::Error> {
tx_req.try_into_tx_env(&evm_env.cfg_env, &evm_env.block_env)
tx_req.try_into_tx_env(evm_env)
}
}
@@ -491,122 +483,6 @@ where
}
}
/// Converts `self` into `T`.
///
/// Should create an executable transaction environment using [`TransactionRequest`].
pub trait TryIntoTxEnv<T, BlockEnv = reth_evm::revm::context::BlockEnv> {
/// An associated error that can occur during the conversion.
type Err;
/// Performs the conversion.
fn try_into_tx_env<Spec>(
self,
cfg_env: &CfgEnv<Spec>,
block_env: &BlockEnv,
) -> Result<T, Self::Err>;
}
/// An Ethereum specific transaction environment error than can occur during conversion from
/// [`TransactionRequest`].
#[derive(Debug, Error)]
pub enum EthTxEnvError {
/// Error while decoding or validating transaction request fees.
#[error(transparent)]
CallFees(#[from] CallFeesError),
/// Both data and input fields are set and not equal.
#[error(transparent)]
Input(#[from] TransactionInputError),
}
impl TryIntoTxEnv<TxEnv> for TransactionRequest {
type Err = EthTxEnvError;
fn try_into_tx_env<Spec>(
self,
cfg_env: &CfgEnv<Spec>,
block_env: &BlockEnv,
) -> Result<TxEnv, Self::Err> {
// Ensure that if versioned hashes are set, they're not empty
if self.blob_versioned_hashes.as_ref().is_some_and(|hashes| hashes.is_empty()) {
return Err(CallFeesError::BlobTransactionMissingBlobHashes.into())
}
let tx_type = self.minimal_tx_type() as u8;
let Self {
from,
to,
gas_price,
max_fee_per_gas,
max_priority_fee_per_gas,
gas,
value,
input,
nonce,
access_list,
chain_id,
blob_versioned_hashes,
max_fee_per_blob_gas,
authorization_list,
transaction_type: _,
sidecar: _,
} = self;
let CallFees { max_priority_fee_per_gas, gas_price, max_fee_per_blob_gas } =
CallFees::ensure_fees(
gas_price.map(U256::from),
max_fee_per_gas.map(U256::from),
max_priority_fee_per_gas.map(U256::from),
U256::from(block_env.basefee),
blob_versioned_hashes.as_deref(),
max_fee_per_blob_gas.map(U256::from),
block_env.blob_gasprice().map(U256::from),
)?;
let gas_limit = gas.unwrap_or(
// Use maximum allowed gas limit. The reason for this
// is that both Erigon and Geth use pre-configured gas cap even if
// it's possible to derive the gas limit from the block:
// <https://github.com/ledgerwatch/erigon/blob/eae2d9a79cb70dbe30b3a6b79c436872e4605458/cmd/rpcdaemon/commands/trace_adhoc.go#L956
// https://github.com/ledgerwatch/erigon/blob/eae2d9a79cb70dbe30b3a6b79c436872e4605458/eth/ethconfig/config.go#L94>
block_env.gas_limit,
);
let chain_id = chain_id.unwrap_or(cfg_env.chain_id);
let caller = from.unwrap_or_default();
let nonce = nonce.unwrap_or_default();
let env = TxEnv {
tx_type,
gas_limit,
nonce,
caller,
gas_price: gas_price.saturating_to(),
gas_priority_fee: max_priority_fee_per_gas.map(|v| v.saturating_to()),
kind: to.unwrap_or(TxKind::Create),
value: value.unwrap_or_default(),
data: input.try_into_unique_input().map_err(EthTxEnvError::from)?.unwrap_or_default(),
chain_id: Some(chain_id),
access_list: access_list.unwrap_or_default(),
// EIP-4844 fields
blob_hashes: blob_versioned_hashes.unwrap_or_default(),
max_fee_per_blob_gas: max_fee_per_blob_gas
.map(|v| v.saturating_to())
.unwrap_or_default(),
// EIP-7702 fields
authorization_list: authorization_list
.unwrap_or_default()
.into_iter()
.map(Either::Left)
.collect(),
};
Ok(env)
}
}
/// Conversion into transaction RPC response failed.
#[derive(Debug, Clone, Error)]
#[error("Failed to convert transaction into RPC response: {0}")]
@@ -990,13 +866,12 @@ where
pub mod op {
use super::*;
use alloy_consensus::SignableTransaction;
use alloy_primitives::{Address, Bytes, Signature};
use alloy_signer::Signature;
use op_alloy_consensus::{
transaction::{OpDepositInfo, OpTransactionInfo},
OpTxEnvelope,
};
use op_alloy_rpc_types::OpTransactionRequest;
use op_revm::OpTransaction;
use reth_optimism_primitives::DepositReceipt;
use reth_primitives_traits::SignedTransaction;
use reth_storage_api::{errors::ProviderError, ReceiptProvider};
@@ -1054,22 +929,6 @@ pub mod op {
Ok(tx.into_signed(signature).into())
}
}
impl TryIntoTxEnv<OpTransaction<TxEnv>> for OpTransactionRequest {
type Err = EthTxEnvError;
fn try_into_tx_env<Spec>(
self,
cfg_env: &CfgEnv<Spec>,
block_env: &BlockEnv,
) -> Result<OpTransaction<TxEnv>, Self::Err> {
Ok(OpTransaction {
base: self.as_ref().clone().try_into_tx_env(cfg_env, block_env)?,
enveloped_tx: Some(Bytes::new()),
deposit: Default::default(),
})
}
}
}
/// Trait for converting network transaction responses to primitive transaction types.
@@ -1146,8 +1005,6 @@ mod transaction_response_tests {
#[cfg(feature = "op")]
mod op {
use super::*;
use crate::transaction::TryIntoTxEnv;
use revm_context::{BlockEnv, CfgEnv};
#[test]
fn test_optimism_transaction_conversion() {
@@ -1180,23 +1037,5 @@ mod transaction_response_tests {
assert!(result.is_ok());
}
#[test]
fn test_op_into_tx_env() {
use op_alloy_rpc_types::OpTransactionRequest;
use op_revm::{transaction::OpTxTr, OpSpecId};
use revm_context::Transaction;
let s = r#"{"from":"0x0000000000000000000000000000000000000000","to":"0x6d362b9c3ab68c0b7c79e8a714f1d7f3af63655f","input":"0x1626ba7ec8ee0d506e864589b799a645ddb88b08f5d39e8049f9f702b3b61fa15e55fc73000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000550000002d6db27c52e3c11c1cf24072004ac75cba49b25bf45f513902e469755e1f3bf2ca8324ad16930b0a965c012a24bb1101f876ebebac047bd3b6bf610205a27171eaaeffe4b5e5589936f4e542d637b627311b0000000000000000000000","data":"0x1626ba7ec8ee0d506e864589b799a645ddb88b08f5d39e8049f9f702b3b61fa15e55fc73000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000550000002d6db27c52e3c11c1cf24072004ac75cba49b25bf45f513902e469755e1f3bf2ca8324ad16930b0a965c012a24bb1101f876ebebac047bd3b6bf610205a27171eaaeffe4b5e5589936f4e542d637b627311b0000000000000000000000","chainId":"0x7a69"}"#;
let req: OpTransactionRequest = serde_json::from_str(s).unwrap();
let cfg = CfgEnv::<OpSpecId>::default();
let block_env = BlockEnv::default();
let tx_env = req.try_into_tx_env(&cfg, &block_env).unwrap();
assert_eq!(tx_env.gas_limit(), block_env.gas_limit);
assert_eq!(tx_env.gas_price(), 0);
assert!(tx_env.enveloped_tx().unwrap().is_empty());
}
}
}

View File

@@ -3,14 +3,15 @@ use crate::{
primitives::{CustomHeader, CustomTransaction},
};
use alloy_consensus::error::ValueError;
use alloy_evm::EvmEnv;
use alloy_network::TxSigner;
use op_alloy_consensus::OpTxEnvelope;
use op_alloy_rpc_types::{OpTransactionReceipt, OpTransactionRequest};
use reth_op::rpc::RpcTypes;
use reth_rpc_api::eth::{
transaction::TryIntoTxEnv, EthTxEnvError, SignTxRequestError, SignableTxRequest, TryIntoSimTx,
EthTxEnvError, SignTxRequestError, SignableTxRequest, TryIntoSimTx, TryIntoTxEnv,
};
use revm::context::{BlockEnv, CfgEnv};
use revm::context::BlockEnv;
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
@@ -34,10 +35,9 @@ impl TryIntoTxEnv<CustomTxEnv> for OpTransactionRequest {
fn try_into_tx_env<Spec>(
self,
cfg_env: &CfgEnv<Spec>,
block_env: &BlockEnv,
evm_env: &EvmEnv<Spec, BlockEnv>,
) -> Result<CustomTxEnv, Self::Err> {
Ok(CustomTxEnv::Op(self.try_into_tx_env(cfg_env, block_env)?))
Ok(CustomTxEnv::Op(self.try_into_tx_env(evm_env)?))
}
}