Skip to content

Commit e4a2fe9

Browse files
chore: add bigint solver in ACVM and add a unit test for bigints in Noir (AztecProtocol#4415)
The PR enable bigints in ACVM, i.e you can now compile and execute Noir programs using bigint opcodes. This is demonstrated in the added unit test. --------- Co-authored-by: kevaundray <[email protected]>
1 parent f09e8fc commit e4a2fe9

File tree

9 files changed

+213
-18
lines changed

9 files changed

+213
-18
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use std::collections::HashMap;
2+
3+
use acir::{
4+
circuit::opcodes::FunctionInput,
5+
native_types::{Witness, WitnessMap},
6+
BlackBoxFunc, FieldElement,
7+
};
8+
9+
use num_bigint::BigUint;
10+
11+
use crate::pwg::OpcodeResolutionError;
12+
13+
/// Resolve BigInt opcodes by storing BigInt values (and their moduli) by their ID in a HashMap:
14+
/// - When it encounters a bigint operation opcode, it performs the operation on the stored values
15+
/// and store the result using the provided ID.
16+
/// - When it gets a to_bytes opcode, it simply looks up the value and resolves the output witness accordingly.
17+
#[derive(Default)]
18+
pub(crate) struct BigIntSolver {
19+
bigint_id_to_value: HashMap<u32, BigUint>,
20+
bigint_id_to_modulus: HashMap<u32, BigUint>,
21+
}
22+
23+
impl BigIntSolver {
24+
pub(crate) fn get_bigint(
25+
&self,
26+
id: u32,
27+
func: BlackBoxFunc,
28+
) -> Result<BigUint, OpcodeResolutionError> {
29+
self.bigint_id_to_value
30+
.get(&id)
31+
.ok_or(OpcodeResolutionError::BlackBoxFunctionFailed(
32+
func,
33+
format!("could not find bigint of id {id}"),
34+
))
35+
.cloned()
36+
}
37+
38+
pub(crate) fn get_modulus(
39+
&self,
40+
id: u32,
41+
func: BlackBoxFunc,
42+
) -> Result<BigUint, OpcodeResolutionError> {
43+
self.bigint_id_to_modulus
44+
.get(&id)
45+
.ok_or(OpcodeResolutionError::BlackBoxFunctionFailed(
46+
func,
47+
format!("could not find bigint of id {id}"),
48+
))
49+
.cloned()
50+
}
51+
pub(crate) fn bigint_from_bytes(
52+
&mut self,
53+
inputs: &[FunctionInput],
54+
modulus: &[u8],
55+
output: u32,
56+
initial_witness: &mut WitnessMap,
57+
) -> Result<(), OpcodeResolutionError> {
58+
let bytes = inputs
59+
.iter()
60+
.map(|input| initial_witness.get(&input.witness).unwrap().to_u128() as u8)
61+
.collect::<Vec<u8>>();
62+
let bigint = BigUint::from_bytes_le(&bytes);
63+
self.bigint_id_to_value.insert(output, bigint);
64+
let modulus = BigUint::from_bytes_le(modulus);
65+
self.bigint_id_to_modulus.insert(output, modulus);
66+
Ok(())
67+
}
68+
69+
pub(crate) fn bigint_to_bytes(
70+
&self,
71+
input: u32,
72+
outputs: &Vec<Witness>,
73+
initial_witness: &mut WitnessMap,
74+
) -> Result<(), OpcodeResolutionError> {
75+
let bigint = self.get_bigint(input, BlackBoxFunc::BigIntToLeBytes)?;
76+
77+
let mut bytes = bigint.to_bytes_le();
78+
while bytes.len() < outputs.len() {
79+
bytes.push(0);
80+
}
81+
bytes.iter().zip(outputs.iter()).for_each(|(byte, output)| {
82+
initial_witness.insert(*output, FieldElement::from(*byte as u128));
83+
});
84+
Ok(())
85+
}
86+
87+
pub(crate) fn bigint_op(
88+
&mut self,
89+
lhs: u32,
90+
rhs: u32,
91+
output: u32,
92+
func: BlackBoxFunc,
93+
) -> Result<(), OpcodeResolutionError> {
94+
let modulus = self.get_modulus(lhs, func)?;
95+
let lhs = self.get_bigint(lhs, func)?;
96+
let rhs = self.get_bigint(rhs, func)?;
97+
let mut result = match func {
98+
BlackBoxFunc::BigIntAdd => lhs + rhs,
99+
BlackBoxFunc::BigIntNeg => {
100+
if lhs >= rhs {
101+
&lhs - &rhs
102+
} else {
103+
&lhs + &modulus - &rhs
104+
}
105+
}
106+
BlackBoxFunc::BigIntMul => lhs * rhs,
107+
BlackBoxFunc::BigIntDiv => {
108+
lhs * rhs.modpow(&(&modulus - BigUint::from(1_u32)), &modulus)
109+
} //TODO ensure that modulus is prime
110+
_ => unreachable!("ICE - bigint_op must be called for an operation"),
111+
};
112+
if result > modulus {
113+
let q = &result / &modulus;
114+
result -= q * &modulus;
115+
}
116+
self.bigint_id_to_value.insert(output, result);
117+
self.bigint_id_to_modulus.insert(output, modulus);
118+
Ok(())
119+
}
120+
}

noir/acvm-repo/acvm/src/pwg/blackbox/mod.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ use acir::{
55
};
66
use acvm_blackbox_solver::{blake2s, blake3, keccak256, keccakf1600, sha256};
77

8-
use self::pedersen::pedersen_hash;
8+
use self::{bigint::BigIntSolver, pedersen::pedersen_hash};
99

1010
use super::{insert_value, OpcodeNotSolvable, OpcodeResolutionError};
1111
use crate::{pwg::witness_to_value, BlackBoxFunctionSolver};
1212

13+
pub(crate) mod bigint;
1314
mod fixed_base_scalar_mul;
1415
mod hash;
1516
mod logic;
@@ -53,6 +54,7 @@ pub(crate) fn solve(
5354
backend: &impl BlackBoxFunctionSolver,
5455
initial_witness: &mut WitnessMap,
5556
bb_func: &BlackBoxFuncCall,
57+
bigint_solver: &mut BigIntSolver,
5658
) -> Result<(), OpcodeResolutionError> {
5759
let inputs = bb_func.get_inputs_vec();
5860
if !contains_all_inputs(initial_witness, &inputs) {
@@ -190,12 +192,18 @@ pub(crate) fn solve(
190192
}
191193
// Recursive aggregation will be entirely handled by the backend and is not solved by the ACVM
192194
BlackBoxFuncCall::RecursiveAggregation { .. } => Ok(()),
193-
BlackBoxFuncCall::BigIntAdd { .. } => todo!(),
194-
BlackBoxFuncCall::BigIntNeg { .. } => todo!(),
195-
BlackBoxFuncCall::BigIntMul { .. } => todo!(),
196-
BlackBoxFuncCall::BigIntDiv { .. } => todo!(),
197-
BlackBoxFuncCall::BigIntFromLeBytes { .. } => todo!(),
198-
BlackBoxFuncCall::BigIntToLeBytes { .. } => todo!(),
195+
BlackBoxFuncCall::BigIntAdd { lhs, rhs, output }
196+
| BlackBoxFuncCall::BigIntNeg { lhs, rhs, output }
197+
| BlackBoxFuncCall::BigIntMul { lhs, rhs, output }
198+
| BlackBoxFuncCall::BigIntDiv { lhs, rhs, output } => {
199+
bigint_solver.bigint_op(*lhs, *rhs, *output, bb_func.get_black_box_func())
200+
}
201+
BlackBoxFuncCall::BigIntFromLeBytes { inputs, modulus, output } => {
202+
bigint_solver.bigint_from_bytes(inputs, modulus, *output, initial_witness)
203+
}
204+
BlackBoxFuncCall::BigIntToLeBytes { input, outputs } => {
205+
bigint_solver.bigint_to_bytes(*input, outputs, initial_witness)
206+
}
199207
BlackBoxFuncCall::Poseidon2Permutation { .. } => todo!(),
200208
BlackBoxFuncCall::Sha256Compression { .. } => todo!(),
201209
}

noir/acvm-repo/acvm/src/pwg/mod.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use acir::{
1010
};
1111
use acvm_blackbox_solver::BlackBoxResolutionError;
1212

13-
use self::{arithmetic::ExpressionSolver, directives::solve_directives, memory_op::MemoryOpSolver};
13+
use self::{
14+
arithmetic::ExpressionSolver, blackbox::bigint::BigIntSolver, directives::solve_directives,
15+
memory_op::MemoryOpSolver,
16+
};
1417
use crate::BlackBoxFunctionSolver;
1518

1619
use thiserror::Error;
@@ -132,6 +135,8 @@ pub struct ACVM<'a, B: BlackBoxFunctionSolver> {
132135
/// Stores the solver for memory operations acting on blocks of memory disambiguated by [block][`BlockId`].
133136
block_solvers: HashMap<BlockId, MemoryOpSolver>,
134137

138+
bigint_solver: BigIntSolver,
139+
135140
/// A list of opcodes which are to be executed by the ACVM.
136141
opcodes: &'a [Opcode],
137142
/// Index of the next opcode to be executed.
@@ -149,6 +154,7 @@ impl<'a, B: BlackBoxFunctionSolver> ACVM<'a, B> {
149154
status,
150155
backend,
151156
block_solvers: HashMap::default(),
157+
bigint_solver: BigIntSolver::default(),
152158
opcodes,
153159
instruction_pointer: 0,
154160
witness_map: initial_witness,
@@ -254,9 +260,12 @@ impl<'a, B: BlackBoxFunctionSolver> ACVM<'a, B> {
254260

255261
let resolution = match opcode {
256262
Opcode::AssertZero(expr) => ExpressionSolver::solve(&mut self.witness_map, expr),
257-
Opcode::BlackBoxFuncCall(bb_func) => {
258-
blackbox::solve(self.backend, &mut self.witness_map, bb_func)
259-
}
263+
Opcode::BlackBoxFuncCall(bb_func) => blackbox::solve(
264+
self.backend,
265+
&mut self.witness_map,
266+
bb_func,
267+
&mut self.bigint_solver,
268+
),
260269
Opcode::Directive(directive) => solve_directives(&mut self.witness_map, directive),
261270
Opcode::MemoryInit { block_id, init } => {
262271
let solver = self.block_solvers.entry(*block_id).or_default();

noir/compiler/noirc_evaluator/src/ssa/acir_gen/acir_ir/acir_variable.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1245,7 +1245,8 @@ impl AcirContext {
12451245
for i in const_inputs {
12461246
field_inputs.push(i?);
12471247
}
1248-
let modulus = self.big_int_ctx.modulus(field_inputs[0]);
1248+
let bigint = self.big_int_ctx.get(field_inputs[0]);
1249+
let modulus = self.big_int_ctx.modulus(bigint.modulus_id());
12491250
let bytes_len = ((modulus - BigUint::from(1_u32)).bits() - 1) / 8 + 1;
12501251
output_count = bytes_len as usize;
12511252
(field_inputs, vec![FieldElement::from(bytes_len as u128)])

noir/noir_stdlib/src/bigint.nr

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,55 @@
11
use crate::ops::{Add, Sub, Mul, Div, Rem,};
22

3+
4+
global bn254_fq = [0x47, 0xFD, 0x7C, 0xD8, 0x16, 0x8C, 0x20, 0x3C, 0x8d, 0xca, 0x71, 0x68, 0x91, 0x6a, 0x81, 0x97,
5+
0x5d, 0x58, 0x81, 0x81, 0xb6, 0x45, 0x50, 0xb8, 0x29, 0xa0, 0x31, 0xe1, 0x72, 0x4e, 0x64, 0x30];
6+
global bn254_fr = [0x01, 0x00, 0x00, 0x00, 0x3F, 0x59, 0x1F, 0x43, 0x09, 0x97, 0xB9, 0x79, 0x48, 0xE8, 0x33, 0x28,
7+
0x5D, 0x58, 0x81, 0x81, 0xB6, 0x45, 0x50, 0xB8, 0x29, 0xA0, 0x31, 0xE1, 0x72, 0x4E, 0x64, 0x30];
8+
global secpk1_fr = [0x41, 0x41, 0x36, 0xD0, 0x8C, 0x5E, 0xD2, 0xBF, 0x3B, 0xA0, 0x48, 0xAF, 0xE6, 0xDC, 0xAE, 0xBA,
9+
0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
10+
global secpk1_fq = [0x2F, 0xFC, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
11+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
12+
global secpr1_fq = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
13+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF];
14+
global secpr1_fr = [0x51, 0x25, 0x63, 0xFC, 0xC2, 0xCA, 0xB9, 0xF3, 0x84, 0x9E, 0x17, 0xA7, 0xAD, 0xFA, 0xE6, 0xBC,
15+
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,0xFF, 0xFF, 0xFF, 0xFF];
16+
17+
318
struct BigInt {
419
pointer: u32,
520
modulus: u32,
621
}
722

823
impl BigInt {
924
#[builtin(bigint_add)]
10-
pub fn bigint_add(self, other: BigInt) -> BigInt {
25+
fn bigint_add(self, other: BigInt) -> BigInt {
1126
}
1227
#[builtin(bigint_neg)]
13-
pub fn bigint_neg(self, other: BigInt) -> BigInt {
28+
fn bigint_neg(self, other: BigInt) -> BigInt {
1429
}
1530
#[builtin(bigint_mul)]
16-
pub fn bigint_mul(self, other: BigInt) -> BigInt {
31+
fn bigint_mul(self, other: BigInt) -> BigInt {
1732
}
1833
#[builtin(bigint_div)]
19-
pub fn bigint_div(self, other: BigInt) -> BigInt {
34+
fn bigint_div(self, other: BigInt) -> BigInt {
2035
}
2136
#[builtin(bigint_from_le_bytes)]
22-
pub fn from_le_bytes(bytes: [u8], modulus: [u8]) -> BigInt {}
37+
fn from_le_bytes(bytes: [u8], modulus: [u8]) -> BigInt {}
2338
#[builtin(bigint_to_le_bytes)]
2439
pub fn to_le_bytes(self) -> [u8] {}
40+
41+
pub fn bn254_fr_from_le_bytes(bytes: [u8]) -> BigInt {
42+
BigInt::from_le_bytes(bytes, bn254_fr)
43+
}
44+
pub fn bn254_fq_from_le_bytes(bytes: [u8]) -> BigInt {
45+
BigInt::from_le_bytes(bytes, bn254_fq)
46+
}
47+
pub fn secpk1_fq_from_le_bytes(bytes: [u8]) -> BigInt {
48+
BigInt::from_le_bytes(bytes, secpk1_fq)
49+
}
50+
pub fn secpk1_fr_from_le_bytes(bytes: [u8]) -> BigInt {
51+
BigInt::from_le_bytes(bytes, secpk1_fr)
52+
}
2553
}
2654

2755
impl Add for BigInt {

noir/noir_stdlib/src/lib.nr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ mod ops;
2525
mod default;
2626
mod prelude;
2727
mod uint128;
28-
// mod bigint;
28+
mod bigint;
2929

3030
// Oracle calls are required to be wrapped in an unconstrained function
3131
// Thus, the only argument to the `println` oracle is expected to always be an ident
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "bigint"
3+
type = "bin"
4+
authors = [""]
5+
6+
[dependencies]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
x = [34,3,5,8,4]
2+
y = [44,7,1,8,8]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use dep::std::bigint;
2+
3+
fn main(mut x: [u8;5], y: [u8;5]) {
4+
let a = bigint::BigInt::secpk1_fq_from_le_bytes([x[0],x[1],x[2],x[3],x[4]]);
5+
let b = bigint::BigInt::secpk1_fq_from_le_bytes([y[0],y[1],y[2],y[3],y[4]]);
6+
7+
let a_bytes = a.to_le_bytes();
8+
let b_bytes = b.to_le_bytes();
9+
for i in 0..5 {
10+
assert(a_bytes[i] == x[i]);
11+
assert(b_bytes[i] == y[i]);
12+
}
13+
14+
let d = a*b - b;
15+
let d_bytes = d.to_le_bytes();
16+
let d1 = bigint::BigInt::secpk1_fq_from_le_bytes(597243850900842442924.to_le_bytes(10));
17+
let d1_bytes = d1.to_le_bytes();
18+
for i in 0..32 {
19+
assert(d_bytes[i] == d1_bytes[i]);
20+
}
21+
}

0 commit comments

Comments
 (0)