1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
13pub enum TempoInvalidTransaction {
14 #[error(transparent)]
16 EthInvalidTransaction(#[from] InvalidTransaction),
17
18 #[error("system transaction must be a call, not a create")]
20 SystemTransactionMustBeCall,
21
22 #[error("system transaction execution failed, result: {_0:?}")]
24 SystemTransactionFailed(Box<ExecutionResult<TempoHaltReason>>),
25
26 #[error("fee payer signature recovery failed")]
31 InvalidFeePayerSignature,
32
33 #[error("fee payer cannot resolve to sender")]
35 SelfSponsoredFeePayer,
36
37 #[error(
42 "transaction not valid yet: current block timestamp {current} < validAfter {valid_after}"
43 )]
44 ValidAfter {
45 current: u64,
47 valid_after: u64,
49 },
50
51 #[error("transaction expired: current block timestamp {current} >= validBefore {valid_before}")]
55 ValidBefore {
56 current: u64,
58 valid_before: u64,
60 },
61
62 #[error("P256 signature verification failed")]
66 InvalidP256Signature,
67
68 #[error("WebAuthn signature verification failed: {reason}")]
72 InvalidWebAuthnSignature {
73 reason: String,
75 },
76
77 #[error("nonce manager error: {0}")]
79 NonceManagerError(String),
80
81 #[error("expiring nonce transaction requires tempo_tx_env")]
83 ExpiringNonceMissingTxEnv,
84
85 #[error("expiring nonce transaction requires valid_before to be set")]
87 ExpiringNonceMissingValidBefore,
88
89 #[error("expiring nonce transaction must have nonce == 0")]
91 ExpiringNonceNonceNotZero,
92
93 #[error("subblock transaction must have zero fee")]
95 SubblockTransactionMustHaveZeroFee,
96
97 #[error("invalid fee token: {0}")]
99 InvalidFeeToken(Address),
100
101 #[error("fee token {address} is not a TIP-20 token; fee tokens must be TIP-20 tokens")]
103 FeeTokenNotTip20 {
104 address: Address,
106 },
107
108 #[error(
110 "fee token {address} uses currency {currency:?}; fee tokens must be USD-denominated TIP-20 tokens"
111 )]
112 FeeTokenNotUsdCurrency {
113 address: Address,
115 currency: String,
117 },
118
119 #[error("fee token {address} is paused and cannot be used for fees")]
121 FeeTokenPaused {
122 address: Address,
124 },
125
126 #[error("value transfer not allowed")]
128 ValueTransferNotAllowed,
129
130 #[error("value transfer in Tempo Transaction not allowed")]
132 ValueTransferNotAllowedInAATx,
133
134 #[error("failed to recover access key address from signature")]
138 AccessKeyRecoveryFailed,
139
140 #[error("access keys cannot authorize other keys, only the root key can authorize new keys")]
145 AccessKeyCannotAuthorizeOtherKeys,
146
147 #[error("failed to recover signer from KeyAuthorization signature")]
151 KeyAuthorizationSignatureRecoveryFailed,
152
153 #[error(
158 "KeyAuthorization must be signed by root account {expected}, but was signed by {actual}"
159 )]
160 KeyAuthorizationNotSignedByRoot {
161 expected: Address,
163 actual: Address,
165 },
166
167 #[error("access key expiry {expiry} is in the past (current timestamp: {current_timestamp})")]
171 AccessKeyExpiryInPast {
172 expiry: u64,
174 current_timestamp: u64,
176 },
177
178 #[error("keychain precompile error: {reason}")]
183 KeychainPrecompileError {
184 reason: String,
186 },
187
188 #[error("keychain user_address {user_address} does not match transaction caller {caller}")]
192 KeychainUserAddressMismatch {
193 user_address: Address,
195 caller: Address,
197 },
198
199 #[error("keychain validation failed: {reason}")]
204 KeychainValidationFailed {
205 reason: String,
207 },
208
209 #[error("KeyAuthorization chain_id mismatch: expected {expected}, got {got}")]
211 KeyAuthorizationChainIdMismatch {
212 expected: u64,
214 got: u64,
216 },
217
218 #[error("legacy V1 keychain signature is no longer accepted, use V2 (type 0x04)")]
223 LegacyKeychainSignature,
224
225 #[error("V2 keychain signature (type 0x04) is not valid before T1C activation")]
233 V2KeychainBeforeActivation,
234
235 #[error("keychain operations are not supported in subblock transactions")]
237 KeychainOpInSubblockTransaction,
238
239 #[error(transparent)]
241 CollectFeePreTx(#[from] FeePaymentError),
242
243 #[error("{0}")]
247 CallsValidation(&'static str),
248}
249
250impl TempoInvalidTransaction {
251 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 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 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#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
374pub enum FeePaymentError {
375 #[error("insufficient liquidity in FeeAMM pool to swap fee tokens (required: {fee})")]
380 InsufficientAmmLiquidity {
381 fee: U256,
383 },
384
385 #[error("insufficient fee token balance: required {fee}, but only have {balance}")]
390 InsufficientFeeTokenBalance {
391 fee: U256,
393 balance: U256,
395 },
396
397 #[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#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_more::From)]
421pub enum TempoHaltReason {
422 #[from]
424 Ethereum(HaltReason),
425 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}