Skip to content
Launch App >

Address Lookup Tables (ALTs) compress transactions by replacing full 32-byte addresses with 1-byte indices. The zklsol program uses LUTs to fit complex operations within Solana’s transaction size limits.

Solana transactions have a 1232 byte limit. Without LUTs:

  • Each account address = 32 bytes
  • A withdraw with 10 accounts = 320 bytes just for addresses
  • Plus instruction data, signatures, headers = easily exceeds limit

With LUTs:

  • Each account address = 1 byte (index into table)
  • Same 10 accounts = 10 bytes
  • Much more room for complex operations

Each user can create their own LUT for frequent addresses:

import { PublicKey } from '@solana/web3.js';
import { Program, BN } from '@coral-xyz/anchor';
const PROGRAM_ID = new PublicKey('FGKoWNsvTTDCGW9JyR2DJWNzSXpejWk7yXcKsFFj9GQp');
async function createUserLUT(
program: Program,
payer: PublicKey,
recentSlot: number
): Promise<{ instruction: TransactionInstruction; lutAddress: PublicKey }> {
// Derive user LUT PDA
const [userLutPda] = PublicKey.findProgramAddressSync(
[Buffer.from('UserAddressLookupTable'), payer.toBuffer()],
PROGRAM_ID
);
const ix = await program.methods
.createAddressLookupTable({
recentSlot: new BN(recentSlot),
})
.accounts({
signer: payer,
systemProgram: SystemProgram.programId,
addressLookupTableProgram: AddressLookupTableProgram.programId,
// ... other accounts
})
.instruction();
return { instruction: ix, lutAddress: userLutPda };
}

Add addresses to your LUT:

async function extendUserLUT(
program: Program,
payer: PublicKey,
addresses: PublicKey[]
): Promise<TransactionInstruction> {
return program.methods
.extendAddressLookupTable()
.accounts({
signer: payer,
// ... accounts
})
.remainingAccounts(
addresses.map((addr) => ({
pubkey: addr,
isSigner: false,
isWritable: false,
}))
)
.instruction();
}

Before closing, deactivate the LUT:

async function deactivateUserLUT(
program: Program,
payer: PublicKey
): Promise<TransactionInstruction> {
return program.methods
.deactivateAddressLookupTable()
.accounts({
signer: payer,
// ... accounts
})
.instruction();
}

After deactivation cooldown, reclaim rent:

async function closeUserLUT(
program: Program,
payer: PublicKey
): Promise<TransactionInstruction> {
return program.methods
.closeAddressLookupTable()
.accounts({
signer: payer,
// ... accounts
})
.instruction();
}
import { AddressLookupTableAccount } from '@solana/web3.js';
async function fetchLUT(
connection: Connection,
lutAddress: PublicKey
): Promise<AddressLookupTableAccount> {
const lutAccount = await connection.getAddressLookupTable(lutAddress);
if (!lutAccount.value) {
throw new Error('LUT not found');
}
return lutAccount.value;
}
import {
TransactionMessage,
VersionedTransaction,
} from '@solana/web3.js';
async function createVersionedTx(
connection: Connection,
payer: PublicKey,
instructions: TransactionInstruction[],
lutAccounts: AddressLookupTableAccount[]
): Promise<VersionedTransaction> {
const { blockhash } = await connection.getLatestBlockhash();
const message = new TransactionMessage({
payerKey: payer,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message(lutAccounts);
return new VersionedTransaction(message);
}

The relayer maintains its own LUTs with common addresses:

  • Program ID
  • Settings PDA
  • Common token mints
  • Token program
  • System program
PracticeReason
Reuse LUTsAvoid creating new ones for each transaction
Include common addressesProgram IDs, system programs, your accounts
Wait for finalizationDon’t use LUT until it’s finalized on-chain
Clean up unused LUTsReclaim rent from old tables

After deactivation, there’s a cooldown before closing:

  • ~512 slots (~3-4 minutes on mainnet)
  • Ensures no in-flight transactions are using the LUT
ErrorCauseSolution
LutNotFoundLUT doesn’t existCreate LUT first
LutNotActiveLUT deactivatedCreate new LUT
LutFullMax addresses reachedCreate additional LUT
CooldownNotCompleteToo soon to closeWait for cooldown