Skip to content

Commit 1f9cad0

Browse files
authored
feat: Added efficient field comparisons for bn254 (#4042)
# Description Adds efficient field comparisons for the bn254 field. ## Problem\* Previously, field comparisons were made by decomposing to bytes. This PR optimizes this by decomposing the field in an unconstrained function. ## Summary\* Field comparisons via byte decomposing had a cost of ~850 constraints. This PR adds some bn254 specific functions to: - assert_gt / assert_lt : ~77 constraints - gt/lt: ~190 constraints ## Additional Context @zac-williamson explanation: decompose(x: field) returns xlo, xhi as fields The algorithm requires knowledge of the field modulus p , as well as it’s 128-bit components plo, phi such that p = plo + phi * 2^{128} plo, phi need to be defined as circuit constants in an unconstrained function: 1. compute xlo, xhi such that x = xlo + 2^{128}xhi 2. compute borrow: bool such that borrow = xlo > plo in a constrained function: 1. assert xlo < 2^{128} 2. assert xhi < 2^{128} 3. assert(x == xlo + 2^{128}xhi 4. let rlo = plo - xlo + borrow*2^{128} 5. let rhi = phi - xhi - borrow 6. assert rlo < 2^{128} 7. assert rhi < 2^{128} --- The purpose of rlo, rhi is to ensure that xlo + 2^{128}xhi does not wrap around the field modulus p With a decompose method implemented, a assert_gt(a, b) method can be built assert_gt(a: field, b: field) 1. let alo, ahi = decompose(a) 2. let blo, bhi = decompose(b) If a > b then we want a - b - 1 >= 0 . We can evaluate this relation over each of the 128-bit slices: in an unconstrained function, let borrow: bool = (alo <= blo) in a constrained function: 1. let rlo = alo - blo - 1 + borrow*2^{128} 2. let rhi = ahi - bhi - borrow 3. assert rlo < 2^{128} 4. assert rhi < 2^{128} --- Also a gt/lt method is provided leveraging a hint + assert_gt. ## Documentation\* Check one: - [ ] No documentation needed. - [ ] 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.
1 parent c631e1b commit 1f9cad0

9 files changed

Lines changed: 241 additions & 20 deletions

File tree

noir_stdlib/src/eddsa.nr

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,7 @@
11
use crate::hash::poseidon;
22
use crate::ec::consts::te::baby_jubjub;
33
use crate::ec::tecurve::affine::Point as TEPoint;
4-
// Returns true if x is less than y
5-
fn lt_bytes32(x: Field, y: Field) -> bool {
6-
let x_bytes = x.to_le_bytes(32);
7-
let y_bytes = y.to_le_bytes(32);
8-
let mut x_is_lt = false;
9-
let mut done = false;
10-
for i in 0..32 {
11-
if (!done) {
12-
let x_byte = x_bytes[31 - i] as u8;
13-
let y_byte = y_bytes[31 - i] as u8;
14-
let bytes_match = x_byte == y_byte;
15-
if !bytes_match {
16-
x_is_lt = x_byte < y_byte;
17-
done = true;
18-
}
19-
}
20-
}
21-
x_is_lt
22-
}
4+
235
// Returns true if signature is valid
246
pub fn eddsa_poseidon_verify(
257
pub_key_x: Field,
@@ -39,7 +21,7 @@ pub fn eddsa_poseidon_verify(
3921
let signature_r8 = TEPoint::new(signature_r8_x, signature_r8_y);
4022
assert(bjj.curve.contains(signature_r8));
4123
// Ensure S < Subgroup Order
42-
assert(lt_bytes32(signature_s, bjj.suborder));
24+
assert(signature_s.lt(bjj.suborder));
4325
// Calculate the h = H(R, A, msg)
4426
let hash: Field = poseidon::bn254::hash_5([signature_r8_x, signature_r8_y, pub_key_x, pub_key_y, message]);
4527
// Calculate second part of the right side: right2 = h*8*A

noir_stdlib/src/field.nr

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
mod bn254;
2+
use bn254::lt as bn254_lt;
3+
14
impl Field {
25
pub fn to_le_bits(self: Self, bit_size: u32) -> [u1] {
36
crate::assert_constant(bit_size);
@@ -74,6 +77,15 @@ impl Field {
7477
pub fn sgn0(self) -> u1 {
7578
self as u1
7679
}
80+
81+
pub fn lt(self, another: Field) -> bool {
82+
if crate::compat::is_bn254() {
83+
bn254_lt(self, another)
84+
} else {
85+
lt_fallback(self, another)
86+
}
87+
}
88+
7789
}
7890

7991
#[builtin(modulus_num_bits)]
@@ -105,3 +117,24 @@ pub fn bytes32_to_field(bytes32: [u8; 32]) -> Field {
105117
// Abuse that a % p + b % p = (a + b) % p and that low < p
106118
low + high * v
107119
}
120+
121+
fn lt_fallback(x: Field, y: Field) -> bool {
122+
let num_bytes = (modulus_num_bits() as u32 + 7) / 8;
123+
let x_bytes = x.to_le_bytes(num_bytes);
124+
let y_bytes = y.to_le_bytes(num_bytes);
125+
let mut x_is_lt = false;
126+
let mut done = false;
127+
for i in 0..num_bytes {
128+
if (!done) {
129+
let x_byte = x_bytes[num_bytes - 1 - i] as u8;
130+
let y_byte = y_bytes[num_bytes - 1 - i] as u8;
131+
let bytes_match = x_byte == y_byte;
132+
if !bytes_match {
133+
x_is_lt = x_byte < y_byte;
134+
done = true;
135+
}
136+
}
137+
}
138+
x_is_lt
139+
}
140+

noir_stdlib/src/field/bn254.nr

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
global PLO: Field = 53438638232309528389504892708671455233;
2+
global PHI: Field = 64323764613183177041862057485226039389;
3+
global TWO_POW_128: Field = 0x100000000000000000000000000000000;
4+
5+
unconstrained fn decompose_unsafe(x: Field) -> (Field, Field) {
6+
let x_bytes = x.to_le_bytes(32);
7+
8+
let mut low: Field = 0;
9+
let mut high: Field = 0;
10+
11+
let mut offset = 1;
12+
for i in 0..16 {
13+
low += (x_bytes[i] as Field) * offset;
14+
high += (x_bytes[i + 16] as Field) * offset;
15+
offset *= 256;
16+
}
17+
18+
(low, high)
19+
}
20+
21+
pub fn decompose(x: Field) -> (Field, Field) {
22+
let (xlo, xhi) = decompose_unsafe(x);
23+
let borrow = lt_unsafe(PLO, xlo, 16);
24+
25+
xlo.assert_max_bit_size(128);
26+
xhi.assert_max_bit_size(128);
27+
28+
assert_eq(x, xlo + TWO_POW_128 * xhi);
29+
let rlo = PLO - xlo + (borrow as Field) * TWO_POW_128;
30+
let rhi = PHI - xhi - (borrow as Field);
31+
32+
rlo.assert_max_bit_size(128);
33+
rhi.assert_max_bit_size(128);
34+
35+
(xlo, xhi)
36+
}
37+
38+
unconstrained fn lt_unsafe(x: Field, y: Field, num_bytes: u32) -> bool {
39+
let x_bytes = x.__to_le_radix(256, num_bytes);
40+
let y_bytes = y.__to_le_radix(256, num_bytes);
41+
let mut x_is_lt = false;
42+
let mut done = false;
43+
for i in 0..num_bytes {
44+
if (!done) {
45+
let x_byte = x_bytes[num_bytes - 1 - i];
46+
let y_byte = y_bytes[num_bytes - 1 - i];
47+
let bytes_match = x_byte == y_byte;
48+
if !bytes_match {
49+
x_is_lt = x_byte < y_byte;
50+
done = true;
51+
}
52+
}
53+
}
54+
x_is_lt
55+
}
56+
57+
unconstrained fn lte_unsafe(x: Field, y: Field, num_bytes: u32) -> bool {
58+
lt_unsafe(x, y, num_bytes) | (x == y)
59+
}
60+
61+
pub fn assert_gt(a: Field, b: Field) {
62+
let (alo, ahi) = decompose(a);
63+
let (blo, bhi) = decompose(b);
64+
65+
let borrow = lte_unsafe(alo, blo, 16);
66+
67+
let rlo = alo - blo - 1 + (borrow as Field) * TWO_POW_128;
68+
let rhi = ahi - bhi - (borrow as Field);
69+
70+
rlo.assert_max_bit_size(128);
71+
rhi.assert_max_bit_size(128);
72+
}
73+
74+
pub fn assert_lt(a: Field, b: Field) {
75+
assert_gt(b, a);
76+
}
77+
78+
pub fn gt(a: Field, b: Field) -> bool {
79+
if a == b {
80+
false
81+
} else if lt_unsafe(a, b, 32) {
82+
assert_gt(b, a);
83+
false
84+
} else {
85+
assert_gt(a, b);
86+
true
87+
}
88+
}
89+
90+
pub fn lt(a: Field, b: Field) -> bool {
91+
gt(b, a)
92+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "field_comparisons"
3+
type = "bin"
4+
authors = [""]
5+
6+
[dependencies]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
use dep::std::field::bn254::{PLO, PHI, TWO_POW_128, decompose, decompose_unsafe, lt_unsafe, lte_unsafe, assert_gt, gt};
2+
3+
fn check_plo_phi() {
4+
assert_eq(PLO + PHI * TWO_POW_128, 0);
5+
let p_bytes = dep::std::field::modulus_le_bytes();
6+
let mut p_low: Field = 0;
7+
let mut p_high: Field = 0;
8+
9+
let mut offset = 1;
10+
for i in 0..16 {
11+
p_low += (p_bytes[i] as Field) * offset;
12+
p_high += (p_bytes[i + 16] as Field) * offset;
13+
offset *= 256;
14+
}
15+
assert_eq(p_low, PLO);
16+
assert_eq(p_high, PHI);
17+
}
18+
19+
fn check_decompose_unsafe() {
20+
assert_eq(decompose_unsafe(TWO_POW_128), (0, 1));
21+
assert_eq(decompose_unsafe(TWO_POW_128 + 0x1234567890), (0x1234567890, 1));
22+
assert_eq(decompose_unsafe(0x1234567890), (0x1234567890, 0));
23+
}
24+
25+
fn check_decompose() {
26+
assert_eq(decompose(TWO_POW_128), (0, 1));
27+
assert_eq(decompose(TWO_POW_128 + 0x1234567890), (0x1234567890, 1));
28+
assert_eq(decompose(0x1234567890), (0x1234567890, 0));
29+
}
30+
31+
fn check_lt_unsafe() {
32+
assert(lt_unsafe(0, 1, 16));
33+
assert(lt_unsafe(0, 0x100, 16));
34+
assert(lt_unsafe(0x100, TWO_POW_128 - 1, 16));
35+
assert(!lt_unsafe(0, TWO_POW_128, 16));
36+
}
37+
38+
fn check_lte_unsafe() {
39+
assert(lte_unsafe(0, 1, 16));
40+
assert(lte_unsafe(0, 0x100, 16));
41+
assert(lte_unsafe(0x100, TWO_POW_128 - 1, 16));
42+
assert(!lte_unsafe(0, TWO_POW_128, 16));
43+
44+
assert(lte_unsafe(0, 0, 16));
45+
assert(lte_unsafe(0x100, 0x100, 16));
46+
assert(lte_unsafe(TWO_POW_128 - 1, TWO_POW_128 - 1, 16));
47+
assert(lte_unsafe(TWO_POW_128, TWO_POW_128, 16));
48+
}
49+
50+
fn check_assert_gt() {
51+
assert_gt(1, 0);
52+
assert_gt(0x100, 0);
53+
assert_gt((0 - 1), (0 - 2));
54+
assert_gt(TWO_POW_128, 0);
55+
assert_gt(0 - 1, 0);
56+
}
57+
58+
fn check_gt() {
59+
assert(gt(1, 0));
60+
assert(gt(0x100, 0));
61+
assert(gt((0 - 1), (0 - 2)));
62+
assert(gt(TWO_POW_128, 0));
63+
assert(!gt(0, 0));
64+
assert(!gt(0, 0x100));
65+
assert(gt(0 - 1, 0 - 2));
66+
assert(!gt(0 - 2, 0 - 1));
67+
}
68+
69+
fn checks() {
70+
check_plo_phi();
71+
check_decompose_unsafe();
72+
check_decompose();
73+
check_lt_unsafe();
74+
check_lte_unsafe();
75+
check_assert_gt();
76+
check_gt();
77+
}
78+
79+
unconstrained fn checks_in_brillig() {
80+
checks();
81+
}
82+
83+
fn main() {
84+
checks();
85+
checks_in_brillig();
86+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[package]
2+
name = "field_comparisons"
3+
type = "bin"
4+
authors = [""]
5+
[dependencies]

test_programs/noir_test_success/field_comparisons/Prover.toml

Whitespace-only changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use dep::std::field::bn254::{TWO_POW_128, assert_gt};
2+
3+
#[test(should_fail)]
4+
fn test_assert_gt_should_fail_eq() {
5+
assert_gt(0, 0);
6+
}
7+
8+
#[test(should_fail)]
9+
fn test_assert_gt_should_fail_low_lt() {
10+
assert_gt(0, 0x100);
11+
}
12+
13+
#[test(should_fail)]
14+
fn test_assert_gt_should_fail_high_lt() {
15+
assert_gt(TWO_POW_128, TWO_POW_128 + 0x100);
16+
}

0 commit comments

Comments
 (0)