Skip to main content

ethrex_levm/
execute_precompile.rs

1//! EXECUTE precompile for Native Rollups — aligned with the l2beat spec.
2//!
3//! This is a thin wrapper around `verify_stateless_new_payload` (accessed via
4//! the `StatelessValidator` trait). The precompile:
5//! 1. Deserializes SSZ `StatelessInput` once (to read gas_limit and check L2 constraints)
6//! 2. Validates L2-specific constraints (no blobs, no withdrawals, etc.)
7//! 3. Charges gas equal to `gas_limit + calldata.len() * EXECUTE_GAS_PER_WITNESS_BYTE`
8//! 4. Delegates to the `StatelessValidator` trait, passing the already-decoded input
9//! 5. On success returns the SSZ-encoded `StatelessValidationResult`.
10//!
11//! Fail-closed semantics: both invalid input and a failed validation return an
12//! `ExceptionalHalt` (CALL-level `success=false`), so a `require(success)` caller
13//! is safe. Invalid input (undecodable SSZ or an L2-constraint violation) is
14//! rejected in steps 1-2, *before* the step-3 gas charge — those checks are cheap
15//! and already priced by the L1 calldata gas the attacker paid to submit the blob.
16//! A *failed validation* (step 4) is rejected *after* the step-3 charge, so an
17//! attacker who forces full re-execution still pays for it: that is the DoS bound.
18
19use 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
27/// WIP DA/decode cost — the SSZ witness decode + trie rebuild scales with input size;
28/// 16 mirrors calldata non-zero byte cost. Tune when EIP-8079 pricing is finalized.
29pub const EXECUTE_GAS_PER_WITNESS_BYTE: u64 = 16;
30
31/// EXECUTE precompile entrypoint.
32///
33/// Input: SSZ-encoded `StatelessInput`.
34/// Output: SSZ-encoded `StatelessValidationResult`.
35///
36/// This wrapper exists to mimic the standard precompile signature
37/// (`fn(&[u8], &mut u64, Fork, &dyn Crypto) -> Result<Bytes, VMError>`) plus
38/// the extra `stateless_validator` slot threaded through the dispatcher. It
39/// also converts the `Option` into an error, so `run_execute` can work with a
40/// guaranteed-`Some` validator.
41pub 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
56/// Validate L2 constraints, charge gas, delegate.
57fn 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    // Schema-prefixed `statelessInputBytes`: a 2-byte big-endian schema id, then
68    // the SSZ body. Since execution-specs #3278 the prefix is the only thing
69    // identifying the fork, so an unexpected id must be rejected. All of it is
70    // attacker-controlled, hence CALL-level failures rather than invariants.
71    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    // `gas_used` is attacker-controlled and does NOT bound verify()'s work:
85    // a malicious sequencer can set gas_used=0 while verify() still does full
86    // native re-execution (bounded by gas_limit), witness decode, and trie rebuild.
87    // `gas_limit` IS the true upper bound on EVM re-execution (execute_block
88    // enforces the block gas limit). The per-byte term bounds witness decode and
89    // trie rebuild costs that scale with input size.
90    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    // Hand the already-decoded `input` to the validator so it doesn't re-parse
100    // the (potentially large) witness a second time.
101    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        // Attacker-controlled: validation failure must be a CALL-level failure so the tx is
111        // includable and the attacker pays Task 1's gas charge. `PrecompileError` → `ExceptionalHalt`
112        // → should_propagate()==false. Using `Internal` here would abort the tx, making it
113        // non-includable and defeating the DoS bound.
114        return Err(PrecompileError::ExecuteValidationFailed.into());
115    }
116
117    Ok(Bytes::from(result))
118}
119
120/// Validate L2-specific constraints on the ExecutionPayload.
121fn validate_l2_constraints(
122    input: &ethrex_common::types::stateless_ssz::SszStatelessInput,
123) -> Result<(), VMError> {
124    let payload = &input.new_payload_request.execution_payload;
125
126    // All checks below are attacker-controllable input violations — CALL-level failures.
127    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        // Rejected by leading EIP-2718 envelope byte: `0x03` (EIP-4844) because
147        // L2 blocks carry no blobs, and `0x06` (frame) / `0x7e` (privileged)
148        // because they carry an explicit sender and no signature. Native rollups
149        // relay L1→L2 messages as signed EIP-1559 transactions, so this costs no
150        // function.
151        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    /// Mock `StatelessValidator` that returns a successful SSZ-encoded
177    /// `SszStatelessValidationResult` without performing any real execution.
178    struct MockValidator;
179
180    /// Mock `StatelessValidator` that returns `successful_validation: false`.
181    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    /// Encode an input as `statelessInputBytes`: the 2-byte big-endian schema id
198    /// followed by the SSZ body. Every test must go through this — a bare body is
199    /// rejected at the prefix check, which would silently turn each constraint
200    /// test below into a test of the prefix check.
201    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    /// Assert an error is the constraint rejection `ExecuteInvalidInput`, and is a
210    /// CALL-level failure (includable, attacker pays) rather than a tx-abort.
211    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    /// Build a minimal L2-valid `SszStatelessInput` with the given gas fields.
224    /// No blobs, no withdrawals, no execution requests — satisfies all
225    /// `validate_l2_constraints` checks.
226    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    /// Fail-closed: `run_execute` must return `Err` when `successful_validation` is `false`,
284    /// and `Ok` when it is `true`.
285    #[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        // Invalid result → must Err (fail-closed) with a NON-propagating error so the tx is
291        // INCLUDABLE and the attacker pays Task 1's gas charge (I1×I13 regression guard).
292        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        // Valid result → must Ok.
302        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    /// `gas_used=0, gas_limit=1_000_000`: the charge must be `gas_limit +
309    /// calldata.len() * EXECUTE_GAS_PER_WITNESS_BYTE`, NOT 0 (the old
310    /// `gas_used`-based charge). Proves the fix bounds verify()'s work.
311    #[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        // Must charge at least gas_limit, NOT 0 (old gas_used-based charge).
329        assert!(
330            charged >= 1_000_000,
331            "must charge at least gas_limit, got {charged}"
332        );
333        // Must equal gas_limit + per-byte witness charge exactly.
334        assert_eq!(
335            charged, expected,
336            "charge must be gas_limit + calldata.len() * EXECUTE_GAS_PER_WITNESS_BYTE"
337        );
338    }
339
340    /// I1 regression (moved from `stateless.rs`) + ElFantasma malformed-SSZ ask:
341    /// `run_execute` must fail closed at the CALL level (non-propagating) when
342    /// the calldata is not a decodable SSZ `StatelessInput`, rejecting it at the
343    /// up-front decode *before* the validator runs — so a garbage or
344    /// bad-offset blob can never be turned into wrong proven values, and the
345    /// error is includable rather than a tx-abort.
346    #[test]
347    fn execute_rejects_malformed_ssz_input() {
348        // 4 bytes of 0xff: an out-of-range SSZ offset, undecodable as StatelessInput.
349        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    // ── Negative constraint tests (I11) ──────────────────────────────────────
357    // Each test builds a fully valid `SszStatelessInput`, violates exactly ONE
358    // `validate_l2_constraints` check, and asserts:
359    //   (a) `run_execute` returns `Err`, and
360    //   (b) the error is non-propagating (CALL-level failure, so the tx is
361    //       includable rather than aborted). Note: a constraint violation is
362    //       rejected *before* the precompile gas charge (cheap reject, already
363    //       priced by L1 calldata gas), unlike a failed *validation*, which is
364    //       charged first — see the module-level fail-closed note.
365
366    /// Constraint 1: `blob_gas_used` must be zero.
367    #[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    /// Constraint 2: `excess_blob_gas` must be zero.
379    #[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    /// Constraint 3: `withdrawals` list must be empty.
391    #[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    /// Constraint 4: `execution_requests` (deposits/withdrawals/consolidations) must all be empty.
410    /// Here we add one deposit — the same check fires for any non-empty field.
411    #[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    /// Constraint 4b: the EIP-8282 builder request lists must be empty too. They
431    /// were added to `ExecutionRequests` after the original three, so this pins
432    /// them against being reached by the constraint check only by accident.
433    #[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    /// Constraint 5: no transaction may carry a rejected EIP-2718 type byte.
466    ///
467    /// `0x03` is a blob transaction. `0x06` (frame) and `0x7e` (privileged) carry
468    /// an explicit sender and no signature. Rejecting them here makes "every
469    /// transaction in the payload is signature-bearing" an enforced invariant
470    /// rather than a property of the current producer.
471    #[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        // A type byte just outside the rejected set must still be accepted, so the
490        // test cannot pass by rejecting everything.
491        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    /// The schema prefix is mandatory, and only `STATELESS_INPUT_SCHEMA_ID` is
502    /// accepted. Since #3278 removed chain configuration from the wire, the prefix
503    /// is the sole carrier of the fork, so an unexpected id must reject rather than
504    /// be validated under Amsterdam rules by default.
505    #[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        // No prefix at all: the body's first bytes are an SSZ offset, not a schema id.
512        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        // A well-formed but different schema id.
518        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        // Too short to hold an id at all.
526        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        // And the expected prefix is accepted, so the test is not vacuous.
532        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}