Appearance
Counter
IMPORTANT
The examples are designed to be run in order after completing the quickstart, which will help you set up your environment and get familiar with common commands.
If you are just here to browse, enjoy!
💡 Background
This example implements a simple on-chain counter program at a PDA account. The program supports two operations: initializing a user's counter, and incrementing a user's counter by a specified amount. The counter PDA account stores the following data:
| Size (bytes) | Description |
|---|---|
| 8 | Counter value (u64) |
| 1 | Bump seed (u8) |
All constants for this example are derived programmatically in Rust, then automatically inserted at the top of the assembly program file during a test:
Constants
rs
use solana_sdk::pubkey::Pubkey;
use std::mem::{offset_of, size_of};
/// In an assembly file, for viewable render on docs site.
const LINE_LENGTH: usize = 75;
/// Alignment for stack and account data.
const ALIGNMENT: usize = 8;
pub fn constants() -> Constants {
// Number of accounts for CPI create account instruction.
const N_ACCOUNTS_CPI: usize = 2;
// Number of signer seeds for PDA.
const N_SIGNER_SEEDS_PDA: usize = 2;
// Number of PDAs in CPI.
const N_PDAS: usize = 1;
/// For an account during an instruction.
const MAX_PERMITTED_DATA_INCREASE: usize = 10240;
/// b"system_program" plus two bytes of padding.
const SYSTEM_PROGRAM_DATA_LEN: usize = "system_program".len();
const SYSTEM_PROGRAM_DATA_WITH_PAD_LEN: usize =
SYSTEM_PROGRAM_DATA_LEN + (ALIGNMENT - SYSTEM_PROGRAM_DATA_LEN % ALIGNMENT) % ALIGNMENT;
const ACCOUNT_STORAGE_OVERHEAD: usize = 128;
#[repr(C)]
struct SolInstruction {
program_id_addr: u64,
accounts_addr: u64,
accounts_len: u64,
data_addr: u64,
data_len: u64,
}
#[repr(C)]
struct SolAccountMeta {
pubkey_addr: u64,
is_writable: bool,
is_signer: bool,
pad: [u8; 6],
}
// Packed to avoid introducing intermediate padding during compilation.
#[repr(C, packed)]
struct CreateAccountInstructionData {
variant: u32,
lamports: u64,
space: u64,
owner: Pubkey,
pad: [u8; 4], // For when on stack.
}
#[repr(C)]
struct SolSignerSeed {
addr: u64,
len: u64,
}
#[repr(C)]
struct SolSignerSeeds {
addr: u64,
len: u64,
}
#[repr(C)]
struct SolAccountInfo {
key_addr: u64,
lamports_addr: u64,
data_len: u64,
data_addr: u64,
owner_addr: u64,
rent_epoch: u64,
is_signer: bool,
is_writable: bool,
executable: bool,
pad: [u8; 5],
}
#[repr(C)]
struct Rent {
lamports_per_byte_year: u64,
exemption_threshold: f64,
burn_percent: u8,
pad: [u8; 7],
}
#[repr(C)]
struct StackFrameInit {
system_program_pubkey: Pubkey, // Zero-initialized.
instruction: SolInstruction,
account_metas: [SolAccountMeta; N_ACCOUNTS_CPI],
instruction_data: CreateAccountInstructionData,
account_infos: [SolAccountInfo; N_ACCOUNTS_CPI],
// User pubkey, then bump seed.
signer_seeds: [SolSignerSeed; N_SIGNER_SEEDS_PDA],
signers_seeds: [SolSignerSeeds; N_PDAS],
pda: Pubkey,
rent: Rent,
bump_seed: u8,
}
#[repr(C)]
struct StackFrameInc {
signer_seeds: [SolSignerSeed; N_SIGNER_SEEDS_PDA],
pda: Pubkey,
}
#[repr(C)]
struct PdaAccountData {
counter: u64,
bump_seed: u8,
}
#[repr(C)]
struct MemoryMapInit {
n_accounts: u64,
user: StandardAccount, // Must be empty, or CreateAccount will fail.
pda: StandardAccount, // Reflects state before CreateAccount CPI.
system_program: SystemProgramAccount,
instruction_data_len: u64, // 0u64 for initialize operation.
program_id: Pubkey,
}
#[repr(C)]
struct MemoryMapInc {
n_accounts: u64,
user: StandardAccount, // Might actually have data.
pda: PdaAccountInitialized,
instruction_data_len: u64, // 1u64 for increment operation.
counter_increment: u64,
program_id: Pubkey,
}
#[allow(dead_code)]
#[repr(C)]
struct AccountLayout<const PADDED_DATA_SIZE: usize> {
non_dup_marker: u8,
is_signer: u8,
is_writable: u8,
is_executable: u8,
original_data_len: [u8; 4],
pubkey: [u8; size_of::<Pubkey>()],
owner: [u8; size_of::<Pubkey>()],
lamports: u64,
data_len: u64,
data_padded: [u8; PADDED_DATA_SIZE],
rent_epoch: u64,
}
type StandardAccount = AccountLayout<MAX_PERMITTED_DATA_INCREASE>;
type SystemProgramAccount =
AccountLayout<{ MAX_PERMITTED_DATA_INCREASE + SYSTEM_PROGRAM_DATA_WITH_PAD_LEN }>;
type PdaAccountInitialized =
AccountLayout<{ MAX_PERMITTED_DATA_INCREASE + size_of::<PdaAccountData>() }>;
Constants::new()
.push(
ConstantGroup::new_error_codes()
.push_error(ErrorCode::new("N_ACCOUNTS", "Invalid number of accounts."))
.push_error(ErrorCode::new(
"USER_DATA_LEN",
"User data length is nonzero.",
))
.push_error(ErrorCode::new("PDA_DATA_LEN", "Invalid PDA data length."))
.push_error(ErrorCode::new(
"SYSTEM_PROGRAM_DATA_LEN",
"System Program data length is nonzero.",
))
.push_error(ErrorCode::new(
"PDA_DUPLICATE",
"PDA is a duplicate account.",
))
.push_error(ErrorCode::new(
"SYSTEM_PROGRAM_DUPLICATE",
"System Program is a duplicate account.",
))
.push_error(ErrorCode::new(
"UNABLE_TO_DERIVE_PDA",
"Unable to derive PDA.",
))
.push_error(ErrorCode::new(
"PDA_MISMATCH",
"Passed PDA does not match computed PDA.",
))
.push_error(ErrorCode::new(
"INVALID_INSTRUCTION_DATA_LEN",
"Invalid instruction data length.",
)),
)
.push(
ConstantGroup::new_with_prefix("Size of assorted types.", "SIZE_OF_")
.push(Constant::new(
"PUBKEY",
size_of::<Pubkey>() as u64,
"Size of Pubkey.",
))
.push(Constant::new("U8", size_of::<u8>() as u64, "Size of u8."))
.push(Constant::new(
"U64",
size_of::<u64>() as u64,
"Size of u64.",
))
.push(Constant::new(
"U64_2X",
(size_of::<u64>() * 2) as u64,
"Size of u64 times 2.",
))
.push(Constant::new(
"U64_3X",
(size_of::<u64>() * 3) as u64,
"Size of u64 times 3.",
)),
)
.push(
ConstantGroup::new("Memory map layout.")
.push(Constant::new_hex(
"NON_DUP_MARKER",
0xff,
"Flag that an account is not a duplicate.",
))
.push(Constant::new("DATA_LEN_ZERO", 0, "Data length of zero."))
.push(Constant::new(
"DATA_LEN_SYSTEM_PROGRAM",
"system_program".len() as u64,
"Data length of System Program.",
))
.push(Constant::new(
"N_ACCOUNTS_INCREMENT",
2,
"Number of accounts for increment operation.",
))
.push(Constant::new(
"N_ACCOUNTS_INIT",
3,
"Number of accounts for initialize operation.",
))
.push(Constant::new_offset(
"N_ACCOUNTS",
0,
"Number of accounts in virtual memory map.",
))
.push(Constant::new_offset(
"USER_DATA_LEN",
(offset_of!(MemoryMapInit, user) + offset_of!(StandardAccount, data_len))
as u64,
"User data length.",
))
.push(Constant::new_offset(
"USER_PUBKEY",
(offset_of!(MemoryMapInit, user) + offset_of!(StandardAccount, pubkey)) as u64,
"User pubkey.",
))
.push(Constant::new_offset(
"USER_DATA_TO_PDA_OWNER",
(offset_of!(MemoryMapInit, pda) + offset_of!(StandardAccount, owner)
- (offset_of!(MemoryMapInit, user)
+ offset_of!(StandardAccount, data_padded))) as u64,
"Offset from user account data to PDA owner.",
))
.push(Constant::new_offset(
"PDA_NON_DUP_MARKER",
(offset_of!(MemoryMapInit, pda) + offset_of!(StandardAccount, non_dup_marker))
as u64,
"PDA non-duplicate marker.",
))
.push(Constant::new_offset(
"PDA_PUBKEY",
(offset_of!(MemoryMapInit, pda) + offset_of!(StandardAccount, pubkey)) as u64,
"PDA pubkey.",
))
.push(Constant::new_offset(
"PDA_DATA_LEN",
(offset_of!(MemoryMapInit, pda) + offset_of!(StandardAccount, data_len)) as u64,
"PDA data length.",
))
.push(Constant::new(
"PDA_DATA_WITH_ACCOUNT_OVERHEAD",
(size_of::<u64>() + size_of::<u8>() + ACCOUNT_STORAGE_OVERHEAD) as u64,
"PDA account data length plus account overhead.",
))
.push(Constant::new_offset(
"PDA_COUNTER",
(offset_of!(MemoryMapInit, pda)
+ offset_of!(PdaAccountInitialized, data_padded)
+ offset_of!(PdaAccountData, counter)) as u64,
"PDA counter.",
))
.push(Constant::new_offset(
"PDA_BUMP_SEED",
(offset_of!(MemoryMapInit, pda)
+ offset_of!(PdaAccountInitialized, data_padded)
+ offset_of!(PdaAccountData, bump_seed)) as u64,
"PDA bump seed.",
))
.push(Constant::new_offset(
"SYSTEM_PROGRAM_NON_DUP_MARKER",
(offset_of!(MemoryMapInit, system_program)
+ offset_of!(SystemProgramAccount, non_dup_marker))
as u64,
"System Program non-duplicate marker.",
))
.push(Constant::new_offset(
"SYSTEM_PROGRAM_DATA_LEN",
(offset_of!(MemoryMapInit, system_program)
+ offset_of!(SystemProgramAccount, data_len)) as u64,
"System program data length.",
))
.push(Constant::new_offset(
"PROGRAM_ID_INIT",
offset_of!(MemoryMapInit, program_id) as u64,
"Program ID during initialize operation.",
))
.push(Constant::new_offset(
"INSTRUCTION_DATA_LEN_INC",
offset_of!(MemoryMapInc, instruction_data_len) as u64,
"Instruction data length during increment operation.",
))
.push(Constant::new_offset(
"COUNTER_INCREMENT",
offset_of!(MemoryMapInc, counter_increment) as u64,
"Counter increment value.",
))
.push(Constant::new_offset(
"PROGRAM_ID_INC",
offset_of!(MemoryMapInc, program_id) as u64,
"Program ID during increment operation.",
)),
)
.push(
ConstantGroup::new_with_prefix("CreateAccount instruction data.", "INIT_CPI_")
.push(Constant::new(
"N_ACCOUNTS",
N_ACCOUNTS_CPI as u64,
"Number of accounts for CPI.",
))
.push(Constant::new(
"INSN_DATA_LEN",
(size_of::<u32>() + size_of::<u64>() + size_of::<u64>() + size_of::<Pubkey>())
as u64,
"Length of instruction data.",
))
.push(Constant::new("DISCRIMINATOR", 0, "Discriminator."))
.push(Constant::new(
"N_SIGNERS_SEEDS",
1,
"Number of signers seeds.",
))
.push(Constant::new(
"ACCT_SIZE",
(size_of::<u64>() + size_of::<u8>()) as u64,
"Account size.",
)),
)
.push(
ConstantGroup::new_stack_layout(
"Stack frame layout for increment operation.",
"STK_INC_",
)
.push(Constant::new_offset(
"SEED_0_ADDR",
(size_of::<StackFrameInc>() - (offset_of!(StackFrameInc, signer_seeds))) as u64,
"Pointer to user pubkey.",
))
.push(Constant::new_offset(
"SEED_0_LEN",
(size_of::<StackFrameInc>()
- (offset_of!(StackFrameInc, signer_seeds) + offset_of!(SolSignerSeed, len)))
as u64,
"Length of user pubkey.",
))
.push(Constant::new_offset(
"SEED_1_ADDR",
(size_of::<StackFrameInc>()
- (offset_of!(StackFrameInc, signer_seeds) + size_of::<SolSignerSeed>()))
as u64,
"Pointer to bump seed.",
))
.push(Constant::new_offset(
"SEED_1_LEN",
(size_of::<StackFrameInc>()
- (offset_of!(StackFrameInc, signer_seeds)
+ size_of::<SolSignerSeed>()
+ offset_of!(SolSignerSeed, len))) as u64,
"Length of bump seed.",
))
.push(Constant::new_offset(
"PDA",
(size_of::<StackFrameInc>() - offset_of!(StackFrameInc, pda)) as u64,
"Pointer to PDA.",
)),
)
.push(
ConstantGroup::new_stack_layout(
"Stack frame layout for initialize operation.",
"STK_INIT_",
)
.push(Constant::new_offset(
"SYSTEM_PROGRAM_PUBKEY",
(size_of::<StackFrameInit>() - offset_of!(StackFrameInit, system_program_pubkey))
as u64,
"System Program pubkey for CreateAccount CPI.",
))
.push(Constant::new_offset(
"INSN",
(size_of::<StackFrameInit>() - offset_of!(StackFrameInit, instruction)) as u64,
"SolInstruction for CreateAccount CPI.",
))
.push(Constant::new_offset(
"INSN_ACCOUNTS_ADDR",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, instruction)
+ offset_of!(SolInstruction, accounts_addr))) as u64,
"Accounts address in SolInstruction.",
))
.push(Constant::new_offset(
"INSN_ACCOUNTS_LEN",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, instruction)
+ offset_of!(SolInstruction, accounts_len))) as u64,
"Accounts length in SolInstruction.",
))
.push(Constant::new_offset(
"INSN_DATA_ADDR",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, instruction)
+ offset_of!(SolInstruction, data_addr))) as u64,
"Data address in SolInstruction.",
))
.push(Constant::new_offset(
"INSN_DATA_LEN",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, instruction)
+ offset_of!(SolInstruction, data_len))) as u64,
"Data length in SolInstruction.",
))
.push(Constant::new_offset(
"SYSTEM_PROGRAM_PUBKEY_TO_ACCOUNT_METAS",
(offset_of!(StackFrameInit, account_metas)
- offset_of!(StackFrameInit, system_program_pubkey)) as u64,
"Offset from System Program pubkey to account metas.",
))
.push(Constant::new_offset(
"ACCOUNT_METAS_TO_INSN_DATA",
(offset_of!(StackFrameInit, instruction_data)
- offset_of!(StackFrameInit, account_metas)) as u64,
"Offset from account metas to instruction data.",
))
.push(Constant::new_offset(
"INSN_DATA",
(size_of::<StackFrameInit>() - offset_of!(StackFrameInit, instruction_data)) as u64,
"CreateAccount instruction data.",
))
.push(Constant::new_maybe_unaligned_offset(
"INSN_DATA_LAMPORTS",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, instruction_data)
+ offset_of!(CreateAccountInstructionData, lamports)))
as u64,
"Offset of lamports field inside CreateAccount instruction data.",
))
.push(Constant::new_maybe_unaligned_offset(
"INSN_DATA_SPACE",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, instruction_data)
+ offset_of!(CreateAccountInstructionData, space))) as u64,
"Offset of space field inside CreateAccount instruction data.",
))
.push(Constant::new_maybe_unaligned_offset(
"INSN_DATA_OWNER",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, instruction_data)
+ offset_of!(CreateAccountInstructionData, owner))) as u64,
"Offset of owner field inside CreateAccount instruction data.",
))
.push(Constant::new_offset(
"ACCT_INFOS",
(size_of::<StackFrameInit>() - offset_of!(StackFrameInit, account_infos)) as u64,
"User account infos.",
))
.push(Constant::new_maybe_unaligned_offset(
"ACCT_META_USER_PUBKEY_ADDR",
(size_of::<StackFrameInit>() - offset_of!(StackFrameInit, account_metas)) as u64,
"User account meta pubkey address.",
))
.push(Constant::new_maybe_unaligned_offset(
"ACCT_META_USER_IS_WRITABLE",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_metas)
+ offset_of!(SolAccountMeta, is_writable))) as u64,
"User account meta is_writable.",
))
.push(Constant::new_maybe_unaligned_offset(
"ACCT_META_USER_IS_SIGNER",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_metas)
+ offset_of!(SolAccountMeta, is_signer))) as u64,
"User account meta is_signer.",
))
.push(Constant::new_maybe_unaligned_offset(
"ACCT_META_PDA_PUBKEY_ADDR",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_metas) + size_of::<SolAccountMeta>()))
as u64,
"PDA account meta pubkey address.",
))
.push(Constant::new_maybe_unaligned_offset(
"ACCT_META_PDA_IS_WRITABLE",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_metas)
+ size_of::<SolAccountMeta>()
+ offset_of!(SolAccountMeta, is_writable))) as u64,
"PDA account meta is_writable.",
))
.push(Constant::new_maybe_unaligned_offset(
"ACCT_META_PDA_IS_SIGNER",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_metas)
+ size_of::<SolAccountMeta>()
+ offset_of!(SolAccountMeta, is_signer))) as u64,
"PDA account meta is_signer.",
))
.push(Constant::new_offset(
"ACCT_INFO_USER_KEY_ADDR",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_infos)
+ offset_of!(SolAccountInfo, key_addr))) as u64,
"User account info key address.",
))
.push(Constant::new_offset(
"ACCT_INFO_PDA_KEY_ADDR",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_infos)
+ size_of::<SolAccountInfo>()
+ offset_of!(SolAccountInfo, key_addr))) as u64,
"PDA account info key address.",
))
.push(Constant::new_offset(
"ACCT_INFO_USER_LAMPORTS_ADDR",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_infos)
+ offset_of!(SolAccountInfo, lamports_addr))) as u64,
"User account info Lamports pointer.",
))
.push(Constant::new_offset(
"ACCT_INFO_PDA_LAMPORTS_ADDR",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_infos)
+ size_of::<SolAccountInfo>()
+ offset_of!(SolAccountInfo, lamports_addr))) as u64,
"PDA account info Lamports pointer.",
))
.push(Constant::new_offset(
"ACCT_INFO_USER_OWNER_ADDR",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_infos)
+ offset_of!(SolAccountInfo, owner_addr))) as u64,
"User account info owner pubkey pointer.",
))
.push(Constant::new_offset(
"ACCT_INFO_PDA_OWNER_ADDR",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_infos)
+ size_of::<SolAccountInfo>()
+ offset_of!(SolAccountInfo, owner_addr))) as u64,
"PDA account info owner pubkey pointer.",
))
.push(Constant::new_offset(
"ACCT_INFO_USER_DATA_ADDR",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_infos)
+ offset_of!(SolAccountInfo, data_addr))) as u64,
"User account info data pointer.",
))
.push(Constant::new_offset(
"ACCT_INFO_PDA_DATA_ADDR",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_infos)
+ size_of::<SolAccountInfo>()
+ offset_of!(SolAccountInfo, data_addr))) as u64,
"PDA account info data pointer.",
))
.push(Constant::new_maybe_unaligned_offset(
"ACCT_INFO_USER_IS_SIGNER",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_infos)
+ offset_of!(SolAccountInfo, is_signer))) as u64,
"User account info is_signer.",
))
.push(Constant::new_maybe_unaligned_offset(
"ACCT_INFO_USER_IS_WRITABLE",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_infos)
+ offset_of!(SolAccountInfo, is_writable))) as u64,
"User account info is_writable.",
))
.push(Constant::new_maybe_unaligned_offset(
"ACCT_INFO_PDA_IS_SIGNER",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_infos)
+ size_of::<SolAccountInfo>()
+ offset_of!(SolAccountInfo, is_signer))) as u64,
"PDA account info is_signer.",
))
.push(Constant::new_maybe_unaligned_offset(
"ACCT_INFO_PDA_IS_WRITABLE",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, account_infos)
+ size_of::<SolAccountInfo>()
+ offset_of!(SolAccountInfo, is_writable))) as u64,
"PDA account info is_writable.",
))
.push(Constant::new_offset(
"SEED_0_ADDR",
(size_of::<StackFrameInit>() - (offset_of!(StackFrameInit, signer_seeds))) as u64,
"Pointer to user pubkey.",
))
.push(Constant::new_offset(
"SEED_0_LEN",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, signer_seeds) + offset_of!(SolSignerSeed, len)))
as u64,
"Length of user pubkey.",
))
.push(Constant::new_offset(
"SEED_1_ADDR",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, signer_seeds) + size_of::<SolSignerSeed>()))
as u64,
"Pointer to bump seed.",
))
.push(Constant::new_offset(
"SEED_1_LEN",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, signer_seeds)
+ size_of::<SolSignerSeed>()
+ offset_of!(SolSignerSeed, len))) as u64,
"Length of bump seed.",
))
.push(Constant::new_offset(
"SIGNERS_SEEDS",
(size_of::<StackFrameInit>() - offset_of!(StackFrameInit, signers_seeds)) as u64,
"Pointer to signer seeds array.",
))
.push(Constant::new_offset(
"SIGNER_SEEDS_0_LEN",
(size_of::<StackFrameInit>()
- (offset_of!(StackFrameInit, signers_seeds) + offset_of!(SolSignerSeeds, len)))
as u64,
"Pointer to signer seeds array element 0 length field.",
))
.push(Constant::new_offset(
"PDA",
(size_of::<StackFrameInit>() - (offset_of!(StackFrameInit, pda))) as u64,
"PDA.",
))
.push(Constant::new_offset(
"RENT",
(size_of::<StackFrameInit>() - (offset_of!(StackFrameInit, rent))) as u64,
"Rent struct return.",
))
.push(Constant::new_offset(
"BUMP_SEED",
(size_of::<StackFrameInit>() - (offset_of!(StackFrameInit, bump_seed))) as u64,
"Bump seed.",
)),
)
.push(
ConstantGroup::new("Assorted constants.")
.push(Constant::new("NO_OFFSET", 0, "Offset of zero."))
.push(Constant::new(
"SUCCESS",
0,
"Indicates successful operation.",
))
.push(Constant::new("BOOL_TRUE", 1, "Boolean true."))
.push(Constant::new_hex(
"BOOL_TRUE_2X",
0x0101, // 0xffff would be simpler but VM rejects it.
"Double wide boolean true for two consecutive fields.",
))
.push(Constant::new(
"N_SIGNER_SEEDS",
2,
"Number of signer seeds for PDA.",
))
.push(Constant::new(
"COMPARE_EQUAL",
0,
"Compare result indicating equality.",
)),
)
}
// Individual constant definition.
struct Constant {
name: &'static str,
value: u64,
is_offset: bool,
may_be_unaligned: bool,
is_hex: bool,
comment: Comment,
}
impl Constant {
const OFFSET_SUFFIX: &str = "_OFF";
fn create(
name: &'static str,
value: u64,
is_offset: bool,
may_be_unaligned: bool,
is_hex: bool,
comment: &'static str,
) -> Self {
assert!(
!name.ends_with(Self::OFFSET_SUFFIX),
"Constant name must not end with {} (added automatically for offsets): {name}",
Self::OFFSET_SUFFIX
);
if is_offset {
assert!(
value <= i16::MAX as u64,
"Offset value must fit in i16: {name} = {value}"
);
}
Self {
name,
value,
is_offset,
may_be_unaligned,
is_hex,
comment: Comment::new(comment),
}
}
fn new(name: &'static str, value: u64, comment: &'static str) -> Self {
Self::create(name, value, false, false, false, comment)
}
fn new_hex(name: &'static str, value: u64, comment: &'static str) -> Self {
Self::create(name, value, false, false, true, comment)
}
fn new_offset(name: &'static str, value: u64, comment: &'static str) -> Self {
Self::create(name, value, true, false, false, comment)
}
fn new_maybe_unaligned_offset(name: &'static str, value: u64, comment: &'static str) -> Self {
Self::create(name, value, true, true, false, comment)
}
fn asm_name(&self) -> String {
if self.is_offset {
format!("{}{}", self.name, Self::OFFSET_SUFFIX)
} else {
self.name.to_string()
}
}
}
// Error code definition.
struct ErrorCode {
name: &'static str,
comment: Comment,
}
impl ErrorCode {
const PREFIX: &str = "E_";
fn new(name: &'static str, comment: &'static str) -> Self {
Self {
name,
comment: Comment::new(comment),
}
}
fn asm_name(&self) -> String {
format!("{}{}", Self::PREFIX, self.name)
}
}
// Group of related constants.
enum ConstantGroup {
// Standard group of constants with optional prefix.
Standard {
comment: Comment,
constants: Vec<Constant>,
prefix: Option<&'static str>,
is_stack: bool,
},
// Error codes group where values are auto-incremented starting from 1.
ErrorCodes {
comment: Comment,
codes: Vec<ErrorCode>,
},
}
impl ConstantGroup {
fn new(comment: &'static str) -> Self {
Self::Standard {
comment: Comment::new(comment),
constants: Vec::new(),
prefix: None,
is_stack: false,
}
}
fn new_with_prefix(comment: &'static str, prefix: &'static str) -> Self {
Self::Standard {
comment: Comment::new(comment),
constants: Vec::new(),
prefix: Some(prefix),
is_stack: false,
}
}
fn new_stack_layout(comment: &'static str, prefix: &'static str) -> Self {
Self::Standard {
comment: Comment::new(comment),
constants: Vec::new(),
prefix: Some(prefix),
is_stack: true,
}
}
fn new_error_codes() -> Self {
Self::ErrorCodes {
comment: Comment::new("Error codes."),
codes: Vec::new(),
}
}
fn push(mut self, constant: Constant) -> Self {
match &mut self {
Self::Standard {
constants,
is_stack,
..
} => {
if *is_stack {
assert!(
constant.is_offset,
"Stack layout group must only contain offsets: {}",
constant.name
);
if !constant.may_be_unaligned {
assert!(
constant.value.is_multiple_of(ALIGNMENT as u64),
"Stack offset must be {}-byte aligned: {} = {}",
ALIGNMENT,
constant.name,
constant.value
);
}
}
constants.push(constant);
}
Self::ErrorCodes { .. } => panic!("Use push_error for error code groups"),
}
self
}
fn push_error(mut self, error: ErrorCode) -> Self {
match &mut self {
Self::Standard { .. } => panic!("Use push for standard groups"),
Self::ErrorCodes { codes, .. } => codes.push(error),
}
self
}
fn comment(&self) -> &Comment {
match self {
Self::Standard { comment, .. } => comment,
Self::ErrorCodes { comment, .. } => comment,
}
}
fn prefix(&self) -> Option<&'static str> {
match self {
Self::Standard { prefix, .. } => *prefix,
Self::ErrorCodes { .. } => Some(ErrorCode::PREFIX),
}
}
}
// Top-level container for all constant groups.
pub struct Constants {
groups: Vec<ConstantGroup>,
}
impl Constants {
fn new() -> Self {
Self { groups: Vec::new() }
}
fn push(mut self, group: ConstantGroup) -> Self {
self.groups.push(group);
self
}
pub fn to_asm(&self) -> String {
use std::collections::HashSet;
// Check for duplicate prefixes.
let mut seen_prefixes: HashSet<&str> = HashSet::new();
for group in &self.groups {
if let Some(prefix) = group.prefix() {
assert!(
seen_prefixes.insert(prefix),
"Duplicate group prefix: {prefix:?}",
);
}
}
// Check for duplicate constant names (after applying prefix and suffix).
let mut seen_names: HashSet<String> = HashSet::new();
for group in &self.groups {
match group {
ConstantGroup::Standard {
constants, prefix, ..
} => {
for constant in constants {
let name = match prefix {
Some(prefix) => format!("{}{}", prefix, constant.asm_name()),
None => constant.asm_name(),
};
assert!(
seen_names.insert(name.clone()),
"Duplicate constant name: {name}"
);
}
}
ConstantGroup::ErrorCodes { codes, .. } => {
for code in codes {
assert!(
seen_names.insert(code.asm_name()),
"Duplicate constant name: {}",
code.asm_name()
);
}
}
}
}
let mut output = String::new();
for (i, group) in self.groups.iter().enumerate() {
if i > 0 {
output.push('\n');
}
output.push_str(&format!("# {}\n", group.comment().as_str()));
output.push_str(&format!(
"# {}\n",
"-".repeat(group.comment().as_str().len())
));
match group {
ConstantGroup::Standard {
constants, prefix, ..
} => {
for constant in constants {
let value = if constant.is_hex {
format!("0x{:x}", constant.value)
} else {
constant.value.to_string()
};
let name = match prefix {
Some(prefix) => format!("{}{}", prefix, constant.asm_name()),
None => constant.asm_name(),
};
// Try inline comment: ".equ NAME, VALUE # Comment."
let inline =
format!(".equ {}, {} # {}", name, value, constant.comment.as_str());
if inline.len() <= LINE_LENGTH {
output.push_str(&inline);
output.push('\n');
} else {
// Comment on separate line.
output.push_str(&format!(
"# {}\n.equ {}, {}\n",
constant.comment.as_str(),
name,
value
));
}
}
}
ConstantGroup::ErrorCodes { codes, .. } => {
for (idx, code) in codes.iter().enumerate() {
let value = 1 + idx as u64; // Error codes start at 1.
let name = code.asm_name();
// Try inline comment: ".equ NAME, VALUE # Comment."
let inline =
format!(".equ {}, {} # {}", name, value, code.comment.as_str());
if inline.len() <= LINE_LENGTH {
output.push_str(&inline);
output.push('\n');
} else {
// Comment on separate line.
output.push_str(&format!(
"# {}\n.equ {}, {}\n",
code.comment.as_str(),
name,
value
));
}
}
}
}
}
output
}
pub fn get(&self, name: &str) -> u64 {
for group in &self.groups {
match group {
ConstantGroup::Standard {
constants, prefix, ..
} => {
for constant in constants {
let full_name = match prefix {
Some(p) => format!("{}{}", p, constant.asm_name()),
None => constant.asm_name(),
};
if full_name == name {
return constant.value;
}
}
}
ConstantGroup::ErrorCodes { codes, .. } => {
for (idx, code) in codes.iter().enumerate() {
if code.asm_name() == name {
return (1 + idx) as u64;
}
}
}
}
}
panic!("Constant not found: {name}");
}
}
// Comment type with validation.
struct Comment(&'static str);
impl Comment {
const MAX_LENGTH: usize = LINE_LENGTH - 2; // Account for "# " prefix.
fn new(text: &'static str) -> Self {
assert!(!text.is_empty(), "Comment must not be empty");
assert!(text.ends_with('.'), "Comment must end with '.': {text}");
assert!(
text.len() <= Self::MAX_LENGTH,
"Comment must not exceed {} characters: {text}",
Self::MAX_LENGTH
);
Self(text)
}
fn as_str(&self) -> &'static str {
self.0
}
}rs
#[test]
fn test_asm_file_constants() {
const GLOBAL_ENTRYPOINT: &str = ".global entrypoint";
// Parse assembly file.
let asm_path = setup_test(ProgramLanguage::Assembly)
.asm_source_path
.expect("Assembly source file not found");
let content = fs::read_to_string(&asm_path).expect("Failed to read assembly file");
let global_pos = content
.find(GLOBAL_ENTRYPOINT)
.expect("Could not find '.global entrypoint' in assembly file");
// Overwrite assembly file with updated constants, asserting nothing changed.
let after_global = &content[global_pos..];
let new_content = format!("{}\n{}", constants().to_asm(), after_global);
let changed = new_content != content;
fs::write(&asm_path, new_content).expect("Failed to write assembly file");
assert!(
!changed,
"Assembly file constants were out of date and have been updated. Please re-run the test."
);
}asm
# Error codes.
# ------------
.equ E_N_ACCOUNTS, 1 # Invalid number of accounts.
.equ E_USER_DATA_LEN, 2 # User data length is nonzero.
.equ E_PDA_DATA_LEN, 3 # Invalid PDA data length.
.equ E_SYSTEM_PROGRAM_DATA_LEN, 4 # System Program data length is nonzero.
.equ E_PDA_DUPLICATE, 5 # PDA is a duplicate account.
.equ E_SYSTEM_PROGRAM_DUPLICATE, 6 # System Program is a duplicate account.
.equ E_UNABLE_TO_DERIVE_PDA, 7 # Unable to derive PDA.
.equ E_PDA_MISMATCH, 8 # Passed PDA does not match computed PDA.
.equ E_INVALID_INSTRUCTION_DATA_LEN, 9 # Invalid instruction data length.
# Size of assorted types.
# -----------------------
.equ SIZE_OF_PUBKEY, 32 # Size of Pubkey.
.equ SIZE_OF_U8, 1 # Size of u8.
.equ SIZE_OF_U64, 8 # Size of u64.
.equ SIZE_OF_U64_2X, 16 # Size of u64 times 2.
.equ SIZE_OF_U64_3X, 24 # Size of u64 times 3.
# Memory map layout.
# ------------------
.equ NON_DUP_MARKER, 0xff # Flag that an account is not a duplicate.
.equ DATA_LEN_ZERO, 0 # Data length of zero.
.equ DATA_LEN_SYSTEM_PROGRAM, 14 # Data length of System Program.
.equ N_ACCOUNTS_INCREMENT, 2 # Number of accounts for increment operation.
.equ N_ACCOUNTS_INIT, 3 # Number of accounts for initialize operation.
.equ N_ACCOUNTS_OFF, 0 # Number of accounts in virtual memory map.
.equ USER_DATA_LEN_OFF, 88 # User data length.
.equ USER_PUBKEY_OFF, 16 # User pubkey.
# Offset from user account data to PDA owner.
.equ USER_DATA_TO_PDA_OWNER_OFF, 10288
.equ PDA_NON_DUP_MARKER_OFF, 10344 # PDA non-duplicate marker.
.equ PDA_PUBKEY_OFF, 10352 # PDA pubkey.
.equ PDA_DATA_LEN_OFF, 10424 # PDA data length.
# PDA account data length plus account overhead.
.equ PDA_DATA_WITH_ACCOUNT_OVERHEAD, 137
.equ PDA_COUNTER_OFF, 10432 # PDA counter.
.equ PDA_BUMP_SEED_OFF, 10440 # PDA bump seed.
# System Program non-duplicate marker.
.equ SYSTEM_PROGRAM_NON_DUP_MARKER_OFF, 20680
.equ SYSTEM_PROGRAM_DATA_LEN_OFF, 20760 # System program data length.
.equ PROGRAM_ID_INIT_OFF, 31040 # Program ID during initialize operation.
# Instruction data length during increment operation.
.equ INSTRUCTION_DATA_LEN_INC_OFF, 20696
.equ COUNTER_INCREMENT_OFF, 20704 # Counter increment value.
.equ PROGRAM_ID_INC_OFF, 20712 # Program ID during increment operation.
# CreateAccount instruction data.
# -------------------------------
.equ INIT_CPI_N_ACCOUNTS, 2 # Number of accounts for CPI.
.equ INIT_CPI_INSN_DATA_LEN, 52 # Length of instruction data.
.equ INIT_CPI_DISCRIMINATOR, 0 # Discriminator.
.equ INIT_CPI_N_SIGNERS_SEEDS, 1 # Number of signers seeds.
.equ INIT_CPI_ACCT_SIZE, 9 # Account size.
# Stack frame layout for increment operation.
# -------------------------------------------
.equ STK_INC_SEED_0_ADDR_OFF, 64 # Pointer to user pubkey.
.equ STK_INC_SEED_0_LEN_OFF, 56 # Length of user pubkey.
.equ STK_INC_SEED_1_ADDR_OFF, 48 # Pointer to bump seed.
.equ STK_INC_SEED_1_LEN_OFF, 40 # Length of bump seed.
.equ STK_INC_PDA_OFF, 32 # Pointer to PDA.
# Stack frame layout for initialize operation.
# --------------------------------------------
# System Program pubkey for CreateAccount CPI.
.equ STK_INIT_SYSTEM_PROGRAM_PUBKEY_OFF, 384
.equ STK_INIT_INSN_OFF, 352 # SolInstruction for CreateAccount CPI.
# Accounts address in SolInstruction.
.equ STK_INIT_INSN_ACCOUNTS_ADDR_OFF, 344
# Accounts length in SolInstruction.
.equ STK_INIT_INSN_ACCOUNTS_LEN_OFF, 336
.equ STK_INIT_INSN_DATA_ADDR_OFF, 328 # Data address in SolInstruction.
.equ STK_INIT_INSN_DATA_LEN_OFF, 320 # Data length in SolInstruction.
# Offset from System Program pubkey to account metas.
.equ STK_INIT_SYSTEM_PROGRAM_PUBKEY_TO_ACCOUNT_METAS_OFF, 72
# Offset from account metas to instruction data.
.equ STK_INIT_ACCOUNT_METAS_TO_INSN_DATA_OFF, 32
.equ STK_INIT_INSN_DATA_OFF, 280 # CreateAccount instruction data.
# Offset of lamports field inside CreateAccount instruction data.
.equ STK_INIT_INSN_DATA_LAMPORTS_OFF, 276
# Offset of space field inside CreateAccount instruction data.
.equ STK_INIT_INSN_DATA_SPACE_OFF, 268
# Offset of owner field inside CreateAccount instruction data.
.equ STK_INIT_INSN_DATA_OWNER_OFF, 260
.equ STK_INIT_ACCT_INFOS_OFF, 224 # User account infos.
# User account meta pubkey address.
.equ STK_INIT_ACCT_META_USER_PUBKEY_ADDR_OFF, 312
# User account meta is_writable.
.equ STK_INIT_ACCT_META_USER_IS_WRITABLE_OFF, 304
# User account meta is_signer.
.equ STK_INIT_ACCT_META_USER_IS_SIGNER_OFF, 303
# PDA account meta pubkey address.
.equ STK_INIT_ACCT_META_PDA_PUBKEY_ADDR_OFF, 296
# PDA account meta is_writable.
.equ STK_INIT_ACCT_META_PDA_IS_WRITABLE_OFF, 288
# PDA account meta is_signer.
.equ STK_INIT_ACCT_META_PDA_IS_SIGNER_OFF, 287
# User account info key address.
.equ STK_INIT_ACCT_INFO_USER_KEY_ADDR_OFF, 224
# PDA account info key address.
.equ STK_INIT_ACCT_INFO_PDA_KEY_ADDR_OFF, 168
# User account info Lamports pointer.
.equ STK_INIT_ACCT_INFO_USER_LAMPORTS_ADDR_OFF, 216
# PDA account info Lamports pointer.
.equ STK_INIT_ACCT_INFO_PDA_LAMPORTS_ADDR_OFF, 160
# User account info owner pubkey pointer.
.equ STK_INIT_ACCT_INFO_USER_OWNER_ADDR_OFF, 192
# PDA account info owner pubkey pointer.
.equ STK_INIT_ACCT_INFO_PDA_OWNER_ADDR_OFF, 136
# User account info data pointer.
.equ STK_INIT_ACCT_INFO_USER_DATA_ADDR_OFF, 200
# PDA account info data pointer.
.equ STK_INIT_ACCT_INFO_PDA_DATA_ADDR_OFF, 144
# User account info is_signer.
.equ STK_INIT_ACCT_INFO_USER_IS_SIGNER_OFF, 176
# User account info is_writable.
.equ STK_INIT_ACCT_INFO_USER_IS_WRITABLE_OFF, 175
# PDA account info is_signer.
.equ STK_INIT_ACCT_INFO_PDA_IS_SIGNER_OFF, 120
# PDA account info is_writable.
.equ STK_INIT_ACCT_INFO_PDA_IS_WRITABLE_OFF, 119
.equ STK_INIT_SEED_0_ADDR_OFF, 112 # Pointer to user pubkey.
.equ STK_INIT_SEED_0_LEN_OFF, 104 # Length of user pubkey.
.equ STK_INIT_SEED_1_ADDR_OFF, 96 # Pointer to bump seed.
.equ STK_INIT_SEED_1_LEN_OFF, 88 # Length of bump seed.
.equ STK_INIT_SIGNERS_SEEDS_OFF, 80 # Pointer to signer seeds array.
# Pointer to signer seeds array element 0 length field.
.equ STK_INIT_SIGNER_SEEDS_0_LEN_OFF, 72
.equ STK_INIT_PDA_OFF, 64 # PDA.
.equ STK_INIT_RENT_OFF, 32 # Rent struct return.
.equ STK_INIT_BUMP_SEED_OFF, 8 # Bump seed.
# Assorted constants.
# -------------------
.equ NO_OFFSET, 0 # Offset of zero.
.equ SUCCESS, 0 # Indicates successful operation.
.equ BOOL_TRUE, 1 # Boolean true.
# Double wide boolean true for two consecutive fields.
.equ BOOL_TRUE_2X, 0x101
.equ N_SIGNER_SEEDS, 2 # Number of signer seeds for PDA.
.equ COMPARE_EQUAL, 0 # Compare result indicating equality.
.global entrypoint
entrypoint:
ldxdw r2, [r1 + N_ACCOUNTS_OFF] # Get n accounts from input buffer.
jeq r2, N_ACCOUNTS_INCREMENT, increment # Fast path to cheap operation.
jeq r2, N_ACCOUNTS_INIT, initialize # Low priority, expensive anyways.
mov64 r0, E_N_ACCOUNTS # Else fail.
exit
initialize:
# Check input memory map.
# -----------------------
ldxdw r2, [r1 + USER_DATA_LEN_OFF] # Get user data length.
jne r2, DATA_LEN_ZERO, e_user_data_len # Exit if user account has data.
ldxb r2, [r1 + PDA_NON_DUP_MARKER_OFF] # Check if PDA is a duplicate.
jne r2, NON_DUP_MARKER, e_pda_duplicate # Exit if PDA is a duplicate.
ldxdw r2, [r1 + PDA_DATA_LEN_OFF] # Get PDA data length.
jne r2, DATA_LEN_ZERO, e_pda_data_len # Exit if PDA account has data.
# Exit early if System Program is a duplicate.
ldxb r2, [r1 + SYSTEM_PROGRAM_NON_DUP_MARKER_OFF]
jne r2, NON_DUP_MARKER, e_system_program_duplicate
# Exit early if System Program data length is invalid.
ldxdw r2, [r1 + SYSTEM_PROGRAM_DATA_LEN_OFF]
jne r2, DATA_LEN_SYSTEM_PROGRAM, e_system_program_data_len
# Initialize signer seed for user pubkey.
# ---------------------------------------
mov64 r2, r1 # Get input buffer pointer.
add64 r2, USER_PUBKEY_OFF # Update pointer to point at user pubkey.
# Store pointer in seed 0 pointer field.
stxdw [r10 - STK_INIT_SEED_0_ADDR_OFF], r2
# Store length in seed 0 length field (32-bit immediate).
stdw [r10 - STK_INIT_SEED_0_LEN_OFF], SIZE_OF_PUBKEY
# Initialize signer seed for PDA bump key.
# ----------------------------------------
mov64 r2, r10 # Get stack frame pointer.
sub64 r2, STK_INIT_BUMP_SEED_OFF # Update to point at PDA bump seed.
# Store pointer in seed 1 pointer field.
stxdw [r10 - STK_INIT_SEED_1_ADDR_OFF], r2
# Store length in seed 1 length field (32-bit immediate).
stdw [r10 - STK_INIT_SEED_1_LEN_OFF], SIZE_OF_U8
# Compute PDA.
# ------------
mov64 r9, r1 # Store input buffer pointer for later.
mov64 r1, r10 # Get stack frame pointer.
# Update to point at user pubkey signer seed.
sub64 r1, STK_INIT_SEED_0_ADDR_OFF
mov64 r2, 1 # Indicate single signer seed (user pubkey).
mov64 r3, r9 # Get input buffer pointer.
add64 r3, PROGRAM_ID_INIT_OFF # Update to point at program ID.
mov64 r4, r10 # Get stack frame pointer.
sub64 r4, STK_INIT_PDA_OFF # Update to point to PDA region on stack.
mov64 r5, r10 # Get stack frame pointer.
sub64 r5, STK_INIT_BUMP_SEED_OFF # Update to point to bump seed region.
call sol_try_find_program_address # Find PDA.
# Skip check to error out if unable to derive a PDA (failure to derive
# is practically impossible to test since odds of not finding bump seed
# are astronomically low):
# ```
# jne r0, SUCCESS, e_unable_to_derive_pda
# ```
mov64 r1, r9 # Restore input buffer pointer.
# Compare computed PDA against passed account.
# --------------------------------------------
# Update input buffer pointer to point to passed PDA.
add64 r1, PDA_PUBKEY_OFF
# As an optimization, store this pointer on the stack in the account
# meta and info for the PDA, rather than deriving the pointer again.
# Note that this must point to the pubkey in the input memory map, not
# the one on the stack, otherwise the CPI will fail.
stxdw [r10 - STK_INIT_ACCT_META_PDA_PUBKEY_ADDR_OFF], r1
stxdw [r10 - STK_INIT_ACCT_INFO_PDA_KEY_ADDR_OFF], r1
mov64 r2, r10 # Get stack frame pointer.
sub64 r2, STK_INIT_PDA_OFF # Update to point to computed PDA.
# Compare the pubkey values in 64-bit chunks, which is less CUs than:
# ```
# mov64 r3, SIZE_OF_PUBKEY # Flag size of bytes to compare.
# mov64 r4, r10 # Get stack frame pointer.
# sub64 r4, STK_INIT_MEMCMP_RESULT_OFF # Update to point to result.
# call sol_memcmp_
# ldxw r2, [r4 + NO_OFFSET] # Get compare result.
# jne r2, COMPARE_EQUAL, e_pda_mismatch # Error out if PDA mismatch.
# ```
ldxdw r3, [r1 + 0]
ldxdw r4, [r2 + 0]
jne r3, r4, e_pda_mismatch
ldxdw r3, [r1 + SIZE_OF_U64]
ldxdw r4, [r2 + SIZE_OF_U64]
jne r3, r4, e_pda_mismatch
ldxdw r3, [r1 + SIZE_OF_U64_2X]
ldxdw r4, [r2 + SIZE_OF_U64_2X]
jne r3, r4, e_pda_mismatch
ldxdw r3, [r1 + SIZE_OF_U64_3X]
ldxdw r4, [r2 + SIZE_OF_U64_3X]
jne r3, r4, e_pda_mismatch
# Skip input buffer restoration since next block overwrites r1:
# ```
# mov64 r1, r9 # Restore input buffer pointer.
# ```
# Calculate Lamports required for new account.
# --------------------------------------------
mov64 r1, r10 # Get stack frame pointer.
sub64 r1, STK_INIT_RENT_OFF # Update to point to Rent struct.
call sol_get_rent_sysvar # Get Rent struct.
ldxdw r2, [r1 + NO_OFFSET] # Get Lamports per byte field.
# Multiply by sum of PDA account data length, account storage overhead.
mul64 r2, PDA_DATA_WITH_ACCOUNT_OVERHEAD
# Store value directly in instruction data on stack.
stxdw [r10 - STK_INIT_INSN_DATA_LAMPORTS_OFF], r2
# Skip input buffer restoration since block after next overwrites r1:
# ```
# mov64 r1, r9 # Restore input buffer pointer.
# ```
# Populate SolInstruction on stack.
# ---------------------------------
mov64 r3, r10 # Get stack frame pointer for stepping through stack.
# Update to point to zero-initialized System Program pubkey on stack.
sub64 r3, STK_INIT_SYSTEM_PROGRAM_PUBKEY_OFF
stxdw [r10 - STK_INIT_INSN_OFF], r3 # Store as CPI program ID.
# Advance to point to account metas.
add64 r3, STK_INIT_SYSTEM_PROGRAM_PUBKEY_TO_ACCOUNT_METAS_OFF
# Store pointer to account metas as CPI account metas address.
stxdw [r10 - STK_INIT_INSN_ACCOUNTS_ADDR_OFF], r3
# Store number of CPI accounts (fits in 32-bit immediate).
stdw [r10 - STK_INIT_INSN_ACCOUNTS_LEN_OFF], INIT_CPI_N_ACCOUNTS
# Advance to point to instruction data.
add64 r3, STK_INIT_ACCOUNT_METAS_TO_INSN_DATA_OFF
stxdw [r10 - STK_INIT_INSN_DATA_ADDR_OFF], r3 # Store CPI data address.
# Store instruction data length (fits in 32-bit immediate).
stdw [r10 - STK_INIT_INSN_DATA_LEN_OFF], INIT_CPI_INSN_DATA_LEN
# Populate CreateAccount instruction data on stack.
# ---------------------------------------------------------------------
# - Discriminator is already set to 0 since stack is zero initialized.
# - Lamports field was already set in the minimum balance calculation.
# ---------------------------------------------------------------------
# Store the data length of the account to create (fits in 32 bits).
stdw [r10 - STK_INIT_INSN_DATA_SPACE_OFF], INIT_CPI_ACCT_SIZE
mov64 r1, r10 # Get pointer to stack frame.
sub64 r1, STK_INIT_INSN_DATA_OWNER_OFF # Point to new owner field.
mov64 r2, r9 # Get input buffer pointer.
add64 r2, PROGRAM_ID_INIT_OFF # Point to program ID.
# Copy the pubkey value in 64-bit chunks, which is less CUs than:
# ```
# mov64 r3, SIZE_OF_PUBKEY # Set length of bytes to copy.
# call sol_memcpy_ # Copy program ID into CreateAccount owner field.
# ```
ldxdw r3, [r2 + 0]
stxdw [r1 + 0], r3
ldxdw r3, [r2 + SIZE_OF_U64]
stxdw [r1 + SIZE_OF_U64], r3
ldxdw r3, [r2 + SIZE_OF_U64_2X]
stxdw [r1 + SIZE_OF_U64_2X], r3
ldxdw r3, [r2 + SIZE_OF_U64_3X]
stxdw [r1 + SIZE_OF_U64_3X], r3
# Flag user and PDA accounts as CPI writable signers.
# ---------------------------------------------------
sth [r10 - STK_INIT_ACCT_META_USER_IS_WRITABLE_OFF], BOOL_TRUE_2X
sth [r10 - STK_INIT_ACCT_META_PDA_IS_WRITABLE_OFF], BOOL_TRUE_2X
sth [r10 - STK_INIT_ACCT_INFO_USER_IS_SIGNER_OFF], BOOL_TRUE_2X
sth [r10 - STK_INIT_ACCT_INFO_PDA_IS_SIGNER_OFF], BOOL_TRUE_2X
# Optimize out 4 CUs by omitting the following assignments, which are
# covered by the double wide boolean true assign since is_signer
# follows is_writable in SolAccountMeta, and is_writable follows
# is_signer in SolAccountInfo.
# ```
# stb [r10 - STK_INIT_ACCT_META_USER_IS_SIGNER_OFF], BOOL_TRUE
# stb [r10 - STK_INIT_ACCT_META_PDA_IS_SIGNER_OFF], BOOL_TRUE
# stb [r10 - STK_INIT_ACCT_INFO_USER_IS_WRITABLE_OFF], BOOL_TRUE
# stb [r10 - STK_INIT_ACCT_INFO_PDA_IS_WRITABLE_OFF], BOOL_TRUE
# ```
# Walk through remaining pointer fields for account metas and infos.
# ---------------------------------------------------------------------
# - Rent epoch is ignored since is not needed.
# - Data length and executable status are ignored since both values are
# zero and the stack is zero-initialized.
# - PDA pubkey is ignored since it was set above as an optimization
# during the PDA compare operation.
# ---------------------------------------------------------------------
mov64 r2, r9 # Get input buffer pointer.
add64 r2, USER_PUBKEY_OFF # Update to point at user pubkey.
# Store in account meta and account info.
stxdw [r10 - STK_INIT_ACCT_META_USER_PUBKEY_ADDR_OFF], r2
stxdw [r10 - STK_INIT_ACCT_INFO_USER_KEY_ADDR_OFF], r2
add64 r2, SIZE_OF_PUBKEY # Advance to point at user owner.
# Store in account info.
stxdw [r10 - STK_INIT_ACCT_INFO_USER_OWNER_ADDR_OFF], r2
add64 r2, SIZE_OF_PUBKEY # Advance to point at user Lamports.
# Store in account info.
stxdw [r10 - STK_INIT_ACCT_INFO_USER_LAMPORTS_ADDR_OFF], r2
add64 r2, SIZE_OF_U64_2X # Advance to point to user account data.
# Store in account info.
stxdw [r10 - STK_INIT_ACCT_INFO_USER_DATA_ADDR_OFF], r2
# Advance to point to PDA owner. Note that this must be used instead of
# the System Program pubkey on the stack or the CPI will fail.
add64 r2, USER_DATA_TO_PDA_OWNER_OFF
stxdw [r10 - STK_INIT_ACCT_INFO_PDA_OWNER_ADDR_OFF], r2
# Advance to point to PDA Lamports.
add64 r2, SIZE_OF_PUBKEY
# Store in account info.
stxdw [r10 - STK_INIT_ACCT_INFO_PDA_LAMPORTS_ADDR_OFF], r2
add64 r2, SIZE_OF_U64_2X # Advance to point to PDA account data.
# Store in account info.
stxdw [r10 - STK_INIT_ACCT_INFO_PDA_DATA_ADDR_OFF], r2
# Populate SignerSeeds structure.
# -------------------------------
mov64 r2, r10 # Get stack frame pointer.
sub64 r2, STK_INIT_SEED_0_ADDR_OFF # Update to point to signer seed 0.
stxdw [r10 - STK_INIT_SIGNERS_SEEDS_OFF], r2 # Store in SignerSeeds.
# Store number of signer seeds for PDA (32-bit immediate).
stdw [r10 - STK_INIT_SIGNER_SEEDS_0_LEN_OFF], N_SIGNER_SEEDS
# Invoke CreateAccount CPI.
# -------------------------
mov64 r1, r10 # Get stack frame pointer.
sub64 r1, STK_INIT_INSN_OFF # Point to instruction.
mov64 r2, r10 # Get stack frame pointer.
sub64 r2, STK_INIT_ACCT_INFOS_OFF # Point to account infos.
mov64 r3, INIT_CPI_N_ACCOUNTS # Indicate number of account infos.
mov64 r4, r10 # Get stack frame pointer.
sub64 r4, STK_INIT_SIGNERS_SEEDS_OFF # Point to single SignerSeeds.
mov64 r5, INIT_CPI_N_SIGNERS_SEEDS # Indicate a single signer.
call sol_invoke_signed_c
# Write bump seed to new account.
# -------------------------------
ldxb r2, [r10 - STK_INIT_BUMP_SEED_OFF] # Load bump seed from stack.
stxb [r9 + PDA_BUMP_SEED_OFF], r2 # Store in new PDA account data.
exit
increment:
# Get user data length with padding.
# ----------------------------------
ldxdw r9, [r1 + USER_DATA_LEN_OFF] # Get user data length.
# Speculatively add max possible padding. This will not overflow
# because max account data length fits in a u32.
add64 r9, 7
# Clear low 3 bits, thereby truncating to 8-byte alignment. This yields
# the data length plus (optional) required padding.
and64 r9, -8
# Check remaining memory map layout.
# ----------------------------------
# Sum input buffer offset and padded user data length, affecting
# subsequent offsets originally calculated assuming no user account
# data: get input buffer pointer offset by padded user data length.
add64 r9, r1
ldxb r8, [r9 + PDA_NON_DUP_MARKER_OFF] # Load PDA duplicate marker.
jne r8, NON_DUP_MARKER, e_pda_duplicate # Exit if PDA is a duplicate.
ldxdw r8, [r9 + PDA_DATA_LEN_OFF] # Get PDA data length.
jne r8, INIT_CPI_ACCT_SIZE, e_pda_data_len # Exit if invalid length.
# Get instruction data length.
ldxdw r8, [r9 + INSTRUCTION_DATA_LEN_INC_OFF]
# Exit if invalid length.
jne r8, SIZE_OF_U64, e_invalid_instruction_data_len
mov64 r3, r9 # Copy input buffer offset by padded user data length.
# Update to point to program ID, for later verification syscall.
add64 r3, PROGRAM_ID_INC_OFF
mov64 r6, r9 # Copy input buffer offset by padded user data length.
# Update to point to PDA pubkey, for later verification syscall.
add64 r6, PDA_PUBKEY_OFF
# Process counter increment instruction.
# ---------------------------------------------------------------------
# This is done speculatively, before PDA bump seeds are verified, to
# minimize number of pointer copies during happy path. If this were
# done after signer seed verification, the pointer to the input buffer
# offset by user data length would need to be copied.
# ---------------------------------------------------------------------
ldxdw r8, [r9 + COUNTER_INCREMENT_OFF] # Get increment amount.
ldxdw r7, [r9 + PDA_COUNTER_OFF] # Get current PDA counter value.
add64 r7, r8 # Wrapping increment counter value by instruction amount.
stxdw [r9 + PDA_COUNTER_OFF], r7 # Store value in PDA account.
# Prepare signer seeds for PDA verification.
# ------------------------------------------
# Directly mutate input buffer pointer to point to user pubkey, since
# this is the last access of the input buffer pointer for this branch
# and a pointer copy can thus be optimized out.
add64 r1, USER_PUBKEY_OFF
# Store pointer in seed 0 pointer field.
stxdw [r10 - STK_INC_SEED_0_ADDR_OFF], r1
# Store length in seed 0 length field (32-bit immediate).
stdw [r10 - STK_INC_SEED_0_LEN_OFF], SIZE_OF_PUBKEY
# Directly mutate input pointer offset by padded user data length to
# point at PDA bump seed, since this is the last access of the offset
# pointer for this branch and a pointer copy can thus be optimized out.
add64 r9, PDA_BUMP_SEED_OFF
# Store pointer in seed 1 pointer field.
stxdw [r10 - STK_INC_SEED_1_ADDR_OFF], r9
# Store length in seed 1 length field (32-bit immediate).
stdw [r10 - STK_INC_SEED_1_LEN_OFF], SIZE_OF_U8
# Re-derive PDA.
# ---------------------------------------------------------------------
# r3 was set to program ID pointer during memory map checks.
# ---------------------------------------------------------------------
mov64 r1, r10 # Get stack frame pointer.
sub64 r1, STK_INC_SEED_0_ADDR_OFF # Update to point to signer seeds.
mov64 r2, N_SIGNER_SEEDS # Load signer seeds count (32-bit immediate).
mov64 r4, r10 # Get stack frame pointer.
sub64 r4, STK_INC_PDA_OFF # Update to point to PDA result on stack.
call sol_create_program_address # Create PDA.
jne r0, SUCCESS, e_unable_to_derive_pda # Error if unable to create.
# Verify PDA.
# ---------------------------------------------------------------------
# r6 was set to passed PDA pubkey pointer during memory map checks.
# ---------------------------------------------------------------------
mov64 r1, r6 # Get pointer to passed PDA pubkey, set above.
mov64 r2, r4 # Get pointer to computed PDA.
# Compare pubkey values in 64-bit chunks (same optimization as in the
# initialize operation):
ldxdw r3, [r1 + 0]
ldxdw r4, [r2 + 0]
jne r3, r4, e_pda_mismatch
ldxdw r3, [r1 + SIZE_OF_U64]
ldxdw r4, [r2 + SIZE_OF_U64]
jne r3, r4, e_pda_mismatch
ldxdw r3, [r1 + SIZE_OF_U64_2X]
ldxdw r4, [r2 + SIZE_OF_U64_2X]
jne r3, r4, e_pda_mismatch
ldxdw r3, [r1 + SIZE_OF_U64_3X]
ldxdw r4, [r2 + SIZE_OF_U64_3X]
jne r3, r4, e_pda_mismatch
exit
e_user_data_len:
mov32 r0, E_USER_DATA_LEN
exit
e_pda_data_len:
mov32 r0, E_PDA_DATA_LEN
exit
e_system_program_data_len:
mov32 r0, E_SYSTEM_PROGRAM_DATA_LEN
exit
e_pda_duplicate:
mov32 r0, E_PDA_DUPLICATE
exit
e_system_program_duplicate:
mov32 r0, E_SYSTEM_PROGRAM_DUPLICATE
exit
e_pda_mismatch:
mov32 r0, E_PDA_MISMATCH
exit
e_invalid_instruction_data_len:
mov32 r0, E_INVALID_INSTRUCTION_DATA_LEN
exit
e_unable_to_derive_pda:
mov32 r0, E_UNABLE_TO_DERIVE_PDA
exitImportantly, this methodology strictly enforces 8-byte aligned stack offsets, as well as i16 offset values since, as of the time of this writing, sbpf silently truncates offsets that are not i16.
🔀 Entrypoint branching
The number of accounts acts as a discriminator for the two operations:
| Operation | Number of accounts | Instruction data |
|---|---|---|
| Initialize | 3 | None |
| Increment | 2 | Increment amount (u64) |
| Account index | Description | Used for initialize? | Use for increment? |
|---|---|---|---|
| 0 | User's account | Yes | Yes |
| 1 | Counter PDA account | Yes | Yes |
| 2 | System Program account | Yes | No |
Only the initialize operation requires the System Program account in order to initialize the PDA account. Hence the entrypoint first checks the number of accounts passed in and branches accordingly, erroring out if the number of accounts is unexpected.
asm
.global entrypoint
entrypoint:
ldxdw r2, [r1 + N_ACCOUNTS_OFF] # Get n accounts from input buffer.
jeq r2, N_ACCOUNTS_INCREMENT, increment # Fast path to cheap operation.
jeq r2, N_ACCOUNTS_INIT, initialize # Low priority, expensive anyways.
mov64 r0, E_N_ACCOUNTS # Else fail.
exit🚀 Initialize operation
🗺️ Layout background
Like in the transfer example, the initialize operation uses a System Program CPI but with CreateAccount instruction data:
| Size (bytes) | Description |
|---|---|
| 4 | Enum variant (0) |
| 8 | Lamports to transfer to new account |
| 8 | Bytes to allocate for new account |
| 32 | Owner program ID for new account (the counter program) |
Notably, create_account calls transfer, which internally disallows account data such that the entire memory map is statically sized for the initialize operation, including the Program ID serialization at end of the input buffer. Hence the initial memory map checks at the start of the initialize operation:
asm
initialize:
# Check input memory map.
# -----------------------
ldxdw r2, [r1 + USER_DATA_LEN_OFF] # Get user data length.
jne r2, DATA_LEN_ZERO, e_user_data_len # Exit if user account has data.
ldxb r2, [r1 + PDA_NON_DUP_MARKER_OFF] # Check if PDA is a duplicate.
jne r2, NON_DUP_MARKER, e_pda_duplicate # Exit if PDA is a duplicate.
ldxdw r2, [r1 + PDA_DATA_LEN_OFF] # Get PDA data length.
jne r2, DATA_LEN_ZERO, e_pda_data_len # Exit if PDA account has data.
# Exit early if System Program is a duplicate.
ldxb r2, [r1 + SYSTEM_PROGRAM_NON_DUP_MARKER_OFF]
jne r2, NON_DUP_MARKER, e_system_program_duplicate
# Exit early if System Program data length is invalid.
ldxdw r2, [r1 + SYSTEM_PROGRAM_DATA_LEN_OFF]
jne r2, DATA_LEN_SYSTEM_PROGRAM, e_system_program_data_lenThe initialize operation stack contains the same allocated regions as the transfer example plus the following additional regions, described below:
| Size (bytes) | Description |
|---|---|
| 16 | SolSignerSeed for user's pubkey |
| 16 | SolSignerSeed for bump seed |
| 16 | SolSignerSeeds for CPI |
| 32 | PDA from sol_try_find_program_address (r4) |
| 24 | Rent from sol_get_rent_sysvar (r1) |
| 4 | Padding to maintain 8-byte alignment |
| 1 | Bump seed from sol_try_find_program_address (r5) |
🌱 Signer seeds
Unlike in the transfer CPI, the CreateAccount instruction CPI requires signer seeds:
| Register | Description |
|---|---|
r4 | Pointer to array of SolSignerSeeds |
r5 | Number of elements in array (1 in this example) |
There is only one PDA signer, such that the single SolSignerSeeds points to the following array of two SolSignerSeed structures:
| Index | Description |
|---|---|
| 0 | User's pubkey |
| 1 | PDA bump seed |
Hence after checking the input memory map, the SolSignerSeed structures are populated on the stack:
asm
# Initialize signer seed for user pubkey.
# ---------------------------------------
mov64 r2, r1 # Get input buffer pointer.
add64 r2, USER_PUBKEY_OFF # Update pointer to point at user pubkey.
# Store pointer in seed 0 pointer field.
stxdw [r10 - STK_INIT_SEED_0_ADDR_OFF], r2
# Store length in seed 0 length field (32-bit immediate).
stdw [r10 - STK_INIT_SEED_0_LEN_OFF], SIZE_OF_PUBKEY
# Initialize signer seed for PDA bump key.
# ----------------------------------------
mov64 r2, r10 # Get stack frame pointer.
sub64 r2, STK_INIT_BUMP_SEED_OFF # Update to point at PDA bump seed.
# Store pointer in seed 1 pointer field.
stxdw [r10 - STK_INIT_SEED_1_ADDR_OFF], r2
# Store length in seed 1 length field (32-bit immediate).
stdw [r10 - STK_INIT_SEED_1_LEN_OFF], SIZE_OF_U8🔍 A priori PDA checks
The PDA and bump seed are then computed by sol_try_find_program_address, whose implementation similarly relies on a SolSignerSeed array, in this case containing a single SolSignerSeed for the user's pubkey:
| Register | Description |
|---|---|
r0 | Return code: set to 0 on success, 1 on fail |
r1 | Pointer to array of SolSignerSeed |
r2 | Number of elements in SolSignerSeed array (1 in this case) |
r3 | PDA owning program ID (counter program ID) |
r4 | Pointer to fill with PDA (unchanged on error) |
r5 | Pointer to fill with bump seed (unchanged on error) |
Notably, the bump seed is temporarily stored on the stack instead of directly in the passed PDA account data, since the CreateAccount instruction CPI processor exit routine later overwrites data on account creation.
asm
# Compute PDA.
# ------------
mov64 r9, r1 # Store input buffer pointer for later.
mov64 r1, r10 # Get stack frame pointer.
# Update to point at user pubkey signer seed.
sub64 r1, STK_INIT_SEED_0_ADDR_OFF
mov64 r2, 1 # Indicate single signer seed (user pubkey).
mov64 r3, r9 # Get input buffer pointer.
add64 r3, PROGRAM_ID_INIT_OFF # Update to point at program ID.
mov64 r4, r10 # Get stack frame pointer.
sub64 r4, STK_INIT_PDA_OFF # Update to point to PDA region on stack.
mov64 r5, r10 # Get stack frame pointer.
sub64 r5, STK_INIT_BUMP_SEED_OFF # Update to point to bump seed region.
call sol_try_find_program_address # Find PDA.
# Skip check to error out if unable to derive a PDA (failure to derive
# is practically impossible to test since odds of not finding bump seed
# are astronomically low):
# ```
# jne r0, SUCCESS, e_unable_to_derive_pda
# ```
mov64 r1, r9 # Restore input buffer pointer.The computed PDA is then compared against the passed PDA account's pubkey using chunked compares. This is more efficient than sol_memcmp, which is subject to metering that charges the larger of a 10 CU base cost, and a per-byte cost of 250 CUs. The inner compare function compare result is 0i32 only if the two regions are equal, and would need to be allocated:
| Register | Description |
|---|---|
r0 | Always returns 0 |
r1 | Pointer to first region |
r2 | Pointer to second region |
r3 | Number of bytes to compare |
r4 | Pointer to fill with compare result (i32) |
asm
# Compare computed PDA against passed account.
# --------------------------------------------
# Update input buffer pointer to point to passed PDA.
add64 r1, PDA_PUBKEY_OFF
# As an optimization, store this pointer on the stack in the account
# meta and info for the PDA, rather than deriving the pointer again.
# Note that this must point to the pubkey in the input memory map, not
# the one on the stack, otherwise the CPI will fail.
stxdw [r10 - STK_INIT_ACCT_META_PDA_PUBKEY_ADDR_OFF], r1
stxdw [r10 - STK_INIT_ACCT_INFO_PDA_KEY_ADDR_OFF], r1
mov64 r2, r10 # Get stack frame pointer.
sub64 r2, STK_INIT_PDA_OFF # Update to point to computed PDA.
# Compare the pubkey values in 64-bit chunks, which is less CUs than:
# ```
# mov64 r3, SIZE_OF_PUBKEY # Flag size of bytes to compare.
# mov64 r4, r10 # Get stack frame pointer.
# sub64 r4, STK_INIT_MEMCMP_RESULT_OFF # Update to point to result.
# call sol_memcmp_
# ldxw r2, [r4 + NO_OFFSET] # Get compare result.
# jne r2, COMPARE_EQUAL, e_pda_mismatch # Error out if PDA mismatch.
# ```
ldxdw r3, [r1 + 0]
ldxdw r4, [r2 + 0]
jne r3, r4, e_pda_mismatch
ldxdw r3, [r1 + SIZE_OF_U64]
ldxdw r4, [r2 + SIZE_OF_U64]
jne r3, r4, e_pda_mismatch
ldxdw r3, [r1 + SIZE_OF_U64_2X]
ldxdw r4, [r2 + SIZE_OF_U64_2X]
jne r3, r4, e_pda_mismatch
ldxdw r3, [r1 + SIZE_OF_U64_3X]
ldxdw r4, [r2 + SIZE_OF_U64_3X]
jne r3, r4, e_pda_mismatch
# Skip input buffer restoration since next block overwrites r1:
# ```
# mov64 r1, r9 # Restore input buffer pointer.
# ```💰 Minimum balance
The testing framework in this example uses the soon-to-be-deprecated Rent::default implementation, so the assembly program relies on sol_get_rent_sysvar which has a return value of Rent, written to the pointer passed in r1. The resulting minimum_balance is then computed as product of:
Rent.lamports_per_byte_year(DEFAULT_LAMPORTS_PER_BYTE_YEAR)- PDA account data length (
9) plusACCOUNT_STORAGE_OVERHEAD
NOTE
As of the time of this writing, rent is under active development: SIMD-0194, which has not yet activated, is superseded by SIMD-0436, which is in turn superseded by SIMD-0437.
This resulting minimum balance is directly stored in the CreateAccount instruction data buffer on the stack:
asm
# Calculate Lamports required for new account.
# --------------------------------------------
mov64 r1, r10 # Get stack frame pointer.
sub64 r1, STK_INIT_RENT_OFF # Update to point to Rent struct.
call sol_get_rent_sysvar # Get Rent struct.
ldxdw r2, [r1 + NO_OFFSET] # Get Lamports per byte field.
# Multiply by sum of PDA account data length, account storage overhead.
mul64 r2, PDA_DATA_WITH_ACCOUNT_OVERHEAD
# Store value directly in instruction data on stack.
stxdw [r10 - STK_INIT_INSN_DATA_LAMPORTS_OFF], r2
# Skip input buffer restoration since block after next overwrites r1:
# ```
# mov64 r1, r9 # Restore input buffer pointer.
# ```🛠️ CPI construction
As in the transfer CPI, the CreateAccount instruction and associated account information regions are populated, this time with an additional optimization: the deprecated rent_epoch field is ignored, since the internal CPI CallerAccount structure does not include it, hence it is unprocessed by update_callee_account.
Notably, the CreateAccount instruction data owner program ID field is populated via chunked loads as opposed to sol_memcpy, which has the same CU cost as sol_memcmp but no compare value return:
Optimized instruction and account region setup
asm
# Populate SolInstruction on stack.
# ---------------------------------
mov64 r3, r10 # Get stack frame pointer for stepping through stack.
# Update to point to zero-initialized System Program pubkey on stack.
sub64 r3, STK_INIT_SYSTEM_PROGRAM_PUBKEY_OFF
stxdw [r10 - STK_INIT_INSN_OFF], r3 # Store as CPI program ID.
# Advance to point to account metas.
add64 r3, STK_INIT_SYSTEM_PROGRAM_PUBKEY_TO_ACCOUNT_METAS_OFF
# Store pointer to account metas as CPI account metas address.
stxdw [r10 - STK_INIT_INSN_ACCOUNTS_ADDR_OFF], r3
# Store number of CPI accounts (fits in 32-bit immediate).
stdw [r10 - STK_INIT_INSN_ACCOUNTS_LEN_OFF], INIT_CPI_N_ACCOUNTS
# Advance to point to instruction data.
add64 r3, STK_INIT_ACCOUNT_METAS_TO_INSN_DATA_OFF
stxdw [r10 - STK_INIT_INSN_DATA_ADDR_OFF], r3 # Store CPI data address.
# Store instruction data length (fits in 32-bit immediate).
stdw [r10 - STK_INIT_INSN_DATA_LEN_OFF], INIT_CPI_INSN_DATA_LEN
# Populate CreateAccount instruction data on stack.
# ---------------------------------------------------------------------
# - Discriminator is already set to 0 since stack is zero initialized.
# - Lamports field was already set in the minimum balance calculation.
# ---------------------------------------------------------------------
# Store the data length of the account to create (fits in 32 bits).
stdw [r10 - STK_INIT_INSN_DATA_SPACE_OFF], INIT_CPI_ACCT_SIZE
mov64 r1, r10 # Get pointer to stack frame.
sub64 r1, STK_INIT_INSN_DATA_OWNER_OFF # Point to new owner field.
mov64 r2, r9 # Get input buffer pointer.
add64 r2, PROGRAM_ID_INIT_OFF # Point to program ID.
# Copy the pubkey value in 64-bit chunks, which is less CUs than:
# ```
# mov64 r3, SIZE_OF_PUBKEY # Set length of bytes to copy.
# call sol_memcpy_ # Copy program ID into CreateAccount owner field.
# ```
ldxdw r3, [r2 + 0]
stxdw [r1 + 0], r3
ldxdw r3, [r2 + SIZE_OF_U64]
stxdw [r1 + SIZE_OF_U64], r3
ldxdw r3, [r2 + SIZE_OF_U64_2X]
stxdw [r1 + SIZE_OF_U64_2X], r3
ldxdw r3, [r2 + SIZE_OF_U64_3X]
stxdw [r1 + SIZE_OF_U64_3X], r3
# Flag user and PDA accounts as CPI writable signers.
# ---------------------------------------------------
sth [r10 - STK_INIT_ACCT_META_USER_IS_WRITABLE_OFF], BOOL_TRUE_2X
sth [r10 - STK_INIT_ACCT_META_PDA_IS_WRITABLE_OFF], BOOL_TRUE_2X
sth [r10 - STK_INIT_ACCT_INFO_USER_IS_SIGNER_OFF], BOOL_TRUE_2X
sth [r10 - STK_INIT_ACCT_INFO_PDA_IS_SIGNER_OFF], BOOL_TRUE_2X
# Optimize out 4 CUs by omitting the following assignments, which are
# covered by the double wide boolean true assign since is_signer
# follows is_writable in SolAccountMeta, and is_writable follows
# is_signer in SolAccountInfo.
# ```
# stb [r10 - STK_INIT_ACCT_META_USER_IS_SIGNER_OFF], BOOL_TRUE
# stb [r10 - STK_INIT_ACCT_META_PDA_IS_SIGNER_OFF], BOOL_TRUE
# stb [r10 - STK_INIT_ACCT_INFO_USER_IS_WRITABLE_OFF], BOOL_TRUE
# stb [r10 - STK_INIT_ACCT_INFO_PDA_IS_WRITABLE_OFF], BOOL_TRUE
# ```
# Walk through remaining pointer fields for account metas and infos.
# ---------------------------------------------------------------------
# - Rent epoch is ignored since is not needed.
# - Data length and executable status are ignored since both values are
# zero and the stack is zero-initialized.
# - PDA pubkey is ignored since it was set above as an optimization
# during the PDA compare operation.
# ---------------------------------------------------------------------
mov64 r2, r9 # Get input buffer pointer.
add64 r2, USER_PUBKEY_OFF # Update to point at user pubkey.
# Store in account meta and account info.
stxdw [r10 - STK_INIT_ACCT_META_USER_PUBKEY_ADDR_OFF], r2
stxdw [r10 - STK_INIT_ACCT_INFO_USER_KEY_ADDR_OFF], r2
add64 r2, SIZE_OF_PUBKEY # Advance to point at user owner.
# Store in account info.
stxdw [r10 - STK_INIT_ACCT_INFO_USER_OWNER_ADDR_OFF], r2
add64 r2, SIZE_OF_PUBKEY # Advance to point at user Lamports.
# Store in account info.
stxdw [r10 - STK_INIT_ACCT_INFO_USER_LAMPORTS_ADDR_OFF], r2
add64 r2, SIZE_OF_U64_2X # Advance to point to user account data.
# Store in account info.
stxdw [r10 - STK_INIT_ACCT_INFO_USER_DATA_ADDR_OFF], r2
# Advance to point to PDA owner. Note that this must be used instead of
# the System Program pubkey on the stack or the CPI will fail.
add64 r2, USER_DATA_TO_PDA_OWNER_OFF
stxdw [r10 - STK_INIT_ACCT_INFO_PDA_OWNER_ADDR_OFF], r2
# Advance to point to PDA Lamports.
add64 r2, SIZE_OF_PUBKEY
# Store in account info.
stxdw [r10 - STK_INIT_ACCT_INFO_PDA_LAMPORTS_ADDR_OFF], r2
add64 r2, SIZE_OF_U64_2X # Advance to point to PDA account data.
# Store in account info.
stxdw [r10 - STK_INIT_ACCT_INFO_PDA_DATA_ADDR_OFF], r2Unlike in the transfer CPI, this example additionally populates a SolSignerSeeds region on the stack since there is a PDA signer:
asm
# Populate SignerSeeds structure.
# -------------------------------
mov64 r2, r10 # Get stack frame pointer.
sub64 r2, STK_INIT_SEED_0_ADDR_OFF # Update to point to signer seed 0.
stxdw [r10 - STK_INIT_SIGNERS_SEEDS_OFF], r2 # Store in SignerSeeds.
# Store number of signer seeds for PDA (32-bit immediate).
stdw [r10 - STK_INIT_SIGNER_SEEDS_0_LEN_OFF], N_SIGNER_SEEDS
# Invoke CreateAccount CPI.
# -------------------------
mov64 r1, r10 # Get stack frame pointer.
sub64 r1, STK_INIT_INSN_OFF # Point to instruction.
mov64 r2, r10 # Get stack frame pointer.
sub64 r2, STK_INIT_ACCT_INFOS_OFF # Point to account infos.
mov64 r3, INIT_CPI_N_ACCOUNTS # Indicate number of account infos.
mov64 r4, r10 # Get stack frame pointer.
sub64 r4, STK_INIT_SIGNERS_SEEDS_OFF # Point to single SignerSeeds.
mov64 r5, INIT_CPI_N_SIGNERS_SEEDS # Indicate a single signer.
call sol_invoke_signed_c💾 Bump seed storage
Finally, the bump seed computed earlier by sol_try_find_program_address is stored in the last byte of the PDA account data:
asm
# Write bump seed to new account.
# -------------------------------
ldxb r2, [r10 - STK_INIT_BUMP_SEED_OFF] # Load bump seed from stack.
stxb [r9 + PDA_BUMP_SEED_OFF], r2 # Store in new PDA account data.
exit➕ Increment operation
📏 User data length
The increment operation starts by checking the user's account data length, padding as needed to ensure 8-byte alignment. Notably, since i32 immediates are cast to i64 by the VM interpreter, then cast to u64 by AND64_IMM, the VM's use of Rust sign extension enables the following concise padding calculation, guaranteed not to overflow given that MAX_PERMITTED_DATA_LENGTH is much less than
asm
increment:
# Get user data length with padding.
# ----------------------------------
ldxdw r9, [r1 + USER_DATA_LEN_OFF] # Get user data length.
# Speculatively add max possible padding. This will not overflow
# because max account data length fits in a u32.
add64 r9, 7
# Clear low 3 bits, thereby truncating to 8-byte alignment. This yields
# the data length plus (optional) required padding.
and64 r9, -8This algorithm is verified with a simple test:
rs
#[test]
fn test_pad_masking() {
let increment = 7;
let mask_immediate = -8i32; // Assembly immediate.
let mask = (mask_immediate as i64) as u64; // VM interpretation.
let hex = 0xffff_ffff_ffff_fff8u64;
let binary = 0b1111111111111111111111111111111111111111111111111111111111111000u64;
assert_eq!(mask, hex);
assert_eq!(mask, u64::MAX - 7u64);
assert_eq!(mask, binary);
let padded_data_len = |data_len: u64| -> u64 { (data_len + increment) & mask };
assert_eq!(padded_data_len(0), 0);
assert_eq!(padded_data_len(1), 8);
assert_eq!(padded_data_len(8), 8);
assert_eq!(padded_data_len(9), 16);
assert_eq!(padded_data_len(15), 16);
}📑 Memory map parsing
The input memory map is then parsed using the calculated user offset with padding, and assorted pointers are stored for future operations:
asm
# Check remaining memory map layout.
# ----------------------------------
# Sum input buffer offset and padded user data length, affecting
# subsequent offsets originally calculated assuming no user account
# data: get input buffer pointer offset by padded user data length.
add64 r9, r1
ldxb r8, [r9 + PDA_NON_DUP_MARKER_OFF] # Load PDA duplicate marker.
jne r8, NON_DUP_MARKER, e_pda_duplicate # Exit if PDA is a duplicate.
ldxdw r8, [r9 + PDA_DATA_LEN_OFF] # Get PDA data length.
jne r8, INIT_CPI_ACCT_SIZE, e_pda_data_len # Exit if invalid length.
# Get instruction data length.
ldxdw r8, [r9 + INSTRUCTION_DATA_LEN_INC_OFF]
# Exit if invalid length.
jne r8, SIZE_OF_U64, e_invalid_instruction_data_len
mov64 r3, r9 # Copy input buffer offset by padded user data length.
# Update to point to program ID, for later verification syscall.
add64 r3, PROGRAM_ID_INC_OFF
mov64 r6, r9 # Copy input buffer offset by padded user data length.
# Update to point to PDA pubkey, for later verification syscall.
add64 r6, PDA_PUBKEY_OFF⚡ Speculative increment
The current counter value is then loaded from the PDA account data, and the u64 increment amount from the instruction data is added to it speculatively before error checks, to minimize the number of future pointer copies:
asm
# Process counter increment instruction.
# ---------------------------------------------------------------------
# This is done speculatively, before PDA bump seeds are verified, to
# minimize number of pointer copies during happy path. If this were
# done after signer seed verification, the pointer to the input buffer
# offset by user data length would need to be copied.
# ---------------------------------------------------------------------
ldxdw r8, [r9 + COUNTER_INCREMENT_OFF] # Get increment amount.
ldxdw r7, [r9 + PDA_COUNTER_OFF] # Get current PDA counter value.
add64 r7, r8 # Wrapping increment counter value by instruction amount.
stxdw [r9 + PDA_COUNTER_OFF], r7 # Store value in PDA account.🔒 PDA check followup
Finally, the PDA and bump seed are verified using sol_create_program_address, which requires an array of two signer seed structures: one containing the user's pubkey and one containing the bump seed. Internally this relies on create_program_address, which can fail if an address can't be found:
| Register | Success | Failure |
|---|---|---|
r0 | 0 | 1 |
r4 | Passed pointer filled with PDA | Unchanged |
asm
# Prepare signer seeds for PDA verification.
# ------------------------------------------
# Directly mutate input buffer pointer to point to user pubkey, since
# this is the last access of the input buffer pointer for this branch
# and a pointer copy can thus be optimized out.
add64 r1, USER_PUBKEY_OFF
# Store pointer in seed 0 pointer field.
stxdw [r10 - STK_INC_SEED_0_ADDR_OFF], r1
# Store length in seed 0 length field (32-bit immediate).
stdw [r10 - STK_INC_SEED_0_LEN_OFF], SIZE_OF_PUBKEY
# Directly mutate input pointer offset by padded user data length to
# point at PDA bump seed, since this is the last access of the offset
# pointer for this branch and a pointer copy can thus be optimized out.
add64 r9, PDA_BUMP_SEED_OFF
# Store pointer in seed 1 pointer field.
stxdw [r10 - STK_INC_SEED_1_ADDR_OFF], r9
# Store length in seed 1 length field (32-bit immediate).
stdw [r10 - STK_INC_SEED_1_LEN_OFF], SIZE_OF_U8
# Re-derive PDA.
# ---------------------------------------------------------------------
# r3 was set to program ID pointer during memory map checks.
# ---------------------------------------------------------------------
mov64 r1, r10 # Get stack frame pointer.
sub64 r1, STK_INC_SEED_0_ADDR_OFF # Update to point to signer seeds.
mov64 r2, N_SIGNER_SEEDS # Load signer seeds count (32-bit immediate).
mov64 r4, r10 # Get stack frame pointer.
sub64 r4, STK_INC_PDA_OFF # Update to point to PDA result on stack.
call sol_create_program_address # Create PDA.
jne r0, SUCCESS, e_unable_to_derive_pda # Error if unable to create.
# Verify PDA.
# ---------------------------------------------------------------------
# r6 was set to passed PDA pubkey pointer during memory map checks.
# ---------------------------------------------------------------------
mov64 r1, r6 # Get pointer to passed PDA pubkey, set above.
mov64 r2, r4 # Get pointer to computed PDA.
# Compare pubkey values in 64-bit chunks (same optimization as in the
# initialize operation):
ldxdw r3, [r1 + 0]
ldxdw r4, [r2 + 0]
jne r3, r4, e_pda_mismatch
ldxdw r3, [r1 + SIZE_OF_U64]
ldxdw r4, [r2 + SIZE_OF_U64]
jne r3, r4, e_pda_mismatch
ldxdw r3, [r1 + SIZE_OF_U64_2X]
ldxdw r4, [r2 + SIZE_OF_U64_2X]
jne r3, r4, e_pda_mismatch
ldxdw r3, [r1 + SIZE_OF_U64_3X]
ldxdw r4, [r2 + SIZE_OF_U64_3X]
jne r3, r4, e_pda_mismatch
exitThis operation relies on the following stack layout:
| Size (bytes) | Description |
|---|---|
| 16 | SolSignerSeed for user's pubkey |
| 16 | SolSignerSeed for bump seed |
| 32 | PDA from sol_create_program_address (r4) |
🦀 Rust implementation
The Rust implementation relies on lazy_program_entrypoint to minimize parsing overhead, with liberal use of transmute for pointer conversion parity with the assembly implementation:
Full program
rs
use core::mem::{size_of, transmute, MaybeUninit};
use pinocchio::{
address::address_eq,
cpi::{invoke_signed_unchecked, Seed, Signer},
entrypoint::{InstructionContext, MaybeAccount},
instruction::{InstructionAccount, InstructionView},
lazy_program_entrypoint, no_allocator, nostd_panic_handler,
sysvars::rent::ACCOUNT_STORAGE_OVERHEAD,
Address, ProgramResult,
};
#[allow(deprecated)]
#[cfg(target_os = "solana")]
use pinocchio::syscalls::sol_get_rent_sysvar;
lazy_program_entrypoint!(process_instruction);
nostd_panic_handler!();
no_allocator!();
const E_N_ACCOUNTS: u32 = 1;
const E_USER_DATA_LEN: u32 = 2;
const E_PDA_DATA_LEN: u32 = 3;
const E_SYSTEM_PROGRAM_DATA_LEN: u32 = 4;
const E_PDA_DUPLICATE: u32 = 5;
const E_SYSTEM_PROGRAM_DUPLICATE: u32 = 6;
const E_UNABLE_TO_DERIVE_PDA: u32 = 7;
const E_PDA_MISMATCH: u32 = 8;
const E_INVALID_INSTRUCTION_DATA_LEN: u32 = 9;
const N_ACCOUNTS_INCREMENT: u64 = 2;
const N_ACCOUNTS_INITIALIZE: u64 = 3;
const SYSTEM_PROGRAM_DATA_LEN: usize = b"system_program".len();
#[inline(always)]
fn err<T>(code: u32) -> Result<T, pinocchio::error::ProgramError> {
Err(pinocchio::error::ProgramError::Custom(code))
}
/// SAFETY: Caller must ensure address points to valid memory of correct size.
#[inline(always)]
unsafe fn user_seed(address: &Address) -> Seed<'_> {
#[allow(clippy::missing_transmute_annotations)]
Seed::from(transmute::<_, &[u8; size_of::<Address>()]>(address))
}
/// SAFETY: Caller must ensure data_ptr points to valid PdaAccountData.
#[inline(always)]
unsafe fn pda_data(data_ptr: *mut u8) -> &'static mut PdaAccountData {
#[allow(clippy::transmute_ptr_to_ref)]
transmute(data_ptr)
}
#[repr(C, packed)]
struct PdaAccountData {
counter: u64,
bump: u8,
}
#[repr(C, packed)]
struct CreateAccountInstructionData {
instruction_tag: u32,
lamports: u64,
space: u64,
owner: Address,
}
pub fn process_instruction(mut context: InstructionContext) -> ProgramResult {
match context.remaining() {
N_ACCOUNTS_INCREMENT => {
// SAFETY: number of accounts has been checked.
let user = unsafe { context.next_account_unchecked().assume_account() };
// SAFETY: number of accounts has been checked.
let pda = match unsafe { context.next_account_unchecked() } {
MaybeAccount::Account(account) => account,
MaybeAccount::Duplicated(_) => return err(E_PDA_DUPLICATE),
};
if pda.data_len() != size_of::<PdaAccountData>() {
return err(E_PDA_DATA_LEN);
}
// SAFETY: All accounts have been consumed.
let instruction_data = unsafe { context.instruction_data_unchecked() };
if instruction_data.len() != size_of::<u64>() {
return err(E_INVALID_INSTRUCTION_DATA_LEN);
}
// SAFETY: PDA account size has been validated.
let pda_data = unsafe { pda_data(pda.data_ptr()) };
// SAFETY: instruction data size has been validated.
pda_data.counter = pda_data.counter.wrapping_add(unsafe {
*transmute::<*const u8, *const u64>(instruction_data.as_ptr())
});
// Prepare PDA seeds, check address.
// SAFETY: accounts and instruction data have been read.
let expected_pda = Address::create_program_address(
&[unsafe { &user_seed(user.address()) }, &[pda_data.bump]],
unsafe { context.program_id_unchecked() },
)
.or_else(|_| err(E_UNABLE_TO_DERIVE_PDA))?;
if !address_eq(pda.address(), &expected_pda) {
return err(E_PDA_MISMATCH);
}
}
N_ACCOUNTS_INITIALIZE => {
// SAFETY: number of accounts has been checked.
let user = unsafe { context.next_account_unchecked().assume_account() };
if !user.is_data_empty() {
return err(E_USER_DATA_LEN);
}
// SAFETY: number of accounts has been checked.
let pda = match unsafe { context.next_account_unchecked() } {
MaybeAccount::Account(account) => account,
MaybeAccount::Duplicated(_) => return err(E_PDA_DUPLICATE),
};
if !pda.is_data_empty() {
return err(E_PDA_DATA_LEN);
}
// SAFETY: number of accounts has been checked.
let system_program = match unsafe { context.next_account_unchecked() } {
MaybeAccount::Account(account) => account,
MaybeAccount::Duplicated(_) => return err(E_SYSTEM_PROGRAM_DUPLICATE),
};
if system_program.data_len() != SYSTEM_PROGRAM_DATA_LEN {
return err(E_SYSTEM_PROGRAM_DATA_LEN);
}
// Prepare PDA seeds, check address.
// SAFETY: known number of accounts have been read.
let program_id = unsafe { context.program_id_unchecked() };
let user_pubkey_seed = unsafe { user_seed(user.address()) };
let (expected_pda, bump) =
Address::find_program_address(&[&user_pubkey_seed], program_id);
if !address_eq(pda.address(), &expected_pda) {
return err(E_PDA_MISMATCH);
}
// Calculate minimum balance for rent exemption using mock of `Rent`, since `Rent`
// fields are private and APIs are unaware of SIMD changes affecting ASM feature parity.
// SAFETY: Rent is #[repr(C)] with lamports_per_byte (u64) as first field.
struct MockRent {
lamports_per_byte: u64,
_ignore: u64,
}
let rent = MaybeUninit::<MockRent>::uninit();
let lamports_per_byte: u64 = unsafe {
#[allow(deprecated)]
#[cfg(target_os = "solana")]
sol_get_rent_sysvar(transmute(&rent));
rent.assume_init().lamports_per_byte
};
let lamports =
(size_of::<PdaAccountData>() as u64 + ACCOUNT_STORAGE_OVERHEAD) * lamports_per_byte;
let instruction_data = CreateAccountInstructionData {
instruction_tag: 0,
lamports,
space: size_of::<PdaAccountData>() as u64,
// Clippy suggests a dereference, which breaks compilation.
#[allow(clippy::clone_on_copy)]
owner: program_id.clone(),
};
// SAFETY: Sizes have been validated a priori.
unsafe {
invoke_signed_unchecked(
&InstructionView {
program_id: &pinocchio_system::ID,
accounts: &[
InstructionAccount::writable_signer(user.address()),
InstructionAccount::writable_signer(pda.address()),
],
#[allow(clippy::missing_transmute_annotations)]
data: transmute::<_, &[u8; size_of::<CreateAccountInstructionData>()]>(
&instruction_data,
),
},
&[(&user).into(), (&pda).into()],
&[Signer::from(&[user_pubkey_seed, Seed::from(&[bump])])],
);
}
// Write bump seed to PDA data.
// SAFETY: PDA account was just created with sufficient space.
unsafe { pda_data(pda.data_ptr()) }.bump = bump;
}
_ => return err(E_N_ACCOUNTS),
}
Ok(())
}Notably, the Rust implementation relies on InstructionAccount::writable_signer and CpiAccount::From<AccountView>, which implement internal full-copy mechanisms that do not leverage the assembly implementation's zero-initialized stack optimizations, since individual stack frames are not zero-initialized during a frame push and therefore may have residual data from prior calls during runtime:
rs
invoke_signed_unchecked(
&InstructionView {
program_id: &pinocchio_system::ID,
accounts: &[
InstructionAccount::writable_signer(user.address()),
InstructionAccount::writable_signer(pda.address()),
],
#[allow(clippy::missing_transmute_annotations)]
data: transmute::<_, &[u8; size_of::<CreateAccountInstructionData>()]>(
&instruction_data,
),
},
&[(&user).into(), (&pda).into()],
&[Signer::from(&[user_pubkey_seed, Seed::from(&[bump])])],
);Compared with the assembly implementation, which verifiably uses only a single stack frame for each operation, the Rust implementation exhibits considerable overhead in particular for the initialize operation happy path, which relies on the aforementioned full-copy mechanisms.
📊 Compute unit analysis
Note the following fixed costs, which can be subtracted from the total compute unit costs for relevant test cases to calculate adjusted overhead values for each operation:
| Operation | Fixed cost (CUs) |
|---|---|
CreateAccount CPI | 1096 |
sol_try_find_program_address | 1500 |
sol_create_program_address | 1500 |
sol_get_rent_sysvar | 117 |
NOTE
The SyscallGetRentSysvar implementation relies on get_sysvar, which has a 100 CU base cost plus the target struct length, in this case 17 bytes for Rent.
🧪 Initialize
| Case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % |
|---|---|---|---|---|
| No accounts passed | 5 | 6 | +1 | +20.0% |
| Too many accounts passed | 5 | 6 | +1 | +20.0% |
| Invalid user data length | 7 | 9 | +2 | +28.6% |
| PDA account is duplicate | 9 | 16 | +7 | +77.8% |
| PDA data length is invalid | 11 | 19 | +8 | +72.7% |
| System program is duplicate | 13 | 26 | +13 | +100.0% |
| System program data length is invalid | 15 | 29 | +14 | +93.3% |
| PDA mismatch | 1543 | 1561 | +18 | +1.2% |
| Happy path | 2834 | 2913 | +79 | +2.8% |
⚗️ Initialize (adjusted)
| Case | Fixed costs | ASM (net) | Rust (net) | Overhead | Overhead % (net) |
|---|---|---|---|---|---|
| PDA mismatch | 1500 | 43 | 61 | +18 | +41.9% |
| Happy path | 2713 | 121 | 200 | +79 | +65.3% |
📦 Increment
| Case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % |
|---|---|---|---|---|
| PDA account is duplicate | 10 | 16 | +6 | +60.0% |
| PDA data length is invalid | 12 | 19 | +7 | +58.3% |
| No instruction data provided | 14 | 26 | +12 | +85.7% |
| Unable to derive PDA | 1535 | 1558 | +23 | +1.5% |
| PDA mismatch | 1540 | 1565 | +25 | +1.6% |
| Happy path | 1548 | 1575 | +27 | +1.7% |
🎁 Increment (adjusted)
| Case | Fixed costs | ASM (net) | Rust (net) | Overhead | Overhead % (net) |
|---|---|---|---|---|---|
| Unable to derive PDA | 1500 | 35 | 58 | +23 | +65.7% |
| PDA mismatch | 1500 | 40 | 65 | +25 | +62.5% |
| Happy path | 1500 | 48 | 75 | +27 | +56.2% |
✅ All tests
tests.rs
rs
use crate::constants::constants;
use mollusk_svm::program;
use mollusk_svm::result::Check;
use solana_rent::ACCOUNT_STORAGE_OVERHEAD;
use solana_sdk::account::Account;
use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::program_error::ProgramError;
use solana_sdk::pubkey::Pubkey;
use std::mem::{offset_of, size_of};
use std::{fs, vec};
use test_utils::{setup_test, ProgramLanguage};
#[test]
fn test_asm_file_constants() {
const GLOBAL_ENTRYPOINT: &str = ".global entrypoint";
// Parse assembly file.
let asm_path = setup_test(ProgramLanguage::Assembly)
.asm_source_path
.expect("Assembly source file not found");
let content = fs::read_to_string(&asm_path).expect("Failed to read assembly file");
let global_pos = content
.find(GLOBAL_ENTRYPOINT)
.expect("Could not find '.global entrypoint' in assembly file");
// Overwrite assembly file with updated constants, asserting nothing changed.
let after_global = &content[global_pos..];
let new_content = format!("{}\n{}", constants().to_asm(), after_global);
let changed = new_content != content;
fs::write(&asm_path, new_content).expect("Failed to write assembly file");
assert!(
!changed,
"Assembly file constants were out of date and have been updated. Please re-run the test."
);
}
const USER_STARTING_LAMPORTS: u64 = 1_000_000;
enum Operation {
Initialize,
Increment,
}
#[repr(C, packed)]
struct CounterAccountData {
counter: u64,
bump_seed: u8,
}
#[repr(C, packed)]
struct CounterAccount {
pubkey: Pubkey,
owner: Pubkey,
lamports: u64,
data: CounterAccountData,
}
impl CounterAccount {
fn check(&self) -> Check<'_> {
Check::account(&self.pubkey)
.data(self.data.as_bytes())
.lamports(self.lamports)
.space(size_of::<CounterAccountData>())
.owner(&self.owner)
.build()
}
}
impl CounterAccountData {
fn as_bytes(&self) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
self as *const Self as *const u8,
std::mem::size_of::<Self>(),
)
}
}
}
enum AccountIndex {
User = 0,
Pda = 1,
SystemProgram = 2,
}
struct ComputeUnits {
asm: u64,
rs: u64,
}
#[derive(Clone, Copy)]
enum Case {
// Initialize error cases (in ASM execution order).
InitializeNoAccounts,
InitializeTooManyAccounts,
InitializeUserDataLen,
InitializePdaDuplicate,
InitializePdaDataLen,
InitializeSystemProgramDuplicate,
InitializeSystemProgramDataLen,
InitializePdaMismatch,
InitializeHappyPath,
// Increment error cases (in ASM execution order).
IncrementPdaDuplicate,
IncrementPdaDataLen,
IncrementNoInstructionData,
IncrementUnableToDerivePda,
IncrementPdaMismatch,
IncrementHappyPath,
}
impl ComputeUnits {
fn for_lang(&self, lang: &ProgramLanguage) -> u64 {
match lang {
ProgramLanguage::Assembly => self.asm,
ProgramLanguage::Rust => self.rs,
}
}
}
/// Fixed costs for syscalls and CPI operations.
mod fixed_costs {
/// Cost for sol_create_program_address / sol_try_find_program_address syscall.
pub const CREATE_PROGRAM_ADDRESS: u64 = 1500;
/// Cost for sol_get_rent_sysvar syscall.
pub const SYSVAR_RENT: u64 = 117;
/// CPI base invocation cost (SIMD-0339).
pub const CPI_BASE: u64 = 946;
/// System Program operation cost.
pub const SYSTEM_PROGRAM: u64 = 150;
}
impl Case {
const fn get(self) -> ComputeUnits {
match self {
// Initialize
Self::InitializeNoAccounts => ComputeUnits { asm: 5, rs: 6 },
Self::InitializeTooManyAccounts => ComputeUnits { asm: 5, rs: 6 },
Self::InitializeUserDataLen => ComputeUnits { asm: 7, rs: 9 },
Self::InitializePdaDuplicate => ComputeUnits { asm: 9, rs: 16 },
Self::InitializePdaDataLen => ComputeUnits { asm: 11, rs: 19 },
Self::InitializeSystemProgramDuplicate => ComputeUnits { asm: 13, rs: 26 },
Self::InitializeSystemProgramDataLen => ComputeUnits { asm: 15, rs: 29 },
Self::InitializePdaMismatch => ComputeUnits {
asm: 1543,
rs: 1561,
},
Self::InitializeHappyPath => ComputeUnits {
asm: 2834,
rs: 2913,
},
// Increment
Self::IncrementPdaDuplicate => ComputeUnits { asm: 10, rs: 16 },
Self::IncrementPdaDataLen => ComputeUnits { asm: 12, rs: 19 },
Self::IncrementNoInstructionData => ComputeUnits { asm: 14, rs: 26 },
Self::IncrementUnableToDerivePda => ComputeUnits {
asm: 1535,
rs: 1558,
},
Self::IncrementPdaMismatch => ComputeUnits {
asm: 1540,
rs: 1565,
},
Self::IncrementHappyPath => ComputeUnits {
asm: 1548,
rs: 1575,
},
}
}
/// Returns the fixed syscall/CPI costs for this case.
/// These costs are identical for both ASM and Rust implementations.
const fn fixed_costs(self) -> u64 {
match self {
// Initialize: early exits have no fixed costs.
Self::InitializeNoAccounts
| Self::InitializeTooManyAccounts
| Self::InitializeUserDataLen
| Self::InitializePdaDuplicate
| Self::InitializePdaDataLen
| Self::InitializeSystemProgramDuplicate
| Self::InitializeSystemProgramDataLen => 0,
// Initialize: PDA mismatch calls sol_try_find_program_address only.
Self::InitializePdaMismatch => fixed_costs::CREATE_PROGRAM_ADDRESS,
// Initialize: Happy path calls sol_try_find_program_address + sol_get_rent_sysvar
// + CPI to System Program.
Self::InitializeHappyPath => {
fixed_costs::CREATE_PROGRAM_ADDRESS
+ fixed_costs::SYSVAR_RENT
+ fixed_costs::CPI_BASE
+ fixed_costs::SYSTEM_PROGRAM
}
// Increment: early exits have no fixed costs.
Self::IncrementPdaDuplicate
| Self::IncrementPdaDataLen
| Self::IncrementNoInstructionData => 0,
// Increment: PDA-related cases call sol_create_program_address.
Self::IncrementUnableToDerivePda
| Self::IncrementPdaMismatch
| Self::IncrementHappyPath => fixed_costs::CREATE_PROGRAM_ADDRESS,
}
}
const fn name(&self) -> &'static str {
match self {
Self::InitializeNoAccounts => "No accounts passed",
Self::InitializeTooManyAccounts => "Too many accounts passed",
Self::InitializeUserDataLen => "Invalid user data length",
Self::InitializePdaDuplicate => "PDA account is duplicate",
Self::InitializePdaDataLen => "PDA data length is invalid",
Self::InitializeSystemProgramDuplicate => "System program is duplicate",
Self::InitializeSystemProgramDataLen => "System program data length is invalid",
Self::InitializePdaMismatch => "PDA mismatch",
Self::InitializeHappyPath => "Happy path",
Self::IncrementPdaDuplicate => "PDA account is duplicate",
Self::IncrementPdaDataLen => "PDA data length is invalid",
Self::IncrementNoInstructionData => "No instruction data provided",
Self::IncrementUnableToDerivePda => "Unable to derive PDA",
Self::IncrementPdaMismatch => "PDA mismatch",
Self::IncrementHappyPath => "Happy path",
}
}
const INITIALIZE_CASES: &'static [Case] = &[
Case::InitializeNoAccounts,
Case::InitializeTooManyAccounts,
Case::InitializeUserDataLen,
Case::InitializePdaDuplicate,
Case::InitializePdaDataLen,
Case::InitializeSystemProgramDuplicate,
Case::InitializeSystemProgramDataLen,
Case::InitializePdaMismatch,
Case::InitializeHappyPath,
];
const INCREMENT_CASES: &'static [Case] = &[
Case::IncrementPdaDuplicate,
Case::IncrementPdaDataLen,
Case::IncrementNoInstructionData,
Case::IncrementUnableToDerivePda,
Case::IncrementPdaMismatch,
Case::IncrementHappyPath,
];
fn generate_markdown_table(title: &str, cases: &[Case]) -> String {
let mut table = format!("### {}\n\n", title);
table.push_str("| Case | ASM (CUs) | Rust (CUs) | Overhead | Overhead % |\n");
table.push_str("|------|-----------|------------|----------|------------|\n");
for case in cases {
let cu = case.get();
let overhead = cu.rs as i64 - cu.asm as i64;
let overhead_pct = if cu.asm > 0 {
(overhead as f64 / cu.asm as f64) * 100.0
} else {
0.0
};
table.push_str(&format!(
"| {} | {} | {} | {:+} | {:+.1}% |\n",
case.name(),
cu.asm,
cu.rs,
overhead,
overhead_pct
));
}
table
}
fn generate_adjusted_table(title: &str, cases: &[Case]) -> String {
// Filter to only cases with fixed costs.
let adjusted_cases: Vec<_> = cases.iter().filter(|c| c.fixed_costs() > 0).collect();
if adjusted_cases.is_empty() {
return String::new();
}
let mut table = format!("### {} (adjusted)\n\n", title);
table.push_str(
"| Case | Fixed costs | ASM (net) | Rust (net) | Overhead | Overhead % (net) |\n",
);
table.push_str(
"|------|-------------|-----------|------------|----------|------------------|\n",
);
for case in adjusted_cases {
let cu = case.get();
let fixed = case.fixed_costs();
let asm_adj = cu.asm.saturating_sub(fixed);
let rs_adj = cu.rs.saturating_sub(fixed);
let overhead = cu.rs as i64 - cu.asm as i64;
let adj_overhead_pct = if asm_adj > 0 {
(overhead as f64 / asm_adj as f64) * 100.0
} else {
f64::NAN
};
let adj_overhead_str = if adj_overhead_pct.is_nan() {
"N/A".to_string()
} else {
format!("{:+.1}%", adj_overhead_pct)
};
table.push_str(&format!(
"| {} | {} | {} | {} | {:+} | {} |\n",
case.name(),
fixed,
asm_adj,
rs_adj,
overhead,
adj_overhead_str
));
}
table
}
fn markdown_tables() -> String {
let mut output = String::new();
output.push_str(&Self::generate_markdown_table(
":test_tube: Initialize",
Self::INITIALIZE_CASES,
));
output.push('\n');
output.push_str(&Self::generate_adjusted_table(
":alembic: Initialize",
Self::INITIALIZE_CASES,
));
output.push('\n');
output.push_str(&Self::generate_markdown_table(
":package: Increment",
Self::INCREMENT_CASES,
));
output.push('\n');
output.push_str(&Self::generate_adjusted_table(
":gift: Increment",
Self::INCREMENT_CASES,
));
output
}
}
fn happy_path_setup(
program_language: ProgramLanguage,
operation: Operation,
) -> (
test_utils::TestSetup,
Instruction,
Vec<(Pubkey, Account)>,
CounterAccount,
) {
let setup = setup_test(program_language);
let (system_program, system_account) = program::keyed_account_for_system_program();
let user_pubkey = Pubkey::new_unique();
let (pda_pubkey, bump_seed) =
Pubkey::find_program_address(&[user_pubkey.as_ref()], &setup.program_id);
let mut instruction = Instruction::new_with_bytes(
setup.program_id,
&[],
vec![
AccountMeta::new(user_pubkey, true),
AccountMeta::new(pda_pubkey, false),
],
);
let mut accounts = vec![
(
instruction.accounts[AccountIndex::User as usize].pubkey,
Account::new(USER_STARTING_LAMPORTS, 0, &system_program),
),
(
instruction.accounts[AccountIndex::Pda as usize].pubkey,
Account::new(0, 0, &system_program),
),
];
let counter_account = CounterAccount {
pubkey: pda_pubkey,
owner: setup.program_id,
lamports: setup.mollusk.sysvars.rent.lamports_per_byte_year
* ((size_of::<CounterAccountData>() as u64) + ACCOUNT_STORAGE_OVERHEAD),
data: CounterAccountData {
counter: 0,
bump_seed,
},
};
match operation {
Operation::Initialize => {
instruction
.accounts
.push(AccountMeta::new_readonly(system_program, false));
accounts.push((system_program, system_account));
}
Operation::Increment => {
let counter_account_info = &mut accounts[AccountIndex::Pda as usize].1;
counter_account_info.lamports = counter_account.lamports;
counter_account_info.data = counter_account.data.as_bytes().to_vec().clone();
counter_account_info.owner = setup.program_id;
}
}
(setup, instruction, accounts, counter_account)
}
fn test_no_accounts(lang: ProgramLanguage) {
let cu = Case::InitializeNoAccounts.get().for_lang(&lang);
let (setup, mut instruction, mut accounts, _) = happy_path_setup(lang, Operation::Initialize);
instruction.accounts.clear();
accounts.clear();
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::Custom(constants().get("E_N_ACCOUNTS") as u32)),
Check::compute_units(cu),
],
);
}
fn test_too_many_accounts(lang: ProgramLanguage) {
let cu = Case::InitializeTooManyAccounts.get().for_lang(&lang);
let (setup, mut instruction, mut accounts, _) = happy_path_setup(lang, Operation::Initialize);
instruction
.accounts
.push(AccountMeta::new_readonly(Pubkey::new_unique(), false));
accounts.push((
instruction.accounts.last().unwrap().pubkey,
Account::default(),
));
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::Custom(constants().get("E_N_ACCOUNTS") as u32)),
Check::compute_units(cu),
],
);
}
fn test_initialize_user_data_len(lang: ProgramLanguage) {
let cu = Case::InitializeUserDataLen.get().for_lang(&lang);
let (setup, instruction, mut accounts, _) = happy_path_setup(lang, Operation::Initialize);
accounts[AccountIndex::User as usize].1.data = vec![1u8; 1];
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::Custom(
constants().get("E_USER_DATA_LEN") as u32
)),
Check::compute_units(cu),
],
);
}
fn test_initialize_pda_duplicate(lang: ProgramLanguage) {
let cu = Case::InitializePdaDuplicate.get().for_lang(&lang);
let (setup, mut instruction, mut accounts, _) = happy_path_setup(lang, Operation::Initialize);
instruction.accounts[AccountIndex::Pda as usize] =
instruction.accounts[AccountIndex::User as usize].clone();
accounts[AccountIndex::Pda as usize] = accounts[AccountIndex::User as usize].clone();
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::Custom(
constants().get("E_PDA_DUPLICATE") as u32
)),
Check::compute_units(cu),
],
);
}
fn test_initialize_pda_data_len(lang: ProgramLanguage) {
let cu = Case::InitializePdaDataLen.get().for_lang(&lang);
let (setup, instruction, mut accounts, _) = happy_path_setup(lang, Operation::Initialize);
accounts[AccountIndex::Pda as usize].1.data = vec![1u8; 1];
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::Custom(
constants().get("E_PDA_DATA_LEN") as u32
)),
Check::compute_units(cu),
],
);
}
fn test_initialize_system_program_duplicate(lang: ProgramLanguage) {
let cu = Case::InitializeSystemProgramDuplicate.get().for_lang(&lang);
let (setup, mut instruction, mut accounts, _) = happy_path_setup(lang, Operation::Initialize);
instruction.accounts[AccountIndex::SystemProgram as usize] =
instruction.accounts[AccountIndex::User as usize].clone();
accounts[AccountIndex::SystemProgram as usize] = accounts[AccountIndex::User as usize].clone();
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::Custom(
constants().get("E_SYSTEM_PROGRAM_DUPLICATE") as u32,
)),
Check::compute_units(cu),
],
);
}
fn test_initialize_system_program_data_len(lang: ProgramLanguage) {
let cu = Case::InitializeSystemProgramDataLen.get().for_lang(&lang);
let (setup, instruction, mut accounts, _) = happy_path_setup(lang, Operation::Initialize);
accounts[AccountIndex::SystemProgram as usize].1.data = vec![];
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::Custom(
constants().get("E_SYSTEM_PROGRAM_DATA_LEN") as u32,
)),
Check::compute_units(cu),
],
);
}
fn test_initialize_pda_mismatch(lang: ProgramLanguage) {
let cu = Case::InitializePdaMismatch.get().for_lang(&lang);
let (setup, instruction, accounts, _) = happy_path_setup(lang, Operation::Initialize);
test_pda_mismatch_chunks(&setup, instruction, accounts, cu, None);
}
fn test_initialize_happy_path(lang: ProgramLanguage) {
let cu = Case::InitializeHappyPath.get().for_lang(&lang);
let (setup, instruction, accounts, counter_account) =
happy_path_setup(lang, Operation::Initialize);
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::success(),
counter_account.check(),
Check::compute_units(cu),
],
);
}
fn test_increment_pda_duplicate(lang: ProgramLanguage) {
let cu = Case::IncrementPdaDuplicate.get().for_lang(&lang);
let (setup, mut instruction, mut accounts, _) = happy_path_setup(lang, Operation::Increment);
instruction.accounts[AccountIndex::Pda as usize] =
instruction.accounts[AccountIndex::User as usize].clone();
accounts[AccountIndex::Pda as usize] = accounts[AccountIndex::User as usize].clone();
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::Custom(
constants().get("E_PDA_DUPLICATE") as u32
)),
Check::compute_units(cu),
],
);
}
fn test_increment_pda_data_len(lang: ProgramLanguage) {
let cu = Case::IncrementPdaDataLen.get().for_lang(&lang);
let (setup, instruction, mut accounts, _) = happy_path_setup(lang, Operation::Increment);
accounts[AccountIndex::Pda as usize].1.data = vec![1u8; 1];
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::Custom(
constants().get("E_PDA_DATA_LEN") as u32
)),
Check::compute_units(cu),
],
);
}
fn test_increment_no_instruction_data(lang: ProgramLanguage) {
let cu = Case::IncrementNoInstructionData.get().for_lang(&lang);
let (setup, instruction, accounts, _) = happy_path_setup(lang, Operation::Increment);
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::Custom(
constants().get("E_INVALID_INSTRUCTION_DATA_LEN") as u32,
)),
Check::compute_units(cu),
],
);
}
fn test_increment_unable_to_derive_pda(lang: ProgramLanguage) {
let cu = Case::IncrementUnableToDerivePda.get().for_lang(&lang);
let (setup, mut instruction, mut accounts, _) = happy_path_setup(lang, Operation::Increment);
instruction.data = 1u64.to_le_bytes().to_vec();
// Find a user pubkey whose PDA bump is < u8::MAX, so bump + 1 is guaranteed to fail since
// find_program_address already rejected it.
let mut user_pubkey = accounts[AccountIndex::User as usize].0;
let (mut pda_pubkey, mut bump_seed) =
Pubkey::find_program_address(&[user_pubkey.as_ref()], &setup.program_id);
while bump_seed == u8::MAX {
user_pubkey = Pubkey::new_unique();
(pda_pubkey, bump_seed) =
Pubkey::find_program_address(&[user_pubkey.as_ref()], &setup.program_id);
}
// Update account keys and set bump seed + 1 in PDA account data.
instruction.accounts[AccountIndex::User as usize].pubkey = user_pubkey;
instruction.accounts[AccountIndex::Pda as usize].pubkey = pda_pubkey;
accounts[AccountIndex::User as usize].0 = user_pubkey;
accounts[AccountIndex::Pda as usize].0 = pda_pubkey;
accounts[AccountIndex::Pda as usize].1.data[offset_of!(CounterAccountData, bump_seed)] =
bump_seed + 1;
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::Custom(
constants().get("E_UNABLE_TO_DERIVE_PDA") as u32,
)),
Check::compute_units(cu),
],
);
}
fn test_increment_pda_mismatch(lang: ProgramLanguage) {
let cu = Case::IncrementPdaMismatch.get().for_lang(&lang);
let (setup, instruction, accounts, _) = happy_path_setup(lang, Operation::Increment);
test_pda_mismatch_chunks(
&setup,
instruction,
accounts,
cu,
Some(1u64.to_le_bytes().to_vec()),
);
}
/// Helper for testing PDA mismatch detection in each 8-byte chunk of the 32-byte pubkey.
fn test_pda_mismatch_chunks(
setup: &test_utils::TestSetup,
instruction: Instruction,
accounts: Vec<(Pubkey, Account)>,
base_cu: u64,
instruction_data: Option<Vec<u8>>,
) {
const CHUNK_INCREMENT: [u64; size_of::<Pubkey>() / size_of::<u64>()] = [0, 3, 6, 9];
const FINAL_BIT: usize = size_of::<u64>() - 1;
for (chunk, &increment) in CHUNK_INCREMENT.iter().enumerate() {
let mut instruction = instruction.clone();
let mut accounts = accounts.clone();
if let Some(ref data) = instruction_data {
instruction.data = data.clone();
}
// Flip the last bit of the chunk to create a mismatch.
let flip_index = (chunk * size_of::<u64>()) + FINAL_BIT;
accounts[AccountIndex::Pda as usize].0.as_mut()[flip_index] ^= 1;
instruction.accounts[AccountIndex::Pda as usize].pubkey =
accounts[AccountIndex::Pda as usize].0;
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::err(ProgramError::Custom(
constants().get("E_PDA_MISMATCH") as u32
)),
Check::compute_units(base_cu + increment),
],
);
}
}
struct IncrementTestCase {
user_account_data_length: u64,
starting_counter: u64,
increment: u64,
}
const INCREMENT_TEST_CASES: &[IncrementTestCase] = &[
// Aligned user data lengths.
IncrementTestCase {
user_account_data_length: 0,
starting_counter: 0,
increment: 1,
},
IncrementTestCase {
user_account_data_length: 0,
starting_counter: 0,
increment: u64::MAX,
},
IncrementTestCase {
user_account_data_length: 0,
starting_counter: u64::MAX,
increment: 1,
},
IncrementTestCase {
user_account_data_length: 0,
starting_counter: u64::MAX,
increment: u64::MAX,
},
IncrementTestCase {
user_account_data_length: 8,
starting_counter: 0,
increment: 1,
},
IncrementTestCase {
user_account_data_length: 16,
starting_counter: 1,
increment: 1,
},
IncrementTestCase {
user_account_data_length: 128,
starting_counter: 100,
increment: 200,
},
// Unaligned user data lengths.
IncrementTestCase {
user_account_data_length: 1,
starting_counter: 0,
increment: 1,
},
IncrementTestCase {
user_account_data_length: 7,
starting_counter: 1,
increment: u64::MAX - 1,
},
IncrementTestCase {
user_account_data_length: 9,
starting_counter: 100,
increment: 200,
},
IncrementTestCase {
user_account_data_length: 15,
starting_counter: u64::MAX,
increment: 1,
},
IncrementTestCase {
user_account_data_length: 100,
starting_counter: u64::MAX,
increment: u64::MAX,
},
];
fn test_increment_happy_path(lang: ProgramLanguage) {
let cu = Case::IncrementHappyPath.get().for_lang(&lang);
for tc in INCREMENT_TEST_CASES {
let (setup, mut instruction, mut accounts, mut counter_account) =
happy_path_setup(lang, Operation::Increment);
instruction.data = tc.increment.to_le_bytes().to_vec();
accounts[AccountIndex::User as usize].1.data =
vec![0u8; tc.user_account_data_length as usize];
accounts[AccountIndex::Pda as usize].1.data[..size_of::<u64>()]
.copy_from_slice(&tc.starting_counter.to_le_bytes());
counter_account.data.counter = tc.starting_counter.wrapping_add(tc.increment);
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[
Check::success(),
counter_account.check(),
Check::compute_units(cu),
],
);
}
}
#[test]
fn test_asm_no_accounts() {
test_no_accounts(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_no_accounts() {
test_no_accounts(ProgramLanguage::Rust);
}
#[test]
fn test_asm_too_many_accounts() {
test_too_many_accounts(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_too_many_accounts() {
test_too_many_accounts(ProgramLanguage::Rust);
}
#[test]
fn test_asm_initialize_user_data_len() {
test_initialize_user_data_len(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_initialize_user_data_len() {
test_initialize_user_data_len(ProgramLanguage::Rust);
}
#[test]
fn test_asm_initialize_pda_duplicate() {
test_initialize_pda_duplicate(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_initialize_pda_duplicate() {
test_initialize_pda_duplicate(ProgramLanguage::Rust);
}
#[test]
fn test_asm_initialize_pda_data_len() {
test_initialize_pda_data_len(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_initialize_pda_data_len() {
test_initialize_pda_data_len(ProgramLanguage::Rust);
}
#[test]
fn test_asm_initialize_system_program_duplicate() {
test_initialize_system_program_duplicate(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_initialize_system_program_duplicate() {
test_initialize_system_program_duplicate(ProgramLanguage::Rust);
}
#[test]
fn test_asm_initialize_system_program_data_len() {
test_initialize_system_program_data_len(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_initialize_system_program_data_len() {
test_initialize_system_program_data_len(ProgramLanguage::Rust);
}
#[test]
fn test_asm_initialize_pda_mismatch() {
test_initialize_pda_mismatch(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_initialize_pda_mismatch() {
test_initialize_pda_mismatch(ProgramLanguage::Rust);
}
#[test]
fn test_asm_initialize_happy_path() {
test_initialize_happy_path(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_initialize_happy_path() {
test_initialize_happy_path(ProgramLanguage::Rust);
}
#[test]
fn test_pad_masking() {
let increment = 7;
let mask_immediate = -8i32; // Assembly immediate.
let mask = (mask_immediate as i64) as u64; // VM interpretation.
let hex = 0xffff_ffff_ffff_fff8u64;
let binary = 0b1111111111111111111111111111111111111111111111111111111111111000u64;
assert_eq!(mask, hex);
assert_eq!(mask, u64::MAX - 7u64);
assert_eq!(mask, binary);
let padded_data_len = |data_len: u64| -> u64 { (data_len + increment) & mask };
assert_eq!(padded_data_len(0), 0);
assert_eq!(padded_data_len(1), 8);
assert_eq!(padded_data_len(8), 8);
assert_eq!(padded_data_len(9), 16);
assert_eq!(padded_data_len(15), 16);
}
#[test]
fn test_asm_increment_pda_duplicate() {
test_increment_pda_duplicate(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_increment_pda_duplicate() {
test_increment_pda_duplicate(ProgramLanguage::Rust);
}
#[test]
fn test_asm_increment_pda_data_len() {
test_increment_pda_data_len(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_increment_pda_data_len() {
test_increment_pda_data_len(ProgramLanguage::Rust);
}
#[test]
fn test_asm_increment_no_instruction_data() {
test_increment_no_instruction_data(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_increment_no_instruction_data() {
test_increment_no_instruction_data(ProgramLanguage::Rust);
}
#[test]
fn test_asm_increment_unable_to_derive_pda() {
test_increment_unable_to_derive_pda(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_increment_unable_to_derive_pda() {
test_increment_unable_to_derive_pda(ProgramLanguage::Rust);
}
#[test]
fn test_asm_increment_pda_mismatch() {
test_increment_pda_mismatch(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_increment_pda_mismatch() {
test_increment_pda_mismatch(ProgramLanguage::Rust);
}
#[test]
fn test_asm_increment_happy_path() {
test_increment_happy_path(ProgramLanguage::Assembly);
}
#[test]
fn test_rs_increment_happy_path() {
test_increment_happy_path(ProgramLanguage::Rust);
}
#[test]
fn test_print_compute_units_tables() {
println!("\n{}", Case::markdown_tables());
}NOTE
The assembly file in this example was adapted from an sbpf example.