Skip to content

Commit 27f8fb3

Browse files
committed
feat: implement record circuit
1 parent 965616b commit 27f8fb3

File tree

2 files changed

+296
-0
lines changed

2 files changed

+296
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
pub(crate) mod public_inputs;
2+
pub(crate) mod record;
3+
4+
use public_inputs::PublicInputs;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
use crate::results_tree::extraction::PublicInputs;
2+
use alloy::primitives::U256;
3+
use mp2_common::{
4+
group_hashing::CircuitBuilderGroupHashing,
5+
poseidon::{empty_poseidon_hash, H},
6+
public_inputs::PublicInputCommon,
7+
serialization::{deserialize, deserialize_long_array, serialize, serialize_long_array},
8+
types::CBuilder,
9+
u256::{CircuitBuilderU256, UInt256Target, WitnessWriteU256},
10+
utils::{greater_than, less_than, SelectHashBuilder, ToTargets},
11+
D, F,
12+
};
13+
use plonky2::{
14+
hash::hash_types::{HashOut, HashOutTarget},
15+
iop::{
16+
target::{BoolTarget, Target},
17+
witness::{PartialWitness, WitnessWrite},
18+
},
19+
plonk::proof::ProofWithPublicInputsTarget,
20+
};
21+
use recursion_framework::circuit_builder::CircuitLogicWires;
22+
use serde::{Deserialize, Serialize};
23+
use std::iter;
24+
25+
#[derive(Clone, Debug, Serialize, Deserialize)]
26+
pub struct RecordWires<const MAX_NUM_RESULTS: usize> {
27+
#[serde(
28+
serialize_with = "serialize_long_array",
29+
deserialize_with = "deserialize_long_array"
30+
)]
31+
indexed_items: [UInt256Target; MAX_NUM_RESULTS],
32+
#[serde(
33+
serialize_with = "serialize_long_array",
34+
deserialize_with = "deserialize_long_array"
35+
)]
36+
index_ids: [Target; 2],
37+
#[serde(serialize_with = "serialize", deserialize_with = "deserialize")]
38+
tree_hash: HashOutTarget,
39+
counter: Target,
40+
#[serde(serialize_with = "serialize", deserialize_with = "deserialize")]
41+
is_stored_in_leaf: BoolTarget,
42+
offset_range_min: Target,
43+
offset_range_max: Target,
44+
}
45+
46+
#[derive(Clone, Debug, Serialize, Deserialize)]
47+
pub struct RecordCircuit<const MAX_NUM_RESULTS: usize> {
48+
/// Values of the indexed items for in this record;
49+
/// if there is no secondary indexed item, just place the dummy value `0`
50+
#[serde(
51+
serialize_with = "serialize_long_array",
52+
deserialize_with = "deserialize_long_array"
53+
)]
54+
pub(crate) indexed_items: [U256; MAX_NUM_RESULTS],
55+
/// Integer identifiers of the indexed items
56+
#[serde(
57+
serialize_with = "serialize_long_array",
58+
deserialize_with = "deserialize_long_array"
59+
)]
60+
pub(crate) index_ids: [F; 2],
61+
/// Hash of the cells tree for this record;
62+
/// could be empty hash if there is no cells tree for this record
63+
pub(crate) tree_hash: HashOut<F>,
64+
/// Counter for the node associated to this record
65+
pub(crate) counter: F,
66+
/// Boolean flag specifying whether this record is stored
67+
/// in a leaf node of a rows tree or not
68+
pub(crate) is_stored_in_leaf: bool,
69+
/// Minimum offset range bound
70+
pub(crate) offset_range_min: F,
71+
/// Maximum offset range bound
72+
pub(crate) offset_range_max: F,
73+
}
74+
75+
impl<const MAX_NUM_RESULTS: usize> RecordCircuit<MAX_NUM_RESULTS> {
76+
pub fn build(b: &mut CBuilder) -> RecordWires<MAX_NUM_RESULTS> {
77+
let ffalse = b._false();
78+
let empty_hash = b.constant_hash(*empty_poseidon_hash());
79+
80+
let indexed_items: [UInt256Target; MAX_NUM_RESULTS] = b.add_virtual_u256_arr_unsafe();
81+
let index_ids: [Target; 2] = b.add_virtual_target_arr();
82+
let tree_hash = b.add_virtual_hash();
83+
let counter = b.add_virtual_target();
84+
let is_stored_in_leaf = b.add_virtual_bool_target_safe();
85+
let [offset_range_min, offset_range_max] = b.add_virtual_target_arr();
86+
87+
// H(H("")||H("")||indexed_items[1]||indexed_items[1]||index_ids[1]||indexed_items[1]||tree_hash)
88+
let tree_hash_inputs = empty_hash
89+
.elements
90+
.iter()
91+
.cloned()
92+
.chain(empty_hash.elements)
93+
.chain(indexed_items[1].to_targets())
94+
.chain(indexed_items[1].to_targets())
95+
.chain(iter::once(index_ids[1]))
96+
.chain(indexed_items[1].to_targets())
97+
.chain(tree_hash.elements)
98+
.collect();
99+
let new_tree_hash = b.hash_n_to_hash_no_pad::<H>(tree_hash_inputs);
100+
let final_tree_hash = b.select_hash(is_stored_in_leaf, &new_tree_hash, &tree_hash);
101+
102+
// D(index_ids[0]||indexed_items[0]||index_ids[1]||indexed_items[1]||tree_hash)
103+
let accumulator_inputs: Vec<_> = iter::once(index_ids[0])
104+
.chain(indexed_items[0].to_targets())
105+
.chain(iter::once(index_ids[1]))
106+
.chain(indexed_items[1].to_targets())
107+
.chain(final_tree_hash.to_targets())
108+
.collect();
109+
let accumulator = b.map_to_curve_point(&accumulator_inputs);
110+
111+
// Ensure the counter associated to the current record is in the range
112+
// specified by the query
113+
// offset_range_min <= counter <= offset_range_max
114+
// -> NOT((counter < offset_range_min) OR (counter > offset_range_max)
115+
let is_less_than = less_than(b, counter, offset_range_min, 32);
116+
let is_greater_than = greater_than(b, counter, offset_range_max, 32);
117+
let is_out_of_range = b.or(is_less_than, is_greater_than);
118+
b.connect(is_out_of_range.target, ffalse.target);
119+
120+
// Register the public inputs.
121+
PublicInputs::<_, MAX_NUM_RESULTS>::new(
122+
&final_tree_hash.to_targets(),
123+
&indexed_items[1].to_targets(),
124+
&indexed_items[1].to_targets(),
125+
&indexed_items[0].to_targets(),
126+
&index_ids,
127+
&[counter],
128+
&[counter],
129+
&[offset_range_min],
130+
&[offset_range_max],
131+
&accumulator.to_targets(),
132+
)
133+
.register(b);
134+
135+
RecordWires {
136+
indexed_items,
137+
index_ids,
138+
tree_hash,
139+
counter,
140+
is_stored_in_leaf,
141+
offset_range_min,
142+
offset_range_max,
143+
}
144+
}
145+
146+
fn assign(&self, pw: &mut PartialWitness<F>, wires: &RecordWires<MAX_NUM_RESULTS>) {
147+
wires
148+
.indexed_items
149+
.iter()
150+
.zip(self.indexed_items)
151+
.for_each(|(t, v)| pw.set_u256_target(t, v));
152+
pw.set_target_arr(&wires.index_ids, &self.index_ids);
153+
pw.set_hash_target(wires.tree_hash, self.tree_hash);
154+
pw.set_target(wires.counter, self.counter);
155+
pw.set_bool_target(wires.is_stored_in_leaf, self.is_stored_in_leaf);
156+
pw.set_target(wires.offset_range_min, self.offset_range_min);
157+
pw.set_target(wires.offset_range_max, self.offset_range_max);
158+
}
159+
}
160+
161+
/// Verified proof number = 0
162+
pub(crate) const NUM_VERIFIED_PROOFS: usize = 0;
163+
164+
impl<const MAX_NUM_RESULTS: usize> CircuitLogicWires<F, D, NUM_VERIFIED_PROOFS>
165+
for RecordWires<MAX_NUM_RESULTS>
166+
{
167+
type CircuitBuilderParams = ();
168+
type Inputs = RecordCircuit<MAX_NUM_RESULTS>;
169+
170+
const NUM_PUBLIC_INPUTS: usize = PublicInputs::<F, MAX_NUM_RESULTS>::total_len();
171+
172+
fn circuit_logic(
173+
builder: &mut CBuilder,
174+
_verified_proofs: [&ProofWithPublicInputsTarget<D>; NUM_VERIFIED_PROOFS],
175+
_builder_parameters: Self::CircuitBuilderParams,
176+
) -> Self {
177+
Self::Inputs::build(builder)
178+
}
179+
180+
fn assign_input(&self, inputs: Self::Inputs, pw: &mut PartialWitness<F>) -> anyhow::Result<()> {
181+
inputs.assign(pw, self);
182+
Ok(())
183+
}
184+
}
185+
186+
#[cfg(test)]
187+
mod tests {
188+
use super::*;
189+
use mp2_common::{utils::ToFields, C};
190+
use mp2_test::{
191+
circuit::{run_circuit, UserCircuit},
192+
utils::{gen_random_field_hash, gen_random_u256},
193+
};
194+
use plonky2::{field::types::Field, plonk::config::Hasher};
195+
use rand::{thread_rng, Rng};
196+
use std::array;
197+
198+
const MAX_NUM_RESULTS: usize = 20;
199+
200+
impl UserCircuit<F, D> for RecordCircuit<MAX_NUM_RESULTS> {
201+
type Wires = RecordWires<MAX_NUM_RESULTS>;
202+
203+
fn build(b: &mut CBuilder) -> Self::Wires {
204+
RecordCircuit::build(b)
205+
}
206+
207+
fn prove(&self, pw: &mut PartialWitness<F>, wires: &Self::Wires) {
208+
self.assign(pw, wires);
209+
}
210+
}
211+
212+
fn test_record_circuit(is_stored_in_leaf: bool) {
213+
// Construct the witness.
214+
let mut rng = thread_rng();
215+
let indexed_items = array::from_fn(|_| gen_random_u256(&mut rng));
216+
let index_ids = array::from_fn(|_| F::from_canonical_usize(rng.gen()));
217+
let tree_hash = gen_random_field_hash();
218+
let counter = F::from_canonical_u32(rng.gen());
219+
let offset_range_min = counter - F::ONE;
220+
let offset_range_max = counter + F::ONE;
221+
222+
// Construct the circuit.
223+
let test_circuit = RecordCircuit {
224+
indexed_items,
225+
index_ids,
226+
tree_hash,
227+
counter,
228+
is_stored_in_leaf,
229+
offset_range_min,
230+
offset_range_max,
231+
};
232+
233+
// Proof for the test circuit.
234+
let proof = run_circuit::<F, D, C, _>(test_circuit);
235+
let pi = PublicInputs::<_, MAX_NUM_RESULTS>::from_slice(&proof.public_inputs);
236+
237+
// Check the public inputs.
238+
239+
// Tree hash
240+
if is_stored_in_leaf {
241+
let empty_hash = empty_poseidon_hash();
242+
let empty_hash_fields = empty_hash.to_fields();
243+
let hash_inputs: Vec<_> = empty_hash_fields
244+
.clone()
245+
.into_iter()
246+
.chain(empty_hash_fields)
247+
.chain(indexed_items[1].to_fields())
248+
.chain(indexed_items[1].to_fields())
249+
.chain(iter::once(index_ids[1]))
250+
.chain(indexed_items[1].to_fields())
251+
.chain(tree_hash.to_fields())
252+
.collect();
253+
let exp_hash = H::hash_no_pad(&hash_inputs);
254+
assert_eq!(pi.tree_hash(), exp_hash);
255+
} else {
256+
assert_eq!(pi.tree_hash(), tree_hash);
257+
};
258+
259+
// Min value
260+
assert_eq!(pi.min_value(), indexed_items[1]);
261+
262+
// Max value
263+
assert_eq!(pi.max_value(), indexed_items[1]);
264+
265+
// Primary index value
266+
assert_eq!(pi.primary_index_value(), indexed_items[0]);
267+
268+
// Index ids
269+
assert_eq!(pi.index_ids(), index_ids);
270+
271+
// Min counter
272+
assert_eq!(pi.min_counter(), counter);
273+
274+
// Max counter
275+
assert_eq!(pi.max_counter(), counter);
276+
277+
// Offset range min
278+
assert_eq!(pi.offset_range_min(), offset_range_min);
279+
280+
// Offset range max
281+
assert_eq!(pi.offset_range_max(), offset_range_max);
282+
}
283+
284+
#[test]
285+
fn test_record_circuit_storing_in_leaf() {
286+
test_record_circuit(true);
287+
}
288+
289+
#[test]
290+
fn test_record_circuit_storing_in_inter() {
291+
test_record_circuit(false);
292+
}
293+
}

0 commit comments

Comments
 (0)