Skip to content

Commit

Permalink
feat: data bus (#3508)
Browse files Browse the repository at this point in the history
# Description

## Problem\*

aztec private kernel is doing a lot of proof recursion along the call
stack of contract calls, resulting in a lot of public inputs required
for the aztec ABI. These amount of public inputs have an impact on the
performance, which can be mitigated by having instead the public inputs
as private and a public commitment of these inputs. We can then leverage
the proving system capability for proving/verifying efficiently
commitments, outside of the noir program.

## Summary\*
In order to interface with the proving system, the user can declare some
inputs as call_data (instead of pub) and the return values as
return_data (instead of pub). The compiler will abstract these into
specific immutable arrays that the proving system will use.



## Additional Context



## Documentation\*

Check one:
- [ ] No documentation needed.
- [X] Documentation included in this PR.
- [ ] **[Exceptional Case]** 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
guipublic and TomAFrench authored Dec 1, 2023
1 parent e5b981b commit 6b0bdbc
Show file tree
Hide file tree
Showing 32 changed files with 402 additions and 67 deletions.
6 changes: 5 additions & 1 deletion compiler/noirc_driver/src/abi_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ use std::collections::BTreeMap;

use acvm::acir::native_types::Witness;
use iter_extended::{btree_map, vecmap};
use noirc_abi::{Abi, AbiParameter, AbiType};
use noirc_abi::{Abi, AbiParameter, AbiReturnType, AbiType};
use noirc_frontend::{
hir::Context,
hir_def::{function::Param, stmt::HirPattern},
node_interner::{FuncId, NodeInterner},
Visibility,
};
use std::ops::Range;

Expand All @@ -17,9 +18,12 @@ pub(super) fn gen_abi(
func_id: &FuncId,
input_witnesses: Vec<Witness>,
return_witnesses: Vec<Witness>,
return_visibility: Visibility,
) -> Abi {
let (parameters, return_type) = compute_function_abi(context, func_id);
let param_witnesses = param_witnesses_from_abi_param(&parameters, input_witnesses);
let return_type = return_type
.map(|typ| AbiReturnType { abi_type: typ, visibility: return_visibility.into() });
Abi { parameters, return_type, param_witnesses, return_witnesses }
}

Expand Down
5 changes: 3 additions & 2 deletions compiler/noirc_driver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,11 +346,12 @@ pub fn compile_no_check(
if !force_compile && hashes_match {
return Ok(cached_program.expect("cache must exist for hashes to match"));
}

let visibility = program.return_visibility;
let (circuit, debug, input_witnesses, return_witnesses, warnings) =
create_circuit(program, options.show_ssa, options.show_brillig)?;

let abi = abi_gen::gen_abi(context, &main_function, input_witnesses, return_witnesses);
let abi =
abi_gen::gen_abi(context, &main_function, input_witnesses, return_witnesses, visibility);
let file_map = filter_relevant_files(&[debug.clone()], &context.file_manager);

Ok(CompiledProgram {
Expand Down
24 changes: 23 additions & 1 deletion compiler/noirc_evaluator/src/ssa/acir_gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::collections::HashSet;
use std::fmt::Debug;

use self::acir_ir::acir_variable::{AcirContext, AcirType, AcirVar};
use super::function_builder::data_bus::DataBus;
use super::ir::dfg::CallStack;
use super::{
ir::{
Expand Down Expand Up @@ -90,6 +91,8 @@ struct Context {
/// Maps SSA array values to their slice size and any nested slices internal to the parent slice.
/// This enables us to maintain the slice structure of a slice when performing an array get.
slice_sizes: HashMap<Id<Value>, Vec<usize>>,

data_bus: DataBus,
}

#[derive(Clone)]
Expand Down Expand Up @@ -199,6 +202,7 @@ impl Context {
internal_mem_block_lengths: HashMap::default(),
max_block_id: 0,
slice_sizes: HashMap::default(),
data_bus: DataBus::default(),
}
}

Expand Down Expand Up @@ -226,6 +230,8 @@ impl Context {
let dfg = &main_func.dfg;
let entry_block = &dfg[main_func.entry_block()];
let input_witness = self.convert_ssa_block_params(entry_block.parameters(), dfg)?;

self.data_bus = dfg.data_bus.to_owned();
let mut warnings = Vec::new();
for instruction_id in entry_block.instructions() {
warnings.extend(self.convert_ssa_instruction(
Expand Down Expand Up @@ -892,10 +898,26 @@ impl Context {
dfg: &DataFlowGraph,
) -> Result<AcirValue, RuntimeError> {
let (array_id, _, block_id) = self.check_array_is_initialized(array, dfg)?;

let results = dfg.instruction_results(instruction);
let res_typ = dfg.type_of_value(results[0]);

// Get operations to call-data parameters are replaced by a get to the call-data-bus array
if let Some(call_data) = self.data_bus.call_data {
if self.data_bus.call_data_map.contains_key(&array_id) {
// TODO: the block_id of call-data must be notified to the backend
// TODO: should we do the same for return-data?
let type_size = res_typ.flattened_size();
let type_size =
self.acir_context.add_constant(FieldElement::from(type_size as i128));
let offset = self.acir_context.mul_var(var_index, type_size)?;
let bus_index = self.acir_context.add_constant(FieldElement::from(
self.data_bus.call_data_map[&array_id] as i128,
));
let new_index = self.acir_context.add_var(offset, bus_index)?;
return self.array_get(instruction, call_data, new_index, dfg);
}
}

let value = if !res_typ.contains_slice_element() {
self.array_get_value(&res_typ, block_id, &mut var_index, &[])?
} else {
Expand Down
144 changes: 144 additions & 0 deletions compiler/noirc_evaluator/src/ssa/function_builder/data_bus.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use std::rc::Rc;

use crate::ssa::ir::{types::Type, value::ValueId};
use acvm::FieldElement;
use fxhash::FxHashMap as HashMap;
use noirc_frontend::hir_def::function::FunctionSignature;

use super::FunctionBuilder;

/// Used to create a data bus, which is an array of private inputs
/// replacing public inputs
pub(crate) struct DataBusBuilder {
pub(crate) values: im::Vector<ValueId>,
index: usize,
pub(crate) map: HashMap<ValueId, usize>,
pub(crate) databus: Option<ValueId>,
}

impl DataBusBuilder {
pub(crate) fn new() -> DataBusBuilder {
DataBusBuilder {
index: 0,
map: HashMap::default(),
databus: None,
values: im::Vector::new(),
}
}

/// Generates a boolean vector telling which (ssa) parameter from the given function signature
/// are tagged with databus visibility
pub(crate) fn is_databus(main_signature: &FunctionSignature) -> Vec<bool> {
let mut params_is_databus = Vec::new();

for param in &main_signature.0 {
let is_databus = match param.2 {
noirc_frontend::Visibility::Public | noirc_frontend::Visibility::Private => false,
noirc_frontend::Visibility::DataBus => true,
};
let len = param.1.field_count() as usize;
params_is_databus.extend(vec![is_databus; len]);
}
params_is_databus
}
}

#[derive(Clone, Default, Debug)]
pub(crate) struct DataBus {
pub(crate) call_data: Option<ValueId>,
pub(crate) call_data_map: HashMap<ValueId, usize>,
pub(crate) return_data: Option<ValueId>,
}

impl DataBus {
/// Updates the databus values with the provided function
pub(crate) fn map_values(&self, mut f: impl FnMut(ValueId) -> ValueId) -> DataBus {
let mut call_data_map = HashMap::default();
for (k, v) in self.call_data_map.iter() {
call_data_map.insert(f(*k), *v);
}
DataBus {
call_data: self.call_data.map(&mut f),
call_data_map,
return_data: self.return_data.map(&mut f),
}
}

/// Construct a databus from call_data and return_data data bus builders
pub(crate) fn get_data_bus(call_data: DataBusBuilder, return_data: DataBusBuilder) -> DataBus {
DataBus {
call_data: call_data.databus,
call_data_map: call_data.map,
return_data: return_data.databus,
}
}
}

impl FunctionBuilder {
/// Insert a value into a data bus builder
fn add_to_data_bus(&mut self, value: ValueId, databus: &mut DataBusBuilder) {
assert!(databus.databus.is_none(), "initialising finalized call data");
let typ = self.current_function.dfg[value].get_type().clone();
match typ {
Type::Numeric(_) => {
databus.values.push_back(value);
databus.index += 1;
}
Type::Reference(_) => unreachable!(),
Type::Array(typ, len) => {
assert!(typ.len() == 1, "unsupported composite type");
databus.map.insert(value, databus.index);
for i in 0..len {
// load each element of the array
let index = self
.current_function
.dfg
.make_constant(FieldElement::from(i as i128), Type::field());
let element = self.insert_array_get(value, index, typ[0].clone());
self.add_to_data_bus(element, databus);
}
}
Type::Slice(_) => unreachable!(),
Type::Function => unreachable!(),
}
}

/// Create a data bus builder from a list of values
pub(crate) fn initialize_data_bus(
&mut self,
values: &[ValueId],
mut databus: DataBusBuilder,
) -> DataBusBuilder {
for value in values {
self.add_to_data_bus(*value, &mut databus);
}
let len = databus.values.len();

let array = if len > 0 {
let array =
self.array_constant(databus.values, Type::Array(Rc::new(vec![Type::field()]), len));
Some(array)
} else {
None
};

DataBusBuilder { index: 0, map: databus.map, databus: array, values: im::Vector::new() }
}

/// Generate the data bus for call-data, based on the parameters of the entry block
/// and a boolean vector telling which ones are call-data
pub(crate) fn call_data_bus(&mut self, is_params_databus: Vec<bool>) -> DataBusBuilder {
//filter parameters of the first block that have call-data visilibity
let first_block = self.current_function.entry_block();
let params = self.current_function.dfg[first_block].parameters();
let mut databus_param = Vec::new();
for (param, is_databus) in params.iter().zip(is_params_databus) {
if is_databus {
databus_param.push(param.to_owned());
}
}
// create the call-data-bus from the filtered list
let call_data = DataBusBuilder::new();
self.initialize_data_bus(&databus_param, call_data)
}
}
2 changes: 2 additions & 0 deletions compiler/noirc_evaluator/src/ssa/function_builder/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub(crate) mod data_bus;

use std::{borrow::Cow, rc::Rc};

use acvm::FieldElement;
Expand Down
4 changes: 3 additions & 1 deletion compiler/noirc_evaluator/src/ssa/ir/dfg.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::borrow::Cow;

use crate::ssa::ir::instruction::SimplifyResult;
use crate::ssa::{function_builder::data_bus::DataBus, ir::instruction::SimplifyResult};

use super::{
basic_block::{BasicBlock, BasicBlockId},
Expand Down Expand Up @@ -80,6 +80,8 @@ pub(crate) struct DataFlowGraph {
/// Instructions inserted by internal SSA passes that don't correspond to user code
/// may not have a corresponding location.
locations: HashMap<InstructionId, CallStack>,

pub(crate) data_bus: DataBus,
}

pub(crate) type CallStack = im::Vector<Location>;
Expand Down
4 changes: 4 additions & 0 deletions compiler/noirc_evaluator/src/ssa/opt/die.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ impl Ssa {
/// of its instructions are needed elsewhere.
fn dead_instruction_elimination(function: &mut Function) {
let mut context = Context::default();
if let Some(call_data) = function.dfg.data_bus.call_data {
context.mark_used_instruction_results(&function.dfg, call_data);
}

let blocks = PostOrder::with_function(function);

for block in blocks.as_slice() {
Expand Down
3 changes: 3 additions & 0 deletions compiler/noirc_evaluator/src/ssa/opt/fill_internal_slices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ impl Ssa {
// The pass is also currently only setup to handle a function with a single flattened block.
// For complex Brillig functions we can expect this pass to panic.
if function.runtime() == RuntimeType::Acir {
let databus = function.dfg.data_bus.clone();
let mut context = Context::new(function);
context.process_blocks();
// update the databus with the new array instructions
function.dfg.data_bus = databus.map_values(|t| context.inserter.resolve(t));
}
}
self
Expand Down
6 changes: 5 additions & 1 deletion compiler/noirc_evaluator/src/ssa/opt/inlining.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,15 @@ impl InlineContext {

context.blocks.insert(context.source_function.entry_block(), entry_block);
context.inline_blocks(ssa);
// translate databus values
let databus = entry_point.dfg.data_bus.map_values(|t| context.translate_value(t));

// Finally, we should have 1 function left representing the inlined version of the target function.
let mut new_ssa = self.builder.finish();
assert_eq!(new_ssa.functions.len(), 1);
new_ssa.functions.pop_first().unwrap().1
let mut new_func = new_ssa.functions.pop_first().unwrap().1;
new_func.dfg.data_bus = databus;
new_func
}

/// Inlines a function into the current function and returns the translated return values
Expand Down
6 changes: 6 additions & 0 deletions compiler/noirc_evaluator/src/ssa/opt/mem2reg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ impl Ssa {
let mut context = PerFunctionContext::new(function);
context.mem2reg();
context.remove_instructions();
context.update_data_bus();
}
self
}
Expand Down Expand Up @@ -362,6 +363,11 @@ impl<'f> PerFunctionContext<'f> {
}
}

fn update_data_bus(&mut self) {
let databus = self.inserter.function.dfg.data_bus.clone();
self.inserter.function.dfg.data_bus = databus.map_values(|t| self.inserter.resolve(t));
}

fn handle_terminator(&mut self, block: BasicBlockId, references: &mut Block) {
self.inserter.map_terminator_in_place(block);

Expand Down
Loading

0 comments on commit 6b0bdbc

Please sign in to comment.