Skip to main content

aztec_core/
validation.rs

1//! Transaction validation logic matching the node's `DataTxValidator`.
2//!
3//! These functions implement the same invariant checks as the upstream
4//! `p2p/src/msg_validators/tx_validator/data_validator.ts`.
5
6use crate::constants::MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS;
7use crate::hash::compute_calldata_hash;
8use crate::kernel_types::PrivateKernelTailPublicInputs;
9use crate::tx::{ContractClassLogFields, TypedTx};
10use crate::types::Fr;
11use crate::Error;
12
13/// Validate the calldata in a typed transaction.
14///
15/// Checks:
16/// - `publicFunctionCalldata.len() == numberOfPublicCalls()`
17/// - total calldata count <= MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS
18/// - each calldata hash matches the corresponding public call request
19pub fn validate_calldata(tx: &TypedTx) -> Result<(), Error> {
20    let expected_count = tx.number_of_public_calls();
21    let actual_count = tx.public_function_calldata.len();
22
23    if actual_count != expected_count {
24        return Err(Error::InvalidData(format!(
25            "TX_ERROR_CALLDATA_COUNT_MISMATCH: expected {expected_count} calldata entries, got {actual_count}"
26        )));
27    }
28
29    let total_fields = tx.get_total_public_calldata_count();
30    if total_fields > MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS {
31        return Err(Error::InvalidData(format!(
32            "TX_ERROR_CALLDATA_COUNT_TOO_LARGE: total calldata fields {total_fields} exceeds max {MAX_FR_CALLDATA_TO_ALL_ENQUEUED_CALLS}"
33        )));
34    }
35
36    // Verify each calldata hash
37    for (request, calldata) in tx.get_public_call_requests_with_calldata() {
38        let computed_hash = compute_calldata_hash(&calldata.values);
39        if computed_hash != request.calldata_hash {
40            return Err(Error::InvalidData(format!(
41                "TX_ERROR_INCORRECT_CALLDATA: calldata hash mismatch for call to {}",
42                request.contract_address
43            )));
44        }
45    }
46
47    Ok(())
48}
49
50/// Validate the contract class logs in a typed transaction.
51///
52/// Checks:
53/// - number of log fields entries matches log hashes in public inputs
54/// - each log hash matches the hash of its corresponding fields
55pub fn validate_contract_class_logs(
56    public_inputs: &PrivateKernelTailPublicInputs,
57    log_fields: &[ContractClassLogFields],
58) -> Result<(), Error> {
59    let log_hashes = public_inputs.get_non_empty_contract_class_logs_hashes();
60
61    if log_hashes.len() != log_fields.len() {
62        return Err(Error::InvalidData(format!(
63            "TX_ERROR_CONTRACT_CLASS_LOG_COUNT: expected {} log field entries, got {}",
64            log_hashes.len(),
65            log_fields.len()
66        )));
67    }
68
69    // Each log's emitted fields must have a minimum non-zero length
70    for (i, fields) in log_fields.iter().enumerate() {
71        let expected_min_length = 1 + fields
72            .fields
73            .iter()
74            .rposition(|f| *f != Fr::zero())
75            .unwrap_or(0);
76
77        if let Some(hash_entry) = log_hashes.get(i) {
78            if (hash_entry.log_hash.length as usize) < expected_min_length {
79                return Err(Error::InvalidData(format!(
80                    "TX_ERROR_CONTRACT_CLASS_LOG_LENGTH: log {} has length {} but minimum is {}",
81                    i, hash_entry.log_hash.length, expected_min_length
82                )));
83            }
84        }
85    }
86
87    Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::kernel_types::*;
94    use crate::tx::*;
95
96    #[test]
97    fn validate_calldata_empty_tx() {
98        let tx = TypedTx {
99            tx_hash: TxHash::zero(),
100            data: PrivateKernelTailPublicInputs {
101                for_rollup: Some(PartialPrivateTailPublicInputsForRollup {
102                    end: PrivateToRollupAccumulatedData::default(),
103                }),
104                ..Default::default()
105            },
106            chonk_proof: ChonkProof::default(),
107            contract_class_log_fields: vec![],
108            public_function_calldata: vec![],
109        };
110        assert!(validate_calldata(&tx).is_ok());
111    }
112
113    #[test]
114    fn validate_calldata_count_mismatch() {
115        let tx = TypedTx {
116            tx_hash: TxHash::zero(),
117            data: PrivateKernelTailPublicInputs {
118                for_public: Some(PartialPrivateTailPublicInputsForPublic {
119                    non_revertible_accumulated_data: PrivateToPublicAccumulatedData {
120                        public_call_requests: vec![PublicCallRequest {
121                            contract_address: crate::types::AztecAddress(Fr::from(1u64)),
122                            ..Default::default()
123                        }],
124                        ..Default::default()
125                    },
126                    ..Default::default()
127                }),
128                ..Default::default()
129            },
130            chonk_proof: ChonkProof::default(),
131            contract_class_log_fields: vec![],
132            public_function_calldata: vec![], // Mismatch: 0 calldata for 1 call
133        };
134        let err = validate_calldata(&tx).unwrap_err();
135        assert!(err.to_string().contains("CALLDATA_COUNT_MISMATCH"));
136    }
137
138    #[test]
139    fn validate_calldata_hash_match() {
140        let calldata = vec![Fr::from(42u64), Fr::from(99u64)];
141        let hash = compute_calldata_hash(&calldata);
142
143        let tx = TypedTx {
144            tx_hash: TxHash::zero(),
145            data: PrivateKernelTailPublicInputs {
146                for_public: Some(PartialPrivateTailPublicInputsForPublic {
147                    non_revertible_accumulated_data: PrivateToPublicAccumulatedData {
148                        public_call_requests: vec![PublicCallRequest {
149                            contract_address: crate::types::AztecAddress(Fr::from(1u64)),
150                            calldata_hash: hash,
151                            ..Default::default()
152                        }],
153                        ..Default::default()
154                    },
155                    ..Default::default()
156                }),
157                ..Default::default()
158            },
159            chonk_proof: ChonkProof::default(),
160            contract_class_log_fields: vec![],
161            public_function_calldata: vec![HashedValues {
162                values: calldata,
163                hash,
164            }],
165        };
166        assert!(validate_calldata(&tx).is_ok());
167    }
168
169    #[test]
170    fn validate_calldata_hash_mismatch() {
171        let calldata = vec![Fr::from(42u64)];
172        let correct_hash = compute_calldata_hash(&calldata);
173        let wrong_hash = Fr::from(999u64);
174
175        let tx = TypedTx {
176            tx_hash: TxHash::zero(),
177            data: PrivateKernelTailPublicInputs {
178                for_public: Some(PartialPrivateTailPublicInputsForPublic {
179                    non_revertible_accumulated_data: PrivateToPublicAccumulatedData {
180                        public_call_requests: vec![PublicCallRequest {
181                            contract_address: crate::types::AztecAddress(Fr::from(1u64)),
182                            calldata_hash: wrong_hash,
183                            ..Default::default()
184                        }],
185                        ..Default::default()
186                    },
187                    ..Default::default()
188                }),
189                ..Default::default()
190            },
191            chonk_proof: ChonkProof::default(),
192            contract_class_log_fields: vec![],
193            public_function_calldata: vec![HashedValues {
194                values: calldata,
195                hash: correct_hash,
196            }],
197        };
198        let err = validate_calldata(&tx).unwrap_err();
199        assert!(err.to_string().contains("INCORRECT_CALLDATA"));
200    }
201
202    #[test]
203    fn validate_contract_class_logs_empty() {
204        let pi = PrivateKernelTailPublicInputs {
205            for_rollup: Some(PartialPrivateTailPublicInputsForRollup {
206                end: PrivateToRollupAccumulatedData::default(),
207            }),
208            ..Default::default()
209        };
210        assert!(validate_contract_class_logs(&pi, &[]).is_ok());
211    }
212
213    #[test]
214    fn validate_contract_class_logs_count_mismatch() {
215        let pi = PrivateKernelTailPublicInputs {
216            for_rollup: Some(PartialPrivateTailPublicInputsForRollup {
217                end: PrivateToRollupAccumulatedData {
218                    contract_class_logs_hashes: vec![ScopedLogHash {
219                        log_hash: LogHash {
220                            value: Fr::from(1u64),
221                            length: 10,
222                        },
223                        contract_address: crate::types::AztecAddress(Fr::from(1u64)),
224                    }],
225                    ..Default::default()
226                },
227            }),
228            ..Default::default()
229        };
230        let err = validate_contract_class_logs(&pi, &[]).unwrap_err();
231        assert!(err.to_string().contains("LOG_COUNT"));
232    }
233}