Solana Accounts Overview
Solana has 3 types of accounts:
- Accounts that store data (non-executable)
- Accounts that store programs (executable)
- Accounts that store native programs (executable)
Accounts in general are just buffers for arbitrary data which is stored inside their data field. So in all 3 cases, the data field is just a byte array, which is storing either program bytecode or arbitrary data.
struct Account {
lamports: u64,
owner: PublicKey,
executable: bool,
data: &str,
rent_epoch: u8
}
An account is a claim to a specific amount of data storage on the blockchain. The maximum data is 10MB. Each account pays for this privilege of storing data. As an incentive, paying for 2 years of rent waives any more payments.
The minimum overhead of an account is 128 bytes.
// https://github.com/anza-xyz/solana-sdk/blob/master/rent/src/lib.rs#L70C1-L70C47
pub const ACCOUNT_STORAGE_OVERHEAD: u64 = 128;
When we create an account we tell the blockchain how much space it will need for storing the specific data inside the buffer.
This space is fixed but can be adjusted using realloc, or more recently resize.
Reading Accounts
All addresses on Solana uniquely identify an account.
We can access the data from an account by asking an RPC node to fetch it for us. We would provide the address of the account and the RPC node:
import { createSolanaRpc } from "@solana/kit"
const rpc = createSolanaRpc("http://localhost:8899");
const accountInfo = await rpc.getAccountInfo(mint.address).send();
console.log(accountInfo);
Then it would return us some JSON:
{
"context": {
"apiVersion": "3.0.7",
"slot": 420371843n
},
"value": {
"data": ["AQAAAPR/cf7PNwHaz9JAucJSQH2hzzb7qCflf/FavQxZ6INHAAAAAAAAAAAJAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", "base64"],
"executable": false,
"lamports": 1461600n,
"owner": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
"rentEpoch": 0n,
"space": 82n
}
}
In this JSON is the data field of the account. This stores the arbitrary data in a base64 encoded form ready to be decoded. We need to decode it according to its data structure using an IDL.
Using these IDL we can generate (or hand write) code that decodes the data into something meaningful:
import {
getStructDecoder,
addDecoderSizePrefix,
getUtf8Decoder,
getU32Decoder
} from "@solana/kit"
const personDecoder: Decoder<PersonData> = getStructDecoder([
['name', addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())],
['age', getU32Decoder()]
]);
const decodedAccount = decodeAccount(account, personDecoder);
Writing Accounts
Anyone can read account data, but only an account's designated owner can modify it. This owner is always a program, usually the system program. To control who can interact through this program, each account has an authority.
This means that if we ask a program to modify one of the accounts it owns, it will only agree to it if the authority for that account signed the transaction.
All human accounts (like wallets) are owned by the system program. The system program can create more accounts and send lamports.
Accounts can only be assigned a new owner once.
#[derive(BorshDeserialize, BorshSerialize, Debug)]
pub struct AddressInfo {
pub name: String,
pub house_number: u8,
pub street: String,
pub city: String
}
Borsh is an encoding for binary objects. Its how we go from bytecode back into something Rust will understand.
Creating an Account
To create an account you first generate a private/public keypair. Then you need to register that account to the blockchain by invoking the create account instruction on the system program:
import { generateKeyPair } from "@solana/keys";
const { privateKey, publicKey } = await generateKeyPair();
Even though we generated a keypair, the account is not actually initialized on the blockchain. When you want to create a new account on-chain we need to do two things:
- Create the account and allocate its space on-chain
- Initialize the account with its data, which is done by the owner
Here is what it looks like on kit:
describe("Create account", async () => {
const { rpc, rpcSubscriptions } = createDefaultSolanaClient()
it("Creates the account", async () => {
// Create signers.
const [payer, mint] = await Promise.all([generateKeyPairSigner(), generateKeyPairSigner()]);
// Create the instructions.
const createAccount = getCreateAccountInstruction({
payer, // <- TransactionSigner
newAccount: mint, // <- TransactionSigner
space,
lamports,
programAddress: TOKEN_PROGRAM_ADDRESS,
});
const initializeMint = getInitializeMintInstruction({
mint: mint.address,
mintAuthority: address("1234..5678"),
decimals: 2,
});
// Create the transaction.
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(payer, tx), // <- TransactionSigner
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([createAccount, initializeMint], tx),
);
// Sign the transaction.
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
// Create a send and confirm function from your RPC and RPC Subscriptions objects.
const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
// Use it to send and confirm any signed transaction.
const transactionSignature = getSignatureFromTransaction(signedTransaction);
await sendAndConfirm(signedTransaction, { commitment: "confirmed" });
})
})
Accounts are Buffers of Bytes
Like we said before, accounts are actually buffers. create_account is basically calloc on the blockchain. Data is stored as an array of bytes:
let number: u32 = 42;
// Convert to little-endian byte array
let number_bytes = number.to_le_bytes();
Account Ownership and Authority
Clients use private keys to sign transactions which mark accounts as signed during program execution. This signed state does not have any special semantics; it’s up to programs to give this signed status meaning.
PDA (Program Derived Addresses)
A PDA address doesn’t have a corresponding private key. Solana lets the program that derived the PDA "sign" during cross-program invocations using invoke_signed.
Bumps
To derive a public key, Solana uses an additional integer (the bump) to avoid collision with actual keypairs. This ensures that a PDA is valid while allowing the same address to be derived consistently.