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;
20pub use bundle_decode::{
21 bundle_actions_by_cmx, decode_bundle_calldata, BundleActionCiphertexts, BundleDecodeError,
22};
23pub use events::{
24 decode_note_added_log, decode_note_confirmed_log, decode_shield_completed_log,
25 note_added_legacy_topic0_hex, note_added_topic0_alternatives, note_added_topic0_hex,
26 note_confirmed_topic0_hex, shield_completed_topic0_hex,
27 DecodedNoteAdded, LogDecodeError,
28};
29
30use ethabi::{encode, Token, Uint};
31use sha3::{Digest, Keccak256};
32use thiserror::Error;
33
34#[derive(Debug, Error)]
37pub enum EthEncodeError {
38 #[error("enc_ciphertext must be 580 bytes (Orchard in-band), got {0}")]
39 BadEncLen(usize),
40}
41
42#[derive(Debug, Clone)]
46pub struct BundleActionArgs {
47 pub cmx: [u8; 32],
48 pub enc_ciphertext: Vec<u8>,
49 pub out_ciphertext: Vec<u8>,
50 pub epk: [u8; 32],
51 pub nf_old: [u8; 32],
52 pub anchor: [u8; 32],
53 pub proof: Vec<u8>,
54 pub pub_fields: [[u8; 32]; 8],
56 pub spend_auth_sig: [[u8; 32]; 3],
57}
58
59#[derive(Debug, Clone)]
62pub struct BundleCalldataArgs {
63 pub actions: Vec<BundleActionArgs>,
64 pub value_balance: [u8; 32],
66 pub amount: u64,
68 pub recipient_meta: [u8; 32],
70 pub binding_sig: [[u8; 32]; 3],
72}
73
74pub fn bundle_function_selector() -> [u8; 4] {
77 Keccak256::digest(
78 b"bundle((bytes32,bytes,bytes,bytes32,bytes32,bytes32,bytes,uint256[8],uint256[3])[],uint256,uint256,bytes32,uint256[3])",
79 )[..4]
80 .try_into()
81 .expect("selector is 4 bytes")
82}
83
84pub fn encode_bundle_calldata(args: &BundleCalldataArgs) -> Result<Vec<u8>, EthEncodeError> {
86 let actions_token = Token::Array(
87 args.actions
88 .iter()
89 .map(|a| {
90 let pub_fields_token = Token::FixedArray(
91 a.pub_fields
92 .iter()
93 .map(|b| Token::Uint(ethabi::Uint::from_big_endian(b)))
94 .collect(),
95 );
96 let spend_auth_sig_token = Token::FixedArray(
97 a.spend_auth_sig
98 .iter()
99 .map(|b| Token::Uint(ethabi::Uint::from_big_endian(b)))
100 .collect(),
101 );
102 Token::Tuple(vec![
103 Token::FixedBytes(a.cmx.to_vec()),
104 Token::Bytes(a.enc_ciphertext.clone()),
105 Token::Bytes(a.out_ciphertext.clone()),
106 Token::FixedBytes(a.epk.to_vec()),
107 Token::FixedBytes(a.nf_old.to_vec()),
108 Token::FixedBytes(a.anchor.to_vec()),
109 Token::Bytes(a.proof.clone()),
110 pub_fields_token,
111 spend_auth_sig_token,
112 ])
113 })
114 .collect(),
115 );
116 let binding_sig_token = Token::FixedArray(
117 args.binding_sig
118 .iter()
119 .map(|b| Token::Uint(ethabi::Uint::from_big_endian(b)))
120 .collect(),
121 );
122 let tokens = vec![
123 actions_token,
124 Token::Uint(ethabi::Uint::from_big_endian(&args.value_balance)),
125 Token::Uint(ethabi::Uint::from(args.amount)),
126 Token::FixedBytes(args.recipient_meta.to_vec()),
127 binding_sig_token,
128 ];
129 let body = encode(&tokens);
130 let mut out = Vec::with_capacity(4 + body.len());
131 out.extend_from_slice(&bundle_function_selector());
132 out.extend_from_slice(&body);
133 Ok(out)
134}
135
136pub const BJJ_SUBGROUP_ORDER_DEC: &str =
140 "2736030358979909402780800718157159386076813972158567259200215660948447373041";
141
142#[inline]
143fn bjj_subgroup_order() -> Uint {
144 Uint::from_dec_str(BJJ_SUBGROUP_ORDER_DEC).expect("valid EIP-2494 subgroup constant")
145}
146
147#[inline]
149pub fn reduce_mod_bjj_subgroup(v: Uint) -> Uint {
150 let l = bjj_subgroup_order();
151 v % l
152}
153
154pub fn binding_challenge_e_bn254(
156 r_x_be32: &[u8; 32],
157 r_y_be32: &[u8; 32],
158 bvk_x_be32: &[u8; 32],
159 bvk_y_be32: &[u8; 32],
160 sighash: &[u8; 32],
161) -> Uint {
162 let mut h = Keccak256::new();
163 h.update(r_x_be32);
164 h.update(r_y_be32);
165 h.update(bvk_x_be32);
166 h.update(bvk_y_be32);
167 h.update(sighash);
168 let digest: [u8; 32] = h.finalize().into();
169 reduce_mod_bjj_subgroup(Uint::from_big_endian(&digest))
170}
171
172fn mulmod(mut a: Uint, mut b: Uint, m: Uint) -> Uint {
174 let mut result = Uint::zero();
175 a %= m;
176 while !b.is_zero() {
177 if b.bit(0) {
178 result = (result + a) % m;
179 }
180 a = (a + a) % m;
181 b >>= 1;
182 }
183 result
184}
185
186#[inline]
188pub fn binding_s_scalar_bn254(r_nonce: Uint, e: Uint, bsk: Uint) -> Uint {
189 let l = bjj_subgroup_order();
190 let r = r_nonce % l;
191 let e = e % l;
192 let bsk = bsk % l;
193 let prod = mulmod(e, bsk, l);
194 (r + prod) % l
195}
196
197#[inline]
198pub fn uint_to_be32(u: &Uint) -> [u8; 32] {
199 let mut out = [0u8; 32];
200 u.to_big_endian(&mut out);
201 out
202}
203
204pub fn circom_field_dec_to_be32(dec: &str) -> [u8; 32] {
206 uint_to_be32(
207 &Uint::from_dec_str(dec.trim()).expect("circom decimal field element"),
208 )
209}
210
211#[inline]
213pub fn uint_mod_subgroup_from_bn254_fr_repr_le(repr_le: &[u8; 32]) -> Uint {
214 let mut be = [0u8; 32];
215 for i in 0..32 {
216 be[i] = repr_le[31 - i];
217 }
218 reduce_mod_bjj_subgroup(Uint::from_big_endian(&be))
219}
220
221pub fn sum_rcv_mod_bjj(rcv_le_slices: &[[u8; 32]]) -> [u8; 32] {
228 let l = bjj_subgroup_order();
229 let mut acc = Uint::from(0u64);
230 for rcv in rcv_le_slices {
231 let u = uint_mod_subgroup_from_bn254_fr_repr_le(rcv);
232 acc = (acc + u) % l;
234 }
235 let be = uint_to_be32(&acc);
236 let mut le = [0u8; 32];
237 for i in 0..32 {
238 le[i] = be[31 - i];
239 }
240 le
241}
242
243pub const BN254_FR_BE: [u8; 32] = [
247 0x30, 0x64, 0x4e, 0x72, 0xe1, 0x31, 0xa0, 0x29, 0xb8, 0x50, 0x45, 0xb6, 0x81, 0x81, 0x58,
248 0x5d, 0x97, 0x81, 0x6a, 0x91, 0x68, 0x71, 0xca, 0x8d, 0x3c, 0x20, 0x8c, 0x16, 0xd8, 0x7c,
249 0xfd, 0x47,
250];
251
252#[derive(Debug, Error)]
253pub enum BindingSighashError {
254 #[error("pool_address must be 20-byte hex (40 hex chars), got len {0}")]
255 BadPoolAddress(usize),
256 #[error("invalid hex: {0}")]
257 Hex(String),
258}
259
260pub fn u256_be_chain_id(chain_id: u64) -> [u8; 32] {
262 let mut out = [0u8; 32];
263 out[24..32].copy_from_slice(&chain_id.to_be_bytes());
264 out
265}
266
267pub fn bundle_value_balance_be(amount_sats: u64, negative: bool) -> [u8; 32] {
279 let mut out = [0u8; 32];
280 out[24..32].copy_from_slice(&amount_sats.to_be_bytes());
281 if negative {
282 out[0] |= 0x80; }
284 out
285}
286
287pub fn decode_value_balance_be(vb: &[u8; 32]) -> (u64, bool) {
289 let negative = (vb[0] & 0x80) != 0;
290 let amount = u64::from_be_bytes(vb[24..32].try_into().unwrap());
291 (amount, negative)
292}
293
294pub fn value_balance_to_bjj_scalar_be(vb: &[u8; 32]) -> [u8; 32] {
300 let (amount, negative) = decode_value_balance_be(vb);
301 if !negative || amount == 0 {
302 let mut out = [0u8; 32];
303 out[24..32].copy_from_slice(&amount.to_be_bytes());
304 out
305 } else {
306 let l = bjj_subgroup_order();
308 let amt: Uint = amount.into();
309 uint_to_be32(&(l - amt % l))
310 }
311}
312
313#[deprecated(note = "use bundle_value_balance_be(amount, false)")]
317pub fn shield_bundle_value_balance_be(amount_sats: u64) -> [u8; 32] {
318 bundle_value_balance_be(amount_sats, false)
319}
320
321#[deprecated(note = "use bundle_value_balance_be(amount, true)")]
323pub fn shield_bundle_value_balance_subgroup_neg_be(amount_sats: u64) -> [u8; 32] {
324 bundle_value_balance_be(amount_sats, true)
325}
326
327pub fn parse_pool_address_hex(addr: &str) -> Result<[u8; 20], BindingSighashError> {
329 let clean = addr.strip_prefix("0x").unwrap_or(addr);
330 let bytes = hex::decode(clean).map_err(|e| BindingSighashError::Hex(e.to_string()))?;
331 if bytes.len() != 20 {
332 return Err(BindingSighashError::BadPoolAddress(bytes.len()));
333 }
334 Ok(bytes.try_into().unwrap())
335}
336
337pub fn parse_bytes32_hex(s: &str) -> Result<[u8; 32], BindingSighashError> {
339 let clean = s.strip_prefix("0x").unwrap_or(s);
340 let bytes = hex::decode(clean).map_err(|e| BindingSighashError::Hex(e.to_string()))?;
341 if bytes.len() != 32 {
342 return Err(BindingSighashError::Hex(format!(
343 "expected 32 bytes, got {}",
344 bytes.len()
345 )));
346 }
347 Ok(bytes.try_into().unwrap())
348}
349
350pub fn binding_sighash_privacy_pool_bundle_v1(
353 chain_id_be32: &[u8; 32],
354 contract_addr_20: &[u8; 20],
355 nullifiers: &[[u8; 32]],
356 commitments: &[[u8; 32]],
357 value_balance_be32: &[u8; 32],
358 recipient_meta: &[u8; 32],
359) -> [u8; 32] {
360 let mut h = Keccak256::new();
361 h.update(b"PrivacyPool.bundle.v1");
362 h.update(chain_id_be32);
363 h.update(contract_addr_20);
364 for nf in nullifiers {
365 h.update(nf);
366 }
367 for cm in commitments {
368 h.update(cm);
369 }
370 h.update(value_balance_be32);
371 h.update(recipient_meta);
372 h.finalize().into()
373}
374
375pub fn spend_auth_sighash_v1(
381 chain_id_be32: &[u8; 32],
382 contract_addr_20: &[u8; 20],
383 nf_old_be32: &[u8; 32],
384 cmx_be32: &[u8; 32],
385 epk_be32: &[u8; 32],
386 enc_ciphertext: &[u8],
387 out_ciphertext: &[u8],
388) -> [u8; 32] {
389 let enc_hash: [u8; 32] = Keccak256::digest(enc_ciphertext).into();
390 let out_hash: [u8; 32] = Keccak256::digest(out_ciphertext).into();
391 let mut h = Keccak256::new();
392 h.update(b"SpendAuth.action.v1");
393 h.update(chain_id_be32);
394 h.update(contract_addr_20);
395 h.update(nf_old_be32);
396 h.update(cmx_be32);
397 h.update(epk_be32);
398 h.update(&enc_hash);
399 h.update(&out_hash);
400 h.finalize().into()
401}
402
403#[derive(Debug, Clone)]
410pub struct FinalizeWithdrawCalldataArgs {
411 pub nf: [u8; 32],
412 pub amount_sats: u64,
413 pub recipient_meta: [u8; 32],
414}
415
416pub fn finalize_withdraw_function_selector() -> [u8; 4] {
418 Keccak256::digest(b"finalizeWithdraw(bytes32,uint256,bytes32)")[..4]
419 .try_into()
420 .expect("selector is 4 bytes")
421}
422
423pub fn encode_finalize_withdraw_calldata(args: &FinalizeWithdrawCalldataArgs) -> Vec<u8> {
425 let tokens = vec![
426 Token::FixedBytes(args.nf.to_vec()),
427 Token::Uint(args.amount_sats.into()),
428 Token::FixedBytes(args.recipient_meta.to_vec()),
429 ];
430 let body = encode(&tokens);
431 let mut out = Vec::with_capacity(4 + body.len());
432 out.extend_from_slice(&finalize_withdraw_function_selector());
433 out.extend_from_slice(&body);
434 out
435}
436
437#[derive(Debug, Clone)]
456pub struct ErcShieldCalldataArgs {
457 pub actions: Vec<BundleActionArgs>,
458 pub amount: u128,
460 pub owner: [u8; 20],
462 pub deadline: u64,
464 pub permit_v: u8,
466 pub permit_r: [u8; 32],
468 pub permit_s: [u8; 32],
470 pub binding_sig: [[u8; 32]; 3],
472}
473
474pub fn erc_shield_function_selector() -> [u8; 4] {
476 Keccak256::digest(
477 b"shield((bytes32,bytes,bytes,bytes32,bytes32,bytes32,bytes,uint256[8],uint256[3])[],uint256,address,uint256,uint8,bytes32,bytes32,uint256[3])",
478 )[..4]
479 .try_into()
480 .expect("selector is 4 bytes")
481}
482
483pub fn encode_erc_shield_calldata(args: &ErcShieldCalldataArgs) -> Result<Vec<u8>, EthEncodeError> {
485 let actions_token = Token::Array(
486 args.actions
487 .iter()
488 .map(|a| {
489 if a.enc_ciphertext.len() != 580 {
490 return Err(EthEncodeError::BadEncLen(a.enc_ciphertext.len()));
491 }
492 let pub_fields_token = Token::FixedArray(
493 a.pub_fields
494 .iter()
495 .map(|x| Token::Uint(Uint::from_big_endian(x)))
496 .collect(),
497 );
498 let spend_auth_token = Token::FixedArray(
499 a.spend_auth_sig
500 .iter()
501 .map(|x| Token::Uint(Uint::from_big_endian(x)))
502 .collect(),
503 );
504 Ok(Token::Tuple(vec![
505 Token::FixedBytes(a.cmx.to_vec()),
506 Token::Bytes(a.enc_ciphertext.clone()),
507 Token::Bytes(a.out_ciphertext.clone()),
508 Token::FixedBytes(a.epk.to_vec()),
509 Token::FixedBytes(a.nf_old.to_vec()),
510 Token::FixedBytes(a.anchor.to_vec()),
511 Token::Bytes(a.proof.clone()),
512 pub_fields_token,
513 spend_auth_token,
514 ]))
515 })
516 .collect::<Result<Vec<_>, _>>()?,
517 );
518
519 let owner_addr = ethabi::Address::from(args.owner);
521
522 let binding_sig_token = Token::FixedArray(
523 args.binding_sig
524 .iter()
525 .map(|x| Token::Uint(Uint::from_big_endian(x)))
526 .collect(),
527 );
528
529 let tokens = vec![
530 actions_token,
531 Token::Uint(args.amount.into()),
532 Token::Address(owner_addr),
533 Token::Uint(args.deadline.into()),
534 Token::Uint(args.permit_v.into()),
535 Token::FixedBytes(args.permit_r.to_vec()),
536 Token::FixedBytes(args.permit_s.to_vec()),
537 binding_sig_token,
538 ];
539
540 let body = encode(&tokens);
541 let mut out = Vec::with_capacity(4 + body.len());
542 out.extend_from_slice(&erc_shield_function_selector());
543 out.extend_from_slice(&body);
544 Ok(out)
545}
546
547pub fn evm_address_to_recipient_meta(addr: &[u8; 20]) -> [u8; 32] {
553 let mut meta = [0u8; 32];
554 meta[12..].copy_from_slice(addr);
555 meta
556}
557
558pub fn parse_evm_address_hex(s: &str) -> Result<[u8; 20], BindingSighashError> {
560 let clean = s.strip_prefix("0x").unwrap_or(s);
561 if clean.len() != 40 {
562 return Err(BindingSighashError::Hex(format!("expected 40 hex chars, got {}", clean.len())));
563 }
564 let bytes = hex::decode(clean).map_err(|e| BindingSighashError::Hex(e.to_string()))?;
565 Ok(bytes.try_into().expect("40 hex chars = 20 bytes"))
566}
567
568#[cfg(test)]
571mod tests {
572 use super::*;
573
574 #[test]
575 fn unshield_value_balance_roundtrip() {
576 let amount: u64 = 100_000;
578 let vb = bundle_value_balance_be(amount, false);
579 let mut expected = [0u8; 32];
580 expected[24..32].copy_from_slice(&amount.to_be_bytes());
581 assert_eq!(vb, expected);
582 }
583
584 #[test]
585 fn finalize_withdraw_calldata_prefixes_selector() {
586 let cd = encode_finalize_withdraw_calldata(&FinalizeWithdrawCalldataArgs {
587 nf: [7u8; 32],
588 amount_sats: 123_456,
589 recipient_meta: [8u8; 32],
590 });
591 assert!(cd.len() > 4);
592 assert_eq!(&cd[..4], &finalize_withdraw_function_selector());
593 }
594
595 #[test]
597 fn decode_failing_calldata_fields() {
598 let raw_hex = "00000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000005dc00000000000000000000000000000000000000000000000000000000000000001c2fbf7c1d370880bd19943877bf41259025e4e877c56fbf26c4576b0e809b001d9022ffbe1768250cedb08257b1776b2f248a0854cd1a7245c42ae2c63ca5650446c7c76fa1736f7e8c47994073011a534af17c591b3d0b51e25b0c5cf57b5b000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000202dab531982047374d79feb10ee3389c3aae694c9d9a76696f1d034c245ac8263000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000004a03955242e589537ceca6b866b35cc16bad5605602c8a2bee463bb72612b6dbd9004cc117c66893f069f160294b22653b890e04abe242379d2dd98956778386fd400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000000000000000000000000000000000000003de55e0d54c5787b64a73d7879b67b9386e7262c20b6963c32d543462bdbc0e16858a895cb5ac289f5af0f0ad2918d6167a63370ed12dd7cd2210e53b6daee004cc117c66893f069f160294b22653b890e04abe242379d2dd98956778386fd417bc7b3fea209a7f90bb47b68453956704bcc38c3d32b1e1ef3c1909ec559f871f4fa732aa884e7020dccf264cf39bfade91074f00ec7b684f170b18531c4d4b2dab531982047374d79feb10ee3389c3aae694c9d9a76696f1d034c245ac82630ba6a64dab4e9fa2f31c9f5a35f3a93c323b160c175398b260a271a094c0fa7713460b57b63c8619062eff726135cabd3a4faf6c56e51afbf173a885d698f5ec026015d5bd814267fb2c44bc2a2336dfa946866960315f49761800cae310ed2c0000000000000000000000000000000000000000000000000000000000000244";
600
601 let body = hex::decode(raw_hex).expect("valid hex");
602
603 let read_u256_at = |offset: usize| -> u128 {
604 let word = &body[offset..offset+32];
605 u128::from_be_bytes(word[16..32].try_into().unwrap())
606 };
607 let read_bytes32_at = |offset: usize| -> String {
608 hex::encode(&body[offset..offset+32])
609 };
610
611 println!("=== Top-level ===");
613 println!("W0 offset_to_actions: {}", read_u256_at(0));
614 println!("W1 valueBalance: {}", read_u256_at(32));
615 println!("W2 amount: {}", read_u256_at(64));
616 println!("W3 recipientMeta: {}", read_bytes32_at(96));
617 println!("W4 bindingSig[0]: {}", read_bytes32_at(128));
618 println!("W5 bindingSig[1]: {}", read_bytes32_at(160));
619 println!("W6 bindingSig[2]: {}", read_bytes32_at(192));
620
621 println!("\n=== actions[] at body offset 224 ===");
623 println!("W7 length: {}", read_u256_at(224));
624 println!("W8 elem0 offset: {}", read_u256_at(256));
625
626 let s = 288_usize;
628 println!("\n=== BundleAction[0] struct at body offset {} ===", s);
629 println!(" cmx: {}", read_bytes32_at(s));
630 let enc_off = read_u256_at(s + 32) as usize;
631 let out_off = read_u256_at(s + 64) as usize;
632 let proof_off= read_u256_at(s + 192) as usize; println!(" enc_offset: {} (0x{:x})", enc_off, enc_off);
634 println!(" out_offset: {} (0x{:x})", out_off, out_off);
635 println!(" epk: {}", read_bytes32_at(s + 96));
636 println!(" nfOld: {}", read_bytes32_at(s + 128));
637 println!(" anchor: {}", read_bytes32_at(s + 160));
638 println!(" proof_offset: {} (0x{:x}) [should be 1312 = 0x520]", proof_off, proof_off);
639 println!(" pubInputs[0]: {}", read_bytes32_at(s + 224));
640 println!(" pubInputs[1]: {}", read_bytes32_at(s + 256));
641 println!(" pubInputs[6]: {}", read_bytes32_at(s + 224 + 6*32));
642
643 let proof_data_body_off = s + proof_off;
645 println!("\n=== Proof data at body offset {} (struct + {}) ===", proof_data_body_off, proof_off);
646 if proof_data_body_off + 32 <= body.len() {
647 let claimed_len = read_u256_at(proof_data_body_off);
648 println!(" claimed length: {} (0x{:x})", claimed_len, claimed_len);
649 } else {
650 println!(" OUT OF BOUNDS (body len = {})", body.len());
651 }
652
653 let enc_abs = s + enc_off;
655 if enc_abs + 32 <= body.len() {
656 let enc_len = read_u256_at(enc_abs);
657 println!("\nenc data at body offset {}: length = {} (expected 580)", enc_abs, enc_len);
658 }
659
660 assert_eq!(proof_off, 82, "confirmed proof_offset = 82 = 0x52 (BUG)");
662 assert_ne!(proof_off, 1312, "proof_offset should be 1312 but it is 82");
663 }
664 #[test]
675 fn bundle_calldata_proof_offset_is_correct() {
676 let proof_bytes = vec![0xabu8; 256]; let enc = vec![0u8; 580];
678 let out = vec![0u8; 80];
679
680 let cd = encode_bundle_calldata(&BundleCalldataArgs {
681 actions: vec![BundleActionArgs {
682 cmx: [1u8; 32],
683 enc_ciphertext: enc,
684 out_ciphertext: out,
685 epk: [2u8; 32],
686 nf_old: [3u8; 32],
687 anchor: [4u8; 32],
688 proof: proof_bytes,
689 pub_fields: [[5u8; 32]; 8],
690 spend_auth_sig: [[6u8; 32]; 3],
691 }],
692 value_balance: [0u8; 32],
693 amount: 0,
694 recipient_meta: [0u8; 32],
695 binding_sig: [[7u8; 32]; 3],
696 })
697 .expect("encode_bundle_calldata failed");
698
699 let body = &cd[4..]; let struct_start = 9 * 32_usize;
724
725 let read_u256 = |offset: usize| -> u128 {
726 let word = &body[offset..offset + 32];
727 u128::from_be_bytes(word[16..32].try_into().unwrap())
728 };
729
730 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");
735 assert_eq!(out_offset, 1216, "out_offset should be 0x4c0 = 1216");
736 assert_eq!(proof_offset, 1344, "proof_offset should be 0x540 = 1344, got {proof_offset:#x}");
737 }
738}