Migrating testing to lib svm (#28)

* Using libSVM for simulation

* Some more fixes for invalid mint authority

* avoiding disabled lst mints

* performance improvements, reduced log-levels

* adding a todo for token extention program
This commit is contained in:
galactus 2024-10-25 17:02:05 +02:00 committed by GitHub
parent 6324916be7
commit a4dc9f19fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 226 additions and 173 deletions

24
Cargo.lock generated
View File

@ -3865,6 +3865,26 @@ version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "litesvm"
version = "0.1.0"
source = "git+https://github.com/blockworks-foundation/litesvm.git?branch=v0.1.0+solana_1.7#4c884606289e484090be42a08b79dc3bc5f449f7"
dependencies = [
"bincode",
"indexmap 2.2.6",
"itertools 0.12.1",
"log 0.4.21",
"solana-address-lookup-table-program",
"solana-bpf-loader-program",
"solana-compute-budget-program",
"solana-loader-v4-program",
"solana-program",
"solana-program-runtime",
"solana-sdk",
"solana-system-program",
"thiserror",
]
[[package]]
name = "lock_api"
version = "0.3.4"
@ -6763,11 +6783,13 @@ dependencies = [
"bincode",
"bonfida-test-utils",
"env_logger",
"litesvm",
"log 0.4.21",
"router-test-lib",
"sha2 0.10.8",
"solana-address-lookup-table-program",
"solana-program",
"solana-program-runtime",
"solana-program-test",
"solana-sdk",
"spl-associated-token-account 1.1.3",
@ -6775,6 +6797,8 @@ dependencies = [
"spl-token-2022 1.0.0",
"test-case",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]

View File

@ -68,6 +68,14 @@ impl DexInterface for InfinityDex {
let lst_mint = lst_data.sol_val_calc.lst_mint();
let account_metas = lst_data.sol_val_calc.ix_accounts();
let num_accounts_for_tx = account_metas.len();
let Ok((lst_state, lst_data)) = amm.find_ready_lst(lst_mint) else {
continue;
};
if lst_state.is_input_disabled != 0 {
continue;
}
for pk in lst_data.sol_val_calc.get_accounts_to_update() {
let edges = vec![
Arc::new(InfinityEdgeIdentifier {

View File

@ -126,7 +126,7 @@ pub async fn run_dump_mainnet_data_with_custom_amount(
}
let accounts = rpc_client.get_multiple_accounts(&accounts_needed).await?;
for (_, account) in accounts {
for (_pk, account) in accounts {
// get buffer for upgradable programs
if account.owner == solana_sdk::bpf_loader_upgradeable::ID {
let state = bincode::deserialize::<UpgradeableLoaderState>(&account.data).unwrap();
@ -260,7 +260,7 @@ pub async fn run_dump_swap_ix_with_custom_amount(
dump.accounts
.insert(id.input_mint(), account.account.clone());
let account = chain_data_reader
.account(&id.input_mint())
.account(&id.output_mint())
.expect("missing mint");
dump.accounts
.insert(id.output_mint(), account.account.clone());
@ -307,9 +307,6 @@ pub async fn run_dump_swap_ix_with_custom_amount(
instruction: bincode::serialize(&swap_exact_out_ix.instruction).unwrap(),
is_exact_out: true,
});
// add exact out accounts
let chain_data_reader = chain_data.read().unwrap();
for account in swap_exact_out_ix.instruction.accounts {
if let Ok(acc) = chain_data_reader.account(&account.pubkey) {
dump.accounts.insert(account.pubkey, acc.account.clone());
@ -351,9 +348,9 @@ pub async fn run_dump_swap_ix_with_custom_amount(
debug!("program : {program:?}");
}
for (pk, program) in &dump.accounts {
for (pk, account_data) in &dump.accounts {
let mut hasher = Sha256::new();
hasher.update(program.data());
hasher.update(account_data.data());
let result = hasher.finalize();
let base64 = base64::encode(result);
debug!("account : {pk:?} dump : {base64:?}");

View File

@ -23,6 +23,7 @@ test-case = "*"
tokio = "1.37.0"
solana-address-lookup-table-program = "1.17"
solana-program-test = "1.17"
solana-program-runtime = "1.17"
solana-sdk = "1.17"
spl-token = { version = "^3.0.0", features = ["no-entrypoint"] }
spl-token-2022 = { version = "1.0.0", features = ["no-entrypoint"] }
@ -33,6 +34,9 @@ env_logger = "0.9.0"
bincode = "1.3.3"
sha2 = "0.10.8"
base64 = "0.12.3"
litesvm = { git = "https://github.com/blockworks-foundation/litesvm.git", branch = "v0.1.0+solana_1.7" }
tracing = "0.1.37"
tracing-subscriber = "0.3.16"
[profile.test]
inherits = "release"

View File

@ -1,30 +1,30 @@
use anyhow::{Context, Error};
use bonfida_test_utils::error::TestError;
use bonfida_test_utils::ProgramTestContextExt;
use log::{debug, error, info, warn};
use anyhow::Error;
use litesvm::LiteSVM;
use log::{error, info, warn};
use router_test_lib::execution_dump::{ExecutionDump, ExecutionItem};
use router_test_lib::{execution_dump, serialize};
use solana_program::clock::{Clock, Epoch};
use sha2::Digest;
use sha2::Sha256;
use solana_program::clock::Clock;
use solana_program::instruction::Instruction;
use solana_program::program_pack::Pack;
use solana_program::program_stubs::{set_syscall_stubs, SyscallStubs};
use solana_program::pubkey::Pubkey;
use solana_program::sysvar::SysvarId;
use solana_program_test::BanksClientError;
use solana_program_test::{ProgramTest, ProgramTestContext};
use solana_sdk::account::{Account, AccountSharedData, ReadableAccount};
use solana_sdk::epoch_info::EpochInfo;
use solana_sdk::bpf_loader_upgradeable::UpgradeableLoaderState;
use solana_sdk::message::{Message, VersionedMessage};
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer;
use solana_sdk::transaction::Transaction;
use spl_associated_token_account::get_associated_token_address;
use solana_sdk::transaction::VersionedTransaction;
use spl_associated_token_account::{
get_associated_token_address, get_associated_token_address_with_program_id,
};
use spl_token::state::AccountState;
use spl_token_2022::state::AccountState as AccountState2022;
use std::collections::HashMap;
use std::process::exit;
use std::path::PathBuf;
use std::str::FromStr;
// use sha2::Sha256;
// use sha2::Digest;
struct TestLogSyscallStubs;
impl SyscallStubs for TestLogSyscallStubs {
@ -73,13 +73,15 @@ async fn test_quote_match_swap_for_infinity() -> anyhow::Result<()> {
}
async fn run_all_swap_from_dump(dump_name: &str) -> Result<Result<(), Error>, Error> {
tracing_subscriber::fmt::init();
let mut skip_count = option_env!("SKIP_COUNT")
.map(|x| u32::from_str(x).unwrap_or(0))
.unwrap_or(0);
let mut stop_at = u32::MAX;
let skip_ixs_index = vec![];
let mut run_lot_size = option_env!("RUN_LOT_SIZE")
let run_lot_size = option_env!("RUN_LOT_SIZE")
.map(|x| u32::from_str(x).unwrap_or(500))
.unwrap_or(500);
@ -126,7 +128,7 @@ async fn run_all_swap_from_dump(dump_name: &str) -> Result<Result<(), Error>, Er
let instruction = deserialize_instruction(&quote.instruction)?;
let programs = data.programs.iter().copied().collect();
let mut ctx = setup_test_chain(&programs, &clock, &data).await;
let mut ctx = setup_test_chain(&programs, &clock, &data, &instruction)?;
create_wallet(&mut ctx, wallet.pubkey());
@ -145,28 +147,31 @@ async fn run_all_swap_from_dump(dump_name: &str) -> Result<Result<(), Error>, Er
quote.input_mint,
initial_in_balance,
input_mint_is_2022,
)
.await?;
)?;
set_balance(
&mut ctx,
wallet.pubkey(),
quote.output_mint,
initial_out_balance,
output_mint_is_2022,
)
.await?;
)?;
for meta in &instruction.accounts {
let Ok(Some(account)) = ctx.banks_client.get_account(meta.pubkey).await else {
let Some(account) = ctx.get_account(&meta.pubkey) else {
log::warn!("missing account : {:?}", meta.pubkey);
continue;
};
// keep code to test hashses
// let mut hasher = Sha256::new();
// hasher.update(account.data());
// let result = hasher.finalize();
// let base64 = base64::encode(result);
// log::debug!("account : {:?} dump : {base64:?} executable : {}", meta.pubkey, account.executable());
let mut hasher = Sha256::new();
hasher.update(account.data());
let result = hasher.finalize();
let base64 = base64::encode(result);
log::debug!(
"account : {:?} dump : {base64:?} executable : {}",
meta.pubkey,
account.executable()
);
}
if let Some(cus) = simulate_cu_usage(&mut ctx, &wallet, &instruction).await {
@ -278,7 +283,7 @@ async fn debug_print_ix(
success: &mut i32,
index: &mut u32,
quote: &ExecutionItem,
ctx: &mut ProgramTestContext,
ctx: &mut LiteSVM,
instruction: &Instruction,
input_mint_is_2022: bool,
output_mint_is_2022: bool,
@ -310,13 +315,8 @@ async fn debug_print_ix(
for acc in &instruction.accounts {
let account = ctx
.banks_client
.get_account(acc.pubkey)
.await
.map(|x| {
x.map(|y| (y.executable, y.owner.to_string()))
.unwrap_or((false, "???".to_string()))
})
.get_account(&acc.pubkey)
.map(|x| (x.executable, x.owner.to_string()))
.unwrap_or((false, "???".to_string()));
warn!(
@ -341,14 +341,53 @@ fn deserialize_instruction(swap_ix: &Vec<u8>) -> anyhow::Result<Instruction> {
Ok(instruction)
}
async fn initialize_accounts(
program_test: &mut ProgramTest,
fn initialize_accounts(
program_test: &mut LiteSVM,
dump: &ExecutionDump,
accounts_list: &Vec<Pubkey>,
) -> anyhow::Result<()> {
println!("initializing accounts : {:?}", dump.accounts.len());
for (pk, account) in &dump.accounts {
println!("Setting data for {}", pk);
program_test.add_account(
log::debug!("initializing accounts : {:?}", dump.accounts.len());
for pk in accounts_list {
let Some(account) = dump.accounts.get(pk) else {
continue;
};
if *account.owner() == solana_sdk::bpf_loader_upgradeable::ID {
log::debug!("{pk:?} has upgradable loader");
let state = bincode::deserialize::<UpgradeableLoaderState>(&account.data()).unwrap();
if let UpgradeableLoaderState::Program {
programdata_address,
} = state
{
// load buffer accounts first
match dump.accounts.get(&programdata_address) {
Some(program_buffer) => {
log::debug!("loading buffer: {programdata_address:?}");
program_test.set_account(
programdata_address,
solana_sdk::account::Account {
lamports: program_buffer.lamports(),
owner: *program_buffer.owner(),
data: program_buffer.data().to_vec(),
rent_epoch: program_buffer.rent_epoch(),
executable: program_buffer.executable(),
},
)?;
}
None => {
error!("{programdata_address:?} is not there");
}
}
}
}
log::debug!(
"Setting data for {} with owner {} and is executable {}",
pk,
account.owner(),
account.executable()
);
log::debug!("Setting data for {}", pk);
program_test.set_account(
*pk,
solana_sdk::account::Account {
lamports: account.lamports(),
@ -357,110 +396,109 @@ async fn initialize_accounts(
rent_epoch: account.rent_epoch(),
executable: account.executable(),
},
);
)?;
}
Ok(())
}
async fn simulate_cu_usage(
ctx: &mut ProgramTestContext,
ctx: &mut LiteSVM,
owner: &Keypair,
instruction: &Instruction,
) -> Option<u64> {
let mut transaction =
Transaction::new_with_payer(&[instruction.clone()], Some(&ctx.payer.pubkey()));
let tx = VersionedTransaction::try_new(
VersionedMessage::Legacy(Message::new(&[instruction.clone()], Some(&owner.pubkey()))),
&[owner],
)
.unwrap();
transaction.sign(&[&ctx.payer, owner], ctx.last_blockhash);
let sim = ctx
.banks_client
.simulate_transaction(transaction.clone())
.await;
let sim = ctx.simulate_transaction(tx);
match sim {
Ok(sim) => {
log::debug!("{:?}", sim.result);
let simulation_details = sim.simulation_details.unwrap();
let cus = simulation_details.units_consumed;
if sim.result.is_some() && sim.result.clone().unwrap().is_ok() {
log::debug!("units consumed : {}", cus);
let cus = sim.compute_units_consumed;
log::debug!("----logs");
for log in sim.logs {
log::debug!("{log:?}");
}
if cus > 0 {
Some(cus)
} else if sim.result.is_some() && sim.result.clone().unwrap().is_err() {
log::debug!("simluation failed : {:?}", sim.result.unwrap());
log::debug!("----logs");
for log in simulation_details.logs {
log::debug!("{log:?}");
}
None
} else {
None
}
}
Err(e) => {
log::warn!("Error simulating : {}", e);
log::warn!("Error simulating : {:?}", e);
None
}
}
}
async fn swap(
ctx: &mut ProgramTestContext,
owner: &Keypair,
instruction: &Instruction,
) -> anyhow::Result<()> {
ctx.get_new_latest_blockhash().await?;
log::info!("swapping");
let result = ctx
.sign_send_instructions(&[instruction.clone()], &[&owner])
.await;
async fn swap(ctx: &mut LiteSVM, owner: &Keypair, instruction: &Instruction) -> anyhow::Result<()> {
let tx = VersionedTransaction::try_new(
VersionedMessage::Legacy(Message::new(&[instruction.clone()], Some(&owner.pubkey()))),
&[owner],
)
.unwrap();
let result = ctx.send_transaction(tx);
match result {
Ok(()) => Ok(()),
Err(e) => Err(anyhow::format_err!("Failed to swap {:?}", e)),
Ok(_) => Ok(()),
Err(e) => {
log::error!("------------- LOGS ------------------");
for log in &e.meta.logs {
log::error!("{log:?}");
}
Err(anyhow::format_err!("Failed to swap {:?}", e.err))
}
}
}
async fn get_balance(
ctx: &mut ProgramTestContext,
ctx: &mut LiteSVM,
owner: Pubkey,
mint: Pubkey,
is_2022: bool,
) -> anyhow::Result<u64> {
let ata_address = get_associated_token_address(&owner, &mint);
let Some(ata) = ctx.get_account(&ata_address) else {
return Ok(0);
};
if is_2022 {
let Ok(ata) = ctx.banks_client.get_account(ata_address).await else {
return Ok(0);
};
let Some(ata) = ata else {
return Ok(0);
};
let ata = spl_token_2022::state::Account::unpack(&ata.data);
if let Ok(ata) = ata {
return Ok(ata.amount);
}
};
if let Ok(ata) = ctx.get_token_account(ata_address).await {
if let Ok(ata) = spl_token::state::Account::unpack(&ata.data) {
Ok(ata.amount)
} else {
Ok(0u64)
}
}
async fn set_balance(
ctx: &mut ProgramTestContext,
fn set_balance(
ctx: &mut LiteSVM,
owner: Pubkey,
mint: Pubkey,
amount: u64,
is_2022: bool,
) -> anyhow::Result<()> {
let ata_address = get_associated_token_address(&owner, &mint);
let token_program_id = if is_2022 {
spl_token_2022::ID
} else {
spl_token::ID
};
let ata_address =
get_associated_token_address_with_program_id(&owner, &mint, &token_program_id);
let mut data = vec![0u8; 165];
if is_2022 {
let mut data = vec![0u8; 165];
// TODO: to properly setup extensions, this is not sufficient
let account = spl_token_2022::state::Account {
mint,
owner,
@ -472,101 +510,83 @@ async fn set_balance(
close_authority: Default::default(),
};
account.pack_into_slice(data.as_mut_slice());
ctx.set_account(
&ata_address,
&AccountSharedData::from(Account {
lamports: 1_000_000_000,
data: data,
owner: spl_token_2022::ID,
executable: false,
rent_epoch: 0,
}),
);
return Ok(());
}
let mut data = vec![0u8; 165];
let account = spl_token::state::Account {
mint,
owner,
amount,
delegate: Default::default(),
state: AccountState::Initialized,
is_native: Default::default(),
delegated_amount: 0,
close_authority: Default::default(),
} else {
let account = spl_token::state::Account {
mint,
owner,
amount,
delegate: Default::default(),
state: AccountState::Initialized,
is_native: Default::default(),
delegated_amount: 0,
close_authority: Default::default(),
};
account.pack_into_slice(data.as_mut_slice());
};
account.pack_into_slice(data.as_mut_slice());
ctx.set_account(
&ata_address,
&AccountSharedData::from(Account {
ata_address,
Account {
lamports: 1_000_000_000,
data: data,
owner: spl_token::ID,
owner: token_program_id,
executable: false,
rent_epoch: 0,
}),
);
rent_epoch: u64::MAX,
},
)?;
Ok(())
}
fn create_wallet(ctx: &mut ProgramTestContext, address: Pubkey) {
ctx.set_account(
&address,
&AccountSharedData::from(Account {
lamports: 1_000_000_000,
data: vec![],
owner: address,
executable: false,
rent_epoch: 0,
}),
);
fn create_wallet(ctx: &mut LiteSVM, address: Pubkey) {
let _ = ctx.airdrop(&address, 1_000_000_000);
}
async fn setup_test_chain(
pub fn find_file(filename: &str) -> Option<PathBuf> {
for dir in default_shared_object_dirs() {
let candidate = dir.join(filename);
if candidate.exists() {
return Some(candidate);
}
}
None
}
fn default_shared_object_dirs() -> Vec<PathBuf> {
let mut search_path = vec![];
if let Ok(bpf_out_dir) = std::env::var("BPF_OUT_DIR") {
search_path.push(PathBuf::from(bpf_out_dir));
} else if let Ok(bpf_out_dir) = std::env::var("SBF_OUT_DIR") {
search_path.push(PathBuf::from(bpf_out_dir));
}
search_path.push(PathBuf::from("tests/fixtures"));
if let Ok(dir) = std::env::current_dir() {
search_path.push(dir);
}
log::trace!("SBF .so search path: {:?}", search_path);
search_path
}
fn setup_test_chain(
programs: &Vec<Pubkey>,
clock: &Clock,
dump: &ExecutionDump,
) -> ProgramTestContext {
// We need to intercept logs to capture program log output
let log_filter = "solana_rbpf=trace,\
solana_runtime::message_processor=debug,\
solana_runtime::system_instruction_processor=trace,\
solana_program_test=info,\
solana_metrics::metrics=warn,\
tarpc=error,\
info";
let env_logger =
env_logger::Builder::from_env(env_logger::Env::new().default_filter_or(log_filter))
.format_timestamp_nanos()
.build();
let _ = log::set_boxed_logger(Box::new(env_logger));
instruction: &Instruction,
) -> anyhow::Result<LiteSVM> {
let mut program_test = LiteSVM::new();
program_test.set_sysvar(clock);
let mut accounts_list = programs.clone();
accounts_list.extend(instruction.accounts.iter().map(|x| x.pubkey));
let mut program_test = ProgramTest::default();
initialize_accounts(&mut program_test, dump).await.unwrap();
program_test.prefer_bpf(true);
for &key in programs {
program_test.add_program(key.to_string().as_str(), key, None);
}
program_test.add_program("autobahn_executor", autobahn_executor::ID, None);
initialize_accounts(&mut program_test, dump, &accounts_list)?;
let path = find_file(format!("autobahn_executor.so").as_str()).unwrap();
log::debug!("Adding program: {:?} at {path:?}", autobahn_executor::ID);
program_test.add_program_from_file(autobahn_executor::ID, path)?;
// TODO: make this dynamic based on routes
program_test.set_compute_max_units(1_400_000);
let mut cb = solana_program_runtime::compute_budget::ComputeBudget::default();
cb.compute_unit_limit = 1_400_000;
program_test.set_compute_budget(cb);
let mut program_test_context = program_test.start_with_context().await;
// Set clock
program_test_context.set_sysvar(clock);
info!("Setting clock to: {}", clock.unix_timestamp);
program_test_context.warp_to_slot(40).unwrap();
program_test_context
Ok(program_test)
}