Setting up rust tests (#292)

* Add tests

* Add tests to CI

* Remote test from precommit hook

* New test

* Remove merge error

* Comments and increase seqno

* Remove unnecesary dep

* Fix rebase

* Fix feedback
This commit is contained in:
guibescos 2022-09-21 12:51:57 -05:00 committed by GitHub
parent 1ad419e0ba
commit c32f2d99b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 3896 additions and 115 deletions

File diff suppressed because it is too large Load Diff

View File

@ -23,3 +23,9 @@ anchor-lang = {version = "0.25.0", features = ["init-if-needed"]}
wormhole-solana = { git = "https://github.com/guibescos/wormhole", branch = "gbescos/sdk-solana"}
wormhole-core = { git = "https://github.com/guibescos/wormhole", branch = "gbescos/sdk-solana"}
boolinator = "2.4.0"
[dev-dependencies]
solana-program-test = "=1.10.31"
tokio = "1.14.1"
solana-sdk = "=1.10.31"
bincode = "1.3.3"

View File

@ -4,6 +4,7 @@
use anchor_lang::{
prelude::*,
solana_program::borsh::get_packed_len,
system_program,
};
use state::{
claim_record::ClaimRecord,
@ -13,6 +14,9 @@ use state::{
mod error;
mod state;
#[cfg(test)] //Conditional compilation of the tests
mod tests;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
@ -78,3 +82,30 @@ pub struct ExecutePostedVaa<'info> {
pub claim_record: Account<'info, ClaimRecord>,
pub system_program: Program<'info, System>,
}
impl crate::accounts::ExecutePostedVaa {
pub fn populate(
program_id: &Pubkey,
payer: &Pubkey,
emitter: &Pubkey,
posted_vaa: &Pubkey,
) -> Self {
let executor_key = Pubkey::find_program_address(
&[EXECUTOR_KEY_SEED.as_bytes(), &emitter.to_bytes()],
program_id,
)
.0;
let claim_record = Pubkey::find_program_address(
&[CLAIM_RECORD_SEED.as_bytes(), &emitter.to_bytes()],
program_id,
)
.0;
crate::accounts::ExecutePostedVaa {
payer: *payer,
executor_key,
claim_record,
posted_vaa: *posted_vaa,
system_program: system_program::ID,
}
}
}

View File

@ -46,6 +46,20 @@ pub struct GovernanceHeader {
pub chain: BigEndianU16,
}
impl GovernanceHeader {
#[allow(unused)] // Only used in tests right now
pub fn executor_governance_header() -> Self {
Self {
magic_number: MAGIC_NUMBER,
module: Module::Executor,
action: Action::ExecutePostedVaa,
chain: BigEndianU16 {
value: Chain::Pythnet.try_into().unwrap(),
},
}
}
}
/// Hack to get Borsh to deserialize, serialize this number with big endian order
#[derive(Eq, PartialEq, Debug)]
pub struct BigEndianU16 {
@ -162,32 +176,18 @@ pub mod tests {
state::governance_payload::InstructionData,
};
use super::{
Action,
BigEndianU16,
ExecutorPayload,
Module,
MAGIC_NUMBER,
};
use super::ExecutorPayload;
use anchor_lang::{
prelude::Pubkey,
AnchorDeserialize,
AnchorSerialize,
};
use wormhole::Chain;
#[test]
fn test_check_deserialization_serialization() {
// No instructions
let payload = ExecutorPayload {
header: super::GovernanceHeader {
magic_number: MAGIC_NUMBER,
module: Module::Executor,
action: Action::ExecutePostedVaa,
chain: BigEndianU16 {
value: Chain::Pythnet.try_into().unwrap(),
},
},
header: super::GovernanceHeader::executor_governance_header(),
instructions: vec![],
};
@ -202,14 +202,8 @@ pub mod tests {
// One instruction
let payload = ExecutorPayload {
header: super::GovernanceHeader {
magic_number: MAGIC_NUMBER,
module: Module::Executor,
action: Action::ExecutePostedVaa,
chain: BigEndianU16 {
value: Chain::Pythnet.try_into().unwrap(),
},
},
header: super::GovernanceHeader::executor_governance_header(),
instructions: vec![InstructionData::from(
&anchor_lang::solana_program::system_instruction::create_account(
&Pubkey::new_unique(),

View File

@ -0,0 +1,226 @@
use std::collections::HashMap;
use anchor_lang::{
prelude::{
Pubkey,
Rent,
UpgradeableLoaderState,
},
solana_program::hash::Hash,
AnchorSerialize,
InstructionData as AnchorInstructionData,
Key,
Owner,
ToAccountMetas,
};
use solana_program_test::{
find_file,
read_file,
BanksClient,
BanksClientError,
ProgramTest,
ProgramTestBanksClientExt,
};
use solana_sdk::{
account::Account,
bpf_loader_upgradeable,
instruction::Instruction,
signature::Keypair,
signer::Signer,
stake_history::Epoch,
system_instruction,
transaction::Transaction,
};
use wormhole::Chain;
use wormhole_solana::VAA;
use crate::state::{
governance_payload::{
ExecutorPayload,
GovernanceHeader,
InstructionData,
},
posted_vaa::AnchorVaa,
};
/// Bench for the tests, the goal of this struct is to be able to setup solana accounts before starting the local validator
pub struct ExecutorBench {
program_test: ProgramTest,
program_id: Pubkey,
seqno: HashMap<Pubkey, u64>,
}
impl ExecutorBench {
/// Deploys the executor program as upgradable
pub fn new() -> ExecutorBench {
let mut bpf_data = read_file(find_file("remote_executor.so").unwrap_or_else(|| {
panic!("Unable to locate {}", "remote_executor.so");
}));
let mut program_test = ProgramTest::default();
let program_key = crate::id();
let programdata_key = Pubkey::new_unique();
let upgrade_authority_keypair = Keypair::new();
let program_deserialized = UpgradeableLoaderState::Program {
programdata_address: programdata_key,
};
let programdata_deserialized = UpgradeableLoaderState::ProgramData {
slot: 1,
upgrade_authority_address: Some(upgrade_authority_keypair.pubkey()),
};
// Program contains a pointer to progradata
let program_vec = bincode::serialize(&program_deserialized).unwrap();
// Programdata contains a header and the binary of the program
let mut programdata_vec = bincode::serialize(&programdata_deserialized).unwrap();
programdata_vec.append(&mut bpf_data);
let program_account = Account {
lamports: Rent::default().minimum_balance(program_vec.len()),
data: program_vec,
owner: bpf_loader_upgradeable::ID,
executable: true,
rent_epoch: Epoch::default(),
};
let programdata_account = Account {
lamports: Rent::default().minimum_balance(programdata_vec.len()),
data: programdata_vec,
owner: bpf_loader_upgradeable::ID,
executable: false,
rent_epoch: Epoch::default(),
};
// Add both accounts to program test, now the program is deployed as upgradable
program_test.add_account(program_key, program_account);
program_test.add_account(programdata_key, programdata_account);
return ExecutorBench {
program_test,
program_id: program_key.key(),
seqno: HashMap::<Pubkey, u64>::new(),
};
}
/// Start local validator based on the current bench
pub async fn start(self) -> ExecutorSimulator {
// Start validator
let (banks_client, genesis_keypair, recent_blockhash) = self.program_test.start().await;
return ExecutorSimulator {
banks_client,
payer: genesis_keypair,
last_blockhash: recent_blockhash,
program_id: self.program_id,
};
}
/// Add VAA account with emitter and instructions for consumption by the remote_executor
pub fn add_vaa_account(&mut self, emitter: &Pubkey, instructions: &Vec<Instruction>) -> Pubkey {
let payload = ExecutorPayload {
header: GovernanceHeader::executor_governance_header(),
instructions: instructions
.iter()
.map(|x| InstructionData::from(x))
.collect(),
};
let payload_bytes = payload.try_to_vec().unwrap();
let vaa = VAA {
vaa_version: 0,
consistency_level: 0,
vaa_time: 0,
vaa_signature_account: Pubkey::new_unique(),
submission_time: 0,
nonce: 0,
sequence: self.seqno.get(&emitter).unwrap_or(&0) + 1,
emitter_chain: Chain::Solana.into(),
emitter_address: emitter.to_bytes(),
payload: payload_bytes,
};
*self.seqno.entry(*emitter).or_insert(0) += 1;
let vaa_bytes = vaa.try_to_vec().unwrap();
let vaa_account = Account {
lamports: Rent::default().minimum_balance(vaa_bytes.len()),
data: vaa_bytes,
owner: AnchorVaa::owner(),
executable: false,
rent_epoch: Epoch::default(),
};
let vaa_pubkey = Pubkey::new_unique();
self.program_test.add_account(vaa_pubkey, vaa_account);
return vaa_pubkey;
}
}
pub struct ExecutorSimulator {
banks_client: BanksClient,
payer: Keypair,
last_blockhash: Hash,
program_id: Pubkey,
}
impl ExecutorSimulator {
#[allow(dead_code)]
pub async fn airdrop(&mut self, to: &Pubkey, lamports: u64) -> Result<(), BanksClientError> {
let instruction = system_instruction::transfer(&self.payer.pubkey(), to, lamports);
self.process_ix(instruction, &vec![]).await
}
/// Process a transaction containing `instruction` signed by `signers`.
/// `payer` is used to pay for and sign the transaction.
async fn process_ix(
&mut self,
instruction: Instruction,
signers: &Vec<&Keypair>,
) -> Result<(), BanksClientError> {
let mut transaction =
Transaction::new_with_payer(&[instruction], Some(&self.payer.pubkey()));
let blockhash = self
.banks_client
.get_new_latest_blockhash(&self.last_blockhash)
.await
.unwrap();
self.last_blockhash = blockhash;
transaction.partial_sign(&[&self.payer], self.last_blockhash);
transaction.partial_sign(signers, self.last_blockhash);
self.banks_client.process_transaction(transaction).await
}
/// Execute the payload contained in the VAA at posted_vaa_address
pub async fn execute_posted_vaa(
&mut self,
posted_vaa_address: &Pubkey,
) -> Result<(), BanksClientError> {
let posted_vaa_data: VAA = self
.banks_client
.get_account_data_with_borsh(*posted_vaa_address)
.await
.unwrap();
let account_metas = crate::accounts::ExecutePostedVaa::populate(
&self.program_id,
&self.payer.pubkey(),
&Pubkey::new(&posted_vaa_data.emitter_address),
&posted_vaa_address,
)
.to_account_metas(None);
let instruction = Instruction {
program_id: self.program_id,
accounts: account_metas,
data: crate::instruction::ExecutePostedVaa.data(),
};
self.process_ix(instruction, &vec![]).await
}
}

View File

@ -0,0 +1,2 @@
mod executor_simulator;
mod test_create_account;

View File

@ -0,0 +1,16 @@
use super::executor_simulator::ExecutorBench;
use anchor_lang::prelude::Pubkey;
#[tokio::test]
async fn test_create_account() {
let mut bench = ExecutorBench::new();
let emitter = Pubkey::new_unique();
let instructions = vec![];
let vaa_account = bench.add_vaa_account(&emitter, &instructions);
let mut sim = bench.start().await;
sim.execute_posted_vaa(&vaa_account).await.unwrap();
}