Skip to main content

hopper_runtime/
cpi.rs

1//! Cross-program invocation for Hopper programs.
2//!
3//! Provides both checked (borrow-validating) and unchecked invoke paths.
4//! Hopper uses direct runtime syscalls after Hopper-level validation.
5
6use crate::account::AccountView;
7use crate::address::{address_eq, Address};
8use crate::error::ProgramError;
9use crate::instruction::{CpiAccount, InstructionView};
10use crate::ProgramResult;
11use core::mem::MaybeUninit;
12
13#[cfg(target_os = "solana")]
14use crate::instruction::InstructionAccount;
15
16// Re-export Signer and Seed so callers can use `cpi::Signer` / `cpi::Seed`.
17pub use crate::instruction::{Seed, Signer};
18
19/// Default stack-sized ceiling for a *static* CPI call.
20///
21/// This is deliberately the low pre-SIMD-0339 value. It is used to size
22/// fixed `MaybeUninit` scratch arrays (e.g. `token.rs`) that live on the
23/// SBF stack, whose per-frame budget is only 4 KiB. Raising this constant
24/// would grow those arrays for every program regardless of need. Wide-CPI
25/// callers instead pick a larger per-call const-generic `MAX_ACCOUNTS`
26/// (bounded by [`MAX_CPI_ACCOUNTS`]), which is zero-cost when unused.
27pub const MAX_STATIC_CPI_ACCOUNTS: usize = 64;
28
29/// Hard ceiling on the number of account-infos in any single CPI.
30///
31/// Raised from 128 to 255 for **SIMD-0339** (`increase_cpi_account_info_limit`,
32/// agave gate `H6iVbVaDZgDphcPbcZwc5LoznMPWQfnJ1AM7L1xzqvt5`, live on testnet
33/// epoch 883), which lifts the runtime CPI account-info limit from 64 to 255.
34/// This is a *ceiling* constant only; it does not size any stack array, so
35/// widening it costs nothing for programs that stay small. The actual scratch
36/// allocation is governed by a per-call const-generic `MAX_ACCOUNTS`.
37///
38/// Under 0339 every distinct account-info also carries a per-info CU cost, so
39/// passing the *fewest* infos per CPI becomes a cost axis. [`DynCpi`] exploits
40/// this by deduplicating account-infos by pubkey; see
41/// [`invoke_signed_deduped`].
42///
43/// [`DynCpi`]: crate::dyn_cpi::DynCpi
44pub const MAX_CPI_ACCOUNTS: usize = 255;
45
46/// Maximum return data size (1 KiB).
47pub const MAX_RETURN_DATA: usize = 1024;
48
49// -- Hopper CPI -------------------------------------------------------
50
51#[cfg(target_os = "solana")]
52#[repr(C)]
53struct CInstruction<'a> {
54    program_id: *const Address,
55    accounts: *const InstructionAccount<'a>,
56    accounts_len: u64,
57    data: *const u8,
58    data_len: u64,
59}
60
61// -- Unchecked invoke -------------------------------------------------
62
63/// Invoke a CPI without borrow validation (lowest CU cost).
64///
65/// # Safety
66///
67/// The caller must ensure no account data borrows conflict with the CPI.
68#[inline]
69pub unsafe fn invoke_unchecked(
70    instruction: &InstructionView<'_, '_, '_, '_>,
71    accounts: &[CpiAccount<'_>],
72) -> ProgramResult {
73    // The signed form with no seeds is the unsigned invoke: the syscall
74    // reads the seed pointer only when the count is nonzero. One wrapper
75    // body serves both, so a program that invokes signed and unsigned links
76    // one syscall site instead of two.
77    // SAFETY: the caller upholds the unchecked CPI contract; forwarded as is.
78    unsafe { invoke_signed_unchecked(instruction, accounts, &[]) }
79}
80
81/// Invoke a signed CPI without borrow validation.
82///
83/// # Safety
84///
85/// The caller must ensure no account data borrows conflict with the CPI.
86#[inline(always)]
87pub unsafe fn invoke_signed_unchecked(
88    instruction: &InstructionView<'_, '_, '_, '_>,
89    accounts: &[CpiAccount<'_>],
90    signers_seeds: &[Signer<'_, '_>],
91) -> ProgramResult {
92    #[cfg(target_os = "solana")]
93    {
94        let c_instruction = CInstruction {
95            program_id: instruction.program_id as *const Address,
96            accounts: instruction.accounts.as_ptr(),
97            accounts_len: instruction.accounts.len() as u64,
98            data: instruction.data.as_ptr(),
99            data_len: instruction.data.len() as u64,
100        };
101
102        // SAFETY: `c_instruction` and the three slices are live for the
103        // synchronous call and have the C layouts the syscall reads; aliasing
104        // and privilege are this function's contract.
105        let result = unsafe {
106            hopper_native::syscalls::sol_invoke_signed_c(
107                &c_instruction as *const _ as *const u8,
108                accounts.as_ptr() as *const u8,
109                accounts.len() as u64,
110                signers_seeds.as_ptr() as *const u8,
111                signers_seeds.len() as u64,
112            )
113        };
114        if result == 0 {
115            Ok(())
116        } else {
117            Err(cpi_error(result))
118        }
119    }
120    #[cfg(not(target_os = "solana"))]
121    {
122        let _ = (instruction, accounts, signers_seeds);
123        Ok(())
124    }
125}
126
127/// Map a failed invoke's return code. Out of line and cold, so the success
128/// path after the syscall is one compare.
129#[cfg(target_os = "solana")]
130#[cold]
131#[inline(never)]
132fn cpi_error(code: u64) -> ProgramError {
133    ProgramError::from(code)
134}
135
136// ---------------------------------------------------------------------
137
138/// Reject duplicate writable accounts before invoking CPI.
139#[inline]
140pub(crate) fn validate_no_duplicate_writable(
141    instruction: &InstructionView<'_, '_, '_, '_>,
142    account_views: &[&AccountView<'_>],
143) -> ProgramResult {
144    let mut i = 0;
145    while i < instruction.accounts.len() {
146        if instruction.accounts[i].is_writable {
147            let mut j = i + 1;
148            while j < instruction.accounts.len() {
149                if instruction.accounts[j].is_writable
150                    && address_eq(account_views[i].address(), account_views[j].address())
151                {
152                    return Err(ProgramError::AccountBorrowFailed);
153                }
154                j += 1;
155            }
156        }
157        i += 1;
158    }
159    Ok(())
160}
161
162#[inline]
163fn signer_authority_supplied(signers_seeds: &[Signer<'_, '_>]) -> bool {
164    // PDA signer addresses are derived with the *calling* program id. A CPI
165    // instruction only carries the callee id, so this layer cannot reproduce
166    // that derivation without accidentally checking against the wrong
167    // program. The SVM's `sol_invoke_signed` syscall performs the
168    // authoritative seed validation and required-signer match. Preflight can
169    // safely reject the unambiguous no-authority case and otherwise defer the
170    // cryptographic check to the runtime.
171    //
172    // Host System-program emulation follows the same rule. It cannot know the
173    // caller id either, so signed host tests should validate their PDA inputs
174    // separately when caller-id correctness is the subject of the test.
175    !signers_seeds.is_empty()
176}
177
178/// Per-account meta↔view correspondence + borrow-state validation, the
179/// borrow-checked tier.
180///
181/// For each account: the view at index `i` must name the same address as
182/// meta `i` (so the borrow check applies to the correct account), then
183/// writable metas must be exclusively borrowable
184/// ([`AccountView::check_borrow_mut`]) and read-only metas must be
185/// shared-borrowable ([`AccountView::check_borrow`]). This is exactly the
186/// per-account check Pinocchio's safe `invoke` performs before a CPI. No
187/// signer, writability, or duplicate-writable validation happens here,
188/// those belong to the default [`invoke_signed`] tier.
189#[inline]
190#[cfg_attr(target_os = "solana", allow(dead_code))]
191fn validate_cpi_borrows(
192    instruction: &InstructionView<'_, '_, '_, '_>,
193    account_views: &[&AccountView<'_>],
194) -> ProgramResult {
195    if account_views.len() < instruction.accounts.len() {
196        return Err(ProgramError::NotEnoughAccountKeys);
197    }
198
199    let mut i = 0;
200    while i < instruction.accounts.len() {
201        // The borrow state must be validated against the account the meta
202        // actually names, not whatever view happens to sit at index `i`.
203        // Without this, a caller passing views in a different order than
204        // the metas would borrow-check the wrong (account, mutability)
205        // pair and then reach `invoke_unchecked` with its aliasing
206        // contract undischarged, UB from safe code. Pinocchio's safe
207        // `invoke` keeps exactly this check for exactly this reason
208        // (solana-instruction-view `cpi.rs`).
209        if !address_eq(account_views[i].address(), instruction.accounts[i].address) {
210            return Err(ProgramError::InvalidArgument);
211        }
212        if instruction.accounts[i].is_writable {
213            account_views[i].check_borrow_mut()?;
214        } else {
215            account_views[i].check_borrow()?;
216        }
217        i += 1;
218    }
219
220    // Sweep the mutation-completeness hand-off gate once per CPI behind the
221    // liveness branch, never reachable from the per-meta loop (the
222    // 2026-07-09 bisect measured closure-reachable gate machinery at
223    // ~+52 CU per router hop for ungated programs; see invoke_signed).
224    if crate::write_policy::lamport_gate_active() {
225        let mut m = 0;
226        while m < instruction.accounts.len() {
227            if instruction.accounts[m].is_writable {
228                crate::write_policy::check_lamport_delegation(account_views[m].address())?;
229            }
230            m += 1;
231        }
232    }
233
234    Ok(())
235}
236
237#[cfg(not(target_os = "solana"))]
238fn is_host_system_transfer(instruction: &InstructionView<'_, '_, '_, '_>) -> bool {
239    // `SYSTEM_PROGRAM_ID` is the all-zero address, so an OR-fold
240    // is-zero check is equivalent to (and cheaper than) comparing
241    // against the constant.
242    crate::address::address_is_zero(instruction.program_id)
243        && instruction.data.len() == 12
244        && instruction.data[0..4] == [2, 0, 0, 0]
245}
246
247// This validator only walks `instruction.accounts` (address/signer/
248// writable/borrow checks); it never inspects `instruction.data`; so it
249// is not actually Transfer-specific. `emulate_host_system_create_account`,
250// `emulate_host_system_allocate`, and `emulate_host_system_assign` below
251// reuse it verbatim for their host emulations instead of duplicating the
252// same four checks under a second name. `min_views` is each instruction's
253// account arity (2 for Transfer/CreateAccount, 1 for Allocate/Assign): the
254// emulations index `account_views[..min_views]` directly, so the guard
255// must refuse a shorter hand-built view list before they do.
256#[cfg(not(target_os = "solana"))]
257fn validate_host_system_transfer(
258    instruction: &InstructionView<'_, '_, '_, '_>,
259    account_views: &[&AccountView<'_>],
260    signers_seeds: &[Signer<'_, '_>],
261    min_views: usize,
262) -> ProgramResult {
263    if account_views.len() < instruction.accounts.len() || account_views.len() < min_views {
264        return Err(ProgramError::NotEnoughAccountKeys);
265    }
266
267    let mut i = 0;
268    while i < instruction.accounts.len() {
269        let expected = &instruction.accounts[i];
270        let actual = account_views[i];
271
272        if !address_eq(actual.address(), expected.address) {
273            return Err(ProgramError::InvalidAccountData);
274        }
275        if expected.is_signer && !actual.is_signer() && !signer_authority_supplied(signers_seeds) {
276            return Err(ProgramError::MissingRequiredSignature);
277        }
278        if expected.is_writable && !actual.is_writable() {
279            return Err(ProgramError::Immutable);
280        }
281        // Mirror the on-chain default tier's borrow-state checks so the
282        // host emulation is not *weaker* than the borrow-checked tier it
283        // sits above (tier ordering: checked ≥ default > borrow_checked).
284        if expected.is_writable {
285            actual.check_borrow_mut()?;
286        } else {
287            actual.check_borrow()?;
288        }
289
290        i += 1;
291    }
292
293    // Sweep the mutation-completeness hand-off gate after the loop, matching the
294    // on-chain tiers' once-per-CPI placement so the host emulation's
295    // error surface (including the borrow-before-delegation precedence)
296    // stays identical to on-chain.
297    if crate::write_policy::lamport_gate_active() {
298        let mut m = 0;
299        while m < instruction.accounts.len() {
300            if instruction.accounts[m].is_writable {
301                crate::write_policy::check_lamport_delegation(account_views[m].address())?;
302            }
303            m += 1;
304        }
305    }
306
307    validate_no_duplicate_writable(instruction, account_views)
308}
309
310#[cfg(not(target_os = "solana"))]
311fn emulate_host_system_transfer(
312    instruction: &InstructionView<'_, '_, '_, '_>,
313    account_views: &[&AccountView<'_>],
314) -> ProgramResult {
315    let amount = u64::from_le_bytes([
316        instruction.data[4],
317        instruction.data[5],
318        instruction.data[6],
319        instruction.data[7],
320        instruction.data[8],
321        instruction.data[9],
322        instruction.data[10],
323        instruction.data[11],
324    ]);
325    let from = account_views[0];
326    let to = account_views[1];
327
328    // Pre-validate both sides against the lamport gate before
329    // any balance mutation. Relying on the per-account `set_lamports`
330    // funnel alone would debit `from` and then have `to` refused at the
331    // funnel, destroying lamports in host state on the error path, a
332    // transfer must be all-or-nothing.
333    crate::write_policy::check_lamport_mutation(from.address())?;
334    crate::write_policy::check_lamport_mutation(to.address())?;
335
336    // Self-transfer (same address = same underlying account): net zero.
337    // Handled explicitly because the compute-both-then-apply sequence
338    // below would otherwise credit from the pre-debit balance and mint
339    // `amount` out of thin air.
340    if address_eq(from.address(), to.address()) {
341        if from.lamports() < amount {
342            return Err(ProgramError::InsufficientFunds);
343        }
344        return Ok(());
345    }
346
347    // Compute both post-balances before applying either, so an
348    // arithmetic refusal (insufficient funds, overflow) also cannot
349    // half-apply the transfer.
350    let debited = from
351        .lamports()
352        .checked_sub(amount)
353        .ok_or(ProgramError::InsufficientFunds)?;
354    let credited = to
355        .lamports()
356        .checked_add(amount)
357        .ok_or(ProgramError::ArithmeticOverflow)?;
358    from.set_lamports(debited)?;
359    to.set_lamports(credited)?;
360    Ok(())
361}
362
363#[cfg(not(target_os = "solana"))]
364fn is_host_system_create_account(instruction: &InstructionView<'_, '_, '_, '_>) -> bool {
365    // `CreateAccount { lamports, space, owner }`,
366    // `[0u32 LE][lamports: u64 LE][space: u64 LE][owner: 32 bytes]`
367    // (52 bytes). See `hopper_system::encoders::encode_create_account`.
368    crate::address::address_is_zero(instruction.program_id)
369        && instruction.data.len() == 52
370        && instruction.data[0..4] == [0, 0, 0, 0]
371}
372
373/// Host-only emulation of the System Program's `CreateAccount`.
374///
375/// Programs that build this CPI directly, via
376/// [`crate::system::CreateAccount`], fund + allocate + assign a brand-new
377/// account with it. (`hopper_init!` now issues `CreateAccountAllowPrefund`
378/// instead; see [`emulate_host_system_create_account_allow_prefund`].)
379/// Off-chain, the raw syscall wrappers ([`invoke_unchecked`] /
380/// [`invoke_signed_unchecked`]) are no-ops by design (there is no runtime
381/// to service the syscall), without this emulation the account is left
382/// at its pre-CPI zero-length state and the header write that immediately
383/// follows fails with `AccountDataTooSmall`, making every `init` /
384/// `init_if_needed` context untestable end-to-end through a host harness.
385/// This reproduces the System Program's own observable effect: debit
386/// `from`, credit `to`, resize `to` to `space` (zero-filling the new
387/// region, mirroring [`AccountView::resize`]'s on-chain growth
388/// semantics), and assign `to`'s owner.
389#[cfg(not(target_os = "solana"))]
390fn emulate_host_system_create_account(
391    instruction: &InstructionView<'_, '_, '_, '_>,
392    account_views: &[&AccountView<'_>],
393) -> ProgramResult {
394    let lamports = u64::from_le_bytes(instruction.data[4..12].try_into().unwrap());
395    let space = u64::from_le_bytes(instruction.data[12..20].try_into().unwrap()) as usize;
396    let mut owner_bytes = [0u8; 32];
397    owner_bytes.copy_from_slice(&instruction.data[20..52]);
398    let owner = Address::new_from_array(owner_bytes);
399
400    let from = account_views[0];
401    let to = account_views[1];
402
403    // The System Program refuses to create over an account that already
404    // carries lamports or data. `hopper_init!` only issues this CPI once
405    // it has already checked `to.data_len() == 0` itself, but the guard
406    // is repeated here so a `CreateAccount` CPI built directly (bypassing
407    // `hopper_init!`) gets the same off-chain refusal it would get
408    // on-chain.
409    if to.lamports() != 0 || to.data_len() != 0 {
410        return Err(ProgramError::AccountAlreadyInitialized);
411    }
412
413    // Pre-validate both sides against the lamport gate before any
414    // balance mutation; see the identical note on
415    // `emulate_host_system_transfer`.
416    crate::write_policy::check_lamport_mutation(from.address())?;
417    crate::write_policy::check_lamport_mutation(to.address())?;
418
419    let debited = from
420        .lamports()
421        .checked_sub(lamports)
422        .ok_or(ProgramError::InsufficientFunds)?;
423    let credited = to
424        .lamports()
425        .checked_add(lamports)
426        .ok_or(ProgramError::ArithmeticOverflow)?;
427    from.set_lamports(debited)?;
428    to.set_lamports(credited)?;
429
430    to.resize(space)?;
431    // SAFETY: `to` was validated writable by `validate_host_system_transfer`
432    // (the generic meta-check reused above) before this point, and this
433    // function stands in for the System Program's own CreateAccount
434    // handler, the one caller the real runtime authorizes to assign a
435    // fresh (System-owned, empty) account's owner.
436    unsafe {
437        to.assign(&owner);
438    }
439
440    Ok(())
441}
442
443#[cfg(not(target_os = "solana"))]
444fn is_host_system_create_account_allow_prefund(
445    instruction: &InstructionView<'_, '_, '_, '_>,
446) -> bool {
447    // `CreateAccountAllowPrefund { lamports, space, owner }`,
448    // `[13u32 LE][lamports: u64 LE][space: u64 LE][owner: 32 bytes]`
449    // (52 bytes). See
450    // `hopper_system::encoders::encode_create_account_allow_prefund`.
451    crate::address::address_is_zero(instruction.program_id)
452        && instruction.data.len() == 52
453        && instruction.data[0..4] == [13, 0, 0, 0]
454}
455
456/// Host-only emulation of the System Program's `CreateAccountAllowPrefund`.
457///
458/// `init` / `init_if_needed` (`hopper_init!` in `hopper-macros`) reaches
459/// this CPI, via [`crate::system::CreateAccountAllowPrefund`], for every
460/// account it creates, pre-funded or not. Off-chain the raw syscall
461/// wrappers are no-ops, so without this emulation the account is left at
462/// zero length and the header write that follows fails with
463/// `AccountDataTooSmall`.
464///
465/// This reproduces the System Program handler's observable effect and
466/// order (agave `system_processor.rs`, `create_account_allow_prefund`):
467/// refuse an account that already carries data or a foreign owner, then
468/// allocate `space` (zero-filled), assign `owner`, and finally transfer
469/// the `lamports` delta from the funding account at index 1 when it is
470/// nonzero. An existing balance on `to` is allowed; that is the
471/// instruction's purpose. The lamport arithmetic is checked before any
472/// mutation so a refused transfer leaves the account untouched, matching
473/// the on-chain transaction rollback.
474#[cfg(not(target_os = "solana"))]
475fn emulate_host_system_create_account_allow_prefund(
476    instruction: &InstructionView<'_, '_, '_, '_>,
477    account_views: &[&AccountView<'_>],
478) -> ProgramResult {
479    let lamports = u64::from_le_bytes(instruction.data[4..12].try_into().unwrap());
480    let space = u64::from_le_bytes(instruction.data[12..20].try_into().unwrap()) as usize;
481    let mut owner_bytes = [0u8; 32];
482    owner_bytes.copy_from_slice(&instruction.data[20..52]);
483    let owner = Address::new_from_array(owner_bytes);
484
485    let to = account_views[0];
486    // SAFETY: the host emulator runs on one thread with no live CPI, so the
487    // owner field cannot change while this reference is held; it is read
488    // once and dropped before any mutation below.
489    let system_owned = crate::address::address_is_zero(unsafe { to.owner() });
490    if to.data_len() != 0 || !system_owned {
491        return Err(ProgramError::AccountAlreadyInitialized);
492    }
493
494    let funding = if lamports > 0 {
495        let from = *account_views
496            .get(1)
497            .ok_or(ProgramError::NotEnoughAccountKeys)?;
498        // Pre-validate both sides against the lamport gate before any
499        // mutation; see the identical note on `emulate_host_system_transfer`.
500        crate::write_policy::check_lamport_mutation(from.address())?;
501        crate::write_policy::check_lamport_mutation(to.address())?;
502        let debited = from
503            .lamports()
504            .checked_sub(lamports)
505            .ok_or(ProgramError::InsufficientFunds)?;
506        let credited = to
507            .lamports()
508            .checked_add(lamports)
509            .ok_or(ProgramError::ArithmeticOverflow)?;
510        Some((from, debited, credited))
511    } else {
512        None
513    };
514
515    to.resize(space)?;
516    // SAFETY: `to` was validated writable by `validate_host_system_transfer`
517    // (the generic meta-check reused at the dispatch site) before this
518    // point, and this function stands in for the System Program's own
519    // handler, the one caller the real runtime authorizes to assign a
520    // fresh (System-owned, empty) account's owner.
521    unsafe {
522        to.assign(&owner);
523    }
524    if let Some((from, debited, credited)) = funding {
525        from.set_lamports(debited)?;
526        to.set_lamports(credited)?;
527    }
528    Ok(())
529}
530
531#[cfg(not(target_os = "solana"))]
532fn is_host_system_allocate(instruction: &InstructionView<'_, '_, '_, '_>) -> bool {
533    // `Allocate { space }`, `[8u32 LE][space: u64 LE]` (12 bytes).
534    // See `hopper_system::encoders::encode_allocate`.
535    crate::address::address_is_zero(instruction.program_id)
536        && instruction.data.len() == 12
537        && instruction.data[0..4] == [8, 0, 0, 0]
538}
539
540/// Host-only emulation of the System Program's `Allocate`.
541///
542/// Programs that build this CPI directly, via [`crate::system::Allocate`],
543/// reach it when they allocate a pre-funded System account by hand.
544/// (`hopper_init!` used to issue Transfer, Allocate, and Assign for that
545/// case and now issues one `CreateAccountAllowPrefund`.) Off-chain the raw
546/// syscall wrappers are no-ops, so without this emulation the account is
547/// left at zero length and any header write that follows fails with
548/// `AccountDataTooSmall`. This reproduces the System Program's own
549/// observable effect: resize the account to `space`, zero-filling the
550/// new region (mirroring [`AccountView::resize`]'s on-chain growth
551/// semantics). No lamports move in an `Allocate`, so unlike the
552/// Transfer/CreateAccount emulations there is deliberately no mutation-completeness
553/// lamport-mutation precheck here; the shared validator's
554/// writable/borrow/delegation sweep is the whole gate, exactly as for
555/// the real instruction.
556#[cfg(not(target_os = "solana"))]
557fn emulate_host_system_allocate(
558    instruction: &InstructionView<'_, '_, '_, '_>,
559    account_views: &[&AccountView<'_>],
560) -> ProgramResult {
561    let space = u64::from_le_bytes(instruction.data[4..12].try_into().unwrap()) as usize;
562    let target = account_views[0];
563
564    // The System Program refuses to allocate an account that already
565    // carries data (the "account already in use" class of refusal).
566    // `hopper_init!` only issues this CPI once it has already checked
567    // `data_len() == 0` itself, but the guard is repeated here so an
568    // `Allocate` CPI built directly (bypassing `hopper_init!`) gets the
569    // same off-chain refusal it would get on-chain.
570    if target.data_len() != 0 {
571        return Err(ProgramError::AccountAlreadyInitialized);
572    }
573
574    target.resize(space)
575}
576
577#[cfg(not(target_os = "solana"))]
578fn is_host_system_assign(instruction: &InstructionView<'_, '_, '_, '_>) -> bool {
579    // `Assign { owner }`, `[1u32 LE][owner: 32 bytes]` (36 bytes).
580    // See `hopper_system::encoders::encode_assign`.
581    crate::address::address_is_zero(instruction.program_id)
582        && instruction.data.len() == 36
583        && instruction.data[0..4] == [1, 0, 0, 0]
584}
585
586/// Host-only emulation of the System Program's `Assign`.
587///
588/// The companion of [`emulate_host_system_allocate`] for programs that
589/// allocate and assign a pre-funded System account by hand, via
590/// [`crate::system::Assign`]. This reproduces the System Program's own observable
591/// effect: set the account's owner. Like the real `Assign`, it moves no
592/// lamports, so there is deliberately no mutation-completeness lamport-mutation
593/// precheck; the shared validator's writable/borrow/delegation sweep is
594/// the whole gate.
595#[cfg(not(target_os = "solana"))]
596fn emulate_host_system_assign(
597    instruction: &InstructionView<'_, '_, '_, '_>,
598    account_views: &[&AccountView<'_>],
599) -> ProgramResult {
600    let mut owner_bytes = [0u8; 32];
601    owner_bytes.copy_from_slice(&instruction.data[4..36]);
602    let owner = Address::new_from_array(owner_bytes);
603
604    let target = account_views[0];
605
606    // SAFETY: `target` was validated writable by
607    // `validate_host_system_transfer` (the generic meta-check reused at
608    // the dispatch site) before this point, and this function stands in
609    // for the System Program's own Assign handler, the one caller the
610    // real runtime authorizes to reassign a System-owned account's owner
611    // (with the assignee's signature, which the same validator checked
612    // against the builder's writable_signer meta).
613    unsafe {
614        target.assign(&owner);
615    }
616
617    Ok(())
618}
619
620// ---------------------------------------------------------------------
621
622/// Invoke a CPI with full validation.
623#[inline]
624pub fn invoke<const ACCOUNTS: usize>(
625    instruction: &InstructionView<'_, '_, '_, '_>,
626    account_views: &[&AccountView<'_>; ACCOUNTS],
627) -> ProgramResult {
628    invoke_signed::<ACCOUNTS>(instruction, account_views, &[])
629}
630
631/// Host-only System Program emulation shared by the checked invoke tiers:
632/// `Some` when the instruction is one of the emulated System instructions
633/// (and carries its result), `None` when the caller should proceed to its
634/// validation pass and the (no-op off-chain) syscall.
635#[cfg(not(target_os = "solana"))]
636#[inline]
637fn emulate_host_system(
638    instruction: &InstructionView<'_, '_, '_, '_>,
639    account_views: &[&AccountView<'_>],
640    signers_seeds: &[Signer<'_, '_>],
641) -> Option<ProgramResult> {
642    if is_host_system_transfer(instruction) {
643        return Some(
644            validate_host_system_transfer(instruction, account_views, signers_seeds, 2)
645                .and_then(|()| emulate_host_system_transfer(instruction, account_views)),
646        );
647    }
648    if is_host_system_create_account(instruction) {
649        return Some(
650            validate_host_system_transfer(instruction, account_views, signers_seeds, 2)
651                .and_then(|()| emulate_host_system_create_account(instruction, account_views)),
652        );
653    }
654    if is_host_system_create_account_allow_prefund(instruction) {
655        return Some(
656            validate_host_system_transfer(instruction, account_views, signers_seeds, 1).and_then(
657                |()| emulate_host_system_create_account_allow_prefund(instruction, account_views),
658            ),
659        );
660    }
661    if is_host_system_allocate(instruction) {
662        return Some(
663            validate_host_system_transfer(instruction, account_views, signers_seeds, 1)
664                .and_then(|()| emulate_host_system_allocate(instruction, account_views)),
665        );
666    }
667    if is_host_system_assign(instruction) {
668        return Some(
669            validate_host_system_transfer(instruction, account_views, signers_seeds, 1)
670                .and_then(|()| emulate_host_system_assign(instruction, account_views)),
671        );
672    }
673    None
674}
675
676/// Invoke a signed CPI with full validation.
677#[inline]
678pub fn invoke_signed<const ACCOUNTS: usize>(
679    instruction: &InstructionView<'_, '_, '_, '_>,
680    account_views: &[&AccountView<'_>; ACCOUNTS],
681    signers_seeds: &[Signer<'_, '_>],
682) -> ProgramResult {
683    #[cfg(not(target_os = "solana"))]
684    if let Some(result) = emulate_host_system(instruction, &account_views[..], signers_seeds) {
685        return result;
686    }
687
688    let metas_len = instruction.accounts.len();
689
690    // Fused validate+build (default tier). `check_meta` runs the default
691    // tier's per-account contract, address identity, required-signer
692    // presence (or supplied PDA authority), writability coverage,
693    // and borrow state, in the *same* pass that materializes each
694    // `CpiAccount` scratch slot. `post_check` then runs the mutation-completeness
695    // lamport-delegation sweep (once per CPI, gate-liveness-guarded; see
696    // the note at the sweep) and the duplicate-writable footgun scan,
697    // then the syscall.
698    dispatch_cpi_fixed::<ACCOUNTS>(
699        instruction,
700        account_views,
701        signers_seeds,
702        metas_len,
703        |i| {
704            let expected = &instruction.accounts[i];
705            let actual = account_views[i];
706
707            if !address_eq(actual.address(), expected.address) {
708                return Err(ProgramError::InvalidAccountData);
709            }
710
711            if expected.is_signer
712                && !actual.is_signer()
713                && !signer_authority_supplied(signers_seeds)
714            {
715                return Err(ProgramError::MissingRequiredSignature);
716            }
717
718            if expected.is_writable && !actual.is_writable() {
719                return Err(ProgramError::Immutable);
720            }
721
722            if expected.is_writable {
723                actual.check_borrow_mut()?;
724            } else {
725                actual.check_borrow()?;
726            }
727
728            Ok(())
729        },
730        || {
731            // A writable CPI meta delegates unbounded data and
732            // lamport mutation to the callee. The delegation sweep runs
733            // ONCE per CPI here (not per meta) behind a liveness branch:
734            // keeping gate machinery reachable from the per-meta closure
735            // was measured to force spill-heavy codegen costing ~+52 CU
736            // per router hop for ungated programs (2026-07-09 bisect).
737            // Gated programs are still refused before the syscall.
738            if crate::write_policy::lamport_gate_active() {
739                let mut i = 0;
740                while i < metas_len {
741                    if instruction.accounts[i].is_writable {
742                        crate::write_policy::check_lamport_delegation(account_views[i].address())?;
743                    }
744                    i += 1;
745                }
746            }
747            validate_no_duplicate_writable(instruction, &account_views[..])
748        },
749    )
750}
751
752/// Fused validate-and-build for the fixed-array CPI tiers, plus the syscall
753/// (a no-op off-chain). Shared tail of the fixed-array invoke tiers.
754///
755/// Performs ONE pass over the account array: for each meta index `i` in
756/// `0..metas_len` it runs the tier-specific per-account check (`check_meta`)
757/// AND writes the `CpiAccount` scratch slot in the same iteration, replacing
758/// the previous validate-walk-then-build-walk pair. Slots `metas_len..
759/// ACCOUNTS` (account infos with no corresponding meta) are build-only, as
760/// before. `post_check` runs once after the pass; e.g. the default tier's
761/// duplicate-writable scan, which needs the full meta list, and before the
762/// syscall.
763///
764/// Fusing preserves observable behavior exactly: `check_meta` is invoked in
765/// ascending meta order, so the first failing meta returns the same error at
766/// the same point as the prior split; building a `CpiAccount` has no side
767/// effects and `CpiAccount` is `Copy`, so a `?` early-return from
768/// `check_meta` or `post_check` discards the never-read `MaybeUninit` scratch
769/// with no drop and no observable difference.
770///
771/// Validation is the **caller's** responsibility via the two closures: every
772/// caller must run at least the borrow-state checks over `account_views` (see
773/// [`invoke_signed`] and [`invoke_signed_borrow_checked`]), which discharges
774/// the `invoke_unchecked` safety contract.
775#[inline]
776fn dispatch_cpi_fixed<const ACCOUNTS: usize>(
777    instruction: &InstructionView<'_, '_, '_, '_>,
778    account_views: &[&AccountView<'_>; ACCOUNTS],
779    signers_seeds: &[Signer<'_, '_>],
780    metas_len: usize,
781    check_meta: impl Fn(usize) -> ProgramResult,
782    post_check: impl FnOnce() -> ProgramResult,
783) -> ProgramResult {
784    if ACCOUNTS < metas_len {
785        return Err(ProgramError::NotEnoughAccountKeys);
786    }
787
788    let mut cpi_accounts: [MaybeUninit<CpiAccount<'_>>; ACCOUNTS] =
789        // SAFETY: an array of `MaybeUninit<T>` is valid in any initialization
790        // state, so materializing it uninitialized is sound; every element is
791        // written by the loop below before it is read, and on an early
792        // `?`-return the array is discarded unread (`CpiAccount` is `Copy`, so
793        // no drop runs on the partially-filled scratch).
794        unsafe { MaybeUninit::uninit().assume_init() };
795
796    let mut i = 0;
797    while i < ACCOUNTS {
798        if i < metas_len {
799            check_meta(i)?;
800        }
801        cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(account_views[i]));
802        i += 1;
803    }
804
805    post_check()?;
806
807    // SAFETY: the loop above initialized all `ACCOUNTS` elements, and
808    // `MaybeUninit<T>` has the same layout as `T`, so reinterpreting the
809    // array as `[CpiAccount; ACCOUNTS]` reads only initialized memory.
810    let accounts: &[CpiAccount<'_>; ACCOUNTS] =
811        unsafe { &*(cpi_accounts.as_ptr() as *const [CpiAccount<'_>; ACCOUNTS]) };
812
813    // SAFETY: `check_meta`/`post_check` validated the borrow state of each
814    // account view (writable metas exclusively borrowable, read-only metas
815    // shared-borrowable), so no live borrow conflicts with the runtime's
816    // access during the CPI, exactly the invariant
817    // `invoke_unchecked`/`invoke_signed_unchecked` require.
818    unsafe { invoke_signed_unchecked(instruction, accounts.as_slice(), signers_seeds) }
819}
820
821/// The tier Hopper's own instruction builders invoke through.
822///
823/// A builder derives every meta from the view it passes at the same index,
824/// so meta and view name the same account by construction and the address
825/// check of [`invoke_signed`] has nothing to find. What stays is what
826/// soundness needs: writable metas must be exclusively borrowable and
827/// read-only metas shared-borrowable, so no live Rust borrow sees the
828/// callee's writes, and, when a lamport write policy is installed, every
829/// writable meta must be one the policy lets the instruction hand off.
830///
831/// Signer and writable privileges are left to the runtime, which refuses
832/// an escalation before the callee runs; the early refusals
833/// [`invoke_signed`] adds are diagnostics, not protection. So is its
834/// repeated-writable scan: the System, Token, and Token-2022 programs these
835/// builders target handle one account named twice (a self-transfer moves
836/// nothing; `CreateAccount` onto its own payer fails in the System
837/// Program). This is the shape of Pinocchio's builders, with the lamport
838/// gate on top.
839///
840/// Off chain the supported System instructions are emulated exactly as in
841/// [`invoke_signed`].
842#[inline(always)]
843pub(crate) fn invoke_signed_builder<const ACCOUNTS: usize>(
844    instruction: &InstructionView<'_, '_, '_, '_>,
845    account_views: &[&AccountView<'_>; ACCOUNTS],
846    signers_seeds: &[Signer<'_, '_>],
847) -> ProgramResult {
848    #[cfg(not(target_os = "solana"))]
849    if let Some(result) = emulate_host_system(instruction, &account_views[..], signers_seeds) {
850        return result;
851    }
852
853    // A plain loop and no closures: builders are inlined into the handler,
854    // and a closure-shaped body there gets outlined with its captures
855    // passed through the stack.
856    let metas = instruction.accounts;
857    let mut i = 0;
858    while i < ACCOUNTS {
859        if i < metas.len() {
860            if metas[i].is_writable {
861                account_views[i].check_borrow_mut()?;
862            } else {
863                account_views[i].check_borrow()?;
864            }
865        }
866        i += 1;
867    }
868    if crate::write_policy::lamport_gate_active() {
869        check_builder_delegation(metas, &account_views[..])?;
870    }
871
872    let mut cpi_accounts: [MaybeUninit<CpiAccount<'_>>; ACCOUNTS] =
873        // SAFETY: an array of `MaybeUninit<T>` is valid in any initialization
874        // state; every element is written below before it is read.
875        unsafe { MaybeUninit::uninit().assume_init() };
876    let mut j = 0;
877    while j < ACCOUNTS {
878        cpi_accounts[j] = MaybeUninit::new(CpiAccount::from(account_views[j]));
879        j += 1;
880    }
881    // SAFETY: the loop above initialized all `ACCOUNTS` elements, and
882    // `MaybeUninit<T>` has the layout of `T`.
883    let accounts: &[CpiAccount<'_>; ACCOUNTS] =
884        unsafe { &*(cpi_accounts.as_ptr() as *const [CpiAccount<'_>; ACCOUNTS]) };
885
886    // SAFETY: every meta's account passed its borrow check above (writable
887    // metas exclusively borrowable, read-only metas shared-borrowable), the
888    // invariant `invoke_signed_unchecked` requires.
889    unsafe { invoke_signed_unchecked(instruction, accounts.as_slice(), signers_seeds) }
890}
891
892/// [`invoke_signed_builder`] with the repeated-writable refusal of
893/// [`invoke_signed`] kept.
894///
895/// For callees that accept one account named twice where a caller almost
896/// never means it: an SPL Token transfer from an account to itself
897/// succeeds and moves nothing, so a program that credits a deposit after
898/// such a transfer would credit tokens it never received. The Token and
899/// Token-2022 builders invoke through this tier.
900#[inline(always)]
901pub(crate) fn invoke_signed_builder_distinct<const ACCOUNTS: usize>(
902    instruction: &InstructionView<'_, '_, '_, '_>,
903    account_views: &[&AccountView<'_>; ACCOUNTS],
904    signers_seeds: &[Signer<'_, '_>],
905) -> ProgramResult {
906    validate_no_duplicate_writable(instruction, &account_views[..])?;
907    invoke_signed_builder(instruction, account_views, signers_seeds)
908}
909
910/// The lamport-delegation sweep of [`invoke_signed_builder`], run only when
911/// a write policy is installed. Out of line so the builders' inlined body
912/// stays the ungated path.
913#[cold]
914#[inline(never)]
915fn check_builder_delegation(
916    metas: &[crate::instruction::InstructionAccount<'_>],
917    account_views: &[&AccountView<'_>],
918) -> ProgramResult {
919    let mut i = 0;
920    while i < metas.len() && i < account_views.len() {
921        if metas[i].is_writable {
922            crate::write_policy::check_lamport_delegation(account_views[i].address())?;
923        }
924        i += 1;
925    }
926    Ok(())
927}
928
929/// Invoke with a dynamic number of accounts (bounded by const generic).
930#[inline]
931pub fn invoke_with_bounds<const MAX_ACCOUNTS: usize>(
932    instruction: &InstructionView<'_, '_, '_, '_>,
933    account_views: &[&AccountView<'_>],
934) -> ProgramResult {
935    invoke_signed_with_bounds::<MAX_ACCOUNTS>(instruction, account_views, &[])
936}
937
938/// Signed invoke with a dynamic number of accounts (bounded by const generic).
939#[inline]
940pub fn invoke_signed_with_bounds<const MAX_ACCOUNTS: usize>(
941    instruction: &InstructionView<'_, '_, '_, '_>,
942    account_views: &[&AccountView<'_>],
943    signers_seeds: &[Signer<'_, '_>],
944) -> ProgramResult {
945    invoke_signed_bounded::<MAX_ACCOUNTS>(instruction, account_views, signers_seeds, false)
946}
947
948/// [`invoke_signed_with_bounds`] for an instruction that carries several
949/// inner instructions and therefore names one account through several
950/// writable metas on purpose: the SPL Token `Batch` (255), whose account
951/// list is the concatenation of every inner instruction's accounts.
952///
953/// Every other check of the default tier runs unchanged (address match per
954/// meta, signer and writability coverage, borrow state per meta, the
955/// lamport hand-off gate). Only the duplicate-writable refusal is skipped,
956/// because for a batch the repeat is the contract, not the footgun. The
957/// runtime serializes a repeated account once and marks the later metas as
958/// duplicates, so the callee sees one account through every one of them.
959/// This low-level function does not parse inner instructions. `TokenBatch`
960/// separately refuses repeated writable accounts within each appended
961/// instruction; custom callers must establish their own alias policy.
962#[inline]
963pub fn invoke_signed_batch_with_bounds<const MAX_ACCOUNTS: usize>(
964    instruction: &InstructionView<'_, '_, '_, '_>,
965    account_views: &[&AccountView<'_>],
966    signers_seeds: &[Signer<'_, '_>],
967) -> ProgramResult {
968    invoke_signed_bounded::<MAX_ACCOUNTS>(instruction, account_views, signers_seeds, true)
969}
970
971// Not inlined: the scratch array is MAX_ACCOUNTS CpiAccounts, and a
972// token builder that inlines this on top of its own meta and view arrays
973// overflows the 4 KiB frame.
974#[inline(never)]
975fn invoke_signed_bounded<const MAX_ACCOUNTS: usize>(
976    instruction: &InstructionView<'_, '_, '_, '_>,
977    account_views: &[&AccountView<'_>],
978    signers_seeds: &[Signer<'_, '_>],
979    repeated_writable_allowed: bool,
980) -> ProgramResult {
981    if account_views.len() > MAX_ACCOUNTS {
982        return Err(ProgramError::InvalidArgument);
983    }
984
985    #[cfg(not(target_os = "solana"))]
986    if let Some(result) = emulate_host_system(instruction, account_views, signers_seeds) {
987        return result;
988    }
989
990    let metas_len = instruction.accounts.len();
991    let count = account_views.len();
992    if count < metas_len {
993        return Err(ProgramError::NotEnoughAccountKeys);
994    }
995
996    let mut cpi_accounts: [MaybeUninit<CpiAccount<'_>>; MAX_ACCOUNTS] =
997        // SAFETY: an array of `MaybeUninit<T>` is valid in any initialization
998        // state; the first `count` slots are written before being read below,
999        // and on an early `?`-return the array is discarded unread
1000        // (`CpiAccount` is `Copy`, so no drop runs on the partial scratch).
1001        unsafe { MaybeUninit::uninit().assume_init() };
1002
1003    // Fused validate+build (default tier, dynamic): one pass runs the default
1004    // per-account contract for each meta AND writes its scratch slot; slots
1005    // `metas_len..count` are build-only. The duplicate-writable scan runs
1006    // afterward, exactly as `validate_cpi_accounts` ordered it.
1007    let mut i = 0;
1008    while i < count {
1009        let actual = account_views[i];
1010        if i < metas_len {
1011            let expected = &instruction.accounts[i];
1012
1013            if !address_eq(actual.address(), expected.address) {
1014                return Err(ProgramError::InvalidAccountData);
1015            }
1016
1017            if expected.is_signer
1018                && !actual.is_signer()
1019                && !signer_authority_supplied(signers_seeds)
1020            {
1021                return Err(ProgramError::MissingRequiredSignature);
1022            }
1023
1024            if expected.is_writable && !actual.is_writable() {
1025                return Err(ProgramError::Immutable);
1026            }
1027
1028            if expected.is_writable {
1029                actual.check_borrow_mut()?;
1030            } else {
1031                actual.check_borrow()?;
1032            }
1033        }
1034        cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(actual));
1035        i += 1;
1036    }
1037
1038    // Sweep the mutation-completeness hand-off gate once per CPI behind the
1039    // liveness branch (never reachable from the hot per-meta loop; see
1040    // the 2026-07-09 bisect note in `invoke_signed`'s sweep).
1041    if crate::write_policy::lamport_gate_active() {
1042        let mut m = 0;
1043        while m < instruction.accounts.len() {
1044            if instruction.accounts[m].is_writable {
1045                crate::write_policy::check_lamport_delegation(account_views[m].address())?;
1046            }
1047            m += 1;
1048        }
1049    }
1050
1051    if !repeated_writable_allowed {
1052        validate_no_duplicate_writable(instruction, account_views)?;
1053    }
1054
1055    // SAFETY: the loop above initialized the first `count` slots, and
1056    // `MaybeUninit<T>` shares `T`'s layout, so reading exactly that prefix
1057    // reads only initialized memory.
1058    let accounts = unsafe {
1059        core::slice::from_raw_parts(cpi_accounts.as_ptr() as *const CpiAccount<'_>, count)
1060    };
1061
1062    // SAFETY: Every account was checked above against the instruction's metas
1063    // (address, privilege, borrow state), which is the contract of the
1064    // unchecked call.
1065    unsafe { invoke_signed_unchecked(instruction, accounts, signers_seeds) }
1066}
1067
1068// -- SIMD-0339 dedup-aware path ---------------------------------------
1069
1070/// Locate the deduplicated info that carries `address` (linear scan).
1071#[inline]
1072fn find_info(infos: &[&AccountView<'_>], address: &Address) -> Option<usize> {
1073    let mut i = 0;
1074    while i < infos.len() {
1075        if address_eq(infos[i].address(), address) {
1076            return Some(i);
1077        }
1078        i += 1;
1079    }
1080    None
1081}
1082
1083/// Validate metas against a **deduplicated** info set (matched by pubkey).
1084///
1085/// Unlike the default tier's positional validation, `infos` is *not*
1086/// positionally aligned
1087/// with `instruction.accounts`: it holds exactly one [`AccountView`] per
1088/// unique address. Each meta is resolved to its info by address. Signer
1089/// presence (or supplied PDA authority), writability coverage,
1090/// per-account borrow state, and the duplicate-writable footgun are all
1091/// enforced over the full (un-deduplicated) meta list, so collapsing the
1092/// info list never weakens what the default tier checks.
1093#[inline]
1094fn validate_cpi_accounts_deduped(
1095    instruction: &InstructionView<'_, '_, '_, '_>,
1096    infos: &[&AccountView<'_>],
1097    signers_seeds: &[Signer<'_, '_>],
1098) -> ProgramResult {
1099    // Duplicate-writable footgun: two writable metas naming one account.
1100    // The infos are deduped, so `validate_no_duplicate_writable`'s
1101    // view-pair scan cannot observe it, check meta addresses directly.
1102    let mut i = 0;
1103    while i < instruction.accounts.len() {
1104        if instruction.accounts[i].is_writable {
1105            let mut j = i + 1;
1106            while j < instruction.accounts.len() {
1107                if instruction.accounts[j].is_writable
1108                    && address_eq(
1109                        instruction.accounts[i].address,
1110                        instruction.accounts[j].address,
1111                    )
1112                {
1113                    return Err(ProgramError::AccountBorrowFailed);
1114                }
1115                j += 1;
1116            }
1117        }
1118        i += 1;
1119    }
1120
1121    let mut i = 0;
1122    while i < instruction.accounts.len() {
1123        let expected = &instruction.accounts[i];
1124        // Resolve this meta to its unique account-info by pubkey. A meta
1125        // whose account was never supplied as an info is a malformed CPI.
1126        let info = match find_info(infos, expected.address) {
1127            Some(idx) => infos[idx],
1128            None => return Err(ProgramError::NotEnoughAccountKeys),
1129        };
1130
1131        if expected.is_signer && !info.is_signer() && !signer_authority_supplied(signers_seeds) {
1132            return Err(ProgramError::MissingRequiredSignature);
1133        }
1134        if expected.is_writable && !info.is_writable() {
1135            return Err(ProgramError::Immutable);
1136        }
1137        // Borrow state is checked per meta; `check_borrow`/`check_borrow_mut`
1138        // only *inspect* the borrow flag (they do not acquire), so resolving
1139        // several metas to the same info and checking each is sound. A
1140        // writable meta demands exclusive borrowability of that one info,
1141        // which is exactly the OR-merged requirement dedup must preserve.
1142        if expected.is_writable {
1143            info.check_borrow_mut()?;
1144        } else {
1145            info.check_borrow()?;
1146        }
1147        i += 1;
1148    }
1149
1150    // Sweep the mutation-completeness hand-off gate over the full, non-deduplicated meta
1151    // list (dedup collapses infos, never the delegation requirement),
1152    // swept once per CPI behind the liveness branch, never reachable
1153    // from the per-meta loop (2026-07-09 bisect; see invoke_signed).
1154    if crate::write_policy::lamport_gate_active() {
1155        let mut m = 0;
1156        while m < instruction.accounts.len() {
1157            let expected = &instruction.accounts[m];
1158            if expected.is_writable {
1159                if let Some(idx) = find_info(infos, expected.address) {
1160                    crate::write_policy::check_lamport_delegation(infos[idx].address())?;
1161                }
1162            }
1163            m += 1;
1164        }
1165    }
1166
1167    Ok(())
1168}
1169
1170/// Invoke a CPI whose account-info list has been **deduplicated by pubkey**,
1171/// the SIMD-0339 fewest-infos-per-CPI optimization.
1172///
1173/// `instruction.accounts` (the metas) may reference the same account in
1174/// several positions and the callee still sees that full ordered list.
1175/// `infos`, by contrast, holds exactly one [`AccountView`] per unique
1176/// address. Because the SVM resolves account-infos to metas by pubkey, N
1177/// metas of one account need only ONE info; under SIMD-0339 every distinct
1178/// info also costs CU, so collapsing them is a measurable saving that a
1179/// naive one-info-per-meta builder cannot claim.
1180///
1181/// `infos.len()` must be `<= MAX_INFOS` (the deduped list is what is handed
1182/// to the syscall). Validation runs over the full, un-deduplicated meta
1183/// list via the private `validate_cpi_accounts_deduped` helper, so this path is
1184/// strict as the default [`invoke_signed`] tier.
1185#[inline]
1186pub fn invoke_signed_deduped<const MAX_INFOS: usize>(
1187    instruction: &InstructionView<'_, '_, '_, '_>,
1188    infos: &[&AccountView<'_>],
1189    signers_seeds: &[Signer<'_, '_>],
1190) -> ProgramResult {
1191    if infos.len() > MAX_INFOS {
1192        return Err(ProgramError::InvalidArgument);
1193    }
1194
1195    #[cfg(not(target_os = "solana"))]
1196    if is_host_system_transfer(instruction) {
1197        validate_cpi_accounts_deduped(instruction, infos, signers_seeds)?;
1198        // This public API accepts any info order. Resolve the System transfer's
1199        // positional metas by address just as the SVM does; an extra info must
1200        // never be mistaken for the debited account.
1201        if instruction.accounts.len() < 2 {
1202            return Err(ProgramError::NotEnoughAccountKeys);
1203        }
1204        let source = find_info(infos, instruction.accounts[0].address)
1205            .ok_or(ProgramError::NotEnoughAccountKeys)?;
1206        let destination = find_info(infos, instruction.accounts[1].address)
1207            .ok_or(ProgramError::NotEnoughAccountKeys)?;
1208        return emulate_host_system_transfer(instruction, &[infos[source], infos[destination]]);
1209    }
1210
1211    validate_cpi_accounts_deduped(instruction, infos, signers_seeds)?;
1212
1213    let mut cpi_accounts: [MaybeUninit<CpiAccount<'_>>; MAX_INFOS] =
1214        // SAFETY: an array of `MaybeUninit<T>` is valid in any initialization
1215        // state, so materializing it uninitialized is sound; the first
1216        // `count` elements are written below before they are read.
1217        unsafe { MaybeUninit::uninit().assume_init() };
1218
1219    let count = infos.len();
1220    let mut i = 0;
1221    while i < count {
1222        cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(infos[i]));
1223        i += 1;
1224    }
1225
1226    // SAFETY: the loop initialized the first `count` elements, and
1227    // `MaybeUninit<T>` shares `T`'s layout, so reading exactly that prefix
1228    // reads only initialized memory.
1229    let accounts = unsafe {
1230        core::slice::from_raw_parts(cpi_accounts.as_ptr() as *const CpiAccount<'_>, count)
1231    };
1232
1233    // SAFETY: `validate_cpi_accounts_deduped` above discharged the borrow /
1234    // aliasing contract (writable infos exclusively borrowable, read-only
1235    // infos shared-borrowable) required by the unchecked syscall wrappers.
1236    unsafe {
1237        if signers_seeds.is_empty() {
1238            invoke_unchecked(instruction, accounts)
1239        } else {
1240            invoke_signed_unchecked(instruction, accounts, signers_seeds)
1241        }
1242    }
1243}
1244
1245/// Explicit alias for Hopper's validated CPI path.
1246#[inline]
1247pub fn invoke_checked<const ACCOUNTS: usize>(
1248    instruction: &InstructionView<'_, '_, '_, '_>,
1249    account_views: &[&AccountView<'_>; ACCOUNTS],
1250) -> ProgramResult {
1251    invoke::<ACCOUNTS>(instruction, account_views)
1252}
1253
1254/// Explicit alias for Hopper's validated signed CPI path.
1255#[inline]
1256pub fn invoke_signed_checked<const ACCOUNTS: usize>(
1257    instruction: &InstructionView<'_, '_, '_, '_>,
1258    account_views: &[&AccountView<'_>; ACCOUNTS],
1259    signers_seeds: &[Signer<'_, '_>],
1260) -> ProgramResult {
1261    invoke_signed::<ACCOUNTS>(instruction, account_views, signers_seeds)
1262}
1263
1264// -- Borrow-checked (Pinocchio-equivalent) tier -------------------------
1265
1266/// Invoke after checking account-address correspondence and live data borrows.
1267///
1268/// Writable metas require an exclusive borrow; readonly metas require a shared
1269/// borrow. When a lamport write policy is active, writable CPI delegation also
1270/// requires whole-account data and lamport permission.
1271///
1272/// Unlike the default [`invoke`] path, this tier omits local signer and writable
1273/// privilege checks and allows duplicate writable metas. The SVM still enforces
1274/// privileges and PDA signer derivation. Choose this tier only when the application
1275/// intends that account aliasing and has validated its account relationships.
1276///
1277/// [`invoke_checked`] is an explicit alias for the default tier. Unsafe
1278/// [`invoke_unchecked`] skips Hopper's checks and requires the caller to uphold
1279/// its documented borrow and descriptor contracts.
1280///
1281/// On host targets, supported System transfers are emulated. Other CPIs are
1282/// validation-only no-ops; exercise real callee behavior in an SVM or on devnet.
1283#[inline]
1284pub fn invoke_borrow_checked<const ACCOUNTS: usize>(
1285    instruction: &InstructionView<'_, '_, '_, '_>,
1286    account_views: &[&AccountView<'_>; ACCOUNTS],
1287) -> ProgramResult {
1288    invoke_signed_borrow_checked::<ACCOUNTS>(instruction, account_views, &[])
1289}
1290
1291/// Signed variant of [`invoke_borrow_checked`]. Signer seeds are forwarded to
1292/// the SVM, which derives and validates the caller's PDA authorities.
1293#[inline]
1294pub fn invoke_signed_borrow_checked<const ACCOUNTS: usize>(
1295    instruction: &InstructionView<'_, '_, '_, '_>,
1296    account_views: &[&AccountView<'_>; ACCOUNTS],
1297    signers_seeds: &[Signer<'_, '_>],
1298) -> ProgramResult {
1299    #[cfg(not(target_os = "solana"))]
1300    if is_host_system_transfer(instruction) {
1301        // The emulation reads views[0] and views[1] directly; guard the
1302        // fixed-array length before indexing (ACCOUNTS may be < 2).
1303        if account_views.len() < 2 {
1304            return Err(ProgramError::NotEnoughAccountKeys);
1305        }
1306        validate_cpi_borrows(instruction, &account_views[..])?;
1307        return emulate_host_system_transfer(instruction, &account_views[..]);
1308    }
1309
1310    let metas_len = instruction.accounts.len();
1311
1312    // Fused validate+build (borrow_checked tier). `check_meta` runs the
1313    // per-account checks `validate_cpi_borrows` did, meta↔view address
1314    // correspondence and borrow state, while the scratch slot is
1315    // materialized in the same pass. The mutation-completeness lamport-delegation scan
1316    // runs ONCE per CPI in `post_check`, NOT per meta: the 2026-07-09
1317    // router bisect measured that any *reachable* gate-machinery call
1318    // inside this per-meta closure forces it into an outlined,
1319    // spill-heavy shape costing ~+52 CU per hop for programs that never
1320    // installed a gate (branch-inside variants only recovered to ~+21;
1321    // machinery-unreachable-from-the-closure recovered fully:
1322    // 1,564/3,044/4,525 → 1,559/3,035/4,512 measured). Gated programs
1323    // keep full enforcement, the sweep still refuses before the syscall
1324    // hand-off in `dispatch_cpi_fixed`, with one documented precedence
1325    // shift: in a multi-fault instruction, borrow errors now surface
1326    // before delegation errors (both are pre-syscall refusals).
1327    dispatch_cpi_fixed::<ACCOUNTS>(
1328        instruction,
1329        account_views,
1330        signers_seeds,
1331        metas_len,
1332        |i| {
1333            // The borrow state must be validated against the account the meta
1334            // actually names, not whatever view happens to sit at index `i`
1335            // (see `validate_cpi_borrows` for why: a mismatched order would
1336            // borrow-check the wrong (account, mutability) pair and reach
1337            // `invoke_unchecked` with its aliasing contract undischarged).
1338            if !address_eq(account_views[i].address(), instruction.accounts[i].address) {
1339                return Err(ProgramError::InvalidArgument);
1340            }
1341            if instruction.accounts[i].is_writable {
1342                account_views[i].check_borrow_mut()?;
1343            } else {
1344                account_views[i].check_borrow()?;
1345            }
1346            Ok(())
1347        },
1348        || {
1349            if crate::write_policy::lamport_gate_active() {
1350                let mut i = 0;
1351                while i < metas_len {
1352                    if instruction.accounts[i].is_writable {
1353                        crate::write_policy::check_lamport_delegation(account_views[i].address())?;
1354                    }
1355                    i += 1;
1356                }
1357            }
1358            Ok(())
1359        },
1360    )
1361}
1362
1363// ---------------------------------------------------------------------
1364
1365/// Set return data for the current instruction.
1366#[inline(always)]
1367pub fn set_return_data(data: &[u8]) {
1368    crate::return_data::set_return_data(data)
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373    use super::*;
1374
1375    use crate::InstructionAccount;
1376    use hopper_native::{
1377        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
1378    };
1379
1380    fn make_account(address: [u8; 32]) -> (std::vec::Vec<u64>, AccountView<'static>) {
1381        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + 16).div_ceil(8)];
1382        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
1383        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
1384        unsafe {
1385            raw.write(RuntimeAccount {
1386                borrow_state: NOT_BORROWED,
1387                is_signer: 0,
1388                is_writable: 1,
1389                executable: 0,
1390                resize_delta: 0,
1391                address: NativeAddress::new_from_array(address),
1392                owner: NativeAddress::new_from_array([9; 32]),
1393                lamports: 1,
1394                data_len: 16,
1395            });
1396        }
1397        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
1398        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
1399        (backing, AccountView::from_backend(backend))
1400    }
1401
1402    #[test]
1403    fn duplicate_writable_accounts_are_rejected_before_cpi() {
1404        let (_first_backing, first) = make_account([3; 32]);
1405        let (_second_backing, second) = make_account([3; 32]);
1406
1407        let instruction_accounts = [
1408            InstructionAccount::writable(first.address()),
1409            InstructionAccount::writable(second.address()),
1410        ];
1411        let program_id = Address::new_from_array([7; 32]);
1412        let instruction = InstructionView {
1413            program_id: &program_id,
1414            data: &[0u8],
1415            accounts: &instruction_accounts,
1416        };
1417
1418        let err = validate_no_duplicate_writable(&instruction, &[&first, &second]).unwrap_err();
1419        assert_eq!(err, ProgramError::AccountBorrowFailed);
1420    }
1421
1422    // -- borrow_checked tier ------------------------------------------
1423
1424    #[test]
1425    fn borrow_checked_rejects_live_mutable_data_borrow() {
1426        let (_backing, account) = make_account([21; 32]);
1427        let metas = [InstructionAccount::writable(account.address())];
1428        let program_id = Address::new_from_array([7; 32]);
1429        let instruction = InstructionView {
1430            program_id: &program_id,
1431            data: &[0u8],
1432            accounts: &metas,
1433        };
1434
1435        let guard = account.try_borrow_mut().unwrap();
1436        let err = invoke_borrow_checked::<1>(&instruction, &[&account]).unwrap_err();
1437        assert_eq!(err, ProgramError::AccountBorrowFailed);
1438        drop(guard);
1439    }
1440
1441    #[test]
1442    fn borrow_checked_succeeds_after_borrow_release() {
1443        let (_backing, account) = make_account([22; 32]);
1444        let metas = [InstructionAccount::writable(account.address())];
1445        let program_id = Address::new_from_array([7; 32]);
1446        let instruction = InstructionView {
1447            program_id: &program_id,
1448            data: &[0u8],
1449            accounts: &metas,
1450        };
1451
1452        let guard = account.try_borrow_mut().unwrap();
1453        assert!(invoke_borrow_checked::<1>(&instruction, &[&account]).is_err());
1454        drop(guard);
1455
1456        // Off-chain the syscall is a no-op, so Ok(()) here proves the
1457        // borrow validation passed once the guard was released.
1458        invoke_borrow_checked::<1>(&instruction, &[&account]).unwrap();
1459    }
1460
1461    #[test]
1462    fn borrow_checked_permits_duplicate_writable_metas_unlike_default_tier() {
1463        let (_first_backing, first) = make_account([23; 32]);
1464        let (_second_backing, second) = make_account([23; 32]);
1465
1466        let metas = [
1467            InstructionAccount::writable(first.address()),
1468            InstructionAccount::writable(second.address()),
1469        ];
1470        let program_id = Address::new_from_array([7; 32]);
1471        let instruction = InstructionView {
1472            program_id: &program_id,
1473            data: &[0u8],
1474            accounts: &metas,
1475        };
1476
1477        // Default tier: duplicate writable metas are rejected, the
1478        // Sealevel double-mutation footgun `validate_no_duplicate_writable`
1479        // exists to guard.
1480        let err = invoke::<2>(&instruction, &[&first, &second]).unwrap_err();
1481        assert_eq!(err, ProgramError::AccountBorrowFailed);
1482
1483        // borrow_checked tier: per-account borrow state ONLY, matching
1484        // what Pinocchio's `invoke` checks. Not rejecting duplicates is
1485        // the documented contract of this tier, callers opt down only
1486        // after `require_unique_writable_accounts` (or a statically
1487        // duplicate-free account shape) has ruled the footgun out.
1488        invoke_borrow_checked::<2>(&instruction, &[&first, &second]).unwrap();
1489    }
1490
1491    #[test]
1492    fn borrow_checked_offchain_noop_path_returns_ok() {
1493        let (_backing, account) = make_account([24; 32]);
1494        let metas = [InstructionAccount::readonly(account.address())];
1495        let program_id = Address::new_from_array([7; 32]);
1496        let instruction = InstructionView {
1497            program_id: &program_id,
1498            data: &[0u8],
1499            accounts: &metas,
1500        };
1501
1502        assert_eq!(
1503            invoke_borrow_checked::<1>(&instruction, &[&account]),
1504            Ok(())
1505        );
1506        assert_eq!(
1507            invoke_signed_borrow_checked::<1>(&instruction, &[&account], &[]),
1508            Ok(())
1509        );
1510    }
1511
1512    // Lamport gate on writable metas.
1513
1514    // Guarded-tier semantics: installs a data-declaring policy, which the
1515    // `unguarded-raw-surfaces` fence refuses at install (covered by its
1516    // own explicit test in that shape).
1517    #[test]
1518    #[cfg(not(feature = "unguarded-raw-surfaces"))]
1519    fn writable_meta_is_refused_unless_both_dimensions_are_declared() {
1520        use crate::write_policy::{
1521            install_lamport_gate, write_policy_violation, WritePolicy, WriteRange,
1522        };
1523
1524        let (_b0, delegable) = make_account([31; 32]);
1525        let (_b1, lamports_only) = make_account([32; 32]);
1526        let (_b2, undeclared) = make_account([33; 32]);
1527        let accounts = [delegable, lamports_only, undeclared];
1528
1529        // Account 0 carries whole-account data + lamports (delegable);
1530        // account 1 lamports only; account 2 nothing.
1531        static P: WritePolicy =
1532            WritePolicy::with_lamports(&[WriteRange::whole_account(0)], &[0, 1]);
1533        let _gate = install_lamport_gate(&accounts, &P);
1534
1535        let program_id = Address::new_from_array([7; 32]);
1536
1537        // Writable meta on the fully declared account: allowed on the
1538        // default AND borrow_checked tiers (off-chain no-op syscall).
1539        let metas0 = [InstructionAccount::writable(accounts[0].address())];
1540        let ix0 = InstructionView {
1541            program_id: &program_id,
1542            data: &[0u8],
1543            accounts: &metas0,
1544        };
1545        invoke::<1>(&ix0, &[&accounts[0]]).unwrap();
1546        invoke_borrow_checked::<1>(&ix0, &[&accounts[0]]).unwrap();
1547
1548        // Lamports-only account: a writable hand-off is unbounded DATA
1549        // delegation too, so it is refused with the indexed policy error.
1550        let metas1 = [InstructionAccount::writable(accounts[1].address())];
1551        let ix1 = InstructionView {
1552            program_id: &program_id,
1553            data: &[0u8],
1554            accounts: &metas1,
1555        };
1556        assert_eq!(
1557            invoke::<1>(&ix1, &[&accounts[1]]).unwrap_err(),
1558            write_policy_violation(1)
1559        );
1560        assert_eq!(
1561            invoke_borrow_checked::<1>(&ix1, &[&accounts[1]]).unwrap_err(),
1562            write_policy_violation(1)
1563        );
1564
1565        // Entirely undeclared account: refused on every safe tier,
1566        // including the deduped path.
1567        let metas2 = [InstructionAccount::writable(accounts[2].address())];
1568        let ix2 = InstructionView {
1569            program_id: &program_id,
1570            data: &[0u8],
1571            accounts: &metas2,
1572        };
1573        assert_eq!(
1574            invoke_signed_deduped::<1>(&ix2, &[&accounts[2]], &[]).unwrap_err(),
1575            write_policy_violation(2)
1576        );
1577
1578        // Read-only metas are never lamport-gated.
1579        let metas_ro = [InstructionAccount::readonly(accounts[2].address())];
1580        let ix_ro = InstructionView {
1581            program_id: &program_id,
1582            data: &[0u8],
1583            accounts: &metas_ro,
1584        };
1585        invoke::<1>(&ix_ro, &[&accounts[2]]).unwrap();
1586    }
1587
1588    // Guarded-tier semantics: installs a data-declaring policy, which the
1589    // `unguarded-raw-surfaces` fence refuses at install (covered by its
1590    // own explicit test in that shape).
1591    #[test]
1592    #[cfg(not(feature = "unguarded-raw-surfaces"))]
1593    fn host_system_transfer_is_gated_through_the_lamport_funnel() {
1594        use crate::write_policy::{
1595            install_lamport_gate, write_policy_violation, WritePolicy, WriteRange,
1596        };
1597
1598        let (_b0, from) = make_account([41; 32]);
1599        let (_b1, to) = make_account([42; 32]);
1600        let accounts = [from, to];
1601
1602        // Both sides declared: the emulated transfer succeeds and the
1603        // balances actually move.
1604        static OPEN: WritePolicy = WritePolicy::with_lamports(
1605            &[WriteRange::whole_account(0), WriteRange::whole_account(1)],
1606            &[0, 1],
1607        );
1608        // Only `from` declared: the transfer must be refused before any
1609        // balance changes.
1610        static HALF: WritePolicy =
1611            WritePolicy::with_lamports(&[WriteRange::whole_account(0)], &[0]);
1612
1613        let system_id = Address::new_from_array([0; 32]);
1614        let mut data = [0u8; 12];
1615        data[0] = 2; // System Transfer tag
1616        data[4..12].copy_from_slice(&1u64.to_le_bytes());
1617        let metas = [
1618            InstructionAccount::writable(accounts[0].address()),
1619            InstructionAccount::writable(accounts[1].address()),
1620        ];
1621        let ix = InstructionView {
1622            program_id: &system_id,
1623            data: &data,
1624            accounts: &metas,
1625        };
1626
1627        {
1628            let _gate = install_lamport_gate(&accounts, &OPEN);
1629            invoke::<2>(&ix, &[&accounts[0], &accounts[1]]).unwrap();
1630            assert_eq!(accounts[0].lamports(), 0);
1631            assert_eq!(accounts[1].lamports(), 2);
1632        }
1633        {
1634            let _gate = install_lamport_gate(&accounts, &HALF);
1635            assert_eq!(
1636                invoke::<2>(&ix, &[&accounts[0], &accounts[1]]).unwrap_err(),
1637                write_policy_violation(1)
1638            );
1639            // Refused before mutation: balances unchanged.
1640            assert_eq!(accounts[0].lamports(), 0);
1641            assert_eq!(accounts[1].lamports(), 2);
1642        }
1643    }
1644
1645    // Guarded-tier semantics: installs a data-declaring policy, which the
1646    // `unguarded-raw-surfaces` fence refuses at install (covered by its
1647    // own explicit test in that shape).
1648    #[test]
1649    #[cfg(not(feature = "unguarded-raw-surfaces"))]
1650    fn host_system_transfer_refusal_leaves_both_balances_untouched() {
1651        use crate::write_policy::{
1652            install_lamport_gate, write_policy_violation, WritePolicy, WriteRange,
1653        };
1654
1655        let (_b0, from) = make_account([43; 32]);
1656        let (_b1, to) = make_account([44; 32]);
1657        let accounts = [from, to];
1658
1659        // Only `from` is declared for lamport mutation.
1660        static HALF: WritePolicy =
1661            WritePolicy::with_lamports(&[WriteRange::whole_account(0)], &[0]);
1662        let _gate = install_lamport_gate(&accounts, &HALF);
1663
1664        let system_id = Address::new_from_array([0; 32]);
1665        let mut data = [0u8; 12];
1666        data[0] = 2; // System Transfer tag
1667        data[4..12].copy_from_slice(&1u64.to_le_bytes());
1668        // `to` is deliberately a READ-ONLY meta: the writable-meta
1669        // delegation gate then never fires for it, so without the
1670        // emulation's own both-sides pre-validation the refusal would
1671        // come from the `set_lamports` funnel *after* `from` was
1672        // already debited, destroying a lamport in host state.
1673        let metas = [
1674            InstructionAccount::writable(accounts[0].address()),
1675            InstructionAccount::readonly(accounts[1].address()),
1676        ];
1677        let ix = InstructionView {
1678            program_id: &system_id,
1679            data: &data,
1680            accounts: &metas,
1681        };
1682
1683        assert_eq!(
1684            invoke_borrow_checked::<2>(&ix, &[&accounts[0], &accounts[1]]).unwrap_err(),
1685            write_policy_violation(1)
1686        );
1687        // Refused BEFORE any mutation: neither side moved (make_account
1688        // seeds each balance with 1 lamport).
1689        assert_eq!(accounts[0].lamports(), 1);
1690        assert_eq!(accounts[1].lamports(), 1);
1691    }
1692
1693    #[test]
1694    fn borrow_checked_requires_enough_account_views() {
1695        let (_first_backing, first) = make_account([25; 32]);
1696        let (_second_backing, second) = make_account([26; 32]);
1697
1698        let metas = [
1699            InstructionAccount::writable(first.address()),
1700            InstructionAccount::writable(second.address()),
1701        ];
1702        let program_id = Address::new_from_array([7; 32]);
1703        let instruction = InstructionView {
1704            program_id: &program_id,
1705            data: &[0u8],
1706            accounts: &metas,
1707        };
1708
1709        let err = invoke_borrow_checked::<1>(&instruction, &[&first]).unwrap_err();
1710        assert_eq!(err, ProgramError::NotEnoughAccountKeys);
1711    }
1712
1713    // -- FUSED-CPI: fused validate+build == prior validate-then-build ------
1714
1715    /// Serialize the built `CpiAccount` scratch to a stable string. The
1716    /// production fused path writes `CpiAccount::from(view)` into each slot;
1717    /// its `Debug` (pointers + flags + lengths) is a faithful fingerprint of
1718    /// the scratch handed to the syscall.
1719    fn scratch_fingerprint(account_views: &[&AccountView<'_>]) -> std::string::String {
1720        let mut s = std::string::String::new();
1721        let mut i = 0;
1722        while i < account_views.len() {
1723            s.push_str(&std::format!(
1724                "[{}]={:?};",
1725                i,
1726                CpiAccount::from(account_views[i])
1727            ));
1728            i += 1;
1729        }
1730        s
1731    }
1732
1733    /// PRE-fusion default tier: validate the *whole* meta list, THEN build
1734    /// the scratch in a second walk. Kept in the test as the byte-for-byte
1735    /// oracle the production fused path must match.
1736    fn reference_split_default(
1737        instruction: &InstructionView<'_, '_, '_, '_>,
1738        account_views: &[&AccountView<'_>],
1739        signers_seeds: &[Signer<'_, '_>],
1740    ) -> Result<std::string::String, ProgramError> {
1741        if account_views.len() < instruction.accounts.len() {
1742            return Err(ProgramError::NotEnoughAccountKeys);
1743        }
1744        let mut i = 0;
1745        while i < instruction.accounts.len() {
1746            let expected = &instruction.accounts[i];
1747            let actual = account_views[i];
1748            if !address_eq(actual.address(), expected.address) {
1749                return Err(ProgramError::InvalidAccountData);
1750            }
1751            if expected.is_signer
1752                && !actual.is_signer()
1753                && !signer_authority_supplied(signers_seeds)
1754            {
1755                return Err(ProgramError::MissingRequiredSignature);
1756            }
1757            if expected.is_writable && !actual.is_writable() {
1758                return Err(ProgramError::Immutable);
1759            }
1760            if expected.is_writable {
1761                actual.check_borrow_mut()?;
1762            } else {
1763                actual.check_borrow()?;
1764            }
1765            i += 1;
1766        }
1767        // Mirrors production: the delegation sweep runs once per CPI
1768        // after the per-meta pass (borrow-before-delegation precedence).
1769        if crate::write_policy::lamport_gate_active() {
1770            let mut m = 0;
1771            while m < instruction.accounts.len() {
1772                if instruction.accounts[m].is_writable {
1773                    crate::write_policy::check_lamport_delegation(account_views[m].address())?;
1774                }
1775                m += 1;
1776            }
1777        }
1778        validate_no_duplicate_writable(instruction, account_views)?;
1779        // Second (build) walk over the FULL view list.
1780        Ok(scratch_fingerprint(account_views))
1781    }
1782
1783    /// The fused default tier reproduced exactly as production `invoke_signed`
1784    /// runs it: interleave per-meta validation with the scratch build, then
1785    /// run the duplicate-writable scan.
1786    fn reference_fused_default(
1787        instruction: &InstructionView<'_, '_, '_, '_>,
1788        account_views: &[&AccountView<'_>],
1789        signers_seeds: &[Signer<'_, '_>],
1790    ) -> Result<std::string::String, ProgramError> {
1791        let metas_len = instruction.accounts.len();
1792        if account_views.len() < metas_len {
1793            return Err(ProgramError::NotEnoughAccountKeys);
1794        }
1795        let mut s = std::string::String::new();
1796        let mut i = 0;
1797        while i < account_views.len() {
1798            let actual = account_views[i];
1799            if i < metas_len {
1800                let expected = &instruction.accounts[i];
1801                if !address_eq(actual.address(), expected.address) {
1802                    return Err(ProgramError::InvalidAccountData);
1803                }
1804                if expected.is_signer
1805                    && !actual.is_signer()
1806                    && !signer_authority_supplied(signers_seeds)
1807                {
1808                    return Err(ProgramError::MissingRequiredSignature);
1809                }
1810                if expected.is_writable && !actual.is_writable() {
1811                    return Err(ProgramError::Immutable);
1812                }
1813                if expected.is_writable {
1814                    actual.check_borrow_mut()?;
1815                } else {
1816                    actual.check_borrow()?;
1817                }
1818            }
1819            s.push_str(&std::format!("[{}]={:?};", i, CpiAccount::from(actual)));
1820            i += 1;
1821        }
1822        // Mirrors production's once-per-CPI delegation sweep placement.
1823        if crate::write_policy::lamport_gate_active() {
1824            let mut m = 0;
1825            while m < metas_len {
1826                if instruction.accounts[m].is_writable {
1827                    crate::write_policy::check_lamport_delegation(account_views[m].address())?;
1828                }
1829                m += 1;
1830            }
1831        }
1832        validate_no_duplicate_writable(instruction, account_views)?;
1833        Ok(s)
1834    }
1835
1836    #[test]
1837    fn signed_preflight_defers_pda_derivation_to_the_svm() {
1838        let (_backing, account) = make_account([50; 32]);
1839        let callee = Address::new_from_array([7; 32]);
1840        let metas = [InstructionAccount::readonly_signer(account.address())];
1841        let instruction = InstructionView {
1842            program_id: &callee,
1843            data: &[0u8],
1844            accounts: &metas,
1845        };
1846        let views = [&account];
1847        let seed_bytes = [9u8];
1848        let seeds = [Seed::from(&seed_bytes)];
1849        let signers = [Signer::from(&seeds)];
1850
1851        // The callee id is not the caller id and therefore cannot be used to
1852        // derive the PDA here. Host invocation is a no-op after preflight;
1853        // on SVM the invoke_signed syscall validates the same seed group
1854        // against the actual caller before granting signer privilege.
1855        assert_eq!(invoke_signed(&instruction, &views, &signers), Ok(()));
1856        assert_eq!(
1857            invoke_signed(&instruction, &views, &[]),
1858            Err(ProgramError::MissingRequiredSignature)
1859        );
1860    }
1861
1862    #[test]
1863    fn fused_build_matches_split_build_and_per_tier_errors() {
1864        use crate::write_policy::{install_lamport_gate, write_policy_violation, WritePolicy};
1865
1866        let program_id = Address::new_from_array([7; 32]);
1867
1868        // (1) Valid multi-account CPI (two distinct writable accounts, no
1869        //     gate installed). Fused and split builds must produce the SAME
1870        //     scratch, and production `invoke` must accept it.
1871        {
1872            let (_a, first) = make_account([51; 32]);
1873            let (_b, second) = make_account([52; 32]);
1874            let metas = [
1875                InstructionAccount::writable(first.address()),
1876                InstructionAccount::writable(second.address()),
1877            ];
1878            let ix = InstructionView {
1879                program_id: &program_id,
1880                data: &[0u8],
1881                accounts: &metas,
1882            };
1883            let views: [&AccountView<'_>; 2] = [&first, &second];
1884
1885            let split = reference_split_default(&ix, &views[..], &[]);
1886            let fused = reference_fused_default(&ix, &views[..], &[]);
1887            assert!(split.is_ok());
1888            // Same scratch bytes, and same Result overall.
1889            assert_eq!(split, fused);
1890            // Production fused path accepts the valid CPI (off-chain no-op).
1891            assert_eq!(invoke::<2>(&ix, &views), Ok(()));
1892        }
1893
1894        // (2) Signer-missing meta: a required-signer meta over a non-signer
1895        //     account. Both builds refuse identically, and production too.
1896        {
1897            let (_a, acct) = make_account([53; 32]);
1898            let metas = [InstructionAccount::readonly_signer(acct.address())];
1899            let ix = InstructionView {
1900                program_id: &program_id,
1901                data: &[0u8],
1902                accounts: &metas,
1903            };
1904            let views: [&AccountView<'_>; 1] = [&acct];
1905
1906            let split = reference_split_default(&ix, &views[..], &[]);
1907            let fused = reference_fused_default(&ix, &views[..], &[]);
1908            assert_eq!(split, Err(ProgramError::MissingRequiredSignature));
1909            assert_eq!(split, fused);
1910            assert_eq!(
1911                invoke::<1>(&ix, &views).unwrap_err(),
1912                ProgramError::MissingRequiredSignature
1913            );
1914        }
1915
1916        // (3) Writable-meta lamport-delegation refusal: an installed gate
1917        //     that declares nothing for the account. The refusal must fire on
1918        //     the fused build exactly as on the split build (indexed policy
1919        //     error), and production must surface the same error.
1920        {
1921            let (_a, acct) = make_account([54; 32]);
1922            let accounts = [acct];
1923            static P: WritePolicy = WritePolicy::with_lamports(&[], &[]);
1924            let _gate = install_lamport_gate(&accounts, &P);
1925
1926            let metas = [InstructionAccount::writable(accounts[0].address())];
1927            let ix = InstructionView {
1928                program_id: &program_id,
1929                data: &[0u8],
1930                accounts: &metas,
1931            };
1932            let views: [&AccountView<'_>; 1] = [&accounts[0]];
1933
1934            let split = reference_split_default(&ix, &views[..], &[]);
1935            let fused = reference_fused_default(&ix, &views[..], &[]);
1936            assert_eq!(split, Err(write_policy_violation(0)));
1937            assert_eq!(split, fused);
1938            assert_eq!(
1939                invoke::<1>(&ix, &views).unwrap_err(),
1940                write_policy_violation(0)
1941            );
1942        }
1943
1944        // (4) Deduped (duplicate account) case: two writable metas naming the
1945        //     SAME account. The deduped tier (unchanged by fusion) must still
1946        //     reject the double-mutation footgun.
1947        {
1948            let (_a, acct) = make_account([55; 32]);
1949            let metas = [
1950                InstructionAccount::writable(acct.address()),
1951                InstructionAccount::writable(acct.address()),
1952            ];
1953            let ix = InstructionView {
1954                program_id: &program_id,
1955                data: &[0u8],
1956                accounts: &metas,
1957            };
1958            // A single deduped info backs both metas.
1959            assert_eq!(
1960                invoke_signed_deduped::<1>(&ix, &[&acct], &[]).unwrap_err(),
1961                ProgramError::AccountBorrowFailed
1962            );
1963        }
1964    }
1965}
1966
1967#[cfg(test)]
1968#[path = "cpi_dedup_tests.rs"]
1969mod dedup_tests;