Skip to main content

tempo_revm/
error.rs

1//! Tempo-specific transaction validation errors.
2
3use alloy_evm::error::InvalidTxError;
4use alloy_primitives::{Address, U256};
5use revm::context::result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction};
6use tempo_primitives::transaction::{KeyAuthorizationChainIdError, KeychainVersionError};
7
8/// Tempo-specific invalid transaction errors.
9///
10/// This enum extends the standard Ethereum [`InvalidTransaction`] with Tempo-specific
11/// validation errors that occur during transaction processing.
12#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
13pub enum TempoInvalidTransaction {
14    /// Standard Ethereum transaction validation error.
15    #[error(transparent)]
16    EthInvalidTransaction(#[from] InvalidTransaction),
17
18    /// System transaction must be a call (not a create).
19    #[error("system transaction must be a call, not a create")]
20    SystemTransactionMustBeCall,
21
22    /// System transaction execution failed.
23    #[error("system transaction execution failed, result: {_0:?}")]
24    SystemTransactionFailed(Box<ExecutionResult<TempoHaltReason>>),
25
26    /// Fee payer signature recovery failed.
27    ///
28    /// This error occurs when a transaction specifies a fee payer but the
29    /// signature recovery for the fee payer fails.
30    #[error("fee payer signature recovery failed")]
31    InvalidFeePayerSignature,
32
33    /// Fee payer cannot resolve to the sender address.
34    #[error("fee payer cannot resolve to sender")]
35    SelfSponsoredFeePayer,
36
37    // Tempo transaction errors
38    /// Transaction cannot be included before validAfter timestamp.
39    ///
40    /// Tempo transactions can specify a validAfter field to restrict when they can be included.
41    #[error(
42        "transaction not valid yet: current block timestamp {current} < validAfter {valid_after}"
43    )]
44    ValidAfter {
45        /// The current block timestamp.
46        current: u64,
47        /// The validAfter constraint from the transaction.
48        valid_after: u64,
49    },
50
51    /// Transaction cannot be included after validBefore timestamp.
52    ///
53    /// Tempo transactions can specify a validBefore field to restrict when they can be included.
54    #[error("transaction expired: current block timestamp {current} >= validBefore {valid_before}")]
55    ValidBefore {
56        /// The current block timestamp.
57        current: u64,
58        /// The validBefore constraint from the transaction.
59        valid_before: u64,
60    },
61
62    /// P256 signature verification failed.
63    ///
64    /// The P256 signature could not be verified against the transaction hash.
65    #[error("P256 signature verification failed")]
66    InvalidP256Signature,
67
68    /// WebAuthn signature verification failed.
69    ///
70    /// The WebAuthn signature validation failed (could be authenticatorData, clientDataJSON, or P256 verification).
71    #[error("WebAuthn signature verification failed: {reason}")]
72    InvalidWebAuthnSignature {
73        /// Specific reason for failure.
74        reason: String,
75    },
76
77    /// Insufficient gas for intrinsic cost.
78    ///
79    /// Tempo transactions have variable intrinsic gas costs based on signature type and nonce usage.
80    /// This error occurs when the gas_limit is less than the calculated intrinsic gas.
81    #[error(
82        "insufficient gas for intrinsic cost: gas_limit {gas_limit} < intrinsic_gas {intrinsic_gas}"
83    )]
84    InsufficientGasForIntrinsicCost {
85        /// The transaction's gas limit.
86        gas_limit: u64,
87        /// The calculated intrinsic gas required.
88        intrinsic_gas: u64,
89    },
90
91    /// Nonce manager error.
92    #[error("nonce manager error: {0}")]
93    NonceManagerError(String),
94
95    /// Expiring nonce transaction missing tempo_tx_env.
96    #[error("expiring nonce transaction requires tempo_tx_env")]
97    ExpiringNonceMissingTxEnv,
98
99    /// Expiring nonce transaction missing valid_before.
100    #[error("expiring nonce transaction requires valid_before to be set")]
101    ExpiringNonceMissingValidBefore,
102
103    /// Expiring nonce transaction must have nonce == 0.
104    #[error("expiring nonce transaction must have nonce == 0")]
105    ExpiringNonceNonceNotZero,
106
107    /// Subblock transaction must have zero fee.
108    #[error("subblock transaction must have zero fee")]
109    SubblockTransactionMustHaveZeroFee,
110
111    /// Invalid fee token.
112    #[error("invalid fee token: {0}")]
113    InvalidFeeToken(Address),
114
115    /// Value transfer not allowed.
116    #[error("value transfer not allowed")]
117    ValueTransferNotAllowed,
118
119    /// Value transfer in Tempo Transaction not allowed.
120    #[error("value transfer in Tempo Transaction not allowed")]
121    ValueTransferNotAllowedInAATx,
122
123    /// Failed to recover access key address from signature.
124    ///
125    /// This error occurs when attempting to recover the access key address from a Keychain signature fails.
126    #[error("failed to recover access key address from signature")]
127    AccessKeyRecoveryFailed,
128
129    /// Access keys cannot authorize other keys.
130    ///
131    /// Only the root key can authorize new access keys. An access key can only authorize itself
132    /// in a same-transaction authorization flow.
133    #[error("access keys cannot authorize other keys, only the root key can authorize new keys")]
134    AccessKeyCannotAuthorizeOtherKeys,
135
136    /// Failed to recover signer from KeyAuthorization signature.
137    ///
138    /// This error occurs when signature recovery from the KeyAuthorization fails.
139    #[error("failed to recover signer from KeyAuthorization signature")]
140    KeyAuthorizationSignatureRecoveryFailed,
141
142    /// KeyAuthorization not signed by root account.
143    ///
144    /// The KeyAuthorization must be signed by the root account (transaction caller),
145    /// but was signed by a different address.
146    #[error(
147        "KeyAuthorization must be signed by root account {expected}, but was signed by {actual}"
148    )]
149    KeyAuthorizationNotSignedByRoot {
150        /// The expected signer (root account).
151        expected: Address,
152        /// The actual signer recovered from the signature.
153        actual: Address,
154    },
155
156    /// Access key expiry is in the past.
157    ///
158    /// An access key cannot be authorized with an expiry timestamp that has already passed.
159    #[error("access key expiry {expiry} is in the past (current timestamp: {current_timestamp})")]
160    AccessKeyExpiryInPast {
161        /// The expiry timestamp from the KeyAuthorization.
162        expiry: u64,
163        /// The current block timestamp.
164        current_timestamp: u64,
165    },
166
167    /// AccountKeychain precompile error during key authorization.
168    ///
169    /// This error occurs when the AccountKeychain precompile rejects the key authorization
170    /// (e.g., key already exists, invalid parameters).
171    #[error("keychain precompile error: {reason}")]
172    KeychainPrecompileError {
173        /// The error message from the precompile.
174        reason: String,
175    },
176
177    /// Keychain user address does not match transaction caller.
178    ///
179    /// For Keychain signatures, the user_address field must match the transaction caller.
180    #[error("keychain user_address {user_address} does not match transaction caller {caller}")]
181    KeychainUserAddressMismatch {
182        /// The user_address from the Keychain signature.
183        user_address: Address,
184        /// The transaction caller.
185        caller: Address,
186    },
187
188    /// Keychain validation failed.
189    ///
190    /// The access key is not authorized in the AccountKeychain precompile for this user,
191    /// or the key has expired, or spending limits are exceeded.
192    #[error("keychain validation failed: {reason}")]
193    KeychainValidationFailed {
194        /// The validation error details.
195        reason: String,
196    },
197
198    /// KeyAuthorization chain_id does not match the current chain.
199    #[error("KeyAuthorization chain_id mismatch: expected {expected}, got {got}")]
200    KeyAuthorizationChainIdMismatch {
201        /// The expected chain ID (current chain).
202        expected: u64,
203        /// The chain ID from the KeyAuthorization.
204        got: u64,
205    },
206
207    /// Legacy V1 keychain signature is no longer accepted (deprecated at T1C).
208    ///
209    /// V1 keychain signatures do not bind the user address into the signature hash.
210    /// Use V2 keychain signatures instead.
211    #[error("legacy V1 keychain signature is no longer accepted, use V2 (type 0x04)")]
212    LegacyKeychainSignature,
213
214    /// V2 keychain signature used before T1C activation.
215    ///
216    /// V2 signatures (type 0x04) are only valid after the T1C hardfork activates.
217    /// Rejecting them before activation prevents chain splits between upgraded and
218    /// non-upgraded nodes.
219    ///
220    /// TODO(tanishk): This variant can be removed after T1C activation on all networks.
221    #[error("V2 keychain signature (type 0x04) is not valid before T1C activation")]
222    V2KeychainBeforeActivation,
223
224    /// Keychain operations are not supported in subblock transactions.
225    #[error("keychain operations are not supported in subblock transactions")]
226    KeychainOpInSubblockTransaction,
227
228    /// Fee payment error.
229    #[error(transparent)]
230    CollectFeePreTx(#[from] FeePaymentError),
231
232    /// Tempo transaction validation error from validate_calls().
233    ///
234    /// This wraps validation errors from the shared validate_calls function.
235    #[error("{0}")]
236    CallsValidation(&'static str),
237}
238
239impl InvalidTxError for TempoInvalidTransaction {
240    fn is_nonce_too_low(&self) -> bool {
241        match self {
242            Self::EthInvalidTransaction(err) => err.is_nonce_too_low(),
243            _ => false,
244        }
245    }
246
247    fn as_invalid_tx_err(&self) -> Option<&InvalidTransaction> {
248        match self {
249            Self::EthInvalidTransaction(err) => Some(err),
250            _ => None,
251        }
252    }
253}
254
255impl<DBError> From<TempoInvalidTransaction> for EVMError<DBError, TempoInvalidTransaction> {
256    fn from(err: TempoInvalidTransaction) -> Self {
257        Self::Transaction(err)
258    }
259}
260
261impl From<&'static str> for TempoInvalidTransaction {
262    fn from(err: &'static str) -> Self {
263        Self::CallsValidation(err)
264    }
265}
266
267impl From<KeychainVersionError> for TempoInvalidTransaction {
268    fn from(err: KeychainVersionError) -> Self {
269        match err {
270            KeychainVersionError::LegacyPostT1C => Self::LegacyKeychainSignature,
271            KeychainVersionError::V2BeforeActivation => Self::V2KeychainBeforeActivation,
272        }
273    }
274}
275
276/// Error type for fee payment errors.
277#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
278pub enum FeePaymentError {
279    /// Insufficient liquidity in the FeeAMM pool to perform fee token swap.
280    ///
281    /// This indicates the user's fee token cannot be swapped for the native token
282    /// because there's insufficient liquidity in the AMM pool.
283    #[error("insufficient liquidity in FeeAMM pool to swap fee tokens (required: {fee})")]
284    InsufficientAmmLiquidity {
285        /// The required fee amount that couldn't be swapped.
286        fee: U256,
287    },
288
289    /// Insufficient fee token balance to pay for transaction fees.
290    ///
291    /// This is distinct from the Ethereum `LackOfFundForMaxFee` error because
292    /// it applies to custom fee tokens, not native balance.
293    #[error("insufficient fee token balance: required {fee}, but only have {balance}")]
294    InsufficientFeeTokenBalance {
295        /// The required fee amount.
296        fee: U256,
297        /// The actual balance available.
298        balance: U256,
299    },
300
301    /// Other error.
302    #[error("{0}")]
303    Other(String),
304}
305
306impl From<KeyAuthorizationChainIdError> for TempoInvalidTransaction {
307    fn from(err: KeyAuthorizationChainIdError) -> Self {
308        Self::KeyAuthorizationChainIdMismatch {
309            expected: err.expected,
310            got: err.got,
311        }
312    }
313}
314
315impl<DBError> From<FeePaymentError> for EVMError<DBError, TempoInvalidTransaction> {
316    fn from(err: FeePaymentError) -> Self {
317        TempoInvalidTransaction::from(err).into()
318    }
319}
320
321/// Tempo-specific halt reason.
322///
323/// Used to extend basic [`HaltReason`] with an edge case of a subblock transaction fee payment error.
324#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_more::From)]
325pub enum TempoHaltReason {
326    /// Basic Ethereum halt reason.
327    #[from]
328    Ethereum(HaltReason),
329    /// Subblock transaction failed to pay fees.
330    SubblockTxFeePayment,
331}
332
333#[cfg(feature = "rpc")]
334impl reth_rpc_eth_types::error::api::FromEvmHalt<TempoHaltReason>
335    for reth_rpc_eth_types::EthApiError
336{
337    fn from_evm_halt(halt_reason: TempoHaltReason, gas_limit: u64) -> Self {
338        match halt_reason {
339            TempoHaltReason::Ethereum(halt_reason) => Self::from_evm_halt(halt_reason, gas_limit),
340            TempoHaltReason::SubblockTxFeePayment => {
341                Self::EvmCustom("subblock transaction failed to pay fees".to_string())
342            }
343        }
344    }
345}
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_error_display() {
352        let err = TempoInvalidTransaction::SystemTransactionMustBeCall;
353        assert_eq!(
354            err.to_string(),
355            "system transaction must be a call, not a create"
356        );
357
358        let err = FeePaymentError::InsufficientAmmLiquidity {
359            fee: U256::from(1000),
360        };
361        assert!(
362            err.to_string()
363                .contains("insufficient liquidity in FeeAMM pool")
364        );
365
366        let err = FeePaymentError::InsufficientFeeTokenBalance {
367            fee: U256::from(1000),
368            balance: U256::from(500),
369        };
370        assert!(err.to_string().contains("insufficient fee token balance"));
371    }
372
373    #[test]
374    fn test_from_invalid_transaction() {
375        let eth_err = InvalidTransaction::PriorityFeeGreaterThanMaxFee;
376        let tempo_err: TempoInvalidTransaction = eth_err.into();
377        assert!(matches!(
378            tempo_err,
379            TempoInvalidTransaction::EthInvalidTransaction(_)
380        ));
381    }
382
383    #[test]
384    fn test_is_nonce_too_low() {
385        let err = TempoInvalidTransaction::EthInvalidTransaction(InvalidTransaction::NonceTooLow {
386            tx: 1,
387            state: 0,
388        });
389        assert!(err.is_nonce_too_low());
390        assert!(err.as_invalid_tx_err().is_some());
391
392        let err = TempoInvalidTransaction::InvalidFeePayerSignature;
393        assert!(!err.is_nonce_too_low());
394        assert!(err.as_invalid_tx_err().is_none());
395
396        let err = TempoInvalidTransaction::SelfSponsoredFeePayer;
397        assert!(!err.is_nonce_too_low());
398        assert!(err.as_invalid_tx_err().is_none());
399    }
400
401    #[test]
402    fn test_fee_payment_error() {
403        let _: EVMError<(), TempoInvalidTransaction> = FeePaymentError::InsufficientAmmLiquidity {
404            fee: U256::from(1000),
405        }
406        .into();
407    }
408}