Skip to content

Commit

Permalink
feat: implement bigint in Noir, using bigint opcodes (#4198)
Browse files Browse the repository at this point in the history
This PR enables big integers in Noir using the bigint opcodes.
It does not implement bigints in ACVM which is required to solve the
opcodes.
It also does not implement bigints opcodes in Barretenberg, which is
required to generate the constraints.

However it provides a BigInt structure in the stdlib so you can write
code like this in Noir, which will be compiled into ACIR using the
bigint opcodes.

```
    let a = bigint::BigInt::from_le_bytes([3], [23]);
    let b = bigint::BigInt::from_le_bytes([5], [23]);
    let c = a+b;
    let bytes = c.to_le_bytes();
    std::println(bytes[0]);

```

I did not add a test yet because ACMV solver is not yet implemented.

---------

Co-authored-by: kevaundray <[email protected]>
  • Loading branch information
guipublic and kevaundray authored Jan 25, 2024
1 parent a656034 commit 3720415
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 38 deletions.
5 changes: 4 additions & 1 deletion noir/compiler/noirc_evaluator/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ pub enum RuntimeError {
AssertConstantFailed { call_stack: CallStack },
#[error("Nested slices are not supported")]
NestedSlice { call_stack: CallStack },
#[error("Big Integer modulus do no match")]
BigIntModulus { call_stack: CallStack },
}

// We avoid showing the actual lhs and rhs since most of the time they are just 0
Expand Down Expand Up @@ -132,7 +134,8 @@ impl RuntimeError {
| RuntimeError::AssertConstantFailed { call_stack }
| RuntimeError::IntegerOutOfBounds { call_stack, .. }
| RuntimeError::UnsupportedIntegerSize { call_stack, .. }
| RuntimeError::NestedSlice { call_stack, .. } => call_stack,
| RuntimeError::NestedSlice { call_stack, .. }
| RuntimeError::BigIntModulus { call_stack, .. } => call_stack,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions noir/compiler/noirc_evaluator/src/ssa/acir_gen/acir_ir.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub(crate) mod acir_variable;
pub(crate) mod big_int;
pub(crate) mod generated_acir;
pub(crate) mod sort;
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::big_int::BigIntContext;
use super::generated_acir::GeneratedAcir;
use crate::brillig::brillig_gen::brillig_directive;
use crate::brillig::brillig_ir::artifact::GeneratedBrillig;
Expand Down Expand Up @@ -107,6 +108,9 @@ pub(crate) struct AcirContext {
/// then the `acir_ir` will be populated to assert this
/// addition.
acir_ir: GeneratedAcir,

/// The BigIntContext, used to generate identifiers for BigIntegers
big_int_ctx: BigIntContext,
}

impl AcirContext {
Expand Down Expand Up @@ -1140,10 +1144,10 @@ impl AcirContext {
&mut self,
name: BlackBoxFunc,
mut inputs: Vec<AcirValue>,
output_count: usize,
mut output_count: usize,
) -> Result<Vec<AcirVar>, RuntimeError> {
// Separate out any arguments that should be constants
let constants = match name {
let (constant_inputs, constant_outputs) = match name {
BlackBoxFunc::PedersenCommitment | BlackBoxFunc::PedersenHash => {
// The last argument of pedersen is the domain separator, which must be a constant
let domain_var = match inputs.pop() {
Expand All @@ -1167,23 +1171,116 @@ impl AcirContext {
}
};

vec![domain_constant]
(vec![domain_constant], Vec::new())
}
BlackBoxFunc::BigIntAdd
| BlackBoxFunc::BigIntNeg
| BlackBoxFunc::BigIntMul
| BlackBoxFunc::BigIntDiv => {
assert_eq!(inputs.len(), 4, "ICE - bigint operation requires 4 inputs");
let const_inputs = vecmap(inputs, |i| {
let var = i.into_var()?;
match self.vars[&var].as_constant() {
Some(const_var) => Ok(const_var),
None => Err(RuntimeError::InternalError(InternalError::NotAConstant {
name: "big integer".to_string(),
call_stack: self.get_call_stack(),
})),
}
});
inputs = Vec::new();
output_count = 0;
let mut field_inputs = Vec::new();
for i in const_inputs {
field_inputs.push(i?);
}
if field_inputs[1] != field_inputs[3] {
return Err(RuntimeError::BigIntModulus { call_stack: self.get_call_stack() });
}

let result_id = self.big_int_ctx.new_big_int(field_inputs[1]);
(
vec![field_inputs[0], field_inputs[2]],
vec![result_id.bigint_id(), result_id.modulus_id()],
)
}
BlackBoxFunc::BigIntToLeBytes => {
let const_inputs = vecmap(inputs, |i| {
let var = i.into_var()?;
match self.vars[&var].as_constant() {
Some(const_var) => Ok(const_var),
None => Err(RuntimeError::InternalError(InternalError::NotAConstant {
name: "big integer".to_string(),
call_stack: self.get_call_stack(),
})),
}
});
inputs = Vec::new();
let mut field_inputs = Vec::new();
for i in const_inputs {
field_inputs.push(i?);
}
let modulus = self.big_int_ctx.modulus(field_inputs[0]);
let bytes_len = ((modulus - BigUint::from(1_u32)).bits() - 1) / 8 + 1;
output_count = bytes_len as usize;
(field_inputs, vec![FieldElement::from(bytes_len as u128)])
}
BlackBoxFunc::BigIntFromLeBytes => {
let invalid_input = "ICE - bigint operation requires 2 inputs";
assert_eq!(inputs.len(), 2, "{invalid_input}");
let mut modulus = Vec::new();
match inputs.pop().expect(invalid_input) {
AcirValue::Array(values) => {
for value in values {
modulus.push(self.vars[&value.into_var()?].as_constant().ok_or(
RuntimeError::InternalError(InternalError::NotAConstant {
name: "big integer".to_string(),
call_stack: self.get_call_stack(),
}),
)?);
}
}
_ => {
return Err(RuntimeError::InternalError(InternalError::MissingArg {
name: "big_int_from_le_bytes".to_owned(),
arg: "modulus".to_owned(),
call_stack: self.get_call_stack(),
}));
}
}
let big_modulus = BigUint::from_bytes_le(&vecmap(&modulus, |b| b.to_u128() as u8));
output_count = 0;

let modulus_id = self.big_int_ctx.get_or_insert_modulus(big_modulus);
let result_id =
self.big_int_ctx.new_big_int(FieldElement::from(modulus_id as u128));
(modulus, vec![result_id.bigint_id(), result_id.modulus_id()])
}
_ => vec![],
_ => (vec![], vec![]),
};

// Convert `AcirVar` to `FunctionInput`
let inputs = self.prepare_inputs_for_black_box_func_call(inputs)?;

// Call Black box with `FunctionInput`
let outputs = self.acir_ir.call_black_box(name, &inputs, constants, output_count)?;
let mut results = vecmap(&constant_outputs, |c| self.add_constant(*c));
let outputs = self.acir_ir.call_black_box(
name,
&inputs,
constant_inputs,
constant_outputs,
output_count,
)?;

// Convert `Witness` values which are now constrained to be the output of the
// black box function call into `AcirVar`s.
//
// We do not apply range information on the output of the black box function.
// See issue #1439
Ok(vecmap(&outputs, |witness_index| self.add_data(AcirVarData::Witness(*witness_index))))
results.extend(vecmap(&outputs, |witness_index| {
self.add_data(AcirVarData::Witness(*witness_index))
}));
Ok(results)
}

/// Black box function calls expect their inputs to be in a specific data structure (FunctionInput).
Expand Down
57 changes: 57 additions & 0 deletions noir/compiler/noirc_evaluator/src/ssa/acir_gen/acir_ir/big_int.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use acvm::FieldElement;
use num_bigint::BigUint;

/// Represents a bigint value in the form (id, modulus) where
/// id is the identifier of the big integer number, and
/// modulus is the identifier of the big integer size
#[derive(Default, Clone, Copy, Debug)]
pub(crate) struct BigIntId {
pub(crate) bigint_id: u32,
pub(crate) modulus_id: u32,
}

impl BigIntId {
pub(crate) fn bigint_id(&self) -> FieldElement {
FieldElement::from(self.bigint_id as u128)
}

pub(crate) fn modulus_id(&self) -> FieldElement {
FieldElement::from(self.modulus_id as u128)
}
}

/// BigIntContext is used to generate identifiers for big integers and their modulus
#[derive(Default, Debug)]
pub(crate) struct BigIntContext {
modulus: Vec<BigUint>,
big_integers: Vec<BigIntId>,
}

impl BigIntContext {
/// Creates a new BigIntId for the given modulus identifier and returns it.
pub(crate) fn new_big_int(&mut self, modulus_id: FieldElement) -> BigIntId {
let id = self.big_integers.len() as u32;
let result = BigIntId { bigint_id: id, modulus_id: modulus_id.to_u128() as u32 };
self.big_integers.push(result);
result
}

/// Returns the modulus corresponding to the given modulus index
pub(crate) fn modulus(&self, idx: FieldElement) -> BigUint {
self.modulus[idx.to_u128() as usize].clone()
}

/// Returns the BigIntId corresponding to the given identifier
pub(crate) fn get(&self, id: FieldElement) -> BigIntId {
self.big_integers[id.to_u128() as usize]
}

/// Adds a modulus to the context (if it is not already present)
pub(crate) fn get_or_insert_modulus(&mut self, modulus: BigUint) -> u32 {
if let Some(pos) = self.modulus.iter().position(|x| x == &modulus) {
return pos as u32;
}
self.modulus.push(modulus);
(self.modulus.len() - 1) as u32
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ impl GeneratedAcir {
&mut self,
func_name: BlackBoxFunc,
inputs: &[Vec<FunctionInput>],
constants: Vec<FieldElement>,
constant_inputs: Vec<FieldElement>,
constant_outputs: Vec<FieldElement>,
output_count: usize,
) -> Result<Vec<Witness>, InternalError> {
let input_count = inputs.iter().fold(0usize, |sum, val| sum + val.len());
Expand Down Expand Up @@ -176,12 +177,12 @@ impl GeneratedAcir {
BlackBoxFunc::PedersenCommitment => BlackBoxFuncCall::PedersenCommitment {
inputs: inputs[0].clone(),
outputs: (outputs[0], outputs[1]),
domain_separator: constants[0].to_u128() as u32,
domain_separator: constant_inputs[0].to_u128() as u32,
},
BlackBoxFunc::PedersenHash => BlackBoxFuncCall::PedersenHash {
inputs: inputs[0].clone(),
output: outputs[0],
domain_separator: constants[0].to_u128() as u32,
domain_separator: constant_inputs[0].to_u128() as u32,
},
BlackBoxFunc::EcdsaSecp256k1 => {
BlackBoxFuncCall::EcdsaSecp256k1 {
Expand Down Expand Up @@ -252,33 +253,34 @@ impl GeneratedAcir {
key_hash: inputs[3][0],
},
BlackBoxFunc::BigIntAdd => BlackBoxFuncCall::BigIntAdd {
lhs: constants[0].to_u128() as u32,
rhs: constants[1].to_u128() as u32,
output: constants[2].to_u128() as u32,
lhs: constant_inputs[0].to_u128() as u32,
rhs: constant_inputs[1].to_u128() as u32,
output: constant_outputs[0].to_u128() as u32,
},
BlackBoxFunc::BigIntNeg => BlackBoxFuncCall::BigIntNeg {
lhs: constants[0].to_u128() as u32,
rhs: constants[1].to_u128() as u32,
output: constants[2].to_u128() as u32,
lhs: constant_inputs[0].to_u128() as u32,
rhs: constant_inputs[1].to_u128() as u32,
output: constant_outputs[0].to_u128() as u32,
},
BlackBoxFunc::BigIntMul => BlackBoxFuncCall::BigIntMul {
lhs: constants[0].to_u128() as u32,
rhs: constants[1].to_u128() as u32,
output: constants[2].to_u128() as u32,
lhs: constant_inputs[0].to_u128() as u32,
rhs: constant_inputs[1].to_u128() as u32,
output: constant_outputs[0].to_u128() as u32,
},
BlackBoxFunc::BigIntDiv => BlackBoxFuncCall::BigIntDiv {
lhs: constants[0].to_u128() as u32,
rhs: constants[1].to_u128() as u32,
output: constants[2].to_u128() as u32,
lhs: constant_inputs[0].to_u128() as u32,
rhs: constant_inputs[1].to_u128() as u32,
output: constant_outputs[0].to_u128() as u32,
},
BlackBoxFunc::BigIntFromLeBytes => BlackBoxFuncCall::BigIntFromLeBytes {
inputs: inputs[0].clone(),
modulus: vecmap(constants, |c| c.to_u128() as u8),
output: todo!(),
modulus: vecmap(constant_inputs, |c| c.to_u128() as u8),
output: constant_outputs[0].to_u128() as u32,
},
BlackBoxFunc::BigIntToLeBytes => BlackBoxFuncCall::BigIntToLeBytes {
input: constant_inputs[0].to_u128() as u32,
outputs,
},
BlackBoxFunc::BigIntToLeBytes => {
BlackBoxFuncCall::BigIntToLeBytes { input: constants[0].to_u128() as u32, outputs }
}
};

self.push_opcode(AcirOpcode::BlackBoxFuncCall(black_box_func_call));
Expand Down Expand Up @@ -636,16 +638,15 @@ fn black_box_func_expected_input_size(name: BlackBoxFunc) -> Option<usize> {
// Doubling over the embedded curve: input is (x,y) coordinate of the point.
BlackBoxFunc::EmbeddedCurveDouble => Some(2),

// Big integer operations take in 2 inputs
// Big integer operations take in 0 inputs. They use constants for their inputs.
BlackBoxFunc::BigIntAdd
| BlackBoxFunc::BigIntNeg
| BlackBoxFunc::BigIntMul
| BlackBoxFunc::BigIntDiv => Some(2),
| BlackBoxFunc::BigIntDiv
| BlackBoxFunc::BigIntToLeBytes => Some(0),

// FromLeBytes takes a variable array of bytes as input
BlackBoxFunc::BigIntFromLeBytes => None,
// ToLeBytes takes a single big integer as input
BlackBoxFunc::BigIntToLeBytes => Some(1),
}
}

Expand Down Expand Up @@ -691,7 +692,7 @@ fn black_box_expected_output_size(name: BlackBoxFunc) -> Option<usize> {
| BlackBoxFunc::BigIntNeg
| BlackBoxFunc::BigIntMul
| BlackBoxFunc::BigIntDiv
| BlackBoxFunc::BigIntFromLeBytes => Some(1),
| BlackBoxFunc::BigIntFromLeBytes => Some(0),

// ToLeBytes returns a variable array of bytes
BlackBoxFunc::BigIntToLeBytes => None,
Expand Down Expand Up @@ -752,5 +753,5 @@ fn intrinsics_check_outputs(name: BlackBoxFunc, output_count: usize) {
None => return,
};

assert_eq!(expected_num_outputs,output_count,"Tried to call black box function {name} with {output_count} inputs, but this function's definition requires {expected_num_outputs} inputs");
assert_eq!(expected_num_outputs,output_count,"Tried to call black box function {name} with {output_count} outputs, but this function's definition requires {expected_num_outputs} outputs");
}
Loading

0 comments on commit 3720415

Please sign in to comment.