Skip to content

Commit 0d377d0

Browse files
committed
solana: populate an address lookup table to reduce tx size
1 parent d56f2c1 commit 0d377d0

File tree

13 files changed

+880
-17
lines changed

13 files changed

+880
-17
lines changed

sdk/__tests__/utils.ts

+9
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,15 @@ async function deploySolana(ctx: Ctx): Promise<Ctx> {
545545
await signSendWait(ctx.context, initTxs, signer);
546546
console.log("Initialized ntt at", manager.program.programId.toString());
547547

548+
// NOTE: this is a hack. The next instruction will fail if we don't wait
549+
// here, because the address lookup table is not yet available, despite
550+
// the transaction having been confirmed.
551+
// Looks like a bug, but I haven't investigated further. In practice, this
552+
// won't be an issue, becase the address lookup table will have been
553+
// created well before anyone is trying to use it, but we might want to be
554+
// mindful in the deploy script too.
555+
await new Promise((resolve) => setTimeout(resolve, 400));
556+
548557
const registrTxs = manager.registerTransceiver({
549558
payer: keypair,
550559
owner: keypair,

sdk/solana/src/ntt.ts

+134-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { Program } from "@coral-xyz/anchor";
1+
import { Program, web3 } from "@coral-xyz/anchor";
22
import * as splToken from "@solana/spl-token";
33
import {
44
createAssociatedTokenAccountInstruction,
55
getAssociatedTokenAddressSync,
66
} from "@solana/spl-token";
77
import {
8+
AddressLookupTableAccount,
9+
AddressLookupTableProgram,
810
Connection,
911
Keypair,
1012
PublicKey,
@@ -71,6 +73,7 @@ export class SolanaNtt<N extends Network, C extends SolanaChains>
7173
program: Program<NttBindings.NativeTokenTransfer>;
7274
config?: NttBindings.Config;
7375
quoter?: NttQuoter<N, C>;
76+
addressLookupTable?: AddressLookupTableAccount;
7477

7578
constructor(
7679
readonly network: N,
@@ -263,6 +266,100 @@ export class SolanaNtt<N extends Network, C extends SolanaChains>
263266
{ transaction: tx, signers: [] },
264267
"Ntt.Initialize"
265268
);
269+
270+
yield* this.initializeOrUpdateLUT({ payer: args.payer });
271+
}
272+
273+
// This function should be called after each upgrade. If there's nothing to
274+
// do, it won't actually submit a transaction, so it's cheap to call.
275+
async *initializeOrUpdateLUT(args: { payer: Keypair }) {
276+
// TODO: find a more robust way of fetching a recent slot
277+
const slot = (await this.program.provider.connection.getSlot()) - 1;
278+
279+
const [_, lutAddress] = web3.AddressLookupTableProgram.createLookupTable({
280+
authority: this.pdas.lutAuthority(),
281+
payer: args.payer.publicKey,
282+
recentSlot: slot,
283+
});
284+
285+
const whAccs = utils.getWormholeDerivedAccounts(
286+
this.program.programId,
287+
this.core.address
288+
);
289+
const config = await this.getConfig();
290+
291+
const entries = {
292+
config: this.pdas.configAccount(),
293+
custody: config.custody,
294+
tokenProgram: config.tokenProgram,
295+
mint: config.mint,
296+
tokenAuthority: this.pdas.tokenAuthority(),
297+
outboxRateLimit: this.pdas.outboxRateLimitAccount(),
298+
wormhole: {
299+
bridge: whAccs.wormholeBridge,
300+
feeCollector: whAccs.wormholeFeeCollector,
301+
sequence: whAccs.wormholeSequence,
302+
program: this.core.address,
303+
systemProgram: SystemProgram.programId,
304+
clock: web3.SYSVAR_CLOCK_PUBKEY,
305+
rent: web3.SYSVAR_RENT_PUBKEY,
306+
},
307+
};
308+
309+
// collect all pubkeys in entries recursively
310+
const collectPubkeys = (obj: any): Array<PublicKey> => {
311+
const pubkeys = new Array<PublicKey>();
312+
for (const key in obj) {
313+
const value = obj[key];
314+
if (value instanceof PublicKey) {
315+
pubkeys.push(value);
316+
} else if (typeof value === "object") {
317+
pubkeys.push(...collectPubkeys(value));
318+
}
319+
}
320+
return pubkeys;
321+
};
322+
const pubkeys = collectPubkeys(entries).map((pk) => pk.toBase58());
323+
324+
var existingLut: web3.AddressLookupTableAccount | null = null;
325+
try {
326+
existingLut = await this.getAddressLookupTable(false);
327+
} catch {
328+
// swallow errors here, it just means that lut doesn't exist
329+
}
330+
331+
if (existingLut !== null) {
332+
const existingPubkeys =
333+
existingLut.state.addresses?.map((a) => a.toBase58()) ?? [];
334+
335+
// if pubkeys contains keys that are not in the existing LUT, we need to
336+
// add them to the LUT
337+
const missingPubkeys = pubkeys.filter(
338+
(pk) => !existingPubkeys.includes(pk)
339+
);
340+
341+
if (missingPubkeys.length === 0) {
342+
return existingLut;
343+
}
344+
}
345+
346+
const ix = await this.program.methods
347+
.initializeLut(new BN(slot))
348+
.accountsStrict({
349+
payer: args.payer.publicKey,
350+
authority: this.pdas.lutAuthority(),
351+
lutAddress,
352+
lut: this.pdas.lutAccount(),
353+
lutProgram: AddressLookupTableProgram.programId,
354+
systemProgram: SystemProgram.programId,
355+
entries,
356+
})
357+
.instruction();
358+
359+
const tx = new Transaction().add(ix);
360+
tx.feePayer = args.payer.publicKey;
361+
362+
yield this.createUnsignedTx({ transaction: tx }, "Ntt.InitializeLUT");
266363
}
267364

268365
async *registerTransceiver(args: {
@@ -294,7 +391,7 @@ export class SolanaNtt<N extends Network, C extends SolanaChains>
294391
);
295392
const broadcastIx = await this.program.methods
296393
.broadcastWormholeId()
297-
.accounts({
394+
.accountsStrict({
298395
payer: args.payer.publicKey,
299396
config: this.pdas.configAccount(),
300397
mint: config.mint,
@@ -306,6 +403,8 @@ export class SolanaNtt<N extends Network, C extends SolanaChains>
306403
sequence: whAccs.wormholeSequence,
307404
program: this.core.address,
308405
systemProgram: SystemProgram.programId,
406+
clock: web3.SYSVAR_CLOCK_PUBKEY,
407+
rent: web3.SYSVAR_RENT_PUBKEY,
309408
},
310409
})
311410
.instruction();
@@ -346,7 +445,7 @@ export class SolanaNtt<N extends Network, C extends SolanaChains>
346445
.instruction(),
347446
this.program.methods
348447
.broadcastWormholePeer({ chainId: toChainId(peer.chain) })
349-
.accounts({
448+
.accountsStrict({
350449
payer: sender,
351450
config: this.pdas.configAccount(),
352451
peer: this.pdas.transceiverPeerAccount(peer.chain),
@@ -357,6 +456,9 @@ export class SolanaNtt<N extends Network, C extends SolanaChains>
357456
feeCollector: whAccs.wormholeFeeCollector,
358457
sequence: whAccs.wormholeSequence,
359458
program: this.core.address,
459+
clock: web3.SYSVAR_CLOCK_PUBKEY,
460+
rent: web3.SYSVAR_RENT_PUBKEY,
461+
systemProgram: SystemProgram.programId,
360462
},
361463
})
362464
.instruction(),
@@ -751,7 +853,7 @@ export class SolanaNtt<N extends Network, C extends SolanaChains>
751853
.releaseWormholeOutbound({
752854
revertOnDelay: args.revertOnDelay,
753855
})
754-
.accounts({
856+
.accountsStrict({
755857
payer: args.payer,
756858
config: { config: this.pdas.configAccount() },
757859
outboxItem: args.outboxItem,
@@ -764,6 +866,8 @@ export class SolanaNtt<N extends Network, C extends SolanaChains>
764866
sequence: whAccs.wormholeSequence,
765867
program: this.core.address,
766868
systemProgram: SystemProgram.programId,
869+
clock: web3.SYSVAR_CLOCK_PUBKEY,
870+
rent: web3.SYSVAR_RENT_PUBKEY,
767871
},
768872
})
769873
.instruction();
@@ -943,6 +1047,32 @@ export class SolanaNtt<N extends Network, C extends SolanaChains>
9431047
}
9441048
}
9451049

1050+
async getAddressLookupTable(
1051+
useCache = true
1052+
): Promise<AddressLookupTableAccount> {
1053+
if (!useCache || !this.addressLookupTable) {
1054+
const lut = await this.program.account.lut.fetchNullable(
1055+
this.pdas.lutAccount()
1056+
);
1057+
if (!lut)
1058+
throw new Error(
1059+
"Address lookup table not found. Did you forget to call initializeLUT?"
1060+
);
1061+
1062+
const response = await this.connection.getAddressLookupTable(lut.address);
1063+
if (response.value === null) throw new Error("Could not fetch LUT");
1064+
1065+
this.addressLookupTable = response.value;
1066+
}
1067+
1068+
if (!this.addressLookupTable)
1069+
throw new Error(
1070+
"Address lookup table not found. Did you forget to call initializeLUT?"
1071+
);
1072+
1073+
return this.addressLookupTable;
1074+
}
1075+
9461076
createUnsignedTx(
9471077
txReq: SolanaTransaction,
9481078
description: string,

sdk/solana/src/utils.ts

+4
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ export const nttAddresses = (programId: PublicKeyInitData) => {
112112
derivePda(["transceiver_message", chainToBytes(chain), id], programId);
113113
const wormholeMessageAccount = (outboxItem: PublicKey): PublicKey =>
114114
derivePda(["message", outboxItem.toBytes()], programId);
115+
const lutAccount = (): PublicKey => derivePda("lut", programId);
116+
const lutAuthority = (): PublicKey => derivePda("lut_authority", programId);
115117
const sessionAuthority = (sender: PublicKey, args: TransferArgs): PublicKey =>
116118
derivePda(
117119
[
@@ -142,6 +144,8 @@ export const nttAddresses = (programId: PublicKeyInitData) => {
142144
transceiverPeerAccount,
143145
transceiverMessageAccount,
144146
registeredTransceiver,
147+
lutAccount,
148+
lutAuthority,
145149
};
146150
};
147151

solana/Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

solana/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ anchor-spl = "0.29.0"
4747
solana-program = "=1.18.10"
4848
solana-program-runtime = "=1.18.10"
4949
solana-program-test = "=1.18.10"
50+
solana-address-lookup-table-program = "=1.18.10"
5051
spl-token = "4.0.0"
5152
spl-token-2022 = "3.0.2"
5253

0 commit comments

Comments
 (0)