Skip to content

Commit

Permalink
feat(acvm_js): Execute program (#4694)
Browse files Browse the repository at this point in the history
# Description

## Problem\*

Resolves #4645 

## Summary\*

In order to have a recursive `execute_circuit` function we now have a
recursive async call. This requires boxing a future and having to making
`execute_circuit` immutable in order to attach the final main witness to
the witness stack. In our normal execution flow we could move the
`ProgramExecutor` after our recursive async call to execute_circuit, we
now need a lifetime on `self`.

I also switched all `execute_circuit` methods in ACVM JS to use
`execute_program` under the hood so that we do not have a breaking
change yet but can still test `execute_program` using all of our already
existing tests.

I then added a couple extra acvm js tests for multiple acir calls and
full witness stack (de)/compression.

## Additional Context



## Documentation\*

Check one:
- [ ] No documentation needed.
- [ ] Documentation included in this PR.
- [X] **[For Experimental Features]** Documentation to be submitted in a
separate PR.

# PR Checklist\*

- [X] I have tested the changes locally.
- [X] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.

---------

Co-authored-by: Tom French <tom@tomfren.ch>
  • Loading branch information
vezenovm and TomAFrench authored Apr 3, 2024
1 parent 463fb77 commit 386f6d0
Show file tree
Hide file tree
Showing 10 changed files with 518 additions and 78 deletions.
36 changes: 22 additions & 14 deletions acvm-repo/acir/src/circuit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,25 +216,33 @@ impl std::fmt::Display for Circuit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "current witness index : {}", self.current_witness_index)?;

let write_public_inputs = |f: &mut std::fmt::Formatter<'_>,
public_inputs: &PublicInputs|
-> Result<(), std::fmt::Error> {
write!(f, "[")?;
let public_input_indices = public_inputs.indices();
for (index, public_input) in public_input_indices.iter().enumerate() {
write!(f, "{public_input}")?;
if index != public_input_indices.len() - 1 {
write!(f, ", ")?;
let write_witness_indices =
|f: &mut std::fmt::Formatter<'_>, indices: &[u32]| -> Result<(), std::fmt::Error> {
write!(f, "[")?;
for (index, witness_index) in indices.iter().enumerate() {
write!(f, "{witness_index}")?;
if index != indices.len() - 1 {
write!(f, ", ")?;
}
}
}
writeln!(f, "]")
};
writeln!(f, "]")
};

write!(f, "private parameters indices : ")?;
write_witness_indices(
f,
&self
.private_parameters
.iter()
.map(|witness| witness.witness_index())
.collect::<Vec<_>>(),
)?;

write!(f, "public parameters indices : ")?;
write_public_inputs(f, &self.public_parameters)?;
write_witness_indices(f, &self.public_parameters.indices())?;

write!(f, "return value indices : ")?;
write_public_inputs(f, &self.return_values)?;
write_witness_indices(f, &self.return_values.indices())?;

for opcode in &self.opcodes {
writeln!(f, "{opcode}")?;
Expand Down
6 changes: 5 additions & 1 deletion acvm-repo/acir/src/native_types/witness_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub struct WitnessStackError(#[from] SerializationError);
/// An ordered set of witness maps for separate circuits
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
pub struct WitnessStack {
pub stack: Vec<StackItem>,
stack: Vec<StackItem>,
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
Expand All @@ -37,6 +37,10 @@ impl WitnessStack {
self.stack.push(StackItem { index, witness });
}

pub fn pop(&mut self) -> Option<StackItem> {
self.stack.pop()
}

pub fn peek(&self) -> Option<&StackItem> {
self.stack.last()
}
Expand Down
93 changes: 93 additions & 0 deletions acvm-repo/acir/tests/test_program_serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,96 @@ fn memory_op_circuit() {

assert_eq!(bytes, expected_serialization)
}

#[test]
fn nested_acir_call_circuit() {
// Circuit for the following program:
// fn main(x: Field, y: pub Field) {
// let z = nested_call(x, y);
// let z2 = nested_call(x, y);
// assert(z == z2);
// }
// #[fold]
// fn nested_call(x: Field, y: Field) -> Field {
// inner_call(x + 2, y)
// }
// #[fold]
// fn inner_call(x: Field, y: Field) -> Field {
// assert(x == y);
// x
// }
let nested_call =
Opcode::Call { id: 1, inputs: vec![Witness(0), Witness(1)], outputs: vec![Witness(2)] };
let nested_call_two =
Opcode::Call { id: 1, inputs: vec![Witness(0), Witness(1)], outputs: vec![Witness(3)] };

let assert_nested_call_results = Opcode::AssertZero(Expression {
mul_terms: Vec::new(),
linear_combinations: vec![
(FieldElement::one(), Witness(2)),
(-FieldElement::one(), Witness(3)),
],
q_c: FieldElement::zero(),
});

let main = Circuit {
current_witness_index: 3,
private_parameters: BTreeSet::from([Witness(0)]),
public_parameters: PublicInputs([Witness(1)].into()),
opcodes: vec![nested_call, nested_call_two, assert_nested_call_results],
..Circuit::default()
};

let call_parameter_addition = Opcode::AssertZero(Expression {
mul_terms: Vec::new(),
linear_combinations: vec![
(FieldElement::one(), Witness(0)),
(-FieldElement::one(), Witness(2)),
],
q_c: FieldElement::one() + FieldElement::one(),
});
let call =
Opcode::Call { id: 2, inputs: vec![Witness(2), Witness(1)], outputs: vec![Witness(3)] };

let nested_call = Circuit {
current_witness_index: 3,
private_parameters: BTreeSet::from([Witness(0), Witness(1)]),
return_values: PublicInputs([Witness(3)].into()),
opcodes: vec![call_parameter_addition, call],
..Circuit::default()
};

let assert_param_equality = Opcode::AssertZero(Expression {
mul_terms: Vec::new(),
linear_combinations: vec![
(FieldElement::one(), Witness(0)),
(-FieldElement::one(), Witness(1)),
],
q_c: FieldElement::zero(),
});

let inner_call = Circuit {
current_witness_index: 1,
private_parameters: BTreeSet::from([Witness(0), Witness(1)]),
return_values: PublicInputs([Witness(0)].into()),
opcodes: vec![assert_param_equality],
..Circuit::default()
};

let program = Program { functions: vec![main, nested_call, inner_call] };

let bytes = Program::serialize_program(&program);

let expected_serialization: Vec<u8> = vec![
31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 205, 146, 97, 10, 195, 32, 12, 133, 163, 66, 207, 147,
24, 109, 227, 191, 93, 101, 50, 123, 255, 35, 172, 99, 25, 83, 17, 250, 99, 14, 250, 224,
97, 144, 16, 146, 143, 231, 224, 45, 167, 126, 105, 57, 108, 14, 91, 248, 202, 168, 65,
255, 207, 122, 28, 180, 250, 244, 221, 244, 197, 223, 68, 182, 154, 197, 184, 134, 80, 54,
95, 136, 233, 142, 62, 101, 137, 24, 98, 94, 133, 132, 162, 196, 135, 23, 230, 34, 65, 182,
148, 211, 134, 137, 2, 23, 218, 99, 226, 93, 135, 185, 121, 123, 33, 84, 12, 234, 218, 192,
64, 174, 3, 248, 47, 88, 48, 17, 150, 157, 183, 151, 95, 244, 86, 91, 221, 61, 10, 81, 31,
178, 190, 110, 194, 102, 96, 76, 251, 202, 80, 13, 204, 77, 224, 25, 176, 70, 79, 197, 128,
18, 64, 3, 4, 0, 0,
];
assert_eq!(bytes, expected_serialization);
}
40 changes: 36 additions & 4 deletions acvm-repo/acvm_js/src/compression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ use acvm::acir::native_types::{WitnessMap, WitnessStack};
use js_sys::JsString;
use wasm_bindgen::prelude::wasm_bindgen;

use crate::JsWitnessMap;
use crate::{JsWitnessMap, JsWitnessStack};

/// Compresses a `WitnessMap` into the binary format outputted by Nargo.
///
/// @param {Uint8Array} compressed_witness - A witness map.
/// @returns {WitnessMap} A compressed witness map
/// @param {WitnessMap} witness_map - A witness map.
/// @returns {Uint8Array} A compressed witness map
#[wasm_bindgen(js_name = compressWitness, skip_jsdoc)]
pub fn compress_witness(witness_map: JsWitnessMap) -> Result<Vec<u8>, JsString> {
console_error_panic_hook::set_once();
Expand All @@ -21,15 +21,47 @@ pub fn compress_witness(witness_map: JsWitnessMap) -> Result<Vec<u8>, JsString>
}

/// Decompresses a compressed witness as outputted by Nargo into a `WitnessMap`.
/// This should be used to only fetch the witness map for the main function.
///
/// @param {Uint8Array} compressed_witness - A compressed witness.
/// @returns {WitnessMap} The decompressed witness map.
#[wasm_bindgen(js_name = decompressWitness, skip_jsdoc)]
pub fn decompress_witness(compressed_witness: Vec<u8>) -> Result<JsWitnessMap, JsString> {
console_error_panic_hook::set_once();

let mut witness_stack =
WitnessStack::try_from(compressed_witness.as_slice()).map_err(|err| err.to_string())?;

let witness =
witness_stack.pop().expect("Should have at least one witness on the stack").witness;
Ok(witness.into())
}

/// Compresses a `WitnessStack` into the binary format outputted by Nargo.
///
/// @param {WitnessStack} witness_stack - A witness stack.
/// @returns {Uint8Array} A compressed witness stack
#[wasm_bindgen(js_name = compressWitnessStack, skip_jsdoc)]
pub fn compress_witness_stack(witness_stack: JsWitnessStack) -> Result<Vec<u8>, JsString> {
console_error_panic_hook::set_once();

let witness_stack = WitnessStack::from(witness_stack);
let compressed_witness_stack: Vec<u8> =
Vec::<u8>::try_from(witness_stack).map_err(|err| err.to_string())?;

Ok(compressed_witness_stack)
}

/// Decompresses a compressed witness stack as outputted by Nargo into a `WitnessStack`.
///
/// @param {Uint8Array} compressed_witness - A compressed witness.
/// @returns {WitnessStack} The decompressed witness stack.
#[wasm_bindgen(js_name = decompressWitnessStack, skip_jsdoc)]
pub fn decompress_witness_stack(compressed_witness: Vec<u8>) -> Result<JsWitnessStack, JsString> {
console_error_panic_hook::set_once();

let witness_stack =
WitnessStack::try_from(compressed_witness.as_slice()).map_err(|err| err.to_string())?;

Ok(witness_stack.stack[0].witness.clone().into())
Ok(witness_stack.into())
}
Loading

0 comments on commit 386f6d0

Please sign in to comment.