Appearance
Transactions
Transactions are the only way to change state on the network. A transaction is an atomic bundle of one or more instructions, each targeting a specific program. If any instruction fails, the entire transaction rolls back and no state changes are persisted.
Upstream-compatible
Transaction format, signing, versioning, and lifecycle on Zink are identical to Solana mainnet. Existing client code that constructs and submits transactions works without modification — just change the RPC endpoint.
Transaction anatomy
A transaction has two top-level components:
Transaction
├── signatures[] — one Ed25519 signature per required signer
└── message
├── header — counts of required signers, read-only signers, read-only non-signers
├── account_keys[] — ordered list of all account public keys referenced by the transaction
├── recent_blockhash — a recent blockhash proving the transaction was created recently
└── instructions[] — the operations to executeMessage header
The header contains three u8 values:
| Field | Meaning |
|---|---|
num_required_signatures | Total number of accounts that must sign |
num_readonly_signed_accounts | Of those signers, how many are read-only |
num_readonly_unsigned_accounts | Of the remaining (non-signer) accounts, how many are read-only |
The runtime uses these counts together with the account_keys array to determine which accounts are writable and which are signers, without repeating that information per-instruction.
Recent blockhash
Every transaction includes a recent blockhash — a hash of a recently produced block. This serves two purposes:
- Replay protection. A transaction with a given blockhash can only be processed once; duplicate submissions are rejected.
- Expiration. If the blockhash is too old (typically older than ~60–90 seconds / ~150 slots), validators reject the transaction as expired.
Clients fetch a recent blockhash via the getLatestBlockhash RPC method before constructing a transaction.
Instructions
Each instruction within a transaction specifies:
| Field | Description |
|---|---|
program_id_index | Index into the transaction's account_keys identifying which program to invoke |
accounts | Array of indices into account_keys for the accounts this instruction needs, each marked as signer/writable |
data | Arbitrary byte array passed to the program — encodes the instruction variant and arguments |
A single transaction can contain instructions targeting different programs. For example, one transaction might:
- Call the Compute Budget program to set a compute unit limit.
- Call the System Program to create an account.
- Call your custom program to initialize that account.
All three execute atomically. This composability at the transaction level — before you even get to CPI — is one of the defining features of the SVM.
Signing
Transactions use Ed25519 signatures. Every account marked as a signer in the message must have a corresponding signature in the signatures array, in the same order as they appear in account_keys.
The first signer is the fee payer — the account that pays the transaction's base fee and any priority fee (see Fees).
For multi-signature transactions, all required signers must sign before the transaction is submitted. Client libraries like @solana/web3.js and solana-sdk provide utilities for collecting signatures from multiple parties.
Transaction lifecycle
Client RPC Node Leader Validator
│ │ │
│ sendTransaction(tx) │ │
│ ─────────────────────────>│ forward to leader │
│ │ ──────────────────────>│
│ │ │ 1. Verify signatures
│ │ │ 2. Check blockhash is recent
│ │ │ 3. Load accounts
│ │ │ 4. Execute instructions (SVM)
│ │ │ 5. Commit or rollback
│ │ │
│ confirmTransaction │ poll for status │
│ ─────────────────────────>│ ──────────────────────>│
│ <─────────────────────────│ │
│ { status, slot, err } │ │Confirmation levels
After submission, clients can query a transaction's status at different commitment levels:
| Level | Meaning |
|---|---|
processed | The transaction has been included in a block by the leader |
confirmed | The block has been voted on by a supermajority of the cluster |
finalized | The block is rooted — 31+ confirmations — and will not be rolled back |
Choose the level appropriate to your use case. For display purposes, confirmed is usually sufficient. For irreversible operations (e.g., bridging), wait for finalized.
Versioned transactions
The SVM supports two transaction formats:
| Version | Description |
|---|---|
| Legacy | Original format. All accounts referenced by the transaction must appear in the account_keys array. |
| V0 (versioned) | Adds support for address lookup tables (ALTs), allowing a transaction to reference more accounts without exceeding the 1232-byte transaction size limit. |
Address lookup tables
A legacy transaction can reference at most ~35 accounts before hitting the size limit, because each account key is 32 bytes. Address lookup tables solve this by storing frequently-used account keys on-chain in a lookup table account. A V0 transaction can then reference those accounts by index (1 byte each) instead of by full public key.
This is especially useful for transactions that interact with programs requiring many accounts — such as DEX swaps or complex CPI chains.
Legacy: account_keys = [key0, key1, key2, ..., keyN] ← 32 bytes each
V0: account_keys = [key0, key1] ← only keys not in ALTs
address_table_lookups = [
{ table: ALT_pubkey, writable: [0, 3], readonly: [1, 2] }
] ← 1 byte per indexTransaction size limits
- Maximum serialized size: 1232 bytes (fits in a single IPv6 MTU packet).
- Maximum accounts per transaction: 256 (includes all accounts across all instructions, plus lookup table accounts themselves in V0).
- Maximum instructions: No hard limit, but bounded by the 1232-byte size and the compute budget.
Simulation
Before submitting a transaction, you can simulate it against the current cluster state:
typescript
const result = await connection.simulateTransaction(transaction);
// result.value.err — null if the simulation succeeded
// result.value.logs — program log output
// result.value.unitsConsumed — compute units usedSimulation is free (no fees charged) and does not modify state. It is the recommended way to:
- Catch errors before spending fees.
- Estimate compute unit consumption for setting an accurate budget (see Fees).
- Debug instruction failures using program logs.
Error handling
If a transaction fails, the runtime returns a TransactionError indicating which instruction failed and why. Common errors include:
| Error | Cause |
|---|---|
InsufficientFundsForFee | Fee payer doesn't have enough lamports |
BlockhashNotFound | The recent blockhash has expired |
AccountNotFound | A referenced account doesn't exist |
InstructionError(idx, reason) | Instruction at idx failed — reason gives the specific program error |
Client libraries surface these errors so you can handle them programmatically and, where appropriate, retry with a fresh blockhash.