vote_commitment_tree_client/
types.rs1use base64::prelude::*;
10use ff::PrimeField;
11use pasta_curves::Fp;
12use serde::Deserialize;
13
14use vote_commitment_tree::MerkleHashVote;
15use vote_commitment_tree::sync_api::{BlockCommitments, TreeState};
16
17#[derive(Debug, thiserror::Error)]
23pub enum ParseError {
24 #[error("missing field: {0}")]
25 MissingField(&'static str),
26
27 #[error("base64 decode error: {0}")]
28 Base64(#[from] base64::DecodeError),
29
30 #[error("invalid Fp encoding ({context}): expected 32 bytes, got {len}")]
31 InvalidFpLength { context: &'static str, len: usize },
32
33 #[error("non-canonical Fp encoding ({context})")]
34 NonCanonicalFp { context: &'static str },
35}
36
37#[derive(Debug, Deserialize)]
43pub(crate) struct ChainTreeState {
44 #[serde(default)]
45 pub next_index: u64,
46 #[serde(default)]
48 pub root: Option<String>,
49 #[serde(default)]
50 pub height: u64,
51}
52
53#[derive(Debug, Deserialize)]
55pub(crate) struct ChainBlockCommitments {
56 #[serde(default)]
57 pub height: u64,
58 #[serde(default)]
59 pub start_index: u64,
60 #[serde(default)]
62 pub leaves: Vec<String>,
63}
64
65#[derive(Debug, Deserialize)]
67pub(crate) struct QueryLatestTreeResponse {
68 pub tree: Option<ChainTreeState>,
69}
70
71#[derive(Debug, Deserialize)]
73pub(crate) struct QueryCommitmentTreeResponse {
74 pub tree: Option<ChainTreeState>,
75}
76
77#[derive(Debug, Deserialize)]
79pub(crate) struct QueryCommitmentLeavesResponse {
80 #[serde(default)]
81 pub blocks: Vec<ChainBlockCommitments>,
82}
83
84fn decode_fp_base64(b64: &str, context: &'static str) -> Result<Fp, ParseError> {
90 let bytes = BASE64_STANDARD.decode(b64)?;
91 if bytes.len() != 32 {
92 return Err(ParseError::InvalidFpLength {
93 context,
94 len: bytes.len(),
95 });
96 }
97 let mut arr = [0u8; 32];
98 arr.copy_from_slice(&bytes);
99 Option::from(Fp::from_repr(arr)).ok_or(ParseError::NonCanonicalFp { context })
100}
101
102impl ChainTreeState {
103 pub fn into_tree_state(self) -> Result<TreeState, ParseError> {
105 let root = match &self.root {
106 Some(b64) if !b64.is_empty() => decode_fp_base64(b64, "tree_state.root")?,
107 _ => Fp::zero(),
108 };
109 Ok(TreeState {
110 next_index: self.next_index,
111 root,
112 height: self.height as u32,
113 })
114 }
115}
116
117impl ChainBlockCommitments {
118 pub fn into_block_commitments(self) -> Result<BlockCommitments, ParseError> {
120 let mut leaves = Vec::with_capacity(self.leaves.len());
121 for (i, b64) in self.leaves.iter().enumerate() {
122 let fp = decode_fp_base64(b64, "block_commitments.leaf")?;
123 leaves.push(MerkleHashVote::from_fp(fp));
124 let _ = i; }
126 Ok(BlockCommitments {
127 height: self.height as u32,
128 start_index: self.start_index,
129 leaves,
130 })
131 }
132}
133
134#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn parse_tree_state_full() {
144 let zero_b64 = BASE64_STANDARD.encode([0u8; 32]);
146 let json = format!(
147 r#"{{"tree":{{"next_index":42,"root":"{}","height":10}}}}"#,
148 zero_b64
149 );
150 let resp: QueryLatestTreeResponse = serde_json::from_str(&json).unwrap();
151 let state = resp.tree.unwrap().into_tree_state().unwrap();
152 assert_eq!(state.next_index, 42);
153 assert_eq!(state.height, 10);
154 assert_eq!(state.root, Fp::zero());
155 }
156
157 #[test]
158 fn parse_tree_state_missing_root() {
159 let json = r#"{"tree":{"next_index":0,"height":0}}"#;
160 let resp: QueryLatestTreeResponse = serde_json::from_str(json).unwrap();
161 let state = resp.tree.unwrap().into_tree_state().unwrap();
162 assert_eq!(state.root, Fp::zero());
163 }
164
165 #[test]
166 fn parse_tree_state_null_tree() {
167 let json = r#"{"tree":null}"#;
168 let resp: QueryLatestTreeResponse = serde_json::from_str(json).unwrap();
169 assert!(resp.tree.is_none());
170 }
171
172 #[test]
173 fn parse_block_commitments_with_leaves() {
174 let one_bytes = Fp::from(1).to_repr();
176 let one_b64 = BASE64_STANDARD.encode(one_bytes);
177 let json = format!(
178 r#"{{"blocks":[{{"height":5,"start_index":0,"leaves":["{}","{}"]}}]}}"#,
179 one_b64, one_b64
180 );
181 let resp: QueryCommitmentLeavesResponse = serde_json::from_str(&json).unwrap();
182 assert_eq!(resp.blocks.len(), 1);
183 let block = resp.blocks.into_iter().next().unwrap().into_block_commitments().unwrap();
184 assert_eq!(block.height, 5);
185 assert_eq!(block.start_index, 0);
186 assert_eq!(block.leaves.len(), 2);
187 assert_eq!(block.leaves[0].inner(), Fp::from(1));
188 }
189
190 #[test]
191 fn parse_empty_blocks() {
192 let json = r#"{"blocks":[]}"#;
193 let resp: QueryCommitmentLeavesResponse = serde_json::from_str(json).unwrap();
194 assert!(resp.blocks.is_empty());
195 }
196
197 #[test]
198 fn parse_omitted_blocks_field() {
199 let json = r#"{}"#;
201 let resp: QueryCommitmentLeavesResponse = serde_json::from_str(json).unwrap();
202 assert!(resp.blocks.is_empty());
203 }
204
205 #[test]
206 fn decode_fp_rejects_short_base64() {
207 let short = BASE64_STANDARD.encode([0u8; 16]);
208 let err = decode_fp_base64(&short, "test").unwrap_err();
209 assert!(matches!(err, ParseError::InvalidFpLength { len: 16, .. }));
210 }
211
212 #[test]
213 fn decode_fp_rejects_non_canonical() {
214 let bad = BASE64_STANDARD.encode([0xFF; 32]);
216 let err = decode_fp_base64(&bad, "test").unwrap_err();
217 assert!(matches!(err, ParseError::NonCanonicalFp { .. }));
218 }
219}