Skip to main content

miden_crypto/hash/eidos/
domain.rs

1//! Typed Eidos domains and registry support.
2//!
3//! Registered Eidos tags use an 8/16/8 hierarchy:
4//!
5//! ```text
6//!  31            24 23                         8 7             0
7//! +----------------+-----------------------------+---------------+
8//! |  namespace: 8  |       local id: 16          |  version: 8   |
9//! +----------------+-----------------------------+---------------+
10//! ```
11//!
12//! A namespace is allocated centrally. Its owner maintains a local registry and is responsible for
13//! assigning each local ID under exactly one versioning policy. Versions `1..=255` identify a
14//! numbered construction. Version `0` delegates versioning to the authenticated payload and may
15//! not coexist with numbered versions of the same local ID.
16//!
17//! The all-zero tag is deliberately not registrable. Together with three zero parameters, it is
18//! reserved for the fixed, one-block Merkle inner-node compression exposed by
19//! [`super::Eidos::merge`].
20
21use alloc::string::String;
22use core::fmt::{self, Write};
23
24use crate::Felt;
25
26/// One centrally allocated 8-bit Eidos namespace.
27///
28/// Add namespaces to [`namespace`] and [`NAMESPACE_REGISTRY`] in `miden-crypto`. Downstream
29/// registries select one of those values rather than constructing their own.
30#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
31pub struct DomainNamespace(u8);
32
33impl DomainNamespace {
34    const fn new(value: u8) -> Self {
35        Self(value)
36    }
37
38    /// Returns the numeric namespace prefix.
39    pub const fn as_u8(self) -> u8 {
40        self.0
41    }
42
43    const fn from_allocated_u8(value: u8) -> Option<Self> {
44        let mut index = 0;
45        while index < NAMESPACE_REGISTRY.len() {
46            let namespace = NAMESPACE_REGISTRY[index].namespace;
47            if namespace.as_u8() == value {
48                return Some(namespace);
49            }
50            index += 1;
51        }
52        None
53    }
54}
55
56/// Centrally allocated Eidos namespaces.
57pub mod namespace {
58    use super::DomainNamespace;
59
60    /// Domains maintained by `miden-crypto`.
61    pub const MIDEN_CRYPTO: DomainNamespace = DomainNamespace::new(0x00);
62
63    /// Domains maintained by `miden-vm`.
64    pub const MIDEN_VM: DomainNamespace = DomainNamespace::new(0x01);
65
66    /// Domains maintained by the Miden protocol repository.
67    pub const MIDEN_PROTOCOL: DomainNamespace = DomainNamespace::new(0x02);
68
69    /// Reserved for a centrally maintained ecosystem registry, not ad hoc allocation.
70    pub const MIDEN_ECOSYSTEM: DomainNamespace = DomainNamespace::new(0x10);
71}
72
73/// Metadata for one centrally allocated namespace.
74#[derive(Debug, Copy, Clone, Eq, PartialEq)]
75pub struct NamespaceDescriptor {
76    /// Human-readable namespace name.
77    pub name: &'static str,
78    /// Numeric namespace prefix.
79    pub namespace: DomainNamespace,
80    /// Repository responsible for the namespace's local registry.
81    pub maintainer: &'static str,
82}
83
84/// The central Eidos namespace registry.
85///
86/// Entries are sorted by numeric prefix. Gaps remain unallocated until they are assigned here.
87pub const NAMESPACE_REGISTRY: &[NamespaceDescriptor] = &[
88    NamespaceDescriptor {
89        name: "Miden cryptographic primitives",
90        namespace: namespace::MIDEN_CRYPTO,
91        maintainer: "https://github.com/0xMiden/miden-vm/tree/next/crates/crypto",
92    },
93    NamespaceDescriptor {
94        name: "Miden VM",
95        namespace: namespace::MIDEN_VM,
96        maintainer: "https://github.com/0xMiden/miden-vm",
97    },
98    NamespaceDescriptor {
99        name: "Miden protocol",
100        namespace: namespace::MIDEN_PROTOCOL,
101        maintainer: "https://github.com/0xMiden/protocol",
102    },
103    NamespaceDescriptor {
104        name: "Miden ecosystem",
105        namespace: namespace::MIDEN_ECOSYSTEM,
106        maintainer: "https://github.com/0xMiden",
107    },
108];
109
110const _: () = assert_namespaces_are_sorted_and_unique(NAMESPACE_REGISTRY);
111
112const fn assert_namespaces_are_sorted_and_unique(entries: &[NamespaceDescriptor]) {
113    let mut index = 1;
114    while index < entries.len() {
115        assert!(
116            entries[index - 1].namespace.as_u8() < entries[index].namespace.as_u8(),
117            "Eidos namespace allocations must be sorted and unique"
118        );
119        index += 1;
120    }
121}
122
123/// The versioning policy encoded in an Eidos domain tag.
124///
125/// Numbered versions identify a specific construction. [`DELEGATED_VERSIONING`] means that the
126/// registered payload schema carries the object version instead.
127#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
128pub struct DomainVersion(u8);
129
130impl DomainVersion {
131    /// Constructs a numbered domain version.
132    ///
133    /// # Panics
134    ///
135    /// Panics if `version` is zero. Use [`DELEGATED_VERSIONING`] for payload-defined versioning.
136    pub const fn numbered(version: u8) -> Self {
137        assert!(version != 0, "numbered Eidos domain versions start at 1");
138        Self(version)
139    }
140
141    /// Returns the encoded version byte.
142    pub const fn as_u8(self) -> u8 {
143        self.0
144    }
145
146    /// Returns true when the authenticated payload carries the object version.
147    pub const fn is_delegated(self) -> bool {
148        self.0 == 0
149    }
150
151    const fn from_tag_byte(version: u8) -> Self {
152        Self(version)
153    }
154}
155
156/// Domain-version marker for constructions whose authenticated payload carries the object
157/// version.
158pub const DELEGATED_VERSIONING: DomainVersion = DomainVersion(0);
159
160/// A structurally valid 32-bit Eidos domain tag.
161///
162/// The tag packs `(namespace, local_id, version)` as `8/16/8` bits. The namespace and local ID
163/// cannot both be zero. A tag becomes registered only when its namespace owner includes it in an
164/// [`EidosDomainRegistry`].
165#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
166pub struct DomainTag(u32);
167
168impl DomainTag {
169    /// Constructs a structurally valid tag in `namespace`.
170    ///
171    /// # Panics
172    ///
173    /// Panics if both the namespace and local ID are zero.
174    pub const fn new(namespace: DomainNamespace, local_id: u16, version: DomainVersion) -> Self {
175        assert!(
176            namespace.as_u8() != 0 || local_id != 0,
177            "the namespace and local ID cannot both be zero"
178        );
179        Self(((namespace.as_u8() as u32) << 24) | ((local_id as u32) << 8) | version.as_u8() as u32)
180    }
181
182    /// Parses the structural representation of a tag.
183    ///
184    /// This rejects the reserved `(namespace, local_id) = (0, 0)` pair and requires a centrally
185    /// allocated namespace. It does not establish that the owner has declared the complete tag in
186    /// its local registry.
187    pub const fn from_u32(value: u32) -> Option<Self> {
188        let version = value as u8;
189        let namespace_byte = (value >> 24) as u8;
190        let local_id = ((value >> 8) & 0xffff) as u16;
191        if namespace_byte == 0 && local_id == 0 {
192            return None;
193        }
194
195        let namespace = match DomainNamespace::from_allocated_u8(namespace_byte) {
196            Some(namespace) => namespace,
197            None => return None,
198        };
199        Some(Self::new(namespace, local_id, DomainVersion::from_tag_byte(version)))
200    }
201
202    /// Returns the complete `namespace || local_id || version` tag.
203    pub const fn as_u32(self) -> u32 {
204        self.0
205    }
206
207    /// Returns the tag's namespace.
208    pub const fn namespace(self) -> DomainNamespace {
209        DomainNamespace::new((self.0 >> 24) as u8)
210    }
211
212    /// Returns the tag's owner-local ID.
213    pub const fn local_id(self) -> u16 {
214        ((self.0 >> 8) & 0xffff) as u16
215    }
216
217    /// Returns the tag's versioning policy.
218    pub const fn version(self) -> DomainVersion {
219        DomainVersion::from_tag_byte(self.0 as u8)
220    }
221
222    /// Returns true when the authenticated payload carries the object version.
223    pub const fn uses_delegated_versioning(self) -> bool {
224        self.version().is_delegated()
225    }
226
227    /// Encodes this tag as a canonical Miden field element.
228    pub const fn as_felt(self) -> Felt {
229        Felt::new_unchecked(self.0 as u64)
230    }
231}
232
233impl fmt::Display for DomainTag {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        write!(f, "0x{:08x}", self.0)
236    }
237}
238
239/// Coarse input encoding recorded for a registered Eidos construction.
240#[derive(Debug, Copy, Clone, Eq, PartialEq)]
241pub enum DomainEncoding {
242    /// Exact-length sequence of Goldilocks field elements.
243    FeltSequence,
244    /// Exact-length byte string encoded into 64-byte Eidos blocks.
245    ByteString,
246    /// Stateful Fiat-Shamir transcript construction.
247    Transcript,
248    /// A construction with a registry-defined fixed or custom schedule.
249    Custom,
250}
251
252/// Type-level description of an Eidos input encoding.
253pub trait EidosEncoding: 'static {
254    /// Runtime metadata written into registry descriptors.
255    const KIND: DomainEncoding;
256}
257
258/// Type marker for the standard exact-length Felt-sequence construction.
259#[derive(Debug)]
260pub enum FeltSequence {}
261
262impl EidosEncoding for FeltSequence {
263    const KIND: DomainEncoding = DomainEncoding::FeltSequence;
264}
265
266/// Type marker for the standard exact-length byte-string construction.
267#[derive(Debug)]
268pub enum ByteString {}
269
270impl EidosEncoding for ByteString {
271    const KIND: DomainEncoding = DomainEncoding::ByteString;
272}
273
274/// Type marker for the Eidos Fiat-Shamir transcript seed construction.
275#[derive(Debug)]
276pub enum Transcript {}
277
278impl EidosEncoding for Transcript {
279    const KIND: DomainEncoding = DomainEncoding::Transcript;
280}
281
282/// Type marker for a domain-specific Eidos schedule.
283#[derive(Debug)]
284pub enum Custom {}
285
286impl EidosEncoding for Custom {
287    const KIND: DomainEncoding = DomainEncoding::Custom;
288}
289
290/// A typed, registered Eidos construction domain.
291///
292/// Implementations must preserve the numeric assignment and encoding declared by the namespace
293/// owner. Use [`crate::eidos_domain_registry!`] rather than implementing this trait manually. The
294/// associated encoding prevents a byte-string domain from being passed to Felt-sequence hashing,
295/// and vice versa.
296pub trait EidosDomain: Copy + 'static {
297    /// Input encoding and schedule family accepted by typed hash APIs.
298    type Encoding: EidosEncoding;
299
300    /// Stable symbolic name used by generated constants and diagnostics.
301    const NAME: &'static str;
302
303    /// Registered numeric tag.
304    const TAG: DomainTag;
305}
306
307/// Structured metadata for one registered Eidos domain.
308#[derive(Debug, Copy, Clone, Eq, PartialEq)]
309pub struct DomainDescriptor {
310    /// Stable symbolic name.
311    pub name: &'static str,
312    /// Registered numeric tag.
313    pub tag: DomainTag,
314    /// Standard encoding family, or [`DomainEncoding::Custom`].
315    pub encoding: DomainEncoding,
316    /// Short human-readable purpose.
317    pub description: &'static str,
318    /// Normative interpretation of the three Eidos parameter lanes and payload schedule.
319    pub schema: &'static str,
320}
321
322/// One owner-maintained registry inside a centrally allocated namespace.
323pub trait EidosDomainRegistry {
324    /// Namespace owned by this registry.
325    const NAMESPACE: DomainNamespace;
326
327    /// Sorted domain declarations maintained by this registry.
328    const DOMAINS: &'static [DomainDescriptor];
329
330    /// Returns the declaration for `tag`, if it belongs to this registry.
331    fn resolve(tag: DomainTag) -> Option<&'static DomainDescriptor> {
332        if tag.namespace() != Self::NAMESPACE {
333            return None;
334        }
335
336        Self::DOMAINS.iter().find(|domain| domain.tag == tag)
337    }
338}
339
340/// Validates one owner-local registry at compile time.
341///
342/// This function is public because the exported registry macro expands in downstream crates.
343#[doc(hidden)]
344pub const fn assert_domain_registry(namespace: DomainNamespace, entries: &[DomainDescriptor]) {
345    let mut index = 0;
346    while index < entries.len() {
347        assert!(
348            entries[index].tag.namespace().as_u8() == namespace.as_u8(),
349            "Eidos domain belongs to the wrong namespace"
350        );
351
352        if index != 0 {
353            let previous = entries[index - 1].tag;
354            let current = entries[index].tag;
355            assert!(
356                previous.as_u32() < current.as_u32(),
357                "Eidos domains must be sorted and unique"
358            );
359            assert!(
360                previous.local_id() != current.local_id()
361                    || (!previous.uses_delegated_versioning()
362                        && !current.uses_delegated_versioning()),
363                "delegated and numbered Eidos versions cannot share a local ID"
364            );
365        }
366        index += 1;
367    }
368}
369
370/// Renders one registry as MASM constants from the same declarations used by Rust.
371///
372/// The generated names are the Rust constant names and the values are complete 32-bit tags.
373pub fn render_masm_constants<R: EidosDomainRegistry>() -> String {
374    let mut output = String::new();
375    writeln!(
376        output,
377        "# Generated Eidos domain tags for namespace 0x{:02x}.",
378        R::NAMESPACE.as_u8()
379    )
380    .expect("writing to a String cannot fail");
381    for domain in R::DOMAINS {
382        writeln!(output, "const {} = 0x{:08x}", domain.name, domain.tag.as_u32())
383            .expect("writing to a String cannot fail");
384    }
385    output
386}
387
388/// Declares every Eidos domain owned by one namespace.
389///
390/// The macro emits typed zero-sized domain values, a structured registry, and compile-time checks
391/// within that declaration. A repository should invoke it once for its allocated namespace.
392#[macro_export]
393macro_rules! eidos_domain_registry {
394    (
395        $(#[$registry_meta:meta])*
396        $registry_vis:vis registry $registry:ident {
397            namespace: $namespace:path;
398            domains: {
399                $(
400                    $(#[$domain_meta:meta])*
401                    $domain_vis:vis $domain_const:ident : $domain_type:ident {
402                        local_id: $local_id:expr,
403                        version: $version:expr,
404                        encoding: $encoding:ty,
405                        description: $description:literal,
406                        schema: $schema:literal $(,)?
407                    }
408                )+
409            }
410        }
411    ) => {
412        $(#[$registry_meta])*
413        #[derive(Debug, Copy, Clone, Eq, PartialEq)]
414        $registry_vis struct $registry;
415
416        $(
417            $(#[$domain_meta])*
418            #[doc = $description]
419            #[derive(Debug, Copy, Clone, Eq, PartialEq)]
420            $domain_vis struct $domain_type;
421
422            $(#[$domain_meta])*
423            #[doc = $description]
424            $domain_vis const $domain_const: $domain_type = $domain_type;
425
426            impl $crate::hash::eidos::domain::EidosDomain for $domain_type {
427                type Encoding = $encoding;
428
429                const NAME: &'static str = stringify!($domain_const);
430                const TAG: $crate::hash::eidos::domain::DomainTag =
431                    $crate::hash::eidos::domain::DomainTag::new(
432                        $namespace,
433                        $local_id,
434                        $version,
435                    );
436            }
437        )+
438
439        impl $registry {
440            /// Returns this registry's structured domain declarations.
441            pub const fn domains() -> &'static [$crate::hash::eidos::domain::DomainDescriptor] {
442                <Self as $crate::hash::eidos::domain::EidosDomainRegistry>::DOMAINS
443            }
444
445            /// Returns the declaration for `tag`, if it belongs to this registry.
446            pub fn resolve(
447                tag: $crate::hash::eidos::domain::DomainTag,
448            ) -> Option<&'static $crate::hash::eidos::domain::DomainDescriptor> {
449                <Self as $crate::hash::eidos::domain::EidosDomainRegistry>::resolve(tag)
450            }
451        }
452
453        impl $crate::hash::eidos::domain::EidosDomainRegistry for $registry {
454            const NAMESPACE: $crate::hash::eidos::domain::DomainNamespace = $namespace;
455            const DOMAINS: &'static [$crate::hash::eidos::domain::DomainDescriptor] = &[
456                $(
457                    $crate::hash::eidos::domain::DomainDescriptor {
458                        name: stringify!($domain_const),
459                        tag: <$domain_type as $crate::hash::eidos::domain::EidosDomain>::TAG,
460                        encoding: <$encoding as $crate::hash::eidos::domain::EidosEncoding>::KIND,
461                        description: $description,
462                        schema: $schema,
463                    },
464                )+
465            ];
466        }
467
468        const _: () = $crate::hash::eidos::domain::assert_domain_registry(
469            $namespace,
470            <$registry as $crate::hash::eidos::domain::EidosDomainRegistry>::DOMAINS,
471        );
472    };
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    const fn test_descriptor(local_id: u16, version: DomainVersion) -> DomainDescriptor {
480        DomainDescriptor {
481            name: "TEST",
482            tag: DomainTag::new(namespace::MIDEN_PROTOCOL, local_id, version),
483            encoding: DomainEncoding::Custom,
484            description: "test",
485            schema: "test",
486        }
487    }
488
489    #[test]
490    fn domain_tag_uses_the_8_16_8_layout() {
491        let tag = DomainTag::new(namespace::MIDEN_PROTOCOL, 0x1234, DomainVersion::numbered(0x56));
492        assert_eq!(tag.as_u32(), 0x0212_3456);
493        assert_eq!(tag.namespace(), namespace::MIDEN_PROTOCOL);
494        assert_eq!(tag.local_id(), 0x1234);
495        assert_eq!(tag.version(), DomainVersion::numbered(0x56));
496        assert!(!tag.uses_delegated_versioning());
497        assert_eq!(tag.as_felt().as_canonical_u64(), 0x0212_3456);
498    }
499
500    #[test]
501    #[should_panic(expected = "numbered Eidos domain versions start at 1")]
502    fn numbered_version_zero_requires_the_delegated_marker() {
503        let _ = DomainVersion::numbered(0);
504    }
505
506    #[test]
507    fn delegated_versioning_uses_version_byte_zero() {
508        let tag = DomainTag::new(namespace::MIDEN_PROTOCOL, 1, DELEGATED_VERSIONING);
509        assert_eq!(tag.as_u32(), 0x0200_0100);
510        assert_eq!(tag.version(), DELEGATED_VERSIONING);
511        assert!(tag.uses_delegated_versioning());
512    }
513
514    #[test]
515    #[should_panic(expected = "delegated and numbered Eidos versions cannot share a local ID")]
516    fn registry_rejects_mixed_versioning_policies_for_one_local_id() {
517        let entries = [
518            test_descriptor(1, DELEGATED_VERSIONING),
519            test_descriptor(1, DomainVersion::numbered(1)),
520        ];
521
522        assert_domain_registry(namespace::MIDEN_PROTOCOL, &entries);
523    }
524
525    #[test]
526    fn registry_allows_multiple_numbered_versions_for_one_local_id() {
527        let entries = [
528            test_descriptor(1, DomainVersion::numbered(1)),
529            test_descriptor(1, DomainVersion::numbered(2)),
530        ];
531
532        assert_domain_registry(namespace::MIDEN_PROTOCOL, &entries);
533    }
534
535    #[test]
536    #[should_panic(expected = "the namespace and local ID cannot both be zero")]
537    fn zero_namespace_and_local_id_are_not_registrable() {
538        let _ = DomainTag::new(namespace::MIDEN_CRYPTO, 0, DomainVersion::numbered(1));
539    }
540
541    #[test]
542    #[should_panic(expected = "the namespace and local ID cannot both be zero")]
543    fn zero_namespace_and_local_id_are_not_registrable_with_delegated_versioning() {
544        let _ = DomainTag::new(namespace::MIDEN_CRYPTO, 0, DELEGATED_VERSIONING);
545    }
546
547    #[test]
548    fn owner_local_zero_is_valid_outside_namespace_zero() {
549        let tag = DomainTag::new(namespace::MIDEN_VM, 0, DomainVersion::numbered(1));
550        assert_eq!(tag.as_u32(), 0x0100_0001);
551    }
552
553    #[test]
554    fn parsing_checks_structure_but_leaves_local_membership_to_the_owner() {
555        let registered =
556            DomainTag::new(namespace::MIDEN_PROTOCOL, 0x1234, DomainVersion::numbered(7));
557        assert_eq!(DomainTag::from_u32(registered.as_u32()), Some(registered));
558
559        // This is structurally valid even though this test does not declare it in the protocol
560        // registry. Dynamic consumers perform that second, context-specific check.
561        assert_eq!(
562            DomainTag::from_u32(0x02ff_ff01),
563            Some(DomainTag::new(namespace::MIDEN_PROTOCOL, 0xffff, DomainVersion::numbered(1),)),
564        );
565        assert_eq!(
566            DomainTag::from_u32(0x0100_0100),
567            Some(DomainTag::new(namespace::MIDEN_VM, 1, DELEGATED_VERSIONING)),
568        );
569
570        assert_eq!(DomainTag::from_u32(0), None);
571        assert_eq!(DomainTag::from_u32(1), None);
572        assert_eq!(DomainTag::from_u32(0x0300_0001), None);
573    }
574
575    #[test]
576    fn masm_constants_come_from_registry_declarations() {
577        let rendered = render_masm_constants::<super::super::domains::MidenCryptoDomainRegistry>();
578        assert!(rendered.contains("const GENERIC_FELT_SEQUENCE = 0x00000a01"));
579        assert!(rendered.contains("const GENERIC_BYTE_STRING = 0x00000301"));
580    }
581}