rwasm 0.4.8

ZK-friendly WebAssembly runtime optimized for blockchain and zero-knowledge applications
Documentation
use rwasm::{CompilationConfig, ExecutionEngine, RwasmModule, RwasmStore, StoreTr, Value};
use rwasm_fuel_policy::FuelCosts;

#[test]
fn test_entrypoint_call_consumes_fuel() {
    let wasm_binary = wat::parse_str(
        r#"
        (module
          (func (export "entry"))
        )
        "#,
    )
    .unwrap();
    let config = CompilationConfig::default()
        .with_entrypoint_name("entry".into())
        .with_consume_fuel(true)
        .with_consume_fuel_for_params_and_locals(false);
    let (module, _) = RwasmModule::compile(config, &wasm_binary).unwrap();

    let fuel_limit = 100;
    let mut store = RwasmStore::<()>::default();
    store.reset_fuel(fuel_limit);
    ExecutionEngine::new()
        .execute(&mut store, &module, &[], &mut [])
        .unwrap();

    assert_eq!(
        fuel_limit - store.remaining_fuel().unwrap(),
        FuelCosts::BASE as u64
    );
}

#[test]
fn test_initial_memory_fuel_respects_bulk_ops_config() {
    const INITIAL_MEMORY_PAGES: u64 = 2;

    let wasm_binary = wat::parse_str(
        r#"
        (module
          (memory 2)
          (func (export "entry"))
        )
        "#,
    )
    .unwrap();
    let fuel_limit = 100_000;

    let consumed_by_entrypoint = |consume_fuel_for_bulk_ops| {
        let config = CompilationConfig::default()
            .with_entrypoint_name("entry".into())
            .with_consume_fuel(true)
            .with_consume_fuel_for_bulk_ops(consume_fuel_for_bulk_ops);
        let (module, _) = RwasmModule::compile(config, &wasm_binary).unwrap();
        let mut store = RwasmStore::<()>::default();
        store.reset_fuel(fuel_limit);
        ExecutionEngine::new()
            .entrypoint(&mut store, &module)
            .unwrap();
        fuel_limit - store.remaining_fuel().unwrap()
    };

    assert_eq!(
        consumed_by_entrypoint(true),
        INITIAL_MEMORY_PAGES * rwasm::N_BYTES_PER_MEMORY_PAGE as u64
            / rwasm_fuel_policy::MEMORY_BYTES_PER_FUEL as u64
    );
    assert_eq!(consumed_by_entrypoint(false), 0);
}

#[test]
fn test_locals_consume_fuel() {
    let fuel_limit = 9999;
    let basic_fuel_consumption = 2;

    let test_cases: &mut [(usize, usize)] = &mut [
        (0, 0),
        (1, 0),
        (3, 0),
        (16, 0),
        (32, 0),
        (1000, 0),
        // locals_count>1000 -> 'function params size is out of bounds' (RwasmModule::compile fails)
    ];
    test_cases
        .iter_mut()
        .for_each(|(p_count, fuel)| *fuel = FuelCosts::fuel_for_locals(*p_count as u32) as usize);
    for (locals_count, fuel_cost) in test_cases.iter().cloned() {
        let mut wat_params: Vec<&str> = Vec::with_capacity(locals_count);
        for _ in 0..locals_count {
            wat_params.push("i32")
        }
        let params_or_locals_str = wat_params.join(" ");
        let params_wat_str = format!(
            r#"
            (module
              (func (export "entry") (param {params_or_locals_str}) (result i32)
                (i32.const 111)
              )
            )
        "#
        );
        let locals_wat_str = format!(
            r#"
            (module
              (func (export "entry") (result i32)
                (local {params_or_locals_str})
                (i32.const 111)
              )
            )
        "#
        );
        let params_wasm_binary = wat::parse_str(params_wat_str).unwrap();
        let locals_wasm_binary = wat::parse_str(locals_wat_str).unwrap();
        let config = CompilationConfig::default()
            .with_entrypoint_name("entry".into())
            .with_allow_malformed_entrypoint_func_type(true)
            .with_consume_fuel(true);
        let (params_rwasm_module, _) =
            RwasmModule::compile(config.clone(), &params_wasm_binary).unwrap();
        println!("params_rwasm_module:{}", params_rwasm_module);
        let (locals_rwasm_module, _) = RwasmModule::compile(config, &locals_wasm_binary).unwrap();
        println!("locals_rwasm_module:{}", locals_rwasm_module);
        let engine = ExecutionEngine::new();
        let mut result = [Value::I32(0); 1];
        let mut params_values = Vec::with_capacity(locals_count);
        for _ in 0..locals_count {
            params_values.push(Value::I32(0));
        }
        for (i, module) in [params_rwasm_module, locals_rwasm_module]
            .iter()
            .enumerate()
        {
            let mut store = RwasmStore::<()>::default();
            store.reset_fuel(fuel_limit);
            engine
                .execute(
                    &mut store,
                    module,
                    if i == 0 { &params_values } else { &[] },
                    &mut result,
                )
                .unwrap();
            let remaining_fuel = store.remaining_fuel();
            assert_eq!(
                Some(fuel_limit - basic_fuel_consumption - fuel_cost as u64),
                remaining_fuel,
                "module {} failed",
                i
            );
            assert_eq!(result[0].i32().unwrap(), 111);
        }
    }
}

#[test]
fn test_memory_fill_fuel_scales_with_length() {
    let wasm_binary = wat::parse_str(
        r#"
        (module
          (memory (export "memory") 1)
          (func (export "entry") (param i32)
            i32.const 1
            memory.grow
            drop
            i32.const 0
            i32.const 7
            local.get 0
            memory.fill
          )
        )
        "#,
    )
    .unwrap();
    let config = CompilationConfig::default()
        .with_entrypoint_name("entry".into())
        .with_consume_fuel(true)
        .with_consume_fuel_for_bulk_ops(true);
    let (module, _) = RwasmModule::compile(config, &wasm_binary).unwrap();
    let engine = ExecutionEngine::new();
    let fuel_limit = 100_000;

    let consumed_for_len = |len| {
        let mut store = RwasmStore::<()>::default();
        let mut result = [];
        store.reset_fuel(fuel_limit);
        engine
            .execute(&mut store, &module, &[Value::I32(len)], &mut result)
            .unwrap();
        fuel_limit - store.remaining_fuel().unwrap()
    };

    assert!(consumed_for_len(128) > consumed_for_len(1));
}

#[test]
fn test_bulk_fuel_checks_are_enabled_by_default() {
    let wasm_binary = wat::parse_str(
        r#"
        (module
          (memory (export "memory") 1)
          (func (export "entry") (param i32)
            i32.const 1
            memory.grow
            drop
            i32.const 0
            i32.const 7
            local.get 0
            memory.fill
          )
        )
        "#,
    )
    .unwrap();
    let base_config = CompilationConfig::default()
        .with_entrypoint_name("entry".into())
        .with_consume_fuel(true);
    let engine = ExecutionEngine::new();
    let fuel_limit = 100_000;

    let consumed_for_len = |config: CompilationConfig, len| {
        let (module, _) = RwasmModule::compile(config, &wasm_binary).unwrap();
        let mut store = RwasmStore::<()>::default();
        let mut result = [];
        store.reset_fuel(fuel_limit);
        engine
            .execute(&mut store, &module, &[Value::I32(len)], &mut result)
            .unwrap();
        fuel_limit - store.remaining_fuel().unwrap()
    };

    let default_short = consumed_for_len(base_config.clone(), 1);
    let default_long = consumed_for_len(base_config.clone(), 128);
    let unchecked_long = consumed_for_len(base_config.with_consume_fuel_for_bulk_ops(false), 128);

    assert!(default_long > default_short);
    assert!(default_long > unchecked_long);
}

#[test]
fn test_table_bulk_ops_compile_with_fuel_metering() {
    let wasm_binary = wat::parse_str(
        r#"
        (module
          (type $t (func))
          (func $f)
          (table 4 funcref)
          (export "table" (table 0))
          (elem funcref (ref.func $f))
          (func (export "entry")
            i32.const 0
            i32.const 0
            i32.const 1
            table.init 0 0

            i32.const 1
            i32.const 0
            i32.const 1
            table.copy 0 0

            i32.const 2
            ref.func $f
            i32.const 1
            table.fill 0
          )
        )
        "#,
    )
    .unwrap();
    let config = CompilationConfig::default()
        .with_entrypoint_name("entry".into())
        .with_consume_fuel(true)
        .with_consume_fuel_for_bulk_ops(true);

    let (module, _) = RwasmModule::compile(config, &wasm_binary).unwrap();
    let fuel_limit = 10_000;
    let mut store = RwasmStore::<()>::default();
    let mut result = [];
    let engine = ExecutionEngine::new();
    engine.entrypoint(&mut store, &module).unwrap();
    store.reset_fuel(fuel_limit);
    engine
        .execute(&mut store, &module, &[], &mut result)
        .unwrap();

    let consumed = fuel_limit - store.remaining_fuel().unwrap();
    assert!(consumed > FuelCosts::BASE as u64);
}

#[test]
fn test_strategy_compatible_config_disables_rwasm_only_fuel() {
    let default_config = CompilationConfig::default();
    assert!(default_config.consume_fuel_for_bulk_ops);
    assert!(default_config.consume_fuel_for_params_and_locals);
    assert!(!default_config.is_strategy_compatible());

    let compatible = CompilationConfig::default_strategy_compatible();
    assert!(!compatible.consume_fuel_for_bulk_ops);
    assert!(!compatible.consume_fuel_for_params_and_locals);
    assert!(compatible.is_strategy_compatible());
    // Everything the Wasmtime path does implement stays at the plain default.
    assert_eq!(compatible.consume_fuel, default_config.consume_fuel);
    assert_eq!(
        compatible.builtins_consume_fuel,
        default_config.builtins_consume_fuel
    );
    assert_eq!(compatible.code_snippets, default_config.code_snippets);
    assert_eq!(
        compatible.max_allowed_memory_pages,
        default_config.max_allowed_memory_pages
    );
}