aztec_account/entrypoint/
encoding.rs

1//! Entrypoint call encoding for account and multi-call entrypoints.
2//!
3//! Ports the `EncodedAppEntrypointCalls` logic from the upstream TS
4//! `yarn-project/entrypoints/src/encoding.ts`.
5
6use aztec_core::abi::FunctionType;
7use aztec_core::constants::domain_separator;
8use aztec_core::hash::{abi_values_to_fields, poseidon2_hash_with_separator};
9use aztec_core::tx::{FunctionCall, HashedValues};
10use aztec_core::types::Fr;
11use aztec_core::Error;
12
13/// Maximum number of function calls in a single entrypoint payload.
14pub const APP_MAX_CALLS: usize = 5;
15
16/// A single encoded function call within an entrypoint payload.
17#[derive(Clone, Debug)]
18struct EncodedCall {
19    args_hash: Fr,
20    function_selector: Fr,
21    target_address: Fr,
22    is_public: bool,
23    hide_msg_sender: bool,
24    is_static: bool,
25}
26
27/// Borrowed view of an encoded function call.
28#[derive(Clone, Copy, Debug)]
29pub struct EncodedCallView {
30    /// Arguments hash for the call.
31    pub args_hash: Fr,
32    /// Function selector as a field.
33    pub function_selector: Fr,
34    /// Target address as a field.
35    pub target_address: Fr,
36    /// Whether the call is public.
37    pub is_public: bool,
38    /// Whether the msg sender is hidden.
39    pub hide_msg_sender: bool,
40    /// Whether the call is static.
41    pub is_static: bool,
42}
43
44impl EncodedCall {
45    /// Serialize this encoded call to field elements.
46    fn to_fields(&self) -> Vec<Fr> {
47        vec![
48            self.args_hash,
49            self.function_selector,
50            self.target_address,
51            Fr::from(self.is_public),
52            Fr::from(self.hide_msg_sender),
53            Fr::from(self.is_static),
54        ]
55    }
56}
57
58/// Encoded entrypoint calls ready for hashing and field serialization.
59pub struct EncodedAppEntrypointCalls {
60    encoded_calls: Vec<EncodedCall>,
61    tx_nonce: Fr,
62    hashed_args_list: Vec<HashedValues>,
63}
64
65impl EncodedAppEntrypointCalls {
66    /// Encode function calls for passing to an account entrypoint.
67    ///
68    /// Pads to `APP_MAX_CALLS` with empty calls.
69    pub fn create(calls: &[FunctionCall], tx_nonce: Option<Fr>) -> Result<Self, Error> {
70        if calls.len() > APP_MAX_CALLS {
71            return Err(Error::InvalidData(format!(
72                "Too many calls: {} > {}",
73                calls.len(),
74                APP_MAX_CALLS
75            )));
76        }
77
78        let tx_nonce = tx_nonce.unwrap_or_else(Fr::random);
79        let mut encoded_calls = Vec::with_capacity(APP_MAX_CALLS);
80        let mut hashed_args_list = Vec::with_capacity(APP_MAX_CALLS);
81        let padded_calls = calls
82            .iter()
83            .cloned()
84            .chain(std::iter::repeat_with(FunctionCall::empty).take(APP_MAX_CALLS - calls.len()));
85
86        for call in padded_calls {
87            let is_public = call.function_type == FunctionType::Public;
88            let arg_fields = abi_values_to_fields(&call.args);
89
90            let (args_hash, hashed_values) = if is_public {
91                let mut calldata = vec![call.selector.to_field()];
92                calldata.extend_from_slice(&arg_fields);
93                let hv = HashedValues::from_calldata(calldata);
94                let h = hv.hash();
95                (h, hv)
96            } else {
97                let hv = HashedValues::from_args(arg_fields);
98                let h = hv.hash();
99                (h, hv)
100            };
101
102            hashed_args_list.push(hashed_values);
103
104            encoded_calls.push(EncodedCall {
105                args_hash,
106                function_selector: call.selector.to_field(),
107                target_address: call.to.0,
108                is_public,
109                hide_msg_sender: call.hide_msg_sender,
110                is_static: call.is_static,
111            });
112        }
113
114        Ok(Self {
115            encoded_calls,
116            tx_nonce,
117            hashed_args_list,
118        })
119    }
120
121    /// Serialize the full payload to field elements for ABI encoding.
122    ///
123    /// Layout: [call_0_fields..., call_1_fields..., ..., call_N_fields..., tx_nonce]
124    pub fn to_fields(&self) -> Vec<Fr> {
125        let mut fields = Vec::new();
126        for call in &self.encoded_calls {
127            fields.extend(call.to_fields());
128        }
129        fields.push(self.tx_nonce);
130        fields
131    }
132
133    /// Hash the payload using Poseidon2 with `SIGNATURE_PAYLOAD` separator.
134    pub fn hash(&self) -> Fr {
135        let fields = self.to_fields();
136        poseidon2_hash_with_separator(&fields, domain_separator::SIGNATURE_PAYLOAD)
137    }
138
139    /// Return the hashed arguments for oracle access.
140    pub fn hashed_args(&self) -> &[HashedValues] {
141        &self.hashed_args_list
142    }
143
144    /// Return encoded calls for ABI construction and inspection.
145    pub fn encoded_calls(&self) -> Vec<EncodedCallView> {
146        self.encoded_calls
147            .iter()
148            .map(|call| EncodedCallView {
149                args_hash: call.args_hash,
150                function_selector: call.function_selector,
151                target_address: call.target_address,
152                is_public: call.is_public,
153                hide_msg_sender: call.hide_msg_sender,
154                is_static: call.is_static,
155            })
156            .collect()
157    }
158
159    /// Return the tx nonce used for this encoding.
160    pub fn tx_nonce(&self) -> Fr {
161        self.tx_nonce
162    }
163}
164
165#[cfg(test)]
166#[allow(clippy::unwrap_used, clippy::expect_used)]
167mod tests {
168    use super::*;
169    use aztec_core::abi::{AbiValue, FunctionSelector, FunctionType};
170    use aztec_core::types::AztecAddress;
171
172    fn make_private_call(addr: u64, selector_hex: &str) -> FunctionCall {
173        FunctionCall {
174            to: AztecAddress::from(addr),
175            selector: FunctionSelector::from_hex(selector_hex).expect("valid"),
176            args: vec![AbiValue::Field(Fr::from(42u64))],
177            function_type: FunctionType::Private,
178            is_static: false,
179            hide_msg_sender: false,
180        }
181    }
182
183    fn make_public_call(addr: u64, selector_hex: &str) -> FunctionCall {
184        FunctionCall {
185            to: AztecAddress::from(addr),
186            selector: FunctionSelector::from_hex(selector_hex).expect("valid"),
187            args: vec![AbiValue::Field(Fr::from(99u64))],
188            function_type: FunctionType::Public,
189            is_static: false,
190            hide_msg_sender: false,
191        }
192    }
193
194    #[test]
195    fn encode_single_private_call() {
196        let call = make_private_call(1, "0x11223344");
197        let encoded =
198            EncodedAppEntrypointCalls::create(&[call], Some(Fr::from(1u64))).expect("encode");
199
200        let fields = encoded.to_fields();
201        // 5 calls * 6 fields each + 1 nonce = 31 fields
202        assert_eq!(fields.len(), 31);
203        // First call should have non-zero args_hash
204        assert_ne!(fields[0], Fr::zero());
205        // Nonce should be last
206        assert_eq!(*fields.last().unwrap(), Fr::from(1u64));
207    }
208
209    #[test]
210    fn encode_pads_to_max_calls() {
211        let call = make_private_call(1, "0x11223344");
212        let encoded =
213            EncodedAppEntrypointCalls::create(&[call], Some(Fr::from(1u64))).expect("encode");
214
215        assert_eq!(encoded.encoded_calls.len(), APP_MAX_CALLS);
216        // Calls 2-5 should be empty (zero target address)
217        for i in 1..APP_MAX_CALLS {
218            assert_eq!(encoded.encoded_calls[i].target_address, Fr::zero());
219        }
220    }
221
222    #[test]
223    fn encode_multiple_calls() {
224        let calls = vec![
225            make_private_call(1, "0x11111111"),
226            make_public_call(2, "0x22222222"),
227        ];
228        let encoded =
229            EncodedAppEntrypointCalls::create(&calls, Some(Fr::from(1u64))).expect("encode");
230
231        assert_eq!(encoded.hashed_args().len(), APP_MAX_CALLS);
232        // Second call should be public
233        assert!(encoded.encoded_calls[1].is_public);
234    }
235
236    #[test]
237    fn encode_rejects_too_many_calls() {
238        let calls: Vec<_> = (0..6)
239            .map(|i| make_private_call(i + 1, "0x11223344"))
240            .collect();
241        let result = EncodedAppEntrypointCalls::create(&calls, None);
242        assert!(result.is_err());
243    }
244
245    #[test]
246    fn hash_is_deterministic() {
247        let call = make_private_call(1, "0x11223344");
248        let nonce = Fr::from(42u64);
249
250        let h1 = EncodedAppEntrypointCalls::create(&[call.clone()], Some(nonce))
251            .expect("encode")
252            .hash();
253        let h2 = EncodedAppEntrypointCalls::create(&[call], Some(nonce))
254            .expect("encode")
255            .hash();
256
257        assert_eq!(h1, h2);
258    }
259
260    #[test]
261    fn different_nonce_different_hash() {
262        let call = make_private_call(1, "0x11223344");
263
264        let h1 = EncodedAppEntrypointCalls::create(&[call.clone()], Some(Fr::from(1u64)))
265            .expect("encode")
266            .hash();
267        let h2 = EncodedAppEntrypointCalls::create(&[call], Some(Fr::from(2u64)))
268            .expect("encode")
269            .hash();
270
271        assert_ne!(h1, h2);
272    }
273
274    #[test]
275    fn encode_mix_of_public_and_private() {
276        let calls = vec![
277            make_private_call(1, "0x11111111"),
278            make_public_call(2, "0x22222222"),
279            make_private_call(3, "0x33333333"),
280        ];
281        let encoded =
282            EncodedAppEntrypointCalls::create(&calls, Some(Fr::from(1u64))).expect("encode");
283
284        assert!(!encoded.encoded_calls[0].is_public);
285        assert!(encoded.encoded_calls[1].is_public);
286        assert!(!encoded.encoded_calls[2].is_public);
287        assert_eq!(encoded.hashed_args().len(), APP_MAX_CALLS);
288    }
289}