Skip to main content

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}