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    /// Nonce manager error.
78    #[error("nonce manager error: {0}")]
79    NonceManagerError(String),
80
81    /// Expiring nonce transaction missing tempo_tx_env.
82    #[error("expiring nonce transaction requires tempo_tx_env")]
83    ExpiringNonceMissingTxEnv,
84
85    /// Expiring nonce transaction missing valid_before.
86    #[error("expiring nonce transaction requires valid_before to be set")]
87    ExpiringNonceMissingValidBefore,
88
89    /// Expiring nonce transaction must have nonce == 0.
90    #[error("expiring nonce transaction must have nonce == 0")]
91    ExpiringNonceNonceNotZero,
92
93    /// Subblock transaction must have zero fee.
94    #[error("subblock transaction must have zero fee")]
95    SubblockTransactionMustHaveZeroFee,
96
97    /// Invalid fee token fallback.
98    #[error("invalid fee token: {0}")]
99    InvalidFeeToken(Address),
100
101    /// Fee token address is not a TIP-20 token.
102    #[error("fee token {address} is not a TIP-20 token; fee tokens must be TIP-20 tokens")]
103    FeeTokenNotTip20 {
104        /// Invalid fee token address.
105        address: Address,
106    },
107
108    /// Fee token is not USD-denominated.
109    #[error(
110        "fee token {address} uses currency {currency:?}; fee tokens must be USD-denominated TIP-20 tokens"
111    )]
112    FeeTokenNotUsdCurrency {
113        /// Invalid fee token address.
114        address: Address,
115        /// Token currency read from TIP-20 metadata.
116        currency: String,
117    },
118
119    /// Fee token is paused.
120    #[error("fee token {address} is paused and cannot be used for fees")]
121    FeeTokenPaused {
122        /// Paused fee token address.
123        address: Address,
124    },
125
126    /// Value transfer not allowed.
127    #[error("value transfer not allowed")]
128    ValueTransferNotAllowed,
129
130    /// Value transfer in Tempo Transaction not allowed.
131    #[error("value transfer in Tempo Transaction not allowed")]
132    ValueTransferNotAllowedInAATx,
133
134    /// Failed to recover access key address from signature.
135    ///
136    /// This error occurs when attempting to recover the access key address from a Keychain signature fails.
137    #[error("failed to recover access key address from signature")]
138    AccessKeyRecoveryFailed,
139
140    /// Access keys cannot authorize other keys.
141    ///
142    /// Only the root key can authorize new access keys. An access key can only authorize itself
143    /// in a same-transaction authorization flow.
144    #[error("access keys cannot authorize other keys, only the root key can authorize new keys")]
145    AccessKeyCannotAuthorizeOtherKeys,
146
147    /// Failed to recover signer from KeyAuthorization signature.
148    ///
149    /// This error occurs when signature recovery from the KeyAuthorization fails.
150    #[error("failed to recover signer from KeyAuthorization signature")]
151    KeyAuthorizationSignatureRecoveryFailed,
152
153    /// KeyAuthorization not signed by root account.
154    ///
155    /// The KeyAuthorization must be signed by the root account (transaction caller),
156    /// but was signed by a different address.
157    #[error(
158        "KeyAuthorization must be signed by root account {expected}, but was signed by {actual}"
159    )]
160    KeyAuthorizationNotSignedByRoot {
161        /// The expected signer (root account).
162        expected: Address,
163        /// The actual signer recovered from the signature.
164        actual: Address,
165    },
166
167    /// Access key expiry is in the past.
168    ///
169    /// An access key cannot be authorized with an expiry timestamp that has already passed.
170    #[error("access key expiry {expiry} is in the past (current timestamp: {current_timestamp})")]
171    AccessKeyExpiryInPast {
172        /// The expiry timestamp from the KeyAuthorization.
173        expiry: u64,
174        /// The current block timestamp.
175        current_timestamp: u64,
176    },
177
178    /// AccountKeychain precompile error during key authorization.
179    ///
180    /// This error occurs when the AccountKeychain precompile rejects the key authorization
181    /// (e.g., key already exists, invalid parameters).
182    #[error("keychain precompile error: {reason}")]
183    KeychainPrecompileError {
184        /// The error message from the precompile.
185        reason: String,
186    },
187
188    /// Keychain user address does not match transaction caller.
189    ///
190    /// For Keychain signatures, the user_address field must match the transaction caller.
191    #[error("keychain user_address {user_address} does not match transaction caller {caller}")]
192    KeychainUserAddressMismatch {
193        /// The user_address from the Keychain signature.
194        user_address: Address,
195        /// The transaction caller.
196        caller: Address,
197    },
198
199    /// Keychain validation failed.
200    ///
201    /// The access key is not authorized in the AccountKeychain precompile for this user,
202    /// or the key has expired, or spending limits are exceeded.
203    #[error("keychain validation failed: {reason}")]
204    KeychainValidationFailed {
205        /// The validation error details.
206        reason: String,
207    },
208
209    /// KeyAuthorization chain_id does not match the current chain.
210    #[error("KeyAuthorization chain_id mismatch: expected {expected}, got {got}")]
211    KeyAuthorizationChainIdMismatch {
212        /// The expected chain ID (current chain).
213        expected: u64,
214        /// The chain ID from the KeyAuthorization.
215        got: u64,
216    },
217
218    /// Legacy V1 keychain signature is no longer accepted (deprecated at T1C).
219    ///
220    /// V1 keychain signatures do not bind the user address into the signature hash.
221    /// Use V2 keychain signatures instead.
222    #[error("legacy V1 keychain signature is no longer accepted, use V2 (type 0x04)")]
223    LegacyKeychainSignature,
224
225    /// V2 keychain signature used before T1C activation.
226    ///
227    /// V2 signatures (type 0x04) are only valid after the T1C hardfork activates.
228    /// Rejecting them before activation prevents chain splits between upgraded and
229    /// non-upgraded nodes.
230    ///
231    /// TODO(tanishk): This variant can be removed after T1C activation on all networks.
232    #[error("V2 keychain signature (type 0x04) is not valid before T1C activation")]
233    V2KeychainBeforeActivation,
234
235    /// Keychain operations are not supported in subblock transactions.
236    #[error("keychain operations are not supported in subblock transactions")]
237    KeychainOpInSubblockTransaction,
238
239    /// Fee payment error.
240    #[error(transparent)]
241    CollectFeePreTx(#[from] FeePaymentError),
242
243    /// Tempo transaction validation error from validate_calls().
244    ///
245    /// This wraps validation errors from the shared validate_calls function.
246    #[error("{0}")]
247    CallsValidation(&'static str),
248}
249
250impl TempoInvalidTransaction {
251    /// Returns `true` if this error is deterministic — i.e. the transaction is inherently
252    /// malformed and will never become valid regardless of state changes.
253    ///
254    /// Returns `false` for state-dependent errors (balance, nonce, expiry, liquidity)
255    /// that may resolve as state advances.
256    pub fn is_bad_transaction(&self) -> bool {
257        match self {
258            Self::EthInvalidTransaction(eth) => match eth {
259                InvalidTransaction::PriorityFeeGreaterThanMaxFee
260                | InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
261                | InvalidTransaction::GasFloorMoreThanGasLimit { .. }
262                | InvalidTransaction::CreateInitCodeSizeLimit
263                | InvalidTransaction::InvalidChainId
264                | InvalidTransaction::MissingChainId
265                | InvalidTransaction::AccessListNotSupported
266                | InvalidTransaction::MaxFeePerBlobGasNotSupported
267                | InvalidTransaction::BlobVersionedHashesNotSupported
268                | InvalidTransaction::EmptyBlobs
269                | InvalidTransaction::BlobCreateTransaction
270                | InvalidTransaction::TooManyBlobs { .. }
271                | InvalidTransaction::BlobVersionNotSupported
272                | InvalidTransaction::AuthorizationListNotSupported
273                | InvalidTransaction::AuthorizationListInvalidFields
274                | InvalidTransaction::EmptyAuthorizationList
275                | InvalidTransaction::Eip2930NotSupported
276                | InvalidTransaction::Eip1559NotSupported
277                | InvalidTransaction::Eip4844NotSupported
278                | InvalidTransaction::Eip7702NotSupported
279                | InvalidTransaction::Eip7873NotSupported
280                | InvalidTransaction::Eip7873MissingTarget
281                | InvalidTransaction::OverflowPaymentInTransaction
282                | InvalidTransaction::NonceOverflowInTransaction
283                | InvalidTransaction::TxGasLimitGreaterThanCap { .. } => true,
284
285                InvalidTransaction::GasPriceLessThanBasefee
286                | InvalidTransaction::CallerGasLimitMoreThanBlock
287                | InvalidTransaction::RejectCallerWithCode
288                | InvalidTransaction::LackOfFundForMaxFee { .. }
289                | InvalidTransaction::NonceTooHigh { .. }
290                | InvalidTransaction::NonceTooLow { .. }
291                | InvalidTransaction::BlobGasPriceGreaterThanMax { .. }
292                | InvalidTransaction::Str(_) => false,
293            },
294
295            // Deterministic: tx is inherently malformed.
296            Self::SystemTransactionMustBeCall
297            | Self::SystemTransactionFailed(_)
298            | Self::InvalidFeePayerSignature
299            | Self::SelfSponsoredFeePayer
300            | Self::InvalidP256Signature
301            | Self::InvalidWebAuthnSignature { .. }
302            | Self::AccessKeyRecoveryFailed
303            | Self::AccessKeyCannotAuthorizeOtherKeys
304            | Self::KeyAuthorizationSignatureRecoveryFailed
305            | Self::KeyAuthorizationNotSignedByRoot { .. }
306            | Self::KeychainUserAddressMismatch { .. }
307            | Self::KeyAuthorizationChainIdMismatch { .. }
308            | Self::ValueTransferNotAllowed
309            | Self::ValueTransferNotAllowedInAATx
310            | Self::ExpiringNonceMissingTxEnv
311            | Self::ExpiringNonceMissingValidBefore
312            | Self::ExpiringNonceNonceNotZero
313            | Self::SubblockTransactionMustHaveZeroFee
314            | Self::KeychainOpInSubblockTransaction
315            | Self::LegacyKeychainSignature
316            | Self::CallsValidation(_) => true,
317
318            // State-dependent: may resolve as state advances.
319            Self::ValidAfter { .. }
320            | Self::ValidBefore { .. }
321            | Self::InvalidFeeToken(_)
322            | Self::FeeTokenNotTip20 { .. }
323            | Self::FeeTokenNotUsdCurrency { .. }
324            | Self::FeeTokenPaused { .. }
325            | Self::AccessKeyExpiryInPast { .. }
326            | Self::KeychainPrecompileError { .. }
327            | Self::KeychainValidationFailed { .. }
328            | Self::CollectFeePreTx(_)
329            | Self::NonceManagerError(_)
330            | Self::V2KeychainBeforeActivation => false,
331        }
332    }
333}
334
335impl InvalidTxError for TempoInvalidTransaction {
336    fn is_nonce_too_low(&self) -> bool {
337        match self {
338            Self::EthInvalidTransaction(err) => err.is_nonce_too_low(),
339            _ => false,
340        }
341    }
342
343    fn as_invalid_tx_err(&self) -> Option<&InvalidTransaction> {
344        match self {
345            Self::EthInvalidTransaction(err) => Some(err),
346            _ => None,
347        }
348    }
349}
350
351impl<DBError> From<TempoInvalidTransaction> for EVMError<DBError, TempoInvalidTransaction> {
352    fn from(err: TempoInvalidTransaction) -> Self {
353        Self::Transaction(err)
354    }
355}
356
357impl From<&'static str> for TempoInvalidTransaction {
358    fn from(err: &'static str) -> Self {
359        Self::CallsValidation(err)
360    }
361}
362
363impl From<KeychainVersionError> for TempoInvalidTransaction {
364    fn from(err: KeychainVersionError) -> Self {
365        match err {
366            KeychainVersionError::LegacyPostT1C => Self::LegacyKeychainSignature,
367            KeychainVersionError::V2BeforeActivation => Self::V2KeychainBeforeActivation,
368        }
369    }
370}
371
372/// Error type for fee payment errors.
373#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
374pub enum FeePaymentError {
375    /// Insufficient liquidity in the FeeAMM pool to perform fee token swap.
376    ///
377    /// This indicates the user's fee token cannot be swapped for the native token
378    /// because there's insufficient liquidity in the AMM pool.
379    #[error("insufficient liquidity in FeeAMM pool to swap fee tokens (required: {fee})")]
380    InsufficientAmmLiquidity {
381        /// The required fee amount that couldn't be swapped.
382        fee: U256,
383    },
384
385    /// Insufficient fee token balance to pay for transaction fees.
386    ///
387    /// This is distinct from the Ethereum `LackOfFundForMaxFee` error because
388    /// it applies to custom fee tokens, not native balance.
389    #[error("insufficient fee token balance: required {fee}, but only have {balance}")]
390    InsufficientFeeTokenBalance {
391        /// The required fee amount.
392        fee: U256,
393        /// The actual balance available.
394        balance: U256,
395    },
396
397    /// Other error.
398    #[error("{0}")]
399    Other(String),
400}
401
402impl From<KeyAuthorizationChainIdError> for TempoInvalidTransaction {
403    fn from(err: KeyAuthorizationChainIdError) -> Self {
404        Self::KeyAuthorizationChainIdMismatch {
405            expected: err.expected,
406            got: err.got,
407        }
408    }
409}
410
411impl<DBError> From<FeePaymentError> for EVMError<DBError, TempoInvalidTransaction> {
412    fn from(err: FeePaymentError) -> Self {
413        TempoInvalidTransaction::from(err).into()
414    }
415}
416
417/// Tempo-specific halt reason.
418///
419/// Used to extend basic [`HaltReason`] with an edge case of a subblock transaction fee payment error.
420#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_more::From)]
421pub enum TempoHaltReason {
422    /// Basic Ethereum halt reason.
423    #[from]
424    Ethereum(HaltReason),
425    /// Subblock transaction failed to pay fees.
426    SubblockTxFeePayment,
427}
428
429#[cfg(feature = "rpc")]
430impl reth_rpc_eth_types::error::api::FromEvmHalt<TempoHaltReason>
431    for reth_rpc_eth_types::EthApiError
432{
433    fn from_evm_halt(halt_reason: TempoHaltReason, gas_limit: u64) -> Self {
434        match halt_reason {
435            TempoHaltReason::Ethereum(halt_reason) => Self::from_evm_halt(halt_reason, gas_limit),
436            TempoHaltReason::SubblockTxFeePayment => {
437                Self::EvmCustom("subblock transaction failed to pay fees".to_string())
438            }
439        }
440    }
441}
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    #[test]
447    fn test_error_display() {
448        let err = TempoInvalidTransaction::SystemTransactionMustBeCall;
449        assert_eq!(
450            err.to_string(),
451            "system transaction must be a call, not a create"
452        );
453
454        let err = FeePaymentError::InsufficientAmmLiquidity {
455            fee: U256::from(1000),
456        };
457        assert!(
458            err.to_string()
459                .contains("insufficient liquidity in FeeAMM pool")
460        );
461
462        let err = FeePaymentError::InsufficientFeeTokenBalance {
463            fee: U256::from(1000),
464            balance: U256::from(500),
465        };
466        assert!(err.to_string().contains("insufficient fee token balance"));
467    }
468
469    #[test]
470    fn test_from_invalid_transaction() {
471        let eth_err = InvalidTransaction::PriorityFeeGreaterThanMaxFee;
472        let tempo_err: TempoInvalidTransaction = eth_err.into();
473        assert!(matches!(
474            tempo_err,
475            TempoInvalidTransaction::EthInvalidTransaction(_)
476        ));
477    }
478
479    #[test]
480    fn test_fee_token_errors_are_not_bad_transactions() {
481        let address = Address::repeat_byte(0x20);
482        let cases = [
483            TempoInvalidTransaction::InvalidFeeToken(address),
484            TempoInvalidTransaction::FeeTokenNotTip20 { address },
485            TempoInvalidTransaction::FeeTokenNotUsdCurrency {
486                address,
487                currency: "EUR".to_string(),
488            },
489            TempoInvalidTransaction::FeeTokenPaused { address },
490        ];
491
492        for err in cases {
493            assert!(!err.is_bad_transaction(), "{err} should not be bad");
494        }
495    }
496
497    #[test]
498    fn test_is_nonce_too_low() {
499        let err = TempoInvalidTransaction::EthInvalidTransaction(InvalidTransaction::NonceTooLow {
500            tx: 1,
501            state: 0,
502        });
503        assert!(err.is_nonce_too_low());
504        assert!(err.as_invalid_tx_err().is_some());
505
506        let err = TempoInvalidTransaction::InvalidFeePayerSignature;
507        assert!(!err.is_nonce_too_low());
508        assert!(err.as_invalid_tx_err().is_none());
509
510        let err = TempoInvalidTransaction::SelfSponsoredFeePayer;
511        assert!(!err.is_nonce_too_low());
512        assert!(err.as_invalid_tx_err().is_none());
513    }
514
515    #[test]
516    fn test_fee_payment_error() {
517        let _: EVMError<(), TempoInvalidTransaction> = FeePaymentError::InsufficientAmmLiquidity {
518            fee: U256::from(1000),
519        }
520        .into();
521    }
522}