Skip to main content

miden_ace_codegen/
registry.rs

1//! Registry construction over the proof orderings of a factored ACE composition.
2//!
3//! A multi-AIR relation whose committed trace order varies per workload needs one ACE
4//! circuit per ordering. The accepted set is committed as a Merkle tree — the *registry*
5//! — whose leaf at index `t` is the circuit commitment of the ordering with
6//! [`order_tag`] `t`, and whose root is a protocol constant bound into the Fiat-Shamir
7//! transcript. Orderings that no workload can produce are covered by a constant
8//! [`padding_leaf`].
9//!
10//! At `n` AIRs there are `n!` orderings, so for anything past a handful the leaves can
11//! be neither checked in nor materialised per process. This module implements the
12//! scheme that avoids both: check in the tree's node row at some
13//! [`RegistryLayout::row_depth`], authenticate that row against the root once
14//! ([`verify_row`]), and per lookup recompute only the addressed leaf's subtree
15//! ([`subtree_leaves`]) before splicing the two halves of its authentication path
16//! ([`path_in_verified_tree`]).
17//!
18//! Everything here is parameterised by [`RegistryLayout`]; `row_depth = 0` degenerates
19//! to "recompute every leaf, check the root", which is the right shape for a registry
20//! small enough to rebuild wholesale.
21
22use miden_core::{Felt, Word, crypto::hash::Poseidon2};
23use miden_crypto::{
24    field::ExtensionField,
25    merkle::{MerklePath, MerkleTree, NodeIndex},
26};
27
28use crate::{
29    AceError,
30    factory::{FactoredCircuitFactory, PackedLeafScratch},
31};
32
33/// Domain tag distinguishing registry padding leaves from circuit commitments.
34const PADDING_DOMAIN: u64 = 0xace;
35
36/// Largest AIR count whose complete permutation set fits in the `u32` registry-tag space.
37///
38/// `12!` fits; `13!` does not. Registry construction must reject larger compositions before
39/// any tag is narrowed to `u32`.
40pub const MAX_REGISTRY_AIRS: usize = 12;
41
42/// Leaf value for registry slots that no proof ordering maps to.
43///
44/// Constant, not index-derived: identical padding leaves let every all-padding subtree
45/// share one root per depth. The domain tag — not the index — is what stops a padding
46/// leaf being read as a circuit commitment, and a tag bound below the active leaf count
47/// stops padding slots being opened at all.
48pub fn padding_leaf() -> Word {
49    Poseidon2::hash_elements(&[Felt::new_unchecked(PADDING_DOMAIN)])
50}
51
52/// Compute `n!`.
53pub const fn factorial(n: usize) -> usize {
54    let mut result: usize = 1;
55    let mut factor: usize = 2;
56    while factor <= n {
57        result = match result.checked_mul(factor) {
58            Some(value) => value,
59            None => panic!("factorial overflows usize"),
60        };
61        factor += 1;
62    }
63    result
64}
65
66/// Return the smallest `d` such that `2^d >= value`.
67pub const fn ceil_log2(value: usize) -> usize {
68    assert!(value > 0, "ceil_log2 is undefined for zero");
69    let mut value = value - 1;
70    let mut result = 0;
71    while value > 0 {
72        value >>= 1;
73        result += 1;
74    }
75    result
76}
77
78/// Registry tag of a proof ordering: its Lehmer rank relative to the canonical
79/// (identity) instance order.
80///
81/// Digit `i` counts the smaller instance indices to the right of position `i`, weighted
82/// by `(n - 1 - i)!`. Panics unless `proof_order` is a nonempty permutation of
83/// `0..proof_order.len()` within [`MAX_REGISTRY_AIRS`].
84pub fn order_tag(proof_order: &[usize]) -> u32 {
85    let num_airs = proof_order.len();
86    assert!(
87        (1..=MAX_REGISTRY_AIRS).contains(&num_airs),
88        "registry order must contain 1..={MAX_REGISTRY_AIRS} AIRs"
89    );
90    assert!(is_permutation(proof_order), "proof order must be a permutation");
91    let mut rank: u64 = 0;
92    for i in 0..num_airs {
93        let smaller_after =
94            proof_order[i + 1..].iter().filter(|&&index| index < proof_order[i]).count();
95        rank += smaller_after as u64 * factorial(num_airs - 1 - i) as u64;
96    }
97    u32::try_from(rank).expect("tags of a supported AIR count fit in u32")
98}
99
100/// Decode a registry tag into its proof ordering over `num_airs` AIRs.
101///
102/// Returns `None` for tags at or above `num_airs!`, i.e. registry padding slots.
103pub fn order_from_tag(tag: u32, num_airs: usize) -> Option<Vec<usize>> {
104    if !(1..=MAX_REGISTRY_AIRS).contains(&num_airs) {
105        return None;
106    }
107    if tag as usize >= factorial(num_airs) {
108        return None;
109    }
110    let mut rank = tag as usize;
111    let mut remaining: Vec<usize> = (0..num_airs).collect();
112    let mut order = Vec::with_capacity(num_airs);
113    for i in 0..num_airs {
114        let factor = factorial(num_airs - 1 - i);
115        // The next Lehmer digit selects an instance index from the remaining ordered list.
116        order.push(remaining.remove(rank / factor));
117        rank %= factor;
118    }
119    Some(order)
120}
121
122fn is_permutation(proof_order: &[usize]) -> bool {
123    let mut seen = vec![false; proof_order.len()];
124    proof_order
125        .iter()
126        .all(|&index| index < seen.len() && !core::mem::replace(&mut seen[index], true))
127}
128
129/// Shape of a registry: how many orderings it covers and where its checked-in node row
130/// sits.
131///
132/// `row_depth` trades checked-in artifact size against per-lookup work: the row holds
133/// `2^row_depth` nodes and each covers `2^(tree_depth - row_depth)` leaves, which is
134/// what a lookup recomputes. `row_depth = 0` means the row is the root itself, i.e.
135/// every lookup rebuilds the whole tree.
136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137pub struct RegistryLayout {
138    num_airs: usize,
139    row_depth: usize,
140}
141
142impl RegistryLayout {
143    /// Build a layout, or `None` if the AIR count cannot be represented by `u32` tags or
144    /// the checked-in row does not sit strictly above the leaves.
145    ///
146    /// A registry needs at least two AIRs. A single AIR has one ordering, while the Merkle
147    /// implementation used by the serving path requires at least two leaves.
148    pub const fn new(num_airs: usize, row_depth: usize) -> Option<Self> {
149        if num_airs < 2 || num_airs > MAX_REGISTRY_AIRS {
150            return None;
151        }
152        if row_depth >= ceil_log2(factorial(num_airs)) {
153            return None;
154        }
155        Some(Self { num_airs, row_depth })
156    }
157
158    /// Number of AIRs in the composition.
159    pub const fn num_airs(&self) -> usize {
160        self.num_airs
161    }
162
163    /// Number of proof orderings, i.e. active leaves (`num_airs!`).
164    pub const fn order_count(&self) -> usize {
165        factorial(self.num_airs)
166    }
167
168    /// Smallest tree depth covering every tag.
169    pub const fn tree_depth(&self) -> usize {
170        ceil_log2(self.order_count())
171    }
172
173    /// Total leaf slots, active plus padding.
174    pub const fn leaf_count(&self) -> usize {
175        1 << self.tree_depth()
176    }
177
178    /// Depth of the checked-in node row.
179    pub const fn row_depth(&self) -> usize {
180        self.row_depth
181    }
182
183    /// Number of nodes in the checked-in row.
184    pub const fn row_len(&self) -> usize {
185        1 << self.row_depth
186    }
187
188    /// Leaves under one row node, i.e. the work one lookup recomputes.
189    pub const fn leaves_per_subtree(&self) -> usize {
190        1 << (self.tree_depth() - self.row_depth)
191    }
192}
193
194/// Compute the leaves of one row node's subtree, in slot order.
195///
196/// Slots below `layout.order_count()` get their ordering's circuit leaf through the
197/// factory's encode-only path; the rest keep [`padding_leaf`]. Realizable tags form a
198/// prefix of the slot range, so at most one subtree is part active and part padding.
199///
200/// This is the unit of work a caller parallelises over (`0..layout.row_len()` when
201/// minting, one index when serving); it is deliberately free of any parallelism itself.
202pub fn subtree_leaves<EF>(
203    factory: &FactoredCircuitFactory<EF>,
204    layout: &RegistryLayout,
205    subtree_index: usize,
206    scratch: &mut PackedLeafScratch,
207) -> Result<Vec<Word>, AceError>
208where
209    EF: ExtensionField<Felt>,
210{
211    let start = subtree_start(layout, subtree_index)?;
212    let realizable = layout.order_count().saturating_sub(start).min(layout.leaves_per_subtree());
213    let orders: Vec<Vec<usize>> = (0..realizable)
214        .map(|offset| {
215            order_from_tag((start + offset) as u32, layout.num_airs())
216                .expect("tag below the order count is realizable")
217        })
218        .collect();
219    let order_refs: Vec<&[usize]> = orders.iter().map(Vec::as_slice).collect();
220
221    let mut leaves = Vec::with_capacity(layout.leaves_per_subtree());
222    if !order_refs.is_empty() {
223        factory.leaves_for_orders(&order_refs, scratch, &mut leaves)?;
224    }
225    leaves.resize(layout.leaves_per_subtree(), padding_leaf());
226    Ok(leaves)
227}
228
229fn subtree_start(layout: &RegistryLayout, subtree_index: usize) -> Result<usize, AceError> {
230    if subtree_index >= layout.row_len() {
231        return Err(AceError::InvalidInputLayout {
232            message: format!(
233                "registry subtree index {subtree_index} is outside 0..{}",
234                layout.row_len()
235            ),
236        });
237    }
238    subtree_index.checked_mul(layout.leaves_per_subtree()).ok_or_else(|| {
239        AceError::InvalidInputLayout {
240            message: "registry subtree offset overflowed".into(),
241        }
242    })
243}
244
245/// Fold a node row up to the tree root.
246pub fn fold_row_to_root(row: &[Word]) -> Word {
247    assert!(row.len().is_power_of_two(), "a node row has a power-of-two length");
248    fold_levels(row).last().expect("root level")[0]
249}
250
251/// Node levels from `row` upward: element 0 is the row itself, the last the one-node root.
252fn fold_levels(row: &[Word]) -> Vec<Vec<Word>> {
253    let mut levels: Vec<Vec<Word>> = Vec::new();
254    levels.push(row.to_vec());
255    while levels.last().expect("at least the row").len() > 1 {
256        let below = levels.last().expect("level exists");
257        #[allow(clippy::chunks_exact_to_as_chunks)]
258        let above: Vec<Word> = below
259            .as_chunks::<2>()
260            .0
261            .iter()
262            .map(|pair| Poseidon2::merge(&[pair[0], pair[1]]))
263            .collect();
264        levels.push(above);
265    }
266    levels
267}
268
269/// Hash a checked-in node row upward and authenticate it against the registry root.
270///
271/// Returns the node pyramid, `pyramid[d]` holding the `2^d` nodes at depth `d` for `d`
272/// in `0..=row_depth`. The row itself is therefore not trust-bearing: a wrong or stale
273/// row fails here rather than producing paths that fail opaquely later. `mismatch_hint`
274/// is appended to the panic message so a caller can say how to regenerate its own
275/// constants.
276pub fn verify_row(
277    layout: &RegistryLayout,
278    row: &[Word],
279    expected_root: Word,
280    mismatch_hint: &str,
281) -> Vec<Vec<Word>> {
282    assert_eq!(
283        row.len(),
284        layout.row_len(),
285        "checked-in node row length does not match the registry layout"
286    );
287    let mut levels = fold_levels(row);
288    levels.reverse();
289    assert_eq!(
290        levels[0][0], expected_root,
291        "checked-in ACE registry node row does not hash to the registry root. {mismatch_hint}",
292    );
293    levels
294}
295
296/// Splice the authentication path for `tag` from its recomputed subtree and the verified
297/// pyramid above the row.
298///
299/// The lower `tree_depth - row_depth` siblings come from `subtree`; the upper
300/// `row_depth` siblings are read off the pyramid, whose entries were authenticated
301/// against the root by [`verify_row`]. The subtree's own root is checked against its row
302/// entry first, which re-derives per lookup the binding the mint established.
303pub fn path_in_verified_tree(
304    layout: &RegistryLayout,
305    pyramid: &[Vec<Word>],
306    subtree: &MerkleTree,
307    tag: u32,
308    mismatch_hint: &str,
309) -> Result<(Word, MerklePath), AceError> {
310    if tag as usize >= layout.leaf_count() {
311        return Err(AceError::InvalidInputLayout {
312            message: format!("registry tag {tag} is outside the tree"),
313        });
314    }
315    if pyramid.len() != layout.row_depth() + 1
316        || pyramid.iter().enumerate().any(|(depth, level)| level.len() != 1 << depth)
317    {
318        return Err(AceError::InvalidInputLayout {
319            message: "registry pyramid does not match the layout".into(),
320        });
321    }
322
323    let subtree_index = tag as usize / layout.leaves_per_subtree();
324    assert_eq!(
325        subtree.root(),
326        pyramid[layout.row_depth()][subtree_index],
327        "recomputed ACE registry subtree {subtree_index} does not match the checked-in \
328         node row. {mismatch_hint}",
329    );
330
331    let index = NodeIndex::new(
332        (layout.tree_depth() - layout.row_depth()) as u8,
333        (tag as usize % layout.leaves_per_subtree()) as u64,
334    )
335    .map_err(|_| AceError::InvalidInputLayout {
336        message: "registry tag does not fit the subtree".into(),
337    })?;
338    let leaf = subtree.get_node(index).map_err(|_| AceError::InvalidInputLayout {
339        message: "registry subtree does not contain the selected leaf".into(),
340    })?;
341    let mut nodes = subtree
342        .get_path(index)
343        .map_err(|_| AceError::InvalidInputLayout {
344            message: "registry subtree cannot authenticate the selected leaf".into(),
345        })?
346        .nodes()
347        .to_vec();
348
349    // Upper siblings: at depth `d` the ancestor of the tag's subtree is
350    // `subtree_index >> (row_depth - d)`, and its sibling flips bit 0.
351    for depth in (1..=layout.row_depth()).rev() {
352        let ancestor = subtree_index >> (layout.row_depth() - depth);
353        nodes.push(pyramid[depth][ancestor ^ 1]);
354    }
355    Ok((leaf, MerklePath::new(nodes)))
356}
357
358#[cfg(test)]
359mod tests {
360    use proptest::prelude::*;
361
362    use super::*;
363
364    fn registry_path_case() -> impl Strategy<Value = (RegistryLayout, u32, u32)> {
365        (2usize..=6).prop_flat_map(|num_airs| {
366            let tree_depth = ceil_log2(factorial(num_airs));
367            (0..tree_depth).prop_flat_map(move |row_depth| {
368                let layout = RegistryLayout::new(num_airs, row_depth).expect("valid layout");
369                let mut boundary_tags = vec![0, layout.order_count() as u32 - 1];
370                if layout.order_count() < layout.leaf_count() {
371                    boundary_tags.push(layout.order_count() as u32);
372                }
373                boundary_tags.push(layout.leaf_count() as u32 - 1);
374                (
375                    Just(layout),
376                    prop_oneof![
377                        3 => proptest::sample::select(boundary_tags),
378                        5 => 0..layout.leaf_count() as u32,
379                    ],
380                    any::<u32>(),
381                )
382            })
383        })
384    }
385
386    #[test]
387    fn order_tags_round_trip_over_the_whole_range() {
388        for num_airs in 1..=6 {
389            for tag in 0..factorial(num_airs) as u32 {
390                let order = order_from_tag(tag, num_airs).expect("tag in range");
391                assert_eq!(order_tag(&order), tag, "round trip fails at {num_airs} AIRs, {tag}");
392            }
393            assert_eq!(order_from_tag(factorial(num_airs) as u32, num_airs), None);
394            let identity: Vec<usize> = (0..num_airs).collect();
395            assert_eq!(order_tag(&identity), 0, "the identity ordering must be tag 0");
396        }
397    }
398
399    proptest! {
400        #![proptest_config(ProptestConfig::with_cases(32))]
401
402        #[test]
403        fn larger_order_tags_round_trip(raw_tags in any::<[u32; 6]>()) {
404            for (num_airs, raw_tag) in (7..=MAX_REGISTRY_AIRS).zip(raw_tags) {
405                let tag = raw_tag % factorial(num_airs) as u32;
406                let order = order_from_tag(tag, num_airs).expect("tag in range");
407                prop_assert_eq!(order_tag(&order), tag);
408            }
409        }
410
411        #[test]
412        fn spliced_paths_match_varied_registry_layouts(
413            (layout, tag, salt) in registry_path_case(),
414        ) {
415            let mut leaves: Vec<Word> = (0..layout.order_count())
416                .map(|index| {
417                    Poseidon2::hash_elements(&[
418                        Felt::new_unchecked(u64::from(salt)),
419                        Felt::new_unchecked(index as u64),
420                    ])
421                })
422                .collect();
423            leaves.resize(layout.leaf_count(), padding_leaf());
424
425            let tree = MerkleTree::new(&leaves).expect("complete tree");
426            let row: Vec<Word> = if layout.row_depth() == 0 {
427                vec![tree.root()]
428            } else {
429                (0..layout.row_len())
430                    .map(|index| {
431                        tree.get_node(
432                            NodeIndex::new(layout.row_depth() as u8, index as u64)
433                                .expect("row index"),
434                        )
435                        .expect("row node")
436                    })
437                    .collect()
438            };
439            let pyramid = verify_row(&layout, &row, tree.root(), "toy row must authenticate");
440            let subtree_index = tag as usize / layout.leaves_per_subtree();
441            let start = subtree_index * layout.leaves_per_subtree();
442            let subtree = MerkleTree::new(&leaves[start..start + layout.leaves_per_subtree()])
443                .expect("complete subtree");
444
445            let (leaf, path) =
446                path_in_verified_tree(&layout, &pyramid, &subtree, tag, "toy path")
447                    .expect("valid path");
448            prop_assert_eq!(leaf, leaves[tag as usize]);
449            prop_assert_eq!(
450                path.compute_root(u64::from(tag), leaf).expect("path root"),
451                tree.root(),
452            );
453        }
454    }
455
456    #[test]
457    fn layout_derives_its_shape_from_the_air_count() {
458        let layout = RegistryLayout::new(10, 12).expect("valid layout");
459        assert_eq!(layout.order_count(), 3_628_800);
460        assert_eq!(layout.tree_depth(), 22);
461        assert_eq!(layout.leaf_count(), 1 << 22);
462        assert_eq!(layout.row_len(), 4096);
463        assert_eq!(layout.leaves_per_subtree(), 1024);
464
465        // row_depth = 0 degenerates to a single whole-tree rebuild per lookup.
466        let whole = RegistryLayout::new(3, 0).expect("valid layout");
467        assert_eq!(whole.row_len(), 1);
468        assert_eq!(whole.leaves_per_subtree(), whole.leaf_count());
469
470        assert!(RegistryLayout::new(3, 3).is_none(), "row must sit above the leaves");
471        assert!(RegistryLayout::new(3, 4).is_none(), "row cannot sit below the leaves");
472        assert!(RegistryLayout::new(0, 0).is_none(), "a registry needs at least two AIRs");
473        assert!(RegistryLayout::new(1, 0).is_none(), "a registry needs at least two leaves");
474        assert!(
475            RegistryLayout::new(MAX_REGISTRY_AIRS + 1, 0).is_none(),
476            "the full permutation set must fit in u32 tags"
477        );
478        assert_eq!(order_from_tag(0, MAX_REGISTRY_AIRS + 1), None);
479    }
480
481    #[test]
482    fn subtree_offsets_reject_indices_outside_the_row() {
483        let layout = RegistryLayout::new(3, 1).expect("valid layout");
484        assert!(subtree_start(&layout, layout.row_len()).is_err());
485        assert!(subtree_start(&layout, usize::MAX).is_err());
486    }
487
488    #[test]
489    #[should_panic(expected = "proof order must be a permutation")]
490    fn order_tag_rejects_invalid_permutations_in_all_builds() {
491        let _ = order_tag(&[0, 0, 2]);
492    }
493
494    #[test]
495    #[should_panic(expected = "node row length does not match the registry layout")]
496    fn verified_rows_are_bound_to_the_layout() {
497        let layout = RegistryLayout::new(3, 1).expect("valid layout");
498        let row = vec![padding_leaf()];
499        let _ = verify_row(&layout, &row, row[0], "test row must be complete");
500    }
501
502    #[test]
503    fn spliced_paths_match_a_materialised_tree_for_every_slot() {
504        // A row depth above 1 makes the pyramid's upper-sibling shift
505        // (`subtree_index >> (row_depth - depth)`) act on more than the trivial
506        // zero-shift level, matching the production depth-12 geometry in miniature.
507        for (num_airs, row_depth) in [(3, 1), (4, 2)] {
508            assert_spliced_paths_match_a_materialised_tree(num_airs, row_depth);
509        }
510    }
511
512    fn assert_spliced_paths_match_a_materialised_tree(num_airs: usize, row_depth: usize) {
513        let layout = RegistryLayout::new(num_airs, row_depth).expect("valid layout");
514        let mut leaves: Vec<Word> = (0..layout.order_count())
515            .map(|tag| Poseidon2::hash_elements(&[Felt::new_unchecked(0x1000 + tag as u64)]))
516            .collect();
517        leaves.resize(layout.leaf_count(), padding_leaf());
518
519        let tree = MerkleTree::new(&leaves).expect("complete tree");
520        let row: Vec<Word> = (0..layout.row_len())
521            .map(|index| {
522                tree.get_node(
523                    NodeIndex::new(layout.row_depth() as u8, index as u64).expect("row index"),
524                )
525                .expect("row node")
526            })
527            .collect();
528        let pyramid = verify_row(&layout, &row, tree.root(), "toy row must authenticate");
529
530        for tag in 0..layout.leaf_count() {
531            let subtree_index = tag / layout.leaves_per_subtree();
532            let start = subtree_index * layout.leaves_per_subtree();
533            let subtree = MerkleTree::new(&leaves[start..start + layout.leaves_per_subtree()])
534                .expect("complete subtree");
535            let (leaf, path) =
536                path_in_verified_tree(&layout, &pyramid, &subtree, tag as u32, "toy path")
537                    .expect("valid path");
538            assert_eq!(leaf, leaves[tag]);
539            assert_eq!(
540                path.compute_root(tag as u64, leaf).expect("path root"),
541                tree.root(),
542                "path does not verify at tag {tag}"
543            );
544        }
545
546        let subtree =
547            MerkleTree::new(&leaves[..layout.leaves_per_subtree()]).expect("complete subtree");
548        assert!(
549            path_in_verified_tree(
550                &layout,
551                &pyramid,
552                &subtree,
553                layout.leaf_count() as u32,
554                "toy path",
555            )
556            .is_err(),
557            "a tag outside the tree must be rejected"
558        );
559        assert!(
560            path_in_verified_tree(&layout, &pyramid[..1], &subtree, 0, "toy path").is_err(),
561            "a pyramid that does not match the layout must be rejected"
562        );
563    }
564}