Skip to content

Commit

Permalink
feat: add support for oracle opcodes
Browse files Browse the repository at this point in the history
  • Loading branch information
TomAFrench committed May 15, 2023
1 parent a05430e commit f06d415
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 51 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ acvm = "0.10.3"
iter-extended = { git = "https://github.com/noir-lang/noir", rev = "7f6dede414c46790545b1994713d1976c5623711"}
noirc_abi = { git = "https://github.com/noir-lang/noir", rev = "7f6dede414c46790545b1994713d1976c5623711"}
wasm-bindgen = { version = "0.2.85", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4.34"
serde = { version = "1.0.136", features = ["derive"] }
log = "0.4.17"
wasm-logger = "0.2.0"
Expand Down
123 changes: 114 additions & 9 deletions src/execute.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
use acvm::{
acir::{
circuit::{opcodes::BlackBoxFuncCall, Circuit},
circuit::{
opcodes::{BlackBoxFuncCall, OracleData},
Circuit,
},
native_types::Witness,
BlackBoxFunc,
},
pwg::{block::Blocks, hash, logic, range, signature},
pwg::{block::Blocks, directives::insert_witness, hash, logic, range, signature},
FieldElement, OpcodeResolution, OpcodeResolutionError, PartialWitnessGenerator,
PartialWitnessGeneratorStatus,
};
use std::collections::BTreeMap;

use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};

use crate::js_transforms::{js_map_to_witness_map, witness_map_to_js_map};
use crate::js_transforms::{
field_element_to_js_string, js_map_to_witness_map, js_value_to_field_element,
witness_map_to_js_map,
};

struct SimulatedBackend;

Expand Down Expand Up @@ -48,16 +54,115 @@ impl PartialWitnessGenerator for SimulatedBackend {
}

#[wasm_bindgen]
pub fn execute_circuit(circuit: Vec<u8>, initial_witness: js_sys::Map) -> js_sys::Map {
pub async fn execute_circuit(
circuit: Vec<u8>,
initial_witness: js_sys::Map,
oracle_resolver: js_sys::Function,
) -> js_sys::Map {
console_error_panic_hook::set_once();
let circuit: Circuit = Circuit::read(&*circuit).expect("Failed to deserialize circuit");
let mut witness_map = js_map_to_witness_map(initial_witness);

let mut blocks = Blocks::default();
let solver_status = SimulatedBackend
.solve(&mut witness_map, &mut blocks, circuit.opcodes)
.expect("Threw error while executing circuit");
assert_eq!(solver_status, PartialWitnessGeneratorStatus::Solved);
let mut opcodes = circuit.opcodes;

let mut i = 0;
loop {
let solver_status = SimulatedBackend
.solve(&mut witness_map, &mut blocks, opcodes)
.expect("Threw error while executing circuit");
i += 1;
if i > 4 {
panic!("too many loops");
}

match solver_status {
PartialWitnessGeneratorStatus::Solved => break,
PartialWitnessGeneratorStatus::RequiresOracleData {
required_oracle_data,
unsolved_opcodes,
} => {
// Perform all oracle queries
let oracle_call_futures: Vec<_> = required_oracle_data
.into_iter()
.map(|oracle_call| resolve_oracle(&oracle_resolver, oracle_call))
.collect();

// Insert results into the witness map
for oracle_call_future in oracle_call_futures {
let resolved_oracle_call: OracleData = oracle_call_future.await.unwrap();
for (i, witness_index) in resolved_oracle_call.outputs.iter().enumerate() {
insert_witness(
*witness_index,
resolved_oracle_call.output_values[i],
&mut witness_map,
)
.expect("inserted inconsistent witness value");
}
}

// Use new opcodes as returned by ACVM.
opcodes = unsolved_opcodes;
}
}
}

witness_map_to_js_map(witness_map)
}

async fn resolve_oracle(
oracle_resolver: &js_sys::Function,
mut unresolved_oracle_call: OracleData,
) -> Result<OracleData, String> {
// Prepare to call
let name = JsValue::from(unresolved_oracle_call.name.clone());
assert_eq!(unresolved_oracle_call.inputs.len(), unresolved_oracle_call.input_values.len());
let inputs = js_sys::Array::default();
for input_value in &unresolved_oracle_call.input_values {
let hex_js_string = field_element_to_js_string(input_value);
inputs.push(&hex_js_string);
}

// Call and await
let this = JsValue::null();
let ret_js_val = oracle_resolver
.call2(&this, &name, &inputs)
.map_err(|err| format!("Error calling oracle_resolver: {}", format_js_err(err)))?;
let ret_js_prom: js_sys::Promise = ret_js_val.into();
let ret_future: wasm_bindgen_futures::JsFuture = ret_js_prom.into();
let js_resolution = ret_future
.await
.map_err(|err| format!("Error awaiting oracle_resolver: {}", format_js_err(err)))?;

// Check that result conforms to expected shape.
if !js_resolution.is_array() {
return Err("oracle_resolver must return a Promise<string[]>".into());
}
let js_arr = js_sys::Array::from(&js_resolution);
let output_len = js_arr.length() as usize;
let expected_output_len = unresolved_oracle_call.outputs.len();
if output_len != expected_output_len {
return Err(format!(
"Expected output from oracle '{}' of {} elements, but instead received {}",
unresolved_oracle_call.name, expected_output_len, output_len
));
}

// Insert result into oracle data.
for elem in js_arr.iter() {
if !elem.is_string() {
return Err("Non-string element in oracle_resolver return".into());
}
unresolved_oracle_call.output_values.push(js_value_to_field_element(elem)?)
}
let resolved_oracle_call = unresolved_oracle_call;

Ok(resolved_oracle_call)
}

fn format_js_err(err: JsValue) -> String {
match err.as_string() {
Some(str) => str,
None => "Unknown".to_owned(),
}
}
42 changes: 21 additions & 21 deletions src/js_transforms.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
use acvm::{acir::native_types::Witness, FieldElement};
use js_sys::JsString;
use std::collections::BTreeMap;
use wasm_bindgen::JsValue;

pub(crate) fn js_value_to_field_element(js_value: JsValue) -> Result<FieldElement, JsString> {
let hex_str =
js_value.as_string().ok_or_else(|| "failed to parse field element from non-string")?;

FieldElement::from_hex(&hex_str)
.ok_or_else(|| format!("Invalid hex string: '{}'", hex_str).into())
}

pub(crate) fn field_element_to_js_string(field_element: &FieldElement) -> JsString {
// This currently maps `0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000000`
// to the bigint `-1n`. This fails when converting back to a `FieldElement`.
// js_sys::BigInt::from_str(&value.to_hex()).unwrap()

format!("0x{}", field_element.to_hex()).into()
}

pub(crate) fn js_map_to_witness_map(js_map: js_sys::Map) -> BTreeMap<Witness, FieldElement> {
let mut witness_map: BTreeMap<Witness, FieldElement> = BTreeMap::new();
js_map.for_each(&mut |value, key| {
let witness_index = Witness(key.as_string().unwrap().parse::<u32>().unwrap());
// let witness_value: String = js_sys::BigInt::from(value)
// .to_string(16)
// .expect("Could not get value of witness")
// .into();
let witness_value: String = value.as_string().expect("Could not get value of witness");

let witness_value =
FieldElement::from_hex(&witness_value).expect("could not convert bigint to fields");
let witness_index = Witness(key.as_f64().unwrap() as u32);
let witness_value = js_value_to_field_element(value).unwrap();
witness_map.insert(witness_index, witness_value);
});
witness_map
Expand All @@ -21,18 +32,7 @@ pub(crate) fn js_map_to_witness_map(js_map: js_sys::Map) -> BTreeMap<Witness, Fi
pub(crate) fn witness_map_to_js_map(witness_map: BTreeMap<Witness, FieldElement>) -> js_sys::Map {
let js_map = js_sys::Map::new();
for (key, value) in witness_map {
// This currently maps `0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000000`
// to the bigint `-1n`. This fails when converting back to a `FieldElement`.

// let witness_bigint = js_sys::BigInt::from_str(&value.to_hex())
// .expect("could not convert field to bigint");

let witness_bigint = wasm_bindgen::JsValue::from_str(&value.to_hex());

js_map.set(
&wasm_bindgen::JsValue::from_str(&key.witness_index().to_string()),
&witness_bigint,
);
js_map.set(&js_sys::Number::from(key.witness_index()), &field_element_to_js_string(&value));
}
js_map
}
Expand Down
2 changes: 1 addition & 1 deletion test/abi_encode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ test('recovers original inputs when abi encoding and decoding', () => {
};
const initial_witness: Map<string, string> = abi_encode(abi, inputs, null);
const decoded_inputs: {inputs: Record<string, any>, return_value: any} = abi_decode(abi, initial_witness);

expect(BigInt(decoded_inputs.inputs.foo)).toBe(BigInt(inputs.foo))
expect(BigInt(decoded_inputs.inputs.bar[0])).toBe(BigInt(inputs.bar[0]))
expect(BigInt(decoded_inputs.inputs.bar[1])).toBe(BigInt(inputs.bar[1]))
Expand Down
91 changes: 71 additions & 20 deletions test/execute_circuit.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
import { expect, test } from "@jest/globals"
import { abi_encode, abi_decode, execute_circuit } from "../pkg/"

// Noir program which enforces that x != y and returns x + y.
const abi = {
parameters:[
{ name:"x", type: { kind: "field" }, visibility:"private" },
{ name:"y", type: { kind: "field" }, visibility:"public" }
],
param_witnesses:
{
x: [1],
y: [2]
},
return_type: { kind: "field" },
return_witnesses: [6]
};
const bytecode = Uint8Array.from([0,0,0,0,7,0,0,0,1,0,0,0,2,0,0,0,1,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,48,100,78,114,225,49,160,41,184,80,69,182,129,129,88,93,40,51,232,72,121,185,112,145,67,225,245,147,240,0,0,0,2,0,0,0,48,100,78,114,225,49,160,41,184,80,69,182,129,129,88,93,40,51,232,72,121,185,112,145,67,225,245,147,240,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,3,0,0,0,4,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,0,0,4,0,0,0,48,100,78,114,225,49,160,41,184,80,69,182,129,129,88,93,40,51,232,72,121,185,112,145,67,225,245,147,240,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,0,0,5,0,0,0,48,100,78,114,225,49,160,41,184,80,69,182,129,129,88,93,40,51,232,72,121,185,112,145,67,225,245,147,240,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,48,100,78,114,225,49,160,41,184,80,69,182,129,129,88,93,40,51,232,72,121,185,112,145,67,225,245,147,240,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,0,0,48,100,78,114,225,49,160,41,184,80,69,182,129,129,88,93,40,51,232,72,121,185,112,145,67,225,245,147,240,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);

test('recovers original inputs when abi encoding and decoding', () => {

test('successfully executes circuit and extracts return value', async () => {
// Noir program which enforces that x != y and returns x + y.
const abi = {
parameters:[
{ name:"x", type: { kind: "field" }, visibility:"private" },
{ name:"y", type: { kind: "field" }, visibility:"public" }
],
param_witnesses:
{
x: [1],
y: [2]
},
return_type: { kind: "field" },
return_witnesses: [6]
};
const bytecode = Uint8Array.from([0,0,0,0,7,0,0,0,1,0,0,0,2,0,0,0,1,0,0,0,6,0,0,0,6,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,48,100,78,114,225,49,160,41,184,80,69,182,129,129,88,93,40,51,232,72,121,185,112,145,67,225,245,147,240,0,0,0,2,0,0,0,48,100,78,114,225,49,160,41,184,80,69,182,129,129,88,93,40,51,232,72,121,185,112,145,67,225,245,147,240,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,3,0,0,0,4,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,0,0,4,0,0,0,48,100,78,114,225,49,160,41,184,80,69,182,129,129,88,93,40,51,232,72,121,185,112,145,67,225,245,147,240,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,0,0,5,0,0,0,48,100,78,114,225,49,160,41,184,80,69,182,129,129,88,93,40,51,232,72,121,185,112,145,67,225,245,147,240,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,48,100,78,114,225,49,160,41,184,80,69,182,129,129,88,93,40,51,232,72,121,185,112,145,67,225,245,147,240,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,0,0,48,100,78,114,225,49,160,41,184,80,69,182,129,129,88,93,40,51,232,72,121,185,112,145,67,225,245,147,240,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);


const inputs = {
x: "1",
y: "2"
};
const return_witness: string = abi.return_witnesses[0].toString()
const return_witness: number = abi.return_witnesses[0]

const initial_witness: Map<string, string> = abi_encode(abi, inputs, null);
const solved_witness: Map<string, string> = execute_circuit(bytecode, initial_witness)
const initial_witness: Map<number, string> = abi_encode(abi, inputs, null);
const solved_witness: Map<number, string> = await execute_circuit(bytecode, initial_witness, () => {throw Error("unexpected oracle")})

// Solved witness should be consistent with initial witness
initial_witness.forEach((value, key) => {
Expand All @@ -40,3 +42,52 @@ test('recovers original inputs when abi encoding and decoding', () => {
expect(BigInt(decoded_inputs.return_value)).toBe(3n)
});


test('successfully processes oracle opcodes', async () => {
// We use a handwritten circuit which uses an oracle to calculate the sum of witnesses 1 and 2
// and stores the result in witness 3. This is then enforced by an arithmetic opcode to check the result is correct.

// let oracle = OracleData {
// name: "example_oracle".to_owned(),
// inputs: vec![Witness(1).into(), Witness(2).into()],
// input_values: Vec::new(),
// outputs: vec![Witness(3)],
// output_values: Vec::new(),
// };
// let check: Expression = Expression {
// mul_terms: Vec::new(),
// linear_combinations: vec![
// (FieldElement::one(), Witness(1)),
// (FieldElement::one(), Witness(2)),
// (-FieldElement::one(), Witness(3)),
// ],
// q_c: FieldElement::zero(),
// };

// let circuit = Circuit {
// current_witness_index: 4,
// opcodes: vec![Opcode::Oracle(oracle), Opcode::Arithmetic(check)],
// public_parameters: PublicInputs::default(),
// return_values: PublicInputs::default(),
// };
let oracle_bytecode = new Uint8Array([0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 6, 14, 0, 0, 0, 101, 120, 97, 109, 112, 108, 101, 95, 111, 114, 97, 99, 108, 101, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 48, 100, 78, 114, 225, 49, 160, 41, 184, 80, 69, 182, 129, 129, 88, 93, 40, 51, 232, 72, 121, 185, 112, 145, 67, 225, 245, 147, 240, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);

const initial_witness: Map<number, string> = new Map()
initial_witness.set(1, "0x0000000000000000000000000000000000000000000000000000000000000001");
initial_witness.set(2, "0x0000000000000000000000000000000000000000000000000000000000000001");

const solved_witness: Map<number, string> = await execute_circuit(oracle_bytecode, initial_witness, async (name: string, inputs: string[]) => {
// We cannot use jest matchers here (or write to a variable in the outside scope) so cannot test that
// the values for `name` and `inputs` are correct, we can `console.log` them however.
// console.log(name)
// console.log(inputs)

// Witness(1) + Witness(2) = 1 + 1 = 2
return ["0x02"]
})

// If incorrect value is written into circuit then execution should halt due to unsatisfied constraint in
// arithmetic opcode. Nevertheless, check that returned value was inserted correctly.
expect(solved_witness.get(3) as string).toBe("0x0000000000000000000000000000000000000000000000000000000000000002")
});

0 comments on commit f06d415

Please sign in to comment.