Appearance
Memo β
IMPORTANT
If you want to run this example interactively, make sure to complete the quickstart first, as it 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 program 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 in the following order:
- The number of accounts as a
u64 - A sequence of serialized accounts
- The length of instruction data as a
u64 - The instruction data itself
Hence for a transaction 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
# Without mock .rodata the dump script fails.
# https://github.com/blueshift-gg/sbpf/issues/82
.rodata
null: .byte 0β οΈ 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_
exit
# Without mock .rodata the dump script fails.
# https://github.com/blueshift-gg/sbpf/issues/82
.rodata
null: .byte 0Note the minimal compute unit consumption for a failure:
sh
[ ... ] Program DASMAC... invoke [1]
[ ... ] Program DASMAC... consumed 3 of 1400000 compute units
[ ... ] Program DASMAC... failed: custom program error: 0x1
test tests::test_asm_fail ... okπ¬ 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_
exit
# Without mock .rodata the dump script fails.
# https://github.com/blueshift-gg/sbpf/issues/82
.rodata
null: .byte 0Note the compute unit consumption for a successful log:
sh
[ ... ] Program DASMAC... invoke [1]
[ ... ] Program log: Hello again, DASMAC!
[ ... ] Program DASMAC... consumed 106 of 1400000 compute units
[ ... ] Program DASMAC... success
test tests::test_asm_pass ... okπ¦ Rust implementation β
The rust implementation similarly calls the pinocchio version of sol_log_ with the passed instruction data.
rs
use pinocchio::{entrypoint, pubkey::Pubkey, 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: &Pubkey,
_accounts: &[pinocchio::account_info::AccountInfo],
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
[ ... ] Program DASMAC... invoke [1]
[ ... ] Program log: Hello again, DASMAC!
[ ... ] Program DASMAC... consumed 111 of 1400000 compute units
[ ... ] Program DASMAC... success
test tests::test_rs ... okβ Tests β
Tests
rs
use mollusk_svm::result::Check;
use solana_sdk::account::AccountSharedData;
use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::program_error::ProgramError;
use solana_sdk::pubkey::Pubkey;
use test_utils::{setup_test, ProgramLanguage};
#[test]
fn test_asm_fail() {
let setup = setup_test!(ProgramLanguage::Assembly);
// Create a mock account will trigger an error when passed.
let mock_account_pubkey = Pubkey::new_unique();
let mock_account_data = AccountSharedData::default();
let accounts = vec![AccountMeta::new(mock_account_pubkey, false)];
let n_accounts = accounts.len() as u32;
let instruction = Instruction::new_with_bytes(setup.program_id, b"Whoops", accounts);
// Verify that the instruction fails with the expected error code.
let result = setup.mollusk.process_and_validate_instruction(
&instruction,
&[(mock_account_pubkey, mock_account_data.into())],
&[Check::err(ProgramError::Custom(n_accounts))],
);
assert!(result.program_result.is_err());
}
#[test]
fn test_asm_pass() {
happy_path(ProgramLanguage::Assembly);
}
#[test]
fn test_rs() {
happy_path(ProgramLanguage::Rust);
}
fn happy_path(program_language: test_utils::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.
assert!(!setup
.mollusk
.process_and_validate_instruction(&instruction, &[], &[Check::success()])
.program_result
.is_err());
}NOTE
The assembly file in this example was adapted from a Helius Blog post