Skip to content

Commit

Permalink
solana: populate an address lookup table to reduce tx size
Browse files Browse the repository at this point in the history
  • Loading branch information
kcsongor committed Apr 25, 2024
1 parent 0495172 commit e7eca16
Show file tree
Hide file tree
Showing 9 changed files with 346 additions and 13 deletions.
9 changes: 9 additions & 0 deletions sdk/__tests__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,15 @@ async function deploySolana(ctx: Ctx): Promise<Ctx> {
await signSendWait(ctx.context, initTxs, signer);
console.log("Initialized ntt at", manager.program.programId.toString());

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

const registrTxs = manager.registerTransceiver({
payer: keypair,
owner: keypair,
Expand Down
1 change: 1 addition & 0 deletions solana/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions solana/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ anchor-spl = "0.29.0"
solana-program = "=1.18.10"
solana-program-runtime = "=1.18.10"
solana-program-test = "=1.18.10"
solana-address-lookup-table-program = "=1.18.10"
spl-token = "4.0.0"
spl-token-2022 = "3.0.2"

Expand Down
1 change: 1 addition & 0 deletions solana/programs/example-native-token-transfers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ bitmaps = "3.2.1"
hex.workspace = true
cfg-if.workspace = true
solana-program.workspace = true
solana-address-lookup-table-program.workspace = true
spl-token-2022 = { workspace = true, features = ["no-entrypoint"] }
wormhole-anchor-sdk.workspace = true
wormhole-io.workspace = true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//! This instructions manages a canonical address lookup table (or LUT) for the
//! NTT program.
//! LUTs in general can be created permissionlessly, so support from the
//! program's side is not strictly necessary. When submitting a transaction, the
//! client could just manage its own ad-hoc lookup table.
//! Nevertheless, we provide this instruction to make it easier for the client
//! to query the lookup table from a deterministic address, and for integrators
//! to be able to fetch the accounts from the LUT in a standardised way.
//!
//! This way, the client sdk can abstract away the lookup table logic in a
//! maintanable way.
//!
//! The [`initialize_lut`] instruction can be called multiple times, each time
//! it will create a new lookup table, with the accounts defined in the
//! [`Entries`] struct.
//! An alternative would be to keep extending the existing lookup table, but
//! ensuring the instruction is idempotent (which requires ensuring no duplicate
//! entries) has O(n^2) complexity (since LUTs are append only, we can't keep it
//! sorted), and in the worst case would require ~16k checks. So we keep things
//! simple, and just create a new LUT each time. This operation won't be called
//! often, so the extra allocation is justifiable.
//!
//! Because of all the above, this instruction can be called permissionlessly.

use anchor_lang::prelude::*;
use solana_address_lookup_table_program;
use solana_program::program::{invoke, invoke_signed};

use crate::{config::Config, queue::outbox::OutboxRateLimit, transceivers::wormhole::accounts::*};

#[account]
#[derive(InitSpace)]
pub struct LUT {
pub bump: u8,
pub address: Pubkey,
}

#[derive(Accounts)]
#[instruction(recent_slot: u64)]
pub struct InitializeLUT<'info> {
#[account(mut)]
pub payer: Signer<'info>,

#[account(
seeds = [b"lut_authority"],
bump
)]
pub authority: AccountInfo<'info>,

#[account(
mut,
seeds = [authority.key().as_ref(), &recent_slot.to_le_bytes()],
seeds::program = solana_address_lookup_table_program::id(),
bump
)]
pub lut_address: AccountInfo<'info>,

#[account(
init_if_needed,
payer = payer,
space = 8 + LUT::INIT_SPACE,
seeds = [b"lut"],
bump
)]
pub lut: Account<'info, LUT>,

/// CHECK: address lookup table program (checked by instruction)
#[account(executable)]
pub lut_program: AccountInfo<'info>,

pub system_program: Program<'info, System>,

/// These are the entries that will populate the LUT.
pub entries: Entries<'info>,
}

#[derive(Accounts)]
pub struct Entries<'info> {
pub config: Account<'info, Config>,

#[account(
constraint = custody.key() == config.custody,
)]
pub custody: AccountInfo<'info>,

#[account(
constraint = token_program.key() == config.token_program,
)]
pub token_program: AccountInfo<'info>,

#[account(
constraint = mint.key() == config.mint,
)]
pub mint: AccountInfo<'info>,

#[account(
seeds = [crate::TOKEN_AUTHORITY_SEED],
bump,
)]
pub token_authority: AccountInfo<'info>,

pub outbox_rate_limit: Account<'info, OutboxRateLimit>,

// NOTE: this includes the system program so we don't need to add it in the outer context
pub wormhole: WormholeAccounts<'info>,
}

pub fn initialize_lut(ctx: Context<InitializeLUT>, recent_slot: u64) -> Result<()> {
let (ix, lut_address) = solana_address_lookup_table_program::instruction::create_lookup_table(
ctx.accounts.authority.key(),
ctx.accounts.payer.key(),
recent_slot,
);

// just a sanity check, should never be hit, so we don't provide a custom
// error message
assert_eq!(lut_address, ctx.accounts.lut_address.key());

// the LUT might already exist, in which case the new one will simply
// override it. Since we don't delete the old LUTs, this is safe -- clients
// holding references to old LUTs will still be able to use them.
ctx.accounts.lut.set_inner(LUT {
bump: ctx.bumps.lut,
address: lut_address,
});

// NOTE: LUTs can be permissionlessly created (i.e. the authority does
// not need to sign the transaction). This means that the LUT might
// already exist (if someone frontran us). However, it's not a problem:
// AddressLookupTable::create_lookup_table checks if the LUT already
// exists and does nothing if it does.
//
// LUTs can only be created permissionlessly, but only the authority is
// authorised to actually populate the fields, so we don't have to worry
// about the frontrunner populating it with junk. The only risk of that would
// be the LUT being filled to capacity (256 addresses), with no
// possibility for us to add our own accounts -- no other security impact.
invoke(
&ix,
&[
ctx.accounts.lut_address.to_account_info(),
ctx.accounts.authority.to_account_info(),
ctx.accounts.payer.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;

let entries_infos = ctx.accounts.entries.to_account_infos();
let mut entries = Vec::with_capacity(1 + entries_infos.len());
entries.push(crate::ID);
entries.extend(entries_infos.into_iter().map(|x| x.key));

let ix = solana_address_lookup_table_program::instruction::extend_lookup_table(
ctx.accounts.lut_address.key(),
ctx.accounts.authority.key(),
Some(ctx.accounts.payer.key()),
entries,
);

invoke_signed(
&ix,
&[
ctx.accounts.lut_address.to_account_info(),
ctx.accounts.authority.to_account_info(),
ctx.accounts.payer.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
&[&[b"lut_authority", &[ctx.bumps.authority]]],
)?;

Ok(())
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
pub mod admin;
pub mod initialize;
pub mod luts;
pub mod redeem;
pub mod release_inbound;
pub mod transfer;

pub use admin::*;
pub use initialize::*;
pub use luts::*;
pub use redeem::*;
pub use release_inbound::*;
pub use transfer::*;
4 changes: 4 additions & 0 deletions solana/programs/example-native-token-transfers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ pub mod example_native_token_transfers {
instructions::initialize(ctx, args)
}

pub fn initialize_lut(ctx: Context<InitializeLUT>, recent_slot: u64) -> Result<()> {
instructions::initialize_lut(ctx, recent_slot)
}

pub fn version(_ctx: Context<Version>) -> Result<String> {
Ok(VERSION.to_string())
}
Expand Down
9 changes: 9 additions & 0 deletions solana/tests/example-native-token-transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,15 @@ describe("example-native-token-transfers", () => {
mode: "burning",
});

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

await ntt.registerTransceiver({
payer,
owner: payer,
Expand Down
Loading

0 comments on commit e7eca16

Please sign in to comment.