-
Notifications
You must be signed in to change notification settings - Fork 187
fix(sequencer): structured JSON errors for skipped transactions #2871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: nightly
Are you sure you want to change the base?
Changes from 6 commits
b09aafa
90751f9
7276682
4258118
f910f18
1858d9e
c32bdea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -982,19 +982,21 @@ fn err_invalid_nonce<S: Spec, Rt: Runtime<S>>( | |
| 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 receipt = TransactionReceipt { | ||
| tx_hash, | ||
| body_to_save: None, | ||
| events: Vec::new(), | ||
| receipt: sov_rollup_interface::stf::TxEffect::Skipped(SkippedTxContents { | ||
| gas_used: <S::Gas as Gas>::zero(), | ||
| error: TxProcessingError::CheckUniquenessFailed(error_msg), | ||
| error: TxProcessingError::CheckUniquenessFailed( | ||
| sov_modules_api::CheckUniquenessError::BadNonce { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be possible to include a |
||
| expected_nonce, | ||
| provided_nonce: tx_nonce, | ||
| }, | ||
| ), | ||
| }), | ||
| }; | ||
| Ok(Err(AcceptTxError::NewTxError(DoNewTxError::ExecutorError( | ||
|
|
@@ -1334,7 +1336,12 @@ mod tests { | |
| events: Vec::new(), | ||
| receipt: sov_rollup_interface::stf::TxEffect::Skipped(SkippedTxContents { | ||
| gas_used: <TestSpec as Spec>::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, | ||
| }, | ||
| ), | ||
| }), | ||
| }; | ||
| return Ok(Err(AcceptTxError::NewTxError(DoNewTxError::ExecutorError( | ||
|
|
@@ -1593,7 +1600,7 @@ mod tests { | |
| result.unwrap().unwrap().await.is_ok(), | ||
| "Expected tx {i} to succeed" | ||
| ), | ||
| Outcome::Err(reason) => { | ||
| Outcome::Err(_reason) => { | ||
| let inner = result.expect("Expected Ok from TransactionReceiverResult"); | ||
| let err = | ||
| inner.expect_err(&format!("Expected tx {i} to fail with nonce error")); | ||
|
|
@@ -1606,28 +1613,10 @@ 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}\"", | ||
| ); | ||
| } | ||
| other => panic!("Tx {i}: Expected CheckUniquenessFailed error, got: {other:?}"), | ||
| TxProcessingError::CheckUniquenessFailed( | ||
| sov_modules_api::CheckUniquenessError::BadNonce { .. }, | ||
| ) => {} | ||
| other => panic!("Tx {i}: Expected CheckUniquenessFailed(BadNonce), got: {other:?}"), | ||
| } | ||
| } | ||
| other => panic!("Tx {i}: Expected Skipped receipt, got: {other:?}"), | ||
|
|
@@ -1691,24 +1680,25 @@ 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, | ||
| AcceptTxError::NewTxError(DoNewTxError::ExecutorError( | ||
| 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:?}" | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| use sov_modules_api::{CredentialId, Spec, StateAccessor, StateReader}; | ||
| use sov_modules_api::{CheckUniquenessError, CredentialId, Spec, StateAccessor, StateReader}; | ||
| use sov_state::User; | ||
|
|
||
| use crate::Uniqueness; | ||
|
|
@@ -8,13 +8,19 @@ impl<S: Spec> Uniqueness<S> { | |
| credential_id: &CredentialId, | ||
| transaction_nonce: u64, | ||
| state: &mut impl StateReader<User>, | ||
| ) -> anyhow::Result<()> { | ||
| let nonce = self.nonces.get(credential_id, state)?.unwrap_or_default(); | ||
|
|
||
| anyhow::ensure!( | ||
| nonce == transaction_nonce, | ||
| "Tx bad nonce for credential id: {credential_id}, expected: {nonce}, but found: {transaction_nonce}", | ||
| ); | ||
| ) -> Result<(), CheckUniquenessError> { | ||
| let nonce = self | ||
| .nonces | ||
| .get(credential_id, state) | ||
| .map_err(|e| CheckUniquenessError::Internal(e.to_string()))? | ||
| .unwrap_or_default(); | ||
|
|
||
| if nonce != transaction_nonce { | ||
| return Err(CheckUniquenessError::BadNonce { | ||
| expected_nonce: nonce, | ||
| provided_nonce: transaction_nonce, | ||
| }); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
@@ -24,13 +30,19 @@ impl<S: Spec> Uniqueness<S> { | |
| credential_id: &CredentialId, | ||
| transaction_nonce: u64, | ||
| state: &mut impl StateReader<User>, | ||
| ) -> anyhow::Result<()> { | ||
| let nonce = self.nonces.get(credential_id, state)?.unwrap_or_default(); | ||
|
|
||
| anyhow::ensure!( | ||
| nonce <= transaction_nonce, | ||
| "Tx bad nonce for credential id: {credential_id}, expected at least: {nonce}, but found: {transaction_nonce}", | ||
| ); | ||
| ) -> Result<(), CheckUniquenessError> { | ||
| let nonce = self | ||
| .nonces | ||
| .get(credential_id, state) | ||
| .map_err(|e| CheckUniquenessError::Internal(e.to_string()))? | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have a And we add a |
||
| .unwrap_or_default(); | ||
|
|
||
| if nonce > transaction_nonce { | ||
| return Err(CheckUniquenessError::NonceTooLow { | ||
| minimum_nonce: nonce, | ||
| provided_nonce: transaction_nonce, | ||
| }); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets impl
ErrorDetailonTxProcessingError&CheckUniquenessError, then here we do something similar to reverted:In the impl on
TxProcessingErrorfor theCheckUniquenessFailedvariant we delegate toinner.error_detail()for all other variants fall back tojson_obj!({"error": format!("{:?}", receipt)})Then we have everything in place to gradually migrate the other variants on
TxProcessingErrorto structured errors