Skip to content

Commit a3ccab3

Browse files
committed
solana: populate an address lookup table to reduce tx size
1 parent 637c03e commit a3ccab3

File tree

9 files changed

+346
-13
lines changed

9 files changed

+346
-13
lines changed

ci_tests/src/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,15 @@ async function initSolana(
330330
});
331331
console.log("Initialized ntt at", manager.program.programId.toString());
332332

333+
// NOTE: this is a hack. The next instruction will fail if we don't wait
334+
// here, because the address lookup table is not yet available, despite
335+
// the transaction having been confirmed.
336+
// Looks like a bug, but I haven't investigated further. In practice, this
337+
// won't be an issue, becase the address lookup table will have been
338+
// created well before anyone is trying to use it, but we might want to be
339+
// mindful in the deploy script too.
340+
await new Promise((resolve) => setTimeout(resolve, 400));
341+
333342
await manager.registerTransceiver({
334343
payer: SOL_PRIVATE_KEY,
335344
owner: SOL_PRIVATE_KEY,

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

solana/programs/example-native-token-transfers/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ bitmaps = "3.2.1"
3838
hex.workspace = true
3939
cfg-if.workspace = true
4040
solana-program.workspace = true
41+
solana-address-lookup-table-program.workspace = true
4142
spl-token-2022 = { workspace = true, features = ["no-entrypoint"] }
4243
wormhole-anchor-sdk.workspace = true
4344
wormhole-io.workspace = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
//! This instructions manages a canonical address lookup table (or LUT) for the
2+
//! NTT program.
3+
//! LUTs in general can be created permissionlessly, so support from the
4+
//! program's side is not strictly necessary. When submitting a transaction, the
5+
//! client could just manage its own ad-hoc lookup table.
6+
//! Nevertheless, we provide this instruction to make it easier for the client
7+
//! to query the lookup table from a deterministic address, and for integrators
8+
//! to be able to fetch the accounts from the LUT in a standardised way.
9+
//!
10+
//! This way, the client sdk can abstract away the lookup table logic in a
11+
//! maintanable way.
12+
//!
13+
//! The [`initialize_lut`] instruction can be called multiple times, each time
14+
//! it will create a new lookup table, with the accounts defined in the
15+
//! [`Entries`] struct.
16+
//! An alternative would be to keep extending the existing lookup table, but
17+
//! ensuring the instruction is idempotent (which requires ensuring no duplicate
18+
//! entries) has O(n^2) complexity (since LUTs are append only, we can't keep it
19+
//! sorted), and in the worst case would require ~16k checks. So we keep things
20+
//! simple, and just create a new LUT each time. This operation won't be called
21+
//! often, so the extra allocation is justifiable.
22+
//!
23+
//! Because of all the above, this instruction can be called permissionlessly.
24+
25+
use anchor_lang::prelude::*;
26+
use solana_address_lookup_table_program;
27+
use solana_program::program::{invoke, invoke_signed};
28+
29+
use crate::{config::Config, queue::outbox::OutboxRateLimit, transceivers::wormhole::accounts::*};
30+
31+
#[account]
32+
#[derive(InitSpace)]
33+
pub struct LUT {
34+
pub bump: u8,
35+
pub address: Pubkey,
36+
}
37+
38+
#[derive(Accounts)]
39+
#[instruction(recent_slot: u64)]
40+
pub struct InitializeLUT<'info> {
41+
#[account(mut)]
42+
pub payer: Signer<'info>,
43+
44+
#[account(
45+
seeds = [b"lut_authority"],
46+
bump
47+
)]
48+
pub authority: AccountInfo<'info>,
49+
50+
#[account(
51+
mut,
52+
seeds = [authority.key().as_ref(), &recent_slot.to_le_bytes()],
53+
seeds::program = solana_address_lookup_table_program::id(),
54+
bump
55+
)]
56+
pub lut_address: AccountInfo<'info>,
57+
58+
#[account(
59+
init_if_needed,
60+
payer = payer,
61+
space = 8 + LUT::INIT_SPACE,
62+
seeds = [b"lut"],
63+
bump
64+
)]
65+
pub lut: Account<'info, LUT>,
66+
67+
/// CHECK: address lookup table program (checked by instruction)
68+
#[account(executable)]
69+
pub lut_program: AccountInfo<'info>,
70+
71+
pub system_program: Program<'info, System>,
72+
73+
/// These are the entries that will populate the LUT.
74+
pub entries: Entries<'info>,
75+
}
76+
77+
#[derive(Accounts)]
78+
pub struct Entries<'info> {
79+
pub config: Account<'info, Config>,
80+
81+
#[account(
82+
constraint = custody.key() == config.custody,
83+
)]
84+
pub custody: AccountInfo<'info>,
85+
86+
#[account(
87+
constraint = token_program.key() == config.token_program,
88+
)]
89+
pub token_program: AccountInfo<'info>,
90+
91+
#[account(
92+
constraint = mint.key() == config.mint,
93+
)]
94+
pub mint: AccountInfo<'info>,
95+
96+
#[account(
97+
seeds = [crate::TOKEN_AUTHORITY_SEED],
98+
bump,
99+
)]
100+
pub token_authority: AccountInfo<'info>,
101+
102+
pub outbox_rate_limit: Account<'info, OutboxRateLimit>,
103+
104+
// NOTE: this includes the system program so we don't need to add it in the outer context
105+
pub wormhole: WormholeAccounts<'info>,
106+
}
107+
108+
pub fn initialize_lut(ctx: Context<InitializeLUT>, recent_slot: u64) -> Result<()> {
109+
let (ix, lut_address) = solana_address_lookup_table_program::instruction::create_lookup_table(
110+
ctx.accounts.authority.key(),
111+
ctx.accounts.payer.key(),
112+
recent_slot,
113+
);
114+
115+
// just a sanity check, should never be hit, so we don't provide a custom
116+
// error message
117+
assert_eq!(lut_address, ctx.accounts.lut_address.key());
118+
119+
// the LUT might already exist, in which case the new one will simply
120+
// override it. Since we don't delete the old LUTs, this is safe -- clients
121+
// holding references to old LUTs will still be able to use them.
122+
ctx.accounts.lut.set_inner(LUT {
123+
bump: ctx.bumps.lut,
124+
address: lut_address,
125+
});
126+
127+
// NOTE: LUTs can be permissionlessly created (i.e. the authority does
128+
// not need to sign the transaction). This means that the LUT might
129+
// already exist (if someone frontran us). However, it's not a problem:
130+
// AddressLookupTable::create_lookup_table checks if the LUT already
131+
// exists and does nothing if it does.
132+
//
133+
// LUTs can only be created permissionlessly, but only the authority is
134+
// authorised to actually populate the fields, so we don't have to worry
135+
// about the frontrunner populating it with junk. The only risk of that would
136+
// be the LUT being filled to capacity (256 addresses), with no
137+
// possibility for us to add our own accounts -- no other security impact.
138+
invoke(
139+
&ix,
140+
&[
141+
ctx.accounts.lut_address.to_account_info(),
142+
ctx.accounts.authority.to_account_info(),
143+
ctx.accounts.payer.to_account_info(),
144+
ctx.accounts.system_program.to_account_info(),
145+
],
146+
)?;
147+
148+
let entries_infos = ctx.accounts.entries.to_account_infos();
149+
let mut entries = Vec::with_capacity(1 + entries_infos.len());
150+
entries.push(crate::ID);
151+
entries.extend(entries_infos.into_iter().map(|x| x.key));
152+
153+
let ix = solana_address_lookup_table_program::instruction::extend_lookup_table(
154+
ctx.accounts.lut_address.key(),
155+
ctx.accounts.authority.key(),
156+
Some(ctx.accounts.payer.key()),
157+
entries,
158+
);
159+
160+
invoke_signed(
161+
&ix,
162+
&[
163+
ctx.accounts.lut_address.to_account_info(),
164+
ctx.accounts.authority.to_account_info(),
165+
ctx.accounts.payer.to_account_info(),
166+
ctx.accounts.system_program.to_account_info(),
167+
],
168+
&[&[b"lut_authority", &[ctx.bumps.authority]]],
169+
)?;
170+
171+
Ok(())
172+
}
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
pub mod admin;
22
pub mod initialize;
3+
pub mod luts;
34
pub mod redeem;
45
pub mod release_inbound;
56
pub mod transfer;
67

78
pub use admin::*;
89
pub use initialize::*;
10+
pub use luts::*;
911
pub use redeem::*;
1012
pub use release_inbound::*;
1113
pub use transfer::*;

solana/programs/example-native-token-transfers/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ pub mod example_native_token_transfers {
7373
instructions::initialize(ctx, args)
7474
}
7575

76+
pub fn initialize_lut(ctx: Context<InitializeLUT>, recent_slot: u64) -> Result<()> {
77+
instructions::initialize_lut(ctx, recent_slot)
78+
}
79+
7680
pub fn version(_ctx: Context<Version>) -> Result<String> {
7781
Ok(VERSION.to_string())
7882
}

solana/tests/example-native-token-transfer.ts

+9
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,15 @@ describe("example-native-token-transfers", () => {
176176
mode: "burning",
177177
});
178178

179+
// NOTE: this is a hack. The next instruction will fail if we don't wait
180+
// here, because the address lookup table is not yet available, despite
181+
// the transaction having been confirmed.
182+
// Looks like a bug, but I haven't investigated further. In practice, this
183+
// won't be an issue, becase the address lookup table will have been
184+
// created well before anyone is trying to use it, but we might want to be
185+
// mindful in the deploy script too.
186+
await new Promise((resolve) => setTimeout(resolve, 200));
187+
179188
await ntt.registerTransceiver({
180189
payer,
181190
owner: payer,

0 commit comments

Comments
 (0)