Skip to main content

zync_core/
state.rs

1//! wallet state types and sparse merkle tree operations
2
3use blake2::{Blake2b512, Digest};
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    DOMAIN_WALLET_STATE, EMPTY_SMT_ROOT, 
8    ORCHARD_ACTIVATION_HEIGHT, GENESIS_EPOCH_HASH,
9    Result, ZyncError,
10};
11
12/// commitment to wallet state (32 bytes)
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub struct WalletStateCommitment(pub [u8; 32]);
15
16impl WalletStateCommitment {
17    pub fn as_bytes(&self) -> &[u8; 32] {
18        &self.0
19    }
20}
21
22impl AsRef<[u8]> for WalletStateCommitment {
23    fn as_ref(&self) -> &[u8] {
24        &self.0
25    }
26}
27
28/// full wallet state
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct WalletState {
31    /// sparse merkle root: nullifier -> bool (has been seen)
32    pub nullifier_set_root: [u8; 32],
33
34    /// sparse merkle root: note_commitment -> NoteData
35    pub owned_notes_root: [u8; 32],
36
37    /// incremental merkle tree frontier for note commitment tree
38    /// allows reconstructing merkle paths for spending
39    /// stored as frontier hashes at each level
40    pub note_tree_frontier: NoteFrontier,
41
42    /// chain position
43    pub block_height: u32,
44    pub block_hash: [u8; 32],
45}
46
47impl WalletState {
48    /// genesis state for fresh wallet (at orchard activation)
49    pub fn genesis() -> Self {
50        Self {
51            nullifier_set_root: EMPTY_SMT_ROOT,
52            owned_notes_root: EMPTY_SMT_ROOT,
53            note_tree_frontier: NoteFrontier::empty(),
54            block_height: ORCHARD_ACTIVATION_HEIGHT,
55            block_hash: [0u8; 32], // filled in during first sync
56        }
57    }
58
59    /// genesis state for testnet
60    pub fn genesis_testnet() -> Self {
61        Self {
62            block_height: crate::ORCHARD_ACTIVATION_HEIGHT_TESTNET,
63            ..Self::genesis()
64        }
65    }
66
67    /// commit to state (domain-separated blake2b)
68    pub fn commit(&self) -> WalletStateCommitment {
69        let mut hasher = Blake2b512::new();
70        hasher.update(DOMAIN_WALLET_STATE);
71        hasher.update(&self.nullifier_set_root);
72        hasher.update(&self.owned_notes_root);
73        hasher.update(&self.note_tree_frontier.root());
74        hasher.update(&self.block_height.to_le_bytes());
75        hasher.update(&self.block_hash);
76        
77        let hash = hasher.finalize();
78        let mut commitment = [0u8; 32];
79        commitment.copy_from_slice(&hash[..32]);
80        WalletStateCommitment(commitment)
81    }
82
83    /// check if this is genesis state
84    pub fn is_genesis(&self) -> bool {
85        self.nullifier_set_root == EMPTY_SMT_ROOT
86            && self.owned_notes_root == EMPTY_SMT_ROOT
87    }
88}
89
90/// incremental merkle tree frontier
91/// stores the rightmost path for efficient appends
92/// depth 32 for orchard note commitment tree
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct NoteFrontier {
95    /// position of next leaf to insert
96    pub position: u64,
97    
98    /// frontier hashes at each level (32 levels for orchard)
99    /// frontier[i] is Some if there's an odd node at level i
100    pub frontier: [Option<[u8; 32]>; 32],
101}
102
103impl NoteFrontier {
104    pub fn empty() -> Self {
105        Self {
106            position: 0,
107            frontier: [None; 32],
108        }
109    }
110
111    /// compute merkle root from frontier
112    pub fn root(&self) -> [u8; 32] {
113        if self.position == 0 {
114            return EMPTY_SMT_ROOT;
115        }
116
117        let mut current = [0u8; 32];
118        let mut have_current = false;
119
120        for (level, frontier_hash) in self.frontier.iter().enumerate() {
121            match (frontier_hash, have_current) {
122                (Some(left), false) => {
123                    // frontier hash becomes current
124                    current = *left;
125                    have_current = true;
126                }
127                (Some(left), true) => {
128                    // hash frontier with current
129                    current = hash_merkle_node(level, left, &current);
130                }
131                (None, true) => {
132                    // hash current with empty
133                    current = hash_merkle_node(level, &current, &empty_hash(level));
134                }
135                (None, false) => {
136                    // nothing at this level
137                }
138            }
139        }
140
141        if have_current {
142            current
143        } else {
144            EMPTY_SMT_ROOT
145        }
146    }
147
148    /// append a leaf to the tree, returning new position
149    pub fn append(&mut self, leaf: [u8; 32]) -> u64 {
150        let pos = self.position;
151        self.position += 1;
152
153        let mut current = leaf;
154        for level in 0..32 {
155            if pos & (1 << level) == 0 {
156                // this level was empty, store and stop
157                self.frontier[level] = Some(current);
158                return pos;
159            } else {
160                // this level had a node, hash together and continue
161                let left = self.frontier[level].take().unwrap_or_else(|| empty_hash(level));
162                current = hash_merkle_node(level, &left, &current);
163            }
164        }
165
166        pos
167    }
168
169    /// get current position (number of leaves)
170    pub fn position(&self) -> u64 {
171        self.position
172    }
173}
174
175/// hash two merkle nodes at given level
176fn hash_merkle_node(level: usize, left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
177    let mut hasher = Blake2b512::new();
178    hasher.update(b"ZYNC_merkle_node");
179    hasher.update(&[level as u8]);
180    hasher.update(left);
181    hasher.update(right);
182    
183    let hash = hasher.finalize();
184    let mut result = [0u8; 32];
185    result.copy_from_slice(&hash[..32]);
186    result
187}
188
189/// empty hash at given level (cached in practice)
190fn empty_hash(level: usize) -> [u8; 32] {
191    // todo: precompute and cache these
192    if level == 0 {
193        [0u8; 32]
194    } else {
195        let child = empty_hash(level - 1);
196        hash_merkle_node(level - 1, &child, &child)
197    }
198}
199
200/// sparse merkle tree operations
201pub mod smt {
202    use super::*;
203
204    /// insert key-value pair into SMT, return new root
205    pub fn insert(
206        root: [u8; 32],
207        key: &[u8; 32],
208        value: &[u8],
209    ) -> [u8; 32] {
210        // simplified: just hash key||value||old_root
211        // full implementation would track actual tree structure
212        let mut hasher = Blake2b512::new();
213        hasher.update(b"ZYNC_smt_insert");
214        hasher.update(&root);
215        hasher.update(key);
216        hasher.update(value);
217        
218        let hash = hasher.finalize();
219        let mut new_root = [0u8; 32];
220        new_root.copy_from_slice(&hash[..32]);
221        new_root
222    }
223
224    /// verify membership proof
225    pub fn verify_membership(
226        root: &[u8; 32],
227        key: &[u8; 32],
228        value: &[u8],
229        proof: &SmtProof,
230    ) -> bool {
231        // todo: implement actual merkle path verification
232        // for now just check proof isn't empty
233        !proof.siblings.is_empty()
234    }
235
236    /// SMT membership proof
237    #[derive(Debug, Clone, Serialize, Deserialize)]
238    pub struct SmtProof {
239        pub siblings: Vec<[u8; 32]>,
240        pub path_bits: Vec<bool>,
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_genesis_state() {
250        let state = WalletState::genesis();
251        assert!(state.is_genesis());
252        assert_eq!(state.block_height, ORCHARD_ACTIVATION_HEIGHT);
253    }
254
255    #[test]
256    fn test_state_commitment_deterministic() {
257        let state = WalletState::genesis();
258        let c1 = state.commit();
259        let c2 = state.commit();
260        assert_eq!(c1, c2);
261    }
262
263    #[test]
264    fn test_frontier_empty() {
265        let frontier = NoteFrontier::empty();
266        assert_eq!(frontier.position(), 0);
267        assert_eq!(frontier.root(), EMPTY_SMT_ROOT);
268    }
269
270    #[test]
271    fn test_frontier_append() {
272        let mut frontier = NoteFrontier::empty();
273        
274        let leaf1 = [1u8; 32];
275        let pos1 = frontier.append(leaf1);
276        assert_eq!(pos1, 0);
277        assert_eq!(frontier.position(), 1);
278        
279        let leaf2 = [2u8; 32];
280        let pos2 = frontier.append(leaf2);
281        assert_eq!(pos2, 1);
282        assert_eq!(frontier.position(), 2);
283        
284        // root should be hash of the two leaves
285        let root = frontier.root();
286        assert_ne!(root, EMPTY_SMT_ROOT);
287    }
288}