Skip to main content

dusk_vm/
execute.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7mod config;
8pub mod feature;
9
10pub use config::Config;
11use dusk_core::abi::{ContractError, ContractId, Metadata};
12use dusk_core::stake::STAKE_CONTRACT;
13use dusk_core::transfer::data::{ContractBytecode, gen_contract_id};
14use dusk_core::transfer::withdraw::{
15    Withdraw, WithdrawReceiver, WithdrawReplayToken,
16};
17use dusk_core::transfer::{TRANSFER_CONTRACT, Transaction};
18use piecrust::{CallReceipt, Session};
19use rkyv::Deserialize;
20use wasmparser::*;
21
22use crate::ExecutionError;
23
24const DEPLOY_FEATURE_VALIDATION_ERROR: &str =
25    "failed deployment: bytecode validation rejected";
26const PHOENIX_DISABLED_ERROR: &str = "phoenix is not enabled in the VM";
27const TRANSFER_WITHDRAWAL_FUNCTIONS: &[&str] = &["mint", "withdraw", "convert"];
28
29/// Executes a transaction in the provided session.
30///
31/// This function processes the transaction, invoking smart contracts or
32/// updating state.
33///
34/// During the execution the following steps are performed:
35///
36/// 1. Check if the transaction contains contract deployment data, and if so,
37///    verifies if gas limit is enough for deployment and if the gas price is
38///    sufficient for deployment. If either gas price or gas limit is not
39///    sufficient for deployment, transaction is discarded.
40///
41/// 2. Call the "spend_and_execute" function on the transfer contract with
42///    unlimited gas. If this fails, an error is returned. If an error is
43///    returned the transaction should be considered unspendable/invalid, but no
44///    re-execution of previous transactions is required.
45///
46/// 3. If the transaction contains contract deployment data, additional checks
47///    are performed and if they pass, deployment is executed. The following
48///    checks are performed:
49///    - gas limit should be is smaller than deploy charge plus gas used for
50///      spending funds
51///    - transaction's bytecode's bytes are consistent with bytecode's hash
52///
53///   Deployment execution may fail for deployment-specific reasons, such as:
54///    - contract already deployed
55///    - corrupted bytecode
56///
57///    If deployment execution fails, the entire gas limit is consumed and error
58///    is returned.
59///
60/// 4. Call the "refund" function on the transfer contract with unlimited gas.
61///    The amount charged depends on the gas spent by the transaction, and the
62///    optional contract call in steps 2 or 3. If this fails, a specific error
63///    `FailedRefund` is returned, then the tx should be considered
64///    unspendable/invalid, and the caller SHALL DO a re-execution of previous
65///    transactions.
66///
67/// Note that deployment transaction will never be re-executed for reasons
68/// related to deployment, as it is either discarded or it charges the
69/// full gas limit. It might be re-executed only if some other transaction
70/// failed to fit the block.
71///
72/// # Arguments
73/// * `session` - A mutable reference to the session executing the transaction.
74/// * `tx` - The transaction to execute.
75/// * `config` - The configuration for the execution of the transaction.
76///
77/// # Returns
78/// A result indicating success or failure.
79pub fn execute(
80    session: &mut Session,
81    tx: &Transaction,
82    config: &Config,
83) -> Result<CallReceipt<Result<Vec<u8>, ContractError>>, ExecutionError> {
84    if config.disable_phoenix && matches!(tx, Transaction::Phoenix(_)) {
85        return Err(ExecutionError::precondition(PHOENIX_DISABLED_ERROR));
86    }
87
88    tx.phoenix_fee_check()?;
89
90    if config.phoenix_refund_check {
91        tx.phoenix_refund_check()?;
92    }
93
94    // Transaction will be discarded if it is a deployment transaction
95    // with gas limit smaller than deploy charge.
96    tx.deploy_check(
97        config.gas_per_deploy_byte,
98        config.min_deploy_gas_price,
99        config.min_deploy_points,
100    )?;
101
102    if let Some(contract_deploy) = tx.deploy() {
103        let is_wasm64 = is_wasm64(&contract_deploy.bytecode.bytes);
104        match (config.disable_wasm32, config.disable_wasm64) {
105            (true, true) => Err(ExecutionError::precondition(
106                "contract deployment is not enabled in the VM",
107            )),
108            (true, false) if !is_wasm64 => Err(ExecutionError::precondition(
109                "32-bit wasm is not enabled in the VM",
110            )),
111            (false, true) if is_wasm64 => Err(ExecutionError::precondition(
112                "64-bit wasm is not enabled in the VM",
113            )),
114            _ => Ok(()),
115        }?
116    }
117
118    if config.disable_3rd_party
119        && let Some(call) = tx.call()
120        && call.contract != TRANSFER_CONTRACT
121        && call.contract != STAKE_CONTRACT
122    {
123        return Err(ExecutionError::precondition(
124            "3rd party contracts are not enabled in the VM",
125        ));
126    }
127
128    let blob_min_charge = tx.blob_check(config.gas_per_blob)?;
129
130    if blob_min_charge.is_some() && !config.with_blob {
131        return Err(ExecutionError::precondition(
132            "Blob processing is not enabled in the VM",
133        ));
134    }
135
136    if config.with_public_sender {
137        let _ = session
138            .set_meta(Metadata::PUBLIC_SENDER, tx.moonlight_sender().copied());
139    }
140
141    let stripped_tx = tx.blob_to_memo().or(tx.strip_off_bytecode());
142
143    // Register one combined call hook for VM execution invariants. Piecrust
144    // supports one active hook, so all hook-based checks must be chained here.
145    //
146    // The withdrawal-nullifier check enforces that Phoenix withdrawal replay
147    // tokens carry exactly the same number of nullifiers as the encapsulating
148    // transaction. This is a defense-in-depth measure against audit finding
149    // P1.6-3 (subset-vs-equality in mint_withdrawal).
150    //
151    // Gated behind the Boreas hard fork activation height.
152    if (config.disable_phoenix || config.withdrawal_nullifier_check)
153        && tx.call().is_some()
154    {
155        let disable_phoenix = config.disable_phoenix;
156        let withdrawal_nullifier_check = config.withdrawal_nullifier_check;
157        let tx_nullifier_count = tx.nullifiers().len();
158        session.set_call_hook(Box::new(move |callee, fn_name, fn_args| {
159            if disable_phoenix {
160                check_phoenix_disabled_call(callee, fn_name, fn_args)?;
161            }
162            if withdrawal_nullifier_check {
163                check_withdrawal_nullifiers(
164                    callee,
165                    fn_name,
166                    fn_args,
167                    tx_nullifier_count,
168                )?;
169            }
170            Ok(())
171        }));
172    }
173
174    // Spend the inputs and execute the call. If this errors the transaction is
175    // unspendable.
176    let mut receipt = session
177        .call::<_, Result<Vec<u8>, ContractError>>(
178            TRANSFER_CONTRACT,
179            "spend_and_execute",
180            stripped_tx.as_ref().unwrap_or(tx),
181            tx.gas_limit(),
182        )
183        .inspect_err(|_| {
184            clear_session(session, config);
185        })
186        .map_err(ExecutionError::from_spend_and_execute)?;
187
188    // Deploy if this is a deployment transaction and spend part is successful.
189    contract_deploy(session, tx, config, &mut receipt);
190
191    // If this is a blob transaction, ensure the gas spent is at least the
192    // minimum charge.
193    if let Some(blob_min_charge) = blob_min_charge
194        && receipt.gas_spent < blob_min_charge
195    {
196        receipt.gas_spent = blob_min_charge;
197    }
198
199    // Ensure all gas is consumed if there's an error in the contract call
200    if receipt.data.is_err() {
201        receipt.gas_spent = receipt.gas_limit;
202    }
203
204    // Refund the appropriate amount to the transaction. If this errors, the
205    // transaction must be discarded by the caller who is also responsible to
206    // revert the state applied during the spend_and_execute.
207    let refund_receipt = session
208        .call::<_, ()>(
209            TRANSFER_CONTRACT,
210            "refund",
211            &receipt.gas_spent,
212            u64::MAX,
213        )
214        .inspect_err(|_| {
215            clear_session(session, config);
216        })
217        .map_err(ExecutionError::FailedRefund)?;
218
219    receipt.events.extend(refund_receipt.events);
220
221    clear_session(session, config);
222
223    Ok(receipt)
224}
225
226fn check_phoenix_disabled_call(
227    callee: &ContractId,
228    fn_name: &str,
229    fn_args: &[u8],
230) -> Result<(), String> {
231    if *callee != TRANSFER_CONTRACT
232        || !TRANSFER_WITHDRAWAL_FUNCTIONS.contains(&fn_name)
233    {
234        return Ok(());
235    }
236    if fn_name == "convert" {
237        // `convert` is the transfer contract's Phoenix/Moonlight bridge
238        // entrypoint. Phoenix-paid transactions are discarded before execution,
239        // so any `convert` that reaches this hook is Moonlight-paid and must
240        // still be rejected when Phoenix is disabled.
241        return Err(PHOENIX_DISABLED_ERROR.into());
242    }
243    let withdraw = deserialize_withdraw(fn_args)?;
244    if withdraw_uses_phoenix(&withdraw) {
245        return Err(PHOENIX_DISABLED_ERROR.into());
246    }
247    Ok(())
248}
249
250fn withdraw_uses_phoenix(withdraw: &Withdraw) -> bool {
251    matches!(withdraw.receiver(), WithdrawReceiver::Phoenix(_))
252        || matches!(withdraw.token(), WithdrawReplayToken::Phoenix(_))
253}
254
255fn deserialize_withdraw(fn_args: &[u8]) -> Result<Withdraw, String> {
256    let Ok(root) = rkyv::check_archived_root::<Withdraw>(fn_args) else {
257        return Err("failed to deserialize withdrawal arguments".into());
258    };
259    match root.deserialize(&mut rkyv::Infallible) {
260        Ok(w) => Ok(w),
261        Err(infallible) => match infallible {},
262    }
263}
264
265fn is_wasm64(bytecode: &[u8]) -> bool {
266    for payload in Parser::new(0).parse_all(bytecode).flatten() {
267        if let Payload::MemorySection(section) = payload {
268            return section
269                .into_iter()
270                .any(|memory| memory.is_ok_and(|m| m.memory64));
271        }
272    }
273    false
274}
275
276fn clear_session(session: &mut Session, config: &Config) {
277    if config.with_public_sender {
278        let _ = session.remove_meta(Metadata::PUBLIC_SENDER);
279    }
280    session.clear_call_hook();
281}
282
283/// Checks that a withdrawal's Phoenix replay token nullifier count matches the
284/// encapsulating transaction's nullifier count.
285///
286/// Returns `Err` when the nullifier count mismatches or the arguments
287/// cannot be deserialized (fail-closed). Returns `Ok(())` for non-withdrawal
288/// calls, Moonlight tokens, or matching counts.
289fn check_withdrawal_nullifiers(
290    callee: &ContractId,
291    fn_name: &str,
292    fn_args: &[u8],
293    tx_nullifier_count: usize,
294) -> Result<(), String> {
295    if *callee != TRANSFER_CONTRACT || fn_name != "withdraw" {
296        return Ok(());
297    }
298    let withdraw = deserialize_withdraw(fn_args)?;
299    if let WithdrawReplayToken::Phoenix(nullifiers) = withdraw.token()
300        && nullifiers.len() != tx_nullifier_count
301    {
302        return Err(format!(
303            "nullifier count mismatch: withdrawal has {}, transaction has {}",
304            nullifiers.len(),
305            tx_nullifier_count,
306        ));
307    }
308    Ok(())
309}
310
311// Contract deployment will fail and charge full gas limit in the
312// following cases:
313// 1) Pre-Boreas: transaction gas limit is smaller than deploy charge plus gas
314//    used for spending funds.
315// 2) Boreas+: remaining gas after spending funds is smaller than deploy charge.
316// 3) Transaction's bytecode's bytes are not consistent with bytecode's hash.
317// 4) Deployment fails for deploy-specific reasons like e.g.:
318//      - contract already deployed
319//      - corrupted bytecode
320//      - sufficient gas to spend funds yet insufficient for deployment
321fn contract_deploy(
322    session: &mut Session,
323    tx: &Transaction,
324    config: &Config,
325    receipt: &mut CallReceipt<Result<Vec<u8>, ContractError>>,
326) {
327    if let Some(deploy) = tx.deploy() {
328        let gas_per_deploy_byte = config.gas_per_deploy_byte;
329        let min_deploy_points = config.min_deploy_points;
330
331        if receipt.data.is_ok() {
332            let Ok(deploy_charge) =
333                tx.deploy_charge(gas_per_deploy_byte, min_deploy_points)
334            else {
335                receipt.data =
336                    Err(ContractError::Panic("deploy charge overflow".into()));
337                return;
338            };
339            if !is_deploy_gas_sufficient(
340                tx.gas_limit(),
341                receipt.gas_spent,
342                deploy_charge,
343                config.deploy_remaining_gas_check,
344            ) {
345                receipt.data = Err(ContractError::OutOfGas);
346            } else if !verify_bytecode_hash(&deploy.bytecode) {
347                receipt.data = Err(ContractError::Panic(
348                    "failed bytecode hash check".into(),
349                ))
350            } else if let Err(err) = validate_deploy_bytecode_features(
351                &deploy.bytecode.bytes,
352                config.with_reference_types,
353            ) {
354                receipt.data = Err(ContractError::Panic(err.into()))
355            } else {
356                let gas_left = tx.gas_limit().saturating_sub(receipt.gas_spent);
357                let init_budget = if config.charge_init_gas {
358                    gas_left.saturating_sub(deploy_charge)
359                } else {
360                    gas_left
361                };
362                let result = session.deploy_raw(
363                    Some(gen_contract_id(
364                        &deploy.bytecode.bytes,
365                        deploy.nonce,
366                        &deploy.owner,
367                    )),
368                    deploy.bytecode.bytes.as_slice(),
369                    deploy.init_args.clone(),
370                    deploy.owner.clone(),
371                    init_budget,
372                );
373                match result {
374                    Ok((_, init_receipt)) => {
375                        receipt.gas_spent =
376                            receipt.gas_spent.saturating_add(deploy_charge);
377                        apply_deploy_init_receipt(
378                            receipt,
379                            init_receipt,
380                            config.charge_init_gas,
381                        );
382                    }
383                    Err(err) => {
384                        let msg = format!("failed deployment: {err:?}");
385                        receipt.data = Err(ContractError::Panic(msg))
386                    }
387                }
388            }
389        }
390    }
391}
392
393fn validate_deploy_bytecode_features(
394    bytecode: &[u8],
395    with_reference_types: bool,
396) -> Result<(), &'static str> {
397    if with_reference_types {
398        return Ok(());
399    }
400
401    Validator::new_with_features(pre_reference_types_deploy_features())
402        .validate_all(bytecode)
403        .map(|_| ())
404        .map_err(|_| DEPLOY_FEATURE_VALIDATION_ERROR)
405}
406
407fn pre_reference_types_deploy_features() -> WasmFeatures {
408    // Keep this independent from `WasmFeatures::default()`: that default is
409    // tied to the local wasmparser version and can grow when the dependency is
410    // bumped. This set mirrors dusk-wasmtime defaults before Piecrust enabled
411    // the `gc` feature, plus the explicit `wasm_memory64(true)` setting used by
412    // Piecrust.
413    WasmFeatures::WASM2
414        .difference(WasmFeatures::REFERENCE_TYPES)
415        .union(WasmFeatures::RELAXED_SIMD)
416        .union(WasmFeatures::MULTI_MEMORY)
417        .union(WasmFeatures::MEMORY64)
418}
419
420fn apply_deploy_init_receipt(
421    receipt: &mut CallReceipt<Result<Vec<u8>, ContractError>>,
422    init_receipt: Option<CallReceipt<Vec<u8>>>,
423    charge_init_gas: bool,
424) {
425    if let Some(init_receipt) = init_receipt {
426        if charge_init_gas {
427            receipt.gas_spent =
428                receipt.gas_spent.saturating_add(init_receipt.gas_spent);
429        }
430        receipt.events.extend(init_receipt.events);
431    }
432}
433
434fn is_deploy_gas_sufficient(
435    gas_limit: u64,
436    gas_spent: u64,
437    deploy_charge: u64,
438    deploy_remaining_gas_check: bool,
439) -> bool {
440    let gas_left = gas_limit.saturating_sub(gas_spent);
441
442    if deploy_remaining_gas_check {
443        gas_left >= deploy_charge
444    } else {
445        gas_spent
446            .checked_add(deploy_charge)
447            .is_some_and(|required| gas_left >= required)
448    }
449}
450
451// Verifies that the stored contract bytecode hash is correct.
452fn verify_bytecode_hash(bytecode: &ContractBytecode) -> bool {
453    let computed: [u8; 32] = blake3::hash(bytecode.bytes.as_slice()).into();
454
455    bytecode.hash == computed
456}
457
458#[cfg(test)]
459mod tests {
460    use alloc::vec;
461
462    use dusk_core::BlsScalar;
463    use dusk_core::abi::{ContractId, Event};
464    use rand::rngs::StdRng;
465    use rand::{RngCore, SeedableRng};
466    // Dev-dependencies only used in integration tests trigger the
467    // unused_crate_dependencies lint, so we re-import them here.
468    use {ff as _, hex as _, once_cell as _};
469
470    use super::*;
471    use crate::CallTree;
472
473    #[test]
474    fn check_withdrawal_nullifiers_matching_count_passes() {
475        let rng = &mut StdRng::seed_from_u64(0xbeef);
476
477        let note_sk = dusk_core::signatures::schnorr::SecretKey::random(rng);
478        let note_pk = dusk_core::signatures::schnorr::PublicKey::from(&note_sk);
479        let address =
480            dusk_core::transfer::phoenix::StealthAddress::from_raw_unchecked(
481                *note_pk.as_ref(),
482                note_pk,
483            );
484
485        let nullifiers = vec![BlsScalar::from(1), BlsScalar::from(2)];
486        let withdraw = dusk_core::transfer::withdraw::Withdraw::new(
487            rng,
488            &note_sk,
489            TRANSFER_CONTRACT,
490            100,
491            dusk_core::transfer::withdraw::WithdrawReceiver::Phoenix(address),
492            dusk_core::transfer::withdraw::WithdrawReplayToken::Phoenix(
493                nullifiers.clone(),
494            ),
495        );
496
497        let args =
498            rkyv::to_bytes::<_, 4096>(&withdraw).expect("should serialize");
499
500        assert!(
501            check_withdrawal_nullifiers(
502                &TRANSFER_CONTRACT,
503                "withdraw",
504                &args,
505                nullifiers.len(),
506            )
507            .is_ok()
508        );
509    }
510
511    #[test]
512    fn check_withdrawal_nullifiers_mismatched_count_rejects() {
513        let rng = &mut StdRng::seed_from_u64(0xbeef);
514
515        let note_sk = dusk_core::signatures::schnorr::SecretKey::random(rng);
516        let note_pk = dusk_core::signatures::schnorr::PublicKey::from(&note_sk);
517        let address =
518            dusk_core::transfer::phoenix::StealthAddress::from_raw_unchecked(
519                *note_pk.as_ref(),
520                note_pk,
521            );
522
523        let nullifiers = vec![BlsScalar::from(1), BlsScalar::from(2)];
524        let withdraw = dusk_core::transfer::withdraw::Withdraw::new(
525            rng,
526            &note_sk,
527            TRANSFER_CONTRACT,
528            100,
529            dusk_core::transfer::withdraw::WithdrawReceiver::Phoenix(address),
530            dusk_core::transfer::withdraw::WithdrawReplayToken::Phoenix(
531                nullifiers,
532            ),
533        );
534
535        let args =
536            rkyv::to_bytes::<_, 4096>(&withdraw).expect("should serialize");
537
538        // Mismatched count: 2 nullifiers in token, but tx has 3
539        let err = check_withdrawal_nullifiers(
540            &TRANSFER_CONTRACT,
541            "withdraw",
542            &args,
543            3,
544        )
545        .unwrap_err();
546        assert!(
547            err.contains("nullifier count mismatch"),
548            "expected mismatch message, got: {err}"
549        );
550        assert!(err.contains("2") && err.contains("3"));
551    }
552
553    #[test]
554    fn check_withdrawal_nullifiers_ignores_non_withdraw_calls() {
555        assert!(
556            check_withdrawal_nullifiers(&TRANSFER_CONTRACT, "refund", &[], 5,)
557                .is_ok()
558        );
559
560        assert!(
561            check_withdrawal_nullifiers(
562                &ContractId::from_bytes([0xAA; 32]),
563                "withdraw",
564                &[],
565                5,
566            )
567            .is_ok()
568        );
569    }
570
571    #[test]
572    fn check_withdrawal_nullifiers_rejects_garbage_args() {
573        // Garbage bytes that cannot be deserialized as a Withdraw must
574        // be rejected (fail-closed).
575        let err = check_withdrawal_nullifiers(
576            &TRANSFER_CONTRACT,
577            "withdraw",
578            &[0xDE, 0xAD, 0xBE, 0xEF],
579            2,
580        )
581        .unwrap_err();
582        assert!(err.contains("deserialize"));
583
584        // Empty args must also be rejected.
585        check_withdrawal_nullifiers(&TRANSFER_CONTRACT, "withdraw", &[], 1)
586            .unwrap_err();
587    }
588
589    #[test]
590    fn check_withdrawal_nullifiers_ignores_moonlight_token() {
591        let rng = &mut StdRng::seed_from_u64(0xdead);
592
593        let moonlight_sk = dusk_core::signatures::bls::SecretKey::random(rng);
594        let moonlight_pk =
595            dusk_core::signatures::bls::PublicKey::from(&moonlight_sk);
596
597        let withdraw = dusk_core::transfer::withdraw::Withdraw::new(
598            rng,
599            &moonlight_sk,
600            TRANSFER_CONTRACT,
601            100,
602            dusk_core::transfer::withdraw::WithdrawReceiver::Moonlight(
603                moonlight_pk,
604            ),
605            dusk_core::transfer::withdraw::WithdrawReplayToken::Moonlight(42),
606        );
607
608        let args =
609            rkyv::to_bytes::<_, 4096>(&withdraw).expect("should serialize");
610
611        // Moonlight token — should return Ok even with mismatched count
612        assert!(
613            check_withdrawal_nullifiers(
614                &TRANSFER_CONTRACT,
615                "withdraw",
616                &args,
617                999,
618            )
619            .is_ok()
620        );
621    }
622
623    #[test]
624    fn test_gen_contract_id() {
625        let mut rng = StdRng::seed_from_u64(42);
626
627        let mut bytes = vec![0; 1000];
628        rng.fill_bytes(&mut bytes);
629
630        let nonce = rng.next_u64();
631
632        let mut owner = vec![0, 100];
633        rng.fill_bytes(&mut owner);
634
635        let contract_id =
636            gen_contract_id(bytes.as_slice(), nonce, owner.as_slice());
637
638        assert_eq!(
639            contract_id.as_bytes(),
640            [
641                45, 168, 182, 39, 119, 137, 168, 140, 114, 21, 120, 158, 34,
642                126, 244, 221, 151, 72, 109, 178, 82, 229, 84, 128, 92, 123,
643                135, 74, 23, 224, 119, 133
644            ]
645        );
646    }
647
648    #[test]
649    fn deploy_gas_check_matches_prefork_and_boreas_rules() {
650        for (gas_limit, gas_spent, deploy_charge, boreas, expected) in [
651            (10_000_000, 3_000_000, 5_000_000, false, false),
652            (10_000_000, 3_000_000, 5_000_000, true, true),
653            (7_000_000, 3_000_000, 5_000_000, false, false),
654            (7_000_000, 3_000_000, 5_000_000, true, false),
655            (u64::MAX, u64::MAX, 1, false, false),
656        ] {
657            assert_eq!(
658                is_deploy_gas_sufficient(
659                    gas_limit,
660                    gas_spent,
661                    deploy_charge,
662                    boreas,
663                ),
664                expected,
665            );
666        }
667    }
668
669    #[test]
670    fn deploy_bytecode_reference_types_are_height_gated() {
671        const EMPTY_MODULE: &[u8] = b"\0asm\x01\0\0\0";
672        const FUNC_WITH_EXTERNREF_MODULE: &[u8] = &[
673            0x00, 0x61, 0x73, 0x6d, // magic
674            0x01, 0x00, 0x00, 0x00, // version
675            0x01, // type section
676            0x05, // section length
677            0x01, // type count
678            0x60, // function type
679            0x01, // one parameter
680            0x6f, // externref
681            0x00, // no results
682        ];
683        const TABLE_WITH_EXTERNREF_MODULE: &[u8] = &[
684            0x00, 0x61, 0x73, 0x6d, // magic
685            0x01, 0x00, 0x00, 0x00, // version
686            0x04, // table section
687            0x04, // section length
688            0x01, // table count
689            0x6f, // externref
690            0x00, // min-only limits
691            0x01, // minimum table size
692        ];
693
694        validate_deploy_bytecode_features(EMPTY_MODULE, false)
695            .expect("MVP bytecode should validate without reference-types");
696
697        let err = validate_deploy_bytecode_features(
698            FUNC_WITH_EXTERNREF_MODULE,
699            false,
700        )
701        .expect_err("reference-types bytecode should fail before activation");
702        assert_eq!(err, DEPLOY_FEATURE_VALIDATION_ERROR);
703
704        let err = validate_deploy_bytecode_features(
705            TABLE_WITH_EXTERNREF_MODULE,
706            false,
707        )
708        .expect_err("reference-types table should fail before activation");
709        assert_eq!(err, DEPLOY_FEATURE_VALIDATION_ERROR);
710
711        Validator::new_with_features(
712            pre_reference_types_deploy_features()
713                .union(WasmFeatures::REFERENCE_TYPES),
714        )
715        .validate_all(FUNC_WITH_EXTERNREF_MODULE)
716        .expect("reference-types bytecode should validate when enabled");
717    }
718
719    #[test]
720    fn pre_reference_types_deploy_features_are_pinned() {
721        let features = pre_reference_types_deploy_features();
722
723        assert!(!features.contains(WasmFeatures::REFERENCE_TYPES));
724        assert!(!features.contains(WasmFeatures::FUNCTION_REFERENCES));
725        assert!(!features.contains(WasmFeatures::GC));
726        assert!(!features.contains(WasmFeatures::THREADS));
727        assert!(!features.contains(WasmFeatures::TAIL_CALL));
728
729        assert!(features.contains(WasmFeatures::BULK_MEMORY));
730        assert!(features.contains(WasmFeatures::MULTI_VALUE));
731        assert!(features.contains(WasmFeatures::SIMD));
732        assert!(features.contains(WasmFeatures::RELAXED_SIMD));
733        assert!(features.contains(WasmFeatures::MULTI_MEMORY));
734        assert!(features.contains(WasmFeatures::MEMORY64));
735        assert!(!features.contains(WasmFeatures::EXCEPTIONS));
736        assert!(!features.contains(WasmFeatures::EXTENDED_CONST));
737    }
738
739    #[test]
740    fn deploy_init_events_are_preserved_before_and_after_boreas() {
741        let init_event = Event {
742            source: ContractId::from_bytes([7; 32]),
743            topic: "runtime_update".into(),
744            data: vec![1, 2, 3, 4],
745            reverted: false,
746        };
747        let build_init_receipt = || CallReceipt {
748            gas_spent: 123,
749            gas_limit: 999,
750            events: vec![init_event.clone()],
751            call_tree: CallTree::default(),
752            data: Vec::new(),
753        };
754
755        let mut prefork_receipt = CallReceipt {
756            gas_spent: 10,
757            gas_limit: 1000,
758            events: vec![],
759            call_tree: CallTree::default(),
760            data: Ok(Vec::new()),
761        };
762        apply_deploy_init_receipt(
763            &mut prefork_receipt,
764            Some(build_init_receipt()),
765            false,
766        );
767        assert_eq!(prefork_receipt.gas_spent, 10);
768        assert_eq!(prefork_receipt.events, vec![init_event.clone()]);
769
770        let mut boreas_receipt = CallReceipt {
771            gas_spent: 10,
772            gas_limit: 1000,
773            events: vec![],
774            call_tree: CallTree::default(),
775            data: Ok(Vec::new()),
776        };
777        apply_deploy_init_receipt(
778            &mut boreas_receipt,
779            Some(build_init_receipt()),
780            true,
781        );
782        assert_eq!(boreas_receipt.gas_spent, 133);
783        assert_eq!(boreas_receipt.events, vec![init_event]);
784    }
785}