Skip to main content

csv_adapter_core/
state_store.rs

1//! State History Store
2//!
3//! Stores the full state history for contracts, enabling client-side validation.
4//! The client stores:
5//! - All commitments from genesis to present
6//! - All state transitions
7//! - All seal assignments and their lifecycle
8//! - Anchors and their inclusion proofs
9//!
10//! This allows the client to verify the complete history without
11//! re-fetching everything from the chain on every validation.
12
13use alloc::collections::BTreeMap;
14use alloc::vec::Vec;
15
16use crate::commitment::Commitment;
17use crate::hash::Hash;
18use crate::right::Right;
19use crate::seal::SealRef;
20
21/// A recorded state transition in the contract history.
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct StateTransitionRecord {
24    /// The commitment that resulted from this transition
25    pub commitment: Commitment,
26    /// The seal that was consumed or assigned
27    pub seal_ref: SealRef,
28    /// The Rights involved in this transition
29    pub rights: Vec<Right>,
30    /// Block height when this was anchored on-chain
31    pub block_height: u64,
32    /// Whether this transition has been verified by the client
33    pub verified: bool,
34}
35
36/// A contract's full state history from genesis to present.
37#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct ContractHistory {
39    /// The contract's unique identifier
40    pub contract_id: Hash,
41    /// All state transitions in chronological order
42    pub transitions: Vec<StateTransitionRecord>,
43    /// Current active Rights (not yet consumed)
44    pub active_rights: BTreeMap<Hash, Right>,
45    /// All consumed seals indexed by seal ID
46    pub consumed_seals: BTreeMap<Vec<u8>, SealRef>,
47    /// The latest commitment hash in the chain
48    pub latest_commitment_hash: Hash,
49}
50
51impl ContractHistory {
52    /// Create a new contract history from genesis.
53    pub fn from_genesis(genesis_commitment: Commitment) -> Self {
54        let contract_id = genesis_commitment.contract_id;
55        let latest_hash = genesis_commitment.hash();
56
57        Self {
58            contract_id,
59            transitions: Vec::new(),
60            active_rights: BTreeMap::new(),
61            consumed_seals: BTreeMap::new(),
62            latest_commitment_hash: latest_hash,
63        }
64    }
65
66    /// Add a state transition to the history.
67    pub fn add_transition(&mut self, transition: StateTransitionRecord) -> Result<(), StoreError> {
68        // Verify this transition's commitment chains from the latest
69        let expected_previous = self.latest_commitment_hash;
70        if transition.commitment.previous_commitment != expected_previous {
71            return Err(StoreError::InvalidHistory(format!(
72                "Transition commitment does not chain from latest: expected {:?}, got {:?}",
73                expected_previous, transition.commitment.previous_commitment
74            )));
75        }
76
77        // Update latest commitment
78        self.latest_commitment_hash = transition.commitment.hash();
79
80        // Add to transitions
81        self.transitions.push(transition);
82
83        Ok(())
84    }
85
86    /// Register a new Right as active.
87    pub fn add_right(&mut self, right: Right) {
88        self.active_rights.insert(right.id.0, right);
89    }
90
91    /// Mark a Right as consumed.
92    pub fn consume_right(&mut self, right_id: &Hash) -> Option<Right> {
93        self.active_rights.remove(right_id)
94    }
95
96    /// Check if a seal has been consumed.
97    pub fn is_seal_consumed(&self, seal_ref: &SealRef) -> bool {
98        self.consumed_seals.contains_key(&seal_ref.to_vec())
99    }
100
101    /// Mark a seal as consumed.
102    pub fn mark_seal_consumed(&mut self, seal_ref: SealRef) {
103        self.consumed_seals.insert(seal_ref.to_vec(), seal_ref);
104    }
105
106    /// Get the number of transitions in this contract's history.
107    pub fn transition_count(&self) -> usize {
108        self.transitions.len()
109    }
110
111    /// Get all active Rights.
112    pub fn get_active_rights(&self) -> Vec<&Right> {
113        self.active_rights.values().collect()
114    }
115}
116
117/// Trait for persisting contract state history.
118pub trait StateHistoryStore: Send + Sync {
119    /// Save or update a contract's history.
120    fn save_contract_history(
121        &mut self,
122        contract_id: Hash,
123        history: &ContractHistory,
124    ) -> Result<(), StoreError>;
125
126    /// Load a contract's history by ID.
127    fn load_contract_history(
128        &self,
129        contract_id: Hash,
130    ) -> Result<Option<ContractHistory>, StoreError>;
131
132    /// Get all known contract IDs.
133    fn list_contracts(&self) -> Result<Vec<Hash>, StoreError>;
134
135    /// Delete a contract's history.
136    fn delete_contract(&mut self, contract_id: Hash) -> Result<(), StoreError>;
137}
138
139/// In-memory implementation of StateHistoryStore.
140#[derive(Default)]
141pub struct InMemoryStateStore {
142    contracts: BTreeMap<Hash, ContractHistory>,
143}
144
145impl InMemoryStateStore {
146    /// Create a new empty in-memory store.
147    pub fn new() -> Self {
148        Self {
149            contracts: BTreeMap::new(),
150        }
151    }
152}
153
154impl StateHistoryStore for InMemoryStateStore {
155    fn save_contract_history(
156        &mut self,
157        contract_id: Hash,
158        history: &ContractHistory,
159    ) -> Result<(), StoreError> {
160        self.contracts.insert(contract_id, history.clone());
161        Ok(())
162    }
163
164    fn load_contract_history(
165        &self,
166        contract_id: Hash,
167    ) -> Result<Option<ContractHistory>, StoreError> {
168        Ok(self.contracts.get(&contract_id).cloned())
169    }
170
171    fn list_contracts(&self) -> Result<Vec<Hash>, StoreError> {
172        Ok(self.contracts.keys().cloned().collect())
173    }
174
175    fn delete_contract(&mut self, contract_id: Hash) -> Result<(), StoreError> {
176        self.contracts.remove(&contract_id);
177        Ok(())
178    }
179}
180
181/// Errors that can occur in state storage.
182#[derive(Debug, thiserror::Error)]
183#[allow(missing_docs)]
184pub enum StoreError {
185    #[error("Contract not found: {0}")]
186    ContractNotFound(Hash),
187    #[error("Serialization error: {0}")]
188    SerializationError(String),
189    #[error("Invalid contract history: {0}")]
190    InvalidHistory(String),
191    #[error("IO error: {0}")]
192    IoError(String),
193}
194
195use serde::{Deserialize, Serialize};
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    fn make_test_commitment(previous: Hash, seal_id: u8) -> Commitment {
202        let domain = [0u8; 32];
203        let seal = SealRef::new(vec![seal_id], None).unwrap();
204        Commitment::simple(
205            Hash::new([0xAB; 32]),
206            previous,
207            Hash::new([0u8; 32]),
208            &seal,
209            domain,
210        )
211    }
212
213    #[test]
214    fn test_contract_history_creation() {
215        let genesis = make_test_commitment(Hash::new([0u8; 32]), 0x01);
216        let history = ContractHistory::from_genesis(genesis.clone());
217
218        assert_eq!(history.contract_id, genesis.contract_id);
219        assert_eq!(history.transition_count(), 0);
220        assert_eq!(history.latest_commitment_hash, genesis.hash());
221    }
222
223    #[test]
224    fn test_add_transition() {
225        let genesis = make_test_commitment(Hash::new([0u8; 32]), 0x01);
226        let mut history = ContractHistory::from_genesis(genesis.clone());
227
228        let transition = StateTransitionRecord {
229            commitment: make_test_commitment(genesis.hash(), 0x02),
230            seal_ref: SealRef::new(vec![0x02], None).unwrap(),
231            rights: Vec::new(),
232            block_height: 100,
233            verified: true,
234        };
235
236        history.add_transition(transition).unwrap();
237        assert_eq!(history.transition_count(), 1);
238    }
239
240    #[test]
241    fn test_right_lifecycle() {
242        let genesis = make_test_commitment(Hash::new([0u8; 32]), 0x01);
243        let mut history = ContractHistory::from_genesis(genesis);
244
245        let right = Right::new(
246            Hash::new([0xCD; 32]),
247            crate::right::OwnershipProof {
248                proof: vec![0x01],
249                owner: vec![0xFF; 32],
250                scheme: None,
251            },
252            &[0x42],
253        );
254
255        history.add_right(right.clone());
256        assert_eq!(history.get_active_rights().len(), 1);
257
258        let consumed = history.consume_right(&right.id.0);
259        assert!(consumed.is_some());
260        assert_eq!(history.get_active_rights().len(), 0);
261    }
262
263    #[test]
264    fn test_seal_consumption_tracking() {
265        let genesis = make_test_commitment(Hash::new([0u8; 32]), 0x01);
266        let mut history = ContractHistory::from_genesis(genesis);
267
268        let seal = SealRef::new(vec![0xAB], None).unwrap();
269        assert!(!history.is_seal_consumed(&seal));
270
271        history.mark_seal_consumed(seal.clone());
272        assert!(history.is_seal_consumed(&seal));
273    }
274
275    #[test]
276    fn test_in_memory_store() {
277        let mut store = InMemoryStateStore::new();
278
279        let genesis = make_test_commitment(Hash::new([0u8; 32]), 0x01);
280        let history = ContractHistory::from_genesis(genesis.clone());
281
282        store
283            .save_contract_history(genesis.contract_id, &history)
284            .unwrap();
285
286        let loaded = store.load_contract_history(genesis.contract_id).unwrap();
287        assert!(loaded.is_some());
288        assert_eq!(loaded.unwrap().contract_id, genesis.contract_id);
289
290        let contracts = store.list_contracts().unwrap();
291        assert_eq!(contracts.len(), 1);
292    }
293}