cosmwasm_vm/
compatibility.rs

1use std::collections::BTreeSet;
2use std::collections::HashSet;
3
4use wasmer::wasmparser::Import;
5use wasmer::wasmparser::TypeRef;
6
7use crate::capabilities::required_capabilities_from_module;
8use crate::config::WasmLimits;
9use crate::errors::{VmError, VmResult};
10use crate::limited::LimitedDisplay;
11use crate::parsed_wasm::ParsedWasm;
12use crate::static_analysis::ExportInfo;
13
14/// Lists all imports we provide upon instantiating the instance in Instance::from_module()
15/// This should be updated when new imports are added
16const SUPPORTED_IMPORTS: &[&str] = &[
17    "env.abort",
18    "env.db_read",
19    "env.db_write",
20    "env.db_remove",
21    "env.addr_validate",
22    "env.addr_canonicalize",
23    "env.addr_humanize",
24    "env.bls12_381_aggregate_g1",
25    "env.bls12_381_aggregate_g2",
26    "env.bls12_381_pairing_equality",
27    "env.bls12_381_hash_to_g1",
28    "env.bls12_381_hash_to_g2",
29    "env.secp256k1_verify",
30    "env.secp256k1_recover_pubkey",
31    "env.secp256r1_verify",
32    "env.secp256r1_recover_pubkey",
33    "env.ed25519_verify",
34    "env.ed25519_batch_verify",
35    "env.debug",
36    "env.query_chain",
37    #[cfg(feature = "iterator")]
38    "env.db_scan",
39    #[cfg(feature = "iterator")]
40    "env.db_next",
41    #[cfg(feature = "iterator")]
42    "env.db_next_key",
43    #[cfg(feature = "iterator")]
44    "env.db_next_value",
45];
46
47/// Lists all entry points we expect to be present when calling a contract.
48/// Other optional exports exist, e.g. "execute", "migrate" and "query".
49/// The marker export interface_version_* is checked separately.
50/// This is unlikely to change much, must be frozen at 1.0 to avoid breaking existing contracts
51const REQUIRED_EXPORTS: &[&str] = &[
52    // IO
53    "allocate",
54    "deallocate",
55];
56
57const INTERFACE_VERSION_PREFIX: &str = "interface_version_";
58const SUPPORTED_INTERFACE_VERSIONS: &[&str] = &["interface_version_8"];
59
60#[derive(Clone, Copy)]
61pub enum LogOutput {
62    StdOut,
63    StdErr,
64}
65#[derive(Clone, Copy, Default)]
66pub enum Logger<'a> {
67    On {
68        prefix: &'a str,
69        output: LogOutput,
70    },
71    #[default]
72    Off,
73}
74
75impl<'a> Logger<'a> {
76    pub fn with_config(output: LogOutput, prefix: &'a str) -> Self {
77        On { output, prefix }
78    }
79
80    /// Adds a message to the logs, if they are enabled.
81    /// This is a convenience method for adding a single message.
82    ///
83    /// Takes a closure that returns the message to add to avoid unnecessary allocations.
84    pub fn add(&self, msg_fn: impl FnOnce() -> String) {
85        if let On { prefix, output } = &self {
86            let msg = msg_fn();
87            match output {
88                LogOutput::StdOut => println!("{prefix}{msg}"),
89                LogOutput::StdErr => eprintln!("{prefix}{msg}"),
90            }
91        }
92    }
93}
94
95use Logger::*;
96
97/// Checks if the data is valid wasm and compatibility with the CosmWasm API (imports and exports)
98pub fn check_wasm(
99    wasm_code: &[u8],
100    available_capabilities: &HashSet<String>,
101    limits: &WasmLimits,
102    logs: Logger<'_>,
103) -> VmResult<()> {
104    logs.add(|| format!("Size of Wasm blob: {}", wasm_code.len()));
105
106    let mut module = ParsedWasm::parse(wasm_code)?;
107
108    check_wasm_tables(&module, limits)?;
109    check_wasm_memories(&module, limits)?;
110    check_interface_version(&module)?;
111    check_wasm_exports(&module, logs)?;
112    check_wasm_imports(&module, SUPPORTED_IMPORTS, limits, logs)?;
113    check_wasm_capabilities(&module, available_capabilities, logs)?;
114    check_wasm_functions(&module, limits, logs)?;
115
116    module.validate_funcs()
117}
118
119fn check_wasm_tables(module: &ParsedWasm, wasm_limits: &WasmLimits) -> VmResult<()> {
120    match module.tables.len() {
121        0 => Ok(()),
122        1 => {
123            let limits = &module.tables[0];
124            if let Some(maximum) = limits.maximum {
125                if maximum > wasm_limits.table_size_limit_elements() as u64 {
126                    return Err(VmError::static_validation_err(
127                        "Wasm contract's first table section has a too large max limit",
128                    ));
129                }
130                Ok(())
131            } else {
132                Err(VmError::static_validation_err(
133                    "Wasm contract must not have unbound table section",
134                ))
135            }
136        }
137        _ => Err(VmError::static_validation_err(
138            "Wasm contract must not have more than 1 table section",
139        )),
140    }
141}
142
143fn check_wasm_memories(module: &ParsedWasm, limits: &WasmLimits) -> VmResult<()> {
144    if module.memories.len() != 1 {
145        return Err(VmError::static_validation_err(
146            "Wasm contract must contain exactly one memory",
147        ));
148    }
149    let memory = &module.memories[0];
150
151    if memory.initial > limits.initial_memory_limit_pages() as u64 {
152        return Err(VmError::static_validation_err(format!(
153            "Wasm contract memory's minimum must not exceed {} pages.",
154            limits.initial_memory_limit_pages()
155        )));
156    }
157
158    if memory.maximum.is_some() {
159        return Err(VmError::static_validation_err(
160            "Wasm contract memory's maximum must be unset. The host will set it for you.",
161        ));
162    }
163    Ok(())
164}
165
166fn check_interface_version(module: &ParsedWasm) -> VmResult<()> {
167    let mut interface_version_exports = module
168        .exported_function_names(Some(INTERFACE_VERSION_PREFIX))
169        .into_iter();
170    if let Some(first_interface_version_export) = interface_version_exports.next() {
171        if interface_version_exports.next().is_some() {
172            Err(VmError::static_validation_err(
173                "Wasm contract contains more than one marker export: interface_version_*",
174            ))
175        } else {
176            // Exactly one interface version found
177            let version_str = first_interface_version_export.as_str();
178            if SUPPORTED_INTERFACE_VERSIONS.contains(&version_str) {
179                Ok(())
180            } else {
181                Err(VmError::static_validation_err(
182                        "Wasm contract has unknown interface_version_* marker export (see https://github.com/CosmWasm/cosmwasm/blob/main/packages/vm/README.md)",
183                ))
184            }
185        }
186    } else {
187        Err(VmError::static_validation_err(
188            "Wasm contract missing a required marker export: interface_version_*",
189        ))
190    }
191}
192
193fn check_wasm_exports(module: &ParsedWasm, logs: Logger) -> VmResult<()> {
194    let available_exports: HashSet<String> = module.exported_function_names(None);
195
196    logs.add(|| format!("Exports: {}", available_exports.to_string_limited(20_000)));
197
198    for required_export in REQUIRED_EXPORTS {
199        if !available_exports.contains(*required_export) {
200            return Err(VmError::static_validation_err(format!(
201                "Wasm contract doesn't have required export: \"{required_export}\". Exports required by VM: {REQUIRED_EXPORTS:?}."
202            )));
203        }
204    }
205    Ok(())
206}
207
208/// Checks if the import requirements of the contract are satisfied.
209/// When this is not the case, we either have an incompatibility between contract and VM
210/// or a error in the contract.
211fn check_wasm_imports(
212    module: &ParsedWasm,
213    supported_imports: &[&str],
214    limits: &WasmLimits,
215    logs: Logger,
216) -> VmResult<()> {
217    logs.add(|| {
218        format!(
219            "Imports ({}): {}",
220            module.imports.len(),
221            module
222                .imports
223                .iter()
224                .map(|import| full_import_name(import))
225                .collect::<Vec<_>>()
226                .join(", ")
227        )
228    });
229
230    if module.imports.len() > limits.max_imports() {
231        return Err(VmError::static_validation_err(format!(
232            "Import count exceeds limit. Imports: {}. Limit: {}.",
233            module.imports.len(),
234            limits.max_imports()
235        )));
236    }
237
238    for required_import in &module.imports {
239        let full_name = full_import_name(required_import);
240        if !supported_imports.contains(&full_name.as_str()) {
241            let required_import_names: BTreeSet<_> =
242                module.imports.iter().map(full_import_name).collect();
243            return Err(VmError::static_validation_err(format!(
244                "Wasm contract requires unsupported import: \"{}\". Required imports: {}. Available imports: {:?}.",
245                full_name, required_import_names.to_string_limited(200), supported_imports
246            )));
247        }
248
249        match required_import.ty {
250            TypeRef::Func(_) => {} // ok
251            _ => return Err(VmError::static_validation_err(format!(
252                "Wasm contract requires non-function import: \"{full_name}\". Right now, all supported imports are functions."
253            )))
254        }
255    }
256    Ok(())
257}
258
259fn full_import_name(ie: &Import) -> String {
260    format!("{}.{}", ie.module, ie.name)
261}
262
263fn check_wasm_capabilities(
264    module: &ParsedWasm,
265    available_capabilities: &HashSet<String>,
266    logs: Logger,
267) -> VmResult<()> {
268    let required_capabilities = required_capabilities_from_module(module);
269    logs.add(|| {
270        format!(
271            "Required capabilities: {}",
272            required_capabilities.to_string_limited(20_000)
273        )
274    });
275    if !required_capabilities.is_subset(available_capabilities) {
276        // We switch to BTreeSet to get a sorted error message
277        let unavailable: BTreeSet<_> = required_capabilities
278            .difference(available_capabilities)
279            .collect();
280        return Err(VmError::static_validation_err(format!(
281            "Wasm contract requires unavailable capabilities: {}",
282            unavailable.to_string_limited(200)
283        )));
284    }
285    Ok(())
286}
287
288fn check_wasm_functions(module: &ParsedWasm, limits: &WasmLimits, logs: Logger) -> VmResult<()> {
289    logs.add(|| format!("Function count: {}", module.function_count));
290    logs.add(|| format!("Max function parameters: {}", module.max_func_params));
291    logs.add(|| format!("Max function results: {}", module.max_func_results));
292    logs.add(|| {
293        format!(
294            "Total function parameter count: {}",
295            module.total_func_params
296        )
297    });
298
299    if module.function_count > limits.max_functions() {
300        return Err(VmError::static_validation_err(format!(
301            "Wasm contract contains more than {} functions",
302            limits.max_functions()
303        )));
304    }
305    if module.max_func_params > limits.max_function_params() {
306        return Err(VmError::static_validation_err(format!(
307            "Wasm contract contains function with more than {} parameters",
308            limits.max_function_params()
309        )));
310    }
311    if module.max_func_results > limits.max_function_results() {
312        return Err(VmError::static_validation_err(format!(
313            "Wasm contract contains function with more than {} results",
314            limits.max_function_results()
315        )));
316    }
317
318    if module.total_func_params > limits.max_total_function_params() {
319        return Err(VmError::static_validation_err(format!(
320            "Wasm contract contains more than {} function parameters in total",
321            limits.max_total_function_params()
322        )));
323    }
324
325    Ok(())
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    static CONTRACT_0_7: &[u8] = include_bytes!("../testdata/hackatom_0.7.wasm");
333    static CONTRACT_0_12: &[u8] = include_bytes!("../testdata/hackatom_0.12.wasm");
334    static CONTRACT_0_14: &[u8] = include_bytes!("../testdata/hackatom_0.14.wasm");
335    static CONTRACT_0_15: &[u8] = include_bytes!("../testdata/hackatom_0.15.wasm");
336    static HACKATOM: &[u8] = include_bytes!("../testdata/hackatom.wasm");
337    static CYBERPUNK: &[u8] = include_bytes!("../testdata/cyberpunk.wasm");
338    static CYBERPUNK_RUST_170: &[u8] = include_bytes!("../testdata/cyberpunk_rust170.wasm");
339
340    fn default_capabilities() -> HashSet<String> {
341        HashSet::from([
342            "cosmwasm_1_1".to_string(),
343            "cosmwasm_1_2".to_string(),
344            "cosmwasm_1_3".to_string(),
345            "cosmwasm_1_4".to_string(),
346            "cosmwasm_1_4".to_string(),
347            "cosmwasm_2_0".to_string(),
348            "cosmwasm_2_1".to_string(),
349            "cosmwasm_2_2".to_string(),
350            "iterator".to_string(),
351            "staking".to_string(),
352            "stargate".to_string(),
353        ])
354    }
355
356    #[test]
357    fn check_wasm_passes_for_latest_contract() {
358        // this is our reference check, must pass
359        check_wasm(
360            HACKATOM,
361            &default_capabilities(),
362            &WasmLimits::default(),
363            Off,
364        )
365        .unwrap();
366        check_wasm(
367            CYBERPUNK,
368            &default_capabilities(),
369            &WasmLimits::default(),
370            Off,
371        )
372        .unwrap();
373    }
374
375    #[test]
376    fn check_wasm_allows_sign_ext() {
377        // See https://github.com/CosmWasm/cosmwasm/issues/1727
378        check_wasm(
379            CYBERPUNK_RUST_170,
380            &default_capabilities(),
381            &WasmLimits::default(),
382            Off,
383        )
384        .unwrap();
385    }
386
387    #[test]
388    fn check_wasm_old_contract() {
389        match check_wasm(CONTRACT_0_15, &default_capabilities(),&WasmLimits::default(),
390        Off) {
391            Err(VmError::StaticValidationErr { msg, .. }) => assert_eq!(
392                msg,
393                "Wasm contract has unknown interface_version_* marker export (see https://github.com/CosmWasm/cosmwasm/blob/main/packages/vm/README.md)"
394            ),
395            Err(e) => panic!("Unexpected error {e:?}"),
396            Ok(_) => panic!("This must not succeed"),
397        };
398
399        match check_wasm(CONTRACT_0_14, &default_capabilities(),&WasmLimits::default(),
400        Off,) {
401            Err(VmError::StaticValidationErr { msg, .. }) => assert_eq!(
402                msg,
403                "Wasm contract has unknown interface_version_* marker export (see https://github.com/CosmWasm/cosmwasm/blob/main/packages/vm/README.md)"
404            ),
405            Err(e) => panic!("Unexpected error {e:?}"),
406            Ok(_) => panic!("This must not succeed"),
407        };
408
409        match check_wasm(
410            CONTRACT_0_12,
411            &default_capabilities(),
412            &WasmLimits::default(),
413            Off,
414        ) {
415            Err(VmError::StaticValidationErr { msg, .. }) => {
416                assert!(msg.contains(
417                    "Wasm contract missing a required marker export: interface_version_*"
418                ))
419            }
420            Err(e) => panic!("Unexpected error {e:?}"),
421            Ok(_) => panic!("This must not succeed"),
422        };
423
424        match check_wasm(
425            CONTRACT_0_7,
426            &default_capabilities(),
427            &WasmLimits::default(),
428            Off,
429        ) {
430            Err(VmError::StaticValidationErr { msg, .. }) => {
431                assert!(msg.contains(
432                    "Wasm contract missing a required marker export: interface_version_*"
433                ))
434            }
435            Err(e) => panic!("Unexpected error {e:?}"),
436            Ok(_) => panic!("This must not succeed"),
437        };
438    }
439
440    #[test]
441    fn check_wasm_tables_works() {
442        let limits = WasmLimits::default();
443        // No tables is fine
444        let wasm = wat::parse_str("(module)").unwrap();
445        assert!(ParsedWasm::parse(&wasm).unwrap().tables.is_empty());
446
447        // One table (bound)
448        let wasm = wat::parse_str("(module (table $name 123 123 funcref))").unwrap();
449        check_wasm_tables(&ParsedWasm::parse(&wasm).unwrap(), &limits).unwrap();
450
451        // One table (bound, initial > max)
452        let wasm = wat::parse_str("(module (table $name 124 123 funcref))").unwrap();
453        // this should be caught by the validator
454        let err = &ParsedWasm::parse(&wasm).unwrap_err();
455        assert!(err
456            .to_string()
457            .contains("size minimum must not be greater than maximum"));
458
459        // One table (bound, max too large)
460        let wasm = wat::parse_str("(module (table $name 100 9999 funcref))").unwrap();
461        let err = check_wasm_tables(&ParsedWasm::parse(&wasm).unwrap(), &limits).unwrap_err();
462        assert!(err
463            .to_string()
464            .contains("Wasm contract's first table section has a too large max limit"));
465
466        // One table (unbound)
467        let wasm = wat::parse_str("(module (table $name 100 funcref))").unwrap();
468        let err = check_wasm_tables(&ParsedWasm::parse(&wasm).unwrap(), &limits).unwrap_err();
469        assert!(err
470            .to_string()
471            .contains("Wasm contract must not have unbound table section"));
472    }
473
474    #[test]
475    fn check_wasm_memories_ok() {
476        let wasm = wat::parse_str("(module (memory 1))").unwrap();
477        check_wasm_memories(&ParsedWasm::parse(&wasm).unwrap(), &WasmLimits::default()).unwrap()
478    }
479
480    #[test]
481    fn check_wasm_memories_no_memory() {
482        let limits = WasmLimits::default();
483        let wasm = wat::parse_str("(module)").unwrap();
484        match check_wasm_memories(&ParsedWasm::parse(&wasm).unwrap(), &limits) {
485            Err(VmError::StaticValidationErr { msg, .. }) => {
486                assert!(msg.starts_with("Wasm contract must contain exactly one memory"));
487            }
488            Err(e) => panic!("Unexpected error {e:?}"),
489            Ok(_) => panic!("Didn't reject wasm with invalid api"),
490        }
491    }
492
493    #[test]
494    fn check_wasm_memories_two_memories() {
495        // Generated manually because wat2wasm protects us from creating such Wasm:
496        // "error: only one memory block allowed"
497        let wasm = hex::decode(concat!(
498            "0061736d", // magic bytes
499            "01000000", // binary version (uint32)
500            "05",       // section type (memory)
501            "05",       // section length
502            "02",       // number of memories
503            "0009",     // element of type "resizable_limits", min=9, max=unset
504            "0009",     // element of type "resizable_limits", min=9, max=unset
505        ))
506        .unwrap();
507
508        // wrong number of memories should be caught by the validator
509        match ParsedWasm::parse(&wasm) {
510            Err(VmError::StaticValidationErr { msg, .. }) => {
511                assert!(msg.contains("multiple memories"));
512            }
513            Err(e) => panic!("Unexpected error {e:?}"),
514            Ok(_) => panic!("Didn't reject wasm with invalid api"),
515        }
516    }
517
518    #[test]
519    fn check_wasm_memories_zero_memories() {
520        // Generated manually because wat2wasm would not create an empty memory section
521        let wasm = hex::decode(concat!(
522            "0061736d", // magic bytes
523            "01000000", // binary version (uint32)
524            "05",       // section type (memory)
525            "01",       // section length
526            "00",       // number of memories
527        ))
528        .unwrap();
529
530        match check_wasm_memories(&ParsedWasm::parse(&wasm).unwrap(), &WasmLimits::default()) {
531            Err(VmError::StaticValidationErr { msg, .. }) => {
532                assert!(msg.starts_with("Wasm contract must contain exactly one memory"));
533            }
534            Err(e) => panic!("Unexpected error {e:?}"),
535            Ok(_) => panic!("Didn't reject wasm with invalid api"),
536        }
537    }
538
539    #[test]
540    fn check_wasm_memories_initial_size() {
541        let limits = WasmLimits::default();
542        let wasm_ok = wat::parse_str("(module (memory 512))").unwrap();
543        check_wasm_memories(&ParsedWasm::parse(&wasm_ok).unwrap(), &limits).unwrap();
544
545        let wasm_too_big = wat::parse_str("(module (memory 513))").unwrap();
546        match check_wasm_memories(&ParsedWasm::parse(&wasm_too_big).unwrap(), &limits) {
547            Err(VmError::StaticValidationErr { msg, .. }) => {
548                assert!(msg.starts_with("Wasm contract memory's minimum must not exceed 512 pages"));
549            }
550            Err(e) => panic!("Unexpected error {e:?}"),
551            Ok(_) => panic!("Didn't reject wasm with invalid api"),
552        }
553    }
554
555    #[test]
556    fn check_wasm_memories_maximum_size() {
557        let wasm_max = wat::parse_str("(module (memory 1 5))").unwrap();
558        match check_wasm_memories(
559            &ParsedWasm::parse(&wasm_max).unwrap(),
560            &WasmLimits::default(),
561        ) {
562            Err(VmError::StaticValidationErr { msg, .. }) => {
563                assert!(msg.starts_with("Wasm contract memory's maximum must be unset"));
564            }
565            Err(e) => panic!("Unexpected error {e:?}"),
566            Ok(_) => panic!("Didn't reject wasm with invalid api"),
567        }
568    }
569
570    #[test]
571    fn check_interface_version_works() {
572        // valid
573        let wasm = wat::parse_str(
574            r#"(module
575                (type (func))
576                (func (type 0) nop)
577                (export "add_one" (func 0))
578                (export "allocate" (func 0))
579                (export "interface_version_8" (func 0))
580                (export "deallocate" (func 0))
581                (export "instantiate" (func 0))
582            )"#,
583        )
584        .unwrap();
585        let module = ParsedWasm::parse(&wasm).unwrap();
586        check_interface_version(&module).unwrap();
587
588        // missing
589        let wasm = wat::parse_str(
590            r#"(module
591                (type (func))
592                (func (type 0) nop)
593                (export "add_one" (func 0))
594                (export "allocate" (func 0))
595                (export "deallocate" (func 0))
596                (export "instantiate" (func 0))
597            )"#,
598        )
599        .unwrap();
600        let module = ParsedWasm::parse(&wasm).unwrap();
601        match check_interface_version(&module).unwrap_err() {
602            VmError::StaticValidationErr { msg, .. } => {
603                assert_eq!(
604                    msg,
605                    "Wasm contract missing a required marker export: interface_version_*"
606                );
607            }
608            err => panic!("Unexpected error {err:?}"),
609        }
610
611        // multiple
612        let wasm = wat::parse_str(
613            r#"(module
614                (type (func))
615                (func (type 0) nop)
616                (export "add_one" (func 0))
617                (export "allocate" (func 0))
618                (export "interface_version_8" (func 0))
619                (export "interface_version_9" (func 0))
620                (export "deallocate" (func 0))
621                (export "instantiate" (func 0))
622            )"#,
623        )
624        .unwrap();
625        let module = ParsedWasm::parse(&wasm).unwrap();
626        match check_interface_version(&module).unwrap_err() {
627            VmError::StaticValidationErr { msg, .. } => {
628                assert_eq!(
629                    msg,
630                    "Wasm contract contains more than one marker export: interface_version_*"
631                );
632            }
633            err => panic!("Unexpected error {err:?}"),
634        }
635
636        // CosmWasm 0.15
637        let wasm = wat::parse_str(
638            r#"(module
639                (type (func))
640                (func (type 0) nop)
641                (export "add_one" (func 0))
642                (export "allocate" (func 0))
643                (export "interface_version_6" (func 0))
644                (export "deallocate" (func 0))
645                (export "instantiate" (func 0))
646            )"#,
647        )
648        .unwrap();
649        let module = ParsedWasm::parse(&wasm).unwrap();
650        match check_interface_version(&module).unwrap_err() {
651            VmError::StaticValidationErr { msg, .. } => {
652                assert_eq!(msg, "Wasm contract has unknown interface_version_* marker export (see https://github.com/CosmWasm/cosmwasm/blob/main/packages/vm/README.md)");
653            }
654            err => panic!("Unexpected error {err:?}"),
655        }
656
657        // Unknown value
658        let wasm = wat::parse_str(
659            r#"(module
660                (type (func))
661                (func (type 0) nop)
662                (export "add_one" (func 0))
663                (export "allocate" (func 0))
664                (export "interface_version_broken" (func 0))
665                (export "deallocate" (func 0))
666                (export "instantiate" (func 0))
667            )"#,
668        )
669        .unwrap();
670        let module = ParsedWasm::parse(&wasm).unwrap();
671        match check_interface_version(&module).unwrap_err() {
672            VmError::StaticValidationErr { msg, .. } => {
673                assert_eq!(msg, "Wasm contract has unknown interface_version_* marker export (see https://github.com/CosmWasm/cosmwasm/blob/main/packages/vm/README.md)");
674            }
675            err => panic!("Unexpected error {err:?}"),
676        }
677    }
678
679    #[test]
680    fn check_wasm_exports_works() {
681        // valid
682        let wasm = wat::parse_str(
683            r#"(module
684                (type (func))
685                (func (type 0) nop)
686                (export "add_one" (func 0))
687                (export "allocate" (func 0))
688                (export "deallocate" (func 0))
689            )"#,
690        )
691        .unwrap();
692        let module = ParsedWasm::parse(&wasm).unwrap();
693        check_wasm_exports(&module, Off).unwrap();
694
695        // this is invalid, as it doesn't any required export
696        let wasm = wat::parse_str(
697            r#"(module
698                (type (func))
699                (func (type 0) nop)
700                (export "add_one" (func 0))
701            )"#,
702        )
703        .unwrap();
704        let module = ParsedWasm::parse(&wasm).unwrap();
705        match check_wasm_exports(&module, Off) {
706            Err(VmError::StaticValidationErr { msg, .. }) => {
707                assert!(msg.starts_with("Wasm contract doesn't have required export: \"allocate\""));
708            }
709            Err(e) => panic!("Unexpected error {e:?}"),
710            Ok(_) => panic!("Didn't reject wasm with invalid api"),
711        }
712
713        // this is invalid, as it doesn't contain all required exports
714        let wasm = wat::parse_str(
715            r#"(module
716                (type (func))
717                (func (type 0) nop)
718                (export "add_one" (func 0))
719                (export "allocate" (func 0))
720            )"#,
721        )
722        .unwrap();
723        let module = ParsedWasm::parse(&wasm).unwrap();
724        match check_wasm_exports(&module, Off) {
725            Err(VmError::StaticValidationErr { msg, .. }) => {
726                assert!(
727                    msg.starts_with("Wasm contract doesn't have required export: \"deallocate\"")
728                );
729            }
730            Err(e) => panic!("Unexpected error {e:?}"),
731            Ok(_) => panic!("Didn't reject wasm with invalid api"),
732        }
733    }
734
735    #[test]
736    fn check_wasm_imports_ok() {
737        let wasm = wat::parse_str(
738            r#"(module
739            (import "env" "db_read" (func (param i32 i32) (result i32)))
740            (import "env" "db_write" (func (param i32 i32) (result i32)))
741            (import "env" "db_remove" (func (param i32) (result i32)))
742            (import "env" "addr_validate" (func (param i32) (result i32)))
743            (import "env" "addr_canonicalize" (func (param i32 i32) (result i32)))
744            (import "env" "addr_humanize" (func (param i32 i32) (result i32)))
745            (import "env" "secp256k1_verify" (func (param i32 i32 i32) (result i32)))
746            (import "env" "secp256k1_recover_pubkey" (func (param i32 i32 i32) (result i64)))
747            (import "env" "secp256r1_verify" (func (param i32 i32 i32) (result i32)))
748            (import "env" "secp256r1_recover_pubkey" (func (param i32 i32 i32) (result i64)))
749            (import "env" "ed25519_verify" (func (param i32 i32 i32) (result i32)))
750            (import "env" "ed25519_batch_verify" (func (param i32 i32 i32) (result i32)))
751        )"#,
752        )
753        .unwrap();
754        check_wasm_imports(
755            &ParsedWasm::parse(&wasm).unwrap(),
756            SUPPORTED_IMPORTS,
757            &WasmLimits::default(),
758            Off,
759        )
760        .unwrap();
761    }
762
763    #[test]
764    fn check_wasm_imports_exceeds_limit() {
765        let wasm = wat::parse_str(
766            r#"(module
767            (import "env" "db_write" (func (param i32 i32) (result i32)))
768            (import "env" "db_remove" (func (param i32) (result i32)))
769            (import "env" "addr_validate" (func (param i32) (result i32)))
770            (import "env" "addr_canonicalize" (func (param i32 i32) (result i32)))
771            (import "env" "addr_humanize" (func (param i32 i32) (result i32)))
772            (import "env" "secp256k1_verify" (func (param i32 i32 i32) (result i32)))
773            (import "env" "secp256k1_recover_pubkey" (func (param i32 i32 i32) (result i64)))
774            (import "env" "secp256r1_verify" (func (param i32 i32 i32) (result i32)))
775            (import "env" "secp256r1_recover_pubkey" (func (param i32 i32 i32) (result i64)))
776            (import "env" "ed25519_verify" (func (param i32 i32 i32) (result i32)))
777            (import "env" "ed25519_batch_verify" (func (param i32 i32 i32) (result i32)))
778            (import "env" "spam01" (func (param i32 i32) (result i32)))
779            (import "env" "spam02" (func (param i32 i32) (result i32)))
780            (import "env" "spam03" (func (param i32 i32) (result i32)))
781            (import "env" "spam04" (func (param i32 i32) (result i32)))
782            (import "env" "spam05" (func (param i32 i32) (result i32)))
783            (import "env" "spam06" (func (param i32 i32) (result i32)))
784            (import "env" "spam07" (func (param i32 i32) (result i32)))
785            (import "env" "spam08" (func (param i32 i32) (result i32)))
786            (import "env" "spam09" (func (param i32 i32) (result i32)))
787            (import "env" "spam10" (func (param i32 i32) (result i32)))
788            (import "env" "spam11" (func (param i32 i32) (result i32)))
789            (import "env" "spam12" (func (param i32 i32) (result i32)))
790            (import "env" "spam13" (func (param i32 i32) (result i32)))
791            (import "env" "spam14" (func (param i32 i32) (result i32)))
792            (import "env" "spam15" (func (param i32 i32) (result i32)))
793            (import "env" "spam16" (func (param i32 i32) (result i32)))
794            (import "env" "spam17" (func (param i32 i32) (result i32)))
795            (import "env" "spam18" (func (param i32 i32) (result i32)))
796            (import "env" "spam19" (func (param i32 i32) (result i32)))
797            (import "env" "spam20" (func (param i32 i32) (result i32)))
798            (import "env" "spam21" (func (param i32 i32) (result i32)))
799            (import "env" "spam22" (func (param i32 i32) (result i32)))
800            (import "env" "spam23" (func (param i32 i32) (result i32)))
801            (import "env" "spam24" (func (param i32 i32) (result i32)))
802            (import "env" "spam25" (func (param i32 i32) (result i32)))
803            (import "env" "spam26" (func (param i32 i32) (result i32)))
804            (import "env" "spam27" (func (param i32 i32) (result i32)))
805            (import "env" "spam28" (func (param i32 i32) (result i32)))
806            (import "env" "spam29" (func (param i32 i32) (result i32)))
807            (import "env" "spam30" (func (param i32 i32) (result i32)))
808            (import "env" "spam31" (func (param i32 i32) (result i32)))
809            (import "env" "spam32" (func (param i32 i32) (result i32)))
810            (import "env" "spam33" (func (param i32 i32) (result i32)))
811            (import "env" "spam34" (func (param i32 i32) (result i32)))
812            (import "env" "spam35" (func (param i32 i32) (result i32)))
813            (import "env" "spam36" (func (param i32 i32) (result i32)))
814            (import "env" "spam37" (func (param i32 i32) (result i32)))
815            (import "env" "spam38" (func (param i32 i32) (result i32)))
816            (import "env" "spam39" (func (param i32 i32) (result i32)))
817            (import "env" "spam40" (func (param i32 i32) (result i32)))
818            (import "env" "spam41" (func (param i32 i32) (result i32)))
819            (import "env" "spam42" (func (param i32 i32) (result i32)))
820            (import "env" "spam43" (func (param i32 i32) (result i32)))
821            (import "env" "spam44" (func (param i32 i32) (result i32)))
822            (import "env" "spam45" (func (param i32 i32) (result i32)))
823            (import "env" "spam46" (func (param i32 i32) (result i32)))
824            (import "env" "spam47" (func (param i32 i32) (result i32)))
825            (import "env" "spam48" (func (param i32 i32) (result i32)))
826            (import "env" "spam49" (func (param i32 i32) (result i32)))
827            (import "env" "spam50" (func (param i32 i32) (result i32)))
828            (import "env" "spam51" (func (param i32 i32) (result i32)))
829            (import "env" "spam52" (func (param i32 i32) (result i32)))
830            (import "env" "spam53" (func (param i32 i32) (result i32)))
831            (import "env" "spam54" (func (param i32 i32) (result i32)))
832            (import "env" "spam55" (func (param i32 i32) (result i32)))
833            (import "env" "spam56" (func (param i32 i32) (result i32)))
834            (import "env" "spam57" (func (param i32 i32) (result i32)))
835            (import "env" "spam58" (func (param i32 i32) (result i32)))
836            (import "env" "spam59" (func (param i32 i32) (result i32)))
837            (import "env" "spam60" (func (param i32 i32) (result i32)))
838            (import "env" "spam61" (func (param i32 i32) (result i32)))
839            (import "env" "spam62" (func (param i32 i32) (result i32)))
840            (import "env" "spam63" (func (param i32 i32) (result i32)))
841            (import "env" "spam64" (func (param i32 i32) (result i32)))
842            (import "env" "spam65" (func (param i32 i32) (result i32)))
843            (import "env" "spam66" (func (param i32 i32) (result i32)))
844            (import "env" "spam67" (func (param i32 i32) (result i32)))
845            (import "env" "spam68" (func (param i32 i32) (result i32)))
846            (import "env" "spam69" (func (param i32 i32) (result i32)))
847            (import "env" "spam70" (func (param i32 i32) (result i32)))
848            (import "env" "spam71" (func (param i32 i32) (result i32)))
849            (import "env" "spam72" (func (param i32 i32) (result i32)))
850            (import "env" "spam73" (func (param i32 i32) (result i32)))
851            (import "env" "spam74" (func (param i32 i32) (result i32)))
852            (import "env" "spam75" (func (param i32 i32) (result i32)))
853            (import "env" "spam76" (func (param i32 i32) (result i32)))
854            (import "env" "spam77" (func (param i32 i32) (result i32)))
855            (import "env" "spam78" (func (param i32 i32) (result i32)))
856            (import "env" "spam79" (func (param i32 i32) (result i32)))
857            (import "env" "spam80" (func (param i32 i32) (result i32)))
858            (import "env" "spam81" (func (param i32 i32) (result i32)))
859            (import "env" "spam82" (func (param i32 i32) (result i32)))
860            (import "env" "spam83" (func (param i32 i32) (result i32)))
861            (import "env" "spam84" (func (param i32 i32) (result i32)))
862            (import "env" "spam85" (func (param i32 i32) (result i32)))
863            (import "env" "spam86" (func (param i32 i32) (result i32)))
864            (import "env" "spam87" (func (param i32 i32) (result i32)))
865            (import "env" "spam88" (func (param i32 i32) (result i32)))
866            (import "env" "spam89" (func (param i32 i32) (result i32)))
867            (import "env" "spam90" (func (param i32 i32) (result i32)))
868        )"#,
869        )
870        .unwrap();
871        let err = check_wasm_imports(
872            &ParsedWasm::parse(&wasm).unwrap(),
873            SUPPORTED_IMPORTS,
874            &WasmLimits::default(),
875            Off,
876        )
877        .unwrap_err();
878        match err {
879            VmError::StaticValidationErr { msg, .. } => {
880                assert_eq!(msg, "Import count exceeds limit. Imports: 101. Limit: 100.");
881            }
882            err => panic!("Unexpected error: {err:?}"),
883        }
884    }
885
886    #[test]
887    fn check_wasm_imports_missing() {
888        let wasm = wat::parse_str(
889            r#"(module
890            (import "env" "foo" (func (param i32 i32) (result i32)))
891            (import "env" "bar" (func (param i32 i32) (result i32)))
892            (import "env" "spammyspam01" (func (param i32 i32) (result i32)))
893            (import "env" "spammyspam02" (func (param i32 i32) (result i32)))
894            (import "env" "spammyspam03" (func (param i32 i32) (result i32)))
895            (import "env" "spammyspam04" (func (param i32 i32) (result i32)))
896            (import "env" "spammyspam05" (func (param i32 i32) (result i32)))
897            (import "env" "spammyspam06" (func (param i32 i32) (result i32)))
898            (import "env" "spammyspam07" (func (param i32 i32) (result i32)))
899            (import "env" "spammyspam08" (func (param i32 i32) (result i32)))
900            (import "env" "spammyspam09" (func (param i32 i32) (result i32)))
901            (import "env" "spammyspam10" (func (param i32 i32) (result i32)))
902        )"#,
903        )
904        .unwrap();
905        let supported_imports: &[&str] = &[
906            "env.db_read",
907            "env.db_write",
908            "env.db_remove",
909            "env.addr_canonicalize",
910            "env.addr_humanize",
911            "env.debug",
912            "env.query_chain",
913        ];
914        let result = check_wasm_imports(
915            &ParsedWasm::parse(&wasm).unwrap(),
916            supported_imports,
917            &WasmLimits::default(),
918            Off,
919        );
920        match result.unwrap_err() {
921            VmError::StaticValidationErr { msg, .. } => {
922                println!("{msg}");
923                assert_eq!(
924                    msg,
925                    r#"Wasm contract requires unsupported import: "env.foo". Required imports: {"env.bar", "env.foo", "env.spammyspam01", "env.spammyspam02", "env.spammyspam03", "env.spammyspam04", "env.spammyspam05", "env.spammyspam06", "env.spammyspam07", "env.spammyspam08", ... 2 more}. Available imports: ["env.db_read", "env.db_write", "env.db_remove", "env.addr_canonicalize", "env.addr_humanize", "env.debug", "env.query_chain"]."#
926                );
927            }
928            err => panic!("Unexpected error: {err:?}"),
929        }
930    }
931
932    #[test]
933    fn check_wasm_imports_of_old_contract() {
934        let module = &ParsedWasm::parse(CONTRACT_0_7).unwrap();
935        let result = check_wasm_imports(module, SUPPORTED_IMPORTS, &WasmLimits::default(), Off);
936        match result.unwrap_err() {
937            VmError::StaticValidationErr { msg, .. } => {
938                assert!(
939                    msg.starts_with("Wasm contract requires unsupported import: \"env.read_db\"")
940                );
941            }
942            err => panic!("Unexpected error: {err:?}"),
943        }
944    }
945
946    #[test]
947    fn check_wasm_imports_wrong_type() {
948        let wasm = wat::parse_str(r#"(module (import "env" "db_read" (memory 1 1)))"#).unwrap();
949        let result = check_wasm_imports(
950            &ParsedWasm::parse(&wasm).unwrap(),
951            SUPPORTED_IMPORTS,
952            &WasmLimits::default(),
953            Off,
954        );
955        match result.unwrap_err() {
956            VmError::StaticValidationErr { msg, .. } => {
957                assert!(
958                    msg.starts_with("Wasm contract requires non-function import: \"env.db_read\"")
959                );
960            }
961            err => panic!("Unexpected error: {err:?}"),
962        }
963    }
964
965    #[test]
966    fn check_wasm_capabilities_ok() {
967        let wasm = wat::parse_str(
968            r#"(module
969            (type (func))
970            (func (type 0) nop)
971            (export "requires_water" (func 0))
972            (export "requires_" (func 0))
973            (export "requires_nutrients" (func 0))
974            (export "require_milk" (func 0))
975            (export "REQUIRES_air" (func 0))
976            (export "requires_sun" (func 0))
977        )"#,
978        )
979        .unwrap();
980        let module = ParsedWasm::parse(&wasm).unwrap();
981        let available = [
982            "water".to_string(),
983            "nutrients".to_string(),
984            "sun".to_string(),
985            "freedom".to_string(),
986        ]
987        .into_iter()
988        .collect();
989        check_wasm_capabilities(&module, &available, Off).unwrap();
990    }
991
992    #[test]
993    fn check_wasm_capabilities_fails_for_missing() {
994        let wasm = wat::parse_str(
995            r#"(module
996            (type (func))
997            (func (type 0) nop)
998            (export "requires_water" (func 0))
999            (export "requires_" (func 0))
1000            (export "requires_nutrients" (func 0))
1001            (export "require_milk" (func 0))
1002            (export "REQUIRES_air" (func 0))
1003            (export "requires_sun" (func 0))
1004        )"#,
1005        )
1006        .unwrap();
1007        let module = ParsedWasm::parse(&wasm).unwrap();
1008
1009        // Available set 1
1010        let available = [
1011            "water".to_string(),
1012            "nutrients".to_string(),
1013            "freedom".to_string(),
1014        ]
1015        .into_iter()
1016        .collect();
1017        match check_wasm_capabilities(&module, &available, Off).unwrap_err() {
1018            VmError::StaticValidationErr { msg, .. } => assert_eq!(
1019                msg,
1020                "Wasm contract requires unavailable capabilities: {\"sun\"}"
1021            ),
1022            _ => panic!("Got unexpected error"),
1023        }
1024
1025        // Available set 2
1026        let available = [
1027            "nutrients".to_string(),
1028            "freedom".to_string(),
1029            "Water".to_string(), // capabilities are case sensitive (and lowercase by convention)
1030        ]
1031        .into_iter()
1032        .collect();
1033        match check_wasm_capabilities(&module, &available, Off).unwrap_err() {
1034            VmError::StaticValidationErr { msg, .. } => assert_eq!(
1035                msg,
1036                "Wasm contract requires unavailable capabilities: {\"sun\", \"water\"}"
1037            ),
1038            _ => panic!("Got unexpected error"),
1039        }
1040
1041        // Available set 3
1042        let available = ["freedom".to_string()].into_iter().collect();
1043        match check_wasm_capabilities(&module, &available, Off).unwrap_err() {
1044            VmError::StaticValidationErr { msg, .. } => assert_eq!(
1045                msg,
1046                "Wasm contract requires unavailable capabilities: {\"nutrients\", \"sun\", \"water\"}"
1047            ),
1048            _ => panic!("Got unexpected error"),
1049        }
1050
1051        // Available set 4
1052        let available = [].into_iter().collect();
1053        match check_wasm_capabilities(&module, &available, Off).unwrap_err() {
1054            VmError::StaticValidationErr { msg, .. } => assert_eq!(
1055                msg,
1056                "Wasm contract requires unavailable capabilities: {\"nutrients\", \"sun\", \"water\"}"
1057            ),
1058            _ => panic!("Got unexpected error"),
1059        }
1060    }
1061
1062    #[test]
1063    fn check_wasm_fails_for_big_functions() {
1064        let limits = WasmLimits::default();
1065        // too many arguments
1066        let args = " i32".repeat(limits.max_function_params() + 1);
1067        let wasm = wat::parse_str(format!(
1068            r#"(module
1069            (type (func (param {args})))
1070            (func (type 0) nop)
1071        )"#
1072        ))
1073        .unwrap();
1074        let module = ParsedWasm::parse(&wasm).unwrap();
1075
1076        match check_wasm_functions(&module, &limits, Off).unwrap_err() {
1077            VmError::StaticValidationErr { msg, .. } => assert_eq!(
1078                msg,
1079                "Wasm contract contains function with more than 100 parameters"
1080            ),
1081            _ => panic!("Got unexpected error"),
1082        }
1083
1084        // too many returns
1085        let return_types = " i32".repeat(limits.max_function_results() + 1);
1086        let returns = " i32.const 42".repeat(limits.max_function_results() + 1);
1087        let wasm = wat::parse_str(format!(
1088            r#"(module
1089            (type (func (result {return_types})))
1090            (func (type 0) {returns})
1091        )"#
1092        ))
1093        .unwrap();
1094        let module = ParsedWasm::parse(&wasm).unwrap();
1095        match check_wasm_functions(&module, &limits, Off).unwrap_err() {
1096            VmError::StaticValidationErr { msg, .. } => assert_eq!(
1097                msg,
1098                "Wasm contract contains function with more than 1 results"
1099            ),
1100            _ => panic!("Got unexpected error"),
1101        }
1102
1103        // too many functions
1104        let functions = "(func (type 0) nop)".repeat(limits.max_functions() + 1);
1105        let wasm = wat::parse_str(format!(
1106            r#"(module
1107            (type (func))
1108            {functions}
1109        )"#
1110        ))
1111        .unwrap();
1112        let module = ParsedWasm::parse(&wasm).unwrap();
1113        match check_wasm_functions(&module, &limits, Off).unwrap_err() {
1114            VmError::StaticValidationErr { msg, .. } => {
1115                assert_eq!(msg, "Wasm contract contains more than 20000 functions")
1116            }
1117            _ => panic!("Got unexpected error"),
1118        }
1119    }
1120}