Skip to content

Commit e63e5d0

Browse files
committed
feat: time-based uniqueness
1 parent d22e4ba commit e63e5d0

File tree

11 files changed

+123
-12
lines changed

11 files changed

+123
-12
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/full-node/sov-sequencer/src/preferred/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,9 @@ where
424424
}
425425

426426
let (outer_res, nonce_to_mark_persisted) = match uniqueness {
427-
UniquenessData::Generation(_) => (
427+
UniquenessData::Generation(_)
428+
| UniquenessData::Timestamp(_)
429+
| UniquenessData::TimestampNonce(_) => (
428430
self.synchronized_state_updator
429431
.accept_tx_msg(
430432
&baked_tx,

crates/module-system/module-implementations/sov-uniqueness/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ serde = { workspace = true }
1818

1919
sov-modules-api = { workspace = true }
2020
sov-state = { workspace = true }
21+
sov-chain-state = { workspace = true }
2122

2223
[dev-dependencies]
2324
alloy-consensus = { workspace = true }

crates/module-system/module-implementations/sov-uniqueness/src/capabilities.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use sov_modules_api::capabilities::UniquenessData;
22
use sov_modules_api::ExecutionContext;
3-
use sov_modules_api::{CredentialId, Spec, StateAccessor, StateReader, TxHash};
4-
use sov_state::User;
3+
use sov_modules_api::{CredentialId, Spec, TimeStateAccessor, TxHash};
54

65
use crate::Uniqueness;
76

@@ -19,7 +18,7 @@ impl<S: Spec> Uniqueness<S> {
1918
transaction_uniqueness: UniquenessData,
2019
transaction_hash: TxHash,
2120
execution_context: &ExecutionContext,
22-
state: &mut impl StateReader<User>,
21+
state: &mut impl TimeStateAccessor,
2322
) -> anyhow::Result<()> {
2423
match transaction_uniqueness {
2524
UniquenessData::Nonce(nonce) => match execution_context {
@@ -31,6 +30,9 @@ impl<S: Spec> Uniqueness<S> {
3130
UniquenessData::Generation(generation) => {
3231
self.check_generation_uniqueness(credential_id, generation, transaction_hash, state)
3332
}
33+
UniquenessData::Timestamp(ts) | UniquenessData::TimestampNonce(ts) => {
34+
self.check_timestamp_uniqueness(credential_id, ts, transaction_hash, state)
35+
}
3436
}
3537
}
3638

@@ -43,7 +45,7 @@ impl<S: Spec> Uniqueness<S> {
4345
credential_id: &CredentialId,
4446
transaction_generation: UniquenessData,
4547
transaction_hash: TxHash,
46-
state: &mut impl StateAccessor,
48+
state: &mut impl TimeStateAccessor,
4749
) -> anyhow::Result<()> {
4850
match transaction_generation {
4951
UniquenessData::Nonce(_) => self.mark_nonce_tx_attempted(credential_id, state),
@@ -53,6 +55,12 @@ impl<S: Spec> Uniqueness<S> {
5355
transaction_hash,
5456
state,
5557
),
58+
UniquenessData::Timestamp(ts) => {
59+
self.mark_timestamp_tx_attempted(None, ts, transaction_hash, state)
60+
}
61+
UniquenessData::TimestampNonce(ts) => {
62+
self.mark_timestamp_tx_attempted(Some(credential_id), ts, transaction_hash, state)
63+
}
5664
}
5765
}
5866
}

crates/module-system/module-implementations/sov-uniqueness/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
mod capabilities;
44
mod generations;
55
mod nonces;
6+
mod timestamp;
67
use std::collections::{BTreeMap, HashSet};
78

89
use sov_modules_api::{
@@ -34,6 +35,14 @@ pub struct Uniqueness<S: Spec> {
3435
#[state]
3536
pub(crate) nonces: StateMap<CredentialId, u64>,
3637

38+
/// Buckets of transactions with their expiry timestamp. The
39+
/// buckets are taken from the first bytes of the TxHash.
40+
#[state]
41+
pub(crate) timestamps: StateMap<u16, Vec<(TxHash, u64)>>,
42+
43+
#[module]
44+
pub(crate) chain_state: sov_chain_state::ChainState<S>,
45+
3746
#[phantom]
3847
phantom: std::marker::PhantomData<S>,
3948
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use sov_modules_api::{CredentialId, Spec, TimeStateAccessor, TxHash};
2+
3+
use crate::Uniqueness;
4+
5+
impl<S: Spec> Uniqueness<S> {
6+
pub(crate) fn check_timestamp_uniqueness(
7+
&self,
8+
credential_id: &CredentialId,
9+
expires: u64,
10+
transaction_hash: TxHash,
11+
state: &mut impl TimeStateAccessor,
12+
) -> anyhow::Result<()> {
13+
let current_time = self.chain_state.get_oracle_time_nanos(state)? / 1000;
14+
anyhow::ensure!(
15+
expires as u128 > current_time,
16+
"Time-expired transaction for credential_id {credential_id}: hash {transaction_hash:}"
17+
);
18+
19+
anyhow::ensure!(
20+
expires as u128 < current_time + 60_000_000,
21+
"Future transaction for credential_id {credential_id}: hash {transaction_hash:} needs to wait."
22+
);
23+
anyhow::ensure!(
24+
expires > self.nonces.get(credential_id, state)?.unwrap_or_default(),
25+
"Nonce-expired transaction for credential_id {credential_id}: hash {transaction_hash:} is invalidated"
26+
);
27+
28+
let bucket = self
29+
.timestamps
30+
.get(&Self::hash_to_bucket(&transaction_hash), state)?
31+
.unwrap_or_default();
32+
33+
if bucket.iter().any(|(tx, _)| transaction_hash == *tx) {
34+
return Err(anyhow::anyhow!("Duplicate transaction for credential_id {credential_id}: hash {transaction_hash:} has already been seen"));
35+
}
36+
Ok(())
37+
}
38+
39+
pub(crate) fn mark_timestamp_tx_attempted(
40+
&mut self,
41+
credential_id: Option<&CredentialId>,
42+
expires: u64,
43+
transaction_hash: TxHash,
44+
state: &mut impl TimeStateAccessor,
45+
) -> anyhow::Result<()> {
46+
let bucket = Self::hash_to_bucket(&transaction_hash);
47+
let current_time = self.chain_state.get_oracle_time_nanos(state)? / 1000;
48+
49+
if let Some(credential_id) = credential_id {
50+
self.nonces.set(credential_id, &expires, state)?;
51+
}
52+
53+
let mut data = self.timestamps.get(&bucket, state)?.unwrap_or_default();
54+
data.retain(|(_, ts)| *ts as u128 > current_time);
55+
data.push((transaction_hash, expires));
56+
Ok(self.timestamps.set(&bucket, &data, state)?)
57+
}
58+
59+
/// Use the first two bytes as bucket value.
60+
fn hash_to_bucket(transaction_hash: &TxHash) -> u16 {
61+
let ptr: &[u8] = transaction_hash.as_ref();
62+
unsafe { core::ptr::read_unaligned(ptr.as_ptr() as *const u16) }
63+
}
64+
}

crates/module-system/sov-capabilities/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ use sov_modules_api::SequencerType;
2020
use sov_modules_api::{
2121
AggregatedProofPublicData, Amount, Context, DaSpec, Gas, GetGasPrice, InfallibleStateAccessor,
2222
InvalidProofError, ModuleInfo, OperatingMode, Rewards, SovAttestation,
23-
SovStateTransitionPublicData, Spec, StateAccessor, StateReader, StateWriter, Storage, TxState,
23+
SovStateTransitionPublicData, Spec, StateAccessor, StateReader, StateWriter, Storage,
24+
TimeStateAccessor, TxState,
2425
};
2526
use sov_rollup_interface::common::SlotNumber;
2627
use sov_rollup_interface::zk::aggregated_proof::SerializedAggregatedProof;
@@ -255,7 +256,7 @@ impl<S: Spec, T> TransactionAuthorizer<S> for StandardProvenRollupCapabilities<'
255256
auth_data: &AuthorizationData<S>,
256257
_context: &Context<S>,
257258
execution_context: &ExecutionContext,
258-
state: &mut impl StateReader<User>,
259+
state: &mut impl TimeStateAccessor,
259260
) -> anyhow::Result<()> {
260261
self.uniqueness.check_uniqueness(
261262
&auth_data.credential_id,
@@ -271,7 +272,7 @@ impl<S: Spec, T> TransactionAuthorizer<S> for StandardProvenRollupCapabilities<'
271272
&mut self,
272273
auth_data: &AuthorizationData<S>,
273274
_sequencer: &<S::Da as DaSpec>::Address,
274-
state: &mut impl StateAccessor,
275+
state: &mut impl TimeStateAccessor,
275276
) -> anyhow::Result<()> {
276277
self.uniqueness.mark_tx_attempted(
277278
&auth_data.credential_id,

crates/module-system/sov-modules-api/src/runtime/capabilities/authorization.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use sov_rollup_interface::{Bytes, TxHash};
1111
use sov_universal_wallet::UniversalWallet;
1212

1313
use crate::transaction::Credentials;
14-
use crate::{Context, SequencerType, Spec, StateAccessor};
14+
use crate::{Context, SequencerType, Spec, StateAccessor, TimeStateAccessor};
1515

1616
/// Authorizes transactions to be executed.
1717
pub trait TransactionAuthorizer<S: Spec> {
@@ -42,15 +42,15 @@ pub trait TransactionAuthorizer<S: Spec> {
4242
auth_data: &AuthorizationData<S>,
4343
context: &Context<S>,
4444
execution_context: &ExecutionContext,
45-
state: &mut impl StateAccessor,
45+
state: &mut impl TimeStateAccessor,
4646
) -> anyhow::Result<()>;
4747

4848
/// Marks a transaction as having been executed, preventing it from executing again.
4949
fn mark_tx_attempted(
5050
&mut self,
5151
auth_data: &AuthorizationData<S>,
5252
sequencer: &<<S as Spec>::Da as DaSpec>::Address,
53-
state: &mut impl StateAccessor,
53+
state: &mut impl TimeStateAccessor,
5454
) -> anyhow::Result<()>;
5555
}
5656

@@ -76,6 +76,12 @@ pub enum UniquenessData {
7676
/// Transactions older than this buffer are invalid, transactions falling within it or with a
7777
/// higher generation are valid but must have a unique hash within their generation
7878
Generation(u64),
79+
/// Timestamp-based uniqueness: a transaction expires at the point given in microseconds of
80+
/// OracleTime.
81+
Timestamp(u64),
82+
/// Timestamp-based uniqueness: a transaction expires at the point given in microseconds of
83+
/// OracleTime but it also updates the nonce so that older transactions get invalidated.
84+
TimestampNonce(u64),
7985
}
8086

8187
/// Data required to authorize a sov-transaction.

crates/module-system/sov-modules-api/src/state/accessors/access_controls.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ impl<S: Spec, I: StateProvider<S>> StateWriter<User> for TxScratchpad<S, I> {
180180
}
181181

182182
impl<S: Spec, I: StateProvider<S>> ProvableStateReader<User> for PreExecWorkingSet<S, I> {}
183+
impl<S: Spec, I: StateProvider<S>> ProvableStateReader<Kernel> for PreExecWorkingSet<S, I> {}
183184
impl<S: Spec, I: StateProvider<S>> ProvableStateWriter<User> for PreExecWorkingSet<S, I> {}
184185

185186
impl<S: Spec, I: StateProvider<S>> ProvableStateReader<User> for WorkingSet<S, I> {}

crates/module-system/sov-modules-api/src/state/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub use traits::{
2525
GenesisState, InfallibleKernelStateAccessor, InfallibleStateAccessor,
2626
InfallibleStateReaderAndWriter, PerBlockCache, PrivilegedKernelAccessor, ProvableStateReader,
2727
ProvableStateWriter, StateAccessor, StateAccessorError, StateReader, StateReaderAndWriter,
28-
StateWriter, TxState, VersionReader,
28+
StateWriter, TimeStateAccessor, TxState, VersionReader,
2929
};
3030

3131
#[cfg(feature = "native")]

0 commit comments

Comments
 (0)