diff --git a/Cargo.lock b/Cargo.lock index 843afd5320..8822860d9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1895,6 +1895,7 @@ dependencies = [ "borsh", "derivative", "env_logger 0.8.4", + "evm-gas-station", "evm-rpc", "evm-state", "hex", @@ -1931,6 +1932,7 @@ dependencies = [ "solana-transaction-status", "solana-version", "structopt", + "thiserror", "tokio", "tracing", "tracing-attributes", @@ -1950,6 +1952,24 @@ dependencies = [ "serde", ] +[[package]] +name = "evm-gas-station" +version = "0.1.0" +dependencies = [ + "arrayref", + "borsh", + "num-derive", + "num-traits", + "primitive-types", + "serde", + "solana-evm-loader-program", + "solana-program 1.9.29", + "solana-program-test", + "solana-sdk", + "thiserror", + "tokio", +] + [[package]] name = "evm-gasometer" version = "0.35.0" @@ -6204,6 +6224,7 @@ dependencies = [ "const_format", "criterion-stats", "ctrlc", + "evm-gas-station", "evm-rpc", "evm-state", "hex", diff --git a/Cargo.toml b/Cargo.toml index d7b25adfe6..c5370da0b7 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/cli/Cargo.toml b/cli/Cargo.toml index 6b70310c04..16ec5b3c08 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 00eef2d97b..07880f24c4 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 6469d64be0..c9137fc5fa 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; @@ -17,15 +20,24 @@ 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, ProcessResult}; +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_pubkey, is_valid_signer}; +use solana_clap_utils::keypair::{DefaultSigner, SignerIndex}; +use solana_client::rpc_response::Response; use solana_cli_output::{return_signers_with_config, ReturnSignersConfig}; 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]; @@ -81,6 +93,68 @@ impl EvmSubCommands for App<'_, '_> { .arg(blockhash_arg()) .arg(sign_only_arg()) + .subcommand( + SubCommand::with_name("create-gas-station-payer") + .about("Create payer account for gas station program") + .display_order(3) + .arg(Arg::with_name("payer_storage_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("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("lamports") + .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(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")) + ) + .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 @@ -169,6 +243,20 @@ pub enum EvmCliCommand { blockhash_query: BlockhashQuery, }, + CreateGasStationPayer { + 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, @@ -224,6 +312,38 @@ impl EvmCliCommand { *no_wait, blockhash_query, ), + Self::CreateGasStationPayer { + payer_storage_signer_index, + payer_owner_signer_index, + gas_station_key, + lamports, + filters, + } => { + create_gas_station_payer( + rpc_client, + config, + *payer_storage_signer_index, + *payer_owner_signer_index, + *gas_station_key, + *lamports, + filters, + )?; + Ok("Ok".to_string()) + } + Self::UpdateGasStationPayer { + payer_storage_pubkey, + gas_station_key, + filters, + } => { + update_gas_station_payer( + rpc_client, + config, + *payer_storage_pubkey, + *gas_station_key, + filters, + )?; + Ok("Ok".to_string()) + } // Hidden commands Self::SendRawTx { raw_tx } => { send_raw_tx(rpc_client, config, raw_tx)?; @@ -333,6 +453,105 @@ fn transfer( } } +fn create_gas_station_payer>( + rpc_client: &RpcClient, + config: &CliConfig, + 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_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()), + (&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 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( + &cli_pubkey, + &payer_storage_pubkey, + minimum_balance, + &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, + payer_storage_pubkey, + payer_owner_pubkey, + 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 + ); + 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); + tx.try_sign(&config.signers, latest_blockhash)?; + let signature = rpc_client.send_and_confirm_transaction_with_spinner(&tx)?; + println!("Transaction signature = {}", signature); + 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, @@ -454,7 +673,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")); @@ -499,6 +723,47 @@ pub fn parse_evm_subcommand(matches: &ArgMatches<'_>) -> Result { + signers = vec![default_signer.signer_from_path(matches, 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_storage_signer_index = payer_storage_signer + .map(|signer| { + signers.push(signer); + 1 + }) + .unwrap(); + let payer_owner_signer_index = payer_owner_signer + .map(|signer| { + signers.push(signer); + 2 + }) + .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); + let filters = value_t_or_exit!(matches, "filters_path", PathBuf); + + EvmCliCommand::CreateGasStationPayer { + 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 } @@ -532,7 +797,6 @@ pub fn parse_evm_subcommand(matches: &ArgMatches<'_>) -> Result, pub batch_state_map: BatchStateMap, max_batch_duration: Option, + gas_station_program_id: Option, + redirect_to_proxy_filters: Vec, } impl EvmBridge { @@ -243,6 +245,8 @@ impl EvmBridge { whitelist: vec![], batch_state_map: Default::default(), max_batch_duration: None, + gas_station_program_id: None, + redirect_to_proxy_filters: vec![], } } @@ -270,6 +274,23 @@ impl EvmBridge { self.max_batch_duration = Some(max_duration); } + 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 + .into_iter() + .map(|mut item| { + let (payer_key, _) = + Pubkey::find_program_address(&[item.owner.as_ref()], &gas_station_program_id); + item.gas_station_payer = payer_key; + item + }) + .collect(); + } + /// Wrap evm tx into solana, optionally add meta keys, to solana signature. async fn send_tx( &self, @@ -1016,6 +1037,55 @@ 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: `{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, + gas_station_payer: Pubkey, + storage_acc: Pubkey, +} + +impl FromStr for EvmContractToPayerKeys { + type Err = ParseEvmContractToPayerKeysError; + + fn from_str(s: &str) -> StdResult { + let (contract, keys) = + s.split_once(':') + .ok_or_else(|| ParseEvmContractToPayerKeysError::InvalidFormat( + s.to_string(), + ))?; + let (owner, storage_acc) = + keys.split_once(':') + .ok_or_else(|| 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, + }) + } +} + #[derive(Debug, structopt::StructOpt)] struct Args { keyfile: Option, @@ -1046,6 +1116,11 @@ struct Args { /// Maximum number of seconds to process batched jsonrpc requests. #[structopt(long = "rpc-max-batch-time")] max_batch_duration: Option, + + #[structopt(long = "gas-station")] + gas_station_program_id: Option, + #[structopt(long = "redirect-to-proxy")] + redirect_contracts_to_proxy: Vec, } impl Args { @@ -1143,6 +1218,17 @@ async fn main(args: Args) -> StdResult<(), Box> { if let Some(max_duration) = args.max_batch_duration.map(Duration::from_secs) { meta.set_max_batch_duration(max_duration); } + 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); let mut io = MetaIoHandler::with_middleware(ProxyMiddleware {}); @@ -1318,6 +1404,8 @@ mod tests { whitelist: vec![], batch_state_map: Default::default(), max_batch_duration: None, + 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..99152e585f 100644 --- a/evm-utils/evm-bridge/src/pool.rs +++ b/evm-utils/evm-bridge/src/pool.rs @@ -594,7 +594,41 @@ 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)) + { + 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, + 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, + bridge.gas_station_program_id.unwrap(), + bridge.key.pubkey(), + val.storage_acc, + val.gas_station_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); 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/Cargo.toml b/evm-utils/programs/gas_station/Cargo.toml new file mode 100644 index 0000000000..c4cf87b0d7 --- /dev/null +++ b/evm-utils/programs/gas_station/Cargo.toml @@ -0,0 +1,26 @@ +[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" +num-derive = "0.3.3" +num-traits = "0.2.15" +primitive-types = { version = "0.11.0", default-features = false, features = ["borsh_no_std", "serde_no_std"] } +serde = "1.0.122" +solana-program = { path = "../../../sdk/program", 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"] } + +[lib] +crate-type = ["lib", "cdylib"] +name = "evm_gas_station" \ No newline at end of file diff --git a/evm-utils/programs/gas_station/README.md b/evm-utils/programs/gas_station/README.md new file mode 100644 index 0000000000..bef097ded1 --- /dev/null +++ b/evm-utils/programs/gas_station/README.md @@ -0,0 +1,59 @@ +## Build and deploy + +Build program: + +``./cargo-build-bpf -- -p evm-gas-station`` + +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: **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 +Example input data (passing 1 as an argument): *0x6057361d0000000000000000000000000000000000000000000000000000000000000001* + +### 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: + +- *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 +- *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*: +``` +[ + { "InputStartsWith": [ "", ] } +] +``` + +### Start bridge + +Run evm-bridge command with next additional options: +``--gas-station --redirect-to-proxy ::`` + +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. diff --git a/evm-utils/programs/gas_station/quickstart.sh b/evm-utils/programs/gas_station/quickstart.sh new file mode 100755 index 0000000000..0f15168248 --- /dev/null +++ b/evm-utils/programs/gas_station/quickstart.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +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; + +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 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" 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 new file mode 100644 index 0000000000..f61eebddc7 --- /dev/null +++ b/evm-utils/programs/gas_station/src/error.rs @@ -0,0 +1,48 @@ +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 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")] + 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")] + 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, + #[error("Overflow occurred during transaction call refund")] + RefundOverflow, + #[error("Functionality is not supported")] + NotSupported, +} + +impl From for ProgramError { + fn from(e: GasStationError) -> Self { + ProgramError::Custom(e as u32) + } +} 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..4f352d9c95 --- /dev/null +++ b/evm-utils/programs/gas_station/src/evm_loader_instructions.rs @@ -0,0 +1,83 @@ +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, +} + +#[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, + }, +} \ 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 new file mode 100644 index 0000000000..dca2cecfb1 --- /dev/null +++ b/evm-utils/programs/gas_station/src/instruction.rs @@ -0,0 +1,43 @@ +use super::*; +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::Deserialize; +use solana_sdk::pubkey::Pubkey; + +#[derive(Clone, Debug, Deserialize, BorshDeserialize, BorshSerialize, PartialEq)] +pub enum TxFilter { + InputStartsWith { + contract: evm_types::Address, + input_prefix: Vec, + }, +} + +impl TxFilter { + pub fn is_match(&self, tx: &evm_types::Transaction) -> bool { + match self { + Self::InputStartsWith{ contract, input_prefix } => { + matches!(tx.action, evm_types::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, + }, + + /// Update filters + UpdateFilters { + 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..51cabb9263 --- /dev/null +++ b/evm-utils/programs/gas_station/src/lib.rs @@ -0,0 +1,123 @@ +mod error; +mod evm_loader_instructions; +pub mod evm_types; +pub mod instruction; +mod processor; +mod state; + +pub use state::get_state_size; + +use processor::process_instruction; +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 create_storage_account( + from_pubkey: &Pubkey, + to_pubkey: &Pubkey, + lamports: u64, + filters: &Vec, + owner: &Pubkey, +) -> Instruction { + solana_sdk::system_instruction::create_account( + from_pubkey, + to_pubkey, + lamports, + 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 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, + 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, + ) +} 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..339ff4d42e --- /dev/null +++ b/evm-utils/programs/gas_station/src/processor.rs @@ -0,0 +1,1432 @@ +use super::*; +use borsh::{BorshDeserialize, BorshSerialize}; +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, 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, PAYER_STATE_SIZE_WITHOUT_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 +} + +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], + 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::UpdateFilters { whitelist } => { + process_update_filters(program_id, accounts, 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 { + 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)?; + 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(GasStationError::NotRentExempt.into()); + } + + 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]]], + )?; // TODO: map into something readable + 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_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], + 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 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 !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); + } + 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() })?; + 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()); + } + + 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()); + } + + { + 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, 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)?; + + 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)?; + + 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)?; + } + + 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()); + } + Ok(()) +} + +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_tx_execute_ix<'a>( + evm_loader: &AccountInfo<'a>, + evm_state: &AccountInfo<'a>, + sender: &AccountInfo<'a>, + tx: evm_types::Transaction, +) -> (Instruction, Vec>) { + use evm_loader_instructions::*; + ( + 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 evm_loader_instructions::*; + ( + 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(), + ], + ) +} + +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 + .lamports() + .checked_sub(amount) + .ok_or_else(|| ProgramError::from(GasStationError::InsufficientPayerBalance))?; + **caller.try_borrow_mut_lamports()? = caller + .lamports() + .checked_add(amount) + .ok_or_else(|| ProgramError::from(GasStationError::RefundOverflow))?; + Ok(()) +} + +#[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}; + use solana_sdk::{ + account::{Account, ReadableAccount}, + signature::{Keypair, Signer}, + system_program, + 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]; + const TEST_CHAIN_ID: u64 = 0xdead; + + 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(), + action: evm::TransactionAction::Call(contract), + value: evm::U256::zero(), + input, + } + .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, + } + } + + 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)); + + let creator = Keypair::new(); + let storage = Keypair::new(); + program_test.add_account( + creator.pubkey(), + Account { + lamports: 10000000, + ..Account::default() + }, + ); + let mut bytes = vec![]; + let filters = dummy_filters(); + BorshSerialize::serialize(&filters, &mut bytes).unwrap(); + program_test.add_account( + storage.pubkey(), + Account::new(10000000, bytes.len() + 64, &program_id), + ); + + let (mut banks_client, _, recent_blockhash) = program_test.start().await; + + 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.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, + whitelist: filters, + }, + account_metas, + ); + 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()) + .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] + 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(); + 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: 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(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 tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + banks_client.process_transaction(tx).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 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( + 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: 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 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_sdk::evm_loader::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_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 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(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), + )); + assert!(matches!( + banks_client.process_transaction(tx).await.unwrap_err(), + _expected_error, + )); + } + + #[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: 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: 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 tx = Transaction::new_with_payer(&[ix], Some(&user.pubkey())); + tx.sign(&[&user], recent_blockhash); + assert!(matches!( + banks_client.process_transaction(tx).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: dummy_filters(), + }; + 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 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(tx).await.unwrap_err(), + _expected_error, + )); + } + } + + #[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: 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 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(tx).await.unwrap_err(), + _expected_error, + )); + } + + #[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: 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(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 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(tx).await.unwrap_err(), + _expected_error, + )); + } + + #[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![1; 4], + }, + TxFilter::InputStartsWith { + contract: evm::Address::from([1u8; 20]), + input_prefix: vec![0; 4], + }, + ], + }; + 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![0; 4])), + }, + account_metas, + ); + 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(tx).await.unwrap_err(), + _expected_error, + )); + } + + #[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: dummy_filters(), + }; + 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 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(tx).await.unwrap_err(), + _expected_error, + )); + + 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 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) }, + account_metas, + ); + 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(tx).await.unwrap_err(), + _expected_error, + )); + } + + #[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: 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 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 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(tx).await.unwrap_err(), + _expected_error, + )); + + 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 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(tx).await.unwrap_err(), + _expected_error, + )); + } + + #[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: 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(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 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(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 new file mode 100644 index 0000000000..6a6a38656f --- /dev/null +++ b/evm-utils/programs/gas_station/src/state.rs @@ -0,0 +1,39 @@ +use super::*; +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_sdk::{ + program_pack::IsInitialized, + pubkey::Pubkey, +}; +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() + PAYER_STATE_SIZE_WITHOUT_FILTERS +} + +#[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_types::Transaction) -> bool { + self.filters.iter().any(|f| { f.is_match(tx) }) + } +} + +impl IsInitialized for Payer { + fn is_initialized(&self) -> bool { + !self.filters.is_empty() + } +} 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" 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")