Skip to main content

sonobe_primitives/transcripts/
mod.rs

1//! Abstractions of sponges and Fiat-Shamir transcripts.
2//!
3//! This module defines the traits that unify hash functions (Poseidon, Griffin,
4//! etc.) behind a common absorb / squeeze interface suitable for building
5//! non-interactive proofs.
6//!
7//! Concrete implementations live in the [`poseidon`] and [`griffin`]
8//! sub-modules.
9
10use ark_ff::{BigInteger, PrimeField};
11use ark_r1cs_std::{boolean::Boolean, convert::ToBitsGadget, fields::fp::FpVar};
12use ark_relations::gr1cs::SynthesisError;
13
14pub use self::absorbable::{Absorbable, AbsorbableVar};
15
16pub mod absorbable;
17pub mod griffin;
18pub mod poseidon;
19pub mod recording;
20pub mod replay;
21
22/// [`Transcript`] is the out-of-circuit widget for transcripts and sponges.
23///
24/// Provers and verifiers can use this trait to absorb messages and squeeze
25/// challenges in a way that is agnostic to the underlying hash function.
26pub trait Transcript<F: PrimeField>: Clone {
27    /// [`Transcript::Config`] is the configuration for the underlying hash
28    /// function of the transcript.
29    type Config: Clone;
30
31    /// [`Transcript::Gadget`] is the in-circuit gadget corresponding to this
32    /// widget.
33    type Gadget: TranscriptGadget<F, Widget = Self>;
34
35    /// [`Transcript::new`] creates a new transcript / sponge under the given
36    /// configuration `config`.
37    fn new(config: Self::Config) -> Self;
38
39    /// [`Transcript::new_with_pp_hash`] is a convenience method for creating a
40    /// new transcript / sponge under the given configuration `config` and
41    /// additionally absorbing a hash of the public parameters `pp_hash`.
42    fn new_with_pp_hash(config: Self::Config, pp_hash: F) -> Self {
43        let mut sponge = Self::new(config);
44        sponge.add_field_elements(&[pp_hash]);
45        sponge
46    }
47
48    /// [`Transcript::add`] absorbs a message `input` that can be any type
49    /// implementing the [`Absorbable`] trait into the transcript / sponge.
50    fn add<A: Absorbable + ?Sized>(&mut self, input: &A) -> &mut Self {
51        let mut elems = Vec::new();
52        input.absorb_into(&mut elems);
53
54        self.add_field_elements(&elems)
55    }
56
57    /// [`Transcript::add_field_elements`] absorbs a message `input` that is
58    /// represented as field elements into the transcript / sponge.
59    fn add_field_elements(&mut self, input: &[F]) -> &mut Self;
60
61    /// [`Transcript::get_bits`] squeezes `num_bits` bits from the transcript /
62    /// sponge.
63    fn get_bits(&mut self, num_bits: usize) -> Vec<bool> {
64        let usable_bits = (F::MODULUS_BIT_SIZE - 1) as usize;
65
66        let num_elements = num_bits.div_ceil(usable_bits);
67        let src_elements = self.get_field_elements(num_elements);
68
69        let mut bits: Vec<bool> = Vec::with_capacity(usable_bits * num_elements);
70        for elem in &src_elements {
71            let elem_bits = elem.into_bigint().to_bits_le();
72            bits.extend_from_slice(&elem_bits[..usable_bits]);
73        }
74
75        bits.truncate(num_bits);
76        bits
77    }
78
79    /// [`Transcript::get_field_element`] squeezes a single field element from
80    /// the transcript / sponge.
81    fn get_field_element(&mut self) -> F {
82        self.get_field_elements(1)[0]
83    }
84
85    /// [`Transcript::get_field_elements`] squeezes `num_elements` field
86    /// elements from the transcript / sponge.
87    fn get_field_elements(&mut self, num_elements: usize) -> Vec<F>;
88
89    /// [`Transcript::separate_domain`] creates a new transcript / sponge by
90    /// applying domain separation using the provided `domain` byte sequence.
91    fn separate_domain(&self, domain: &[u8]) -> Self {
92        let mut new_sponge = self.clone();
93
94        // Encode the domain length with a fixed-width `u64` so the derived
95        // challenges are identical across targets.
96        let mut input = (domain.len() as u64).to_le_bytes().to_vec();
97        input.extend_from_slice(domain);
98
99        // Chunk into `(MODULUS_BIT_SIZE - 1) / 8` bytes so a full chunk is
100        // always `< 2^(MODULUS_BIT_SIZE - 1) <= MODULUS`
101        let limbs = input
102            .chunks((F::MODULUS_BIT_SIZE as usize - 1) / 8)
103            .map(|chunk| F::from_le_bytes_mod_order(chunk))
104            .collect::<Vec<_>>();
105
106        new_sponge.add_field_elements(&limbs);
107
108        new_sponge
109    }
110
111    /// [`Transcript::challenge_field_element`] squeezes a challenge from the
112    /// transcript as a field element.
113    ///
114    /// Internally, it first squeezes a field element and then absorbs it back
115    /// into the transcript to ensure security.
116    fn challenge_field_element(&mut self) -> F {
117        let c = self.get_field_elements(1);
118        self.add_field_elements(&c);
119        c[0]
120    }
121
122    /// [`Transcript::challenge_bits`] squeezes a challenge from the transcript
123    /// as a bit vector.
124    ///
125    /// Internally, it squeezes several field elements, absorbs them back to the
126    /// transcript (for strong Fiat-Shamir), and decomposes them into bits.
127    fn challenge_bits(&mut self, num_bits: usize) -> Vec<bool> {
128        let usable_bits = (F::MODULUS_BIT_SIZE - 1) as usize;
129
130        let num_elements = num_bits.div_ceil(usable_bits);
131        let src_elements = self.challenge_field_elements(num_elements);
132
133        let mut bits: Vec<bool> = Vec::with_capacity(usable_bits * num_elements);
134        for elem in &src_elements {
135            let elem_bits = elem.into_bigint().to_bits_le();
136            bits.extend_from_slice(&elem_bits[..usable_bits]);
137        }
138
139        bits.truncate(num_bits);
140        bits
141    }
142
143    /// [`Transcript::challenge_field_elements`] squeezes `n` challenges from
144    /// the transcript as field elements.
145    ///
146    /// Internally, it first squeezes the field elements and then absorbs them
147    /// back into the transcript to ensure security.
148    fn challenge_field_elements(&mut self, n: usize) -> Vec<F> {
149        let c = self.get_field_elements(n);
150        self.add_field_elements(&c);
151        c
152    }
153}
154
155/// [`TranscriptGadget`] is the in-circuit gadget for transcripts and sponges.
156pub trait TranscriptGadget<F: PrimeField>: Clone {
157    /// [`TranscriptGadget::Config`] is the configuration for the underlying
158    /// hash function of the transcript gadget.
159    type Config: Clone;
160
161    /// [`TranscriptGadget::Widget`] points to the out-of-circuit widget for
162    /// this transcript gadget.
163    type Widget: Transcript<F, Gadget = Self>;
164
165    /// [`TranscriptGadget::new`] creates a new transcript / sponge variable
166    /// under the given configuration `config`.
167    fn new(config: Self::Config) -> Self;
168
169    /// [`TranscriptGadget::new_with_pp_hash`] is a convenience method for
170    /// creating a new transcript / sponge variable under the given
171    /// configuration `config` and additionally absorbing a hash of the public
172    /// parameters `pp_hash`.
173    fn new_with_pp_hash(config: Self::Config, pp_hash: &FpVar<F>) -> Result<Self, SynthesisError> {
174        let mut sponge = Self::new(config);
175        sponge.add(&pp_hash)?;
176        Ok(sponge)
177    }
178
179    /// [`TranscriptGadget::add`] absorbs a message `input` that can be any type
180    /// implementing the [`AbsorbableVar`] trait into the transcript / sponge
181    /// variable.
182    fn add<A: AbsorbableVar<F>>(&mut self, input: &A) -> Result<&mut Self, SynthesisError>;
183
184    /// [`TranscriptGadget::get_bits`] squeezes `num_bits` bit variables from
185    /// the transcript / sponge variable.
186    fn get_bits(&mut self, num_bits: usize) -> Result<Vec<Boolean<F>>, SynthesisError> {
187        let usable_bits = (F::MODULUS_BIT_SIZE - 1) as usize;
188
189        let num_elements = num_bits.div_ceil(usable_bits);
190        let src_elements = self.get_field_elements(num_elements)?;
191
192        let mut bits: Vec<Boolean<F>> = Vec::with_capacity(usable_bits * num_elements);
193        for elem in &src_elements {
194            bits.extend_from_slice(&elem.to_bits_le()?[..usable_bits]);
195        }
196
197        bits.truncate(num_bits);
198        Ok(bits)
199    }
200
201    /// [`TranscriptGadget::get_field_element`] squeezes a single field element
202    /// variable from the transcript / sponge variable.
203    fn get_field_element(&mut self) -> Result<FpVar<F>, SynthesisError> {
204        Ok(self.get_field_elements(1)?.swap_remove(0))
205    }
206
207    /// [`TranscriptGadget::get_field_elements`] squeezes `num_elements` field
208    /// element variables from the transcript / sponge variable.
209    fn get_field_elements(&mut self, num_elements: usize) -> Result<Vec<FpVar<F>>, SynthesisError>;
210
211    /// [`TranscriptGadget::separate_domain`] creates a new transcript / sponge
212    /// variable by applying domain separation using the provided `domain` byte
213    /// sequence.
214    fn separate_domain(&self, domain: &[u8]) -> Result<Self, SynthesisError> {
215        let mut new_sponge = self.clone();
216
217        // Encode the domain length with a fixed-width `u64` so the derived
218        // challenges are identical across targets.
219        let mut input = (domain.len() as u64).to_le_bytes().to_vec();
220        input.extend_from_slice(domain);
221
222        // Chunk into `(MODULUS_BIT_SIZE - 1) / 8` bytes so a full chunk is
223        // always `< 2^(MODULUS_BIT_SIZE - 1) <= MODULUS`
224        let limbs = input
225            .chunks((F::MODULUS_BIT_SIZE as usize - 1) / 8)
226            .map(|chunk| FpVar::Constant(F::from_le_bytes_mod_order(chunk)))
227            .collect::<Vec<_>>();
228
229        new_sponge.add(&limbs)?;
230
231        Ok(new_sponge)
232    }
233
234    /// [`TranscriptGadget::challenge_field_element`] squeezes a challenge from
235    /// the transcript variable as a field element variable.
236    ///
237    /// Internally, it first squeezes a field element variable and then absorbs
238    /// it back into the transcript variable to ensure security.
239    fn challenge_field_element(&mut self) -> Result<FpVar<F>, SynthesisError> {
240        let mut c = self.get_field_elements(1)?;
241        self.add(&c[0])?;
242        Ok(c.swap_remove(0))
243    }
244
245    /// [`TranscriptGadget::challenge_bits`] squeezes a challenge from the
246    /// transcript variable as a vector of bit variables.
247    ///
248    /// Internally, it squeezes several field element variables, absorbs them
249    /// back to the transcript variable (for strong Fiat-Shamir), and decomposes
250    /// them into bit variables.
251    fn challenge_bits(&mut self, num_bits: usize) -> Result<Vec<Boolean<F>>, SynthesisError> {
252        let usable_bits = (F::MODULUS_BIT_SIZE - 1) as usize;
253
254        let num_elements = num_bits.div_ceil(usable_bits);
255        let src_elements = self.challenge_field_elements(num_elements)?;
256
257        let mut bits: Vec<Boolean<F>> = Vec::with_capacity(usable_bits * num_elements);
258        for elem in &src_elements {
259            bits.extend_from_slice(&elem.to_bits_le()?[..usable_bits]);
260        }
261
262        bits.truncate(num_bits);
263        Ok(bits)
264    }
265
266    /// [`TranscriptGadget::challenge_field_elements`] squeezes `n` challenges
267    /// from the transcript variable as field element variables.
268    ///
269    /// Internally, it first squeezes the field element variables and then
270    /// absorbs them back into the transcript variable to ensure
271    /// security.
272    fn challenge_field_elements(&mut self, n: usize) -> Result<Vec<FpVar<F>>, SynthesisError> {
273        let c = self.get_field_elements(n)?;
274        self.add(&c)?;
275        Ok(c)
276    }
277}