Solana Development Tutorial: Things you should know before structuring your code
If you have some programming experience with other chains like ETH and EOS, etc. You may get confused when things behave not as you expected when you start programming in Solana. As an early developer in Solana ecosystem, this article hopes to help you clear some of these confusions and give some suggestions to make your onboarding experience more smooth.
Size of Account is fixed
Account is used to store data in Solana, like RAM of EOS. However once the account is initiated with the size, you cannot change the size later. If you are familiar with Solana SDK, you should have see this function:
pub fn allocate(pubkey: &Pubkey, space: u64) -> Instruction {
let account_metas = vec![AccountMeta::new(*pubkey, true)];
Instruction::new(
system_program::id(),
&SystemInstruction::Allocate { space },
account_metas,
)
}
According to the future plans, Solana may add the ability to change the size. You can see some of our discussions here .
When we usually deal with memory or other storages, we can always realloc more space later as it’s hard to predict how many users you will have or how much data you need to store, so things would seem different here, let’s see how things will be optimized from here, luckily the space of Solana is really cheap.
Get AccountInfo only through runtime
The structure of AccountInfo is:
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,
}
This structure can store all kinds of accounts on-chain, it can represent a SOL account, or a SPL account or a program account.
Normally, we may think that if we have an account address like “HSfwVfB7RUF1SKCd4yrz8KZp7TU262Y5BeZZN1tdCTVk”, we can use functions like
AccountInfo::new
AccountInfo::From
to get AccountInfo and use it in our programs, unfortunately, you cannot. The only way for now you can get AccountInfo of an account is through runtime of a Solana node which gets the address from client and load the account info and feed back to you.
For example, when a program account want to transfer SOL to another account, we use the transfer function of Token program which requires three AccountInfo:
- src_info: source account info
- dst_info: destination account info
- system_program_info: system program account info
You cannot get there accounts on-chain through program, you need to get them from clients through PRC.
So you cannot do things like:
Choose an account stored in our program account and send it some SOL
You need first choose the account on-chain, then get the result, and send the AccountInfo of this account through a client and use it to send the SOL.
Program account cannot directly transfer SOL
Suppose we want to build a simple lottery program: Every player transfer 1 SOL to the program, then the program select on winner and transfer all the SOL to it, how many Account we need ?
Normally, you may think 1 account is enough, it receives, selects and sends, all-in-one, well, it works in some other chains, but it’s not how Solana programs work.
First, you need to understand SOL, SOL is a token, every SOL account’s owner is System program — “11111111111111111111111111111111”
Then let’s take a look at program account.
The owner of program account is BPFLoader2 — “BPFLoader2111111111111111111111111111111111”, and the biggest difference is that the “executable” property of program account is “Yes”, as we see the code of transfer:
// The balance of read-only and executable accounts may not change
if self.lamports != post.lamports {
if !self.is_writable {
return Err(InstructionError::ReadonlyLamportChange);
}
if self.is_executable {
return Err(InstructionError::ExecutableLamportChange);
}
}
if the account is executable, we cannot make it happen.
So the work around here is using another SOL account to receive all the SOL and send them to the winner at last with winner feed in like the case we should before.
Limit of computation and log
When we debug a solana program, we may see errors like:
Program GJqD99MTrSmQLN753x5ynkHdVGPrRGp35WqNnkXL3j1C consumed 200000 of 200000 compute units
Program GJqD99MTrSmQLN753x5ynkHdVGPrRGp35WqNnkXL3j1C BPF VM error: exceeded maximum number of instructions allowed (193200)
Program GJqD99MTrSmQLN753x5ynkHdVGPrRGp35WqNnkXL3j1C failed: custom program error: 0xb9f0002
It means that you are running out of computations resources. The runtime has added some limits to the programs, you can find them in SDK:
BpfComputeBudget {
max_units: 100_000,
log_units: 0,
log_64_units: 0,
create_program_address_units: 0,
invoke_units: 0,
max_invoke_depth: 1,
sha256_base_cost: 85,
sha256_byte_cost: 1,
max_call_depth: 20,
stack_frame_size: 4_096,
log_pubkey_units: 0,
}; if feature_set.is_active(&bpf_compute_budget_balancing::id()) {
bpf_compute_budget = BpfComputeBudget {
max_units: 200_000,
log_units: 100,
log_64_units: 100,
create_program_address_units: 1500,
invoke_units: 1000,
..bpf_compute_budget
};
}
Currently for both mainnet and testnet,the limit of computation units is 200K,for log is is 100.
You may also see:
Transaction simulation failed: Error processing Instruction 0: Program failed to complete
Program N42Qjxtrb1KMwCshrpbSJxj3khrZwN51VVv5Zdv2AFL invoke [1]
Program log: [solong-lottery]:Instruction: SignIn
Program log: [solong-lottery]:process_signin lottery:award[0] fund[1000000000] price[1000000000] billboard[Epj4jWrXq4JsEAhvDKMAdR47GqZve8dyKp29KdGfR4X] pool[255]
Program log: Error: memory allocation failed, out of memory
Program N42Qjxtrb1KMwCshrpbSJxj3khrZwN51VVv5Zdv2AFL consumed 109954 of 200000 compute units
Program N42Qjxtrb1KMwCshrpbSJxj3khrZwN51VVv5Zdv2AFL BPF VM error: BPF program Panicked in at 0:0
Program failed to complete: UserError(SyscallError(Panic("", 0, 0)))
Program N42Qjxtrb1KMwCshrpbSJxj3khrZwN51VVv5Zdv2AFL failed: Program failed to c
It means that you run out of memory, space limit for stack is 4kb and 32kb for heap. You should be really careful when you use structure like Vec or Box. This is blockchain world, always pay attention to memory limit.
PS: Pay attention to recursive functions calls, the depth should not exceed 20.
ELF error
We may also see errors like this when we publish our program:
Error: dynamic program error: ELF error: ELF error: .bss section not supported
or:
Failed to parse ELF file: read-write: base offet 207896
It means that we may have used some Rust structures/features Solana does not support, like hashmap or function like find_program_address.
Here are some rules:
No access to
rand
or any crates that depend on itstd::fs
std::net
std::os
std::future
std::process
std::sync
std::task
std::thread
std::time
Limited access to:
std::hash
std::os
- Bincode is extremely computationally expensive in both cycles and call depth and should be avoided
- String formatting should be avoided since it is also computationally expensive
- No support for
println!
,print!
, the Solana SDK helpers insrc/log.rs
should be used instead - The runtime enforces a limit on the number of instructions a program can execute during the processing of one instruction
You can always check the official documents.
Program Update
Finally we get our program deployed, then we want to change some features can we update our program without changing the account/address:
The answer is NO for now, but the official team is working on this.
For now there are some tips you can use:
- Do not hardcode the program address in frontend, always get your program address(es) through an interface in case you re-deploy it.
- You can create a copy function for your account, once you need to update but do not want to lose the data, you can use this copy function to migrate your data from old account to new account.
Conclusion
Due to Solana’s unique design and chase of efficiency, the program might builds/runs different from other chains, is may seem weird at first, but you will get used to it and enjoy the efficiency once you understand the philosophies of it, hope this article can help you with a better onboarding experience and we would love you to share your thoughts/experience/stories of programming in Solana.