diff --git a/crates/full-node/sov-sequencer/src/preferred/block_executor.rs b/crates/full-node/sov-sequencer/src/preferred/block_executor.rs index 9666ca6871..7aac9cf322 100644 --- a/crates/full-node/sov-sequencer/src/preferred/block_executor.rs +++ b/crates/full-node/sov-sequencer/src/preferred/block_executor.rs @@ -13,9 +13,9 @@ use sov_modules_api::capabilities::{ }; use sov_modules_api::macros::config_value; use sov_modules_api::{ - call_message_repr, Amount, BlobDataWithId, ChangeSet, DaSpec, ExecutionContext, FullyBakedTx, - Gas, GasSpec, HexString, KernelStateAccessor, NoOpControlFlow, RejectReason, Runtime, - RuntimeEventProcessor, RuntimeEventResponse, SelectedBlob, Spec, StateCheckpoint, + call_message_repr, Amount, BlobDataWithId, ChangeSet, DaSpec, ErrorDetail, ExecutionContext, + FullyBakedTx, Gas, GasSpec, HexString, KernelStateAccessor, NoOpControlFlow, RejectReason, + Runtime, RuntimeEventProcessor, RuntimeEventResponse, SelectedBlob, Spec, StateCheckpoint, TransactionReceipt, TxChangeSet, TxHash, VersionReader, VisibleSlotNumber, }; use sov_modules_api::{CryptoSpec, HDTimestamp}; @@ -95,9 +95,10 @@ impl RollupBlockExecutorError { sov_rollup_interface::stf::TxEffect::Reverted(reverted) => { reverted.reason.error_detail().unwrap_or(json_obj!({})) } - _ => json_obj!({ - "error": format!("{:?}", receipt), - }), + sov_rollup_interface::stf::TxEffect::Skipped(skipped) => { + skipped.error.error_detail().unwrap_or(json_obj!({})) + } + _ => json_obj!({"error": format!("{:?}", receipt)}), }; ErrorObject { status: StatusCode::BAD_REQUEST, diff --git a/crates/full-node/sov-sequencer/src/preferred/nonce_buffer_task.rs b/crates/full-node/sov-sequencer/src/preferred/nonce_buffer_task.rs index ca657bb57a..ba0b47eb00 100644 --- a/crates/full-node/sov-sequencer/src/preferred/nonce_buffer_task.rs +++ b/crates/full-node/sov-sequencer/src/preferred/nonce_buffer_task.rs @@ -2,7 +2,8 @@ use async_trait::async_trait; use sov_modules_api::prelude::UnwrapInfallible; use sov_modules_api::rest::ApiState; use sov_modules_api::{ - FullyBakedTx, Gas, Runtime, SkippedTxContents, Spec, TransactionReceipt, TxProcessingError, + BadNonceReason, FullyBakedTx, Gas, Runtime, SkippedTxContents, Spec, TransactionReceipt, + TxProcessingError, }; use sov_rollup_interface::{ crypto::CredentialId, @@ -982,19 +983,29 @@ fn err_invalid_nonce>( InvalidNonceReason::Replaced => format!("{was_queued_msg} But a new transaction with the same nonce has arrived and replaced it in the account's queue."), InvalidNonceReason::AlreadyQueued => format!("An identical transaction with the same hash is already in the nonce queue; its existing status is unchanged from this request. {was_queued_msg}"), }; - let error_msg = format!( - "Tx bad nonce for credential id: {credential_id}, expected: {expected_nonce}, but found: {tx_nonce}. {queue_error_msg}" - ); tracing::debug!( - "Sequencer rejecting nonce transaction with error: {error_msg}, tx_hash: {tx_hash}" + "Sequencer rejecting nonce transaction: credential_id={credential_id}, expected_nonce={expected_nonce}, tx_nonce={tx_nonce}, reason={queue_error_msg}, tx_hash={tx_hash}" ); + let bad_nonce_reason = match queue_rejection_reason { + InvalidNonceReason::Invalid => BadNonceReason::OutsideQueueRange, + InvalidNonceReason::Timeout => BadNonceReason::QueueTimeout, + InvalidNonceReason::EvictedBeforeExecution => BadNonceReason::QueueEvicted, + InvalidNonceReason::Replaced => BadNonceReason::Replaced, + InvalidNonceReason::AlreadyQueued => BadNonceReason::AlreadyQueued, + }; let receipt = TransactionReceipt { tx_hash, body_to_save: None, events: Vec::new(), receipt: sov_rollup_interface::stf::TxEffect::Skipped(SkippedTxContents { gas_used: ::zero(), - error: TxProcessingError::CheckUniquenessFailed(error_msg), + error: TxProcessingError::CheckUniquenessFailed( + sov_modules_api::CheckUniquenessError::BadNonce { + expected_nonce, + provided_nonce: tx_nonce, + reason: bad_nonce_reason, + }, + ), }), }; Ok(Err(AcceptTxError::NewTxError(DoNewTxError::ExecutorError( @@ -1334,7 +1345,13 @@ mod tests { events: Vec::new(), receipt: sov_rollup_interface::stf::TxEffect::Skipped(SkippedTxContents { gas_used: ::Gas::from([0, 0]), - error: TxProcessingError::CheckUniquenessFailed(format!("Tx bad nonce for credential id: {}, expected: {executor_nonce}, but found: {tx_nonce}", CredentialId::from_bytes([1u8; 32]))), + error: TxProcessingError::CheckUniquenessFailed( + sov_modules_api::CheckUniquenessError::BadNonce { + expected_nonce: executor_nonce, + provided_nonce: tx_nonce, + reason: BadNonceReason::WrongNonce, + }, + ), }), }; return Ok(Err(AcceptTxError::NewTxError(DoNewTxError::ExecutorError( @@ -1593,12 +1610,19 @@ mod tests { result.unwrap().unwrap().await.is_ok(), "Expected tx {i} to succeed" ), - Outcome::Err(reason) => { + Outcome::Err(queue_reason) => { let inner = result.expect("Expected Ok from TransactionReceiverResult"); let err = inner.expect_err(&format!("Expected tx {i} to fail with nonce error")); - // Pattern match to extract the error message + let expected_reason = match queue_reason { + InvalidNonceReason::Invalid => BadNonceReason::OutsideQueueRange, + InvalidNonceReason::Timeout => BadNonceReason::QueueTimeout, + InvalidNonceReason::EvictedBeforeExecution => BadNonceReason::QueueEvicted, + InvalidNonceReason::Replaced => BadNonceReason::Replaced, + InvalidNonceReason::AlreadyQueued => BadNonceReason::AlreadyQueued, + }; + match err { AcceptTxError::NewTxError(DoNewTxError::ExecutorError( RollupBlockExecutorError::UnsuccessfulTransaction { receipt }, @@ -1606,28 +1630,16 @@ mod tests { match receipt.receipt { sov_rollup_interface::stf::TxEffect::Skipped(contents) => { match contents.error { - TxProcessingError::CheckUniquenessFailed(msg) => { - // Verify the message contains "bad nonce" - assert!( - msg.contains("bad nonce"), - "Tx {i}: Error should mention bad nonce: \"{msg}\"", - ); - - // Verify the reason-specific substring - let expected_substring = match reason { - InvalidNonceReason::Invalid => "did not attempt to queue", - InvalidNonceReason::Timeout => "has timed out and is being evicted", - InvalidNonceReason::EvictedBeforeExecution => "dropped from the queue for an unknown reason", - - InvalidNonceReason::Replaced => "new transaction with the same nonce has arrived and replaced it", - InvalidNonceReason::AlreadyQueued => "identical transaction with the same hash", - }; - assert!( - msg.contains(expected_substring), - "Tx {i}: Error message should contain \"{expected_substring}\" for reason {reason:?}, but got: \"{msg}\"", + TxProcessingError::CheckUniquenessFailed( + sov_modules_api::CheckUniquenessError::BadNonce { reason, .. }, + ) => { + assert_eq!( + reason, + expected_reason, + "Tx {i}: BadNonce reason mismatch", ); } - other => panic!("Tx {i}: Expected CheckUniquenessFailed error, got: {other:?}"), + other => panic!("Tx {i}: Expected CheckUniquenessFailed(BadNonce), got: {other:?}"), } } other => panic!("Tx {i}: Expected Skipped receipt, got: {other:?}"), @@ -1691,10 +1703,6 @@ mod tests { let inner = result.expect("Expected Ok from TransactionReceiverResult"); let err = inner.expect_err(&format!("Expected tx {i} to fail with rejection")); - let expected_msg = format!( - "Tx bad nonce for credential id: {}, expected: {expected}, but found: {tx}", - CredentialId::from_bytes([1u8; 32]) - ); assert!( matches!( &err, @@ -1702,13 +1710,19 @@ mod tests { RollupBlockExecutorError::UnsuccessfulTransaction { receipt: TransactionReceipt { receipt: sov_rollup_interface::stf::TxEffect::Skipped(SkippedTxContents { - error: TxProcessingError::CheckUniquenessFailed(msg), + error: TxProcessingError::CheckUniquenessFailed( + sov_modules_api::CheckUniquenessError::BadNonce { + expected_nonce, + provided_nonce, + .. + } + ), .. }), .. } }, - )) if msg == &expected_msg + )) if *expected_nonce == expected as u64 && *provided_nonce == tx as u64 ), "Tx {i}: Expected STF nonce rejection with expected={expected}, tx={tx}, got: {err:?}" ); diff --git a/crates/module-system/module-implementations/sov-uniqueness/src/capabilities.rs b/crates/module-system/module-implementations/sov-uniqueness/src/capabilities.rs index 02d5d4892f..6ffb22001c 100644 --- a/crates/module-system/module-implementations/sov-uniqueness/src/capabilities.rs +++ b/crates/module-system/module-implementations/sov-uniqueness/src/capabilities.rs @@ -1,6 +1,8 @@ use sov_modules_api::capabilities::UniquenessData; use sov_modules_api::ExecutionContext; -use sov_modules_api::{CredentialId, Spec, StateAccessor, StateReader, TxHash}; +use sov_modules_api::{ + CheckUniquenessError, CredentialId, Spec, StateAccessor, StateReader, TxHash, +}; use sov_state::User; use crate::Uniqueness; @@ -20,7 +22,7 @@ impl Uniqueness { transaction_hash: TxHash, execution_context: &ExecutionContext, state: &mut impl StateReader, - ) -> anyhow::Result<()> { + ) -> Result<(), CheckUniquenessError> { match transaction_uniqueness { UniquenessData::Nonce(nonce) => match execution_context { ExecutionContext::SequencerWarmUp => { diff --git a/crates/module-system/module-implementations/sov-uniqueness/src/generations.rs b/crates/module-system/module-implementations/sov-uniqueness/src/generations.rs index 543f4ca06a..68415a2ccf 100644 --- a/crates/module-system/module-implementations/sov-uniqueness/src/generations.rs +++ b/crates/module-system/module-implementations/sov-uniqueness/src/generations.rs @@ -1,5 +1,7 @@ use sov_modules_api::macros::config_value; -use sov_modules_api::{CredentialId, Spec, StateAccessor, StateReader, TxHash}; +use sov_modules_api::{ + CheckUniquenessError, CoreModuleError, CredentialId, Spec, StateAccessor, StateReader, TxHash, +}; use sov_state::User; use crate::Uniqueness; @@ -10,10 +12,11 @@ impl Uniqueness { transaction_generation: u64, transaction_hash: TxHash, state: &mut impl StateReader, - ) -> anyhow::Result<()> { + ) -> Result<(), CheckUniquenessError> { let mut senders_buckets = self .generations - .get(credential_id, state)? + .get(credential_id, state) + .map_err(CoreModuleError::state_read)? .unwrap_or_default(); // The "currently active" generations is the range containing the latest seen generation @@ -24,15 +27,9 @@ impl Uniqueness { let past_transaction_generations: u64 = config_value!("PAST_TRANSACTION_GENERATIONS"); let transaction_generation_cutoff: u64 = past_transaction_generations.checked_sub(1) - .ok_or( anyhow::anyhow!("PAST_TRANSACTION_GENERATIONS should be greater than 0. Please ensure you have set this value correctly"))?; + .ok_or_else(|| CheckUniquenessError::from(anyhow::anyhow!("PAST_TRANSACTION_GENERATIONS should be greater than 0. Please ensure you have set this value correctly")))?; - // If we're below the current generation range, always fail - // Note about the arithmetic: for a given PAST_TRANSACTION_GENERATIONS, the correct - // comparison is - // `transaction_generation <= latest_generation - PAST_TRANSACTION_GENERATIONS`. - // For example, at generation 10 and the last 5 being valid, the valid generations are 6, - // 7, 8, 9 and 10; thus we fail the transaction if `transaction_generation <= 5`. - // Ensure we're not below the current generation range + // If we're below the current generation range, reject this transaction. // Note about the arithmetic: for a given PAST_TRANSACTION_GENERATIONS, the correct // comparison is // `transaction_generation > latest_generation - PAST_TRANSACTION_GENERATIONS`. @@ -43,14 +40,23 @@ impl Uniqueness { // `transaction_generation >= latest_generation - (PAST_TRANSACTION_GENERATIONS - 1)`. // N.B. this does add one edge case where an extra generation is accepted when `latest generation == PAST_TRANSACTION_GENERATIONS`, which is deemed acceptable. // which amounts to `latest_generation - (PAST_TRANSACTION_GENERATIONS - 1) <= transaction_generation` - anyhow::ensure!(latest_generation.saturating_sub(transaction_generation_cutoff) <= transaction_generation, - "Bad generation number for credential id: {credential_id}, latest known generation is: {latest_generation}, provided generation {transaction_generation} is older than cutoff limit"); + if latest_generation.saturating_sub(transaction_generation_cutoff) > transaction_generation + { + return Err(CheckUniquenessError::BadGeneration { + latest_generation, + provided_generation: transaction_generation, + }); + } // If we're within or above the active range, we check the hash against previously stored hashes in // the same generation, if any if let Some(bucket) = senders_buckets.get(&transaction_generation) { - anyhow::ensure!(!bucket.contains(&transaction_hash), "Duplicate transaction for credential_id {credential_id} at generation {transaction_generation}: hash {transaction_hash:} has already been seen"); - }; + if bucket.contains(&transaction_hash) { + return Err(CheckUniquenessError::DuplicateGeneration { + generation: transaction_generation, + }); + } + } // If we reach this point, the transaction is not a duplicate. However, we may still need to reject it to avoid overflowing our capacity for // remembering past transactions. @@ -68,35 +74,40 @@ impl Uniqueness { let num_txs_after_increment = senders_buckets .values() .try_fold(0_u64, |acc, bucket| { - let bucket_len = bucket.len().try_into().map_err(|e| { - anyhow::anyhow!("Overflow when converting the bucket length to u64 {e}") + let bucket_len: u64 = bucket.len().try_into().map_err(|e| { + CheckUniquenessError::from(anyhow::anyhow!( + "Overflow when converting bucket length: {e}" + )) })?; - - let acc = acc.checked_add(bucket_len).ok_or(anyhow::anyhow!( - "Overflow when adding number of transactions in bucket" - ))?; - - anyhow::Ok(acc) + acc.checked_add(bucket_len).ok_or_else(|| { + CheckUniquenessError::from(anyhow::anyhow!( + "Overflow when summing transaction counts" + )) + }) })? .checked_add(1) - .ok_or(anyhow::anyhow!( - "Overflow when adding 1 to the number of transactions in bucket" - ))?; + .ok_or_else(|| { + CheckUniquenessError::from(anyhow::anyhow!( + "Overflow when incrementing transaction count" + )) + })?; if num_txs_after_increment > config_value!("MAX_STORED_TX_HASHES_PER_CREDENTIAL") { - // If we overflow, compute the next generation number that the user needs and include it in the error message. let earliest_valid_bucket = senders_buckets .keys() .next() .expect("Since `num_txs_after_increment` is greater than 0, there must be at least one non-empty bucket in the iterator"); - let last_generation_before_pruning = earliest_valid_bucket + let next_valid_generation = earliest_valid_bucket .checked_add(config_value!("PAST_TRANSACTION_GENERATIONS")) - .ok_or(anyhow::anyhow!( - "Overflow when computing last non empty generation. This shouldn't happen. It means that a user a) fills up their existing buckets AND b) has already incremented their generation to `u64::MAX` and can no longer prune transactions. This account can no longer accept transactions." - ))?; - - anyhow::bail!("Too many transactions for credential_id {credential_id} at generation {transaction_generation}: hash {transaction_hash:} would cause the bucket to overflow. Increment your generation number to a value greater than {last_generation_before_pruning} and try again."); + .ok_or_else(|| CheckUniquenessError::from(anyhow::anyhow!( + "Overflow when computing next valid generation. This account can no longer accept transactions." + )))?; + + return Err(CheckUniquenessError::GenerationCapacityExceeded { + current_generation: transaction_generation, + next_valid_generation, + }); } Ok(()) diff --git a/crates/module-system/module-implementations/sov-uniqueness/src/nonces.rs b/crates/module-system/module-implementations/sov-uniqueness/src/nonces.rs index 9fec9b80c0..1394b5c295 100644 --- a/crates/module-system/module-implementations/sov-uniqueness/src/nonces.rs +++ b/crates/module-system/module-implementations/sov-uniqueness/src/nonces.rs @@ -1,4 +1,7 @@ -use sov_modules_api::{CredentialId, Spec, StateAccessor, StateReader}; +use sov_modules_api::{ + BadNonceReason, CheckUniquenessError, CoreModuleError, CredentialId, Spec, StateAccessor, + StateReader, +}; use sov_state::User; use crate::Uniqueness; @@ -8,13 +11,20 @@ impl Uniqueness { credential_id: &CredentialId, transaction_nonce: u64, state: &mut impl StateReader, - ) -> anyhow::Result<()> { - let nonce = self.nonces.get(credential_id, state)?.unwrap_or_default(); + ) -> Result<(), CheckUniquenessError> { + let nonce = self + .nonces + .get(credential_id, state) + .map_err(CoreModuleError::state_read)? + .unwrap_or_default(); - anyhow::ensure!( - nonce == transaction_nonce, - "Tx bad nonce for credential id: {credential_id}, expected: {nonce}, but found: {transaction_nonce}", - ); + if nonce != transaction_nonce { + return Err(CheckUniquenessError::BadNonce { + expected_nonce: nonce, + provided_nonce: transaction_nonce, + reason: BadNonceReason::WrongNonce, + }); + } Ok(()) } @@ -24,13 +34,19 @@ impl Uniqueness { credential_id: &CredentialId, transaction_nonce: u64, state: &mut impl StateReader, - ) -> anyhow::Result<()> { - let nonce = self.nonces.get(credential_id, state)?.unwrap_or_default(); + ) -> Result<(), CheckUniquenessError> { + let nonce = self + .nonces + .get(credential_id, state) + .map_err(CoreModuleError::state_read)? + .unwrap_or_default(); - anyhow::ensure!( - nonce <= transaction_nonce, - "Tx bad nonce for credential id: {credential_id}, expected at least: {nonce}, but found: {transaction_nonce}", - ); + if nonce > transaction_nonce { + return Err(CheckUniquenessError::NonceTooLow { + minimum_nonce: nonce, + provided_nonce: transaction_nonce, + }); + } Ok(()) } diff --git a/crates/module-system/module-implementations/sov-uniqueness/tests/integration/call_tests.rs b/crates/module-system/module-implementations/sov-uniqueness/tests/integration/call_tests.rs index 867c0865f4..d0049c1df5 100644 --- a/crates/module-system/module-implementations/sov-uniqueness/tests/integration/call_tests.rs +++ b/crates/module-system/module-implementations/sov-uniqueness/tests/integration/call_tests.rs @@ -1,5 +1,5 @@ use sov_modules_api::macros::config_value; -use sov_modules_api::{CredentialId, TxEffect}; +use sov_modules_api::{CheckUniquenessError, CredentialId, TxEffect}; use sov_test_utils::{BatchType, SlotInput, TransactionTestCase, TxProcessingError}; use sov_uniqueness::Uniqueness; @@ -93,9 +93,9 @@ fn do_max_stored_tx_hashes_per_credential_test() { panic!("Transaction should be skipped"); }; match skipped.error { - TxProcessingError::CheckUniquenessFailed(reason) => { - assert!(reason.contains("Too many transactions for credential_id")); - } + TxProcessingError::CheckUniquenessFailed( + CheckUniquenessError::GenerationCapacityExceeded { .. }, + ) => {} _ => { panic!("Transaction should be rejected because it's not unique"); } diff --git a/crates/module-system/sov-capabilities/src/lib.rs b/crates/module-system/sov-capabilities/src/lib.rs index 7fb8b2169c..1d78cf8838 100644 --- a/crates/module-system/sov-capabilities/src/lib.rs +++ b/crates/module-system/sov-capabilities/src/lib.rs @@ -278,7 +278,7 @@ impl TransactionAuthorizer for StandardProvenRollupCapabilities<' _context: &Context, execution_context: &ExecutionContext, state: &mut impl StateReader, - ) -> anyhow::Result<()> { + ) -> Result<(), sov_modules_api::CheckUniquenessError> { self.uniqueness.check_uniqueness( &auth_data.credential_id, auth_data.uniqueness, diff --git a/crates/module-system/sov-modules-api/src/common/error.rs b/crates/module-system/sov-modules-api/src/common/error.rs index 0a0ceb1fa1..9215e1317b 100644 --- a/crates/module-system/sov-modules-api/src/common/error.rs +++ b/crates/module-system/sov-modules-api/src/common/error.rs @@ -100,6 +100,34 @@ pub enum CoreModuleError { StateWrite(Box), } +impl Clone for CoreModuleError { + fn clone(&self) -> Self { + CoreModuleError::Generic(anyhow::anyhow!("{}", self)) + } +} + +impl PartialEq for CoreModuleError { + fn eq(&self, other: &Self) -> bool { + self.to_string() == other.to_string() + } +} + +impl Eq for CoreModuleError {} + +impl<'de> serde::Deserialize<'de> for CoreModuleError { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + struct Helper { + message: String, + } + let h = Helper::deserialize(deserializer)?; + Ok(CoreModuleError::Generic(anyhow::anyhow!("{}", h.message))) + } +} + impl CoreModuleError { /// Creates a new `StateRead` error variant from any error type. /// diff --git a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs index c76624583f..d752e0307d 100644 --- a/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs +++ b/crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs @@ -11,7 +11,7 @@ use sov_rollup_interface::{Bytes, TxHash}; use sov_universal_wallet::UniversalWallet; use crate::transaction::Credentials; -use crate::{Context, SequencerType, Spec, StateAccessor}; +use crate::{CheckUniquenessError, Context, SequencerType, Spec, StateAccessor}; /// Authorizes transactions to be executed. pub trait TransactionAuthorizer { @@ -43,7 +43,7 @@ pub trait TransactionAuthorizer { context: &Context, execution_context: &ExecutionContext, state: &mut impl StateAccessor, - ) -> anyhow::Result<()>; + ) -> Result<(), CheckUniquenessError>; /// Marks a transaction as having been executed, preventing it from executing again. fn mark_tx_attempted( diff --git a/crates/module-system/sov-modules-api/src/tx_receipt.rs b/crates/module-system/sov-modules-api/src/tx_receipt.rs index 92324e9da9..058de6d0aa 100644 --- a/crates/module-system/sov-modules-api/src/tx_receipt.rs +++ b/crates/module-system/sov-modules-api/src/tx_receipt.rs @@ -1,5 +1,6 @@ pub use crate::common::ModuleError as Error; use crate::Spec; +use crate::{CoreModuleError, ErrorContext, ErrorDetail}; /// The receipt type for a transaction using the STF blueprint. pub type TransactionReceipt = @@ -74,6 +75,94 @@ impl PartialEq for SkippedTxContents { } impl Eq for SkippedTxContents {} +/// Why a nonce-based transaction was rejected. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BadNonceReason { + /// Nonce did not match the expected consecutive value. + WrongNonce, + /// Nonce is outside the valid queue range (past or beyond the max limit); rejected immediately. + OutsideQueueRange, + /// Transaction was queued but timed out before its predecessor was executed. + QueueTimeout, + /// Transaction was queued but dropped, usually because the sequencer is shutting down. + QueueEvicted, + /// A new transaction with the same nonce arrived and replaced this one in the queue. + Replaced, + /// An identical transaction (same nonce and hash) is already in the queue. + AlreadyQueued, +} + +/// Structured error returned when a transaction's uniqueness check fails. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, thiserror::Error)] +#[serde(rename_all = "snake_case", tag = "kind")] +pub enum CheckUniquenessError { + /// The generation number is older than the sequencer's acceptance window. + #[error("bad generation: latest known generation is {latest_generation}, provided {provided_generation} is too old")] + BadGeneration { + /// The latest generation the sequencer has seen for this credential. + latest_generation: u64, + /// The generation provided in the transaction. + provided_generation: u64, + }, + /// The transaction hash was already seen at this generation. + #[error("duplicate transaction at generation {generation}")] + DuplicateGeneration { + /// The generation at which the duplicate was detected. + generation: u64, + }, + /// Too many transactions at the current generation; the credential must increment it. + #[error("too many transactions at generation {current_generation}: increment generation to {next_valid_generation}")] + GenerationCapacityExceeded { + /// The generation that is full. + current_generation: u64, + /// The minimum generation value that will be accepted next. + next_valid_generation: u64, + }, + /// The nonce was not the expected next value. + #[error("bad nonce: expected {expected_nonce}, provided {provided_nonce} ({reason:?})")] + BadNonce { + /// The nonce the sequencer expected. + expected_nonce: u64, + /// The nonce provided in the transaction. + provided_nonce: u64, + /// Why the nonce was rejected. + reason: BadNonceReason, + }, + /// The nonce was below the minimum accepted value (warm-up / non-consecutive mode). + #[error("nonce too low: minimum {minimum_nonce}, provided {provided_nonce}")] + NonceTooLow { + /// The minimum nonce the sequencer will accept. + minimum_nonce: u64, + /// The nonce provided in the transaction. + provided_nonce: u64, + }, + /// An unexpected internal error occurred during the uniqueness check. + #[error(transparent)] + Internal(#[from] CoreModuleError), +} + +impl From for CheckUniquenessError { + fn from(e: anyhow::Error) -> Self { + CheckUniquenessError::Internal(CoreModuleError::Generic(e)) + } +} + +impl ErrorDetail for CheckUniquenessError { + fn error_detail(&self) -> Result> { + Ok(crate::err_detail!(self)) + } +} + +impl ErrorDetail for TxProcessingError { + fn error_detail(&self) -> Result> { + match self { + TxProcessingError::CheckUniquenessFailed(inner) => inner.error_detail(), + _ => Ok(crate::err_detail!({"error": format!("{:?}", self)})), + } + } +} + /// The transaction processing error. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, thiserror::Error)] #[serde(rename_all = "snake_case")] @@ -83,7 +172,7 @@ pub enum TxProcessingError { AuthenticationFailed(String), /// The uniqueness check failed. #[error("The uniqueness check failed. Reason: {0}.")] - CheckUniquenessFailed(String), + CheckUniquenessFailed(CheckUniquenessError), /// Impossible to reserve gas for the transaction to be executed. #[error("Impossible to reserve gas for the transaction to be executed, reason: {0}.")] CannotReserveGas(String), diff --git a/crates/module-system/sov-modules-stf-blueprint/src/sequencer_mode/registered.rs b/crates/module-system/sov-modules-stf-blueprint/src/sequencer_mode/registered.rs index 10bae1312b..36bdf79705 100644 --- a/crates/module-system/sov-modules-stf-blueprint/src/sequencer_mode/registered.rs +++ b/crates/module-system/sov-modules-stf-blueprint/src/sequencer_mode/registered.rs @@ -232,10 +232,7 @@ where ) { let (scratchpad, pre_exec_gas_meter) = pre_exec_working_set.revert(); return ( - Err(( - TxProcessingError::CheckUniquenessFailed(err.to_string()), - raw_tx, - )), + Err((TxProcessingError::CheckUniquenessFailed(err), raw_tx)), scratchpad, pre_exec_gas_meter, ); diff --git a/crates/module-system/sov-modules-stf-blueprint/src/sequencer_mode/unregistered.rs b/crates/module-system/sov-modules-stf-blueprint/src/sequencer_mode/unregistered.rs index e02061fd1d..746984e573 100644 --- a/crates/module-system/sov-modules-stf-blueprint/src/sequencer_mode/unregistered.rs +++ b/crates/module-system/sov-modules-stf-blueprint/src/sequencer_mode/unregistered.rs @@ -59,7 +59,7 @@ pub fn process_unauthorized_tx>( ) { let (scratchpad, pre_exec_gas_meter) = pre_exec_working_set.revert(); return ( - Err(TxProcessingError::CheckUniquenessFailed(e.to_string())), + Err(TxProcessingError::CheckUniquenessFailed(e)), scratchpad.commit(), pre_exec_gas_meter, );