From 36f42677a42b6144e4be3aa5635a780b5ef0c34b Mon Sep 17 00:00:00 2001 From: Vadim Date: Fri, 18 Nov 2022 16:30:50 +0200 Subject: [PATCH 01/19] Feat(programs): implement gas-station --- Cargo.lock | 25 +- Cargo.toml | 1 + evm-utils/programs/gas_station/Cargo.toml | 25 ++ evm-utils/programs/gas_station/src/error.rs | 30 ++ .../programs/gas_station/src/instruction.rs | 37 ++ evm-utils/programs/gas_station/src/lib.rs | 10 + .../programs/gas_station/src/processor.rs | 347 ++++++++++++++++++ evm-utils/programs/gas_station/src/state.rs | 32 ++ 8 files changed, 503 insertions(+), 4 deletions(-) create mode 100644 evm-utils/programs/gas_station/Cargo.toml create mode 100644 evm-utils/programs/gas_station/src/error.rs create mode 100644 evm-utils/programs/gas_station/src/instruction.rs create mode 100644 evm-utils/programs/gas_station/src/lib.rs create mode 100644 evm-utils/programs/gas_station/src/processor.rs create mode 100644 evm-utils/programs/gas_station/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 3b4fa504a3..0881ad3f3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1886,6 +1886,23 @@ dependencies = [ "serde", ] +[[package]] +name = "evm-gas-station" +version = "0.1.0" +dependencies = [ + "arrayref", + "borsh", + "evm-rpc", + "num-derive", + "num-traits", + "solana-evm-loader-program", + "solana-program 1.9.29", + "solana-program-test", + "solana-sdk", + "thiserror", + "tokio", +] + [[package]] name = "evm-gasometer" version = "0.35.0" @@ -8071,18 +8088,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.32" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.32" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2 1.0.43", "quote 1.0.21", diff --git a/Cargo.toml b/Cargo.toml index 6d78614472..440ab9be21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ members = [ "evm-utils/evm-block-recovery", "evm-utils/evm-bridge", "evm-utils/programs/evm_loader", + "evm-utils/programs/gas_station", "evm-utils/evm-state", "evm-utils/evm-rpc", "poh", diff --git a/evm-utils/programs/gas_station/Cargo.toml b/evm-utils/programs/gas_station/Cargo.toml new file mode 100644 index 0000000000..c8034aaf3a --- /dev/null +++ b/evm-utils/programs/gas_station/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "evm-gas-station" +version = "0.1.0" +description = "EVM gas-station" +license = "Apache-2.0" +edition = "2021" + +[dependencies] +arrayref = "0.3.6" +borsh = "0.9.3" +evm-rpc = { path = "../../evm-rpc" } +num-derive = "0.3.3" +num-traits = "0.2.15" +solana-evm-loader-program = { path = "../evm_loader" } +solana-program = { path = "../../../sdk/program", version = "=1.9.29" } +solana-sdk = { path = "../../../sdk", version = "=1.9.29" } +thiserror = "1.0.37" + +[dev-dependencies] +solana-program-test = { path = "../../../program-test", version = "=1.9.29" } +tokio = { version = "~1.14.1", features = ["full"] } + +[lib] +crate-type = ["lib", "cdylib"] +name = "evm_gas_station" \ No newline at end of file diff --git a/evm-utils/programs/gas_station/src/error.rs b/evm-utils/programs/gas_station/src/error.rs new file mode 100644 index 0000000000..02984d2ca8 --- /dev/null +++ b/evm-utils/programs/gas_station/src/error.rs @@ -0,0 +1,30 @@ +use num_derive::FromPrimitive; +use solana_sdk::program_error::ProgramError; +use thiserror::Error; + +#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +pub enum GasStationError { + /// The account cannot be initialized because it is already being used. + #[error("Account is already in use")] + AccountInUse, + #[error("Account storage isn't uninitialized")] + AccountNotInitialized, + #[error("Unable to deserialize borsh encoded account data")] + InvalidAccountBorshData, + #[error("Unable to deserialize big transaction account data")] + InvalidBigTransactionData, + #[error("Lamport balance below rent-exempt threshold")] + NotRentExempt, + #[error("Payer account doesn't match key from payer storage")] + PayerAccountMismatch, + #[error("None of payer filters correspond to evm transaction")] + PayerFilterMismatch, + #[error("PDA account info doesn't match DPA derived by this program id")] + PdaAccountMismatch, +} + +impl From for ProgramError { + fn from(e: GasStationError) -> Self { + ProgramError::Custom(e as u32) + } +} diff --git a/evm-utils/programs/gas_station/src/instruction.rs b/evm-utils/programs/gas_station/src/instruction.rs new file mode 100644 index 0000000000..d0ba3152ff --- /dev/null +++ b/evm-utils/programs/gas_station/src/instruction.rs @@ -0,0 +1,37 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_evm_loader_program::scope::evm; +use solana_sdk::pubkey::Pubkey; + +#[derive(Debug, BorshDeserialize, BorshSerialize)] +pub enum TxFilter { + InputStartsWith { + contract: evm::Address, + input_prefix: Vec, + }, +} + +impl TxFilter { + pub fn is_match(&self, tx: &evm::Transaction) -> bool { + match self { + Self::InputStartsWith{ contract, input_prefix } => { + matches!(tx.action, evm::TransactionAction::Call(addr) if addr == *contract) + && tx.input.starts_with(&input_prefix) + } + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub enum GasStationInstruction { + /// Register new payer + RegisterPayer { + owner: Pubkey, + transfer_amount: u64, + whitelist: Vec, + }, + + /// Execute evm transaction + ExecuteWithPayer { + tx: Option, + } +} \ No newline at end of file diff --git a/evm-utils/programs/gas_station/src/lib.rs b/evm-utils/programs/gas_station/src/lib.rs new file mode 100644 index 0000000000..da0b4aa4e0 --- /dev/null +++ b/evm-utils/programs/gas_station/src/lib.rs @@ -0,0 +1,10 @@ +mod error; +mod instruction; +mod processor; +mod state; + +use processor::process_instruction; +use solana_program::entrypoint; + +// Declare and export the program's entrypoint +entrypoint!(process_instruction); diff --git a/evm-utils/programs/gas_station/src/processor.rs b/evm-utils/programs/gas_station/src/processor.rs new file mode 100644 index 0000000000..68dba6ea28 --- /dev/null +++ b/evm-utils/programs/gas_station/src/processor.rs @@ -0,0 +1,347 @@ +use super::*; +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_evm_loader_program::scope::evm; +use solana_program::{program_memory::sol_memcmp, pubkey::PUBKEY_BYTES}; +use solana_sdk::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + instruction::{AccountMeta, Instruction}, + msg, + program::invoke_signed, + program_error::ProgramError, + program_pack::IsInitialized, + pubkey::Pubkey, + rent::Rent, + system_instruction, + sysvar::Sysvar, +}; + +use error::GasStationError; +use instruction::{GasStationInstruction, TxFilter}; +use state::{Payer, MAX_FILTERS}; + +const EXECUTE_CALL_REFUND_AMOUNT: u64 = 10000; + + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let ix = BorshDeserialize::deserialize(&mut &*instruction_data) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + match ix { + GasStationInstruction::RegisterPayer { + owner, + transfer_amount, + whitelist, + } => process_register_payer(program_id, accounts, owner, transfer_amount, whitelist), + GasStationInstruction::ExecuteWithPayer { tx } => { + process_execute_with_payer(program_id, accounts, tx) + } + } +} + +fn process_register_payer( + program_id: &Pubkey, + accounts: &[AccountInfo], + owner: Pubkey, + transfer_amount: u64, + whitelist: Vec, +) -> ProgramResult { + if whitelist.len() > MAX_FILTERS { + return Err(ProgramError::InvalidInstructionData); + } + let account_info_iter = &mut accounts.iter(); + let creator_info = next_account_info(account_info_iter)?; + let storage_acc_info = next_account_info(account_info_iter)?; + let payer_acc_info = next_account_info(account_info_iter)?; + let system_program = next_account_info(account_info_iter)?; + + let mut payer: Payer = BorshDeserialize::deserialize(&mut &**storage_acc_info.data.borrow()) + .map_err(|_e| -> ProgramError { GasStationError::InvalidAccountBorshData.into() })?; + if payer.is_initialized() { + return Err(GasStationError::AccountInUse.into()); + } + + let rent = Rent::get()?; + let payer_data_len = storage_acc_info.data_len(); + if !rent.is_exempt(storage_acc_info.lamports(), payer_data_len) { + return Err(ProgramError::AccountNotRentExempt); + } + + let (payer_acc, bump_seed) = Pubkey::find_program_address(&[owner.as_ref()], program_id); + let rent_lamports = rent.minimum_balance(0); + invoke_signed( + &system_instruction::create_account( + creator_info.key, + &payer_acc, + rent_lamports + transfer_amount, + 0, + program_id, + ), + &[ + creator_info.clone(), + payer_acc_info.clone(), + system_program.clone(), + ], + &[&[owner.as_ref(), &[bump_seed]]], + )?; + msg!("PDA created: {}", payer_acc); + + payer.owner = owner; + payer.payer = payer_acc; + payer.filters = whitelist; + BorshSerialize::serialize(&payer, &mut &mut storage_acc_info.data.borrow_mut()[..]).unwrap(); + Ok(()) +} + +fn process_execute_with_payer( + program_id: &Pubkey, + accounts: &[AccountInfo], + tx: Option, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let sender = next_account_info(account_info_iter)?; + let payer_storage_info = next_account_info(account_info_iter)?; + let payer_info = next_account_info(account_info_iter)?; + let evm_loader = next_account_info(account_info_iter)?; + let evm_state = next_account_info(account_info_iter)?; + let system_program = next_account_info(account_info_iter)?; + + let unpacked_tx = match tx.as_ref() { + None => { + let big_tx_storage_info = next_account_info(account_info_iter)?; + get_big_tx_from_storage(big_tx_storage_info)? + } + Some(tx) => tx.clone(), + }; + + if !cmp_pubkeys(program_id, payer_storage_info.owner) { + return Err(ProgramError::IncorrectProgramId); + } + let payer: Payer = BorshDeserialize::deserialize(&mut &**payer_storage_info.data.borrow()) + .map_err(|_e| -> ProgramError { GasStationError::InvalidAccountBorshData.into() })?; + if !payer.is_initialized() { + return Err(GasStationError::AccountNotInitialized.into()); + } + if payer.payer != *payer_info.key { + return Err(GasStationError::PayerAccountMismatch.into()); + } + if !payer.do_filter_match(&unpacked_tx) { + return Err(GasStationError::PayerFilterMismatch.into()); + } + + { + let (_payer_acc, bump_seed) = + Pubkey::find_program_address(&[payer.owner.as_ref()], program_id); + // pass sender acc to evm loader, execute tx restore ownership + payer_info.assign(&solana_sdk::evm_loader::ID); + + let ix = make_evm_loader_execute_ix(*evm_loader.key, *evm_state.key, *payer_info.key, tx); + let account_infos = vec![evm_loader.clone(), evm_state.clone(), payer_info.clone()]; + invoke_signed(&ix, &account_infos, &[&[payer.owner.as_ref(), &[bump_seed]]])?; + + let ix = solana_evm_loader_program::free_ownership(*payer_info.key); + let account_infos = vec![evm_loader.clone(), evm_state.clone(), payer_info.clone()]; + invoke_signed( + &ix, + &account_infos, + &[&[payer.owner.as_ref(), &[bump_seed]]], + )?; + + let ix = system_instruction::assign(payer_info.key, &program_id); + let account_infos = vec![system_program.clone(), payer_info.clone()]; + invoke_signed( + &ix, + &account_infos, + &[&[payer.owner.as_ref(), &[bump_seed]]], + )?; + } + + let refund_amount = EXECUTE_CALL_REFUND_AMOUNT; + refund_native_fee(sender, payer_info, refund_amount) +} + +pub fn cmp_pubkeys(a: &Pubkey, b: &Pubkey) -> bool { + sol_memcmp(a.as_ref(), b.as_ref(), PUBKEY_BYTES) == 0 +} + +fn get_big_tx_from_storage(storage_acc: &AccountInfo) -> Result { + let mut bytes: &[u8] = &storage_acc.try_borrow_data().unwrap(); + msg!("Trying to deserialize tx chunks byte = {:?}", bytes); + BorshDeserialize::deserialize(&mut bytes) + .map_err(|_e| GasStationError::InvalidBigTransactionData.into()) +} + +fn make_evm_loader_execute_ix( + evm_loader: Pubkey, + evm_state: Pubkey, + sender: Pubkey, + tx: Option, +) -> Instruction { + use solana_evm_loader_program::instructions::*; + solana_evm_loader_program::create_evm_instruction_with_borsh( + evm_loader, + &EvmInstruction::ExecuteTransaction { + tx: ExecuteTransaction::Signed { tx }, + fee_type: FeePayerType::Native, + }, + vec![ + AccountMeta::new(evm_state, false), + AccountMeta::new(sender, true), + ], + ) +} + +fn refund_native_fee(caller: &AccountInfo, payer: &AccountInfo, amount: u64) -> ProgramResult { + **payer.try_borrow_mut_lamports()? -= amount; + **caller.try_borrow_mut_lamports()? += amount; + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use solana_program_test::{processor, ProgramTest}; + use solana_sdk::transaction::Transaction; + use solana_sdk::{ + account::Account, + signature::{Keypair, Signer}, + system_program, + }; + + const SECRET_KEY_DUMMY: [u8; 32] = [1; 32]; + const TEST_CHAIN_ID: u64 = 0xdead; + + pub fn dummy_eth_tx() -> evm::Transaction { + evm::UnsignedTransaction { + nonce: evm::U256::zero(), + gas_price: evm::U256::zero(), + gas_limit: evm::U256::zero(), + action: evm::TransactionAction::Call(evm::H160::zero()), + value: evm::U256::zero(), + input: vec![], + } + .sign( + &evm::SecretKey::from_slice(&SECRET_KEY_DUMMY).unwrap(), + Some(TEST_CHAIN_ID), + ) + } + + #[ignore] + #[tokio::test] + async fn test_register_payer() { + let program_id = Pubkey::new_unique(); + + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + + let creator = Keypair::new(); + program_test.add_account( + creator.pubkey(), + Account { + lamports: 10000000, + ..Account::default() + }, + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + let (storage, _) = + Pubkey::find_program_address(&[creator.pubkey().as_ref(), &[0]], &program_id); + let (payer, _) = + Pubkey::find_program_address(&[creator.pubkey().as_ref(), &[1]], &program_id); + let account_metas = vec![ + AccountMeta::new(creator.pubkey(), true), + AccountMeta::new(storage, false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(solana_sdk::system_program::id(), false), + ]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::RegisterPayer { + owner: creator.pubkey(), + transfer_amount: 0, + whitelist: vec![TxFilter::InputStartsWith { contract: evm::Address::zero(), input_prefix: vec![] }], + }, + account_metas, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&creator.pubkey())); + transaction.sign(&[&creator], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + } + + #[tokio::test] + async fn test_execute() { + let program_id = Pubkey::new_unique(); + + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + + let alice = Keypair::new(); + let owner = Keypair::new(); + let storage = Keypair::new(); + let (payer, _) = Pubkey::find_program_address(&[owner.pubkey().as_ref()], &program_id); + program_test.add_account( + alice.pubkey(), + Account { + lamports: 1000000, + ..Account::default() + }, + ); + program_test.add_account( + payer, + Account { + lamports: 1000000, + owner: program_id, + ..Account::default() + }, + ); + program_test.add_account( + solana_sdk::evm_state::ID, + Account { + lamports: 1000000, + ..Account::default() + }, + ); + let payer_data = Payer { + owner: owner.pubkey(), + payer, + filters: vec![TxFilter::InputStartsWith { contract: evm::Address::zero(), input_prefix: vec![] }], + }; + let mut payer_bytes = vec![0u8; 93]; + BorshSerialize::serialize(&payer_data, &mut payer_bytes.as_mut_slice()).unwrap(); + program_test.add_account( + storage.pubkey(), + Account { + lamports: 10000000, + owner: program_id, + data: payer_bytes, + ..Account::default() + }, + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + let account_metas = vec![ + AccountMeta::new(alice.pubkey(), true), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { + tx: Some(dummy_eth_tx()), + }, + account_metas, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&alice.pubkey())); + transaction.sign(&[&alice], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + } +} diff --git a/evm-utils/programs/gas_station/src/state.rs b/evm-utils/programs/gas_station/src/state.rs new file mode 100644 index 0000000000..21c63e8583 --- /dev/null +++ b/evm-utils/programs/gas_station/src/state.rs @@ -0,0 +1,32 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_evm_loader_program::scope::evm; +use solana_sdk::{ + program_pack::IsInitialized, + pubkey::Pubkey, +}; +use crate::instruction::TxFilter; + +pub const MAX_FILTERS: usize = 5; + +#[repr(C)] +#[derive(BorshDeserialize, BorshSerialize, Debug)] +pub struct Payer { + /// The owner of this account. + pub owner: Pubkey, + /// Account that will pay for evm transaction + pub payer: Pubkey, + /// List of filters to define what transactions will be paid by this payer + pub filters: Vec, +} + +impl Payer { + pub fn do_filter_match(&self, tx: &evm::Transaction) -> bool { + self.filters.iter().any(|f| { f.is_match(tx) }) + } +} + +impl IsInitialized for Payer { + fn is_initialized(&self) -> bool { + !self.filters.is_empty() + } +} From 65cb293485e8d507fdc2d6d013715b7904e51026 Mon Sep 17 00:00:00 2001 From: Vadim Date: Thu, 24 Nov 2022 17:39:02 +0200 Subject: [PATCH 02/19] Feat(programs): implement gas-station tests + small fixes --- evm-utils/programs/gas_station/src/error.rs | 2 + .../programs/gas_station/src/processor.rs | 537 ++++++++++++++++-- evm-utils/programs/gas_station/src/state.rs | 2 +- 3 files changed, 492 insertions(+), 49 deletions(-) diff --git a/evm-utils/programs/gas_station/src/error.rs b/evm-utils/programs/gas_station/src/error.rs index 02984d2ca8..d8d45c1f96 100644 --- a/evm-utils/programs/gas_station/src/error.rs +++ b/evm-utils/programs/gas_station/src/error.rs @@ -13,6 +13,8 @@ pub enum GasStationError { InvalidAccountBorshData, #[error("Unable to deserialize big transaction account data")] InvalidBigTransactionData, + #[error("Invalid filter amount")] + InvalidFilterAmount, #[error("Lamport balance below rent-exempt threshold")] NotRentExempt, #[error("Payer account doesn't match key from payer storage")] diff --git a/evm-utils/programs/gas_station/src/processor.rs b/evm-utils/programs/gas_station/src/processor.rs index 68dba6ea28..82828e7ccd 100644 --- a/evm-utils/programs/gas_station/src/processor.rs +++ b/evm-utils/programs/gas_station/src/processor.rs @@ -17,12 +17,12 @@ use solana_sdk::{ }; use error::GasStationError; +use evm_rpc::Either; use instruction::{GasStationInstruction, TxFilter}; use state::{Payer, MAX_FILTERS}; const EXECUTE_CALL_REFUND_AMOUNT: u64 = 10000; - pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], @@ -50,8 +50,8 @@ fn process_register_payer( transfer_amount: u64, whitelist: Vec, ) -> ProgramResult { - if whitelist.len() > MAX_FILTERS { - return Err(ProgramError::InvalidInstructionData); + if whitelist.is_empty() || whitelist.len() > MAX_FILTERS { + return Err(GasStationError::InvalidFilterAmount.into()); } let account_info_iter = &mut accounts.iter(); let creator_info = next_account_info(account_info_iter)?; @@ -110,22 +110,26 @@ fn process_execute_with_payer( let evm_state = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; - let unpacked_tx = match tx.as_ref() { + let (unpacked_tx, big_tx_storage_info) = match tx.as_ref() { None => { let big_tx_storage_info = next_account_info(account_info_iter)?; - get_big_tx_from_storage(big_tx_storage_info)? + (get_big_tx_from_storage(big_tx_storage_info)?, Some(big_tx_storage_info)) } - Some(tx) => tx.clone(), + Some(tx) => (tx.clone(), None), }; if !cmp_pubkeys(program_id, payer_storage_info.owner) { return Err(ProgramError::IncorrectProgramId); } - let payer: Payer = BorshDeserialize::deserialize(&mut &**payer_storage_info.data.borrow()) + let mut payer_data_buf: &[u8] = &**payer_storage_info.data.borrow(); + let payer: Payer = BorshDeserialize::deserialize(&mut payer_data_buf) .map_err(|_e| -> ProgramError { GasStationError::InvalidAccountBorshData.into() })?; if !payer.is_initialized() { return Err(GasStationError::AccountNotInitialized.into()); } + if !payer_data_buf.is_empty() { + return Err(GasStationError::InvalidAccountBorshData.into()); + } if payer.payer != *payer_info.key { return Err(GasStationError::PayerAccountMismatch.into()); } @@ -136,28 +140,24 @@ fn process_execute_with_payer( { let (_payer_acc, bump_seed) = Pubkey::find_program_address(&[payer.owner.as_ref()], program_id); + let signers_seeds: &[&[&[u8]]] = &[&[payer.owner.as_ref(), &[bump_seed]]]; // pass sender acc to evm loader, execute tx restore ownership payer_info.assign(&solana_sdk::evm_loader::ID); - let ix = make_evm_loader_execute_ix(*evm_loader.key, *evm_state.key, *payer_info.key, tx); - let account_infos = vec![evm_loader.clone(), evm_state.clone(), payer_info.clone()]; - invoke_signed(&ix, &account_infos, &[&[payer.owner.as_ref(), &[bump_seed]]])?; + let ix = make_evm_loader_execute_ix(*evm_loader.key, *evm_state.key, *payer_info.key, tx.map_or_else(|| Either::Right(*big_tx_storage_info.unwrap().key), |tx| Either::Left(tx))); + let account_infos = match big_tx_storage_info { + Some(big_tx_storage_info) => vec![evm_loader.clone(), big_tx_storage_info.clone(), evm_state.clone(), payer_info.clone()], + None => vec![evm_loader.clone(), evm_state.clone(), payer_info.clone()], + }; + invoke_signed(&ix, &account_infos, signers_seeds)?; let ix = solana_evm_loader_program::free_ownership(*payer_info.key); let account_infos = vec![evm_loader.clone(), evm_state.clone(), payer_info.clone()]; - invoke_signed( - &ix, - &account_infos, - &[&[payer.owner.as_ref(), &[bump_seed]]], - )?; + invoke_signed(&ix, &account_infos, signers_seeds)?; let ix = system_instruction::assign(payer_info.key, &program_id); let account_infos = vec![system_program.clone(), payer_info.clone()]; - invoke_signed( - &ix, - &account_infos, - &[&[payer.owner.as_ref(), &[bump_seed]]], - )?; + invoke_signed(&ix, &account_infos, signers_seeds)?; } let refund_amount = EXECUTE_CALL_REFUND_AMOUNT; @@ -179,19 +179,27 @@ fn make_evm_loader_execute_ix( evm_loader: Pubkey, evm_state: Pubkey, sender: Pubkey, - tx: Option, + tx: Either, ) -> Instruction { use solana_evm_loader_program::instructions::*; + let (tx, accounts) = match tx { + Either::Left(tx) => (Some(tx), vec![ + AccountMeta::new(evm_state, false), + AccountMeta::new(sender, true), + ]), + Either::Right(big_tx_storage_key) => (None, vec![ + AccountMeta::new(evm_state, false), + AccountMeta::new(big_tx_storage_key, true), + AccountMeta::new(sender, true), + ]), + }; solana_evm_loader_program::create_evm_instruction_with_borsh( evm_loader, &EvmInstruction::ExecuteTransaction { tx: ExecuteTransaction::Signed { tx }, fee_type: FeePayerType::Native, }, - vec![ - AccountMeta::new(evm_state, false), - AccountMeta::new(sender, true), - ], + accounts, ) } @@ -204,30 +212,32 @@ fn refund_native_fee(caller: &AccountInfo, payer: &AccountInfo, amount: u64) -> #[cfg(test)] mod test { use super::*; + use solana_program::instruction::InstructionError::{Custom, IncorrectProgramId}; use solana_program_test::{processor, ProgramTest}; - use solana_sdk::transaction::Transaction; use solana_sdk::{ account::Account, signature::{Keypair, Signer}, system_program, + transaction::{Transaction, TransactionError::InstructionError}, + transport::TransportError::TransactionError, }; const SECRET_KEY_DUMMY: [u8; 32] = [1; 32]; const TEST_CHAIN_ID: u64 = 0xdead; - pub fn dummy_eth_tx() -> evm::Transaction { + pub fn dummy_eth_tx(contract: evm::H160, input: Vec) -> evm::Transaction { evm::UnsignedTransaction { nonce: evm::U256::zero(), gas_price: evm::U256::zero(), gas_limit: evm::U256::zero(), - action: evm::TransactionAction::Call(evm::H160::zero()), + action: evm::TransactionAction::Call(contract), value: evm::U256::zero(), - input: vec![], + input, } - .sign( - &evm::SecretKey::from_slice(&SECRET_KEY_DUMMY).unwrap(), - Some(TEST_CHAIN_ID), - ) + .sign( + &evm::SecretKey::from_slice(&SECRET_KEY_DUMMY).unwrap(), + Some(TEST_CHAIN_ID), + ) } #[ignore] @@ -264,7 +274,10 @@ mod test { &GasStationInstruction::RegisterPayer { owner: creator.pubkey(), transfer_amount: 0, - whitelist: vec![TxFilter::InputStartsWith { contract: evm::Address::zero(), input_prefix: vec![] }], + whitelist: vec![TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + }], }, account_metas, ); @@ -274,45 +287,465 @@ mod test { } #[tokio::test] - async fn test_execute() { + async fn test_execute_tx() { let program_id = Pubkey::new_unique(); + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + let user = Keypair::new(); + let owner = Keypair::new(); + let storage = Keypair::new(); + let (payer, _) = Pubkey::find_program_address(&[owner.pubkey().as_ref()], &program_id); + program_test.add_account( + user.pubkey(), + Account::new(1000000, 0, &system_program::id()), + ); + program_test.add_account(payer, Account::new(1000000, 0, &program_id)); + program_test.add_account( + solana_sdk::evm_state::ID, + solana_evm_loader_program::create_state_account(1000000).into(), + ); + let payer_data = Payer { + owner: owner.pubkey(), + payer, + filters: vec![TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + }], + }; + let mut payer_bytes = vec![]; + BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); + program_test.add_account( + storage.pubkey(), + Account { + lamports: 10000000, + owner: program_id, + data: payer_bytes, + ..Account::default() + }, + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + let account_metas = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { + tx: Some(dummy_eth_tx(evm::H160::zero(), vec![])), + }, + account_metas, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + transaction.sign(&[&user], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + } + + #[tokio::test] + async fn test_execute_big_tx() { + let program_id = Pubkey::new_unique(); let mut program_test = ProgramTest::new("gas-station", program_id, processor!(process_instruction)); - let alice = Keypair::new(); + let user = Keypair::new(); let owner = Keypair::new(); let storage = Keypair::new(); + let big_tx_storage = Keypair::new(); let (payer, _) = Pubkey::find_program_address(&[owner.pubkey().as_ref()], &program_id); program_test.add_account( - alice.pubkey(), + user.pubkey(), + Account::new(1000000, 0, &system_program::id()), + ); + program_test.add_account(payer, Account::new(1000000, 0, &program_id)); + program_test.add_account( + solana_sdk::evm_state::ID, + solana_evm_loader_program::create_state_account(1000000).into(), + ); + let payer_data = Payer { + owner: owner.pubkey(), + payer, + filters: vec![TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + }], + }; + let mut payer_bytes = vec![]; + BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); + program_test.add_account( + storage.pubkey(), Account { - lamports: 1000000, + lamports: 10000000, + owner: program_id, + data: payer_bytes, ..Account::default() }, ); + let big_tx = dummy_eth_tx(evm::H160::zero(), vec![0; 1000]); + let mut big_tx_bytes = vec![]; + BorshSerialize::serialize(&big_tx, &mut big_tx_bytes).unwrap(); program_test.add_account( + big_tx_storage.pubkey(), + Account { + lamports: 10000000, + owner: solana_evm_loader_program::ID, + data: big_tx_bytes, + ..Account::default() + }, + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + let account_metas = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new(big_tx_storage.pubkey(), true), + ]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { tx: None }, + account_metas, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + transaction.sign(&[&user, &big_tx_storage], recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + } + + #[tokio::test] + async fn test_invalid_storage_account_owner() { + let program_id = Pubkey::new_unique(); + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + + let user = Keypair::new(); + let owner = Keypair::new(); + let storage = Keypair::new(); + let (payer, _) = Pubkey::find_program_address(&[owner.pubkey().as_ref()], &program_id); + program_test.add_account( + user.pubkey(), + Account::new(1000000, 0, &system_program::id()), + ); + program_test.add_account(payer, Account::new(1000000, 0, &program_id)); + program_test.add_account( + solana_sdk::evm_state::ID, + solana_evm_loader_program::create_state_account(1000000).into(), + ); + let payer_data = Payer { + owner: owner.pubkey(), payer, + filters: vec![TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + }], + }; + let mut payer_bytes = vec![]; + BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); + program_test.add_account( + storage.pubkey(), Account { - lamports: 1000000, + lamports: 10000000, + owner: system_program::id(), + data: payer_bytes, + ..Account::default() + }, + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + let account_metas = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { + tx: Some(dummy_eth_tx(evm::H160::zero(), vec![])), + }, + account_metas, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + transaction.sign(&[&user], recent_blockhash); + assert!(matches!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err(), + TransactionError(InstructionError(0, IncorrectProgramId)) + )); + } + + #[tokio::test] + async fn test_invalid_storage_data() { + let program_id = Pubkey::new_unique(); + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + + let user = Keypair::new(); + let owner = Keypair::new(); + let storage1 = Keypair::new(); + let storage2 = Keypair::new(); + let storage3 = Keypair::new(); + let (payer, _) = Pubkey::find_program_address(&[owner.pubkey().as_ref()], &program_id); + program_test.add_account( + user.pubkey(), + Account::new(1000000, 0, &system_program::id()), + ); + program_test.add_account(payer, Account::new(1000000, 0, &program_id)); + program_test.add_account( + solana_sdk::evm_state::ID, + solana_evm_loader_program::create_state_account(1000000).into(), + ); + let short_payer_bytes = vec![0u8; 64]; + program_test.add_account( + storage1.pubkey(), + Account { + lamports: 10000000, owner: program_id, + data: short_payer_bytes.clone(), // data too short ..Account::default() }, ); + program_test.add_account( + storage2.pubkey(), + Account { + lamports: 10000000, + owner: program_id, + data: short_payer_bytes + .into_iter() + // this 4 bytes mean that the size of filter array is 1 but there's no data after + .chain([0, 0, 0, 1].into_iter()) + .collect(), + ..Account::default() + }, + ); + let payer_data = Payer { + owner: owner.pubkey(), + payer, + filters: vec![TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + }], + }; + let mut valid_payer_bytes = vec![]; + BorshSerialize::serialize(&payer_data, &mut valid_payer_bytes).unwrap(); + program_test.add_account( + storage3.pubkey(), + Account { + lamports: 10000000, + owner: program_id, + data: valid_payer_bytes + .into_iter() + // add 1 extra byte + .chain([0].into_iter()) + .collect(), + ..Account::default() + }, + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + for storage in [storage1, storage2, storage3] { + let account_metas = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { + tx: Some(dummy_eth_tx(evm::H160::zero(), vec![])), + }, + account_metas, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + transaction.sign(&[&user], recent_blockhash); + assert!(matches!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err(), + TransactionError(InstructionError(0, Custom(2))) + )); + } + } + + #[tokio::test] + async fn test_storage_not_initialized() { + let program_id = Pubkey::new_unique(); + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + + let user = Keypair::new(); + let owner = Keypair::new(); + let storage = Keypair::new(); + let (payer, _) = Pubkey::find_program_address(&[owner.pubkey().as_ref()], &program_id); + program_test.add_account( + user.pubkey(), + Account::new(1000000, 0, &system_program::id()), + ); + program_test.add_account(payer, Account::new(1000000, 0, &program_id)); program_test.add_account( solana_sdk::evm_state::ID, + solana_evm_loader_program::create_state_account(1000000).into(), + ); + let payer_bytes = vec![0u8; 93]; + program_test.add_account( + storage.pubkey(), Account { - lamports: 1000000, + lamports: 10000000, + owner: program_id, + data: payer_bytes, ..Account::default() }, ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + let account_metas = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { + tx: Some(dummy_eth_tx(evm::H160::zero(), vec![])), + }, + account_metas, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + transaction.sign(&[&user], recent_blockhash); + assert!(matches!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err(), + TransactionError(InstructionError(0, Custom(1))) + )); + } + + #[tokio::test] + async fn test_payer_account_mismatch() { + let program_id = Pubkey::new_unique(); + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + + let user = Keypair::new(); + let owner1 = Keypair::new(); + let owner2 = Keypair::new(); + let storage = Keypair::new(); + let (payer1, _) = Pubkey::find_program_address(&[owner1.pubkey().as_ref()], &program_id); + let (payer2, _) = Pubkey::find_program_address(&[owner2.pubkey().as_ref()], &program_id); + program_test.add_account( + user.pubkey(), + Account::new(1000000, 0, &system_program::id()), + ); + program_test.add_account(payer1, Account::new(1000000, 0, &program_id)); + program_test.add_account( + solana_sdk::evm_state::ID, + solana_evm_loader_program::create_state_account(1000000).into(), + ); + let payer_data = Payer { + owner: owner1.pubkey(), + payer: payer1, + filters: vec![TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + }], + }; + let mut payer_bytes = vec![]; + BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); + program_test.add_account( + storage.pubkey(), + Account { + lamports: 10000000, + owner: program_id, + data: payer_bytes, + ..Account::default() + }, + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + let account_metas = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new(payer2, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { + tx: Some(dummy_eth_tx(evm::H160::zero(), vec![0; 4])), + }, + account_metas, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + transaction.sign(&[&user], recent_blockhash); + assert!(matches!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err(), + TransactionError(InstructionError(0, Custom(6))) + )); + } + + #[tokio::test] + async fn test_payer_filter_mismatch() { + let program_id = Pubkey::new_unique(); + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + + let user = Keypair::new(); + let owner = Keypair::new(); + let storage = Keypair::new(); + let (payer, _) = Pubkey::find_program_address(&[owner.pubkey().as_ref()], &program_id); + program_test.add_account( + user.pubkey(), + Account::new(1000000, 0, &system_program::id()), + ); + program_test.add_account(payer, Account::new(1000000, 0, &program_id)); + program_test.add_account( + solana_sdk::evm_state::ID, + solana_evm_loader_program::create_state_account(1000000).into(), + ); let payer_data = Payer { owner: owner.pubkey(), payer, - filters: vec![TxFilter::InputStartsWith { contract: evm::Address::zero(), input_prefix: vec![] }], + filters: vec![ + TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![1; 4], + }, + TxFilter::InputStartsWith { + contract: evm::Address::from([1u8; 20]), + input_prefix: vec![0; 4], + }, + ], }; - let mut payer_bytes = vec![0u8; 93]; - BorshSerialize::serialize(&payer_data, &mut payer_bytes.as_mut_slice()).unwrap(); + let mut payer_bytes = vec![]; + BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); program_test.add_account( storage.pubkey(), Account { @@ -326,7 +759,7 @@ mod test { let (mut banks_client, _, recent_blockhash) = program_test.start().await; let account_metas = vec![ - AccountMeta::new(alice.pubkey(), true), + AccountMeta::new(user.pubkey(), true), AccountMeta::new(storage.pubkey(), false), AccountMeta::new(payer, false), AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), @@ -336,12 +769,20 @@ mod test { let ix = Instruction::new_with_borsh( program_id, &GasStationInstruction::ExecuteWithPayer { - tx: Some(dummy_eth_tx()), + tx: Some(dummy_eth_tx(evm::H160::zero(), vec![0; 4])), }, account_metas, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&alice.pubkey())); - transaction.sign(&[&alice], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + transaction.sign(&[&user], recent_blockhash); + assert!(matches!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err(), + TransactionError(InstructionError(0, Custom(7))) + )); } + + // TODO: add test for insufficient_funds during refund } diff --git a/evm-utils/programs/gas_station/src/state.rs b/evm-utils/programs/gas_station/src/state.rs index 21c63e8583..e425157d05 100644 --- a/evm-utils/programs/gas_station/src/state.rs +++ b/evm-utils/programs/gas_station/src/state.rs @@ -6,7 +6,7 @@ use solana_sdk::{ }; use crate::instruction::TxFilter; -pub const MAX_FILTERS: usize = 5; +pub const MAX_FILTERS: usize = 10; #[repr(C)] #[derive(BorshDeserialize, BorshSerialize, Debug)] From 72ca2c8d6aa0d598b591dc94acf34bf849802ff5 Mon Sep 17 00:00:00 2001 From: Vadim Date: Thu, 24 Nov 2022 22:41:00 +0200 Subject: [PATCH 03/19] Feat(programs): refactoring --- evm-utils/programs/gas_station/src/error.rs | 2 + .../programs/gas_station/src/processor.rs | 128 ++++++++++++------ 2 files changed, 91 insertions(+), 39 deletions(-) diff --git a/evm-utils/programs/gas_station/src/error.rs b/evm-utils/programs/gas_station/src/error.rs index d8d45c1f96..ec0d1da931 100644 --- a/evm-utils/programs/gas_station/src/error.rs +++ b/evm-utils/programs/gas_station/src/error.rs @@ -9,6 +9,8 @@ pub enum GasStationError { AccountInUse, #[error("Account storage isn't uninitialized")] AccountNotInitialized, + #[error("Account info for big transaction storage is missing")] + BigTxStorageMissing, #[error("Unable to deserialize borsh encoded account data")] InvalidAccountBorshData, #[error("Unable to deserialize big transaction account data")] diff --git a/evm-utils/programs/gas_station/src/processor.rs b/evm-utils/programs/gas_station/src/processor.rs index 82828e7ccd..c40bbc6ff1 100644 --- a/evm-utils/programs/gas_station/src/processor.rs +++ b/evm-utils/programs/gas_station/src/processor.rs @@ -17,7 +17,6 @@ use solana_sdk::{ }; use error::GasStationError; -use evm_rpc::Either; use instruction::{GasStationInstruction, TxFilter}; use state::{Payer, MAX_FILTERS}; @@ -110,13 +109,11 @@ fn process_execute_with_payer( let evm_state = next_account_info(account_info_iter)?; let system_program = next_account_info(account_info_iter)?; - let (unpacked_tx, big_tx_storage_info) = match tx.as_ref() { - None => { - let big_tx_storage_info = next_account_info(account_info_iter)?; - (get_big_tx_from_storage(big_tx_storage_info)?, Some(big_tx_storage_info)) - } - Some(tx) => (tx.clone(), None), - }; + let big_tx_storage_info = next_account_info(account_info_iter); + let tx_passed_directly = tx.is_some(); + if !tx_passed_directly && big_tx_storage_info.is_err() { + return Err(GasStationError::BigTxStorageMissing.into()); + } if !cmp_pubkeys(program_id, payer_storage_info.owner) { return Err(ProgramError::IncorrectProgramId); @@ -133,6 +130,14 @@ fn process_execute_with_payer( if payer.payer != *payer_info.key { return Err(GasStationError::PayerAccountMismatch.into()); } + + let unpacked_tx = match tx { + None => { + let big_tx_storage_info = big_tx_storage_info.clone().unwrap(); + get_big_tx_from_storage(big_tx_storage_info)? + } + Some(tx) => tx, + }; if !payer.do_filter_match(&unpacked_tx) { return Err(GasStationError::PayerFilterMismatch.into()); } @@ -144,10 +149,15 @@ fn process_execute_with_payer( // pass sender acc to evm loader, execute tx restore ownership payer_info.assign(&solana_sdk::evm_loader::ID); - let ix = make_evm_loader_execute_ix(*evm_loader.key, *evm_state.key, *payer_info.key, tx.map_or_else(|| Either::Right(*big_tx_storage_info.unwrap().key), |tx| Either::Left(tx))); - let account_infos = match big_tx_storage_info { - Some(big_tx_storage_info) => vec![evm_loader.clone(), big_tx_storage_info.clone(), evm_state.clone(), payer_info.clone()], - None => vec![evm_loader.clone(), evm_state.clone(), payer_info.clone()], + let (ix, account_infos) = if tx_passed_directly { + make_evm_loader_tx_execute_ix(evm_loader, evm_state, payer_info, unpacked_tx) + } else { + make_evm_loader_big_tx_execute_ix( + evm_loader, + evm_state, + payer_info, + big_tx_storage_info.unwrap(), + ) }; invoke_signed(&ix, &account_infos, signers_seeds)?; @@ -175,31 +185,55 @@ fn get_big_tx_from_storage(storage_acc: &AccountInfo) -> Result, -) -> Instruction { +fn make_evm_loader_tx_execute_ix<'a>( + evm_loader: &AccountInfo<'a>, + evm_state: &AccountInfo<'a>, + sender: &AccountInfo<'a>, + tx: evm::Transaction, +) -> (Instruction, Vec>) { use solana_evm_loader_program::instructions::*; - let (tx, accounts) = match tx { - Either::Left(tx) => (Some(tx), vec![ - AccountMeta::new(evm_state, false), - AccountMeta::new(sender, true), - ]), - Either::Right(big_tx_storage_key) => (None, vec![ - AccountMeta::new(evm_state, false), - AccountMeta::new(big_tx_storage_key, true), - AccountMeta::new(sender, true), - ]), - }; - solana_evm_loader_program::create_evm_instruction_with_borsh( - evm_loader, - &EvmInstruction::ExecuteTransaction { - tx: ExecuteTransaction::Signed { tx }, - fee_type: FeePayerType::Native, - }, - accounts, + ( + solana_evm_loader_program::create_evm_instruction_with_borsh( + *evm_loader.key, + &EvmInstruction::ExecuteTransaction { + tx: ExecuteTransaction::Signed { tx: Some(tx) }, + fee_type: FeePayerType::Native, + }, + vec![ + AccountMeta::new(*evm_state.key, false), + AccountMeta::new(*sender.key, true), + ], + ), + vec![evm_loader.clone(), evm_state.clone(), sender.clone()], + ) +} + +fn make_evm_loader_big_tx_execute_ix<'a>( + evm_loader: &AccountInfo<'a>, + evm_state: &AccountInfo<'a>, + sender: &AccountInfo<'a>, + big_tx_storage: &AccountInfo<'a>, +) -> (Instruction, Vec>) { + use solana_evm_loader_program::instructions::*; + ( + solana_evm_loader_program::create_evm_instruction_with_borsh( + *evm_loader.key, + &EvmInstruction::ExecuteTransaction { + tx: ExecuteTransaction::Signed { tx: None }, + fee_type: FeePayerType::Native, + }, + vec![ + AccountMeta::new(*evm_state.key, false), + AccountMeta::new(*big_tx_storage.key, true), + AccountMeta::new(*sender.key, true), + ], + ), + vec![ + evm_loader.clone(), + big_tx_storage.clone(), + evm_state.clone(), + sender.clone(), + ], ) } @@ -409,11 +443,27 @@ mod test { AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new(big_tx_storage.pubkey(), true), ]; + let ix_no_big_tx_storage = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { tx: None }, + account_metas.split_last().unwrap().1.into(), + ); let ix = Instruction::new_with_borsh( program_id, &GasStationInstruction::ExecuteWithPayer { tx: None }, account_metas, ); + // this will fail because neither evm tx nor big tx storage provided + let mut transaction = + Transaction::new_with_payer(&[ix_no_big_tx_storage], Some(&user.pubkey())); + transaction.sign(&[&user], recent_blockhash); + assert!(matches!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err(), + TransactionError(InstructionError(0, Custom(2))) + )); let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); transaction.sign(&[&user, &big_tx_storage], recent_blockhash); banks_client.process_transaction(transaction).await.unwrap(); @@ -579,7 +629,7 @@ mod test { .process_transaction(transaction) .await .unwrap_err(), - TransactionError(InstructionError(0, Custom(2))) + TransactionError(InstructionError(0, Custom(3))) )); } } @@ -707,7 +757,7 @@ mod test { .process_transaction(transaction) .await .unwrap_err(), - TransactionError(InstructionError(0, Custom(6))) + TransactionError(InstructionError(0, Custom(7))) )); } @@ -780,7 +830,7 @@ mod test { .process_transaction(transaction) .await .unwrap_err(), - TransactionError(InstructionError(0, Custom(7))) + TransactionError(InstructionError(0, Custom(8))) )); } From 9a32ac4909ee31831479f16e7b18c33cdd4262f5 Mon Sep 17 00:00:00 2001 From: Vadim Date: Mon, 28 Nov 2022 15:17:29 +0200 Subject: [PATCH 04/19] Feat(programs): add math checks, add test --- evm-utils/programs/gas_station/src/error.rs | 4 + .../programs/gas_station/src/processor.rs | 146 +++++++++++++++++- 2 files changed, 142 insertions(+), 8 deletions(-) diff --git a/evm-utils/programs/gas_station/src/error.rs b/evm-utils/programs/gas_station/src/error.rs index ec0d1da931..4dde35980a 100644 --- a/evm-utils/programs/gas_station/src/error.rs +++ b/evm-utils/programs/gas_station/src/error.rs @@ -11,6 +11,8 @@ pub enum GasStationError { AccountNotInitialized, #[error("Account info for big transaction storage is missing")] BigTxStorageMissing, + #[error("Payer is unable to pay for transaction")] + InsufficientPayerBalance, #[error("Unable to deserialize borsh encoded account data")] InvalidAccountBorshData, #[error("Unable to deserialize big transaction account data")] @@ -25,6 +27,8 @@ pub enum GasStationError { PayerFilterMismatch, #[error("PDA account info doesn't match DPA derived by this program id")] PdaAccountMismatch, + #[error("Overflow occurred during transaction call refund")] + RefundOverflow, } impl From for ProgramError { diff --git a/evm-utils/programs/gas_station/src/processor.rs b/evm-utils/programs/gas_station/src/processor.rs index c40bbc6ff1..d52dce6ad8 100644 --- a/evm-utils/programs/gas_station/src/processor.rs +++ b/evm-utils/programs/gas_station/src/processor.rs @@ -238,8 +238,17 @@ fn make_evm_loader_big_tx_execute_ix<'a>( } fn refund_native_fee(caller: &AccountInfo, payer: &AccountInfo, amount: u64) -> ProgramResult { - **payer.try_borrow_mut_lamports()? -= amount; - **caller.try_borrow_mut_lamports()? += amount; + **payer.try_borrow_mut_lamports()? = + payer + .lamports() + .checked_sub(amount) + .ok_or(ProgramError::from( + GasStationError::InsufficientPayerBalance, + ))?; + **caller.try_borrow_mut_lamports()? = caller + .lamports() + .checked_add(amount) + .ok_or(ProgramError::from(GasStationError::RefundOverflow))?; Ok(()) } @@ -256,7 +265,8 @@ mod test { transport::TransportError::TransactionError, }; - const SECRET_KEY_DUMMY: [u8; 32] = [1; 32]; + const SECRET_KEY_DUMMY_ONES: [u8; 32] = [1; 32]; + const SECRET_KEY_DUMMY_TWOS: [u8; 32] = [2; 32]; const TEST_CHAIN_ID: u64 = 0xdead; pub fn dummy_eth_tx(contract: evm::H160, input: Vec) -> evm::Transaction { @@ -269,7 +279,7 @@ mod test { input, } .sign( - &evm::SecretKey::from_slice(&SECRET_KEY_DUMMY).unwrap(), + &evm::SecretKey::from_slice(&SECRET_KEY_DUMMY_ONES).unwrap(), Some(TEST_CHAIN_ID), ) } @@ -629,7 +639,7 @@ mod test { .process_transaction(transaction) .await .unwrap_err(), - TransactionError(InstructionError(0, Custom(3))) + TransactionError(InstructionError(0, Custom(4))) )); } } @@ -757,7 +767,7 @@ mod test { .process_transaction(transaction) .await .unwrap_err(), - TransactionError(InstructionError(0, Custom(7))) + TransactionError(InstructionError(0, Custom(8))) )); } @@ -830,9 +840,129 @@ mod test { .process_transaction(transaction) .await .unwrap_err(), - TransactionError(InstructionError(0, Custom(8))) + TransactionError(InstructionError(0, Custom(9))) )); } - // TODO: add test for insufficient_funds during refund + #[tokio::test] + async fn test_insufficient_payer_funds() { + let program_id = Pubkey::new_unique(); + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + + let user = Keypair::new(); + let owner1 = Keypair::new(); + let owner2 = Keypair::new(); + let storage1 = Keypair::new(); + let storage2 = Keypair::new(); + let (payer1, _) = Pubkey::find_program_address(&[owner1.pubkey().as_ref()], &program_id); + let (payer2, _) = Pubkey::find_program_address(&[owner2.pubkey().as_ref()], &program_id); + program_test.add_account( + user.pubkey(), + Account::new(1000000, 0, &system_program::id()), + ); + // Total lamports needed for successful execution: 42000 (evm call) + 10000 (native call refund) + program_test.add_account(payer1, Account::new(51999, 0, &program_id)); + program_test.add_account(payer2, Account::new(41999, 0, &program_id)); + program_test.add_account( + solana_sdk::evm_state::ID, + solana_evm_loader_program::create_state_account(1000000).into(), + ); + let mut payer_data = Payer { + owner: owner1.pubkey(), + payer: payer1, + filters: vec![TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + }], + }; + let mut payer_bytes = vec![]; + BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); + program_test.add_account( + storage1.pubkey(), + Account { + lamports: 10000000, + owner: program_id, + data: payer_bytes, + ..Account::default() + }, + ); + payer_data.owner = owner2.pubkey(); + payer_data.payer = payer2; + let mut payer_bytes = vec![]; + BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); + program_test.add_account( + storage2.pubkey(), + Account { + lamports: 10000000, + owner: program_id, + data: payer_bytes, + ..Account::default() + }, + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + let account_metas = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage1.pubkey(), false), + AccountMeta::new(payer1, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { + tx: Some(dummy_eth_tx(evm::H160::zero(), vec![])), + }, + account_metas, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + transaction.sign(&[&user], recent_blockhash); + // This tx has funds for evm call but will fail on refund attempt + assert!(matches!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err(), + TransactionError(InstructionError(0, Custom(3))) // GasStationError::InsufficientPayerBalance + )); + + let account_metas = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage2.pubkey(), false), + AccountMeta::new(payer2, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let tx = evm::UnsignedTransaction { + nonce: evm::U256::zero(), + gas_price: evm::U256::zero(), + gas_limit: evm::U256::zero(), + action: evm::TransactionAction::Call(evm::H160::zero()), + value: evm::U256::zero(), + input: vec![], + } + .sign( + &evm::SecretKey::from_slice(&SECRET_KEY_DUMMY_TWOS).unwrap(), + Some(TEST_CHAIN_ID), + ); + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { tx: Some(tx) }, + account_metas, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + transaction.sign(&[&user], recent_blockhash); + // This tx will fail on evm side due to insufficient funds for evm transaction + assert!(matches!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err(), + TransactionError(InstructionError(0, Custom(18))) // NativeAccountInsufficientFunds from evm_loader + )); + } } From 20ee7990757e8599c1bf5e36966dafe5862bf799 Mon Sep 17 00:00:00 2001 From: Vadim Date: Mon, 28 Nov 2022 17:15:09 +0200 Subject: [PATCH 05/19] Feat(programs): fix register payer test --- .../programs/gas_station/src/instruction.rs | 2 +- .../programs/gas_station/src/processor.rs | 40 ++++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/evm-utils/programs/gas_station/src/instruction.rs b/evm-utils/programs/gas_station/src/instruction.rs index d0ba3152ff..d828ecaa1c 100644 --- a/evm-utils/programs/gas_station/src/instruction.rs +++ b/evm-utils/programs/gas_station/src/instruction.rs @@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_evm_loader_program::scope::evm; use solana_sdk::pubkey::Pubkey; -#[derive(Debug, BorshDeserialize, BorshSerialize)] +#[derive(Debug, BorshDeserialize, BorshSerialize, PartialEq)] pub enum TxFilter { InputStartsWith { contract: evm::Address, diff --git a/evm-utils/programs/gas_station/src/processor.rs b/evm-utils/programs/gas_station/src/processor.rs index d52dce6ad8..f245f7eea0 100644 --- a/evm-utils/programs/gas_station/src/processor.rs +++ b/evm-utils/programs/gas_station/src/processor.rs @@ -258,7 +258,7 @@ mod test { use solana_program::instruction::InstructionError::{Custom, IncorrectProgramId}; use solana_program_test::{processor, ProgramTest}; use solana_sdk::{ - account::Account, + account::{Account, ReadableAccount}, signature::{Keypair, Signer}, system_program, transaction::{Transaction, TransactionError::InstructionError}, @@ -284,7 +284,6 @@ mod test { ) } - #[ignore] #[tokio::test] async fn test_register_payer() { let program_id = Pubkey::new_unique(); @@ -293,6 +292,7 @@ mod test { ProgramTest::new("gas-station", program_id, processor!(process_instruction)); let creator = Keypair::new(); + let storage = Keypair::new(); program_test.add_account( creator.pubkey(), Account { @@ -300,24 +300,27 @@ mod test { ..Account::default() }, ); + program_test.add_account( + storage.pubkey(), + Account::new(10000000, 93, &program_id), + ); let (mut banks_client, _, recent_blockhash) = program_test.start().await; - let (storage, _) = - Pubkey::find_program_address(&[creator.pubkey().as_ref(), &[0]], &program_id); - let (payer, _) = - Pubkey::find_program_address(&[creator.pubkey().as_ref(), &[1]], &program_id); + let (payer_key, _) = + Pubkey::find_program_address(&[creator.pubkey().as_ref()], &program_id); let account_metas = vec![ AccountMeta::new(creator.pubkey(), true), - AccountMeta::new(storage, false), - AccountMeta::new(payer, false), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new(payer_key, false), AccountMeta::new_readonly(solana_sdk::system_program::id(), false), ]; + let transfer_amount = 1000000; let ix = Instruction::new_with_borsh( program_id, &GasStationInstruction::RegisterPayer { owner: creator.pubkey(), - transfer_amount: 0, + transfer_amount, whitelist: vec![TxFilter::InputStartsWith { contract: evm::Address::zero(), input_prefix: vec![], @@ -328,6 +331,25 @@ mod test { let mut transaction = Transaction::new_with_payer(&[ix], Some(&creator.pubkey())); transaction.sign(&[&creator], recent_blockhash); banks_client.process_transaction(transaction).await.unwrap(); + + let account = banks_client.get_account(storage.pubkey()).await.unwrap().unwrap(); + assert_eq!(account.owner, program_id); + assert_eq!(account.lamports, 10000000); + assert_eq!(account.data.len(), 93); + let mut data_slice: &[u8] = account.data(); + let payer: Payer = BorshDeserialize::deserialize(&mut data_slice).unwrap(); + assert_eq!(payer.payer, payer_key); + assert_eq!(payer.owner, creator.pubkey()); + assert_eq!(payer.filters.len(), 1); + assert_eq!(payer.filters[0], TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + }); + + let rent = banks_client.get_rent().await.unwrap(); + let pda_account = banks_client.get_account(payer_key).await.unwrap().unwrap(); + assert_eq!(pda_account.owner, program_id); + assert_eq!(pda_account.lamports, rent.minimum_balance(0) + transfer_amount); } #[tokio::test] From 7e3c365b1d71f2f771ebabce38a4a496b04b50fe Mon Sep 17 00:00:00 2001 From: Vadim Date: Mon, 5 Dec 2022 15:54:36 +0200 Subject: [PATCH 06/19] Feat(programs): integrate gas-station into bridge --- Cargo.lock | 2 + evm-utils/evm-bridge/Cargo.toml | 2 + evm-utils/evm-bridge/src/main.rs | 61 +++++++++++++++++++++++ evm-utils/evm-bridge/src/pool.rs | 32 +++++++++++- evm-utils/evm-bridge/src/tx_filter.rs | 8 ++- evm-utils/programs/gas_station/src/lib.rs | 50 ++++++++++++++++++- 6 files changed, 151 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0881ad3f3b..0c5e3108bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1831,6 +1831,7 @@ dependencies = [ "borsh", "derivative", "env_logger 0.8.4", + "evm-gas-station", "evm-rpc", "evm-state", "hex", @@ -1867,6 +1868,7 @@ dependencies = [ "solana-transaction-status", "solana-version", "structopt", + "thiserror", "tokio", "tracing", "tracing-attributes", diff --git a/evm-utils/evm-bridge/Cargo.toml b/evm-utils/evm-bridge/Cargo.toml index aca34b3020..dd5042bcb8 100644 --- a/evm-utils/evm-bridge/Cargo.toml +++ b/evm-utils/evm-bridge/Cargo.toml @@ -30,6 +30,7 @@ hex = "0.4.2" primitive-types = "0.11.0" secp256k1 = { version = "0.19.0", features = ["recovery", "global-context"] } evm-state = { path = "../evm-state" } +evm-gas-station = { path = "../programs/gas_station" } log = "0.4.11" jsonrpc-core = "18.0.0" jsonrpc-core-client = { version = "18.0.0", features = ["ws"] } @@ -39,6 +40,7 @@ jsonrpc-pubsub = "18.0.0" jsonrpc-ws-server = "18.0.0" snafu = "0.7" anyhow = "1.0" +thiserror = "1.0" tokio = "1" txpool = { git = "https://github.com/velas/transaction-pool", tag = "v1.0.0-alpha" } regex = "1.5.4" diff --git a/evm-utils/evm-bridge/src/main.rs b/evm-utils/evm-bridge/src/main.rs index c08e7e0e67..7e0e84f7fc 100644 --- a/evm-utils/evm-bridge/src/main.rs +++ b/evm-utils/evm-bridge/src/main.rs @@ -190,6 +190,8 @@ pub struct EvmBridge { pool: EthPool, min_gas_price: U256, whitelist: Vec, + gas_station_program_id: Option, + redirect_to_proxy_filters: Vec, } impl EvmBridge { @@ -237,6 +239,8 @@ impl EvmBridge { pool, min_gas_price, whitelist: vec![], + gas_station_program_id: None, + redirect_to_proxy_filters: vec![], } } @@ -244,6 +248,15 @@ impl EvmBridge { self.whitelist = whitelist; } + fn set_redirect_to_proxy_filters( + &mut self, + gas_station_program_id: Pubkey, + redirect_to_proxy_filters: Vec, + ) { + self.gas_station_program_id = Some(gas_station_program_id); + self.redirect_to_proxy_filters = redirect_to_proxy_filters; + } + /// Wrap evm tx into solana, optionally add meta keys, to solana signature. async fn send_tx( &self, @@ -990,6 +1003,43 @@ pub(crate) fn from_client_error(client_error: ClientError) -> evm_rpc::Error { } } +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum ParseEvmContractToPayerKeysError { + #[error("Evm contract string is invalid")] + InvalidEvmContract, + #[error("Input format is invalid, provide string of the next format: \":\"")] + InvalidFormat, + #[error("Invalid pubkey")] + InvalidPubkey, +} + +#[derive(Debug)] +struct EvmContractToPayerKeys { + contract: Address, + payer: Pubkey, + storage_acc: Pubkey, +} + +impl FromStr for EvmContractToPayerKeys { + type Err = ParseEvmContractToPayerKeysError; + + fn from_str(s: &str) -> StdResult { + let (addr, keys) = s + .split_once(':') + .ok_or(ParseEvmContractToPayerKeysError::InvalidFormat)?; + let (key1, key2) = keys + .split_once(':') + .ok_or(ParseEvmContractToPayerKeysError::InvalidFormat)?; + let contract = Address::from_str(addr) + .map_err(|_| ParseEvmContractToPayerKeysError::InvalidEvmContract)?; + let payer = Pubkey::from_str(key1) + .map_err(|_| ParseEvmContractToPayerKeysError::InvalidPubkey)?; + let storage_acc = Pubkey::from_str(key2) + .map_err(|_| ParseEvmContractToPayerKeysError::InvalidPubkey)?; + Ok(Self { contract, payer, storage_acc }) + } +} + #[derive(Debug, structopt::StructOpt)] struct Args { keyfile: Option, @@ -1016,6 +1066,11 @@ struct Args { #[structopt(long = "whitelist-path")] whitelist_path: Option, + + #[structopt(long = "gas-station")] + gas_station_program_id: Option, + #[structopt(long = "redirect-to-proxy")] + redirect_contracts_to_proxy: Option>, } impl Args { @@ -1110,6 +1165,10 @@ async fn main(args: Args) -> StdResult<(), Box> { min_gas_price, ); meta.set_whitelist(whitelist); + if let Some(redirect_list) = args.redirect_contracts_to_proxy { + let gas_station_program_id = args.gas_station_program_id.expect("gas-station program id is missing"); + meta.set_redirect_to_proxy_filters(gas_station_program_id, redirect_list); + } let meta = Arc::new(meta); let mut io = MetaIoHandler::with_middleware(ProxyMiddleware {}); @@ -1283,6 +1342,8 @@ mod tests { pool: EthPool::new(SystemClock), min_gas_price: 0.into(), whitelist: vec![], + gas_station_program_id: None, + redirect_to_proxy_filters: vec![], }); let rpc = BridgeErpcImpl {}; diff --git a/evm-utils/evm-bridge/src/pool.rs b/evm-utils/evm-bridge/src/pool.rs index bdc12b02ff..3aa7dea2f4 100644 --- a/evm-utils/evm-bridge/src/pool.rs +++ b/evm-utils/evm-bridge/src/pool.rs @@ -594,7 +594,21 @@ async fn process_tx( } } - let instructions = bridge.make_send_tx_instructions(&tx, &meta_keys); + let instructions = if let Some(val) = bridge + .redirect_to_proxy_filters + .iter() + .find(|val| matches!(tx.action, TransactionAction::Call(addr) if addr == val.contract)) + { + vec![evm_gas_station::execute_tx_with_payer( + tx.clone(), + bridge.gas_station_program_id.unwrap(), + bridge.key.pubkey(), + val.storage_acc, + val.payer, + )] + } else { + bridge.make_send_tx_instructions(&tx, &meta_keys) + }; let message = Message::new(&instructions, Some(&bridge.key.pubkey())); let mut send_raw_tx: solana::Transaction = solana::Transaction::new_unsigned(message); @@ -788,7 +802,21 @@ async fn deploy_big_tx( .map_err(|e| into_native_error(e, bridge.verbose_errors))? .value; - let instructions = bridge.make_send_big_tx_instructions(tx, storage_pubkey, payer_pubkey); + let instructions = if let Some(val) = bridge + .redirect_to_proxy_filters + .iter() + .find(|val| matches!(tx.action, TransactionAction::Call(addr) if addr == val.contract)) + { + vec![evm_gas_station::execute_big_tx_with_payer( + bridge.gas_station_program_id.unwrap(), + bridge.key.pubkey(), + val.storage_acc, + val.payer, + storage_pubkey, + )] + } else { + bridge.make_send_big_tx_instructions(tx, storage_pubkey, payer_pubkey) + }; let execute_tx = solana::Transaction::new_signed_with_payer( &instructions, Some(&payer_pubkey), diff --git a/evm-utils/evm-bridge/src/tx_filter.rs b/evm-utils/evm-bridge/src/tx_filter.rs index 2af664c86e..724195d8b3 100644 --- a/evm-utils/evm-bridge/src/tx_filter.rs +++ b/evm-utils/evm-bridge/src/tx_filter.rs @@ -4,10 +4,13 @@ use serde::Deserialize; #[derive(Debug, Deserialize)] pub enum TxFilter { - InputStartsWith{ + InputStartsWith { contract: Address, input_prefix: Bytes }, + ByReceiver { + contract: Address, + } } impl TxFilter { @@ -17,6 +20,9 @@ impl TxFilter { matches!(tx.action, TransactionAction::Call(addr) if addr == *contract) && tx.input.starts_with(&input_prefix.0) } + Self::ByReceiver { contract } => { + matches!(tx.action, TransactionAction::Call(addr) if addr == *contract) + } } } } diff --git a/evm-utils/programs/gas_station/src/lib.rs b/evm-utils/programs/gas_station/src/lib.rs index da0b4aa4e0..401b33af76 100644 --- a/evm-utils/programs/gas_station/src/lib.rs +++ b/evm-utils/programs/gas_station/src/lib.rs @@ -4,7 +4,55 @@ mod processor; mod state; use processor::process_instruction; -use solana_program::entrypoint; +use solana_evm_loader_program::scope::evm; +use solana_program::instruction::{AccountMeta, Instruction}; +use solana_program::pubkey::Pubkey; +use solana_program::{entrypoint, system_program}; // Declare and export the program's entrypoint entrypoint!(process_instruction); + +pub fn execute_tx_with_payer( + tx: evm::Transaction, + program_id: Pubkey, + signer: Pubkey, + storage: Pubkey, + payer: Pubkey, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(signer, true), + AccountMeta::new(storage, false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + Instruction::new_with_borsh( + program_id, + &instruction::GasStationInstruction::ExecuteWithPayer { tx: Some(tx) }, + account_metas, + ) +} + +pub fn execute_big_tx_with_payer( + program_id: Pubkey, + signer: Pubkey, + storage: Pubkey, + payer: Pubkey, + big_tx_storage: Pubkey, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(signer, true), + AccountMeta::new(storage, false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new(big_tx_storage, true), + ]; + Instruction::new_with_borsh( + program_id, + &instruction::GasStationInstruction::ExecuteWithPayer { tx: None }, + account_metas, + ) +} From 15f4b761ae86cc754dd1974191444448059f64b5 Mon Sep 17 00:00:00 2001 From: Vadim Date: Fri, 9 Dec 2022 13:08:34 +0200 Subject: [PATCH 07/19] Chore(deps): switch to no_std borsh version --- Cargo.lock | 25 ++++++++++++------------ Cargo.toml | 5 +++-- evm-utils/evm-state/Cargo.toml | 2 +- evm-utils/programs/evm_loader/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c5e3108bc..14486129a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2097,7 +2097,7 @@ dependencies = [ [[package]] name = "fixed-hash" version = "0.7.0" -source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support#974f23ebc1956239cd8150e0ae80517343d82836" +source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support-nostd#55f2602045ea3088d0484b15f617bd0cd230501c" dependencies = [ "byteorder", "rand 0.8.5", @@ -2778,7 +2778,7 @@ checksum = "9007da9cacbd3e6343da136e98b0d2df013f553d35bdec8b518f07bea768e19c" [[package]] name = "impl-borsh" version = "0.1.0" -source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support#974f23ebc1956239cd8150e0ae80517343d82836" +source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support-nostd#55f2602045ea3088d0484b15f617bd0cd230501c" dependencies = [ "borsh", ] @@ -2795,7 +2795,7 @@ dependencies = [ [[package]] name = "impl-codec" version = "0.6.0" -source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support#974f23ebc1956239cd8150e0ae80517343d82836" +source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support-nostd#55f2602045ea3088d0484b15f617bd0cd230501c" dependencies = [ "parity-scale-codec", ] @@ -2812,7 +2812,7 @@ dependencies = [ [[package]] name = "impl-rlp" version = "0.3.0" -source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support#974f23ebc1956239cd8150e0ae80517343d82836" +source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support-nostd#55f2602045ea3088d0484b15f617bd0cd230501c" dependencies = [ "rlp", ] @@ -2829,7 +2829,7 @@ dependencies = [ [[package]] name = "impl-serde" version = "0.3.2" -source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support#974f23ebc1956239cd8150e0ae80517343d82836" +source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support-nostd#55f2602045ea3088d0484b15f617bd0cd230501c" dependencies = [ "serde", ] @@ -4256,13 +4256,13 @@ dependencies = [ [[package]] name = "primitive-types" version = "0.11.1" -source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support#974f23ebc1956239cd8150e0ae80517343d82836" +source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support-nostd#55f2602045ea3088d0484b15f617bd0cd230501c" dependencies = [ - "fixed-hash 0.7.0 (git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support)", + "fixed-hash 0.7.0 (git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support-nostd)", "impl-borsh", - "impl-codec 0.6.0 (git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support)", - "impl-rlp 0.3.0 (git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support)", - "impl-serde 0.3.2 (git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support)", + "impl-codec 0.6.0 (git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support-nostd)", + "impl-rlp 0.3.0 (git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support-nostd)", + "impl-serde 0.3.2 (git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support-nostd)", "scale-info", "uint", ] @@ -5003,8 +5003,7 @@ dependencies = [ [[package]] name = "rlp" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5" +source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support-nostd#55f2602045ea3088d0484b15f617bd0cd230501c" dependencies = [ "bytes 1.2.1", "rustc-hex", @@ -8858,7 +8857,7 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "uint" version = "0.9.3" -source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support#974f23ebc1956239cd8150e0ae80517343d82836" +source = "git+https://github.com/velas/parity-common?tag=primitive-types-v0.11-with-borsh-support-nostd#55f2602045ea3088d0484b15f617bd0cd230501c" dependencies = [ "byteorder", "crunchy", diff --git a/Cargo.toml b/Cargo.toml index 440ab9be21..f9b3549980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,8 +104,9 @@ resolver = "2" split-debuginfo = "unpacked" [patch.crates-io] -primitive-types = { git = "https://github.com/velas/parity-common", tag = "primitive-types-v0.11-with-borsh-support" } -uint = { git = "https://github.com/velas/parity-common", tag = "primitive-types-v0.11-with-borsh-support" } +primitive-types = { git = "https://github.com/velas/parity-common", tag = "primitive-types-v0.11-with-borsh-support-nostd" } +rlp = { git = "https://github.com/velas/parity-common", tag = "primitive-types-v0.11-with-borsh-support-nostd" } +uint = { git = "https://github.com/velas/parity-common", tag = "primitive-types-v0.11-with-borsh-support-nostd" } spl-memo = { git = "https://github.com/velas/spl-memo", branch = "solana-1.9.29" } # TODO: remove once jsonrpc-core-client 18.0.1 is released jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc", rev = "e724d087defc0af35bc1c844049d1611588d8466", version = "18.0.0" } diff --git a/evm-utils/evm-state/Cargo.toml b/evm-utils/evm-state/Cargo.toml index b8ddd3857b..8acf7a60b0 100644 --- a/evm-utils/evm-state/Cargo.toml +++ b/evm-utils/evm-state/Cargo.toml @@ -14,7 +14,7 @@ rocksdb = { git = "https://github.com/velas/rust-rocksdb", version = "0.19.0", d triedb = { git = "https://github.com/velas/triedb", tag = "rocksdb-v0.19", features = ["rocksdb"] } # triedb = { path = "../../../triedb", features = ["rocksdb"] } -primitive-types = "0.11.0" +primitive-types = { version = "0.11.0", features = ["borsh"] } keccak-hash = "0.9.0" log = "0.4.11" simple_logger = "2.2" diff --git a/evm-utils/programs/evm_loader/Cargo.toml b/evm-utils/programs/evm_loader/Cargo.toml index 5400239271..b41792b1d8 100644 --- a/evm-utils/programs/evm_loader/Cargo.toml +++ b/evm-utils/programs/evm_loader/Cargo.toml @@ -16,7 +16,7 @@ assert_matches = "1.4" bincode = "1.3.1" borsh = "0.9.3" serde = "1.0" -primitive-types = "0.11.0" +primitive-types = { version = "0.11.0", features = ["borsh"] } hex = "0.4.2" simple_logger = "2.2.0" sha3 = "0.9.1" From a18dfef5594b91f5dd4711bd06765db513d06b90 Mon Sep 17 00:00:00 2001 From: Vadim Date: Mon, 12 Dec 2022 23:54:45 +0200 Subject: [PATCH 08/19] Fix(deps): make evm dependencies optional for sdk --- sdk/Cargo.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 20096c3daf..0ada8c8ecc 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -34,6 +34,8 @@ full = [ "libsecp256k1", "sha3", "digest", + "evm-state", + "evm-rpc", ] [dependencies] @@ -80,8 +82,8 @@ thiserror = "1.0" uriparse = "0.6.3" wasm-bindgen = "0.2" -evm-state = { path = "../evm-utils/evm-state", version = "0.1" } -evm-rpc = { path = "../evm-utils/evm-rpc", version = "0.1" } +evm-state = { path = "../evm-utils/evm-state", version = "0.1", optional = true } +evm-rpc = { path = "../evm-utils/evm-rpc", version = "0.1", optional = true } rlp = "0.5" tempfile = "3.2" once_cell = "1.7.2" From 7bc4d70c6916704b8f3f2c6eab7ae38486de5c47 Mon Sep 17 00:00:00 2001 From: Vadim Date: Mon, 12 Dec 2022 23:57:11 +0200 Subject: [PATCH 09/19] Fix(gas-station): copy evm types to gas station crate --- Cargo.lock | 43 +++--- evm-utils/evm-bridge/src/pool.rs | 18 +++ evm-utils/programs/gas_station/Cargo.toml | 6 +- .../src/evm_loader_instructions.rs | 126 ++++++++++++++++++ .../programs/gas_station/src/evm_types.rs | 41 ++++++ .../programs/gas_station/src/instruction.rs | 10 +- evm-utils/programs/gas_station/src/lib.rs | 5 +- .../programs/gas_station/src/processor.rs | 75 +++++++++-- evm-utils/programs/gas_station/src/state.rs | 4 +- 9 files changed, 287 insertions(+), 41 deletions(-) create mode 100644 evm-utils/programs/gas_station/src/evm_loader_instructions.rs create mode 100644 evm-utils/programs/gas_station/src/evm_types.rs diff --git a/Cargo.lock b/Cargo.lock index 14486129a9..e0782c5260 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1894,9 +1894,9 @@ version = "0.1.0" dependencies = [ "arrayref", "borsh", - "evm-rpc", "num-derive", "num-traits", + "primitive-types", "solana-evm-loader-program", "solana-program 1.9.29", "solana-program-test", @@ -3868,9 +3868,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.1.5" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9182e4a71cae089267ab03e67c99368db7cd877baf50f931e5d6d4b71e195ac0" +checksum = "366e44391a8af4cfd6002ef6ba072bae071a96aafca98d7d448a34c5dca38b6a" dependencies = [ "arrayvec", "bitvec", @@ -4830,7 +4830,7 @@ dependencies = [ "solana-program-runtime", "solana-sdk", "solana_rbpf", - "time 0.3.13", + "time 0.3.17", ] [[package]] @@ -5191,9 +5191,9 @@ dependencies = [ [[package]] name = "scale-info" -version = "2.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c46be926081c9f4dd5dd9b6f1d3e3229f2360bc6502dd8836f84a93b7c75e99a" +checksum = "88d8a765117b237ef233705cc2cc4c6a27fccd46eea6ef0c8c6dae5f3ef407f8" dependencies = [ "bitvec", "cfg-if 1.0.0", @@ -5204,9 +5204,9 @@ dependencies = [ [[package]] name = "scale-info-derive" -version = "2.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e334bb10a245e28e5fd755cabcafd96cfcd167c99ae63a46924ca8d8703a3c" +checksum = "cdcd47b380d8c4541044e341dcd9475f55ba37ddc50c908d945fc036a8642496" dependencies = [ "proc-macro-crate 1.2.1", "proc-macro2 1.0.43", @@ -5626,14 +5626,14 @@ checksum = "2a30f10c911c0355f80f1c2faa8096efc4a58cdf8590b954d5b395efa071c711" [[package]] name = "simple_logger" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "166fea527c36d9b8a0a88c0c6d4c5077c699d9ffb5cf890b231a3f08c35f3d40" +checksum = "48047e77b528151aaf841a10a9025f9459da80ba820e425ff7eb005708a76dc7" dependencies = [ "atty", "colored", "log 0.4.17", - "time 0.3.13", + "time 0.3.17", "winapi 0.3.9", ] @@ -8193,16 +8193,24 @@ dependencies = [ [[package]] name = "time" -version = "0.3.13" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ "itoa 1.0.3", "libc", "num_threads", - "time-macros 0.2.4", + "serde", + "time-core", + "time-macros 0.2.6", ] +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + [[package]] name = "time-macros" version = "0.1.1" @@ -8215,9 +8223,12 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] [[package]] name = "time-macros-impl" diff --git a/evm-utils/evm-bridge/src/pool.rs b/evm-utils/evm-bridge/src/pool.rs index 3aa7dea2f4..ddf83d7e28 100644 --- a/evm-utils/evm-bridge/src/pool.rs +++ b/evm-utils/evm-bridge/src/pool.rs @@ -599,6 +599,24 @@ async fn process_tx( .iter() .find(|val| matches!(tx.action, TransactionAction::Call(addr) if addr == val.contract)) { + let tx = evm_gas_station::evm_types::Transaction { + nonce: tx.nonce, + gas_price: tx.gas_price, + gas_limit: tx.gas_limit, + action: match tx.action { + TransactionAction::Create => evm_gas_station::evm_types::TransactionAction::Create, + TransactionAction::Call(addr) => { + evm_gas_station::evm_types::TransactionAction::Call(addr) + } + }, + value: tx.value, + signature: evm_gas_station::evm_types::TransactionSignature { + v: tx.signature.v, + r: tx.signature.r, + s: tx.signature.s, + }, + input: tx.input.clone(), + }; vec![evm_gas_station::execute_tx_with_payer( tx.clone(), bridge.gas_station_program_id.unwrap(), diff --git a/evm-utils/programs/gas_station/Cargo.toml b/evm-utils/programs/gas_station/Cargo.toml index c8034aaf3a..ab18ab2bcf 100644 --- a/evm-utils/programs/gas_station/Cargo.toml +++ b/evm-utils/programs/gas_station/Cargo.toml @@ -8,15 +8,15 @@ edition = "2021" [dependencies] arrayref = "0.3.6" borsh = "0.9.3" -evm-rpc = { path = "../../evm-rpc" } num-derive = "0.3.3" num-traits = "0.2.15" -solana-evm-loader-program = { path = "../evm_loader" } +primitive-types = { version = "0.11.0", default-features = false, features = ["borsh_no_std"] } solana-program = { path = "../../../sdk/program", version = "=1.9.29" } -solana-sdk = { path = "../../../sdk", version = "=1.9.29" } +solana-sdk = { path = "../../../sdk", version = "=1.9.29", default-features = false } thiserror = "1.0.37" [dev-dependencies] +solana-evm-loader-program = { path = "../evm_loader" } solana-program-test = { path = "../../../program-test", version = "=1.9.29" } tokio = { version = "~1.14.1", features = ["full"] } diff --git a/evm-utils/programs/gas_station/src/evm_loader_instructions.rs b/evm-utils/programs/gas_station/src/evm_loader_instructions.rs new file mode 100644 index 0000000000..f5bd7e8eb9 --- /dev/null +++ b/evm-utils/programs/gas_station/src/evm_loader_instructions.rs @@ -0,0 +1,126 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use super::evm_types::{Address, Transaction, UnsignedTransaction}; + +pub const EVM_INSTRUCTION_BORSH_PREFIX: u8 = 255u8; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq, Eq)] +pub enum FeePayerType { + Evm, + Native, +} + +impl FeePayerType { + pub fn is_evm(&self) -> bool { + *self == FeePayerType::Evm + } + pub fn is_native(&self) -> bool { + *self == FeePayerType::Native + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq, Eq)] +pub enum EvmBigTransaction { + /// Allocate data in storage, pay fee should be taken from EVM. + EvmTransactionAllocate { size: u64 }, + + /// Store part of EVM transaction into temporary storage, in order to execute it later. + EvmTransactionWrite { offset: u64, data: Vec }, +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq, Eq)] +pub enum ExecuteTransaction { + Signed { + tx: Option, + }, + ProgramAuthorized { + tx: Option, + from: Address, + }, +} + +#[allow(clippy::large_enum_variant)] +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq, Eq)] +pub enum EvmInstruction { + /// Transfer native lamports to ethereum. + /// + /// Outer args: + /// account_key[0] - `[writable]`. EVM state account, used for lock. + /// account_key[1] - `[writable, signer]`. Owner account that's allowed to manage withdrawal of his account by transfering ownership. + /// + /// Inner args: + /// amount - count of lamports to be transfered. + /// ether_key - recevier etherium address. + /// + SwapNativeToEther { + lamports: u64, + evm_address: Address, + }, + + /// Transfer user account ownership back to system program. + /// + /// Outer args: + /// account_key[0] - `[writable]`. EVM state account, used for lock. + /// account_key[1] - `[writable, signer]`. Owner account that's allowed to manage withdrawal of his account by transfering ownership. + /// + FreeOwnership {}, + + /// Allocate / push data / execute Big Transaction + /// + /// Outer args: + /// account_key[0] - `[writable]`. EVM state account. used for lock. + /// account_key[1] - `[writable]`. Big Transaction data storage. + EvmBigTransaction(EvmBigTransaction), + + /// Execute native EVM transaction + /// + /// Outer args: + /// account_key[0] - `[writable]`. EVM state account, used for lock. + /// account_key[1] - `[readable]`. Optional argument, used in case tokens swaps from EVM back to native. + /// + /// Outer args (Big tx case): + /// account_key[0] - `[writable]`. EVM state account. used for lock. + /// account_key[1] - `[writable]`. Big Transaction data storage. + /// + /// Inner args: + /// tx - information about transaction execution: + /// who authorized and whether or not should we get transaction from account data storage + /// fee_type - which side will be used for charging fee: Native or Evm + ExecuteTransaction { + tx: ExecuteTransaction, + fee_type: FeePayerType, + }, +} + +impl EvmInstruction { + pub fn new_execute_tx(tx: Transaction, fee_type: FeePayerType) -> Self { + Self::ExecuteTransaction { + tx: ExecuteTransaction::Signed { tx: Some(tx) }, + fee_type, + } + } + + pub fn new_execute_authorized_tx( + tx: UnsignedTransaction, + from: Address, + fee_type: FeePayerType, + ) -> Self { + Self::ExecuteTransaction { + tx: ExecuteTransaction::ProgramAuthorized { tx: Some(tx), from }, + fee_type, + } + } + + pub fn new_execute_big_tx(fee_type: FeePayerType) -> Self { + Self::ExecuteTransaction { + tx: ExecuteTransaction::Signed { tx: None }, + fee_type, + } + } + + pub fn new_execute_big_authorized_tx(from: Address, fee_type: FeePayerType) -> Self { + Self::ExecuteTransaction { + tx: ExecuteTransaction::ProgramAuthorized { tx: None, from }, + fee_type, + } + } +} \ No newline at end of file diff --git a/evm-utils/programs/gas_station/src/evm_types.rs b/evm-utils/programs/gas_station/src/evm_types.rs new file mode 100644 index 0000000000..72f5c25900 --- /dev/null +++ b/evm-utils/programs/gas_station/src/evm_types.rs @@ -0,0 +1,41 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use primitive_types::{H160, H256, U256}; + +pub type Address = H160; +pub type Gas = U256; + + +/// Etherium transaction. +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, PartialEq, Eq)] +pub struct Transaction { + pub nonce: U256, + pub gas_price: Gas, + pub gas_limit: Gas, + pub action: TransactionAction, + pub value: U256, + pub signature: TransactionSignature, + pub input: Vec, +} + +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, PartialEq, Eq)] +pub struct UnsignedTransaction { + pub nonce: U256, + pub gas_price: U256, + pub gas_limit: U256, + pub action: TransactionAction, + pub value: U256, + pub input: Vec, +} + +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, PartialEq, Eq)] +pub enum TransactionAction { + Call(Address), + Create, +} + +#[derive(BorshDeserialize, BorshSerialize, Clone, Debug, PartialEq, Eq)] +pub struct TransactionSignature { + pub v: u64, + pub r: H256, + pub s: H256, +} diff --git a/evm-utils/programs/gas_station/src/instruction.rs b/evm-utils/programs/gas_station/src/instruction.rs index d828ecaa1c..bc36c45e71 100644 --- a/evm-utils/programs/gas_station/src/instruction.rs +++ b/evm-utils/programs/gas_station/src/instruction.rs @@ -1,20 +1,20 @@ +use super::*; use borsh::{BorshDeserialize, BorshSerialize}; -use solana_evm_loader_program::scope::evm; use solana_sdk::pubkey::Pubkey; #[derive(Debug, BorshDeserialize, BorshSerialize, PartialEq)] pub enum TxFilter { InputStartsWith { - contract: evm::Address, + contract: evm_types::Address, input_prefix: Vec, }, } impl TxFilter { - pub fn is_match(&self, tx: &evm::Transaction) -> bool { + pub fn is_match(&self, tx: &evm_types::Transaction) -> bool { match self { Self::InputStartsWith{ contract, input_prefix } => { - matches!(tx.action, evm::TransactionAction::Call(addr) if addr == *contract) + matches!(tx.action, evm_types::TransactionAction::Call(addr) if addr == *contract) && tx.input.starts_with(&input_prefix) } } @@ -32,6 +32,6 @@ pub enum GasStationInstruction { /// Execute evm transaction ExecuteWithPayer { - tx: Option, + tx: Option, } } \ No newline at end of file diff --git a/evm-utils/programs/gas_station/src/lib.rs b/evm-utils/programs/gas_station/src/lib.rs index 401b33af76..a86f8b42e1 100644 --- a/evm-utils/programs/gas_station/src/lib.rs +++ b/evm-utils/programs/gas_station/src/lib.rs @@ -1,10 +1,11 @@ mod error; +mod evm_loader_instructions; +pub mod evm_types; mod instruction; mod processor; mod state; use processor::process_instruction; -use solana_evm_loader_program::scope::evm; use solana_program::instruction::{AccountMeta, Instruction}; use solana_program::pubkey::Pubkey; use solana_program::{entrypoint, system_program}; @@ -13,7 +14,7 @@ use solana_program::{entrypoint, system_program}; entrypoint!(process_instruction); pub fn execute_tx_with_payer( - tx: evm::Transaction, + tx: evm_types::Transaction, program_id: Pubkey, signer: Pubkey, storage: Pubkey, diff --git a/evm-utils/programs/gas_station/src/processor.rs b/evm-utils/programs/gas_station/src/processor.rs index f245f7eea0..a622881711 100644 --- a/evm-utils/programs/gas_station/src/processor.rs +++ b/evm-utils/programs/gas_station/src/processor.rs @@ -1,6 +1,5 @@ use super::*; use borsh::{BorshDeserialize, BorshSerialize}; -use solana_evm_loader_program::scope::evm; use solana_program::{program_memory::sol_memcmp, pubkey::PUBKEY_BYTES}; use solana_sdk::{ account_info::{next_account_info, AccountInfo}, @@ -22,6 +21,17 @@ use state::{Payer, MAX_FILTERS}; const EXECUTE_CALL_REFUND_AMOUNT: u64 = 10000; + +pub fn create_evm_instruction_with_borsh( + program_id: Pubkey, + data: &evm_loader_instructions::EvmInstruction, + accounts: Vec, +) -> Instruction { + let mut res = Instruction::new_with_borsh(program_id, data, accounts); + res.data.insert(0, evm_loader_instructions::EVM_INSTRUCTION_BORSH_PREFIX); + res +} + pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], @@ -99,7 +109,7 @@ fn process_register_payer( fn process_execute_with_payer( program_id: &Pubkey, accounts: &[AccountInfo], - tx: Option, + tx: Option, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let sender = next_account_info(account_info_iter)?; @@ -161,7 +171,7 @@ fn process_execute_with_payer( }; invoke_signed(&ix, &account_infos, signers_seeds)?; - let ix = solana_evm_loader_program::free_ownership(*payer_info.key); + let ix = make_free_ownership_ix(*payer_info.key); let account_infos = vec![evm_loader.clone(), evm_state.clone(), payer_info.clone()]; invoke_signed(&ix, &account_infos, signers_seeds)?; @@ -178,7 +188,7 @@ pub fn cmp_pubkeys(a: &Pubkey, b: &Pubkey) -> bool { sol_memcmp(a.as_ref(), b.as_ref(), PUBKEY_BYTES) == 0 } -fn get_big_tx_from_storage(storage_acc: &AccountInfo) -> Result { +fn get_big_tx_from_storage(storage_acc: &AccountInfo) -> Result { let mut bytes: &[u8] = &storage_acc.try_borrow_data().unwrap(); msg!("Trying to deserialize tx chunks byte = {:?}", bytes); BorshDeserialize::deserialize(&mut bytes) @@ -189,11 +199,11 @@ fn make_evm_loader_tx_execute_ix<'a>( evm_loader: &AccountInfo<'a>, evm_state: &AccountInfo<'a>, sender: &AccountInfo<'a>, - tx: evm::Transaction, + tx: evm_types::Transaction, ) -> (Instruction, Vec>) { - use solana_evm_loader_program::instructions::*; + use evm_loader_instructions::*; ( - solana_evm_loader_program::create_evm_instruction_with_borsh( + create_evm_instruction_with_borsh( *evm_loader.key, &EvmInstruction::ExecuteTransaction { tx: ExecuteTransaction::Signed { tx: Some(tx) }, @@ -214,9 +224,9 @@ fn make_evm_loader_big_tx_execute_ix<'a>( sender: &AccountInfo<'a>, big_tx_storage: &AccountInfo<'a>, ) -> (Instruction, Vec>) { - use solana_evm_loader_program::instructions::*; + use evm_loader_instructions::*; ( - solana_evm_loader_program::create_evm_instruction_with_borsh( + create_evm_instruction_with_borsh( *evm_loader.key, &EvmInstruction::ExecuteTransaction { tx: ExecuteTransaction::Signed { tx: None }, @@ -237,6 +247,18 @@ fn make_evm_loader_big_tx_execute_ix<'a>( ) } +fn make_free_ownership_ix(owner: Pubkey) -> Instruction { + use evm_loader_instructions::*; + create_evm_instruction_with_borsh( + solana_sdk::evm_loader::ID, + &EvmInstruction::FreeOwnership {}, + vec![ + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new(owner, true), + ], + ) +} + fn refund_native_fee(caller: &AccountInfo, payer: &AccountInfo, amount: u64) -> ProgramResult { **payer.try_borrow_mut_lamports()? = payer @@ -255,6 +277,7 @@ fn refund_native_fee(caller: &AccountInfo, payer: &AccountInfo, amount: u64) -> #[cfg(test)] mod test { use super::*; + use solana_evm_loader_program::scope::evm; use solana_program::instruction::InstructionError::{Custom, IncorrectProgramId}; use solana_program_test::{processor, ProgramTest}; use solana_sdk::{ @@ -269,8 +292,8 @@ mod test { const SECRET_KEY_DUMMY_TWOS: [u8; 32] = [2; 32]; const TEST_CHAIN_ID: u64 = 0xdead; - pub fn dummy_eth_tx(contract: evm::H160, input: Vec) -> evm::Transaction { - evm::UnsignedTransaction { + pub fn dummy_eth_tx(contract: evm::H160, input: Vec) -> evm_types::Transaction { + let tx = evm::UnsignedTransaction { nonce: evm::U256::zero(), gas_price: evm::U256::zero(), gas_limit: evm::U256::zero(), @@ -281,7 +304,20 @@ mod test { .sign( &evm::SecretKey::from_slice(&SECRET_KEY_DUMMY_ONES).unwrap(), Some(TEST_CHAIN_ID), - ) + ); + evm_types::Transaction { + nonce: tx.nonce, + gas_price: tx.gas_price, + gas_limit: tx.gas_limit, + action: evm_types::TransactionAction::Call(contract), + value: tx.value, + signature: evm_types::TransactionSignature { + v: tx.signature.v, + r: tx.signature.r, + s: tx.signature.s + }, + input: tx.input, + } } #[tokio::test] @@ -459,7 +495,7 @@ mod test { big_tx_storage.pubkey(), Account { lamports: 10000000, - owner: solana_evm_loader_program::ID, + owner: solana_sdk::evm_loader::ID, data: big_tx_bytes, ..Account::default() }, @@ -971,6 +1007,19 @@ mod test { &evm::SecretKey::from_slice(&SECRET_KEY_DUMMY_TWOS).unwrap(), Some(TEST_CHAIN_ID), ); + let tx = evm_types::Transaction { + nonce: tx.nonce, + gas_price: tx.gas_price, + gas_limit: tx.gas_limit, + action: evm_types::TransactionAction::Call(evm::H160::zero()), + value: tx.value, + signature: evm_types::TransactionSignature { + v: tx.signature.v, + r: tx.signature.r, + s: tx.signature.s + }, + input: tx.input, + }; let ix = Instruction::new_with_borsh( program_id, &GasStationInstruction::ExecuteWithPayer { tx: Some(tx) }, diff --git a/evm-utils/programs/gas_station/src/state.rs b/evm-utils/programs/gas_station/src/state.rs index e425157d05..74d94e1ef1 100644 --- a/evm-utils/programs/gas_station/src/state.rs +++ b/evm-utils/programs/gas_station/src/state.rs @@ -1,5 +1,5 @@ +use super::*; use borsh::{BorshDeserialize, BorshSerialize}; -use solana_evm_loader_program::scope::evm; use solana_sdk::{ program_pack::IsInitialized, pubkey::Pubkey, @@ -20,7 +20,7 @@ pub struct Payer { } impl Payer { - pub fn do_filter_match(&self, tx: &evm::Transaction) -> bool { + pub fn do_filter_match(&self, tx: &evm_types::Transaction) -> bool { self.filters.iter().any(|f| { f.is_match(tx) }) } } From b21d6622451875fe41bffafdc9147890472e2228 Mon Sep 17 00:00:00 2001 From: Vadim Date: Tue, 13 Dec 2022 15:07:33 +0200 Subject: [PATCH 10/19] Feat(gas-station): check evm accounts, check rent exemption, add tests --- evm-utils/programs/gas_station/src/error.rs | 4 + .../programs/gas_station/src/processor.rs | 183 +++++++++++++++++- 2 files changed, 183 insertions(+), 4 deletions(-) diff --git a/evm-utils/programs/gas_station/src/error.rs b/evm-utils/programs/gas_station/src/error.rs index 4dde35980a..baa80f0013 100644 --- a/evm-utils/programs/gas_station/src/error.rs +++ b/evm-utils/programs/gas_station/src/error.rs @@ -17,6 +17,10 @@ pub enum GasStationError { InvalidAccountBorshData, #[error("Unable to deserialize big transaction account data")] InvalidBigTransactionData, + #[error("Invalid evm loader account")] + InvalidEvmLoader, + #[error("Invalid evm state account")] + InvalidEvmState, #[error("Invalid filter amount")] InvalidFilterAmount, #[error("Lamport balance below rent-exempt threshold")] diff --git a/evm-utils/programs/gas_station/src/processor.rs b/evm-utils/programs/gas_station/src/processor.rs index a622881711..c8fdf30286 100644 --- a/evm-utils/programs/gas_station/src/processor.rs +++ b/evm-utils/programs/gas_station/src/processor.rs @@ -77,7 +77,7 @@ fn process_register_payer( let rent = Rent::get()?; let payer_data_len = storage_acc_info.data_len(); if !rent.is_exempt(storage_acc_info.lamports(), payer_data_len) { - return Err(ProgramError::AccountNotRentExempt); + return Err(GasStationError::NotRentExempt.into()); } let (payer_acc, bump_seed) = Pubkey::find_program_address(&[owner.as_ref()], program_id); @@ -128,6 +128,12 @@ fn process_execute_with_payer( if !cmp_pubkeys(program_id, payer_storage_info.owner) { return Err(ProgramError::IncorrectProgramId); } + if !cmp_pubkeys(evm_loader.key, &solana_sdk::evm_loader::ID) { + return Err(GasStationError::InvalidEvmLoader.into()); + } + if !cmp_pubkeys(evm_state.key, &solana_sdk::evm_state::ID) { + return Err(GasStationError::InvalidEvmState.into()); + } let mut payer_data_buf: &[u8] = &**payer_storage_info.data.borrow(); let payer: Payer = BorshDeserialize::deserialize(&mut payer_data_buf) .map_err(|_e| -> ProgramError { GasStationError::InvalidAccountBorshData.into() })?; @@ -181,7 +187,14 @@ fn process_execute_with_payer( } let refund_amount = EXECUTE_CALL_REFUND_AMOUNT; - refund_native_fee(sender, payer_info, refund_amount) + refund_native_fee(sender, payer_info, refund_amount)?; + + + let rent = Rent::get()?; + if !rent.is_exempt(payer_info.lamports(), payer_info.data_len()) { + return Err(GasStationError::NotRentExempt.into()); + } + Ok(()) } pub fn cmp_pubkeys(a: &Pubkey, b: &Pubkey) -> bool { @@ -825,7 +838,7 @@ mod test { .process_transaction(transaction) .await .unwrap_err(), - TransactionError(InstructionError(0, Custom(8))) + TransactionError(InstructionError(0, Custom(10))) )); } @@ -898,7 +911,7 @@ mod test { .process_transaction(transaction) .await .unwrap_err(), - TransactionError(InstructionError(0, Custom(9))) + TransactionError(InstructionError(0, Custom(11))) )); } @@ -1036,4 +1049,166 @@ mod test { TransactionError(InstructionError(0, Custom(18))) // NativeAccountInsufficientFunds from evm_loader )); } + + #[tokio::test] + async fn test_invalid_evm_accounts() { + let program_id = Pubkey::new_unique(); + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + + let user = Keypair::new(); + let owner = Keypair::new(); + let storage = Keypair::new(); + let (payer, _) = Pubkey::find_program_address(&[owner.pubkey().as_ref()], &program_id); + program_test.add_account( + user.pubkey(), + Account::new(1000000, 0, &system_program::id()), + ); + program_test.add_account(payer, Account::new(1000000, 0, &program_id)); + program_test.add_account( + solana_sdk::evm_state::ID, + solana_evm_loader_program::create_state_account(1000000).into(), + ); + let payer_data = Payer { + owner: owner.pubkey(), + payer, + filters: vec![TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + }], + }; + let mut payer_bytes = vec![]; + BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); + program_test.add_account( + storage.pubkey(), + Account { + lamports: 10000000, + owner: program_id, + data: payer_bytes, + ..Account::default() + }, + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + let third_party_keypair = Keypair::new(); + let account_metas_invalid_evm_loader = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(third_party_keypair.pubkey(), false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { + tx: Some(dummy_eth_tx(evm::H160::zero(), vec![])), + }, + account_metas_invalid_evm_loader, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + transaction.sign(&[&user], recent_blockhash); + assert!(matches!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err(), + TransactionError(InstructionError(0, Custom(6))) // InvalidEvmLoader + )); + + let account_metas_invalid_evm_state = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(third_party_keypair.pubkey(), false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { + tx: Some(dummy_eth_tx(evm::H160::zero(), vec![])), + }, + account_metas_invalid_evm_state, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + transaction.sign(&[&user], recent_blockhash); + assert!(matches!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err(), + TransactionError(InstructionError(0, Custom(7))) // InvalidEvmLoader + )); + } + + #[tokio::test] + async fn test_rent_exemption() { + let program_id = Pubkey::new_unique(); + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + + let user = Keypair::new(); + let owner = Keypair::new(); + let storage = Keypair::new(); + let (payer, _) = Pubkey::find_program_address(&[owner.pubkey().as_ref()], &program_id); + program_test.add_account( + user.pubkey(), + Account::new(1000000, 0, &system_program::id()), + ); + // 890880 for rent exemption + 42000 for evm execution + 10000 refund = 942880 needed + let payer_lamports = 942879; + program_test.add_account(payer, Account::new(payer_lamports, 0, &program_id)); + program_test.add_account( + solana_sdk::evm_state::ID, + solana_evm_loader_program::create_state_account(1000000).into(), + ); + let payer_data = Payer { + owner: owner.pubkey(), + payer, + filters: vec![TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + }], + }; + let mut payer_bytes = vec![]; + BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); + program_test.add_account( + storage.pubkey(), + Account { + lamports: 10000000, + owner: program_id, + data: payer_bytes, + ..Account::default() + }, + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + let account_metas = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(solana_sdk::evm_loader::ID, false), + AccountMeta::new(solana_sdk::evm_state::ID, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::ExecuteWithPayer { + tx: Some(dummy_eth_tx(evm::H160::zero(), vec![])), + }, + account_metas, + ); + let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + transaction.sign(&[&user], recent_blockhash); + assert!(matches!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err(), + TransactionError(InstructionError(0, Custom(9))) // NotRentExempt + )); + } } From 490f16e0c805a4e84f02197393ab1b4fe12c3009 Mon Sep 17 00:00:00 2001 From: Vadim Date: Thu, 15 Dec 2022 12:27:49 +0200 Subject: [PATCH 11/19] Feat(gas-station): disallow big transaction --- evm-utils/evm-bridge/src/pool.rs | 16 +--------------- evm-utils/programs/gas_station/src/error.rs | 2 ++ evm-utils/programs/gas_station/src/lib.rs | 2 +- evm-utils/programs/gas_station/src/processor.rs | 11 ++++++++++- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/evm-utils/evm-bridge/src/pool.rs b/evm-utils/evm-bridge/src/pool.rs index ddf83d7e28..6a28042897 100644 --- a/evm-utils/evm-bridge/src/pool.rs +++ b/evm-utils/evm-bridge/src/pool.rs @@ -820,21 +820,7 @@ async fn deploy_big_tx( .map_err(|e| into_native_error(e, bridge.verbose_errors))? .value; - let instructions = if let Some(val) = bridge - .redirect_to_proxy_filters - .iter() - .find(|val| matches!(tx.action, TransactionAction::Call(addr) if addr == val.contract)) - { - vec![evm_gas_station::execute_big_tx_with_payer( - bridge.gas_station_program_id.unwrap(), - bridge.key.pubkey(), - val.storage_acc, - val.payer, - storage_pubkey, - )] - } else { - bridge.make_send_big_tx_instructions(tx, storage_pubkey, payer_pubkey) - }; + let instructions = bridge.make_send_big_tx_instructions(tx, storage_pubkey, payer_pubkey); let execute_tx = solana::Transaction::new_signed_with_payer( &instructions, Some(&payer_pubkey), diff --git a/evm-utils/programs/gas_station/src/error.rs b/evm-utils/programs/gas_station/src/error.rs index baa80f0013..7b3f19608e 100644 --- a/evm-utils/programs/gas_station/src/error.rs +++ b/evm-utils/programs/gas_station/src/error.rs @@ -33,6 +33,8 @@ pub enum GasStationError { PdaAccountMismatch, #[error("Overflow occurred during transaction call refund")] RefundOverflow, + #[error("Functionality is not supported")] + NotSupported, } impl From for ProgramError { diff --git a/evm-utils/programs/gas_station/src/lib.rs b/evm-utils/programs/gas_station/src/lib.rs index a86f8b42e1..94d37d98d5 100644 --- a/evm-utils/programs/gas_station/src/lib.rs +++ b/evm-utils/programs/gas_station/src/lib.rs @@ -1,7 +1,7 @@ mod error; mod evm_loader_instructions; pub mod evm_types; -mod instruction; +pub mod instruction; mod processor; mod state; diff --git a/evm-utils/programs/gas_station/src/processor.rs b/evm-utils/programs/gas_station/src/processor.rs index c8fdf30286..bc1317d0d6 100644 --- a/evm-utils/programs/gas_station/src/processor.rs +++ b/evm-utils/programs/gas_station/src/processor.rs @@ -124,6 +124,9 @@ fn process_execute_with_payer( if !tx_passed_directly && big_tx_storage_info.is_err() { return Err(GasStationError::BigTxStorageMissing.into()); } + if !tx_passed_directly { // Big tx not supported at the moment + return Err(GasStationError::NotSupported.into()); + } if !cmp_pubkeys(program_id, payer_storage_info.owner) { return Err(ProgramError::IncorrectProgramId); @@ -547,7 +550,13 @@ mod test { )); let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); transaction.sign(&[&user, &big_tx_storage], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); + assert!(matches!( + banks_client + .process_transaction(transaction) + .await + .unwrap_err(), + TransactionError(InstructionError(0, Custom(14))) // NotSupported + )); } #[tokio::test] From 9f48a31bda50c4f4e215e0db6694300036bf5467 Mon Sep 17 00:00:00 2001 From: Vadim Date: Thu, 15 Dec 2022 15:42:22 +0200 Subject: [PATCH 12/19] Feat(gas-station): fix clippy --- evm-utils/evm-bridge/src/pool.rs | 2 +- .../src/evm_loader_instructions.rs | 43 ------------- .../programs/gas_station/src/instruction.rs | 2 +- evm-utils/programs/gas_station/src/lib.rs | 19 ++++++ .../programs/gas_station/src/processor.rs | 61 ++++++++++++------- 5 files changed, 59 insertions(+), 68 deletions(-) diff --git a/evm-utils/evm-bridge/src/pool.rs b/evm-utils/evm-bridge/src/pool.rs index 6a28042897..58aeba127d 100644 --- a/evm-utils/evm-bridge/src/pool.rs +++ b/evm-utils/evm-bridge/src/pool.rs @@ -618,7 +618,7 @@ async fn process_tx( input: tx.input.clone(), }; vec![evm_gas_station::execute_tx_with_payer( - tx.clone(), + tx, bridge.gas_station_program_id.unwrap(), bridge.key.pubkey(), val.storage_acc, diff --git a/evm-utils/programs/gas_station/src/evm_loader_instructions.rs b/evm-utils/programs/gas_station/src/evm_loader_instructions.rs index f5bd7e8eb9..4f352d9c95 100644 --- a/evm-utils/programs/gas_station/src/evm_loader_instructions.rs +++ b/evm-utils/programs/gas_station/src/evm_loader_instructions.rs @@ -9,15 +9,6 @@ pub enum FeePayerType { Native, } -impl FeePayerType { - pub fn is_evm(&self) -> bool { - *self == FeePayerType::Evm - } - pub fn is_native(&self) -> bool { - *self == FeePayerType::Native - } -} - #[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq, Eq)] pub enum EvmBigTransaction { /// Allocate data in storage, pay fee should be taken from EVM. @@ -89,38 +80,4 @@ pub enum EvmInstruction { tx: ExecuteTransaction, fee_type: FeePayerType, }, -} - -impl EvmInstruction { - pub fn new_execute_tx(tx: Transaction, fee_type: FeePayerType) -> Self { - Self::ExecuteTransaction { - tx: ExecuteTransaction::Signed { tx: Some(tx) }, - fee_type, - } - } - - pub fn new_execute_authorized_tx( - tx: UnsignedTransaction, - from: Address, - fee_type: FeePayerType, - ) -> Self { - Self::ExecuteTransaction { - tx: ExecuteTransaction::ProgramAuthorized { tx: Some(tx), from }, - fee_type, - } - } - - pub fn new_execute_big_tx(fee_type: FeePayerType) -> Self { - Self::ExecuteTransaction { - tx: ExecuteTransaction::Signed { tx: None }, - fee_type, - } - } - - pub fn new_execute_big_authorized_tx(from: Address, fee_type: FeePayerType) -> Self { - Self::ExecuteTransaction { - tx: ExecuteTransaction::ProgramAuthorized { tx: None, from }, - fee_type, - } - } } \ No newline at end of file diff --git a/evm-utils/programs/gas_station/src/instruction.rs b/evm-utils/programs/gas_station/src/instruction.rs index bc36c45e71..1fa011ed94 100644 --- a/evm-utils/programs/gas_station/src/instruction.rs +++ b/evm-utils/programs/gas_station/src/instruction.rs @@ -15,7 +15,7 @@ impl TxFilter { match self { Self::InputStartsWith{ contract, input_prefix } => { matches!(tx.action, evm_types::TransactionAction::Call(addr) if addr == *contract) - && tx.input.starts_with(&input_prefix) + && tx.input.starts_with(input_prefix) } } } diff --git a/evm-utils/programs/gas_station/src/lib.rs b/evm-utils/programs/gas_station/src/lib.rs index 94d37d98d5..b4c3444834 100644 --- a/evm-utils/programs/gas_station/src/lib.rs +++ b/evm-utils/programs/gas_station/src/lib.rs @@ -5,6 +5,7 @@ pub mod instruction; mod processor; mod state; +use borsh::BorshSerialize; use processor::process_instruction; use solana_program::instruction::{AccountMeta, Instruction}; use solana_program::pubkey::Pubkey; @@ -13,6 +14,24 @@ use solana_program::{entrypoint, system_program}; // Declare and export the program's entrypoint entrypoint!(process_instruction); +pub fn create_storage_account( + from_pubkey: &Pubkey, + to_pubkey: &Pubkey, + lamports: u64, + filters: &Vec, + owner: &Pubkey, +) -> Instruction { + let mut bytes = vec![]; + BorshSerialize::serialize(filters, &mut bytes).unwrap(); + solana_sdk::system_instruction::create_account( + from_pubkey, + to_pubkey, + lamports, + bytes.len() as u64 + 64, + owner, + ) +} + pub fn execute_tx_with_payer( tx: evm_types::Transaction, program_id: Pubkey, diff --git a/evm-utils/programs/gas_station/src/processor.rs b/evm-utils/programs/gas_station/src/processor.rs index bc1317d0d6..0dbeffaf90 100644 --- a/evm-utils/programs/gas_station/src/processor.rs +++ b/evm-utils/programs/gas_station/src/processor.rs @@ -21,14 +21,14 @@ use state::{Payer, MAX_FILTERS}; const EXECUTE_CALL_REFUND_AMOUNT: u64 = 10000; - pub fn create_evm_instruction_with_borsh( program_id: Pubkey, data: &evm_loader_instructions::EvmInstruction, accounts: Vec, ) -> Instruction { let mut res = Instruction::new_with_borsh(program_id, data, accounts); - res.data.insert(0, evm_loader_instructions::EVM_INSTRUCTION_BORSH_PREFIX); + res.data + .insert(0, evm_loader_instructions::EVM_INSTRUCTION_BORSH_PREFIX); res } @@ -124,7 +124,8 @@ fn process_execute_with_payer( if !tx_passed_directly && big_tx_storage_info.is_err() { return Err(GasStationError::BigTxStorageMissing.into()); } - if !tx_passed_directly { // Big tx not supported at the moment + if !tx_passed_directly { + // Big tx not supported at the moment return Err(GasStationError::NotSupported.into()); } @@ -184,7 +185,7 @@ fn process_execute_with_payer( let account_infos = vec![evm_loader.clone(), evm_state.clone(), payer_info.clone()]; invoke_signed(&ix, &account_infos, signers_seeds)?; - let ix = system_instruction::assign(payer_info.key, &program_id); + let ix = system_instruction::assign(payer_info.key, program_id); let account_infos = vec![system_program.clone(), payer_info.clone()]; invoke_signed(&ix, &account_infos, signers_seeds)?; } @@ -192,7 +193,6 @@ fn process_execute_with_payer( let refund_amount = EXECUTE_CALL_REFUND_AMOUNT; refund_native_fee(sender, payer_info, refund_amount)?; - let rent = Rent::get()?; if !rent.is_exempt(payer_info.lamports(), payer_info.data_len()) { return Err(GasStationError::NotRentExempt.into()); @@ -204,7 +204,9 @@ pub fn cmp_pubkeys(a: &Pubkey, b: &Pubkey) -> bool { sol_memcmp(a.as_ref(), b.as_ref(), PUBKEY_BYTES) == 0 } -fn get_big_tx_from_storage(storage_acc: &AccountInfo) -> Result { +fn get_big_tx_from_storage( + storage_acc: &AccountInfo, +) -> Result { let mut bytes: &[u8] = &storage_acc.try_borrow_data().unwrap(); msg!("Trying to deserialize tx chunks byte = {:?}", bytes); BorshDeserialize::deserialize(&mut bytes) @@ -276,17 +278,14 @@ fn make_free_ownership_ix(owner: Pubkey) -> Instruction { } fn refund_native_fee(caller: &AccountInfo, payer: &AccountInfo, amount: u64) -> ProgramResult { - **payer.try_borrow_mut_lamports()? = - payer - .lamports() - .checked_sub(amount) - .ok_or(ProgramError::from( - GasStationError::InsufficientPayerBalance, - ))?; + **payer.try_borrow_mut_lamports()? = payer + .lamports() + .checked_sub(amount) + .ok_or_else(|| ProgramError::from(GasStationError::InsufficientPayerBalance))?; **caller.try_borrow_mut_lamports()? = caller .lamports() .checked_add(amount) - .ok_or(ProgramError::from(GasStationError::RefundOverflow))?; + .ok_or_else(|| ProgramError::from(GasStationError::RefundOverflow))?; Ok(()) } @@ -330,7 +329,7 @@ mod test { signature: evm_types::TransactionSignature { v: tx.signature.v, r: tx.signature.r, - s: tx.signature.s + s: tx.signature.s, }, input: tx.input, } @@ -352,9 +351,15 @@ mod test { ..Account::default() }, ); + let mut bytes = vec![]; + let filters = vec![TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + }]; + BorshSerialize::serialize(&filters, &mut bytes).unwrap(); program_test.add_account( storage.pubkey(), - Account::new(10000000, 93, &program_id), + Account::new(10000000, bytes.len() + 64, &program_id), ); let (mut banks_client, _, recent_blockhash) = program_test.start().await; @@ -384,7 +389,11 @@ mod test { transaction.sign(&[&creator], recent_blockhash); banks_client.process_transaction(transaction).await.unwrap(); - let account = banks_client.get_account(storage.pubkey()).await.unwrap().unwrap(); + let account = banks_client + .get_account(storage.pubkey()) + .await + .unwrap() + .unwrap(); assert_eq!(account.owner, program_id); assert_eq!(account.lamports, 10000000); assert_eq!(account.data.len(), 93); @@ -393,15 +402,21 @@ mod test { assert_eq!(payer.payer, payer_key); assert_eq!(payer.owner, creator.pubkey()); assert_eq!(payer.filters.len(), 1); - assert_eq!(payer.filters[0], TxFilter::InputStartsWith { - contract: evm::Address::zero(), - input_prefix: vec![], - }); + assert_eq!( + payer.filters[0], + TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + } + ); let rent = banks_client.get_rent().await.unwrap(); let pda_account = banks_client.get_account(payer_key).await.unwrap().unwrap(); assert_eq!(pda_account.owner, program_id); - assert_eq!(pda_account.lamports, rent.minimum_balance(0) + transfer_amount); + assert_eq!( + pda_account.lamports, + rent.minimum_balance(0) + transfer_amount + ); } #[tokio::test] @@ -1038,7 +1053,7 @@ mod test { signature: evm_types::TransactionSignature { v: tx.signature.v, r: tx.signature.r, - s: tx.signature.s + s: tx.signature.s, }, input: tx.input, }; From 3761db15c6934dbc25cfaeae3a29c88ba7d1455a Mon Sep 17 00:00:00 2001 From: Vadim Date: Thu, 5 Jan 2023 16:27:58 +0200 Subject: [PATCH 13/19] Feat(gas-station): bump bpf version --- sdk/bpf/scripts/install.sh | 2 +- sdk/cargo-build-bpf/src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/bpf/scripts/install.sh b/sdk/bpf/scripts/install.sh index 1a42b647ca..205d98407f 100755 --- a/sdk/bpf/scripts/install.sh +++ b/sdk/bpf/scripts/install.sh @@ -102,7 +102,7 @@ if [[ ! -e criterion-$version.md || ! -e criterion ]]; then fi # Install Rust-BPF -version=v1.25 +version=v1.29 if [[ ! -e bpf-tools-$version.md || ! -e bpf-tools ]]; then ( set -e diff --git a/sdk/cargo-build-bpf/src/main.rs b/sdk/cargo-build-bpf/src/main.rs index 92666134ac..a161982b7f 100644 --- a/sdk/cargo-build-bpf/src/main.rs +++ b/sdk/cargo-build-bpf/src/main.rs @@ -477,7 +477,7 @@ fn build_bpf_package(config: &Config, target_directory: &Path, package: &cargo_m // The following line is scanned by CI configuration script to // separate cargo caches according to the version of sbf-tools. - let bpf_tools_version = "v1.25"; + let bpf_tools_version = "v1.29"; let package = "bpf-tools"; let target_path = home_dir .join(".cache") From b7bed61d09e29be980f2ac87c419b1cac42e071a Mon Sep 17 00:00:00 2001 From: Vadim Date: Tue, 17 Jan 2023 22:18:39 +0200 Subject: [PATCH 14/19] Feat(gas-station): add CLI command to register payer --- Cargo.lock | 2 + cli/Cargo.toml | 1 + cli/src/cli.rs | 2 +- cli/src/evm.rs | 187 +++++++++++++++++- evm-utils/programs/gas_station/Cargo.toml | 3 +- evm-utils/programs/gas_station/README.md | 17 ++ .../programs/gas_station/src/instruction.rs | 3 +- evm-utils/programs/gas_station/src/lib.rs | 33 +++- evm-utils/programs/gas_station/src/state.rs | 6 + 9 files changed, 240 insertions(+), 14 deletions(-) create mode 100644 evm-utils/programs/gas_station/README.md diff --git a/Cargo.lock b/Cargo.lock index e0782c5260..2a6e231170 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1897,6 +1897,7 @@ dependencies = [ "num-derive", "num-traits", "primitive-types", + "serde", "solana-evm-loader-program", "solana-program 1.9.29", "solana-program-test", @@ -6052,6 +6053,7 @@ dependencies = [ "const_format", "criterion-stats", "ctrlc", + "evm-gas-station", "evm-rpc", "evm-state", "hex", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0add84c63f..a3d6f7cf37 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -46,6 +46,7 @@ spl-memo = { version = "=3.0.1", features = ["no-entrypoint"] } thiserror = "1.0.30" tiny-bip39 = "0.8.2" +evm-gas-station = { path = "../evm-utils/programs/gas_station" } evm-state = { path = "../evm-utils/evm-state" } evm-rpc = { path = "../evm-utils/evm-rpc" } solana-evm-loader-program = { path = "../evm-utils/programs/evm_loader" } diff --git a/cli/src/cli.rs b/cli/src/cli.rs index fffa63ae76..545e16679e 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -895,7 +895,7 @@ pub fn parse_command( } ("transfer", Some(matches)) => parse_transfer(matches, default_signer, wallet_manager), // - ("evm", Some(matches)) => parse_evm_subcommand(matches), + ("evm", Some(matches)) => parse_evm_subcommand(matches, default_signer, wallet_manager), // ("", None) => { eprintln!("{}", matches.usage()); diff --git a/cli/src/evm.rs b/cli/src/evm.rs index f2d9f95209..6a6a036006 100644 --- a/cli/src/evm.rs +++ b/cli/src/evm.rs @@ -1,8 +1,11 @@ use std::{ convert::Infallible, - fs, io, + fs, + fs::File, + io, path::{Path, PathBuf}, str::FromStr, + sync::Arc, }; use anyhow::anyhow; @@ -13,14 +16,22 @@ use solana_sdk::{ commitment_config::CommitmentConfig, message::Message, native_token::{lamports_to_sol, LAMPORTS_PER_VLX}, + pubkey::Pubkey, + system_instruction, system_program, transaction::Transaction, }; use crate::cli::{CliCommand, CliCommandInfo, CliConfig, CliError}; +use crate::checks::check_unique_pubkeys; +use evm_gas_station::instruction::TxFilter; use evm_rpc::Hex; use evm_state::{self as evm, FromKey}; +use solana_clap_utils::input_parsers::signer_of; +use solana_clap_utils::input_validators::is_valid_signer; +use solana_clap_utils::keypair::{DefaultSigner, SignerIndex}; use solana_evm_loader_program::{instructions::FeePayerType, scope::evm::gweis_to_lamports}; +use solana_remote_wallet::remote_wallet::RemoteWalletManager; const SECRET_KEY_DUMMY: [u8; 32] = [1; 32]; @@ -68,6 +79,47 @@ impl EvmSubCommands for App<'_, '_> { .long("lamports") .help("Amount in lamports"))) + .subcommand( + SubCommand::with_name("create-gas-station-payer") + .about("Create payer account for gas station program") + .display_order(3) + .arg( + Arg::with_name("payer_account") + .index(1) + .value_name("ACCOUNT_KEYPAIR") + .takes_value(true) + .required(true) + .validator(is_valid_signer) + .help("Keypair of the payer storage account"), + ).arg( + Arg::with_name("payer_owner") + .index(2) + .value_name("ACCOUNT_KEYPAIR") + .takes_value(true) + .required(true) + .validator(is_valid_signer) + .help("Keypair of the owner account"), + ) + .arg(Arg::with_name("gas-station-key") + .index(3) + .takes_value(true) + .required(true) + .value_name("PROGRAM ID") + .help("Public key of gas station program")) + .arg(Arg::with_name("lamports") + .index(4) + .takes_value(true) + .required(true) + .value_name("AMOUNT") + .help("Amount in lamports to transfer to created account")) + .arg(Arg::with_name("filters_path") + .index(5) + .takes_value(true) + .required(true) + .value_name("PATH") + .help("Path to json file with filter to store in payer storage")) + ) + // Hidden commands @@ -152,6 +204,14 @@ pub enum EvmCliCommand { amount: u64, }, + CreateGasStationPayer { + payer_signer_index: SignerIndex, + payer_owner_signer_index: SignerIndex, + gas_station_key: Pubkey, + lamports: u64, + filters: PathBuf, + }, + // Hidden commands SendRawTx { raw_tx: PathBuf, @@ -192,6 +252,27 @@ impl EvmCliCommand { Self::TransferToEvm { address, amount } => { transfer(rpc_client, config, *address, *amount)?; } + Self::CreateGasStationPayer { + payer_signer_index, + payer_owner_signer_index, + gas_station_key, + lamports, + filters, + } => { + println!( + "CreateGasStationPayer: {}, {}, {:?}", + gas_station_key, lamports, filters + ); + create_gas_station_payer( + rpc_client, + config, + *payer_signer_index, + *payer_owner_signer_index, + *gas_station_key, + *lamports, + filters, + )?; + } // Hidden commands Self::SendRawTx { raw_tx } => { send_raw_tx(rpc_client, config, raw_tx)?; @@ -262,8 +343,8 @@ fn transfer( let message = Message::new(&ixs, Some(&from.pubkey())); let mut create_account_tx = Transaction::new_unsigned(message); - let (blockhash, _last_height) = rpc_client - .get_latest_blockhash_with_commitment(CommitmentConfig::default())?; + let (blockhash, _last_height) = + rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::default())?; create_account_tx.sign(&config.signers, blockhash); @@ -276,6 +357,65 @@ fn transfer( Ok(()) } +fn create_gas_station_payer>( + rpc_client: &RpcClient, + config: &CliConfig, + payer_signer_index: SignerIndex, + payer_owner_signer_index: SignerIndex, + gas_station_key: Pubkey, + transfer_amount: u64, + filters: P, +) -> anyhow::Result<()> { + let cli_pubkey = config.signers[0].pubkey(); + let payer_storage_pubkey = config.signers[payer_signer_index].pubkey(); + let payer_owner_pubkey = config.signers[payer_owner_signer_index].pubkey(); + check_unique_pubkeys( + (&payer_storage_pubkey, "payer_storage_pubkey".to_string()), + (&payer_owner_pubkey, "payer_owner_pubkey".to_string()), + )?; + + let file = File::open(filters) + .map_err(|e| custom_error(format!("Unable to open filters file: {:?}", e)))?; + let filters: Vec = serde_json::from_reader(file) + .map_err(|e| custom_error(format!("Unable to decode json: {:?}", e)))?; + + let create_owner_ix = system_instruction::create_account( + &cli_pubkey, + &payer_owner_pubkey, + rpc_client.get_minimum_balance_for_rent_exemption(0)?, + 0, + &system_program::id(), + ); + let state_size = evm_gas_station::get_state_size(&filters); + let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption(state_size)?; + let create_storage_ix = evm_gas_station::create_storage_account( + &cli_pubkey, + &payer_storage_pubkey, + minimum_balance, + &filters, + &gas_station_key, + ); + let register_payer_ix = evm_gas_station::register_payer( + gas_station_key, + cli_pubkey, + payer_storage_pubkey, + payer_owner_pubkey, + transfer_amount, + filters, + ); + let message = Message::new( + &[create_owner_ix, create_storage_ix, register_payer_ix], + Some(&cli_pubkey), + ); + let latest_blockhash = rpc_client.get_latest_blockhash()?; + + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, latest_blockhash)?; + let signature = rpc_client.send_and_confirm_transaction_with_spinner(&tx)?; + println!("Transaction signature = {}", signature); + Ok(()) +} + fn find_block_header( rpc_client: &RpcClient, expected_block_hash: evm::H256, @@ -332,8 +472,8 @@ fn send_raw_tx>( let msg = Message::new(&[ix], Some(&signer.pubkey())); let mut tx = Transaction::new_unsigned(msg); - let (blockhash, _last_height) = rpc_client - .get_latest_blockhash_with_commitment(CommitmentConfig::default())?; + let (blockhash, _last_height) = + rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::default())?; tx.sign(&config.signers, blockhash); debug!("sending tx: {:?}", tx); @@ -397,7 +537,12 @@ fn call_dummy( Ok(()) } -pub fn parse_evm_subcommand(matches: &ArgMatches<'_>) -> Result { +pub fn parse_evm_subcommand( + matches: &ArgMatches<'_>, + default_signer: &DefaultSigner, + wallet_manager: &mut Option>, +) -> Result { + let mut signers = vec![]; let subcommand = match matches.subcommand() { ("get-evm-balance", Some(matches)) => { assert!(matches.is_present("key_source")); @@ -431,6 +576,35 @@ pub fn parse_evm_subcommand(matches: &ArgMatches<'_>) -> Result { + signers = vec![default_signer.signer_from_path(matches, wallet_manager)?]; + let (payer_signer, _address) = signer_of(matches, "payer_account", wallet_manager)?; + let (payer_owner_signer, _address) = signer_of(matches, "payer_owner", wallet_manager)?; + let payer_signer_index = payer_signer + .map(|signer| { + signers.push(signer); + 1 + }) + .unwrap(); + let payer_owner_signer_index = payer_owner_signer + .map(|signer| { + signers.push(signer); + 2 + }) + .unwrap(); + + let gas_station_key = value_t_or_exit!(matches, "gas-station-key", Pubkey); + let lamports = value_t_or_exit!(matches, "lamports", u64); + let filters = value_t_or_exit!(matches, "filters_path", PathBuf); + + EvmCliCommand::CreateGasStationPayer { + payer_signer_index, + payer_owner_signer_index, + gas_station_key, + lamports, + filters, + } + } ("send-raw-tx", Some(matches)) => { let raw_tx = value_t_or_exit!(matches, "raw_tx", PathBuf); EvmCliCommand::SendRawTx { raw_tx } @@ -464,7 +638,6 @@ pub fn parse_evm_subcommand(matches: &ArgMatches<'_>) -> Result, owner: &Pubkey, ) -> Instruction { - let mut bytes = vec![]; - BorshSerialize::serialize(filters, &mut bytes).unwrap(); solana_sdk::system_instruction::create_account( from_pubkey, to_pubkey, lamports, - bytes.len() as u64 + 64, + get_state_size(filters) as u64, owner, ) } +pub fn register_payer( + program_id: Pubkey, + signer: Pubkey, + storage: Pubkey, + owner: Pubkey, + transfer_amount: u64, + filters: Vec, +) -> Instruction { + let (payer_key, _) = Pubkey::find_program_address(&[owner.as_ref()], &program_id); + let account_metas = vec![ + AccountMeta::new(signer, true), + AccountMeta::new(storage, false), + AccountMeta::new(payer_key, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + Instruction::new_with_borsh( + program_id, + &instruction::GasStationInstruction::RegisterPayer { + owner, + transfer_amount, + whitelist: filters, + }, + account_metas, + ) +} + pub fn execute_tx_with_payer( tx: evm_types::Transaction, program_id: Pubkey, diff --git a/evm-utils/programs/gas_station/src/state.rs b/evm-utils/programs/gas_station/src/state.rs index 74d94e1ef1..45a62927c2 100644 --- a/evm-utils/programs/gas_station/src/state.rs +++ b/evm-utils/programs/gas_station/src/state.rs @@ -8,6 +8,12 @@ use crate::instruction::TxFilter; pub const MAX_FILTERS: usize = 10; +pub fn get_state_size(filters: &Vec) -> usize { + let mut bytes = vec![]; + BorshSerialize::serialize(filters, &mut bytes).unwrap(); + bytes.len() + 64 +} + #[repr(C)] #[derive(BorshDeserialize, BorshSerialize, Debug)] pub struct Payer { From 723331bcfafcc077f045554ddd715bd3f5bc4516 Mon Sep 17 00:00:00 2001 From: Vadim Date: Wed, 18 Jan 2023 16:39:38 +0200 Subject: [PATCH 15/19] Feat(gas-station): add readme; change bridge command line args --- evm-utils/evm-bridge/src/main.rs | 14 +++++-- evm-utils/programs/gas_station/README.md | 52 ++++++++++++++++++++---- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/evm-utils/evm-bridge/src/main.rs b/evm-utils/evm-bridge/src/main.rs index 7e0e84f7fc..1a267ccab1 100644 --- a/evm-utils/evm-bridge/src/main.rs +++ b/evm-utils/evm-bridge/src/main.rs @@ -254,7 +254,14 @@ impl EvmBridge { redirect_to_proxy_filters: Vec, ) { self.gas_station_program_id = Some(gas_station_program_id); - self.redirect_to_proxy_filters = redirect_to_proxy_filters; + self.redirect_to_proxy_filters = redirect_to_proxy_filters + .into_iter() + .map(|mut item| { + let (payer_key, _) = Pubkey::find_program_address(&[item.owner.as_ref()], &gas_station_program_id); + item.payer = payer_key; + item + }) + .collect(); } /// Wrap evm tx into solana, optionally add meta keys, to solana signature. @@ -1016,6 +1023,7 @@ pub enum ParseEvmContractToPayerKeysError { #[derive(Debug)] struct EvmContractToPayerKeys { contract: Address, + owner: Pubkey, payer: Pubkey, storage_acc: Pubkey, } @@ -1032,11 +1040,11 @@ impl FromStr for EvmContractToPayerKeys { .ok_or(ParseEvmContractToPayerKeysError::InvalidFormat)?; let contract = Address::from_str(addr) .map_err(|_| ParseEvmContractToPayerKeysError::InvalidEvmContract)?; - let payer = Pubkey::from_str(key1) + let owner = Pubkey::from_str(key1) .map_err(|_| ParseEvmContractToPayerKeysError::InvalidPubkey)?; let storage_acc = Pubkey::from_str(key2) .map_err(|_| ParseEvmContractToPayerKeysError::InvalidPubkey)?; - Ok(Self { contract, payer, storage_acc }) + Ok(Self { contract, owner, payer: Pubkey::default(), storage_acc }) } } diff --git a/evm-utils/programs/gas_station/README.md b/evm-utils/programs/gas_station/README.md index 3f184cbd20..86309aa14e 100644 --- a/evm-utils/programs/gas_station/README.md +++ b/evm-utils/programs/gas_station/README.md @@ -1,17 +1,53 @@ -## Build +## Build and deploy + +Build program: ``./cargo-build-bpf -- -p evm-gas-station`` -## Deploy +Resulting .so file will be located at *./target/deploy/* + +Deploy program: + +``velas program deploy -u -k path/to/evm_gas_station.so`` + +## User-Program-Bridge interaction + +On testnet you can use deployed gas station program: **99GZJejoz8L521qEgjDbQ4ucheCsw17SL8FWCYXmDCH7** + +### Register payer + +Given you have successfully deployed gas station program your next step is +to register payer account that will be paying for incoming evm transactions +if they meet its filter conditions: + +``velas evm create-gas-station-payer -u -k + `` + +Where: -``./target/release/velas program deploy -u t -k ../keypairs/main.testnet.json ./target/deploy/evm_gas_station.so`` +- *signer keypair* - path to keypair of an account that will pay for this transaction +- *storage keypair* - path to keypair of payer storage account that will hold filters data +- *owner keypair* - keypair of payer owner account that will have write access to payer storage (for a future use) +- *program id* - gas station program id +- *lamports* - amount of tokens (above rent exemption) that will be transferred to gas station pda account to pay for future evm transactions +- *filters file* - path to JSON file with filters to store in payer storage -## Create payer +Example *filters file*: +``` +[ + { "InputStartsWith": [ "", ] } +] +``` -Create velas account for payer storage: +### Start bridge -``./target/release/velas evm create-gas-station-payer -u t -k ../keypairs/main.testnet.json ../keypairs/testnet_payer_owner.json 8K3MnqwSAuhKezhP1aW63mjiC46NiroUAkVyfiQFvz79 1566000 ../tmp/whitelist_b.json`` +Run evm-bridge command with next additional options: +``--gas-station --redirect-to-proxy ::`` -Register payer on gas station: +Where: +- *program id* - gas station program id +- *evm contract address* - address of evm contract you want to pay for. It should match one of addresses provided in *filters file* during register payer step +- *payer owner pubkey* - pubkey of a payer owner account. It should match the pubkey of *owner keypair* used during register payer step +- *payer storage pubkey* - pubkey of a payer storage account. It should match the pubkey of *storage keypair* used during register payer step -```` +After these steps bridge will be redirecting incoming evm transactions to gas station program. From d5cd1fe3c62bb1b8fef5c1370ababaedd2e7befe Mon Sep 17 00:00:00 2001 From: Vadim Date: Wed, 25 Jan 2023 21:21:53 +0200 Subject: [PATCH 16/19] Feat(gas-station): improove logging, fix naming, change CLI args, fix readme accordingly, add script to deploy test gas-station, create payer and run bridge --- cli/src/evm.rs | 60 +++++++++------- evm-utils/evm-bridge/src/main.rs | 71 ++++++++++++------- evm-utils/evm-bridge/src/pool.rs | 4 +- evm-utils/programs/gas_station/README.md | 10 ++- evm-utils/programs/gas_station/quickstart.sh | 34 +++++++++ .../programs/gas_station/src/processor.rs | 2 +- 6 files changed, 124 insertions(+), 57 deletions(-) create mode 100755 evm-utils/programs/gas_station/quickstart.sh diff --git a/cli/src/evm.rs b/cli/src/evm.rs index 6a6a036006..4b72f8fb41 100644 --- a/cli/src/evm.rs +++ b/cli/src/evm.rs @@ -30,6 +30,7 @@ use evm_state::{self as evm, FromKey}; use solana_clap_utils::input_parsers::signer_of; use solana_clap_utils::input_validators::is_valid_signer; use solana_clap_utils::keypair::{DefaultSigner, SignerIndex}; +use solana_client::rpc_response::Response; use solana_evm_loader_program::{instructions::FeePayerType, scope::evm::gweis_to_lamports}; use solana_remote_wallet::remote_wallet::RemoteWalletManager; @@ -83,41 +84,37 @@ impl EvmSubCommands for App<'_, '_> { SubCommand::with_name("create-gas-station-payer") .about("Create payer account for gas station program") .display_order(3) - .arg( - Arg::with_name("payer_account") + .arg(Arg::with_name("payer_account") .index(1) .value_name("ACCOUNT_KEYPAIR") .takes_value(true) .required(true) .validator(is_valid_signer) - .help("Keypair of the payer storage account"), - ).arg( - Arg::with_name("payer_owner") - .index(2) - .value_name("ACCOUNT_KEYPAIR") - .takes_value(true) - .required(true) - .validator(is_valid_signer) - .help("Keypair of the owner account"), - ) + .help("Keypair of the payer storage account")) .arg(Arg::with_name("gas-station-key") - .index(3) + .index(2) .takes_value(true) .required(true) .value_name("PROGRAM ID") .help("Public key of gas station program")) .arg(Arg::with_name("lamports") - .index(4) + .index(3) .takes_value(true) .required(true) .value_name("AMOUNT") .help("Amount in lamports to transfer to created account")) .arg(Arg::with_name("filters_path") - .index(5) + .index(4) .takes_value(true) .required(true) .value_name("PATH") .help("Path to json file with filter to store in payer storage")) + .arg(Arg::with_name("payer_owner") + .index(5) + .value_name("ACCOUNT_KEYPAIR") + .takes_value(true) + .validator(is_valid_signer) + .help("Keypair of the owner account")) ) @@ -379,13 +376,20 @@ fn create_gas_station_payer>( let filters: Vec = serde_json::from_reader(file) .map_err(|e| custom_error(format!("Unable to decode json: {:?}", e)))?; - let create_owner_ix = system_instruction::create_account( - &cli_pubkey, - &payer_owner_pubkey, - rpc_client.get_minimum_balance_for_rent_exemption(0)?, - 0, - &system_program::id(), - ); + let mut instructions = vec![]; + if let Response { value: None, .. } = + rpc_client.get_account_with_commitment(&payer_owner_pubkey, CommitmentConfig::default())? + { + let create_owner_ix = system_instruction::create_account( + &cli_pubkey, + &payer_owner_pubkey, + rpc_client.get_minimum_balance_for_rent_exemption(0)?, + 0, + &system_program::id(), + ); + info!("Add instruction to create owner: {}", payer_owner_pubkey); + instructions.push(create_owner_ix); + } let state_size = evm_gas_station::get_state_size(&filters); let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption(state_size)?; let create_storage_ix = evm_gas_station::create_storage_account( @@ -395,6 +399,8 @@ fn create_gas_station_payer>( &filters, &gas_station_key, ); + info!("Add instruction to create storage: {}", payer_storage_pubkey); + instructions.push(create_storage_ix); let register_payer_ix = evm_gas_station::register_payer( gas_station_key, cli_pubkey, @@ -403,10 +409,10 @@ fn create_gas_station_payer>( transfer_amount, filters, ); - let message = Message::new( - &[create_owner_ix, create_storage_ix, register_payer_ix], - Some(&cli_pubkey), - ); + info!("Add instruction to register payer: gas-station={}, signer={}, storage={}, owner={}", + gas_station_key, cli_pubkey, payer_storage_pubkey, payer_owner_pubkey); + instructions.push(register_payer_ix); + let message = Message::new(&instructions, Some(&cli_pubkey)); let latest_blockhash = rpc_client.get_latest_blockhash()?; let mut tx = Transaction::new_unsigned(message); @@ -591,7 +597,7 @@ pub fn parse_evm_subcommand( signers.push(signer); 2 }) - .unwrap(); + .unwrap_or(0); let gas_station_key = value_t_or_exit!(matches, "gas-station-key", Pubkey); let lamports = value_t_or_exit!(matches, "lamports", u64); diff --git a/evm-utils/evm-bridge/src/main.rs b/evm-utils/evm-bridge/src/main.rs index 1a267ccab1..f350ade70f 100644 --- a/evm-utils/evm-bridge/src/main.rs +++ b/evm-utils/evm-bridge/src/main.rs @@ -257,8 +257,9 @@ impl EvmBridge { self.redirect_to_proxy_filters = redirect_to_proxy_filters .into_iter() .map(|mut item| { - let (payer_key, _) = Pubkey::find_program_address(&[item.owner.as_ref()], &gas_station_program_id); - item.payer = payer_key; + let (payer_key, _) = + Pubkey::find_program_address(&[item.owner.as_ref()], &gas_station_program_id); + item.gas_station_payer = payer_key; item }) .collect(); @@ -1012,19 +1013,19 @@ pub(crate) fn from_client_error(client_error: ClientError) -> evm_rpc::Error { #[derive(thiserror::Error, Debug, PartialEq)] pub enum ParseEvmContractToPayerKeysError { - #[error("Evm contract string is invalid")] - InvalidEvmContract, - #[error("Input format is invalid, provide string of the next format: \":\"")] - InvalidFormat, - #[error("Invalid pubkey")] - InvalidPubkey, + #[error("Evm contract string is invalid: `{0}`")] + InvalidEvmContract(String), + #[error("Input format is invalid: `{0}`, provide string of the next format: \"::\"")] + InvalidFormat(String), + #[error("Invalid pubkey: `{0}`")] + InvalidPubkey(String), } #[derive(Debug)] struct EvmContractToPayerKeys { contract: Address, owner: Pubkey, - payer: Pubkey, + gas_station_payer: Pubkey, storage_acc: Pubkey, } @@ -1032,19 +1033,30 @@ impl FromStr for EvmContractToPayerKeys { type Err = ParseEvmContractToPayerKeysError; fn from_str(s: &str) -> StdResult { - let (addr, keys) = s - .split_once(':') - .ok_or(ParseEvmContractToPayerKeysError::InvalidFormat)?; - let (key1, key2) = keys - .split_once(':') - .ok_or(ParseEvmContractToPayerKeysError::InvalidFormat)?; - let contract = Address::from_str(addr) - .map_err(|_| ParseEvmContractToPayerKeysError::InvalidEvmContract)?; - let owner = Pubkey::from_str(key1) - .map_err(|_| ParseEvmContractToPayerKeysError::InvalidPubkey)?; - let storage_acc = Pubkey::from_str(key2) - .map_err(|_| ParseEvmContractToPayerKeysError::InvalidPubkey)?; - Ok(Self { contract, owner, payer: Pubkey::default(), storage_acc }) + let (contract, keys) = + s.split_once(':') + .ok_or(ParseEvmContractToPayerKeysError::InvalidFormat( + s.to_string(), + ))?; + let (owner, storage_acc) = + keys.split_once(':') + .ok_or(ParseEvmContractToPayerKeysError::InvalidFormat( + s.to_string(), + ))?; + let contract = Address::from_str(contract).map_err(|_| { + ParseEvmContractToPayerKeysError::InvalidEvmContract(contract.to_string()) + })?; + let owner = Pubkey::from_str(owner) + .map_err(|_| ParseEvmContractToPayerKeysError::InvalidPubkey(owner.to_string()))?; + let storage_acc = Pubkey::from_str(storage_acc).map_err(|_| { + ParseEvmContractToPayerKeysError::InvalidPubkey(storage_acc.to_string()) + })?; + Ok(Self { + contract, + owner, + gas_station_payer: Pubkey::default(), + storage_acc, + }) } } @@ -1078,7 +1090,7 @@ struct Args { #[structopt(long = "gas-station")] gas_station_program_id: Option, #[structopt(long = "redirect-to-proxy")] - redirect_contracts_to_proxy: Option>, + redirect_contracts_to_proxy: Vec, } impl Args { @@ -1173,9 +1185,16 @@ async fn main(args: Args) -> StdResult<(), Box> { min_gas_price, ); meta.set_whitelist(whitelist); - if let Some(redirect_list) = args.redirect_contracts_to_proxy { - let gas_station_program_id = args.gas_station_program_id.expect("gas-station program id is missing"); - meta.set_redirect_to_proxy_filters(gas_station_program_id, redirect_list); + if !args.redirect_contracts_to_proxy.is_empty() { + let gas_station_program_id = args + .gas_station_program_id + .expect("gas-station program id is missing"); + info!("Redirecting evm transaction to gas station: {}, filters: {:?}", + gas_station_program_id, args.redirect_contracts_to_proxy); + meta.set_redirect_to_proxy_filters( + gas_station_program_id, + args.redirect_contracts_to_proxy, + ); } let meta = Arc::new(meta); diff --git a/evm-utils/evm-bridge/src/pool.rs b/evm-utils/evm-bridge/src/pool.rs index 58aeba127d..99152e585f 100644 --- a/evm-utils/evm-bridge/src/pool.rs +++ b/evm-utils/evm-bridge/src/pool.rs @@ -599,6 +599,8 @@ async fn process_tx( .iter() .find(|val| matches!(tx.action, TransactionAction::Call(addr) if addr == val.contract)) { + info!("Bridge filters matched. Sending transaction {} to gas station {}", + tx.tx_id_hash(), bridge.gas_station_program_id.unwrap()); let tx = evm_gas_station::evm_types::Transaction { nonce: tx.nonce, gas_price: tx.gas_price, @@ -622,7 +624,7 @@ async fn process_tx( bridge.gas_station_program_id.unwrap(), bridge.key.pubkey(), val.storage_acc, - val.payer, + val.gas_station_payer, )] } else { bridge.make_send_tx_instructions(&tx, &meta_keys) diff --git a/evm-utils/programs/gas_station/README.md b/evm-utils/programs/gas_station/README.md index 86309aa14e..c5e297871f 100644 --- a/evm-utils/programs/gas_station/README.md +++ b/evm-utils/programs/gas_station/README.md @@ -14,6 +14,10 @@ Deploy program: On testnet you can use deployed gas station program: **99GZJejoz8L521qEgjDbQ4ucheCsw17SL8FWCYXmDCH7** +As a test evm contract you can use this one: **0x507AAe92E8a024feDCbB521d11EC406eEfB4488F**. +It has one method that accepts uint256. Valid input data will be *0x6057361d* as a method selector following by encoded uint256 +Example input data (passing 1 as an argument): *0x6057361d0000000000000000000000000000000000000000000000000000000000000001* + ### Register payer Given you have successfully deployed gas station program your next step is @@ -21,16 +25,18 @@ to register payer account that will be paying for incoming evm transactions if they meet its filter conditions: ``velas evm create-gas-station-payer -u -k - `` + []`` Where: - *signer keypair* - path to keypair of an account that will pay for this transaction - *storage keypair* - path to keypair of payer storage account that will hold filters data -- *owner keypair* - keypair of payer owner account that will have write access to payer storage (for a future use) - *program id* - gas station program id - *lamports* - amount of tokens (above rent exemption) that will be transferred to gas station pda account to pay for future evm transactions - *filters file* - path to JSON file with filters to store in payer storage +- *owner keypair* - (**OPTIONAL**) keypair of payer owner account that will have write access to payer storage (for a future use) + Default value: *signer keypair* + *Only one registered payer per owner supported* Example *filters file*: ``` diff --git a/evm-utils/programs/gas_station/quickstart.sh b/evm-utils/programs/gas_station/quickstart.sh new file mode 100755 index 0000000000..d1c941ee7a --- /dev/null +++ b/evm-utils/programs/gas_station/quickstart.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +evm_contract=0x507AAe92E8a024feDCbB521d11EC406eEfB4488F; + +signer_keypair=$1; +out_dir=$2; +project_root=$3; + +gas_station_keypair=$out_dir/gas_station_keypair.json; +payer_info_storage_keypair=$out_dir/payer_storage_keypair.json; + +mkdir -p "$out_dir" +velas-keygen new -o "$payer_info_storage_keypair" +velas-keygen new -o "$gas_station_keypair" + +owner_key=$(velas -u t -k "$signer_keypair" address) +storage_key=$(velas -u t -k "$payer_info_storage_keypair" address) +gas_station_key=$(velas -u t -k "$gas_station_keypair" address) +echo "Keys used: signer/owner: $owner_key, storage: $storage_key, gas_station: $gas_station_key" + +echo Building.. +"$project_root"/cargo-build-bpf -- -p evm-gas-station +echo Deploying.. +velas program deploy -u t -k "$signer_keypair" --program-id "$gas_station_keypair" "$project_root"/target/deploy/evm_gas_station.so + +echo "Registering payer.." +gas_station_filter=$out_dir/gas_station_filter.json +echo "[{ \"InputStartsWith\": [ \"$evm_contract\", [96, 87, 54, 29] ] }]" > "$gas_station_filter" +velas evm create-gas-station-payer -u t -k "$signer_keypair" \ + "$payer_info_storage_keypair" "$gas_station_key" 100000 "$gas_station_filter" + +echo "Starting bridge.." +RUST_LOG=info evm-bridge "$signer_keypair" https://api.testnet.velas.com 127.0.0.1:8545 111 \ + --gas-station "$gas_station_key" --redirect-to-proxy "$evm_contract:$owner_key:$storage_key" diff --git a/evm-utils/programs/gas_station/src/processor.rs b/evm-utils/programs/gas_station/src/processor.rs index 0dbeffaf90..7830fa88e4 100644 --- a/evm-utils/programs/gas_station/src/processor.rs +++ b/evm-utils/programs/gas_station/src/processor.rs @@ -96,7 +96,7 @@ fn process_register_payer( system_program.clone(), ], &[&[owner.as_ref(), &[bump_seed]]], - )?; + )?; // TODO: map into something readable msg!("PDA created: {}", payer_acc); payer.owner = owner; From 3b53badabfcb1f210c83235d7bb07cf0121b2cc0 Mon Sep 17 00:00:00 2001 From: Vadim Date: Thu, 26 Jan 2023 10:43:49 +0200 Subject: [PATCH 17/19] Feat(gas-station): add usage to script --- evm-utils/programs/gas_station/quickstart.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/evm-utils/programs/gas_station/quickstart.sh b/evm-utils/programs/gas_station/quickstart.sh index d1c941ee7a..262c97a8bc 100755 --- a/evm-utils/programs/gas_station/quickstart.sh +++ b/evm-utils/programs/gas_station/quickstart.sh @@ -2,6 +2,11 @@ evm_contract=0x507AAe92E8a024feDCbB521d11EC406eEfB4488F; +if [[ $# -lt 3 ]] ; then + echo 'Usage: ./quickstart.sh path/to/signer_keypair.json ' + exit 0 +fi + signer_keypair=$1; out_dir=$2; project_root=$3; From 033113831ad40ca2a7d5dfb257a36d64380a961c Mon Sep 17 00:00:00 2001 From: Vadim Date: Fri, 27 Jan 2023 17:47:24 +0200 Subject: [PATCH 18/19] Feat(gas-station): add instruction to update filters, add tests, add CLI command for update --- cli/src/evm.rs | 115 ++++- evm-utils/programs/gas_station/README.md | 2 +- evm-utils/programs/gas_station/quickstart.sh | 4 +- evm-utils/programs/gas_station/src/error.rs | 4 + .../programs/gas_station/src/instruction.rs | 7 +- evm-utils/programs/gas_station/src/lib.rs | 20 + .../programs/gas_station/src/processor.rs | 466 +++++++++++++----- evm-utils/programs/gas_station/src/state.rs | 3 +- 8 files changed, 464 insertions(+), 157 deletions(-) diff --git a/cli/src/evm.rs b/cli/src/evm.rs index 4b72f8fb41..df33dc3259 100644 --- a/cli/src/evm.rs +++ b/cli/src/evm.rs @@ -28,7 +28,7 @@ use evm_gas_station::instruction::TxFilter; use evm_rpc::Hex; use evm_state::{self as evm, FromKey}; use solana_clap_utils::input_parsers::signer_of; -use solana_clap_utils::input_validators::is_valid_signer; +use solana_clap_utils::input_validators::{is_pubkey, is_valid_signer}; use solana_clap_utils::keypair::{DefaultSigner, SignerIndex}; use solana_client::rpc_response::Response; use solana_evm_loader_program::{instructions::FeePayerType, scope::evm::gweis_to_lamports}; @@ -84,7 +84,7 @@ impl EvmSubCommands for App<'_, '_> { SubCommand::with_name("create-gas-station-payer") .about("Create payer account for gas station program") .display_order(3) - .arg(Arg::with_name("payer_account") + .arg(Arg::with_name("payer_storage_account") .index(1) .value_name("ACCOUNT_KEYPAIR") .takes_value(true) @@ -116,6 +116,31 @@ impl EvmSubCommands for App<'_, '_> { .validator(is_valid_signer) .help("Keypair of the owner account")) ) + .subcommand( + SubCommand::with_name("update-gas-station-payer") + .about("Update filters in payer account for gas station program") + .display_order(4) + .arg( + Arg::with_name("payer_storage_pubkey") + .index(1) + .value_name("PUBKEY") + .takes_value(true) + .required(true) + .validator(is_pubkey) + .help("The pubkey of the payer storage account to update")) + .arg(Arg::with_name("gas-station-key") + .index(2) + .takes_value(true) + .required(true) + .value_name("PROGRAM ID") + .help("Public key of gas station program")) + .arg(Arg::with_name("filters_path") + .index(3) + .takes_value(true) + .required(true) + .value_name("PATH") + .help("Path to json file with filter to store in payer storage")) + ) // Hidden commands @@ -202,13 +227,19 @@ pub enum EvmCliCommand { }, CreateGasStationPayer { - payer_signer_index: SignerIndex, + payer_storage_signer_index: SignerIndex, payer_owner_signer_index: SignerIndex, gas_station_key: Pubkey, lamports: u64, filters: PathBuf, }, + UpdateGasStationPayer { + payer_storage_pubkey: Pubkey, + gas_station_key: Pubkey, + filters: PathBuf, + }, + // Hidden commands SendRawTx { raw_tx: PathBuf, @@ -250,26 +281,35 @@ impl EvmCliCommand { transfer(rpc_client, config, *address, *amount)?; } Self::CreateGasStationPayer { - payer_signer_index, + payer_storage_signer_index, payer_owner_signer_index, gas_station_key, lamports, filters, } => { - println!( - "CreateGasStationPayer: {}, {}, {:?}", - gas_station_key, lamports, filters - ); create_gas_station_payer( rpc_client, config, - *payer_signer_index, + *payer_storage_signer_index, *payer_owner_signer_index, *gas_station_key, *lamports, filters, )?; } + Self::UpdateGasStationPayer { + payer_storage_pubkey, + gas_station_key, + filters, + } => { + update_gas_station_payer( + rpc_client, + config, + *payer_storage_pubkey, + *gas_station_key, + filters, + )?; + } // Hidden commands Self::SendRawTx { raw_tx } => { send_raw_tx(rpc_client, config, raw_tx)?; @@ -357,14 +397,14 @@ fn transfer( fn create_gas_station_payer>( rpc_client: &RpcClient, config: &CliConfig, - payer_signer_index: SignerIndex, + payer_storage_signer_index: SignerIndex, payer_owner_signer_index: SignerIndex, gas_station_key: Pubkey, transfer_amount: u64, filters: P, ) -> anyhow::Result<()> { let cli_pubkey = config.signers[0].pubkey(); - let payer_storage_pubkey = config.signers[payer_signer_index].pubkey(); + let payer_storage_pubkey = config.signers[payer_storage_signer_index].pubkey(); let payer_owner_pubkey = config.signers[payer_owner_signer_index].pubkey(); check_unique_pubkeys( (&payer_storage_pubkey, "payer_storage_pubkey".to_string()), @@ -409,8 +449,10 @@ fn create_gas_station_payer>( transfer_amount, filters, ); - info!("Add instruction to register payer: gas-station={}, signer={}, storage={}, owner={}", - gas_station_key, cli_pubkey, payer_storage_pubkey, payer_owner_pubkey); + info!( + "Add instruction to register payer: gas-station={}, signer={}, storage={}, owner={}", + gas_station_key, cli_pubkey, payer_storage_pubkey, payer_owner_pubkey + ); instructions.push(register_payer_ix); let message = Message::new(&instructions, Some(&cli_pubkey)); let latest_blockhash = rpc_client.get_latest_blockhash()?; @@ -422,6 +464,35 @@ fn create_gas_station_payer>( Ok(()) } +fn update_gas_station_payer>( + rpc_client: &RpcClient, + config: &CliConfig, + payer_storage_pubkey: Pubkey, + gas_station_key: Pubkey, + filters: P, +) -> anyhow::Result<()> { + let file = File::open(filters) + .map_err(|e| custom_error(format!("Unable to open filters file: {:?}", e)))?; + let filters: Vec = serde_json::from_reader(file) + .map_err(|e| custom_error(format!("Unable to decode json: {:?}", e)))?; + + let sender_pubkey = config.signers[0].pubkey(); + let ix = evm_gas_station::update_filters( + gas_station_key, + sender_pubkey, + payer_storage_pubkey, + filters, + ); + let message = Message::new(&[ix], Some(&sender_pubkey)); + let latest_blockhash = rpc_client.get_latest_blockhash()?; + + let mut tx = Transaction::new_unsigned(message); + tx.try_sign(&config.signers, latest_blockhash)?; + let signature = rpc_client.send_and_confirm_transaction_with_spinner(&tx)?; + println!("Transaction signature = {}", signature); + Ok(()) +} + fn find_block_header( rpc_client: &RpcClient, expected_block_hash: evm::H256, @@ -584,9 +655,10 @@ pub fn parse_evm_subcommand( } ("create-gas-station-payer", Some(matches)) => { signers = vec![default_signer.signer_from_path(matches, wallet_manager)?]; - let (payer_signer, _address) = signer_of(matches, "payer_account", wallet_manager)?; + let (payer_storage_signer, _address) = + signer_of(matches, "payer_storage_account", wallet_manager)?; let (payer_owner_signer, _address) = signer_of(matches, "payer_owner", wallet_manager)?; - let payer_signer_index = payer_signer + let payer_storage_signer_index = payer_storage_signer .map(|signer| { signers.push(signer); 1 @@ -604,13 +676,24 @@ pub fn parse_evm_subcommand( let filters = value_t_or_exit!(matches, "filters_path", PathBuf); EvmCliCommand::CreateGasStationPayer { - payer_signer_index, + payer_storage_signer_index, payer_owner_signer_index, gas_station_key, lamports, filters, } } + ("update-gas-station-payer", Some(matches)) => { + let payer_storage_pubkey = value_t_or_exit!(matches, "payer_storage_pubkey", Pubkey); + let gas_station_key = value_t_or_exit!(matches, "gas-station-key", Pubkey); + let filters = value_t_or_exit!(matches, "filters_path", PathBuf); + + EvmCliCommand::UpdateGasStationPayer { + payer_storage_pubkey, + gas_station_key, + filters, + } + } ("send-raw-tx", Some(matches)) => { let raw_tx = value_t_or_exit!(matches, "raw_tx", PathBuf); EvmCliCommand::SendRawTx { raw_tx } diff --git a/evm-utils/programs/gas_station/README.md b/evm-utils/programs/gas_station/README.md index c5e297871f..bef097ded1 100644 --- a/evm-utils/programs/gas_station/README.md +++ b/evm-utils/programs/gas_station/README.md @@ -12,7 +12,7 @@ Deploy program: ## User-Program-Bridge interaction -On testnet you can use deployed gas station program: **99GZJejoz8L521qEgjDbQ4ucheCsw17SL8FWCYXmDCH7** +On testnet you can use deployed gas station program: **6KJGNdovYX3NrzfEYDTdhbwQbTvoWHAVrz9buuEPKthy** As a test evm contract you can use this one: **0x507AAe92E8a024feDCbB521d11EC406eEfB4488F**. It has one method that accepts uint256. Valid input data will be *0x6057361d* as a method selector following by encoded uint256 diff --git a/evm-utils/programs/gas_station/quickstart.sh b/evm-utils/programs/gas_station/quickstart.sh index 262c97a8bc..0f15168248 100755 --- a/evm-utils/programs/gas_station/quickstart.sh +++ b/evm-utils/programs/gas_station/quickstart.sh @@ -21,7 +21,6 @@ velas-keygen new -o "$gas_station_keypair" owner_key=$(velas -u t -k "$signer_keypair" address) storage_key=$(velas -u t -k "$payer_info_storage_keypair" address) gas_station_key=$(velas -u t -k "$gas_station_keypair" address) -echo "Keys used: signer/owner: $owner_key, storage: $storage_key, gas_station: $gas_station_key" echo Building.. "$project_root"/cargo-build-bpf -- -p evm-gas-station @@ -32,8 +31,9 @@ echo "Registering payer.." gas_station_filter=$out_dir/gas_station_filter.json echo "[{ \"InputStartsWith\": [ \"$evm_contract\", [96, 87, 54, 29] ] }]" > "$gas_station_filter" velas evm create-gas-station-payer -u t -k "$signer_keypair" \ - "$payer_info_storage_keypair" "$gas_station_key" 100000 "$gas_station_filter" + "$payer_info_storage_keypair" "$gas_station_key" 1000000 "$gas_station_filter" +echo "Keys used: signer/owner: $owner_key, storage: $storage_key, gas_station: $gas_station_key" echo "Starting bridge.." RUST_LOG=info evm-bridge "$signer_keypair" https://api.testnet.velas.com 127.0.0.1:8545 111 \ --gas-station "$gas_station_key" --redirect-to-proxy "$evm_contract:$owner_key:$storage_key" diff --git a/evm-utils/programs/gas_station/src/error.rs b/evm-utils/programs/gas_station/src/error.rs index 7b3f19608e..f61eebddc7 100644 --- a/evm-utils/programs/gas_station/src/error.rs +++ b/evm-utils/programs/gas_station/src/error.rs @@ -7,10 +7,14 @@ pub enum GasStationError { /// The account cannot be initialized because it is already being used. #[error("Account is already in use")] AccountInUse, + #[error("Account isn't authorized for this instruction")] + AccountNotAuthorized, #[error("Account storage isn't uninitialized")] AccountNotInitialized, #[error("Account info for big transaction storage is missing")] BigTxStorageMissing, + #[error("Filters provided in instruction are the same as in storage")] + FiltersNotChanged, #[error("Payer is unable to pay for transaction")] InsufficientPayerBalance, #[error("Unable to deserialize borsh encoded account data")] diff --git a/evm-utils/programs/gas_station/src/instruction.rs b/evm-utils/programs/gas_station/src/instruction.rs index ed497c1957..dca2cecfb1 100644 --- a/evm-utils/programs/gas_station/src/instruction.rs +++ b/evm-utils/programs/gas_station/src/instruction.rs @@ -3,7 +3,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use serde::Deserialize; use solana_sdk::pubkey::Pubkey; -#[derive(Debug, Deserialize, BorshDeserialize, BorshSerialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, BorshDeserialize, BorshSerialize, PartialEq)] pub enum TxFilter { InputStartsWith { contract: evm_types::Address, @@ -31,6 +31,11 @@ pub enum GasStationInstruction { whitelist: Vec, }, + /// Update filters + UpdateFilters { + whitelist: Vec, + }, + /// Execute evm transaction ExecuteWithPayer { tx: Option, diff --git a/evm-utils/programs/gas_station/src/lib.rs b/evm-utils/programs/gas_station/src/lib.rs index d30c2e7042..51cabb9263 100644 --- a/evm-utils/programs/gas_station/src/lib.rs +++ b/evm-utils/programs/gas_station/src/lib.rs @@ -57,6 +57,26 @@ pub fn register_payer( ) } +pub fn update_filters( + program_id: Pubkey, + signer: Pubkey, + storage: Pubkey, + filters: Vec, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(signer, true), + AccountMeta::new(storage, false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + Instruction::new_with_borsh( + program_id, + &instruction::GasStationInstruction::UpdateFilters { + whitelist: filters, + }, + account_metas, + ) +} + pub fn execute_tx_with_payer( tx: evm_types::Transaction, program_id: Pubkey, diff --git a/evm-utils/programs/gas_station/src/processor.rs b/evm-utils/programs/gas_station/src/processor.rs index 7830fa88e4..339ff4d42e 100644 --- a/evm-utils/programs/gas_station/src/processor.rs +++ b/evm-utils/programs/gas_station/src/processor.rs @@ -6,7 +6,7 @@ use solana_sdk::{ entrypoint::ProgramResult, instruction::{AccountMeta, Instruction}, msg, - program::invoke_signed, + program::{invoke, invoke_signed}, program_error::ProgramError, program_pack::IsInitialized, pubkey::Pubkey, @@ -17,7 +17,7 @@ use solana_sdk::{ use error::GasStationError; use instruction::{GasStationInstruction, TxFilter}; -use state::{Payer, MAX_FILTERS}; +use state::{Payer, MAX_FILTERS, PAYER_STATE_SIZE_WITHOUT_FILTERS}; const EXECUTE_CALL_REFUND_AMOUNT: u64 = 10000; @@ -32,6 +32,13 @@ pub fn create_evm_instruction_with_borsh( res } +fn check_whitelist(whitelist: &[TxFilter]) -> ProgramResult { + if whitelist.is_empty() || whitelist.len() > MAX_FILTERS { + return Err(GasStationError::InvalidFilterAmount.into()); + } + Ok(()) +} + pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], @@ -46,6 +53,9 @@ pub fn process_instruction( transfer_amount, whitelist, } => process_register_payer(program_id, accounts, owner, transfer_amount, whitelist), + GasStationInstruction::UpdateFilters { whitelist } => { + process_update_filters(program_id, accounts, whitelist) + } GasStationInstruction::ExecuteWithPayer { tx } => { process_execute_with_payer(program_id, accounts, tx) } @@ -59,9 +69,8 @@ fn process_register_payer( transfer_amount: u64, whitelist: Vec, ) -> ProgramResult { - if whitelist.is_empty() || whitelist.len() > MAX_FILTERS { - return Err(GasStationError::InvalidFilterAmount.into()); - } + check_whitelist(&whitelist)?; + let account_info_iter = &mut accounts.iter(); let creator_info = next_account_info(account_info_iter)?; let storage_acc_info = next_account_info(account_info_iter)?; @@ -106,6 +115,61 @@ fn process_register_payer( Ok(()) } +fn process_update_filters( + program_id: &Pubkey, + accounts: &[AccountInfo], + whitelist: Vec, +) -> ProgramResult { + check_whitelist(&whitelist)?; + + let account_info_iter = &mut accounts.iter(); + let owner_info = next_account_info(account_info_iter)?; + let storage_acc_info = next_account_info(account_info_iter)?; + let system_program = next_account_info(account_info_iter)?; + + if !owner_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + if !cmp_pubkeys(program_id, storage_acc_info.owner) { + return Err(ProgramError::IncorrectProgramId); + } + + let payer: Payer = BorshDeserialize::deserialize(&mut &**storage_acc_info.data.borrow()) + .map_err(|_e| -> ProgramError { GasStationError::InvalidAccountBorshData.into() })?; + if !payer.is_initialized() { + return Err(GasStationError::AccountNotInitialized.into()); + } + if !cmp_pubkeys(owner_info.key, &payer.owner) { + return Err(GasStationError::AccountNotAuthorized.into()); + } + if whitelist == payer.filters { + return Err(GasStationError::FiltersNotChanged.into()); + } + + let mut new_filters_bytes = vec![]; + BorshSerialize::serialize(&whitelist, &mut new_filters_bytes).unwrap(); + let new_total_size = PAYER_STATE_SIZE_WITHOUT_FILTERS + new_filters_bytes.len(); + let lamports_required = (Rent::get()?).minimum_balance(new_total_size); + if lamports_required > storage_acc_info.lamports() { + let diff = lamports_required - storage_acc_info.lamports(); + invoke( + &system_instruction::transfer(owner_info.key, storage_acc_info.key, diff), + &[ + owner_info.clone(), + storage_acc_info.clone(), + system_program.clone(), + ], + )?; + } + + if new_total_size != storage_acc_info.data_len() { + storage_acc_info.realloc(new_total_size, false)?; + } + storage_acc_info.data.borrow_mut()[PAYER_STATE_SIZE_WITHOUT_FILTERS..new_total_size] + .copy_from_slice(&new_filters_bytes); + Ok(()) +} + fn process_execute_with_payer( program_id: &Pubkey, accounts: &[AccountInfo], @@ -292,6 +356,7 @@ fn refund_native_fee(caller: &AccountInfo, payer: &AccountInfo, amount: u64) -> #[cfg(test)] mod test { use super::*; + use solana_evm_loader_program::error::EvmError; use solana_evm_loader_program::scope::evm; use solana_program::instruction::InstructionError::{Custom, IncorrectProgramId}; use solana_program_test::{processor, ProgramTest}; @@ -302,6 +367,7 @@ mod test { transaction::{Transaction, TransactionError::InstructionError}, transport::TransportError::TransactionError, }; + use std::str::FromStr; const SECRET_KEY_DUMMY_ONES: [u8; 32] = [1; 32]; const SECRET_KEY_DUMMY_TWOS: [u8; 32] = [2; 32]; @@ -335,10 +401,16 @@ mod test { } } + pub fn dummy_filters() -> Vec { + vec![TxFilter::InputStartsWith { + contract: evm::Address::zero(), + input_prefix: vec![], + }] + } + #[tokio::test] async fn test_register_payer() { let program_id = Pubkey::new_unique(); - let mut program_test = ProgramTest::new("gas-station", program_id, processor!(process_instruction)); @@ -352,10 +424,7 @@ mod test { }, ); let mut bytes = vec![]; - let filters = vec![TxFilter::InputStartsWith { - contract: evm::Address::zero(), - input_prefix: vec![], - }]; + let filters = dummy_filters(); BorshSerialize::serialize(&filters, &mut bytes).unwrap(); program_test.add_account( storage.pubkey(), @@ -378,16 +447,13 @@ mod test { &GasStationInstruction::RegisterPayer { owner: creator.pubkey(), transfer_amount, - whitelist: vec![TxFilter::InputStartsWith { - contract: evm::Address::zero(), - input_prefix: vec![], - }], + whitelist: filters, }, account_metas, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&creator.pubkey())); - transaction.sign(&[&creator], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); + let mut tx = Transaction::new_with_payer(&[ix], Some(&creator.pubkey())); + tx.sign(&[&creator], recent_blockhash); + banks_client.process_transaction(tx).await.unwrap(); let account = banks_client .get_account(storage.pubkey()) @@ -419,6 +485,151 @@ mod test { ); } + #[tokio::test] + async fn test_update_filters() { + let program_id = Pubkey::new_unique(); + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + + let user = Keypair::new(); + let storage = Keypair::new(); + let (payer, _) = Pubkey::find_program_address(&[user.pubkey().as_ref()], &program_id); + program_test.add_account( + user.pubkey(), + Account::new(1000000, 0, &system_program::id()), + ); + program_test.add_account(payer, Account::new(1000000, 0, &program_id)); + program_test.add_account( + solana_sdk::evm_state::ID, + solana_evm_loader_program::create_state_account(1000000).into(), + ); + let payer_data = Payer { + owner: user.pubkey(), + payer, + filters: dummy_filters(), + }; + let mut payer_bytes = vec![]; + BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); + program_test.add_account( + storage.pubkey(), + Account { + lamports: 10000000, + owner: program_id, + data: payer_bytes, + ..Account::default() + }, + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + let account_metas = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let new_filters = vec![TxFilter::InputStartsWith { + contract: evm::Address::from_str("0x507AAe92E8a024feDCbB521d11EC406eEfB4488F") + .unwrap(), + input_prefix: vec![96, 87, 54, 29], + }]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::UpdateFilters { + whitelist: new_filters.clone(), + }, + account_metas, + ); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + banks_client.process_transaction(tx).await.unwrap(); + + let storage_account = banks_client + .get_account(storage.pubkey()) + .await + .unwrap() + .unwrap(); + let updated_payer: Payer = BorshDeserialize::deserialize(&mut &*storage_account.data).unwrap(); + assert_eq!(new_filters, updated_payer.filters); + } + + #[tokio::test] + async fn test_shrink_filters() { + let program_id = Pubkey::new_unique(); + let mut program_test = + ProgramTest::new("gas-station", program_id, processor!(process_instruction)); + + let user = Keypair::new(); + let storage = Keypair::new(); + let (payer, _) = Pubkey::find_program_address(&[user.pubkey().as_ref()], &program_id); + program_test.add_account( + user.pubkey(), + Account::new(1000000, 0, &system_program::id()), + ); + program_test.add_account(payer, Account::new(1000000, 0, &program_id)); + program_test.add_account( + solana_sdk::evm_state::ID, + solana_evm_loader_program::create_state_account(1000000).into(), + ); + let payer_data = Payer { + owner: user.pubkey(), + payer, + filters: vec![ + TxFilter::InputStartsWith { + contract: evm::Address::from_str("0x507AAe92E8a024feDCbB521d11EC406eEfB4488F") + .unwrap(), + input_prefix: vec![96, 87, 54, 29, 1, 1, 1, 1], + }, + TxFilter::InputStartsWith { + contract: evm::Address::from_str("0x8065CB50F72c28668C5bf17DfeEFa9eB2485783a") + .unwrap(), + input_prefix: vec![], + }, + ], + }; + let mut payer_bytes = vec![]; + BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); + program_test.add_account( + storage.pubkey(), + Account { + lamports: 10000000, + owner: program_id, + data: payer_bytes, + ..Account::default() + }, + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + let account_metas = vec![ + AccountMeta::new(user.pubkey(), true), + AccountMeta::new(storage.pubkey(), false), + AccountMeta::new_readonly(system_program::id(), false), + ]; + let new_filters = vec![TxFilter::InputStartsWith { + contract: evm::Address::from_str("0x507AAe92E8a024feDCbB521d11EC406eEfB4488F") + .unwrap(), + input_prefix: vec![96, 87, 54, 29], + }]; + let ix = Instruction::new_with_borsh( + program_id, + &GasStationInstruction::UpdateFilters { + whitelist: new_filters.clone(), + }, + account_metas, + ); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + banks_client.process_transaction(tx).await.unwrap(); + + let storage_account = banks_client + .get_account(storage.pubkey()) + .await + .unwrap() + .unwrap(); + let updated_payer: Payer = BorshDeserialize::deserialize(&mut &*storage_account.data).unwrap(); + assert_eq!(new_filters, updated_payer.filters); + } + #[tokio::test] async fn test_execute_tx() { let program_id = Pubkey::new_unique(); @@ -441,10 +652,7 @@ mod test { let payer_data = Payer { owner: owner.pubkey(), payer, - filters: vec![TxFilter::InputStartsWith { - contract: evm::Address::zero(), - input_prefix: vec![], - }], + filters: dummy_filters(), }; let mut payer_bytes = vec![]; BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); @@ -475,9 +683,9 @@ mod test { }, account_metas, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); - transaction.sign(&[&user], recent_blockhash); - banks_client.process_transaction(transaction).await.unwrap(); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + banks_client.process_transaction(tx).await.unwrap(); } #[tokio::test] @@ -503,10 +711,7 @@ mod test { let payer_data = Payer { owner: owner.pubkey(), payer, - filters: vec![TxFilter::InputStartsWith { - contract: evm::Address::zero(), - input_prefix: vec![], - }], + filters: dummy_filters(), }; let mut payer_bytes = vec![]; BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); @@ -553,24 +758,25 @@ mod test { account_metas, ); // this will fail because neither evm tx nor big tx storage provided - let mut transaction = - Transaction::new_with_payer(&[ix_no_big_tx_storage], Some(&user.pubkey())); - transaction.sign(&[&user], recent_blockhash); + let mut tx = Transaction::new_with_payer(&[ix_no_big_tx_storage], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + let _expected_error = TransactionError(InstructionError( + 0, + Custom(GasStationError::BigTxStorageMissing as u32), + )); assert!(matches!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err(), - TransactionError(InstructionError(0, Custom(2))) + banks_client.process_transaction(tx).await.unwrap_err(), + _expected_error, + )); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user, &big_tx_storage], recent_blockhash); + let _expected_error = TransactionError(InstructionError( + 0, + Custom(GasStationError::NotSupported as u32), )); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); - transaction.sign(&[&user, &big_tx_storage], recent_blockhash); assert!(matches!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err(), - TransactionError(InstructionError(0, Custom(14))) // NotSupported + banks_client.process_transaction(tx).await.unwrap_err(), + _expected_error, )); } @@ -596,10 +802,7 @@ mod test { let payer_data = Payer { owner: owner.pubkey(), payer, - filters: vec![TxFilter::InputStartsWith { - contract: evm::Address::zero(), - input_prefix: vec![], - }], + filters: dummy_filters(), }; let mut payer_bytes = vec![]; BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); @@ -630,13 +833,10 @@ mod test { }, account_metas, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); - transaction.sign(&[&user], recent_blockhash); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); assert!(matches!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err(), + banks_client.process_transaction(tx).await.unwrap_err(), TransactionError(InstructionError(0, IncorrectProgramId)) )); } @@ -688,10 +888,7 @@ mod test { let payer_data = Payer { owner: owner.pubkey(), payer, - filters: vec![TxFilter::InputStartsWith { - contract: evm::Address::zero(), - input_prefix: vec![], - }], + filters: dummy_filters(), }; let mut valid_payer_bytes = vec![]; BorshSerialize::serialize(&payer_data, &mut valid_payer_bytes).unwrap(); @@ -727,14 +924,15 @@ mod test { }, account_metas, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); - transaction.sign(&[&user], recent_blockhash); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + let _expected_error = TransactionError(InstructionError( + 0, + Custom(GasStationError::InvalidAccountBorshData as u32), + )); assert!(matches!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err(), - TransactionError(InstructionError(0, Custom(4))) + banks_client.process_transaction(tx).await.unwrap_err(), + _expected_error, )); } } @@ -786,14 +984,15 @@ mod test { }, account_metas, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); - transaction.sign(&[&user], recent_blockhash); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + let _expected_error = TransactionError(InstructionError( + 0, + Custom(GasStationError::AccountNotInitialized as u32), + )); assert!(matches!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err(), - TransactionError(InstructionError(0, Custom(1))) + banks_client.process_transaction(tx).await.unwrap_err(), + _expected_error, )); } @@ -821,10 +1020,7 @@ mod test { let payer_data = Payer { owner: owner1.pubkey(), payer: payer1, - filters: vec![TxFilter::InputStartsWith { - contract: evm::Address::zero(), - input_prefix: vec![], - }], + filters: dummy_filters(), }; let mut payer_bytes = vec![]; BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); @@ -855,14 +1051,15 @@ mod test { }, account_metas, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); - transaction.sign(&[&user], recent_blockhash); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + let _expected_error = TransactionError(InstructionError( + 0, + Custom(GasStationError::PayerAccountMismatch as u32), + )); assert!(matches!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err(), - TransactionError(InstructionError(0, Custom(10))) + banks_client.process_transaction(tx).await.unwrap_err(), + _expected_error, )); } @@ -928,14 +1125,15 @@ mod test { }, account_metas, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); - transaction.sign(&[&user], recent_blockhash); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + let _expected_error = TransactionError(InstructionError( + 0, + Custom(GasStationError::PayerFilterMismatch as u32), + )); assert!(matches!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err(), - TransactionError(InstructionError(0, Custom(11))) + banks_client.process_transaction(tx).await.unwrap_err(), + _expected_error, )); } @@ -966,10 +1164,7 @@ mod test { let mut payer_data = Payer { owner: owner1.pubkey(), payer: payer1, - filters: vec![TxFilter::InputStartsWith { - contract: evm::Address::zero(), - input_prefix: vec![], - }], + filters: dummy_filters(), }; let mut payer_bytes = vec![]; BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); @@ -1013,15 +1208,16 @@ mod test { }, account_metas, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); - transaction.sign(&[&user], recent_blockhash); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); // This tx has funds for evm call but will fail on refund attempt + let _expected_error = TransactionError(InstructionError( + 0, + Custom(GasStationError::InsufficientPayerBalance as u32), + )); assert!(matches!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err(), - TransactionError(InstructionError(0, Custom(3))) // GasStationError::InsufficientPayerBalance + banks_client.process_transaction(tx).await.unwrap_err(), + _expected_error, )); let account_metas = vec![ @@ -1062,15 +1258,16 @@ mod test { &GasStationInstruction::ExecuteWithPayer { tx: Some(tx) }, account_metas, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); - transaction.sign(&[&user], recent_blockhash); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); // This tx will fail on evm side due to insufficient funds for evm transaction + let _expected_error = TransactionError(InstructionError( + 0, + Custom(EvmError::NativeAccountInsufficientFunds as u32), + )); assert!(matches!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err(), - TransactionError(InstructionError(0, Custom(18))) // NativeAccountInsufficientFunds from evm_loader + banks_client.process_transaction(tx).await.unwrap_err(), + _expected_error, )); } @@ -1096,10 +1293,7 @@ mod test { let payer_data = Payer { owner: owner.pubkey(), payer, - filters: vec![TxFilter::InputStartsWith { - contract: evm::Address::zero(), - input_prefix: vec![], - }], + filters: dummy_filters(), }; let mut payer_bytes = vec![]; BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); @@ -1131,14 +1325,15 @@ mod test { }, account_metas_invalid_evm_loader, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); - transaction.sign(&[&user], recent_blockhash); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + let _expected_error = TransactionError(InstructionError( + 0, + Custom(GasStationError::InvalidEvmLoader as u32), + )); assert!(matches!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err(), - TransactionError(InstructionError(0, Custom(6))) // InvalidEvmLoader + banks_client.process_transaction(tx).await.unwrap_err(), + _expected_error, )); let account_metas_invalid_evm_state = vec![ @@ -1156,14 +1351,15 @@ mod test { }, account_metas_invalid_evm_state, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); - transaction.sign(&[&user], recent_blockhash); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + let _expected_error = TransactionError(InstructionError( + 0, + Custom(GasStationError::InvalidEvmLoader as u32), + )); assert!(matches!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err(), - TransactionError(InstructionError(0, Custom(7))) // InvalidEvmLoader + banks_client.process_transaction(tx).await.unwrap_err(), + _expected_error, )); } @@ -1191,10 +1387,7 @@ mod test { let payer_data = Payer { owner: owner.pubkey(), payer, - filters: vec![TxFilter::InputStartsWith { - contract: evm::Address::zero(), - input_prefix: vec![], - }], + filters: dummy_filters(), }; let mut payer_bytes = vec![]; BorshSerialize::serialize(&payer_data, &mut payer_bytes).unwrap(); @@ -1225,14 +1418,15 @@ mod test { }, account_metas, ); - let mut transaction = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); - transaction.sign(&[&user], recent_blockhash); + let mut tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + let _expected_error = TransactionError(InstructionError( + 0, + Custom(GasStationError::NotRentExempt as u32), + )); assert!(matches!( - banks_client - .process_transaction(transaction) - .await - .unwrap_err(), - TransactionError(InstructionError(0, Custom(9))) // NotRentExempt + banks_client.process_transaction(tx).await.unwrap_err(), + _expected_error )); } } diff --git a/evm-utils/programs/gas_station/src/state.rs b/evm-utils/programs/gas_station/src/state.rs index 45a62927c2..6a6a38656f 100644 --- a/evm-utils/programs/gas_station/src/state.rs +++ b/evm-utils/programs/gas_station/src/state.rs @@ -7,11 +7,12 @@ use solana_sdk::{ use crate::instruction::TxFilter; pub const MAX_FILTERS: usize = 10; +pub const PAYER_STATE_SIZE_WITHOUT_FILTERS: usize = 64; pub fn get_state_size(filters: &Vec) -> usize { let mut bytes = vec![]; BorshSerialize::serialize(filters, &mut bytes).unwrap(); - bytes.len() + 64 + bytes.len() + PAYER_STATE_SIZE_WITHOUT_FILTERS } #[repr(C)] From cabfaf111d08a3bf39092cbb4f2a02ecbd4829d0 Mon Sep 17 00:00:00 2001 From: Vadim Date: Mon, 27 Feb 2023 15:26:07 +0200 Subject: [PATCH 19/19] Feat(evm-bridge): fix clippy --- evm-utils/evm-bridge/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evm-utils/evm-bridge/src/main.rs b/evm-utils/evm-bridge/src/main.rs index f350ade70f..524126af7e 100644 --- a/evm-utils/evm-bridge/src/main.rs +++ b/evm-utils/evm-bridge/src/main.rs @@ -1035,12 +1035,12 @@ impl FromStr for EvmContractToPayerKeys { fn from_str(s: &str) -> StdResult { let (contract, keys) = s.split_once(':') - .ok_or(ParseEvmContractToPayerKeysError::InvalidFormat( + .ok_or_else(|| ParseEvmContractToPayerKeysError::InvalidFormat( s.to_string(), ))?; let (owner, storage_acc) = keys.split_once(':') - .ok_or(ParseEvmContractToPayerKeysError::InvalidFormat( + .ok_or_else(|| ParseEvmContractToPayerKeysError::InvalidFormat( s.to_string(), ))?; let contract = Address::from_str(contract).map_err(|_| {