1use bytes::Bytes;
20
21use ethrex_common::types::Fork;
22use ethrex_crypto::Crypto;
23
24use crate::errors::{InternalError, PrecompileError, VMError};
25use crate::precompiles::increase_precompile_consumed_gas;
26
27pub const EXECUTE_GAS_PER_WITNESS_BYTE: u64 = 16;
30
31pub fn execute_precompile(
42 calldata: &Bytes,
43 gas_remaining: &mut u64,
44 _fork: Fork,
45 _crypto: &dyn Crypto,
46 stateless_validator: Option<&dyn crate::StatelessValidator>,
47) -> Result<Bytes, VMError> {
48 let validator = stateless_validator.ok_or_else(|| {
49 VMError::Internal(InternalError::Custom(
50 "EXECUTE precompile requires a StatelessValidator but none was provided".to_string(),
51 ))
52 })?;
53 run_execute(validator, calldata, gas_remaining)
54}
55
56fn run_execute(
58 validator: &dyn crate::StatelessValidator,
59 calldata: &Bytes,
60 gas_remaining: &mut u64,
61) -> Result<Bytes, VMError> {
62 use ethrex_common::types::stateless_ssz::{
63 STATELESS_INPUT_SCHEMA_ID, STATELESS_INPUT_SCHEMA_ID_SIZE, SszStatelessInput,
64 };
65 use libssz::SszDecode;
66
67 let (schema_bytes, body) = calldata
72 .split_first_chunk::<STATELESS_INPUT_SCHEMA_ID_SIZE>()
73 .ok_or(VMError::from(PrecompileError::ExecuteInvalidInput))?;
74 let schema_id = u16::from_be_bytes(*schema_bytes);
75 if schema_id != STATELESS_INPUT_SCHEMA_ID {
76 return Err(VMError::from(PrecompileError::ExecuteInvalidInput));
77 }
78
79 let input = SszStatelessInput::from_ssz_bytes(body)
80 .map_err(|_| VMError::from(PrecompileError::ExecuteInvalidInput))?;
81
82 validate_l2_constraints(&input)?;
83
84 let gas_limit = input.new_payload_request.execution_payload.gas_limit;
91 #[expect(
92 clippy::as_conversions,
93 reason = "usize to u64: safe on 64-bit targets; calldata len cannot exceed u64::MAX"
94 )]
95 let witness_charge = (calldata.len() as u64).saturating_mul(EXECUTE_GAS_PER_WITNESS_BYTE);
96 let charge = gas_limit.saturating_add(witness_charge);
97 increase_precompile_consumed_gas(charge, gas_remaining)?;
98
99 let result = validator.verify(&input)?;
102
103 use ethrex_common::types::stateless_ssz::SszStatelessValidationResult;
104 let parsed = SszStatelessValidationResult::from_ssz_bytes(&result).map_err(|e| {
105 VMError::Internal(InternalError::Custom(format!(
106 "EXECUTE: bad validation result: {e}"
107 )))
108 })?;
109 if !parsed.successful_validation {
110 return Err(PrecompileError::ExecuteValidationFailed.into());
115 }
116
117 Ok(Bytes::from(result))
118}
119
120fn validate_l2_constraints(
122 input: ðrex_common::types::stateless_ssz::SszStatelessInput,
123) -> Result<(), VMError> {
124 let payload = &input.new_payload_request.execution_payload;
125
126 if payload.blob_gas_used != 0 {
128 return Err(PrecompileError::ExecuteInvalidInput.into());
129 }
130 if payload.excess_blob_gas != 0 {
131 return Err(PrecompileError::ExecuteInvalidInput.into());
132 }
133 if !payload.withdrawals.is_empty() {
134 return Err(PrecompileError::ExecuteInvalidInput.into());
135 }
136 let reqs = &input.new_payload_request.execution_requests;
137 if !reqs.deposits.is_empty()
138 || !reqs.withdrawals.is_empty()
139 || !reqs.consolidations.is_empty()
140 || !reqs.builder_deposits.is_empty()
141 || !reqs.builder_exits.is_empty()
142 {
143 return Err(PrecompileError::ExecuteInvalidInput.into());
144 }
145 for tx_bytes in payload.transactions.iter() {
146 if let Some(&(0x03 | 0x06 | 0x7e)) = tx_bytes.iter().next() {
153 return Err(PrecompileError::ExecuteInvalidInput.into());
154 }
155 }
156
157 Ok(())
158}
159
160#[cfg(test)]
161#[allow(
162 clippy::unwrap_used,
163 clippy::expect_used,
164 clippy::arithmetic_side_effects,
165 clippy::as_conversions
166)]
167mod tests {
168 use super::{EXECUTE_GAS_PER_WITNESS_BYTE, run_execute};
169 use bytes::Bytes;
170 use ethrex_common::types::stateless_ssz::{
171 BuilderDepositRequest, BuilderExitRequest, Bytes20, DepositRequest, ExecutionPayload,
172 ExecutionRequests, NewPayloadRequest, SszExecutionWitness, SszStatelessInput,
173 SszStatelessValidationResult, Withdrawal,
174 };
175 use libssz::SszEncode;
176
177 struct MockValidator;
180
181 struct MockInvalidValidator;
183
184 impl crate::StatelessValidator for MockValidator {
185 fn verify(&self, _input: &SszStatelessInput) -> Result<Vec<u8>, crate::errors::VMError> {
186 let result = SszStatelessValidationResult {
187 new_payload_request_root: [0u8; 32],
188 successful_validation: true,
189 chain_id: 1,
190 schema_id: 0x1501,
191 };
192 let mut buf = Vec::new();
193 result.ssz_append(&mut buf);
194 Ok(buf)
195 }
196 }
197
198 fn calldata_for(input: &SszStatelessInput) -> Bytes {
203 let mut buf = ethrex_common::types::stateless_ssz::STATELESS_INPUT_SCHEMA_ID
204 .to_be_bytes()
205 .to_vec();
206 input.ssz_append(&mut buf);
207 Bytes::from(buf)
208 }
209
210 fn assert_invalid_input(err: &crate::errors::VMError, what: &str) {
213 assert_eq!(
214 err,
215 &crate::errors::VMError::from(crate::errors::PrecompileError::ExecuteInvalidInput),
216 "{what} must be rejected as ExecuteInvalidInput; got: {err:?}"
217 );
218 assert!(
219 !err.should_propagate(),
220 "{what} must be a CALL-level failure (non-propagating); got: {err:?}"
221 );
222 }
223
224 fn l2_valid_input(gas_used: u64, gas_limit: u64) -> SszStatelessInput {
228 SszStatelessInput {
229 new_payload_request: NewPayloadRequest {
230 execution_payload: ExecutionPayload {
231 parent_hash: [0u8; 32],
232 fee_recipient: Bytes20([0u8; 20]),
233 state_root: [0u8; 32],
234 receipts_root: [0u8; 32],
235 logs_bloom: vec![0u8; 256].try_into().expect("logs_bloom"),
236 prev_randao: [0u8; 32],
237 block_number: 1,
238 gas_limit,
239 gas_used,
240 timestamp: 1_000_000,
241 extra_data: vec![].try_into().expect("extra_data"),
242 base_fee_per_gas: [0u8; 32],
243 block_hash: [0u8; 32],
244 transactions: Vec::new().into(),
245 withdrawals: Vec::new().into(),
246 blob_gas_used: 0,
247 excess_blob_gas: 0,
248 block_access_list: Vec::new().into(),
249 slot_number: 0,
250 },
251 versioned_hashes: Vec::new().into(),
252 parent_beacon_block_root: [0u8; 32],
253 execution_requests: ExecutionRequests {
254 deposits: Vec::new().into(),
255 withdrawals: Vec::new().into(),
256 consolidations: Vec::new().into(),
257 builder_deposits: Vec::new().into(),
258 builder_exits: Vec::new().into(),
259 },
260 },
261 witness: SszExecutionWitness {
262 state: vec![].try_into().expect("state"),
263 codes: vec![].try_into().expect("codes"),
264 headers: vec![].try_into().expect("headers"),
265 },
266 chain_id: 1,
267 public_keys: vec![].try_into().expect("public_keys"),
268 }
269 }
270
271 impl crate::StatelessValidator for MockInvalidValidator {
272 fn verify(&self, _input: &SszStatelessInput) -> Result<Vec<u8>, crate::errors::VMError> {
273 let result = SszStatelessValidationResult {
274 new_payload_request_root: [0u8; 32],
275 successful_validation: false,
276 chain_id: 1,
277 schema_id: 0x1501,
278 };
279 let mut buf = Vec::new();
280 result.ssz_append(&mut buf);
281 Ok(buf)
282 }
283 }
284
285 #[test]
288 fn execute_fails_closed_on_invalid() {
289 let input = l2_valid_input(0, 1_000_000);
290 let calldata = calldata_for(&input);
291
292 let mut gas = 100_000_000u64;
295 let mock_invalid = MockInvalidValidator;
296 let r = run_execute(&mock_invalid, &calldata, &mut gas);
297 let err = r.expect_err("invalid validation must revert (fail-closed), not return Ok");
298 assert!(
299 !err.should_propagate(),
300 "invalid EXECUTE must be a CALL-level failure (includable, attacker pays), not a tx-abort; got: {err:?}"
301 );
302
303 let mut gas2 = 100_000_000u64;
305 let mock_valid = MockValidator;
306 let r2 = run_execute(&mock_valid, &calldata, &mut gas2);
307 assert!(r2.is_ok(), "valid validation must return Ok(result)");
308 }
309
310 #[test]
314 fn execute_charges_gas_limit_not_gas_used() {
315 let input = l2_valid_input(0, 1_000_000);
316 let calldata = calldata_for(&input);
317
318 let start_gas = 100_000_000u64;
319 let mut gas_remaining = start_gas;
320 let mock = MockValidator;
321 let _ = run_execute(&mock, &calldata, &mut gas_remaining);
322
323 let charged = start_gas.saturating_sub(gas_remaining);
324 let expected = 1_000_000u64.saturating_add(
325 u64::try_from(calldata.len())
326 .expect("calldata len fits u64")
327 .saturating_mul(EXECUTE_GAS_PER_WITNESS_BYTE),
328 );
329
330 assert!(
332 charged >= 1_000_000,
333 "must charge at least gas_limit, got {charged}"
334 );
335 assert_eq!(
337 charged, expected,
338 "charge must be gas_limit + calldata.len() * EXECUTE_GAS_PER_WITNESS_BYTE"
339 );
340 }
341
342 #[test]
349 fn execute_rejects_malformed_ssz_input() {
350 let calldata = Bytes::from(vec![0xffu8, 0xff, 0xff, 0xff]);
352 let mut gas = 100_000_000u64;
353 let err = run_execute(&MockValidator, &calldata, &mut gas)
354 .expect_err("malformed SSZ input must be rejected");
355 assert_invalid_input(&err, "malformed SSZ input");
356 }
357
358 #[test]
370 fn execute_rejects_blob_gas_used_nonzero() {
371 let mut input = l2_valid_input(0, 1_000_000);
372 input.new_payload_request.execution_payload.blob_gas_used = 1;
373 let calldata = calldata_for(&input);
374 let mut gas = 100_000_000u64;
375 let err = run_execute(&MockValidator, &calldata, &mut gas)
376 .expect_err("blob_gas_used != 0 must be rejected");
377 assert_invalid_input(&err, "blob_gas_used != 0");
378 }
379
380 #[test]
382 fn execute_rejects_excess_blob_gas_nonzero() {
383 let mut input = l2_valid_input(0, 1_000_000);
384 input.new_payload_request.execution_payload.excess_blob_gas = 1;
385 let calldata = calldata_for(&input);
386 let mut gas = 100_000_000u64;
387 let err = run_execute(&MockValidator, &calldata, &mut gas)
388 .expect_err("excess_blob_gas != 0 must be rejected");
389 assert_invalid_input(&err, "excess_blob_gas != 0");
390 }
391
392 #[test]
394 fn execute_rejects_nonempty_withdrawals() {
395 let mut input = l2_valid_input(0, 1_000_000);
396 input.new_payload_request.execution_payload.withdrawals = vec![Withdrawal {
397 index: 0,
398 validator_index: 0,
399 address: Bytes20([0u8; 20]),
400 amount: 1,
401 }]
402 .try_into()
403 .expect("withdrawals");
404 let calldata = calldata_for(&input);
405 let mut gas = 100_000_000u64;
406 let err = run_execute(&MockValidator, &calldata, &mut gas)
407 .expect_err("non-empty withdrawals must be rejected");
408 assert_invalid_input(&err, "non-empty withdrawals");
409 }
410
411 #[test]
414 fn execute_rejects_nonempty_execution_requests() {
415 let mut input = l2_valid_input(0, 1_000_000);
416 input.new_payload_request.execution_requests.deposits = vec![DepositRequest {
417 pubkey: [0u8; 48],
418 withdrawal_credentials: [0u8; 32],
419 amount: 1,
420 signature: [0u8; 96],
421 index: 0,
422 }]
423 .try_into()
424 .expect("deposits");
425 let calldata = calldata_for(&input);
426 let mut gas = 100_000_000u64;
427 let err = run_execute(&MockValidator, &calldata, &mut gas)
428 .expect_err("non-empty execution_requests must be rejected");
429 assert_invalid_input(&err, "non-empty execution_requests");
430 }
431
432 #[test]
436 fn execute_rejects_nonempty_builder_requests() {
437 let mut input = l2_valid_input(0, 1_000_000);
438 input
439 .new_payload_request
440 .execution_requests
441 .builder_deposits = vec![BuilderDepositRequest {
442 pubkey: [0u8; 48],
443 withdrawal_credentials: [0u8; 32],
444 amount: 1,
445 signature: [0u8; 96],
446 }]
447 .try_into()
448 .expect("builder_deposits");
449 let mut gas = 100_000_000u64;
450 let err = run_execute(&MockValidator, &calldata_for(&input), &mut gas)
451 .expect_err("non-empty builder_deposits must be rejected");
452 assert_invalid_input(&err, "non-empty builder_deposits");
453
454 let mut input = l2_valid_input(0, 1_000_000);
455 input.new_payload_request.execution_requests.builder_exits = vec![BuilderExitRequest {
456 source_address: Bytes20([0u8; 20]),
457 pubkey: [0u8; 48],
458 }]
459 .try_into()
460 .expect("builder_exits");
461 let mut gas = 100_000_000u64;
462 let err = run_execute(&MockValidator, &calldata_for(&input), &mut gas)
463 .expect_err("non-empty builder_exits must be rejected");
464 assert_invalid_input(&err, "non-empty builder_exits");
465 }
466
467 #[test]
477 fn execute_rejects_unsupported_transaction_types() {
478 for (type_byte, what) in [
479 (0x03u8, "blob-typed transaction"),
480 (0x06u8, "frame transaction"),
481 (0x7eu8, "privileged transaction"),
482 ] {
483 let mut input = l2_valid_input(0, 1_000_000);
484 let tx = vec![type_byte, 0x00, 0x00].try_into().expect("tx bytes");
485 input.new_payload_request.execution_payload.transactions =
486 vec![tx].try_into().expect("transactions");
487 let calldata = calldata_for(&input);
488 let mut gas = 100_000_000u64;
489 let err = run_execute(&MockValidator, &calldata, &mut gas)
490 .expect_err("unsupported transaction type must be rejected");
491 assert_invalid_input(&err, what);
492 }
493
494 let mut input = l2_valid_input(0, 1_000_000);
497 let tx = vec![0x02u8, 0x00, 0x00].try_into().expect("tx bytes");
498 input.new_payload_request.execution_payload.transactions =
499 vec![tx].try_into().expect("transactions");
500 let calldata = calldata_for(&input);
501 let mut gas = 100_000_000u64;
502 run_execute(&MockValidator, &calldata, &mut gas)
503 .expect("an EIP-1559 transaction must be accepted");
504 }
505
506 #[test]
511 fn execute_requires_the_expected_schema_prefix() {
512 let input = l2_valid_input(0, 1_000_000);
513 let mut body = Vec::new();
514 input.ssz_append(&mut body);
515
516 let mut gas = 100_000_000u64;
518 let err = run_execute(&MockValidator, &Bytes::from(body.clone()), &mut gas)
519 .expect_err("unprefixed input must be rejected");
520 assert_invalid_input(&err, "unprefixed input");
521
522 let mut wrong = vec![0x15u8, 0x02];
524 wrong.extend_from_slice(&body);
525 let mut gas = 100_000_000u64;
526 let err = run_execute(&MockValidator, &Bytes::from(wrong), &mut gas)
527 .expect_err("unexpected schema id must be rejected");
528 assert_invalid_input(&err, "unexpected schema id");
529
530 let mut gas = 100_000_000u64;
532 let err = run_execute(&MockValidator, &Bytes::from(vec![0x15u8]), &mut gas)
533 .expect_err("truncated input must be rejected");
534 assert_invalid_input(&err, "input too short for a schema id");
535
536 let mut gas = 100_000_000u64;
538 run_execute(&MockValidator, &calldata_for(&input), &mut gas)
539 .expect("the expected schema prefix must be accepted");
540 }
541}