Address Lookup Tables and Versioned Transactions on Solana

Address Lookup Tables and Versioned Transactions on Solana

In this guide, we will learn how to create an Address Lookup Table and send a Versioned Transaction on the Solana blockchain.

Β·

8 min read

Solana is a high-performant blockchain that is currently doing an average of ~2.5k-3k transactions per second (TPS)1. Solana has a theoretical throughput of ~65k TPS, far from its current state. Hence, it would require tons of effort to scale to that level.

To achieve this highly ambitious goal, the Solana Foundation team constantly updates the blockchain with new features, enhancements, and improvements. They can back this claim with data, as we can see that the team has made 4349 commits in the past one year2.

Recently, they released a new feature 'Address Lookup Tables'; and furthermore, another feature 'Versioned Transactions' to support the lookup tables.

In this guide, we will

  • Learn about the Address Lookup Tables and the Versioned Transactions, and,
  • Create an Address Lookup Table and send a Versioned Transaction using it.

Versioned Transactions

Versioned Transactions are the new transaction format that provide additional runtime functionality to the transactions on the Solana blockchain. This means new transaction features would be available without changing the on-chain programs.

The first of these Versioned Transactions is a v0 or version 0 transaction that allows the use of an Address Lookup Table program in a transaction.

Note: All the transactions in the old format would now be tagged as legacy version transactions.

Address Lookup Tables

The Address Lookup Tables or lookup tables allow the developers to create a collection of related account addresses directly on-chain, empowering them to load more addresses in a single transaction efficiently.

A single account address is a 32-byte of arbitrary data on the Solana blockchain. And, every transaction on the Solana blockchain requires listing all the addresses it has interacted with during that transaction. This means that a single transaction can have a maximum of 35 addresses3 4.

Using the lookup tables, each address can be referenced as a 1-byte index in the transaction (instead of a full 32-byte address). This method effectively compresses a 32-byte address into a 1-byte index value, allowing the users to store up to 256 addresses in a single transaction β€” 8x more than the old format!

Well, let us see it live... πŸ‘€

Requirements

  • Node.js / Solana SDK
    Though you can use the programming language of your choice, we will use Node.js with the @solana/web3.js SDK in this guide.

    • Install Node.js on your local machine.
    • Install @solana/web3.js using the Node Package Manager (NPM):
      npm i @solana/web3.js
      
  • RPC API endpoint (QuickNode)
    We will use QuickNode as it provides the fastest, most stable, and most reliable RPC API endpoints.

    • Register for a free account on QuickNode and verify your email address.
    • Create a Solana-Devnet endpoint. Screenshot 2022-10-15 at 16.04.45.gif
  • Solana wallet (Phantom)
    You can use any Solana-compatible wallet that grants you access to your private keys. We will use the Phantom wallet because of its simple UI and smooth UX.

    • Download the Phantom wallet for your browser.
    • Create a new wallet.
    • Change the Phantom wallet network to Devnet. Screenshot 2022-10-15 at 16.15.05.gif
    • Export your Private Key and store it somewhere safe. ❗️NEVER SHARE YOUR PRIVATE KEY WITH ANYONE ❗️ Screenshot 2022-10-15 at 16.18.25.gif

Tip: We can also use Solana's File System Wallet to generate a wallet Keypair using CLI without the need for the Phantom wallet.

  • Solana faucet (QuickNode Faucet)
    We will require some test funds β€” SOL β€” to experiment. Again, QuickNode provides a free multi-chain faucet to help us with this.
    • Go to the Faucet webpage.
    • Connect your (Phantom) wallet.
    • Select the Solana chain and Devnet network.
    • Follow the next steps to request your free 1 SOL on the Solana-Devnet. Screenshot 2022-10-15 at 16.56.32.gif

That's all. We are set. πŸƒπŸ»

Let's Code!

Define functions and variables

1. Import required modules

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

Along with the @solana/web3.js SDK, we use the bs58 module to decode our private key.

2. Set required variables

const PRIVATE_KEY = [YOUR_PRIVATE_KEY_FROM_PHANTOM_WALLET];
const ENDPOINT = [YOUR_QUICKNODE_ENDPOINT_URL];

Set the variable values with the ones you have.

3. Add account addresses

let tableEntries = [
  // List of account addresses for address lookup table
];

These account addresses will be added to the lookup table later and can be any valid account address on the Solana blockchain.

4. pause()

async function pause() {
    await new Promise((resolve) => setTimeout(resolve, 5000)); // Value in ms
}

We will use this function to pause the app at a few stages to ensure that we are waiting and fetching the updated data from the blockchain.

5. createConn()

async function createConn() {
    return new solana.Connection(ENDPOINT, 'confirmed');
}

We will use this function to create an active (endpoint) connection to interact with the blockchain.

6. getLatestBlockhash()

const getLatestBlockhash = (connection) => {
    return connection
        .getLatestBlockhash('finalized')
        .then((res) => res.blockhash);
};

We will need the latest blockhash to send the transactions. This function will use the active connection to fetch the latest blockhash from the blockchain.

7. createLookupTable()

async function createLookupTable(connection, payer) {
    const [lookupTableInst, lookupTableAddress] =
        solana.AddressLookupTableProgram.createLookupTable({
            authority: payer.publicKey,
            payer: payer.publicKey,
            recentSlot: await connection.getSlot(),
        });
    return [lookupTableInst, lookupTableAddress];
}

We will use this function to create the lookup table by passing an active connection and account owner to it. It will return lookup table instructions and address, which we will use to register the lookup table on-chain through a transaction.

8. extendLookupTable()

async function extendLookupTable(payer, lookupTableAddress, addressList) {
    const extendInst = solana.AddressLookupTableProgram.extendLookupTable({
        addresses: addressList,
        authority: payer.publicKey,
        lookupTable: lookupTableAddress,
        payer: payer.publicKey,
    });
    return extendInst;
}

We will use this function to extend the lookup table by passing a list of account addresses. It will return lookup table instructions which we will use to update the lookup table on-chain through a transaction.

9. sendTx()

async function sendTx(connection, payer, inst, lookupTable, compareTxSize) {
    let message;

    if (lookupTable) {
        message = new solana.TransactionMessage({
            instructions: inst,
            payerKey: payer.publicKey,
            recentBlockhash: await getLatestBlockhash(connection),
        }).compileToV0Message([lookupTable]);
    } else {
        message = new solana.TransactionMessage({
            instructions: inst,
            payerKey: payer.publicKey,
            recentBlockhash: await getLatestBlockhash(connection),
        }).compileToV0Message();
    }

    const tx = new solana.VersionedTransaction(message);
    tx.sign([payer]);

    if (compareTxSize) {
        console.log(
            lookupTable
                ? '🟒 [With Address Lookup Table]'
                : 'πŸ”΄ [Without Address Lookup Table]',
            'Tx Size:',
            tx.serialize().length,
            'bytes'
        );
    }

    const txId = await connection.sendTransaction(tx);
    return txId;
}

We will use this as our primary function to send transactions (register events on the blockchain). It accepts multiple parameters:

  • Active connection
  • Account owner
  • Transaction instructions
  • Lookup table account
  • Choice to calculate transaction message size

It's a multi-step process β€” create message; create versioned transaction; sign transaction; send transaction.

Define main app

10. Create main app()

async function app() {
  // Everything after this point would be added to this function
}

11. Setup wallet Keypair

  console.log('πŸ‘€ Solana - Address Lookup Tables / Versioned Transactions πŸ‘€');

  const privKey = bs58.decode(PRIVATE_KEY);
  const myWallet = solana.Keypair.fromSecretKey(privKey);

12. Establish connection

    const conn = await createConn();
    console.log('βœ… Endpoint connected!');

13. Create lookup table

    const [lookupTableInst, lookupTableAddress] = await createLookupTable(
        conn,
        myWallet
    );
    console.log(
        `πŸ“ Address Lookup Table Details --- ${lookupTableAddress} | https://explorer.solana.com/address/${lookupTableAddress.toBase58()}?cluster=devnet`
    );

    await pause();

    const createTableTxId = await sendTx(conn, myWallet, [lookupTableInst]);
    console.log(
        `βœ… Address Lookup Table Created: https://explorer.solana.com/tx/${createTableTxId}?cluster=devnet`
    );

    await pause();

14. Extend lookup table

    let addressList = new Array();
    addressList.push(myWallet.publicKey);
    tableEntries.forEach((entry) => {
        addressList.push(new solana.PublicKey(entry));
    });

    const extendInst = await extendLookupTable(
        myWallet,
        lookupTableAddress,
        addressList
    );

    const ExtTableTxId = await sendTx(conn, myWallet, [extendInst]);
    console.log(
        `βœ… Address Lookup Table Extended: https://explorer.solana.com/tx/${ExtTableTxId}?cluster=devnet`
    );

    await pause();

15. Fetch lookup table data

    const lookupTableAccount = await conn
        .getAddressLookupTable(lookupTableAddress)
        .then((res) => res.value);

    console.log('🧾 Verifying Addresses in the Lookup Table...');

    for (let i = 0; i < lookupTableAccount.state.addresses.length; i++) {
        const address = lookupTableAccount.state.addresses[i];
        console.log('Address', i + 1, address.toBase58());
    }

16. Create transaction instructions

    const txInst = new Array();
    for (let i = 0; i < lookupTableAccount.state.addresses.length; i++) {
        const address = lookupTableAccount.state.addresses[i];
        txInst.push(
            solana.SystemProgram.transfer({
                fromPubkey: myWallet.publicKey,
                toPubkey: address,
                lamports: 0.0001 * solana.LAMPORTS_PER_SOL, // 0.0001 SOL
            })
        );
    }

17. Send transactions

    // Tx with lookup table
    const sendNewTx1 = await sendTx(
        conn,
        myWallet,
        txInst,
        lookupTableAccount,
        true
    );

    // Tx without lookup table
    const sendNewTx2 = await sendTx(conn, myWallet, txInst, null, true);

    console.log(
        `πŸš€ Versioned Transaction with the Address Lookup Table: https://explorer.solana.com/tx/${sendNewTx1}?cluster=devnet`
    );

18. Initialize app()

app();

Output

Screenshot 2022-10-16 at 11.39.16.gif

Screenshot 2022-10-16 at 11.39.52@2x.png

πŸ‘€ Solana - Address Lookup Tables / Versioned Transactions πŸ‘€
βœ… Endpoint connected!
πŸ“ Address Lookup Table Details --- 7x3zgryXe9RSucuV3DriSqTcj4kKPBikZPTEnAWRK8kT | https://explorer.solana.com/address/7x3zgryXe9RSucuV3DriSqTcj4kKPBikZPTEnAWRK8kT?cluster=devnet
βœ… Address Lookup Table Created: https://explorer.solana.com/tx/3BcHwivCv2NQjdkZpfbpJwjUMfRQbbjTFhxTTekKsKBCPQGvd6MKKZsfQ7r8A8ZZvkpQVVY1PJvrV1rJBgEtcGZh?cluster=devnet
βœ… Address Lookup Table Extended: https://explorer.solana.com/tx/3nqHuPBRxS7rupwninpTwrTxikDwGWBM8F5gGbxjKUbQ1zHEg8fTKoeJbkLaajRv7PbM48HLc43GmLXKmGe8ZLqm?cluster=devnet
🧾 Verifying Addresses in the Lookup Table...
Address 1 5FoZtVFxvSwkyyJWKiadcX4vQrTDdEcHVhwB6179bRgq
Address 2 BXBk9wAw424EQEpxMFjaqjs3D3rjfqezuKh8sobbPVy5
Address 3 FN69Pe14FDC6iEeQEb1usyy3m5EMZB2xBED6o91YaNU4
Address 4 4PUuD5o6fzjGniqTbsUKaW5cJACoJAVeJQ91JkPmTzyH
🟒 [With Address Lookup Table] Tx Size: 273 bytes
πŸ”΄ [Without Address Lookup Table] Tx Size: 332 bytes
πŸš€ Versioned Transaction with the Address Lookup Table: https://explorer.solana.com/tx/47ftfMgLVcSxjdCSiGiXbcnLACmsSXHPzQwH953MXC74tdDLkDGtAchfYvQLaj9ADUMEYJvvfthzA2TD2i7d735a?cluster=devnet

Conclusion

As you notice, there is a good difference in the size of the transaction message with the Address Lookup Table. The benefit would be more noticeable during transactions with custom programs and a large number of account addresses.

Hopefully, this is the first of many transaction features to come that would make the Solana blockchain more efficient.

Also, if you use RPC methods to fetch the blockchain data, it is essential to note that these changes have also impacted some RPC methods. I have posted a Twitter thread with the details about how this feature activation would affect the developers.

I hope you found this guide helpful! Until next time... 🫑

Full Code


Other Resources


  1. At the time of writing this guide. ↩

  2. At the time of writing this guide. ↩

  3. Solana blockchain uses a conservative MTU size of 1280 bytes. After accounting for headers, signatures, and other metadata, the transaction is left with around 1120 bytes. ↩

  4. github.com/solana-labs/solana/blob/090e1121.. ↩


Β