Skip to content

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 execute

Message header

The header contains three u8 values:

FieldMeaning
num_required_signaturesTotal number of accounts that must sign
num_readonly_signed_accountsOf those signers, how many are read-only
num_readonly_unsigned_accountsOf 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:

  1. Replay protection. A transaction with a given blockhash can only be processed once; duplicate submissions are rejected.
  2. 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:

FieldDescription
program_id_indexIndex into the transaction's account_keys identifying which program to invoke
accountsArray of indices into account_keys for the accounts this instruction needs, each marked as signer/writable
dataArbitrary 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:

  1. Call the Compute Budget program to set a compute unit limit.
  2. Call the System Program to create an account.
  3. 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:

LevelMeaning
processedThe transaction has been included in a block by the leader
confirmedThe block has been voted on by a supermajority of the cluster
finalizedThe 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:

VersionDescription
LegacyOriginal 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 index

Transaction 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 used

Simulation 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:

ErrorCause
InsufficientFundsForFeeFee payer doesn't have enough lamports
BlockhashNotFoundThe recent blockhash has expired
AccountNotFoundA 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.

Zink is a general-purpose SVM network for programs, operators, and bridge integrations.