Skip to main content

verified_anchor/
lib.rs

1//! Verified Anchor runtime support (Milestone 2).
2use solana_program::account_info::AccountInfo;
3use solana_program::pubkey::Pubkey;
4
5// Re-export the crates the generated code references, so a user needs ONLY `verified-anchor`
6// as a dependency (mirrors how `anchor_lang` re-exports `solana_program`). The macros emit
7// `::verified_anchor::solana_program::…` and `::verified_anchor::borsh::…` paths.
8pub use borsh;
9pub use solana_program;
10
11pub mod account_data;
12pub use account_data::{AccountData, ProgramId, System};
13
14pub mod account;
15pub use account::{Account, Signer, Program, SystemAccount, UncheckedAccount};
16
17pub mod context;
18pub use context::Context;
19
20pub mod prelude;
21
22pub use verified_anchor_macros::VerifiedAccounts;
23pub use verified_anchor_macros::AccountData as AccountData;
24pub use verified_anchor_macros::account;
25
26/// Why account validation failed. `field` is the struct field name that failed.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum VAError {
29    MissingSigner { field: &'static str },
30    NotWritable { field: &'static str },
31    WrongOwner { field: &'static str },
32    /// Fewer accounts were supplied than the struct declares.
33    NotEnoughAccounts { expected: usize, got: usize },
34    WrongHasOne { field: &'static str, target: &'static str },
35    InitFailed { field: &'static str },
36    CloseFailed { field: &'static str },
37    WrongPda { field: &'static str },
38    WrongBump { field: &'static str },
39    WrongDiscriminator { field: &'static str },
40    BorshFailed { field: &'static str },
41}
42
43impl core::fmt::Display for VAError {
44    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
45        match self {
46            VAError::MissingSigner { field } => write!(f, "account `{field}` must be a signer"),
47            VAError::NotWritable { field } => write!(f, "account `{field}` must be writable"),
48            VAError::WrongOwner { field } => write!(f, "account `{field}` has the wrong owner"),
49            VAError::NotEnoughAccounts { expected, got } =>
50                write!(f, "expected {expected} accounts, got {got}"),
51            VAError::WrongHasOne { field, target } =>
52                write!(f, "account `{field}` field does not match `{target}`"),
53            VAError::InitFailed { field } => write!(f, "init failed for `{field}`"),
54            VAError::CloseFailed { field } => write!(f, "close failed for `{field}`"),
55            VAError::WrongPda { field } => write!(f, "account `{field}` is not the expected PDA"),
56            VAError::WrongBump { field } => write!(f, "account `{field}` has a non-canonical bump"),
57            VAError::WrongDiscriminator { field } => write!(f, "account `{field}` has the wrong 8-byte discriminator"),
58            VAError::BorshFailed { field } => write!(f, "Borsh deserialization failed for `{field}`"),
59        }
60    }
61}
62
63impl std::error::Error for VAError {}
64
65/// So a handler returning `ProgramResult` can use `Transfer::try_accounts(...)?` directly.
66/// Each variant maps to a distinct custom code so clients can disambiguate the failed check.
67impl From<VAError> for solana_program::program_error::ProgramError {
68    fn from(e: VAError) -> Self {
69        let code: u32 = match e {
70            VAError::MissingSigner { .. } => 1,
71            VAError::NotWritable { .. } => 2,
72            VAError::WrongOwner { .. } => 3,
73            VAError::NotEnoughAccounts { .. } => 4,
74            VAError::WrongHasOne { .. } => 5,
75            VAError::InitFailed { .. } => 6,
76            VAError::CloseFailed { .. } => 7,
77            VAError::WrongPda { .. } => 8,
78            VAError::WrongBump { .. } => 9,
79            VAError::WrongDiscriminator { .. } => 10,
80            VAError::BorshFailed { .. } => 11,
81        };
82        solana_program::program_error::ProgramError::Custom(code)
83    }
84}
85
86/// Implemented by `#[derive(VerifiedAccounts)]`. Validation is positional over the
87/// runtime account slice (index = field declaration order), matching the Lean `Ctx`.
88pub trait Validate {
89    fn validate(
90        accounts: &[AccountInfo],
91        instr_data: &[u8],
92        program_id: &Pubkey,
93    ) -> Result<(), VAError>;
94}
95
96/// THE DEVELOPER SURFACE (M7a). `try_accounts` calls `Self::validate`
97/// (the proven layer) and Borsh-deserialises each `Account<T>` field.
98pub trait Accounts<'info>: Sized {
99    type Bumps;
100    fn try_accounts(
101        program_id: &Pubkey,
102        accounts: &'info [AccountInfo<'info>],
103        instr_data: &[u8],
104    ) -> Result<(Self, Self::Bumps), VAError>;
105}
106
107// The spec-collection machinery uses `inventory`, whose `#[used]` link-section statics
108// corrupt the Solana SBF ELF (invalid PT_DYNAMIC -> loader rejects with InvalidAccountData).
109// It is host-only: gate ALL of it out of the `target_os = "solana"` (BPF) build, the same way
110// Anchor gates host-only code. Native builds (the example crate + `cargo verified-anchor check`,
111// which runs `cargo test --lib` natively) keep it.
112
113/// Re-exported so the derive macro can emit `::verified_anchor::inventory::submit!`.
114#[cfg(not(target_os = "solana"))]
115pub use inventory;
116
117/// One registered `#[derive(VerifiedAccounts)]` struct.
118#[cfg(not(target_os = "solana"))]
119pub struct SpecEntry {
120    pub name: &'static str,
121    /// The Milestone-1 `AccountsStruct` literal (Lean source).
122    pub lean_spec: fn() -> String,
123    /// True if any field carries an `init`/`close` constraint (selects the obligation kind).
124    pub has_lifecycle: bool,
125}
126
127#[cfg(not(target_os = "solana"))]
128inventory::collect!(SpecEntry);
129
130/// All registered structs in the current compilation artifact.
131#[cfg(not(target_os = "solana"))]
132pub fn collect_specs() -> Vec<&'static SpecEntry> {
133    inventory::iter::<SpecEntry>.into_iter().collect()
134}
135
136/// Write one spec file per registered struct into `dir`. Filename is `<name>.<kind>` where
137/// kind is `lifecycle` or `validation`; the file content is the `lean_spec()` literal.
138/// (No JSON — the literal is the whole content, so there's nothing to escape.)
139#[cfg(not(target_os = "solana"))]
140pub fn write_spec_files(dir: &std::path::Path) -> std::io::Result<()> {
141    std::fs::create_dir_all(dir)?;
142    for e in collect_specs() {
143        let kind = if e.has_lifecycle { "lifecycle" } else { "validation" };
144        std::fs::write(dir.join(format!("{}.{}", e.name, kind)), (e.lean_spec)())?;
145    }
146    Ok(())
147}
148
149/// Drop ONE call in your crate's lib (e.g. bottom of `src/lib.rs`). Expands to a test that,
150/// when `VERIFIED_ANCHOR_SPEC_DIR` is set (by `cargo verified-anchor check`), writes spec
151/// files for every derived struct in this crate. Placing it in the lib is REQUIRED: the
152/// emitter must be same-crate as the `inventory::submit!`s (cross-crate harnesses dead-strip).
153#[macro_export]
154macro_rules! emit_specs {
155    () => {
156        #[cfg(test)]
157        #[test]
158        fn __verified_anchor_emit_specs() {
159            if let Ok(dir) = ::std::env::var("VERIFIED_ANCHOR_SPEC_DIR") {
160                ::verified_anchor::write_spec_files(::std::path::Path::new(&dir)).unwrap();
161            }
162        }
163    };
164}
165
166#[cfg(test)]
167mod spec_collection_tests {
168    use super::*;
169
170    // A manually-registered entry (same crate → inventory sees it).
171    inventory::submit! { SpecEntry { name: "FakeStruct", lean_spec: || "FAKE-SPEC".to_string(), has_lifecycle: false } }
172
173    #[test]
174    fn write_spec_files_emits_one_file_per_entry() {
175        let dir = std::env::temp_dir().join("va-m1-spec-test");
176        let _ = std::fs::remove_dir_all(&dir);
177        write_spec_files(&dir).unwrap();
178        let f = dir.join("FakeStruct.validation");
179        assert!(f.exists(), "expected {f:?}");
180        assert_eq!(std::fs::read_to_string(&f).unwrap(), "FAKE-SPEC");
181    }
182}