Skip to main content

vote_commitment_tree/
lib.rs

1//! Append-only Poseidon Merkle tree for the Vote Commitment Tree (Gov Steps V1).
2//!
3//! This tree holds both **Vote Authority Notes (VANs)** and **Vote Commitments (VCs)** as leaves.
4//! Domain separation (DOMAIN_VAN / DOMAIN_VC) is applied when *constructing* leaf values
5//! (in circuits / chain); this crate stores and hashes already-committed field elements.
6//!
7//! Insertion order (per cosmos-sdk-messages-spec):
8//! - `MsgDelegateVote` → append 1 leaf (VAN)
9//! - `MsgCastVote` → append 2 leaves (new VAN, then VC)
10//!
11//! ## Architecture
12//!
13//! The crate is split into server and client layers with a sync API boundary:
14//!
15//! - **Shared types** ([`MerkleHashVote`], [`Anchor`], [`MerklePath`]) — used by both sides.
16//! - **[`TreeServer`]** — authoritative full tree: append, checkpoint, serve data via [`TreeSyncApi`].
17//! - **[`TreeClient`]** — sparse tree: sync from server, mark positions, generate witnesses.
18//! - **[`TreeSyncApi`]** — trait defining the communication boundary (in-process for POC,
19//!   maps to Cosmos SDK endpoints in production).
20//!
21//! Built on `incrementalmerkletree` / `shardtree` (same crates that back Orchard's
22//! note commitment tree), with two substitutions:
23//! - **Hash:** Poseidon (no layer tagging) instead of Sinsemilla
24//! - **Empty leaf:** `poseidon_hash(0, 0)` instead of `Fp::from(2)`
25
26// -- Modules ---------------------------------------------------------------
27
28mod anchor;
29pub mod client;
30mod hash;
31pub mod kv_shard_store;
32pub mod memory_server;
33mod path;
34pub mod serde;
35pub mod server;
36pub mod sync_api;
37
38// -- Re-exports (public API) -----------------------------------------------
39
40pub use anchor::Anchor;
41pub use client::{SyncError, TreeClient};
42pub use hash::{MerkleHashVote, TREE_DEPTH};
43pub use path::{MerklePath, MERKLE_PATH_BYTES};
44pub use server::{AppendFromKvError, MemoryTreeServer, SyncableServer, TreeServer};
45pub use sync_api::TreeSyncApi;
46
47// -- Shared utilities ------------------------------------------------------
48
49use pasta_curves::Fp;
50
51/// Domain tag for Vote Commitments (matches `orchard::vote_proof::circuit::DOMAIN_VC`).
52pub const DOMAIN_VC: u64 = 1;
53
54/// Poseidon hash of two field elements (delegates to imt-tree for circuit consistency).
55#[inline]
56pub fn poseidon_hash(left: Fp, right: Fp) -> Fp {
57    imt_tree::poseidon_hash(left, right)
58}
59
60/// Poseidon hash of six field elements (`ConstantLength<6>`, width 3, rate 2).
61pub fn poseidon_hash_6(a: Fp, b: Fp, c: Fp, d: Fp, e: Fp, f: Fp) -> Fp {
62    use halo2_gadgets::poseidon::primitives::{self as poseidon, ConstantLength, P128Pow5T3};
63
64    poseidon::Hash::<_, P128Pow5T3, ConstantLength<6>, 3, 2>::init().hash([a, b, c, d, e, f])
65}
66
67/// Compute the vote commitment leaf hash (arity-5 Poseidon).
68///
69/// ```text
70/// vote_commitment_hash(voting_round_id, shares_hash, proposal_id, vote_decision) =
71///     Poseidon(DOMAIN_VC, voting_round_id, shares_hash, proposal_id, vote_decision)
72/// ```
73///
74/// This must produce identical output to `orchard::vote_proof::vote_commitment_hash`.
75pub fn vote_commitment_hash(
76    voting_round_id: Fp,
77    shares_hash: Fp,
78    proposal_id: Fp,
79    vote_decision: Fp,
80) -> Fp {
81    use halo2_gadgets::poseidon::primitives::{self as poseidon, ConstantLength, P128Pow5T3};
82
83    poseidon::Hash::<_, P128Pow5T3, ConstantLength<5>, 3, 2>::init().hash([
84        Fp::from(DOMAIN_VC),
85        voting_round_id,
86        shares_hash,
87        proposal_id,
88        vote_decision,
89    ])
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_vote_commitment_hash_basic() {
98        // Sanity: different inputs → different outputs.
99        let round = Fp::from(99u64);
100        let a = vote_commitment_hash(round, Fp::from(1u64), Fp::from(2u64), Fp::from(0u64));
101        let b = vote_commitment_hash(round, Fp::from(1u64), Fp::from(2u64), Fp::from(1u64));
102        assert_ne!(a, b);
103    }
104
105    #[test]
106    fn test_vote_commitment_hash_deterministic() {
107        let round = Fp::from(99u64);
108        let h1 = vote_commitment_hash(round, Fp::from(42u64), Fp::from(3u64), Fp::from(1u64));
109        let h2 = vote_commitment_hash(round, Fp::from(42u64), Fp::from(3u64), Fp::from(1u64));
110        assert_eq!(h1, h2);
111    }
112
113    #[test]
114    fn test_vote_commitment_hash_cross_validates_with_orchard() {
115        use voting_circuits::vote_proof;
116
117        let voting_round_id = Fp::from(0xCAFEu64);
118        let shares_hash = Fp::from(0xDEAD_BEEFu64);
119        let proposal_id = Fp::from(5u64);
120        let vote_decision = Fp::from(1u64);
121
122        let ours = vote_commitment_hash(voting_round_id, shares_hash, proposal_id, vote_decision);
123        let theirs = vote_proof::vote_commitment_hash(voting_round_id, shares_hash, proposal_id, vote_decision);
124        assert_eq!(ours, theirs, "vote_commitment_hash must match orchard circuit helper");
125    }
126
127    #[test]
128    fn test_domain_vc_matches_orchard() {
129        assert_eq!(DOMAIN_VC, voting_circuits::vote_proof::DOMAIN_VC);
130    }
131}