Skip to main content

privacy_core/ethereum/
mod.rs

1//! Calldata encoding for `PrivacyBTC.sol` — all operations go through `bundle()`.
2//!
3//! valueBalance encoding (sign-bit / readable int64 convention, mirrors Zcash §7.1):
4//!   0                     → transfer  (Σv_old == Σv_new)
5//!   +amount  (bit255 = 0) → unshield  (value leaving pool, positive satoshis)
6//!   -amount  (bit255 = 1) → shield    (value entering pool, high bit = sign)
7//!
8//! The raw satoshi count is always stored in the lower 64 bits; bit 255 is the sign.
9//! When doing BJJ cryptography (BindingSig), the contract/prover decodes the sign
10//! and computes the actual scalar: positive → scalar, negative → ℓ − scalar.
11
12pub 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// ── Error ────────────────────────────────────────────────────────────────────
35
36#[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// ── bundle(BundleAction[],uint256,uint256,bytes32,uint256[3]) ─────────────────
43
44/// One action passed to `bundle()`.
45#[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    /// 8 BN254 calldata fields: [anchor, cv_x, cv_y, nf, rk_x, rk_y, cmx, rt_frozen].
55    pub pub_fields: [[u8; 32]; 8],
56    pub spend_auth_sig: [[u8; 32]; 3],
57}
58
59/// Arguments for
60/// `bundle(BundleAction[],uint256,uint256,bytes32,uint256[3])`.
61#[derive(Debug, Clone)]
62pub struct BundleCalldataArgs {
63    pub actions: Vec<BundleActionArgs>,
64    /// Net value balance (Fr element BE); 0x00 for pure transfer, +v for unshield, Fr−v for shield.
65    pub value_balance: [u8; 32],
66    /// Absolute satoshi amount for unshield; 0 for transfer.
67    pub amount: u64,
68    /// BTC recipient hash for unshield; [0u8; 32] for transfer.
69    pub recipient_meta: [u8; 32],
70    /// Baby JubJub Schnorr binding signature [Rx, Ry, s]; bsk = Σ rcv_i.
71    pub binding_sig: [[u8; 32]; 3],
72}
73
74/// First 4 bytes of
75/// `keccak256("bundle((bytes32,bytes,bytes,bytes32,bytes32,bytes32,bytes,uint256[8],uint256[3])[],…")`.
76pub 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
84/// ABI-encode `bundle` calldata (selector + body).
85pub 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
136// ── Baby Jubjub binding Schnorr (matches `BindingSignature.verify`) ──────────
137
138/// EIP-2494 prime subgroup order ℓ (`curve_order = 8·ℓ`; differs from BN254 field modulus).
139pub 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/// Interpret digest / coefficient integer modulo ℓ (challenge scalar).
148#[inline]
149pub fn reduce_mod_bjj_subgroup(v: Uint) -> Uint {
150    let l = bjj_subgroup_order();
151    v % l
152}
153
154/// Fiat–Shamir challenge `e = keccak256(R ‖ bvk ‖ sighash) mod ℓ`.
155pub 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
172/// (a * b) mod m using binary doubling — avoids U256 overflow for ~251-bit inputs.
173fn 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/// Schnorr scalar `s = (r + e·bsk) mod ℓ`.
187#[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
204/// Circom / snarkjs decimal field element → 32-byte big-endian for ABI `uint256`.
205pub 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/// BN254 `Fr` limb repr (little-endian per `ff`) → big-endian uint, reduced mod ℓ.
212#[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
221/// Sum multiple rcv scalars modulo ℓ (BJJ subgroup order) and return the result
222/// as a canonical little-endian `[u8; 32]` suitable for `Fr::from_repr`.
223///
224/// Using BN254 Fr addition (`mod r`) for this sum is WRONG because `ℓ ≪ r`:
225/// when `rcv0 + rcv1 ≥ r`, Fr wraps modulo `r`, but the BJJ group reduces mod ℓ,
226/// so `bsk_fr mod ℓ ≠ Σ rcv_i mod ℓ`, breaking `bvk = bsk · G_RANDOM`.
227pub 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        // Use checked add + manual mod to avoid overflow of U256 on adversarial input.
233        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
243// ── BindingSignature.buildSighash (Solidity `abi.encodePacked` mirror) ───────
244
245/// BN254 scalar field modulus as big-endian `uint256` (matches `BabyJubJub.Fr`).
246pub 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
260/// Encode `chainId` as 32-byte big-endian `uint256` for packed sighash preimage.
261pub 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
267/// Unified `valueBalance` encoder (sign-bit convention, mirrors Zcash protocol §7.1).
268///
269/// Encoding:
270///   transfer:  `[0u8; 32]`                      → 0 (balanced)
271///   unshield:  `bundle_value_balance_be(v, false)` → +v (bit255=0, positive satoshis)
272///   shield:    `bundle_value_balance_be(v, true)`  → high-bit flag + v (bit255=1, negative)
273///
274/// The BindingSignature contract (and prover) decode the sign bit to get the BJJ
275/// scalar for bvk computation:
276///   bit255=0  → scalar = amount_sats  (unshield: bvk = Σcv − amount·G_VALUE)
277///   bit255=1  → scalar = ℓ − amount   (shield:   bvk = Σcv + amount·G_VALUE)
278pub 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; // set bit 255 as sign flag
283    }
284    out
285}
286
287/// Decode a sign-bit-encoded `valueBalance` into `(abs_amount_sats, is_negative)`.
288pub 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
294/// Decode a sign-bit-encoded `valueBalance` into the actual BJJ subgroup scalar
295/// suitable for `scalarMul(G_VALUE, scalar)` in BindingSignature verification.
296///
297///   bit255=0 (positive, unshield): scalar = amount_sats
298///   bit255=1 (negative, shield):   scalar = ℓ − amount_sats  (additive inverse mod ℓ)
299pub 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        // ℓ − amount  (BJJ subgroup order minus abs value)
307        let l = bjj_subgroup_order();
308        let amt: Uint = amount.into();
309        uint_to_be32(&(l - amt % l))
310    }
311}
312
313// ── Deprecated helpers kept for reference ─────────────────────────────────────
314
315/// Deprecated: use `bundle_value_balance_be(amount, false)` instead.
316#[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: use `bundle_value_balance_be(amount, true)` instead.
322#[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
327/// Parse `0x`-prefixed 20-byte contract address.
328pub 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
337/// Parse `0x`-prefixed 32-byte hash / field element (canonical BE layout).
338pub 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
350/// Keccak256 preimage matching `BindingSignature.buildSighash`:
351/// `abi.encodePacked("PrivacyPool.bundle.v1", chainId, pool, nullifiers[], commitments[], valueBalance, recipientMeta)`.
352pub 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
375/// Keccak256 preimage matching `SpendAuthSignature.buildSighash`:
376/// `abi.encodePacked("SpendAuth.action.v1", chainId, contractAddr,
377///                   nfOld, cmx, epk, keccak256(encCiphertext), keccak256(outCiphertext))`.
378///
379/// Mirrors the Solidity `SpendAuthSignature.buildSighash` exactly.
380pub 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// ── finalizeWithdraw(bytes32,uint256,bytes32) — legacy relayer path ──────────
404
405/// Arguments for `finalizeWithdraw(bytes32,uint256,bytes32)`.
406///
407/// Legacy federation-trusted path — kept for backward-compat.
408/// New deployments should prefer `unshield()`.
409#[derive(Debug, Clone)]
410pub struct FinalizeWithdrawCalldataArgs {
411    pub nf: [u8; 32],
412    pub amount_sats: u64,
413    pub recipient_meta: [u8; 32],
414}
415
416/// First 4 bytes of `keccak256("finalizeWithdraw(bytes32,uint256,bytes32)")`.
417pub 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
423/// ABI-encode `finalizeWithdraw` calldata (selector + body).
424pub 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// ── PrivacyERC.shield() calldata ──────────────────────────────────────────────
438//
439// Signature:
440//   shield(
441//     (bytes32,bytes,bytes,bytes32,bytes32,bytes32,bytes,uint256[8],uint256[3])[] actions,
442//     uint256 amount,
443//     address owner,
444//     uint256 deadline,
445//     uint8   v,
446//     bytes32 r,
447//     bytes32 s,
448//     uint256[3] bindingSig
449//   )
450//
451// For native ETH pools (isNativeEth=true) the permit params (owner/deadline/v/r/s)
452// are ignored by the contract; pass zeroes.
453
454/// Arguments for `PrivacyERC.shield()`.
455#[derive(Debug, Clone)]
456pub struct ErcShieldCalldataArgs {
457    pub actions: Vec<BundleActionArgs>,
458    /// Token amount in the token's smallest unit (wei / 6-decimal USDC unit / …).
459    pub amount: u128,
460    /// EIP-2612 permit: token owner address (20 bytes, right-padded to 32).
461    pub owner: [u8; 20],
462    /// EIP-2612 permit: expiry unix timestamp.
463    pub deadline: u64,
464    /// EIP-2612 permit: signature v (1 byte).
465    pub permit_v: u8,
466    /// EIP-2612 permit: signature r (32 bytes).
467    pub permit_r: [u8; 32],
468    /// EIP-2612 permit: signature s (32 bytes).
469    pub permit_s: [u8; 32],
470    /// Baby JubJub Schnorr binding signature [Rx, Ry, s].
471    pub binding_sig: [[u8; 32]; 3],
472}
473
474/// First 4 bytes of the `PrivacyERC.shield()` function selector.
475pub 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
483/// ABI-encode `PrivacyERC.shield()` calldata (selector + body).
484pub 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    // owner address as ABI address token
520    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
547// ── PrivacyERC unshield helpers ───────────────────────────────────────────────
548
549/// Encode an EVM address (20 bytes) into the 32-byte `recipientMeta` field used
550/// by `PrivacyERC._onAssetRelease()`.  The address occupies the low 20 bytes
551/// (right-aligned), matching `address(uint160(uint256(recipientMeta)))`.
552pub 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
558/// Parse a 0x-prefixed EVM address hex string into 20 bytes.
559pub 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// ── Tests ─────────────────────────────────────────────────────────────────────
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    #[test]
575    fn unshield_value_balance_roundtrip() {
576        // Unshield: positive satoshis, bit255 = 0.
577        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    /// Decode the actual failing calldata and inspect the BundleAction fields.
596    #[test]
597    fn decode_failing_calldata_fields() {
598        // First 10468 bytes of the failing calldata (skip selector)
599        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        // Top-level layout
612        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        // actions[] starts at offset 224 (= 7*32)
622        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        // struct elem 0 at body offset 288 (= 9*32)
627        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;  // slot 6 (192 = 6*32)
633        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        // Check what's at the proof data location
644        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        // Check enc data
654        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        // Confirm proof_offset is wrong
661        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    ///
665    /// BundleAction static header = 9 fields, all 32 bytes each in head:
666    ///   cmx(32) + enc_off(32) + out_off(32) + epk(32) + nfOld(32)
667    ///   + anchor(32) + proof_off(32) + pubFields[8](256) + spendAuth[3](96)
668    ///   = 576 bytes total head
669    ///
670    /// Dynamic data layout:
671    ///   enc  at offset 576        (32 len + 580 data padded to 608)  → end 576+640=1216
672    ///   out  at offset 1216       (32 len +  80 data padded to  96)  → end 1216+128=1344
673    ///   proof at offset 1344 = 0x540
674    #[test]
675    fn bundle_calldata_proof_offset_is_correct() {
676        let proof_bytes = vec![0xabu8; 256]; // Groth16 abi.encode(pA,pB,pC) size
677        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        // Skip selector (4 bytes).  Body layout:
700        //   W0  = offset_to_actions (= 0xe0 = 224)
701        //   W1  = valueBalance
702        //   W2  = amount
703        //   W3  = recipientMeta
704        //   W4-W6 = bindingSig[3]
705        //   W7  = actions.length (1)
706        //   W8  = offset_to_elem0 (32)
707        //   W9  = struct elem0 starts here
708        //
709        // Within struct elem0 (offsets relative to W9):
710        //   +0   = cmx
711        //   +32  = enc_offset    ← should be 576 = 0x240
712        //   +64  = out_offset    ← should be 1216 = 0x4c0
713        //   +96  = epk
714        //   +128 = nfOld
715        //   +160 = anchor
716        //   +192 = proof_offset  ← should be 1344 = 0x540
717        //   +224..+479 = pubFields[8]
718        //   +480..+575 = spendAuthSig[3]
719
720        let body = &cd[4..]; // skip selector
721
722        // struct elem0 starts at word 9 (= byte 288)
723        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);   // +32
731        let out_offset   = read_u256(struct_start + 64);   // +64
732        let proof_offset = read_u256(struct_start + 192);  // +192
733
734        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}