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.
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.
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.
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 andDevnet
network. - Follow the next steps to request your free 1 SOL on the Solana-Devnet.
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
π 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
- docs.solana.com/developing/programming-mode..
- docs.solana.com/proposals/transactions-v2
- solana-labs.github.io/solana-web3.js
At the time of writing this guide. β©
At the time of writing this guide. β©
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. β©