dig_network_block/
dig_l2_definition.rs

1//! CAPITALIZED spec functions and hashing domains for the L2 block.
2//!
3//! This module centralizes the specification-defined functions so they can be
4//! imported and used by the concrete types (`header`, `body`, `block`, etc.).
5//!
6//! Contents:
7//! - Domain constants used for SHA-256 domain separation
8//! - `COMPUTE_DATA_HASH`
9//! - `COMPUTE_EMISSION_HASH`
10//! - `MERKLE_ROOT`
11//! - `COMPUTE_BODY_ROOT`
12//! - `COMPUTE_HEADER_ROOT`
13//! - `COMPUTE_BLOCK_ROOT`
14//! - `BUILD_CONSENSUS_EMISSIONS` (returns simple tuples for later conversion)
15//!
16//! All functions are deterministic and documented. Merkle construction uses
17//! classic odd-leaf duplication and distinct leaf/node domains.
18
19#![allow(non_snake_case)]
20
21use crate::header::L2BlockHeader;
22use sha2::{Digest, Sha256};
23use thiserror::Error;
24
25/// 32-byte hash type used across the spec.
26pub type Hash32 = [u8; 32];
27
28/// Domain separation for individual header fields.
29pub const HEADER_FIELD_DOMAIN: &[u8] = b"dig:l2:header_field:";
30/// Domain separation for the block root composition.
31pub const BLOCK_ROOT_DOMAIN: &[u8] = b"dig:l2:block_root:";
32/// Domain separation for application data items (single byte per spec here).
33pub const DATA_HASH_DOMAIN: &[u8] = b"dig:l2:data:";
34/// Domain separation for standardized emissions.
35pub const EMISSION_HASH_DOMAIN: &[u8] = b"dig:l2:emission:";
36/// Domain separation for Merkle leaf nodes.
37pub const MERKLE_LEAF_DOMAIN: &[u8] = b"dig:l2:merkle:leaf:";
38/// Domain separation for Merkle internal nodes.
39pub const MERKLE_NODE_DOMAIN: &[u8] = b"dig:l2:merkle:node:";
40/// Domain for the empty Merkle root.
41pub const MERKLE_EMPTY_DOMAIN: &[u8] = b"dig:l2:merkle:empty:";
42
43/// Errors for definition-level functions.
44#[derive(Debug, Error)]
45pub enum DefinitionError {
46    /// Attempted to assign non-zero attester share with zero attesters; division is undefined.
47    #[error("attester_reward_share is non-zero but no attesters provided")]
48    NoAttestersForNonZeroShare,
49}
50
51fn sha256_concat(parts: &[&[u8]]) -> Hash32 {
52    let mut hasher = Sha256::new();
53    for p in parts {
54        hasher.update(p);
55    }
56    hasher.finalize().into()
57}
58
59/// Compute the hash for a single data item (a single byte for this chain).
60///
61/// Per spec: `SHA256(DATA_HASH_DOMAIN || item.data)`.
62pub fn COMPUTE_DATA_HASH(data_byte: u8) -> Hash32 {
63    let b = [data_byte];
64    sha256_concat(&[DATA_HASH_DOMAIN, &b])
65}
66
67/// Compute the hash for a single emission.
68///
69/// Per spec: `SHA256(EMISSION_HASH_DOMAIN || emission.pubkey || emission.weight_le)`.
70pub fn COMPUTE_EMISSION_HASH(pubkey: &[u8; 48], weight: u64) -> Hash32 {
71    let w = weight.to_le_bytes();
72    sha256_concat(&[EMISSION_HASH_DOMAIN, pubkey, &w])
73}
74
75/// Compute a Merkle root from a slice of leaves.
76///
77/// - Leaves are first converted to domain-separated leaf nodes: `H = SHA256(MERKLE_LEAF_DOMAIN || leaf)`
78/// - Internal nodes are `SHA256(MERKLE_NODE_DOMAIN || left || right)`
79/// - Odd number of nodes duplicates the last one to make a pair.
80/// - Empty slice returns `SHA256(MERKLE_EMPTY_DOMAIN)`.
81pub fn MERKLE_ROOT(leaves: &[Hash32]) -> Hash32 {
82    if leaves.is_empty() {
83        return sha256_concat(&[MERKLE_EMPTY_DOMAIN]);
84    }
85
86    let mut level: Vec<Hash32> = leaves
87        .iter()
88        .map(|leaf| sha256_concat(&[MERKLE_LEAF_DOMAIN, leaf]))
89        .collect();
90
91    while level.len() > 1 {
92        if level.len() % 2 == 1 {
93            let last = *level.last().unwrap();
94            level.push(last);
95        }
96        let mut next = Vec::with_capacity(level.len() / 2);
97        for pair in level.chunks(2) {
98            let combined = sha256_concat(&[MERKLE_NODE_DOMAIN, &pair[0], &pair[1]]);
99            next.push(combined);
100        }
101        level = next;
102    }
103    level[0]
104}
105
106/// Compute the body root from the two subroots `DATA_ROOT` and `EMISSIONS_ROOT`.
107///
108/// Implemented as a 2-leaf Merkle root of `[data_root, emissions_root]`.
109pub fn COMPUTE_BODY_ROOT(data_root: &Hash32, emissions_root: &Hash32) -> Hash32 {
110    MERKLE_ROOT(&[*data_root, *emissions_root])
111}
112
113/// Compute the header root from individual header fields, allowing proofs of each field.
114///
115/// Instead of taking a header struct (to avoid module coupling), we accept individual fields.
116/// The field label is included literally to avoid positional ambiguity.
117pub fn COMPUTE_HEADER_ROOT(args: &L2BlockHeader) -> Hash32 {
118    let v_bytes = args.version.to_le_bytes();
119    let e_bytes = args.epoch.to_le_bytes();
120    let dc_bytes = args.data_count.to_le_bytes();
121    let ec_bytes = args.emissions_count.to_le_bytes();
122
123    let leaves: [Hash32; 8] = [
124        sha256_concat(&[HEADER_FIELD_DOMAIN, b"version", &v_bytes]),
125        sha256_concat(&[HEADER_FIELD_DOMAIN, b"network_id", &args.network_id]),
126        sha256_concat(&[HEADER_FIELD_DOMAIN, b"epoch", &e_bytes]),
127        sha256_concat(&[
128            HEADER_FIELD_DOMAIN,
129            b"prev_block_root",
130            &args.prev_block_root,
131        ]),
132        sha256_concat(&[HEADER_FIELD_DOMAIN, b"body_root", &args.body_root]),
133        sha256_concat(&[HEADER_FIELD_DOMAIN, b"data_count", &dc_bytes]),
134        sha256_concat(&[HEADER_FIELD_DOMAIN, b"emissions_count", &ec_bytes]),
135        sha256_concat(&[
136            HEADER_FIELD_DOMAIN,
137            b"proposer_pubkey",
138            &args.proposer_pubkey,
139        ]),
140    ];
141    MERKLE_ROOT(&leaves)
142}
143
144/// Compute the block root from `HEADER_ROOT` and `BODY_ROOT`.
145///
146/// Per spec: `SHA256(BLOCK_ROOT_DOMAIN || header_root || body_root)`.
147pub fn COMPUTE_BLOCK_ROOT(header_root: &Hash32, body_root: &Hash32) -> Hash32 {
148    sha256_concat(&[BLOCK_ROOT_DOMAIN, header_root, body_root])
149}
150
151/// Simple emission tuple returned by `BUILD_CONSENSUS_EMISSIONS`.
152/// Concrete `Emission` types can convert from this tuple.
153pub type EmissionTuple = ([u8; 48], u64);
154
155/// Build the required consensus emissions: one proposer record plus attester records.
156///
157/// - `proposer_reward_share` is a fixed weight (e.g., 12 for 12.5%).
158/// - `attester_reward_share` is split equally among attesters using integer division; remainder is undistributed.
159/// - If `attester_reward_share > 0` while `attester_pubkeys` is empty, returns an error.
160pub fn BUILD_CONSENSUS_EMISSIONS(
161    proposer_pubkey: [u8; 48],
162    attester_pubkeys: &[[u8; 48]],
163    proposer_reward_share: u64,
164    attester_reward_share: u64,
165) -> Result<Vec<EmissionTuple>, DefinitionError> {
166    let mut out = Vec::with_capacity(1 + attester_pubkeys.len());
167    out.push((proposer_pubkey, proposer_reward_share));
168
169    if attester_pubkeys.is_empty() {
170        if attester_reward_share > 0 {
171            return Err(DefinitionError::NoAttestersForNonZeroShare);
172        }
173        return Ok(out);
174    }
175
176    let per_attester = attester_reward_share / (attester_pubkeys.len() as u64);
177    for pk in attester_pubkeys {
178        out.push((*pk, per_attester));
179    }
180    Ok(out)
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    fn h32(x: u8) -> Hash32 {
188        // helper deterministic array for testing merkle behavior
189        let mut a = [0u8; 32];
190        a[0] = x;
191        a
192    }
193
194    #[test]
195    fn data_hash_changes_with_value() {
196        let h1 = COMPUTE_DATA_HASH(0);
197        let h2 = COMPUTE_DATA_HASH(1);
198        assert_ne!(h1, h2);
199    }
200
201    #[test]
202    fn emission_hash_domain_separated() {
203        let pk = [1u8; 48];
204        let h1 = COMPUTE_EMISSION_HASH(&pk, 10);
205        let h2 = sha256_concat(&[DATA_HASH_DOMAIN, &pk, &10u64.to_le_bytes()]);
206        assert_ne!(h1, h2); // different domain
207    }
208
209    #[test]
210    fn merkle_root_empty() {
211        let r = MERKLE_ROOT(&[]);
212        let expect = sha256_concat(&[MERKLE_EMPTY_DOMAIN]);
213        assert_eq!(r, expect);
214    }
215
216    #[test]
217    fn merkle_root_single() {
218        let leaf = h32(7);
219        let r = MERKLE_ROOT(&[leaf]);
220        // When single, result is SHA(leaf_domain || leaf), not just the leaf
221        assert_eq!(r, sha256_concat(&[MERKLE_LEAF_DOMAIN, &leaf]));
222    }
223
224    #[test]
225    fn merkle_root_odd_duplication() {
226        let leaves = [h32(1), h32(2), h32(3)];
227        let r = MERKLE_ROOT(&leaves);
228        // Basic sanity: changing last element changes root
229        let r2 = MERKLE_ROOT(&[h32(1), h32(2), h32(4)]);
230        assert_ne!(r, r2);
231    }
232
233    #[test]
234    fn body_root_is_merkle_of_two() {
235        let d = h32(0x11);
236        let e = h32(0x22);
237        let r = COMPUTE_BODY_ROOT(&d, &e);
238        let r2 = MERKLE_ROOT(&[d, e]);
239        assert_eq!(r, r2);
240    }
241
242    #[test]
243    fn header_root_field_permutation_changes_root() {
244        let network_id = [2u8; 32];
245        let prev = [3u8; 32];
246        let body = [4u8; 32];
247        let proposer = [5u8; 48];
248        let r1_header = L2BlockHeader {
249            version: 1,
250            network_id,
251            epoch: 2,
252            prev_block_root: prev,
253            body_root: body,
254            data_count: 3,
255            emissions_count: 4,
256            proposer_pubkey: proposer,
257        };
258        let r2_header = L2BlockHeader {
259            version: 1,
260            network_id,
261            epoch: 2,
262            prev_block_root: prev,
263            body_root: body,
264            data_count: 4,
265            emissions_count: 3,
266            proposer_pubkey: proposer,
267        };
268        let r1 = COMPUTE_HEADER_ROOT(&r1_header);
269        let r2 = COMPUTE_HEADER_ROOT(&r2_header);
270        assert_ne!(r1, r2);
271    }
272
273    #[test]
274    fn block_root_composition() {
275        let header_root = h32(0xaa);
276        let body_root = h32(0xbb);
277        let r = COMPUTE_BLOCK_ROOT(&header_root, &body_root);
278        let expect = sha256_concat(&[BLOCK_ROOT_DOMAIN, &header_root, &body_root]);
279        assert_eq!(r, expect);
280    }
281
282    #[test]
283    fn build_consensus_emissions_basic() {
284        let proposer = [7u8; 48];
285        let attesters = vec![[1u8; 48], [2u8; 48], [3u8; 48]];
286        let v = BUILD_CONSENSUS_EMISSIONS(proposer, &attesters, 12, 88).unwrap();
287        assert_eq!(v.len(), 1 + attesters.len());
288        assert_eq!(v[0], (proposer, 12));
289        // 88 / 3 = 29 per attester
290        assert_eq!(v[1].1, 29);
291        assert_eq!(v[2].1, 29);
292        assert_eq!(v[3].1, 29);
293    }
294
295    #[test]
296    fn build_consensus_emissions_zero_attesters_policy() {
297        let proposer = [9u8; 48];
298        let v = BUILD_CONSENSUS_EMISSIONS(proposer, &[], 12, 0).unwrap();
299        assert_eq!(v.len(), 1);
300
301        let err = BUILD_CONSENSUS_EMISSIONS(proposer, &[], 12, 1).unwrap_err();
302        match err {
303            DefinitionError::NoAttestersForNonZeroShare => {}
304        }
305    }
306}