Skip to content

Commit b211a5d

Browse files
committed
add pruning of generations
1 parent 65f8994 commit b211a5d

File tree

8 files changed

+594
-9
lines changed

8 files changed

+594
-9
lines changed

Cargo.lock

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

crates/module-system/module-implementations/sov-chain-state/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,14 @@ impl<S: Spec> ChainState<S> {
553553
.unwrap_infallible();
554554
}
555555

556+
/// Returns the admin address, if one has been set.
557+
pub fn admin_address<Accessor: StateReader<User>>(
558+
&self,
559+
state: &mut Accessor,
560+
) -> Result<Option<S::Address>, Accessor::Error> {
561+
self.admin_address.get(state)
562+
}
563+
556564
/// Returns the current operating mode of the rollup.
557565
pub fn operating_mode<Accessor: StateReader<User>>(
558566
&self,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ borsh = { workspace = true, features = ["rc"] }
1616
schemars = { workspace = true }
1717
serde = { workspace = true }
1818

19+
serde_json = { workspace = true }
20+
sov-chain-state = { workspace = true }
1921
sov-modules-api = { workspace = true }
2022
sov-state = { workspace = true }
23+
strum = { workspace = true }
24+
thiserror = { workspace = true }
2125

2226
[dev-dependencies]
2327
alloy-consensus = { workspace = true }
@@ -57,6 +61,7 @@ arbitrary = [
5761
]
5862
native = [
5963
"reth-primitives/std",
64+
"sov-chain-state/native",
6065
"sov-modules-api/native",
6166
"sov-uniqueness/native",
6267
"sov-state/native",
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use schemars::JsonSchema;
2+
use sov_modules_api::macros::{serialize, UniversalWallet};
3+
use sov_modules_api::{Context, CoreModuleError, CredentialId, Spec, TxState};
4+
use strum::{EnumDiscriminants, EnumIs, VariantArray};
5+
6+
use crate::error::{PruneGenerationsError, PruneSelfGenerationsError};
7+
use crate::Uniqueness;
8+
9+
/// The available call messages for the `sov-uniqueness` module.
10+
#[derive(Debug, PartialEq, Eq, Clone, JsonSchema, EnumDiscriminants, EnumIs, UniversalWallet)]
11+
#[serialize(Borsh, Serde)]
12+
#[schemars(rename = "CallMessage")]
13+
#[strum_discriminants(derive(VariantArray, EnumIs))]
14+
#[serde(rename_all = "snake_case")]
15+
pub enum CallMessage {
16+
/// Prune generation deduplication data for the specified credentials.
17+
/// Only callable by the chain state admin.
18+
PruneGenerations {
19+
/// The credential IDs whose generation data should be deleted.
20+
credential_ids: Vec<CredentialId>,
21+
},
22+
/// Prune the caller's own generation deduplication data.
23+
/// The caller must provide their credential ID, and the module verifies
24+
/// that it derives to the sender's address.
25+
PruneSelfGenerations {
26+
/// The caller's credential ID whose generation data should be deleted.
27+
credential_id: CredentialId,
28+
},
29+
}
30+
31+
impl<S: Spec> Uniqueness<S> {
32+
/// Prune generation deduplication data for the specified credentials.
33+
///
34+
/// Only the chain state admin is authorized to call this method.
35+
/// Each credential's entire generation entry is removed from state.
36+
pub(crate) fn prune_generations(
37+
&mut self,
38+
credential_ids: &[CredentialId],
39+
context: &Context<S>,
40+
state: &mut impl TxState<S>,
41+
) -> Result<(), PruneGenerationsError> {
42+
let admin = self
43+
.chain_state
44+
.admin_address(state)
45+
.map_err(CoreModuleError::state_read)?
46+
.ok_or(PruneGenerationsError::NoAdmin)?;
47+
48+
if context.sender() != &admin {
49+
return Err(PruneGenerationsError::NotAdmin {
50+
admin: admin.to_string(),
51+
sender: context.sender().to_string(),
52+
});
53+
}
54+
55+
for credential_id in credential_ids {
56+
self.generations
57+
.delete(credential_id, state)
58+
.map_err(CoreModuleError::state_write)?;
59+
}
60+
61+
Ok(())
62+
}
63+
64+
/// Prune the caller's own generation deduplication data.
65+
///
66+
/// The caller provides their credential ID, and the module verifies that
67+
/// `S::Address::from(credential_id) == context.sender()` before deleting.
68+
pub(crate) fn prune_self_generations(
69+
&mut self,
70+
credential_id: &CredentialId,
71+
context: &Context<S>,
72+
state: &mut impl TxState<S>,
73+
) -> Result<(), PruneSelfGenerationsError> {
74+
let expected_sender = S::Address::from(*credential_id);
75+
if *context.sender() != expected_sender {
76+
return Err(PruneSelfGenerationsError::NotOwner {
77+
credential_id: credential_id.to_string(),
78+
expected_sender: expected_sender.to_string(),
79+
actual_sender: context.sender().to_string(),
80+
});
81+
}
82+
83+
self.generations
84+
.delete(credential_id, state)
85+
.map_err(CoreModuleError::state_write)?;
86+
87+
Ok(())
88+
}
89+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use sov_modules_api::{err_detail, CoreModuleError, ErrorContext, ErrorDetail};
2+
3+
/// Errors that can occur when pruning generation data (admin operation).
4+
#[derive(Debug, thiserror::Error, serde::Serialize)]
5+
#[serde(tag = "error_code", rename_all = "snake_case")]
6+
pub enum PruneGenerationsError {
7+
/// A core module error occurred (e.g. state read/write failure).
8+
#[error(transparent)]
9+
CoreModuleError(#[from] CoreModuleError),
10+
/// The sender is not the chain state admin.
11+
#[error("Only the chain state admin can prune generations. Admin: {admin}, sender: {sender}")]
12+
NotAdmin {
13+
/// The expected admin address.
14+
admin: String,
15+
/// The actual sender address.
16+
sender: String,
17+
},
18+
/// No admin address has been configured in chain state.
19+
#[error("No admin address configured in chain state")]
20+
NoAdmin,
21+
}
22+
23+
/// Errors that can occur when pruning own generation data (self-service operation).
24+
#[derive(Debug, thiserror::Error, serde::Serialize)]
25+
#[serde(tag = "error_code", rename_all = "snake_case")]
26+
pub enum PruneSelfGenerationsError {
27+
/// A core module error occurred (e.g. state read/write failure).
28+
#[error(transparent)]
29+
CoreModuleError(#[from] CoreModuleError),
30+
/// The provided credential ID does not derive to the sender's address.
31+
#[error("Credential {credential_id} does not belong to sender. Expected sender: {expected_sender}, actual: {actual_sender}")]
32+
NotOwner {
33+
/// The credential ID that was provided.
34+
credential_id: String,
35+
/// The address derived from the credential ID.
36+
expected_sender: String,
37+
/// The actual sender address.
38+
actual_sender: String,
39+
},
40+
}
41+
42+
/// The top-level error type for all uniqueness module operations.
43+
#[derive(Debug, thiserror::Error, serde::Serialize)]
44+
#[serde(tag = "call", rename_all = "snake_case")]
45+
pub enum Error {
46+
/// An error occurred during admin generation pruning.
47+
#[error("Prune generations error: {0}")]
48+
PruneGenerations(#[from] PruneGenerationsError),
49+
/// An error occurred during self generation pruning.
50+
#[error("Prune self generations error: {0}")]
51+
PruneSelfGenerations(#[from] PruneSelfGenerationsError),
52+
}
53+
54+
impl ErrorDetail for Error {
55+
fn error_detail(&self) -> Result<ErrorContext, Box<dyn std::error::Error + Send + Sync>> {
56+
Ok(err_detail!(self))
57+
}
58+
}
59+
60+
impl From<anyhow::Error> for Error {
61+
fn from(err: anyhow::Error) -> Self {
62+
PruneGenerationsError::from(CoreModuleError::Generic(err)).into()
63+
}
64+
}

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

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
#![deny(missing_docs)]
22
#![doc = include_str!("../README.md")]
3+
/// Contains the call methods used by the module.
4+
mod call;
35
mod capabilities;
6+
/// Error types for the module.
7+
pub mod error;
48
mod generations;
59
mod nonces;
610
use std::collections::{BTreeMap, HashSet};
711

12+
pub use call::*;
813
use sov_modules_api::{
9-
Context, CredentialId, DaSpec, GenesisState, Module, ModuleId, ModuleInfo, ModuleRestApi,
10-
NotInstantiable, Spec, StateMap, StateReader, TxHash, TxState,
14+
Context, CredentialId, DaSpec, GenesisState, Module, ModuleId, ModuleInfo, ModuleRestApi, Spec,
15+
StateMap, StateReader, TxHash, TxState,
1116
};
1217
use sov_state::User;
1318

@@ -34,6 +39,10 @@ pub struct Uniqueness<S: Spec> {
3439
#[state]
3540
pub(crate) nonces: StateMap<CredentialId, u64>,
3641

42+
/// Reference to the chain state module for admin address lookup.
43+
#[module]
44+
pub(crate) chain_state: sov_chain_state::ChainState<S>,
45+
3746
#[phantom]
3847
phantom: std::marker::PhantomData<S>,
3948
}
@@ -100,27 +109,34 @@ impl<S: Spec> Module for Uniqueness<S> {
100109

101110
type Config = ();
102111

103-
type CallMessage = NotInstantiable;
112+
type CallMessage = call::CallMessage;
104113

105114
type Event = ();
106115

107-
type Error = anyhow::Error;
116+
type Error = error::Error;
108117

109118
fn genesis(
110119
&mut self,
111120
_genesis_rollup_header: &<<S as Spec>::Da as DaSpec>::BlockHeader,
112121
_config: &Self::Config,
113122
_state: &mut impl GenesisState<S>,
114-
) -> Result<(), Self::Error> {
123+
) -> anyhow::Result<()> {
115124
Ok(())
116125
}
117126

118127
fn call(
119128
&mut self,
120-
_msg: Self::CallMessage,
121-
_context: &Context<S>,
122-
_state: &mut impl TxState<S>,
129+
msg: Self::CallMessage,
130+
context: &Context<S>,
131+
state: &mut impl TxState<S>,
123132
) -> Result<(), Self::Error> {
124-
unreachable!()
133+
match msg {
134+
CallMessage::PruneGenerations { credential_ids } => {
135+
Ok(self.prune_generations(&credential_ids, context, state)?)
136+
}
137+
CallMessage::PruneSelfGenerations { credential_id } => {
138+
Ok(self.prune_self_generations(&credential_id, context, state)?)
139+
}
140+
}
125141
}
126142
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod call_tests;
22
mod hooks_tests;
3+
mod prune_tests;
34
mod runtime;
45
pub mod utils;

0 commit comments

Comments
 (0)