564 lines
20 KiB
Rust
564 lines
20 KiB
Rust
use helpers::{Executable, Signed};
|
|
use std::fmt;
|
|
|
|
use rustc_hex::FromHex;
|
|
use serde::{Deserialize, Deserializer};
|
|
use std::hash::{Hash, Hasher};
|
|
use std::rc::Rc;
|
|
use wasmi::{ImportsBuilder, ModuleInstance, ModuleRef, NopExternals};
|
|
use zokrates_field::Field;
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
pub struct WasmHelper(
|
|
#[serde(skip)] std::rc::Rc<ModuleRef>,
|
|
#[serde(serialize_with = "serde_bytes::serialize")] Vec<u8>,
|
|
);
|
|
|
|
impl WasmHelper {
|
|
// Hand-coded assembly for identity.
|
|
// Source available at https://gist.github.com/gballet/f14d11053d8f846bfbb3687581b0eecb#file-identity-wast
|
|
pub const IDENTITY_WASM: &'static str = "0061736d010000000105016000017f030302000005030100010615047f0041010b7f0041010b7f0041200b7f0141000b074b06066d656d6f727902000e6765745f696e707574735f6f6666000105736f6c766500000a6d696e5f696e7075747303000b6d696e5f6f75747075747303010a6669656c645f73697a6503020a300229000340412023036a410023036a280200360200230341016a240323032302470d000b4100240341200b040041000b0b4b020041000b20ffffffff000000000000000000000000ffffffff0000000000000000000000000041200b20deadbeef000000000000000000000000deadbeef000000000000000000000000";
|
|
|
|
pub fn from_hex<U: Into<String>>(u: U) -> Self {
|
|
let code_hex = u.into();
|
|
let code = FromHex::from_hex(&code_hex[..])
|
|
.expect(format!("invalid bytecode: {}", code_hex).as_str());
|
|
WasmHelper::from(code)
|
|
}
|
|
}
|
|
|
|
impl<U: Into<Vec<u8>>> From<U> for WasmHelper {
|
|
fn from(code: U) -> Self {
|
|
let code_vec = code.into();
|
|
let module = wasmi::Module::from_buffer(code_vec.clone()).expect("Error decoding buffer");
|
|
let modinst = ModuleInstance::new(&module, &ImportsBuilder::default())
|
|
.expect("Failed to instantiate module")
|
|
.assert_no_start();
|
|
WasmHelper(Rc::new(modinst), code_vec)
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for WasmHelper {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let hex: Vec<u8> = serde_bytes::deserialize(deserializer)?;
|
|
Ok(WasmHelper::from(hex))
|
|
}
|
|
}
|
|
|
|
impl PartialEq for WasmHelper {
|
|
fn eq(&self, other: &WasmHelper) -> bool {
|
|
self.1 == other.1
|
|
}
|
|
}
|
|
|
|
impl Eq for WasmHelper {}
|
|
|
|
impl Hash for WasmHelper {
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
self.1.hash(state);
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for WasmHelper {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(f, "Hex(\"{:?}\")", &self.1[..])
|
|
}
|
|
}
|
|
|
|
fn get_export<T: wasmi::FromRuntimeValue>(varname: &str, modref: &ModuleRef) -> Result<T, String> {
|
|
modref
|
|
.export_by_name(varname)
|
|
.ok_or(&format!("Could not find exported symbol `{}` in module", varname)[..])?
|
|
.as_global()
|
|
.ok_or(format!(
|
|
"Error getting {} from the list of globals",
|
|
varname
|
|
))?
|
|
.get()
|
|
.try_into::<T>()
|
|
.ok_or(format!("Error converting `{}` to i32", varname))
|
|
}
|
|
|
|
impl Signed for WasmHelper {
|
|
fn get_signature(&self) -> (usize, usize) {
|
|
// Check that the module has the following exports:
|
|
// * min_inputs = the (minimum) number of inputs
|
|
// * min_outputs = the (minimum) number of outputs
|
|
let ni = get_export::<i32>("min_inputs", self.0.as_ref()).unwrap();
|
|
let no = get_export::<i32>("min_outputs", self.0.as_ref()).unwrap();
|
|
|
|
(ni as usize, no as usize)
|
|
}
|
|
}
|
|
|
|
impl<T: Field> Executable<T> for WasmHelper {
|
|
fn execute(&self, inputs: &Vec<T>) -> Result<Vec<T>, String> {
|
|
let field_size = get_export::<i32>("field_size", self.0.as_ref())? as usize;
|
|
let ninputs = get_export::<i32>("min_inputs", self.0.as_ref())? as usize;
|
|
|
|
if ninputs != inputs.len() {
|
|
return Err(format!(
|
|
"`solve` expected {} inputs, received {}",
|
|
ninputs,
|
|
inputs.len()
|
|
));
|
|
}
|
|
|
|
/* Prepare the inputs */
|
|
let input_offset = self
|
|
.0
|
|
.invoke_export("get_inputs_off", &[], &mut NopExternals)
|
|
.map_err(|e| format!("Error getting the input offset: {}", e.to_string()))?
|
|
.ok_or("`get_inputs_off` did not return any value")?
|
|
.try_into::<i32>()
|
|
.ok_or("`get_inputs_off` returned the wrong type")?;
|
|
|
|
let mem = self
|
|
.0
|
|
.as_ref()
|
|
.export_by_name("memory")
|
|
.ok_or("Module didn't export its memory section")?
|
|
.as_memory()
|
|
.unwrap()
|
|
.clone();
|
|
|
|
for (index, input) in inputs.iter().enumerate() {
|
|
// Get the field's bytes and check they correspond to
|
|
// the value that the module expects.
|
|
let mut bv = input.into_byte_vector();
|
|
if bv.len() > field_size {
|
|
return Err(format!(
|
|
"Input #{} is stored on {} bytes which is greater than the field size of {}",
|
|
index,
|
|
bv.len(),
|
|
field_size
|
|
));
|
|
} else {
|
|
bv.resize(field_size, 0);
|
|
}
|
|
|
|
let addr = (input_offset as u32) + (index as u32) * (field_size as u32);
|
|
mem.set(addr, &bv[..])
|
|
.map_err(|_e| format!("Could not write at memory address {}", addr))?;
|
|
}
|
|
|
|
let output_offset = self
|
|
.0
|
|
.as_ref()
|
|
.invoke_export("solve", &[], &mut NopExternals)
|
|
.map_err(|e| format!("Error solving the problem: {}", e.to_string()))?
|
|
.ok_or("`solve` did not return any value")?
|
|
.try_into::<i32>()
|
|
.ok_or("`solve returned the wrong type`")?;
|
|
|
|
// NOTE: The question regarding the way that an error code is
|
|
// returned is still open.
|
|
//
|
|
// The current model considers that 2GB is more than enough
|
|
// to store the output data.
|
|
//
|
|
// This being said this approach is tacky and I am considering
|
|
// others at this point:
|
|
//
|
|
// 1. Use a 64 bit return code, values greater than 32-bits
|
|
// are considered error codes.
|
|
// 2. Export an extra global called `errno` which contains
|
|
// the error code.
|
|
// 3. 32-bit alignment gives a 2-bit error field
|
|
// 4. Return a pointer to a structure that contains
|
|
// the error code just before the output data.
|
|
//
|
|
// Experimenting with other languages will help decide what
|
|
// is the better approach.
|
|
if output_offset > 0 {
|
|
let mut outputs = Vec::new();
|
|
let noutputs = get_export::<i32>("min_outputs", self.0.as_ref())?;
|
|
for i in 0..noutputs {
|
|
let index = i as u32;
|
|
let fs = field_size as u32;
|
|
let value = mem
|
|
.get(output_offset as u32 + fs * index, field_size)
|
|
.map_err(|e| format!("Could not retrieve the output offset: {}", e))?;
|
|
|
|
outputs.push(T::from_byte_vector(value));
|
|
}
|
|
|
|
Ok(outputs)
|
|
} else {
|
|
Err(format!("`solve` returned error code {}", output_offset))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use parity_wasm::builder::*;
|
|
use parity_wasm::elements::{Instruction, Instructions, ValueType};
|
|
use std::panic;
|
|
use zokrates_field::Bn128Field;
|
|
|
|
fn remove_export(code: &str, symbol: &str) -> Vec<u8> {
|
|
let code = FromHex::from_hex(code).unwrap();
|
|
let mut idmod: parity_wasm::elements::Module = parity_wasm::deserialize_buffer(&code[..])
|
|
.expect("Could not deserialize Identity module");
|
|
idmod
|
|
.export_section_mut()
|
|
.expect("Could not get export section")
|
|
.entries_mut()
|
|
.retain(|ref export| export.field() != symbol);
|
|
parity_wasm::serialize(idmod).expect("Could not serialize buffer")
|
|
}
|
|
|
|
fn replace_function(
|
|
code: &str,
|
|
symbol: &str,
|
|
params: Vec<ValueType>,
|
|
ret: Option<ValueType>,
|
|
instr: Vec<Instruction>,
|
|
) -> Vec<u8> {
|
|
/* Deserialize to parity_wasm format */
|
|
let code = FromHex::from_hex(code).unwrap();
|
|
let mut pwmod: parity_wasm::elements::Module = parity_wasm::deserialize_buffer(&code[..])
|
|
.expect("Could not deserialize Identity module");
|
|
|
|
/* Remove export, if it exists */
|
|
pwmod
|
|
.export_section_mut()
|
|
.expect("Could not get export section")
|
|
.entries_mut()
|
|
.retain(|ref export| export.field() != symbol);
|
|
|
|
/* Add a new function and give it the export name */
|
|
let wmod: parity_wasm::elements::Module = from_module(pwmod)
|
|
.function()
|
|
.signature()
|
|
.with_params(params)
|
|
.with_return_type(ret)
|
|
.build()
|
|
.body()
|
|
.with_instructions(Instructions::new(instr))
|
|
.build()
|
|
.build()
|
|
.export()
|
|
.field(symbol)
|
|
.internal()
|
|
.func(2)
|
|
.build()
|
|
.build();
|
|
parity_wasm::serialize(wmod).expect("Could not serialize buffer")
|
|
}
|
|
|
|
fn replace_global(code: &str, symbol: &str, value: i32) -> Vec<u8> {
|
|
/* Deserialize to parity_wasm format */
|
|
let code = FromHex::from_hex(code).unwrap();
|
|
let mut pwmod: parity_wasm::elements::Module = parity_wasm::deserialize_buffer(&code[..])
|
|
.expect("Could not deserialize Identity module");
|
|
|
|
/* Remove export, if it exists */
|
|
pwmod
|
|
.export_section_mut()
|
|
.expect("Could not get export section")
|
|
.entries_mut()
|
|
.retain(|ref export| export.field() != symbol);
|
|
|
|
/* Add a new function and give it the export name */
|
|
let wmod: parity_wasm::elements::Module = from_module(pwmod)
|
|
.global()
|
|
.value_type()
|
|
.i32()
|
|
.init_expr(Instruction::I32Const(value))
|
|
.build()
|
|
.export()
|
|
.field(symbol)
|
|
.internal()
|
|
.global(4)
|
|
.build()
|
|
.build();
|
|
parity_wasm::serialize(wmod).expect("Could not serialize buffer")
|
|
}
|
|
|
|
fn replace_global_type(code: &str, symbol: &str) -> Vec<u8> {
|
|
/* Deserialize to parity_wasm format */
|
|
let code = FromHex::from_hex(code).unwrap();
|
|
let mut pwmod: parity_wasm::elements::Module = parity_wasm::deserialize_buffer(&code[..])
|
|
.expect("Could not deserialize Identity module");
|
|
|
|
/* Remove export, if it exists */
|
|
pwmod
|
|
.export_section_mut()
|
|
.expect("Could not get export section")
|
|
.entries_mut()
|
|
.retain(|ref export| export.field() != symbol);
|
|
|
|
/* Add a new function and give it the export name */
|
|
let wmod: parity_wasm::elements::Module = from_module(pwmod)
|
|
.global()
|
|
.value_type()
|
|
.f32()
|
|
.init_expr(Instruction::F32Const(0))
|
|
.build()
|
|
.export()
|
|
.field(symbol)
|
|
.internal()
|
|
.global(4)
|
|
.build()
|
|
.build();
|
|
parity_wasm::serialize(wmod).expect("Could not serialize buffer")
|
|
}
|
|
|
|
#[test]
|
|
fn check_signatures() {
|
|
let h1 = WasmHelper::from_hex(WasmHelper::IDENTITY_WASM);
|
|
assert_eq!(h1.get_signature(), (1, 1));
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(
|
|
expected = "invalid bytecode: invalid bytecode: Invalid character 'i' at position 0"
|
|
)]
|
|
fn check_invalid_bytecode_fails() {
|
|
WasmHelper::from_hex("invalid bytecode");
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "Error decoding buffer: Validation(\"I/O Error: UnexpectedEof\")")]
|
|
fn check_truncated_bytecode_fails() {
|
|
WasmHelper::from_hex(&WasmHelper::IDENTITY_WASM[..20]);
|
|
}
|
|
|
|
#[test]
|
|
fn validate_exports() {
|
|
/* Test identity without the `solve` export */
|
|
let id = WasmHelper::from(remove_export(WasmHelper::IDENTITY_WASM, "solve"));
|
|
let input = vec![Bn128Field::from(1)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from(
|
|
"Error solving the problem: Function: Module doesn\'t have export solve",
|
|
))
|
|
);
|
|
|
|
/* Test identity, without the `get_inputs_off` export */
|
|
let id = WasmHelper::from(remove_export(WasmHelper::IDENTITY_WASM, "get_inputs_off"));
|
|
let input = vec![Bn128Field::from(1)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from(
|
|
"Error getting the input offset: Function: Module doesn\'t have export get_inputs_off",
|
|
))
|
|
);
|
|
|
|
/* Test identity, without the `min_inputs` export */
|
|
let id = WasmHelper::from(remove_export(WasmHelper::IDENTITY_WASM, "min_inputs"));
|
|
let input = vec![Bn128Field::from(1)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from(
|
|
"Could not find exported symbol `min_inputs` in module",
|
|
))
|
|
);
|
|
|
|
/* Test identity, without the `min_outputs` export */
|
|
let id = WasmHelper::from(remove_export(WasmHelper::IDENTITY_WASM, "min_outputs"));
|
|
let input = vec![Bn128Field::from(1)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from(
|
|
"Could not find exported symbol `min_outputs` in module",
|
|
))
|
|
);
|
|
|
|
/* Test identity, without the `field_size` export */
|
|
let id = WasmHelper::from(remove_export(WasmHelper::IDENTITY_WASM, "field_size"));
|
|
let input = vec![Bn128Field::from(1)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from(
|
|
"Could not find exported symbol `field_size` in module",
|
|
))
|
|
);
|
|
|
|
/* Test identity, without the `memory` export */
|
|
let id = WasmHelper::from(remove_export(WasmHelper::IDENTITY_WASM, "memory"));
|
|
let input = vec![Bn128Field::from(1)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from("Module didn\'t export its memory section"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn check_invalid_function_type() {
|
|
/* Test identity, with a different function return type */
|
|
let id = WasmHelper::from(replace_function(
|
|
WasmHelper::IDENTITY_WASM,
|
|
"get_inputs_off",
|
|
Vec::new(),
|
|
Some(ValueType::I64),
|
|
vec![Instruction::I64Const(0), Instruction::End],
|
|
));
|
|
let input = vec![Bn128Field::from(1)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from("`get_inputs_off` returned the wrong type"))
|
|
);
|
|
|
|
/* Test identity, with no return type for function */
|
|
let id = WasmHelper::from(replace_function(
|
|
WasmHelper::IDENTITY_WASM,
|
|
"get_inputs_off",
|
|
Vec::new(),
|
|
None,
|
|
vec![Instruction::Nop, Instruction::End],
|
|
));
|
|
let input = vec![Bn128Field::from(1)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from("`get_inputs_off` did not return any value"))
|
|
);
|
|
|
|
/* Test identity, with extra parameter for function */
|
|
let id = WasmHelper::from(replace_function(
|
|
WasmHelper::IDENTITY_WASM,
|
|
"get_inputs_off",
|
|
vec![ValueType::I64],
|
|
Some(ValueType::I32),
|
|
vec![Instruction::I32Const(0), Instruction::End],
|
|
));
|
|
let input = vec![Bn128Field::from(1)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from(
|
|
"Error getting the input offset: Trap: Trap { kind: UnexpectedSignature }",
|
|
))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn check_invalid_field_size() {
|
|
/* Test identity, with 1-byte filed size */
|
|
let id = WasmHelper::from(replace_global(WasmHelper::IDENTITY_WASM, "field_size", 1));
|
|
let input = vec![Bn128Field::from(65536)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from(
|
|
"Input #0 is stored on 3 bytes which is greater than the field size of 1",
|
|
))
|
|
);
|
|
|
|
/* Test identity, tweaked so that field_size is a f32 */
|
|
let id = WasmHelper::from(replace_global_type(WasmHelper::IDENTITY_WASM, "field_size"));
|
|
let input = vec![Bn128Field::from(65536)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from("Error converting `field_size` to i32"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn check_identity() {
|
|
let id = WasmHelper::from_hex(WasmHelper::IDENTITY_WASM);
|
|
let input = vec![Bn128Field::from(1)];
|
|
let outputs = id.execute(&input).expect("Identity call failed");
|
|
assert_eq!(outputs, input);
|
|
|
|
let id = WasmHelper::from_hex(WasmHelper::IDENTITY_WASM);
|
|
let input = vec![Bn128Field::from(0)];
|
|
let outputs = id.execute(&input).expect("Identity call failed");
|
|
assert_eq!(outputs, input);
|
|
}
|
|
|
|
#[test]
|
|
fn check_identity_3_bytes() {
|
|
let id = WasmHelper::from_hex(WasmHelper::IDENTITY_WASM);
|
|
let input = vec![Bn128Field::from(16777216)];
|
|
let outputs = id.execute(&input).expect("Identity call failed");
|
|
assert_eq!(outputs, input);
|
|
}
|
|
|
|
#[test]
|
|
fn check_identity_multiple_calls() {
|
|
let id = WasmHelper::from_hex(WasmHelper::IDENTITY_WASM);
|
|
let input = vec![Bn128Field::from(16777216)];
|
|
for _i in 0..10 {
|
|
let outputs = id.execute(&input).expect("Identity call failed");
|
|
assert_eq!(outputs, input);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn check_invalid_arg_number() {
|
|
let id = WasmHelper::from_hex(WasmHelper::IDENTITY_WASM);
|
|
let input = vec![Bn128Field::from(1)];
|
|
let outputs = id.execute(&input).expect("Identity call failed");
|
|
assert_eq!(outputs, input);
|
|
}
|
|
|
|
#[test]
|
|
fn check_memory_boundaries() {
|
|
// Check that input writes are boundary-checked: same as identity, but
|
|
// get_inputs_off returns an OOB offset.
|
|
let id = WasmHelper::from(replace_function(
|
|
WasmHelper::IDENTITY_WASM,
|
|
"get_inputs_off",
|
|
Vec::new(),
|
|
Some(ValueType::I32),
|
|
vec![Instruction::I32Const(65536), Instruction::End],
|
|
));
|
|
let input = vec![Bn128Field::from(65536)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from("Could not write at memory address 65536"))
|
|
);
|
|
|
|
/* Check that output writes are boundary-checked */
|
|
// Check that input writes are boundary-checked: same as identity, but
|
|
// solve returns an OOB offset.
|
|
let id = WasmHelper::from(replace_function(
|
|
WasmHelper::IDENTITY_WASM,
|
|
"solve",
|
|
Vec::new(),
|
|
Some(ValueType::I32),
|
|
vec![Instruction::I32Const(65536), Instruction::End],
|
|
));
|
|
let input = vec![Bn128Field::from(65536)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(
|
|
outputs,
|
|
Err(String::from(
|
|
"Could not retrieve the output offset: Memory: trying to access region [65536..65568] in memory [0..64]",
|
|
))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn check_negative_output_value() {
|
|
/* Same as identity, but `solve` returns -1 */
|
|
let id = WasmHelper::from(replace_function(
|
|
WasmHelper::IDENTITY_WASM,
|
|
"solve",
|
|
Vec::new(),
|
|
Some(ValueType::I32),
|
|
vec![Instruction::I32Const(-1), Instruction::End],
|
|
));
|
|
let input = vec![Bn128Field::from(1)];
|
|
let outputs = id.execute(&input);
|
|
assert_eq!(outputs, Err(String::from("`solve` returned error code -1")));
|
|
}
|
|
}
|