Appearance
Accounts
Accounts are the fundamental unit of state on the SVM. Every piece of persistent data — program bytecode, token balances, user records, configuration — lives inside an account. There are no implicit storage slots or contract-scoped databases; if data exists on-chain, it exists as an account.
Upstream-compatible
The account model on Zink is identical to Solana mainnet. Accounts have the same structure, the same ownership rules, and the same rent mechanics. Existing code that reads or writes accounts requires no changes.
Account structure
Every account on the network is a contiguous region of bytes associated with a public key. The runtime tracks five fields per account:
| Field | Type | Description |
|---|---|---|
key | Pubkey (32 bytes) | The address of the account — its unique identifier on the network |
lamports | u64 | The account's balance in lamports (1 SOL = 1,000,000,000 lamports) |
data | Vec<u8> | An arbitrary byte buffer that holds the account's state |
owner | Pubkey (32 bytes) | The program ID that owns this account and is allowed to modify its data |
executable | bool | Whether this account contains executable program code |
Additionally, the runtime tracks a rent_epoch field used internally for rent collection bookkeeping.
┌──────────────────────────────────────────────┐
│ Account (on-chain) │
├──────────────┬───────────────────────────────┤
│ key │ 8Xkco...9vBn (32 bytes) │
│ lamports │ 1_000_000_000 │
│ data │ [u8; N] │
│ owner │ TokenkegQf... (program ID) │
│ executable │ false │
│ rent_epoch │ 365 │
└──────────────┴───────────────────────────────┘Ownership rules
Every account has exactly one owner — a program ID. The ownership model enforces two critical invariants:
- Only the owner program can modify an account's
databytes. Any program can read any account, but write access is exclusive to the owner. - Only the owner program can debit an account's
lamports. Any program can credit lamports to any account (increase the balance), but only the owner can debit (decrease the balance).
When a new account is created via the System Program, its owner is initially set to the System Program (11111111111111111111111111111111). Programs typically reassign ownership to themselves as part of account initialization by calling the System Program's create_account instruction, which atomically allocates space, transfers lamports, and assigns the new owner.
Zink recommendation
Programs that expect other applications to compose with their state should follow established account layout conventions to simplify indexing and integration. Consistent discriminator and serialization patterns make it possible for ecosystem tools to decode accounts uniformly.
Rent and rent-exemption
Accounts consume validator memory, and the network charges rent for that storage. In practice, all accounts should be made rent-exempt by holding a minimum lamport balance proportional to their data size.
The minimum balance for rent-exemption is:
minimum_balance = (128 + data_length) × rent_rate_per_byte_year × 2The factor of 128 accounts for the fixed overhead of the account metadata. The multiplier of 2 means the account must hold two years' worth of rent to be exempt.
You can query the exact value from the runtime:
bash
# CLI
solana rent <DATA_SIZE_IN_BYTES>rust
// On-chain (inside a program)
let min_balance = Rent::get()?.minimum_balance(data_len);Zink-specific
Rent parameters on Zink clusters are configured independently. Query the cluster directly rather than assuming Solana mainnet defaults. Use solana rent <size> --url <ZINK_RPC> to get the current value.
Since the introduction of rent-exemption enforcement, accounts that fall below the minimum balance are rejected at transaction time. In practice this means every account you create must be funded to at least the rent-exempt minimum.
Creating accounts
The System Program is the only program that can create new accounts. The standard flow is:
- Determine the data size your account needs.
- Query
Rent::get()?.minimum_balance(data_size)for the required lamports. - Issue a
create_accountinstruction to the System Program specifying the payer, new account key, lamports, space, and owner program.
In Starframe, Init plus Create handles the account creation flow while keeping the payer, seeds, and account type explicit:
rust
#[derive(AccountSet, Debug)]
pub struct InitializeAccounts {
#[validate(funder)]
pub authority: Signer<Mut<SystemAccount>>,
#[validate(arg = (
Create(()),
Seeds(MyDataSeeds { authority: *self.authority.pubkey() }),
))]
pub my_account: Init<Seeded<Account<MyData>>>,
pub system_program: Program<System>,
}Size calculations are still your responsibility. Reserve enough bytes for the data layout your program actually writes, including any discriminator or custom header you choose to include.
Account size limits
- Maximum account data size: 10 MiB (10,485,760 bytes).
- Accounts can be resized after creation using
realloc(in native programs) orreallochelpers or explicit System Program allocation flows, up to the 10 MiB cap. Each realloc may require additional lamports to maintain rent-exemption. - The size of an account can also be decreased, refunding the excess lamports to a designated account.
Data serialization
The runtime treats the data field as an opaque byte buffer. Programs choose their own serialization format:
| Approach | Description |
|---|---|
| Borsh | The default in the Solana ecosystem. Deterministic, compact, and well-supported in Rust and JS. |
| Zero-copy | Maps a struct directly onto the account's byte buffer with no (de)serialization cost. Useful for large, frequently-accessed accounts. Starframe supports this with #[zero_copy(pod)] plus fixed-size account types. |
| Custom | Programs can use any format (MessagePack, Protobuf, raw byte packing) as long as they handle serialization themselves. |
The relationship between accounts and programs
A program is itself an account — one with the executable flag set to true. The program's bytecode is stored in a separate ProgramData account managed by the BPF Loader. When you deploy a program, the loader creates both the executable account (the program ID) and the ProgramData account that holds the actual sBPF bytecode.
Every non-executable account is owned by exactly one program. That program defines the rules for reading and writing the account's data. This is why you pass accounts into program instructions: the runtime checks at the boundary that the instruction's program is the owner of every account it tries to modify.
For more on programs themselves, see Programs. For how programs derive deterministic account addresses, see PDAs.