Marketplace has launched, further enabling blockchain developers! Learn more

How to Use Lookup Tables on Solana

October 20, 2022

Overview

On October 10, 2022 (Epoch 358), Solana added support for new transaction version types through a concept known as "Versioned Transactions". After this change, the Solana runtime now supports two types of transactions: "legacy" (older transactions) and "0" (transactions that include Address Lookup Tables). Address lookup tables bring a new way for developers to efficiently load many addresses into a transaction, so if you have had issues with storage size due to accounts, this could help you out!

The Address Lookup Table Program (Program ID: AddressLookupTab1e1111111111111111111111111) allows you to store public keys in on-chain lookup tables and call the Lookup Table in your Versioned Transaction. Because serialized transactions transmitted to Solana validators must not exceed 1,232 bytes (Source Code, Reference), leveraging a Lookup Table can reduce transaction size and enable more complex transaction instructions (e.g., more accounts, more integrated Cross-Program Invocations, etc.). Solana lookup tables "effectively 'compress' a 32-byte address into a 1-byte index value" (Source). This means by using lookup tables, our transactions sizes will be smaller (or that we can pack more into our transactions)!

What You Will Do

In this guide, you will:

  • create and execute a Version 0 (V0) Transaction,
  • create and populate an Address Lookup Table, and
  • compare the transaction size of two nearly identical transactions (one using a lookup table and one without).

If you need help with ensuring your existing client-side apps can support Versioned Transactions, check out our Guide: How to Update Your Solana Client to Handle Versioned Transactions.

What You Will Need


If you're coming over from our previous Guide: How to Use Versioned Transactions on Solana, you can reuse your app.ts and can Skip to the Assemble a Version 0 Transaction section. Otherwise, set up your project:

Set Up Your Project

Create a new project directory in your terminal with the following:

set up your project

Copy
mkdir solana-versioned-tx
cd solana-versioned-tx

Create a file for your app, app.ts:

set up your project

Copy
echo > app.ts

Initialize your project with the "yes" flag to use default values for your new package:

set up your project

Copy
yarn init --yes
#or
npm init --yes

Create a tsconfig.json with .json importing enabled:

set up your project

Copy
tsc -init --resolveJsonModule true

Install Solana Web3 Dependency

We will need to add the Solana Web3 library for this exercise. In your terminal, type:

set up your project

Copy
yarn add @solana/web3.js
#or
npm install @solana/web3.js

Create a Wallet and Airdrop SOL

You'll need to create a Solana File System Wallet (secret key written to a guideSecret.json file) and airdrop some SOL to it. You can do this using Solana CLI or use this script we have created for you.

Make sure you save your secret key to your project directory as guideSecret.json.

Let's get started.

Set Up Your App

Import Necessary Dependencies

Open app.ts, and paste the following imports on line 1:

set up your app

Copy
import { AddressLookupTableProgram, Connection, Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction, TransactionInstruction, TransactionMessage, VersionedTransaction } from '@solana/web3.js';
import secret from './guideSecret.json';

const SIGNER_WALLET = Keypair.fromSecretKey(new Uint8Array(secret));
const DESTINATION_WALLET = Keypair.generate();
//const LOOKUP_TABLE_ADDRESS = new PublicKey(""); // We will add this later
In addition to the wallet we created in the previous step, we are also importing a few essential methods and classes from the Solana Web3 library. There are probably a couple of imports you have not seen before (e.g., AddressLookupTableProgram and VersionedTransaction)--we will cover those later in this guide.

We have defined two wallets: SIGNER_WALLET will send SOL to our DESTINATION_WALLET. We have also added a constant, LOOKUP_TABLE_ADDRESS, which we will update later to reference our Lookup Table's on-chain account ID.

Set Up Your QuickNode Endpoint

To build on Solana, you'll need an API endpoint to connect with the network. You're welcome to use public nodes or deploy and manage your own infrastructure; however, if you'd like 8x faster response times, you can leave the heavy lifting to us. See why over 50% of projects on Solana choose QuickNode and sign up for a free account here. We're going to use a Solana Devnet node.

Copy the HTTP Provider link:



Inside app.ts under your import statements, declare your RPC and establish your Connection to Solana:

set up your app

Copy
const QUICKNODE_RPC = 'https://example.solana-devnet.quiknode.pro/0123456/';
const SOLANA_CONNECTION = new Connection(QUICKNODE_RPC);

Your environment should look like this:



Alright, let's BUILD!

Assemble a Version 0 Transaction

To use Lookup Tables, you must use Version 0 transactions. If you're unfamiliar with constructing and executing Versioned Transactions, check out our guide, here.

Let's start by creating a new function, createAndSendV0Tx, that accepts an array of TransactionInstructions, txInstructions:

assemble a version 0 transaction

Copy
async function createAndSendV0Tx(txInstructions: TransactionInstruction[]) {
    // Step 1 - Fetch Latest Blockhash
    let latestBlockhash = await SOLANA_CONNECTION.getLatestBlockhash('finalized');
    console.log("   ✅ - Fetched latest blockhash. Last valid height:", latestBlockhash.lastValidBlockHeight);

    // Step 2 - Generate Transaction Message
    const messageV0 = new TransactionMessage({
        payerKey: SIGNER_WALLET.publicKey,
        recentBlockhash: latestBlockhash.blockhash,
        instructions: txInstructions
    }).compileToV0Message();
    console.log("   ✅ - Compiled transaction message");
    const transaction = new VersionedTransaction(messageV0);

    // Step 3 - Sign your transaction with the required `Signers`
    transaction.sign([SIGNER_WALLET]);
    console.log("   ✅ - Transaction Signed");

    // Step 4 - Send our v0 transaction to the cluster
    const txid = await SOLANA_CONNECTION.sendTransaction(transaction, { maxRetries: 5 });
    console.log("   ✅ - Transaction sent to network");

    // Step 5 - Confirm Transaction 
    const confirmation = await SOLANA_CONNECTION.confirmTransaction({
        signature: txid,
        blockhash: latestBlockhash.blockhash,
        lastValidBlockHeight: latestBlockhash.lastValidBlockHeight
    });
    if (confirmation.value.err) { throw new Error("   ❌ - Transaction not confirmed.") }
    console.log('🎉 Transaction succesfully confirmed!', '\n', `https://explorer.solana.com/tx/${txid}?cluster=devnet`);
}

Let's walk through our code:

  • Step 1: We fetch the latest blockhash from the network. Note: We pass the parameter, 'finalized', to make sure the block does not belong to a dropped fork.
  • Step 2: Using our txInstructions parameter and the latestBlockhash, we create a new MessageV0 by building a Message and executing the .compileToV0Message() method.
  • Step 3: We sign the transaction with an array of signers. In this case, it is just our SIGNER_WALLET.
  • Step 4: We send the transaction to the cluster using sendTransaction, which will return a transaction signature/id.
  • Step 5: We wait for the cluster to confirm the transaction has succeeded. If it succeeds, we log our explorer URL; otherwise, we throw an error.

Alright! We're all set. Let's add some instructions.

Create an Address Lookup Table

Create a new async function, createLookupTable, that will build our transaction instruction and invoke createAndSendV0Tx:

create an address lookup table

Copy
async function createLookupTable() {
    // Step 1 - Get a lookup table address and create lookup table instruction
    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]);
}

Breaking down our code:

  • Step 1: We 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.
  • Step 2: We log the table's address (which we will need later in this exercise).
  • Step 3: Finaly, we call createAndSendV0Tx by passing lookupTableInst inside of an array to match our type requirements.

Awesome! At this point, you should be able to run your code and create an empty lookup table. After your function, call it by adding:

create an address lookup table

Copy
createLookupTable();

Then in your terminal enter:

create an address lookup table

Copy
ts-node app.ts

You should see your transaction progressing in your terminal and ultimately receive a URL to your transaction page on Solana Explorer:


Our lookup table account address is: 3uBhgRWPTPLfvfqxi4M9eVZC8nS1kDG9XPkdHKgG69nw:



Great Job! You have made your first lookup table.

Before we move on, let's do some cleanup:

  1. Remove your call to createLookupTable(). We won't need it again.
  2. Remember the LOOKUP_TABLE_ADDRESS constant we created a while ago? Remove the comment backslashes, and add your table lookup address from your console to your PublicKey declaration like so (this is line 6 for us):

create an address lookup table

Copy
const LOOKUP_TABLE_ADDRESS = new PublicKey("YOUR_TABLE_ADDRESS_HERE"); 
// e.g., const LOOKUP_TABLE_ADDRESS = new PublicKey("3uBhgRWPTPLfvfqxi4M9eVZC8nS1kDG9XPkdHKgG69nw");

Add Addresses to Your Lookup Table

Since we have already created createAndSendV0Tx, adding an address to a lookup table is easy! All we need to do is create a TransactionInstruction. Create a new async function, addAddressesToTable that uses the AddressLookupTableProgram.extendLookupTable() method:

add addresses to your lookup table

Copy
async function addAddressesToTable() {
    // Step 1 - Create Transaction Instruction
    const addAddressesInstruction = AddressLookupTableProgram.extendLookupTable({
        payer: SIGNER_WALLET.publicKey,
        authority: SIGNER_WALLET.publicKey,
        lookupTable: LOOKUP_TABLE_ADDRESS,
        addresses: [
            Keypair.generate().publicKey,
            Keypair.generate().publicKey,
            Keypair.generate().publicKey,
            Keypair.generate().publicKey,
            Keypair.generate().publicKey
        ],
    });
    // 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`)

}

Let's break down what's going on with the extendLookupTable method:

  1. We pass our SIGNER_WALLET to pay the transaction fees and any additional rent incurred.
  2. We define our update authority - in our case, we set that as the SIGNER_WALLET in our table creation step above.
  3. We pass in the lookup table account address (which we defined as LOOKUP_TABLE_ADDRESS).
  4. We pass an array of addresses into our lookup table. We will pass in a few random public keys, but you can pass in any public key that you like! The Program's "compression" supports storing up to 256 addresses in a single lookup table!
  5. Let's also log a link to our lookup table entries for easy access after the transaction is complete. Finally, we pass our TransactionInstruction into createAndSendV0Tx to generate a transaction and send it to the network!

After your function, call your new function by adding:

add addresses to your lookup table

Copy
addAddressesToTable();

Run your code--in your terminal, use the up arrow on your keyboard to retrieve the previous command, ts-node app.ts, and hit Enter to run your code.

You should see a similar transaction flow in your terminal and success with a link to your transaction and your lookup table entries on Solana Explorer. Go to the lookup table entries. You should see a list of all of your table's stored public keys:



Nice work! You can modify your addresses array and rerun it to add additional public keys to your lookup table.

Before moving on, delete your call to addAddressesToTable();. We won't need it again.

Lookup Addresses in Your Lookup Table

Okay, so if you're following along, you should have an Address Lookup Table populated with a few addresses. Now, we are going to fetch all of our addresses and log them to your terminal. Create a new function, findAddressesInTable:

lookup addresses in your lookup table

Copy
async function findAddressesInTable() {
    // Step 1 - Fetch our address lookup table
    const lookupTableAccount = await SOLANA_CONNECTION.getAddressLookupTable(LOOKUP_TABLE_ADDRESS)
    console.log(`Successfully found lookup table: `, lookupTableAccount.value?.key.toString());

    // Step 2 - Make sure our search returns a valid table
    if (!lookupTableAccount.value) return;

    // Step 3 - Log each table address to console
    for (let i = 0; i < lookupTableAccount.value.state.addresses.length; i++) {
        const address = lookupTableAccount.value.state.addresses[i];
        console.log(`   Address ${(i + 1)}: ${address.toBase58()}`);
    }
}
Here's a summary of what we are doing here:

  1. Fetch our lookup table using a method called getAddressLookupTable() and pass our lookup table's account address. If successful, the query should return an AddressLookupTableAccount object.
  2. Stop the function if we don't find a valid lookup table.
  3. Iterate through each address found in our lookup table and log it to the console.

Not bad, right?

Call your new function and then run it:

lookup addresses in your lookup table

Copy
findAddressesInTable();

In your terminal, use the up arrow on your keyboard to retrieve the previous command, ts-node app.ts, and hit Enter to run your code.

You should see your list of addresses like this:



If you're not seeing that or having trouble with any of the code, feel free to drop us a line on Discord -- we're here to help!

Remove or comment out your call to findAddressesInTable(); before moving on.

Put Lookup Tables to the Test

You're well on your way to becoming a Solana lookup table master--give yourself a quick pat on the back for making it this far. But what's the point of all of this? Good question! Solana lookup tables "effectively 'compress' a 32-byte address into a 1-byte index value" (Source). This means by using lookup tables, our transactions sizes will be smaller (or that we can pack more into our transactions)--let's prove it!

We're going to create a function that generates two transactions using the same transfer instruction; in one of them, we will pass our lookup table, and in the other we will not. We will check each transaction's size and then see which is smaller!

Add this new function, compareTxSize, to your code, and then we will walk through it together:

put lookup tables to the test

Copy
async function compareTxSize() {
    // Step 1 - Fetch the lookup table
    const lookupTable = (await SOLANA_CONNECTION.getAddressLookupTable(LOOKUP_TABLE_ADDRESS)).value;
    if (!lookupTable) return;
    console.log("   ✅ - Fetched lookup table:", lookupTable.key.toString());

    // Step 2 - Generate an array of Solana transfer instruction to each address in our lookup table
    const txInstructions: TransactionInstruction[] = [];
    for (let i = 0; i < lookupTable.state.addresses.length; i++) {
        const address = lookupTable.state.addresses[i];
        txInstructions.push(
            SystemProgram.transfer({
                fromPubkey: SIGNER_WALLET.publicKey,
                toPubkey: address,
                lamports: 0.01 * LAMPORTS_PER_SOL,
            })
        )
    }

    // Step 3 - Fetch the latest Blockhash
    let latestBlockhash = await SOLANA_CONNECTION.getLatestBlockhash('finalized');
    console.log("   ✅ - Fetched latest blockhash. Last valid height:", latestBlockhash.lastValidBlockHeight);

    // Step 4 - Generate and sign a transaction that uses a lookup table
    const messageWithLookupTable = new TransactionMessage({
        payerKey: SIGNER_WALLET.publicKey,
        recentBlockhash: latestBlockhash.blockhash,
        instructions: txInstructions
    }).compileToV0Message([lookupTable]); // 👈 NOTE: We DO include the lookup table
    const transactionWithLookupTable = new VersionedTransaction(messageWithLookupTable);
    transactionWithLookupTable.sign([SIGNER_WALLET]);

    // Step 5 - Generate and sign a transaction that DOES NOT use a lookup table
    const messageWithoutLookupTable = new TransactionMessage({
        payerKey: SIGNER_WALLET.publicKey,
        recentBlockhash: latestBlockhash.blockhash,
        instructions: txInstructions
    }).compileToV0Message(); // 👈 NOTE: We do NOT include the lookup table
    const transactionWithoutLookupTable = new VersionedTransaction(messageWithoutLookupTable);
    transactionWithoutLookupTable.sign([SIGNER_WALLET]);

    console.log("   ✅ - Compiled transactions");

    // Step 6 - Log our transaction size
    console.log('Transaction size without address lookup table: ', transactionWithoutLookupTable.serialize().length, 'bytes');
    console.log('Transaction size with address lookup table:    ', transactionWithLookupTable.serialize().length, 'bytes');
}

This looks like a lot, but it's reusing the code we created earlier in this guide. Let's step through: Step 1. We fetch our lookup table and make sure that we return a valid result. Step 2. Using a similar loop to our previous address log exercise, we iterate through each address in our table, create a transfer instruction to that address, and we push each TransactionInstruction to an array, txInstructions. Step 3. We fetch the latest blockhash, which we need to generate our messages. Step 4. We generate a transaction that uses our lookup table. Note that we pass [lookupTable] into the .compileToV0Message() method. When we used this before, we did not pass a value here. This lets our transaction know to use our lookup table. To calculate the size of our transaction, we also need to sign it--we do so with .sign(). Step 5. We generate the same transaction WITHOUT calling our lookup table. We now have two transactions that should process the same instructions, but one uses a lookup table, and the other just uses the public keys that were passed into each instruction. Step 6. Finally, we log each transaction's size. You can calculate a transaction size by serializing it using .serialize() (which yields a Uint8Array) and calculating its length using .length.

We did a lot there, but I hope everything is starting to feel familiar! Call your function by adding compareTxSize(); to the end of your code:

put lookup tables to the test

Copy
compareTxSize();

In your terminal, use the up arrow on your keyboard to retrieve the previous command, ts-node app.ts, and hit Enter to run your code.

Do you see results like this?

put lookup tables to the test

Copy
Transaction Size without Address Lookup Table:  413 bytes
Transaction Size with Address Lookup Table:     292 bytes

Wow! That's a lot of savings. My lookup table saved me over 120 bytes (your savings may be more or less depending on the number of addresses in your table)! In an environment where transactions are limited to 1,232 bytes, that's a big deal.

Wrap Up

That concludes our guide! If you'd like to check your final code against ours, you can find it on GitHub, here.

Address Lookup Tables are an exciting new addition to Solana. As you continue building out more complex transactions, you should find that they create flexibility in how you approach your programs and dApps.

Want to keep building?


If you're stuck, have questions, or just want to talk about what you're building, drop us a line on Discord or Twitter!

We <3 Feedback!

If you have any feedback on this guide, let us know. We’d love to hear from you.

Related articles 33

Como crear un NFT en SOLANA
Published: Dec 27, 2021
Updated: Sep 23, 2022

¡Hola querido lector! Bienvenidos a una nueva guía de Solana.Solana es una blockchain que promete mucho a la hora de intentar resolver los problemas de escalabilidad que...

Continue reading
How to Get Transaction Logs on Solana
Published: Jun 24, 2022
Updated: Oct 27, 2022

Ever need to pull all the transactions associated with a Wallet? Want to see all of the mint transactions associated with a Candy Machine? Or maybe see transaction history of an NFT? Solana's...

Continue reading
How to Transfer SPL Tokens on Solana
Published: Sep 23, 2022
Updated: Sep 23, 2022

Sending Solana Program Library (SPL) Tokens is a critical mechanism for Solana development. Whether you are airdropping whitelist tokens to your community, bulk sending NFTs to another wallet,...

Continue reading
How to Mint an NFT on Solana
Published: Aug 27, 2021
Updated: Sep 23, 2022

Updated at: April 10, 2022Welcome to another QuickNode guide on Solana - the up-and-coming blockchain that seeks to solve the scalability issues of Ethereum. We will be walking through...

Continue reading
How to Send Bulk Transactions on Solana
Published: Aug 31, 2022
Updated: Oct 3, 2022

Are you running a batch process that has many transactions? Perhaps an airdrop to your community's NFT holders or a token distribution to early users of your dApp. Solana transaction...

Continue reading
Solana Fundamentals Reference Guide
Published: Oct 27, 2022
Updated: Oct 27, 2022

The Solana blockchain is a powerful tool, delivering thousands of transactions per second with almost no-cost transaction fees. If you are new to Web3 or have developed on EVM-based...

Continue reading