Best practices
Here are some essential principles to consider when building on-chain programs in Solana:
Store Keys in the Account
It's beneficial to store keys in the account when creating Program Derived Accounts (PDAs) using seeds. While this may increase account rent slightly, it offers significant advantages.
By having all the necessary keys in the account, it becomes effortless to locate the account (since you can recreate its public key). Additionally, this approach works seamlessly with Anchor's has_one
clause, streamlining the process.
Keep seeds simple
When creating PDA seeds, prioritize simplicity. Using a straightforward logic for seeds makes it easier to remember and clarifies the relationship between accounts.
A logical approach is to first include the seeds of the parent account and then use the current object's identifiers, preferably in alphabetical order.
Minimize Instruction's Scope
Keeping each instruction's scope as small as possible is crucial for several reasons.
- It helps reduce transaction size by limiting the number of accounts touched simultaneously.
- It enhances composability, readability, and security.
However, a trade-off to consider is that it may lead to an increase in Lines Of Code (LOC).
Use InitSpace
It will automatically determine the right space to allocate the account with
use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct Offer {
pub id: u64,
pub maker: Pubkey,
pub token_mint_a: Pubkey,
pub token_mint_b: Pubkey,
pub token_b_wanted_amount: u64,
pub bump: u8,
}
Testing
When it comes to testing, there are no canonical best practices. However some fundamental principles applies to unique projects that handle large TVL:
- Optimize compute budget
- Save PDA bumps in account state
- Separation of concern between account funder (payer) and account owner (authority)
- Implement invariants to validate properties
- Implement Fuzz testing
- Account structure should be optimized—static size fields should be at the beginning of structures while variable sized field should be at the end.
- Proper re-initialization checks should be put in place for init-if-needed constrained accounts. init constraints are still the best.
- Re-run your security scans in CI/CD pipeline.
There are efficient testing tools and frameworks in the Solana tooling ecosystem that could help you achieve these aim but they are adapted for unique situations:
- LiteSVM for rust testing—this is the go-to tool for in-process Solana VM simulation
- Mucho To Simulate programs
- Surfpool is useful for testing and visualizing in real time as slots turns to blocks
- bankrun which is an alternative runtime that allows restarting the validator between tests.
solana-test-validator
for spinning up a local validator for integration tests.- Jest is mostly useful for testing async features and is also well known its better compatibility with @solana/web3.js. However to simulate the runtime, bankrun is the still best tool out there.
Use appropriate preflight commitment level
Even if you use skipPreflight
you should always set the preflightCommitment
to the same commitment level used to fetch your transaction's blockhash for both sendTransaction
and simulateTransaction
.
Turn skipPreflight
on when testing on chain programs to get better errors.
Avoid lagging RPC nodes
For sendTransaction
requests, clients should keep resending a transaction to a RPC node on a frequent interval so that if an RPC node is slightly lagging behind the cluster, it will eventually catch up and detect your transaction’s expiration properly.
For simulateTransaction
requests, clients should use the replaceRecentBlockhash
parameter to tell the RPC node to replace the simulated transaction’s blockhash with a blockhash that will always be valid for simulation.
Avoid reusing stale blockhashes
Even if your application has fetched a very recent blockhash, be sure that you’re not reusing that blockhash in transactions for too long. The ideal scenario is that a recent blockhash is fetched right before a user signs their transaction.
Recommendation for applications
Poll for new recent blockhashes on a frequent basis to ensure that whenever a user triggers an action that creates a transaction, your application already has a fresh blockhash that’s ready to go.
Recommendation for wallets
Poll for new recent blockhashes on a frequent basis and replace a transaction’s recent blockhash right before they sign the transaction to ensure the blockhash is as fresh as possible.
Monitor RPC nodes for health
Monitor the health of your RPC nodes to ensure that they have an up-to-date view of the cluster state with one of the following methods:
Fetch your RPC node’s highest processed slot by using the
getSlot
RPC API with theprocessed
commitment level and then call thegetMaxShredInsertSlot
RPC API to get the highest slot that your RPC node has received a “shred” of a block for. If the difference between these responses is very large, the cluster is producing blocks far ahead of what the RPC node has processed.Call the
getLatestBlockhash
RPC API with theconfirmed
commitment level on a few different RPC API nodes and use the blockhash from the node that returns the highest slot for its context slot.