diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 3e192cccd2b..2070704d534 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -925,7 +925,7 @@ pub fn genesis() -> Genesis { }, max_expected_time_per_block: namada::types::time::DurationSecs(30), max_proposal_bytes: Default::default(), - max_block_gas: u64::MAX, //FIXME: adjust this value + max_block_gas: 100_000_000, //FIXME: adjust this value vp_whitelist: vec![], tx_whitelist: vec![], implicit_vp_code_path: vp_implicit_path.into(), @@ -936,7 +936,7 @@ pub fn genesis() -> Genesis { pos_gain_d: dec!(0.1), staked_ratio: dec!(0.0), pos_inflation_amount: 0, - wrapper_tx_fees: Some(token::Amount::whole(0)), + wrapper_tx_fees: Some(token::Amount::whole(100)), gas_table: BTreeMap::default(), }; let albert = EstablishedAccount { diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index e58ef58b0d1..e7e7597ebcb 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -534,7 +534,7 @@ where let fee_payer = if wrapper.pk != address::masp_tx_key().ref_to() { wrapper.fee_payer() } else { - address::masp() + address::masp() //FIXME: here? }; let balance_key = token::balance_key(&wrapper.fee.token, &fee_payer); @@ -593,6 +593,7 @@ mod test_finalize_block { InitProposalData, VoteProposalData, }; use namada::types::transaction::{EncryptionKey, Fee, WrapperTx, MIN_FEE}; + use tendermint_proto::abci::RequestInitChain; use super::*; use crate::node::ledger::shell::test_utils::*; @@ -1096,16 +1097,12 @@ mod test_finalize_block { }, &keypair, Epoch(0), - GAS_LIMIT_MULTIPLIER.into(), + 0.into(), raw_tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] None, ); - let signed_wrapper = wrapper_tx - .sign(&keypair, shell.chain_id.clone(), None) - .unwrap() - .to_bytes(); // Write inner hash in storage let inner_hash_key = @@ -1128,30 +1125,101 @@ mod test_finalize_block { info: "".into(), }, }; - let gas_limit = - u64::from(&wrapper_tx.gas_limit) - signed_wrapper.len() as u64; + let gas_limit = u64::from(&wrapper_tx.gas_limit); shell.enqueue_tx(wrapper_tx, gas_limit); - let _event = &shell + let event = &shell .finalize_block(FinalizeBlock { txs: vec![processed_tx], ..Default::default() }) .expect("Test failed")[0]; - // FIXME: uncomment when proper gas metering is in place - // // Check inner tx hash has been removed from storage - // assert_eq!(event.event_type.to_string(), String::from("applied")); - // let code = event.attributes.get("code").expect("Test - // failed").as_str(); assert_eq!(code, - // String::from(ErrorCodes::WasmRuntimeError).as_str()); - - // assert!( - // !shell - // .storage - // .has_key(&inner_hash_key) - // .expect("Test failed") - // .0 + // Check inner tx hash has been removed from storage + assert_eq!(event.event_type.to_string(), String::from("applied")); + let code = event.attributes.get("code").expect("Testfailed").as_str(); + assert_eq!(code, String::from(ErrorCodes::WasmRuntimeError).as_str()); + + assert!(!shell + .shell + .wl_storage + .has_key(&inner_hash_key) + .expect("Test failed")) + } + + /// Test that a wrapper transaction rejected by [`process_proposal`] because of gas, + /// still pays the fee + #[test] + fn test_rejected_wrapper_for_gas_pays_fee() { + let (mut shell, _) = setup(); + + let keypair = gen_keypair(); + let address = Address::from(&keypair.to_public()); + + let mut wasm_path = top_level_directory(); + wasm_path.push("wasm_for_tests/tx_no_op.wasm"); + let tx_code = std::fs::read(wasm_path) + .expect("Expected a file at given code path"); + let raw_tx = Tx::new( + tx_code, + Some("Encrypted transaction data".as_bytes().to_owned()), + shell.chain_id.clone(), + None, + ); + let wrapper_tx = WrapperTx::new( + Fee { + amount: 1.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + &keypair, + Epoch(0), + 1.into(), + raw_tx.clone(), + Default::default(), + #[cfg(not(feature = "mainnet"))] + None, + ); + + let wrapper = wrapper_tx + .sign(&keypair, shell.chain_id.clone(), None) + .expect("Test failed"); + + let processed_tx = ProcessedTx { + tx: wrapper.to_bytes(), + result: TxResult { + code: ErrorCodes::TxGasLimit.into(), + info: "".into(), + }, + }; + + let initial_balance = Amount::whole(1_000_000); + let balance_key = token::balance_key(&wrapper_tx.fee.token, &address); + shell + .wl_storage + .storage + .write(&balance_key, initial_balance.try_to_vec().unwrap()) + .unwrap(); + + //FIXME: uncomment when variable fees + // let event = &shell + // .finalize_block(FinalizeBlock { + // txs: vec![processed_tx], + // ..Default::default() + // }) + // .expect("Test failed")[0]; + + // assert_eq!(event.event_type.to_string(), String::from("accepted")); + // let code = event.attributes.get("code").expect("Testfailed").as_str(); + // assert_eq!(code, String::from(ErrorCodes::TxGasLimit).as_str()); + + // assert_eq!( + // storage_api::token::read_balance( + // &shell.wl_storage, + // &wrapper_tx.fee.token, + // &address + // ) + // .unwrap(), + // Amount::whole(1000) // ) } } diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index dfd8f3ab3a7..bb089877111 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -128,7 +128,7 @@ pub enum ErrorCodes { Ok = 0, InvalidDecryptedChainId = 1, ExpiredDecryptedTx = 2, - TxGasLimit = 3, + DecryptedTxGasLimit = 3, WasmRuntimeError = 4, InvalidTx = 5, InvalidSig = 6, @@ -139,6 +139,7 @@ pub enum ErrorCodes { InvalidChainId = 11, ExpiredTx = 12, BlockGasLimit = 13, + TxGasLimit = 14, } impl ErrorCodes { @@ -685,9 +686,7 @@ where .read_storage_key(¶meters::storage::get_max_block_gas_key()) .expect("Missing max_block_gas parameter in storage"); let mut block_gas_meter = BlockGasMeter::new(block_gas_limit); - if let Err(_) = block_gas_meter - .finalize_transaction(gas_meter) - { + if let Err(_) = block_gas_meter.finalize_transaction(gas_meter) { response.code = ErrorCodes::BlockGasLimit.into(); response.log = "Wrapper transaction exceeds the maximum block gas limit" @@ -773,6 +772,7 @@ where #[allow(dead_code)] /// Simulate validation and application of a transaction. fn dry_run_tx(&self, tx_bytes: &[u8]) -> response::Query { + //FIXME: should we simulate also the wrapper part for gas? let mut response = response::Query::default(); let gas_table: BTreeMap = self .read_storage_key(¶meters::storage::get_gas_table_storage_key()) @@ -1012,7 +1012,8 @@ mod test_utils { let base_dir = tempdir().unwrap().as_ref().canonicalize().unwrap(); let vp_wasm_compilation_cache = 50 * 1024 * 1024; // 50 kiB let tx_wasm_compilation_cache = 50 * 1024 * 1024; // 50 kiB - let mut shell = Self { + ( + Self { shell: Shell::::new( config::Ledger::new( base_dir, @@ -1026,73 +1027,7 @@ mod test_utils { tx_wasm_compilation_cache, address::nam(), ), - }; - - // Initialize gas table - let mut checksums: BTreeMap = serde_json::from_slice( - &std::fs::read("../wasm/checksums.json").unwrap(), - ) - .unwrap(); - // Extend gas table with test transactions and vps - checksums.extend(serde_json::from_slice::>( - &std::fs::read("../wasm_for_tests/checksums.json").unwrap(), - ) - .unwrap().into_iter()); - - let gas_file: BTreeMap = serde_json::from_slice( - &std::fs::read("../wasm/gas.json").unwrap(), - ) - .unwrap(); - - let mut gas_table = BTreeMap::::new(); - - for id in checksums.keys().chain(gas_file.keys()){ - // Get tx/vp hash (or name if native) - let hash = match checksums.get(id.as_str()) { - Some(v) => { -v - .split_once('.') - .unwrap() - .1 - .split_once('.') - .unwrap() - .0.to_owned() - } - None => { - id.to_owned() - } - }; - let gas = gas_file.get(id).unwrap_or(&1_000).to_owned(); - gas_table.insert(hash, gas); - } - - - let gas_table_key = - namada::core::ledger::parameters::storage::get_gas_table_storage_key(); - shell.wl_storage - .storage - .write(&gas_table_key, - namada::core::ledger::storage::types::encode(&gas_table)) - .expect( - "Gas table parameter must be initialized in the genesis block", - ); - - let max_block_gas_key = - namada::core::ledger::parameters::storage::get_max_block_gas_key( - ); - shell.wl_storage - .storage - .write( - &max_block_gas_key, - namada::core::ledger::storage::types::encode( - &10_000_000_u64, - ), - ) - .expect( - "Max block gas parameter must be initialized in storage", - ); - ( - shell, + }, receiver, ) } @@ -1231,7 +1166,7 @@ v }, &keypair, Epoch(0), - 1.into(), + 0.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1299,16 +1234,16 @@ v /// Test the failure cases of [`mempool_validate`] #[cfg(test)] mod test_mempool_validate { + use crate::facade::tendermint_proto::abci::RequestInitChain; use namada::proof_of_stake::Epoch; use namada::proto::SignedTxData; - use namada::types::transaction::{Fee, WrapperTx}; + use namada::types::transaction::{ + Fee, GasLimit, WrapperTx, GAS_LIMIT_RESOLUTION, + }; use super::test_utils::TestShell; use super::{MempoolTxType, *}; - -const GAS_LIMIT_MULTIPLIER: u64 = 1; - /// Mempool validation must reject unsigned wrappers #[test] fn test_missing_signature() { @@ -1488,7 +1423,7 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; }, &keypair, Epoch(0), - GAS_LIMIT_MULTIPLIER.into(), + 0.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1629,4 +1564,97 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; ); assert_eq!(result.code, u32::from(ErrorCodes::ExpiredTx)); } + + /// Check that a tx requiring more gas than the block limit gets rejected + #[test] + fn test_exceeding_max_block_gas_tx() { + let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); + + let block_gas_limit: u64 = shell + .read_storage_key(¶meters::storage::get_max_block_gas_key()) + .expect("Missing max_block_gas parameter in storage"); + let keypair = super::test_utils::gen_keypair(); + + let tx = Tx::new( + "wasm_code".as_bytes().to_owned(), + Some("transaction data".as_bytes().to_owned()), + shell.chain_id.clone(), + None, + ); + + let wrapper = WrapperTx::new( + Fee { + amount: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + &keypair, + Epoch(0), + (block_gas_limit + 1).into(), + tx, + Default::default(), + #[cfg(not(feature = "mainnet"))] + None, + ) + .sign(&keypair, shell.chain_id.clone(), None) + .expect("Wrapper signing failed"); + + let result = shell.mempool_validate( + wrapper.to_bytes().as_ref(), + MempoolTxType::NewTransaction, + ); + assert_eq!(result.code, u32::from(ErrorCodes::BlockGasLimit)); + } + + // Check that a tx requiring more gas than its limit gets rejected + #[test] + fn test_exceeding_gas_limit_tx() { + let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); + + let keypair = super::test_utils::gen_keypair(); + + let tx = Tx::new( + "wasm_code".as_bytes().to_owned(), + Some("transaction data".as_bytes().to_owned()), + shell.chain_id.clone(), + None, + ); + + let wrapper = WrapperTx::new( + Fee { + amount: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + &keypair, + Epoch(0), + 0.into(), + tx, + Default::default(), + #[cfg(not(feature = "mainnet"))] + None, + ) + .sign(&keypair, shell.chain_id.clone(), None) + .expect("Wrapper signing failed"); + + let result = shell.mempool_validate( + wrapper.to_bytes().as_ref(), + MempoolTxType::NewTransaction, + ); + assert_eq!(result.code, u32::from(ErrorCodes::TxGasLimit)); + } } diff --git a/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/apps/src/lib/node/ledger/shell/prepare_proposal.rs index f5f8ae78b28..55396f3d687 100644 --- a/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -230,7 +230,9 @@ mod test_prepare_proposal { use borsh::BorshSerialize; use namada::proof_of_stake::Epoch; + use namada::types::chain::ChainId; use namada::types::transaction::{Fee, WrapperTx}; + use tendermint_proto::abci::RequestInitChain; use super::*; use crate::node::ledger::shell::test_utils::{gen_keypair, TestShell}; @@ -242,7 +244,15 @@ mod test_prepare_proposal { /// proposed block. #[test] fn test_prepare_proposal_rejects_non_wrapper_tx() { - let (shell, _) = TestShell::new(); + let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let tx = Tx::new( "wasm_code".as_bytes().to_owned(), Some("transaction_data".as_bytes().to_owned()), @@ -268,7 +278,15 @@ mod test_prepare_proposal { /// we simply exclude it from the proposal #[test] fn test_error_in_processing_tx() { - let (shell, _) = TestShell::new(); + let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = gen_keypair(); let tx = Tx::new( "wasm_code".as_bytes().to_owned(), @@ -321,6 +339,14 @@ mod test_prepare_proposal { #[test] fn test_decrypted_txs_in_correct_order() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = gen_keypair(); let mut expected_wrapper = vec![]; let mut expected_decrypted = vec![]; @@ -424,7 +450,15 @@ mod test_prepare_proposal { /// Test that expired wrapper transactions are not included in the block #[test] fn test_expired_wrapper_tx() { - let (shell, _) = TestShell::new(); + let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = gen_keypair(); let tx_time = DateTimeUtc::now(); let tx = Tx::new( @@ -474,4 +508,121 @@ mod test_prepare_proposal { assert!(result.txs.is_empty()); } } + + /// Check that a tx requiring more gas than the block limit is not included in the block + #[test] + fn test_exceeding_max_block_gas_tx() { + let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); + + let block_gas_limit: u64 = shell + .read_storage_key(¶meters::storage::get_max_block_gas_key()) + .expect("Missing max_block_gas parameter in storage"); + let keypair = gen_keypair(); + + let tx = Tx::new( + "wasm_code".as_bytes().to_owned(), + Some("transaction data".as_bytes().to_owned()), + shell.chain_id.clone(), + None, + ); + + let wrapper = WrapperTx::new( + Fee { + amount: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + &keypair, + Epoch(0), + (block_gas_limit + 1).into(), + tx, + Default::default(), + #[cfg(not(feature = "mainnet"))] + None, + ) + .sign(&keypair, shell.chain_id.clone(), None) + .expect("Wrapper signing failed"); + + let req = RequestPrepareProposal { + txs: vec![wrapper.to_bytes()], + max_tx_bytes: 0, + time: None, + ..Default::default() + }; + #[cfg(feature = "abcipp")] + assert_eq!( + shell.prepare_proposal(req).tx_records, + vec![record::remove(wrapper.to_bytes())] + ); + #[cfg(not(feature = "abcipp"))] + { + let result = shell.prepare_proposal(req); + eprintln!("Proposal: {:?}", result.txs); + assert!(result.txs.is_empty()); + } + } + + // Check that a wrapper requiring more gas than its limit is not included in the block + #[test] + fn test_exceeding_gas_limit_wrapper() { + let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); + + let keypair = gen_keypair(); + + let tx = Tx::new( + "wasm_code".as_bytes().to_owned(), + Some("transaction data".as_bytes().to_owned()), + shell.chain_id.clone(), + None, + ); + + let wrapper = WrapperTx::new( + Fee { + amount: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + &keypair, + Epoch(0), + 0.into(), + tx, + Default::default(), + #[cfg(not(feature = "mainnet"))] + None, + ) + .sign(&keypair, shell.chain_id.clone(), None) + .expect("Wrapper signing failed"); + + let req = RequestPrepareProposal { + txs: vec![wrapper.to_bytes()], + max_tx_bytes: 0, + time: None, + ..Default::default() + }; + #[cfg(feature = "abcipp")] + assert_eq!( + shell.prepare_proposal(req).tx_records, + vec![record::remove(wrapper.to_bytes())] + ); + #[cfg(not(feature = "abcipp"))] + { + let result = shell.prepare_proposal(req); + eprintln!("Proposal: {:?}", result.txs); + assert!(result.txs.is_empty()); + } + } } diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index 022537db66b..a971b473ee8 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -65,7 +65,7 @@ where self.read_storage_key( ¶meters::storage::get_max_block_gas_key(), ) - .expect("Missing parameter in storage"), + .expect("Missing max_block_gas parameter in storage"), ); let gas_table: BTreeMap = self .read_storage_key(¶meters::storage::get_gas_table_storage_key()) @@ -258,7 +258,7 @@ where if let Err(e) = tx_gas_meter.add(tx_gas) { return TxResult { - code: ErrorCodes::TxGasLimit.into(), + code: ErrorCodes::DecryptedTxGasLimit.into(), info: format!("Decrypted transaction gas error: {}", e) }; } @@ -291,10 +291,9 @@ where TxType::Wrapper(wrapper) => { // Account for gas. This is done even if the transaction is later deemed invalid, to incentivize the proposer to // include only valid transaction and avoid wasting block gas limit - // Wrapper gas limit, Max block gas and cumulated block gas let mut tx_gas_meter = TxGasMeter::new(u64::from(&wrapper.gas_limit)); if let Err(_) = tx_gas_meter.add_tx_size_gas(tx_bytes.len()) { - // Add the declared tx gas limit to the block gas meter even in case of error + // Add the declared tx gas limit to the block gas meter even in case of an error let _ = temp_block_gas_meter.finalize_transaction(tx_gas_meter); return TxResult { @@ -484,6 +483,14 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_unsigned_wrapper_rejected() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = gen_keypair(); let tx = Tx::new( "wasm_code".as_bytes().to_owned(), @@ -536,6 +543,14 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_wrapper_bad_signature_rejected() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = gen_keypair(); let tx = Tx::new( "wasm_code".as_bytes().to_owned(), @@ -626,7 +641,26 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_wrapper_unknown_address() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = crate::wallet::defaults::keys().remove(0).1; +// reduce address balance to match the 100 token min fee + let balance_key = token::balance_key( + &shell.wl_storage.storage.native_token, + &Address::from(&keypair.ref_to()), + ); + shell + .wl_storage + .storage + .write(&balance_key, Amount::whole(99).try_to_vec().unwrap()) + .unwrap(); + let tx = Tx::new( "wasm_code".as_bytes().to_owned(), Some("transaction data".as_bytes().to_owned()), @@ -675,8 +709,16 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_wrapper_insufficient_balance_address() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = crate::wallet::defaults::daewon_keypair(); - // reduce address balance to match the 100 token fee + // reduce address balance to match the 100 token min fee let balance_key = token::balance_key( &shell.wl_storage.storage.native_token, &Address::from(&keypair.ref_to()), @@ -695,7 +737,7 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; ); let wrapper = WrapperTx::new( Fee { - amount: Amount::whole(1_000_100), + amount: Amount::whole(100), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, @@ -736,6 +778,14 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_decrypted_txs_out_of_order() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = gen_keypair(); let mut txs = vec![]; for i in 0..3 { @@ -814,6 +864,14 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_incorrectly_labelled_as_undecryptable() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = gen_keypair(); let tx = Tx::new( @@ -985,6 +1043,14 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_too_many_decrypted_txs() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let tx = Tx::new( "wasm_code".as_bytes().to_owned(), @@ -1025,6 +1091,14 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_raw_tx_rejected() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let tx = Tx::new( "wasm_code".as_bytes().to_owned(), @@ -1061,6 +1135,14 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_wrapper_tx_hash() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = crate::wallet::defaults::daewon_keypair(); @@ -1124,6 +1206,14 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_wrapper_tx_hash_same_block() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = crate::wallet::defaults::daewon_keypair(); @@ -1193,6 +1283,14 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_inner_tx_hash() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = crate::wallet::defaults::daewon_keypair(); @@ -1256,6 +1354,14 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_inner_tx_hash_same_block() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = crate::wallet::defaults::daewon_keypair(); let keypair_2 = crate::wallet::defaults::daewon_keypair(); @@ -1350,8 +1456,16 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; /// Test that a wrapper or protocol transaction with a mismatching chain id /// causes the entire block to be rejected #[test] - fn test_wong_chain_id() { + fn test_wrong_chain_id() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = crate::wallet::defaults::daewon_keypair(); let tx = Tx::new( @@ -1412,8 +1526,16 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; /// Test that a decrypted transaction with a mismatching chain id gets /// rejected without rejecting the entire block #[test] - fn test_decrypted_wong_chain_id() { + fn test_decrypted_wrong_chain_id() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = crate::wallet::defaults::daewon_keypair(); let wrong_chain_id = ChainId("Wrong chain id".to_string()); @@ -1479,6 +1601,14 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; #[test] fn test_expired_wrapper() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = crate::wallet::defaults::daewon_keypair(); let tx = Tx::new( @@ -1519,11 +1649,19 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; } } - /// Test that an expired decrypted transaction is correctlye marked as so + /// Test that an expired decrypted transaction is correctly marked as so /// without rejecting the entire block #[test] fn test_expired_decrypted() { let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); let keypair = crate::wallet::defaults::daewon_keypair(); let tx = Tx::new( @@ -1574,4 +1712,177 @@ const GAS_LIMIT_MULTIPLIER: u64 = 1; Err(_) => panic!("Test failed"), } } + + + /// Test that a decrypted transaction requiring more gas than the limit imposed + /// by its wrapper is rejected. + #[test] + fn test_decrypted_gas_limit() { + let (mut shell, _) = TestShell::new(); +shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); + let keypair = crate::wallet::defaults::daewon_keypair(); + + let tx = Tx::new( + "wasm_code".as_bytes().to_owned(), + Some("new transaction data".as_bytes().to_owned()), + shell.chain_id.clone(), + None + ); + let decrypted: Tx = DecryptedTx::Decrypted { + tx: tx.clone(), + has_valid_pow: false, + } + .into(); + let signed_decrypted = decrypted.sign(&keypair); + let wrapper = WrapperTx::new( + Fee { + amount: 0.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + &keypair, + Epoch(0), + 0.into(), + tx, + Default::default(), + #[cfg(not(feature = "mainnet"))] + None, + ); + let gas = u64::from(&wrapper.gas_limit) ; + let wrapper_in_queue = WrapperTxInQueue { + tx: wrapper, + gas, + has_valid_pow: false, + }; + shell.wl_storage.storage.tx_queue.push(wrapper_in_queue); + + // Run validation + let request = ProcessProposal { + txs: vec![signed_decrypted.to_bytes()], + }; + match shell.process_proposal(request) { + Ok(response) => { + assert_eq!( + response[0].result.code, + u32::from(ErrorCodes::DecryptedTxGasLimit) + ); + } + Err(_) => panic!("Test failed"), + } + } + + /// Check that a tx requiring more gas than the block limit causes a block rejection + #[test] + fn test_exceeding_max_block_gas_tx() { + let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); + + let block_gas_limit: u64 = shell + .read_storage_key(¶meters::storage::get_max_block_gas_key()) + .expect("Missing max_block_gas parameter in storage"); + let keypair = super::test_utils::gen_keypair(); + + let tx = Tx::new( + "wasm_code".as_bytes().to_owned(), + Some("transaction data".as_bytes().to_owned()), + shell.chain_id.clone(), + None, + ); + + let wrapper = WrapperTx::new( + Fee { + amount: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + &keypair, + Epoch(0), + (block_gas_limit + 1).into(), + tx, + Default::default(), + #[cfg(not(feature = "mainnet"))] + None, + ) + .sign(&keypair, shell.chain_id.clone(), None) + .expect("Wrapper signing failed"); + + // Run validation + let request = ProcessProposal { + txs: vec![wrapper.to_bytes()], + }; +match shell.process_proposal(request) { + Ok(_) => panic!("Test failed"), + Err(TestError::RejectProposal(response)) => { + assert_eq!( + response[0].result.code, + u32::from(ErrorCodes::BlockGasLimit) + ); + } + } + } + +// Check that a wrapper requiring more gas than its limit causes a block rejection + #[test] + fn test_exceeding_gas_limit_wrapper() { + let (mut shell, _) = TestShell::new(); + shell.init_chain(RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }); + + let keypair = super::test_utils::gen_keypair(); + + let tx = Tx::new( + "wasm_code".as_bytes().to_owned(), + Some("transaction data".as_bytes().to_owned()), + shell.chain_id.clone(), + None, + ); + + let wrapper = WrapperTx::new( + Fee { + amount: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + &keypair, + Epoch(0), + 0.into(), + tx, + Default::default(), + #[cfg(not(feature = "mainnet"))] + None, + ) + .sign(&keypair, shell.chain_id.clone(), None) + .expect("Wrapper signing failed"); + + // Run validation + let request = ProcessProposal { + txs: vec![wrapper.to_bytes()], + }; +match shell.process_proposal(request) { + Ok(_) => panic!("Test failed"), + Err(TestError::RejectProposal(response)) => { + assert_eq!( + response[0].result.code, + u32::from(ErrorCodes::TxGasLimit) + ); + } + } + } } diff --git a/core/src/ledger/gas.rs b/core/src/ledger/gas.rs index a3da4eef950..746260e93f6 100644 --- a/core/src/ledger/gas.rs +++ b/core/src/ledger/gas.rs @@ -29,7 +29,8 @@ pub const WASM_VALIDATION_GAS_PER_BYTE: u64 = 1; /// Gas module result for functions that may fail pub type Result = std::result::Result; -/// Gas metering in a block. +/// Gas metering in a block. The amount of gas consumed in a block is based on the +/// tx_gas_limit declared by each [`TxGasMeter`] #[derive(Debug, Clone)] pub struct BlockGasMeter { /// The max amount of gas allowed per block, defined by the protocol parameter @@ -74,7 +75,7 @@ impl BlockGasMeter { } } - /// Add the transaction gas to the block's total gas. It will return + /// Add the transaction gas limit to the block's total gas. It will return /// error when the consumed gas exceeds the block gas limit, but the state /// will still be updated. This function consumes the [`TxGasMeter`] which shouldn't be updated after this point. pub fn finalize_transaction( @@ -83,7 +84,7 @@ impl BlockGasMeter { ) -> Result<()> { self.block_gas = self .block_gas - .checked_add(tx_gas_meter.transaction_gas) + .checked_add(tx_gas_meter.tx_gas_limit) .ok_or(Error::GasOverflow)?; if self.block_gas > self.block_gas_limit { @@ -92,7 +93,7 @@ impl BlockGasMeter { Ok(()) } - /// Tries to add the transaction gas to the block's total gas. + /// Tries to add the transaction gas limit to the block's total gas. /// If the operation returns an error, propagates this errors without updating the state. This function consumes the [`TxGasMeter`] which shouldn't be updated after this point. pub fn try_finalize_transaction( &mut self, @@ -100,7 +101,7 @@ impl BlockGasMeter { ) -> Result<()> { let updated_gas = self .block_gas - .checked_add(tx_gas_meter.transaction_gas) + .checked_add(tx_gas_meter.tx_gas_limit) .ok_or(Error::GasOverflow)?; if updated_gas > self.block_gas_limit { @@ -272,10 +273,10 @@ mod tests { #[test] fn test_block_gas_meter_add(gas in 0..BLOCK_GAS_LIMIT) { let mut meter = BlockGasMeter::new(BLOCK_GAS_LIMIT); - let mut tx_gas_meter = TxGasMeter::new(BLOCK_GAS_LIMIT + 1); - tx_gas_meter.add( gas).expect("cannot add the gas"); + let mut tx_gas_meter = TxGasMeter::new(BLOCK_GAS_LIMIT ); + tx_gas_meter.add(gas).expect("cannot add the gas"); meter.finalize_transaction(tx_gas_meter).expect("cannot finalize the tx"); - assert_eq!(meter.block_gas, gas); + assert_eq!(meter.block_gas, BLOCK_GAS_LIMIT); } } @@ -325,19 +326,13 @@ mod tests { // add the maximum tx gas for _ in 0..(BLOCK_GAS_LIMIT / transaction_gas) { - let mut tx_gas_meter = TxGasMeter::new(transaction_gas + 1); - tx_gas_meter - .add(transaction_gas) - .expect("over the tx gas limit"); + let tx_gas_meter = TxGasMeter::new(transaction_gas); meter .finalize_transaction(tx_gas_meter) .expect("over the block gas limit"); } - let mut tx_gas_meter = TxGasMeter::new(transaction_gas + 1); - tx_gas_meter - .add(transaction_gas) - .expect("over the tx gas limit"); + let tx_gas_meter = TxGasMeter::new(transaction_gas); match meter .finalize_transaction(tx_gas_meter) .expect_err("unexpectedly succeeded")