Skip to content

Commit 413a4e0

Browse files
authored
feat(simulator): Fetch return values at circuit execution (#5642)
We were deserializing a `Program` twice in the simulator. Once to execute the circuit and again to fetch the return witness. Every call to `executeCircuitWithBlackBoxSolver` is followed by a call to `getReturnWitness` in the simulator. We should just pass the return witness right away as to not cross the WASM boundary a second time and as to avoid a second deserialization. I settled on a new ACVM method rather than using abi decoding as the return witnesses are stripped from the ABI. We can have a method that returns both the fully solved witness and the return witness based upon the circuit. This allows us to both avoid storing duplicate return witness information and an unnecessary deserialization.
1 parent 25cc70d commit 413a4e0

9 files changed

Lines changed: 125 additions & 29 deletions

File tree

noir/noir-repo/acvm-repo/acvm_js/src/execute.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ use wasm_bindgen::prelude::wasm_bindgen;
1313

1414
use crate::{
1515
foreign_call::{resolve_brillig, ForeignCallHandler},
16-
JsExecutionError, JsWitnessMap, JsWitnessStack,
16+
public_witness::extract_indices,
17+
JsExecutionError, JsSolvedAndReturnWitness, JsWitnessMap, JsWitnessStack,
1718
};
1819

1920
#[wasm_bindgen]
@@ -58,6 +59,44 @@ pub async fn execute_circuit(
5859
Ok(witness_map.into())
5960
}
6061

62+
/// Executes an ACIR circuit to generate the solved witness from the initial witness.
63+
/// This method also extracts the public return values from the solved witness into its own return witness.
64+
///
65+
/// @param {&WasmBlackBoxFunctionSolver} solver - A black box solver.
66+
/// @param {Uint8Array} circuit - A serialized representation of an ACIR circuit
67+
/// @param {WitnessMap} initial_witness - The initial witness map defining all of the inputs to `circuit`..
68+
/// @param {ForeignCallHandler} foreign_call_handler - A callback to process any foreign calls from the circuit.
69+
/// @returns {SolvedAndReturnWitness} The solved witness calculated by executing the circuit on the provided inputs, as well as the return witness indices as specified by the circuit.
70+
#[wasm_bindgen(js_name = executeCircuitWithReturnWitness, skip_jsdoc)]
71+
pub async fn execute_circuit_with_return_witness(
72+
solver: &WasmBlackBoxFunctionSolver,
73+
program: Vec<u8>,
74+
initial_witness: JsWitnessMap,
75+
foreign_call_handler: ForeignCallHandler,
76+
) -> Result<JsSolvedAndReturnWitness, Error> {
77+
console_error_panic_hook::set_once();
78+
79+
let program: Program = Program::deserialize_program(&program)
80+
.map_err(|_| JsExecutionError::new("Failed to deserialize circuit. This is likely due to differing serialization formats between ACVM_JS and your compiler".to_string(), None))?;
81+
82+
let mut witness_stack = execute_program_with_native_program_and_return(
83+
solver,
84+
&program,
85+
initial_witness,
86+
&foreign_call_handler,
87+
)
88+
.await?;
89+
let solved_witness =
90+
witness_stack.pop().expect("Should have at least one witness on the stack").witness;
91+
92+
let main_circuit = &program.functions[0];
93+
let return_witness =
94+
extract_indices(&solved_witness, main_circuit.return_values.0.iter().copied().collect())
95+
.map_err(|err| JsExecutionError::new(err, None))?;
96+
97+
Ok((solved_witness, return_witness).into())
98+
}
99+
61100
/// Executes an ACIR circuit to generate the solved witness from the initial witness.
62101
///
63102
/// @param {&WasmBlackBoxFunctionSolver} solver - A black box solver.
@@ -127,6 +166,21 @@ async fn execute_program_with_native_type_return(
127166
let program: Program = Program::deserialize_program(&program)
128167
.map_err(|_| JsExecutionError::new("Failed to deserialize circuit. This is likely due to differing serialization formats between ACVM_JS and your compiler".to_string(), None))?;
129168

169+
execute_program_with_native_program_and_return(
170+
solver,
171+
&program,
172+
initial_witness,
173+
foreign_call_executor,
174+
)
175+
.await
176+
}
177+
178+
async fn execute_program_with_native_program_and_return(
179+
solver: &WasmBlackBoxFunctionSolver,
180+
program: &Program,
181+
initial_witness: JsWitnessMap,
182+
foreign_call_executor: &ForeignCallHandler,
183+
) -> Result<WitnessStack, Error> {
130184
let executor = ProgramExecutor::new(&program.functions, &solver.0, foreign_call_executor);
131185
let witness_stack = executor.execute(initial_witness.into()).await?;
132186

noir/noir-repo/acvm-repo/acvm_js/src/js_witness_map.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@ use acvm::{
22
acir::native_types::{Witness, WitnessMap},
33
FieldElement,
44
};
5-
use js_sys::{JsString, Map};
5+
use js_sys::{JsString, Map, Object};
66
use wasm_bindgen::prelude::{wasm_bindgen, JsValue};
77

88
#[wasm_bindgen(typescript_custom_section)]
99
const WITNESS_MAP: &'static str = r#"
1010
// Map from witness index to hex string value of witness.
1111
export type WitnessMap = Map<number, string>;
12+
13+
/**
14+
* An execution result containing two witnesses.
15+
* 1. The full solved witness of the execution.
16+
* 2. The return witness which contains the given public return values within the full witness.
17+
*/
18+
export type SolvedAndReturnWitness = {
19+
solvedWitness: WitnessMap;
20+
returnWitness: WitnessMap;
21+
}
1222
"#;
1323

1424
// WitnessMap
@@ -21,6 +31,12 @@ extern "C" {
2131
#[wasm_bindgen(constructor, js_class = "Map")]
2232
pub fn new() -> JsWitnessMap;
2333

34+
#[wasm_bindgen(extends = Object, js_name = "SolvedAndReturnWitness", typescript_type = "SolvedAndReturnWitness")]
35+
#[derive(Clone, Debug, PartialEq, Eq)]
36+
pub type JsSolvedAndReturnWitness;
37+
38+
#[wasm_bindgen(constructor, js_class = "Object")]
39+
pub fn new() -> JsSolvedAndReturnWitness;
2440
}
2541

2642
impl Default for JsWitnessMap {
@@ -29,6 +45,12 @@ impl Default for JsWitnessMap {
2945
}
3046
}
3147

48+
impl Default for JsSolvedAndReturnWitness {
49+
fn default() -> Self {
50+
Self::new()
51+
}
52+
}
53+
3254
impl From<WitnessMap> for JsWitnessMap {
3355
fn from(witness_map: WitnessMap) -> Self {
3456
let js_map = JsWitnessMap::new();
@@ -54,6 +76,20 @@ impl From<JsWitnessMap> for WitnessMap {
5476
}
5577
}
5678

79+
impl From<(WitnessMap, WitnessMap)> for JsSolvedAndReturnWitness {
80+
fn from(witness_maps: (WitnessMap, WitnessMap)) -> Self {
81+
let js_solved_witness = JsWitnessMap::from(witness_maps.0);
82+
let js_return_witness = JsWitnessMap::from(witness_maps.1);
83+
84+
let entry_map = Map::new();
85+
entry_map.set(&JsValue::from_str("solvedWitness"), &js_solved_witness);
86+
entry_map.set(&JsValue::from_str("returnWitness"), &js_return_witness);
87+
88+
let solved_and_return_witness = Object::from_entries(&entry_map).unwrap();
89+
JsSolvedAndReturnWitness { obj: solved_and_return_witness }
90+
}
91+
}
92+
5793
pub(crate) fn js_value_to_field_element(js_value: JsValue) -> Result<FieldElement, JsString> {
5894
let hex_str = js_value.as_string().ok_or("failed to parse field element from non-string")?;
5995

noir/noir-repo/acvm-repo/acvm_js/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ pub use compression::{
2222
};
2323
pub use execute::{
2424
create_black_box_solver, execute_circuit, execute_circuit_with_black_box_solver,
25-
execute_program, execute_program_with_black_box_solver,
25+
execute_circuit_with_return_witness, execute_program, execute_program_with_black_box_solver,
2626
};
2727
pub use js_execution_error::JsExecutionError;
28+
pub use js_witness_map::JsSolvedAndReturnWitness;
2829
pub use js_witness_map::JsWitnessMap;
2930
pub use js_witness_stack::JsWitnessStack;
3031
pub use logging::init_log_level;

noir/noir-repo/acvm-repo/acvm_js/src/public_witness.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use wasm_bindgen::prelude::wasm_bindgen;
77

88
use crate::JsWitnessMap;
99

10-
fn extract_indices(witness_map: &WitnessMap, indices: Vec<Witness>) -> Result<WitnessMap, String> {
10+
pub(crate) fn extract_indices(
11+
witness_map: &WitnessMap,
12+
indices: Vec<Witness>,
13+
) -> Result<WitnessMap, String> {
1114
let mut extracted_witness_map = WitnessMap::new();
1215
for witness in indices {
1316
let witness_value = witness_map.get(&witness).ok_or(format!(
@@ -44,7 +47,7 @@ pub fn get_return_witness(
4447
let witness_map = WitnessMap::from(witness_map);
4548

4649
let return_witness =
47-
extract_indices(&witness_map, circuit.return_values.0.clone().into_iter().collect())?;
50+
extract_indices(&witness_map, circuit.return_values.0.iter().copied().collect())?;
4851

4952
Ok(JsWitnessMap::from(return_witness))
5053
}
@@ -71,7 +74,7 @@ pub fn get_public_parameters_witness(
7174
let witness_map = WitnessMap::from(solved_witness);
7275

7376
let public_params_witness =
74-
extract_indices(&witness_map, circuit.public_parameters.0.clone().into_iter().collect())?;
77+
extract_indices(&witness_map, circuit.public_parameters.0.iter().copied().collect())?;
7578

7679
Ok(JsWitnessMap::from(public_params_witness))
7780
}

yarn-project/simulator/src/acvm/acvm.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
type ForeignCallInput,
88
type ForeignCallOutput,
99
type WasmBlackBoxFunctionSolver,
10-
executeCircuitWithBlackBoxSolver,
10+
executeCircuitWithReturnWitness,
1111
} from '@noir-lang/acvm_js';
1212

1313
import { traverseCauseChain } from '../common/errors.js';
@@ -27,9 +27,12 @@ type ACIRCallback = Record<
2727
*/
2828
export interface ACIRExecutionResult {
2929
/**
30-
* The partial witness of the execution.
30+
* An execution result contains two witnesses.
31+
* 1. The partial witness of the execution.
32+
* 2. The return witness which contains the given public return values within the full witness.
3133
*/
3234
partialWitness: ACVMWitness;
35+
returnWitness: ACVMWitness;
3336
}
3437

3538
/**
@@ -89,7 +92,7 @@ export async function acvm(
8992
): Promise<ACIRExecutionResult> {
9093
const logger = createDebugLogger('aztec:simulator:acvm');
9194

92-
const partialWitness = await executeCircuitWithBlackBoxSolver(
95+
const solvedAndReturnWitness = await executeCircuitWithReturnWitness(
9396
solver,
9497
acir,
9598
initialWitness,
@@ -127,7 +130,7 @@ export async function acvm(
127130
throw err;
128131
});
129132

130-
return { partialWitness };
133+
return { partialWitness: solvedAndReturnWitness.solvedWitness, returnWitness: solvedAndReturnWitness.returnWitness };
131134
}
132135

133136
/**

yarn-project/simulator/src/acvm/deserialize.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { Fr } from '@aztec/foundation/fields';
22

3-
import { getReturnWitness } from '@noir-lang/acvm_js';
4-
53
import { type ACVMField, type ACVMWitness } from './acvm_types.js';
64

75
/**
@@ -32,13 +30,11 @@ export function frToBoolean(fr: Fr): boolean {
3230
}
3331

3432
/**
35-
* Extracts the return fields of a given partial witness.
36-
* @param acir - The bytecode of the function.
37-
* @param partialWitness - The witness to extract from.
33+
* Transforms a witness map to its field elements.
34+
* @param witness - The witness to extract from.
3835
* @returns The return values.
3936
*/
40-
export function extractReturnWitness(acir: Buffer, partialWitness: ACVMWitness): Fr[] {
41-
const returnWitness = getReturnWitness(acir, partialWitness);
42-
const sortedKeys = [...returnWitness.keys()].sort((a, b) => a - b);
43-
return sortedKeys.map(key => returnWitness.get(key)!).map(fromACVMField);
37+
export function witnessMapToFields(witness: ACVMWitness): Fr[] {
38+
const sortedKeys = [...witness.keys()].sort((a, b) => a - b);
39+
return sortedKeys.map(key => witness.get(key)!).map(fromACVMField);
4440
}

yarn-project/simulator/src/client/private_execution.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { type AztecAddress } from '@aztec/foundation/aztec-address';
44
import { Fr } from '@aztec/foundation/fields';
55
import { createDebugLogger } from '@aztec/foundation/log';
66

7-
import { extractReturnWitness } from '../acvm/deserialize.js';
7+
import { witnessMapToFields } from '../acvm/deserialize.js';
88
import { Oracle, acvm, extractCallStack } from '../acvm/index.js';
99
import { ExecutionError } from '../common/errors.js';
1010
import { type ClientExecutionContext } from './client_execution_context.js';
@@ -26,7 +26,7 @@ export async function executePrivateFunction(
2626
const acir = artifact.bytecode;
2727
const initialWitness = context.getInitialWitness(artifact);
2828
const acvmCallback = new Oracle(context);
29-
const { partialWitness } = await acvm(await AcirSimulator.getSolver(), acir, initialWitness, acvmCallback).catch(
29+
const acirExecutionResult = await acvm(await AcirSimulator.getSolver(), acir, initialWitness, acvmCallback).catch(
3030
(err: Error) => {
3131
throw new ExecutionError(
3232
err.message,
@@ -39,8 +39,8 @@ export async function executePrivateFunction(
3939
);
4040
},
4141
);
42-
43-
const returnWitness = extractReturnWitness(acir, partialWitness);
42+
const partialWitness = acirExecutionResult.partialWitness;
43+
const returnWitness = witnessMapToFields(acirExecutionResult.returnWitness);
4444
const publicInputs = PrivateCircuitPublicInputs.fromFields(returnWitness);
4545

4646
const encryptedLogs = context.getEncryptedLogs();

yarn-project/simulator/src/client/unconstrained_execution.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { type AztecAddress } from '@aztec/foundation/aztec-address';
44
import { type Fr } from '@aztec/foundation/fields';
55
import { createDebugLogger } from '@aztec/foundation/log';
66

7-
import { extractReturnWitness } from '../acvm/deserialize.js';
7+
import { witnessMapToFields } from '../acvm/deserialize.js';
88
import { Oracle, acvm, extractCallStack, toACVMWitness } from '../acvm/index.js';
99
import { ExecutionError } from '../common/errors.js';
1010
import { AcirSimulator } from './simulator.js';
@@ -27,7 +27,7 @@ export async function executeUnconstrainedFunction(
2727

2828
const acir = artifact.bytecode;
2929
const initialWitness = toACVMWitness(0, args);
30-
const { partialWitness } = await acvm(
30+
const acirExecutionResult = await acvm(
3131
await AcirSimulator.getSolver(),
3232
acir,
3333
initialWitness,
@@ -44,6 +44,7 @@ export async function executeUnconstrainedFunction(
4444
);
4545
});
4646

47-
return decodeReturnValues(artifact, extractReturnWitness(acir, partialWitness));
47+
const returnWitness = witnessMapToFields(acirExecutionResult.returnWitness);
48+
return decodeReturnValues(artifact, returnWitness);
4849
}
4950
// docs:end:execute_unconstrained_function

yarn-project/simulator/src/public/executor.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { spawn } from 'child_process';
66
import fs from 'fs/promises';
77
import path from 'path';
88

9-
import { Oracle, acvm, extractCallStack, extractReturnWitness } from '../acvm/index.js';
9+
import { Oracle, acvm, extractCallStack, witnessMapToFields } from '../acvm/index.js';
1010
import { AvmContext } from '../avm/avm_context.js';
1111
import { AvmMachineState } from '../avm/avm_machine_state.js';
1212
import { AvmSimulator } from '../avm/avm_simulator.js';
@@ -97,11 +97,12 @@ async function executePublicFunctionAcvm(
9797
const initialWitness = context.getInitialWitness();
9898
const acvmCallback = new Oracle(context);
9999

100-
const { partialWitness, reverted, revertReason } = await (async () => {
100+
const { partialWitness, returnWitnessMap, reverted, revertReason } = await (async () => {
101101
try {
102102
const result = await acvm(await AcirSimulator.getSolver(), acir, initialWitness, acvmCallback);
103103
return {
104104
partialWitness: result.partialWitness,
105+
returnWitnessMap: result.returnWitness,
105106
reverted: false,
106107
revertReason: undefined,
107108
};
@@ -123,6 +124,7 @@ async function executePublicFunctionAcvm(
123124
} else {
124125
return {
125126
partialWitness: undefined,
127+
returnWitnessMap: undefined,
126128
reverted: true,
127129
revertReason: createSimulationError(ee),
128130
};
@@ -159,7 +161,7 @@ async function executePublicFunctionAcvm(
159161
throw new Error('No partial witness returned from ACVM');
160162
}
161163

162-
const returnWitness = extractReturnWitness(acir, partialWitness);
164+
const returnWitness = witnessMapToFields(returnWitnessMap);
163165
const {
164166
returnValues,
165167
nullifierReadRequests: nullifierReadRequestsPadded,

0 commit comments

Comments
 (0)