Skip to main content

vote_commitment_tree/
hash.rs

1//! Vote commitment tree hash types: [`MerkleHashVote`], `EMPTY_ROOTS`, and tree constants.
2//!
3//! These types are shared between the server and client sides of the vote commitment tree.
4
5use core::iter;
6
7use ff::PrimeField;
8use incrementalmerkletree::{Hashable, Level};
9use lazy_static::lazy_static;
10use pasta_curves::Fp;
11
12use crate::poseidon_hash;
13
14/// Fixed depth of the Vote Commitment Tree (2^24 ≈ 16.7M leaf capacity).
15///
16/// Reduced from Zcash's depth 32 (~4.3B) because governance voting produces
17/// far fewer leaves than a full shielded pool. Each voter generates 1 leaf per
18/// delegation + 2 per vote, so even 10K voters × 50 proposals = ~1M leaves per
19/// round — well within 2^24. This saves 8 Poseidon hashes per ZKP proof
20/// (~2,000 fewer constraints) and shrinks Merkle paths from 1,028 to 772 bytes.
21pub const TREE_DEPTH: usize = 24;
22
23/// Shard height for the underlying `ShardTree` (each shard covers 2^4 = 16 leaves).
24pub(crate) const SHARD_HEIGHT: u8 = 4;
25
26/// Maximum number of checkpoints retained by the tree.
27pub(crate) const MAX_CHECKPOINTS: usize = 1000;
28
29lazy_static! {
30    /// Precomputed empty subtree hashes for each level.
31    ///
32    /// `EMPTY_ROOTS[0]` = `empty_leaf()` = `poseidon_hash(0, 0)`
33    /// `EMPTY_ROOTS[i]` = `combine(i-1, EMPTY_ROOTS[i-1], EMPTY_ROOTS[i-1])`
34    pub(crate) static ref EMPTY_ROOTS: Vec<MerkleHashVote> = {
35        iter::empty()
36            .chain(Some(MerkleHashVote::empty_leaf()))
37            .chain(
38                (0..TREE_DEPTH).scan(MerkleHashVote::empty_leaf(), |state, l| {
39                    let l = l as u8;
40                    *state = MerkleHashVote::combine(l.into(), state, state);
41                    Some(*state)
42                }),
43            )
44            .collect()
45    };
46}
47
48// ---------------------------------------------------------------------------
49// MerkleHashVote
50// ---------------------------------------------------------------------------
51
52/// Leaf and internal node digest for the vote commitment tree.
53///
54/// Wraps a Pallas base field element (`Fp`). Implements `Hashable` so it plugs
55/// directly into `incrementalmerkletree` / `shardtree`.
56#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
57pub struct MerkleHashVote(pub(crate) Fp);
58
59impl MerkleHashVote {
60    /// Create a digest from a raw field element (e.g. a VAN or VC commitment).
61    pub fn from_fp(value: Fp) -> Self {
62        MerkleHashVote(value)
63    }
64
65    /// Extract the inner field element.
66    pub fn inner(&self) -> Fp {
67        self.0
68    }
69
70    /// Serialize to canonical 32-byte little-endian representation.
71    pub fn to_bytes(&self) -> [u8; 32] {
72        self.0.to_repr()
73    }
74
75    /// Deserialize from 32 bytes. Returns `None` for non-canonical encodings.
76    pub fn from_bytes(bytes: &[u8; 32]) -> Option<Self> {
77        Option::from(Fp::from_repr(*bytes).map(MerkleHashVote))
78    }
79}
80
81impl Hashable for MerkleHashVote {
82    fn empty_leaf() -> Self {
83        MerkleHashVote(poseidon_hash(Fp::zero(), Fp::zero()))
84    }
85
86    /// Poseidon(left, right) — no layer tagging (unlike Orchard's Sinsemilla
87    /// which prepends the level index).
88    fn combine(_level: Level, left: &Self, right: &Self) -> Self {
89        MerkleHashVote(poseidon_hash(left.0, right.0))
90    }
91
92    fn empty_root(level: Level) -> Self {
93        EMPTY_ROOTS[usize::from(level)]
94    }
95}