Appearance
Memo β
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!
πΊοΈ Memory map background β
The SBPF instruction set architecture defines 12 registers, including 10 general-purpose registers r0 through r9. At the start of instruction execution, r1 is initialized to the input buffer address MM_INPUT_START, corresponding to one of several runtime memory map regions.
Within the input buffer, data is serialized as follows:
| Description | Size (bytes) |
|---|---|
The number of accounts as a u64 | 8 |
| A sequence of serialized accounts | Variable |
The length of instruction data as a u64 | 8 |
| Instruction data | Variable |
| The calling program ID | 32 |
TIP
A new virtual memory map is created for every instruction and for every instance of a CPI (which contains an inner call to an instruction processor whose own inner call generates a fresh memory map).
Hence for an instruction that accepts no accounts and includes a memo string to print, the input buffer layout is as follows:
| Offset | Size | Description |
|---|---|---|
0 | 8 bytes | Number of accounts (0) |
8 | 8 bytes | Length of instruction data |
16 | N bytes | Instruction data (memo) |
Related constants are defined at the top of the assembly implementation and used throughout the remainder of the program:
asm
.equ NUM_ACCOUNTS_OFFSET, 0
.equ INSTRUCTION_DATA_LENGTH_OFFSET, 8
.equ INSTRUCTION_DATA_OFFSET, 16
.globl entrypoint
entrypoint:Full program
asm
.equ NUM_ACCOUNTS_OFFSET, 0
.equ INSTRUCTION_DATA_LENGTH_OFFSET, 8
.equ INSTRUCTION_DATA_OFFSET, 16
.globl entrypoint
entrypoint:
# Indexed load the number of accounts into the return code.
ldxdw r0, [r1 + NUM_ACCOUNTS_OFFSET]
# If nonzero number of accounts, jump to exit instruction.
jne r0, r4, 3
# Indexed load the message data length.
ldxdw r2, [r1 + INSTRUCTION_DATA_LENGTH_OFFSET]
# Increment pointer in r1 by the instruction data offset.
add64 r1, INSTRUCTION_DATA_OFFSET
call sol_log_
exitβ οΈ Error checking β
The value in r0 at the conclusion of an SBPF program is considered the return value, where a return value of 0 indicates success.
Hence the ldxdw (load indexed double word) LD_DW_REG opcode effectively triggers an error code if a nonzero number of accounts are passed, by loading into r0 the value pointed to by r1 + NUM_ACCOUNTS_OFFSET: the number of accounts passed.
Once the return code is set, the jne (jump if not equal) JNE_REG opcode then compares it against the value in r4, which is initialized to zero: by default all registers are initialized to zero in a new virtual machine instance except for an immediate modification to the frame pointer register (r10), and pre-execution modifications to r1 and optionally r2. If r0 and r4 are unequal (if the number of accounts is nonzero), the program jumps immediately to the exit opcode.
Notably this comparison is performed using registers instead of using an immediate value of zero, for example jne r0, 0, 3, since this approach would use JNE_IMM and therefore only compare r0 against 32 bits from an immediate as opposed to all 64 register bits from r4:
asm
.globl entrypoint
entrypoint:
# Indexed load the number of accounts into the return code.
ldxdw r0, [r1 + NUM_ACCOUNTS_OFFSET]
# If nonzero number of accounts, jump to exit instruction.
jne r0, r4, 3Full program
asm
.equ NUM_ACCOUNTS_OFFSET, 0
.equ INSTRUCTION_DATA_LENGTH_OFFSET, 8
.equ INSTRUCTION_DATA_OFFSET, 16
.globl entrypoint
entrypoint:
# Indexed load the number of accounts into the return code.
ldxdw r0, [r1 + NUM_ACCOUNTS_OFFSET]
# If nonzero number of accounts, jump to exit instruction.
jne r0, r4, 3
# Indexed load the message data length.
ldxdw r2, [r1 + INSTRUCTION_DATA_LENGTH_OFFSET]
# Increment pointer in r1 by the instruction data offset.
add64 r1, INSTRUCTION_DATA_OFFSET
call sol_log_
exitNote the minimal compute unit consumption for a failure:
sh
test tests::test_asm_fail ... ok
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program DASMAC... consumed 3 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... failed: custom program error: 0x1test_asm_fail
rs
#[test]
fn test_asm_fail() {
let setup = setup_test(ProgramLanguage::Assembly);
// Create a mock account that will trigger an error when passed.
let (account, accounts) = single_mock_account();
// Verify that the instruction fails with the expected error code.
setup.mollusk.process_and_validate_instruction(
&Instruction::new_with_bytes(setup.program_id, b"Whoops", accounts.clone()),
&[account],
&[Check::err(ProgramError::Custom(accounts.len() as u32))],
);
}π¬ Logging β
Assuming no accounts are passed, the length of the message is similarly loaded via ldxdw (load indexed double word) LD_DW_REG into r2 via an offset reference to r1. Then r1 is itself incremented via add64 (add to 64-bit register an immediate value) ADD64_IMM by the instruction data offset, a value known to fit in 32 bits.
These operations preposition a call via CALL_IMM to sol_log_, which takes the following arguments:
| Register | Value |
|---|---|
r1 | The address of the message to log |
r2 | The number of bytes to log |
After the logging operation, the program concludes:
asm
entrypoint:
# Indexed load the number of accounts into the return code.
ldxdw r0, [r1 + NUM_ACCOUNTS_OFFSET]
# If nonzero number of accounts, jump to exit instruction.
jne r0, r4, 3
# Indexed load the message data length.
ldxdw r2, [r1 + INSTRUCTION_DATA_LENGTH_OFFSET]
# Increment pointer in r1 by the instruction data offset.
add64 r1, INSTRUCTION_DATA_OFFSET
call sol_log_
exitFull program
asm
.equ NUM_ACCOUNTS_OFFSET, 0
.equ INSTRUCTION_DATA_LENGTH_OFFSET, 8
.equ INSTRUCTION_DATA_OFFSET, 16
.globl entrypoint
entrypoint:
# Indexed load the number of accounts into the return code.
ldxdw r0, [r1 + NUM_ACCOUNTS_OFFSET]
# If nonzero number of accounts, jump to exit instruction.
jne r0, r4, 3
# Indexed load the message data length.
ldxdw r2, [r1 + INSTRUCTION_DATA_LENGTH_OFFSET]
# Increment pointer in r1 by the instruction data offset.
add64 r1, INSTRUCTION_DATA_OFFSET
call sol_log_
exitNote the compute unit consumption for a successful log:
sh
test tests::test_asm_pass ... ok
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program log: Hello again, DASMAC!
[ ... DEBUG ... ] Program DASMAC... consumed 106 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... successtest_asm_pass
rs
#[test]
fn test_asm_pass() {
happy_path(ProgramLanguage::Assembly);
}π¦ Rust implementation β
The Rust implementation similarly calls the pinocchio version of sol_log_ with the passed instruction data.
rs
use pinocchio::{entrypoint, AccountView, Address, ProgramResult};
#[cfg(target_os = "solana")]
use pinocchio::syscalls::sol_log_;
entrypoint!(process_instruction);
#[cfg_attr(not(target_os = "solana"), allow(unused_variables))]
fn process_instruction(
_program_id: &Address,
_accounts: &[AccountView],
instruction_data: &[u8],
) -> ProgramResult {
#[cfg(target_os = "solana")]
unsafe {
sol_log_(instruction_data.as_ptr(), instruction_data.len() as u64);
}
Ok(())
}Notably, however, it introduces compute unit overhead:
sh
test tests::test_rs ... ok
[ ... DEBUG ... ] Program DASMAC... invoke [1]
[ ... DEBUG ... ] Program log: Hello again, DASMAC!
[ ... DEBUG ... ] Program DASMAC... consumed 111 of 1400000 compute units
[ ... DEBUG ... ] Program DASMAC... successtest_rs
rs
#[test]
fn test_rs() {
happy_path(ProgramLanguage::Rust);
}β All tests β
tests.rs
rs
use mollusk_svm::result::Check;
use solana_sdk::instruction::Instruction;
use solana_sdk::program_error::ProgramError;
use test_utils::{setup_test, single_mock_account, ProgramLanguage};
#[test]
fn test_asm_fail() {
let setup = setup_test(ProgramLanguage::Assembly);
// Create a mock account that will trigger an error when passed.
let (account, accounts) = single_mock_account();
// Verify that the instruction fails with the expected error code.
setup.mollusk.process_and_validate_instruction(
&Instruction::new_with_bytes(setup.program_id, b"Whoops", accounts.clone()),
&[account],
&[Check::err(ProgramError::Custom(accounts.len() as u32))],
);
}
#[test]
fn test_asm_pass() {
happy_path(ProgramLanguage::Assembly);
}
#[test]
fn test_rs() {
happy_path(ProgramLanguage::Rust);
}
fn happy_path(program_language: ProgramLanguage) {
let setup = setup_test(program_language);
// Create an instruction with a simple memo message.
let instruction =
Instruction::new_with_bytes(setup.program_id, b"Hello again, DASMAC!", vec![]);
// Verify the instruction completes successfully.
setup
.mollusk
.process_and_validate_instruction(&instruction, &[], &[Check::success()]);
}NOTE
The assembly file in this example was adapted from a Helius Blog post