Everything You Need to Know — Address Lookup Tables in Solana | by Raunak Raj Rauniyar | Nov, 2023

As a Solana developer, of course, you are familiar with addresses in the blockchain. In the Solana world, everything is stored in accounts. If Linux stores data into files, Solana stores data into accounts like a file system. (Just had to compare!). So every transaction requires a listing of every address that is interacted with as part of the transaction. So you’ll find yourself listing tons of addresses more often than not, but like everything, transactions have a limit of 32 addresses per transaction. That’s where Address Lookup Tables come in!

This tutorial covers the use cases of Address Lookup Tables in-depth, involving what value they provide, practical implementation, codebase, and just everything you need to know about ALTs.

Address Lookup Tables, commonly referred to as “lookup tables” or “ALTs” for short, allow developers to create a collection of related addresses to efficiently load more addresses in a single transaction.

What this effectively means is with Address Lookup Tables, a transaction would now be able to raise that limit to 256 addresses per transaction surpassing the 32 addresses limit. Sounds great right? To understand what can be done with ALTs, let’s understand why it was made.

The Solana protocol, like any other, operates within certain limits and constraints. Presently, the maximum acceptable size for a packet to be processed by the Solana validator software stands at 1280 bytes (the minimum transmission unit of IPv6). After accounting for headers, this permits only 1232 bytes for the payload, effectively capping the size of a Solana transaction.

Address Lookup Tables were designed as a solution to these limits as described in the following proposal.

So to utilize an Address Lookup Table inside a transaction, we must use v0 transactions that were introduced with the new Versioned Transaction format. Versioned transactions are modified from the former referred to as legacy transactions to incorporate address table lookups. The idea behind ALTs is to store account addresses in a table-like (array) data structure on-chain.

Let’s understand how some really smart people have figured out how to put 256 addresses in a transaction. What effectively happens is after all the addresses have been stored in an ALT, each address can be referenced inside a transaction by its 1-byte index within the table (instead of their full 32-byte address). This lookup method effectively “compresses” a 32-byte address into a 1-byte index value.

This opens up space as addresses need not be stored inside the transaction message anymore. They only need to be referenced in the form of an index within the array-like ALT table. This leads to a possibility of referencing 2⁸=256 accounts, as accounts are referenced using a u8 index.

Comparison of account space distribution between legacy and versioned transactions

So like in the image, the 32-byte address is compressed into a 1-byte index value and can be referenced inside the transaction drastically reducing the size and letting us have more addresses in the transaction. Wohhooo, that’s some interior designer work to free up space.

Transaction Efficiency:

As we discussed in the earlier section, ALTs let you load 252 addresses in a single transaction resolving the former 32-address limit. The table involves consolidating all the necessary addresses required for a transaction into a single account, and conveniently referencing them using the 1-byte indexes, easing the whole process by executing group of addresses in a single step.

Data, Size, and Cost:

Since ALTs compasse 32-bit addresses into 1-bit indexes, they primarily optimize the on-chain storage by storing all addresses into a single account (the lookup table), structuring the way of handling and accessing data. It reduces the overall cost as the use of ALTs reduces the network bandwidth due to smaller transactions and therefore smaller blocks. More transactions substituted by just one saves the gas fees.

Optimized Updating:

When multiple accounts need to be updated together in a transaction, ensuring atomicity is crucial for maintaining data consistency. ALTs provide a way to update related data atomically, ensuring that all changes occur in a single, indivisible transaction.

Metadata and easy appending:

All resolved address from an address lookup table that is stored in the transaction metadata allows quick referencing. This avoids the need for clients to make multiple RPC round trips to fetch all accounts loaded by a v0 transaction. It will also make it easier to use the ledger tool to analyze account access patterns. Also, once an ALT is created, it can be extended, ie., accounts can be appended to the table, making it highly customizable and scalable.

After knowing this, I don’t know about you but I’d definitely use it.

Let’s go through a practical implementation where Address Lookup Tables really make the code efficient and the developers’ lives much easier. So a really cool feature in crypto is the Multiswap. It creates a liquidity pool of several different tokens that can be easily traded with each other.

This allows users to exchange tokens across multiple pools in a single transaction, potentially achieving better exchange rates. So I can swap my SOL to USDC, ETH, BTC, etc from a single transaction. Traditionally this would require two pools, to individually swap requiring multiple transactions.

But that’s the beauty of ALTs. It serves as a central repository of addresses for the accessed accounts, in the swap pools. By referencing the ALT instead of explicitly listing each address in the transaction, the transaction size is significantly reduced, enabling more instructions to fit within the block limit and enhancing overall network efficiency.

So instead of writing serval transactions for individual swap pools, and a bunch of addresses, we can fit it all in one versioned transaction. Isn’t that great? Let’s see the code now!

1. Establishing Swap Pools

It’s okay if you don’t understand the following block of code completely. I am just providing it for context to understand the problem ALT is solving.

Generally, here we are initializing a swap pool for each pair of mints, and performing a swap transaction using each pool, starting from mint0 and gradually swapping all the way to mintN.

// Create swap pools for each pair of mints
const NUMBER_OF_MINTS: usize = 9;
let mut swap_ixs = Vec::new();
let mut pool_keys = Vec::new();

for i in 0..(NUMBER_OF_MINTS - 1) {
let token_swap_harness = token_swap_harness::initialize_pool(
&payer,
&token_mints[i],
&token_mints[i + 1],
1_000_000,
1_000_000,
&connection,
);

// Generate swap instruction for each pool
let ix = token_swap_harness.create_swap_instruction(
&payer.pubkey(),
&payer.pubkey(),
true,
spl_token_swap::instruction::Swap {
amount_in: 1_000 - i as u64 * 10, // Slow decay to account for cpamm formula
minimum_amount_out: 0,
},
&connection,
);
swap_ixs.push(ix);

// Store pool addresses for later use
pool_keys.extend(token_swap_harness.get_keys(&connection));
}

2. Constructing an Address Lookup Table (ALT)

Here comes the magic! We create an address lookup table (ALT) to store the addresses of all created swap pools. And extend the ALT to include the remaining pool addresses in batches of 20.

// Create an ALT to store swap pool addresses
let recent_slot = connection.get_slot_with_commitment(CommitmentConfig::finalized()).unwrap();
let (create_ix, table_pk) = solana_address_lookup_table_program::instruction::create_lookup_table(
payer.pubkey(),
payer.pubkey(),
recent_slot,
);

// Submit transaction to create the ALT
let latest_blockhash = connection.get_latest_blockhash().unwrap();
connection.send_and_confirm_transaction(&Transaction::new_signed_with_payer(
&[create_ix],
Some(&payer.pubkey()),
&[&payer],
latest_blockhash,
)).unwrap();

3. Executing Multi-Hop Swaps Leveraging the ALT

And finally, we create a multi-hop swap transaction that traverses through all swap pools, utilizing the ALT to efficiently reference their addresses. Then we submit the multi-hop swap transaction and confirm its execution.

// Extend the ALT to include all swap pool addresses
let mut signature = Signature::default();
let latest_blockhash = connection.get_latest_blockhash().unwrap();

for selected_pool_keys in pool_keys.chunks(20) {
let extend_ix = solana_address_lookup_table_program::instruction::extend_lookup_table(
table_pk,
payer.pubkey(),
Some(payer.pubkey()),
selected_pool_keys.to_vec(),
);

// Submit

Smooth, right? That’s one of many implementations of leveraging ALTs to solve efficiency problems and make things simple. You can always check the multi-swap implementation in-depth from here.

First things first, you deserve a pat on the back for coming this far. Let’s talk about some general implementation of ALTs and how we can incorporate them into our projects. We’ll cover the general steps of creating one, interacting with it, and using it in a transaction.

Creating an Adress Lookup Table

We use the @solana/web3.js library‘s createLookupTable function to construct the instruction needed to create a new lookup table, as well as determine its address:

const web3 = require("@solana/web3.js");

// connect to a cluster and get the current `slot`
const connection = new web3.Connection(web3.clusterApiUrl("devnet"));
const slot = await connection.getSlot();

const [lookupTableInst, lookupTableAddress] =
AddressLookupTableProgram.createLookupTable({
authority: SIGNER_WALLET.publicKey,
payer: SIGNER_WALLET.publicKey,
recentSlot: await SOLANA_CONNECTION.getSlot(),
});

// Step 2 - Log Lookup Table Address
console.log("Lookup Table Address:", lookupTableAddress.toBase58());

// Step 3 - Generate a transaction and send it to the network
createAndSendV0Tx([lookupTableInst]);

We basically create two variables, lookupTableInst, and lookupTableAddress, by destructuring the results of the createLookupTable method. This method returns the public key for the table once created and a TransactionInstruction that can be passed into our createAndSendV0Tx function. Finally, we call createAndSendV0Tx by passing lookupTableInst inside of an array to match our type requirements. Easy, huh?

Add addresses to a lookup table

Once the lookup table is created we need to add addresses in that. That’s what it’s used for right? Adding addresses to a lookup table is known as “extending”. So all the addresses involving the transaction go in here.

// add addresses to the `lookupTableAddress` table via an `extend` instruction

// Step 1 - Create Transaction Instruction
const extendInstruction = web3.AddressLookupTableProgram.extendLookupTable({
payer: payer.publicKey,
authority: payer.publicKey,
lookupTable: lookupTableAddress,
addresses: [
payer.publicKey,
web3.SystemProgram.programId,
Keypair.generate().publicKey,
Keypair.generate().publicKey,
Keypair.generate().publicKey,
// list more `publicKey` addresses here
],
});

// Step 2 - Generate a transaction and send it to the network
await createAndSendV0Tx([addAddressesInstruction]);
console.log(`Lookup Table Entries: `,`https://explorer.solana.com/address/${LOOKUP_TABLE_ADDRESS.toString()}/entries?cluster=devnet`)

It’s pretty simple, we use the extendLookupTable method to add addresses to the lookup table. Firstly, we pass in the lookup table account address (which we defined as lookupTableAddress in the previous step). Then we pass an array of addresses into our lookup table. I have added some random addresses there, but you’re supposed to add addresses that you later wanna reference in the transaction. The Program’s “compression” supports storing up to 256 addresses in a single lookup table!

Fetch an Address Lookup Table

So by now, we have a lookup table created with addresses stacked in. Now more often than not, we’ll have to fetch these addresses from the ALTs. It’s similar to requesting another account (or PDA) from the cluster, you can fetch a complete Address Lookup Table with the getAddressLookupTable method:

// define the `PublicKey` of the lookup table to fetch
const lookupTableAddress = new web3.PublicKey("");

// get the table from the cluster
const lookupTableAccount = (await connection.getAddressLookupTable(lookupTableAddress)).value;

// `lookupTableAccount` will now be a `AddressLookupTableAccount` object

console.log("Table address from cluster:", lookupTableAccount.key.toBase58());

Not bad, right? We have created an ALT, stored addresses in it, and even fetched those to our program. Now comes the final step, suing this ALT in a transaction.

Using an ALT in a transaction

After we have created a lookup table, and stored the needed address on chain (via extending the lookup table), we can create a v0 transaction to utilize the on-chain lookup capabilities. Let’s see how.

// Assumptions:
// - `arrayOfInstructions` has been created as an `array` of `TransactionInstruction`
// - we are using the `lookupTableAccount` obtained above

// construct a v0 compatible transaction `Message`
const messageV0 = new web3.TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: arrayOfInstructions, // note this is an array of instructions
}).compileToV0Message([lookupTableAccount]);

// create a v0 transaction from the v0 message
const transactionV0 = new web3.VersionedTransaction(messageV0);

// sign the v0 transaction using the file system wallet we created named `payer`
transactionV0.sign([payer]);

// send and confirm the transaction
// (NOTE: There is NOT an array of Signers here; see the note below...)
const txid = await web3.sendAndConfirmTransaction(connection, transactionV0);

console.log(
`Transaction: https://explorer.solana.com/tx/${txid}?cluster=devnet`,
);

We just construct a v0-compatible transaction message and then create a v0 transaction from that v0 message. Then we sign the transaction using the payer wallet (which speaks for itself) and finally send and confirm the transaction.

One thing to note is when sending a VersionedTransaction to the cluster, it must be signed BEFORE calling the sendAndConfirmTransaction method (Like above). If you pass an array of Signer (like with legacy transactions) the method will trigger an error!

OH, that’s a lot. Let’s take a moment to step back and see what we’ve done. We’ve managed to magically reduce the size of transactions and fit in way more addresses, wow!

We went through the general building scenario in Solana and learned what ALTs can do and why it was proposed. We discussed versioned transactions a lot. We went deeper into the why and how of ALTs with their real-world implementation and the comprehensive process of building and interacting with ALTS.

Embracing technology is magical, and we are the people who get to do it. There are countless more things you can do in Solana especially when tools like ALTs make things so easy. I want to see what story you tell, and what magic you build. The Solana team, countless blogs and resources like this, and the whole ecosystem are at your disposal to help you navigate when you hit a bump, you just need to start! Some resources are below:

  1. Multi-Swap Repository
  2. Solana Cookbook for versioned transactions
  3. Solana Documentation on Lookup Tables
  4. QuickNode Guide on Using Lookup Tables on Solana
  5. Proposal for Address Lookup Tables

If you’ve read this far, thank you, I believe this tutorial was helpful and encourage you to reach out with any further queries on Twitter