Skip to content

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:

DescriptionSize (bytes)
The number of accounts as a u648
A sequence of serialized accountsVariable
The length of instruction data as a u648
Instruction dataVariable
The calling program ID32

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:

OffsetSizeDescription
08 bytesNumber of accounts (0)
88 bytesLength of instruction data
16N bytesInstruction 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, 3
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

Note 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: 0x1
test_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:

RegisterValue
r1The address of the message to log
r2The 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_
    exit
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

Note 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... success
test_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... success
test_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