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}