From fe1fdeb990a0b5074c1fc5a537e35f85fc87b673 Mon Sep 17 00:00:00 2001 From: porcuquine <1746729+porcuquine@users.noreply.github.com> Date: Tue, 30 Jan 2024 16:03:03 -0800 Subject: [PATCH] Separate coroutine circuits. (#1076) * Separate coroutine circuits. * Add per-coroutine-circuit rc. --------- Co-authored-by: porcuquine --- src/coroutine/memoset/demo.rs | 38 +-- src/coroutine/memoset/mod.rs | 469 ++++++++++++++++++++++++++------- src/coroutine/memoset/query.rs | 9 +- 3 files changed, 397 insertions(+), 119 deletions(-) diff --git a/src/coroutine/memoset/demo.rs b/src/coroutine/memoset/demo.rs index dabef14a8d..09b6df15ca 100644 --- a/src/coroutine/memoset/demo.rs +++ b/src/coroutine/memoset/demo.rs @@ -2,7 +2,7 @@ use bellpepper_core::{num::AllocatedNum, ConstraintSystem, SynthesisError}; use super::{ query::{CircuitQuery, Query}, - CircuitScope, CircuitTranscript, LogMemo, Scope, + CircuitScope, CircuitTranscript, LogMemo, LogMemoCircuit, Scope, }; use crate::circuit::gadgets::constraints::alloc_is_zero; use crate::circuit::gadgets::pointer::AllocatedPtr; @@ -88,6 +88,22 @@ impl Query for DemoQuery { } } + fn to_circuit>(&self, cs: &mut CS, s: &Store) -> Self::CQ { + match self { + DemoQuery::Factorial(n) => { + Self::CQ::Factorial(AllocatedPtr::alloc_infallible(cs, || s.hash_ptr(n))) + } + _ => unreachable!(), + } + } + + fn dummy_from_index(s: &Store, index: usize) -> Self { + match index { + 0 => Self::Factorial(s.num(0.into())), + _ => unreachable!(), + } + } + fn index(&self) -> usize { match self { Self::Factorial(_) => 0, @@ -106,7 +122,7 @@ impl CircuitQuery for DemoCircuitQuery { cs: &mut CS, g: &GlobalAllocator, store: &Store, - scope: &mut CircuitScope>, + scope: &mut CircuitScope>, acc: &AllocatedPtr, transcript: &CircuitTranscript, ) -> Result<(AllocatedPtr, AllocatedPtr, CircuitTranscript), SynthesisError> { @@ -208,19 +224,11 @@ impl CircuitQuery for DemoCircuitQuery { } fn from_ptr>(cs: &mut CS, s: &Store, ptr: &Ptr) -> Option { - let query = DemoQuery::from_ptr(s, ptr); - if let Some(q) = query { - match q { - DemoQuery::Factorial(n) => { - Some(Self::Factorial(AllocatedPtr::alloc_infallible(cs, || { - s.hash_ptr(&n) - }))) - } - _ => unreachable!(), - } - } else { - None - } + DemoQuery::from_ptr(s, ptr).map(|q| q.to_circuit(cs, s)) + } + + fn dummy_from_index>(cs: &mut CS, s: &Store, index: usize) -> Self { + DemoQuery::dummy_from_index(s, index).to_circuit(cs, s) } fn symbol(&self) -> Symbol { diff --git a/src/coroutine/memoset/mod.rs b/src/coroutine/memoset/mod.rs index ea99acf7ad..da8aa3bc4d 100644 --- a/src/coroutine/memoset/mod.rs +++ b/src/coroutine/memoset/mod.rs @@ -27,6 +27,7 @@ //! results computed 'naturally' during evaluation. We then separate and sort in an order matching that which the NIVC //! prover will follow when provably maintaining the multiset accumulator and Fiat-Shamir transcript in the circuit. +use itertools::Itertools; use std::collections::HashMap; use std::marker::PhantomData; @@ -131,9 +132,9 @@ impl CircuitTranscript { s: &Store, item: &AllocatedPtr, ) -> Result { - Ok(Self { - acc: construct_cons(cs, g, s, item, &self.acc)?, - }) + let acc = construct_cons(cs, g, s, item, &self.acc)?; + + Ok(Self { acc }) } fn make_kv>( @@ -176,7 +177,7 @@ impl CircuitTranscript { } } -#[derive(Debug)] +#[derive(Clone, Debug)] /// A `Scope` tracks the queries made while evaluating, including the subqueries that result from evaluating other /// queries -- then makes use of the bookkeeping performed at evaluation time to synthesize proof of each query /// performed. @@ -193,18 +194,21 @@ pub struct Scope { /// unique keys: query-index -> [key] unique_inserted_keys: HashMap>, transcribe_internal_insertions: bool, + // This may become an explicit map or something allowing more fine-grained control. + default_rc: usize, } +const DEFAULT_RC_FOR_QUERY: usize = 1; const DEFAULT_TRANSCRIBE_INTERNAL_INSERTIONS: bool = false; impl Default for Scope> { fn default() -> Self { - Self::new(DEFAULT_TRANSCRIBE_INTERNAL_INSERTIONS) + Self::new(DEFAULT_TRANSCRIBE_INTERNAL_INSERTIONS, DEFAULT_RC_FOR_QUERY) } } impl Scope> { - fn new(transcribe_internal_insertions: bool) -> Self { + fn new(transcribe_internal_insertions: bool, default_rc: usize) -> Self { Self { memoset: Default::default(), queries: Default::default(), @@ -213,12 +217,14 @@ impl Scope> { internal_insertions: Default::default(), unique_inserted_keys: Default::default(), transcribe_internal_insertions, + default_rc, } } } -pub struct CircuitScope { - memoset: M, +#[derive(Debug, Clone)] +pub struct CircuitScope { + memoset: CM, // CircuitMemoSet /// k -> v queries: HashMap, ZPtr>, /// k -> allocated v @@ -227,6 +233,92 @@ pub struct CircuitScope { transcribe_internal_insertions: bool, } +pub struct CoroutineCircuit<'a, F: LurkField, CM, Q> { + queries: &'a HashMap, + memoset: CM, + keys: Vec, + query_index: usize, + store: &'a Store, + transcribe_internal_insertions: bool, + rc: usize, + _p: PhantomData, +} + +// TODO: Make this generic rather than specialized to LogMemo. +// That will require a CircuitScopeTrait. +impl<'a, F: LurkField, Q: Query> CoroutineCircuit<'a, F, LogMemoCircuit, Q> { + fn new( + scope: &'a Scope>, + memoset: LogMemoCircuit, + keys: Vec, + query_index: usize, + store: &'a Store, + rc: usize, + ) -> Self { + assert!(keys.len() <= rc); + Self { + memoset, + queries: &scope.queries, + keys, + query_index, + store, + transcribe_internal_insertions: scope.transcribe_internal_insertions, + rc, + _p: Default::default(), + } + } + + // This is a supernova::StepCircuit method. + // // TODO: we need to create a supernova::StepCircuit that will prove up to a fixed number of queries of a given type. + fn synthesize>( + &mut self, + cs: &mut CS, + z: &[AllocatedPtr], + ) -> Result<(Option>, Vec>), SynthesisError> { + let g = &mut GlobalAllocator::::default(); + + assert_eq!(6, z.len()); + let [c, e, k, memoset_acc, transcript, r] = z else { + unreachable!() + }; + + let mut circuit_scope: CircuitScope> = CircuitScope::from_queries( + cs, + g, + self.store, + self.memoset.clone(), + self.queries, + self.transcribe_internal_insertions, + ); + circuit_scope.update_from_io(memoset_acc.clone(), transcript.clone(), r); + + for (i, key) in self + .keys + .iter() + .map(Some) + .pad_using(self.rc, |_| None) + .enumerate() + { + let cs = &mut cs.namespace(|| format!("internal-{i}")); + circuit_scope.synthesize_prove_key_query::<_, Q>( + cs, + g, + self.store, + key, + self.query_index, + )?; + } + + let (memoset_acc, transcript, r_num) = circuit_scope.io(); + let r = AllocatedPtr::alloc_tag(&mut cs.namespace(|| "r"), ExprTag::Num.to_field(), r_num)?; + + let z_out = vec![c.clone(), e.clone(), k.clone(), memoset_acc, transcript, r]; + + let next_pc = None; // FIXME. + Ok((next_pc, z_out)) + } +} + impl> Scope> { pub fn query(&mut self, s: &Store, form: Ptr) -> Ptr { let (response, kv_ptr) = self.query_aux(s, form); @@ -362,36 +454,108 @@ impl> Scope> { s: &Store, ) -> Result<(), SynthesisError> { self.ensure_transcript_finalized(s); - let mut circuit_scope = - CircuitScope::from_scope(&mut cs.namespace(|| "transcript"), g, s, self); + // FIXME: Do we need to allocate a new GlobalAllocator here? + // Is it okay for this memoset circuit to be shared between all CoroutineCircuits? + let memoset_circuit = self + .memoset + .to_circuit(&mut cs.namespace(|| "memoset_circuit")); + + let mut circuit_scope = CircuitScope::from_queries( + &mut cs.namespace(|| "transcript"), + g, + s, + memoset_circuit.clone(), + &self.queries, + self.transcribe_internal_insertions, + ); circuit_scope.init(cs, g, s); { circuit_scope.synthesize_insert_toplevel_queries(self, cs, g, s)?; - circuit_scope.synthesize_prove_all_queries(self, cs, g, s)?; + + { + let (memoset_acc, transcript, r_num) = circuit_scope.io(); + let r = AllocatedPtr::alloc_tag( + &mut cs.namespace(|| "r"), + ExprTag::Num.to_field(), + r_num, + )?; + let dummy = g.alloc_ptr(cs, &s.intern_nil(), s); + let mut z = vec![ + dummy.clone(), + dummy.clone(), + dummy.clone(), + memoset_acc, + transcript, + r, + ]; + for (index, keys) in self.unique_inserted_keys.iter() { + let cs = &mut cs.namespace(|| format!("query-index-{index}")); + + let rc = self.rc_for_query(*index); + + for (i, chunk) in keys.chunks(rc).enumerate() { + // This namespace exists only because we are putting multiple 'chunks' into a single, larger circuit (as a stage in development). + // It shouldn't exist, when instead we have only the single NIVC circuit repeated multiple times. + let cs = &mut cs.namespace(|| format!("chunk-{i}")); + + let mut circuit: CoroutineCircuit<'_, F, LogMemoCircuit, Q> = + CoroutineCircuit::new( + self, + memoset_circuit.clone(), + chunk.to_vec(), + *index, + s, + rc, + ); + + let (_next_pc, z_out) = circuit.synthesize(cs, &z)?; + { + let memoset_acc = &z_out[3]; + let transcript = &z_out[4]; + let r = &z_out[5]; + + circuit_scope.update_from_io( + memoset_acc.clone(), + transcript.clone(), + r, + ); + + z = z_out; + } + } + } + } } + circuit_scope.finalize(cs, g); Ok(()) } + + fn rc_for_query(&self, _index: usize) -> usize { + self.default_rc + } } -impl CircuitScope> { - fn from_scope, Q: Query>( +impl CircuitScope> { + fn from_queries>( cs: &mut CS, g: &mut GlobalAllocator, s: &Store, - scope: &Scope>, + memoset: LogMemoCircuit, + queries: &HashMap, + transcribe_internal_insertions: bool, ) -> Self { - let queries = scope - .queries + let queries = queries .iter() .map(|(k, v)| (s.hash_ptr(k), s.hash_ptr(v))) .collect(); + Self { - memoset: scope.memoset.clone(), + memoset, queries, transcript: CircuitTranscript::new(cs, g, s), acc: Default::default(), - transcribe_internal_insertions: scope.transcribe_internal_insertions, + transcribe_internal_insertions, } } @@ -409,6 +573,25 @@ impl CircuitScope> { self.transcript = CircuitTranscript::new(cs, g, s); } + fn io(&self) -> (AllocatedPtr, AllocatedPtr, AllocatedNum) { + ( + self.acc.as_ref().unwrap().clone(), + self.transcript.acc.clone(), + self.memoset.r.clone(), + ) + } + + fn update_from_io( + &mut self, + acc: AllocatedPtr, + transcript: AllocatedPtr, + r: &AllocatedPtr, + ) { + self.acc = Some(acc); + self.transcript.acc = transcript; + self.memoset.r = r.hash().clone(); + } + fn synthesize_insert_query>( &self, cs: &mut CS, @@ -486,7 +669,7 @@ impl CircuitScope> { } fn finalize>(&mut self, cs: &mut CS, _g: &mut GlobalAllocator) { - let r = self.memoset.allocated_r(cs); + let r = self.memoset.allocated_r(); enforce_equal(cs, || "r_matches_transcript", self.transcript.r(), &r); enforce_equal_zero(cs, || "acc_is_zero", self.acc.clone().unwrap().hash()); } @@ -595,52 +778,39 @@ impl CircuitScope> { Ok(()) } - fn synthesize_prove_all_queries, Q: Query>( - &mut self, - scope: &mut Scope>, - cs: &mut CS, - g: &mut GlobalAllocator, - s: &Store, - ) -> Result<(), SynthesisError> { - for (index, keys) in scope.unique_inserted_keys.iter() { - let cs = &mut cs.namespace(|| format!("query-index-{index}")); - self.synthesize_prove_queries::<_, Q>(cs, g, s, keys)?; - } - Ok(()) - } - - fn synthesize_prove_queries, Q: Query>( - &mut self, - cs: &mut CS, - g: &mut GlobalAllocator, - s: &Store, - keys: &[Ptr], - ) -> Result<(), SynthesisError> { - for (i, key) in keys.iter().enumerate() { - let cs = &mut cs.namespace(|| format!("internal-{i}")); - - self.synthesize_prove_key_query::<_, Q>(cs, g, s, key)?; - } - Ok(()) - } - fn synthesize_prove_key_query, Q: Query>( &mut self, cs: &mut CS, g: &mut GlobalAllocator, s: &Store, - key: &Ptr, + key: Option<&Ptr>, + index: usize, ) -> Result<(), SynthesisError> { - let allocated_key = - AllocatedPtr::alloc( - &mut cs.namespace(|| "allocated_key"), - || Ok(s.hash_ptr(key)), - ) - .unwrap(); + let allocated_key = AllocatedPtr::alloc(&mut cs.namespace(|| "allocated_key"), || { + if let Some(key) = key { + Ok(s.hash_ptr(key)) + } else { + Ok(s.hash_ptr(&s.intern_nil())) + } + }) + .unwrap(); + + let circuit_query = if let Some(key) = key { + Q::CQ::from_ptr(&mut cs.namespace(|| "circuit_query"), s, key).unwrap() + } else { + Q::CQ::dummy_from_index(&mut cs.namespace(|| "circuit_query"), s, index) + }; - let circuit_query = Q::CQ::from_ptr(&mut cs.namespace(|| "circuit_query"), s, key).unwrap(); + let not_dummy = key.is_some(); - self.synthesize_prove_query::<_, Q::CQ>(cs, g, s, &allocated_key, &circuit_query)?; + self.synthesize_prove_query::<_, Q::CQ>( + cs, + g, + s, + &allocated_key, + &circuit_query, + not_dummy, + )?; Ok(()) } @@ -651,6 +821,7 @@ impl CircuitScope> { s: &Store, allocated_key: &AllocatedPtr, circuit_query: &CQ, + not_dummy: bool, ) -> Result<(), SynthesisError> { let acc = self.acc.clone().unwrap(); let transcript = self.transcript.clone(); @@ -662,8 +833,22 @@ impl CircuitScope> { let (new_acc, new_transcript) = self.synthesize_remove(cs, g, s, &new_acc, &new_transcript, allocated_key, &val)?; - self.acc = Some(new_acc); - self.transcript = new_transcript; + // Prover can choose non-deterministically whether or not a given query is a dummy, to allow for padding. + let final_acc = AllocatedPtr::pick( + &mut cs.namespace(|| "final_acc"), + &Boolean::Constant(not_dummy), + &new_acc, + self.acc.as_ref().expect("acc missing"), + )?; + let final_transcript = CircuitTranscript::pick( + &mut cs.namespace(|| "final_transcripot"), + &Boolean::Constant(not_dummy), + &new_transcript, + &self.transcript, + )?; + + self.acc = Some(final_acc); + self.transcript = final_transcript; Ok(()) } @@ -674,12 +859,7 @@ impl CircuitScope> { } } -pub trait MemoSet: Clone { - fn is_finalized(&self) -> bool; - fn finalize_transcript(&mut self, s: &Store, transcript: Transcript); - fn r(&self) -> Option<&F>; - fn map_to_element(&self, x: F) -> Option; - fn add(&mut self, kv: Ptr); +pub trait CircuitMemoSet: Clone { fn synthesize_remove_n>( &self, cs: &mut CS, @@ -687,11 +867,8 @@ pub trait MemoSet: Clone { kv: &AllocatedPtr, count: &AllocatedNum, ) -> Result, SynthesisError>; - fn count(&self, form: &Ptr) -> usize; - - // Circuit - fn allocated_r>(&self, cs: &mut CS) -> AllocatedNum; + fn allocated_r(&self) -> AllocatedNum; // x is H(k,v) = hash part of (cons k v) fn synthesize_map_to_element>( @@ -706,6 +883,22 @@ pub trait MemoSet: Clone { acc: &AllocatedNum, kv: &AllocatedPtr, ) -> Result, SynthesisError>; + + fn count(&self, form: &Ptr) -> usize; +} + +pub trait MemoSet: Clone { + type CM: CircuitMemoSet; + + fn into_circuit>(self, cs: &mut CS) -> Self::CM; + fn to_circuit>(&self, cs: &mut CS) -> Self::CM; + + fn is_finalized(&self) -> bool; + fn finalize_transcript(&mut self, s: &Store, transcript: Transcript); + fn r(&self) -> Option<&F>; + fn map_to_element(&self, x: F) -> Option; + fn add(&mut self, kv: Ptr); + fn count(&self, form: &Ptr) -> usize; } #[derive(Debug, Clone)] @@ -714,9 +907,16 @@ pub struct LogMemo { r: OnceCell, transcript: OnceCell>, + // Allocated only after transcript has been finalized. allocated_r: OnceCell>>, } +#[derive(Debug, Clone)] +pub struct LogMemoCircuit { + multiset: MultiSet, + r: AllocatedNum, +} + impl Default for LogMemo { fn default() -> Self { // Be explicit. @@ -728,8 +928,37 @@ impl Default for LogMemo { } } } +impl LogMemo { + fn allocated_r>(&self, cs: &mut CS) -> AllocatedNum { + self.allocated_r + .get_or_init(|| { + self.r() + .map(|r| AllocatedNum::alloc_infallible(&mut cs.namespace(|| "r"), || *r)) + }) + .clone() + .unwrap() + } +} impl MemoSet for LogMemo { + type CM = LogMemoCircuit; + + fn into_circuit>(self, cs: &mut CS) -> Self::CM { + let r = self.allocated_r(cs); + LogMemoCircuit { + multiset: self.multiset, + r, + } + } + + fn to_circuit>(&self, cs: &mut CS) -> Self::CM { + let r = self.allocated_r(cs); + LogMemoCircuit { + multiset: self.multiset.clone(), + r, + } + } + fn count(&self, form: &Ptr) -> usize { self.multiset.get(form).unwrap_or(0) } @@ -751,16 +980,6 @@ impl MemoSet for LogMemo { self.r.get() } - fn allocated_r>(&self, cs: &mut CS) -> AllocatedNum { - self.allocated_r - .get_or_init(|| { - self.r() - .map(|r| AllocatedNum::alloc_infallible(&mut cs.namespace(|| "r"), || *r)) - }) - .clone() - .unwrap() - } - // x is H(k,v) = hash part of (cons k v) fn map_to_element(&self, x: F) -> Option { self.r().and_then(|r| { @@ -769,22 +988,15 @@ impl MemoSet for LogMemo { }) } - // x is H(k,v) = hash part of (cons k v) - // 1 / r + x - fn synthesize_map_to_element>( - &self, - cs: &mut CS, - x: AllocatedNum, - ) -> Result, SynthesisError> { - let r = self.allocated_r(cs); - let r_plus_x = r.add(&mut cs.namespace(|| "r+x"), &x)?; - - invert(&mut cs.namespace(|| "invert(r+x)"), &r_plus_x) - } - fn add(&mut self, kv: Ptr) { self.multiset.add(kv); } +} + +impl CircuitMemoSet for LogMemoCircuit { + fn allocated_r(&self) -> AllocatedNum { + self.r.clone() + } fn synthesize_add>( &self, @@ -809,6 +1021,23 @@ impl MemoSet for LogMemo { let scaled = element.mul(&mut cs.namespace(|| "scaled"), count)?; sub(&mut cs.namespace(|| "add to acc"), acc, &scaled) } + + // x is H(k,v) = hash part of (cons k v) + // 1 / r + x + fn synthesize_map_to_element>( + &self, + cs: &mut CS, + x: AllocatedNum, + ) -> Result, SynthesisError> { + let r = self.r.clone(); + let r_plus_x = r.add(&mut cs.namespace(|| "r+x"), &x)?; + + invert(&mut cs.namespace(|| "invert(r+x)"), &r_plus_x) + } + + fn count(&self, form: &Ptr) -> usize { + self.multiset.get(form).unwrap_or(0) + } } #[cfg(test)] @@ -826,10 +1055,27 @@ mod test { fn test_query_with_internal_insertion_transcript() { test_query_aux( true, - expect!["10826"], - expect!["10859"], - expect!["11408"], - expect!["11445"], + expect!["10875"], + expect!["10908"], + expect!["11457"], + expect!["11494"], + 1, + ); + test_query_aux( + true, + expect!["12908"], + expect!["12947"], + expect!["13490"], + expect!["13533"], + 3, + ); + test_query_aux( + true, + expect!["21106"], + expect!["21169"], + expect!["21688"], + expect!["21755"], + 10, ) } @@ -837,10 +1083,27 @@ mod test { fn test_query_without_internal_insertion_transcript() { test_query_aux( false, - expect!["9381"], - expect!["9414"], - expect!["9963"], - expect!["10000"], + expect!["9430"], + expect!["9463"], + expect!["10012"], + expect!["10049"], + 1, + ); + test_query_aux( + false, + expect!["11174"], + expect!["11213"], + expect!["11756"], + expect!["11799"], + 3, + ); + test_query_aux( + false, + expect!["18216"], + expect!["18279"], + expect!["18798"], + expect!["18865"], + 10, ) } @@ -850,9 +1113,11 @@ mod test { expected_aux_simple: Expect, expected_constraints_compound: Expect, expected_aux_compound: Expect, + circuit_query_rc: usize, ) { let s = &Store::::default(); - let mut scope: Scope, LogMemo> = Scope::new(transcribe_internal_insertions); + let mut scope: Scope, LogMemo> = + Scope::new(transcribe_internal_insertions, circuit_query_rc); let state = State::init_lurk_state(); let fact_4 = s.read_with_default_state("(factorial 4)").unwrap(); @@ -905,7 +1170,7 @@ mod test { { let mut scope: Scope, LogMemo> = - Scope::new(transcribe_internal_insertions); + Scope::new(transcribe_internal_insertions, circuit_query_rc); scope.query(s, fact_4); scope.query(s, fact_3); diff --git a/src/coroutine/memoset/query.rs b/src/coroutine/memoset/query.rs index 24f4a1760f..cc19aaec10 100644 --- a/src/coroutine/memoset/query.rs +++ b/src/coroutine/memoset/query.rs @@ -1,6 +1,6 @@ use bellpepper_core::{ConstraintSystem, SynthesisError}; -use super::{CircuitScope, CircuitTranscript, LogMemo, Scope}; +use super::{CircuitScope, CircuitTranscript, LogMemo, LogMemoCircuit, Scope}; use crate::circuit::gadgets::pointer::AllocatedPtr; use crate::field::LurkField; use crate::lem::circuit::GlobalAllocator; @@ -22,6 +22,9 @@ where ) -> Ptr; fn from_ptr(s: &Store, ptr: &Ptr) -> Option; fn to_ptr(&self, s: &Store) -> Ptr; + fn to_circuit>(&self, cs: &mut CS, s: &Store) -> Self::CQ; + fn dummy_from_index(s: &Store, index: usize) -> Self; + fn symbol(&self) -> Symbol; fn symbol_ptr(&self, s: &Store) -> Ptr { s.intern_symbol(&self.symbol()) @@ -42,7 +45,7 @@ where cs: &mut CS, g: &GlobalAllocator, store: &Store, - scope: &mut CircuitScope>, + scope: &mut CircuitScope>, acc: &AllocatedPtr, transcript: &CircuitTranscript, ) -> Result<(AllocatedPtr, AllocatedPtr, CircuitTranscript), SynthesisError>; @@ -54,4 +57,6 @@ where } fn from_ptr>(cs: &mut CS, s: &Store, ptr: &Ptr) -> Option; + + fn dummy_from_index>(cs: &mut CS, s: &Store, index: usize) -> Self; }