Skip to content

Commit 2ace790

Browse files
committed
nova support signed constant ROM encoding
1 parent 64f6fdf commit 2ace790

File tree

5 files changed

+155
-22
lines changed

5 files changed

+155
-22
lines changed

nova/README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,27 @@ Each instruction is compiled into a step circuit, following Nova(Supernova) pape
1414
An augmented circuit := step circuit + nova folding verification circuit.
1515
Furthermore, an augmented circuit has it own isolated constraints system, means there will be no shared circuit among different augmented circuits. Due to the fact, we can also call it instruction-circuit. There will be `#inst` instruction-circuit (More accurate, `#inst + 1` for 2 cycle curve implementation)
1616

17-
### Nova state & Constraints
18-
public input layout as z0 = `(pc, [writable register...] + [rom_value_pc1, rom_value_pc2, rom_value_pc3...])`
17+
### Nova state & constraints
18+
Nova state layout as z0 = `(pc, [writable register...] ++ ROM)`
19+
where the ROM is defined as an array `[rom_value_pc1, rom_value_pc2, rom_value_pc3...]`
1920
Each round an instruction is invoked, and in instruction-circuit it will constraints
20-
1. sequence constraints => `zi[offset + pc] - linear-combination([opcode_index, input reg1, input reg2,...], 1 << limb_width) = 0`
21+
1. sequence constraints => `zi[offset + pc] - linear-combination([opcode_index, input param1, input param2,...output param1, ...], 1 << limb_width) = 0`
2122
2. writable register read/write are value are constraint and match.
2223

2324
> While which instruction-circuit is invoked determined by prover, an maliculous prover can not invoke arbitrary any instruction-circuit, otherwise sequence constraints will be failed to pass `is_sat` check in the final stage.
2425
26+
### Sequence constraints
27+
As mentioned, to constraints the sequence, a ROM array is introduced and attach at the end of Nova state. For input params in different type, the linear combination strategy will be adjust accordingly.
28+
29+
- `reg index`, i.e. x2. `2` will be treat as unsigned index and put into the value
30+
- `sign/unsigned` const. For unsigned value will be put in lc directly. While signed part, it will be convert to signed limb, and on circuit side, signed limb will be convert to negative field value accordingly.
31+
- `label`. i.e. `loop_start`, label will be convert to integer
32+
33+
Since each instruction circuit has it own params type definition, different constraints circuit will be compiled to handle above situation automatically.
2534

2635
### R1CS constraints
2736
An augmented circuit can be viewed as a individual constraint system. PIL in powdr-asm instruction definition body will be compile into respective R1CS constraints. More detail, constraints can be categorized into 2 group
28-
1. sequence constraints + writable register RW => this constraints will be insert into R1CS circuit automatically and transparent to powdr-asm PIL.
37+
1. sequence constraints + writable register RW (or signed/unsigned/label) => this constraints will be insert into R1CS circuit automatically and transparent to powdr-asm PIL.
2938
2. Powdr-asm PIL constraints: will be compiled into R1CS constraints
3039

3140
Giving simple example

nova/src/circuit.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{
22
collections::BTreeMap,
3+
iter,
34
marker::PhantomData,
45
sync::{Arc, Mutex},
56
};
@@ -22,7 +23,7 @@ use crate::{
2223
nonnative::{bignat::BigNat, util::Num},
2324
utils::{
2425
add_allocated_num, alloc_const, alloc_num_equals, alloc_one, conditionally_select,
25-
evaluate_expr, find_pc_expression, get_num_at_index, WitnessGen,
26+
evaluate_expr, find_pc_expression, get_num_at_index, signed_limb_to_neg, WitnessGen,
2627
},
2728
LIMB_WIDTH,
2829
};
@@ -104,6 +105,19 @@ where
104105
// add constant 1
105106
poly_map.insert("ONE".to_string(), alloc_one(cs.namespace(|| "constant 1"))?);
106107

108+
// add constant 2^(LIMB_WIDTH + 1)
109+
let mut max_limb_plus_one = vec![0u8; 64];
110+
max_limb_plus_one[LIMB_WIDTH / 8] = 1u8;
111+
let max_limb_plus_one = F::from_uniform(&max_limb_plus_one[..]);
112+
poly_map.insert(
113+
"1 <<(LIMB_WIDTH + 1)".to_string(),
114+
alloc_const(
115+
cs.namespace(|| "constant 1 <<(LIMB_WIDTH + 1)"),
116+
max_limb_plus_one,
117+
LIMB_WIDTH + 1,
118+
)?,
119+
);
120+
107121
// parse inst part to construct step circuit
108122
// decompose ROM[pc] into linear combination lc(opcode_index, operand_index1, operand_index2, ... operand_output)
109123
// Noted that here only support single output
@@ -131,11 +145,27 @@ where
131145
let input_output_params_allocnum = rom_value_bignat
132146
.as_limbs()
133147
.iter()
148+
.zip_eq(
149+
iter::once::<Option<&Param>>(None)
150+
.chain(input_params.iter().map(|param| Some(param)))
151+
.chain(output_params.iter().map(|param| Some(param))),
152+
)
134153
.enumerate()
135-
.map(|(limb_index, limb)| {
136-
limb.as_allocated_num(
137-
cs.namespace(|| format!("rom decompose index {}", limb_index)),
138-
)
154+
.map(|(limb_index, (limb, param))| {
155+
match param {
156+
// signed handling
157+
Some(Param {
158+
ty: Some(type_str), ..
159+
}) if type_str == "signed" => signed_limb_to_neg(
160+
cs.namespace(|| format!("limb index {}", limb_index)),
161+
limb,
162+
&poly_map["1 <<(LIMB_WIDTH + 1)"],
163+
LIMB_WIDTH,
164+
),
165+
_ => limb.as_allocated_num(
166+
cs.namespace(|| format!("rom decompose index {}", limb_index)),
167+
),
168+
}
139169
})
140170
.collect::<Result<Vec<AllocatedNum<F>>, SynthesisError>>()?;
141171

nova/src/circuit_builder.rs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use ast::{
1414
UnaryOperator,
1515
},
1616
};
17-
use ff::{Field, PrimeField};
18-
use nova_snark::provider::bn256_grumpkin;
17+
use ff::PrimeField;
18+
use nova_snark::provider::bn256_grumpkin::{self};
1919
use nova_snark::{
2020
compute_digest,
2121
supernova::{gen_commitmentkey_by_r1cs, PublicParams, RecursiveSNARK, RunningClaim},
@@ -129,7 +129,7 @@ pub(crate) fn nova_prove<T: FieldElement>(
129129
let value = <G1 as Group>::Scalar::from_bytes(&n.to_bytes_le().try_into().unwrap()).unwrap();
130130
match ope {
131131
UnaryOperator::Plus => value,
132-
UnaryOperator::Minus => unimplemented!("not support signed constant"),
132+
UnaryOperator::Minus => get_neg_value_within_limbsize(value, LIMB_WIDTH),
133133
}
134134
},
135135
x => unimplemented!("unsupported expression {}", x),
@@ -161,7 +161,7 @@ pub(crate) fn nova_prove<T: FieldElement>(
161161
let value = <G1 as Group>::Scalar::from_bytes(&n.to_bytes_le().try_into().unwrap()).unwrap();
162162
match ope {
163163
UnaryOperator::Plus => value,
164-
UnaryOperator::Minus => value.invert().unwrap(),
164+
UnaryOperator::Minus => get_neg_value_within_limbsize(value, LIMB_WIDTH),
165165
}
166166
},
167167
x => unimplemented!("unsupported expression {:?}", x),
@@ -339,3 +339,32 @@ pub(crate) fn nova_prove<T: FieldElement>(
339339
// println!("zi_secondary: {:?}", zi_secondary);
340340
// println!("final program_counter: {:?}", program_counter);
341341
}
342+
343+
/// get additive negative of value within limbsize
344+
fn get_neg_value_within_limbsize(
345+
value: <G1 as Group>::Scalar,
346+
nbit: usize,
347+
) -> <G1 as Group>::Scalar {
348+
let value = value.to_bytes();
349+
let (lsb, msb) = value.split_at(nbit / 8);
350+
assert_eq!(
351+
msb.iter().map(|v| *v as usize).sum::<usize>(),
352+
0,
353+
"value {:?} is overflow",
354+
value
355+
);
356+
let mut lsb = lsb.to_vec();
357+
lsb.resize(32, 0);
358+
let value = <G1 as Group>::Scalar::from_bytes(lsb[..].try_into().unwrap()).unwrap();
359+
360+
let mut max_limb_plus_one_bytes = vec![0u8; nbit / 8 + 1];
361+
max_limb_plus_one_bytes[nbit / 8] = 1u8;
362+
max_limb_plus_one_bytes.resize(32, 0);
363+
let max_limb_plus_one =
364+
<G1 as Group>::Scalar::from_bytes(max_limb_plus_one_bytes[..].try_into().unwrap()).unwrap();
365+
366+
let mut value_neg = (max_limb_plus_one - value).to_bytes()[0..nbit / 8].to_vec();
367+
value_neg.resize(32, 0);
368+
369+
<G1 as Group>::Scalar::from_bytes(&value_neg[..].try_into().unwrap()).unwrap()
370+
}

nova/src/utils.rs

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use std::{
55
sync::{Arc, Mutex},
66
};
77

8+
use crate::nonnative::util::Num;
9+
810
#[allow(dead_code)]
911
use super::nonnative::bignat::{nat_to_limbs, BigNat};
1012
use ast::{
@@ -98,7 +100,8 @@ pub fn alloc_const<F: PrimeField, CS: ConstraintSystem<F>>(
98100
) -> Result<AllocatedNum<F>, SynthesisError> {
99101
let allocations: Vec<Variable> = value.to_repr().as_bits::<Lsb0>()[0..n_bits]
100102
.iter()
101-
.map(|raw_bit| {
103+
.enumerate()
104+
.map(|(index, raw_bit)| {
102105
let bit = cs.alloc(
103106
|| "boolean",
104107
|| {
@@ -111,14 +114,14 @@ pub fn alloc_const<F: PrimeField, CS: ConstraintSystem<F>>(
111114
)?;
112115
if *raw_bit {
113116
cs.enforce(
114-
|| format!("bit{raw_bit} == 1"),
117+
|| format!("{:?} index {index} true", value),
115118
|lc| lc + bit,
116119
|lc| lc + CS::one(),
117120
|lc| lc + CS::one(),
118121
);
119122
} else {
120123
cs.enforce(
121-
|| format!("bit{raw_bit} == 0"),
124+
|| format!("{:?} index {index} false", value),
122125
|lc| lc + bit,
123126
|lc| lc + CS::one(),
124127
|lc| lc,
@@ -135,15 +138,18 @@ pub fn alloc_const<F: PrimeField, CS: ConstraintSystem<F>>(
135138
f = f.double();
136139
l
137140
});
138-
let value = AllocatedNum::alloc(cs.namespace(|| "alloc const"), || Ok(value))?;
139-
let sum_lc = LinearCombination::zero() + value.get_variable() - &sum;
141+
let value_alloc =
142+
AllocatedNum::alloc(cs.namespace(|| format!("{:?} alloc const", value)), || {
143+
Ok(value)
144+
})?;
145+
let sum_lc = LinearCombination::zero() + value_alloc.get_variable() - &sum;
140146
cs.enforce(
141-
|| "sum - value = 0",
147+
|| format!("{:?} sum - value = 0", value),
142148
|lc| lc + &sum_lc,
143149
|lc| lc + CS::one(),
144150
|lc| lc,
145151
);
146-
Ok(value)
152+
Ok(value_alloc)
147153
}
148154

149155
/// Allocate a scalar as a base. Only to be used is the scalar fits in base!
@@ -640,6 +646,66 @@ pub fn get_num_at_index<F: PrimeFieldExt, CS: ConstraintSystem<F>>(
640646
Ok(selected_num)
641647
}
642648

649+
/// get negative field value from signed limb
650+
pub fn signed_limb_to_neg<F: PrimeFieldExt, CS: ConstraintSystem<F>>(
651+
mut cs: CS,
652+
limb: &Num<F>,
653+
max_limb_plus_one_const: &AllocatedNum<F>,
654+
nbit: usize,
655+
) -> Result<AllocatedNum<F>, SynthesisError> {
656+
let limb_alloc = limb.as_allocated_num(cs.namespace(|| "rom decompose index"))?;
657+
let bits = limb.decompose(cs.namespace(|| "index decompose bits"), nbit)?;
658+
let signed_bit = &bits.allocations[nbit - 1];
659+
let twos_complement = AllocatedNum::alloc(cs.namespace(|| "alloc twos complement"), || {
660+
max_limb_plus_one_const
661+
.get_value()
662+
.zip(limb_alloc.get_value())
663+
.map(|(a, b)| a - b)
664+
.ok_or(SynthesisError::AssignmentMissing)
665+
})?;
666+
cs.enforce(
667+
|| "constraints 2's complement",
668+
|lc| lc + twos_complement.get_variable() + limb_alloc.get_variable(),
669+
|lc| lc + CS::one(),
670+
|lc| lc + max_limb_plus_one_const.get_variable(),
671+
);
672+
let twos_complement_neg = AllocatedNum::alloc(cs.namespace(|| " 2's complment neg"), || {
673+
twos_complement
674+
.get_value()
675+
.map(|v| v.neg())
676+
.ok_or(SynthesisError::AssignmentMissing)
677+
})?;
678+
cs.enforce(
679+
|| "constraints 2's complement additive neg",
680+
|lc| lc + twos_complement.get_variable() + twos_complement_neg.get_variable(),
681+
|lc| lc + CS::one(),
682+
|lc| lc,
683+
);
684+
685+
let c = AllocatedNum::alloc(cs.namespace(|| "conditional select"), || {
686+
signed_bit
687+
.value
688+
.map(|signed_bit_value| {
689+
if signed_bit_value {
690+
twos_complement_neg.get_value().unwrap()
691+
} else {
692+
limb_alloc.get_value().unwrap()
693+
}
694+
})
695+
.ok_or(SynthesisError::AssignmentMissing)
696+
})?;
697+
698+
// twos_complement_neg * condition + limb_alloc*(1-condition) = c ->
699+
// twos_complement_neg * condition - limb_alloc*condition = c - limb_alloc
700+
cs.enforce(
701+
|| "index conditional select",
702+
|lc| lc + twos_complement_neg.get_variable() - limb_alloc.get_variable(),
703+
|_| signed_bit.bit.clone(),
704+
|lc| lc + c.get_variable() - limb_alloc.get_variable(),
705+
);
706+
Ok(c)
707+
}
708+
643709
// TODO optmize constraints to leverage R1CS cost-free additive
644710
// TODO combine FieldElement & PrimeField
645711
pub fn evaluate_expr<'a, T: FieldElement, F: PrimeFieldExt, CS: ConstraintSystem<F>>(

nova/zero.asm

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@ machine NovaZero {
7070
x1 <=Y= incr(x0); // x1 = 1
7171
x0 <=Z= add(x1, x1); // x0 = 1 + 1
7272
x0 <=Z= addi(x0, 1); // x0 = 2 + 1
73-
// x0 <=Z= addi(x0, -1); // TODO need support signed constant in instruction assignment
74-
x0 <=Y= decr(x0); // x0 = 2
73+
x0 <=Z= addi(x0, -2); // x0 = 3 - 2
7574
x0 <=Z= sub(x0, x0); // x0 - x0 = 0
7675
assert_zero x0; // x0 == 0
7776
loop;

0 commit comments

Comments
 (0)