From e317af5504ff152ad65fdb6ca540f6c619a56077 Mon Sep 17 00:00:00 2001 From: franRappazzini Date: Mon, 17 Aug 2026 23:27:03 -0300 Subject: [PATCH 1/2] feat(lang-v2): support Sysvar for introspection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Sysvar` only wrapped syscall-backed sysvars, so instruction introspection was unreachable from `#[derive(Accounts)]`. Split the wrapper's bound in two: `SysvarId` still supplies the well-known address, and a new `SysvarLoad` says how to read the value. `Clock` / `Rent` read from `sol_get_sysvar`; `Instructions` has no syscall and borrows the account data instead, holding a `'static` `Ref` guard for the wrapper's lifetime as `SerializedAccount::load` does. No proc-macro change needed — v2 dispatches account types by trait. Covered by wrapper unit tests over a synthetic sysvar blob, a Miri witness for the transmute, and LiteSVM tests on real SBF --- lang-v2/README.md | 1 + lang-v2/src/accounts/mod.rs | 2 +- lang-v2/src/accounts/sysvar.rs | 163 ++++++++++++++---- lang-v2/src/prelude.rs | 4 +- lang-v2/tests/account_wrapper_checks.rs | 214 +++++++++++++++++++++++- lang-v2/tests/macro_diagnostics.rs | 8 +- lang-v2/tests/miri_wrapper_accounts.rs | 57 ++++++- tests-v2/programs/accounts/src/lib.rs | 45 +++++ tests-v2/tests/accounts.rs | 31 ++++ tests-v2/tests/sysvar_idl.rs | 22 ++- 10 files changed, 510 insertions(+), 37 deletions(-) diff --git a/lang-v2/README.md b/lang-v2/README.md index d8b8bdca01..cb8e9f1438 100644 --- a/lang-v2/README.md +++ b/lang-v2/README.md @@ -108,6 +108,7 @@ None of these carry an `'info` lifetime — pinocchio's account model is static- | `SystemAccount` | System-owned account. Owner check only. (v1 compat) | | `UncheckedAccount` | Escape hatch. No validation. No generic `close` support. (v1 compat) | | `Sysvar` | `Sysvar`, `Sysvar`. Prefer `Clock::get()` / `Rent::get()` syscalls where possible. (v1 compat) | +| `Sysvar` | Instruction introspection. No syscall exists for this sysvar, so the account must be passed in the transaction; the wrapper reads its data and derefs to pinocchio's `Instructions`. | ## CPI Semantics diff --git a/lang-v2/src/accounts/mod.rs b/lang-v2/src/accounts/mod.rs index d0715a591f..6341f2bba5 100644 --- a/lang-v2/src/accounts/mod.rs +++ b/lang-v2/src/accounts/mod.rs @@ -20,7 +20,7 @@ pub use { slab::{HeaderOnly, Slab}, slab_hooks::{SlabInit, SlabSchema}, system_account::SystemAccount, - sysvar::{Sysvar, SysvarId}, + sysvar::{Instructions, Sysvar, SysvarId, SysvarLoad}, unchecked_account::UncheckedAccount, }; diff --git a/lang-v2/src/accounts/sysvar.rs b/lang-v2/src/accounts/sysvar.rs index cf1bfa510c..51cbbeb342 100644 --- a/lang-v2/src/accounts/sysvar.rs +++ b/lang-v2/src/accounts/sysvar.rs @@ -1,7 +1,11 @@ use { crate::{require, AnchorAccount}, - core::{marker::PhantomData, ops::Deref}, - pinocchio::{account::AccountView, address::Address, sysvars::Sysvar as PinocchioSysvar}, + core::ops::Deref, + pinocchio::{ + account::{AccountView, Ref}, + address::Address, + sysvars::Sysvar as PinocchioSysvar, + }, solana_program_error::ProgramError, }; @@ -19,28 +23,136 @@ pub trait SysvarId { const IDL_ADDRESS: &'static str = ""; } -impl SysvarId for pinocchio::sysvars::clock::Clock { - const SYSVAR_ID: Address = pinocchio::sysvars::clock::CLOCK_ID; - const IDL_ADDRESS: &'static str = "SysvarC1ock11111111111111111111111111111111"; +/// How [`Sysvar`] obtains `T`'s value once the account address checks out. +/// +/// Split out of [`SysvarId`] because the two sysvar families read differently: +/// `Clock` / `Rent` come from the `sol_get_sysvar` syscall and never touch +/// account data, while `Instructions` has no syscall at all and must be read +/// out of the account's data buffer. +/// +/// A blanket `impl SysvarLoad for T` is not possible: it +/// would overlap the [`Instructions`] impl, and rustc cannot prove +/// `Instructions: !PinocchioSysvar` (negative reasoning about a foreign trait +/// on a foreign type). Syscall-backed sysvars therefore get an explicit impl +/// each, via `impl_syscall_sysvar!`. +#[diagnostic::on_unimplemented( + message = "`{Self}` is not a sysvar Anchor can load", + label = "unsupported sysvar", + note = "supported: `Clock`, `Rent`, `Instructions`. A sysvar needs both `SysvarId` (its \ + well-known address) and `SysvarLoad` (how to read its value)." +)] +pub trait SysvarLoad: SysvarId + Sized { + /// Read the sysvar's value. + /// + /// [`Sysvar::load`] has already verified that + /// `view.address() == Self::SYSVAR_ID` before this runs, so implementors + /// may skip any address check of their own. + /// + /// [`Sysvar::load`]: AnchorAccount::load + fn read(view: &AccountView) -> Result; } +/// Registers a sysvar that the runtime exposes through `sol_get_sysvar`. +/// +/// `read` ignores the account view entirely — the value comes from the +/// syscall, so the account's data is never deserialized. +macro_rules! impl_syscall_sysvar { + ($ty:ty, $id:expr, $idl:literal) => { + impl SysvarId for $ty { + const SYSVAR_ID: Address = $id; + const IDL_ADDRESS: &'static str = $idl; + } + + impl SysvarLoad for $ty { + #[inline(always)] + fn read(_view: &AccountView) -> Result { + <$ty as PinocchioSysvar>::get().map_err(|_| ProgramError::UnsupportedSysvar) + } + } + }; +} + +impl_syscall_sysvar!( + pinocchio::sysvars::clock::Clock, + pinocchio::sysvars::clock::CLOCK_ID, + "SysvarC1ock11111111111111111111111111111111" +); + +impl_syscall_sysvar!( + pinocchio::sysvars::rent::Rent, + pinocchio::sysvars::rent::RENT_ID, + "SysvarRent111111111111111111111111111111111" +); + +// FIXME: Add `EpochSchedule`: https://github.com/anza-xyz/pinocchio/pull/411 + +// Deliberately generic over `T`: the address and IDL string are the same for +// every instantiation, and `tests-v2/tests/sysvar_idl.rs` asserts on +// `Instructions<&'static [u8]>`. Only the `Instructions` alias below — the one +// instantiation that can outlive `load` — gets a `SysvarLoad` impl. impl> SysvarId for pinocchio::sysvars::instructions::Instructions { const SYSVAR_ID: Address = pinocchio::sysvars::instructions::INSTRUCTIONS_ID; const IDL_ADDRESS: &'static str = "Sysvar1nstructions1111111111111111111111111"; } -impl SysvarId for pinocchio::sysvars::rent::Rent { - const SYSVAR_ID: Address = pinocchio::sysvars::rent::RENT_ID; - const IDL_ADDRESS: &'static str = "SysvarRent111111111111111111111111111111111"; -} +/// The instructions sysvar — the entry point for instruction introspection. +/// +/// Instantiates pinocchio's `Instructions` at the one `T` that can outlive +/// [`AnchorAccount::load`]: a `'static` borrow guard over the account's data. +/// +/// Use it as `Sysvar` in a `#[derive(Accounts)]` struct, then +/// reach the introspection methods through the wrapper's `Deref`: +/// +/// ```ignore +/// #[derive(Accounts)] +/// pub struct Introspect { +/// pub instructions: Sysvar, +/// } +/// +/// let previous = ctx.accounts.instructions.get_instruction_relative(-1)?; +/// let caller = previous.get_program_id(); +/// ``` +/// +/// Unlike `Clock` / `Rent`, there is no syscall for this sysvar: the account +/// must be passed in the transaction, and the wrapper holds a shared borrow of +/// its data for as long as it is alive. +pub type Instructions = pinocchio::sysvars::instructions::Instructions>; -// FIXME: Add `EpochSchedule`: https://github.com/anza-xyz/pinocchio/pull/411 +impl SysvarLoad for Instructions { + #[inline(always)] + fn read(view: &AccountView) -> Result { + // A well-formed instructions sysvar is at minimum `[u16 num = 0]` + + // `[u16 current_index]`. The address check in `Sysvar::load` already + // guarantees this is the genuine runtime-populated sysvar; the guard + // only stops a hand-rolled mock view from underflowing the pointer + // arithmetic in `load_current_index`. + #[cfg(feature = "guardrails")] + require!(view.data_len() >= 4, ProgramError::AccountDataTooSmall); + + let data_ref = view.try_borrow()?; + // SAFETY: the AccountView's data pointer is valid for the entire + // instruction (Solana runtime guarantee), and `Ref` stores raw pointers + // into runtime memory rather than into `view` — so moving `view` into + // `Sysvar` afterwards does not invalidate it. Holding the guard + // prevents subsequent mutable borrows of the same account. Same + // reasoning as `SerializedAccount::load`. + let guard: Ref<'static, [u8]> = unsafe { core::mem::transmute(data_ref) }; + // SAFETY: `Sysvar::load` already verified the address against + // `INSTRUCTIONS_ID`, which is exactly what pinocchio's + // `TryFrom<&AccountView>` checks. Going through `new_unchecked` avoids + // redoing that compare and lets us transmute the `Ref` alone rather + // than the whole `Instructions<_>`. + Ok(unsafe { Self::new_unchecked(guard) }) + } +} /// Account wrapper for sysvars. /// -/// Validates that the passed account address matches `T::SYSVAR_ID`, -/// then reads the sysvar directly from the runtime via pinocchio's -/// `Sysvar::get()` syscall (account data is not deserialized). +/// Validates that the passed account address matches `T::SYSVAR_ID`, then +/// defers to [`SysvarLoad::read`] for the value. For `Clock` / `Rent` that +/// reads directly from the runtime via pinocchio's `Sysvar::get()` syscall and +/// never touches account data; for [`Instructions`] it borrows the account's +/// data and holds that shared borrow for the wrapper's lifetime. /// /// ## `#[account(address = X @ MyErr)]` does NOT surface `MyErr` /// @@ -49,13 +161,12 @@ impl SysvarId for pinocchio::sysvars::rent::Rent { /// `ProgramError::InvalidArgument`, never as the user's `@ MyErr` code. /// If you need a custom error code on a sysvar address mismatch, use /// `UncheckedAccount` and add `address = X @ MyErr` in the derive. -pub struct Sysvar { +pub struct Sysvar { view: AccountView, data: T, - _phantom: PhantomData, } -impl AnchorAccount for Sysvar { +impl AnchorAccount for Sysvar { type Data = T; fn load(view: AccountView) -> Result { @@ -65,14 +176,8 @@ impl AnchorAccount for Sysvar { crate::address_eq(view.address(), &id), ProgramError::InvalidArgument ); - // Use pinocchio's Sysvar::get() which reads directly from the runtime - // via syscall, avoiding the need to deserialize from account data. - let data = T::get().map_err(|_| ProgramError::UnsupportedSysvar)?; - Ok(Self { - view, - data, - _phantom: PhantomData, - }) + let data = T::read(&view)?; + Ok(Self { view, data }) } fn account(&self) -> &AccountView { @@ -80,27 +185,27 @@ impl AnchorAccount for Sysvar { } } -impl Deref for Sysvar { +impl Deref for Sysvar { type Target = T; fn deref(&self) -> &T { &self.data } } -impl AsRef for Sysvar { +impl AsRef for Sysvar { fn as_ref(&self) -> &AccountView { &self.view } } -impl crate::ToCpiHandle for Sysvar { +impl crate::ToCpiHandle for Sysvar { #[inline(always)] fn to_cpi_handle(&self) -> crate::CpiHandle<'_> { crate::AnchorAccount::cpi_handle(self) } } -impl crate::ToCpiHandleMut for Sysvar { +impl crate::ToCpiHandleMut for Sysvar { #[inline(always)] fn try_to_cpi_handle_mut( &mut self, @@ -110,7 +215,7 @@ impl crate::ToCpiHandleMut for Sysvar { } #[doc(hidden)] -impl crate::IdlAccountType for Sysvar { +impl crate::IdlAccountType for Sysvar { const __IDL_ADDRESS: Option<&'static str> = if T::IDL_ADDRESS.is_empty() { None } else { diff --git a/lang-v2/src/prelude.rs b/lang-v2/src/prelude.rs index 6aec1ea2d2..ba4e259676 100644 --- a/lang-v2/src/prelude.rs +++ b/lang-v2/src/prelude.rs @@ -9,8 +9,8 @@ pub use crate::{ account, // Account types accounts::{ - Account, BorshAccount, Interface, InterfaceAccount, Program, Signer, SlabSchema, - SystemAccount, Sysvar, SysvarId, UncheckedAccount, + Account, BorshAccount, Instructions, Interface, InterfaceAccount, Program, Signer, + SlabSchema, SystemAccount, Sysvar, SysvarId, SysvarLoad, UncheckedAccount, }, constant, create_account, diff --git a/lang-v2/tests/account_wrapper_checks.rs b/lang-v2/tests/account_wrapper_checks.rs index ea1f1bc4ed..8acba9cf3e 100644 --- a/lang-v2/tests/account_wrapper_checks.rs +++ b/lang-v2/tests/account_wrapper_checks.rs @@ -16,8 +16,8 @@ use { anchor_lang::{ accounts::{ - Account, BorshAccount, Interface, Program, Signer, SlabSchema, SystemAccount, Sysvar, - UncheckedAccount, + Account, BorshAccount, Instructions, Interface, Program, Signer, SlabSchema, + SystemAccount, Sysvar, UncheckedAccount, }, programs::{System, Token}, testing::AccountBuffer, @@ -389,6 +389,216 @@ fn sysvar_load_rejects_wrong_address() { assert_eq!(err, ProgramError::InvalidArgument); } +// -- Sysvar ----------------------------------------------- +// +// Unlike `Clock` / `Rent`, this sysvar has no `sol_get_sysvar` syscall — the +// value is read out of the account's data buffer. These tests build a +// synthetic sysvar blob matching the runtime's serialization so the pointer +// arithmetic in pinocchio's `Instructions` accessors is exercised for real. +// +// Layout (agave's `construct_instructions_data`): +// +// sysvar := [u16 num_ix] [u16 offset; num_ix] [u16 current_index] +// ix blob := [u16 num_accounts] [{u8 flags, [u8; 32] key} * num_accounts] +// [[u8; 32] program_id] [u16 data_len] [data] +// flags := 0b01 signer | 0b10 writable +// +// `current_index` lives in the *last two bytes*, so `data_len` in the account +// header must match the blob length exactly. + +struct TestIx { + program_id: [u8; 32], + /// `(key, is_signer, is_writable)` + accounts: Vec<([u8; 32], bool, bool)>, + data: Vec, +} + +fn build_instructions_sysvar(ixs: &[TestIx], current_index: u16) -> Vec { + let header_len = 2 + 2 * ixs.len(); + let mut offsets = Vec::with_capacity(ixs.len()); + let mut blobs = Vec::with_capacity(ixs.len()); + let mut cursor = header_len; + + for ix in ixs { + offsets.push(cursor as u16); + let mut blob = Vec::new(); + blob.extend_from_slice(&(ix.accounts.len() as u16).to_le_bytes()); + for (key, is_signer, is_writable) in &ix.accounts { + let mut flags = 0u8; + if *is_signer { + flags |= 0b01; + } + if *is_writable { + flags |= 0b10; + } + blob.push(flags); + blob.extend_from_slice(key); + } + blob.extend_from_slice(&ix.program_id); + blob.extend_from_slice(&(ix.data.len() as u16).to_le_bytes()); + blob.extend_from_slice(&ix.data); + cursor += blob.len(); + blobs.push(blob); + } + + let mut out = Vec::with_capacity(cursor + 2); + out.extend_from_slice(&(ixs.len() as u16).to_le_bytes()); + for offset in &offsets { + out.extend_from_slice(&offset.to_le_bytes()); + } + for blob in &blobs { + out.extend_from_slice(blob); + } + out.extend_from_slice(¤t_index.to_le_bytes()); + out +} + +fn instructions_sysvar_id() -> [u8; 32] { + pinocchio::sysvars::instructions::INSTRUCTIONS_ID.to_bytes() +} + +fn sample_instructions() -> Vec { + vec![ + TestIx { + program_id: [0xAA; 32], + accounts: vec![([0x11; 32], true, true), ([0x22; 32], false, false)], + data: vec![1, 2, 3, 4], + }, + TestIx { + program_id: PROGRAM_ID, + accounts: vec![([0x33; 32], false, true)], + data: vec![9], + }, + ] +} + +#[test] +fn sysvar_instructions_load_rejects_wrong_address() { + // The address gate must reject before the data borrow, exactly like + // `Sysvar` — a non-instructions account never reaches + // `SysvarLoad::read`. + let buf = AccountBuffer::<512>::new(); + let blob = build_instructions_sysvar(&sample_instructions(), 0); + buf.init([0x01; 32], [0u8; 32], blob.len(), false, false, false); + buf.write_data(&blob); + let view = unsafe { buf.view() }; + let err = expect_err(Sysvar::::load(view)); + assert_eq!(err, ProgramError::InvalidArgument); +} + +#[test] +fn sysvar_instructions_reads_synthetic_blob() { + let buf = AccountBuffer::<512>::new(); + let blob = build_instructions_sysvar(&sample_instructions(), 1); + buf.init( + instructions_sysvar_id(), + [0u8; 32], + blob.len(), + false, + false, + false, + ); + buf.write_data(&blob); + let view = unsafe { buf.view() }; + let sysvar = Sysvar::::load(view).unwrap(); + + assert_eq!(sysvar.num_instructions(), 2); + assert_eq!(sysvar.load_current_index(), 1); + + let first = sysvar.load_instruction_at(0).unwrap(); + assert_eq!(first.get_program_id().to_bytes(), [0xAA; 32]); + assert_eq!(first.get_instruction_data(), &[1, 2, 3, 4]); + assert_eq!(first.num_account_metas(), 2); + + let signer = first.get_instruction_account_at(0).unwrap(); + assert_eq!(signer.key.to_bytes(), [0x11; 32]); + assert!(signer.is_signer()); + assert!(signer.is_writable()); + + let readonly = first.get_instruction_account_at(1).unwrap(); + assert_eq!(readonly.key.to_bytes(), [0x22; 32]); + assert!(!readonly.is_signer()); + assert!(!readonly.is_writable()); + + // `current_index` is 1, so relative 0 is the second instruction and + // relative -1 walks back to the first. + let current = sysvar.get_instruction_relative(0).unwrap(); + assert_eq!(current.get_program_id().to_bytes(), PROGRAM_ID); + assert_eq!(current.get_instruction_data(), &[9]); + + let previous = sysvar.get_instruction_relative(-1).unwrap(); + assert_eq!(previous.get_program_id().to_bytes(), [0xAA; 32]); +} + +#[test] +fn sysvar_instructions_rejects_out_of_range_index() { + let buf = AccountBuffer::<512>::new(); + let blob = build_instructions_sysvar(&sample_instructions(), 0); + buf.init( + instructions_sysvar_id(), + [0u8; 32], + blob.len(), + false, + false, + false, + ); + buf.write_data(&blob); + let view = unsafe { buf.view() }; + let sysvar = Sysvar::::load(view).unwrap(); + + assert_eq!( + expect_err(sysvar.load_instruction_at(2)), + ProgramError::InvalidInstructionData + ); + // `current_index` is 0, so there is no preceding instruction. + assert_eq!( + expect_err(sysvar.get_instruction_relative(-1)), + ProgramError::InvalidInstructionData + ); +} + +#[test] +fn sysvar_instructions_holds_a_shared_borrow_not_an_exclusive_one() { + // The wrapper keeps a `Ref` alive for its whole lifetime. That must stay a + // *shared* borrow: `program.rs` calls `check_borrow()` before a readonly + // CPI, and an exclusive marker would break passing the sysvar through. + let buf = AccountBuffer::<512>::new(); + let blob = build_instructions_sysvar(&sample_instructions(), 0); + buf.init( + instructions_sysvar_id(), + [0u8; 32], + blob.len(), + false, + false, + false, + ); + buf.write_data(&blob); + let view = unsafe { buf.view() }; + let sysvar = Sysvar::::load(view).unwrap(); + + assert!(sysvar.account().check_borrow().is_ok()); + assert!(sysvar.account().check_borrow_mut().is_err()); + + // Dropping the wrapper releases the guard. + drop(sysvar); + let view = unsafe { buf.view() }; + assert!(view.check_borrow_mut().is_ok()); +} + +#[cfg(feature = "guardrails")] +#[test] +fn sysvar_instructions_rejects_undersized_data() { + // A genuine sysvar is always at least `[u16 num][u16 current_index]`. The + // guardrails check stops a truncated mock from underflowing the pointer + // arithmetic in `load_current_index`. + let buf = AccountBuffer::<128>::new(); + buf.init(instructions_sysvar_id(), [0u8; 32], 2, false, false, false); + buf.write_data(&[0u8; 2]); + let view = unsafe { buf.view() }; + let err = expect_err(Sysvar::::load(view)); + assert_eq!(err, ProgramError::AccountDataTooSmall); +} + // -- Account / Slab ---------------------------------- #[test] diff --git a/lang-v2/tests/macro_diagnostics.rs b/lang-v2/tests/macro_diagnostics.rs index 396d016708..2d67188afe 100644 --- a/lang-v2/tests/macro_diagnostics.rs +++ b/lang-v2/tests/macro_diagnostics.rs @@ -682,7 +682,13 @@ fn check() { assert_anchor_account::>(); } "#, - &["SlotHashes", "SysvarId"], + // `SysvarLoad` is the bound `Sysvar` actually requires; the + // `on_unimplemented` note names `SysvarId` alongside it. + &[ + "SlotHashes", + "SysvarLoad", + "is not a sysvar Anchor can load", + ], ); } diff --git a/lang-v2/tests/miri_wrapper_accounts.rs b/lang-v2/tests/miri_wrapper_accounts.rs index f43bc008b3..aad3152c2a 100644 --- a/lang-v2/tests/miri_wrapper_accounts.rs +++ b/lang-v2/tests/miri_wrapper_accounts.rs @@ -10,7 +10,7 @@ use anchor_lang::testing::AccountBuffer; use anchor_lang::{ - accounts::{SystemAccount, UncheckedAccount}, + accounts::{Instructions, SystemAccount, Sysvar, UncheckedAccount}, prelude::{Program, Signer}, programs::{System, Token}, AnchorAccount, @@ -133,3 +133,58 @@ fn distinct_wrapper_types_on_distinct_buffers() { assert_ne!(sys.address().to_bytes(), unchecked.address().to_bytes()); } + +// -- Sysvar -------------------------------------------- +// +// The one wrapper here that stores a `'static`-transmuted borrow guard +// alongside its `AccountView` (`SysvarLoad for Instructions` in +// `accounts/sysvar.rs`). The claim under test: `Ref` holds raw pointers into +// the account's runtime memory, not into the `AccountView`, so moving the view +// into `Sysvar` after taking the borrow keeps the guard's provenance valid +// — and dropping the wrapper releases the borrow flag exactly once. + +/// Minimal well-formed sysvar blob: one instruction owned by `PROGRAM_ID` +/// with a single readonly account and one data byte, `current_index = 0`. +fn one_instruction_sysvar() -> [u8; 43] { + let mut blob = [0u8; 43]; + blob[0..2].copy_from_slice(&1u16.to_le_bytes()); // num_instructions + blob[2..4].copy_from_slice(&4u16.to_le_bytes()); // offset of ix 0 + blob[4..6].copy_from_slice(&1u16.to_le_bytes()); // num_accounts + blob[6] = 0b10; // writable, not signer + blob[7..39].copy_from_slice(&[0x77; 32]); // account key + blob[39..41].copy_from_slice(&PROGRAM_ID[0..2]); // program id (truncated) + blob[41..43].copy_from_slice(&0u16.to_le_bytes()); // current_index + blob +} + +#[test] +fn sysvar_instructions_guard_survives_the_view_move() { + let buf = AccountBuffer::<256>::new(); + let blob = one_instruction_sysvar(); + buf.init( + pinocchio::sysvars::instructions::INSTRUCTIONS_ID.to_bytes(), + [0; 32], + blob.len(), + false, + false, + false, + ); + buf.write_data(&blob); + + let view = unsafe { buf.view() }; + let sysvar = Sysvar::::load(view).unwrap(); + + // Reading through the transmuted guard must stay in-bounds of the + // provenance established by `try_borrow()`. + assert_eq!(sysvar.num_instructions(), 1); + assert_eq!(sysvar.load_current_index(), 0); + + // The guard is shared, not exclusive. + assert!(sysvar.account().check_borrow().is_ok()); + assert!(sysvar.account().check_borrow_mut().is_err()); + + // ... and dropping releases it, leaving the borrow state where it started. + drop(sysvar); + let view = unsafe { buf.view() }; + assert!(view.check_borrow_mut().is_ok()); +} diff --git a/tests-v2/programs/accounts/src/lib.rs b/tests-v2/programs/accounts/src/lib.rs index bda4e99447..92f998c37f 100644 --- a/tests-v2/programs/accounts/src/lib.rs +++ b/tests-v2/programs/accounts/src/lib.rs @@ -129,6 +129,46 @@ pub mod accounts_test { Ok(()) } + /// Reads the Instructions sysvar for instruction introspection. + /// + /// Unlike Clock/Rent there is no syscall for this one — the value comes + /// from the account's data, which the wrapper borrows on load. + #[discrim = 41] + pub fn read_instructions(ctx: &mut Context) -> Result<()> { + let instructions = &*ctx.accounts.instructions; + + let current_index = instructions.load_current_index() as usize; + require!( + current_index < instructions.num_instructions(), + ProgramError::InvalidAccountData + ); + + // Relative 0 is this very instruction, so its program id must be ours + // and its data must start with this handler's discriminant. + let current = instructions.get_instruction_relative(0)?; + require!( + anchor_lang::address_eq(current.get_program_id(), &ID), + ProgramError::IncorrectProgramId + ); + require!( + current.get_instruction_data().first() == Some(&41), + ProgramError::InvalidInstructionData + ); + + // The sysvar account itself is the only account on this instruction. + let meta = current.get_instruction_account_at(0)?; + require!( + anchor_lang::address_eq( + &meta.key, + &pinocchio::sysvars::instructions::INSTRUCTIONS_ID + ), + ProgramError::InvalidAccountData + ); + require!(!meta.is_writable(), ProgramError::InvalidAccountData); + + Ok(()) + } + /// Takes a `SystemAccount`, which validates that the account is owned by /// the System program. #[discrim = 7] @@ -930,6 +970,11 @@ pub struct ReadRent { pub rent: Sysvar, } +#[derive(Accounts)] +pub struct ReadInstructions { + pub instructions: Sysvar, +} + #[derive(Accounts)] pub struct CheckSystem { pub wallet: SystemAccount, diff --git a/tests-v2/tests/accounts.rs b/tests-v2/tests/accounts.rs index 9ac7eef568..be8d86dce7 100644 --- a/tests-v2/tests/accounts.rs +++ b/tests-v2/tests/accounts.rs @@ -35,6 +35,12 @@ fn rent_sysvar_id() -> Pubkey { .unwrap() } +fn instructions_sysvar_id() -> Pubkey { + "Sysvar1nstructions1111111111111111111111111" + .parse() + .unwrap() +} + fn recent_blockhashes_sysvar_id() -> Pubkey { "SysvarRecentB1ockHashes11111111111111111111" .parse() @@ -594,6 +600,31 @@ fn read_clock_rejects_wrong_sysvar() { ); } +#[test] +fn read_instructions_introspects_the_current_instruction() { + let (mut svm, payer) = setup(); + // The handler asserts, from inside the program, that relative index 0 is + // this very instruction: right program id, discriminant 41 in the data, + // and the sysvar itself as the sole readonly account meta. + let metas = vec![AccountMeta::new_readonly(instructions_sysvar_id(), false)]; + send_instruction(&mut svm, program_id(), vec![41], metas, &payer, &[]) + .expect("read_instructions should succeed"); +} + +#[test] +fn read_instructions_rejects_wrong_sysvar() { + let (mut svm, payer) = setup(); + // Passing rent instead of the instructions sysvar trips the `T::SYSVAR_ID` + // compare in `Sysvar::load`, before `SysvarLoad::read` borrows any data. + assert_single_account_instruction_rejects( + &mut svm, + &payer, + 41, + rent_sysvar_id(), + "wrong sysvar should be rejected for instructions", + ); +} + #[test] fn read_rent_succeeds_and_has_positive_minimum_balance() { let (mut svm, payer) = setup(); diff --git a/tests-v2/tests/sysvar_idl.rs b/tests-v2/tests/sysvar_idl.rs index e5146558b8..a532687210 100644 --- a/tests-v2/tests/sysvar_idl.rs +++ b/tests-v2/tests/sysvar_idl.rs @@ -1,5 +1,5 @@ use { - anchor_lang::accounts::SysvarId, + anchor_lang::{accounts::SysvarId, IdlAccountType}, pinocchio::sysvars::{clock::Clock, instructions::Instructions, rent::Rent}, }; @@ -19,6 +19,26 @@ fn sysvar_idl_addresses_match_well_known_accounts() { ); } +#[test] +fn sysvar_wrappers_surface_their_idl_address() { + // End of the chain the IDL builder actually reads: + // `SysvarId::IDL_ADDRESS` -> `IdlAccountType::__IDL_ADDRESS`. + use anchor_lang::accounts::{Instructions as AnchorInstructions, Sysvar}; + + assert_eq!( + as IdlAccountType>::__IDL_ADDRESS, + Some("SysvarC1ock11111111111111111111111111111111") + ); + assert_eq!( + as IdlAccountType>::__IDL_ADDRESS, + Some("SysvarRent111111111111111111111111111111111") + ); + assert_eq!( + as IdlAccountType>::__IDL_ADDRESS, + Some("Sysvar1nstructions1111111111111111111111111") + ); +} + #[test] fn instructions_sysvar_id_is_not_the_system_program() { assert_eq!( From d5cbb2aee88428bd3bec7fd919e765cb14119c49 Mon Sep 17 00:00:00 2001 From: franRappazzini Date: Thu, 20 Aug 2026 15:51:12 -0300 Subject: [PATCH 2/2] refactor(lang-v2/sysvar): rename `Instructions` and update diagnostic note - Renamed the `Instructions` type alias to `SysvarInstructions` to prevent namespace collisions in the prelude for downstream programs. - Removed the hardcoded list of supported sysvars from the `SysvarLoad` trait's diagnostic note to reduce maintenance overhead. --- lang-v2/src/accounts/mod.rs | 2 +- lang-v2/src/accounts/sysvar.rs | 28 ++++++++++++++----------- lang-v2/src/prelude.rs | 4 ++-- lang-v2/tests/account_wrapper_checks.rs | 20 +++++++++--------- tests-v2/programs/accounts/src/lib.rs | 2 +- tests-v2/tests/sysvar_idl.rs | 2 +- 6 files changed, 31 insertions(+), 27 deletions(-) diff --git a/lang-v2/src/accounts/mod.rs b/lang-v2/src/accounts/mod.rs index 6341f2bba5..178549df5f 100644 --- a/lang-v2/src/accounts/mod.rs +++ b/lang-v2/src/accounts/mod.rs @@ -20,7 +20,7 @@ pub use { slab::{HeaderOnly, Slab}, slab_hooks::{SlabInit, SlabSchema}, system_account::SystemAccount, - sysvar::{Instructions, Sysvar, SysvarId, SysvarLoad}, + sysvar::{SysvarInstructions, Sysvar, SysvarId, SysvarLoad}, unchecked_account::UncheckedAccount, }; diff --git a/lang-v2/src/accounts/sysvar.rs b/lang-v2/src/accounts/sysvar.rs index 51cbbeb342..eb3fca1dc7 100644 --- a/lang-v2/src/accounts/sysvar.rs +++ b/lang-v2/src/accounts/sysvar.rs @@ -27,19 +27,18 @@ pub trait SysvarId { /// /// Split out of [`SysvarId`] because the two sysvar families read differently: /// `Clock` / `Rent` come from the `sol_get_sysvar` syscall and never touch -/// account data, while `Instructions` has no syscall at all and must be read +/// account data, while `SysvarInstructions` has no syscall at all and must be read /// out of the account's data buffer. /// /// A blanket `impl SysvarLoad for T` is not possible: it -/// would overlap the [`Instructions`] impl, and rustc cannot prove -/// `Instructions: !PinocchioSysvar` (negative reasoning about a foreign trait +/// would overlap the [`SysvarInstructions`] impl, and rustc cannot prove +/// `SysvarInstructions: !PinocchioSysvar` (negative reasoning about a foreign trait /// on a foreign type). Syscall-backed sysvars therefore get an explicit impl /// each, via `impl_syscall_sysvar!`. #[diagnostic::on_unimplemented( message = "`{Self}` is not a sysvar Anchor can load", label = "unsupported sysvar", - note = "supported: `Clock`, `Rent`, `Instructions`. A sysvar needs both `SysvarId` (its \ - well-known address) and `SysvarLoad` (how to read its value)." + note = "A sysvar needs both `SysvarId` (its well-known address) and `SysvarLoad` (how to read its value)." )] pub trait SysvarLoad: SysvarId + Sized { /// Read the sysvar's value. @@ -88,7 +87,7 @@ impl_syscall_sysvar!( // Deliberately generic over `T`: the address and IDL string are the same for // every instantiation, and `tests-v2/tests/sysvar_idl.rs` asserts on -// `Instructions<&'static [u8]>`. Only the `Instructions` alias below — the one +// `Instructions<&'static [u8]>`. Only the `SysvarInstructions` alias below — the one // instantiation that can outlive `load` — gets a `SysvarLoad` impl. impl> SysvarId for pinocchio::sysvars::instructions::Instructions { const SYSVAR_ID: Address = pinocchio::sysvars::instructions::INSTRUCTIONS_ID; @@ -100,25 +99,30 @@ impl> SysvarId for pinocchio::sysvars::instructions::Ins /// Instantiates pinocchio's `Instructions` at the one `T` that can outlive /// [`AnchorAccount::load`]: a `'static` borrow guard over the account's data. /// -/// Use it as `Sysvar` in a `#[derive(Accounts)]` struct, then +/// **Note on naming:** This type is intentionally named `SysvarInstructions` rather +/// than `Instructions` to avoid namespace collisions in the prelude. Downstream programs +/// commonly define their own `Instructions` type (e.g., an enum of program instructions), +/// which would conflict when writing `Sysvar`. +/// +/// Use it as `Sysvar` in a `#[derive(Accounts)]` struct, then /// reach the introspection methods through the wrapper's `Deref`: /// /// ```ignore /// #[derive(Accounts)] /// pub struct Introspect { -/// pub instructions: Sysvar, +/// pub sysvar_instructions: Sysvar, /// } /// -/// let previous = ctx.accounts.instructions.get_instruction_relative(-1)?; +/// let previous = ctx.accounts.sysvar_instructions.get_instruction_relative(-1)?; /// let caller = previous.get_program_id(); /// ``` /// /// Unlike `Clock` / `Rent`, there is no syscall for this sysvar: the account /// must be passed in the transaction, and the wrapper holds a shared borrow of /// its data for as long as it is alive. -pub type Instructions = pinocchio::sysvars::instructions::Instructions>; +pub type SysvarInstructions = pinocchio::sysvars::instructions::Instructions>; -impl SysvarLoad for Instructions { +impl SysvarLoad for SysvarInstructions { #[inline(always)] fn read(view: &AccountView) -> Result { // A well-formed instructions sysvar is at minimum `[u16 num = 0]` + @@ -151,7 +155,7 @@ impl SysvarLoad for Instructions { /// Validates that the passed account address matches `T::SYSVAR_ID`, then /// defers to [`SysvarLoad::read`] for the value. For `Clock` / `Rent` that /// reads directly from the runtime via pinocchio's `Sysvar::get()` syscall and -/// never touches account data; for [`Instructions`] it borrows the account's +/// never touches account data; for [`SysvarInstructions`] it borrows the account's /// data and holds that shared borrow for the wrapper's lifetime. /// /// ## `#[account(address = X @ MyErr)]` does NOT surface `MyErr` diff --git a/lang-v2/src/prelude.rs b/lang-v2/src/prelude.rs index ba4e259676..333ce42392 100644 --- a/lang-v2/src/prelude.rs +++ b/lang-v2/src/prelude.rs @@ -9,8 +9,8 @@ pub use crate::{ account, // Account types accounts::{ - Account, BorshAccount, Instructions, Interface, InterfaceAccount, Program, Signer, - SlabSchema, SystemAccount, Sysvar, SysvarId, SysvarLoad, UncheckedAccount, + Account, BorshAccount, Interface, InterfaceAccount, Program, Signer, SlabSchema, + SystemAccount, Sysvar, SysvarId, SysvarInstructions, SysvarLoad, UncheckedAccount, }, constant, create_account, diff --git a/lang-v2/tests/account_wrapper_checks.rs b/lang-v2/tests/account_wrapper_checks.rs index 8acba9cf3e..810f14fce7 100644 --- a/lang-v2/tests/account_wrapper_checks.rs +++ b/lang-v2/tests/account_wrapper_checks.rs @@ -16,13 +16,13 @@ use { anchor_lang::{ accounts::{ - Account, BorshAccount, Instructions, Interface, Program, Signer, SlabSchema, - SystemAccount, Sysvar, UncheckedAccount, + Account, BorshAccount, Interface, Program, Signer, SlabSchema, SystemAccount, Sysvar, + SysvarInstructions, UncheckedAccount, }, programs::{System, Token}, testing::AccountBuffer, - Accounts, AnchorAccount, AnchorDeserialize, AnchorSerialize, Discriminator, ErrorCode, - Ids, Owner, TryAccounts, + Accounts, AnchorAccount, AnchorDeserialize, AnchorSerialize, Discriminator, ErrorCode, Ids, + Owner, TryAccounts, }, bytemuck::{Pod, Zeroable}, pinocchio::address::Address, @@ -389,7 +389,7 @@ fn sysvar_load_rejects_wrong_address() { assert_eq!(err, ProgramError::InvalidArgument); } -// -- Sysvar ----------------------------------------------- +// -- Sysvar ----------------------------------------------- // // Unlike `Clock` / `Rent`, this sysvar has no `sol_get_sysvar` syscall — the // value is read out of the account's data buffer. These tests build a @@ -482,7 +482,7 @@ fn sysvar_instructions_load_rejects_wrong_address() { buf.init([0x01; 32], [0u8; 32], blob.len(), false, false, false); buf.write_data(&blob); let view = unsafe { buf.view() }; - let err = expect_err(Sysvar::::load(view)); + let err = expect_err(Sysvar::::load(view)); assert_eq!(err, ProgramError::InvalidArgument); } @@ -500,7 +500,7 @@ fn sysvar_instructions_reads_synthetic_blob() { ); buf.write_data(&blob); let view = unsafe { buf.view() }; - let sysvar = Sysvar::::load(view).unwrap(); + let sysvar = Sysvar::::load(view).unwrap(); assert_eq!(sysvar.num_instructions(), 2); assert_eq!(sysvar.load_current_index(), 1); @@ -544,7 +544,7 @@ fn sysvar_instructions_rejects_out_of_range_index() { ); buf.write_data(&blob); let view = unsafe { buf.view() }; - let sysvar = Sysvar::::load(view).unwrap(); + let sysvar = Sysvar::::load(view).unwrap(); assert_eq!( expect_err(sysvar.load_instruction_at(2)), @@ -574,7 +574,7 @@ fn sysvar_instructions_holds_a_shared_borrow_not_an_exclusive_one() { ); buf.write_data(&blob); let view = unsafe { buf.view() }; - let sysvar = Sysvar::::load(view).unwrap(); + let sysvar = Sysvar::::load(view).unwrap(); assert!(sysvar.account().check_borrow().is_ok()); assert!(sysvar.account().check_borrow_mut().is_err()); @@ -595,7 +595,7 @@ fn sysvar_instructions_rejects_undersized_data() { buf.init(instructions_sysvar_id(), [0u8; 32], 2, false, false, false); buf.write_data(&[0u8; 2]); let view = unsafe { buf.view() }; - let err = expect_err(Sysvar::::load(view)); + let err = expect_err(Sysvar::::load(view)); assert_eq!(err, ProgramError::AccountDataTooSmall); } diff --git a/tests-v2/programs/accounts/src/lib.rs b/tests-v2/programs/accounts/src/lib.rs index fd9aa02d23..be7b8388e9 100644 --- a/tests-v2/programs/accounts/src/lib.rs +++ b/tests-v2/programs/accounts/src/lib.rs @@ -980,7 +980,7 @@ pub struct ReadRent { #[derive(Accounts)] pub struct ReadInstructions { - pub instructions: Sysvar, + pub instructions: Sysvar, } #[derive(Accounts)] diff --git a/tests-v2/tests/sysvar_idl.rs b/tests-v2/tests/sysvar_idl.rs index a532687210..e4d1fb1e85 100644 --- a/tests-v2/tests/sysvar_idl.rs +++ b/tests-v2/tests/sysvar_idl.rs @@ -23,7 +23,7 @@ fn sysvar_idl_addresses_match_well_known_accounts() { fn sysvar_wrappers_surface_their_idl_address() { // End of the chain the IDL builder actually reads: // `SysvarId::IDL_ADDRESS` -> `IdlAccountType::__IDL_ADDRESS`. - use anchor_lang::accounts::{Instructions as AnchorInstructions, Sysvar}; + use anchor_lang::accounts::{Sysvar, SysvarInstructions as AnchorInstructions}; assert_eq!( as IdlAccountType>::__IDL_ADDRESS,