Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/reusable-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,8 @@ jobs:
path: tests/declare-id
- cmd: cd tests/declare-program && anchor test --skip-lint
path: tests/declare-program
- cmd: cd tests/custom-program && anchor test --skip-lint
path: tests/custom-program
- cmd: cd tests/typescript && anchor test --skip-lint && npx tsc --noEmit
path: tests/typescript
# zero-copy tests cause `/usr/bin/ld: final link failed: No space left on device`
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The minor version will be incremented upon a breaking change and the patch versi
- cli: Replace `anchor verify` to use `solana-verify` under the hood, adding automatic installation via AVM, local path support, and future-proof argument passing ([#3768](https://github.com/solana-foundation/anchor/pull/3768)).
- lang: Replace `solana-program` crate with smaller crates ([#3819](https://github.com/solana-foundation/anchor/pull/3819)).
- cli: Make `anchor deploy` to upload the IDL to the cluster by default unless `--no-idl` is passed ([#3863](https://github.com/solana-foundation/anchor/pull/3863)).
- lang: Add generic program validation support to `Program` type allowing `Program<'info>` for executable-only validation ([#3878](https://github.com/solana-foundation/anchor/pull/3878)).
- lang: Use `solana-invoke` instead of `solana_cpi::invoke` ([#3900](https://github.com/solana-foundation/anchor/pull/3900)).
- client: remove `solana-client` from `anchor-client` and `cli` ([#3877](https://github.com/solana-foundation/anchor/pull/3877)).
- idl: Build IDL on stable Rustc ([#3842](https://github.com/solana-foundation/anchor/pull/3842)).
Expand Down
6 changes: 3 additions & 3 deletions bench/COMPUTE_UNITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,9 @@ Solana version: 2.1.0
| interface4 | 1,189 | - |
| interface8 | 1,748 | - |
| program1 | 779 | - |
| program2 | 920 | - |
| program4 | 1,193 | - |
| program8 | 1,744 | - |
| program2 | 934 | 🔴 **+14 (1.52%)** |
| program4 | 1,221 | 🔴 **+28 (2.35%)** |
| program8 | 1,800 | 🔴 **+56 (3.21%)** |
| signer1 | 774 | - |
| signer2 | 1,064 | - |
| signer4 | 1,637 | - |
Expand Down
37 changes: 34 additions & 3 deletions lang/src/accounts/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,24 @@ use std::ops::Deref;
///
/// # Table of Contents
/// - [Basic Functionality](#basic-functionality)
/// - [Generic Program Validation](#generic-program-validation)
/// - [Out of the Box Types](#out-of-the-box-types)
///
/// # Basic Functionality
///
/// For `Program<'info, T>` where T implements Id:
/// Checks:
///
/// - `account_info.key == expected_program`
/// - `account_info.executable == true`
///
/// # Generic Program Validation
///
/// For `Program<'info>` (without type parameter):
/// - Only checks: `account_info.executable == true`
/// - Use this when you only need to verify that an address is executable,
/// without validating against a specific program ID.
///
/// # Example
/// ```ignore
/// #[program]
Expand Down Expand Up @@ -65,6 +74,16 @@ use std::ops::Deref;
/// - `program_data`'s constraint checks that its upgrade authority is the `authority` account.
/// - Finally, `authority` needs to sign the transaction.
///
/// ## Generic Program Example
/// ```ignore
/// #[derive(Accounts)]
/// pub struct ValidateExecutableProgram<'info> {
/// // Only validates that the provided account is executable
/// pub any_program: Program<'info>,
/// pub authority: Signer<'info>,
/// }
/// ```
///
/// # Out of the Box Types
///
/// Between the [`anchor_lang`](https://docs.rs/anchor-lang/latest/anchor_lang) and [`anchor_spl`](https://docs.rs/anchor_spl/latest/anchor_spl) crates,
Expand All @@ -75,7 +94,7 @@ use std::ops::Deref;
/// - [`Token`](https://docs.rs/anchor-spl/latest/anchor_spl/token/struct.Token.html)
///
#[derive(Clone)]
pub struct Program<'info, T> {
pub struct Program<'info, T = ()> {
info: &'info AccountInfo<'info>,
_phantom: PhantomData<T>,
}
Expand Down Expand Up @@ -128,13 +147,15 @@ impl<'a, T: Id> TryFrom<&'a AccountInfo<'a>> for Program<'a, T> {
type Error = Error;
/// Deserializes the given `info` into a `Program`.
fn try_from(info: &'a AccountInfo<'a>) -> Result<Self> {
if info.key != &T::id() {
// Special handling for unit type () - only check executable, not program ID
let is_unit_type = T::id() == Pubkey::default();

if !is_unit_type && info.key != &T::id() {
return Err(Error::from(ErrorCode::InvalidProgramId).with_pubkeys((*info.key, T::id())));
}
if !info.executable {
return Err(ErrorCode::InvalidProgramExecutable.into());
}

Ok(Program::new(info))
}
}
Expand Down Expand Up @@ -195,3 +216,13 @@ impl<T: AccountDeserialize> Key for Program<'_, T> {
*self.info.key
}
}

// Implement Id trait for unit type to support Program<'info> without type parameter
impl crate::Id for () {
fn id() -> Pubkey {
// For generic programs, this should never be called since they don't validate specific program IDs.
// However, we need to implement it to satisfy the trait bounds.
// Using a special marker value that indicates "any program"
Pubkey::default()
}
}
15 changes: 11 additions & 4 deletions lang/syn/src/idl/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,17 @@ fn get_address(acc: &Field) -> TokenStream {
match &acc.ty {
Ty::Program(_) | Ty::Sysvar(_) => {
let ty = acc.account_ty();
let id_trait = matches!(acc.ty, Ty::Program(_))
.then(|| quote!(anchor_lang::Id))
.unwrap_or_else(|| quote!(anchor_lang::solana_program::sysvar::SysvarId));
quote! { Some(<#ty as #id_trait>::id().to_string()) }
// Check if this is the unit type marker (for generic Program<'info>)
let ty_str = quote!(#ty).to_string();
if ty_str == "" || ty_str == "__SolanaProgramUnitType" {
// For generic programs, we don't have a specific address
quote! { None }
} else {
let id_trait = matches!(acc.ty, Ty::Program(_))
.then(|| quote!(anchor_lang::Id))
.unwrap_or_else(|| quote!(anchor_lang::solana_program::sysvar::SysvarId));
quote! { Some(<#ty as #id_trait>::id().to_string()) }
}
}
_ => acc
.constraints
Expand Down
24 changes: 22 additions & 2 deletions lang/syn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,20 @@ impl Field {
Sysvar<#account>
}
}
Ty::Program(ty) => {
let program = &ty.account_type_path;
// Check if this is the generic Program<'info> (unit type)
let program_str = quote!(#program).to_string();
if program_str == "__SolanaProgramUnitType" {
quote! {
#container_ty<'info>
}
} else {
quote! {
#container_ty<'info, #program>
}
}
}
_ => quote! {
#container_ty<#account_ty>
},
Expand Down Expand Up @@ -543,8 +557,14 @@ impl Field {
},
Ty::Program(ty) => {
let program = &ty.account_type_path;
quote! {
#program
// Check if this is the special marker for generic Program<'info> (unit type)
let program_str = quote!(#program).to_string();
if program_str == "__SolanaProgramUnitType" {
quote! {}
} else {
quote! {
#program
}
}
}
Ty::Interface(ty) => {
Expand Down
48 changes: 47 additions & 1 deletion lang/syn/src/parser/accounts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ fn parse_interface_account_ty(path: &syn::Path) -> ParseResult<InterfaceAccountT
}

fn parse_program_ty(path: &syn::Path) -> ParseResult<ProgramTy> {
let account_type_path = parse_account(path)?;
let account_type_path = parse_program_account(path)?;
Ok(ProgramTy { account_type_path })
}

Expand All @@ -486,6 +486,52 @@ fn parse_interface_ty(path: &syn::Path) -> ParseResult<InterfaceTy> {
Ok(InterfaceTy { account_type_path })
}

// Special parsing function for Program that handles both Program<'info> and Program<'info, T>
fn parse_program_account(path: &syn::Path) -> ParseResult<syn::TypePath> {
let segments = &path.segments[0];
match &segments.arguments {
syn::PathArguments::AngleBracketed(args) => {
match args.args.len() {
// Program<'info> - only lifetime, no type parameter
1 => {
// Create a special marker for unit type that gets handled later
use syn::{Path, PathSegment, PathArguments};
let path_segment = PathSegment {
ident: syn::Ident::new("__SolanaProgramUnitType", proc_macro2::Span::call_site()),
arguments: PathArguments::None,
};

Ok(syn::TypePath {
qself: None,
path: Path {
leading_colon: None,
segments: std::iter::once(path_segment).collect(),
},
})
}
// Program<'info, T> - lifetime and type
2 => {
match &args.args[1] {
syn::GenericArgument::Type(syn::Type::Path(ty_path)) => Ok(ty_path.clone()),
_ => Err(ParseError::new(
args.args[1].span(),
"second bracket argument must be a type",
)),
}
}
_ => Err(ParseError::new(
args.args.span(),
"Program must have either just a lifetime (Program<'info>) or a lifetime and type (Program<'info, T>)",
)),
}
}
_ => Err(ParseError::new(
segments.arguments.span(),
"expected angle brackets with lifetime or lifetime and type",
)),
}
}

// TODO: this whole method is a hack. Do something more idiomatic.
fn parse_account(mut path: &syn::Path) -> ParseResult<syn::TypePath> {
let path_str = parser::tts_to_string(path).replace(' ', "");
Expand Down
12 changes: 6 additions & 6 deletions tests/bench/bench.json
Original file line number Diff line number Diff line change
Expand Up @@ -1380,9 +1380,9 @@
"interface4": 1189,
"interface8": 1748,
"program1": 779,
"program2": 920,
"program4": 1193,
"program8": 1744,
"program2": 934,
"program4": 1221,
"program8": 1800,
"signer1": 774,
"signer2": 1064,
"signer4": 1637,
Expand Down Expand Up @@ -1752,9 +1752,9 @@
"interface4": 1301,
"interface8": 1867,
"program1": 890,
"program2": 1035,
"program4": 1313,
"program8": 1879,
"program2": 1051,
"program4": 1345,
"program8": 1943,
"signer1": 874,
"signer2": 1173,
"signer4": 1759,
Expand Down
21 changes: 21 additions & 0 deletions tests/custom-program/Anchor.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"

[programs.localnet]
custom_program = "FdQ5d5kJDidxLP8qBm2d4G47QbDMWk6iWJ3QkYY2UAP7"

[scripts]
test = "yarn run ts-mocha -t 1000000 tests/*.ts"

[test.validator]
url = "https://api.mainnet-beta.solana.com"

[[test.validator.clone]]
address = "9cxLzxjrTeodcbaEU3KCNGE1a4yFZEcdJ7uEXN378S4U"

[[test.validator.clone]]
address = "PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY"

[[test.validator.clone]]
address = "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH"
8 changes: 8 additions & 0 deletions tests/custom-program/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[workspace]
members = [
"programs/*"
]
resolver = "2"

[profile.release]
overflow-checks = true
22 changes: 22 additions & 0 deletions tests/custom-program/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "custom-program",
"version": "0.31.1",
"license": "(MIT OR Apache-2.0)",
"homepage": "https://github.com/coral-xyz/anchor#readme",
"bugs": {
"url": "https://github.com/coral-xyz/anchor/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/coral-xyz/anchor.git"
},
"engines": {
"node": ">=17"
},
"scripts": {
"test": "anchor test"
},
"dependencies": {
"ts-mocha": "^11.1.0"
}
}
17 changes: 17 additions & 0 deletions tests/custom-program/programs/custom-program/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "custom-program"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"

[lib]
crate-type = ["cdylib", "lib"]
name = "custom_program"

[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
idl-build = ["anchor-lang/idl-build"]

[dependencies]
anchor-lang = { path = "../../../../lang" }
2 changes: 2 additions & 0 deletions tests/custom-program/programs/custom-program/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
54 changes: 54 additions & 0 deletions tests/custom-program/programs/custom-program/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use anchor_lang::prelude::*;

declare_id!("FdQ5d5kJDidxLP8qBm2d4G47QbDMWk6iWJ3QkYY2UAP7");

pub const CUSTOM_PROGRAM_ID: Pubkey = pubkey!("PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY");
pub const NON_EXECUTABLE_ACCOUNT_ID: Pubkey =
pubkey!("2myyNegEA6pjAHmmEsJC6JdYhW51gwxQW7ZCTWvwaKTk");
pub const CUSTOM_PROGRAM_ADDRESS: Pubkey = pubkey!("dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH");

// Define a marker struct for our custom program ID
pub struct CustomProgramMarker;

impl Id for CustomProgramMarker {
fn id() -> Pubkey {
id()
}
}

#[program]
mod custom_program {
use super::*;

pub fn test_program_validation(ctx: Context<TestProgramValidation>) -> Result<()> {
// This demonstrates both types of program validation:
// - generic_program: only validates executable (any program)
// - system_program: validates both program ID and executable
msg!(
"Generic program key: {}",
ctx.accounts.generic_program.key()
);
msg!("System program key: {}", ctx.accounts.system_program.key());
msg!(
"Custom program key: {}",
ctx.accounts.custom_program_input.key()
);
Ok(())
}
}

#[derive(Accounts)]
pub struct TestProgramValidation<'info> {
/// Generic program - only validates executable (any program ID)
pub generic_program: Program<'info>,

/// Specific system program - validates both program ID and executable
pub system_program: Program<'info, System>,

/// Custom program with specific type - validates both program ID and executable
pub custom_program_input: Program<'info, CustomProgramMarker>,

/// Program with an address constraint - validates both program ID and executable
#[account(address = CUSTOM_PROGRAM_ADDRESS)]
pub custom_program_address: Program<'info>,
}
Loading