jiminy_anchor/lib.rs
1//! # jiminy-anchor
2//!
3//! Interoperability bridge between Anchor-framework accounts and Jiminy
4//! programs. No dependency on `anchor-lang` itself - this crate operates
5//! purely on raw byte layouts and deterministic hash conventions.
6//!
7//! ## Anchor Account Format
8//!
9//! Anchor accounts use an 8-byte discriminator at bytes `[0..8]`,
10//! computed as `sha256("account:<TypeName>")[..8]`. The remaining bytes
11//! are Borsh-serialized data. Anchor's `zero_copy` attribute uses
12//! `#[repr(C)]` overlays but still prefixes the same 8-byte discriminator.
13//!
14//! Anchor instructions also carry an 8-byte discriminator, computed as
15//! `sha256("global:<function_name>")[..8]`, placed at the start of
16//! instruction data.
17//!
18//! ## What This Crate Provides
19//!
20//! ### Account discriminators
21//!
22//! - [`anchor_disc`] - compute the 8-byte Anchor account discriminator at compile time
23//! - [`check_anchor_disc`] - validate an Anchor discriminator on raw account data
24//! - [`AnchorHeader`] - zero-copy overlay for the 8-byte Anchor discriminator
25//!
26//! ### Instruction discriminators
27//!
28//! - [`anchor_ix_disc`] - compute the 8-byte Anchor instruction discriminator at compile time
29//! - [`check_anchor_ix_disc`] - validate an instruction discriminator on instruction data
30//!
31//! ### Body access
32//!
33//! - [`anchor_body`] / [`anchor_body_mut`] - get the body slice (bytes `[8..]`) from Anchor account data
34//! - [`check_and_body`] - discriminator check + body slice in one call
35//! - [`check_and_overlay`] / [`check_and_overlay_mut`] - discriminator check + Pod overlay on the body
36//!
37//! ### Cross-framework verification
38//!
39//! - [`check_anchor_with_layout_id`] - verify both Anchor disc and Jiminy `layout_id`
40//! - [`check_anchor_with_version`] - verify disc + Jiminy layout_id + version for versioned interop
41//!
42//! ### AccountView helpers
43//!
44//! - [`load_anchor_account`] - validate owner + Anchor disc + borrow from an `AccountView`
45//! - [`load_anchor_overlay`] - validate owner + Anchor disc, borrow, then Pod overlay the body
46//!
47//! ## Integration Pattern: Anchor + Jiminy
48//!
49//! Use Anchor for orchestration (instruction routing, account
50//! deserialization, constraint macros) and Jiminy for the performance-critical
51//! hot path (zero-copy reads, math, CPI guards). Jiminy's `zero_copy_layout!`
52//! accounts can coexist with Anchor's `#[account(zero_copy)]` by sharing
53//! a common `#[repr(C)]` body layout.
54//!
55//! A typical pattern:
56//!
57//! 1. Anchor program creates accounts with its discriminator.
58//! 2. Jiminy helper program reads those accounts via [`check_and_overlay`].
59//! 3. If the Anchor body is itself a Jiminy layout (with `AccountHeader`),
60//! use [`check_anchor_with_layout_id`] for full cross-framework verification.
61//!
62//! ## Example: Reading an Anchor account from a Jiminy program
63//!
64//! ```rust,ignore
65//! use jiminy_anchor::{anchor_disc, check_anchor_disc, anchor_body};
66//! use jiminy_core::account::{pod_from_bytes, Pod, FixedLayout};
67//!
68//! // Compute Anchor's discriminator for "Vault" at compile time.
69//! const VAULT_DISC: [u8; 8] = anchor_disc("Vault");
70//!
71//! #[repr(C)]
72//! #[derive(Clone, Copy)]
73//! struct AnchorVaultBody {
74//! balance: [u8; 8], // u64 LE
75//! authority: [u8; 32],
76//! }
77//! unsafe impl Pod for AnchorVaultBody {}
78//! impl FixedLayout for AnchorVaultBody { const SIZE: usize = 40; }
79//!
80//! fn read_anchor_vault(data: &[u8]) -> Result<&AnchorVaultBody, jiminy_core::ProgramError> {
81//! check_anchor_disc(data, &VAULT_DISC)?;
82//! pod_from_bytes::<AnchorVaultBody>(&data[8..])
83//! }
84//! ```
85//!
86//! ## Example: Routing Anchor instructions from a Jiminy program
87//!
88//! ```rust,ignore
89//! use jiminy_anchor::anchor_ix_disc;
90//!
91//! const IX_DEPOSIT: [u8; 8] = anchor_ix_disc("deposit");
92//! const IX_WITHDRAW: [u8; 8] = anchor_ix_disc("withdraw");
93//!
94//! fn process_instruction(data: &[u8]) -> ProgramResult {
95//! let (disc, body) = data.split_at(8);
96//! match disc.try_into().unwrap_or(&[0u8; 8]) {
97//! &IX_DEPOSIT => process_deposit(body),
98//! &IX_WITHDRAW => process_withdraw(body),
99//! _ => Err(ProgramError::InvalidInstructionData),
100//! }
101//! }
102//! ```
103
104#![no_std]
105
106use pinocchio::error::ProgramError;
107
108/// Compute the Anchor 8-byte discriminator for an account type name
109/// at compile time.
110///
111/// Anchor uses `sha256("account:<TypeName>")[..8]`.
112///
113/// ```rust
114/// use jiminy_anchor::anchor_disc;
115///
116/// const VAULT_DISC: [u8; 8] = anchor_disc("Vault");
117/// // This is deterministic - same input always produces same output.
118/// assert_eq!(VAULT_DISC, anchor_disc("Vault"));
119/// ```
120pub const fn anchor_disc(type_name: &str) -> [u8; 8] {
121 // Build "account:<TypeName>" input.
122 let prefix = b"account:";
123 let name = type_name.as_bytes();
124 let total_len = prefix.len() + name.len();
125
126 // Concatenate into a fixed buffer (max 256 bytes should be plenty).
127 let mut buf = [0u8; 256];
128 let mut i = 0;
129 while i < prefix.len() {
130 buf[i] = prefix[i];
131 i += 1;
132 }
133 let mut j = 0;
134 while j < name.len() {
135 buf[i + j] = name[j];
136 j += 1;
137 }
138
139 // SHA-256 the input.
140 let hash = sha2_const_stable::Sha256::new()
141 .update(const_slice(&buf, total_len))
142 .finalize();
143
144 // Take first 8 bytes.
145 [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]]
146}
147
148/// Helper: slice a fixed-size array to `len` bytes in const context.
149const fn const_slice(buf: &[u8; 256], len: usize) -> &[u8] {
150 // SAFETY: len <= 256 guaranteed by caller.
151 // In const context we can use split_at.
152 let (head, _) = buf.split_at(len);
153 head
154}
155
156/// Validate that the first 8 bytes of account data match the expected
157/// Anchor discriminator.
158///
159/// # Errors
160///
161/// - `AccountDataTooSmall` - data shorter than 8 bytes.
162/// - `InvalidAccountData` - discriminator does not match `expected`.
163#[inline(always)]
164pub fn check_anchor_disc(data: &[u8], expected: &[u8; 8]) -> Result<(), ProgramError> {
165 if data.len() < 8 {
166 return Err(ProgramError::AccountDataTooSmall);
167 }
168 if data[0] != expected[0]
169 || data[1] != expected[1]
170 || data[2] != expected[2]
171 || data[3] != expected[3]
172 || data[4] != expected[4]
173 || data[5] != expected[5]
174 || data[6] != expected[6]
175 || data[7] != expected[7]
176 {
177 return Err(ProgramError::InvalidAccountData);
178 }
179 Ok(())
180}
181
182/// Zero-copy overlay for the 8-byte Anchor discriminator.
183#[repr(C)]
184#[derive(Clone, Copy)]
185pub struct AnchorHeader {
186 /// The 8-byte discriminator (`sha256("account:<Type>")[..8]`).
187 pub discriminator: [u8; 8],
188}
189
190unsafe impl jiminy_core::account::Pod for AnchorHeader {}
191impl jiminy_core::account::FixedLayout for AnchorHeader {
192 const SIZE: usize = 8;
193}
194
195/// Get the body of an Anchor account (everything after the 8-byte discriminator).
196///
197/// # Errors
198///
199/// - `AccountDataTooSmall` - data shorter than 8 bytes.
200#[inline(always)]
201pub fn anchor_body(data: &[u8]) -> Result<&[u8], ProgramError> {
202 if data.len() < 8 {
203 return Err(ProgramError::AccountDataTooSmall);
204 }
205 Ok(&data[8..])
206}
207
208/// Get the mutable body of an Anchor account.
209///
210/// # Errors
211///
212/// - `AccountDataTooSmall` - data shorter than 8 bytes.
213#[inline(always)]
214pub fn anchor_body_mut(data: &mut [u8]) -> Result<&mut [u8], ProgramError> {
215 if data.len() < 8 {
216 return Err(ProgramError::AccountDataTooSmall);
217 }
218 Ok(&mut data[8..])
219}
220
221/// Convenience: check discriminator and return the body slice in one call.
222///
223/// # Errors
224///
225/// - `AccountDataTooSmall` - data shorter than 8 bytes.
226/// - `InvalidAccountData` - discriminator does not match `expected`.
227#[inline(always)]
228pub fn check_and_body<'a>(data: &'a [u8], expected: &[u8; 8]) -> Result<&'a [u8], ProgramError> {
229 check_anchor_disc(data, expected)?;
230 Ok(&data[8..])
231}
232
233/// Check the Anchor discriminator and overlay a `Pod` type on the body.
234///
235/// Validates the 8-byte Anchor discriminator, then reinterprets the
236/// remaining bytes as an immutable reference to `T`. This is the
237/// primary way to read an Anchor `zero_copy` account body as a Jiminy
238/// overlay.
239///
240/// # Errors
241///
242/// - `AccountDataTooSmall` - data shorter than `8 + T::SIZE`.
243/// - `InvalidAccountData` - discriminator mismatch.
244#[inline(always)]
245pub fn check_and_overlay<'a, T: jiminy_core::account::Pod + jiminy_core::account::FixedLayout>(
246 data: &'a [u8],
247 expected_disc: &[u8; 8],
248) -> Result<&'a T, ProgramError> {
249 check_anchor_disc(data, expected_disc)?;
250 jiminy_core::account::pod_from_bytes::<T>(&data[8..])
251}
252
253/// Check the Anchor discriminator and overlay a mutable `Pod` type on the body.
254///
255/// # Errors
256///
257/// - `AccountDataTooSmall` - data shorter than `8 + T::SIZE`.
258/// - `InvalidAccountData` - discriminator mismatch.
259#[inline(always)]
260pub fn check_and_overlay_mut<'a, T: jiminy_core::account::Pod + jiminy_core::account::FixedLayout>(
261 data: &'a mut [u8],
262 expected_disc: &[u8; 8],
263) -> Result<&'a mut T, ProgramError> {
264 check_anchor_disc(data, expected_disc)?;
265 jiminy_core::account::pod_from_bytes_mut::<T>(&mut data[8..])
266}
267
268/// Validate that an Anchor `zero_copy` account body carries a Jiminy
269/// `layout_id` at the expected position.
270///
271/// Anchor `zero_copy` accounts prefix the body with an 8-byte
272/// discriminator. If the body is itself a Jiminy layout (starting with
273/// an `AccountHeader`), the `layout_id` lives at body offset `4..12`
274/// (i.e. account offset `12..20`).
275///
276/// This enables cross-framework verification: a Jiminy program can
277/// confirm that an Anchor account's body matches a known Jiminy schema.
278///
279/// # Errors
280///
281/// - `AccountDataTooSmall` - data shorter than `8 + 12` bytes.
282/// - `InvalidAccountData` - discriminator or layout_id mismatch.
283#[inline(always)]
284pub fn check_anchor_with_layout_id(
285 data: &[u8],
286 expected_disc: &[u8; 8],
287 expected_layout_id: &[u8; 8],
288) -> Result<(), ProgramError> {
289 check_anchor_disc(data, expected_disc)?;
290 let body = &data[8..];
291 if body.len() < 12 {
292 return Err(ProgramError::AccountDataTooSmall);
293 }
294 // layout_id sits at body offset 4..12 (after disc(1) + version(1) + flags(2))
295 if body[4..12] != expected_layout_id[..] {
296 return Err(ProgramError::InvalidAccountData);
297 }
298 Ok(())
299}
300
301// ── Instruction discriminator ─────────────────────────────────────────────────
302
303/// Compute the Anchor 8-byte instruction discriminator at compile time.
304///
305/// Anchor uses `sha256("global:<function_name>")[..8]` for instruction
306/// routing. This matches Anchor's `#[instruction]` attribute behavior.
307///
308/// ```rust
309/// use jiminy_anchor::anchor_ix_disc;
310///
311/// const DEPOSIT: [u8; 8] = anchor_ix_disc("deposit");
312/// const WITHDRAW: [u8; 8] = anchor_ix_disc("withdraw");
313/// assert_ne!(DEPOSIT, WITHDRAW);
314/// ```
315pub const fn anchor_ix_disc(fn_name: &str) -> [u8; 8] {
316 let prefix = b"global:";
317 let name = fn_name.as_bytes();
318 let total_len = prefix.len() + name.len();
319
320 let mut buf = [0u8; 256];
321 let mut i = 0;
322 while i < prefix.len() {
323 buf[i] = prefix[i];
324 i += 1;
325 }
326 let mut j = 0;
327 while j < name.len() {
328 buf[i + j] = name[j];
329 j += 1;
330 }
331
332 let hash = sha2_const_stable::Sha256::new()
333 .update(const_slice(&buf, total_len))
334 .finalize();
335
336 [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]]
337}
338
339/// Validate that instruction data starts with the expected Anchor
340/// instruction discriminator.
341///
342/// # Errors
343///
344/// - `InvalidInstructionData` - data shorter than 8 bytes or discriminator mismatch.
345#[inline(always)]
346pub fn check_anchor_ix_disc(data: &[u8], expected: &[u8; 8]) -> Result<(), ProgramError> {
347 if data.len() < 8 {
348 return Err(ProgramError::InvalidInstructionData);
349 }
350 if data[0] != expected[0]
351 || data[1] != expected[1]
352 || data[2] != expected[2]
353 || data[3] != expected[3]
354 || data[4] != expected[4]
355 || data[5] != expected[5]
356 || data[6] != expected[6]
357 || data[7] != expected[7]
358 {
359 return Err(ProgramError::InvalidInstructionData);
360 }
361 Ok(())
362}
363
364/// Check the instruction discriminator and return the remaining
365/// instruction body (bytes after the 8-byte discriminator).
366///
367/// # Errors
368///
369/// - `InvalidInstructionData` - data shorter than 8 bytes or discriminator mismatch.
370#[inline(always)]
371pub fn check_ix_and_body<'a>(
372 data: &'a [u8],
373 expected: &[u8; 8],
374) -> Result<&'a [u8], ProgramError> {
375 check_anchor_ix_disc(data, expected)?;
376 Ok(&data[8..])
377}
378
379// ── Version-aware cross-framework verification ───────────────────────────────
380
381/// Validate Anchor discriminator, Jiminy `layout_id`, and Jiminy
382/// `version` on a cross-framework account.
383///
384/// This is the strongest cross-framework check: it proves the account
385/// has the right Anchor type, the right Jiminy ABI fingerprint, *and*
386/// the right schema version.
387///
388/// The Jiminy header sits at the Anchor body start:
389/// ```text
390/// [0..8] Anchor disc
391/// [8] Jiminy disc (1 byte)
392/// [9] Jiminy version (1 byte)
393/// [10..12] Jiminy flags (2 bytes)
394/// [12..20] Jiminy layout_id (8 bytes)
395/// [20..24] Jiminy reserved (4 bytes)
396/// ```
397///
398/// # Errors
399///
400/// - `AccountDataTooSmall` - data shorter than 24 bytes.
401/// - `InvalidAccountData` - discriminator, layout_id, or version mismatch.
402#[inline(always)]
403pub fn check_anchor_with_version(
404 data: &[u8],
405 expected_disc: &[u8; 8],
406 expected_layout_id: &[u8; 8],
407 expected_version: u8,
408) -> Result<(), ProgramError> {
409 check_anchor_disc(data, expected_disc)?;
410 let body = &data[8..];
411 if body.len() < 16 {
412 return Err(ProgramError::AccountDataTooSmall);
413 }
414 // version at body offset 1
415 if body[1] != expected_version {
416 return Err(ProgramError::InvalidAccountData);
417 }
418 // layout_id at body offset 4..12
419 if body[4..12] != expected_layout_id[..] {
420 return Err(ProgramError::InvalidAccountData);
421 }
422 Ok(())
423}
424
425// ── AccountView helpers ──────────────────────────────────────────────────────
426
427/// Load an Anchor account from an `AccountView`: validate owner +
428/// discriminator, then borrow the data.
429///
430/// This is the Anchor equivalent of Jiminy's Tier-1 `load()`. It
431/// verifies that the account is owned by the expected program and
432/// carries the correct Anchor discriminator before returning a
433/// borrowed reference to the raw data.
434///
435/// # Errors
436///
437/// - `IllegalOwner` - account not owned by `expected_owner`.
438/// - `AccountDataTooSmall` - data shorter than 8 bytes.
439/// - `InvalidAccountData` - discriminator mismatch.
440#[inline(always)]
441pub fn load_anchor_account<'a>(
442 account: &'a pinocchio::AccountView,
443 expected_owner: &pinocchio::Address,
444 expected_disc: &[u8; 8],
445) -> Result<pinocchio::account::Ref<'a, [u8]>, ProgramError> {
446 // SAFETY: owner() returns a pointer to data owned by the runtime;
447 // we only read 32 bytes from it for comparison.
448 if unsafe { account.owner() } != expected_owner {
449 return Err(ProgramError::IllegalOwner);
450 }
451 let data = account.try_borrow()?;
452 if data.len() < 8 {
453 return Err(ProgramError::AccountDataTooSmall);
454 }
455 if data[0] != expected_disc[0]
456 || data[1] != expected_disc[1]
457 || data[2] != expected_disc[2]
458 || data[3] != expected_disc[3]
459 || data[4] != expected_disc[4]
460 || data[5] != expected_disc[5]
461 || data[6] != expected_disc[6]
462 || data[7] != expected_disc[7]
463 {
464 return Err(ProgramError::InvalidAccountData);
465 }
466 Ok(data)
467}
468
469/// Load an Anchor account, validate owner + discriminator, then overlay
470/// the body as a `Pod` type.
471///
472/// Combines owner validation, discriminator checking, borrowing, and
473/// Pod overlay into a single call - the recommended way to read an
474/// Anchor `zero_copy` account from a Jiminy program.
475///
476/// # Errors
477///
478/// - `IllegalOwner` - account not owned by `expected_owner`.
479/// - `AccountDataTooSmall` - data too short for disc + body.
480/// - `InvalidAccountData` - discriminator mismatch or body too small.
481#[inline(always)]
482pub fn load_anchor_overlay<'a, T: jiminy_core::account::Pod + jiminy_core::account::FixedLayout>(
483 account: &'a pinocchio::AccountView,
484 expected_owner: &pinocchio::Address,
485 expected_disc: &[u8; 8],
486) -> Result<pinocchio::account::Ref<'a, [u8]>, ProgramError> {
487 // SAFETY: owner() returns a pointer to data owned by the runtime;
488 // we only read 32 bytes from it for comparison.
489 if unsafe { account.owner() } != expected_owner {
490 return Err(ProgramError::IllegalOwner);
491 }
492 let data = account.try_borrow()?;
493 if data.len() < 8 + T::SIZE {
494 return Err(ProgramError::AccountDataTooSmall);
495 }
496 if data[0] != expected_disc[0]
497 || data[1] != expected_disc[1]
498 || data[2] != expected_disc[2]
499 || data[3] != expected_disc[3]
500 || data[4] != expected_disc[4]
501 || data[5] != expected_disc[5]
502 || data[6] != expected_disc[6]
503 || data[7] != expected_disc[7]
504 {
505 return Err(ProgramError::InvalidAccountData);
506 }
507 Ok(data)
508}
509
510// ── Anchor event discriminator ───────────────────────────────────────────────
511
512/// Compute the Anchor 8-byte event discriminator at compile time.
513///
514/// Anchor events use `sha256("event:<EventName>")[..8]`.
515///
516/// ```rust
517/// use jiminy_anchor::anchor_event_disc;
518///
519/// const DEPOSIT_EVENT: [u8; 8] = anchor_event_disc("DepositEvent");
520/// assert_eq!(DEPOSIT_EVENT, anchor_event_disc("DepositEvent"));
521/// ```
522pub const fn anchor_event_disc(event_name: &str) -> [u8; 8] {
523 let prefix = b"event:";
524 let name = event_name.as_bytes();
525 let total_len = prefix.len() + name.len();
526
527 let mut buf = [0u8; 256];
528 let mut i = 0;
529 while i < prefix.len() {
530 buf[i] = prefix[i];
531 i += 1;
532 }
533 let mut j = 0;
534 while j < name.len() {
535 buf[i + j] = name[j];
536 j += 1;
537 }
538
539 let hash = sha2_const_stable::Sha256::new()
540 .update(const_slice(&buf, total_len))
541 .finalize();
542
543 [hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]]
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549
550 #[test]
551 fn disc_is_deterministic() {
552 let d1 = anchor_disc("Vault");
553 let d2 = anchor_disc("Vault");
554 assert_eq!(d1, d2);
555 }
556
557 #[test]
558 fn different_names_different_discs() {
559 let v = anchor_disc("Vault");
560 let p = anchor_disc("Pool");
561 assert_ne!(v, p);
562 }
563
564 #[test]
565 fn check_disc_succeeds() {
566 let disc = anchor_disc("Vault");
567 let mut data = [0u8; 48];
568 data[..8].copy_from_slice(&disc);
569 assert!(check_anchor_disc(&data, &disc).is_ok());
570 }
571
572 #[test]
573 fn check_disc_rejects_wrong() {
574 let disc = anchor_disc("Vault");
575 let data = [0u8; 48]; // all zeros
576 assert!(check_anchor_disc(&data, &disc).is_err());
577 }
578
579 #[test]
580 fn check_disc_rejects_short() {
581 let disc = anchor_disc("Vault");
582 let data = [0u8; 4]; // too short
583 assert!(check_anchor_disc(&data, &disc).is_err());
584 }
585
586 #[test]
587 fn anchor_body_returns_tail() {
588 let mut data = [0u8; 16];
589 data[8] = 42;
590 let body = anchor_body(&data).unwrap();
591 assert_eq!(body.len(), 8);
592 assert_eq!(body[0], 42);
593 }
594
595 #[test]
596 fn check_and_body_combined() {
597 let disc = anchor_disc("Pool");
598 let mut data = [0u8; 32];
599 data[..8].copy_from_slice(&disc);
600 data[8] = 0xFF;
601 let body = check_and_body(&data, &disc).unwrap();
602 assert_eq!(body[0], 0xFF);
603 }
604
605 #[test]
606 fn check_and_overlay_reads_body() {
607 // Create an Anchor account with an 8-byte disc + a u64 body.
608 #[repr(C)]
609 #[derive(Clone, Copy)]
610 struct Balance { val: [u8; 8] }
611 unsafe impl jiminy_core::account::Pod for Balance {}
612 impl jiminy_core::account::FixedLayout for Balance {
613 const SIZE: usize = 8;
614 }
615
616 let disc = anchor_disc("Balance");
617 let mut data = [0u8; 16];
618 data[..8].copy_from_slice(&disc);
619 data[8..16].copy_from_slice(&42u64.to_le_bytes());
620
621 let overlay = check_and_overlay::<Balance>(&data, &disc).unwrap();
622 assert_eq!(u64::from_le_bytes(overlay.val), 42);
623 }
624
625 #[test]
626 fn check_and_overlay_rejects_wrong_disc() {
627 #[repr(C)]
628 #[derive(Clone, Copy)]
629 struct Dummy { v: [u8; 4] }
630 unsafe impl jiminy_core::account::Pod for Dummy {}
631 impl jiminy_core::account::FixedLayout for Dummy {
632 const SIZE: usize = 4;
633 }
634
635 let disc = anchor_disc("X");
636 let data = [0u8; 16];
637 assert!(check_and_overlay::<Dummy>(&data, &disc).is_err());
638 }
639
640 #[test]
641 fn check_and_overlay_mut_writes() {
642 #[repr(C)]
643 #[derive(Clone, Copy)]
644 struct Val { v: [u8; 8] }
645 unsafe impl jiminy_core::account::Pod for Val {}
646 impl jiminy_core::account::FixedLayout for Val {
647 const SIZE: usize = 8;
648 }
649
650 let disc = anchor_disc("Val");
651 let mut data = [0u8; 16];
652 data[..8].copy_from_slice(&disc);
653
654 let overlay = check_and_overlay_mut::<Val>(&mut data, &disc).unwrap();
655 overlay.v = 99u64.to_le_bytes();
656 assert_eq!(&data[8..16], &99u64.to_le_bytes());
657 }
658
659 #[test]
660 fn layout_id_check_passes() {
661 let disc = anchor_disc("MyLayout");
662 let layout_id = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
663 // Anchor disc (8) + jiminy header body: disc(1) + ver(1) + flags(2) + layout_id(8) = 20 total
664 let mut data = [0u8; 24];
665 data[..8].copy_from_slice(&disc);
666 // body offset 4..12 = account offset 12..20
667 data[12..20].copy_from_slice(&layout_id);
668
669 assert!(check_anchor_with_layout_id(&data, &disc, &layout_id).is_ok());
670 }
671
672 #[test]
673 fn layout_id_check_rejects_mismatch() {
674 let disc = anchor_disc("MyLayout");
675 let layout_id = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
676 let mut data = [0u8; 24];
677 data[..8].copy_from_slice(&disc);
678 // leave layout_id area as zeros - mismatch
679
680 assert!(check_anchor_with_layout_id(&data, &disc, &layout_id).is_err());
681 }
682
683 #[test]
684 fn layout_id_check_rejects_too_short() {
685 let disc = anchor_disc("MyLayout");
686 let layout_id = [0x11; 8];
687 let mut data = [0u8; 14]; // too short for disc + 12 body bytes
688 data[..8].copy_from_slice(&disc);
689
690 assert!(check_anchor_with_layout_id(&data, &disc, &layout_id).is_err());
691 }
692
693 // ── Instruction discriminator tests ──────────────────────────────
694
695 #[test]
696 fn ix_disc_is_deterministic() {
697 let d1 = anchor_ix_disc("deposit");
698 let d2 = anchor_ix_disc("deposit");
699 assert_eq!(d1, d2);
700 }
701
702 #[test]
703 fn ix_disc_differs_from_account_disc() {
704 // "global:Vault" vs "account:Vault" - different prefixes
705 let ix = anchor_ix_disc("Vault");
706 let acct = anchor_disc("Vault");
707 assert_ne!(ix, acct);
708 }
709
710 #[test]
711 fn different_ix_names_different_discs() {
712 let d = anchor_ix_disc("deposit");
713 let w = anchor_ix_disc("withdraw");
714 assert_ne!(d, w);
715 }
716
717 #[test]
718 fn check_ix_disc_succeeds() {
719 let disc = anchor_ix_disc("deposit");
720 let mut data = [0u8; 32];
721 data[..8].copy_from_slice(&disc);
722 assert!(check_anchor_ix_disc(&data, &disc).is_ok());
723 }
724
725 #[test]
726 fn check_ix_disc_rejects_wrong() {
727 let disc = anchor_ix_disc("deposit");
728 let data = [0u8; 32]; // all zeros
729 assert!(check_anchor_ix_disc(&data, &disc).is_err());
730 }
731
732 #[test]
733 fn check_ix_disc_rejects_short() {
734 let disc = anchor_ix_disc("deposit");
735 let data = [0u8; 4]; // too short
736 assert!(check_anchor_ix_disc(&data, &disc).is_err());
737 }
738
739 #[test]
740 fn check_ix_and_body_returns_tail() {
741 let disc = anchor_ix_disc("process");
742 let mut data = [0u8; 16];
743 data[..8].copy_from_slice(&disc);
744 data[8] = 42;
745 let body = check_ix_and_body(&data, &disc).unwrap();
746 assert_eq!(body.len(), 8);
747 assert_eq!(body[0], 42);
748 }
749
750 // ── Version-aware cross-framework tests ──────────────────────────
751
752 #[test]
753 fn version_check_passes() {
754 let disc = anchor_disc("MyLayout");
755 let layout_id = [0xAA; 8];
756 let mut data = [0u8; 28];
757 data[..8].copy_from_slice(&disc);
758 data[9] = 2; // version at body offset 1
759 data[12..20].copy_from_slice(&layout_id);
760
761 assert!(check_anchor_with_version(&data, &disc, &layout_id, 2).is_ok());
762 }
763
764 #[test]
765 fn version_check_rejects_wrong_version() {
766 let disc = anchor_disc("MyLayout");
767 let layout_id = [0xAA; 8];
768 let mut data = [0u8; 28];
769 data[..8].copy_from_slice(&disc);
770 data[9] = 1; // version 1
771 data[12..20].copy_from_slice(&layout_id);
772
773 assert!(check_anchor_with_version(&data, &disc, &layout_id, 2).is_err());
774 }
775
776 #[test]
777 fn version_check_rejects_short() {
778 let disc = anchor_disc("MyLayout");
779 let layout_id = [0xAA; 8];
780 let mut data = [0u8; 20]; // needs 24 (8 disc + 16 header)
781 data[..8].copy_from_slice(&disc);
782
783 assert!(check_anchor_with_version(&data, &disc, &layout_id, 1).is_err());
784 }
785
786 // ── Event discriminator tests ────────────────────────────────────
787
788 #[test]
789 fn event_disc_is_deterministic() {
790 let d1 = anchor_event_disc("TransferEvent");
791 let d2 = anchor_event_disc("TransferEvent");
792 assert_eq!(d1, d2);
793 }
794
795 #[test]
796 fn event_disc_differs_from_account_and_ix() {
797 let event = anchor_event_disc("X");
798 let acct = anchor_disc("X");
799 let ix = anchor_ix_disc("X");
800 assert_ne!(event, acct);
801 assert_ne!(event, ix);
802 }
803}