Skip to main content

privacy_core/ethereum/
bundle_decode.rs

1//! Decode `PrivacyPool.bundle()` calldata (inverse of [`super::encode_bundle_calldata`]).
2//!
3//! Used by the indexer as a fallback for legacy pools whose `NoteAdded` logs omit
4//! `outCiphertext` / `cvNetX` (pre-extension event layout).
5
6use ethabi::{decode, ParamType, Token, Uint};
7use thiserror::Error;
8
9use super::{bundle_function_selector, BundleActionArgs, BundleCalldataArgs, EthEncodeError};
10
11#[derive(Debug, Error)]
12pub enum BundleDecodeError {
13    #[error("calldata too short")]
14    TooShort,
15    #[error("wrong function selector")]
16    BadSelector,
17    #[error("ABI decode failed: {0}")]
18    Abi(String),
19    #[error("unexpected token layout")]
20    Layout,
21    #[error("{0}")]
22    Encode(#[from] EthEncodeError),
23}
24
25fn bundle_action_param() -> ParamType {
26    ParamType::Tuple(vec![
27        ParamType::FixedBytes(32),
28        ParamType::Bytes,
29        ParamType::Bytes,
30        ParamType::FixedBytes(32),
31        ParamType::FixedBytes(32),
32        ParamType::FixedBytes(32),
33        ParamType::Bytes,
34        ParamType::FixedArray(Box::new(ParamType::Uint(256)), 8),
35        ParamType::FixedArray(Box::new(ParamType::Uint(256)), 3),
36    ])
37}
38
39fn bundle_top_params() -> Vec<ParamType> {
40    vec![
41        ParamType::Array(Box::new(bundle_action_param())),
42        ParamType::Uint(256),
43        ParamType::Uint(256),
44        ParamType::FixedBytes(32),
45        ParamType::FixedArray(Box::new(ParamType::Uint(256)), 3),
46    ]
47}
48
49fn token_bytes32(t: &Token) -> Result<[u8; 32], BundleDecodeError> {
50    match t {
51        Token::FixedBytes(b) if b.len() == 32 => {
52            let mut out = [0u8; 32];
53            out.copy_from_slice(b);
54            Ok(out)
55        }
56        Token::Bytes(b) if b.len() == 32 => {
57            let mut out = [0u8; 32];
58            out.copy_from_slice(b);
59            Ok(out)
60        }
61        Token::Uint(u) => Ok(uint_to_be32(u)),
62        _ => Err(BundleDecodeError::Layout),
63    }
64}
65
66fn uint_to_be32(u: &Uint) -> [u8; 32] {
67    let mut out = [0u8; 32];
68    u.to_big_endian(&mut out);
69    out
70}
71
72fn parse_action(token: &Token) -> Result<BundleActionArgs, BundleDecodeError> {
73    let fields = match token {
74        Token::Tuple(v) if v.len() == 9 => v,
75        _ => return Err(BundleDecodeError::Layout),
76    };
77    let cmx = token_bytes32(&fields[0])?;
78    let enc_ciphertext = match &fields[1] {
79        Token::Bytes(b) => b.clone(),
80        _ => return Err(BundleDecodeError::Layout),
81    };
82    let out_ciphertext = match &fields[2] {
83        Token::Bytes(b) => b.clone(),
84        _ => return Err(BundleDecodeError::Layout),
85    };
86    let epk = token_bytes32(&fields[3])?;
87    let nf_old = token_bytes32(&fields[4])?;
88    let anchor = token_bytes32(&fields[5])?;
89    let proof = match &fields[6] {
90        Token::Bytes(b) => b.clone(),
91        _ => return Err(BundleDecodeError::Layout),
92    };
93    let pub_fields = match &fields[7] {
94        Token::FixedArray(v) if v.len() == 8 => {
95            let mut out = [[0u8; 32]; 8];
96            for (i, t) in v.iter().enumerate() {
97                out[i] = token_bytes32(t)?;
98            }
99            out
100        }
101        _ => return Err(BundleDecodeError::Layout),
102    };
103    let spend_auth_sig = match &fields[8] {
104        Token::FixedArray(v) if v.len() == 3 => {
105            let mut out = [[0u8; 32]; 3];
106            for (i, t) in v.iter().enumerate() {
107                out[i] = token_bytes32(t)?;
108            }
109            out
110        }
111        _ => return Err(BundleDecodeError::Layout),
112    };
113    Ok(BundleActionArgs {
114        cmx,
115        enc_ciphertext,
116        out_ciphertext,
117        epk,
118        nf_old,
119        anchor,
120        proof,
121        pub_fields,
122        spend_auth_sig,
123    })
124}
125
126/// Decode full `bundle()` calldata (4-byte selector + ABI body).
127pub fn decode_bundle_calldata(calldata: &[u8]) -> Result<BundleCalldataArgs, BundleDecodeError> {
128    if calldata.len() < 4 {
129        return Err(BundleDecodeError::TooShort);
130    }
131    if calldata[..4] != bundle_function_selector() {
132        return Err(BundleDecodeError::BadSelector);
133    }
134    let tokens = decode(&bundle_top_params(), &calldata[4..])
135        .map_err(|e| BundleDecodeError::Abi(e.to_string()))?;
136    if tokens.len() != 5 {
137        return Err(BundleDecodeError::Layout);
138    }
139    let actions: Vec<BundleActionArgs> = match &tokens[0] {
140        Token::Array(items) => items.iter().map(parse_action).collect::<Result<_, _>>()?,
141        _ => return Err(BundleDecodeError::Layout),
142    };
143    let value_balance = token_bytes32(&tokens[1])?;
144    let amount = match &tokens[2] {
145        Token::Uint(u) => u.as_u64(),
146        _ => return Err(BundleDecodeError::Layout),
147    };
148    let recipient_meta = token_bytes32(&tokens[3])?;
149    let binding_sig = match &tokens[4] {
150        Token::FixedArray(v) if v.len() == 3 => {
151            let mut out = [[0u8; 32]; 3];
152            for (i, t) in v.iter().enumerate() {
153                out[i] = token_bytes32(t)?;
154            }
155            out
156        }
157        _ => return Err(BundleDecodeError::Layout),
158    };
159    Ok(BundleCalldataArgs {
160        actions,
161        value_balance,
162        amount,
163        recipient_meta,
164        binding_sig,
165    })
166}
167
168/// Per-action ciphertext fields needed for OVK recovery (keyed by `cmx`).
169#[derive(Debug, Clone)]
170pub struct BundleActionCiphertexts {
171    pub out_ciphertext: Vec<u8>,
172    /// `pubFields[1]` = `cv_net_x` (BE uint256).
173    pub cv_net_x: [u8; 32],
174}
175
176/// Build a `cmx → ciphertexts` map from decoded bundle calldata.
177pub fn bundle_actions_by_cmx(
178    calldata: &[u8],
179) -> Result<std::collections::HashMap<[u8; 32], BundleActionCiphertexts>, BundleDecodeError> {
180    let bundle = decode_bundle_calldata(calldata)?;
181    let mut map = std::collections::HashMap::new();
182    for a in bundle.actions {
183        map.insert(
184            a.cmx,
185            BundleActionCiphertexts {
186                out_ciphertext: a.out_ciphertext,
187                cv_net_x: a.pub_fields[1],
188            },
189        );
190    }
191    Ok(map)
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::ethereum::{encode_bundle_calldata, BundleCalldataArgs};
198
199    #[test]
200    fn bundle_calldata_roundtrip_decode() {
201        let proof_bytes = vec![0xabu8; 256];
202        let enc = vec![0xCCu8; 580];
203        let out = vec![0xDDu8; 80];
204        let args = BundleCalldataArgs {
205            actions: vec![BundleActionArgs {
206                cmx: [1u8; 32],
207                enc_ciphertext: enc.clone(),
208                out_ciphertext: out.clone(),
209                epk: [2u8; 32],
210                nf_old: [3u8; 32],
211                anchor: [4u8; 32],
212                proof: proof_bytes,
213                pub_fields: [
214                    [4u8; 32],
215                    [5u8; 32],
216                    [0u8; 32],
217                    [3u8; 32],
218                    [0u8; 32],
219                    [0u8; 32],
220                    [1u8; 32],
221                    [0u8; 32],
222                ],
223                spend_auth_sig: [[6u8; 32]; 3],
224            }],
225            value_balance: [0u8; 32],
226            amount: 0,
227            recipient_meta: [0u8; 32],
228            binding_sig: [[7u8; 32]; 3],
229        };
230        let cd = encode_bundle_calldata(&args).expect("encode");
231        let decoded = decode_bundle_calldata(&cd).expect("decode");
232        assert_eq!(decoded.actions.len(), 1);
233        assert_eq!(decoded.actions[0].enc_ciphertext, enc);
234        assert_eq!(decoded.actions[0].out_ciphertext, out);
235        assert_eq!(decoded.actions[0].pub_fields[1], [5u8; 32]);
236        let map = bundle_actions_by_cmx(&cd).expect("map");
237        assert_eq!(map.get(&[1u8; 32]).unwrap().out_ciphertext, out);
238    }
239}