Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions core/crates/gem_solana/src/builder/transaction/accounts.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use std::collections::HashMap;

use crate::{AccountMeta, CompiledInstruction, Instruction, MessageHeader, Pubkey, Result, SolanaError};

pub(super) const MAX_ACCOUNT_KEYS: usize = u8::MAX as usize + 1;
use crate::{AccountMeta, CompiledInstruction, Instruction, MessageHeader, Pubkey, Result, SolanaError, types::MAX_ACCOUNT_KEYS};

#[derive(Default)]
pub(crate) struct AccountBuckets {
Expand Down
89 changes: 42 additions & 47 deletions core/crates/gem_solana/src/builder/transaction/builder.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::{collections::HashSet, iter::once};

use crate::{AccountMeta, AddressLookupTableAccount, Instruction, Message, Pubkey, Result, SignatureBytes, SolanaError, VersionedMessageV0, VersionedTransaction};
use crate::{AccountMeta, AddressLookupTableAccount, Instruction, Message, Pubkey, Result, SignatureBytes, SolanaError, VersionedMessageV0, VersionedTransaction, instructions::system::is_advance_nonce_account, types::MAX_ACCOUNT_KEYS};

use super::{
accounts::{AccountBuckets, MAX_ACCOUNT_KEYS, collect_accounts, compile_instructions, index_accounts},
accounts::{AccountBuckets, collect_accounts, compile_instructions, index_accounts},
lookup::{LoadedAccounts, lookup_locations},
};

Expand Down Expand Up @@ -42,13 +42,14 @@ impl TransactionBuilder {
pub fn build_v0(self, address_lookup_tables: &[AddressLookupTableAccount]) -> Result<VersionedTransaction> {
let lookup_locations = lookup_locations(address_lookup_tables);
let program_ids = self.instructions.iter().map(|instruction| instruction.program_id).collect::<HashSet<_>>();
let nonce_account = durable_nonce_account(&self.instructions);
let accounts = collect_accounts(self.fee_payer, program_first_accounts(&self.instructions));
let mut static_accounts = AccountBuckets::default();
let mut loaded_accounts = LoadedAccounts::new(address_lookup_tables.len());

for account in accounts {
match lookup_locations.get(&account.pubkey).copied() {
Some(location) if !account.is_signer && !program_ids.contains(&account.pubkey) => loaded_accounts.push(account, location),
Some(location) if !account.is_signer && !program_ids.contains(&account.pubkey) && Some(account.pubkey) != nonce_account => loaded_accounts.push(account, location),
_ => static_accounts.push(account),
}
}
Expand Down Expand Up @@ -90,6 +91,11 @@ impl TransactionBuilder {
}
}

fn durable_nonce_account(instructions: &[Instruction]) -> Option<Pubkey> {
let instruction = instructions.first().filter(|instruction| is_advance_nonce_account(instruction))?;
instruction.accounts.first().map(|account| account.pubkey)
}

fn program_first_accounts(instructions: &[Instruction]) -> impl Iterator<Item = AccountMeta> + '_ {
instructions.iter().flat_map(|instruction| once(AccountMeta::new_readonly(instruction.program_id)).chain(instruction.accounts.iter().cloned()))
}
Expand Down Expand Up @@ -119,14 +125,14 @@ mod tests {

use super::TransactionBuilder;
use crate::{
AccountMeta, AddressLookupTableAccount, CompiledInstruction, Instruction, Message, MessageAddressTableLookup, MessageHeader, Pubkey, SignatureBytes, SolanaError, VersionedMessageV0, VersionedTransaction,
AccountMeta, AddressLookupTableAccount, CompiledInstruction, Instruction, MessageAddressTableLookup, Pubkey, SignatureBytes, SolanaError, VersionedTransaction,
builder::InstructionBuilder,
instructions::{
program_ids::{system_program, token_program},
system::transfer,
system::{ADVANCE_NONCE_ACCOUNT_DISCRIMINANT, transfer},
token::transfer_checked,
},
testkit::TEST_BLOCKHASH,
testkit::{TEST_BLOCKHASH, mock_v0_transaction},
};

#[test]
Expand Down Expand Up @@ -188,28 +194,32 @@ mod tests {

let mut data = 2u32.to_le_bytes().to_vec();
data.extend_from_slice(&123u64.to_le_bytes());
assert_eq!(parsed, mock_v0_transaction(vec![fee_payer, recipient, system_program()], vec![CompiledInstruction::mock(2, vec![0, 1], data)], vec![],));
}

#[test]
fn test_versioned_transaction_builder_keeps_the_nonce_account_static() {
let fee_payer = Pubkey::mock(1);
let nonce_account = Pubkey::mock(2);
let looked_up_account = Pubkey::mock(3);
let advance_nonce = InstructionBuilder::new(system_program())
.account(nonce_account, false, true)
.account(looked_up_account, false, false)
.data(ADVANCE_NONCE_ACCOUNT_DISCRIMINANT.to_vec())
.build();
let lookup_table = AddressLookupTableAccount::new(Pubkey::mock(4), vec![nonce_account, looked_up_account]);

let mut builder = TransactionBuilder::new(fee_payer, TEST_BLOCKHASH);
builder.add_instruction(advance_nonce);
let transaction = builder.build_v0(&[lookup_table]).unwrap();

assert_eq!(
parsed,
VersionedTransaction::V0 {
signatures: vec![SignatureBytes::default()],
message: VersionedMessageV0 {
message: Message {
header: MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1
},
account_keys: vec![fee_payer, recipient, system_program()],
recent_blockhash: TEST_BLOCKHASH,
instructions: vec![CompiledInstruction {
program_id_index: 2,
accounts: vec![0, 1],
data
}],
},
address_table_lookups: vec![],
},
}
transaction,
mock_v0_transaction(
vec![fee_payer, nonce_account, system_program()],
vec![CompiledInstruction::mock(2, vec![1, 3], ADVANCE_NONCE_ACCOUNT_DISCRIMINANT.to_vec())],
vec![MessageAddressTableLookup::new(Pubkey::mock(4), vec![], vec![1])],
)
);
}

Expand All @@ -232,26 +242,11 @@ mod tests {

assert_eq!(
parsed,
VersionedTransaction::V0 {
signatures: vec![SignatureBytes::default()],
message: VersionedMessageV0 {
message: Message {
header: MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1
},
account_keys: vec![fee_payer, program_id],
recent_blockhash: TEST_BLOCKHASH,
instructions: vec![CompiledInstruction {
program_id_index: 1,
accounts: vec![0, 2],
data: vec![1, 2, 3]
}],
},
address_table_lookups: vec![MessageAddressTableLookup::new(Pubkey::mock(4), vec![0], vec![])],
},
}
mock_v0_transaction(
vec![fee_payer, program_id],
vec![CompiledInstruction::mock(1, vec![0, 2], vec![1, 2, 3])],
vec![MessageAddressTableLookup::new(Pubkey::mock(4), vec![0], vec![])],
)
);
}

Expand Down
25 changes: 22 additions & 3 deletions core/crates/gem_solana/src/builder/transaction/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::HashMap;

use crate::{AccountMeta, AddressLookupTableAccount, MessageAddressTableLookup, Pubkey};

use super::accounts::MAX_ACCOUNT_KEYS;
use crate::types::MAX_ACCOUNT_KEYS;

pub(super) struct LoadedAccounts {
writable: Vec<Vec<(Pubkey, u8)>>,
Expand Down Expand Up @@ -45,10 +45,29 @@ impl LoadedAccounts {

pub(super) fn lookup_locations(address_lookup_tables: &[AddressLookupTableAccount]) -> HashMap<Pubkey, (usize, u8)> {
let mut locations = HashMap::new();
for (table_index, table) in address_lookup_tables.iter().enumerate().rev() {
for (table_index, table) in address_lookup_tables.iter().enumerate() {
for (entry_index, address) in table.addresses.iter().take(MAX_ACCOUNT_KEYS).enumerate() {
locations.insert(*address, (table_index, entry_index as u8));
locations.entry(*address).or_insert((table_index, entry_index as u8));
}
}
locations
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_lookup_locations() {
let duplicated = Pubkey::mock(1);
let shared = Pubkey::mock(2);
let first_table = AddressLookupTableAccount::new(Pubkey::mock(10), vec![duplicated, shared, duplicated]);
let second_table = AddressLookupTableAccount::new(Pubkey::mock(11), vec![Pubkey::mock(3), shared]);

let locations = lookup_locations(&[first_table, second_table]);

assert_eq!(locations.get(&duplicated), Some(&(0, 0)));
assert_eq!(locations.get(&shared), Some(&(0, 1)));
assert_eq!(locations.get(&Pubkey::mock(3)), Some(&(1, 0)));
}
}
14 changes: 10 additions & 4 deletions core/crates/gem_solana/src/instructions/associated_token.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::{
Result,
instructions::program_ids::{associated_token_program, rent_sysvar, system_program},
instructions::program_ids::{associated_token_program, system_program},
types::{AccountMeta, Instruction, Pubkey, find_program_address},
};

const CREATE_DISCRIMINANT: u8 = 0;
const CREATE_IDEMPOTENT_DISCRIMINANT: u8 = 1;

pub fn create_associated_token_account_idempotent(payer: &Pubkey, wallet: &Pubkey, mint: &Pubkey, token_program: &Pubkey) -> Result<Instruction> {
Expand All @@ -21,12 +22,15 @@ pub fn create_associated_token_account_idempotent_with_address(payer: &Pubkey, a
AccountMeta::new_readonly(*mint),
AccountMeta::new_readonly(system_program()),
AccountMeta::new_readonly(*token_program),
AccountMeta::new_readonly(rent_sysvar()),
],
data: vec![CREATE_IDEMPOTENT_DISCRIMINANT],
}
}

pub fn is_create_account_data(data: &[u8]) -> bool {
data.is_empty() || data == [CREATE_DISCRIMINANT] || data == [CREATE_IDEMPOTENT_DISCRIMINANT]
}

pub fn get_associated_token_address_with_program_id(wallet: &Pubkey, mint: &Pubkey, token_program: &Pubkey) -> Result<Pubkey> {
let seeds = [wallet.as_bytes().as_slice(), token_program.as_bytes().as_slice(), mint.as_bytes().as_slice()];
find_program_address(&associated_token_program(), &seeds).map(|(address, _)| address)
Expand All @@ -49,8 +53,10 @@ mod tests {

let instruction = create_associated_token_account_idempotent(&payer, &wallet, &mint, &token_program()).unwrap();
assert_eq!(instruction.program_id, associated_token_program());
assert_eq!(instruction.accounts[1].pubkey, token_address);
assert_eq!(instruction.accounts[5].pubkey, token_program());
assert_eq!(
instruction.accounts.iter().map(|account| account.pubkey).collect::<Vec<_>>(),
vec![payer, token_address, wallet, mint, system_program(), token_program()]
);
assert_eq!(instruction.data, [CREATE_IDEMPOTENT_DISCRIMINANT]);
}
}
59 changes: 43 additions & 16 deletions core/crates/gem_solana/src/instructions/compute_budget.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use hex_lit::hex;

use crate::{
instructions::program_ids::{compute_budget_program, system_program},
instructions::{program_ids::compute_budget_program, system::is_advance_nonce_account},
types::Instruction,
};

pub const SET_COMPUTE_UNIT_LIMIT_DISCRIMINANT: u8 = 2;
pub const SET_COMPUTE_UNIT_PRICE_DISCRIMINANT: u8 = 3;
const ADVANCE_NONCE_ACCOUNT_DISCRIMINANT: [u8; 4] = hex!("04000000");

pub fn set_compute_unit_price(micro_lamports: u64) -> Instruction {
let mut data = Vec::with_capacity(9);
Expand All @@ -33,18 +30,29 @@ pub fn set_compute_unit_limit(units: u32) -> Instruction {

pub fn parse_compute_unit_limit_data(data: &[u8]) -> Option<u32> {
let bytes = data.strip_prefix(&[SET_COMPUTE_UNIT_LIMIT_DISCRIMINANT])?;
u32::from_le_bytes(bytes.try_into().ok()?).into()
Some(u32::from_le_bytes(<[u8; 4]>::try_from(bytes.get(..4)?).ok()?))
}

pub fn parse_compute_unit_price_data(data: &[u8]) -> Option<u64> {
let bytes = data.strip_prefix(&[SET_COMPUTE_UNIT_PRICE_DISCRIMINANT])?;
u64::from_le_bytes(bytes.try_into().ok()?).into()
Some(u64::from_le_bytes(<[u8; 8]>::try_from(bytes.get(..8)?).ok()?))
}

pub(crate) fn find_unique_compute_unit_limit<'a>(instruction_data: impl Iterator<Item = &'a [u8]>) -> Option<u32> {
unique(instruction_data.filter_map(parse_compute_unit_limit_data))
}

pub(crate) fn find_unique_compute_unit_price<'a>(instruction_data: impl Iterator<Item = &'a [u8]>) -> Option<u64> {
unique(instruction_data.filter_map(parse_compute_unit_price_data))
}

fn unique<T>(mut values: impl Iterator<Item = T>) -> Option<T> {
let value = values.next()?;
values.next().is_none().then_some(value)
}

pub fn get_compute_unit_limit(instructions: &[Instruction]) -> Option<u32> {
instructions
.iter()
.find_map(|instruction| (instruction.program_id == compute_budget_program()).then(|| parse_compute_unit_limit_data(&instruction.data)).flatten())
find_unique_compute_unit_limit(instructions.iter().filter(|instruction| instruction.program_id == compute_budget_program()).map(|instruction| instruction.data.as_slice()))
}

pub fn ensure_compute_unit_price(instructions: &mut Vec<Instruction>, micro_lamports: u64) -> bool {
Expand All @@ -55,32 +63,51 @@ pub fn ensure_compute_unit_price(instructions: &mut Vec<Instruction>, micro_lamp
return false;
}

let insertion_index = usize::from(
instructions
.first()
.is_some_and(|instruction| instruction.program_id == system_program() && instruction.data.get(0..4) == Some(&ADVANCE_NONCE_ACCOUNT_DISCRIMINANT)),
);
let insertion_index = usize::from(instructions.first().is_some_and(is_advance_nonce_account));
instructions.insert(insertion_index, set_compute_unit_price(micro_lamports));
true
}

#[cfg(test)]
mod tests {
use hex_lit::hex;

use super::*;
use crate::Pubkey;
use crate::instructions::system::transfer;
use crate::instructions::program_ids::system_program;
use crate::instructions::system::{ADVANCE_NONCE_ACCOUNT_DISCRIMINANT, transfer};

#[test]
fn test_compute_budget_wire_formats_and_parsing() {
let limit = set_compute_unit_limit(200_000);
assert_eq!(limit.data, hex!("02400d0300"));
assert_eq!(parse_compute_unit_limit_data(&limit.data), Some(200_000));
assert_eq!(parse_compute_unit_limit_data(&hex!("02400d0300ff")), Some(200_000));
assert_eq!(parse_compute_unit_limit_data(&[2, 1]), None);

let price = set_compute_unit_price(1_000);
assert_eq!(price.data, hex!("03e803000000000000"));
assert_eq!(parse_compute_unit_price_data(&price.data), Some(1_000));
assert_eq!(get_compute_unit_limit(&[price, limit]), Some(200_000));
assert_eq!(parse_compute_unit_price_data(&hex!("03e803000000000000ff")), Some(1_000));
assert_eq!(parse_compute_unit_price_data(&[3, 1]), None);
}

#[test]
fn test_get_compute_unit_limit() {
let limit = set_compute_unit_limit(200_000);
let price = set_compute_unit_price(1_000);

assert_eq!(get_compute_unit_limit(&[price.clone(), limit.clone()]), Some(200_000));
assert_eq!(get_compute_unit_limit(&[limit.clone(), set_compute_unit_limit(300_000)]), None);
assert_eq!(get_compute_unit_limit(&[price]), None);
assert_eq!(
get_compute_unit_limit(&[Instruction {
program_id: system_program(),
accounts: vec![],
data: limit.data,
}]),
None
);
}

#[test]
Expand Down
1 change: 0 additions & 1 deletion core/crates/gem_solana/src/instructions/program_ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@ program_id!(token_2022_program, "06ddf6e1ee758fde18425dbce46ccddab61afc4d83b90d2
program_id!(associated_token_program, "8c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859");
program_id!(memo_program, "054a535a992921064d24e87160da387c7c35b5ddbc92bb81e41fa8404105448d");
program_id!(compute_budget_program, "0306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a40000000");
program_id!(rent_sysvar, "06a7d517192c5c51218cc94c3d4af17f58daee089ba1fd44e3dbd98a00000000");
9 changes: 9 additions & 0 deletions core/crates/gem_solana/src/instructions/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ use crate::{
};

const TRANSFER_DISCRIMINANT: [u8; 4] = hex!("02000000");
pub const ADVANCE_NONCE_ACCOUNT_DISCRIMINANT: [u8; 4] = hex!("04000000");

pub fn is_advance_nonce_account_data(data: &[u8]) -> bool {
data.get(..4) == Some(ADVANCE_NONCE_ACCOUNT_DISCRIMINANT.as_slice())
}

pub fn is_advance_nonce_account(instruction: &Instruction) -> bool {
instruction.program_id == system_program() && is_advance_nonce_account_data(&instruction.data)
}

pub fn transfer(from: &Pubkey, to: &Pubkey, lamports: u64) -> Instruction {
let mut data = Vec::with_capacity(12);
Expand Down
3 changes: 2 additions & 1 deletion core/crates/gem_solana/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ pub use transaction::{
try_decode_transaction,
};
pub use types::{
AccountMeta, AddressLookupTableAccount, CompiledInstruction, Instruction, MAX_TRANSACTION_SIZE, Message, MessageAddressTableLookup, MessageHeader, Pubkey, SignatureBytes, VersionedMessageV0, VersionedTransaction, find_program_address,
AccountMeta, AddressLookupTableAccount, CompiledInstruction, Instruction, MAX_TRANSACTION_SIZE, Message, MessageAddressTableLookup, MessageHeader, Pubkey, SignatureBytes, TransactionConfig, VersionedMessageV0, VersionedMessageV1,
VersionedTransaction, find_program_address,
};

pub(crate) use error::Result;
Expand Down
Loading
Loading