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: &[u8],
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: &[u8],
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() {
152 return Err(PrecompileError::ExecuteInvalidInput.into());
153 }
154 }
155
156 Ok(())
157}
158
159#[cfg(test)]
160#[allow(
161 clippy::unwrap_used,
162 clippy::expect_used,
163 clippy::arithmetic_side_effects,
164 clippy::as_conversions
165)]
166mod tests {
167 use super::{EXECUTE_GAS_PER_WITNESS_BYTE, run_execute};
168 use bytes::Bytes;
169 use ethrex_common::types::stateless_ssz::{
170 BuilderDepositRequest, BuilderExitRequest, Bytes20, DepositRequest, ExecutionPayload,
171 ExecutionRequests, NewPayloadRequest, SszExecutionWitness, SszStatelessInput,
172 SszStatelessValidationResult, Withdrawal,
173 };
174 use libssz::SszEncode;
175
176 struct MockValidator;
179
180 struct MockInvalidValidator;
182
183 impl crate::StatelessValidator for MockValidator {
184 fn verify(&self, _input: &SszStatelessInput) -> Result<Vec<u8>, crate::errors::VMError> {
185 let result = SszStatelessValidationResult {
186 new_payload_request_root: [0u8; 32],
187 successful_validation: true,
188 chain_id: 1,
189 schema_id: 0x1501,
190 };
191 let mut buf = Vec::new();
192 result.ssz_append(&mut buf);
193 Ok(buf)
194 }
195 }
196
197 fn calldata_for(input: &SszStatelessInput) -> Bytes {
202 let mut buf = ethrex_common::types::stateless_ssz::STATELESS_INPUT_SCHEMA_ID
203 .to_be_bytes()
204 .to_vec();
205 input.ssz_append(&mut buf);
206 Bytes::from(buf)
207 }
208
209 fn assert_invalid_input(err: &crate::errors::VMError, what: &str) {
212 assert_eq!(
213 err,
214 &crate::errors::VMError::from(crate::errors::PrecompileError::ExecuteInvalidInput),
215 "{what} must be rejected as ExecuteInvalidInput; got: {err:?}"
216 );
217 assert!(
218 !err.should_propagate(),
219 "{what} must be a CALL-level failure (non-propagating); got: {err:?}"
220 );
221 }
222
223 fn l2_valid_input(gas_used: u64, gas_limit: u64) -> SszStatelessInput {
227 SszStatelessInput {
228 new_payload_request: NewPayloadRequest {
229 execution_payload: ExecutionPayload {
230 parent_hash: [0u8; 32],
231 fee_recipient: Bytes20([0u8; 20]),
232 state_root: [0u8; 32],
233 receipts_root: [0u8; 32],
234 logs_bloom: vec![0u8; 256].try_into().expect("logs_bloom"),
235 prev_randao: [0u8; 32],
236 block_number: 1,
237 gas_limit,
238 gas_used,
239 timestamp: 1_000_000,
240 extra_data: vec![].try_into().expect("extra_data"),
241 base_fee_per_gas: [0u8; 32],
242 block_hash: [0u8; 32],
243 transactions: Vec::new().into(),
244 withdrawals: Vec::new().into(),
245 blob_gas_used: 0,
246 excess_blob_gas: 0,
247 block_access_list: Vec::new().into(),
248 slot_number: 0,
249 },
250 versioned_hashes: Vec::new().into(),
251 parent_beacon_block_root: [0u8; 32],
252 execution_requests: ExecutionRequests {
253 deposits: Vec::new().into(),
254 withdrawals: Vec::new().into(),
255 consolidations: Vec::new().into(),
256 builder_deposits: Vec::new().into(),
257 builder_exits: Vec::new().into(),
258 },
259 },
260 witness: SszExecutionWitness {
261 state: vec![].try_into().expect("state"),
262 codes: vec![].try_into().expect("codes"),
263 headers: vec![].try_into().expect("headers"),
264 },
265 chain_id: 1,
266 }
267 }
268
269 impl crate::StatelessValidator for MockInvalidValidator {
270 fn verify(&self, _input: &SszStatelessInput) -> Result<Vec<u8>, crate::errors::VMError> {
271 let result = SszStatelessValidationResult {
272 new_payload_request_root: [0u8; 32],
273 successful_validation: false,
274 chain_id: 1,
275 schema_id: 0x1501,
276 };
277 let mut buf = Vec::new();
278 result.ssz_append(&mut buf);
279 Ok(buf)
280 }
281 }
282
283 #[test]
286 fn execute_fails_closed_on_invalid() {
287 let input = l2_valid_input(0, 1_000_000);
288 let calldata = calldata_for(&input);
289
290 let mut gas = 100_000_000u64;
293 let mock_invalid = MockInvalidValidator;
294 let r = run_execute(&mock_invalid, &calldata, &mut gas);
295 let err = r.expect_err("invalid validation must revert (fail-closed), not return Ok");
296 assert!(
297 !err.should_propagate(),
298 "invalid EXECUTE must be a CALL-level failure (includable, attacker pays), not a tx-abort; got: {err:?}"
299 );
300
301 let mut gas2 = 100_000_000u64;
303 let mock_valid = MockValidator;
304 let r2 = run_execute(&mock_valid, &calldata, &mut gas2);
305 assert!(r2.is_ok(), "valid validation must return Ok(result)");
306 }
307
308 #[test]
312 fn execute_charges_gas_limit_not_gas_used() {
313 let input = l2_valid_input(0, 1_000_000);
314 let calldata = calldata_for(&input);
315
316 let start_gas = 100_000_000u64;
317 let mut gas_remaining = start_gas;
318 let mock = MockValidator;
319 let _ = run_execute(&mock, &calldata, &mut gas_remaining);
320
321 let charged = start_gas.saturating_sub(gas_remaining);
322 let expected = 1_000_000u64.saturating_add(
323 u64::try_from(calldata.len())
324 .expect("calldata len fits u64")
325 .saturating_mul(EXECUTE_GAS_PER_WITNESS_BYTE),
326 );
327
328 assert!(
330 charged >= 1_000_000,
331 "must charge at least gas_limit, got {charged}"
332 );
333 assert_eq!(
335 charged, expected,
336 "charge must be gas_limit + calldata.len() * EXECUTE_GAS_PER_WITNESS_BYTE"
337 );
338 }
339
340 #[test]
347 fn execute_rejects_malformed_ssz_input() {
348 let calldata = Bytes::from(vec![0xffu8, 0xff, 0xff, 0xff]);
350 let mut gas = 100_000_000u64;
351 let err = run_execute(&MockValidator, &calldata, &mut gas)
352 .expect_err("malformed SSZ input must be rejected");
353 assert_invalid_input(&err, "malformed SSZ input");
354 }
355
356 #[test]
368 fn execute_rejects_blob_gas_used_nonzero() {
369 let mut input = l2_valid_input(0, 1_000_000);
370 input.new_payload_request.execution_payload.blob_gas_used = 1;
371 let calldata = calldata_for(&input);
372 let mut gas = 100_000_000u64;
373 let err = run_execute(&MockValidator, &calldata, &mut gas)
374 .expect_err("blob_gas_used != 0 must be rejected");
375 assert_invalid_input(&err, "blob_gas_used != 0");
376 }
377
378 #[test]
380 fn execute_rejects_excess_blob_gas_nonzero() {
381 let mut input = l2_valid_input(0, 1_000_000);
382 input.new_payload_request.execution_payload.excess_blob_gas = 1;
383 let calldata = calldata_for(&input);
384 let mut gas = 100_000_000u64;
385 let err = run_execute(&MockValidator, &calldata, &mut gas)
386 .expect_err("excess_blob_gas != 0 must be rejected");
387 assert_invalid_input(&err, "excess_blob_gas != 0");
388 }
389
390 #[test]
392 fn execute_rejects_nonempty_withdrawals() {
393 let mut input = l2_valid_input(0, 1_000_000);
394 input.new_payload_request.execution_payload.withdrawals = vec![Withdrawal {
395 index: 0,
396 validator_index: 0,
397 address: Bytes20([0u8; 20]),
398 amount: 1,
399 }]
400 .try_into()
401 .expect("withdrawals");
402 let calldata = calldata_for(&input);
403 let mut gas = 100_000_000u64;
404 let err = run_execute(&MockValidator, &calldata, &mut gas)
405 .expect_err("non-empty withdrawals must be rejected");
406 assert_invalid_input(&err, "non-empty withdrawals");
407 }
408
409 #[test]
412 fn execute_rejects_nonempty_execution_requests() {
413 let mut input = l2_valid_input(0, 1_000_000);
414 input.new_payload_request.execution_requests.deposits = vec![DepositRequest {
415 pubkey: [0u8; 48],
416 withdrawal_credentials: [0u8; 32],
417 amount: 1,
418 signature: [0u8; 96],
419 index: 0,
420 }]
421 .try_into()
422 .expect("deposits");
423 let calldata = calldata_for(&input);
424 let mut gas = 100_000_000u64;
425 let err = run_execute(&MockValidator, &calldata, &mut gas)
426 .expect_err("non-empty execution_requests must be rejected");
427 assert_invalid_input(&err, "non-empty execution_requests");
428 }
429
430 #[test]
434 fn execute_rejects_nonempty_builder_requests() {
435 let mut input = l2_valid_input(0, 1_000_000);
436 input
437 .new_payload_request
438 .execution_requests
439 .builder_deposits = vec![BuilderDepositRequest {
440 pubkey: [0u8; 48],
441 withdrawal_credentials: [0u8; 32],
442 amount: 1,
443 signature: [0u8; 96],
444 }]
445 .try_into()
446 .expect("builder_deposits");
447 let mut gas = 100_000_000u64;
448 let err = run_execute(&MockValidator, &calldata_for(&input), &mut gas)
449 .expect_err("non-empty builder_deposits must be rejected");
450 assert_invalid_input(&err, "non-empty builder_deposits");
451
452 let mut input = l2_valid_input(0, 1_000_000);
453 input.new_payload_request.execution_requests.builder_exits = vec![BuilderExitRequest {
454 source_address: Bytes20([0u8; 20]),
455 pubkey: [0u8; 48],
456 }]
457 .try_into()
458 .expect("builder_exits");
459 let mut gas = 100_000_000u64;
460 let err = run_execute(&MockValidator, &calldata_for(&input), &mut gas)
461 .expect_err("non-empty builder_exits must be rejected");
462 assert_invalid_input(&err, "non-empty builder_exits");
463 }
464
465 #[test]
472 fn execute_rejects_unsupported_transaction_types() {
473 for (type_byte, what) in [
474 (0x03u8, "blob-typed transaction"),
475 (0x06u8, "frame transaction"),
476 (0x7eu8, "privileged transaction"),
477 ] {
478 let mut input = l2_valid_input(0, 1_000_000);
479 let tx = vec![type_byte, 0x00, 0x00].try_into().expect("tx bytes");
480 input.new_payload_request.execution_payload.transactions =
481 vec![tx].try_into().expect("transactions");
482 let calldata = calldata_for(&input);
483 let mut gas = 100_000_000u64;
484 let err = run_execute(&MockValidator, &calldata, &mut gas)
485 .expect_err("unsupported transaction type must be rejected");
486 assert_invalid_input(&err, what);
487 }
488
489 let mut input = l2_valid_input(0, 1_000_000);
492 let tx = vec![0x02u8, 0x00, 0x00].try_into().expect("tx bytes");
493 input.new_payload_request.execution_payload.transactions =
494 vec![tx].try_into().expect("transactions");
495 let calldata = calldata_for(&input);
496 let mut gas = 100_000_000u64;
497 run_execute(&MockValidator, &calldata, &mut gas)
498 .expect("an EIP-1559 transaction must be accepted");
499 }
500
501 #[test]
506 fn execute_requires_the_expected_schema_prefix() {
507 let input = l2_valid_input(0, 1_000_000);
508 let mut body = Vec::new();
509 input.ssz_append(&mut body);
510
511 let mut gas = 100_000_000u64;
513 let err = run_execute(&MockValidator, &Bytes::from(body.clone()), &mut gas)
514 .expect_err("unprefixed input must be rejected");
515 assert_invalid_input(&err, "unprefixed input");
516
517 let mut wrong = vec![0x15u8, 0x02];
519 wrong.extend_from_slice(&body);
520 let mut gas = 100_000_000u64;
521 let err = run_execute(&MockValidator, &Bytes::from(wrong), &mut gas)
522 .expect_err("unexpected schema id must be rejected");
523 assert_invalid_input(&err, "unexpected schema id");
524
525 let mut gas = 100_000_000u64;
527 let err = run_execute(&MockValidator, &Bytes::from(vec![0x15u8]), &mut gas)
528 .expect_err("truncated input must be rejected");
529 assert_invalid_input(&err, "input too short for a schema id");
530
531 let mut gas = 100_000_000u64;
533 run_execute(&MockValidator, &calldata_for(&input), &mut gas)
534 .expect("the expected schema prefix must be accepted");
535 }
536}