Skip to main content

zync_core/
actions.rs

1//! Actions merkle root computation and running commitment chain.
2//!
3//! Shared between client and server to ensure identical computation.
4//! The actions root binds compact block actions to the ligerito-proven header chain.
5
6use sha2::{Digest, Sha256};
7
8/// Compute merkle root over a block's orchard actions.
9///
10/// Each leaf is `SHA256(cmx || nullifier || ephemeral_key)`.
11/// Tree is a standard binary SHA256 merkle tree with zero-padding to next power of 2.
12/// Empty block (no actions) returns all-zeros root.
13pub fn compute_actions_root(
14    actions: &[([u8; 32], [u8; 32], [u8; 32])],
15) -> [u8; 32] {
16    if actions.is_empty() {
17        return [0u8; 32];
18    }
19
20    // compute leaf hashes
21    let mut leaves: Vec<[u8; 32]> = actions
22        .iter()
23        .map(|(cmx, nullifier, epk)| {
24            let mut hasher = Sha256::new();
25            hasher.update(cmx);
26            hasher.update(nullifier);
27            hasher.update(epk);
28            hasher.finalize().into()
29        })
30        .collect();
31
32    // pad to next power of 2
33    let target_len = leaves.len().next_power_of_two();
34    leaves.resize(target_len, [0u8; 32]);
35
36    // build binary merkle tree bottom-up
37    let mut level = leaves;
38    while level.len() > 1 {
39        let mut next_level = Vec::with_capacity(level.len() / 2);
40        for pair in level.chunks(2) {
41            let mut hasher = Sha256::new();
42            hasher.update(pair[0]);
43            hasher.update(pair[1]);
44            next_level.push(hasher.finalize().into());
45        }
46        level = next_level;
47    }
48
49    level[0]
50}
51
52/// Update the running actions commitment chain.
53///
54/// `chain_i = BLAKE2b-256("ZYNC_actions_v1" || chain_{i-1} || actions_root_i || height_i)`
55///
56/// This mirrors the existing `update_state_commitment` pattern in header_chain.rs.
57pub fn update_actions_commitment(
58    prev: &[u8; 32],
59    actions_root: &[u8; 32],
60    height: u32,
61) -> [u8; 32] {
62    use blake2::digest::typenum::U32;
63    use blake2::Blake2b;
64
65    let mut hasher = <Blake2b<U32>>::new();
66    hasher.update(b"ZYNC_actions_v1");
67    hasher.update(prev);
68    hasher.update(actions_root);
69    hasher.update(height.to_le_bytes());
70
71    let hash = hasher.finalize();
72    let mut result = [0u8; 32];
73    result.copy_from_slice(&hash);
74    result
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_empty_actions_root() {
83        let root = compute_actions_root(&[]);
84        assert_eq!(root, [0u8; 32]);
85    }
86
87    #[test]
88    fn test_single_action_root() {
89        let cmx = [1u8; 32];
90        let nullifier = [2u8; 32];
91        let epk = [3u8; 32];
92
93        let root = compute_actions_root(&[(cmx, nullifier, epk)]);
94
95        // single leaf: root should be the leaf hash itself (padded to 2, then
96        // root = SHA256(leaf || zero))
97        let mut hasher = Sha256::new();
98        hasher.update(cmx);
99        hasher.update(nullifier);
100        hasher.update(epk);
101        let leaf: [u8; 32] = hasher.finalize().into();
102
103        // with padding to power of 2 (size 1 -> 1, which is already power of 2)
104        // actually 1 is 2^0, so next_power_of_two(1) = 1, meaning no padding needed
105        // and the loop stops immediately since level.len() == 1
106        assert_eq!(root, leaf);
107        assert_ne!(root, [0u8; 32]);
108    }
109
110    #[test]
111    fn test_actions_root_deterministic() {
112        let actions = vec![
113            ([10u8; 32], [20u8; 32], [30u8; 32]),
114            ([40u8; 32], [50u8; 32], [60u8; 32]),
115            ([70u8; 32], [80u8; 32], [90u8; 32]),
116        ];
117
118        let root1 = compute_actions_root(&actions);
119        let root2 = compute_actions_root(&actions);
120        assert_eq!(root1, root2);
121        assert_ne!(root1, [0u8; 32]);
122    }
123
124    #[test]
125    fn test_actions_commitment_chain() {
126        let prev = [0u8; 32];
127        let root_a = [1u8; 32];
128        let root_b = [2u8; 32];
129
130        // different actions roots produce different commitments
131        let c1 = update_actions_commitment(&prev, &root_a, 100);
132        let c2 = update_actions_commitment(&prev, &root_b, 100);
133        assert_ne!(c1, c2);
134
135        // different heights produce different commitments
136        let c3 = update_actions_commitment(&prev, &root_a, 101);
137        assert_ne!(c1, c3);
138
139        // different prev produce different commitments
140        let c4 = update_actions_commitment(&[0xffu8; 32], &root_a, 100);
141        assert_ne!(c1, c4);
142
143        // deterministic
144        let c5 = update_actions_commitment(&prev, &root_a, 100);
145        assert_eq!(c1, c5);
146    }
147
148    #[test]
149    fn test_two_actions_root() {
150        let a1 = ([1u8; 32], [2u8; 32], [3u8; 32]);
151        let a2 = ([4u8; 32], [5u8; 32], [6u8; 32]);
152
153        let root = compute_actions_root(&[a1, a2]);
154
155        // manually compute: 2 leaves, already power of 2
156        let mut h1 = Sha256::new();
157        h1.update(a1.0);
158        h1.update(a1.1);
159        h1.update(a1.2);
160        let leaf1: [u8; 32] = h1.finalize().into();
161
162        let mut h2 = Sha256::new();
163        h2.update(a2.0);
164        h2.update(a2.1);
165        h2.update(a2.2);
166        let leaf2: [u8; 32] = h2.finalize().into();
167
168        let mut h3 = Sha256::new();
169        h3.update(leaf1);
170        h3.update(leaf2);
171        let expected: [u8; 32] = h3.finalize().into();
172
173        assert_eq!(root, expected);
174    }
175
176    #[test]
177    fn test_three_actions_padded() {
178        // 3 actions -> padded to 4 leaves
179        let a1 = ([1u8; 32], [2u8; 32], [3u8; 32]);
180        let a2 = ([4u8; 32], [5u8; 32], [6u8; 32]);
181        let a3 = ([7u8; 32], [8u8; 32], [9u8; 32]);
182
183        let root = compute_actions_root(&[a1, a2, a3]);
184        assert_ne!(root, [0u8; 32]);
185
186        // order matters
187        let root_reordered = compute_actions_root(&[a2, a1, a3]);
188        assert_ne!(root, root_reordered);
189    }
190}