Solana Development Tutorial: Key Concepts

Solong
5 min readDec 16, 2020

There is a lot of terms and explanations in Solana developer docs, but we do not need to know them all to start development. Solana smart contract is also called on-chain program. Unlike WebAssembly bytecode run by some other chains like EOS, the executable of Solana is BPF bytecode which will be loaded by Solana node runtime and executed.

So how we generate BPF bytecode? We need compilers which can generate BPF bytecodes. Normally, LLVM can compile C and Rust to BPF bytecode. As Solana is built on top of rust, so here we also use Rust as examples.

Transaction

Transaction is used to send requests to Solana node. A transaction may contain multiple instructions. Once a node has received a transaction, it will parse the instructions, and call related on-chain programs according to program_id param passed by every instruction.

Instruction

Instruction is the minimum unit that a Solana node to execute:

Dapp send serialized program params and account info to Solana node, Solana node find the related on-chain programs and send the data to the program, then the program deserialize it and execute the command using the params.

Account

Solana on-chain resources include RAM, file, CPU(Compute budget), etc. Unlike the RAM and CPU of EOS. Solana has defined the max number for each program can use, like stack size(4MB), CPU time(200,000BPF), stack depth(64), etc. So we would not see programs competing resources. Every piece of information saved on-chain is a file(PS: People always get confused with account and file in Solana, they the the same thing actually, like in Unix, everything is a file). So people need to pay SOL for the space of file. If you want to close a file, you just transfer all the SOL of this file out, since there is no fee to pay the space of this file.

Runtime

As we mentioned before, the runtime a Solana runs BPF bytecodes, you may wonder why it choose BPF, not WebAssembly, or Lua, python ? I think the main reason is the performance, TPS is always of the first priority of Solana, and BPF consumes lower resources to run. The restrictions of resources for on-program can be found is Solana SDK.

pub struct BpfComputeBudget {
/// Number of compute units that an instruction is allowed. Compute units
/// are consumed by program execution, resources they use, etc...
pub max_units: u64,
/// Number of compute units consumed by a log call
pub log_units: u64,
/// Number of compute units consumed by a log_u64 call
pub log_64_units: u64,
/// Number of compute units consumed by a create_program_address call
pub create_program_address_units: u64,
/// Number of compute units consumed by an invoke call (not including the cost incurred by
/// the called program)
pub invoke_units: u64,
/// Maximum cross-program invocation depth allowed including the original caller
pub max_invoke_depth: usize,
/// Base number of compute units consumed to call SHA256
pub sha256_base_cost: u64,
/// Incremental number of units consumed by SHA256 (based on bytes)
pub sha256_byte_cost: u64,
/// Maximum BPF to BPF call depth
pub max_call_depth: usize,
/// Size of a stack frame in bytes, must match the size specified in the LLVM BPF backend
pub stack_frame_size: usize,
/// Number of compute units consumed by logging a `Pubkey`
pub log_pubkey_units: u64,
}

Key Data Structures

1. Pubkey

#[repr(transparent)]
#[derive(
Serialize, Deserialize, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash, AbiExample,
)]
pub struct Pubkey([u8; 32]);]

Pubkey represents an account address of Base58 format, also in Instruction the ProgramId is also of the same format,just as we said before Program is a as the same as account as a file, the only difference is that it is executable.

2. AccountInfo

/// Account information
#[derive(Clone)]
pub struct AccountInfo<'a> {
/// Public key of the account
pub key: &'a Pubkey,
/// Was the transaction signed by this account's public key?
pub is_signer: bool,
/// Is the account writable?
pub is_writable: bool,
/// The lamports in the account. Modifiable by programs.
pub lamports: Rc<RefCell<&'a mut u64>>,
/// The data held in this account. Modifiable by programs.
pub data: Rc<RefCell<&'a mut [u8]>>,
/// Program that owns this account
pub owner: &'a Pubkey,
/// This account's data contains a loaded program (and is now read-only)
pub executable: bool,
/// The epoch at which this account will next owe rent
pub rent_epoch: Epoch,
}

AccountInfo is how we store an account on-chain. You can also view these properties as file properties. Key is the id of this file, which is a base58 address. lamports represents the rent fee for the space of the file, so lamports cannot be 0, 0 means the file is closed. is_writable represents this is a executable file(program) or a normal account. data stores the content of the file which is buffer of binary data. Every file/account is created by program, which called owner.

3. ProgramResult

/// Reasons the program may fail
#[derive(Clone, Debug, Deserialize, Eq, Error, PartialEq, Serialize)]
pub enum ProgramError {
/// Allows on-chain programs to implement program-specific error types and see them returned
/// by the Solana runtime. A program-specific error may be any type that is represented as
/// or serialized to a u32 integer.
#[error("Custom program error: {0:#x}")]
Custom(u32)
...
}use std::{
result::Result as ResultGeneric,
};
pub type ProgramResult = ResultGeneric<(), ProgramError>;

ProgramResult is a Result of type ProgramError which is defined as an enum of errors thrown by runtime. If a program runs well, we call Ok() to get the right result, if there is something wrong,We return ProgramError.

4. AccountMeta

/// Account metadata used to define Instructions
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct AccountMeta {
/// An account's public key
pub pubkey: Pubkey,
/// True if an Instruction requires a Transaction signature matching `pubkey`.
pub is_signer: bool,
/// True if the `pubkey` can be loaded as a read-write account.
pub is_writable: bool,
}

AccountMeta is mainly used in Instruction, passing in information like iaccount address, whether this account is a signer, and whether its content is writable.

5. Instruction

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct Instruction {
/// Pubkey of the instruction processor that executes this instruction
pub program_id: Pubkey,
/// Metadata for what accounts should be passed to the instruction processor
pub accounts: Vec<AccountMeta>,
/// Opaque data passed to the instruction processor
pub data: Vec<u8>,
}

We have mentioned Instruction before, here is a detailed look at it, only the related program know how to deserialize the data, so even we intercept this instruction, it would be very hard to see what does it do.

Conclusion

Manipulating a Solana program is more like doing CRUD operations to a file. In our next tutorial, we will show some common ways to implement these operations.

--

--