1use ethabi::{decode, ParamType, Token};
4use sha3::{Digest, Keccak256};
5use thiserror::Error;
6
7pub fn note_added_topic0_hex() -> String {
9 format!(
10 "0x{}",
11 hex::encode(Keccak256::digest(
12 b"NoteAdded(bytes32,bytes,bytes,bytes32,bytes32,bytes32)"
13 ))
14 )
15}
16
17pub fn note_added_legacy_topic0_hex() -> String {
19 format!(
20 "0x{}",
21 hex::encode(Keccak256::digest(
22 b"NoteAdded(bytes32,bytes,bytes32,bytes32)"
23 ))
24 )
25}
26
27pub fn note_added_topic0_alternatives() -> Vec<String> {
29 vec![
30 note_added_topic0_hex(),
31 note_added_legacy_topic0_hex(),
32 ]
33}
34
35pub fn note_confirmed_topic0_hex() -> String {
37 format!(
38 "0x{}",
39 hex::encode(Keccak256::digest(b"NoteConfirmed(bytes32,bytes32,uint256)"))
40 )
41}
42
43pub fn shield_completed_topic0_hex() -> String {
45 format!(
46 "0x{}",
47 hex::encode(Keccak256::digest(b"ShieldCompleted(bytes32,uint256)"))
48 )
49}
50
51#[derive(Debug, Clone)]
52pub struct DecodedNoteAdded {
53 pub cmx: [u8; 32],
54 pub enc_ciphertext: Vec<u8>,
55 pub out_ciphertext: Vec<u8>,
57 pub epk: [u8; 32],
58 pub nf_old: [u8; 32],
59 pub cv_net_x: Option<[u8; 32]>,
61}
62
63#[derive(Debug, Error)]
64pub enum LogDecodeError {
65 #[error("invalid topics/data for NoteAdded")]
66 BadNoteAdded,
67 #[error("invalid topics/data for NoteConfirmed")]
68 BadNoteConfirmed,
69 #[error("invalid topics/data for ShieldCompleted")]
70 BadShieldCompleted,
71 #[error("ethabi decode: {0}")]
72 EthAbi(String),
73}
74
75fn topic_to_bytes32(topic: &str) -> Option<[u8; 32]> {
76 let t = topic.strip_prefix("0x").unwrap_or(topic);
77 let b = hex::decode(t).ok()?;
78 if b.len() != 32 {
79 return None;
80 }
81 Some(b.try_into().ok()?)
82}
83
84fn decode_note_added_v2(data: &[u8]) -> Result<DecodedNoteAdded, LogDecodeError> {
85 let tokens = decode(
86 &[
87 ParamType::Bytes,
88 ParamType::Bytes,
89 ParamType::FixedBytes(32),
90 ParamType::FixedBytes(32),
91 ParamType::FixedBytes(32),
92 ],
93 data,
94 )
95 .map_err(|e| LogDecodeError::EthAbi(e.to_string()))?;
96 if tokens.len() != 5 {
97 return Err(LogDecodeError::BadNoteAdded);
98 }
99 let enc = match &tokens[0] {
100 Token::Bytes(b) => b.clone(),
101 _ => return Err(LogDecodeError::BadNoteAdded),
102 };
103 let out = match &tokens[1] {
104 Token::Bytes(b) => b.clone(),
105 _ => return Err(LogDecodeError::BadNoteAdded),
106 };
107 let epk = token_bytes32(&tokens[2])?;
108 let nf_old = token_bytes32(&tokens[3])?;
109 let cv_net_x = token_bytes32(&tokens[4])?;
110 Ok(DecodedNoteAdded {
111 cmx: [0u8; 32], enc_ciphertext: enc,
113 out_ciphertext: out,
114 epk,
115 nf_old,
116 cv_net_x: Some(cv_net_x),
117 })
118}
119
120fn decode_note_added_legacy(data: &[u8]) -> Result<DecodedNoteAdded, LogDecodeError> {
121 let tokens = decode(
122 &[
123 ParamType::Bytes,
124 ParamType::FixedBytes(32),
125 ParamType::FixedBytes(32),
126 ],
127 data,
128 )
129 .map_err(|e| LogDecodeError::EthAbi(e.to_string()))?;
130 if tokens.len() != 3 {
131 return Err(LogDecodeError::BadNoteAdded);
132 }
133 let enc = match &tokens[0] {
134 Token::Bytes(b) => b.clone(),
135 _ => return Err(LogDecodeError::BadNoteAdded),
136 };
137 let epk = token_bytes32(&tokens[1])?;
138 let nf_old = token_bytes32(&tokens[2])?;
139 Ok(DecodedNoteAdded {
140 cmx: [0u8; 32],
141 enc_ciphertext: enc,
142 out_ciphertext: Vec::new(),
143 epk,
144 nf_old,
145 cv_net_x: None,
146 })
147}
148
149pub fn decode_note_added_log(topics: &[String], data_hex: &str) -> Result<DecodedNoteAdded, LogDecodeError> {
154 let cmx = topics
155 .get(1)
156 .and_then(|t| topic_to_bytes32(t))
157 .ok_or(LogDecodeError::BadNoteAdded)?;
158 let raw = hex::decode(data_hex.strip_prefix("0x").unwrap_or(data_hex))
159 .map_err(|_| LogDecodeError::BadNoteAdded)?;
160
161 let norm = |s: &str| {
162 s.strip_prefix("0x")
163 .unwrap_or(s)
164 .to_ascii_lowercase()
165 };
166 let topic0 = topics.first().map(|s| norm(s));
167 let legacy = norm(¬e_added_legacy_topic0_hex());
168 let current = norm(¬e_added_topic0_hex());
169 let mut decoded = if topic0.as_deref() == Some(current.as_str()) {
170 decode_note_added_v2(&raw)?
171 } else if topic0.as_deref() == Some(legacy.as_str()) {
172 decode_note_added_v2(&raw).or_else(|_| decode_note_added_legacy(&raw))?
175 } else {
176 decode_note_added_v2(&raw).or_else(|_| decode_note_added_legacy(&raw))?
177 };
178 decoded.cmx = cmx;
179 Ok(decoded)
180}
181
182fn token_bytes32(t: &Token) -> Result<[u8; 32], LogDecodeError> {
183 match t {
184 Token::FixedBytes(b) if b.len() == 32 => Ok(b[..].try_into().unwrap()),
185 Token::Bytes(b) if b.len() == 32 => Ok(b[..].try_into().unwrap()),
186 _ => Err(LogDecodeError::BadNoteAdded),
187 }
188}
189
190pub fn decode_note_confirmed_log(topics: &[String], data_hex: &str) -> Result<([u8; 32], [u8; 32], u64), LogDecodeError> {
192 let cmx = topics
193 .get(1)
194 .and_then(|t| topic_to_bytes32(t))
195 .ok_or(LogDecodeError::BadNoteConfirmed)?;
196 let raw = hex::decode(data_hex.strip_prefix("0x").unwrap_or(data_hex))
197 .map_err(|_| LogDecodeError::BadNoteConfirmed)?;
198 let tokens = decode(&[ParamType::FixedBytes(32), ParamType::Uint(256)], &raw)
200 .map_err(|e| LogDecodeError::EthAbi(e.to_string()))?;
201 let root = token_bytes32_confirmed(tokens.first().ok_or(LogDecodeError::BadNoteConfirmed)?)?;
202 let position = match tokens.get(1) {
203 Some(Token::Uint(u)) => u64::try_from(*u).unwrap_or(u64::MAX),
204 _ => return Err(LogDecodeError::BadNoteConfirmed),
205 };
206 Ok((cmx, root, position))
207}
208
209fn token_bytes32_confirmed(t: &Token) -> Result<[u8; 32], LogDecodeError> {
210 match t {
211 Token::FixedBytes(b) if b.len() == 32 => Ok(b[..].try_into().unwrap()),
212 _ => Err(LogDecodeError::BadNoteConfirmed),
213 }
214}
215
216pub fn decode_shield_completed_log(topics: &[String], data_hex: &str) -> Result<([u8; 32], u128), LogDecodeError> {
217 let cmx = topics
218 .get(1)
219 .and_then(|t| topic_to_bytes32(t))
220 .ok_or(LogDecodeError::BadShieldCompleted)?;
221 let raw = hex::decode(data_hex.strip_prefix("0x").unwrap_or(data_hex))
222 .map_err(|_| LogDecodeError::BadShieldCompleted)?;
223 let tokens =
224 decode(&[ParamType::Uint(256)], &raw).map_err(|e| LogDecodeError::EthAbi(e.to_string()))?;
225 let amt = match tokens.get(0) {
226 Some(Token::Uint(u)) => u128::try_from(*u).map_err(|_| LogDecodeError::BadShieldCompleted)?,
227 _ => return Err(LogDecodeError::BadShieldCompleted),
228 };
229 Ok((cmx, amt))
230}
231
232#[cfg(test)]
233mod tests {
234 use ethabi::{encode, Token};
235
236 use super::*;
237
238 #[test]
239 fn topic0_lengths() {
240 assert_eq!(
241 note_added_topic0_hex().len(),
242 2 + 64,
243 "topic0 is 32 bytes hex"
244 );
245 assert_ne!(
246 note_added_topic0_hex(),
247 note_added_legacy_topic0_hex(),
248 "new NoteAdded changes topic0"
249 );
250 }
251
252 #[test]
253 fn note_added_v2_roundtrip() {
254 let cmx = [0x11u8; 32];
255 let enc = vec![0xABu8; 580];
256 let out = vec![0xCDu8; 80];
257 let epk = [0x22u8; 32];
258 let nf = [0x33u8; 32];
259 let cv = [0x44u8; 32];
260 let data = encode(&[
261 Token::Bytes(enc.clone()),
262 Token::Bytes(out.clone()),
263 Token::FixedBytes(epk.to_vec()),
264 Token::FixedBytes(nf.to_vec()),
265 Token::FixedBytes(cv.to_vec()),
266 ]);
267 let topics = vec![
268 note_added_topic0_hex(),
269 format!("0x{}", hex::encode(cmx)),
270 ];
271 let decoded =
272 decode_note_added_log(&topics, &format!("0x{}", hex::encode(&data))).unwrap();
273 assert_eq!(decoded.cmx, cmx);
274 assert_eq!(decoded.enc_ciphertext, enc);
275 assert_eq!(decoded.out_ciphertext, out);
276 assert_eq!(decoded.epk, epk);
277 assert_eq!(decoded.nf_old, nf);
278 assert_eq!(decoded.cv_net_x, Some(cv));
279 }
280
281 #[test]
282 fn note_added_legacy_roundtrip() {
283 let cmx = [0x55u8; 32];
284 let enc = vec![0x01u8; 580];
285 let epk = [0x66u8; 32];
286 let nf = [0x77u8; 32];
287 let data = encode(&[
288 Token::Bytes(enc.clone()),
289 Token::FixedBytes(epk.to_vec()),
290 Token::FixedBytes(nf.to_vec()),
291 ]);
292 let topics = vec![
293 note_added_legacy_topic0_hex(),
294 format!("0x{}", hex::encode(cmx)),
295 ];
296 let decoded =
297 decode_note_added_log(&topics, &format!("0x{}", hex::encode(&data))).unwrap();
298 assert_eq!(decoded.cmx, cmx);
299 assert_eq!(decoded.enc_ciphertext, enc);
300 assert!(decoded.out_ciphertext.is_empty());
301 assert_eq!(decoded.cv_net_x, None);
302 }
303
304 #[test]
305 fn note_added_legacy_topic_v2_body_roundtrip() {
306 let cmx = [0x88u8; 32];
307 let enc = vec![0xABu8; 580];
308 let out = vec![0xCDu8; 80];
309 let epk = [0x22u8; 32];
310 let nf = [0x33u8; 32];
311 let cv = [0x44u8; 32];
312 let data = encode(&[
313 Token::Bytes(enc.clone()),
314 Token::Bytes(out.clone()),
315 Token::FixedBytes(epk.to_vec()),
316 Token::FixedBytes(nf.to_vec()),
317 Token::FixedBytes(cv.to_vec()),
318 ]);
319 let topics = vec![
320 note_added_legacy_topic0_hex(),
321 format!("0x{}", hex::encode(cmx)),
322 ];
323 let decoded =
324 decode_note_added_log(&topics, &format!("0x{}", hex::encode(&data))).unwrap();
325 assert_eq!(decoded.out_ciphertext, out);
326 assert_eq!(decoded.cv_net_x, Some(cv));
327 }
328}