use super::utils::decode_revert_reason;
use crate::rpc::error::RpcErrorData;
use crate::shim::clock::ChainEpoch;
use crate::shim::error::ExitCode;
use crate::shim::executor::ApplyRet;
use crate::utils::encoding::hex;
use fvm_ipld_encoding::RawBytes;
use serde::Serialize;
use std::fmt::Debug;
use thiserror::Error;
pub const OUT_OF_GAS_CODE: i32 = 2;
pub const EXECUTION_REVERTED_CODE: i32 = 3;
pub const LIMIT_EXCEEDED_CODE: i32 = -32005;
pub const NULL_ROUND_CODE: i32 = 12;
pub const INVALID_INPUT_CODE: i32 = -32000;
pub const TRANSACTION_REJECTED_CODE: i32 = -32003;
#[derive(Clone, Debug, Error, Serialize)]
pub enum EthErrors {
#[error("call ran out of gas")]
OutOfGas,
#[error("{message}")]
ExecutionReverted { message: String, data: String },
#[error("{message}")]
BlockRangeExceeded {
max: i64,
given: i64,
message: String,
},
#[error("events for the requested block are not yet available")]
EventsNotYetAvailable,
#[error("requested epoch was a null round ({epoch})")]
NullRound { epoch: ChainEpoch },
#[error("gas required exceeds allowance ({gas_limit})")]
GasRequiredExceedsAllowance { gas_limit: u64 },
#[error("out of gas: gas required exceeds: {gas_limit}")]
InsufficientGasLimit { gas_limit: u64 },
}
impl EthErrors {
pub fn execution_reverted(exit_code: ExitCode, reason: &str, error: &str, data: &[u8]) -> Self {
let revert_reason = if reason.is_empty() {
String::new()
} else {
format!(", revert reason=[{reason}]")
};
Self::ExecutionReverted {
message: format!(
"message execution failed (exit=[{exit_code}]{revert_reason}, vm error=[{error}])"
),
data: hex::encode_prefixed(data),
}
}
pub fn execution_reverted_from_result(
exit_code: impl Into<ExitCode>,
return_data: RawBytes,
vm_error: &str,
) -> Self {
let (data, reason) = decode_revert_reason(return_data);
Self::execution_reverted(exit_code.into(), &reason, vm_error, &data)
}
pub fn execution_reverted_from_apply_ret(apply_ret: &ApplyRet) -> Self {
Self::execution_reverted_from_result(
apply_ret.exit_code(),
apply_ret.return_data(),
&apply_ret.failure_info().unwrap_or_default(),
)
}
pub fn limit_exceeded(max_block_range: i64, given: i64) -> Self {
Self::BlockRangeExceeded {
max: max_block_range,
given,
message: format!("block range exceeds maximum of {max_block_range} (got {given})"),
}
}
pub fn null_round(epoch: ChainEpoch) -> Self {
Self::NullRound { epoch }
}
}
impl RpcErrorData for EthErrors {
fn error_code(&self) -> Option<i32> {
match self {
EthErrors::OutOfGas => Some(OUT_OF_GAS_CODE),
EthErrors::ExecutionReverted { .. } => Some(EXECUTION_REVERTED_CODE),
EthErrors::BlockRangeExceeded { .. } => Some(LIMIT_EXCEEDED_CODE),
EthErrors::EventsNotYetAvailable => None,
EthErrors::NullRound { .. } => Some(NULL_ROUND_CODE),
EthErrors::GasRequiredExceedsAllowance { .. } => Some(INVALID_INPUT_CODE),
EthErrors::InsufficientGasLimit { .. } => Some(TRANSACTION_REJECTED_CODE),
}
}
fn error_message(&self) -> Option<String> {
match self {
EthErrors::OutOfGas => Some(self.to_string()),
EthErrors::ExecutionReverted { message, .. } => Some(message.clone()),
EthErrors::BlockRangeExceeded { message, .. } => Some(message.clone()),
EthErrors::EventsNotYetAvailable => Some(self.to_string()),
EthErrors::NullRound { .. } => Some(self.to_string()),
EthErrors::GasRequiredExceedsAllowance { .. }
| EthErrors::InsufficientGasLimit { .. } => Some(self.to_string()),
}
}
fn error_data(&self) -> Option<serde_json::Value> {
match self {
EthErrors::ExecutionReverted { data, .. } => {
Some(serde_json::Value::String(data.clone()))
}
EthErrors::OutOfGas
| EthErrors::BlockRangeExceeded { .. }
| EthErrors::EventsNotYetAvailable
| EthErrors::GasRequiredExceedsAllowance { .. }
| EthErrors::InsufficientGasLimit { .. } => None,
EthErrors::NullRound { epoch } => Some(serde_json::Value::from(*epoch)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rpc::error::ServerError;
#[test]
fn test_block_range_exceeded_converts_to_server_error_with_correct_code() {
let err = EthErrors::limit_exceeded(100, 500);
let server_err: ServerError = err.into();
assert_eq!(server_err.code(), LIMIT_EXCEEDED_CODE);
assert_eq!(
server_err.message(),
"block range exceeds maximum of 100 (got 500)"
);
}
#[test]
fn test_block_range_exceeded_via_anyhow_preserves_code() {
let eth_err = EthErrors::limit_exceeded(2880, 5000);
let anyhow_err: anyhow::Error = eth_err.into();
let server_err: ServerError = anyhow_err.into();
assert_eq!(server_err.code(), LIMIT_EXCEEDED_CODE);
assert_eq!(
server_err.message(),
"block range exceeds maximum of 2880 (got 5000)"
);
}
#[test]
fn test_null_round_converts_to_server_error_matching_lotus() {
let err = EthErrors::null_round(3847253);
let server_err: ServerError = err.into();
assert_eq!(server_err.code(), NULL_ROUND_CODE);
assert_eq!(
server_err.message(),
"requested epoch was a null round (3847253)"
);
assert_eq!(
server_err.data().map(|d| d.to_string()),
Some("3847253".to_string())
);
}
#[test]
fn test_null_round_via_anyhow_preserves_code() {
let anyhow_err: anyhow::Error = EthErrors::null_round(42).into();
let server_err: ServerError = anyhow_err.into();
assert_eq!(server_err.code(), NULL_ROUND_CODE);
assert_eq!(
server_err.message(),
"requested epoch was a null round (42)"
);
}
#[test]
fn test_events_not_yet_available_converts_to_server_error() {
let err = EthErrors::EventsNotYetAvailable;
let server_err: ServerError = err.into();
assert_eq!(
server_err.message(),
"events for the requested block are not yet available"
);
}
#[test]
fn test_out_of_gas_converts_to_server_error_matching_lotus() {
let err = EthErrors::OutOfGas;
let server_err: ServerError = err.into();
assert_eq!(server_err.code(), OUT_OF_GAS_CODE);
assert_eq!(server_err.message(), "call ran out of gas");
}
#[test]
fn test_gas_required_exceeds_allowance_converts_to_server_error() {
let server_err: ServerError =
EthErrors::GasRequiredExceedsAllowance { gas_limit: 1000 }.into();
assert_eq!(server_err.code(), INVALID_INPUT_CODE);
assert_eq!(
server_err.message(),
"gas required exceeds allowance (1000)"
);
assert!(server_err.data().is_none());
}
#[test]
fn test_insufficient_gas_limit_converts_to_server_error() {
let server_err: ServerError = EthErrors::InsufficientGasLimit { gas_limit: 25000 }.into();
assert_eq!(server_err.code(), TRANSACTION_REJECTED_CODE);
assert_eq!(
server_err.message(),
"out of gas: gas required exceeds: 25000"
);
assert!(server_err.data().is_none());
}
}