Appearance
Transfer β
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 demonstrates how to transfer SOL (Lamports) between accounts using SBPF assembly. This is a fundamental operation in Solana programs, requiring proper account validation, ownership checks, and Lamport arithmetic.
A transfer operation requires three accounts:
| Account | Description |
|---|---|
| Sender | The account to transfer from |
| Recipient | The account to transfer to |
| System | System Program (for CPI) |
πΊοΈ Account layout background β
Accounts in the input buffer are serialized and deserialized with the following offsets relative to the start of the account, assuming non-duplicate accounts without any account data:
| Offset (bytes) | Length (bytes) | Description |
|---|---|---|
| 0 | 1 | NON_DUP_MARKER |
| 1 | 1 | Is signer? |
| 2 | 1 | Is writable? |
| 3 | 1 | Is executable? |
| 4 | 4 | Original account data length |
| 8 | 32 | Account pubkey |
| 40 | 32 | Account owner |
| 72 | 8 | Lamports balance |
| 80 | 8 | Account data length |
| 88 | 0 | Account data (none) |
| 88 | 10240 | Account data padding |
| 10328 | 8 | Account rent epoch |
The account data padding length is the sum of:
MAX_PERMITTED_DATA_INCREASE.- Additional padding to align the account data length to an 8-byte boundary.
Note however that the System Program is a builtin, which means that its account data is its name, specifically b"system_program" (14 bytes). This means that the System Program has the following:
| Offset (bytes) | Length (bytes) | Description |
|---|---|---|
| 88 | 14 | Account data |
| 102 | 10242 | Account data padding |
| 10344 | 8 | Account rent epoch |
π‘οΈ Input validation β
Input offsets are validated in Rust using struct operations:
asm
# Account layout.
.equ N_ACCOUNTS_OFFSET, 0
.equ N_ACCOUNTS_EXPECTED, 3
.equ NON_DUP_MARKER, 0xff
.equ DATA_LENGTH_ZERO, 0
.equ PUBKEY_SIZE_OF, 32
.equ U8_SIZE_OF, 8
.equ U128_SIZE_OF, 16
.equ BOOL_TRUE, 1
.equ BOOL_FALSE, 0
# Sender account.
.equ SENDER_OFFSET, 8
.equ SENDER_IS_SIGNER_OFFSET, 9
.equ SENDER_IS_WRITABLE_OFFSET, 10
.equ SENDER_IS_EXECUTABLE_OFFSET, 11
.equ SENDER_PUBKEY_OFFSET, 16
.equ SENDER_LAMPORTS_OFFSET, 80
.equ SENDER_DATA_LENGTH_OFFSET, 88
.equ SENDER_RENT_EPOCH_OFFSET, 10336
# Recipient account.
.equ RECIPIENT_OFFSET, 10344
.equ RECIPIENT_PUBKEY_OFFSET, 10352
.equ RECIPIENT_IS_SIGNER_OFFSET, 10345
.equ RECIPIENT_IS_WRITABLE_OFFSET, 10346
.equ RECIPIENT_IS_EXECUTABLE_OFFSET, 10347
.equ RECIPIENT_DATA_LENGTH_OFFSET, 10424
.equ RECIPIENT_RENT_EPOCH_OFFSET, 20672
.equ RECIPIENT_PUBKEY_OFFSET_RELATIVE_TO_SENDER_DATA_OFFSET, 10256
# System program account.
.equ SYSTEM_PROGRAM_OFFSET, 20680
.equ SYSTEM_PROGRAM_PUBKEY_OFFSET, 20688
# Transfer input.
.equ INSTRUCTION_DATA_LENGTH_OFFSET, 31032
.equ INSTRUCTION_DATA_LENGTH_EXPECTED, 8
.equ INSTRUCTION_DATA_OFFSET, 31040
.global entrypointrs
#[test]
fn test_input_offsets() {
const MAX_PERMITTED_DATA_INCREASE: usize = 10240;
#[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; 32],
owner: [u8; 32],
lamports: u64,
data_length: u64,
data_padded: [u8; PADDED_DATA_SIZE],
rent_epoch: u64,
}
type StandardAccount = AccountLayout<MAX_PERMITTED_DATA_INCREASE>;
type SystemProgramAccount = AccountLayout<{ MAX_PERMITTED_DATA_INCREASE + 16 }>;
// Sender.
const SENDER_OFFSET: usize = 8;
const SENDER_PUBKEY_OFFSET: usize = 16;
const SENDER_IS_SIGNER_OFFSET: usize = 9;
const SENDER_IS_WRITABLE_OFFSET: usize = 10;
const SENDER_IS_EXECUTABLE_OFFSET: usize = 11;
const SENDER_LAMPORTS_OFFSET: usize = 80;
const SENDER_DATA_LENGTH_OFFSET: usize = 88;
const SENDER_DATA_OFFSET: usize = 96;
const SENDER_RENT_EPOCH_OFFSET: usize = 10336;
// Recipient.
const RECIPIENT_OFFSET: usize = 10344;
const RECIPIENT_PUBKEY_OFFSET: usize = 10352;
const RECIPIENT_IS_SIGNER_OFFSET: usize = 10345;
const RECIPIENT_IS_WRITABLE_OFFSET: usize = 10346;
const RECIPIENT_IS_EXECUTABLE_OFFSET: usize = 10347;
const RECIPIENT_DATA_LENGTH_OFFSET: usize = 10424;
const RECIPIENT_RENT_EPOCH_OFFSET: usize = 20672;
const RECIPIENT_PUBKEY_OFFSET_RELATIVE_TO_SENDER_DATA_OFFSET: usize = 10256;
// System program.
const SYSTEM_PROGRAM_OFFSET: usize = 20680;
const SYSTEM_PROGRAM_PUBKEY_OFFSET: usize = 20688;
// Instruction data.
const INSTRUCTION_DATA_LENGTH_OFFSET: usize = 31032;
const INSTRUCTION_DATA_OFFSET: usize = 31040;
// Sender checks.
assert_eq!(
SENDER_IS_SIGNER_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, is_signer),
);
assert_eq!(
SENDER_IS_WRITABLE_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, is_writable),
);
assert_eq!(
SENDER_IS_EXECUTABLE_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, is_executable),
);
assert_eq!(
SENDER_LAMPORTS_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, lamports),
);
assert_eq!(
SENDER_DATA_LENGTH_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, data_length),
);
assert_eq!(
SENDER_PUBKEY_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, pubkey),
);
assert_eq!(
SENDER_RENT_EPOCH_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, rent_epoch),
);
assert_eq!(
SENDER_DATA_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, data_padded),
);
// Recipient checks.
assert_eq!(
RECIPIENT_OFFSET,
SENDER_OFFSET + size_of::<StandardAccount>()
);
assert_eq!(
RECIPIENT_DATA_LENGTH_OFFSET,
RECIPIENT_OFFSET + offset_of!(StandardAccount, data_length),
);
assert_eq!(
RECIPIENT_PUBKEY_OFFSET,
RECIPIENT_OFFSET + offset_of!(StandardAccount, pubkey),
);
assert_eq!(
RECIPIENT_IS_SIGNER_OFFSET,
RECIPIENT_OFFSET + offset_of!(StandardAccount, is_signer),
);
assert_eq!(
RECIPIENT_IS_WRITABLE_OFFSET,
RECIPIENT_OFFSET + offset_of!(StandardAccount, is_writable),
);
assert_eq!(
RECIPIENT_IS_EXECUTABLE_OFFSET,
RECIPIENT_OFFSET + offset_of!(StandardAccount, is_executable),
);
assert_eq!(
RECIPIENT_RENT_EPOCH_OFFSET,
RECIPIENT_OFFSET + offset_of!(StandardAccount, rent_epoch),
);
assert_eq!(
RECIPIENT_PUBKEY_OFFSET_RELATIVE_TO_SENDER_DATA_OFFSET,
RECIPIENT_PUBKEY_OFFSET - SENDER_DATA_OFFSET
);
// System program checks.
assert_eq!(
SYSTEM_PROGRAM_OFFSET,
RECIPIENT_OFFSET + size_of::<StandardAccount>()
);
assert_eq!(
SYSTEM_PROGRAM_PUBKEY_OFFSET,
SYSTEM_PROGRAM_OFFSET + offset_of!(SystemProgramAccount, pubkey),
);
assert_eq!(
INSTRUCTION_DATA_LENGTH_OFFSET,
SYSTEM_PROGRAM_OFFSET + size_of::<SystemProgramAccount>()
);
assert_eq!(
INSTRUCTION_DATA_OFFSET,
INSTRUCTION_DATA_LENGTH_OFFSET + size_of::<u64>(),
);
}Due to the account layout order, account layout validation takes place in a specific sequence, before the final Lamport balance check:
asm
.global entrypoint
entrypoint:
# Check number of accounts.
ldxdw r2, [r1 + N_ACCOUNTS_OFFSET]
jne r2, N_ACCOUNTS_EXPECTED, e_n_accounts
# Check sender data length, since a nonzero length would invalidate
# subsequent offsets.
ldxdw r2, [r1 + SENDER_DATA_LENGTH_OFFSET]
jne r2, DATA_LENGTH_ZERO, e_data_length_nonzero_sender
# Check if the recipient account is a duplicate, since duplicate
# accounts have different field layouts.
ldxb r2, [r1 + RECIPIENT_OFFSET]
jne r2, NON_DUP_MARKER, e_duplicate_account_recipient
# Check recipient data length, since a nonzero length would invalidate
# subsequent offsets.
ldxdw r2, [r1 + RECIPIENT_DATA_LENGTH_OFFSET]
jne r2, DATA_LENGTH_ZERO, e_data_length_nonzero_recipient
# Check if the System Account is a duplicate, since duplicate accounts
# have different field layouts. The check for account data length
# is omitted for the System Program because if a caller passes an
# account that is not the System Account, the CPI will fail during
# program ID checks.
ldxb r2, [r1 + SYSTEM_PROGRAM_OFFSET]
jne r2, NON_DUP_MARKER, e_duplicate_account_system_program
# Check instruction data length.
ldxdw r4, [r1 + INSTRUCTION_DATA_LENGTH_OFFSET]
jne r4, INSTRUCTION_DATA_LENGTH_EXPECTED, e_instruction_data_length
# Verify sender has at least as many Lamports as they are trying to
# send. Technically this could be done after checking the number of
# accounts since Lamports balance comes before account data length, but
# in the happy path both checks need to be done anyways and it is
# cleaner to do all layout validation first.
ldxdw r4, [r1 + INSTRUCTION_DATA_OFFSET]
ldxdw r2, [r1 + SENDER_LAMPORTS_OFFSET]
jlt r2, r4, e_insufficient_lamportsπ€ Transfer CPI layout β
The System Program is responsible for transferring Lamports between accounts, and is invoked internally in this example using a CPI via the sol_invoke_signed_c syscall, which accepts the following parameters:
| Register | Description |
|---|---|
r1 | Instruction pointer |
r2 | Account info array pointer |
r3 | Account info array length |
r4 | Signer seeds array pointer |
r5 | Signer seeds array length |
The instruction layout is as follows:
Offset (bytes) Length (bytes) Description 0 8 Program ID (System Program pointer) 8 8 Account metadata array pointer 16 8 Account metadata array length 24 8 Transfer instruction data pointer 32 8 Transfer instruction data length Each element in the account metadata array has the following layout:
Offset (bytes) Length (bytes) Description 0 8 Account pubkey pointer 8 1 Is writable? 9 1 Is signer? 10 6 C-style array padding The transfer instruction data is encoded via
bincode, which usesu32enum variants such that the transfer instruction data layout is as follows:
Offset (bytes) Length (bytes) Description 0 4 Transfer instruction enum variant ( 2)8 8 Amount of Lamports to send
Each account info element has the following layout:
Offset (bytes) Length (bytes) Description 0 8 Account pubkey pointer 8 8 Lamports balance pointer 16 8 Account data length 24 8 Account data pointer 32 8 Account owner pointer 40 8 Account rent epoch 48 1 Is signer? 49 1 Is writable? 50 1 Is executable? 51 5 C-style array padding
In this example, no signer seeds are required due to the lack of a PDA signer.
Since the data required by the CPI is too wide to fit in one of the 64-bit general purpose registers, it must be allocated within a stack frame, which is 4096 bytes wide and pointed to by r10. Moreover, since CPI processor checks rely on inner alignment checks, any data allocated on the stack must be aligned to at least an 8-byte boundary since the largest primitive data type used across the instruction, account metadata, and account info data structures is a u64 pointer:
r10 Offset | Length | Description |
|---|---|---|
| 200 | 40 | Instruction |
| 160 | 16 | Encoded transfer instruction data (with padding) |
| 144 | 32 | Account metadata array (2 accounts) |
| 112 | 112 | Account info array (2 accounts) |
Stack CPI offsets are validated in Rust using struct operations:
TIP
The additional SYSTEM_PROGRAM_PUBKEY_OFFSET constant is used to reference the System Program pubkey within the instruction structure; see below for details.
asm
# Stack offsets.
.equ STACK_SYSTEM_PROGRAM_PUBKEY_OFFSET, 232
.equ STACK_INSN_OFFSET, 200
.equ STACK_INSN_DATA_OFFSET, 160
.equ STACK_ACCT_METAS_OFFSET, 144
.equ STACK_ACCT_INFOS_OFFSET, 112
# CPI instruction offsets.
.equ CPI_INSN_PROGRAM_ID_ADDR_OFFSET, 0
.equ CPI_INSN_ACCOUNTS_ADDR_OFFSET, 8
.equ CPI_INSN_ACCOUNTS_LEN_OFFSET, 16
.equ CPI_INSN_DATA_ADDR_OFFSET, 24
.equ CPI_INSN_DATA_LEN_OFFSET, 32
# CPI account meta offsets.
.equ CPI_ACCT_META_PUBKEY_ADDR_OFFSET, 0
.equ CPI_ACCT_META_IS_WRITABLE_OFFSET, 8
.equ CPI_ACCT_META_IS_SIGNER_OFFSET, 9
.equ CPI_ACCT_META_SIZE_OF, 16
# CPI account meta offsets for recipient.
.equ CPI_ACCT_META_PUBKEY_ADDR_RECIPIENT_OFFSET, 16
.equ CPI_ACCT_META_IS_WRITABLE_RECIPIENT_OFFSET, 24
.equ CPI_ACCT_META_IS_SIGNER_RECIPIENT_OFFSET, 25
# CPI account info offsets.
.equ CPI_ACCT_INFO_KEY_ADDR_OFFSET, 0
.equ CPI_ACCT_INFO_LAMPORTS_ADDR_OFFSET, 8
.equ CPI_ACCT_INFO_DATA_LEN_OFFSET, 16
.equ CPI_ACCT_INFO_DATA_ADDR_OFFSET, 24
.equ CPI_ACCT_INFO_OWNER_ADDR_OFFSET, 32
.equ CPI_ACCT_INFO_RENT_EPOCH_OFFSET, 40
.equ CPI_ACCT_INFO_IS_SIGNER_OFFSET, 48
.equ CPI_ACCT_INFO_IS_WRITABLE_OFFSET, 49
.equ CPI_ACCT_INFO_EXECUTABLE_OFFSET, 50
.equ CPI_ACCT_INFO_SIZE_OF, 56
# CPI account info offsets for recipient.
.equ CPI_ACCT_INFO_KEY_ADDR_RECIPIENT_OFFSET, 56
.equ CPI_ACCT_INFO_LAMPORTS_ADDR_RECIPIENT_OFFSET, 64
.equ CPI_ACCT_INFO_DATA_LEN_RECIPIENT_OFFSET, 72
.equ CPI_ACCT_INFO_DATA_ADDR_RECIPIENT_OFFSET, 80
.equ CPI_ACCT_INFO_OWNER_ADDR_RECIPIENT_OFFSET, 88
.equ CPI_ACCT_INFO_RENT_EPOCH_RECIPIENT_OFFSET, 96
.equ CPI_ACCT_INFO_IS_SIGNER_RECIPIENT_OFFSET, 104
.equ CPI_ACCT_INFO_IS_WRITABLE_RECIPIENT_OFFSET, 105
.equ CPI_ACCT_INFO_EXECUTABLE_RECIPIENT_OFFSET, 106
# CPI instruction data offsets.
.equ CPI_INSN_DATA_VARIANT_OFFSET, 0
.equ CPI_INSN_DATA_AMOUNT_OFFSET, 4
.equ CPI_INSN_DATA_LEN, 12rs
#[test]
fn test_cpi_offsets() {
#[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,
padding: [u8; 6],
}
#[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,
padding: [u8; 5],
}
#[repr(C)]
struct InstructionData {
variant: [u8; 4],
amount: [u8; 8],
padding: [u8; 4],
}
// CPI instruction offsets.
const CPI_INSN_PROGRAM_ID_ADDR_OFFSET: usize = 0;
const CPI_INSN_ACCOUNTS_ADDR_OFFSET: usize = 8;
const CPI_INSN_ACCOUNTS_LEN_OFFSET: usize = 16;
const CPI_INSN_DATA_ADDR_OFFSET: usize = 24;
const CPI_INSN_DATA_LEN_OFFSET: usize = 32;
// CPI account meta offsets.
const CPI_ACCT_META_PUBKEY_ADDR_OFFSET: usize = 0;
const CPI_ACCT_META_IS_WRITABLE_OFFSET: usize = 8;
const CPI_ACCT_META_IS_SIGNER_OFFSET: usize = 9;
const CPI_ACCT_META_SIZE_OF: usize = 16;
// CPI account meta offsets for recipient.
const CPI_ACCT_META_PUBKEY_ADDR_RECIPIENT_OFFSET: usize = 16;
const CPI_ACCT_META_IS_WRITABLE_RECIPIENT_OFFSET: usize = 24;
const CPI_ACCT_META_IS_SIGNER_RECIPIENT_OFFSET: usize = 25;
// CPI account info offsets.
const CPI_ACCT_INFO_KEY_ADDR_OFFSET: usize = 0;
const CPI_ACCT_INFO_LAMPORTS_ADDR_OFFSET: usize = 8;
const CPI_ACCT_INFO_DATA_LEN_OFFSET: usize = 16;
const CPI_ACCT_INFO_DATA_ADDR_OFFSET: usize = 24;
const CPI_ACCT_INFO_OWNER_ADDR_OFFSET: usize = 32;
const CPI_ACCT_INFO_RENT_EPOCH_OFFSET: usize = 40;
const CPI_ACCT_INFO_IS_SIGNER_OFFSET: usize = 48;
const CPI_ACCT_INFO_IS_WRITABLE_OFFSET: usize = 49;
const CPI_ACCT_INFO_EXECUTABLE_OFFSET: usize = 50;
const CPI_ACCT_INFO_SIZE_OF: usize = 56;
// CPI account info offsets for recipient.
const CPI_ACCT_INFO_KEY_ADDR_RECIPIENT_OFFSET: usize = 56;
const CPI_ACCT_INFO_LAMPORTS_ADDR_RECIPIENT_OFFSET: usize = 64;
const CPI_ACCT_INFO_DATA_LEN_RECIPIENT_OFFSET: usize = 72;
const CPI_ACCT_INFO_DATA_ADDR_RECIPIENT_OFFSET: usize = 80;
const CPI_ACCT_INFO_OWNER_ADDR_RECIPIENT_OFFSET: usize = 88;
const CPI_ACCT_INFO_RENT_EPOCH_RECIPIENT_OFFSET: usize = 96;
const CPI_ACCT_INFO_IS_SIGNER_RECIPIENT_OFFSET: usize = 104;
const CPI_ACCT_INFO_IS_WRITABLE_RECIPIENT_OFFSET: usize = 105;
const CPI_ACCT_INFO_EXECUTABLE_RECIPIENT_OFFSET: usize = 106;
// CPI instruction data offsets.
const CPI_INSN_DATA_VARIANT_OFFSET: usize = 0;
const CPI_INSN_DATA_AMOUNT_OFFSET: usize = 4;
const CPI_INSN_DATA_LEN: usize = 12;
// Stack offsets.
const STACK_INSN_OFFSET: usize = 200;
const STACK_INSN_DATA_OFFSET: usize = 160;
const STACK_ACCT_METAS_OFFSET: usize = 144;
const STACK_ACCT_INFOS_OFFSET: usize = 112;
// CPI instruction checks.
assert_eq!(
CPI_INSN_PROGRAM_ID_ADDR_OFFSET,
offset_of!(SolInstruction, program_id_addr)
);
assert_eq!(
CPI_INSN_ACCOUNTS_ADDR_OFFSET,
offset_of!(SolInstruction, accounts_addr)
);
assert_eq!(
CPI_INSN_ACCOUNTS_LEN_OFFSET,
offset_of!(SolInstruction, accounts_len)
);
assert_eq!(
CPI_INSN_DATA_ADDR_OFFSET,
offset_of!(SolInstruction, data_addr)
);
assert_eq!(
CPI_INSN_DATA_LEN_OFFSET,
offset_of!(SolInstruction, data_len)
);
// CPI account meta checks.
assert_eq!(
CPI_ACCT_META_PUBKEY_ADDR_OFFSET,
offset_of!(SolAccountMeta, pubkey_addr)
);
assert_eq!(
CPI_ACCT_META_IS_WRITABLE_OFFSET,
offset_of!(SolAccountMeta, is_writable)
);
assert_eq!(
CPI_ACCT_META_IS_SIGNER_OFFSET,
offset_of!(SolAccountMeta, is_signer)
);
assert!(size_of::<SolAccountMeta>().is_multiple_of(ALIGNMENT));
assert_eq!(CPI_ACCT_META_SIZE_OF, size_of::<SolAccountMeta>());
// CPI account meta checks for recipient.
assert_eq!(
CPI_ACCT_META_PUBKEY_ADDR_RECIPIENT_OFFSET,
CPI_ACCT_META_SIZE_OF + offset_of!(SolAccountMeta, pubkey_addr)
);
assert_eq!(
CPI_ACCT_META_IS_WRITABLE_RECIPIENT_OFFSET,
CPI_ACCT_META_SIZE_OF + offset_of!(SolAccountMeta, is_writable)
);
assert_eq!(
CPI_ACCT_META_IS_SIGNER_RECIPIENT_OFFSET,
CPI_ACCT_META_SIZE_OF + offset_of!(SolAccountMeta, is_signer)
);
// CPI account info checks.
assert_eq!(
CPI_ACCT_INFO_KEY_ADDR_OFFSET,
offset_of!(SolAccountInfo, key_addr)
);
assert_eq!(
CPI_ACCT_INFO_LAMPORTS_ADDR_OFFSET,
offset_of!(SolAccountInfo, lamports_addr)
);
assert_eq!(
CPI_ACCT_INFO_DATA_LEN_OFFSET,
offset_of!(SolAccountInfo, data_len)
);
assert_eq!(
CPI_ACCT_INFO_DATA_ADDR_OFFSET,
offset_of!(SolAccountInfo, data_addr)
);
assert_eq!(
CPI_ACCT_INFO_OWNER_ADDR_OFFSET,
offset_of!(SolAccountInfo, owner_addr)
);
assert_eq!(
CPI_ACCT_INFO_RENT_EPOCH_OFFSET,
offset_of!(SolAccountInfo, rent_epoch)
);
assert_eq!(
CPI_ACCT_INFO_IS_SIGNER_OFFSET,
offset_of!(SolAccountInfo, is_signer)
);
assert_eq!(
CPI_ACCT_INFO_IS_WRITABLE_OFFSET,
offset_of!(SolAccountInfo, is_writable)
);
assert_eq!(
CPI_ACCT_INFO_EXECUTABLE_OFFSET,
offset_of!(SolAccountInfo, executable)
);
assert_eq!(CPI_ACCT_INFO_SIZE_OF, size_of::<SolAccountInfo>());
assert!(size_of::<SolAccountMeta>().is_multiple_of(ALIGNMENT));
// CPI account info checks for recipient.
assert_eq!(
CPI_ACCT_INFO_KEY_ADDR_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, key_addr)
);
assert_eq!(
CPI_ACCT_INFO_LAMPORTS_ADDR_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, lamports_addr)
);
assert_eq!(
CPI_ACCT_INFO_DATA_LEN_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, data_len)
);
assert_eq!(
CPI_ACCT_INFO_DATA_ADDR_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, data_addr)
);
assert_eq!(
CPI_ACCT_INFO_OWNER_ADDR_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, owner_addr)
);
assert_eq!(
CPI_ACCT_INFO_RENT_EPOCH_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, rent_epoch)
);
assert_eq!(
CPI_ACCT_INFO_IS_SIGNER_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, is_signer)
);
assert_eq!(
CPI_ACCT_INFO_IS_WRITABLE_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, is_writable)
);
assert_eq!(
CPI_ACCT_INFO_EXECUTABLE_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, executable)
);
// CPI instruction data checks.
assert_eq!(
CPI_INSN_DATA_VARIANT_OFFSET,
offset_of!(InstructionData, variant)
);
assert_eq!(
CPI_INSN_DATA_AMOUNT_OFFSET,
offset_of!(InstructionData, amount)
);
assert!(size_of::<InstructionData>().is_multiple_of(ALIGNMENT));
assert_eq!(CPI_INSN_DATA_LEN, size_of::<u32>() + size_of::<u64>(),);
// Stack offset checks.
assert_eq!(STACK_ACCT_INFOS_OFFSET, 2 * size_of::<SolAccountInfo>());
assert_eq!(
STACK_ACCT_METAS_OFFSET,
STACK_ACCT_INFOS_OFFSET + 2 * size_of::<SolAccountMeta>()
);
assert_eq!(
STACK_INSN_DATA_OFFSET,
STACK_ACCT_METAS_OFFSET + size_of::<InstructionData>()
);
assert_eq!(
STACK_INSN_OFFSET,
STACK_INSN_DATA_OFFSET + size_of::<SolInstruction>()
);
assert!(STACK_ACCT_INFOS_OFFSET.is_multiple_of(ALIGNMENT));
assert!(STACK_ACCT_METAS_OFFSET.is_multiple_of(ALIGNMENT));
assert!(STACK_INSN_DATA_OFFSET.is_multiple_of(ALIGNMENT));
assert!(STACK_INSN_OFFSET.is_multiple_of(ALIGNMENT));
}π§ CPI construction β
CPI data regions are first allocated on the stack using the calculated offsets:
asm
jlt r2, r4, e_insufficient_lamports
# Allocate CPI data regions on stack.
mov64 r9, r10
sub64 r9, STACK_INSN_OFFSET
mov64 r8, r10
sub64 r8, STACK_INSN_DATA_OFFSET
mov64 r7, r10
sub64 r7, STACK_ACCT_METAS_OFFSET
mov64 r6, r10
sub64 r6, STACK_ACCT_INFOS_OFFSET
# Set up instruction.Instruction data is then populated, leveraging zero-initialized stack memory to encode the System Program pubkey rather than load it from the passed account:
TIP
The System Program pubkey is 111111... in base58, which is all zeros in binary.
asm
sub64 r6, STACK_ACCT_INFOS_OFFSET
# Set up instruction.
mov64 r2, r9 # Load pointer to CPI instruction on stack.
mov64 r3, r10 # Get stack frame pointer.
# Point to known zeroes, rather than trust user passed System Program.
sub64 r3, STACK_SYSTEM_PROGRAM_PUBKEY_OFFSET
stxdw [r2 + CPI_INSN_PROGRAM_ID_ADDR_OFFSET], r3
mov64 r3, r7 # Load pointer to CPI instruction account metas on stack.
stxdw [r2 + CPI_INSN_ACCOUNTS_ADDR_OFFSET], r3
# Accounts length fits in 32-bit immediate.
stdw [r2 + CPI_INSN_ACCOUNTS_LEN_OFFSET], CPI_ACCOUNTS_LEN
mov64 r3, r8 # Load pointer to CPI instruction data on stack.
stxdw [r2 + CPI_INSN_DATA_ADDR_OFFSET], r3
# Instruction data length fits in 32-bit immediate.
stdw [r2 + CPI_INSN_DATA_LEN_OFFSET], CPI_INSN_DATA_LEN
# Set up instruction data.
mov64 r2, r8 # Pointer to instruction data on stack.
mov32 r3, CPI_INSN_DATA_VARIANT
stxw [r2 + CPI_INSN_DATA_VARIANT_OFFSET], r3
stxdw [r2 + CPI_INSN_DATA_AMOUNT_OFFSET], r4
# Parse sender account from input buffer into CPI metadata and info.Account information is then copied into the account metadata and account info arrays, with optimizations that leverage the zero-initialized stack memory and known offsets:
Account transcription
asm
stxdw [r2 + CPI_INSN_DATA_AMOUNT_OFFSET], r4
# Parse sender account from input buffer into CPI metadata and info.
# Start with 1-byte fields that are copied, then 8-byte fields that are
# copied, then step through 8-byte pointers.
mov64 r2, r7 # Account metadata array pointer.
mov64 r3, r6 # Account info array pointer.
# Optimize out 2 CUs by replacing:
# ```
# ldxb r4, [r1 + SENDER_IS_SIGNER_OFFSET]
# stxb [r2 + CPI_ACCT_META_IS_SIGNER_OFFSET], r4
# stxb [r3 + CPI_ACCT_INFO_IS_SIGNER_OFFSET], r4
# ldxb r4, [r1 + SENDER_IS_WRITABLE_OFFSET]
# stxb [r2 + CPI_ACCT_META_IS_WRITABLE_OFFSET], r4
# stxb [r3 + CPI_ACCT_INFO_IS_WRITABLE_OFFSET], r4
# ```
# with the following, since the CPI call checks if the sender is both
# a signer and writable anyways:
stb [r2 + CPI_ACCT_META_IS_SIGNER_OFFSET], BOOL_TRUE
stb [r3 + CPI_ACCT_INFO_IS_SIGNER_OFFSET], BOOL_TRUE
stb [r2 + CPI_ACCT_META_IS_WRITABLE_OFFSET], BOOL_TRUE
stb [r3 + CPI_ACCT_INFO_IS_WRITABLE_OFFSET], BOOL_TRUE
# Optimize out 2 CUs by simply omitting the following, since the CPI
# checks anyways and the stack initializes to zero.
# ```
# ldxb r4, [r1 + SENDER_IS_EXECUTABLE_OFFSET]
# stxb [r3 + CPI_ACCT_INFO_EXECUTABLE_OFFSET], r4
# ```
# Optimize out two CUs by simply omitting these lines, which aren't
# necessary since data length has been verified as zero and the stack
# is initially zeroed out.
# ```
# ldxdw r4, [r1 + SENDER_DATA_LENGTH_OFFSET]
# stxdw [r3 + CPI_ACCT_INFO_DATA_LEN_OFFSET], r4
# ```
ldxdw r4, [r1 + SENDER_RENT_EPOCH_OFFSET]
stxdw [r3 + CPI_ACCT_INFO_RENT_EPOCH_OFFSET], r4
mov64 r4, r1 # Begin stepping through pointer fields.
add64 r4, SENDER_PUBKEY_OFFSET # Step to pubkey field pointer.
stxdw [r2 + CPI_ACCT_META_PUBKEY_ADDR_OFFSET], r4
stxdw [r3 + CPI_ACCT_INFO_KEY_ADDR_OFFSET], r4
add64 r4, PUBKEY_SIZE_OF # Step to owner field pointer.
stxdw [r3 + CPI_ACCT_INFO_OWNER_ADDR_OFFSET], r4
add64 r4, PUBKEY_SIZE_OF # Step to Lamports balance pointer.
stxdw [r3 + CPI_ACCT_INFO_LAMPORTS_ADDR_OFFSET], r4
add64 r4, U128_SIZE_OF # Step over data length, to account data pointer.
stxdw [r3 + CPI_ACCT_INFO_DATA_ADDR_OFFSET], r4
# Repeat for recipient account, but start by stepping through pointer
# fields as an optimization. Optimize out two CUs by eliminating the
# following:
# ```
# add64 r2, CPI_ACCT_META_SIZE_OF # Step to next array element.
# add64 r3, CPI_ACCT_INFO_SIZE_OF # Step to next array element.
# ```
# in lieu of using account meta and account info offsets specifically
# for the recipient account. Specifically, replace any `CPI_..._OFFSET`
# values from the sender account parsing block with corresponding
# `CPI_..._RECIPIENT_OFFSET` values.
# Optimize out one CU by replacing the following:
# ```
# mov64 r4, r1
# add64 r4, RECIPIENT_PUBKEY_OFFSET
# ```
# with the following line, which uses a single known relative offset,
# to continue stepping through successive pointers.
add64 r4, RECIPIENT_PUBKEY_OFFSET_RELATIVE_TO_SENDER_DATA_OFFSET
stxdw [r2 + CPI_ACCT_META_PUBKEY_ADDR_RECIPIENT_OFFSET], r4
stxdw [r3 + CPI_ACCT_INFO_KEY_ADDR_RECIPIENT_OFFSET], r4
add64 r4, PUBKEY_SIZE_OF # Step to owner field pointer.
stxdw [r3 + CPI_ACCT_INFO_OWNER_ADDR_RECIPIENT_OFFSET], r4
add64 r4, PUBKEY_SIZE_OF # Step to Lamports balance pointer.
stxdw [r3 + CPI_ACCT_INFO_LAMPORTS_ADDR_RECIPIENT_OFFSET], r4
add64 r4, U128_SIZE_OF # Step over data length, to account data pointer.
stxdw [r3 + CPI_ACCT_INFO_DATA_ADDR_RECIPIENT_OFFSET], r4
# Copy individual fields, optimizing out 5 CUs by simply omitting the
# following, which are validated by CPI anyways and since the stack
# initializes to zero. Note that it doesn't actually matter if the
# recipient is a signer or not.
# ```
# ldxb r4, [r1 + RECIPIENT_IS_SIGNER_OFFSET]
# stxb [r2 + CPI_ACCT_META_IS_SIGNER_RECIPIENT_OFFSET], r4
# stxb [r3 + CPI_ACCT_INFO_IS_SIGNER_RECIPIENT_OFFSET], r4
# ldxb r4, [r1 + RECIPIENT_IS_EXECUTABLE_OFFSET]
# stxb [r3 + CPI_ACCT_INFO_EXECUTABLE_RECIPIENT_OFFSET], r4
# ```
# Optimize out 1 CU by replacing the following:
# ```
# ldxb r4, [r1 + RECIPIENT_IS_WRITABLE_OFFSET]
# stxb [r2 + CPI_ACCT_META_IS_WRITABLE_RECIPIENT_OFFSET], r4
# stxb [r3 + CPI_ACCT_INFO_IS_WRITABLE_RECIPIENT_OFFSET], r4
# ```
# with:
stb [r2 + CPI_ACCT_META_IS_WRITABLE_RECIPIENT_OFFSET], BOOL_TRUE
stb [r3 + CPI_ACCT_INFO_IS_WRITABLE_RECIPIENT_OFFSET], BOOL_TRUE
# Optimize out two CUs by simply omitting these lines, which aren't
# necessary since data length has been verified as zero and the stack
# is initially zeroed out.
# ```
# ldxdw r4, [r1 + RECIPIENT_DATA_LENGTH_OFFSET]
# stxdw [r3 + CPI_ACCT_INFO_DATA_LEN_RECIPIENT_OFFSET], r4
# ```
ldxdw r4, [r1 + RECIPIENT_RENT_EPOCH_OFFSET]
stxdw [r3 + CPI_ACCT_INFO_RENT_EPOCH_RECIPIENT_OFFSET], r4
# Invoke CPI.Finally, the CPI is invoked, leveraging the zero-initialized r5 memory for another optimization since no signer seeds are required:
asm
stxdw [r3 + CPI_ACCT_INFO_RENT_EPOCH_RECIPIENT_OFFSET], r4
# Invoke CPI.
mov64 r1, r9 # Instruction.
mov64 r2, r6 # Account infos.
mov64 r3, CPI_ACCOUNTS_LEN # Number of account infos.
mov64 r4, 0 # No signer seeds.
# Optimize out 1 CU by eliminating the following:
# ```
# mov64 r5, 0 # No signer seeds.
# ```
# Since r5 is initialized to zero and is not used in this example.
call sol_invoke_signed_c
exitFull program
asm
# Invalid number of accounts.
.equ E_N_ACCOUNTS, 1
# Sender data length is nonzero.
.equ E_DATA_LENGTH_NONZERO_SENDER, 2
# Recipient account is a duplicate.
.equ E_DUPLICATE_ACCOUNT_RECIPIENT, 3
# Recipient data length is nonzero.
.equ E_DATA_LENGTH_NONZERO_RECIPIENT, 4
# System program account is a duplicate.
.equ E_DUPLICATE_ACCOUNT_SYSTEM_PROGRAM, 5
# Invalid instruction data length.
.equ E_INSTRUCTION_DATA_LENGTH, 6
# Sender has insufficient Lamports.
.equ E_INSUFFICIENT_LAMPORTS, 7
# Stack offsets.
.equ STACK_SYSTEM_PROGRAM_PUBKEY_OFFSET, 232
.equ STACK_INSN_OFFSET, 200
.equ STACK_INSN_DATA_OFFSET, 160
.equ STACK_ACCT_METAS_OFFSET, 144
.equ STACK_ACCT_INFOS_OFFSET, 112
# CPI instruction offsets.
.equ CPI_INSN_PROGRAM_ID_ADDR_OFFSET, 0
.equ CPI_INSN_ACCOUNTS_ADDR_OFFSET, 8
.equ CPI_INSN_ACCOUNTS_LEN_OFFSET, 16
.equ CPI_INSN_DATA_ADDR_OFFSET, 24
.equ CPI_INSN_DATA_LEN_OFFSET, 32
# CPI account meta offsets.
.equ CPI_ACCT_META_PUBKEY_ADDR_OFFSET, 0
.equ CPI_ACCT_META_IS_WRITABLE_OFFSET, 8
.equ CPI_ACCT_META_IS_SIGNER_OFFSET, 9
.equ CPI_ACCT_META_SIZE_OF, 16
# CPI account meta offsets for recipient.
.equ CPI_ACCT_META_PUBKEY_ADDR_RECIPIENT_OFFSET, 16
.equ CPI_ACCT_META_IS_WRITABLE_RECIPIENT_OFFSET, 24
.equ CPI_ACCT_META_IS_SIGNER_RECIPIENT_OFFSET, 25
# CPI account info offsets.
.equ CPI_ACCT_INFO_KEY_ADDR_OFFSET, 0
.equ CPI_ACCT_INFO_LAMPORTS_ADDR_OFFSET, 8
.equ CPI_ACCT_INFO_DATA_LEN_OFFSET, 16
.equ CPI_ACCT_INFO_DATA_ADDR_OFFSET, 24
.equ CPI_ACCT_INFO_OWNER_ADDR_OFFSET, 32
.equ CPI_ACCT_INFO_RENT_EPOCH_OFFSET, 40
.equ CPI_ACCT_INFO_IS_SIGNER_OFFSET, 48
.equ CPI_ACCT_INFO_IS_WRITABLE_OFFSET, 49
.equ CPI_ACCT_INFO_EXECUTABLE_OFFSET, 50
.equ CPI_ACCT_INFO_SIZE_OF, 56
# CPI account info offsets for recipient.
.equ CPI_ACCT_INFO_KEY_ADDR_RECIPIENT_OFFSET, 56
.equ CPI_ACCT_INFO_LAMPORTS_ADDR_RECIPIENT_OFFSET, 64
.equ CPI_ACCT_INFO_DATA_LEN_RECIPIENT_OFFSET, 72
.equ CPI_ACCT_INFO_DATA_ADDR_RECIPIENT_OFFSET, 80
.equ CPI_ACCT_INFO_OWNER_ADDR_RECIPIENT_OFFSET, 88
.equ CPI_ACCT_INFO_RENT_EPOCH_RECIPIENT_OFFSET, 96
.equ CPI_ACCT_INFO_IS_SIGNER_RECIPIENT_OFFSET, 104
.equ CPI_ACCT_INFO_IS_WRITABLE_RECIPIENT_OFFSET, 105
.equ CPI_ACCT_INFO_EXECUTABLE_RECIPIENT_OFFSET, 106
# CPI instruction data offsets.
.equ CPI_INSN_DATA_VARIANT_OFFSET, 0
.equ CPI_INSN_DATA_AMOUNT_OFFSET, 4
.equ CPI_INSN_DATA_LEN, 12
# CPI general constants.
.equ CPI_INSN_DATA_VARIANT, 2
.equ CPI_ACCOUNTS_LEN, 2
# Account layout.
.equ N_ACCOUNTS_OFFSET, 0
.equ N_ACCOUNTS_EXPECTED, 3
.equ NON_DUP_MARKER, 0xff
.equ DATA_LENGTH_ZERO, 0
.equ PUBKEY_SIZE_OF, 32
.equ U8_SIZE_OF, 8
.equ U128_SIZE_OF, 16
.equ BOOL_TRUE, 1
.equ BOOL_FALSE, 0
# Sender account.
.equ SENDER_OFFSET, 8
.equ SENDER_IS_SIGNER_OFFSET, 9
.equ SENDER_IS_WRITABLE_OFFSET, 10
.equ SENDER_IS_EXECUTABLE_OFFSET, 11
.equ SENDER_PUBKEY_OFFSET, 16
.equ SENDER_LAMPORTS_OFFSET, 80
.equ SENDER_DATA_LENGTH_OFFSET, 88
.equ SENDER_RENT_EPOCH_OFFSET, 10336
# Recipient account.
.equ RECIPIENT_OFFSET, 10344
.equ RECIPIENT_PUBKEY_OFFSET, 10352
.equ RECIPIENT_IS_SIGNER_OFFSET, 10345
.equ RECIPIENT_IS_WRITABLE_OFFSET, 10346
.equ RECIPIENT_IS_EXECUTABLE_OFFSET, 10347
.equ RECIPIENT_DATA_LENGTH_OFFSET, 10424
.equ RECIPIENT_RENT_EPOCH_OFFSET, 20672
.equ RECIPIENT_PUBKEY_OFFSET_RELATIVE_TO_SENDER_DATA_OFFSET, 10256
# System program account.
.equ SYSTEM_PROGRAM_OFFSET, 20680
.equ SYSTEM_PROGRAM_PUBKEY_OFFSET, 20688
# Transfer input.
.equ INSTRUCTION_DATA_LENGTH_OFFSET, 31032
.equ INSTRUCTION_DATA_LENGTH_EXPECTED, 8
.equ INSTRUCTION_DATA_OFFSET, 31040
.global entrypoint
entrypoint:
# Check number of accounts.
ldxdw r2, [r1 + N_ACCOUNTS_OFFSET]
jne r2, N_ACCOUNTS_EXPECTED, e_n_accounts
# Check sender data length, since a nonzero length would invalidate
# subsequent offsets.
ldxdw r2, [r1 + SENDER_DATA_LENGTH_OFFSET]
jne r2, DATA_LENGTH_ZERO, e_data_length_nonzero_sender
# Check if the recipient account is a duplicate, since duplicate
# accounts have different field layouts.
ldxb r2, [r1 + RECIPIENT_OFFSET]
jne r2, NON_DUP_MARKER, e_duplicate_account_recipient
# Check recipient data length, since a nonzero length would invalidate
# subsequent offsets.
ldxdw r2, [r1 + RECIPIENT_DATA_LENGTH_OFFSET]
jne r2, DATA_LENGTH_ZERO, e_data_length_nonzero_recipient
# Check if the System Account is a duplicate, since duplicate accounts
# have different field layouts. The check for account data length
# is omitted for the System Program because if a caller passes an
# account that is not the System Account, the CPI will fail during
# program ID checks.
ldxb r2, [r1 + SYSTEM_PROGRAM_OFFSET]
jne r2, NON_DUP_MARKER, e_duplicate_account_system_program
# Check instruction data length.
ldxdw r4, [r1 + INSTRUCTION_DATA_LENGTH_OFFSET]
jne r4, INSTRUCTION_DATA_LENGTH_EXPECTED, e_instruction_data_length
# Verify sender has at least as many Lamports as they are trying to
# send. Technically this could be done after checking the number of
# accounts since Lamports balance comes before account data length, but
# in the happy path both checks need to be done anyways and it is
# cleaner to do all layout validation first.
ldxdw r4, [r1 + INSTRUCTION_DATA_OFFSET]
ldxdw r2, [r1 + SENDER_LAMPORTS_OFFSET]
jlt r2, r4, e_insufficient_lamports
# Allocate CPI data regions on stack.
mov64 r9, r10
sub64 r9, STACK_INSN_OFFSET
mov64 r8, r10
sub64 r8, STACK_INSN_DATA_OFFSET
mov64 r7, r10
sub64 r7, STACK_ACCT_METAS_OFFSET
mov64 r6, r10
sub64 r6, STACK_ACCT_INFOS_OFFSET
# Set up instruction.
mov64 r2, r9 # Load pointer to CPI instruction on stack.
mov64 r3, r10 # Get stack frame pointer.
# Point to known zeroes, rather than trust user passed System Program.
sub64 r3, STACK_SYSTEM_PROGRAM_PUBKEY_OFFSET
stxdw [r2 + CPI_INSN_PROGRAM_ID_ADDR_OFFSET], r3
mov64 r3, r7 # Load pointer to CPI instruction account metas on stack.
stxdw [r2 + CPI_INSN_ACCOUNTS_ADDR_OFFSET], r3
# Accounts length fits in 32-bit immediate.
stdw [r2 + CPI_INSN_ACCOUNTS_LEN_OFFSET], CPI_ACCOUNTS_LEN
mov64 r3, r8 # Load pointer to CPI instruction data on stack.
stxdw [r2 + CPI_INSN_DATA_ADDR_OFFSET], r3
# Instruction data length fits in 32-bit immediate.
stdw [r2 + CPI_INSN_DATA_LEN_OFFSET], CPI_INSN_DATA_LEN
# Set up instruction data.
mov64 r2, r8 # Pointer to instruction data on stack.
mov32 r3, CPI_INSN_DATA_VARIANT
stxw [r2 + CPI_INSN_DATA_VARIANT_OFFSET], r3
stxdw [r2 + CPI_INSN_DATA_AMOUNT_OFFSET], r4
# Parse sender account from input buffer into CPI metadata and info.
# Start with 1-byte fields that are copied, then 8-byte fields that are
# copied, then step through 8-byte pointers.
mov64 r2, r7 # Account metadata array pointer.
mov64 r3, r6 # Account info array pointer.
# Optimize out 2 CUs by replacing:
# ```
# ldxb r4, [r1 + SENDER_IS_SIGNER_OFFSET]
# stxb [r2 + CPI_ACCT_META_IS_SIGNER_OFFSET], r4
# stxb [r3 + CPI_ACCT_INFO_IS_SIGNER_OFFSET], r4
# ldxb r4, [r1 + SENDER_IS_WRITABLE_OFFSET]
# stxb [r2 + CPI_ACCT_META_IS_WRITABLE_OFFSET], r4
# stxb [r3 + CPI_ACCT_INFO_IS_WRITABLE_OFFSET], r4
# ```
# with the following, since the CPI call checks if the sender is both
# a signer and writable anyways:
stb [r2 + CPI_ACCT_META_IS_SIGNER_OFFSET], BOOL_TRUE
stb [r3 + CPI_ACCT_INFO_IS_SIGNER_OFFSET], BOOL_TRUE
stb [r2 + CPI_ACCT_META_IS_WRITABLE_OFFSET], BOOL_TRUE
stb [r3 + CPI_ACCT_INFO_IS_WRITABLE_OFFSET], BOOL_TRUE
# Optimize out 2 CUs by simply omitting the following, since the CPI
# checks anyways and the stack initializes to zero.
# ```
# ldxb r4, [r1 + SENDER_IS_EXECUTABLE_OFFSET]
# stxb [r3 + CPI_ACCT_INFO_EXECUTABLE_OFFSET], r4
# ```
# Optimize out two CUs by simply omitting these lines, which aren't
# necessary since data length has been verified as zero and the stack
# is initially zeroed out.
# ```
# ldxdw r4, [r1 + SENDER_DATA_LENGTH_OFFSET]
# stxdw [r3 + CPI_ACCT_INFO_DATA_LEN_OFFSET], r4
# ```
ldxdw r4, [r1 + SENDER_RENT_EPOCH_OFFSET]
stxdw [r3 + CPI_ACCT_INFO_RENT_EPOCH_OFFSET], r4
mov64 r4, r1 # Begin stepping through pointer fields.
add64 r4, SENDER_PUBKEY_OFFSET # Step to pubkey field pointer.
stxdw [r2 + CPI_ACCT_META_PUBKEY_ADDR_OFFSET], r4
stxdw [r3 + CPI_ACCT_INFO_KEY_ADDR_OFFSET], r4
add64 r4, PUBKEY_SIZE_OF # Step to owner field pointer.
stxdw [r3 + CPI_ACCT_INFO_OWNER_ADDR_OFFSET], r4
add64 r4, PUBKEY_SIZE_OF # Step to Lamports balance pointer.
stxdw [r3 + CPI_ACCT_INFO_LAMPORTS_ADDR_OFFSET], r4
add64 r4, U128_SIZE_OF # Step over data length, to account data pointer.
stxdw [r3 + CPI_ACCT_INFO_DATA_ADDR_OFFSET], r4
# Repeat for recipient account, but start by stepping through pointer
# fields as an optimization. Optimize out two CUs by eliminating the
# following:
# ```
# add64 r2, CPI_ACCT_META_SIZE_OF # Step to next array element.
# add64 r3, CPI_ACCT_INFO_SIZE_OF # Step to next array element.
# ```
# in lieu of using account meta and account info offsets specifically
# for the recipient account. Specifically, replace any `CPI_..._OFFSET`
# values from the sender account parsing block with corresponding
# `CPI_..._RECIPIENT_OFFSET` values.
# Optimize out one CU by replacing the following:
# ```
# mov64 r4, r1
# add64 r4, RECIPIENT_PUBKEY_OFFSET
# ```
# with the following line, which uses a single known relative offset,
# to continue stepping through successive pointers.
add64 r4, RECIPIENT_PUBKEY_OFFSET_RELATIVE_TO_SENDER_DATA_OFFSET
stxdw [r2 + CPI_ACCT_META_PUBKEY_ADDR_RECIPIENT_OFFSET], r4
stxdw [r3 + CPI_ACCT_INFO_KEY_ADDR_RECIPIENT_OFFSET], r4
add64 r4, PUBKEY_SIZE_OF # Step to owner field pointer.
stxdw [r3 + CPI_ACCT_INFO_OWNER_ADDR_RECIPIENT_OFFSET], r4
add64 r4, PUBKEY_SIZE_OF # Step to Lamports balance pointer.
stxdw [r3 + CPI_ACCT_INFO_LAMPORTS_ADDR_RECIPIENT_OFFSET], r4
add64 r4, U128_SIZE_OF # Step over data length, to account data pointer.
stxdw [r3 + CPI_ACCT_INFO_DATA_ADDR_RECIPIENT_OFFSET], r4
# Copy individual fields, optimizing out 5 CUs by simply omitting the
# following, which are validated by CPI anyways and since the stack
# initializes to zero. Note that it doesn't actually matter if the
# recipient is a signer or not.
# ```
# ldxb r4, [r1 + RECIPIENT_IS_SIGNER_OFFSET]
# stxb [r2 + CPI_ACCT_META_IS_SIGNER_RECIPIENT_OFFSET], r4
# stxb [r3 + CPI_ACCT_INFO_IS_SIGNER_RECIPIENT_OFFSET], r4
# ldxb r4, [r1 + RECIPIENT_IS_EXECUTABLE_OFFSET]
# stxb [r3 + CPI_ACCT_INFO_EXECUTABLE_RECIPIENT_OFFSET], r4
# ```
# Optimize out 1 CU by replacing the following:
# ```
# ldxb r4, [r1 + RECIPIENT_IS_WRITABLE_OFFSET]
# stxb [r2 + CPI_ACCT_META_IS_WRITABLE_RECIPIENT_OFFSET], r4
# stxb [r3 + CPI_ACCT_INFO_IS_WRITABLE_RECIPIENT_OFFSET], r4
# ```
# with:
stb [r2 + CPI_ACCT_META_IS_WRITABLE_RECIPIENT_OFFSET], BOOL_TRUE
stb [r3 + CPI_ACCT_INFO_IS_WRITABLE_RECIPIENT_OFFSET], BOOL_TRUE
# Optimize out two CUs by simply omitting these lines, which aren't
# necessary since data length has been verified as zero and the stack
# is initially zeroed out.
# ```
# ldxdw r4, [r1 + RECIPIENT_DATA_LENGTH_OFFSET]
# stxdw [r3 + CPI_ACCT_INFO_DATA_LEN_RECIPIENT_OFFSET], r4
# ```
ldxdw r4, [r1 + RECIPIENT_RENT_EPOCH_OFFSET]
stxdw [r3 + CPI_ACCT_INFO_RENT_EPOCH_RECIPIENT_OFFSET], r4
# Invoke CPI.
mov64 r1, r9 # Instruction.
mov64 r2, r6 # Account infos.
mov64 r3, CPI_ACCOUNTS_LEN # Number of account infos.
mov64 r4, 0 # No signer seeds.
# Optimize out 1 CU by eliminating the following:
# ```
# mov64 r5, 0 # No signer seeds.
# ```
# Since r5 is initialized to zero and is not used in this example.
call sol_invoke_signed_c
exit
e_duplicate_account_recipient:
mov32 r0, E_DUPLICATE_ACCOUNT_RECIPIENT
exit
e_duplicate_account_system_program:
mov32 r0, E_DUPLICATE_ACCOUNT_SYSTEM_PROGRAM
exit
e_instruction_data_length:
mov32 r0, E_INSTRUCTION_DATA_LENGTH
exit
e_insufficient_lamports:
mov32 r0, E_INSUFFICIENT_LAMPORTS
exit
e_n_accounts:
mov32 r0, E_N_ACCOUNTS
exit
e_data_length_nonzero_recipient:
mov32 r0, E_DATA_LENGTH_NONZERO_RECIPIENT
exit
e_data_length_nonzero_sender:
mov32 r0, E_DATA_LENGTH_NONZERO_SENDER
exitπ¦ Rust implementation β
The Rust implementation uses a similar flow to construct the CPI, but is absent some parsing checks that are handled internally by pinocchio:
program.rs
rs
use core::mem::size_of;
use pinocchio::{
cpi::invoke,
error::ProgramError,
instruction::{InstructionAccount, InstructionView},
no_allocator, nostd_panic_handler, program_entrypoint, AccountView, Address, ProgramResult,
};
const SYSTEM_PROGRAM_TRANSFER_DISCRIMINANT: u32 = 2;
const SYSTEM_PROGRAM_ID: Address = Address::new_from_array([0u8; 32]);
const E_N_ACCOUNTS: u32 = 1;
const E_INSTRUCTION_DATA_LENGTH: u32 = 6;
const E_INSUFFICIENT_LAMPORTS: u32 = 7;
const CPI_DATA_SIZE: usize = size_of::<u32>() + size_of::<u64>();
const N_CPI_ACCOUNTS: usize = 2;
program_entrypoint!(process_instruction);
nostd_panic_handler!();
no_allocator!();
fn process_instruction(
_program_id: &Address,
accounts: &[AccountView],
instruction_data: &[u8],
) -> ProgramResult {
let [sender, recipient, _system_program] = accounts else {
return Err(ProgramError::Custom(E_N_ACCOUNTS));
};
// Parse transfer amount.
if instruction_data.len() != size_of::<u64>() {
return Err(ProgramError::Custom(E_INSTRUCTION_DATA_LENGTH));
};
// SAFETY: instruction_data is validated to be the correct length.
let amount = unsafe {
u64::from_le_bytes(*(instruction_data.as_ptr() as *const [u8; size_of::<u64>()]))
};
// Validate sender has sufficient Lamports.
if sender.lamports() < amount {
return Err(ProgramError::Custom(E_INSUFFICIENT_LAMPORTS));
}
// Build CPI instruction data.
let mut cpi_data = core::mem::MaybeUninit::<[u8; CPI_DATA_SIZE]>::uninit();
// SAFETY: Sources are aligned, initialized, and valid.
let cpi_data = unsafe {
let ptr = cpi_data.as_mut_ptr() as *mut u8;
core::ptr::copy_nonoverlapping(
SYSTEM_PROGRAM_TRANSFER_DISCRIMINANT.to_le_bytes().as_ptr(),
ptr,
size_of::<u32>(),
);
core::ptr::copy_nonoverlapping(
amount.to_le_bytes().as_ptr(),
ptr.add(size_of::<u32>()),
size_of::<u64>(),
);
cpi_data.assume_init()
};
// Build CPI instruction.
let instruction_accounts: [InstructionAccount; N_CPI_ACCOUNTS] = [
InstructionAccount::writable_signer(sender.address()),
InstructionAccount::writable(recipient.address()),
];
let instruction = InstructionView {
program_id: &SYSTEM_PROGRAM_ID,
accounts: &instruction_accounts,
data: &cpi_data,
};
// Invoke the System Program transfer.
invoke(&instruction, &[sender, recipient])
}The parsing logic differences are demonstrated in corresponding test cases, where the Rust implementation parses anyways but typically still fails later:
| Test case | Assembly error code | Rust behavior |
|---|---|---|
| Nonzero sender data length | E_DATA_LENGTH_NONZERO_SENDER | Fails at CPI invocation time |
| Duplicate recipient account | E_DUPLICATE_ACCOUNT_RECIPIENT | Sender sends Lamports to self |
| Nonzero recipient data length | E_DATA_LENGTH_NONZERO_RECIPIENT | Succeeds |
| Duplicate System Program account | E_DUPLICATE_ACCOUNT_SYSTEM_PROGRAM | Fails at CPI invocation time |
Test cases
rs
#[test]
fn test_asm() {
let setup = setup_test(ProgramLanguage::Assembly);
let (happy_path_instruction, happy_path_accounts) = happy_path_setup(setup.program_id);
// Check no accounts passed.
let mut instruction = happy_path_instruction.clone();
instruction.accounts.clear();
setup.mollusk.process_and_validate_instruction(
&instruction,
&[],
&[Check::err(ProgramError::Custom(E_N_ACCOUNTS))],
);
// Check nonzero sender data length.
let mut accounts = happy_path_accounts.clone();
accounts[AccountIndex::Sender as usize].1.data = vec![0];
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::err(ProgramError::Custom(
E_DATA_LENGTH_NONZERO_SENDER,
))],
);
// Check duplicate recipient account.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Recipient as usize] =
happy_path_instruction.accounts[AccountIndex::Sender as usize].clone();
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Recipient as usize] =
happy_path_accounts[AccountIndex::Sender as usize].clone();
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::err(ProgramError::Custom(
E_DUPLICATE_ACCOUNT_RECIPIENT,
))],
);
// Check nonzero recipient data length.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Recipient as usize].1.data = vec![0];
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::err(ProgramError::Custom(
E_DATA_LENGTH_NONZERO_RECIPIENT,
))],
);
// Check duplicate system program account.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::SystemProgram as usize] =
happy_path_instruction.accounts[AccountIndex::Recipient as usize].clone();
accounts = happy_path_accounts.clone();
accounts[AccountIndex::SystemProgram as usize] =
happy_path_accounts[AccountIndex::Recipient as usize].clone();
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::err(ProgramError::Custom(
E_DUPLICATE_ACCOUNT_SYSTEM_PROGRAM,
))],
);
// Check invalid instruction data length.
instruction = happy_path_instruction.clone();
instruction.data.clear();
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::err(ProgramError::Custom(E_INSTRUCTION_DATA_LENGTH))],
);
// Check insufficient Lamports.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Sender as usize].1.lamports = TRANSFER_AMOUNT - 1;
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::err(ProgramError::Custom(E_INSUFFICIENT_LAMPORTS))],
);
// Check invalid System Program account.
let mock_pubkey = Pubkey::new_unique();
assert_ne!(
mock_pubkey,
happy_path_instruction.accounts[AccountIndex::SystemProgram as usize].pubkey
);
accounts = happy_path_accounts.clone();
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::SystemProgram as usize].pubkey = mock_pubkey;
accounts[AccountIndex::SystemProgram as usize].0 = mock_pubkey;
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::err(ProgramError::NotEnoughAccountKeys)],
);
// Check sender is not signer.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Sender as usize].is_signer = false;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::instruction_err(
InstructionError::PrivilegeEscalation,
)],
);
// Check sender is not writable.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Sender as usize].is_writable = false;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::instruction_err(
InstructionError::PrivilegeEscalation,
)],
);
// Check sender is executable.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Sender as usize].1.executable = true;
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::instruction_err(
InstructionError::UnbalancedInstruction,
)],
);
// Check recipient is signer.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Recipient as usize].is_signer = true;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::success()],
);
// Check recipient is not writable.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Recipient as usize].is_writable = false;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::instruction_err(
InstructionError::PrivilegeEscalation,
)],
);
// Check recipient is executable.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Recipient as usize].1.executable = true;
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::instruction_err(
InstructionError::ExternalAccountLamportSpend,
)],
);
// Check happy path.
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&happy_path_accounts,
&happy_path_checks(&happy_path_instruction, EXPECTED_ASM_COMPUTE_UNITS),
);
}rs
#[test]
fn test_rs() {
let setup = setup_test(ProgramLanguage::Rust);
let (happy_path_instruction, happy_path_accounts) = happy_path_setup(setup.program_id);
// Check no accounts passed.
let mut instruction = happy_path_instruction.clone();
instruction.accounts.clear();
setup.mollusk.process_and_validate_instruction(
&instruction,
&[],
&[Check::err(ProgramError::Custom(E_N_ACCOUNTS))],
);
// Check nonzero sender data length.
let mut accounts = happy_path_accounts.clone();
accounts[AccountIndex::Sender as usize].1.data = vec![0];
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::err(ProgramError::InvalidArgument)],
);
// Check duplicate recipient account: just transfers Lamports back to sender.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Recipient as usize] =
happy_path_instruction.accounts[AccountIndex::Sender as usize].clone();
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Recipient as usize] =
happy_path_accounts[AccountIndex::Sender as usize].clone();
setup
.mollusk
.process_and_validate_instruction(&instruction, &accounts, &[Check::success()]);
// Check nonzero recipient data length: still parses and transfers Lamports.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Recipient as usize].1.data = vec![0];
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&happy_path_checks(&happy_path_instruction, EXPECTED_RS_COMPUTE_UNITS),
);
// Check duplicate system program account.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::SystemProgram as usize] =
happy_path_instruction.accounts[AccountIndex::Recipient as usize].clone();
accounts = happy_path_accounts.clone();
accounts[AccountIndex::SystemProgram as usize] =
happy_path_accounts[AccountIndex::Recipient as usize].clone();
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::err(ProgramError::NotEnoughAccountKeys)],
);
// Check invalid instruction data length.
instruction = happy_path_instruction.clone();
instruction.data.clear();
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::err(ProgramError::Custom(E_INSTRUCTION_DATA_LENGTH))],
);
// Check insufficient Lamports.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Sender as usize].1.lamports = TRANSFER_AMOUNT - 1;
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::err(ProgramError::Custom(E_INSUFFICIENT_LAMPORTS))],
);
// Check invalid System Program account.
let mock_pubkey = Pubkey::new_unique();
assert_ne!(
mock_pubkey,
happy_path_instruction.accounts[AccountIndex::SystemProgram as usize].pubkey
);
accounts = happy_path_accounts.clone();
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::SystemProgram as usize].pubkey = mock_pubkey;
accounts[AccountIndex::SystemProgram as usize].0 = mock_pubkey;
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::err(ProgramError::NotEnoughAccountKeys)],
);
// Check sender is not signer.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Sender as usize].is_signer = false;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::instruction_err(
InstructionError::PrivilegeEscalation,
)],
);
// Check sender is not writable.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Sender as usize].is_writable = false;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::instruction_err(
InstructionError::PrivilegeEscalation,
)],
);
// Check sender is executable.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Sender as usize].1.executable = true;
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::instruction_err(
InstructionError::UnbalancedInstruction,
)],
);
// Check recipient is signer.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Recipient as usize].is_signer = true;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::success()],
);
// Check recipient is not writable.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Recipient as usize].is_writable = false;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::instruction_err(
InstructionError::PrivilegeEscalation,
)],
);
// Check recipient is executable.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Recipient as usize].1.executable = true;
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::instruction_err(
InstructionError::ExternalAccountLamportSpend,
)],
);
// Check happy path.
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&happy_path_accounts,
&happy_path_checks(&happy_path_instruction, EXPECTED_RS_COMPUTE_UNITS),
);
}β½ Compute unit analysis β
The final test case for both test_asm and test_rs performs a happy path test case to verify that a specified amount of Lamports are transferred to the recipient, with comparable total compute units consumed overall:
| Implementation | Compute units consumed |
|---|---|
| Assembly | 1170 |
| Rust | 1232 |
Test runs
txt
test tests::test_asm ... ok
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program DASMAC... consumed 4 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program DASMAC... consumed 6 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x2
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program DASMAC... consumed 8 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x3
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program DASMAC... consumed 10 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x4
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program DASMAC... consumed 12 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x5
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program DASMAC... consumed 14 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x6
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program DASMAC... consumed 17 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x7
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Unknown program 11111111111111111111111111111111
[ ... DEBUG ... ] Program DASMAC... consumed 1019 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: An account required by the instruction is missing
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs's signer privilege escalated
[ ... DEBUG ... ] Program DASMAC... consumed 1019 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: Cross-program invocation with unauthorized signer or writable account
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs's writable privilege escalated
[ ... DEBUG ... ] Program DASMAC... consumed 1019 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: Cross-program invocation with unauthorized signer or writable account
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 success
[ ... DEBUG ... ] Program DASMAC... consumed 1170 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... success
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 success
[ ... DEBUG ... ] Program DASMAC... consumed 1170 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... success
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] 1117mWrzzrZr312ebPDHu8tbfMwFNvCvMbr6WepCNG's writable privilege escalated
[ ... DEBUG ... ] Program DASMAC... consumed 1019 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: Cross-program invocation with unauthorized signer or writable account
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 success
[ ... DEBUG ... ] Program DASMAC... consumed 1170 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: instruction spent from the balance of an account it does not own
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 success
[ ... DEBUG ... ] Program DASMAC... consumed 1170 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... successtxt
test tests::test_rs ... ok
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program DASMAC... consumed 5 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2]
[ ... DEBUG ... ] Transfer: `from` must not carry data
[ ... DEBUG ... ] Program 11111111111111111111111111111111 failed: invalid program argument
[ ... DEBUG ... ] Program DASMAC... consumed 1229 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: invalid program argument
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 success
[ ... DEBUG ... ] Program DASMAC... consumed 1233 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... success
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 success
[ ... DEBUG ... ] Program DASMAC... consumed 1232 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... success
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Unknown program 11111111111111111111111111111111
[ ... DEBUG ... ] Program DASMAC... consumed 1081 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: An account required by the instruction is missing
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program DASMAC... consumed 39 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x6
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program DASMAC... consumed 43 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x7
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Unknown program 11111111111111111111111111111111
[ ... DEBUG ... ] Program DASMAC... consumed 1079 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: An account required by the instruction is missing
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs's signer privilege escalated
[ ... DEBUG ... ] Program DASMAC... consumed 1080 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: Cross-program invocation with unauthorized signer or writable account
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs's writable privilege escalated
[ ... DEBUG ... ] Program DASMAC... consumed 1080 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: Cross-program invocation with unauthorized signer or writable account
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 success
[ ... DEBUG ... ] Program DASMAC... consumed 1231 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... success
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 success
[ ... DEBUG ... ] Program DASMAC... consumed 1231 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... success
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] 1117mWrzzrZr312ebPDHu8tbfMwFNvCvMbr6WepCNG's writable privilege escalated
[ ... DEBUG ... ] Program DASMAC... consumed 1080 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: Cross-program invocation with unauthorized signer or writable account
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 success
[ ... DEBUG ... ] Program DASMAC... consumed 1231 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: instruction spent from the balance of an account it does not own
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 invoke [2]
[ ... DEBUG ... ] Program 11111111111111111111111111111111 success
[ ... DEBUG ... ] Program DASMAC... consumed 1232 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... successHowever, the vast majority of these totals are due to fixed CPI costs, which are incurred regardless of the implementation language used. Specifically, each CPI invocation has a base cost of 946 CUs, and a 250 bytes per unit cost individually assessed on instruction data, account metas, account infos, and account data. In this example, all values besides the base cost truncate to zero since they are individually less than 250 bytes. Beyond this general CPI invocation cost, there is also the 150 Compute Units consumed by the System Program itself to perform the Lamport transfer, leading to a total of 946 + 150 = 1096 CUs consumed by the CPI transfer call alone. Hence the differences between assembly and Rust implementations are more dramatic when isolated to non-CPI compute units:
| Implementation | Non-CPI compute units |
|---|---|
| Assembly | 74 |
| Rust | 136 |
That is, the Rust implementation consumes 62 more CUs for general program logic, some 84% overhead versus the assembly version.
β All tests β
tests.rs
rs
use mollusk_svm::program;
use mollusk_svm::result::Check;
use solana_sdk::account::Account;
use solana_sdk::instruction::{AccountMeta, Instruction, InstructionError};
use solana_sdk::program_error::ProgramError;
use solana_sdk::pubkey::Pubkey;
use std::mem::{offset_of, size_of};
use test_utils::{setup_test, ProgramLanguage};
const E_N_ACCOUNTS: u32 = 1;
const E_DATA_LENGTH_NONZERO_SENDER: u32 = 2;
const E_DUPLICATE_ACCOUNT_RECIPIENT: u32 = 3;
const E_DATA_LENGTH_NONZERO_RECIPIENT: u32 = 4;
const E_DUPLICATE_ACCOUNT_SYSTEM_PROGRAM: u32 = 5;
const E_INSTRUCTION_DATA_LENGTH: u32 = 6;
const E_INSUFFICIENT_LAMPORTS: u32 = 7;
enum AccountIndex {
Sender = 0,
Recipient = 1,
SystemProgram = 2,
}
const TRANSFER_AMOUNT: u64 = 10;
const COMPUTE_UNIT_OVERHEAD: u64 = 10_000;
const EXPECTED_ASM_COMPUTE_UNITS: u64 = 1170;
const EXPECTED_RS_COMPUTE_UNITS: u64 = 1232;
const ALIGNMENT: usize = 8;
fn happy_path_checks(instruction: &Instruction, expected_compute_units: u64) -> Vec<Check<'_>> {
vec![
Check::success(),
Check::account(&instruction.accounts[AccountIndex::Recipient as usize].pubkey)
.lamports(TRANSFER_AMOUNT)
.build(),
Check::compute_units(expected_compute_units),
]
}
fn happy_path_setup(program_id: Pubkey) -> (Instruction, Vec<(Pubkey, Account)>) {
let (system_program, system_account) = program::keyed_account_for_system_program();
let instruction = Instruction::new_with_bytes(
program_id,
&TRANSFER_AMOUNT.to_le_bytes(),
vec![
AccountMeta::new(Pubkey::new_unique(), true),
AccountMeta::new(Pubkey::new_unique(), false),
AccountMeta::new_readonly(system_program, false),
],
);
let accounts = vec![
(
instruction.accounts[AccountIndex::Sender as usize].pubkey,
Account::new(TRANSFER_AMOUNT + COMPUTE_UNIT_OVERHEAD, 0, &system_program),
),
(
instruction.accounts[AccountIndex::Recipient as usize].pubkey,
Account::new(0, 0, &system_program),
),
(system_program, system_account),
];
(instruction, accounts)
}
#[test]
fn test_system_program_pubkey() {
let (pubkey, _) = program::keyed_account_for_system_program();
assert_eq!(pubkey.to_bytes(), [0u8; 32]);
}
#[test]
fn test_asm() {
let setup = setup_test(ProgramLanguage::Assembly);
let (happy_path_instruction, happy_path_accounts) = happy_path_setup(setup.program_id);
// Check no accounts passed.
let mut instruction = happy_path_instruction.clone();
instruction.accounts.clear();
setup.mollusk.process_and_validate_instruction(
&instruction,
&[],
&[Check::err(ProgramError::Custom(E_N_ACCOUNTS))],
);
// Check nonzero sender data length.
let mut accounts = happy_path_accounts.clone();
accounts[AccountIndex::Sender as usize].1.data = vec![0];
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::err(ProgramError::Custom(
E_DATA_LENGTH_NONZERO_SENDER,
))],
);
// Check duplicate recipient account.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Recipient as usize] =
happy_path_instruction.accounts[AccountIndex::Sender as usize].clone();
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Recipient as usize] =
happy_path_accounts[AccountIndex::Sender as usize].clone();
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::err(ProgramError::Custom(
E_DUPLICATE_ACCOUNT_RECIPIENT,
))],
);
// Check nonzero recipient data length.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Recipient as usize].1.data = vec![0];
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::err(ProgramError::Custom(
E_DATA_LENGTH_NONZERO_RECIPIENT,
))],
);
// Check duplicate system program account.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::SystemProgram as usize] =
happy_path_instruction.accounts[AccountIndex::Recipient as usize].clone();
accounts = happy_path_accounts.clone();
accounts[AccountIndex::SystemProgram as usize] =
happy_path_accounts[AccountIndex::Recipient as usize].clone();
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::err(ProgramError::Custom(
E_DUPLICATE_ACCOUNT_SYSTEM_PROGRAM,
))],
);
// Check invalid instruction data length.
instruction = happy_path_instruction.clone();
instruction.data.clear();
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::err(ProgramError::Custom(E_INSTRUCTION_DATA_LENGTH))],
);
// Check insufficient Lamports.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Sender as usize].1.lamports = TRANSFER_AMOUNT - 1;
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::err(ProgramError::Custom(E_INSUFFICIENT_LAMPORTS))],
);
// Check invalid System Program account.
let mock_pubkey = Pubkey::new_unique();
assert_ne!(
mock_pubkey,
happy_path_instruction.accounts[AccountIndex::SystemProgram as usize].pubkey
);
accounts = happy_path_accounts.clone();
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::SystemProgram as usize].pubkey = mock_pubkey;
accounts[AccountIndex::SystemProgram as usize].0 = mock_pubkey;
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::err(ProgramError::NotEnoughAccountKeys)],
);
// Check sender is not signer.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Sender as usize].is_signer = false;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::instruction_err(
InstructionError::PrivilegeEscalation,
)],
);
// Check sender is not writable.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Sender as usize].is_writable = false;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::instruction_err(
InstructionError::PrivilegeEscalation,
)],
);
// Check sender is executable.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Sender as usize].1.executable = true;
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::instruction_err(
InstructionError::UnbalancedInstruction,
)],
);
// Check recipient is signer.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Recipient as usize].is_signer = true;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::success()],
);
// Check recipient is not writable.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Recipient as usize].is_writable = false;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::instruction_err(
InstructionError::PrivilegeEscalation,
)],
);
// Check recipient is executable.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Recipient as usize].1.executable = true;
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::instruction_err(
InstructionError::ExternalAccountLamportSpend,
)],
);
// Check happy path.
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&happy_path_accounts,
&happy_path_checks(&happy_path_instruction, EXPECTED_ASM_COMPUTE_UNITS),
);
}
#[test]
fn test_rs() {
let setup = setup_test(ProgramLanguage::Rust);
let (happy_path_instruction, happy_path_accounts) = happy_path_setup(setup.program_id);
// Check no accounts passed.
let mut instruction = happy_path_instruction.clone();
instruction.accounts.clear();
setup.mollusk.process_and_validate_instruction(
&instruction,
&[],
&[Check::err(ProgramError::Custom(E_N_ACCOUNTS))],
);
// Check nonzero sender data length.
let mut accounts = happy_path_accounts.clone();
accounts[AccountIndex::Sender as usize].1.data = vec![0];
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::err(ProgramError::InvalidArgument)],
);
// Check duplicate recipient account: just transfers Lamports back to sender.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Recipient as usize] =
happy_path_instruction.accounts[AccountIndex::Sender as usize].clone();
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Recipient as usize] =
happy_path_accounts[AccountIndex::Sender as usize].clone();
setup
.mollusk
.process_and_validate_instruction(&instruction, &accounts, &[Check::success()]);
// Check nonzero recipient data length: still parses and transfers Lamports.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Recipient as usize].1.data = vec![0];
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&happy_path_checks(&happy_path_instruction, EXPECTED_RS_COMPUTE_UNITS),
);
// Check duplicate system program account.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::SystemProgram as usize] =
happy_path_instruction.accounts[AccountIndex::Recipient as usize].clone();
accounts = happy_path_accounts.clone();
accounts[AccountIndex::SystemProgram as usize] =
happy_path_accounts[AccountIndex::Recipient as usize].clone();
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::err(ProgramError::NotEnoughAccountKeys)],
);
// Check invalid instruction data length.
instruction = happy_path_instruction.clone();
instruction.data.clear();
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::err(ProgramError::Custom(E_INSTRUCTION_DATA_LENGTH))],
);
// Check insufficient Lamports.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Sender as usize].1.lamports = TRANSFER_AMOUNT - 1;
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::err(ProgramError::Custom(E_INSUFFICIENT_LAMPORTS))],
);
// Check invalid System Program account.
let mock_pubkey = Pubkey::new_unique();
assert_ne!(
mock_pubkey,
happy_path_instruction.accounts[AccountIndex::SystemProgram as usize].pubkey
);
accounts = happy_path_accounts.clone();
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::SystemProgram as usize].pubkey = mock_pubkey;
accounts[AccountIndex::SystemProgram as usize].0 = mock_pubkey;
setup.mollusk.process_and_validate_instruction(
&instruction,
&accounts,
&[Check::err(ProgramError::NotEnoughAccountKeys)],
);
// Check sender is not signer.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Sender as usize].is_signer = false;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::instruction_err(
InstructionError::PrivilegeEscalation,
)],
);
// Check sender is not writable.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Sender as usize].is_writable = false;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::instruction_err(
InstructionError::PrivilegeEscalation,
)],
);
// Check sender is executable.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Sender as usize].1.executable = true;
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::instruction_err(
InstructionError::UnbalancedInstruction,
)],
);
// Check recipient is signer.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Recipient as usize].is_signer = true;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::success()],
);
// Check recipient is not writable.
instruction = happy_path_instruction.clone();
instruction.accounts[AccountIndex::Recipient as usize].is_writable = false;
setup.mollusk.process_and_validate_instruction(
&instruction,
&happy_path_accounts,
&[Check::instruction_err(
InstructionError::PrivilegeEscalation,
)],
);
// Check recipient is executable.
accounts = happy_path_accounts.clone();
accounts[AccountIndex::Recipient as usize].1.executable = true;
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&accounts,
&[Check::instruction_err(
InstructionError::ExternalAccountLamportSpend,
)],
);
// Check happy path.
setup.mollusk.process_and_validate_instruction(
&happy_path_instruction,
&happy_path_accounts,
&happy_path_checks(&happy_path_instruction, EXPECTED_RS_COMPUTE_UNITS),
);
}
#[test]
fn test_input_offsets() {
const MAX_PERMITTED_DATA_INCREASE: usize = 10240;
#[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; 32],
owner: [u8; 32],
lamports: u64,
data_length: u64,
data_padded: [u8; PADDED_DATA_SIZE],
rent_epoch: u64,
}
type StandardAccount = AccountLayout<MAX_PERMITTED_DATA_INCREASE>;
type SystemProgramAccount = AccountLayout<{ MAX_PERMITTED_DATA_INCREASE + 16 }>;
// Sender.
const SENDER_OFFSET: usize = 8;
const SENDER_PUBKEY_OFFSET: usize = 16;
const SENDER_IS_SIGNER_OFFSET: usize = 9;
const SENDER_IS_WRITABLE_OFFSET: usize = 10;
const SENDER_IS_EXECUTABLE_OFFSET: usize = 11;
const SENDER_LAMPORTS_OFFSET: usize = 80;
const SENDER_DATA_LENGTH_OFFSET: usize = 88;
const SENDER_DATA_OFFSET: usize = 96;
const SENDER_RENT_EPOCH_OFFSET: usize = 10336;
// Recipient.
const RECIPIENT_OFFSET: usize = 10344;
const RECIPIENT_PUBKEY_OFFSET: usize = 10352;
const RECIPIENT_IS_SIGNER_OFFSET: usize = 10345;
const RECIPIENT_IS_WRITABLE_OFFSET: usize = 10346;
const RECIPIENT_IS_EXECUTABLE_OFFSET: usize = 10347;
const RECIPIENT_DATA_LENGTH_OFFSET: usize = 10424;
const RECIPIENT_RENT_EPOCH_OFFSET: usize = 20672;
const RECIPIENT_PUBKEY_OFFSET_RELATIVE_TO_SENDER_DATA_OFFSET: usize = 10256;
// System program.
const SYSTEM_PROGRAM_OFFSET: usize = 20680;
const SYSTEM_PROGRAM_PUBKEY_OFFSET: usize = 20688;
// Instruction data.
const INSTRUCTION_DATA_LENGTH_OFFSET: usize = 31032;
const INSTRUCTION_DATA_OFFSET: usize = 31040;
// Sender checks.
assert_eq!(
SENDER_IS_SIGNER_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, is_signer),
);
assert_eq!(
SENDER_IS_WRITABLE_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, is_writable),
);
assert_eq!(
SENDER_IS_EXECUTABLE_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, is_executable),
);
assert_eq!(
SENDER_LAMPORTS_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, lamports),
);
assert_eq!(
SENDER_DATA_LENGTH_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, data_length),
);
assert_eq!(
SENDER_PUBKEY_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, pubkey),
);
assert_eq!(
SENDER_RENT_EPOCH_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, rent_epoch),
);
assert_eq!(
SENDER_DATA_OFFSET,
SENDER_OFFSET + offset_of!(StandardAccount, data_padded),
);
// Recipient checks.
assert_eq!(
RECIPIENT_OFFSET,
SENDER_OFFSET + size_of::<StandardAccount>()
);
assert_eq!(
RECIPIENT_DATA_LENGTH_OFFSET,
RECIPIENT_OFFSET + offset_of!(StandardAccount, data_length),
);
assert_eq!(
RECIPIENT_PUBKEY_OFFSET,
RECIPIENT_OFFSET + offset_of!(StandardAccount, pubkey),
);
assert_eq!(
RECIPIENT_IS_SIGNER_OFFSET,
RECIPIENT_OFFSET + offset_of!(StandardAccount, is_signer),
);
assert_eq!(
RECIPIENT_IS_WRITABLE_OFFSET,
RECIPIENT_OFFSET + offset_of!(StandardAccount, is_writable),
);
assert_eq!(
RECIPIENT_IS_EXECUTABLE_OFFSET,
RECIPIENT_OFFSET + offset_of!(StandardAccount, is_executable),
);
assert_eq!(
RECIPIENT_RENT_EPOCH_OFFSET,
RECIPIENT_OFFSET + offset_of!(StandardAccount, rent_epoch),
);
assert_eq!(
RECIPIENT_PUBKEY_OFFSET_RELATIVE_TO_SENDER_DATA_OFFSET,
RECIPIENT_PUBKEY_OFFSET - SENDER_DATA_OFFSET
);
// System program checks.
assert_eq!(
SYSTEM_PROGRAM_OFFSET,
RECIPIENT_OFFSET + size_of::<StandardAccount>()
);
assert_eq!(
SYSTEM_PROGRAM_PUBKEY_OFFSET,
SYSTEM_PROGRAM_OFFSET + offset_of!(SystemProgramAccount, pubkey),
);
assert_eq!(
INSTRUCTION_DATA_LENGTH_OFFSET,
SYSTEM_PROGRAM_OFFSET + size_of::<SystemProgramAccount>()
);
assert_eq!(
INSTRUCTION_DATA_OFFSET,
INSTRUCTION_DATA_LENGTH_OFFSET + size_of::<u64>(),
);
}
#[test]
fn test_cpi_offsets() {
#[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,
padding: [u8; 6],
}
#[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,
padding: [u8; 5],
}
#[repr(C)]
struct InstructionData {
variant: [u8; 4],
amount: [u8; 8],
padding: [u8; 4],
}
// CPI instruction offsets.
const CPI_INSN_PROGRAM_ID_ADDR_OFFSET: usize = 0;
const CPI_INSN_ACCOUNTS_ADDR_OFFSET: usize = 8;
const CPI_INSN_ACCOUNTS_LEN_OFFSET: usize = 16;
const CPI_INSN_DATA_ADDR_OFFSET: usize = 24;
const CPI_INSN_DATA_LEN_OFFSET: usize = 32;
// CPI account meta offsets.
const CPI_ACCT_META_PUBKEY_ADDR_OFFSET: usize = 0;
const CPI_ACCT_META_IS_WRITABLE_OFFSET: usize = 8;
const CPI_ACCT_META_IS_SIGNER_OFFSET: usize = 9;
const CPI_ACCT_META_SIZE_OF: usize = 16;
// CPI account meta offsets for recipient.
const CPI_ACCT_META_PUBKEY_ADDR_RECIPIENT_OFFSET: usize = 16;
const CPI_ACCT_META_IS_WRITABLE_RECIPIENT_OFFSET: usize = 24;
const CPI_ACCT_META_IS_SIGNER_RECIPIENT_OFFSET: usize = 25;
// CPI account info offsets.
const CPI_ACCT_INFO_KEY_ADDR_OFFSET: usize = 0;
const CPI_ACCT_INFO_LAMPORTS_ADDR_OFFSET: usize = 8;
const CPI_ACCT_INFO_DATA_LEN_OFFSET: usize = 16;
const CPI_ACCT_INFO_DATA_ADDR_OFFSET: usize = 24;
const CPI_ACCT_INFO_OWNER_ADDR_OFFSET: usize = 32;
const CPI_ACCT_INFO_RENT_EPOCH_OFFSET: usize = 40;
const CPI_ACCT_INFO_IS_SIGNER_OFFSET: usize = 48;
const CPI_ACCT_INFO_IS_WRITABLE_OFFSET: usize = 49;
const CPI_ACCT_INFO_EXECUTABLE_OFFSET: usize = 50;
const CPI_ACCT_INFO_SIZE_OF: usize = 56;
// CPI account info offsets for recipient.
const CPI_ACCT_INFO_KEY_ADDR_RECIPIENT_OFFSET: usize = 56;
const CPI_ACCT_INFO_LAMPORTS_ADDR_RECIPIENT_OFFSET: usize = 64;
const CPI_ACCT_INFO_DATA_LEN_RECIPIENT_OFFSET: usize = 72;
const CPI_ACCT_INFO_DATA_ADDR_RECIPIENT_OFFSET: usize = 80;
const CPI_ACCT_INFO_OWNER_ADDR_RECIPIENT_OFFSET: usize = 88;
const CPI_ACCT_INFO_RENT_EPOCH_RECIPIENT_OFFSET: usize = 96;
const CPI_ACCT_INFO_IS_SIGNER_RECIPIENT_OFFSET: usize = 104;
const CPI_ACCT_INFO_IS_WRITABLE_RECIPIENT_OFFSET: usize = 105;
const CPI_ACCT_INFO_EXECUTABLE_RECIPIENT_OFFSET: usize = 106;
// CPI instruction data offsets.
const CPI_INSN_DATA_VARIANT_OFFSET: usize = 0;
const CPI_INSN_DATA_AMOUNT_OFFSET: usize = 4;
const CPI_INSN_DATA_LEN: usize = 12;
// Stack offsets.
const STACK_INSN_OFFSET: usize = 200;
const STACK_INSN_DATA_OFFSET: usize = 160;
const STACK_ACCT_METAS_OFFSET: usize = 144;
const STACK_ACCT_INFOS_OFFSET: usize = 112;
// CPI instruction checks.
assert_eq!(
CPI_INSN_PROGRAM_ID_ADDR_OFFSET,
offset_of!(SolInstruction, program_id_addr)
);
assert_eq!(
CPI_INSN_ACCOUNTS_ADDR_OFFSET,
offset_of!(SolInstruction, accounts_addr)
);
assert_eq!(
CPI_INSN_ACCOUNTS_LEN_OFFSET,
offset_of!(SolInstruction, accounts_len)
);
assert_eq!(
CPI_INSN_DATA_ADDR_OFFSET,
offset_of!(SolInstruction, data_addr)
);
assert_eq!(
CPI_INSN_DATA_LEN_OFFSET,
offset_of!(SolInstruction, data_len)
);
// CPI account meta checks.
assert_eq!(
CPI_ACCT_META_PUBKEY_ADDR_OFFSET,
offset_of!(SolAccountMeta, pubkey_addr)
);
assert_eq!(
CPI_ACCT_META_IS_WRITABLE_OFFSET,
offset_of!(SolAccountMeta, is_writable)
);
assert_eq!(
CPI_ACCT_META_IS_SIGNER_OFFSET,
offset_of!(SolAccountMeta, is_signer)
);
assert!(size_of::<SolAccountMeta>().is_multiple_of(ALIGNMENT));
assert_eq!(CPI_ACCT_META_SIZE_OF, size_of::<SolAccountMeta>());
// CPI account meta checks for recipient.
assert_eq!(
CPI_ACCT_META_PUBKEY_ADDR_RECIPIENT_OFFSET,
CPI_ACCT_META_SIZE_OF + offset_of!(SolAccountMeta, pubkey_addr)
);
assert_eq!(
CPI_ACCT_META_IS_WRITABLE_RECIPIENT_OFFSET,
CPI_ACCT_META_SIZE_OF + offset_of!(SolAccountMeta, is_writable)
);
assert_eq!(
CPI_ACCT_META_IS_SIGNER_RECIPIENT_OFFSET,
CPI_ACCT_META_SIZE_OF + offset_of!(SolAccountMeta, is_signer)
);
// CPI account info checks.
assert_eq!(
CPI_ACCT_INFO_KEY_ADDR_OFFSET,
offset_of!(SolAccountInfo, key_addr)
);
assert_eq!(
CPI_ACCT_INFO_LAMPORTS_ADDR_OFFSET,
offset_of!(SolAccountInfo, lamports_addr)
);
assert_eq!(
CPI_ACCT_INFO_DATA_LEN_OFFSET,
offset_of!(SolAccountInfo, data_len)
);
assert_eq!(
CPI_ACCT_INFO_DATA_ADDR_OFFSET,
offset_of!(SolAccountInfo, data_addr)
);
assert_eq!(
CPI_ACCT_INFO_OWNER_ADDR_OFFSET,
offset_of!(SolAccountInfo, owner_addr)
);
assert_eq!(
CPI_ACCT_INFO_RENT_EPOCH_OFFSET,
offset_of!(SolAccountInfo, rent_epoch)
);
assert_eq!(
CPI_ACCT_INFO_IS_SIGNER_OFFSET,
offset_of!(SolAccountInfo, is_signer)
);
assert_eq!(
CPI_ACCT_INFO_IS_WRITABLE_OFFSET,
offset_of!(SolAccountInfo, is_writable)
);
assert_eq!(
CPI_ACCT_INFO_EXECUTABLE_OFFSET,
offset_of!(SolAccountInfo, executable)
);
assert_eq!(CPI_ACCT_INFO_SIZE_OF, size_of::<SolAccountInfo>());
assert!(size_of::<SolAccountMeta>().is_multiple_of(ALIGNMENT));
// CPI account info checks for recipient.
assert_eq!(
CPI_ACCT_INFO_KEY_ADDR_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, key_addr)
);
assert_eq!(
CPI_ACCT_INFO_LAMPORTS_ADDR_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, lamports_addr)
);
assert_eq!(
CPI_ACCT_INFO_DATA_LEN_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, data_len)
);
assert_eq!(
CPI_ACCT_INFO_DATA_ADDR_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, data_addr)
);
assert_eq!(
CPI_ACCT_INFO_OWNER_ADDR_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, owner_addr)
);
assert_eq!(
CPI_ACCT_INFO_RENT_EPOCH_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, rent_epoch)
);
assert_eq!(
CPI_ACCT_INFO_IS_SIGNER_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, is_signer)
);
assert_eq!(
CPI_ACCT_INFO_IS_WRITABLE_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, is_writable)
);
assert_eq!(
CPI_ACCT_INFO_EXECUTABLE_RECIPIENT_OFFSET,
CPI_ACCT_INFO_SIZE_OF + offset_of!(SolAccountInfo, executable)
);
// CPI instruction data checks.
assert_eq!(
CPI_INSN_DATA_VARIANT_OFFSET,
offset_of!(InstructionData, variant)
);
assert_eq!(
CPI_INSN_DATA_AMOUNT_OFFSET,
offset_of!(InstructionData, amount)
);
assert!(size_of::<InstructionData>().is_multiple_of(ALIGNMENT));
assert_eq!(CPI_INSN_DATA_LEN, size_of::<u32>() + size_of::<u64>(),);
// Stack offset checks.
assert_eq!(STACK_ACCT_INFOS_OFFSET, 2 * size_of::<SolAccountInfo>());
assert_eq!(
STACK_ACCT_METAS_OFFSET,
STACK_ACCT_INFOS_OFFSET + 2 * size_of::<SolAccountMeta>()
);
assert_eq!(
STACK_INSN_DATA_OFFSET,
STACK_ACCT_METAS_OFFSET + size_of::<InstructionData>()
);
assert_eq!(
STACK_INSN_OFFSET,
STACK_INSN_DATA_OFFSET + size_of::<SolInstruction>()
);
assert!(STACK_ACCT_INFOS_OFFSET.is_multiple_of(ALIGNMENT));
assert!(STACK_ACCT_METAS_OFFSET.is_multiple_of(ALIGNMENT));
assert!(STACK_INSN_DATA_OFFSET.is_multiple_of(ALIGNMENT));
assert!(STACK_INSN_OFFSET.is_multiple_of(ALIGNMENT));
}NOTE
The assembly file and testing framework in this example were adapted from an sbpf example.