Skip to main content

aztec_core/
tx.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::fmt;
3
4use crate::abi::{AbiValue, FunctionSelector, FunctionType};
5use crate::fee::GasSettings;
6use crate::types::{decode_fixed_hex, encode_hex, AztecAddress, Fr};
7use crate::Error;
8
9/// A 32-byte transaction hash.
10#[derive(Clone, Copy, PartialEq, Eq, Hash)]
11pub struct TxHash(pub [u8; 32]);
12
13impl TxHash {
14    /// The zero hash.
15    pub const fn zero() -> Self {
16        Self([0u8; 32])
17    }
18
19    /// Parse from a hex string (e.g. `"0xdead..."`).
20    pub fn from_hex(value: &str) -> Result<Self, Error> {
21        Ok(Self(decode_fixed_hex::<32>(value)?))
22    }
23}
24
25impl fmt::Display for TxHash {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        f.write_str(&encode_hex(&self.0))
28    }
29}
30
31impl fmt::Debug for TxHash {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        write!(f, "TxHash({self})")
34    }
35}
36
37impl Serialize for TxHash {
38    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
39    where
40        S: Serializer,
41    {
42        serializer.serialize_str(&self.to_string())
43    }
44}
45
46impl<'de> Deserialize<'de> for TxHash {
47    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
48    where
49        D: Deserializer<'de>,
50    {
51        let s = String::deserialize(deserializer)?;
52        Self::from_hex(&s).map_err(serde::de::Error::custom)
53    }
54}
55
56/// Transaction lifecycle status.
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum TxStatus {
60    /// Transaction was dropped from the mempool.
61    Dropped,
62    /// Transaction is pending in the mempool.
63    Pending,
64    /// Transaction has been proposed in a block.
65    Proposed,
66    /// Transaction's block has been checkpointed to L1.
67    Checkpointed,
68    /// Transaction's block has been proven.
69    Proven,
70    /// Transaction's block has been finalized on L1.
71    Finalized,
72}
73
74/// Outcome of transaction execution within a block.
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum TxExecutionResult {
78    /// All phases executed successfully.
79    Success,
80    /// The app logic phase reverted.
81    AppLogicReverted,
82    /// The teardown phase reverted.
83    TeardownReverted,
84    /// Both app logic and teardown phases reverted.
85    BothReverted,
86}
87
88/// A transaction receipt returned by the node.
89#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
90pub struct TxReceipt {
91    /// Hash of the transaction.
92    pub tx_hash: TxHash,
93    /// Current lifecycle status.
94    pub status: TxStatus,
95    /// Execution outcome (present once the tx has been included in a block).
96    pub execution_result: Option<TxExecutionResult>,
97    /// Error message if the transaction failed.
98    pub error: Option<String>,
99    /// Total fee paid for the transaction.
100    pub transaction_fee: Option<u128>,
101    /// Hash of the block containing this transaction.
102    #[serde(default, with = "option_hex_bytes_32")]
103    pub block_hash: Option<[u8; 32]>,
104    /// Block number containing this transaction.
105    pub block_number: Option<u64>,
106    /// Epoch number containing this transaction.
107    pub epoch_number: Option<u64>,
108}
109
110mod option_hex_bytes_32 {
111    use serde::{Deserialize, Deserializer, Serializer};
112
113    use crate::types::{decode_fixed_hex, encode_hex};
114
115    #[allow(clippy::ref_option)]
116    pub fn serialize<S>(value: &Option<[u8; 32]>, serializer: S) -> Result<S::Ok, S::Error>
117    where
118        S: Serializer,
119    {
120        match value {
121            Some(bytes) => serializer.serialize_some(&encode_hex(bytes)),
122            None => serializer.serialize_none(),
123        }
124    }
125
126    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<[u8; 32]>, D::Error>
127    where
128        D: Deserializer<'de>,
129    {
130        let opt: Option<String> = Option::deserialize(deserializer)?;
131        match opt {
132            Some(s) => {
133                let bytes = decode_fixed_hex::<32>(&s).map_err(serde::de::Error::custom)?;
134                Ok(Some(bytes))
135            }
136            None => Ok(None),
137        }
138    }
139}
140
141impl TxReceipt {
142    /// Returns `true` if the transaction has been included in a block.
143    pub const fn is_mined(&self) -> bool {
144        matches!(
145            self.status,
146            TxStatus::Proposed | TxStatus::Checkpointed | TxStatus::Proven | TxStatus::Finalized
147        )
148    }
149
150    /// Returns `true` if the transaction is pending in the mempool.
151    pub fn is_pending(&self) -> bool {
152        self.status == TxStatus::Pending
153    }
154
155    /// Returns `true` if the transaction was dropped from the mempool.
156    pub fn is_dropped(&self) -> bool {
157        self.status == TxStatus::Dropped
158    }
159
160    /// Returns `true` if execution completed successfully.
161    pub fn has_execution_succeeded(&self) -> bool {
162        self.execution_result == Some(TxExecutionResult::Success)
163    }
164
165    /// Returns `true` if any execution phase reverted.
166    pub fn has_execution_reverted(&self) -> bool {
167        self.execution_result.is_some() && !self.has_execution_succeeded()
168    }
169}
170
171/// A single function call to a contract.
172#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
173pub struct FunctionCall {
174    /// Target contract address.
175    pub to: AztecAddress,
176    /// Function selector identifying the function to call.
177    pub selector: FunctionSelector,
178    /// Encoded function arguments.
179    pub args: Vec<AbiValue>,
180    /// The type of function being called.
181    pub function_type: FunctionType,
182    /// Whether this is a static (read-only) call.
183    pub is_static: bool,
184    /// Whether the msg_sender should be hidden from the callee.
185    #[serde(default)]
186    pub hide_msg_sender: bool,
187}
188
189impl FunctionCall {
190    /// The canonical empty function call, used for padding entrypoint payloads.
191    pub fn empty() -> Self {
192        Self {
193            to: AztecAddress::zero(),
194            selector: FunctionSelector::empty(),
195            args: vec![],
196            function_type: FunctionType::Private,
197            is_static: false,
198            hide_msg_sender: false,
199        }
200    }
201
202    /// Returns `true` if this is the canonical empty call.
203    pub fn is_empty(&self) -> bool {
204        self.to == AztecAddress::zero() && self.selector == FunctionSelector::empty()
205    }
206}
207
208/// An authorization witness proving the caller's intent.
209#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
210pub struct AuthWitness {
211    /// The message hash this witness authorizes.
212    #[serde(default)]
213    pub request_hash: Fr,
214    /// Field elements comprising the witness data.
215    #[serde(default)]
216    pub fields: Vec<Fr>,
217}
218
219/// Private data capsule passed alongside a transaction.
220///
221/// Structured capsule with contract address, storage slot, and field data.
222/// Used for passing auxiliary data (e.g., packed bytecode) to protocol contracts.
223#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
224pub struct Capsule {
225    /// The contract address this capsule targets.
226    pub contract_address: AztecAddress,
227    /// The storage slot within the target contract.
228    pub storage_slot: Fr,
229    /// Capsule data as field elements.
230    pub data: Vec<Fr>,
231}
232
233/// Transaction context.
234///
235/// Carries the replay-protection metadata and gas settings used to build a
236/// transaction execution request.
237#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct TxContext {
240    /// L1 chain ID used for replay protection.
241    pub chain_id: Fr,
242    /// Rollup protocol version used for replay protection.
243    pub version: Fr,
244    /// Gas settings for the transaction.
245    pub gas_settings: GasSettings,
246}
247
248/// Pre-hashed values included in a transaction for oracle access.
249#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
250#[serde(rename_all = "camelCase")]
251pub struct HashedValues {
252    /// Field elements to be hashed.
253    #[serde(default)]
254    pub values: Vec<Fr>,
255    /// Pre-computed hash of `values`.
256    #[serde(default)]
257    pub hash: Fr,
258}
259
260impl HashedValues {
261    /// Create hashed values from raw argument fields.
262    pub fn from_args(args: Vec<Fr>) -> Self {
263        let hash = crate::hash::compute_var_args_hash(&args);
264        Self { values: args, hash }
265    }
266
267    /// Create hashed values from calldata (selector + args for public calls).
268    pub fn from_calldata(calldata: Vec<Fr>) -> Self {
269        let hash = crate::hash::compute_calldata_hash(&calldata);
270        Self {
271            values: calldata,
272            hash,
273        }
274    }
275
276    /// Return the stored hash of the contained values.
277    pub fn hash(&self) -> Fr {
278        self.hash
279    }
280}
281
282/// A complete transaction execution payload.
283///
284/// Groups one or more [`FunctionCall`]s with their associated auth witnesses,
285/// capsules, hashed args, and an optional fee payer override.
286#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
287pub struct ExecutionPayload {
288    /// Function calls to execute.
289    #[serde(default)]
290    pub calls: Vec<FunctionCall>,
291    /// Authorization witnesses for the calls.
292    #[serde(default)]
293    pub auth_witnesses: Vec<AuthWitness>,
294    /// Private data capsules.
295    #[serde(default)]
296    pub capsules: Vec<Capsule>,
297    /// Extra hashed arguments for oracle access.
298    #[serde(default)]
299    pub extra_hashed_args: Vec<HashedValues>,
300    /// Override the fee payer for this payload.
301    pub fee_payer: Option<AztecAddress>,
302}
303
304impl ExecutionPayload {
305    /// Merge multiple execution payloads into a single payload.
306    ///
307    /// Combines all calls, auth witnesses, capsules, and hashed args.
308    /// If multiple payloads specify a `fee_payer`, they must all agree
309    /// on the same address — otherwise this returns an error.
310    pub fn merge(payloads: Vec<ExecutionPayload>) -> Result<Self, Error> {
311        let mut calls = Vec::new();
312        let mut auth_witnesses = Vec::new();
313        let mut capsules = Vec::new();
314        let mut extra_hashed_args = Vec::new();
315        let mut fee_payer: Option<AztecAddress> = None;
316
317        for payload in payloads {
318            calls.extend(payload.calls);
319            auth_witnesses.extend(payload.auth_witnesses);
320            capsules.extend(payload.capsules);
321            extra_hashed_args.extend(payload.extra_hashed_args);
322
323            if let Some(payer) = payload.fee_payer {
324                if let Some(existing) = fee_payer {
325                    if existing != payer {
326                        return Err(Error::InvalidData(format!(
327                            "conflicting fee payers: {existing} vs {payer}"
328                        )));
329                    }
330                }
331                fee_payer = Some(payer);
332            }
333        }
334
335        Ok(ExecutionPayload {
336            calls,
337            auth_witnesses,
338            capsules,
339            extra_hashed_args,
340            fee_payer,
341        })
342    }
343}
344
345#[cfg(test)]
346#[allow(clippy::expect_used, clippy::panic)]
347mod tests {
348    use super::*;
349
350    fn make_receipt(status: TxStatus, exec: Option<TxExecutionResult>) -> TxReceipt {
351        TxReceipt {
352            tx_hash: TxHash::zero(),
353            status,
354            execution_result: exec,
355            error: None,
356            transaction_fee: None,
357            block_hash: None,
358            block_number: None,
359            epoch_number: None,
360        }
361    }
362
363    #[test]
364    fn tx_hash_hex_roundtrip() {
365        let hash = TxHash([0xab; 32]);
366        let json = serde_json::to_string(&hash).expect("serialize TxHash");
367        assert!(json.contains("0xabab"), "should serialize as hex string");
368        let decoded: TxHash = serde_json::from_str(&json).expect("deserialize TxHash");
369        assert_eq!(decoded, hash);
370    }
371
372    #[test]
373    fn tx_hash_from_hex() {
374        let hash =
375            TxHash::from_hex("0x0000000000000000000000000000000000000000000000000000000000000001")
376                .expect("valid hex");
377        assert_eq!(hash.0[31], 1);
378        assert_eq!(hash.0[0], 0);
379    }
380
381    #[test]
382    fn tx_hash_display() {
383        let hash = TxHash::zero();
384        let s = hash.to_string();
385        assert_eq!(
386            s,
387            "0x0000000000000000000000000000000000000000000000000000000000000000"
388        );
389    }
390
391    #[test]
392    fn tx_status_roundtrip() {
393        let statuses = [
394            (TxStatus::Dropped, "\"dropped\""),
395            (TxStatus::Pending, "\"pending\""),
396            (TxStatus::Proposed, "\"proposed\""),
397            (TxStatus::Checkpointed, "\"checkpointed\""),
398            (TxStatus::Proven, "\"proven\""),
399            (TxStatus::Finalized, "\"finalized\""),
400        ];
401
402        for (status, expected_json) in statuses {
403            let json = serde_json::to_string(&status).expect("serialize TxStatus");
404            assert_eq!(json, expected_json);
405            let decoded: TxStatus = serde_json::from_str(&json).expect("deserialize TxStatus");
406            assert_eq!(decoded, status);
407        }
408    }
409
410    #[test]
411    fn tx_execution_result_roundtrip() {
412        let results = [
413            TxExecutionResult::Success,
414            TxExecutionResult::AppLogicReverted,
415            TxExecutionResult::TeardownReverted,
416            TxExecutionResult::BothReverted,
417        ];
418
419        for result in results {
420            let json = serde_json::to_string(&result).expect("serialize TxExecutionResult");
421            let decoded: TxExecutionResult =
422                serde_json::from_str(&json).expect("deserialize TxExecutionResult");
423            assert_eq!(decoded, result);
424        }
425    }
426
427    #[test]
428    fn receipt_mined_success() {
429        let receipt = TxReceipt {
430            tx_hash: TxHash::zero(),
431            status: TxStatus::Checkpointed,
432            execution_result: Some(TxExecutionResult::Success),
433            error: None,
434            transaction_fee: Some(1000),
435            block_hash: Some([0x11; 32]),
436            block_number: Some(42),
437            epoch_number: Some(1),
438        };
439
440        assert!(receipt.is_mined());
441        assert!(!receipt.is_pending());
442        assert!(!receipt.is_dropped());
443        assert!(receipt.has_execution_succeeded());
444        assert!(!receipt.has_execution_reverted());
445    }
446
447    #[test]
448    fn receipt_pending() {
449        let receipt = make_receipt(TxStatus::Pending, None);
450        assert!(!receipt.is_mined());
451        assert!(receipt.is_pending());
452        assert!(!receipt.is_dropped());
453        assert!(!receipt.has_execution_succeeded());
454        assert!(!receipt.has_execution_reverted());
455    }
456
457    #[test]
458    fn receipt_dropped() {
459        let receipt = make_receipt(TxStatus::Dropped, None);
460        assert!(!receipt.is_mined());
461        assert!(!receipt.is_pending());
462        assert!(receipt.is_dropped());
463    }
464
465    #[test]
466    fn receipt_reverted() {
467        let receipt = make_receipt(
468            TxStatus::Checkpointed,
469            Some(TxExecutionResult::AppLogicReverted),
470        );
471        assert!(receipt.is_mined());
472        assert!(!receipt.has_execution_succeeded());
473        assert!(receipt.has_execution_reverted());
474    }
475
476    #[test]
477    fn receipt_both_reverted() {
478        let receipt = make_receipt(
479            TxStatus::Checkpointed,
480            Some(TxExecutionResult::BothReverted),
481        );
482        assert!(receipt.has_execution_reverted());
483    }
484
485    #[test]
486    fn receipt_all_mined_statuses() {
487        for status in [
488            TxStatus::Proposed,
489            TxStatus::Checkpointed,
490            TxStatus::Proven,
491            TxStatus::Finalized,
492        ] {
493            let receipt = make_receipt(status, Some(TxExecutionResult::Success));
494            assert!(receipt.is_mined(), "{status:?} should count as mined");
495        }
496    }
497
498    #[test]
499    fn receipt_json_roundtrip() {
500        let receipt = TxReceipt {
501            tx_hash: TxHash::from_hex(
502                "0x00000000000000000000000000000000000000000000000000000000deadbeef",
503            )
504            .expect("valid hex"),
505            status: TxStatus::Finalized,
506            execution_result: Some(TxExecutionResult::Success),
507            error: None,
508            transaction_fee: Some(5000),
509            block_hash: Some([0xcc; 32]),
510            block_number: Some(100),
511            epoch_number: Some(10),
512        };
513
514        let json = serde_json::to_string(&receipt).expect("serialize receipt");
515        assert!(json.contains("deadbeef"), "tx_hash should be hex");
516        assert!(json.contains("0xcc"), "block_hash should be hex");
517
518        let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
519        assert_eq!(decoded, receipt);
520    }
521
522    #[test]
523    fn receipt_json_roundtrip_with_nulls() {
524        let receipt = TxReceipt {
525            tx_hash: TxHash::zero(),
526            status: TxStatus::Pending,
527            execution_result: None,
528            error: None,
529            transaction_fee: None,
530            block_hash: None,
531            block_number: None,
532            epoch_number: None,
533        };
534
535        let json = serde_json::to_string(&receipt).expect("serialize receipt");
536        let decoded: TxReceipt = serde_json::from_str(&json).expect("deserialize receipt");
537        assert_eq!(decoded, receipt);
538    }
539
540    #[test]
541    fn payload_serializes() {
542        let payload = ExecutionPayload::default();
543        let json = serde_json::to_string(&payload).expect("serialize ExecutionPayload");
544        assert!(json.contains("\"calls\":[]"));
545    }
546
547    #[test]
548    fn merge_empty_payloads() {
549        let result = ExecutionPayload::merge(vec![]).expect("merge empty");
550        assert!(result.calls.is_empty());
551        assert!(result.auth_witnesses.is_empty());
552        assert!(result.capsules.is_empty());
553        assert!(result.extra_hashed_args.is_empty());
554        assert!(result.fee_payer.is_none());
555    }
556
557    #[test]
558    fn merge_single_payload() {
559        let payer = AztecAddress(Fr::from(1u64));
560        let payload = ExecutionPayload {
561            calls: vec![FunctionCall {
562                to: AztecAddress(Fr::from(2u64)),
563                selector: FunctionSelector::from_hex("0x11223344").expect("valid"),
564                args: vec![],
565                function_type: FunctionType::Private,
566                is_static: false,
567                hide_msg_sender: false,
568            }],
569            auth_witnesses: vec![AuthWitness {
570                fields: vec![Fr::from(9u64)],
571                ..Default::default()
572            }],
573            capsules: vec![],
574            extra_hashed_args: vec![],
575            fee_payer: Some(payer),
576        };
577
578        let merged = ExecutionPayload::merge(vec![payload]).expect("merge single");
579        assert_eq!(merged.calls.len(), 1);
580        assert_eq!(merged.fee_payer, Some(payer));
581    }
582
583    #[test]
584    fn merge_concatenates_fields() {
585        let p1 = ExecutionPayload {
586            calls: vec![FunctionCall {
587                to: AztecAddress(Fr::from(1u64)),
588                selector: FunctionSelector::from_hex("0x11111111").expect("valid"),
589                args: vec![],
590                function_type: FunctionType::Private,
591                is_static: false,
592                hide_msg_sender: false,
593            }],
594            auth_witnesses: vec![AuthWitness {
595                fields: vec![Fr::from(1u64)],
596                ..Default::default()
597            }],
598            capsules: vec![],
599            extra_hashed_args: vec![],
600            fee_payer: None,
601        };
602
603        let p2 = ExecutionPayload {
604            calls: vec![FunctionCall {
605                to: AztecAddress(Fr::from(2u64)),
606                selector: FunctionSelector::from_hex("0x22222222").expect("valid"),
607                args: vec![],
608                function_type: FunctionType::Public,
609                is_static: false,
610                hide_msg_sender: false,
611            }],
612            auth_witnesses: vec![AuthWitness {
613                fields: vec![Fr::from(2u64)],
614                ..Default::default()
615            }],
616            capsules: vec![],
617            extra_hashed_args: vec![],
618            fee_payer: None,
619        };
620
621        let merged = ExecutionPayload::merge(vec![p1, p2]).expect("merge two");
622        assert_eq!(merged.calls.len(), 2);
623        assert_eq!(merged.auth_witnesses.len(), 2);
624        assert!(merged.fee_payer.is_none());
625    }
626
627    #[test]
628    fn merge_same_fee_payer_succeeds() {
629        let payer = AztecAddress(Fr::from(5u64));
630        let p1 = ExecutionPayload {
631            fee_payer: Some(payer),
632            ..Default::default()
633        };
634        let p2 = ExecutionPayload {
635            fee_payer: Some(payer),
636            ..Default::default()
637        };
638
639        let merged = ExecutionPayload::merge(vec![p1, p2]).expect("same payer");
640        assert_eq!(merged.fee_payer, Some(payer));
641    }
642
643    #[test]
644    fn merge_conflicting_fee_payer_errors() {
645        let p1 = ExecutionPayload {
646            fee_payer: Some(AztecAddress(Fr::from(1u64))),
647            ..Default::default()
648        };
649        let p2 = ExecutionPayload {
650            fee_payer: Some(AztecAddress(Fr::from(2u64))),
651            ..Default::default()
652        };
653
654        let result = ExecutionPayload::merge(vec![p1, p2]);
655        assert!(result.is_err());
656    }
657
658    #[test]
659    fn merge_mixed_fee_payer_takes_defined() {
660        let payer = AztecAddress(Fr::from(3u64));
661        let p1 = ExecutionPayload {
662            fee_payer: None,
663            ..Default::default()
664        };
665        let p2 = ExecutionPayload {
666            fee_payer: Some(payer),
667            ..Default::default()
668        };
669
670        let merged = ExecutionPayload::merge(vec![p1, p2]).expect("mixed payer");
671        assert_eq!(merged.fee_payer, Some(payer));
672    }
673
674    #[test]
675    fn payload_with_calls_roundtrip() {
676        let payload = ExecutionPayload {
677            calls: vec![FunctionCall {
678                to: AztecAddress(Fr::from(1u64)),
679                selector: crate::abi::FunctionSelector::from_hex("0xaabbccdd")
680                    .expect("valid selector"),
681                args: vec![AbiValue::Field(Fr::from(42u64))],
682                function_type: FunctionType::Private,
683                is_static: false,
684                hide_msg_sender: false,
685            }],
686            auth_witnesses: vec![AuthWitness {
687                fields: vec![Fr::from(1u64)],
688                ..Default::default()
689            }],
690            capsules: vec![],
691            extra_hashed_args: vec![],
692            fee_payer: Some(AztecAddress(Fr::from(99u64))),
693        };
694
695        let json = serde_json::to_string(&payload).expect("serialize payload");
696        let decoded: ExecutionPayload = serde_json::from_str(&json).expect("deserialize payload");
697        assert_eq!(decoded, payload);
698    }
699}