diff --git a/Cargo.toml b/Cargo.toml index de858bfb0..25123ea42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ brillig = { version = "0.20.1", path = "brillig", default-features = false } blackbox_solver = { package = "acvm_blackbox_solver", version = "0.20.1", path = "blackbox_solver", default-features = false } bincode = "1.3.3" -rmp-serde = "1.1.0" num-bigint = "0.4" num-traits = "0.2" diff --git a/acir/Cargo.toml b/acir/Cargo.toml index ab289cd1d..a703fcf12 100644 --- a/acir/Cargo.toml +++ b/acir/Cargo.toml @@ -15,7 +15,7 @@ acir_field.workspace = true brillig.workspace = true serde.workspace = true thiserror.workspace = true -rmp-serde = { workspace = true, optional = true } +rmp-serde = { version = "1.1.0", optional = true } flate2 = "1.0.24" bincode.workspace = true diff --git a/acir/src/circuit/brillig.rs b/acir/src/circuit/brillig.rs index 8bf59dd1a..5b1ec9d03 100644 --- a/acir/src/circuit/brillig.rs +++ b/acir/src/circuit/brillig.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; /// Inputs for the Brillig VM. These are the initial inputs /// that the Brillig VM will use to start. -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] pub enum BrilligInputs { Single(Expression), Array(Vec), @@ -13,7 +13,7 @@ pub enum BrilligInputs { /// Outputs for the Brillig VM. Once the VM has completed /// execution, this will be the object that is returned. -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] pub enum BrilligOutputs { Simple(Witness), Array(Vec), diff --git a/acir/src/circuit/mod.rs b/acir/src/circuit/mod.rs index a9cd30b8f..2e75a3fe5 100644 --- a/acir/src/circuit/mod.rs +++ b/acir/src/circuit/mod.rs @@ -20,6 +20,8 @@ pub struct Circuit { pub current_witness_index: u32, pub opcodes: Vec, + /// The set of private inputs to the circuit. + pub private_parameters: BTreeSet, // ACIR distinguishes between the public inputs which are provided externally or calculated within the circuit and returned. // The elements of these sets may not be mutually exclusive, i.e. a parameter may be returned from the circuit. // All public inputs (parameters and return values) must be provided to the verifier at verification time. @@ -43,6 +45,11 @@ impl Circuit { self.current_witness_index + 1 } + /// Returns all witnesses which are required to execute the circuit successfully. + pub fn circuit_arguments(&self) -> BTreeSet { + self.private_parameters.union(&self.public_parameters.0).cloned().collect() + } + /// Returns all public inputs. This includes those provided as parameters to the circuit and those /// computed as return values. pub fn public_inputs(&self) -> PublicInputs { @@ -178,6 +185,7 @@ mod tests { let circuit = Circuit { current_witness_index: 5, opcodes: vec![and_opcode(), range_opcode(), directive_opcode()], + private_parameters: BTreeSet::new(), public_parameters: PublicInputs(BTreeSet::from_iter(vec![Witness(2), Witness(12)])), return_values: PublicInputs(BTreeSet::from_iter(vec![Witness(4), Witness(12)])), }; @@ -206,6 +214,7 @@ mod tests { range_opcode(), and_opcode(), ], + private_parameters: BTreeSet::new(), public_parameters: PublicInputs(BTreeSet::from_iter(vec![Witness(2)])), return_values: PublicInputs(BTreeSet::from_iter(vec![Witness(2)])), }; diff --git a/acir/src/circuit/opcodes.rs b/acir/src/circuit/opcodes.rs index 9acd35e64..7d6e5efb2 100644 --- a/acir/src/circuit/opcodes.rs +++ b/acir/src/circuit/opcodes.rs @@ -194,7 +194,7 @@ impl std::fmt::Display for Opcode { let is_read = op.operation.is_zero(); let is_write = op.operation == Expression::one(); if is_read { - write!(f, "(id: {}, read at: {}) ", block_id.0, op.index) + write!(f, "(id: {}, read at: {}, value: {}) ", block_id.0, op.index, op.value) } else if is_write { write!(f, "(id: {}, write {} at: {}) ", block_id.0, op.value, op.index) } else { diff --git a/acvm/Cargo.toml b/acvm/Cargo.toml index 2a035e7b2..d61a3e4fd 100644 --- a/acvm/Cargo.toml +++ b/acvm/Cargo.toml @@ -14,7 +14,6 @@ repository.workspace = true num-bigint.workspace = true num-traits.workspace = true thiserror.workspace = true -rmp-serde.workspace = true acir.workspace = true stdlib.workspace = true diff --git a/acvm/src/compiler/mod.rs b/acvm/src/compiler/mod.rs index cc05d3ed1..80e9cfb77 100644 --- a/acvm/src/compiler/mod.rs +++ b/acvm/src/compiler/mod.rs @@ -1,5 +1,8 @@ use acir::{ - circuit::{opcodes::UnsupportedMemoryOpcode, Circuit, Opcode, OpcodeLabel}, + circuit::{ + brillig::BrilligOutputs, directives::Directive, opcodes::UnsupportedMemoryOpcode, Circuit, + Opcode, OpcodeLabel, + }, native_types::{Expression, Witness}, BlackBoxFunc, FieldElement, }; @@ -57,12 +60,18 @@ pub fn compile( let range_optimizer = RangeOptimizer::new(acir); let (acir, opcode_label) = range_optimizer.replace_redundant_ranges(opcode_label); - let transformer = match &np_language { + let mut transformer = match &np_language { crate::Language::R1CS => { let transformer = R1CSTransformer::new(acir); return Ok((transformer.transform(), opcode_label)); } - crate::Language::PLONKCSat { width } => CSatTransformer::new(*width), + crate::Language::PLONKCSat { width } => { + let mut csat = CSatTransformer::new(*width); + for value in acir.circuit_arguments() { + csat.mark_solvable(value); + } + csat + } }; // TODO: the code below is only for CSAT transformer @@ -101,26 +110,123 @@ pub fn compile( new_gates.push(intermediate_gate); } new_gates.push(arith_expr); - new_gates.sort(); for gate in new_gates { new_opcode_labels.push(opcode_label[index]); transformed_gates.push(Opcode::Arithmetic(gate)); } } - other_gate => { + Opcode::BlackBoxFuncCall(func) => { + match func { + acir::circuit::opcodes::BlackBoxFuncCall::AND { output, .. } + | acir::circuit::opcodes::BlackBoxFuncCall::XOR { output, .. } => { + transformer.mark_solvable(*output) + } + acir::circuit::opcodes::BlackBoxFuncCall::RANGE { .. } => (), + acir::circuit::opcodes::BlackBoxFuncCall::SHA256 { outputs, .. } + | acir::circuit::opcodes::BlackBoxFuncCall::Keccak256 { outputs, .. } + | acir::circuit::opcodes::BlackBoxFuncCall::Keccak256VariableLength { + outputs, + .. + } + | acir::circuit::opcodes::BlackBoxFuncCall::RecursiveAggregation { + output_aggregation_object: outputs, + .. + } + | acir::circuit::opcodes::BlackBoxFuncCall::Blake2s { outputs, .. } => { + for witness in outputs { + transformer.mark_solvable(*witness); + } + } + acir::circuit::opcodes::BlackBoxFuncCall::FixedBaseScalarMul { + outputs, + .. + } + | acir::circuit::opcodes::BlackBoxFuncCall::Pedersen { outputs, .. } => { + transformer.mark_solvable(outputs.0); + transformer.mark_solvable(outputs.1) + } + acir::circuit::opcodes::BlackBoxFuncCall::HashToField128Security { + output, + .. + } + | acir::circuit::opcodes::BlackBoxFuncCall::EcdsaSecp256k1 { output, .. } + | acir::circuit::opcodes::BlackBoxFuncCall::EcdsaSecp256r1 { output, .. } + | acir::circuit::opcodes::BlackBoxFuncCall::SchnorrVerify { output, .. } => { + transformer.mark_solvable(*output) + } + } + + new_opcode_labels.push(opcode_label[index]); + transformed_gates.push(opcode.clone()); + } + Opcode::Directive(directive) => { + match directive { + Directive::Invert { result, .. } => { + transformer.mark_solvable(*result); + } + Directive::Quotient(quotient_directive) => { + transformer.mark_solvable(quotient_directive.q); + transformer.mark_solvable(quotient_directive.r); + } + Directive::ToLeRadix { b, .. } => { + for witness in b { + transformer.mark_solvable(*witness); + } + } + Directive::PermutationSort { bits, .. } => { + for witness in bits { + transformer.mark_solvable(*witness); + } + } + Directive::Log(_) => (), + } + new_opcode_labels.push(opcode_label[index]); + transformed_gates.push(opcode.clone()); + } + Opcode::MemoryInit { .. } => { + // `MemoryInit` does not write values to the `WitnessMap` new_opcode_labels.push(opcode_label[index]); - transformed_gates.push(other_gate.clone()) + transformed_gates.push(opcode.clone()); + } + Opcode::MemoryOp { op, .. } => { + for (_, witness1, witness2) in &op.value.mul_terms { + transformer.mark_solvable(*witness1); + transformer.mark_solvable(*witness2); + } + for (_, witness) in &op.value.linear_combinations { + transformer.mark_solvable(*witness); + } + new_opcode_labels.push(opcode_label[index]); + transformed_gates.push(opcode.clone()); + } + + Opcode::Block(_) | Opcode::ROM(_) | Opcode::RAM(_) => { + unimplemented!("Stepwise execution is not compatible with {}", opcode.name()) + } + Opcode::Brillig(brillig) => { + for output in &brillig.outputs { + match output { + BrilligOutputs::Simple(w) => transformer.mark_solvable(*w), + BrilligOutputs::Array(v) => { + for witness in v { + transformer.mark_solvable(*witness); + } + } + } + } + new_opcode_labels.push(opcode_label[index]); + transformed_gates.push(opcode.clone()); } } } let current_witness_index = next_witness_index - 1; - Ok(( Circuit { current_witness_index, opcodes: transformed_gates, // The optimizer does not add new public inputs + private_parameters: acir.private_parameters, public_parameters: acir.public_parameters, return_values: acir.return_values, }, diff --git a/acvm/src/compiler/optimizers/redundant_range.rs b/acvm/src/compiler/optimizers/redundant_range.rs index 62f149990..c4feb8dac 100644 --- a/acvm/src/compiler/optimizers/redundant_range.rs +++ b/acvm/src/compiler/optimizers/redundant_range.rs @@ -112,6 +112,7 @@ impl RangeOptimizer { Circuit { current_witness_index: self.circuit.current_witness_index, opcodes: optimized_opcodes, + private_parameters: self.circuit.private_parameters, public_parameters: self.circuit.public_parameters, return_values: self.circuit.return_values, }, @@ -139,6 +140,8 @@ fn extract_range_opcode(opcode: &Opcode) -> Option<(Witness, u32)> { #[cfg(test)] mod tests { + use std::collections::BTreeSet; + use crate::compiler::optimizers::redundant_range::{extract_range_opcode, RangeOptimizer}; use acir::{ circuit::{ @@ -163,6 +166,7 @@ mod tests { Circuit { current_witness_index: 1, opcodes, + private_parameters: BTreeSet::new(), public_parameters: PublicInputs::default(), return_values: PublicInputs::default(), } diff --git a/acvm/src/compiler/transformers/csat.rs b/acvm/src/compiler/transformers/csat.rs index 3fd4d037c..b520056cd 100644 --- a/acvm/src/compiler/transformers/csat.rs +++ b/acvm/src/compiler/transformers/csat.rs @@ -1,4 +1,4 @@ -use std::cmp::Ordering; +use std::{cmp::Ordering, collections::HashSet}; use acir::{ native_types::{Expression, Witness}, @@ -17,6 +17,8 @@ use indexmap::IndexMap; // Have a single transformer that you instantiate with a width, then pass many gates through pub(crate) struct CSatTransformer { width: usize, + /// Track the witness that can be solved + solvable_witness: HashSet, } impl CSatTransformer { @@ -24,14 +26,46 @@ impl CSatTransformer { pub(crate) fn new(width: usize) -> CSatTransformer { assert!(width > 2); - CSatTransformer { width } + CSatTransformer { width, solvable_witness: HashSet::new() } + } + + /// Check if the equation 'expression=0' can be solved, and if yes, add the solved witness to set of solvable witness + fn try_solve(&mut self, gate: &Expression) { + let mut unresolved = Vec::new(); + for (_, w1, w2) in &gate.mul_terms { + if !self.solvable_witness.contains(w1) { + unresolved.push(w1); + if !self.solvable_witness.contains(w2) { + return; + } + } + if !self.solvable_witness.contains(w2) { + unresolved.push(w2); + if !self.solvable_witness.contains(w1) { + return; + } + } + } + for (_, w) in &gate.linear_combinations { + if !self.solvable_witness.contains(w) { + unresolved.push(w); + } + } + if unresolved.len() == 1 { + self.mark_solvable(*unresolved[0]); + } + } + + /// Adds the witness to set of solvable witness + pub(crate) fn mark_solvable(&mut self, witness: Witness) { + self.solvable_witness.insert(witness); } // Still missing dead witness optimization. // To do this, we will need the whole set of arithmetic gates // I think it can also be done before the local optimization seen here, as dead variables will come from the user pub(crate) fn transform( - &self, + &mut self, gate: Expression, intermediate_variables: &mut IndexMap, num_witness: &mut u32, @@ -45,6 +79,7 @@ impl CSatTransformer { let mut gate = self.partial_gate_scan_optimization(gate, intermediate_variables, num_witness); gate.sort(); + self.try_solve(&gate); gate } @@ -72,7 +107,7 @@ impl CSatTransformer { // We can no longer extract another full gate, hence the algorithm terminates. Creating two intermediate variables t and t2. // This stage of preprocessing does not guarantee that all polynomials can fit into a gate. It only guarantees that all full gates have been extracted from each polynomial fn full_gate_scan_optimization( - &self, + &mut self, mut gate: Expression, intermediate_variables: &mut IndexMap, num_witness: &mut u32, @@ -98,9 +133,15 @@ impl CSatTransformer { // This will be our new gate which will be equal to `self` except we will have intermediate variables that will be constrained to any // subset of the terms that can be represented as full gates let mut new_gate = Expression::default(); - - while !gate.mul_terms.is_empty() { - let pair = gate.mul_terms[0]; + let mut remaining_mul_terms = Vec::with_capacity(gate.mul_terms.len()); + for pair in gate.mul_terms { + // We want to layout solvable intermediate variable, if we cannot solve one of the witness + // that means the intermediate gate will not be immediately solvable + if !self.solvable_witness.contains(&pair.1) || !self.solvable_witness.contains(&pair.2) + { + remaining_mul_terms.push(pair); + continue; + } // Check if this pair is present in the simplified fan-in // We are assuming that the fan-in/fan-out has been simplified. @@ -153,17 +194,23 @@ impl CSatTransformer { } // Now we have used up 2 spaces in our arithmetic gate. The width now dictates, how many more we can add - let remaining_space = self.width - 2 - 1; // We minus 1 because we need an extra space to contain the intermediate variable - // Keep adding terms until we have no more left, or we reach the width - for _ in 0..remaining_space { + let mut remaining_space = self.width - 2 - 1; // We minus 1 because we need an extra space to contain the intermediate variable + // Keep adding terms until we have no more left, or we reach the width + let mut remaining_linear_terms = + Vec::with_capacity(gate.linear_combinations.len()); + while remaining_space > 0 { if let Some(wire_term) = gate.linear_combinations.pop() { // Add this element into the new gate - intermediate_gate.linear_combinations.push(wire_term); + if self.solvable_witness.contains(&wire_term.1) { + intermediate_gate.linear_combinations.push(wire_term); + remaining_space -= 1; + } else { + remaining_linear_terms.push(wire_term); + } } else { - // No more elements left in the old gate, we could stop the whole function - // We could alternative let it keep going, as it will never reach this branch again since there are no more elements left - // XXX: Future optimization - // no_more_left = true + // No more usable elements left in the old gate + gate.linear_combinations = remaining_linear_terms; + break; } } // Constraint this intermediate_gate to be equal to the temp variable by adding it into the IndexMap @@ -179,12 +226,12 @@ impl CSatTransformer { ); // Add intermediate variable to the new gate instead of the full gate + self.mark_solvable(inter_var.1); new_gate.linear_combinations.push(inter_var); } }; - // Remove this term as we are finished processing it - gate.mul_terms.remove(0); } + gate.mul_terms = remaining_mul_terms; // Add the rest of the elements back into the new_gate new_gate.mul_terms.extend(gate.mul_terms.clone()); @@ -268,7 +315,7 @@ impl CSatTransformer { // // Cases, a lot of mul terms, a lot of fan-in terms, 50/50 fn partial_gate_scan_optimization( - &self, + &mut self, mut gate: Expression, intermediate_variables: &mut IndexMap, num_witness: &mut u32, @@ -283,24 +330,32 @@ impl CSatTransformer { } // 2. Create Intermediate variables for the multiplication gates + let mut remaining_mul_terms = Vec::with_capacity(gate.mul_terms.len()); for mul_term in gate.mul_terms.clone().into_iter() { - let mut intermediate_gate = Expression::default(); - - // Push mul term into the gate - intermediate_gate.mul_terms.push(mul_term); - // Get an intermediate variable which squashes the multiplication term - let inter_var = Self::get_or_create_intermediate_vars( - intermediate_variables, - intermediate_gate, - num_witness, - ); - - // Add intermediate variable as a part of the fan-in for the original gate - gate.linear_combinations.push(inter_var); + if self.solvable_witness.contains(&mul_term.1) + && self.solvable_witness.contains(&mul_term.2) + { + let mut intermediate_gate = Expression::default(); + + // Push mul term into the gate + intermediate_gate.mul_terms.push(mul_term); + // Get an intermediate variable which squashes the multiplication term + let inter_var = Self::get_or_create_intermediate_vars( + intermediate_variables, + intermediate_gate, + num_witness, + ); + + // Add intermediate variable as a part of the fan-in for the original gate + gate.linear_combinations.push(inter_var); + self.mark_solvable(inter_var.1); + } else { + remaining_mul_terms.push(mul_term); + } } // Remove all of the mul terms as we have intermediate variables to represent them now - gate.mul_terms.clear(); + gate.mul_terms = remaining_mul_terms; // We now only have a polynomial with only fan-in/fan-out terms i.e. terms of the form Ax + By + Cd + ... // Lets create intermediate variables if all of them cannot fit into the width @@ -318,29 +373,37 @@ impl CSatTransformer { // Collect as many terms up to the given width-1 and constrain them to an intermediate variable let mut intermediate_gate = Expression::default(); - for _ in 0..(self.width - 1) { - match gate.linear_combinations.pop() { - Some(term) => { - intermediate_gate.linear_combinations.push(term); - } - None => { - break; // We can also do nothing here - } - }; - } - let inter_var = Self::get_or_create_intermediate_vars( - intermediate_variables, - intermediate_gate, - num_witness, - ); + let mut remaining_linear_terms = Vec::with_capacity(gate.linear_combinations.len()); - added.push(inter_var); + for term in gate.linear_combinations { + if self.solvable_witness.contains(&term.1) + && intermediate_gate.linear_combinations.len() < self.width - 1 + { + intermediate_gate.linear_combinations.push(term); + } else { + remaining_linear_terms.push(term); + } + } + gate.linear_combinations = remaining_linear_terms; + let not_full = intermediate_gate.linear_combinations.len() < self.width - 1; + if intermediate_gate.linear_combinations.len() > 1 { + let inter_var = Self::get_or_create_intermediate_vars( + intermediate_variables, + intermediate_gate, + num_witness, + ); + self.mark_solvable(inter_var.1); + added.push(inter_var); + } + // The intermediate gate is not full, but the gate still has too many terms + if not_full && gate.linear_combinations.len() > self.width { + unreachable!("Could not reduce the expression"); + } } // Add back the intermediate variables to // keep consistency with the original equation. gate.linear_combinations.extend(added); - self.partial_gate_scan_optimization(gate, intermediate_variables, num_witness) } } @@ -368,14 +431,17 @@ fn simple_reduction_smoke_test() { let mut num_witness = 4; - let optimizer = CSatTransformer::new(3); + let mut optimizer = CSatTransformer::new(3); + optimizer.mark_solvable(b); + optimizer.mark_solvable(c); + optimizer.mark_solvable(d); let got_optimized_gate_a = optimizer.transform(gate_a, &mut intermediate_variables, &mut num_witness); // a = b + c + d => a - b - c - d = 0 // For width3, the result becomes: - // a - b + e = 0 - // - c - d - e = 0 + // a - d + e = 0 + // - c - b - e = 0 // // a - b + e = 0 let e = Witness(4); @@ -383,7 +449,7 @@ fn simple_reduction_smoke_test() { mul_terms: vec![], linear_combinations: vec![ (FieldElement::one(), a), - (-FieldElement::one(), b), + (-FieldElement::one(), d), (FieldElement::one(), e), ], q_c: FieldElement::zero(), @@ -392,13 +458,52 @@ fn simple_reduction_smoke_test() { assert_eq!(intermediate_variables.len(), 1); - // e = - c - d + // e = - c - b let expected_intermediate_gate = Expression { mul_terms: vec![], - linear_combinations: vec![(-FieldElement::one(), d), (-FieldElement::one(), c)], + linear_combinations: vec![(-FieldElement::one(), c), (-FieldElement::one(), b)], q_c: FieldElement::zero(), }; let (_, normalized_gate) = CSatTransformer::normalize(expected_intermediate_gate); assert!(intermediate_variables.contains_key(&normalized_gate)); assert_eq!(intermediate_variables[&normalized_gate].1, e); } + +#[test] +fn stepwise_reduction_test() { + let a = Witness(0); + let b = Witness(1); + let c = Witness(2); + let d = Witness(3); + let e = Witness(4); + + // a = b + c + d + e; + let gate_a = Expression { + mul_terms: vec![], + linear_combinations: vec![ + (-FieldElement::one(), a), + (FieldElement::one(), b), + (FieldElement::one(), c), + (FieldElement::one(), d), + (FieldElement::one(), e), + ], + q_c: FieldElement::zero(), + }; + + let mut intermediate_variables: IndexMap = IndexMap::new(); + + let mut num_witness = 4; + + let mut optimizer = CSatTransformer::new(3); + optimizer.mark_solvable(a); + optimizer.mark_solvable(c); + optimizer.mark_solvable(d); + optimizer.mark_solvable(e); + let got_optimized_gate_a = + optimizer.transform(gate_a, &mut intermediate_variables, &mut num_witness); + + let witnesses: Vec = + got_optimized_gate_a.linear_combinations.iter().map(|(_, w)| *w).collect(); + // Since b is not known, it cannot be put inside intermediate gates, so it must belong to the transformed gate. + assert!(witnesses.contains(&b)); +} diff --git a/acvm/src/compiler/transformers/fallback.rs b/acvm/src/compiler/transformers/fallback.rs index e196a5630..7bc530add 100644 --- a/acvm/src/compiler/transformers/fallback.rs +++ b/acvm/src/compiler/transformers/fallback.rs @@ -67,6 +67,7 @@ impl FallbackTransformer { Circuit { current_witness_index: witness_idx, opcodes: acir_supported_opcodes, + private_parameters: acir.private_parameters, public_parameters: acir.public_parameters, return_values: acir.return_values, }, diff --git a/acvm/src/pwg/block.rs b/acvm/src/pwg/block.rs index fa18ffa3a..a5cbeb4e6 100644 --- a/acvm/src/pwg/block.rs +++ b/acvm/src/pwg/block.rs @@ -6,8 +6,8 @@ use acir::{ FieldElement, }; +use super::OpcodeResolutionError; use super::{arithmetic::ArithmeticSolver, get_value, insert_value, witness_to_value}; -use super::{OpcodeResolution, OpcodeResolutionError}; type MemoryIndex = u32; @@ -17,7 +17,6 @@ type MemoryIndex = u32; #[derive(Default)] pub(super) struct BlockSolver { block_value: HashMap, - solved_operations: usize, } impl BlockSolver { @@ -44,21 +43,6 @@ impl BlockSolver { Ok(()) } - // Helper function which tries to solve a Block opcode - // As long as operations are resolved, we update/read from the block_value - // We stop when an operation cannot be resolved - fn solve_helper( - &mut self, - initial_witness: &mut WitnessMap, - trace: &[MemOp], - ) -> Result<(), OpcodeResolutionError> { - for block_op in trace.iter().skip(self.solved_operations) { - self.solve_memory_op(block_op, initial_witness)?; - self.solved_operations += 1; - } - Ok(()) - } - pub(crate) fn solve_memory_op( &mut self, op: &MemOp, @@ -104,29 +88,6 @@ impl BlockSolver { Ok(()) } } - - // Try to solve block operations from the trace - // The function calls solve_helper() for solving the opcode - // and converts its result into GateResolution - pub(crate) fn solve( - &mut self, - initial_witness: &mut WitnessMap, - trace: &[MemOp], - ) -> Result { - let initial_solved_operations = self.solved_operations; - - match self.solve_helper(initial_witness, trace) { - Ok(()) => Ok(OpcodeResolution::Solved), - Err(OpcodeResolutionError::OpcodeNotSolvable(err)) => { - if self.solved_operations > initial_solved_operations { - Ok(OpcodeResolution::InProgress) - } else { - Ok(OpcodeResolution::Stalled(err)) - } - } - Err(err) => Err(err), - } - } } #[cfg(test)] @@ -159,7 +120,9 @@ mod tests { let mut block_solver = BlockSolver::default(); block_solver.init(&init, &initial_witness).unwrap(); - block_solver.solve(&mut initial_witness, &trace).unwrap(); + for op in trace { + block_solver.solve_memory_op(&op, &mut initial_witness).unwrap(); + } assert_eq!(initial_witness[&Witness(4)], FieldElement::one()); } } diff --git a/acvm/src/pwg/brillig.rs b/acvm/src/pwg/brillig.rs index 0a026a60d..acdc59322 100644 --- a/acvm/src/pwg/brillig.rs +++ b/acvm/src/pwg/brillig.rs @@ -135,8 +135,8 @@ impl BrilligSolver { insert_value(witness, FieldElement::zero(), initial_witness)? } BrilligOutputs::Array(witness_arr) => { - for w in witness_arr { - insert_value(w, FieldElement::zero(), initial_witness)? + for witness in witness_arr { + insert_value(witness, FieldElement::zero(), initial_witness)? } } } diff --git a/acvm/src/pwg/mod.rs b/acvm/src/pwg/mod.rs index 2b6c1e0e8..7754456a9 100644 --- a/acvm/src/pwg/mod.rs +++ b/acvm/src/pwg/mod.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use acir::{ brillig::ForeignCallResult, - circuit::{brillig::Brillig, opcodes::BlockId, Opcode, OpcodeLabel}, + circuit::{opcodes::BlockId, Opcode, OpcodeLabel}, native_types::{Expression, Witness, WitnessMap}, BlackBoxFunc, FieldElement, }; @@ -47,7 +47,7 @@ pub enum ACVMStatus { /// to the ACVM using [`ACVM::resolve_pending_foreign_call`]. /// /// Once this is done, the ACVM can be restarted to solve the remaining opcodes. - RequiresForeignCall, + RequiresForeignCall(ForeignCallWaitInfo), } #[derive(Debug, PartialEq)] @@ -113,37 +113,24 @@ pub struct ACVM { /// Stores the solver for each [block][`Opcode::Block`] opcode. This persists their internal state to prevent recomputation. block_solvers: HashMap, - /// A list of opcodes which are to be executed by the ACVM, along with their label - /// - /// Note that this doesn't include any opcodes which are waiting on a pending foreign call. - opcodes_and_labels: Vec<(Opcode, OpcodeLabel)>, + /// A list of opcodes which are to be executed by the ACVM. + opcodes: Vec, + /// Index of the next opcode to be executed. + instruction_pointer: usize, witness_map: WitnessMap, - - /// A list of foreign calls which must be resolved before the ACVM can resume execution. - pending_foreign_calls: Vec, - - /// Map from a canonical hash of an unresolved Brillig call to its opcode label. - pending_brillig_label_maps: HashMap, } impl ACVM { pub fn new(backend: B, opcodes: Vec, initial_witness: WitnessMap) -> Self { - let opcodes_and_labels = opcodes - .iter() - .enumerate() - .map(|(opcode_index, opcode)| { - (opcode.clone(), OpcodeLabel::Resolved(opcode_index as u64)) - }) - .collect(); + let status = if opcodes.is_empty() { ACVMStatus::Solved } else { ACVMStatus::InProgress }; ACVM { - status: ACVMStatus::InProgress, + status, backend, block_solvers: HashMap::default(), - opcodes_and_labels, + opcodes, + instruction_pointer: 0, witness_map: initial_witness, - pending_foreign_calls: Vec::new(), - pending_brillig_label_maps: HashMap::new(), } } @@ -154,11 +141,22 @@ impl ACVM { &self.witness_map } - /// Returns a slice containing the opcodes which remain to be solved. - /// - /// Note: this doesn't include any opcodes which are waiting on a pending foreign call. - pub fn unresolved_opcodes(&self) -> &[(Opcode, OpcodeLabel)] { - &self.opcodes_and_labels + /// Returns a slice containing the opcodes of the circuit being executed. + pub fn opcodes(&self) -> &[Opcode] { + &self.opcodes + } + + /// Returns the index of the current opcode to be executed. + pub fn instruction_pointer(&self) -> usize { + self.instruction_pointer + } + + /// Finalize the ACVM execution, returning the resulting [`WitnessMap`]. + pub fn finalize(self) -> WitnessMap { + if self.status != ACVMStatus::Solved { + panic!("ACVM is not ready to be finalized"); + } + self.witness_map } /// Updates the current status of the VM. @@ -174,29 +172,35 @@ impl ACVM { self.status(ACVMStatus::Failure(error)) } - /// Finalize the ACVM execution, returning the resulting [`WitnessMap`]. - pub fn finalize(self) -> WitnessMap { - if self.status != ACVMStatus::Solved { - panic!("ACVM is not ready to be finalized"); - } - self.witness_map + /// Sets the status of the VM to `RequiresForeignCall`. + /// Indicating that the VM is now waiting for a foreign call to be resolved. + fn wait_for_foreign_call(&mut self, foreign_call: ForeignCallWaitInfo) -> ACVMStatus { + self.status(ACVMStatus::RequiresForeignCall(foreign_call)) } /// Return a reference to the arguments for the next pending foreign call, if one exists. pub fn get_pending_foreign_call(&self) -> Option<&ForeignCallWaitInfo> { - self.pending_foreign_calls.first().map(|foreign_call| &foreign_call.foreign_call_wait_info) + if let ACVMStatus::RequiresForeignCall(foreign_call) = &self.status { + Some(foreign_call) + } else { + None + } } - /// Resolves a pending foreign call using a result calculated outside of the ACVM. + /// Resolves a foreign call's [result][acir::brillig_vm::ForeignCallResult] using a result calculated outside of the ACVM. + /// + /// The ACVM can then be restarted to solve the remaining Brillig VM process as well as the remaining ACIR opcodes. pub fn resolve_pending_foreign_call(&mut self, foreign_call_result: ForeignCallResult) { - // Remove the first foreign call and inject the result to create a new opcode. - let foreign_call = self.pending_foreign_calls.remove(0); - let resolved_brillig = foreign_call.resolve(foreign_call_result); - - // Mark this opcode to be executed next. - let hash = canonical_brillig_hash(&resolved_brillig); - self.opcodes_and_labels - .insert(0, (Opcode::Brillig(resolved_brillig), self.pending_brillig_label_maps[&hash])); + let opcode = &mut self.opcodes[self.instruction_pointer]; + if let Opcode::Brillig(brillig) = opcode { + // Overwrite the brillig opcode with a new one with the foreign call response. + brillig.foreign_call_results.push(foreign_call_result); + + // Now that the foreign call has been resolved then we can resume execution. + self.status(ACVMStatus::InProgress); + } else { + panic!("Brillig resolution for non brillig opcode"); + } } /// Executes the ACVM's circuit until execution halts. @@ -204,121 +208,86 @@ impl ACVM { /// Execution can halt due to three reasons: /// 1. All opcodes have been executed successfully. /// 2. The circuit has been found to be unsatisfiable. - /// 2. A Brillig [foreign call][`UnresolvedBrilligCall`] has been encountered and must be resolved. + /// 2. A Brillig [foreign call][`ForeignCallWaitInfo`] has been encountered and must be resolved. pub fn solve(&mut self) -> ACVMStatus { - // TODO: Prevent execution with outstanding foreign calls? - let mut unresolved_opcodes: Vec<(Opcode, OpcodeLabel)> = Vec::new(); - while !self.opcodes_and_labels.is_empty() { - unresolved_opcodes.clear(); - let mut stalled = true; - let mut opcode_not_solvable = None; - for (opcode, opcode_label) in &self.opcodes_and_labels { - let mut resolution = match opcode { - Opcode::Arithmetic(expr) => { - ArithmeticSolver::solve(&mut self.witness_map, expr) - } - Opcode::BlackBoxFuncCall(bb_func) => { - blackbox::solve(&self.backend, &mut self.witness_map, bb_func) - } - Opcode::Directive(directive) => { - solve_directives(&mut self.witness_map, directive) - } - Opcode::Block(block) | Opcode::ROM(block) | Opcode::RAM(block) => { - let solver = self.block_solvers.entry(block.id).or_default(); - solver.solve(&mut self.witness_map, &block.trace) - } - Opcode::Brillig(brillig) => { - BrilligSolver::solve(&mut self.witness_map, brillig, &self.backend) - } - Opcode::MemoryInit { block_id, init } => { - let solver = self.block_solvers.entry(*block_id).or_default(); - solver.init(init, &self.witness_map).map(|_| OpcodeResolution::Solved) - } - Opcode::MemoryOp { block_id, op } => { - let solver = self.block_solvers.entry(*block_id).or_default(); - solver - .solve_memory_op(op, &mut self.witness_map) - .map(|_| OpcodeResolution::Solved) - } - }; + while self.status == ACVMStatus::InProgress { + self.solve_opcode(); + } + self.status.clone() + } - // If we have an unsatisfied constraint, the opcode label will be unresolved - // because the solvers do not have knowledge of this information. - // We resolve, by setting this to the corresponding opcode that we just attempted to solve. - if let Err(OpcodeResolutionError::UnsatisfiedConstrain { - opcode_label: opcode_index, - }) = &mut resolution - { - *opcode_index = *opcode_label; - } - // If a brillig function has failed, we return an unsatisfied constraint error - // We intentionally ignore the brillig failure message, as there is no way to - // propagate this to the caller. - if let Err(OpcodeResolutionError::BrilligFunctionFailed(_)) = &mut resolution { - // - // std::mem::swap(x, y) - resolution = Err(OpcodeResolutionError::UnsatisfiedConstrain { - opcode_label: *opcode_label, - }) - } + pub fn solve_opcode(&mut self) -> ACVMStatus { + let opcode = &self.opcodes[self.instruction_pointer]; + + let resolution = match opcode { + Opcode::Arithmetic(expr) => ArithmeticSolver::solve(&mut self.witness_map, expr), + Opcode::BlackBoxFuncCall(bb_func) => { + blackbox::solve(&self.backend, &mut self.witness_map, bb_func) + } + Opcode::Directive(directive) => solve_directives(&mut self.witness_map, directive), + Opcode::MemoryInit { block_id, init } => { + let solver = self.block_solvers.entry(*block_id).or_default(); + solver.init(init, &self.witness_map).map(|_| OpcodeResolution::Solved) + } + Opcode::MemoryOp { block_id, op } => { + let solver = self.block_solvers.entry(*block_id).or_default(); + solver.solve_memory_op(op, &mut self.witness_map).map(|_| OpcodeResolution::Solved) + } + Opcode::Brillig(brillig) => { + let resolution = + BrilligSolver::solve(&mut self.witness_map, brillig, &self.backend); match resolution { - Ok(OpcodeResolution::Solved) => { - stalled = false; - } - Ok(OpcodeResolution::InProgress) => { - stalled = false; - unresolved_opcodes.push((opcode.clone(), *opcode_label)); + Ok(OpcodeResolution::InProgressBrillig(foreign_call)) => { + return self.wait_for_foreign_call(foreign_call) } - Ok(OpcodeResolution::InProgressBrillig(oracle_wait_info)) => { - stalled = false; - // InProgressBrillig Oracles must be externally re-solved - let brillig = match &opcode { - Opcode::Brillig(brillig) => brillig.clone(), - _ => unreachable!("Brillig resolution for non brillig opcode"), - }; - let hash = canonical_brillig_hash(&brillig); - self.pending_brillig_label_maps.insert(hash, *opcode_label); - self.pending_foreign_calls.push(UnresolvedBrilligCall { - brillig, - foreign_call_wait_info: oracle_wait_info, - }) - } - Ok(OpcodeResolution::Stalled(not_solvable)) => { - if opcode_not_solvable.is_none() { - // we keep track of the first unsolvable opcode - opcode_not_solvable = Some(not_solvable); - } - // We push those opcodes not solvable to the back as - // it could be because the opcodes are out of order, i.e. this assignment - // relies on a later opcodes' results - unresolved_opcodes.push((opcode.clone(), *opcode_label)); - } - Err(OpcodeResolutionError::OpcodeNotSolvable(_)) => { - unreachable!("ICE - Result should have been converted to GateResolution") - } - Err(error) => return self.fail(error), + res => res, } } - - // Before potentially ending execution, we must save the list of opcodes which remain to be solved. - std::mem::swap(&mut self.opcodes_and_labels, &mut unresolved_opcodes); - - // We have oracles that must be externally resolved - if self.get_pending_foreign_call().is_some() { - return self.status(ACVMStatus::RequiresForeignCall); + Opcode::Block(_) | Opcode::ROM(_) | Opcode::RAM(_) => { + panic!("Block, ROM and RAM opcodes are not supported by stepwise ACVM") } - - // We are stalled because of an opcode being bad - if stalled && !self.opcodes_and_labels.is_empty() { - let error = OpcodeResolutionError::OpcodeNotSolvable( - opcode_not_solvable - .expect("infallible: cannot be stalled and None at the same time"), - ); - return self.fail(error); + }; + match resolution { + Ok(OpcodeResolution::Solved) => { + self.instruction_pointer += 1; + if self.instruction_pointer == self.opcodes.len() { + self.status(ACVMStatus::Solved) + } else { + self.status(ACVMStatus::InProgress) + } + } + Ok(OpcodeResolution::InProgress) => { + unreachable!("Opcodes_and_labels should be immediately solvable"); + } + Ok(OpcodeResolution::InProgressBrillig(_)) => { + unreachable!("Handled above") + } + Ok(OpcodeResolution::Stalled(not_solvable)) => { + self.fail(OpcodeResolutionError::OpcodeNotSolvable(not_solvable)) + } + Err(mut error) => { + let opcode_label = + OpcodeLabel::Resolved(self.instruction_pointer.try_into().unwrap()); + match &mut error { + // If we have an unsatisfied constraint, the opcode label will be unresolved + // because the solvers do not have knowledge of this information. + // We resolve, by setting this to the corresponding opcode that we just attempted to solve. + OpcodeResolutionError::UnsatisfiedConstrain { opcode_label: opcode_index } => { + *opcode_index = opcode_label; + } + // If a brillig function has failed, we return an unsatisfied constraint error + // We intentionally ignore the brillig failure message, as there is no way to + // propagate this to the caller. + OpcodeResolutionError::BrilligFunctionFailed(_) => { + error = OpcodeResolutionError::UnsatisfiedConstrain { opcode_label } + } + // All other errors are thrown normally. + _ => (), + }; + self.fail(error) } } - self.status(ACVMStatus::Solved) } } @@ -390,30 +359,6 @@ fn any_witness_from_expression(expr: &Expression) -> Option { } } -/// A Brillig VM process has requested the caller to solve a [foreign call][brillig_vm::Opcode::ForeignCall] externally -/// and to re-run the process with the foreign call's resolved outputs. -#[derive(Debug, PartialEq, Clone)] -pub struct UnresolvedBrilligCall { - /// The current Brillig VM process that has been paused. - /// This process will be updated by the caller after resolving a foreign call's result. - /// - /// This can be done using [`UnresolvedBrilligCall::resolve`]. - pub brillig: Brillig, - /// Inputs for a pending foreign call required to restart bytecode processing. - pub foreign_call_wait_info: brillig::ForeignCallWaitInfo, -} - -impl UnresolvedBrilligCall { - /// Inserts the [foreign call's result][acir::brillig_vm::ForeignCallResult] into the calling [`Brillig` opcode][Brillig]. - /// - /// The [ACVM][solve] can then be restarted with the updated [Brillig opcode][Opcode::Brillig] - /// to solve the remaining Brillig VM process as well as the remaining ACIR opcodes. - pub fn resolve(mut self, foreign_call_result: ForeignCallResult) -> Brillig { - self.brillig.foreign_call_results.push(foreign_call_result); - self.brillig - } -} - #[deprecated( note = "For backwards compatibility, this method allows you to derive _sensible_ defaults for opcode support based on the np language. \n Backends should simply specify what they support." )] @@ -440,28 +385,3 @@ pub fn default_is_opcode_supported(language: Language) -> fn(&Opcode) -> bool { Language::PLONKCSat { .. } => plonk_is_supported, } } - -/// Canonically hashes the Brillig struct. -/// -/// Some Brillig instances may or may not be resolved, so we do -/// not hash the `foreign_call_results`. -/// -/// This gives us a consistent hash that will be used to track `Brillig` -/// even when it is put back into the list of opcodes out of order. -/// This happens when we resolve a Brillig opcode call. -pub fn canonical_brillig_hash(brillig: &Brillig) -> UnresolvedBrilligCallHash { - let mut serialized_vector = rmp_serde::to_vec(&brillig.inputs).unwrap(); - serialized_vector.extend(rmp_serde::to_vec(&brillig.outputs).unwrap()); - serialized_vector.extend(rmp_serde::to_vec(&brillig.bytecode).unwrap()); - serialized_vector.extend(rmp_serde::to_vec(&brillig.predicate).unwrap()); - - use std::collections::hash_map::DefaultHasher; - use std::hash::Hasher; - - let mut hasher = DefaultHasher::new(); - hasher.write(&serialized_vector); - hasher.finish() -} - -/// Hash of an unresolved brillig call instance -pub(crate) type UnresolvedBrilligCallHash = u64; diff --git a/acvm/tests/solver.rs b/acvm/tests/solver.rs index 59c3df5a7..638d254ec 100644 --- a/acvm/tests/solver.rs +++ b/acvm/tests/solver.rs @@ -5,6 +5,7 @@ use acir::{ circuit::{ brillig::{Brillig, BrilligInputs, BrilligOutputs}, directives::Directive, + opcodes::{BlockId, MemOp}, Opcode, OpcodeLabel, }, native_types::{Expression, Witness, WitnessMap}, @@ -130,12 +131,11 @@ fn inversion_brillig_oracle_equivalence() { // use the partial witness generation solver with our acir program let solver_status = acvm.solve(); - assert_eq!( - solver_status, - ACVMStatus::RequiresForeignCall, + assert!( + matches!(solver_status, ACVMStatus::RequiresForeignCall(_)), "should require foreign call response" ); - assert!(acvm.unresolved_opcodes().is_empty(), "brillig should have been removed"); + assert_eq!(acvm.instruction_pointer(), 0, "brillig should have been removed"); let foreign_call_wait_info: &ForeignCallWaitInfo = acvm.get_pending_foreign_call().expect("should have a brillig foreign call request"); @@ -260,12 +260,11 @@ fn double_inversion_brillig_oracle() { // use the partial witness generation solver with our acir program let solver_status = acvm.solve(); - assert_eq!( - solver_status, - ACVMStatus::RequiresForeignCall, + assert!( + matches!(solver_status, ACVMStatus::RequiresForeignCall(_)), "should require foreign call response" ); - assert!(acvm.unresolved_opcodes().is_empty(), "brillig should have been removed"); + assert_eq!(acvm.instruction_pointer(), 0, "should stall on brillig"); let foreign_call_wait_info: &ForeignCallWaitInfo = acvm.get_pending_foreign_call().expect("should have a brillig foreign call request"); @@ -278,12 +277,11 @@ fn double_inversion_brillig_oracle() { // After filling data request, continue solving let solver_status = acvm.solve(); - assert_eq!( - solver_status, - ACVMStatus::RequiresForeignCall, + assert!( + matches!(solver_status, ACVMStatus::RequiresForeignCall(_)), "should require foreign call response" ); - assert!(acvm.unresolved_opcodes().is_empty(), "should be fully solved"); + assert_eq!(acvm.instruction_pointer(), 0, "should stall on brillig"); let foreign_call_wait_info = acvm.get_pending_foreign_call().expect("should have a brillig foreign call request"); @@ -383,17 +381,11 @@ fn oracle_dependent_execution() { // use the partial witness generation solver with our acir program let solver_status = acvm.solve(); - assert_eq!( - solver_status, - ACVMStatus::RequiresForeignCall, + assert!( + matches!(solver_status, ACVMStatus::RequiresForeignCall(_)), "should require foreign call response" ); - assert_eq!(acvm.unresolved_opcodes().len(), 1, "brillig should have been removed"); - assert_eq!( - acvm.unresolved_opcodes()[0].0, - Opcode::Arithmetic(inverse_equality_check.clone()), - "Equality check of inverses should still be waiting to be resolved" - ); + assert_eq!(acvm.instruction_pointer(), 1, "should stall on brillig"); let foreign_call_wait_info: &ForeignCallWaitInfo = acvm.get_pending_foreign_call().expect("should have a brillig foreign call request"); @@ -405,17 +397,11 @@ fn oracle_dependent_execution() { // After filling data request, continue solving let solver_status = acvm.solve(); - assert_eq!( - solver_status, - ACVMStatus::RequiresForeignCall, + assert!( + matches!(solver_status, ACVMStatus::RequiresForeignCall(_)), "should require foreign call response" ); - assert_eq!(acvm.unresolved_opcodes().len(), 1, "brillig should have been removed"); - assert_eq!( - acvm.unresolved_opcodes()[0].0, - Opcode::Arithmetic(inverse_equality_check), - "Equality check of inverses should still be waiting to be resolved" - ); + assert_eq!(acvm.instruction_pointer(), 1, "should stall on brillig"); let foreign_call_wait_info: &ForeignCallWaitInfo = acvm.get_pending_foreign_call().expect("should have a brillig foreign call request"); @@ -639,3 +625,40 @@ fn unsatisfied_opcode_resolved_brillig() { "The first gate is not satisfiable, expected an error indicating this" ); } + +#[test] +fn memory_operations() { + let initial_witness = WitnessMap::from(BTreeMap::from_iter([ + (Witness(1), FieldElement::from(1u128)), + (Witness(2), FieldElement::from(2u128)), + (Witness(3), FieldElement::from(3u128)), + (Witness(4), FieldElement::from(4u128)), + (Witness(5), FieldElement::from(5u128)), + (Witness(6), FieldElement::from(4u128)), + ])); + + let block_id = BlockId(0); + + let init = Opcode::MemoryInit { block_id, init: (1..6).map(Witness).collect() }; + + let read_op = + Opcode::MemoryOp { block_id, op: MemOp::read_at_mem_index(Witness(6).into(), Witness(7)) }; + + let expression = Opcode::Arithmetic(Expression { + mul_terms: Vec::new(), + linear_combinations: vec![ + (FieldElement::one(), Witness(7)), + (-FieldElement::one(), Witness(8)), + ], + q_c: FieldElement::one(), + }); + + let opcodes = vec![init, read_op, expression]; + + let mut acvm = ACVM::new(StubbedBackend, opcodes, initial_witness); + let solver_status = acvm.solve(); + assert_eq!(solver_status, ACVMStatus::Solved); + let witness_map = acvm.finalize(); + + assert_eq!(witness_map[&Witness(8)], FieldElement::from(6u128)); +} diff --git a/acvm/tests/stdlib.rs b/acvm/tests/stdlib.rs index bbf60864d..88bbd4314 100644 --- a/acvm/tests/stdlib.rs +++ b/acvm/tests/stdlib.rs @@ -281,8 +281,12 @@ macro_rules! test_hashes { } // compile circuit - let circuit = Circuit {current_witness_index: witness_assignments.len() as u32 + 32, - opcodes, public_parameters: PublicInputs(BTreeSet::new()), return_values: PublicInputs(BTreeSet::new()) }; + let circuit = Circuit { + current_witness_index: witness_assignments.len() as u32 + 32, + opcodes, + private_parameters: BTreeSet::new(), // This is not correct but is unused in this test. + public_parameters: PublicInputs(BTreeSet::new()), + return_values: PublicInputs(BTreeSet::new()) }; let circuit = compile(circuit, Language::PLONKCSat{ width: 3 }, $opcode_support).unwrap().0; // solve witnesses @@ -333,8 +337,13 @@ proptest! { opcodes.push(Opcode::Arithmetic(output_constraint)); // compile circuit - let circuit = Circuit {current_witness_index: witness_assignments.len() as u32 + 1, - opcodes, public_parameters: PublicInputs(BTreeSet::new()), return_values: PublicInputs(BTreeSet::new()) }; + let circuit = Circuit { + current_witness_index: witness_assignments.len() as u32 + 1, + opcodes, + private_parameters: BTreeSet::new(), // This is not correct but is unused in this test. + public_parameters: PublicInputs(BTreeSet::new()), + return_values: PublicInputs(BTreeSet::new()) + }; let circuit = compile(circuit, Language::PLONKCSat{ width: 3 }, does_not_support_hash_to_field).unwrap().0; // solve witnesses