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}