1use alloc::string::String;
22use core::fmt::{self, Write};
23
24use crate::Felt;
25
26#[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 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
56pub mod namespace {
58 use super::DomainNamespace;
59
60 pub const MIDEN_CRYPTO: DomainNamespace = DomainNamespace::new(0x00);
62
63 pub const MIDEN_VM: DomainNamespace = DomainNamespace::new(0x01);
65
66 pub const MIDEN_PROTOCOL: DomainNamespace = DomainNamespace::new(0x02);
68
69 pub const MIDEN_ECOSYSTEM: DomainNamespace = DomainNamespace::new(0x10);
71}
72
73#[derive(Debug, Copy, Clone, Eq, PartialEq)]
75pub struct NamespaceDescriptor {
76 pub name: &'static str,
78 pub namespace: DomainNamespace,
80 pub maintainer: &'static str,
82}
83
84pub 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#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
128pub struct DomainVersion(u8);
129
130impl DomainVersion {
131 pub const fn numbered(version: u8) -> Self {
137 assert!(version != 0, "numbered Eidos domain versions start at 1");
138 Self(version)
139 }
140
141 pub const fn as_u8(self) -> u8 {
143 self.0
144 }
145
146 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
156pub const DELEGATED_VERSIONING: DomainVersion = DomainVersion(0);
159
160#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
166pub struct DomainTag(u32);
167
168impl DomainTag {
169 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 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 pub const fn as_u32(self) -> u32 {
204 self.0
205 }
206
207 pub const fn namespace(self) -> DomainNamespace {
209 DomainNamespace::new((self.0 >> 24) as u8)
210 }
211
212 pub const fn local_id(self) -> u16 {
214 ((self.0 >> 8) & 0xffff) as u16
215 }
216
217 pub const fn version(self) -> DomainVersion {
219 DomainVersion::from_tag_byte(self.0 as u8)
220 }
221
222 pub const fn uses_delegated_versioning(self) -> bool {
224 self.version().is_delegated()
225 }
226
227 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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
241pub enum DomainEncoding {
242 FeltSequence,
244 ByteString,
246 Transcript,
248 Custom,
250}
251
252pub trait EidosEncoding: 'static {
254 const KIND: DomainEncoding;
256}
257
258#[derive(Debug)]
260pub enum FeltSequence {}
261
262impl EidosEncoding for FeltSequence {
263 const KIND: DomainEncoding = DomainEncoding::FeltSequence;
264}
265
266#[derive(Debug)]
268pub enum ByteString {}
269
270impl EidosEncoding for ByteString {
271 const KIND: DomainEncoding = DomainEncoding::ByteString;
272}
273
274#[derive(Debug)]
276pub enum Transcript {}
277
278impl EidosEncoding for Transcript {
279 const KIND: DomainEncoding = DomainEncoding::Transcript;
280}
281
282#[derive(Debug)]
284pub enum Custom {}
285
286impl EidosEncoding for Custom {
287 const KIND: DomainEncoding = DomainEncoding::Custom;
288}
289
290pub trait EidosDomain: Copy + 'static {
297 type Encoding: EidosEncoding;
299
300 const NAME: &'static str;
302
303 const TAG: DomainTag;
305}
306
307#[derive(Debug, Copy, Clone, Eq, PartialEq)]
309pub struct DomainDescriptor {
310 pub name: &'static str,
312 pub tag: DomainTag,
314 pub encoding: DomainEncoding,
316 pub description: &'static str,
318 pub schema: &'static str,
320}
321
322pub trait EidosDomainRegistry {
324 const NAMESPACE: DomainNamespace;
326
327 const DOMAINS: &'static [DomainDescriptor];
329
330 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#[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
370pub 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#[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 pub const fn domains() -> &'static [$crate::hash::eidos::domain::DomainDescriptor] {
442 <Self as $crate::hash::eidos::domain::EidosDomainRegistry>::DOMAINS
443 }
444
445 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 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}