diff --git a/Cargo.lock b/Cargo.lock index 1d3af58dae..829d3216af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12671,6 +12671,7 @@ dependencies = [ "secp256k1 0.30.0", "serde", "sov-address", + "sov-chain-state", "sov-eth-dev-signer", "sov-evm", "sov-evm-test-utils", diff --git a/crates/full-node/sov-sequencer/src/preferred/mod.rs b/crates/full-node/sov-sequencer/src/preferred/mod.rs index fa5d019bc0..54e1b33638 100644 --- a/crates/full-node/sov-sequencer/src/preferred/mod.rs +++ b/crates/full-node/sov-sequencer/src/preferred/mod.rs @@ -424,7 +424,9 @@ where } let (outer_res, nonce_to_mark_persisted) = match uniqueness { - UniquenessData::Generation(_) => ( + UniquenessData::Generation(_) + | UniquenessData::Timestamp(_) + | UniquenessData::TimestampNonce(_) => ( self.synchronized_state_updator .accept_tx_msg( &baked_tx, diff --git a/crates/module-system/module-implementations/sov-uniqueness/Cargo.toml b/crates/module-system/module-implementations/sov-uniqueness/Cargo.toml index 01b39c77bf..9a7e0c57b1 100644 --- a/crates/module-system/module-implementations/sov-uniqueness/Cargo.toml +++ b/crates/module-system/module-implementations/sov-uniqueness/Cargo.toml @@ -18,6 +18,7 @@ serde = { workspace = true } sov-modules-api = { workspace = true } sov-state = { workspace = true } +sov-chain-state = { workspace = true } [dev-dependencies] alloy-consensus = { workspace = true } 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..6ac08d393c 100644 --- a/crates/module-system/module-implementations/sov-uniqueness/src/capabilities.rs +++ b/crates/module-system/module-implementations/sov-uniqueness/src/capabilities.rs @@ -1,7 +1,6 @@ use sov_modules_api::capabilities::UniquenessData; use sov_modules_api::ExecutionContext; -use sov_modules_api::{CredentialId, Spec, StateAccessor, StateReader, TxHash}; -use sov_state::User; +use sov_modules_api::{CredentialId, Spec, TimeStateAccessor, TxHash}; use crate::Uniqueness; @@ -19,7 +18,7 @@ impl Uniqueness { transaction_uniqueness: UniquenessData, transaction_hash: TxHash, execution_context: &ExecutionContext, - state: &mut impl StateReader, + state: &mut impl TimeStateAccessor, ) -> anyhow::Result<()> { match transaction_uniqueness { UniquenessData::Nonce(nonce) => match execution_context { @@ -31,6 +30,9 @@ impl Uniqueness { UniquenessData::Generation(generation) => { self.check_generation_uniqueness(credential_id, generation, transaction_hash, state) } + UniquenessData::Timestamp(ts) | UniquenessData::TimestampNonce(ts) => { + self.check_timestamp_uniqueness(credential_id, ts, transaction_hash, state) + } } } @@ -43,7 +45,7 @@ impl Uniqueness { credential_id: &CredentialId, transaction_generation: UniquenessData, transaction_hash: TxHash, - state: &mut impl StateAccessor, + state: &mut impl TimeStateAccessor, ) -> anyhow::Result<()> { match transaction_generation { UniquenessData::Nonce(_) => self.mark_nonce_tx_attempted(credential_id, state), @@ -53,6 +55,12 @@ impl Uniqueness { transaction_hash, state, ), + UniquenessData::Timestamp(ts) => { + self.mark_timestamp_tx_attempted(None, ts, transaction_hash, state) + } + UniquenessData::TimestampNonce(ts) => { + self.mark_timestamp_tx_attempted(Some(credential_id), ts, transaction_hash, state) + } } } } diff --git a/crates/module-system/module-implementations/sov-uniqueness/src/lib.rs b/crates/module-system/module-implementations/sov-uniqueness/src/lib.rs index 97e13b29a2..77f6b3f858 100644 --- a/crates/module-system/module-implementations/sov-uniqueness/src/lib.rs +++ b/crates/module-system/module-implementations/sov-uniqueness/src/lib.rs @@ -3,6 +3,7 @@ mod capabilities; mod generations; mod nonces; +mod timestamp; use std::collections::{BTreeMap, HashSet}; use sov_modules_api::{ @@ -34,6 +35,14 @@ pub struct Uniqueness { #[state] pub(crate) nonces: StateMap, + /// Buckets of transactions with their expiry timestamp. The + /// buckets are taken from the first bytes of the TxHash. + #[state] + pub(crate) timestamps: StateMap>, + + #[module] + pub(crate) chain_state: sov_chain_state::ChainState, + #[phantom] phantom: std::marker::PhantomData, } diff --git a/crates/module-system/module-implementations/sov-uniqueness/src/timestamp.rs b/crates/module-system/module-implementations/sov-uniqueness/src/timestamp.rs new file mode 100644 index 0000000000..88de9bb36f --- /dev/null +++ b/crates/module-system/module-implementations/sov-uniqueness/src/timestamp.rs @@ -0,0 +1,63 @@ +use sov_modules_api::{CredentialId, Spec, TimeStateAccessor, TxHash}; + +use crate::Uniqueness; + +impl Uniqueness { + pub(crate) fn check_timestamp_uniqueness( + &self, + credential_id: &CredentialId, + expires: u64, + transaction_hash: TxHash, + state: &mut impl TimeStateAccessor, + ) -> anyhow::Result<()> { + let current_time = self.chain_state.get_oracle_time_nanos(state)? / 1000; + anyhow::ensure!( + expires as u128 > current_time, + "Time-expired transaction for credential_id {credential_id}: hash {transaction_hash:}" + ); + + anyhow::ensure!((expires as u128) < current_time + 60_000_000, + "Future transaction for credential_id {credential_id}: hash {transaction_hash:} needs to wait." + ); + anyhow::ensure!( + expires > self.nonces.get(credential_id, state)?.unwrap_or_default(), + "Nonce-expired transaction for credential_id {credential_id}: hash {transaction_hash:} is invalidated" + ); + + let bucket = self + .timestamps + .get(&Self::hash_to_bucket(&transaction_hash), state)? + .unwrap_or_default(); + + if bucket.iter().any(|(tx, _)| transaction_hash == *tx) { + return Err(anyhow::anyhow!("Duplicate transaction for credential_id {credential_id}: hash {transaction_hash:} has already been seen")); + } + Ok(()) + } + + pub(crate) fn mark_timestamp_tx_attempted( + &mut self, + credential_id: Option<&CredentialId>, + expires: u64, + transaction_hash: TxHash, + state: &mut impl TimeStateAccessor, + ) -> anyhow::Result<()> { + let bucket = Self::hash_to_bucket(&transaction_hash); + let current_time = self.chain_state.get_oracle_time_nanos(state)? / 1000; + + if let Some(credential_id) = credential_id { + self.nonces.set(credential_id, &expires, state)?; + } + + let mut data = self.timestamps.get(&bucket, state)?.unwrap_or_default(); + data.retain(|(_, ts)| *ts as u128 > current_time); + data.push((transaction_hash, expires)); + Ok(self.timestamps.set(&bucket, &data, state)?) + } + + /// Use the first two bytes as bucket value. + fn hash_to_bucket(transaction_hash: &TxHash) -> u16 { + let ptr: &[u8] = transaction_hash.as_ref(); + unsafe { core::ptr::read_unaligned(ptr.as_ptr() as *const u16) } + } +} 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..d7e40644df 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,3 +1,4 @@ +use sov_modules_api::capabilities::UniquenessData; use sov_modules_api::macros::config_value; use sov_modules_api::{CredentialId, TxEffect}; use sov_test_utils::{BatchType, SlotInput, TransactionTestCase, TxProcessingError}; @@ -58,14 +59,18 @@ fn do_max_stored_tx_hashes_per_credential_test() { // Generate txs to fill up our "bucket" of stored transaction hashes. for i in 0..txs_per_generation { for generation in 0..num_generations { - txs.push(generate_value_setter_tx(generation, i as u32, &admin)); + txs.push(generate_value_setter_tx( + UniquenessData::Generation(generation), + i as u32, + &admin, + )); } } // We divided txs evenly across generations - if there was a remainder, account for it by putting the // extra txs in the first bucket. for i in 0..extra_txs_in_first_generation { txs.push(generate_value_setter_tx( - 0, + UniquenessData::Generation(0), (i + txs_per_generation) as u32, &admin, )) @@ -87,7 +92,7 @@ fn do_max_stored_tx_hashes_per_credential_test() { // Send one more transaction with a current generation number. // This transaction should be skipped because it would cause the bucket to overflow. runner.execute_transaction(TransactionTestCase { - input: generate_value_setter_tx(0, u32::MAX, &admin), + input: generate_value_setter_tx(UniquenessData::Generation(0), u32::MAX, &admin), assert: Box::new(move |ctx, _| { let TxEffect::Skipped(skipped) = ctx.tx_receipt else { panic!("Transaction should be skipped"); @@ -106,7 +111,11 @@ fn do_max_stored_tx_hashes_per_credential_test() { // Increment the generation number. Now the transaction should be accepted because it won't cause the bucket to overflow. // Note that we need to add 1 to the number of generations because we have a strict inequality comparison for buckets. runner.execute_transaction(TransactionTestCase { - input: generate_value_setter_tx(num_generations + 1, txs_per_generation as u32, &admin), + input: generate_value_setter_tx( + UniquenessData::Generation(num_generations + 1), + txs_per_generation as u32, + &admin, + ), assert: Box::new(move |ctx, _| { assert!( ctx.tx_receipt.is_successful(), diff --git a/crates/module-system/module-implementations/sov-uniqueness/tests/integration/utils.rs b/crates/module-system/module-implementations/sov-uniqueness/tests/integration/utils.rs index dbf6e301fb..4f9407e04f 100644 --- a/crates/module-system/module-implementations/sov-uniqueness/tests/integration/utils.rs +++ b/crates/module-system/module-implementations/sov-uniqueness/tests/integration/utils.rs @@ -79,12 +79,12 @@ pub(crate) fn generate_default_tx( create_contract_tx, )) } - UniquenessData::Generation(generation) => generate_value_setter_tx(generation, 10, admin), + x => generate_value_setter_tx(x, 10, admin), } } pub(crate) fn generate_value_setter_tx( - generation: u64, + nonce: UniquenessData, value: u32, admin: &TestUser, ) -> TransactionType { @@ -99,7 +99,7 @@ pub(crate) fn generate_value_setter_tx( config_chain_id(), TEST_DEFAULT_MAX_PRIORITY_FEE, TEST_DEFAULT_MAX_FEE, - UniquenessData::Generation(generation), + nonce, None, ); diff --git a/crates/module-system/sov-capabilities/src/lib.rs b/crates/module-system/sov-capabilities/src/lib.rs index 1a45676e83..a37d4f2454 100644 --- a/crates/module-system/sov-capabilities/src/lib.rs +++ b/crates/module-system/sov-capabilities/src/lib.rs @@ -20,7 +20,8 @@ use sov_modules_api::SequencerType; use sov_modules_api::{ AggregatedProofPublicData, Amount, Context, DaSpec, Gas, GetGasPrice, InfallibleStateAccessor, InvalidProofError, ModuleInfo, OperatingMode, Rewards, SovAttestation, - SovStateTransitionPublicData, Spec, StateAccessor, StateReader, StateWriter, Storage, TxState, + SovStateTransitionPublicData, Spec, StateAccessor, StateReader, StateWriter, Storage, + TimeStateAccessor, TxState, }; use sov_rollup_interface::common::SlotNumber; use sov_rollup_interface::zk::aggregated_proof::SerializedAggregatedProof; @@ -255,7 +256,7 @@ impl TransactionAuthorizer for StandardProvenRollupCapabilities<' auth_data: &AuthorizationData, _context: &Context, execution_context: &ExecutionContext, - state: &mut impl StateReader, + state: &mut impl TimeStateAccessor, ) -> anyhow::Result<()> { self.uniqueness.check_uniqueness( &auth_data.credential_id, @@ -271,7 +272,7 @@ impl TransactionAuthorizer for StandardProvenRollupCapabilities<' &mut self, auth_data: &AuthorizationData, _sequencer: &::Address, - state: &mut impl StateAccessor, + state: &mut impl TimeStateAccessor, ) -> anyhow::Result<()> { self.uniqueness.mark_tx_attempted( &auth_data.credential_id, 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..9863d9fed9 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::{Context, SequencerType, Spec, StateAccessor, TimeStateAccessor}; /// Authorizes transactions to be executed. pub trait TransactionAuthorizer { @@ -42,7 +42,7 @@ pub trait TransactionAuthorizer { auth_data: &AuthorizationData, context: &Context, execution_context: &ExecutionContext, - state: &mut impl StateAccessor, + state: &mut impl TimeStateAccessor, ) -> anyhow::Result<()>; /// Marks a transaction as having been executed, preventing it from executing again. @@ -50,7 +50,7 @@ pub trait TransactionAuthorizer { &mut self, auth_data: &AuthorizationData, sequencer: &<::Da as DaSpec>::Address, - state: &mut impl StateAccessor, + state: &mut impl TimeStateAccessor, ) -> anyhow::Result<()>; } @@ -76,6 +76,12 @@ pub enum UniquenessData { /// Transactions older than this buffer are invalid, transactions falling within it or with a /// higher generation are valid but must have a unique hash within their generation Generation(u64), + /// Timestamp-based uniqueness: a transaction expires at the point given in microseconds of + /// OracleTime. + Timestamp(u64), + /// Timestamp-based uniqueness: a transaction expires at the point given in microseconds of + /// OracleTime but it also updates the nonce so that older transactions get invalidated. + TimestampNonce(u64), } /// Data required to authorize a sov-transaction. diff --git a/crates/module-system/sov-modules-api/src/state/accessors/access_controls.rs b/crates/module-system/sov-modules-api/src/state/accessors/access_controls.rs index 53f77e4974..f9a651289f 100644 --- a/crates/module-system/sov-modules-api/src/state/accessors/access_controls.rs +++ b/crates/module-system/sov-modules-api/src/state/accessors/access_controls.rs @@ -180,6 +180,7 @@ impl> StateWriter for TxScratchpad { } impl> ProvableStateReader for PreExecWorkingSet {} +impl> ProvableStateReader for PreExecWorkingSet {} impl> ProvableStateWriter for PreExecWorkingSet {} impl> ProvableStateReader for WorkingSet {} diff --git a/crates/module-system/sov-modules-api/src/state/mod.rs b/crates/module-system/sov-modules-api/src/state/mod.rs index 4ba3bf635d..e30e51bc32 100644 --- a/crates/module-system/sov-modules-api/src/state/mod.rs +++ b/crates/module-system/sov-modules-api/src/state/mod.rs @@ -25,7 +25,7 @@ pub use traits::{ GenesisState, InfallibleKernelStateAccessor, InfallibleStateAccessor, InfallibleStateReaderAndWriter, PerBlockCache, PrivilegedKernelAccessor, ProvableStateReader, ProvableStateWriter, StateAccessor, StateAccessorError, StateReader, StateReaderAndWriter, - StateWriter, TxState, VersionReader, + StateWriter, TimeStateAccessor, TxState, VersionReader, }; #[cfg(feature = "native")] diff --git a/crates/module-system/sov-modules-api/src/state/traits.rs b/crates/module-system/sov-modules-api/src/state/traits.rs index 5ba367eb50..831506d3bf 100644 --- a/crates/module-system/sov-modules-api/src/state/traits.rs +++ b/crates/module-system/sov-modules-api/src/state/traits.rs @@ -49,6 +49,24 @@ pub trait StateAccessor: StateReaderAndWriter { } } +/// Trait to access time through sov_chain_state. +pub trait TimeStateAccessor: + StateReader> + + StateWriter>::Error> + + StateReader>::Error> + + VersionReader +{ +} + +/// Auto implement trait. +impl TimeStateAccessor for T where + T: StateReader> + + StateWriter>::Error> + + StateReader>::Error> + + VersionReader +{ +} + /// A trait that represents a [`StateAccessor`] that never fails on state accesses. Accessing the state with structs that implement /// this trait will return [`Infallible`]. ///