use std::time::Duration;
use alloy_sol_types::decode_revert_reason;
use reth_errors::RethError;
use reth_primitives::{revm_primitives::InvalidHeader, Address, Bytes};
use reth_rpc_server_types::result::{
internal_rpc_err, invalid_params_rpc_err, rpc_err, rpc_error_with_code,
};
use reth_rpc_types::{
error::EthRpcErrorCode, request::TransactionInputError, BlockError, ToRpcError,
};
use reth_transaction_pool::error::{
Eip4844PoolTransactionError, InvalidPoolTransactionError, PoolError, PoolErrorKind,
PoolTransactionError,
};
use revm::primitives::{EVMError, ExecutionResult, HaltReason, OutOfGasError};
use revm_inspectors::tracing::{js::JsInspectorError, MuxError};
use tracing::error;
pub type EthResult<T> = Result<T, EthApiError>;
#[derive(Debug, thiserror::Error)]
pub enum EthApiError {
#[error("empty transaction data")]
EmptyRawTransactionData,
#[error("failed to decode signed transaction")]
FailedToDecodeSignedTransaction,
#[error("invalid transaction signature")]
InvalidTransactionSignature,
#[error(transparent)]
PoolError(RpcPoolError),
#[error("unknown block number")]
UnknownBlockNumber,
#[error("unknown block")]
UnknownSafeOrFinalizedBlock,
#[error("unknown block or tx index")]
UnknownBlockOrTxIndex,
#[error("invalid block range")]
InvalidBlockRange,
#[error("distance to target block exceeds maximum proof window")]
ExceedsMaxProofWindow,
#[error("prevrandao not in the EVM's environment after merge")]
PrevrandaoNotSet,
#[error("excess blob gas missing in the EVM's environment after Cancun")]
ExcessBlobGasNotSet,
#[error("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")]
ConflictingFeeFieldsInRequest,
#[error(transparent)]
InvalidTransaction(#[from] RpcInvalidTransactionError),
#[error(transparent)]
InvalidBlockData(#[from] BlockError),
#[error("account {0:?} has both 'state' and 'stateDiff'")]
BothStateAndStateDiffInOverride(Address),
#[error(transparent)]
Internal(RethError),
#[error(transparent)]
Signing(#[from] SignError),
#[error("transaction not found")]
TransactionNotFound,
#[error("unsupported")]
Unsupported(&'static str),
#[error("{0}")]
InvalidParams(String),
#[error("invalid tracer config")]
InvalidTracerConfig,
#[error("invalid reward percentiles")]
InvalidRewardPercentiles,
#[error("internal blocking task error")]
InternalBlockingTaskError,
#[error("internal eth error")]
InternalEthError,
#[error("execution aborted (timeout = {0:?})")]
ExecutionTimedOut(Duration),
#[error("{0}")]
InternalJsTracerError(String),
#[error(transparent)]
TransactionInputError(#[from] TransactionInputError),
#[error("Revm error: {0}")]
EvmCustom(String),
#[error("Revm precompile error: {0}")]
EvmPrecompile(String),
#[error("Transaction conversion error")]
TransactionConversionError,
#[error(transparent)]
MuxTracerError(#[from] MuxError),
#[error("{0}")]
Other(Box<dyn ToRpcError>),
}
impl EthApiError {
pub fn other<E: ToRpcError>(err: E) -> Self {
Self::Other(Box::new(err))
}
pub const fn is_gas_too_high(&self) -> bool {
matches!(self, Self::InvalidTransaction(RpcInvalidTransactionError::GasTooHigh))
}
}
impl From<EthApiError> for jsonrpsee_types::error::ErrorObject<'static> {
fn from(error: EthApiError) -> Self {
match error {
EthApiError::FailedToDecodeSignedTransaction |
EthApiError::InvalidTransactionSignature |
EthApiError::EmptyRawTransactionData |
EthApiError::InvalidBlockRange |
EthApiError::ExceedsMaxProofWindow |
EthApiError::ConflictingFeeFieldsInRequest |
EthApiError::Signing(_) |
EthApiError::BothStateAndStateDiffInOverride(_) |
EthApiError::InvalidTracerConfig |
EthApiError::TransactionConversionError => invalid_params_rpc_err(error.to_string()),
EthApiError::InvalidTransaction(err) => err.into(),
EthApiError::PoolError(err) => err.into(),
EthApiError::PrevrandaoNotSet |
EthApiError::ExcessBlobGasNotSet |
EthApiError::InvalidBlockData(_) |
EthApiError::Internal(_) |
EthApiError::TransactionNotFound |
EthApiError::EvmCustom(_) |
EthApiError::EvmPrecompile(_) |
EthApiError::InvalidRewardPercentiles => internal_rpc_err(error.to_string()),
EthApiError::UnknownBlockNumber | EthApiError::UnknownBlockOrTxIndex => {
rpc_error_with_code(EthRpcErrorCode::ResourceNotFound.code(), error.to_string())
}
EthApiError::UnknownSafeOrFinalizedBlock => {
rpc_error_with_code(EthRpcErrorCode::UnknownBlock.code(), error.to_string())
}
EthApiError::Unsupported(msg) => internal_rpc_err(msg),
EthApiError::InternalJsTracerError(msg) => internal_rpc_err(msg),
EthApiError::InvalidParams(msg) => invalid_params_rpc_err(msg),
err @ EthApiError::ExecutionTimedOut(_) => rpc_error_with_code(
jsonrpsee_types::error::CALL_EXECUTION_FAILED_CODE,
err.to_string(),
),
err @ EthApiError::InternalBlockingTaskError | err @ EthApiError::InternalEthError => {
internal_rpc_err(err.to_string())
}
err @ EthApiError::TransactionInputError(_) => invalid_params_rpc_err(err.to_string()),
EthApiError::Other(err) => err.to_rpc_error(),
EthApiError::MuxTracerError(msg) => internal_rpc_err(msg.to_string()),
}
}
}
impl From<JsInspectorError> for EthApiError {
fn from(error: JsInspectorError) -> Self {
match error {
err @ JsInspectorError::JsError(_) => Self::InternalJsTracerError(err.to_string()),
err => Self::InvalidParams(err.to_string()),
}
}
}
impl From<RethError> for EthApiError {
fn from(error: RethError) -> Self {
match error {
RethError::Provider(err) => err.into(),
err => Self::Internal(err),
}
}
}
impl From<reth_errors::ProviderError> for EthApiError {
fn from(error: reth_errors::ProviderError) -> Self {
use reth_errors::ProviderError;
match error {
ProviderError::HeaderNotFound(_) |
ProviderError::BlockHashNotFound(_) |
ProviderError::BestBlockNotFound |
ProviderError::BlockNumberForTransactionIndexNotFound |
ProviderError::TotalDifficultyNotFound { .. } |
ProviderError::UnknownBlockHash(_) => Self::UnknownBlockNumber,
ProviderError::FinalizedBlockNotFound | ProviderError::SafeBlockNotFound => {
Self::UnknownSafeOrFinalizedBlock
}
err => Self::Internal(err.into()),
}
}
}
impl<T> From<EVMError<T>> for EthApiError
where
T: Into<Self>,
{
fn from(err: EVMError<T>) -> Self {
match err {
EVMError::Transaction(err) => RpcInvalidTransactionError::from(err).into(),
EVMError::Header(InvalidHeader::PrevrandaoNotSet) => Self::PrevrandaoNotSet,
EVMError::Header(InvalidHeader::ExcessBlobGasNotSet) => Self::ExcessBlobGasNotSet,
EVMError::Database(err) => err.into(),
EVMError::Custom(err) => Self::EvmCustom(err),
EVMError::Precompile(err) => Self::EvmPrecompile(err),
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum RpcInvalidTransactionError {
#[error("nonce too low")]
NonceTooLow,
#[error("nonce too high")]
NonceTooHigh,
#[error("nonce has max value")]
NonceMaxValue,
#[error("insufficient funds for transfer")]
InsufficientFundsForTransfer,
#[error("max initcode size exceeded")]
MaxInitCodeSizeExceeded,
#[error("insufficient funds for gas * price + value")]
InsufficientFunds,
#[error("gas uint64 overflow")]
GasUintOverflow,
#[error("intrinsic gas too low")]
GasTooLow,
#[error("intrinsic gas too high")]
GasTooHigh,
#[error("transaction type not supported")]
TxTypeNotSupported,
#[error("max priority fee per gas higher than max fee per gas")]
TipAboveFeeCap,
#[error("max priority fee per gas higher than 2^256-1")]
TipVeryHigh,
#[error("max fee per gas higher than 2^256-1")]
FeeCapVeryHigh,
#[error("max fee per gas less than block base fee")]
FeeCapTooLow,
#[error("sender is not an EOA")]
SenderNoEOA,
#[error("out of gas: gas required exceeds allowance: {0}")]
BasicOutOfGas(u64),
#[error("out of gas: gas exhausted during memory expansion: {0}")]
MemoryOutOfGas(u64),
#[error("out of gas: gas exhausted during precompiled contract execution: {0}")]
PrecompileOutOfGas(u64),
#[error("out of gas: invalid operand to an opcode; {0}")]
InvalidOperandOutOfGas(u64),
#[error(transparent)]
Revert(RevertError),
#[error("EVM error: {0:?}")]
EvmHalt(HaltReason),
#[error("invalid chain ID")]
InvalidChainId,
#[error("transactions before Spurious Dragon should not have a chain ID")]
OldLegacyChainId,
#[error("transactions before Berlin should not have access list")]
AccessListNotSupported,
#[error("max_fee_per_blob_gas is not supported for blocks before the Cancun hardfork")]
MaxFeePerBlobGasNotSupported,
#[error("blob_versioned_hashes is not supported for blocks before the Cancun hardfork")]
BlobVersionedHashesNotSupported,
#[error("max fee per blob gas less than block blob gas fee")]
BlobFeeCapTooLow,
#[error("blob hash version mismatch")]
BlobHashVersionMismatch,
#[error("blob transaction missing blob hashes")]
BlobTransactionMissingBlobHashes,
#[error("blob transaction exceeds max blobs per block; got {have}, max {max}")]
TooManyBlobs {
max: usize,
have: usize,
},
#[error("blob transaction is a create transaction")]
BlobTransactionIsCreate,
#[error("EOF crate should have `to` address")]
EofCrateShouldHaveToAddress,
#[error("EIP-7702 authorization list not supported")]
AuthorizationListNotSupported,
#[error("EIP-7702 authorization list has invalid fields")]
AuthorizationListInvalidFields,
#[error("{0}")]
Other(Box<dyn ToRpcError>),
#[error("unexpected transaction error")]
UnexpectedTransactionError,
}
impl RpcInvalidTransactionError {
pub fn other<E: ToRpcError>(err: E) -> Self {
Self::Other(Box::new(err))
}
}
impl RpcInvalidTransactionError {
const fn error_code(&self) -> i32 {
match self {
Self::InvalidChainId | Self::GasTooLow | Self::GasTooHigh => {
EthRpcErrorCode::InvalidInput.code()
}
Self::Revert(_) => EthRpcErrorCode::ExecutionError.code(),
_ => EthRpcErrorCode::TransactionRejected.code(),
}
}
pub const fn halt(reason: HaltReason, gas_limit: u64) -> Self {
match reason {
HaltReason::OutOfGas(err) => Self::out_of_gas(err, gas_limit),
HaltReason::NonceOverflow => Self::NonceMaxValue,
err => Self::EvmHalt(err),
}
}
pub const fn out_of_gas(reason: OutOfGasError, gas_limit: u64) -> Self {
match reason {
OutOfGasError::Basic => Self::BasicOutOfGas(gas_limit),
OutOfGasError::Memory | OutOfGasError::MemoryLimit => Self::MemoryOutOfGas(gas_limit),
OutOfGasError::Precompile => Self::PrecompileOutOfGas(gas_limit),
OutOfGasError::InvalidOperand => Self::InvalidOperandOutOfGas(gas_limit),
}
}
}
impl From<RpcInvalidTransactionError> for jsonrpsee_types::error::ErrorObject<'static> {
fn from(err: RpcInvalidTransactionError) -> Self {
match err {
RpcInvalidTransactionError::Revert(revert) => {
rpc_err(
revert.error_code(),
revert.to_string(),
revert.output.as_ref().map(|out| out.as_ref()),
)
}
RpcInvalidTransactionError::Other(err) => err.to_rpc_error(),
err => rpc_err(err.error_code(), err.to_string(), None),
}
}
}
impl From<revm::primitives::InvalidTransaction> for RpcInvalidTransactionError {
fn from(err: revm::primitives::InvalidTransaction) -> Self {
use revm::primitives::InvalidTransaction;
match err {
InvalidTransaction::InvalidChainId => Self::InvalidChainId,
InvalidTransaction::PriorityFeeGreaterThanMaxFee => Self::TipAboveFeeCap,
InvalidTransaction::GasPriceLessThanBasefee => Self::FeeCapTooLow,
InvalidTransaction::CallerGasLimitMoreThanBlock |
InvalidTransaction::CallGasCostMoreThanGasLimit => Self::GasTooHigh,
InvalidTransaction::RejectCallerWithCode => Self::SenderNoEOA,
InvalidTransaction::LackOfFundForMaxFee { .. } => Self::InsufficientFunds,
InvalidTransaction::OverflowPaymentInTransaction => Self::GasUintOverflow,
InvalidTransaction::NonceOverflowInTransaction => Self::NonceMaxValue,
InvalidTransaction::CreateInitCodeSizeLimit => Self::MaxInitCodeSizeExceeded,
InvalidTransaction::NonceTooHigh { .. } => Self::NonceTooHigh,
InvalidTransaction::NonceTooLow { .. } => Self::NonceTooLow,
InvalidTransaction::AccessListNotSupported => Self::AccessListNotSupported,
InvalidTransaction::MaxFeePerBlobGasNotSupported => Self::MaxFeePerBlobGasNotSupported,
InvalidTransaction::BlobVersionedHashesNotSupported => {
Self::BlobVersionedHashesNotSupported
}
InvalidTransaction::BlobGasPriceGreaterThanMax => Self::BlobFeeCapTooLow,
InvalidTransaction::EmptyBlobs => Self::BlobTransactionMissingBlobHashes,
InvalidTransaction::BlobVersionNotSupported => Self::BlobHashVersionMismatch,
InvalidTransaction::TooManyBlobs { max, have } => Self::TooManyBlobs { max, have },
InvalidTransaction::BlobCreateTransaction => Self::BlobTransactionIsCreate,
InvalidTransaction::EofCrateShouldHaveToAddress => Self::EofCrateShouldHaveToAddress,
InvalidTransaction::AuthorizationListNotSupported => {
Self::AuthorizationListNotSupported
}
InvalidTransaction::AuthorizationListInvalidFields => {
Self::AuthorizationListInvalidFields
}
#[allow(unreachable_patterns)]
_ => {
error!(target: "rpc",
?err,
"unexpected transaction error"
);
Self::UnexpectedTransactionError
}
}
}
}
impl From<reth_primitives::InvalidTransactionError> for RpcInvalidTransactionError {
fn from(err: reth_primitives::InvalidTransactionError) -> Self {
use reth_primitives::InvalidTransactionError;
match err {
InvalidTransactionError::InsufficientFunds { .. } => Self::InsufficientFunds,
InvalidTransactionError::NonceNotConsistent => Self::NonceTooLow,
InvalidTransactionError::OldLegacyChainId => {
Self::OldLegacyChainId
}
InvalidTransactionError::ChainIdMismatch => Self::InvalidChainId,
InvalidTransactionError::Eip2930Disabled |
InvalidTransactionError::Eip1559Disabled |
InvalidTransactionError::Eip4844Disabled |
InvalidTransactionError::Eip7702Disabled |
InvalidTransactionError::TxTypeNotSupported => Self::TxTypeNotSupported,
InvalidTransactionError::GasUintOverflow => Self::GasUintOverflow,
InvalidTransactionError::GasTooLow => Self::GasTooLow,
InvalidTransactionError::GasTooHigh => Self::GasTooHigh,
InvalidTransactionError::TipAboveFeeCap => Self::TipAboveFeeCap,
InvalidTransactionError::FeeCapTooLow => Self::FeeCapTooLow,
InvalidTransactionError::SignerAccountHasBytecode => Self::SenderNoEOA,
}
}
}
#[derive(Debug, Clone)]
pub struct RevertError {
output: Option<Bytes>,
}
impl RevertError {
pub fn new(output: Bytes) -> Self {
if output.is_empty() {
Self { output: None }
} else {
Self { output: Some(output) }
}
}
const fn error_code(&self) -> i32 {
EthRpcErrorCode::ExecutionError.code()
}
}
impl std::fmt::Display for RevertError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("execution reverted")?;
if let Some(reason) = self.output.as_ref().and_then(|bytes| decode_revert_reason(bytes)) {
write!(f, ": {reason}")?;
}
Ok(())
}
}
impl std::error::Error for RevertError {}
#[derive(Debug, thiserror::Error)]
pub enum RpcPoolError {
#[error("already known")]
AlreadyKnown,
#[error("invalid sender")]
InvalidSender,
#[error("transaction underpriced")]
Underpriced,
#[error("txpool is full")]
TxPoolOverflow,
#[error("replacement transaction underpriced")]
ReplaceUnderpriced,
#[error("exceeds block gas limit")]
ExceedsGasLimit,
#[error("negative value")]
NegativeValue,
#[error("oversized data")]
OversizedData,
#[error("max initcode size exceeded")]
ExceedsMaxInitCodeSize,
#[error(transparent)]
Invalid(#[from] RpcInvalidTransactionError),
#[error(transparent)]
PoolTransactionError(Box<dyn PoolTransactionError>),
#[error(transparent)]
Eip4844(#[from] Eip4844PoolTransactionError),
#[error("address already reserved")]
AddressAlreadyReserved,
#[error(transparent)]
Other(Box<dyn std::error::Error + Send + Sync>),
}
impl From<RpcPoolError> for jsonrpsee_types::error::ErrorObject<'static> {
fn from(error: RpcPoolError) -> Self {
match error {
RpcPoolError::Invalid(err) => err.into(),
error => internal_rpc_err(error.to_string()),
}
}
}
impl From<PoolError> for RpcPoolError {
fn from(err: PoolError) -> Self {
match err.kind {
PoolErrorKind::ReplacementUnderpriced => Self::ReplaceUnderpriced,
PoolErrorKind::FeeCapBelowMinimumProtocolFeeCap(_) => Self::Underpriced,
PoolErrorKind::SpammerExceededCapacity(_) | PoolErrorKind::DiscardedOnInsert => {
Self::TxPoolOverflow
}
PoolErrorKind::InvalidTransaction(err) => err.into(),
PoolErrorKind::Other(err) => Self::Other(err),
PoolErrorKind::AlreadyImported => Self::AlreadyKnown,
PoolErrorKind::ExistingConflictingTransactionType(_, _) => Self::AddressAlreadyReserved,
}
}
}
impl From<InvalidPoolTransactionError> for RpcPoolError {
fn from(err: InvalidPoolTransactionError) -> Self {
match err {
InvalidPoolTransactionError::Consensus(err) => Self::Invalid(err.into()),
InvalidPoolTransactionError::ExceedsGasLimit(_, _) => Self::ExceedsGasLimit,
InvalidPoolTransactionError::ExceedsMaxInitCodeSize(_, _) => {
Self::ExceedsMaxInitCodeSize
}
InvalidPoolTransactionError::IntrinsicGasTooLow => {
Self::Invalid(RpcInvalidTransactionError::GasTooLow)
}
InvalidPoolTransactionError::OversizedData(_, _) => Self::OversizedData,
InvalidPoolTransactionError::Underpriced => Self::Underpriced,
InvalidPoolTransactionError::Other(err) => Self::PoolTransactionError(err),
InvalidPoolTransactionError::Eip4844(err) => Self::Eip4844(err),
InvalidPoolTransactionError::Overdraft => {
Self::Invalid(RpcInvalidTransactionError::InsufficientFunds)
}
}
}
}
impl From<PoolError> for EthApiError {
fn from(err: PoolError) -> Self {
Self::PoolError(RpcPoolError::from(err))
}
}
#[derive(Debug, thiserror::Error)]
pub enum SignError {
#[error("could not sign")]
CouldNotSign,
#[error("unknown account")]
NoAccount,
#[error("given typed data is not valid")]
InvalidTypedData,
#[error("invalid transaction request")]
InvalidTransactionRequest,
#[error("no chainid")]
NoChainId,
}
pub fn ensure_success(result: ExecutionResult) -> EthResult<Bytes> {
match result {
ExecutionResult::Success { output, .. } => Ok(output.into_data()),
ExecutionResult::Revert { output, .. } => {
Err(RpcInvalidTransactionError::Revert(RevertError::new(output)).into())
}
ExecutionResult::Halt { reason, gas_used } => {
Err(RpcInvalidTransactionError::halt(reason, gas_used).into())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timed_out_error() {
let err = EthApiError::ExecutionTimedOut(Duration::from_secs(10));
assert_eq!(err.to_string(), "execution aborted (timeout = 10s)");
}
}