miden_ace_codegen/
factory.rs1use miden_core::{Felt, Word, crypto::hash::Poseidon2};
15use miden_crypto::{
16 field::{ExtensionField, Field},
17 hash::poseidon2::Poseidon2Permutation256,
18 stark::symmetric::Permutation,
19};
20use miden_field::{PackedValue, PrimeCharacteristicRing};
21
22use crate::{
23 AceError, EXT_DEGREE, encode::EncodedCircuit, factored::ShuffleEncodeBuffer,
24 pipeline::FactoredMultiAirCircuit,
25};
26
27const RATE_WIDTH: usize = Poseidon2::RATE_RANGE.end - Poseidon2::RATE_RANGE.start;
29
30type PackedFelt = <Felt as Field>::Packing;
32
33pub const LEAF_LANES: usize = <PackedFelt as PackedValue>::WIDTH;
36
37#[derive(Default)]
39pub struct PackedLeafScratch {
40 buffer: ShuffleEncodeBuffer,
41 streams: Vec<Vec<Felt>>,
42}
43
44impl PackedLeafScratch {
45 pub fn new() -> Self {
47 Self::default()
48 }
49}
50
51#[derive(Clone, Debug)]
53pub struct FactoredEncodedCircuit {
54 pub encoded: EncodedCircuit,
56 pub shuffle_prefix_len: usize,
58 pub shuffle_commitment: Word,
60 pub common_commitment: Word,
62 pub commitment: Word,
64}
65
66pub struct FactoredCircuitFactory<EF> {
68 factored: FactoredMultiAirCircuit<EF>,
69 constants_state: [Felt; Poseidon2::STATE_WIDTH],
77 const_felts: usize,
79 common_commitment: Word,
81}
82
83impl<EF> FactoredCircuitFactory<EF>
84where
85 EF: ExtensionField<Felt>,
86{
87 pub fn new(factored: FactoredMultiAirCircuit<EF>) -> Result<Self, AceError> {
96 let canonical: Vec<usize> = (0..factored.num_airs()).collect();
97 let circuit = factored.circuit_for_order(&canonical)?;
98 let encoded = circuit.to_ace()?;
99 let instructions = encoded.instructions();
100 let const_felts = encoded.num_constants() * EXT_DEGREE;
101 let prefix_len = const_felts + factored.num_shuffle_ops();
102 if !const_felts.is_multiple_of(RATE_WIDTH)
103 || !prefix_len.is_multiple_of(RATE_WIDTH)
104 || prefix_len >= instructions.len()
105 {
106 return Err(AceError::InvalidInputLayout {
107 message: "ACE stream sections must be rate-aligned for prefix resumption".into(),
108 });
109 }
110
111 let mut constants_state = [<Felt as PrimeCharacteristicRing>::ZERO; Poseidon2::STATE_WIDTH];
112 absorb_rate_blocks(&mut constants_state, &instructions[..const_felts]);
113 let common_commitment = Poseidon2::hash_elements(&instructions[prefix_len..]);
114
115 let mut buffer = ShuffleEncodeBuffer::new();
116 let fast = factored.encode_shuffle_section_for_order(&canonical, &mut buffer)?;
117 if fast != &instructions[const_felts..prefix_len] {
118 return Err(AceError::InvalidInputLayout {
119 message: "encode-only shuffle section diverges from the assembled stream".into(),
120 });
121 }
122 let mut resumed = constants_state;
123 absorb_rate_blocks(&mut resumed, fast);
124 let resumed_prefix =
125 Word::new(resumed[Poseidon2::RATE0_RANGE].try_into().expect("digest is one word"));
126 if resumed_prefix != Poseidon2::hash_elements(&instructions[..prefix_len]) {
127 return Err(AceError::InvalidInputLayout {
128 message: "resumed prefix hash diverges from hashing the full prefix".into(),
129 });
130 }
131
132 Ok(Self {
133 factored,
134 constants_state,
135 const_felts,
136 common_commitment,
137 })
138 }
139
140 pub fn factored(&self) -> &FactoredMultiAirCircuit<EF> {
142 &self.factored
143 }
144
145 pub fn const_felts(&self) -> usize {
147 self.const_felts
148 }
149
150 pub fn leaf_for_order(
159 &self,
160 proof_order: &[usize],
161 buffer: &mut ShuffleEncodeBuffer,
162 ) -> Result<Word, AceError> {
163 let shuffle = self.factored.encode_shuffle_section_for_order(proof_order, buffer)?;
164 let mut state = self.constants_state;
165 absorb_rate_blocks(&mut state, shuffle);
166 let shuffle_commitment =
167 Word::new(state[Poseidon2::RATE0_RANGE].try_into().expect("digest is one word"));
168 Ok(Poseidon2::merge(&[shuffle_commitment, self.common_commitment]))
169 }
170
171 pub fn leaves_for_orders(
182 &self,
183 orders: &[&[usize]],
184 scratch: &mut PackedLeafScratch,
185 out: &mut Vec<Word>,
186 ) -> Result<(), AceError> {
187 scratch.streams.resize_with(LEAF_LANES, Vec::new);
188 for chunk in orders.chunks(LEAF_LANES) {
189 for lane in 0..LEAF_LANES {
190 let order = chunk.get(lane).copied().unwrap_or(chunk[chunk.len() - 1]);
192 let shuffle =
193 self.factored.encode_shuffle_section_for_order(order, &mut scratch.buffer)?;
194 scratch.streams[lane].clear();
195 scratch.streams[lane].extend_from_slice(shuffle);
196 }
197
198 let mut state: [PackedFelt; Poseidon2::STATE_WIDTH] = core::array::from_fn(|e| {
201 let mut packed = <PackedFelt as PrimeCharacteristicRing>::ZERO;
202 packed.as_slice_mut().fill(self.constants_state[e]);
203 packed
204 });
205 assert!(
208 scratch.streams[0].len().is_multiple_of(RATE_WIDTH),
209 "shuffle streams must be rate-aligned"
210 );
211 let blocks = scratch.streams[0].len() / RATE_WIDTH;
212 for block in 0..blocks {
213 for i in 0..RATE_WIDTH {
214 let elem = &mut state[Poseidon2::RATE_RANGE.start + i];
215 for lane in 0..LEAF_LANES {
216 elem.as_slice_mut()[lane] = scratch.streams[lane][block * RATE_WIDTH + i];
217 }
218 }
219 Poseidon2Permutation256.permute_mut(&mut state);
220 }
221
222 let mut merge_state: [PackedFelt; Poseidon2::STATE_WIDTH] =
225 core::array::from_fn(|_| <PackedFelt as PrimeCharacteristicRing>::ZERO);
226 for i in 0..4 {
227 merge_state[i] = state[Poseidon2::RATE0_RANGE.start + i];
228 let mut common = <PackedFelt as PrimeCharacteristicRing>::ZERO;
229 common.as_slice_mut().fill(self.common_commitment[i]);
230 merge_state[4 + i] = common;
231 }
232 Poseidon2Permutation256.permute_mut(&mut merge_state);
233
234 for lane in 0..chunk.len() {
235 let leaf: [Felt; 4] = core::array::from_fn(|i| merge_state[i].as_slice()[lane]);
236 out.push(Word::new(leaf));
237 }
238 }
239 Ok(())
240 }
241
242 pub fn circuit_for_order(
249 &self,
250 proof_order: &[usize],
251 ) -> Result<FactoredEncodedCircuit, AceError> {
252 let circuit = self.factored.circuit_for_order(proof_order)?;
253 let encoded = circuit.to_ace()?;
254 let instructions = encoded.instructions();
255 let stream_len = encoded.size_in_felt();
256 if stream_len != instructions.len() {
257 return Err(AceError::InvalidInputLayout {
258 message: format!(
259 "ACE circuit stream length ({stream_len}) does not match instruction count \
260 ({})",
261 instructions.len()
262 ),
263 });
264 }
265 let shuffle_prefix_len = self.const_felts + self.factored.num_shuffle_ops();
266 if encoded.num_constants() * EXT_DEGREE != self.const_felts
267 || !stream_len.is_multiple_of(RATE_WIDTH)
268 || shuffle_prefix_len >= stream_len
269 {
270 return Err(AceError::InvalidInputLayout {
271 message: "assembled ACE stream does not match the factored section layout".into(),
272 });
273 }
274
275 let mut state = self.constants_state;
276 absorb_rate_blocks(&mut state, &instructions[self.const_felts..shuffle_prefix_len]);
277 let shuffle_commitment =
278 Word::new(state[Poseidon2::RATE0_RANGE].try_into().expect("digest is one word"));
279 let common_commitment = self.common_commitment;
280 let commitment = Poseidon2::merge(&[shuffle_commitment, common_commitment]);
281
282 Ok(FactoredEncodedCircuit {
283 encoded,
284 shuffle_prefix_len,
285 shuffle_commitment,
286 common_commitment,
287 commitment,
288 })
289 }
290}
291
292fn absorb_rate_blocks(state: &mut [Felt; Poseidon2::STATE_WIDTH], elements: &[Felt]) {
294 assert!(
297 elements.len().is_multiple_of(RATE_WIDTH),
298 "sponge absorption requires whole rate blocks"
299 );
300 for block in elements.as_chunks::<RATE_WIDTH>().0 {
301 state[Poseidon2::RATE_RANGE].copy_from_slice(block);
302 Poseidon2::apply_permutation(state);
303 }
304}