Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Migrating from Anchor


This guide maps common Anchor patterns to their Pina equivalents. If you have an existing Anchor program and want to rewrite it with Pina for lower compute usage and smaller binaries, this is the reference to follow.

The repository includes several Anchor parity example programs (ports of Anchor’s own test suite, without the anchor_ prefix) that demonstrate direct parity with Anchor’s behavior. These are referenced throughout this guide.

Program structure


Anchor

#![allow(unused)]
fn main() {
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXk...");

#[program]
pub mod my_program {
	use super::*;

	pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
		// ...
		Ok(())
	}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
	#[account(mut)]
	pub user: Signer<'info>,
	#[account(init, payer = user, space = 8 + MyAccount::INIT_SPACE)]
	pub my_account: Account<'info, MyAccount>,
	pub system_program: Program<'info, System>,
}
}

Pina

#![allow(unused)]
fn main() {
use pina::*;

declare_id!("Fg6PaFpoGXk...");

#[discriminator]
pub enum MyInstruction {
	Initialize = 0,
}

#[instruction(discriminator = MyInstruction::Initialize)]
pub struct InitializeInstruction {}

#[derive(Accounts, Debug)]
pub struct InitializeAccounts<'a> {
	pub user: &'a AccountView,
	pub my_account: &'a mut AccountView,
	pub system_program: &'a AccountView,
}

impl<'a> ProcessAccountInfos<'a> for InitializeAccounts<'a> {
	fn process(self, data: &[u8]) -> ProgramResult {
		let _ = InitializeInstruction::try_from_bytes(data)?;
		self.user.assert_signer()?.assert_writable()?;
		self.my_account.assert_empty()?.assert_writable()?;
		self.system_program.assert_address(&system::ID)?;
		// ...
		Ok(())
	}
}
}

Key differences:

  • No #[program] module. Pina uses explicit discriminator enums and a manual match in the entrypoint.
  • No Context<T>. The entrypoint receives &mut [AccountView], #[derive(Accounts)] maps that mutable slice into typed fields, and the processor receives raw data: &[u8].
  • Constraints are code, not attributes. Validation happens inside process via chained assertions rather than #[account(...)] attribute directives.

Account constraints to validation chains


Anchor expresses constraints as attributes on account fields. Pina uses explicit method calls on AccountView references.

Anchor attributePina equivalent
Signer<'info>account.assert_signer()?
#[account(mut)]account.assert_writable()?
#[account(owner = program)]account.assert_owner(&program_id)?
#[account(address = KEY)]account.assert_address(&KEY)?
#[account(seeds = [...], bump)]account.assert_seeds_with_bump(seeds, &ID)?
#[account(init, ...)]account.assert_empty()? then canonical CreateProgramAccount { ... }.invoke::<MyData>()
#[account(constraint = expr)]Write the check directly in process and return an error
Account<'info, T>account.as_account::<T>(&owner)? or account.as_account_mut::<T>(&owner)?

Pina’s assertion methods return the same reference type they receive, so shared chains stay shared and mutable chains stay mutable. Typed loaders perform their own validation; do not precede them with assert_type:

#![allow(unused)]
fn main() {
let mut counter = self.counter.as_account_mut::<CounterState>(&ID)?;
}

For a fixed account with a stored #[pda(bump = ...)], prefer its generated Type::load_pda or Type::load_pda_mut method. These methods also validate the PDA address. Keep assert_type for the narrower case where the handler only validates an existing fixed account and never accesses typed fields.

See examples/counter_program for a complete PDA creation and validation example, and examples/duplicate_mutable_accounts for explicit duplicate-account safety checks.

Account data: Borsh to Pod


Anchor (Borsh)

#![allow(unused)]
fn main() {
#[account]
pub struct MyAccount {
	pub authority: Pubkey,
	pub value: u64,
	pub active: bool,
}
}

Anchor uses Borsh serialization by default. The #[account] macro adds an 8-byte discriminator (SHA-256 hash prefix) and derives BorshSerialize/BorshDeserialize.

Pina (Pod / zero-copy)

#![allow(unused)]
fn main() {
#[account(discriminator = MyAccountType)]
pub struct MyAccount {
	pub authority: Address,
	pub value: PodU64,
	pub active: PodBool,
}
}

Pina uses PinaPod-validated zero-copy layouts. Fixed accounts reserve the full capacity of bounded collections, while compact accounts place supported collections in variable-length tails.

Anchor typePina schema typeNotes
PubkeyAddressBoth store 32 bytes
u64, u32, u16, i64Same native typePinaPod generates little-endian alignment-one storage
boolboolPinaPod generates a checked one-byte representation
StringString<N>Choose a byte capacity; fixed accounts reserve all N bytes
Vec<T>Vec<T, N>Choose an element capacity; compact accounts store only active elements
Option<T>Option<T>T must have a supported fixed representation

PinaPod’s generated storage wrappers keep every field alignment one and provide recursive validation. Direct Pod* wrappers still convert to and from native types with From:

#![allow(unused)]
fn main() {
// Creating Pod values
let value = PodU64::from(42);
let active = PodBool::from(true);

// Reading Pod values
let n: u64 = value.into();
let b: bool = active.into();
}

The #[account] macro’s discriminator is a single u8 (or configurable width) rather than Anchor’s 8-byte hash. This saves 7 bytes per account.

Discriminators


Anchor

Anchor generates 8-byte discriminators from sha256("account:<StructName>") or sha256("global:<method_name>"). These are implicit – you never write them manually.

Pina

Pina uses explicit discriminator enums with numeric values:

#![allow(unused)]
fn main() {
#[discriminator]
pub enum MyInstruction {
	Initialize = 0,
	Update = 1,
}

#[discriminator]
pub enum MyAccountType {
	MyAccount = 1,
}
}

Each #[instruction] or #[account] macro references its discriminator enum and variant:

#![allow(unused)]
fn main() {
#[instruction(discriminator = MyInstruction::Initialize)]
pub struct InitializeInstruction {
	// ...
}

#[account(discriminator = MyAccountType)]
pub struct MyAccount {
	// ...
}
}

Benefits of explicit discriminators:

  • Stable, human-readable values (not hash-dependent).
  • Single byte by default (configurable to u16/u32/u64), saving space.
  • No hidden behavior – you control the exact values.

Migration from fixed 8-byte prefixes (Anchor-compatible data)

If you are coming from Anchor/Borsh with implicit 8-byte discriminators, there are two practical migration paths:

1) Keep old on-chain layouts and add compatibility readers

Use a lightweight adapter struct for legacy decoding, then convert into a pinned Pina struct in memory. This is useful when you cannot migrate all existing accounts immediately.

#![allow(unused)]
fn main() {
#[repr(C)]
pub struct LegacyAccountV0 {
	discriminator: [u8; 8],
	owner: [u8; 32],
	value: PodU64,
}

#[discriminator]
pub enum MyAccountType {
	MyAccountV0 = 0,
	MyAccount = 1,
}

impl LegacyAccountV0 {
	pub fn into_live(self) -> Result<MyAccount, ProgramError> {
		if self.discriminator != LEGACY_ACCOUNT_DISCRIMINATOR {
			return Err(ProgramError::InvalidAccountData);
		}
		Ok(MyAccount {
			discriminator: [MyAccountType::MyAccount as u8],
			owner: self.owner,
			value: self.value,
		})
	}
}
}

For long-lived accounts, add a migration instruction that rewrites every stored account from the legacy header to the new first-field discriminator layout. This gives you one canonical on-chain schema thereafter.

Discriminator layout decision matrix

The discriminator strategy determines byte layout, parser guarantees, and cross-protocol compatibility.

GoalRecommended layout
Keep layout minimal and zero-copy while staying explicitCurrent Pina model: discriminator bytes are the first field inside #[account], #[instruction], and #[event] structs.
Preserve compatibility with existing Anchor-account payloads (SHA-256 hash prefixes)Legacy adapter model: custom raw wrapper types parse/write the existing 8-byte external prefix before converting to typed structs.
Minimize account size growth when you have many typesUse u8 (default) discriminator width.
You need more than 256 route variantsUse u16 / u32 / u64 by setting #[discriminator(primitive = ...)].
Avoid schema migrations across existing serialized dataKeep existing field order and discriminator values; only append fields.

Raw discriminator width by use-case

WidthMax variantsStorage cost (bytes)Recommended when
u82561Most programs and instructions
u1665,5362Medium-large routing tables and explicit version partitioning
u324,294,967,2964Very large enums, rarely needed
u6418,446,744,073,709,551,6168Legacy interoperability shims or reserved growth
  • Discriminator width only affects the first field bytes.
  • Widths above 8 are rejected at macro expansion time.
  • Wider discriminators improve variant space, but increase CPI payload and account rent by the exact number of bytes.
  • These widths describe discriminators only. The migration version envelope is a separate setting and accepts u8, u16, or u32 — never u64.

Discriminators and ABI migrations

ChangeCompatibility impact
Add a new discriminator variantBackward-compatible; existing routes keep their identity
Change an existing discriminator valueBreaking for every historical byte slice
Change a migration-aware account or enveloped instruction payloadCompatible only when the checked-in history has an adjacent transition
Change a published versioned event schemaCompatible; a new version is appended and clients decode each version with its own schema
Change a published snapshot-only instruction payloadBreaking; create a new instruction discriminator
Append optional accounts to an instruction routeCompatible when the existing positional list remains an identical prefix
Reorder, remove, or escalate an instruction slotBreaking; create a new instruction discriminator
Change the migration version width after releaseBreaking for every enveloped wire contract

Add migrations to an account, instruction, or event attribute to opt one contract into a framework-owned version field, or opt whole contract kinds in when you record the history:

pina migrations create --auto true # or --auto accounts,events,instructions

--auto accepts true, false, or a comma-separated list of accounts, events, and instructions. pina migrations create records the policy in migrations/manifest.json, its only home, and snapshots every contract of the listed kinds; a later run without the flag keeps the recorded policy. pina.toml holds no migration policy. Macros read the policy from the manifest, so a new struct still fails the build with “run pina migrations create” until it has a snapshot. Once a policy is recorded, create also scaffolds a build.rs emitting cargo:rerun-if-changed=migrations/manifest.json, so flipping the policy re-expands every contract without editing source. Add migrations = false to keep one contract out of an auto policy; removing an envelope the manifest already records is an error instead of a silent opt-out, because stripping an envelope is itself a wire-format change.

The policy gives accounts and events the version field. It records each instruction as a snapshot without one: the payload keeps its [discriminator][payload] wire format, and the build fails when the struct drifts from the snapshot. Add migrations to an #[instruction] to give that instruction the version field and adjacent transitions instead; the #[discriminator(entrypoint)] dispatcher then converts an older payload to the current layout before the handler runs.

Pina places the version field immediately after the discriminator. The accepted encodings are u8, u16, and u32; u8 is the default and the recommended choice. Versions are tracked per contract, not per program: each account, instruction, and event owns an independent history that starts at version 0, so u8 gives every contract its own 255-version budget. Rewriting one contract 255 times is not a realistic outcome, and the narrower field costs one byte in every enveloped account. Choose a wider encoding with pina migrations create --version-type u16 (or u32) before the first release only when you expect a single contract to exceed 255 versions. The width is program-wide, recorded as versionType in the manifest, and freezes at the first published release: after that the flag fails instead of widening it. Any other value, including u64, is rejected with an error naming the supported widths. Discriminator width is a separate setting, and that one does support u64.

Run pina migrations create before a release. Pina updates the replaceable draft when the current version is unpublished. After pina deploy records a non-local publication, the next schema change creates a new version, with an adjacent transition for an account or an enveloped instruction; the payload of a published snapshot-only instruction cannot change. Normal builds run pina migrations check and fail on drift, incomplete manual transitions, or changed published code.

An old instruction can omit only newly appended optional accounts. Pina does not synthesize signers, writable privileges, PDAs, or required accounts. Any change to an existing process slot requires a new discriminator.

Historical events are immutable, so Pina versions events instead of migrating them. The program emits only the current version. Generated clients decode each earlier version with its own schema, as a separate <Event>V<n> event, and a log record whose version no generated event describes fails instead of being misread.

Errors


Anchor

#![allow(unused)]
fn main() {
#[error_code]
pub enum MyError {
	#[msg("Value is too large")]
	ValueTooLarge,
}
}

Anchor assigns error codes starting at 6000 and provides #[msg] for error messages.

Pina

#![allow(unused)]
fn main() {
#[error]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MyError {
	ValueTooLarge = 6000,
}
}

Pina’s #[error] macro generates From<MyError> for ProgramError using ProgramError::Custom(code). You choose the numeric code explicitly. To return an error:

#![allow(unused)]
fn main() {
return Err(MyError::ValueTooLarge.into());
}

See examples/custom_errors for a complete parity port of Anchor’s error handling, including guard helpers like require_eq and require_gt.

Events


Anchor

#![allow(unused)]
fn main() {
#[event]
pub struct MyEvent {
	pub data: u64,
	pub label: String,
}

emit!(MyEvent {
	data: 5,
	label: "hello".into()
});
}

Pina

#![allow(unused)]
fn main() {
#[discriminator]
pub enum EventDiscriminator {
	MyEvent = 1,
}

#[event(discriminator = EventDiscriminator)]
#[derive(Debug)]
pub struct MyEvent {
	pub data: u64,
	pub label: [u8; 8],
}

// Emit the event to the `Program data:` transaction log.
MyEvent::emit(|event| {
	event.data = 5;
	event.label = *b"hello\0\0\0";
	Ok(())
})?;
}

Pina events are native PinaPod schemas with explicit discriminators, like accounts and instructions. The macro generates a validated MyEventZc storage view plus an emit helper. emit builds the [discriminator][payload] record, with the schema version after the discriminator when the event opts into migrations, through the same validated path as try_from_bytes and writes it to the transaction log as a Program data: line — the record the generated Rust, TypeScript, and Dart decoders read. Pina does not expose an object-representation to_bytes() method; use emit so the framework owns and initializes the output buffer.

emit needs Pina’s logs feature. Enable it in your program’s manifest:

pina = { version = "...", features = ["logs", "derive"] }

See examples/events for the full parity port.

CPI (Cross-Program Invocation)


Anchor

#![allow(unused)]
fn main() {
let cpi_accounts = Transfer {
	from: ctx.accounts.from.to_account_info(),
	to: ctx.accounts.to.to_account_info(),
	authority: ctx.accounts.authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts);
token::transfer(cpi_ctx, amount)?;
}

Pina

#![allow(unused)]
fn main() {
token::instructions::TransferChecked::new(
	self.from,
	self.mint,
	self.to,
	self.authority,
	amount,
	decimals,
)
.invoke_with_program(self.token_program.address())?;
}

Pina’s CPI helpers (enabled with features = ["token"]) are typed instruction builders. Construct one with new() and call .invoke_with_program() or .invoke_signed_with_program() for PDA-authorized calls. No CpiContext wrapper is needed.

See examples/escrow_program for CPI usage with both token transfers and ATA creation.

Account creation


Anchor

#![allow(unused)]
fn main() {
#[account(init, payer = user, space = 8 + 32 + 8)]
pub my_account: Account<'info, MyData>,
}

Pina

#![allow(unused)]
fn main() {
// For PDA accounts:
CreateProgramAccount {
	account: self.my_account,
	payer: self.payer,
	owner: &ID,
	seeds,
}
.invoke::<MyData>()?;

// For regular accounts:
CreateAccount {
	from: self.payer,
	to: self.my_account,
	space: MyData::SIZE as u64,
	owner: &ID,
}
.invoke()?;
}

Space is automatically computed from MyData::SIZE for the PDA builder. For CreateAccount you pass the size explicitly. In both cases, rent-exemption lamports are calculated and transferred automatically.

CreateProgramAccount derives the canonical bump itself. Use CreateProgramAccountWithBump only when the instruction intentionally supplies a bump; that explicit-bump variant verifies the supplied value is canonical before creating the account.

Use invoke::<MyData>() when the discriminator plus zeroed fields is already a valid complete value. Use invoke_with when creation must set fields before final PinaPod validation. If the payer also needs PDA signer seeds, use invoke_signed_with::<MyData>(signers, initialize).

no_std and the entrypoint


Anchor programs use #[program] which generates the entrypoint. Pina programs are #![no_std] and use a feature-gated entrypoint module:

#![allow(unused)]
#![no_std]

fn main() {
#[cfg(feature = "bpf-entrypoint")]
pub mod entrypoint {
	use pina::*;

	use super::*;

	nostd_entrypoint!(process_instruction);

	#[inline(always)]
	pub fn process_instruction(
		program_id: &Address,
		accounts: &mut [AccountView],
		data: &[u8],
	) -> ProgramResult {
		let instruction: MyInstruction = parse_instruction(program_id, &ID, data)?;

		match instruction {
			MyInstruction::Initialize => {
				InitializeAccounts::try_from((program_id, accounts))?.process(data)
			}
		}
	}
}
}

The feature gate means tests compile without BPF entrypoint overhead. The nostd_entrypoint! macro wires up the BPF program entrypoint, a minimal panic handler, and a no-allocation stub.

Testing


Anchor

Anchor programs are typically tested with TypeScript/Mocha tests that run against a local validator via anchor test.

Pina

Pina programs are tested as regular Rust libraries:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn discriminator_roundtrip() {
		assert!(MyInstruction::try_from(0u8).is_ok());
		assert!(MyInstruction::try_from(99u8).is_err());
	}
}
}

For integration tests, use mollusk-svm (a Solana SVM simulator) instead of a full validator:

[dev-dependencies]
mollusk-svm = { workspace = true }

This gives you fast, deterministic tests without network I/O.

Migration checklist


  1. Replace anchor_lang::prelude::* with use pina::*.
  2. Convert #[account] structs from Borsh to Pod types (PodU64, PodBool, Address, fixed-size arrays).
  3. Define explicit #[discriminator] enums for instructions and accounts.
  4. Replace #[account(...)] constraint attributes with validation chain calls in process.
  5. Replace Context<T> with #[derive(Accounts)] structs and ProcessAccountInfos.
  6. Replace CpiContext patterns with Pina’s typed CPI instruction builders.
  7. Replace #[error_code] with #[error] and explicit numeric codes.
  8. Replace #[event] + emit! with Pina’s Pod-based event structs.
  9. Add #![no_std] and the bpf-entrypoint feature gate.
  10. Port TypeScript tests to Rust using mollusk-svm or native unit tests.