vote_commitment_tree_client/
types.rs1use base64::prelude::*;
10use ff::PrimeField;
11use pasta_curves::Fp;
12use serde::Deserialize;
13
14use vote_commitment_tree::sync_api::{BlockCommitments, BlockCommitmentsPage, TreeState};
15use vote_commitment_tree::MerkleHashVote;
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 #[serde(default)]
65 pub root: Option<String>,
66}
67
68#[derive(Debug, Deserialize)]
70pub(crate) struct QueryLatestTreeResponse {
71 pub tree: Option<ChainTreeState>,
72}
73
74#[derive(Debug, Deserialize)]
76pub(crate) struct QueryCommitmentTreeResponse {
77 pub tree: Option<ChainTreeState>,
78}
79
80#[derive(Debug, Deserialize)]
82pub(crate) struct QueryCommitmentLeavesResponse {
83 #[serde(default)]
84 pub blocks: Vec<ChainBlockCommitments>,
85 #[serde(default)]
86 pub next_from_height: u64,
87}
88
89fn decode_fp_base64(b64: &str, context: &'static str) -> Result<Fp, ParseError> {
95 let bytes = BASE64_STANDARD.decode(b64)?;
96 if bytes.len() != 32 {
97 return Err(ParseError::InvalidFpLength {
98 context,
99 len: bytes.len(),
100 });
101 }
102 let mut arr = [0u8; 32];
103 arr.copy_from_slice(&bytes);
104 Option::from(Fp::from_repr(arr)).ok_or(ParseError::NonCanonicalFp { context })
105}
106
107impl ChainTreeState {
108 pub fn into_tree_state(self) -> Result<TreeState, ParseError> {
110 let root = match &self.root {
111 Some(b64) if !b64.is_empty() => decode_fp_base64(b64, "tree_state.root")?,
112 _ => Fp::zero(),
113 };
114 Ok(TreeState {
115 next_index: self.next_index,
116 root,
117 height: self.height as u32,
118 })
119 }
120}
121
122impl ChainBlockCommitments {
123 pub fn into_block_commitments(self) -> Result<BlockCommitments, ParseError> {
125 let mut leaves = Vec::with_capacity(self.leaves.len());
126 for (i, b64) in self.leaves.iter().enumerate() {
127 let fp = decode_fp_base64(b64, "block_commitments.leaf")?;
128 leaves.push(MerkleHashVote::from_fp(fp));
129 let _ = i; }
131 Ok(BlockCommitments {
132 height: self.height as u32,
133 start_index: self.start_index,
134 leaves,
135 root: decode_fp_base64(
136 self.root
137 .as_deref()
138 .ok_or(ParseError::MissingField("block_commitments.root"))?,
139 "block_commitments.root",
140 )?,
141 })
142 }
143}
144
145impl QueryCommitmentLeavesResponse {
146 pub fn into_block_commitments_page(self) -> Result<BlockCommitmentsPage, ParseError> {
148 let blocks = self
149 .blocks
150 .into_iter()
151 .map(ChainBlockCommitments::into_block_commitments)
152 .collect::<Result<Vec<_>, _>>()?;
153 Ok(BlockCommitmentsPage {
154 blocks,
155 next_from_height: self.next_from_height as u32,
156 })
157 }
158}
159
160#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn parse_tree_state_full() {
170 let zero_b64 = BASE64_STANDARD.encode([0u8; 32]);
172 let json = format!(
173 r#"{{"tree":{{"next_index":42,"root":"{}","height":10}}}}"#,
174 zero_b64
175 );
176 let resp: QueryLatestTreeResponse = serde_json::from_str(&json).unwrap();
177 let state = resp.tree.unwrap().into_tree_state().unwrap();
178 assert_eq!(state.next_index, 42);
179 assert_eq!(state.height, 10);
180 assert_eq!(state.root, Fp::zero());
181 }
182
183 #[test]
184 fn parse_tree_state_missing_root() {
185 let json = r#"{"tree":{"next_index":0,"height":0}}"#;
186 let resp: QueryLatestTreeResponse = serde_json::from_str(json).unwrap();
187 let state = resp.tree.unwrap().into_tree_state().unwrap();
188 assert_eq!(state.root, Fp::zero());
189 }
190
191 #[test]
192 fn parse_tree_state_null_tree() {
193 let json = r#"{"tree":null}"#;
194 let resp: QueryLatestTreeResponse = serde_json::from_str(json).unwrap();
195 assert!(resp.tree.is_none());
196 }
197
198 #[test]
199 fn parse_block_commitments_with_leaves() {
200 let one_bytes = Fp::from(1).to_repr();
202 let one_b64 = BASE64_STANDARD.encode(one_bytes);
203 let json = format!(
204 r#"{{"blocks":[{{"height":5,"start_index":0,"leaves":["{}","{}"],"root":"{}"}}],"next_from_height":9}}"#,
205 one_b64, one_b64, one_b64
206 );
207 let resp: QueryCommitmentLeavesResponse = serde_json::from_str(&json).unwrap();
208 let page = resp.into_block_commitments_page().unwrap();
209 assert_eq!(page.next_from_height, 9);
210 assert_eq!(page.blocks.len(), 1);
211 let block = &page.blocks[0];
212 assert_eq!(block.height, 5);
213 assert_eq!(block.start_index, 0);
214 assert_eq!(block.leaves.len(), 2);
215 assert_eq!(block.leaves[0].inner(), Fp::from(1));
216 assert_eq!(block.root, Fp::from(1));
217 }
218
219 #[test]
220 fn parse_empty_blocks() {
221 let json = r#"{"blocks":[],"next_from_height":0}"#;
222 let resp: QueryCommitmentLeavesResponse = serde_json::from_str(json).unwrap();
223 let page = resp.into_block_commitments_page().unwrap();
224 assert!(page.blocks.is_empty());
225 assert_eq!(page.next_from_height, 0);
226 }
227
228 #[test]
229 fn parse_omitted_blocks_field() {
230 let json = r#"{}"#;
232 let resp: QueryCommitmentLeavesResponse = serde_json::from_str(json).unwrap();
233 let page = resp.into_block_commitments_page().unwrap();
234 assert!(page.blocks.is_empty());
235 assert_eq!(page.next_from_height, 0);
236 }
237
238 #[test]
239 fn parse_block_commitments_rejects_missing_root() {
240 let one_b64 = BASE64_STANDARD.encode(Fp::from(1).to_repr());
241 let json = format!(
242 r#"{{"blocks":[{{"height":5,"start_index":0,"leaves":["{}"]}}]}}"#,
243 one_b64
244 );
245 let resp: QueryCommitmentLeavesResponse = serde_json::from_str(&json).unwrap();
246 let err = resp.into_block_commitments_page().unwrap_err();
247 assert!(matches!(
248 err,
249 ParseError::MissingField("block_commitments.root")
250 ));
251 }
252
253 #[test]
254 fn decode_fp_rejects_short_base64() {
255 let short = BASE64_STANDARD.encode([0u8; 16]);
256 let err = decode_fp_base64(&short, "test").unwrap_err();
257 assert!(matches!(err, ParseError::InvalidFpLength { len: 16, .. }));
258 }
259
260 #[test]
261 fn decode_fp_rejects_non_canonical() {
262 let bad = BASE64_STANDARD.encode([0xFF; 32]);
264 let err = decode_fp_base64(&bad, "test").unwrap_err();
265 assert!(matches!(err, ParseError::NonCanonicalFp { .. }));
266 }
267}