Address Lookup Tables
Using Address Lookup Tables for transaction optimization
Address Lookup Tables (LUT)
Section titled “Address Lookup Tables (LUT)”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.
Why LUTs?
Section titled “Why LUTs?”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
LUT Architecture
Section titled “LUT Architecture”User LUT Management
Section titled “User LUT Management”Each user can create their own LUT for frequent addresses:
Create LUT
Section titled “Create LUT”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 };}Extend LUT
Section titled “Extend LUT”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();}Deactivate LUT
Section titled “Deactivate LUT”Before closing, deactivate the LUT:
async function deactivateUserLUT( program: Program, payer: PublicKey): Promise<TransactionInstruction> { return program.methods .deactivateAddressLookupTable() .accounts({ signer: payer, // ... accounts }) .instruction();}Close LUT
Section titled “Close LUT”After deactivation cooldown, reclaim rent:
async function closeUserLUT( program: Program, payer: PublicKey): Promise<TransactionInstruction> { return program.methods .closeAddressLookupTable() .accounts({ signer: payer, // ... accounts }) .instruction();}Using LUTs in Transactions
Section titled “Using LUTs in Transactions”Fetch LUT Account
Section titled “Fetch LUT Account”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;}Create Versioned Transaction
Section titled “Create Versioned Transaction”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);}Relayer LUTs
Section titled “Relayer LUTs”The relayer maintains its own LUTs with common addresses:
- Program ID
- Settings PDA
- Common token mints
- Token program
- System program
Best Practices
Section titled “Best Practices”| Practice | Reason |
|---|---|
| Reuse LUTs | Avoid creating new ones for each transaction |
| Include common addresses | Program IDs, system programs, your accounts |
| Wait for finalization | Don’t use LUT until it’s finalized on-chain |
| Clean up unused LUTs | Reclaim rent from old tables |
LUT Lifecycle
Section titled “LUT Lifecycle”Cooldown Period
Section titled “Cooldown Period”After deactivation, there’s a cooldown before closing:
- ~512 slots (~3-4 minutes on mainnet)
- Ensures no in-flight transactions are using the LUT
Error Handling
Section titled “Error Handling”| Error | Cause | Solution |
|---|---|---|
LutNotFound | LUT doesn’t exist | Create LUT first |
LutNotActive | LUT deactivated | Create new LUT |
LutFull | Max addresses reached | Create additional LUT |
CooldownNotComplete | Too soon to close | Wait for cooldown |
Next Steps
Section titled “Next Steps”- API Reference - Relay API documentation
- IDL Reference - Full instruction reference