1#![cfg_attr(not(feature = "std"), no_std)]
51
52pub const MANIFEST_VERSION: &str = "manifest-v1";
58
59#[cfg(feature = "codegen")]
60pub mod codegen;
61
62#[cfg(feature = "std")]
63pub mod idl;
64
65#[cfg(feature = "std")]
66pub mod indexer;
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum CanonicalType {
75 U8,
77 U16,
79 U32,
81 U64,
83 U128,
85 I8,
87 I16,
89 I32,
91 I64,
93 I128,
95 Bool,
97 Pubkey,
99 Header,
101 Bytes(usize),
103}
104
105impl CanonicalType {
106 pub const fn as_str(&self) -> &'static str {
108 match self {
109 Self::U8 => "u8",
110 Self::U16 => "u16",
111 Self::U32 => "u32",
112 Self::U64 => "u64",
113 Self::U128 => "u128",
114 Self::I8 => "i8",
115 Self::I16 => "i16",
116 Self::I32 => "i32",
117 Self::I64 => "i64",
118 Self::I128 => "i128",
119 Self::Bool => "bool",
120 Self::Pubkey => "pubkey",
121 Self::Header => "header",
122 Self::Bytes(_) => "bytes", }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct FieldDescriptor {
130 pub name: &'static str,
132 pub canonical_type: CanonicalType,
134 pub size: usize,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct SegmentFieldDescriptor {
141 pub name: &'static str,
143 pub element_type: &'static str,
145 pub element_size: usize,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct LayoutManifest {
157 pub name: &'static str,
159 pub version: u8,
161 pub discriminator: u8,
163 pub layout_id: [u8; 8],
165 pub fields: &'static [FieldDescriptor],
167 pub segments: &'static [SegmentFieldDescriptor],
169}
170
171impl LayoutManifest {
172 pub const fn total_size(&self) -> usize {
174 let mut total = 0;
175 let mut i = 0;
176 while i < self.fields.len() {
177 total += self.fields[i].size;
178 i += 1;
179 }
180 total
181 }
182
183 pub const fn field_count(&self) -> usize {
185 self.fields.len()
186 }
187
188 pub fn field_offset(&self, name: &str) -> Option<usize> {
192 let mut offset = 0;
193 for field in self.fields {
194 if field.name == name {
195 return Some(offset);
196 }
197 offset += field.size;
198 }
199 None
200 }
201
202 pub fn field(&self, name: &str) -> Option<&FieldDescriptor> {
204 self.fields.iter().find(|f| f.name == name)
205 }
206
207 #[cfg(feature = "std")]
213 pub fn hash_input(&self) -> String {
214 use std::fmt::Write;
215 let mut s = String::new();
216 write!(s, "jiminy:v1:{}:{}:", self.name, self.version).unwrap();
217 for field in self.fields {
218 match field.canonical_type {
219 CanonicalType::Bytes(n) => {
220 write!(s, "{}:bytes{{{}}}:{},", field.name, n, field.size).unwrap();
221 }
222 _ => {
223 write!(s, "{}:{}:{},", field.name, field.canonical_type.as_str(), field.size).unwrap();
224 }
225 }
226 }
227 for seg in self.segments {
228 write!(s, "seg:{}:{}:{},", seg.name, seg.element_type, seg.element_size).unwrap();
229 }
230 s
231 }
232
233 #[cfg(feature = "std")]
238 pub fn export_json(&self) -> String {
239 use std::fmt::Write;
240 let mut s = String::new();
241 s.push_str("{\n");
242 writeln!(s, " \"version\": \"{}\",", MANIFEST_VERSION).unwrap();
243 writeln!(s, " \"name\": \"{}\",", self.name).unwrap();
244 writeln!(s, " \"schema_version\": {},", self.version).unwrap();
245 writeln!(s, " \"discriminator\": {},", self.discriminator).unwrap();
246 s.push_str(" \"layout_id\": \"");
247 for byte in &self.layout_id {
248 write!(s, "{byte:02x}").unwrap();
249 }
250 s.push_str("\",\n");
251 writeln!(s, " \"total_size\": {},", self.total_size()).unwrap();
252 s.push_str(" \"fields\": [\n");
253 let mut offset = 0usize;
254 for (i, field) in self.fields.iter().enumerate() {
255 let type_str = match field.canonical_type {
256 CanonicalType::Bytes(n) => {
257 let mut t = String::from("bytes{");
258 write!(t, "{n}").unwrap();
259 t.push('}');
260 t
261 }
262 other => String::from(other.as_str()),
263 };
264 write!(
265 s,
266 " {{ \"name\": \"{}\", \"type\": \"{}\", \"size\": {}, \"offset\": {} }}",
267 field.name, type_str, field.size, offset,
268 )
269 .unwrap();
270 if i + 1 < self.fields.len() {
271 s.push(',');
272 }
273 s.push('\n');
274 offset += field.size;
275 }
276 if self.segments.is_empty() {
277 s.push_str(" ]\n");
278 } else {
279 s.push_str(" ],\n");
280 s.push_str(" \"segments\": [\n");
281 for (i, seg) in self.segments.iter().enumerate() {
282 write!(
283 s,
284 " {{ \"name\": \"{}\", \"element_type\": \"{}\", \"element_size\": {} }}",
285 seg.name, seg.element_type, seg.element_size,
286 )
287 .unwrap();
288 if i + 1 < self.segments.len() {
289 s.push(',');
290 }
291 s.push('\n');
292 }
293 s.push_str(" ]\n");
294 }
295 s.push('}');
296 s
297 }
298
299 #[cfg(feature = "std")]
311 pub fn verify(&self) -> Result<(), String> {
312 if self.fields.is_empty() {
313 return Err("manifest has no fields".into());
314 }
315
316 let first = &self.fields[0];
318 if first.canonical_type != CanonicalType::Header || first.size != 16 {
319 return Err(format!(
320 "first field must be Header(16), got {:?}({})",
321 first.canonical_type, first.size,
322 ));
323 }
324
325 for field in self.fields {
327 if field.size == 0 {
328 return Err(format!("field '{}' has zero size", field.name));
329 }
330 }
331
332 for (i, a) in self.fields.iter().enumerate() {
334 for b in &self.fields[i + 1..] {
335 if a.name == b.name {
336 return Err(format!("duplicate field name '{}'", a.name));
337 }
338 }
339 }
340
341 Ok(())
342 }
343
344 pub fn verify_account(&self, data: &[u8]) -> Result<(), &'static str> {
355 let expected_size = self.total_size();
356 if data.len() < expected_size {
357 return Err("account data too small for manifest");
358 }
359 if data[0] != self.discriminator {
360 return Err("discriminator mismatch");
361 }
362 if data.len() < 12 {
363 return Err("account data too small for header");
364 }
365 if data[4..12] != self.layout_id {
366 return Err("layout_id mismatch");
367 }
368 Ok(())
369 }
370
371 pub fn verify_hash(&self, full_sha256: &[u8; 32]) -> Result<(), &'static str> {
383 if full_sha256[..8] != self.layout_id {
384 return Err("layout_id does not match truncated SHA-256");
385 }
386 Ok(())
387 }
388}
389
390#[macro_export]
402macro_rules! layout_manifest {
403 (
404 $name:ident,
405 $( $field:ident : $ctype:ident = $size:expr ),+ $(,)?
406 ) => {
407 $crate::LayoutManifest {
408 name: stringify!($name),
409 version: $name::VERSION,
410 discriminator: $name::DISC,
411 layout_id: $name::LAYOUT_ID,
412 fields: &[
413 $( $crate::FieldDescriptor {
414 name: stringify!($field),
415 canonical_type: $crate::CanonicalType::$ctype,
416 size: $size,
417 }, )+
418 ],
419 segments: &[],
420 }
421 };
422 (
424 $name:ident,
425 $( $field:ident : $ctype:ident = $size:expr ),+ $(,)?
426 ;
427 segments { $( $seg_name:ident : $seg_elem_type:ident = $seg_elem_size:expr ),+ $(,)? }
428 ) => {
429 $crate::LayoutManifest {
430 name: stringify!($name),
431 version: $name::VERSION,
432 discriminator: $name::DISC,
433 layout_id: $name::SEGMENTED_LAYOUT_ID,
434 fields: &[
435 $( $crate::FieldDescriptor {
436 name: stringify!($field),
437 canonical_type: $crate::CanonicalType::$ctype,
438 size: $size,
439 }, )+
440 ],
441 segments: &[
442 $( $crate::SegmentFieldDescriptor {
443 name: stringify!($seg_name),
444 element_type: stringify!($seg_elem_type),
445 element_size: $seg_elem_size,
446 }, )+
447 ],
448 }
449 };
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 #[test]
457 fn total_size_sums_fields() {
458 let manifest = LayoutManifest {
459 name: "Test",
460 version: 1,
461 discriminator: 1,
462 layout_id: [0; 8],
463 fields: &[
464 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
465 FieldDescriptor { name: "value", canonical_type: CanonicalType::U64, size: 8 },
466 ],
467 segments: &[],
468 };
469 assert_eq!(manifest.total_size(), 24);
470 }
471
472 #[test]
473 fn field_offset_finds_correct_position() {
474 let manifest = LayoutManifest {
475 name: "Test",
476 version: 1,
477 discriminator: 1,
478 layout_id: [0; 8],
479 fields: &[
480 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
481 FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
482 FieldDescriptor { name: "authority", canonical_type: CanonicalType::Pubkey, size: 32 },
483 ],
484 segments: &[],
485 };
486 assert_eq!(manifest.field_offset("header"), Some(0));
487 assert_eq!(manifest.field_offset("balance"), Some(16));
488 assert_eq!(manifest.field_offset("authority"), Some(24));
489 assert_eq!(manifest.field_offset("nonexistent"), None);
490 }
491
492 #[test]
493 fn hash_input_format() {
494 let manifest = LayoutManifest {
495 name: "Vault",
496 version: 1,
497 discriminator: 1,
498 layout_id: [0; 8],
499 fields: &[
500 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
501 FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
502 FieldDescriptor { name: "authority", canonical_type: CanonicalType::Pubkey, size: 32 },
503 ],
504 segments: &[],
505 };
506 let input = manifest.hash_input();
507 assert_eq!(input, "jiminy:v1:Vault:1:header:header:16,balance:u64:8,authority:pubkey:32,");
508 }
509
510 #[test]
511 fn canonical_type_string_roundtrip() {
512 assert_eq!(CanonicalType::U8.as_str(), "u8");
513 assert_eq!(CanonicalType::U64.as_str(), "u64");
514 assert_eq!(CanonicalType::Pubkey.as_str(), "pubkey");
515 assert_eq!(CanonicalType::Header.as_str(), "header");
516 assert_eq!(CanonicalType::Bool.as_str(), "bool");
517 assert_eq!(CanonicalType::I128.as_str(), "i128");
518 }
519
520 #[test]
521 fn export_json_structure() {
522 let manifest = LayoutManifest {
523 name: "Vault",
524 version: 1,
525 discriminator: 1,
526 layout_id: [0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89],
527 fields: &[
528 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
529 FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
530 ],
531 segments: &[],
532 };
533 let json = manifest.export_json();
534 assert!(json.contains("\"name\": \"Vault\""));
535 assert!(json.contains("\"total_size\": 24"));
536 assert!(json.contains("\"layout_id\": \"abcdef0123456789\""));
537 assert!(json.contains("\"offset\": 0"));
538 assert!(json.contains("\"offset\": 16"));
539 }
540
541 #[test]
542 fn verify_valid_manifest() {
543 let manifest = LayoutManifest {
544 name: "Vault",
545 version: 1,
546 discriminator: 1,
547 layout_id: [0; 8],
548 fields: &[
549 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
550 FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
551 ],
552 segments: &[],
553 };
554 assert!(manifest.verify().is_ok());
555 }
556
557 #[test]
558 fn verify_rejects_no_header() {
559 let manifest = LayoutManifest {
560 name: "Bad",
561 version: 1,
562 discriminator: 1,
563 layout_id: [0; 8],
564 fields: &[
565 FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
566 ],
567 segments: &[],
568 };
569 assert!(manifest.verify().is_err());
570 }
571
572 #[test]
573 fn verify_rejects_duplicate_names() {
574 let manifest = LayoutManifest {
575 name: "Bad",
576 version: 1,
577 discriminator: 1,
578 layout_id: [0; 8],
579 fields: &[
580 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
581 FieldDescriptor { name: "x", canonical_type: CanonicalType::U64, size: 8 },
582 FieldDescriptor { name: "x", canonical_type: CanonicalType::U32, size: 4 },
583 ],
584 segments: &[],
585 };
586 let err = manifest.verify().unwrap_err();
587 assert!(err.contains("duplicate"));
588 }
589
590 #[test]
591 fn verify_rejects_empty() {
592 let manifest = LayoutManifest {
593 name: "Empty",
594 version: 1,
595 discriminator: 1,
596 layout_id: [0; 8],
597 fields: &[],
598 segments: &[],
599 };
600 assert!(manifest.verify().is_err());
601 }
602
603 #[test]
604 fn verify_rejects_zero_size_field() {
605 let manifest = LayoutManifest {
606 name: "Bad",
607 version: 1,
608 discriminator: 1,
609 layout_id: [0; 8],
610 fields: &[
611 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
612 FieldDescriptor { name: "empty", canonical_type: CanonicalType::U8, size: 0 },
613 ],
614 segments: &[],
615 };
616 let err = manifest.verify().unwrap_err();
617 assert!(err.contains("zero size"));
618 }
619
620 #[test]
621 fn field_count_returns_number_of_fields() {
622 let manifest = LayoutManifest {
623 name: "Test",
624 version: 1,
625 discriminator: 1,
626 layout_id: [0; 8],
627 fields: &[
628 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
629 FieldDescriptor { name: "a", canonical_type: CanonicalType::U64, size: 8 },
630 FieldDescriptor { name: "b", canonical_type: CanonicalType::U32, size: 4 },
631 ],
632 segments: &[],
633 };
634 assert_eq!(manifest.field_count(), 3);
635 }
636
637 #[test]
638 fn field_lookup_returns_descriptor() {
639 let manifest = LayoutManifest {
640 name: "Test",
641 version: 1,
642 discriminator: 1,
643 layout_id: [0; 8],
644 fields: &[
645 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
646 FieldDescriptor { name: "amount", canonical_type: CanonicalType::U64, size: 8 },
647 ],
648 segments: &[],
649 };
650 let f = manifest.field("amount").unwrap();
651 assert_eq!(f.canonical_type, CanonicalType::U64);
652 assert_eq!(f.size, 8);
653 assert!(manifest.field("nonexistent").is_none());
654 }
655
656 #[test]
657 fn hash_input_with_segments() {
658 let manifest = LayoutManifest {
659 name: "Pool",
660 version: 1,
661 discriminator: 3,
662 layout_id: [0; 8],
663 fields: &[
664 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
665 FieldDescriptor { name: "total", canonical_type: CanonicalType::U64, size: 8 },
666 ],
667 segments: &[
668 SegmentFieldDescriptor { name: "stakes", element_type: "StakeEntry", element_size: 48 },
669 ],
670 };
671 let input = manifest.hash_input();
672 assert!(input.contains("seg:stakes:StakeEntry:48,"));
673 assert!(input.starts_with("jiminy:v1:Pool:1:"));
674 }
675
676 #[test]
677 fn hash_input_with_bytes_field() {
678 let manifest = LayoutManifest {
679 name: "Buffer",
680 version: 1,
681 discriminator: 2,
682 layout_id: [0; 8],
683 fields: &[
684 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
685 FieldDescriptor { name: "data", canonical_type: CanonicalType::Bytes(64), size: 64 },
686 ],
687 segments: &[],
688 };
689 let input = manifest.hash_input();
690 assert!(input.contains("data:bytes{64}:64,"));
691 }
692
693 #[test]
694 fn export_json_with_segments() {
695 let manifest = LayoutManifest {
696 name: "OrderBook",
697 version: 1,
698 discriminator: 5,
699 layout_id: [0x11; 8],
700 fields: &[
701 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
702 FieldDescriptor { name: "base", canonical_type: CanonicalType::Pubkey, size: 32 },
703 ],
704 segments: &[
705 SegmentFieldDescriptor { name: "bids", element_type: "Order", element_size: 48 },
706 SegmentFieldDescriptor { name: "asks", element_type: "Order", element_size: 48 },
707 ],
708 };
709 let json = manifest.export_json();
710 assert!(json.contains("\"segments\":"));
711 assert!(json.contains("\"name\": \"bids\""));
712 assert!(json.contains("\"element_type\": \"Order\""));
713 assert!(json.contains("\"element_size\": 48"));
714 }
715
716 #[test]
717 fn canonical_type_bytes_as_str() {
718 assert_eq!(CanonicalType::Bytes(32).as_str(), "bytes");
720 assert_eq!(CanonicalType::Bytes(128).as_str(), "bytes");
721 }
722
723 #[test]
724 fn verify_account_accepts_valid_data() {
725 let layout_id = [0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89];
726 let manifest = LayoutManifest {
727 name: "Vault",
728 version: 1,
729 discriminator: 1,
730 layout_id,
731 fields: &[
732 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
733 FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
734 ],
735 segments: &[],
736 };
737 let mut data = vec![0u8; 24];
738 data[0] = 1; data[4..12].copy_from_slice(&layout_id);
740 assert!(manifest.verify_account(&data).is_ok());
741 }
742
743 #[test]
744 fn verify_account_rejects_wrong_disc() {
745 let layout_id = [0xAB; 8];
746 let manifest = LayoutManifest {
747 name: "Vault",
748 version: 1,
749 discriminator: 1,
750 layout_id,
751 fields: &[
752 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
753 FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
754 ],
755 segments: &[],
756 };
757 let mut data = vec![0u8; 24];
758 data[0] = 99; data[4..12].copy_from_slice(&layout_id);
760 assert_eq!(manifest.verify_account(&data).unwrap_err(), "discriminator mismatch");
761 }
762
763 #[test]
764 fn verify_account_rejects_wrong_layout_id() {
765 let manifest = LayoutManifest {
766 name: "Vault",
767 version: 1,
768 discriminator: 1,
769 layout_id: [0xAB; 8],
770 fields: &[
771 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
772 FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
773 ],
774 segments: &[],
775 };
776 let mut data = vec![0u8; 24];
777 data[0] = 1;
778 data[4..12].copy_from_slice(&[0xFF; 8]); assert_eq!(manifest.verify_account(&data).unwrap_err(), "layout_id mismatch");
780 }
781
782 #[test]
783 fn verify_account_rejects_too_small() {
784 let manifest = LayoutManifest {
785 name: "Vault",
786 version: 1,
787 discriminator: 1,
788 layout_id: [0; 8],
789 fields: &[
790 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
791 FieldDescriptor { name: "balance", canonical_type: CanonicalType::U64, size: 8 },
792 ],
793 segments: &[],
794 };
795 let data = vec![0u8; 10]; assert_eq!(manifest.verify_account(&data).unwrap_err(), "account data too small for manifest");
797 }
798
799 #[test]
800 fn manifest_version_is_v1() {
801 assert_eq!(MANIFEST_VERSION, "manifest-v1");
802 }
803
804 #[test]
805 fn verify_hash_accepts_matching_prefix() {
806 let layout_id = [0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89];
807 let manifest = LayoutManifest {
808 name: "Test",
809 version: 1,
810 discriminator: 1,
811 layout_id,
812 fields: &[
813 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
814 ],
815 segments: &[],
816 };
817 let mut hash = [0u8; 32];
818 hash[..8].copy_from_slice(&layout_id);
819 hash[8] = 0xFF;
821 assert!(manifest.verify_hash(&hash).is_ok());
822 }
823
824 #[test]
825 fn verify_hash_rejects_mismatch() {
826 let manifest = LayoutManifest {
827 name: "Test",
828 version: 1,
829 discriminator: 1,
830 layout_id: [0xAA; 8],
831 fields: &[
832 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
833 ],
834 segments: &[],
835 };
836 let wrong_hash = [0xBB; 32];
837 assert!(manifest.verify_hash(&wrong_hash).is_err());
838 }
839
840 #[test]
841 fn export_json_contains_manifest_version() {
842 let manifest = LayoutManifest {
843 name: "V",
844 version: 1,
845 discriminator: 1,
846 layout_id: [0; 8],
847 fields: &[
848 FieldDescriptor { name: "header", canonical_type: CanonicalType::Header, size: 16 },
849 ],
850 segments: &[],
851 };
852 let json = manifest.export_json();
853 assert!(json.contains(&format!("\"version\": \"{}\"", MANIFEST_VERSION)));
854 }
855}