1pub mod groth16_proof;
13pub use groth16_proof::{
14 encode_groth16_proof_components, encode_groth16_proof_from_snarkjs_json,
15 p_a_from_snarkjs_pi_a, p_b_from_snarkjs_pi_b, p_c_from_snarkjs_pi_c, Groth16ProofError,
16};
17
18mod bundle_decode;
19mod events;
20mod perc20;
21pub use bundle_decode::{
22 bundle_actions_by_cmx, decode_bundle_calldata, BundleActionCiphertexts, BundleDecodeError,
23};
24pub use events::{
25 decode_note_added_log, decode_note_confirmed_log, decode_shield_completed_log,
26 decode_shielded_log, decode_shield_pool_created_log, decode_unshielded_log,
27 note_added_legacy_topic0_hex, note_added_topic0_alternatives, note_added_topic0_hex,
28 note_confirmed_topic0_hex, perc20_created_topic0_hex, shield_completed_topic0_hex,
29 shield_pool_created_topic0_hex, shield_pool_deployed_topic0_hex, shielded_topic0_hex,
30 unshielded_topic0_hex, DecodedNoteAdded, DecodedShielded,
31 DecodedShieldPoolCreated, LogDecodeError,
32};
33pub use perc20::{
34 compute_swap_id, encode_perc20_transfer_calldata, encode_perc20_transfer_executor_calldata,
35 encode_swap_initiate_calldata, encode_swap_join_calldata, encode_swap_settle_calldata,
36 encode_wrapped_shield_calldata, encode_wrapped_unshield_calldata, perc20_transfer_executor_selector,
37 perc20_transfer_selector, privacy_call_commit, swap_initiate_selector, swap_join_selector,
38 swap_settle_selector, wrapped_shield_selector, wrapped_unshield_selector, PrivacyCallArgs,
39};
40
41use ethabi::{encode, Token, Uint};
42use sha3::{Digest, Keccak256};
43use thiserror::Error;
44
45#[derive(Debug, Error)]
48pub enum EthEncodeError {
49 #[error("enc_ciphertext must be 580 bytes (Orchard in-band), got {0}")]
50 BadEncLen(usize),
51}
52
53#[derive(Debug, Clone)]
57pub struct BundleActionArgs {
58 pub cmx: [u8; 32],
59 pub enc_ciphertext: Vec<u8>,
60 pub out_ciphertext: Vec<u8>,
61 pub epk: [u8; 32],
62 pub nf_old: [u8; 32],
63 pub anchor: [u8; 32],
64 pub proof: Vec<u8>,
65 pub pub_fields: [[u8; 32]; 8],
67 pub spend_auth_sig: [[u8; 32]; 3],
68}
69
70#[derive(Debug, Clone)]
73pub struct BundleCalldataArgs {
74 pub actions: Vec<BundleActionArgs>,
75 pub value_balance: [u8; 32],
77 pub amount: u64,
79 pub recipient_meta: [u8; 32],
81 pub binding_sig: [[u8; 32]; 3],
83}
84
85pub fn bundle_function_selector() -> [u8; 4] {
88 Keccak256::digest(
89 b"bundle((bytes32,bytes,bytes,bytes32,bytes32,bytes32,bytes,uint256[8],uint256[3])[],uint256,uint256,bytes32,uint256[3])",
90 )[..4]
91 .try_into()
92 .expect("selector is 4 bytes")
93}
94
95pub fn encode_bundle_calldata(args: &BundleCalldataArgs) -> Result<Vec<u8>, EthEncodeError> {
97 let actions_token = Token::Array(
98 args.actions
99 .iter()
100 .map(|a| {
101 let pub_fields_token = Token::FixedArray(
102 a.pub_fields
103 .iter()
104 .map(|b| Token::Uint(ethabi::Uint::from_big_endian(b)))
105 .collect(),
106 );
107 let spend_auth_sig_token = Token::FixedArray(
108 a.spend_auth_sig
109 .iter()
110 .map(|b| Token::Uint(ethabi::Uint::from_big_endian(b)))
111 .collect(),
112 );
113 Token::Tuple(vec![
114 Token::FixedBytes(a.cmx.to_vec()),
115 Token::Bytes(a.enc_ciphertext.clone()),
116 Token::Bytes(a.out_ciphertext.clone()),
117 Token::FixedBytes(a.epk.to_vec()),
118 Token::FixedBytes(a.nf_old.to_vec()),
119 Token::FixedBytes(a.anchor.to_vec()),
120 Token::Bytes(a.proof.clone()),
121 pub_fields_token,
122 spend_auth_sig_token,
123 ])
124 })
125 .collect(),
126 );
127 let binding_sig_token = Token::FixedArray(
128 args.binding_sig
129 .iter()
130 .map(|b| Token::Uint(ethabi::Uint::from_big_endian(b)))
131 .collect(),
132 );
133 let tokens = vec![
134 actions_token,
135 Token::Uint(ethabi::Uint::from_big_endian(&args.value_balance)),
136 Token::Uint(ethabi::Uint::from(args.amount)),
137 Token::FixedBytes(args.recipient_meta.to_vec()),
138 binding_sig_token,
139 ];
140 let body = encode(&tokens);
141 let mut out = Vec::with_capacity(4 + body.len());
142 out.extend_from_slice(&bundle_function_selector());
143 out.extend_from_slice(&body);
144 Ok(out)
145}
146
147pub const BJJ_SUBGROUP_ORDER_DEC: &str =
151 "2736030358979909402780800718157159386076813972158567259200215660948447373041";
152
153#[inline]
154fn bjj_subgroup_order() -> Uint {
155 Uint::from_dec_str(BJJ_SUBGROUP_ORDER_DEC).expect("valid EIP-2494 subgroup constant")
156}
157
158#[inline]
160pub fn reduce_mod_bjj_subgroup(v: Uint) -> Uint {
161 let l = bjj_subgroup_order();
162 v % l
163}
164
165pub fn binding_challenge_e_bn254(
167 r_x_be32: &[u8; 32],
168 r_y_be32: &[u8; 32],
169 bvk_x_be32: &[u8; 32],
170 bvk_y_be32: &[u8; 32],
171 sighash: &[u8; 32],
172) -> Uint {
173 let mut h = Keccak256::new();
174 h.update(r_x_be32);
175 h.update(r_y_be32);
176 h.update(bvk_x_be32);
177 h.update(bvk_y_be32);
178 h.update(sighash);
179 let digest: [u8; 32] = h.finalize().into();
180 reduce_mod_bjj_subgroup(Uint::from_big_endian(&digest))
181}
182
183fn mulmod(mut a: Uint, mut b: Uint, m: Uint) -> Uint {
185 let mut result = Uint::zero();
186 a %= m;
187 while !b.is_zero() {
188 if b.bit(0) {
189 result = (result + a) % m;
190 }
191 a = (a + a) % m;
192 b >>= 1;
193 }
194 result
195}
196
197#[inline]
199pub fn binding_s_scalar_bn254(r_nonce: Uint, e: Uint, bsk: Uint) -> Uint {
200 let l = bjj_subgroup_order();
201 let r = r_nonce % l;
202 let e = e % l;
203 let bsk = bsk % l;
204 let prod = mulmod(e, bsk, l);
205 (r + prod) % l
206}
207
208#[inline]
209pub fn uint_to_be32(u: &Uint) -> [u8; 32] {
210 let mut out = [0u8; 32];
211 u.to_big_endian(&mut out);
212 out
213}
214
215pub fn circom_field_dec_to_be32(dec: &str) -> [u8; 32] {
217 uint_to_be32(
218 &Uint::from_dec_str(dec.trim()).expect("circom decimal field element"),
219 )
220}
221
222#[inline]
224pub fn uint_mod_subgroup_from_bn254_fr_repr_le(repr_le: &[u8; 32]) -> Uint {
225 let mut be = [0u8; 32];
226 for i in 0..32 {
227 be[i] = repr_le[31 - i];
228 }
229 reduce_mod_bjj_subgroup(Uint::from_big_endian(&be))
230}
231
232pub fn sum_rcv_mod_bjj(rcv_le_slices: &[[u8; 32]]) -> [u8; 32] {
239 let l = bjj_subgroup_order();
240 let mut acc = Uint::from(0u64);
241 for rcv in rcv_le_slices {
242 let u = uint_mod_subgroup_from_bn254_fr_repr_le(rcv);
243 acc = (acc + u) % l;
245 }
246 let be = uint_to_be32(&acc);
247 let mut le = [0u8; 32];
248 for i in 0..32 {
249 le[i] = be[31 - i];
250 }
251 le
252}
253
254pub const BN254_FR_BE: [u8; 32] = [
258 0x30, 0x64, 0x4e, 0x72, 0xe1, 0x31, 0xa0, 0x29, 0xb8, 0x50, 0x45, 0xb6, 0x81, 0x81, 0x58,
259 0x5d, 0x97, 0x81, 0x6a, 0x91, 0x68, 0x71, 0xca, 0x8d, 0x3c, 0x20, 0x8c, 0x16, 0xd8, 0x7c,
260 0xfd, 0x47,
261];
262
263#[derive(Debug, Error)]
264pub enum BindingSighashError {
265 #[error("pool_address must be 20-byte hex (40 hex chars), got len {0}")]
266 BadPoolAddress(usize),
267 #[error("invalid hex: {0}")]
268 Hex(String),
269}
270
271pub fn u256_be_chain_id(chain_id: u64) -> [u8; 32] {
273 let mut out = [0u8; 32];
274 out[24..32].copy_from_slice(&chain_id.to_be_bytes());
275 out
276}
277
278pub fn bundle_value_balance_be(amount_sats: u64, negative: bool) -> [u8; 32] {
290 let mut out = [0u8; 32];
291 out[24..32].copy_from_slice(&amount_sats.to_be_bytes());
292 if negative {
293 out[0] |= 0x80; }
295 out
296}
297
298pub fn decode_value_balance_be(vb: &[u8; 32]) -> (u64, bool) {
300 let negative = (vb[0] & 0x80) != 0;
301 let amount = u64::from_be_bytes(vb[24..32].try_into().unwrap());
302 (amount, negative)
303}
304
305pub fn value_balance_to_bjj_scalar_be(vb: &[u8; 32]) -> [u8; 32] {
311 let (amount, negative) = decode_value_balance_be(vb);
312 if !negative || amount == 0 {
313 let mut out = [0u8; 32];
314 out[24..32].copy_from_slice(&amount.to_be_bytes());
315 out
316 } else {
317 let l = bjj_subgroup_order();
319 let amt: Uint = amount.into();
320 uint_to_be32(&(l - amt % l))
321 }
322}
323
324#[deprecated(note = "use bundle_value_balance_be(amount, false)")]
328pub fn shield_bundle_value_balance_be(amount_sats: u64) -> [u8; 32] {
329 bundle_value_balance_be(amount_sats, false)
330}
331
332#[deprecated(note = "use bundle_value_balance_be(amount, true)")]
334pub fn shield_bundle_value_balance_subgroup_neg_be(amount_sats: u64) -> [u8; 32] {
335 bundle_value_balance_be(amount_sats, true)
336}
337
338pub fn parse_pool_address_hex(addr: &str) -> Result<[u8; 20], BindingSighashError> {
340 let clean = addr.strip_prefix("0x").unwrap_or(addr);
341 let bytes = hex::decode(clean).map_err(|e| BindingSighashError::Hex(e.to_string()))?;
342 if bytes.len() != 20 {
343 return Err(BindingSighashError::BadPoolAddress(bytes.len()));
344 }
345 Ok(bytes.try_into().unwrap())
346}
347
348pub fn parse_bytes32_hex(s: &str) -> Result<[u8; 32], BindingSighashError> {
350 let clean = s.strip_prefix("0x").unwrap_or(s);
351 let bytes = hex::decode(clean).map_err(|e| BindingSighashError::Hex(e.to_string()))?;
352 if bytes.len() != 32 {
353 return Err(BindingSighashError::Hex(format!(
354 "expected 32 bytes, got {}",
355 bytes.len()
356 )));
357 }
358 Ok(bytes.try_into().unwrap())
359}
360
361pub fn binding_sighash_privacy_pool_bundle_v1(
364 chain_id_be32: &[u8; 32],
365 contract_addr_20: &[u8; 20],
366 nullifiers: &[[u8; 32]],
367 commitments: &[[u8; 32]],
368 value_balance_be32: &[u8; 32],
369 recipient_meta: &[u8; 32],
370) -> [u8; 32] {
371 let mut h = Keccak256::new();
372 h.update(b"PrivacyPool.bundle.v1");
373 h.update(chain_id_be32);
374 h.update(contract_addr_20);
375 for nf in nullifiers {
376 h.update(nf);
377 }
378 for cm in commitments {
379 h.update(cm);
380 }
381 h.update(value_balance_be32);
382 h.update(recipient_meta);
383 h.finalize().into()
384}
385
386pub fn spend_auth_sighash_v1(
392 chain_id_be32: &[u8; 32],
393 contract_addr_20: &[u8; 20],
394 nf_old_be32: &[u8; 32],
395 cmx_be32: &[u8; 32],
396 epk_be32: &[u8; 32],
397 enc_ciphertext: &[u8],
398 out_ciphertext: &[u8],
399) -> [u8; 32] {
400 let enc_hash: [u8; 32] = Keccak256::digest(enc_ciphertext).into();
401 let out_hash: [u8; 32] = Keccak256::digest(out_ciphertext).into();
402 let mut h = Keccak256::new();
403 h.update(b"SpendAuth.action.v1");
404 h.update(chain_id_be32);
405 h.update(contract_addr_20);
406 h.update(nf_old_be32);
407 h.update(cmx_be32);
408 h.update(epk_be32);
409 h.update(&enc_hash);
410 h.update(&out_hash);
411 h.finalize().into()
412}
413
414#[derive(Debug, Clone)]
421pub struct FinalizeWithdrawCalldataArgs {
422 pub nf: [u8; 32],
423 pub amount_sats: u64,
424 pub recipient_meta: [u8; 32],
425}
426
427pub fn finalize_withdraw_function_selector() -> [u8; 4] {
429 Keccak256::digest(b"finalizeWithdraw(bytes32,uint256,bytes32)")[..4]
430 .try_into()
431 .expect("selector is 4 bytes")
432}
433
434pub fn encode_finalize_withdraw_calldata(args: &FinalizeWithdrawCalldataArgs) -> Vec<u8> {
436 let tokens = vec![
437 Token::FixedBytes(args.nf.to_vec()),
438 Token::Uint(args.amount_sats.into()),
439 Token::FixedBytes(args.recipient_meta.to_vec()),
440 ];
441 let body = encode(&tokens);
442 let mut out = Vec::with_capacity(4 + body.len());
443 out.extend_from_slice(&finalize_withdraw_function_selector());
444 out.extend_from_slice(&body);
445 out
446}
447
448#[derive(Debug, Clone)]
467pub struct ErcShieldCalldataArgs {
468 pub actions: Vec<BundleActionArgs>,
469 pub amount: u128,
471 pub owner: [u8; 20],
473 pub deadline: u64,
475 pub permit_v: u8,
477 pub permit_r: [u8; 32],
479 pub permit_s: [u8; 32],
481 pub binding_sig: [[u8; 32]; 3],
483}
484
485pub fn erc_shield_function_selector() -> [u8; 4] {
487 Keccak256::digest(
488 b"shield((bytes32,bytes,bytes,bytes32,bytes32,bytes32,bytes,uint256[8],uint256[3])[],uint256,address,uint256,uint8,bytes32,bytes32,uint256[3])",
489 )[..4]
490 .try_into()
491 .expect("selector is 4 bytes")
492}
493
494pub fn encode_erc_shield_calldata(args: &ErcShieldCalldataArgs) -> Result<Vec<u8>, EthEncodeError> {
496 let actions_token = Token::Array(
497 args.actions
498 .iter()
499 .map(|a| {
500 if a.enc_ciphertext.len() != 580 {
501 return Err(EthEncodeError::BadEncLen(a.enc_ciphertext.len()));
502 }
503 let pub_fields_token = Token::FixedArray(
504 a.pub_fields
505 .iter()
506 .map(|x| Token::Uint(Uint::from_big_endian(x)))
507 .collect(),
508 );
509 let spend_auth_token = Token::FixedArray(
510 a.spend_auth_sig
511 .iter()
512 .map(|x| Token::Uint(Uint::from_big_endian(x)))
513 .collect(),
514 );
515 Ok(Token::Tuple(vec![
516 Token::FixedBytes(a.cmx.to_vec()),
517 Token::Bytes(a.enc_ciphertext.clone()),
518 Token::Bytes(a.out_ciphertext.clone()),
519 Token::FixedBytes(a.epk.to_vec()),
520 Token::FixedBytes(a.nf_old.to_vec()),
521 Token::FixedBytes(a.anchor.to_vec()),
522 Token::Bytes(a.proof.clone()),
523 pub_fields_token,
524 spend_auth_token,
525 ]))
526 })
527 .collect::<Result<Vec<_>, _>>()?,
528 );
529
530 let owner_addr = ethabi::Address::from(args.owner);
532
533 let binding_sig_token = Token::FixedArray(
534 args.binding_sig
535 .iter()
536 .map(|x| Token::Uint(Uint::from_big_endian(x)))
537 .collect(),
538 );
539
540 let tokens = vec![
541 actions_token,
542 Token::Uint(args.amount.into()),
543 Token::Address(owner_addr),
544 Token::Uint(args.deadline.into()),
545 Token::Uint(args.permit_v.into()),
546 Token::FixedBytes(args.permit_r.to_vec()),
547 Token::FixedBytes(args.permit_s.to_vec()),
548 binding_sig_token,
549 ];
550
551 let body = encode(&tokens);
552 let mut out = Vec::with_capacity(4 + body.len());
553 out.extend_from_slice(&erc_shield_function_selector());
554 out.extend_from_slice(&body);
555 Ok(out)
556}
557
558pub fn evm_address_to_recipient_meta(addr: &[u8; 20]) -> [u8; 32] {
564 let mut meta = [0u8; 32];
565 meta[12..].copy_from_slice(addr);
566 meta
567}
568
569pub fn parse_evm_address_hex(s: &str) -> Result<[u8; 20], BindingSighashError> {
571 let clean = s.strip_prefix("0x").unwrap_or(s);
572 if clean.len() != 40 {
573 return Err(BindingSighashError::Hex(format!("expected 40 hex chars, got {}", clean.len())));
574 }
575 let bytes = hex::decode(clean).map_err(|e| BindingSighashError::Hex(e.to_string()))?;
576 Ok(bytes.try_into().expect("40 hex chars = 20 bytes"))
577}
578
579#[cfg(test)]
582mod tests {
583 use super::*;
584
585 #[test]
586 fn unshield_value_balance_roundtrip() {
587 let amount: u64 = 100_000;
589 let vb = bundle_value_balance_be(amount, false);
590 let mut expected = [0u8; 32];
591 expected[24..32].copy_from_slice(&amount.to_be_bytes());
592 assert_eq!(vb, expected);
593 }
594
595 #[test]
596 fn finalize_withdraw_calldata_prefixes_selector() {
597 let cd = encode_finalize_withdraw_calldata(&FinalizeWithdrawCalldataArgs {
598 nf: [7u8; 32],
599 amount_sats: 123_456,
600 recipient_meta: [8u8; 32],
601 });
602 assert!(cd.len() > 4);
603 assert_eq!(&cd[..4], &finalize_withdraw_function_selector());
604 }
605
606 #[test]
608 fn decode_failing_calldata_fields() {
609 let raw_hex = "00000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000000001c2fbf7c1d370880bd19943877bf41259025e4e877c56fbf26c4576b0e809b001d9022ffbe1768250cedb08257b1776b2f248a0854cd1a7245c42ae2c63ca5650446c7c76fa1736f7e8c47994073011a534af17c591b3d0b51e25b0c5cf57b5b000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000202dab531982047374d79feb10ee3389c3aae694c9d9a76696f1d034c245ac8263000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000004a03955242e589537ceca6b866b35cc16bad5605602c8a2bee463bb72612b6dbd9004cc117c66893f069f160294b22653b890e04abe242379d2dd98956778386fd400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000000000000000000000000000000000000003de55e0d54c5787b64a73d7879b67b9386e7262c20b6963c32d543462bdbc0e16858a895cb5ac289f5af0f0ad2918d6167a63370ed12dd7cd2210e53b6daee004cc117c66893f069f160294b22653b890e04abe242379d2dd98956778386fd417bc7b3fea209a7f90bb47b68453956704bcc38c3d32b1e1ef3c1909ec559f871f4fa732aa884e7020dccf264cf39bfade91074f00ec7b684f170b18531c4d4b2dab531982047374d79feb10ee3389c3aae694c9d9a76696f1d034c245ac82630ba6a64dab4e9fa2f31c9f5a35f3a93c323b160c175398b260a271a094c0fa7713460b57b63c8619062eff726135cabd3a4faf6c56e51afbf173a885d698f5ec026015d5bd814267fb2c44bc2a2336dfa946866960315f49761800cae310ed2c0000000000000000000000000000000000000000000000000000000000000244";
611
612 let body = hex::decode(raw_hex).expect("valid hex");
613
614 let read_u256_at = |offset: usize| -> u128 {
615 let word = &body[offset..offset+32];
616 u128::from_be_bytes(word[16..32].try_into().unwrap())
617 };
618 let read_bytes32_at = |offset: usize| -> String {
619 hex::encode(&body[offset..offset+32])
620 };
621
622 println!("=== Top-level ===");
624 println!("W0 offset_to_actions: {}", read_u256_at(0));
625 println!("W1 valueBalance: {}", read_u256_at(32));
626 println!("W2 amount: {}", read_u256_at(64));
627 println!("W3 recipientMeta: {}", read_bytes32_at(96));
628 println!("W4 bindingSig[0]: {}", read_bytes32_at(128));
629 println!("W5 bindingSig[1]: {}", read_bytes32_at(160));
630 println!("W6 bindingSig[2]: {}", read_bytes32_at(192));
631
632 println!("\n=== actions[] at body offset 224 ===");
634 println!("W7 length: {}", read_u256_at(224));
635 println!("W8 elem0 offset: {}", read_u256_at(256));
636
637 let s = 288_usize;
639 println!("\n=== BundleAction[0] struct at body offset {} ===", s);
640 println!(" cmx: {}", read_bytes32_at(s));
641 let enc_off = read_u256_at(s + 32) as usize;
642 let out_off = read_u256_at(s + 64) as usize;
643 let proof_off= read_u256_at(s + 192) as usize; println!(" enc_offset: {} (0x{:x})", enc_off, enc_off);
645 println!(" out_offset: {} (0x{:x})", out_off, out_off);
646 println!(" epk: {}", read_bytes32_at(s + 96));
647 println!(" nfOld: {}", read_bytes32_at(s + 128));
648 println!(" anchor: {}", read_bytes32_at(s + 160));
649 println!(" proof_offset: {} (0x{:x}) [should be 1312 = 0x520]", proof_off, proof_off);
650 println!(" pubInputs[0]: {}", read_bytes32_at(s + 224));
651 println!(" pubInputs[1]: {}", read_bytes32_at(s + 256));
652 println!(" pubInputs[6]: {}", read_bytes32_at(s + 224 + 6*32));
653
654 let proof_data_body_off = s + proof_off;
656 println!("\n=== Proof data at body offset {} (struct + {}) ===", proof_data_body_off, proof_off);
657 if proof_data_body_off + 32 <= body.len() {
658 let claimed_len = read_u256_at(proof_data_body_off);
659 println!(" claimed length: {} (0x{:x})", claimed_len, claimed_len);
660 } else {
661 println!(" OUT OF BOUNDS (body len = {})", body.len());
662 }
663
664 let enc_abs = s + enc_off;
666 if enc_abs + 32 <= body.len() {
667 let enc_len = read_u256_at(enc_abs);
668 println!("\nenc data at body offset {}: length = {} (expected 580)", enc_abs, enc_len);
669 }
670
671 assert_eq!(proof_off, 82, "confirmed proof_offset = 82 = 0x52 (BUG)");
673 assert_ne!(proof_off, 1312, "proof_offset should be 1312 but it is 82");
674 }
675 #[test]
686 fn bundle_calldata_proof_offset_is_correct() {
687 let proof_bytes = vec![0xabu8; 256]; let enc = vec![0u8; 580];
689 let out = vec![0u8; 80];
690
691 let cd = encode_bundle_calldata(&BundleCalldataArgs {
692 actions: vec![BundleActionArgs {
693 cmx: [1u8; 32],
694 enc_ciphertext: enc,
695 out_ciphertext: out,
696 epk: [2u8; 32],
697 nf_old: [3u8; 32],
698 anchor: [4u8; 32],
699 proof: proof_bytes,
700 pub_fields: [[5u8; 32]; 8],
701 spend_auth_sig: [[6u8; 32]; 3],
702 }],
703 value_balance: [0u8; 32],
704 amount: 0,
705 recipient_meta: [0u8; 32],
706 binding_sig: [[7u8; 32]; 3],
707 })
708 .expect("encode_bundle_calldata failed");
709
710 let body = &cd[4..]; let struct_start = 9 * 32_usize;
735
736 let read_u256 = |offset: usize| -> u128 {
737 let word = &body[offset..offset + 32];
738 u128::from_be_bytes(word[16..32].try_into().unwrap())
739 };
740
741 let enc_offset = read_u256(struct_start + 32); let out_offset = read_u256(struct_start + 64); let proof_offset = read_u256(struct_start + 192); assert_eq!(enc_offset, 576, "enc_offset should be 0x240 = 576");
746 assert_eq!(out_offset, 1216, "out_offset should be 0x4c0 = 1216");
747 assert_eq!(proof_offset, 1344, "proof_offset should be 0x540 = 1344, got {proof_offset:#x}");
748 }
749}