diff --git a/.changelog/unreleased/1242-whitelist-gas-accounting.md b/.changelog/unreleased/1242-whitelist-gas-accounting.md new file mode 100644 index 0000000000..7387faab86 --- /dev/null +++ b/.changelog/unreleased/1242-whitelist-gas-accounting.md @@ -0,0 +1,3 @@ +- Implemented a first gas metering system based on the + runtime cost of whitelisted transactions and VPs. + ([#1242](https://github.com/anoma/namada/pull/1242)) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e8189c9780..49741e263c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "ansi_term" version = "0.12.1" @@ -1100,6 +1106,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.0.73" @@ -1173,6 +1185,33 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" +[[package]] +name = "ciborium" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde 1.0.145", +] + +[[package]] +name = "ciborium-io" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369" + +[[package]] +name = "ciborium-ll" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.3.0" @@ -1211,14 +1250,35 @@ dependencies = [ "bitflags", "indexmap", "lazy_static", - "os_str_bytes", + "os_str_bytes 2.4.0", "strsim", "termcolor", - "textwrap", + "textwrap 0.12.1", "unicode-width", "vec_map", ] +[[package]] +name = "clap" +version = "3.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +dependencies = [ + "bitflags", + "clap_lex", + "indexmap", + "textwrap 0.16.0", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes 6.5.0", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -1431,6 +1491,42 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap 3.2.23", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits 0.2.15", + "oorandom", + "plotters", + "rayon", + "regex", + "serde 1.0.145", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + [[package]] name = "crossbeam-channel" version = "0.4.4" @@ -3958,7 +4054,7 @@ dependencies = [ "borsh 0.9.4", "byte-unit", "byteorder", - "clap", + "clap 3.0.0-beta.2", "color-eyre", "config", "data-encoding", @@ -4031,6 +4127,29 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "namada_benchmarks" +version = "0.15.0" +dependencies = [ + "async-trait", + "borsh 0.9.4", + "criterion", + "ferveo-common", + "ibc-proto 0.26.0 (git+https://github.com/heliaxdev/ibc-proto-rs.git?rev=acc378e5e1865fbf559fa4e36e3c2b0dc1da51bb)", + "ibc-relayer", + "ibc-relayer-types", + "masp_primitives", + "namada", + "namada_apps", + "namada_test_utils", + "prost", + "rand 0.8.5", + "rand_core 0.6.4", + "rust_decimal", + "tempfile", + "tokio", +] + [[package]] name = "namada_core" version = "0.15.2" @@ -4485,6 +4604,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + [[package]] name = "opaque-debug" version = "0.2.3" @@ -4587,6 +4712,12 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" +[[package]] +name = "os_str_bytes" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" + [[package]] name = "output_vt100" version = "0.1.3" @@ -4857,6 +4988,34 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "plotters" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +dependencies = [ + "num-traits 0.2.15", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" + +[[package]] +name = "plotters-svg" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" +dependencies = [ + "plotters-backend", +] + [[package]] name = "polling" version = "2.3.0" @@ -6809,6 +6968,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + [[package]] name = "thiserror" version = "1.0.38" @@ -6919,6 +7084,16 @@ dependencies = [ "url 2.3.1", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde 1.0.145", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index e6908495a1..264c366ddc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "apps", + "benches", "core", "proof_of_stake", "shared", diff --git a/Makefile b/Makefile index d42b3fe3ec..696189ef57 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ wasm_templates := wasm/tx_template wasm/vp_template audit-ignores += RUSTSEC-2021-0076 build: - $(cargo) build + $(cargo) build --workspace --exclude namada_benchmarks build-test: $(cargo) +$(nightly) build --tests -Z unstable-options @@ -36,7 +36,8 @@ package: build-release check-wasm = $(cargo) check --target wasm32-unknown-unknown --manifest-path $(wasm)/Cargo.toml check: - $(cargo) check && \ + $(cargo) check --workspace --exclude namada_benchmarks && \ + $(cargo) +$(nightly) check --benches -Z unstable-options && \ make -C $(wasms) check && \ make -C $(wasms_for_tests) check && \ $(foreach wasm,$(wasm_templates),$(check-wasm) && ) true @@ -45,6 +46,7 @@ check-abcipp: $(cargo) +$(nightly) check \ --workspace \ --exclude namada_tests \ + --exclude namada_benchmarks \ --all-targets \ --no-default-features \ --features "abcipp ibc-mocks-abcipp testing" \ @@ -213,6 +215,9 @@ watch: clean: $(cargo) clean +bench: + $(cargo) +$(nightly) bench -Z unstable-options + build-doc: $(cargo) doc --no-deps @@ -266,4 +271,4 @@ test-miri: MIRIFLAGS="-Zmiri-disable-isolation" $(cargo) +$(nightly) miri test -.PHONY : build check build-release clippy install run-ledger run-gossip reset-ledger test test-debug fmt watch clean build-doc doc build-wasm-scripts-docker debug-wasm-scripts-docker build-wasm-scripts debug-wasm-scripts clean-wasm-scripts dev-deps test-miri test-unit test-unit-abcipp clippy-abcipp +.PHONY : build check build-release clippy install run-ledger run-gossip reset-ledger test test-debug fmt watch clean build-doc doc build-wasm-scripts-docker debug-wasm-scripts-docker build-wasm-scripts debug-wasm-scripts clean-wasm-scripts dev-deps test-miri test-unit test-unit-abcipp clippy-abcipp bench diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 253d73a353..c08a7ed2d3 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1702,12 +1702,12 @@ pub mod args { const EXPIRATION_OPT: ArgOpt = arg_opt("expiration"); const FORCE: ArgFlag = flag("force"); const DONT_PREFETCH_WASM: ArgFlag = flag("dont-prefetch-wasm"); - const GAS_AMOUNT: ArgDefault = - arg_default("gas-amount", DefaultFn(|| token::Amount::from(0))); - const GAS_LIMIT: ArgDefault = - arg_default("gas-limit", DefaultFn(|| token::Amount::from(0))); - const GAS_TOKEN: ArgDefaultFromCtx = - arg_default_from_ctx("gas-token", DefaultFn(|| "NAM".into())); + const FEE_AMOUNT: ArgDefault = + arg_default("fee-amount", DefaultFn(|| token::Amount::from(0))); + const GAS_LIMIT: ArgDefault = + arg_default("gas-limit", DefaultFn(|| GasLimit::from(10))); + const FEE_TOKEN: ArgDefaultFromCtx = + arg_default_from_ctx("fee-token", DefaultFn(|| "NAM".into())); const GENESIS_PATH: Arg = arg("genesis-path"); const GENESIS_VALIDATOR: ArgOpt = arg("genesis-validator").opt(); const HALT_ACTION: ArgFlag = flag("halt"); @@ -3024,7 +3024,7 @@ pub mod args { /// If any new account is initialized by the tx, use the given alias to /// save it in the wallet. pub initialized_account_alias: Option, - /// The amount being payed to include the transaction + /// The amount being payed (for gas unit) to include the transaction pub fee_amount: token::Amount, /// The token in which the fee is being paid pub fee_token: WalletAddress, @@ -3066,15 +3066,15 @@ pub mod args { initialized, the alias will be the prefix of each new \ address joined with a number.", )) - .arg(GAS_AMOUNT.def().about( - "The amount being paid for the inclusion of this transaction", + .arg(FEE_AMOUNT.def().about( + "The amount being paid, per gas unit, for the inclusion of \ + this transaction", + )) + .arg(FEE_TOKEN.def().about("The token for paying the gas")) + .arg(GAS_LIMIT.def().about( + "The multiplier of the gas limit resolution defining the \ + maximum amount of gas needed to run transaction", )) - .arg(GAS_TOKEN.def().about("The token for paying the gas")) - .arg( - GAS_LIMIT.def().about( - "The maximum amount of gas needed to run transaction", - ), - ) .arg(EXPIRATION_OPT.def().about( "The expiration datetime of the transaction, after which the \ tx won't be accepted anymore. All of these examples are \ @@ -3109,9 +3109,9 @@ pub mod args { let broadcast_only = BROADCAST_ONLY.parse(matches); let ledger_address = LEDGER_ADDRESS_DEFAULT.parse(matches); let initialized_account_alias = ALIAS_OPT.parse(matches); - let fee_amount = GAS_AMOUNT.parse(matches); - let fee_token = GAS_TOKEN.parse(matches); - let gas_limit = GAS_LIMIT.parse(matches).into(); + let fee_amount = FEE_AMOUNT.parse(matches); + let fee_token = FEE_TOKEN.parse(matches); + let gas_limit = GAS_LIMIT.parse(matches); let expiration = EXPIRATION_OPT.parse(matches); let signing_key = SIGNING_KEY_OPT.parse(matches); diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index dd3b5304e5..686a348151 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -14,9 +14,8 @@ use namada::types::masp::*; use super::args; use crate::client::tx::ShieldedContext; -use crate::config::genesis::genesis_config; use crate::config::global::GlobalConfig; -use crate::config::{self, Config}; +use crate::config::{self, genesis, Config}; use crate::wallet::{AddressVpType, Wallet}; use crate::wasm_loader; @@ -92,15 +91,29 @@ impl Context { let chain_dir = global_args .base_dir .join(global_config.default_chain_id.as_str()); - let genesis_file_path = global_args - .base_dir - .join(format!("{}.toml", global_config.default_chain_id.as_str())); - let genesis = genesis_config::read_genesis_config(&genesis_file_path); + + #[cfg(not(feature = "dev"))] + let genesis = genesis::genesis( + &global_args.base_dir, + &global_config.default_chain_id, + ); + #[cfg(feature = "dev")] + let genesis = genesis::genesis(1); + let native_token = genesis.native_token; - let default_genesis = - genesis_config::open_genesis_config(genesis_file_path)?; - let wallet = - Wallet::load_or_new_from_genesis(&chain_dir, default_genesis); + #[cfg(not(feature = "dev"))] + let wallet = { + let genesis_file_path = global_args.base_dir.join(format!( + "{}.toml", + global_config.default_chain_id.as_str() + )); + let default_genesis = genesis::genesis_config::open_genesis_config( + genesis_file_path, + )?; + Wallet::load_or_new_from_genesis(&chain_dir, default_genesis) + }; + #[cfg(feature = "dev")] + let wallet = Wallet::load_or_new(&chain_dir); // If the WASM dir specified, put it in the config match global_args.wasm_dir.as_ref() { diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index f5e9fa8c57..b78bba930a 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -3,6 +3,7 @@ use std::cmp::Ordering; use std::collections::{BTreeMap, HashMap, HashSet}; use std::convert::TryInto; +use std::fmt::Display; use std::fs::File; use std::io::{self, Write}; use std::iter::Iterator; @@ -31,7 +32,7 @@ use namada::ledger::parameters::{storage as param_storage, EpochDuration}; use namada::ledger::pos::{ self, BondId, BondsAndUnbondsDetail, CommissionPair, PosParams, Slash, }; -use namada::ledger::queries::{self, RPC}; +use namada::ledger::queries::RPC; use namada::ledger::storage::ConversionState; use namada::proto::{SignedTxData, Tx}; use namada::types::address::{masp, Address}; @@ -125,7 +126,10 @@ pub async fn query_and_print_epoch(args: args::Query) -> Epoch { } /// Query the epoch of the last committed block -pub async fn query_epoch(client: &HttpClient) -> Epoch { +pub async fn query_epoch(client: &CLIENT) -> Epoch +where + CLIENT: namada::ledger::queries::Client + Sync, +{ unwrap_client_response(RPC.shell().epoch(client).await) } @@ -171,7 +175,7 @@ pub async fn query_tx_deltas( .values() .map(|fvk| ExtendedFullViewingKey::from(*fvk).fvk.vk) .collect(); - ctx.shielded.fetch(&ledger_address, &[], &fvks).await; + ctx.shielded.fetch(&client, &[], &fvks).await; // Save the update state so that future fetches can be short-circuited let _ = ctx.shielded.save(); // Required for filtering out rejected transactions from Tendermint @@ -305,15 +309,14 @@ pub async fn query_transfers(mut ctx: Context, args: args::QueryTransfers) { let amt = ctx .shielded .compute_exchanged_amount( - client.clone(), + &client, amt, epoch, Conversions::new(), ) .await .0; - let dec = - ctx.shielded.decode_amount(client.clone(), amt, epoch).await; + let dec = ctx.shielded.decode_amount(&client, amt, epoch).await; shielded_accounts.insert(acc, dec); } // Check if this transfer pertains to the supplied token @@ -634,10 +637,8 @@ pub async fn query_pinned_balance(ctx: &mut Context, args: args::QueryBalance) { (Ok((balance, epoch)), None) => { let mut found_any = false; // Print balances by human-readable token names - let balance = ctx - .shielded - .decode_amount(client.clone(), balance, epoch) - .await; + let balance = + ctx.shielded.decode_amount(&client, balance, epoch).await; for (addr, value) in balance.components() { let asset_value = token::Amount::from(*value as u64); if !found_any { @@ -896,15 +897,13 @@ pub async fn query_shielded_balance( .iter() .map(|fvk| ExtendedFullViewingKey::from(*fvk).fvk.vk) .collect(); - ctx.shielded - .fetch(&args.query.ledger_address, &[], &fvks) - .await; + // Establish connection with which to do exchange rate queries + let client = HttpClient::new(args.query.ledger_address.clone()).unwrap(); + ctx.shielded.fetch(&client, &[], &fvks).await; // Save the update state so that future fetches can be short-circuited let _ = ctx.shielded.save(); // The epoch is required to identify timestamped tokens let epoch = query_and_print_epoch(args.query.clone()).await; - // Establish connection with which to do exchange rate queries - let client = HttpClient::new(args.query.ledger_address.clone()).unwrap(); // Map addresses to token names let tokens = ctx.tokens(); match (args.token, owner.is_some()) { @@ -982,10 +981,8 @@ pub async fn query_shielded_balance( // Print non-zero balances whose asset types can be decoded for (asset_type, balances) in balances { // Decode the asset type - let decoded = ctx - .shielded - .decode_asset_type(client.clone(), asset_type) - .await; + let decoded = + ctx.shielded.decode_asset_type(&client, asset_type).await; match decoded { Some((addr, asset_epoch)) if asset_epoch == epoch => { // Only assets with the current timestamp count @@ -1084,10 +1081,8 @@ pub async fn query_shielded_balance( .compute_shielded_balance(&viewing_key) .expect("context should contain viewing key"); // Print balances by human-readable token names - let decoded_balance = ctx - .shielded - .decode_all_amounts(client.clone(), balance) - .await; + let decoded_balance = + ctx.shielded.decode_all_amounts(&client, balance).await; print_decoded_balance_with_epoch(ctx, decoded_balance); } else { balance = ctx @@ -1100,10 +1095,8 @@ pub async fn query_shielded_balance( .await .expect("context should contain viewing key"); // Print balances by human-readable token names - let decoded_balance = ctx - .shielded - .decode_amount(client.clone(), balance, epoch) - .await; + let decoded_balance = + ctx.shielded.decode_amount(&client, balance, epoch).await; print_decoded_balance(ctx, decoded_balance); } } @@ -1928,17 +1921,20 @@ pub async fn query_conversions(ctx: Context, args: args::QueryConversions) { } /// Query a conversion. -pub async fn query_conversion( - client: HttpClient, +pub async fn query_conversion( + client: &CLIENT, asset_type: AssetType, ) -> Option<( Address, Epoch, masp_primitives::transaction::components::Amount, MerklePath, -)> { +)> +where + CLIENT: namada::ledger::queries::Client + Sync, +{ Some(unwrap_client_response( - RPC.shell().read_conversion(&client, &asset_type).await, + RPC.shell().read_conversion(client, &asset_type).await, )) } @@ -1969,7 +1965,7 @@ pub async fn query_wasm_code_hash( /// Query a storage value and decode it with [`BorshDeserialize`]. pub async fn query_storage_value( - client: &HttpClient, + client: &(impl namada::ledger::queries::Client + Sync), key: &storage::Key, ) -> Option where @@ -2609,7 +2605,10 @@ fn lookup_alias(ctx: &Context, addr: &Address) -> String { } /// A helper to unwrap client's response. Will shut down process on error. -fn unwrap_client_response(response: Result) -> T { +fn unwrap_client_response(response: Result) -> T +where + E: Display, +{ response.unwrap_or_else(|err| { eprintln!("Error in the query {}", err); cli::safe_exit(1) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 70762443ca..66612d2461 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -8,8 +8,8 @@ use std::io::{Read, Write}; use std::path::PathBuf; use std::str::FromStr; +use async_std::io; use async_std::io::prelude::WriteExt; -use async_std::io::{self}; use borsh::{BorshDeserialize, BorshSerialize}; use data_encoding::HEXLOWER_PERMISSIVE; use itertools::Either::*; @@ -42,7 +42,7 @@ use namada::ledger::governance::storage as gov_storage; use namada::ledger::masp; use namada::ledger::pos::{CommissionPair, PosParams}; use namada::proto::Tx; -use namada::types::address::{masp, masp_tx_key, Address}; +use namada::types::address::{masp, Address}; use namada::types::governance::{ OfflineProposal, OfflineVote, Proposal, ProposalVote, VoteType, }; @@ -71,7 +71,7 @@ use crate::cli::{args, safe_exit, Context}; use crate::client::rpc::{ query_conversion, query_epoch, query_storage_value, query_wasm_code_hash, }; -use crate::client::signing::{find_keypair, sign_tx, tx_signer, TxSigningKey}; +use crate::client::signing::{find_keypair, sign_tx, TxSigningKey}; use crate::client::tendermint_rpc_types::{TxBroadcastData, TxResponse}; use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::facade::tendermint_rpc::endpoint::broadcast::tx_sync::Response; @@ -644,12 +644,14 @@ impl ShieldedContext { /// Fetch the current state of the multi-asset shielded pool into a /// ShieldedContext - pub async fn fetch( + pub async fn fetch( &mut self, - ledger_address: &TendermintAddress, + client: &CLIENT, sks: &[ExtendedSpendingKey], fvks: &[ViewingKey], - ) { + ) where + CLIENT: namada::ledger::queries::Client + Sync, + { // First determine which of the keys requested to be fetched are new. // Necessary because old transactions will need to be scanned for new // keys. @@ -671,7 +673,7 @@ impl ShieldedContext { let (txs, mut tx_iter); if !unknown_keys.is_empty() { // Load all transactions accepted until this point - txs = Self::fetch_shielded_transfers(ledger_address, 0).await; + txs = Self::fetch_shielded_transfers(client, 0).await; tx_iter = txs.iter(); // Do this by constructing a shielding context only for unknown keys let mut tx_ctx = ShieldedContext::new(self.context_dir.clone()); @@ -691,9 +693,7 @@ impl ShieldedContext { self.merge(tx_ctx); } else { // Load only transactions accepted from last_txid until this point - txs = - Self::fetch_shielded_transfers(ledger_address, self.last_txidx) - .await; + txs = Self::fetch_shielded_transfers(client, self.last_txidx).await; tx_iter = txs.iter(); } // Now that we possess the unspent notes corresponding to both old and @@ -733,11 +733,13 @@ impl ShieldedContext { /// transactions as a vector. More concretely, the HEAD_TX_KEY location /// stores the index of the last accepted transaction and each transaction /// is stored at a key derived from its index. - pub async fn fetch_shielded_transfers( - ledger_address: &TendermintAddress, + pub async fn fetch_shielded_transfers( + client: &CLIENT, last_txidx: u64, - ) -> BTreeMap<(BlockHeight, TxIndex), (Epoch, Transfer)> { - let client = HttpClient::new(ledger_address.clone()).unwrap(); + ) -> BTreeMap<(BlockHeight, TxIndex), (Epoch, Transfer)> + where + CLIENT: namada::ledger::queries::Client + Sync, + { // The address of the MASP account let masp_addr = masp(); // Construct the key where last transaction pointer is stored @@ -745,7 +747,7 @@ impl ShieldedContext { .push(&HEAD_TX_KEY.to_owned()) .expect("Cannot obtain a storage key"); // Query for the index of the last accepted transaction - let head_txidx = query_storage_value::(&client, &head_tx_key) + let head_txidx = query_storage_value::(client, &head_tx_key) .await .unwrap_or(0); let mut shielded_txs = BTreeMap::new(); @@ -758,7 +760,7 @@ impl ShieldedContext { // Obtain the current transaction let (tx_epoch, tx_height, tx_index, current_tx) = query_storage_value::<(Epoch, BlockHeight, TxIndex, Transfer)>( - &client, + client, ¤t_tx_key, ) .await @@ -917,11 +919,14 @@ impl ShieldedContext { /// Query the ledger for the decoding of the given asset type and cache it /// if it is found. - pub async fn decode_asset_type( + pub async fn decode_asset_type( &mut self, - client: HttpClient, + client: &CLIENT, asset_type: AssetType, - ) -> Option<(Address, Epoch)> { + ) -> Option<(Address, Epoch)> + where + CLIENT: namada::ledger::queries::Client + Sync, + { // Try to find the decoding in the cache if let decoded @ Some(_) = self.asset_types.get(&asset_type) { return decoded.cloned(); @@ -935,12 +940,15 @@ impl ShieldedContext { /// Query the ledger for the conversion that is allowed for the given asset /// type and cache it. - async fn query_allowed_conversion<'a>( + async fn query_allowed_conversion<'a, CLIENT>( &'a mut self, - client: HttpClient, + client: &CLIENT, asset_type: AssetType, conversions: &'a mut Conversions, - ) -> Option<&'a mut (AllowedConversion, MerklePath, i64)> { + ) -> Option<&'a mut (AllowedConversion, MerklePath, i64)> + where + CLIENT: namada::ledger::queries::Client + Sync, + { match conversions.entry(asset_type) { Entry::Occupied(conv_entry) => Some(conv_entry.into_mut()), Entry::Vacant(conv_entry) => { @@ -973,7 +981,7 @@ impl ShieldedContext { // And then exchange balance into current asset types Some( self.compute_exchanged_amount( - client, + &client, balance, target_epoch, HashMap::new(), @@ -1027,13 +1035,16 @@ impl ShieldedContext { /// note of the conversions that were used. Note that this function does /// not assume that allowed conversions from the ledger are expressed in /// terms of the latest asset types. - pub async fn compute_exchanged_amount( + pub async fn compute_exchanged_amount( &mut self, - client: HttpClient, + client: &CLIENT, mut input: Amount, target_epoch: Epoch, mut conversions: Conversions, - ) -> (Amount, Conversions) { + ) -> (Amount, Conversions) + where + CLIENT: namada::ledger::queries::Client + Sync, + { // Where we will store our exchanged value let mut output = Amount::zero(); // Repeatedly exchange assets until it is no longer possible @@ -1041,14 +1052,14 @@ impl ShieldedContext { input.components().next().map(cloned_pair) { let target_asset_type = self - .decode_asset_type(client.clone(), asset_type) + .decode_asset_type(client, asset_type) .await .map(|(addr, _epoch)| make_asset_type(target_epoch, &addr)) .unwrap_or(asset_type); let at_target_asset_type = asset_type == target_asset_type; if let (Some((conv, _wit, usage)), false) = ( self.query_allowed_conversion( - client.clone(), + client, asset_type, &mut conversions, ) @@ -1071,7 +1082,7 @@ impl ShieldedContext { ); } else if let (Some((conv, _wit, usage)), false) = ( self.query_allowed_conversion( - client.clone(), + client, target_asset_type, &mut conversions, ) @@ -1107,9 +1118,9 @@ impl ShieldedContext { /// of the specified asset type. Return the total value accumulated plus /// notes and the corresponding diversifiers/merkle paths that were used to /// achieve the total value. - pub async fn collect_unspent_notes( + pub async fn collect_unspent_notes( &mut self, - ledger_address: TendermintAddress, + client: &CLIENT, vk: &ViewingKey, target: Amount, target_epoch: Epoch, @@ -1117,9 +1128,10 @@ impl ShieldedContext { Amount, Vec<(Diversifier, Note, MerklePath)>, Conversions, - ) { - // Establish connection with which to do exchange rate queries - let client = HttpClient::new(ledger_address.clone()).unwrap(); + ) + where + CLIENT: namada::ledger::queries::Client + Sync, + { let mut conversions = HashMap::new(); let mut val_acc = Amount::zero(); let mut notes = Vec::new(); @@ -1143,7 +1155,7 @@ impl ShieldedContext { .expect("received note has invalid value or asset type"); let (contr, proposed_convs) = self .compute_exchanged_amount( - client.clone(), + client, pre_contr, target_epoch, conversions.clone(), @@ -1264,7 +1276,7 @@ impl ShieldedContext { let client = HttpClient::new(ledger_address.clone()).unwrap(); // Finally, exchange the balance to the transaction's epoch Ok(( - self.compute_exchanged_amount(client, amt, ep, HashMap::new()) + self.compute_exchanged_amount(&client, amt, ep, HashMap::new()) .await .0, ep, @@ -1276,15 +1288,14 @@ impl ShieldedContext { /// the given epoch are ignored. pub async fn decode_amount( &mut self, - client: HttpClient, + client: &HttpClient, amt: Amount, target_epoch: Epoch, ) -> Amount
{ let mut res = Amount::zero(); for (asset_type, val) in amt.components() { // Decode the asset type - let decoded = - self.decode_asset_type(client.clone(), *asset_type).await; + let decoded = self.decode_asset_type(client, *asset_type).await; // Only assets with the target timestamp count match decoded { Some((addr, epoch)) if epoch == target_epoch => { @@ -1300,14 +1311,13 @@ impl ShieldedContext { /// Addresses that they decode to. pub async fn decode_all_amounts( &mut self, - client: HttpClient, + client: &HttpClient, amt: Amount, ) -> Amount<(Address, Epoch)> { let mut res = Amount::zero(); for (asset_type, val) in amt.components() { // Decode the asset type - let decoded = - self.decode_asset_type(client.clone(), *asset_type).await; + let decoded = self.decode_asset_type(client, *asset_type).await; // Only assets with the target timestamp count if let Some((addr, epoch)) = decoded { res += &Amount::from_pair((addr, epoch), *val).unwrap() @@ -1347,12 +1357,14 @@ fn convert_amount( /// transactions balanced, but it is understood that transparent account changes /// are effected only by the amounts and signatures specified by the containing /// Transfer object. -async fn gen_shielded_transfer( +pub async fn gen_shielded_transfer( ctx: &mut Context, - client: &HttpClient, + client: &CLIENT, args: &args::TxTransfer, - shielded_gas: bool, -) -> Result, builder::Error> { +) -> Result, builder::Error> +where + CLIENT: namada::ledger::queries::Client + Sync, +{ // No shielded components are needed when neither source nor destination // are shielded let spending_key = ctx.get_cached(&args.source).spending_key(); @@ -1368,9 +1380,7 @@ async fn gen_shielded_transfer( // Load the current shielded context given the spending key we // possess let _ = ctx.shielded.load(); - ctx.shielded - .fetch(&args.tx.ledger_address, &spending_keys, &[]) - .await; + ctx.shielded.fetch(client, &spending_keys, &[]).await; // Save the update state so that future fetches can be // short-circuited let _ = ctx.shielded.save(); @@ -1388,26 +1398,18 @@ async fn gen_shielded_transfer( let (asset_type, amount) = convert_amount(epoch, &ctx.get(&args.token), args.amount); + // Fees are always paid outside of MASP + builder.set_fee(Amount::zero())?; + // If there are shielded inputs if let Some(sk) = spending_key { - // Transaction fees need to match the amount in the wrapper Transfer - // when MASP source is used - let (_, fee) = convert_amount( - epoch, - &ctx.get(&args.tx.fee_token), - args.tx.fee_amount, - ); - builder.set_fee(fee.clone())?; - // If the gas is coming from the shielded pool, then our shielded inputs - // must also cover the gas fee - let required_amt = if shielded_gas { amount + fee } else { amount }; // Locate unspent notes that can help us meet the transaction amount let (_, unspent_notes, used_convs) = ctx .shielded .collect_unspent_notes( - args.tx.ledger_address.clone(), + client, &to_viewing_key(&sk).vk, - required_amt, + amount, epoch, ) .await; @@ -1426,9 +1428,6 @@ async fn gen_shielded_transfer( } } } else { - // No transfer fees come from the shielded transaction for non-MASP - // sources - builder.set_fee(Amount::zero())?; // We add a dummy UTXO to our transaction, but only the source of the // parent Transfer object is used to validate fund availability let secp_sk = @@ -1437,8 +1436,9 @@ async fn gen_shielded_transfer( let secp_pk = secp256k1::PublicKey::from_secret_key(&secp_ctx, &secp_sk) .serialize(); - let hash = - ripemd160::Ripemd160::digest(&sha2::Sha256::digest(&secp_pk)); + let hash = ripemd160::Ripemd160::digest( + sha2::Sha256::digest(&secp_pk).as_slice(), + ); let script = TransparentAddress::PublicKey(hash.into()).script(); builder.add_transparent_input( secp_sk, @@ -1470,9 +1470,9 @@ async fn gen_shielded_transfer( .expect("target address should be transparent") .try_to_vec() .expect("target address encoding"); - let hash = ripemd160::Ripemd160::digest(&sha2::Sha256::digest( - target_enc.as_ref(), - )); + let hash = ripemd160::Ripemd160::digest( + sha2::Sha256::digest(target_enc.as_ref()).as_slice(), + ); builder.add_transparent_output( &TransparentAddress::PublicKey(hash.into()), asset_type, @@ -1576,37 +1576,15 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { }; let masp_addr = masp(); - // For MASP sources, use a special sentinel key recognized by VPs as default - // signer. Also, if the transaction is shielded, redact the amount and token + // For MASP sources, redact the amount and token // types by setting the transparent value to 0 and token type to a constant. // This has no side-effect because transaction is to self. - let (default_signer, amount, token) = - if source == masp_addr && target == masp_addr { - // TODO Refactor me, we shouldn't rely on any specific token here. - ( - TxSigningKey::SecretKey(masp_tx_key()), - 0.into(), - ctx.native_token.clone(), - ) - } else if source == masp_addr { - ( - TxSigningKey::SecretKey(masp_tx_key()), - args.amount, - token.clone(), - ) - } else { - ( - TxSigningKey::WalletAddress(args.source.to_address()), - args.amount, - token, - ) - }; - // If our chosen signer is the MASP sentinel key, then our shielded inputs - // will need to cover the gas fees. - let chosen_signer = tx_signer(&mut ctx, &args.tx, default_signer.clone()) - .await - .ref_to(); - let shielded_gas = masp_tx_key().ref_to() == chosen_signer; + let (amount, token) = if source == masp_addr && target == masp_addr { + // TODO Refactor me, we shouldn't rely on any specific token here. + (0.into(), token) + } else { + (args.amount, token) + }; // Determine whether to pin this transaction to a storage key let key = match ctx.get(&args.target) { TransferTarget::PaymentAddress(pa) if pa.is_pinned() => Some(pa.hash()), @@ -1626,8 +1604,7 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { // Loop twice in case the first submission attempt fails for _ in 0..2 { // Construct the shielded part of the transaction, if any - let stx_result = - gen_shielded_transfer(&mut ctx, &client, &args, shielded_gas).await; + let stx_result = gen_shielded_transfer(&mut ctx, &client, &args).await; let (shielded, shielded_tx_epoch) = match stx_result { Ok(stx) => unzip_option(stx.map(|x| (x.0, x.2))), diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index a73df375b1..fb85d8d5ec 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -422,15 +422,7 @@ pub fn init_network( config.wasm.iter_mut().for_each(|(name, config)| { // Find the sha256 from checksums.json let name = format!("{}.wasm", name); - // Full name in format `{name}.{sha256}.wasm` - let full_name = checksums.0.get(&name).unwrap(); - let hash = full_name - .split_once('.') - .unwrap() - .1 - .split_once('.') - .unwrap() - .0; + let hash = checksums.0.get(&name).unwrap().get("hash").unwrap(); config.sha256 = Some(genesis_config::HexString(hash.to_owned())); }); diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 922b0c445f..f54a4d2b41 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -1,6 +1,6 @@ //! The parameters used for the chain's genesis -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; #[cfg(not(feature = "dev"))] use std::path::Path; @@ -24,7 +24,7 @@ use rust_decimal::Decimal; /// Genesis configuration file format pub mod genesis_config { use std::array::TryFromSliceError; - use std::collections::HashMap; + use std::collections::{BTreeMap, HashMap}; use std::convert::TryInto; use std::path::Path; use std::str::FromStr; @@ -247,6 +247,8 @@ pub mod genesis_config { /// room for header data, evidence and protobuf /// serialization overhead in Tendermint blocks. pub max_proposal_bytes: ProposalBytes, + /// Max block gas + pub max_block_gas: u64, /// Minimum number of blocks per epoch. // XXX: u64 doesn't work with toml-rs! pub min_num_of_blocks: u64, @@ -270,6 +272,8 @@ pub mod genesis_config { #[cfg(not(feature = "mainnet"))] /// Fix wrapper tx fees pub wrapper_tx_fees: Option, + /// Gas table + pub gas_table: Option>, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -593,6 +597,7 @@ pub mod genesis_config { let min_duration: i64 = 60 * 60 * 24 * 365 / (parameters.epochs_per_year as i64); + let parameters = Parameters { epoch_duration: EpochDuration { min_num_of_blocks: parameters.min_num_of_blocks, @@ -607,6 +612,7 @@ pub mod genesis_config { ) .into(), max_proposal_bytes: parameters.max_proposal_bytes, + max_block_gas: parameters.max_block_gas, vp_whitelist: parameters.vp_whitelist.unwrap_or_default(), tx_whitelist: parameters.tx_whitelist.unwrap_or_default(), implicit_vp_code_path, @@ -617,8 +623,25 @@ pub mod genesis_config { staked_ratio: Decimal::ZERO, pos_inflation_amount: 0, wrapper_tx_fees: parameters.wrapper_tx_fees, + gas_table: parameters.gas_table.unwrap_or_default(), }; + // Check validity of gas table + if parameters.gas_table.len() + != parameters.tx_whitelist.len() + parameters.vp_whitelist.len() + { + panic!("Mismatching length of gas table and txs/vps whitelists"); + } + for hash in parameters + .tx_whitelist + .iter() + .chain(parameters.vp_whitelist.iter()) + { + if !parameters.gas_table.contains_key(&hash.to_lowercase()) { + panic!("Missing gas cost for hash {}", hash); + } + } + let GovernanceParamsConfig { min_proposal_fund, max_proposal_code_size, @@ -840,6 +863,8 @@ pub struct ImplicitAccount { pub struct Parameters { // Max payload size, in bytes, for a tx batch proposal. pub max_proposal_bytes: ProposalBytes, + /// Max block gas + pub max_block_gas: u64, /// Epoch duration pub epoch_duration: EpochDuration, /// Maximum expected time per block @@ -865,6 +890,8 @@ pub struct Parameters { /// Fixed Wrapper tx fees #[cfg(not(feature = "mainnet"))] pub wrapper_tx_fees: Option, + /// Gas table + pub gas_table: BTreeMap, } #[cfg(not(feature = "dev"))] @@ -951,6 +978,7 @@ pub fn genesis(num_validators: u64) -> Genesis { }, max_expected_time_per_block: namada::types::time::DurationSecs(30), max_proposal_bytes: Default::default(), + max_block_gas: 100_000_000, vp_whitelist: vec![], tx_whitelist: vec![], implicit_vp_code_path: vp_implicit_path.into(), @@ -961,7 +989,8 @@ pub fn genesis(num_validators: u64) -> 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 { address: wallet::defaults::albert_address(), diff --git a/apps/src/lib/node/ledger/mod.rs b/apps/src/lib/node/ledger/mod.rs index 810b2eead8..8ea421af56 100644 --- a/apps/src/lib/node/ledger/mod.rs +++ b/apps/src/lib/node/ledger/mod.rs @@ -1,6 +1,6 @@ mod abortable; mod broadcaster; -mod shell; +pub mod shell; mod shims; pub mod storage; pub mod tendermint_node; diff --git a/apps/src/lib/node/ledger/shell/block_space_alloc/states.rs b/apps/src/lib/node/ledger/shell/block_space_alloc/states.rs index 18712998e3..84292049a4 100644 --- a/apps/src/lib/node/ledger/shell/block_space_alloc/states.rs +++ b/apps/src/lib/node/ledger/shell/block_space_alloc/states.rs @@ -39,44 +39,26 @@ pub enum EncryptedTxBatchAllocator { /// The leader of the current Tendermint round is building /// a new batch of DKG decrypted transactions. -/// -/// For more info, read the module docs of -/// [`crate::node::ledger::shell::prepare_proposal::block_space_alloc::states`]. pub enum BuildingDecryptedTxBatch {} /// The leader of the current Tendermint round is building /// a new batch of Namada protocol transactions. -/// -/// For more info, read the module docs of -/// [`crate::node::ledger::shell::prepare_proposal::block_space_alloc::states`]. pub enum BuildingProtocolTxBatch {} /// The leader of the current Tendermint round is building /// a new batch of DKG encrypted transactions. -/// -/// For more info, read the module docs of -/// [`crate::node::ledger::shell::prepare_proposal::block_space_alloc::states`]. pub struct BuildingEncryptedTxBatch { /// One of [`WithEncryptedTxs`] and [`WithoutEncryptedTxs`]. _mode: Mode, } /// Allow block proposals to include encrypted txs. -/// -/// For more info, read the module docs of -/// [`crate::node::ledger::shell::prepare_proposal::block_space_alloc::states`]. pub enum WithEncryptedTxs {} /// Prohibit block proposals from including encrypted txs. -/// -/// For more info, read the module docs of -/// [`crate::node::ledger::shell::prepare_proposal::block_space_alloc::states`]. pub enum WithoutEncryptedTxs {} /// Try to allocate a new transaction on a [`BlockSpaceAllocator`] state. -/// -/// For more info, read the module docs of -/// [`crate::node::ledger::shell::prepare_proposal::block_space_alloc::states`]. pub trait TryAlloc { /// Try to allocate space for a new transaction. fn try_alloc(&mut self, tx: &[u8]) -> Result<(), AllocFailure>; @@ -85,11 +67,8 @@ pub trait TryAlloc { /// Represents a state transition in the [`BlockSpaceAllocator`] state machine. /// /// This trait should not be used directly. Instead, consider using one of -/// [`NextState`], [`NextStateWithEncryptedTxs`] or -/// [`NextStateWithoutEncryptedTxs`]. -/// -/// For more info, read the module docs of -/// [`crate::node::ledger::shell::prepare_proposal::block_space_alloc::states`]. +/// [`NextState`], [`WithEncryptedTxs`] or +/// [`WithoutEncryptedTxs`]. pub trait NextStateImpl { /// The next state in the [`BlockSpaceAllocator`] state machine. type Next; @@ -101,9 +80,6 @@ pub trait NextStateImpl { /// Convenience extension of [`NextStateImpl`], to transition to a new /// state with a null transition function. -/// -/// For more info, read the module docs of -/// [`crate::node::ledger::shell::prepare_proposal::block_space_alloc::states`]. pub trait NextState: NextStateImpl { /// Transition to the next state in the [`BlockSpaceAllocator`] state, /// using a null transiiton function. diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index f252b03cbc..cfeaa93b15 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1,8 +1,9 @@ //! Implementation of the `FinalizeBlock` ABCI++ method for the Shell -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use data_encoding::HEXUPPER; +use namada::ledger::gas::TxGasMeter; use namada::ledger::parameters::storage as params_storage; use namada::ledger::pos::types::{decimal_mult_u64, into_tm_voting_power}; use namada::ledger::pos::{namada_proof_of_stake, staking_token_address}; @@ -18,9 +19,11 @@ use namada::proof_of_stake::{ write_last_block_proposer_address, }; use namada::types::address::Address; +use namada::types::hash; use namada::types::key::tm_raw_hash_to_string; use namada::types::storage::{BlockHash, BlockResults, Epoch, Header}; use namada::types::token::{total_supply_key, Amount}; +use namada::types::transaction::WrapperTx; use rust_decimal::prelude::Decimal; use super::governance::execute_governance_proposals; @@ -60,9 +63,6 @@ where &mut self, req: shim::request::FinalizeBlock, ) -> Result { - // Reset the gas meter before we start - self.gas_meter.reset(); - let mut response = shim::response::FinalizeBlock::default(); // Begin the new block and check if a new epoch has begun @@ -79,6 +79,11 @@ where "Block height: {height}, epoch: {current_epoch}, new epoch: \ {new_epoch}." ); + let gas_table: BTreeMap = self + .wl_storage + .read(¶meters::storage::get_gas_table_storage_key()) + .expect("Error while reading from storage") + .expect("Missing gas table in storage"); if new_epoch { namada::ledger::storage::update_allowed_conversions( @@ -86,7 +91,7 @@ where )?; let _proposals_result = - execute_governance_proposals(self, &mut response)?; + execute_governance_proposals(self, &mut response, &gas_table)?; // Copy the new_epoch + pipeline_len - 1 validator set into // new_epoch + pipeline_len @@ -105,7 +110,6 @@ where // `copy_validator_sets_and_positions` if we're starting a new epoch self.slash(); - let wrapper_fees = self.get_wrapper_tx_fees(); let mut stats = InternalStats::default(); // Tracks the accepted transactions @@ -121,7 +125,6 @@ where ); continue; }; - let tx_length = processed_tx.tx.len(); // If [`process_proposal`] rejected a Tx due to invalid signature, // emit an event here and move on to next tx. if ErrorCodes::from_u32(processed_tx.result.code).unwrap() @@ -195,148 +198,174 @@ where .delete(&tx_hash_key) .expect("Error while deleting tx hash from storage"); } + + #[cfg(not(any(feature = "abciplus", feature = "abcipp")))] + if let TxType::Wrapper(wrapper) = &tx_type { + // Charge fee if wrapper transaction went out of gas + if ErrorCodes::from_u32(processed_tx.result.code).unwrap() + == ErrorCodes::TxGasLimit + { + #[cfg(not(feature = "mainnet"))] + let has_valid_pow = + self.invalidate_pow_solution_if_valid(wrapper); + let _ = self.charge_fee( + wrapper, + #[cfg(not(feature = "mainnet"))] + has_valid_pow, + ); + } + } + continue; } - let (mut tx_event, tx_unsigned_hash) = match &tx_type { - TxType::Wrapper(wrapper) => { - let mut tx_event = Event::new_tx_event(&tx_type, height.0); + let (mut tx_event, tx_unsigned_hash, mut tx_gas_meter) = + match &tx_type { + TxType::Wrapper(wrapper) => { + let mut tx_event = + Event::new_tx_event(&tx_type, height.0); + + let mut gas_meter = + TxGasMeter::new(u64::from(&wrapper.gas_limit)); + + // Writes both txs hash to storage + let tx = + Tx::try_from(processed_tx.tx.as_ref()).unwrap(); + let wrapper_tx_hash_key = + replay_protection::get_tx_hash_key(&hash::Hash( + tx.unsigned_hash(), + )); + self.wl_storage + .storage + .write(&wrapper_tx_hash_key, vec![]) + .expect("Error while writing tx hash to storage"); + + let inner_tx_hash_key = + replay_protection::get_tx_hash_key( + &wrapper.tx_hash, + ); + self.wl_storage + .storage + .write(&inner_tx_hash_key, vec![]) + .expect("Error while writing tx hash to storage"); - // Writes both txs hash to storage - let tx = Tx::try_from(processed_tx.tx.as_ref()).unwrap(); - let wrapper_tx_hash_key = - replay_protection::get_tx_hash_key(&hash::Hash( - tx.unsigned_hash(), - )); - self.wl_storage - .storage - .write(&wrapper_tx_hash_key, vec![]) - .expect("Error while writing tx hash to storage"); + // Charge fee before performing any fallible operations + #[cfg(not(feature = "mainnet"))] + let has_valid_pow = + self.invalidate_pow_solution_if_valid(wrapper); - let inner_tx_hash_key = - replay_protection::get_tx_hash_key(&wrapper.tx_hash); - self.wl_storage - .storage - .write(&inner_tx_hash_key, vec![]) - .expect("Error while writing tx hash to storage"); + if let Err(e) = self.charge_fee( + wrapper, + #[cfg(not(feature = "mainnet"))] + has_valid_pow, + ) { + tx_event["info"] = e.to_string(); + tx_event["code"] = ErrorCodes::InvalidTx.into(); + tx_event["gas_used"] = gas_meter + .get_current_transaction_gas() + .to_string(); - #[cfg(not(feature = "mainnet"))] - let has_valid_pow = - self.invalidate_pow_solution_if_valid(wrapper); - - // Charge fee - let fee_payer = - if wrapper.pk != address::masp_tx_key().ref_to() { - wrapper.fee_payer() - } else { - address::masp() - }; - - let balance_key = - token::balance_key(&wrapper.fee.token, &fee_payer); - let balance: token::Amount = self - .wl_storage - .read(&balance_key) - .expect("must be able to read") - .unwrap_or_default(); + response.events.push(tx_event); + continue; + } - match balance.checked_sub(wrapper_fees) { - Some(amount) => { - self.wl_storage - .storage - .write( - &balance_key, - amount.try_to_vec().unwrap(), - ) - .unwrap(); + // Account for gas + if let Err(e) = + gas_meter.add_tx_size_gas(processed_tx.tx.len()) + { + tx_event["info"] = format!("{}", e); + tx_event["code"] = ErrorCodes::TxGasLimit.into(); + tx_event["gas_used"] = gas_meter + .get_current_transaction_gas() + .to_string(); + + response.events.push(tx_event); + continue; } - None => { - #[cfg(not(feature = "mainnet"))] - let reject = !has_valid_pow; - #[cfg(feature = "mainnet")] - let reject = true; - if reject { - // Burn remaining funds - self.wl_storage - .storage - .write( - &balance_key, - Amount::from(0).try_to_vec().unwrap(), + + let spare_gas = u64::from(&wrapper.gas_limit) + - gas_meter.get_current_transaction_gas(); + self.wl_storage.storage.tx_queue.push( + WrapperTxInQueue { + tx: wrapper.clone(), + gas: spare_gas, + #[cfg(not(feature = "mainnet"))] + has_valid_pow, + }, + ); + ( + tx_event, None, + gas_meter, + // This is just for + // logging/events + // purposes, no more + // gas is actually + // used by the + // wrapper + ) + } + TxType::Decrypted(inner) => { + // We remove the corresponding wrapper tx from the queue + let wrapper = self + .wl_storage + .storage + .tx_queue + .pop() + .expect("Missing wrapper tx in queue"); + let mut event = Event::new_tx_event(&tx_type, height.0); + + match inner { + DecryptedTx::Decrypted { + tx, + has_valid_pow: _, + } => { + stats.increment_tx_type( + namada::core::types::hash::Hash( + tx.code_hash(), ) - .unwrap(); - tx_event["info"] = - "Insufficient balance for fee".into(); - tx_event["code"] = ErrorCodes::InvalidTx.into(); - tx_event["gas_used"] = "0".to_string(); - - response.events.push(tx_event); - continue; + .to_string(), + ); + } + DecryptedTx::Undecryptable(_) => { + event["log"] = "Transaction could not be \ + decrypted." + .into(); + event["code"] = + ErrorCodes::Undecryptable.into(); } } - } - self.wl_storage.storage.tx_queue.push(WrapperTxInQueue { - tx: wrapper.clone(), - #[cfg(not(feature = "mainnet"))] - has_valid_pow, - }); - (tx_event, None) - } - TxType::Decrypted(inner) => { - // We remove the corresponding wrapper tx from the queue - let wrapper_hash = self - .wl_storage - .storage - .tx_queue - .pop() - .expect("Missing wrapper tx in queue") - .tx - .tx_hash; - let mut event = Event::new_tx_event(&tx_type, height.0); - - match inner { - DecryptedTx::Decrypted { - tx, - has_valid_pow: _, - } => { - stats.increment_tx_type( - namada::core::types::hash::Hash(tx.code_hash()) - .to_string(), - ); - } - DecryptedTx::Undecryptable(_) => { - event["log"] = - "Transaction could not be decrypted.".into(); - event["code"] = ErrorCodes::Undecryptable.into(); - } + ( + event, + Some(wrapper.tx.tx_hash), + TxGasMeter::new(wrapper.gas), + ) } - (event, Some(wrapper_hash)) - } - TxType::Raw(_) => { - tracing::error!( - "Internal logic error: FinalizeBlock received a \ - TxType::Raw transaction" - ); - continue; - } - TxType::Protocol(_) => { - tracing::error!( - "Internal logic error: FinalizeBlock received a \ - TxType::Protocol transaction" - ); - continue; - } - }; + TxType::Raw(_) => { + tracing::error!( + "Internal logic error: FinalizeBlock received a \ + TxType::Raw transaction" + ); + continue; + } + TxType::Protocol(_) => { + tracing::error!( + "Internal logic error: FinalizeBlock received a \ + TxType::Protocol transaction" + ); + continue; + } + }; match protocol::apply_tx( tx_type, - tx_length, TxIndex( tx_index .try_into() .expect("transaction index out of bounds"), ), - &mut self.gas_meter, + &mut tx_gas_meter, + &gas_table, &mut self.wl_storage.write_log, &self.wl_storage.storage, &mut self.vp_wasm_cache, @@ -408,7 +437,7 @@ where // out of gas, remove its hash from storage to allow // rewrapping it if let Some(hash) = tx_unsigned_hash { - if let Error::TxApply(protocol::Error::GasError(namada::ledger::gas::Error::TransactionGasExceededError)) = + if let Error::TxApply(protocol::Error::GasError(_)) = msg { let tx_hash_key = @@ -417,16 +446,15 @@ where .storage .delete(&tx_hash_key) .expect( - "Error while deleting tx hash key from storage", - ); + "Error while deleting tx hash key from \ + storage", + ); } } self.wl_storage.drop_tx(); - tx_event["gas_used"] = self - .gas_meter - .get_current_transaction_gas() - .to_string(); + tx_event["gas_used"] = + tx_gas_meter.get_current_transaction_gas().to_string(); tx_event["info"] = msg.to_string(); tx_event["code"] = ErrorCodes::WasmRuntimeError.into(); } @@ -504,11 +532,6 @@ where )?; } - let _ = self - .gas_meter - .finalize_transaction() - .map_err(|_| Error::GasOverflow)?; - self.event_log_mut().log_events(response.events.clone()); tracing::debug!("End finalize_block {height} of epoch {current_epoch}"); @@ -528,8 +551,6 @@ where ) -> (BlockHeight, bool) { let height = self.wl_storage.storage.last_height + 1; - self.gas_meter.reset(); - self.wl_storage .storage .begin_block(hash, height) @@ -809,6 +830,53 @@ where Ok(()) } + + /// Charge fee for the provided wrapper transaction + fn charge_fee( + &mut self, + wrapper: &WrapperTx, + #[cfg(not(feature = "mainnet"))] has_valid_pow: bool, + ) -> Result<()> { + // Charge fee + let balance_key = + token::balance_key(&wrapper.fee.token, &wrapper.fee_payer()); + let balance: token::Amount = self + .wl_storage + .read(&balance_key) + .expect("must be able to read") + .unwrap_or_default(); + + let wrapper_fees = self.get_wrapper_tx_fees(); + match balance.checked_sub(wrapper_fees) { + Some(amount) => { + self.wl_storage + .storage + .write(&balance_key, amount.try_to_vec().unwrap()) + .unwrap(); + } + None => { + #[cfg(not(feature = "mainnet"))] + let reject = !has_valid_pow; + #[cfg(feature = "mainnet")] + let reject = true; + + if reject { + // Burn remaining funds + self.wl_storage + .storage + .write( + &balance_key, + Amount::from(0).try_to_vec().unwrap(), + ) + .unwrap(); + + return Err(Error::TxApply(protocol::Error::FeeError)); + } + } + } + + Ok(()) + } } /// Convert ABCI vote info to PoS vote info. Any info which fails the conversion @@ -916,6 +984,8 @@ mod test_finalize_block { FinalizeBlock, ProcessedTx, }; + const GAS_LIMIT_MULTIPLIER: u64 = 1; + /// Check that if a wrapper tx was rejected by [`process_proposal`], /// check that the correct event is returned. Check that it does /// not appear in the queue of txs to be decrypted @@ -952,7 +1022,7 @@ mod test_finalize_block { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), raw_tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -971,7 +1041,10 @@ mod test_finalize_block { }, }); } else { - shell.enqueue_tx(wrapper.clone()); + shell.enqueue_tx( + wrapper.clone(), + u64::from(&wrapper.gas_limit) - tx.to_bytes().len() as u64, + ); } if i != 3 { @@ -1009,7 +1082,7 @@ mod test_finalize_block { } /// Check that if a decrypted tx was rejected by [`process_proposal`], - /// check that the correct event is returned. Check that it is still + /// the correct event is returned. Check that it is still /// removed from the queue of txs to be included in the next block /// proposal #[test] @@ -1029,12 +1102,16 @@ mod test_finalize_block { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), raw_tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] None, ); + let signed_wrapper = wrapper + .sign(&keypair, shell.chain_id.clone(), None) + .unwrap() + .to_bytes(); let processed_tx = ProcessedTx { tx: Tx::from(TxType::Decrypted(DecryptedTx::Decrypted { @@ -1048,7 +1125,9 @@ mod test_finalize_block { info: "".into(), }, }; - shell.enqueue_tx(wrapper); + let gas_limit = + u64::from(&wrapper.gas_limit) - signed_wrapper.len() as u64; + shell.enqueue_tx(wrapper, gas_limit); // check that the decrypted tx was not applied for event in shell @@ -1087,12 +1166,16 @@ mod test_finalize_block { }, pk: keypair.ref_to(), epoch: Epoch(0), - gas_limit: 0.into(), + gas_limit: GAS_LIMIT_MULTIPLIER.into(), inner_tx, tx_hash: hash_tx(&tx), #[cfg(not(feature = "mainnet"))] pow_solution: None, }; + let signed_wrapper = wrapper + .sign(&keypair, shell.chain_id.clone(), None) + .unwrap() + .to_bytes(); let processed_tx = ProcessedTx { tx: Tx::from(TxType::Decrypted(DecryptedTx::Undecryptable( wrapper.clone(), @@ -1104,7 +1187,9 @@ mod test_finalize_block { }, }; - shell.enqueue_tx(wrapper); + let gas_limit = + u64::from(&wrapper.gas_limit) - signed_wrapper.len() as u64; + shell.enqueue_tx(wrapper, gas_limit); // check that correct error message is returned for event in shell @@ -1165,13 +1250,19 @@ mod test_finalize_block { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), raw_tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] None, ); - shell.enqueue_tx(wrapper_tx); + let signed_wrapper = wrapper_tx + .sign(&keypair, shell.chain_id.clone(), None) + .unwrap() + .to_bytes(); + let gas_limit = + u64::from(&wrapper_tx.gas_limit) - signed_wrapper.len() as u64; + shell.enqueue_tx(wrapper_tx, gas_limit); processed_txs.push(ProcessedTx { tx: Tx::from(TxType::Decrypted(DecryptedTx::Decrypted { tx: raw_tx, @@ -1204,7 +1295,7 @@ mod test_finalize_block { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), raw_tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1741,28 +1832,103 @@ mod test_finalize_block { info: "".into(), }, }; - shell.enqueue_tx(wrapper_tx); + 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 + .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(1); + + 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, + 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/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index dfdae4d04e..b590f17183 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use namada::core::ledger::slash_fund::ADDRESS as slash_fund_address; use namada::core::types::transaction::governance::ProposalType; use namada::ledger::events::EventType; @@ -27,6 +29,7 @@ pub struct ProposalsResult { pub fn execute_governance_proposals( shell: &mut Shell, response: &mut shim::response::FinalizeBlock, + gas_table: &BTreeMap, ) -> Result where D: DB + for<'iter> DBIter<'iter> + Sync + 'static, @@ -76,7 +79,9 @@ where let transfer_address = match tally_result { TallyResult::Passed(tally) => { let (successful_execution, proposal_event) = match tally { - Tally::Default => execute_default_proposal(shell, id), + Tally::Default => { + execute_default_proposal(shell, id, gas_table) + } Tally::PGFCouncil(council) => { execute_pgf_proposal(id, council) } @@ -138,6 +143,7 @@ where fn execute_default_proposal( shell: &mut Shell, id: u64, + gas_table: &BTreeMap, ) -> (bool, Event) where D: DB + for<'iter> DBIter<'iter> + Sync + 'static, @@ -166,11 +172,10 @@ where .expect("Should be able to write to storage."); let tx_result = protocol::apply_tx( tx_type, - 0, /* this is used to compute the fee - * based on the code size. We dont - * need it here. */ TxIndex::default(), - &mut BlockGasMeter::default(), + &mut TxGasMeter::new(u64::MAX), /* No gas limit for + * governance proposals */ + gas_table, &mut shell.wl_storage.write_log, &shell.wl_storage.storage, &mut shell.vp_wasm_cache, diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 3aabed3196..fa6fa3314e 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -77,6 +77,7 @@ where let genesis::Parameters { epoch_duration, max_proposal_bytes, + max_block_gas, max_expected_time_per_block, vp_whitelist, tx_whitelist, @@ -88,6 +89,7 @@ where staked_ratio, pos_inflation_amount, wrapper_tx_fees, + gas_table, } = genesis.parameters; #[cfg(not(feature = "mainnet"))] // Try to find a faucet account @@ -109,14 +111,18 @@ where // Store wasm codes into storage let checksums = wasm_loader::Checksums::read_checksums(&self.wasm_dir); - for (name, full_name) in checksums.0.iter() { + for (name, info) in checksums.0.iter() { let code = wasm_loader::read_wasm(&self.wasm_dir, name) .map_err(Error::ReadingWasm)?; let code_hash = CodeHash::sha256(&code); + let code_len = u64::try_from(code.len()) + .map_err(|e| Error::LoadingWasm(e.to_string()))?; - let elements = full_name.split('.').collect::>(); - let checksum = elements.get(1).ok_or_else(|| { - Error::LoadingWasm(format!("invalid full name: {}", full_name)) + let checksum = info.get("hash").ok_or_else(|| { + Error::LoadingWasm(format!( + "Missing wasm hash for tx: {}", + name + )) })?; assert_eq!( code_hash.to_string(), @@ -139,6 +145,9 @@ where let code_key = Key::wasm_code(&code_hash); self.wl_storage.write_bytes(&code_key, code)?; + let code_len_key = Key::wasm_code_len(&code_hash); + self.wl_storage.write(&code_len_key, code_len)?; + let hash_key = Key::wasm_hash(name); self.wl_storage.write_bytes(&hash_key, code_hash)?; } else { @@ -170,6 +179,7 @@ where let parameters = Parameters { epoch_duration, max_proposal_bytes, + max_block_gas, max_expected_time_per_block, vp_whitelist, tx_whitelist, @@ -183,6 +193,7 @@ where faucet_account, #[cfg(not(feature = "mainnet"))] wrapper_tx_fees, + gas_table, }; parameters .init_storage(&mut self.wl_storage) diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 8c617e396b..ead7a55417 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -5,16 +5,16 @@ //! and [`Shell::process_proposal`] must be also reverted //! (unless we can simply overwrite them in the next block). //! More info in . -mod block_space_alloc; +pub mod block_space_alloc; mod finalize_block; mod governance; mod init_chain; -mod prepare_proposal; -mod process_proposal; +pub mod prepare_proposal; +pub mod process_proposal; mod queries; mod stats; -use std::collections::HashSet; +use std::collections::{BTreeMap, HashSet}; use std::convert::{TryFrom, TryInto}; use std::mem; use std::path::{Path, PathBuf}; @@ -24,7 +24,7 @@ use std::rc::Rc; use borsh::{BorshDeserialize, BorshSerialize}; use namada::ledger::events::log::EventLog; use namada::ledger::events::Event; -use namada::ledger::gas::BlockGasMeter; +use namada::ledger::gas::{BlockGasMeter, TxGasMeter}; use namada::ledger::pos::namada_proof_of_stake::types::{ ConsensusValidator, ValidatorSetUpdate, }; @@ -33,23 +33,22 @@ use namada::ledger::storage::{ DBIter, Sha256Hasher, Storage, StorageHasher, WlStorage, DB, }; use namada::ledger::storage_api::{self, StorageRead}; -use namada::ledger::{ibc, pos, protocol, replay_protection}; +use namada::ledger::{ibc, parameters, pos, protocol, replay_protection}; use namada::proof_of_stake::{self, read_pos_params, slash}; use namada::proto::{self, Tx}; -use namada::types::address::{masp, masp_tx_key, Address}; +use namada::types::address::Address; use namada::types::chain::ChainId; use namada::types::internal::WrapperTxInQueue; use namada::types::key::*; use namada::types::storage::{BlockHeight, Key, TxIndex}; use namada::types::time::{DateTimeUtc, TimeZone, Utc}; -use namada::types::token::{self}; #[cfg(not(feature = "mainnet"))] use namada::types::transaction::MIN_FEE; use namada::types::transaction::{ hash_tx, process_tx, verify_decrypted_correctly, AffineCurve, DecryptedTx, EllipticCurve, PairingEngine, TxType, }; -use namada::types::{address, hash}; +use namada::types::{address, hash, token}; use namada::vm::wasm::{TxCache, VpCache}; use namada::vm::WasmCacheRwAccess; use num_derive::{FromPrimitive, ToPrimitive}; @@ -96,8 +95,6 @@ pub enum Error { TxDecoding(proto::Error), #[error("Error trying to apply a transaction: {0}")] TxApply(protocol::Error), - #[error("Gas limit exceeding while applying transactions in block")] - GasOverflow, #[error("{0}")] Tendermint(tendermint_node::Error), #[error("Server error: {0}")] @@ -131,16 +128,19 @@ pub enum ErrorCodes { Ok = 0, InvalidDecryptedChainId = 1, ExpiredDecryptedTx = 2, - WasmRuntimeError = 3, - InvalidTx = 4, - InvalidSig = 5, - InvalidOrder = 6, - ExtraTxs = 7, - Undecryptable = 8, - AllocationError = 9, - ReplayTx = 10, - InvalidChainId = 11, - ExpiredTx = 12, + DecryptedTxGasLimit = 3, + WasmRuntimeError = 4, + InvalidTx = 5, + InvalidSig = 6, + InvalidOrder = 7, + ExtraTxs = 8, + Undecryptable = 9, + AllocationError = 10, + ReplayTx = 11, + InvalidChainId = 12, + ExpiredTx = 13, + BlockGasLimit = 14, + TxGasLimit = 15, } impl ErrorCodes { @@ -154,10 +154,11 @@ impl ErrorCodes { Ok | InvalidDecryptedChainId | ExpiredDecryptedTx - | WasmRuntimeError => true, + | WasmRuntimeError + | DecryptedTxGasLimit => true, InvalidTx | InvalidSig | InvalidOrder | ExtraTxs | Undecryptable | AllocationError | ReplayTx | InvalidChainId - | ExpiredTx => false, + | ExpiredTx | BlockGasLimit | TxGasLimit => false, } } } @@ -246,9 +247,7 @@ where #[allow(dead_code)] chain_id: ChainId, /// The persistent storage with write log - pub(super) wl_storage: WlStorage, - /// Gas meter for the current block - gas_meter: BlockGasMeter, + pub wl_storage: WlStorage, /// Byzantine validators given from ABCI++ `prepare_proposal` are stored in /// this field. They will be slashed when we finalize the block. byzantine_validators: Vec, @@ -261,9 +260,9 @@ where #[allow(dead_code)] mode: ShellMode, /// VP WASM compilation cache - vp_wasm_cache: VpCache, + pub vp_wasm_cache: VpCache, /// Tx WASM compilation cache - tx_wasm_cache: TxCache, + pub tx_wasm_cache: TxCache, /// Taken from config `storage_read_past_height_limit`. When set, will /// limit the how many block heights in the past can the storage be /// queried for reading values. @@ -375,7 +374,6 @@ where Self { chain_id, wl_storage, - gas_meter: BlockGasMeter::default(), byzantine_validators: vec![], base_dir, wasm_dir, @@ -709,6 +707,30 @@ where // Tx type check if let TxType::Wrapper(wrapper) = tx_type { + // Tx gas limit + let mut gas_meter = TxGasMeter::new(u64::from(&wrapper.gas_limit)); + if gas_meter.add_tx_size_gas(tx_bytes.len()).is_err() { + response.code = ErrorCodes::TxGasLimit.into(); + response.log = + "Wrapper transactions exceeds its gas limit".to_string(); + return response; + } + + // Max block gas + let block_gas_limit: u64 = self + .wl_storage + .read(¶meters::storage::get_max_block_gas_key()) + .expect("Error while reading from storage") + .expect("Missing max_block_gas parameter in storage"); + let mut block_gas_meter = BlockGasMeter::new(block_gas_limit); + if block_gas_meter.finalize_transaction(gas_meter).is_err() { + response.code = ErrorCodes::BlockGasLimit.into(); + response.log = "Wrapper transaction exceeds the maximum block \ + gas limit" + .to_string(); + return response; + } + // Replay protection check let inner_hash_key = replay_protection::get_tx_hash_key(&wrapper.tx_hash); @@ -749,14 +771,9 @@ where return response; } - // Check balance for fee - let fee_payer = if wrapper.pk != masp_tx_key().ref_to() { - wrapper.fee_payer() - } else { - masp() - }; // check that the fee payer has sufficient balance - let balance = self.get_balance(&wrapper.fee.token, &fee_payer); + let balance = + self.get_balance(&wrapper.fee.token, &wrapper.fee_payer()); // In testnets with a faucet, tx is allowed to skip fees if // it includes a valid PoW @@ -788,7 +805,17 @@ where /// Simulate validation and application of a transaction. fn dry_run_tx(&self, tx_bytes: &[u8]) -> response::Query { let mut response = response::Query::default(); - let mut gas_meter = BlockGasMeter::default(); + let gas_table: BTreeMap = self + .wl_storage + .read(¶meters::storage::get_gas_table_storage_key()) + .expect("Error while reading from storage") + .expect("Missing gas table in storage"); + let mut gas_meter = TxGasMeter::new( + self.wl_storage + .read(¶meters::storage::get_max_block_gas_key()) + .expect("Error while reading from storage") + .expect("Missing max_block_gas parameter in storage"), + ); let mut write_log = WriteLog::default(); let mut vp_wasm_cache = self.vp_wasm_cache.read_only(); let mut tx_wasm_cache = self.tx_wasm_cache.read_only(); @@ -803,9 +830,9 @@ where }); match protocol::apply_tx( tx, - tx_bytes.len(), TxIndex::default(), &mut gas_meter, + &gas_table, &mut write_log, &self.wl_storage.storage, &mut vp_wasm_cache, @@ -1085,15 +1112,17 @@ mod test_utils { } /// Add a wrapper tx to the queue of txs to be decrypted - /// in the current block proposal + /// in the current block proposal. Takes the length of the encoded + /// wrapper as parameter. #[cfg(test)] - pub fn enqueue_tx(&mut self, wrapper: WrapperTx) { + pub fn enqueue_tx(&mut self, wrapper: WrapperTx, inner_tx_gas: u64) { self.shell .wl_storage .storage .tx_queue .push(WrapperTxInQueue { tx: wrapper, + gas: inner_tx_gas, #[cfg(not(feature = "mainnet"))] has_valid_pow: false, }); @@ -1118,6 +1147,8 @@ mod test_utils { }, num_validators, ); + test.commit(); + (test, receiver) } @@ -1183,14 +1214,21 @@ mod test_utils { }, &keypair, Epoch(0), - 0.into(), + 1.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] None, ); + let signed_wrapper = wrapper + .sign(&keypair, shell.chain_id.clone(), None) + .unwrap() + .to_bytes(); + let gas_limit = + u64::from(&wrapper.gas_limit) - signed_wrapper.len() as u64; shell.wl_storage.storage.tx_queue.push(WrapperTxInQueue { tx: wrapper, + gas: gas_limit, #[cfg(not(feature = "mainnet"))] has_valid_pow: false, }); @@ -1394,8 +1432,7 @@ mod test_mempool_validate { /// transactions #[test] fn test_replay_attack() { - let (mut shell, _) = TestShell::new(); - + let (mut shell, _) = test_utils::setup(1); let keypair = super::test_utils::gen_keypair(); let tx = Tx::new( @@ -1412,7 +1449,7 @@ mod test_mempool_validate { }, &keypair, Epoch(0), - 0.into(), + 1.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1553,4 +1590,83 @@ mod test_mempool_validate { ); 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 (shell, _) = test_utils::setup(1); + + let block_gas_limit: u64 = shell + .wl_storage + .read(¶meters::storage::get_max_block_gas_key()) + .expect("Error while reading from storage") + .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 (shell, _) = test_utils::setup(1); + + 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 d48d5cfcec..56c97bd20c 100644 --- a/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -1,7 +1,11 @@ //! Implementation of the [`RequestPrepareProposal`] ABCI++ method for the Shell use namada::core::hints; +use namada::core::ledger::gas::TxGasMeter; +use namada::core::ledger::parameters; +use namada::ledger::gas::BlockGasMeter; use namada::ledger::storage::{DBIter, StorageHasher, DB}; +use namada::ledger::storage_api::StorageRead; use namada::proof_of_stake::pos_queries::PosQueries; use namada::proto::Tx; use namada::types::internal::WrapperTxInQueue; @@ -11,8 +15,6 @@ use namada::types::transaction::wrapper::wrapper_tx::PairingEngine; use namada::types::transaction::{AffineCurve, DecryptedTx, EllipticCurve}; use super::super::*; -#[allow(unused_imports)] -use super::block_space_alloc; use super::block_space_alloc::states::{ BuildingDecryptedTxBatch, BuildingProtocolTxBatch, EncryptedTxBatchAllocator, NextState, TryAlloc, @@ -32,8 +34,8 @@ where { /// Begin a new block. /// - /// Block construction is documented in [`block_space_alloc`] - /// and [`block_space_alloc::states`]. + /// Block construction is documented in [`super::block_space_alloc`] + /// and [`super::block_space_alloc::states`]. /// /// INVARIANT: Any changes applied in this method must be reverted if /// the proposal is rejected (unless we can simply overwrite @@ -128,6 +130,13 @@ where // valid because of mempool check TryInto::::try_into(block_time).ok() }); + let mut temp_block_gas_meter = BlockGasMeter::new( + self.wl_storage + .read(¶meters::storage::get_max_block_gas_key()) + .expect("Error while reading from storage") + .expect("Missing max_block_gas parameter in storage"), + ); + let txs = txs .iter() .filter_map(|tx_bytes| { @@ -138,8 +147,18 @@ where if let (Some(block_time), Some(exp)) = (block_time.as_ref(), &tx.expiration) { if block_time > exp { return None } } - if let Ok(TxType::Wrapper(_)) = process_tx(tx) { + if let Ok(TxType::Wrapper(wrapper)) = process_tx(tx) { + + // Check tx gas limit + let mut tx_gas_meter = TxGasMeter::new(wrapper.gas_limit.into()); + if tx_gas_meter + .add_tx_size_gas(tx_bytes.len()).is_err() { + return None; + } + + if temp_block_gas_meter.try_finalize_transaction(tx_gas_meter).is_ok() { return Some(tx_bytes.clone()); + } } } None @@ -205,6 +224,7 @@ where .map( |WrapperTxInQueue { tx, + gas: _, #[cfg(not(feature = "mainnet"))] has_valid_pow, }| { @@ -277,6 +297,8 @@ mod test_prepare_proposal { use super::*; use crate::node::ledger::shell::test_utils::{self, gen_keypair}; + const GAS_LIMIT_MULTIPLIER: u64 = 1; + /// Test that if a tx from the mempool is not a /// WrapperTx type, it is not included in the /// proposed block. @@ -376,7 +398,7 @@ mod test_prepare_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -385,7 +407,7 @@ mod test_prepare_proposal { let wrapper = wrapper_tx .sign(&keypair, shell.chain_id.clone(), None) .expect("Test failed"); - shell.enqueue_tx(wrapper_tx); + shell.enqueue_tx(wrapper_tx, 0); expected_wrapper.push(wrapper.clone()); req.txs.push(wrapper.to_bytes()); } @@ -457,4 +479,92 @@ mod test_prepare_proposal { eprintln!("Proposal: {:?}", result.txs); 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 (shell, _) = test_utils::setup(1); + + let block_gas_limit: u64 = shell + .wl_storage + .read(¶meters::storage::get_max_block_gas_key()) + .expect("Error while reading from storage") + .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() + }; + 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 (shell, _) = test_utils::setup(1); + 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() + }; + 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 acc57e5979..692f94eacd 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -1,12 +1,16 @@ //! Implementation of the ['VerifyHeader`], [`ProcessProposal`], //! and [`RevertProposal`] ABCI++ methods for the Shell +use std::collections::BTreeMap; + use data_encoding::HEXUPPER; use namada::core::hints; use namada::core::ledger::storage::WlStorage; use namada::core::types::hash::Hash; +use namada::ledger::gas::TxVpGasMetering; use namada::ledger::storage::TempWlStorage; use namada::proof_of_stake::pos_queries::PosQueries; +use namada::types::hash::HASH_LENGTH; use namada::types::internal::WrapperTxInQueue; use super::*; @@ -139,6 +143,20 @@ where let mut tx_queue_iter = self.wl_storage.storage.tx_queue.iter(); let mut temp_wl_storage = TempWlStorage::new(&self.wl_storage.storage); let mut metadata = ValidationMeta::from(&self.wl_storage); + let mut temp_block_gas_meter = BlockGasMeter::new( + self.wl_storage + .read(¶meters::storage::get_max_block_gas_key()) + .expect("Error while reading from storage") + .expect("Missing max_block_gas parameter in storage"), + ); + let gas_table: BTreeMap = self + .wl_storage + .read(¶meters::storage::get_gas_table_storage_key()) + .expect("Error while reading from storage") + .expect("Missing gas table in storage"); + + let mut wrapper_index = 0; + let tx_results = txs .iter() .map(|tx_bytes| { @@ -147,7 +165,10 @@ where &mut tx_queue_iter, &mut metadata, &mut temp_wl_storage, + &mut temp_block_gas_meter, block_time, + &gas_table, + &mut wrapper_index, ); if let ErrorCodes::Ok = ErrorCodes::from_u32(result.code).unwrap() @@ -186,13 +207,17 @@ where /// INVARIANT: Any changes applied in this method must be reverted if the /// proposal is rejected (unless we can simply overwrite them in the /// next block). - pub(crate) fn process_single_tx<'a>( + #[allow(clippy::too_many_arguments)] + pub fn process_single_tx<'a>( &self, tx_bytes: &[u8], tx_queue_iter: &mut impl Iterator, metadata: &mut ValidationMeta, temp_wl_storage: &mut TempWlStorage, + temp_block_gas_meter: &mut BlockGasMeter, block_time: DateTimeUtc, + gas_table: &BTreeMap, + wrapper_index: &mut usize, ) -> TxResult { // try to allocate space for this tx if let Err(e) = metadata.txs_bin.try_dump(tx_bytes) { @@ -287,7 +312,10 @@ where } } TxType::Decrypted(tx) => { - metadata.has_decrypted_txs = true; + // Increase wrapper index + let tx_index = *wrapper_index; + *wrapper_index += 1; + match tx_queue_iter.next() { Some(wrapper) => { if wrapper.tx.tx_hash != tx.hash_commitment() { @@ -333,7 +361,56 @@ where }; } } + + // Tx gas (partial check) + let tx_hash = + if tx.code_or_hash.len() == HASH_LENGTH { + match Hash::try_from( + tx.code_or_hash.as_slice(), + ) { + Ok(hash) => hash, + Err(_) => return TxResult { + code: + ErrorCodes::DecryptedTxGasLimit + .into(), + info: "Failed conversion of \ + transaction's hash" + .to_string(), + }, + } + } else { + Hash(tx.code_hash()) + }; + let tx_gas = match gas_table.get( + &tx_hash.to_string().to_ascii_lowercase(), + ) { + Some(gas) => gas.to_owned(), + #[cfg(test)] + None => 1_000, + #[cfg(not(test))] + None => 0, /* VPs will rejected the + * non-whitelisted tx */ + }; + let inner_tx_gas_limit = temp_wl_storage + .storage + .tx_queue + .get(tx_index) + .map_or(0, |wrapper| wrapper.gas); + let mut tx_gas_meter = + TxGasMeter::new(inner_tx_gas_limit); + if let Err(e) = tx_gas_meter.add(tx_gas) { + return TxResult { + code: ErrorCodes::DecryptedTxGasLimit + .into(), + info: format!( + "Decrypted transaction gas error: \ + {}", + e + ), + }; + } } + TxResult { code: ErrorCodes::Ok.into(), info: "Process Proposal accepted this \ @@ -358,6 +435,38 @@ 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 (ABCI) + let mut tx_gas_meter = + TxGasMeter::new(u64::from(&wrapper.gas_limit)); + if tx_gas_meter.add_tx_size_gas(tx_bytes.len()).is_err() { + // 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 { + code: ErrorCodes::TxGasLimit.into(), + info: "Wrapper transactions exceeds its gas limit" + .to_string(), + }; + } + + if temp_block_gas_meter + .finalize_transaction(tx_gas_meter) + .is_err() + { + return TxResult { + code: ErrorCodes::BlockGasLimit.into(), + + info: "Wrapper transaction exceeds the maximum block \ + gas limit" + .to_string(), + }; + } + // decrypted txs shouldn't show up before wrapper txs if metadata.has_decrypted_txs { return TxResult { @@ -477,18 +586,9 @@ where .write(&wrapper_hash_key, vec![]) .expect("Couldn't write wrapper tx hash to write log"); - // If the public key corresponds to the MASP sentinel - // transaction key, then the fee payer is effectively - // the MASP, otherwise derive - // they payer from public key. - let fee_payer = if wrapper.pk != masp_tx_key().ref_to() { - wrapper.fee_payer() - } else { - masp() - }; // check that the fee payer has sufficient balance - let balance = - self.get_balance(&wrapper.fee.token, &fee_payer); + let balance = self + .get_balance(&wrapper.fee.token, &wrapper.fee_payer()); // In testnets, tx is allowed to skip fees if it // includes a valid PoW @@ -539,6 +639,7 @@ where mod test_process_proposal { use borsh::BorshDeserialize; use namada::ledger::parameters::storage::get_wrapper_tx_fees_key; + use namada::ledger::storage_api::StorageWrite; use namada::proto::SignedTxData; use namada::types::hash::Hash; use namada::types::key::*; @@ -553,6 +654,8 @@ mod test_process_proposal { self, gen_keypair, ProcessProposal, TestError, }; + const GAS_LIMIT_MULTIPLIER: u64 = 1; + /// Test that if a wrapper tx is not signed, the block is rejected /// by [`process_proposal`]. #[test] @@ -572,7 +675,7 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -625,7 +728,7 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -709,6 +812,16 @@ mod test_process_proposal { ) .unwrap(); let keypair = gen_keypair(); + // 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 + .write(&balance_key, Amount::whole(99)) + .unwrap(); + let tx = Tx::new( "wasm_code".as_bytes().to_owned(), Some("transaction data".as_bytes().to_owned()), @@ -722,7 +835,7 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -758,15 +871,14 @@ mod test_process_proposal { fn test_wrapper_insufficient_balance_address() { let (mut shell, _) = test_utils::setup(1); 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()), ); shell .wl_storage - .write_log - .write(&balance_key, Amount::whole(99).try_to_vec().unwrap()) + .write(&balance_key, Amount::whole(99)) .unwrap(); shell .wl_storage @@ -785,12 +897,12 @@ mod test_process_proposal { ); let wrapper = WrapperTx::new( Fee { - amount: Amount::whole(1_000_100), + amount: Amount::whole(100), token: shell.wl_storage.storage.native_token.clone(), }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -842,13 +954,19 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] None, ); - shell.enqueue_tx(wrapper); + let signed_wrapper = wrapper + .sign(&keypair, shell.chain_id.clone(), None) + .unwrap() + .to_bytes(); + let gas_limit = + u64::from(&wrapper.gas_limit) - signed_wrapper.len() as u64; + shell.enqueue_tx(wrapper, gas_limit); let mut decrypted_tx = Tx::from(TxType::Decrypted(DecryptedTx::Decrypted { tx, @@ -905,13 +1023,20 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] None, ); - shell.enqueue_tx(wrapper.clone()); + let signed_wrapper = wrapper + .sign(&keypair, shell.chain_id.clone(), None) + .unwrap() + .to_bytes(); + shell.enqueue_tx( + wrapper.clone(), + u64::from(&wrapper.gas_limit) - signed_wrapper.len() as u64, + ); let mut tx = Tx::from(TxType::Decrypted(DecryptedTx::Undecryptable(wrapper))); @@ -960,15 +1085,22 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] None, ); wrapper.tx_hash = Hash([0; 32]); + let signed_wrapper = wrapper + .sign(&keypair, shell.chain_id.clone(), None) + .unwrap() + .to_bytes(); - shell.enqueue_tx(wrapper.clone()); + shell.enqueue_tx( + wrapper.clone(), + u64::from(&wrapper.gas_limit) - signed_wrapper.len() as u64, + ); let mut tx = Tx::from(TxType::Decrypted(DecryptedTx::Undecryptable( #[allow(clippy::redundant_clone)] wrapper.clone(), @@ -1008,14 +1140,21 @@ mod test_process_proposal { }, pk: keypair.ref_to(), epoch: Epoch(0), - gas_limit: 0.into(), + gas_limit: GAS_LIMIT_MULTIPLIER.into(), inner_tx, tx_hash: hash_tx(&tx), #[cfg(not(feature = "mainnet"))] pow_solution: None, }; - shell.enqueue_tx(wrapper.clone()); + let signed_wrapper = wrapper + .sign(&keypair, shell.chain_id.clone(), None) + .unwrap() + .to_bytes(); + shell.enqueue_tx( + wrapper.clone(), + u64::from(&wrapper.gas_limit) - signed_wrapper.len() as u64, + ); let mut signed = Tx::from(TxType::Decrypted(DecryptedTx::Undecryptable( #[allow(clippy::redundant_clone)] @@ -1134,7 +1273,7 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1208,7 +1347,7 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1266,7 +1405,7 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1352,7 +1491,7 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1370,7 +1509,7 @@ mod test_process_proposal { }, &keypair_2, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1424,7 +1563,7 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx.clone(), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1493,14 +1632,21 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] None, ); + let signed_wrapper = wrapper + .sign(&keypair, shell.chain_id.clone(), None) + .unwrap() + .to_bytes(); + let gas_limit = + u64::from(&wrapper.gas_limit) - signed_wrapper.len() as u64; let wrapper_in_queue = WrapperTxInQueue { tx: wrapper, + gas: gas_limit, has_valid_pow: false, }; shell.wl_storage.storage.tx_queue.push(wrapper_in_queue); @@ -1548,7 +1694,7 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1599,14 +1745,21 @@ mod test_process_proposal { }, &keypair, Epoch(0), - 0.into(), + GAS_LIMIT_MULTIPLIER.into(), tx, Default::default(), #[cfg(not(feature = "mainnet"))] None, ); + let signed_wrapper_tx = wrapper + .sign(&keypair, shell.chain_id.clone(), None) + .unwrap() + .to_bytes(); + let gas_limit = + u64::from(&wrapper.gas_limit) - signed_wrapper_tx.len() as u64; let wrapper_in_queue = WrapperTxInQueue { tx: wrapper, + gas: gas_limit, has_valid_pow: false, }; shell.wl_storage.storage.tx_queue.push(wrapper_in_queue); @@ -1625,4 +1778,155 @@ mod test_process_proposal { 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, _) = test_utils::setup(1); + 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, _) = test_utils::setup(1); + + let block_gas_limit: u64 = shell + .wl_storage + .read(¶meters::storage::get_max_block_gas_key()) + .expect("Error while reading from storage") + .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, _) = test_utils::setup(1); + 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/apps/src/lib/wallet/mod.rs b/apps/src/lib/wallet/mod.rs index d823334a00..9e73bf1e43 100644 --- a/apps/src/lib/wallet/mod.rs +++ b/apps/src/lib/wallet/mod.rs @@ -258,7 +258,7 @@ impl Wallet { } pub fn find_viewing_key( - &mut self, + &self, alias: impl AsRef, ) -> Result<&ExtendedViewingKey, FindKeyError> { self.store diff --git a/apps/src/lib/wasm_loader/mod.rs b/apps/src/lib/wasm_loader/mod.rs index 9a075fbcf8..743e9d7b4e 100644 --- a/apps/src/lib/wasm_loader/mod.rs +++ b/apps/src/lib/wasm_loader/mod.rs @@ -31,7 +31,7 @@ pub enum Error { /// including SHA256 hash #[derive(Debug, Serialize, Deserialize)] #[serde(transparent)] -pub struct Checksums(pub HashMap); +pub struct Checksums(pub HashMap>); const S3_URL: &str = "https://namada-wasm-master.s3.eu-west-1.amazonaws.com"; @@ -132,8 +132,10 @@ pub async fn pre_fetch_wasm(wasm_directory: impl AsRef) { // load json with wasm hashes let checksums = Checksums::read_checksums_async(&wasm_directory).await; - join_all(checksums.0.into_iter().map(|(name, full_name)| { + join_all(checksums.0.into_iter().map(|(name, map)| { let wasm_directory = wasm_directory.as_ref().to_owned(); + let full_name = + name.replace('.', &format!(".{}.", map.get("hash").unwrap())); // Async check and download (if needed) each file tokio::spawn(async move { @@ -267,9 +269,15 @@ pub fn read_wasm( if let Some(os_name) = file_path.as_ref().file_name() { if let Some(name) = os_name.to_str() { let wasm_path = match checksums.0.get(name) { - Some(wasm_filename) => { - wasm_directory.as_ref().join(wasm_filename) - } + Some(map) => wasm_directory.as_ref().join(name.replace( + '.', + &format!( + ".{}.", + map.get("hash").ok_or_else(|| eyre!( + "Missing hash field in checksum" + ))? + ), + )), None => { if !file_path.as_ref().is_absolute() { wasm_directory.as_ref().join(file_path.as_ref()) diff --git a/benches/Cargo.toml b/benches/Cargo.toml new file mode 100644 index 0000000000..5acf2042ad --- /dev/null +++ b/benches/Cargo.toml @@ -0,0 +1,58 @@ +[package] +authors = ["Heliax AG "] +description = "Namada benchmarks" +edition = "2021" +license = "GPL-3.0" +name = "namada_benchmarks" +resolver = "2" +version = "0.15.0" + +[lib] +name = "namada_benches" +path = "mod.rs" + +[[bench]] +name = "whitelisted_txs" +harness = false +path = "txs.rs" + +[[bench]] +name = "whitelisted_vps" +harness = false +path = "vps.rs" + +[[bench]] +name = "native_vps" +harness = false +path = "native_vps.rs" + +[[bench]] +name = "process_wrapper" +harness = false +path = "process_wrapper.rs" + +[[bench]] +name = "host_env" +harness = false +path = "host_env.rs" + +[dependencies] +async-trait = "0.1.51" +borsh = "0.9.0" +ferveo-common = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} +ibc-relayer = { version = "0.22.0", default-features = false } +ibc-relayer-types = { version = "0.22.0", default-features = false } +ibc-proto = { version = "0.26.0", default-features = false } +masp_primitives = { git = "https://github.com/anoma/masp", rev = "bee40fc465f6afbd10558d12fe96eb1742eee45c" } +namada = { path = "../shared" } +namada_apps = { path = "../apps" } +namada_test_utils = { path = "../test_utils" } +prost = "0.11.6" +rand = "0.8" +rand_core = "0.6" +rust_decimal = "1.26.1" +tokio = "1.8.2" +tempfile = "3.2.0" + +[dev-dependencies] +criterion = { version = "0.4", features = ["html_reports"] } diff --git a/benches/host_env.rs b/benches/host_env.rs new file mode 100644 index 0000000000..f936dd8145 --- /dev/null +++ b/benches/host_env.rs @@ -0,0 +1,37 @@ +use borsh::BorshDeserialize; +use criterion::{criterion_group, criterion_main, Criterion}; +use namada::core::proto::SignedTxData; +use namada::core::types::address; +use namada::core::types::key::RefTo; +use namada::core::types::token::{Amount, Transfer}; +use namada_apps::wallet::defaults; +use namada_benches::{generate_tx, TX_TRANSFER_WASM}; + +fn tx_signature_validation(c: &mut Criterion) { + let tx = generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: defaults::albert_address(), + target: defaults::bertha_address(), + token: address::nam(), + sub_prefix: None, + amount: Amount::whole(500), + key: None, + shielded: None, + }, + &defaults::albert_keypair(), + ); + + let SignedTxData { data: _, ref sig } = + SignedTxData::try_from_slice(tx.data.as_ref().unwrap()).unwrap(); + + c.bench_function("tx_signature_validation", |b| { + b.iter(|| { + tx.verify_sig(&defaults::albert_keypair().ref_to(), sig) + .unwrap() + }) + }); +} + +criterion_group!(host_env, tx_signature_validation); +criterion_main!(host_env); diff --git a/benches/mod.rs b/benches/mod.rs new file mode 100644 index 0000000000..6c43b403d4 --- /dev/null +++ b/benches/mod.rs @@ -0,0 +1,624 @@ +//! Benchmarks module based on criterion. +//! +//! Measurements are taken on the elapsed wall-time. +//! +//! The benchmarks only focus on sucessfull transactions and vps: in case of +//! failure, the bench function shall panic to avoid timing incomplete execution +//! paths. +//! +//! In addition, this module also contains benchmarks for +//! [`WrapperTx`][`namada::core::types::transaction::wrapper::WrapperTx`] +//! validation and [`host_env`][`namada::vm::host_env`] exposed functions that +//! define the gas constants of [`gas`][`namada::core::ledger::gas`]. +//! +//! For more realistic results these benchmarks should be run on all the +//! combination of supported OS/architecture. + +use std::collections::BTreeMap; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; + +use borsh::BorshSerialize; +use ibc_proto::google::protobuf::Any; +use masp_primitives::zip32::ExtendedFullViewingKey; +use namada::core::ledger::ibc::storage::port_key; +use namada::core::types::address::{self, Address}; +use namada::core::types::key::common::SecretKey; +use namada::core::types::storage::Key; +use namada::core::types::token::{Amount, Transfer}; +use namada::ibc::applications::transfer::msgs::transfer::MsgTransfer; +use namada::ibc::clients::ics07_tendermint::client_state::{ + AllowUpdate, ClientState, +}; +use namada::ibc::clients::ics07_tendermint::consensus_state::ConsensusState; +use namada::ibc::core::ics02_client::client_type::ClientType; +use namada::ibc::core::ics02_client::trust_threshold::TrustThreshold; +use namada::ibc::core::ics03_connection::connection::{ + ConnectionEnd, Counterparty, State as ConnectionState, +}; +use namada::ibc::core::ics03_connection::version::Version; +use namada::ibc::core::ics04_channel::channel::{ + ChannelEnd, Counterparty as ChannelCounterparty, Order, State, +}; +use namada::ibc::core::ics04_channel::timeout::TimeoutHeight; +use namada::ibc::core::ics04_channel::Version as ChannelVersion; +use namada::ibc::core::ics23_commitment::commitment::{ + CommitmentPrefix, CommitmentRoot, +}; +use namada::ibc::core::ics23_commitment::specs::ProofSpecs; +use namada::ibc::core::ics24_host::identifier::{ + ChainId as IbcChainId, ChannelId, ClientId, ConnectionId, PortChannelId, + PortId, +}; +use namada::ibc::core::ics24_host::Path as IbcPath; +use namada::ibc::signer::Signer; +use namada::ibc::timestamp::Timestamp as IbcTimestamp; +use namada::ibc::tx_msg::Msg; +use namada::ibc::Height as IbcHeight; +use namada::ibc_proto::cosmos::base::v1beta1::Coin; +use namada::ibc_proto::protobuf::Protobuf; +use namada::ledger::gas::TxGasMeter; +use namada::ledger::ibc::storage::{channel_key, connection_key}; +use namada::ledger::queries::{ + Client, EncodedResponseQuery, RequestCtx, RequestQuery, Router, RPC, +}; +use namada::proof_of_stake; +use namada::proto::Tx; +use namada::tendermint::Hash; +use namada::types::address::InternalAddress; +use namada::types::chain::ChainId; +use namada::types::masp::{ + ExtendedViewingKey, PaymentAddress, TransferSource, TransferTarget, +}; +use namada::types::storage::{BlockHeight, KeySeg, TxIndex}; +use namada::types::time::DateTimeUtc; +use namada::types::transaction::governance::{InitProposalData, ProposalType}; +use namada::types::transaction::pos::Bond; +use namada::types::transaction::GasLimit; +use namada::vm::wasm::run; +use namada_apps::cli::args::{Tx as TxArgs, TxTransfer}; +use namada_apps::cli::context::FromContext; +use namada_apps::cli::Context; +use namada_apps::client::tx; +use namada_apps::config::TendermintMode; +use namada_apps::facade::tendermint_config::net::Address as TendermintAddress; +use namada_apps::facade::tendermint_proto::abci::RequestInitChain; +use namada_apps::facade::tendermint_proto::google::protobuf::Timestamp; +use namada_apps::node::ledger::shell::Shell; +use namada_apps::wallet::defaults; +use namada_apps::{config, wasm_loader}; +use namada_test_utils::tx_data::TxWriteData; +use rand_core::OsRng; +use tempfile::TempDir; + +pub const WASM_DIR: &str = "../wasm"; +pub const TX_BOND_WASM: &str = "tx_bond.wasm"; +pub const TX_TRANSFER_WASM: &str = "tx_transfer.wasm"; +pub const TX_UPDATE_VP_WASM: &str = "tx_update_vp.wasm"; +pub const TX_VOTE_PROPOSAL_WASM: &str = "tx_vote_proposal.wasm"; +pub const TX_UNBOND_WASM: &str = "tx_unbond.wasm"; +pub const TX_INIT_PROPOSAL_WASM: &str = "tx_init_proposal.wasm"; +pub const TX_REVEAL_PK_WASM: &str = "tx_reveal_pk.wasm"; +pub const TX_CHANGE_VALIDATOR_COMMISSION_WASM: &str = + "tx_change_validator_commission.wasm"; +pub const TX_IBC_WASM: &str = "tx_ibc.wasm"; +pub const VP_VALIDATOR_WASM: &str = "vp_validator.wasm"; + +pub const ALBERT_PAYMENT_ADDRESS: &str = "albert_payment"; +pub const ALBERT_SPENDING_KEY: &str = "albert_spending"; +pub const BERTHA_PAYMENT_ADDRESS: &str = "bertha_payment"; +const BERTHA_SPENDING_KEY: &str = "bertha_spending"; + +pub struct BenchShell { + pub inner: Shell, + /// NOTE: Temporary directory should be dropped last since Shell need to + /// flush data on drop + tempdir: TempDir, +} + +impl Deref for BenchShell { + type Target = Shell; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for BenchShell { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl Default for BenchShell { + fn default() -> Self { + let (sender, _) = tokio::sync::mpsc::unbounded_channel(); + let tempdir = tempfile::tempdir().unwrap(); + let path = tempdir.path().canonicalize().unwrap(); + + let mut shell = Shell::new( + config::Ledger::new( + path, + Default::default(), + TendermintMode::Validator, + ), + WASM_DIR.into(), + sender, + None, + 50 * 1024 * 1024, // 50 kiB + 50 * 1024 * 1024, // 50 kiB + address::nam(), + ); + + shell + .init_chain( + RequestInitChain { + time: Some(Timestamp { + seconds: 0, + nanos: 0, + }), + chain_id: ChainId::default().to_string(), + ..Default::default() + }, + 1, + ) + .unwrap(); + + // Bond from Albert to validator + let bond = Bond { + validator: defaults::validator_address(), + amount: Amount::whole(1000), + source: Some(defaults::albert_address()), + }; + let signed_tx = + generate_tx(TX_BOND_WASM, bond, &defaults::albert_keypair()); + + let mut bench_shell = BenchShell { + inner: shell, + tempdir, + }; + + bench_shell.execute_tx(&signed_tx); + bench_shell.wl_storage.commit_tx(); + + // Initialize governance proposal + let signed_tx = generate_tx( + TX_INIT_PROPOSAL_WASM, + InitProposalData { + id: None, + content: vec![], + author: defaults::albert_address(), + r#type: ProposalType::Default(None), + voting_start_epoch: 12.into(), + voting_end_epoch: 15.into(), + grace_epoch: 18.into(), + }, + &defaults::albert_keypair(), + ); + + bench_shell.execute_tx(&signed_tx); + bench_shell.wl_storage.commit_tx(); + bench_shell.commit(); + + // Advance epoch for pos benches + for _ in 0..=12 { + bench_shell.advance_epoch(); + } + + bench_shell + } +} + +impl BenchShell { + pub fn execute_tx(&mut self, tx: &Tx) { + run::tx( + &self.inner.wl_storage.storage, + &mut self.inner.wl_storage.write_log, + &mut TxGasMeter::new(u64::MAX), + &BTreeMap::default(), + &TxIndex(0), + &tx.code_or_hash, + tx.data.as_ref().unwrap(), + &mut self.inner.vp_wasm_cache, + &mut self.inner.tx_wasm_cache, + ) + .unwrap(); + } + + pub fn advance_epoch(&mut self) { + let pipeline_len = + proof_of_stake::read_pos_params(&self.inner.wl_storage) + .unwrap() + .pipeline_len; + + self.wl_storage.storage.block.epoch = + self.wl_storage.storage.block.epoch.next(); + let current_epoch = self.wl_storage.storage.block.epoch; + + proof_of_stake::copy_validator_sets_and_positions( + &mut self.wl_storage, + current_epoch, + current_epoch + pipeline_len, + &proof_of_stake::consensus_validator_set_handle(), + &proof_of_stake::below_capacity_validator_set_handle(), + ) + .unwrap(); + } + + pub fn init_ibc_channel(&mut self) { + // Set connection open + let client_id = + ClientId::new(ClientType::new("01-tendermint".to_string()), 1) + .unwrap(); + let connection = ConnectionEnd::new( + ConnectionState::Open, + client_id.clone(), + Counterparty::new( + client_id, + Some(ConnectionId::new(1)), + CommitmentPrefix::try_from(b"ibc".to_vec()).unwrap(), + ), + vec![Version::default()], + std::time::Duration::new(100, 0), + ); + + let addr_key = + Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()); + + let connection_key = connection_key(&ConnectionId::new(1)); + self.wl_storage + .storage + .write(&connection_key, connection.encode_vec().unwrap()) + .unwrap(); + + // Set port + let port_key = port_key(&PortId::transfer()); + + let index_key = addr_key + .join(&Key::from("capabilities/index".to_string().to_db_key())); + self.wl_storage + .storage + .write(&index_key, 1u64.to_be_bytes()) + .unwrap(); + self.wl_storage + .storage + .write(&port_key, 1u64.to_be_bytes()) + .unwrap(); + let cap_key = + addr_key.join(&Key::from("capabilities/1".to_string().to_db_key())); + self.wl_storage + .storage + .write(&cap_key, PortId::transfer().as_bytes()) + .unwrap(); + + // Set Channel open + let counterparty = ChannelCounterparty::new( + PortId::transfer(), + Some(ChannelId::new(5)), + ); + let channel = ChannelEnd::new( + State::Open, + Order::Unordered, + counterparty, + vec![ConnectionId::new(1)], + ChannelVersion::new("ics20-1".to_string()), + ); + let channel_key = channel_key(&PortChannelId::new( + ChannelId::new(5), + PortId::transfer(), + )); + self.wl_storage + .storage + .write(&channel_key, channel.encode_vec().unwrap()) + .unwrap(); + + // Set client state + let client_id = + ClientId::new(ClientType::new("01-tendermint".to_string()), 1) + .unwrap(); + let client_state_key = addr_key.join(&Key::from( + IbcPath::ClientState( + namada::ibc::core::ics24_host::path::ClientStatePath( + client_id.clone(), + ), + ) + .to_string() + .to_db_key(), + )); + let client_state = ClientState::new( + IbcChainId::from(ChainId::default().to_string()), + TrustThreshold::ONE_THIRD, + std::time::Duration::new(1, 0), + std::time::Duration::new(2, 0), + std::time::Duration::new(1, 0), + IbcHeight::new(0, 1).unwrap(), + ProofSpecs::cosmos(), + vec![], + AllowUpdate { + after_expiry: true, + after_misbehaviour: true, + }, + None, + ) + .unwrap(); + self.wl_storage + .storage + .write( + &client_state_key, + >::encode_vec(&client_state) + .unwrap(), + ) + .expect("write failed"); + + // Set consensus state + let now: namada::tendermint::Time = + DateTimeUtc::now().try_into().unwrap(); + let consensus_key = addr_key.join(&Key::from( + IbcPath::ClientConsensusState( + namada::ibc::core::ics24_host::path::ClientConsensusStatePath { + client_id, + epoch: 0, + height: 1, + }, + ) + .to_string() + .to_db_key(), + )); + + let consensus_state = ConsensusState { + timestamp: now, + root: CommitmentRoot::from_bytes(&[]), + next_validators_hash: Hash::Sha256([0u8; 32]), + }; + + self.wl_storage + .storage + .write( + &consensus_key, + >::encode_vec(&consensus_state) + .unwrap(), + ) + .unwrap(); + } +} + +pub fn generate_tx( + wasm_code_path: &str, + data: impl BorshSerialize, + signer: &SecretKey, +) -> Tx { + let tx = Tx::new( + wasm_loader::read_wasm_or_exit(WASM_DIR, wasm_code_path), + Some(data.try_to_vec().unwrap()), + ChainId::default(), + None, + ); + + tx.sign(signer) +} + +pub fn generate_foreign_key_tx(signer: &SecretKey) -> Tx { + let wasm_code = std::fs::read("../wasm_for_tests/tx_write.wasm").unwrap(); + + let tx = Tx::new( + wasm_code, + Some( + TxWriteData { + key: Key::from("bench_foreing_key".to_string().to_db_key()), + value: vec![0; 64], + } + .try_to_vec() + .unwrap(), + ), + ChainId::default(), + None, + ); + + tx.sign(signer) +} + +pub fn generate_ibc_transfer_tx() -> Tx { + let token = Coin { + denom: address::nam().to_string(), + amount: Amount::whole(1000).to_string(), + }; + + let timeout_height = TimeoutHeight::At(IbcHeight::new(0, 100).unwrap()); + + let now: namada::tendermint::Time = DateTimeUtc::now().try_into().unwrap(); + let now: IbcTimestamp = now.into(); + let timeout_timestamp = (now + std::time::Duration::new(3600, 0)).unwrap(); + + let msg = MsgTransfer { + port_id_on_a: PortId::transfer(), + chan_id_on_a: ChannelId::new(5), + token, + sender: Signer::from_str(&defaults::albert_address().to_string()) + .unwrap(), + receiver: Signer::from_str(&defaults::bertha_address().to_string()) + .unwrap(), + timeout_height_on_b: timeout_height, + timeout_timestamp_on_b: timeout_timestamp, + }; + let any_msg = msg.to_any(); + let mut data = vec![]; + prost::Message::encode(&any_msg, &mut data).unwrap(); + + // Don't use execute_tx to avoid serializing the data again with borsh + Tx::new( + wasm_loader::read_wasm_or_exit(WASM_DIR, TX_IBC_WASM), + Some(data), + ChainId::default(), + None, + ) + .sign(&defaults::albert_keypair()) +} + +pub struct BenchShieldedCtx { + pub ctx: Context, + pub shell: BenchShell, +} + +#[async_trait::async_trait(?Send)] +impl Client for BenchShell { + type Error = std::io::Error; + + async fn request( + &self, + path: String, + data: Option>, + height: Option, + prove: bool, + ) -> Result { + let data = data.unwrap_or_default(); + let height = height.unwrap_or_default(); + + let request = RequestQuery { + data, + path, + height, + prove, + }; + + let ctx = RequestCtx { + wl_storage: &self.wl_storage, + event_log: self.event_log(), + vp_wasm_cache: self.vp_wasm_cache.read_only(), + tx_wasm_cache: self.tx_wasm_cache.read_only(), + storage_read_past_height_limit: None, + }; + + RPC.handle(ctx, &request) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::NotFound)) + } +} + +impl Default for BenchShieldedCtx { + fn default() -> Self { + let mut shell = BenchShell::default(); + + let mut ctx = Context::new(namada_apps::cli::args::Global { + chain_id: None, + base_dir: shell.tempdir.as_ref().canonicalize().unwrap(), + wasm_dir: None, + mode: None, + }) + .unwrap(); + + // Generate spending key for Albert and Bertha + ctx.wallet + .gen_spending_key(ALBERT_SPENDING_KEY.to_string(), true); + ctx.wallet + .gen_spending_key(BERTHA_SPENDING_KEY.to_string(), true); + ctx.wallet.save().unwrap(); + + // Generate payment addresses for both Albert and Bertha + for (alias, viewing_alias) in [ + (ALBERT_PAYMENT_ADDRESS, ALBERT_SPENDING_KEY), + (BERTHA_PAYMENT_ADDRESS, BERTHA_SPENDING_KEY), + ] + .map(|(p, s)| (p.to_owned(), s.to_owned())) + { + let viewing_key: FromContext = FromContext::new( + ctx.wallet + .find_viewing_key(viewing_alias) + .unwrap() + .to_string(), + ); + let viewing_key = + ExtendedFullViewingKey::from(ctx.get_cached(&viewing_key)) + .fvk + .vk; + let (div, _g_d) = tx::find_valid_diversifier(&mut OsRng); + let payment_addr = viewing_key.to_payment_address(div).unwrap(); + let _ = ctx + .wallet + .insert_payment_addr( + alias, + PaymentAddress::from(payment_addr).pinned(false), + ) + .unwrap(); + } + + ctx.wallet.save().unwrap(); + namada::ledger::storage::update_allowed_conversions( + &mut shell.wl_storage, + ) + .unwrap(); + + Self { ctx, shell } + } +} + +impl BenchShieldedCtx { + pub fn generate_masp_tx( + &mut self, + amount: Amount, + source: TransferSource, + target: TransferTarget, + ) -> Tx { + let mock_args = TxArgs { + dry_run: false, + dump_tx: false, + force: false, + broadcast_only: false, + ledger_address: TendermintAddress::Tcp { + peer_id: None, + host: "bench-host".to_string(), + port: 1, + }, + initialized_account_alias: None, + fee_amount: Amount::whole(0), + fee_token: FromContext::new(address::nam().to_string()), + gas_limit: GasLimit::from(u64::MAX), + expiration: None, + signing_key: Some(FromContext::new( + defaults::albert_keypair().to_string(), + )), + signer: None, + }; + + let args = TxTransfer { + tx: mock_args, + source: FromContext::new(source.to_string()), + target: FromContext::new(target.to_string()), + token: FromContext::new(address::nam().to_string()), + sub_prefix: None, + amount, + }; + + let async_runtime = tokio::runtime::Runtime::new().unwrap(); + let spending_key = self + .ctx + .wallet + .find_spending_key(ALBERT_SPENDING_KEY) + .unwrap(); + async_runtime.block_on(self.ctx.shielded.fetch( + &self.shell, + &[spending_key.into()], + &[], + )); + let shielded = async_runtime + .block_on(tx::gen_shielded_transfer( + &mut self.ctx, + &self.shell, + &args, + )) + .unwrap() + .map(|x| x.0); + + generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: source.effective_address(), + target: target.effective_address(), + token: address::nam(), + sub_prefix: None, + amount, + key: None, + shielded, + }, + &defaults::albert_keypair(), + ) + } +} diff --git a/benches/native_vps.rs b/benches/native_vps.rs new file mode 100644 index 0000000000..dea8585d13 --- /dev/null +++ b/benches/native_vps.rs @@ -0,0 +1,450 @@ +use std::collections::BTreeSet; +use std::str::FromStr; + +use criterion::{criterion_group, criterion_main, Criterion}; +use namada::core::types::address::{self, Address}; +use namada::ibc::core::ics02_client::client_type::ClientType; +use namada::ibc::core::ics03_connection::connection::Counterparty; +use namada::ibc::core::ics03_connection::msgs::conn_open_init::MsgConnectionOpenInit; +use namada::ibc::core::ics03_connection::version::Version; +use namada::ibc::core::ics04_channel::channel::Order; +use namada::ibc::core::ics04_channel::msgs::chan_open_init::MsgChannelOpenInit; +use namada::ibc::core::ics04_channel::Version as ChannelVersion; +use namada::ibc::core::ics23_commitment::commitment::CommitmentPrefix; +use namada::ibc::core::ics24_host::identifier::{ + ChannelId, ClientId, ConnectionId, PortId, +}; +use namada::ibc::signer::Signer; +use namada::ibc::tx_msg::Msg; +use namada::ledger::gas::VpGasMeter; +use namada::ledger::governance; +use namada::ledger::ibc::vp::{Ibc, IbcToken}; +use namada::ledger::native_vp::replay_protection::ReplayProtectionVp; +use namada::ledger::native_vp::slash_fund::SlashFundVp; +use namada::ledger::native_vp::{Ctx, NativeVp}; +use namada::ledger::storage_api::StorageRead; +use namada::proto::Tx; +use namada::types::address::InternalAddress; +use namada::types::chain::ChainId; +use namada::types::governance::{ProposalVote, VoteType}; +use namada::types::storage::TxIndex; +use namada::types::transaction::governance::{ + InitProposalData, ProposalType, VoteProposalData, +}; +use namada_apps::wallet::defaults; +use namada_apps::wasm_loader; +use namada_benches::{ + generate_foreign_key_tx, generate_ibc_transfer_tx, generate_tx, BenchShell, + TX_IBC_WASM, TX_INIT_PROPOSAL_WASM, TX_VOTE_PROPOSAL_WASM, WASM_DIR, +}; + +fn replay_protection(c: &mut Criterion) { + // Write a random key under the replay protection subspace + let tx = generate_foreign_key_tx(&defaults::albert_keypair()); + let mut shell = BenchShell::default(); + + shell.execute_tx(&tx); + let (verifiers, keys_changed) = shell + .wl_storage + .write_log + .verifiers_and_changed_keys(&BTreeSet::default()); + + let replay_protection = ReplayProtectionVp { + ctx: Ctx::new( + &Address::Internal(InternalAddress::ReplayProtection), + &shell.wl_storage.storage, + &shell.wl_storage.write_log, + &tx, + &TxIndex(0), + VpGasMeter::new(u64::MAX, 0), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + ), + }; + + c.bench_function("vp_replay_protection", |b| { + b.iter(|| { + // NOTE: thiv VP will always fail when triggered so don't assert + // here + replay_protection + .validate_tx( + tx.data.as_ref().unwrap(), + replay_protection.ctx.keys_changed, + replay_protection.ctx.verifiers, + ) + .unwrap() + }) + }); +} + +fn governance(c: &mut Criterion) { + let mut group = c.benchmark_group("vp_governance"); + + for bench_name in [ + "foreign_key_write", + "delegator_vote", + "validator_vote", + "minimal_proposal", + "complete_proposal", + ] { + let mut shell = BenchShell::default(); + + let signed_tx = match bench_name { + "foreign_key_write" => { + generate_foreign_key_tx(&defaults::albert_keypair()) + } + "delegator_vote" => generate_tx( + TX_VOTE_PROPOSAL_WASM, + VoteProposalData { + id: 0, + vote: ProposalVote::Yay(VoteType::Default), + voter: defaults::albert_address(), + delegations: vec![defaults::validator_address()], + }, + &defaults::albert_keypair(), + ), + "validator_vote" => generate_tx( + TX_VOTE_PROPOSAL_WASM, + VoteProposalData { + id: 0, + vote: namada::types::governance::ProposalVote::Nay, + voter: defaults::validator_address(), + delegations: vec![], + }, + &defaults::validator_keypair(), + ), + "minimal_proposal" => generate_tx( + TX_INIT_PROPOSAL_WASM, + InitProposalData { + id: None, + content: vec![], + author: defaults::albert_address(), + r#type: ProposalType::Default(None), + voting_start_epoch: 12.into(), + voting_end_epoch: 15.into(), + grace_epoch: 18.into(), + }, + &defaults::albert_keypair(), + ), + "complete_proposal" => { + let max_code_size_key = + governance::storage::get_max_proposal_code_size_key(); + let max_proposal_content_key = + governance::storage::get_max_proposal_content_key(); + let max_code_size = shell + .wl_storage + .read(&max_code_size_key) + .expect("Error while reading from storage") + .expect("Missing max_code_size parameter in storage"); + let max_proposal_content_size = shell + .wl_storage + .read(&max_proposal_content_key) + .expect("Error while reading from storage") + .expect( + "Missing max_proposal_content parameter in storage", + ); + + generate_tx( + TX_INIT_PROPOSAL_WASM, + InitProposalData { + id: Some(1), + content: vec![0; max_proposal_content_size], + author: defaults::albert_address(), + r#type: ProposalType::Default(Some(vec![ + 0; + max_code_size + ])), + voting_start_epoch: 12.into(), + voting_end_epoch: 15.into(), + grace_epoch: 18.into(), + }, + &defaults::albert_keypair(), + ) + } + _ => panic!("Unexpected bench test"), + }; + + // Run the tx to validate + shell.execute_tx(&signed_tx); + + let (verifiers, keys_changed) = shell + .wl_storage + .write_log + .verifiers_and_changed_keys(&BTreeSet::default()); + + let governance = SlashFundVp { + ctx: Ctx::new( + &Address::Internal(InternalAddress::Governance), + &shell.wl_storage.storage, + &shell.wl_storage.write_log, + &signed_tx, + &TxIndex(0), + VpGasMeter::new(u64::MAX, 0), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + ), + }; + + group.bench_function(bench_name, |b| { + b.iter(|| { + assert!( + governance + .validate_tx( + signed_tx.data.as_ref().unwrap(), + governance.ctx.keys_changed, + governance.ctx.verifiers, + ) + .unwrap() + ) + }) + }); + } + + group.finish(); +} + +fn slash_fund(c: &mut Criterion) { + let mut group = c.benchmark_group("vp_slash_fund"); + + // Write a random key under a foreign subspace + let foreign_key_write = + generate_foreign_key_tx(&defaults::albert_keypair()); + + let governance_proposal = generate_tx( + TX_INIT_PROPOSAL_WASM, + InitProposalData { + id: None, + content: vec![], + author: defaults::albert_address(), + r#type: ProposalType::Default(None), + voting_start_epoch: 12.into(), + voting_end_epoch: 15.into(), + grace_epoch: 18.into(), + }, + &defaults::albert_keypair(), + ); + + for (tx, bench_name) in [foreign_key_write, governance_proposal] + .into_iter() + .zip(["foreign_key_write", "governance_proposal"]) + { + let mut shell = BenchShell::default(); + + // Run the tx to validate + shell.execute_tx(&tx); + + let (verifiers, keys_changed) = shell + .wl_storage + .write_log + .verifiers_and_changed_keys(&BTreeSet::default()); + + let slash_fund = SlashFundVp { + ctx: Ctx::new( + &Address::Internal(InternalAddress::SlashFund), + &shell.wl_storage.storage, + &shell.wl_storage.write_log, + &tx, + &TxIndex(0), + VpGasMeter::new(u64::MAX, 0), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + ), + }; + + group.bench_function(bench_name, |b| { + b.iter(|| { + assert!( + slash_fund + .validate_tx( + tx.data.as_ref().unwrap(), + slash_fund.ctx.keys_changed, + slash_fund.ctx.verifiers, + ) + .unwrap() + ) + }) + }); + } + + group.finish(); +} + +fn ibc(c: &mut Criterion) { + let mut group = c.benchmark_group("vp_ibc"); + + // Connection handshake + let msg = MsgConnectionOpenInit { + client_id_on_a: ClientId::new( + ClientType::new("01-tendermint".to_string()), + 1, + ) + .unwrap(), + counterparty: Counterparty::new( + ClientId::from_str("01-tendermint-1").unwrap(), + Some(ConnectionId::new(1)), + CommitmentPrefix::try_from(b"ibc".to_vec()).unwrap(), + ), + version: Some(Version::default()), + delay_period: std::time::Duration::new(100, 0), + signer: Signer::from_str(&defaults::albert_address().to_string()) + .unwrap(), + }; + let any_msg = msg.to_any(); + let mut data = vec![]; + prost::Message::encode(&any_msg, &mut data) + .expect("Encoding tx data shouldn't fail"); + + // Avoid serializing the data again with borsh + let open_connection = Tx::new( + wasm_loader::read_wasm_or_exit(WASM_DIR, TX_IBC_WASM), + Some(data), + ChainId::default(), + None, + ) + .sign(&defaults::albert_keypair()); + + // Channel handshake + let msg = MsgChannelOpenInit { + port_id_on_a: PortId::transfer(), + connection_hops_on_a: vec![ConnectionId::new(1)], + port_id_on_b: PortId::transfer(), + ordering: Order::Unordered, + signer: Signer::from_str(&defaults::albert_address().to_string()) + .unwrap(), + version_proposal: ChannelVersion::new("ics20-1".to_string()), + }; + + let any_msg = msg.to_any(); + let mut data = vec![]; + prost::Message::encode(&any_msg, &mut data) + .expect("Encoding tx data shouldn't fail"); + + // Avoid serializing the data again with borsh + let open_channel = Tx::new( + wasm_loader::read_wasm_or_exit(WASM_DIR, TX_IBC_WASM), + Some(data), + ChainId::default(), + None, + ) + .sign(&defaults::albert_keypair()); + + // Ibc transfer + let outgoing_transfer = generate_ibc_transfer_tx(); + + for (signed_tx, bench_name) in + [open_connection, open_channel, outgoing_transfer] + .iter() + .zip(["open_connection", "open_channel", "outgoing_transfer"]) + { + let mut shell = BenchShell::default(); + shell.init_ibc_channel(); + + shell.execute_tx(signed_tx); + let (verifiers, keys_changed) = shell + .wl_storage + .write_log + .verifiers_and_changed_keys(&BTreeSet::default()); + + let ibc = Ibc { + ctx: Ctx::new( + &Address::Internal(InternalAddress::Ibc), + &shell.wl_storage.storage, + &shell.wl_storage.write_log, + signed_tx, + &TxIndex(0), + VpGasMeter::new(u64::MAX, 0), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + ), + }; + + group.bench_function(bench_name, |b| { + b.iter(|| { + assert!( + ibc.validate_tx( + signed_tx.data.as_ref().unwrap(), + ibc.ctx.keys_changed, + ibc.ctx.verifiers, + ) + .unwrap() + ) + }) + }); + } + + group.finish(); +} + +fn ibc_token(c: &mut Criterion) { + let mut group = c.benchmark_group("vp_ibc_token"); + + let foreign_key_write = + generate_foreign_key_tx(&defaults::albert_keypair()); + let outgoing_transfer = generate_ibc_transfer_tx(); + + for (signed_tx, bench_name) in [foreign_key_write, outgoing_transfer] + .iter() + .zip(["foreign_key_write", "outgoing_transfer"]) + { + let mut shell = BenchShell::default(); + shell.init_ibc_channel(); + + shell.execute_tx(signed_tx); + + let (verifiers, keys_changed) = shell + .wl_storage + .write_log + .verifiers_and_changed_keys(&BTreeSet::default()); + + let ibc_token_address = + namada::core::types::address::InternalAddress::ibc_token_address( + PortId::transfer().to_string(), + ChannelId::new(5).to_string(), + &address::nam(), + ); + let internal_address = Address::Internal(ibc_token_address); + + let ibc = IbcToken { + ctx: Ctx::new( + &internal_address, + &shell.wl_storage.storage, + &shell.wl_storage.write_log, + signed_tx, + &TxIndex(0), + VpGasMeter::new(u64::MAX, 0), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + ), + }; + + group.bench_function(bench_name, |b| { + b.iter(|| { + assert!( + ibc.validate_tx( + signed_tx.data.as_ref().unwrap(), + ibc.ctx.keys_changed, + ibc.ctx.verifiers, + ) + .unwrap() + ) + }) + }); + } + + group.finish(); +} + +criterion_group!( + native_vps, + replay_protection, + governance, + slash_fund, + ibc, + ibc_token +); +criterion_main!(native_vps); diff --git a/benches/process_wrapper.rs b/benches/process_wrapper.rs new file mode 100644 index 0000000000..ee64f992ec --- /dev/null +++ b/benches/process_wrapper.rs @@ -0,0 +1,94 @@ +use std::collections::BTreeMap; + +use criterion::{criterion_group, criterion_main, Criterion}; +use namada::core::types::address; +use namada::core::types::token::{Amount, Transfer}; +use namada::ledger::gas::BlockGasMeter; +use namada::ledger::storage::TempWlStorage; +use namada::types::chain::ChainId; +use namada::types::storage::BlockHeight; +use namada::types::time::DateTimeUtc; +use namada::types::transaction::{Fee, WrapperTx}; +use namada_apps::node::ledger::shell::process_proposal::ValidationMeta; +use namada_apps::wallet::defaults; +use namada_benches::{generate_tx, BenchShell, TX_TRANSFER_WASM}; + +fn process_tx(c: &mut Criterion) { + let mut shell = BenchShell::default(); + // Advance chain height to allow the inclusion of wrapper txs by the block + // space allocator + shell.wl_storage.storage.last_height = BlockHeight(2); + let tx = generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: defaults::albert_address(), + target: defaults::bertha_address(), + token: address::nam(), + sub_prefix: None, + amount: Amount::whole(1000), + key: None, + shielded: None, + }, + &defaults::albert_keypair(), + ); + + let wrapper = WrapperTx::new( + Fee { + token: address::nam(), + amount: Amount::whole(200), + }, + &defaults::albert_keypair(), + 0.into(), + 1000.into(), + tx, + Default::default(), + None, + ) + .sign(&defaults::albert_keypair(), ChainId::default(), None) + .unwrap() + .to_bytes(); + + let datetime = DateTimeUtc::now(); + let gas_table = BTreeMap::default(); + + c.bench_function("wrapper_tx_validation", |b| { + b.iter_batched( + || { + ( + shell.wl_storage.storage.tx_queue.clone(), + // Prevent block out of gas and replay protection + TempWlStorage::new(&shell.wl_storage.storage), + BlockGasMeter::new(u64::MAX), + ValidationMeta::from(&shell.wl_storage), + ) + }, + |( + tx_queue, + mut temp_wl_storage, + mut block_gas_meter, + mut validation_meta, + )| { + assert_eq!( + // Assert that the wrapper transaction was valid + shell + .process_single_tx( + &wrapper, + &mut tx_queue.iter(), + &mut validation_meta, + &mut temp_wl_storage, + &mut block_gas_meter, + datetime, + &gas_table, + &mut 0, + ) + .code, + 0 + ) + }, + criterion::BatchSize::LargeInput, + ) + }); +} + +criterion_group!(process_wrapper, process_tx); +criterion_main!(process_wrapper); diff --git a/benches/txs.rs b/benches/txs.rs new file mode 100644 index 0000000000..ae05465835 --- /dev/null +++ b/benches/txs.rs @@ -0,0 +1,563 @@ +use borsh::BorshSerialize; +use criterion::{criterion_group, criterion_main, Criterion}; +use namada::core::types::key::{ + common, SecretKey as SecretKeyInterface, SigScheme, +}; +use namada::core::types::token::Amount; +use namada::ledger::governance; +use namada::ledger::storage_api::StorageRead; +use namada::proof_of_stake; +use namada::proto::Tx; +use namada::types::chain::ChainId; +use namada::types::governance::{ProposalVote, VoteType}; +use namada::types::hash::Hash; +use namada::types::key::{ed25519, secp256k1}; +use namada::types::masp::{TransferSource, TransferTarget}; +use namada::types::storage::Key; +use namada::types::transaction::governance::{ + InitProposalData, ProposalType, VoteProposalData, +}; +use namada::types::transaction::pos::{Bond, CommissionChange, Withdraw}; +use namada::types::transaction::{ + EllipticCurve, InitAccount, InitValidator, UpdateVp, +}; +use namada_apps::wallet::defaults; +use namada_apps::wasm_loader; +use namada_benches::{ + generate_ibc_transfer_tx, generate_tx, BenchShell, BenchShieldedCtx, + ALBERT_PAYMENT_ADDRESS, ALBERT_SPENDING_KEY, BERTHA_PAYMENT_ADDRESS, + TX_BOND_WASM, TX_CHANGE_VALIDATOR_COMMISSION_WASM, TX_INIT_PROPOSAL_WASM, + TX_REVEAL_PK_WASM, TX_UNBOND_WASM, TX_UPDATE_VP_WASM, + TX_VOTE_PROPOSAL_WASM, VP_VALIDATOR_WASM, WASM_DIR, +}; +use rand::rngs::StdRng; +use rand::SeedableRng; +use rust_decimal::Decimal; + +const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm"; +const TX_INIT_ACCOUNT_WASM: &str = "tx_init_account.wasm"; +const TX_INIT_VALIDATOR_WASM: &str = "tx_init_validator.wasm"; + +fn transfer(c: &mut Criterion) { + let mut group = c.benchmark_group("transfer"); + let amount = Amount::whole(500); + + for bench_name in ["transparent", "shielding", "unshielding", "shielded"] { + group.bench_function(bench_name, |b| { + b.iter_batched_ref( + || { + let mut shielded_ctx = BenchShieldedCtx::default(); + + let albert_spending_key = shielded_ctx + .ctx + .wallet + .find_spending_key(ALBERT_SPENDING_KEY) + .unwrap() + .to_owned(); + let albert_payment_addr = shielded_ctx + .ctx + .wallet + .find_payment_addr(ALBERT_PAYMENT_ADDRESS) + .unwrap() + .to_owned(); + let bertha_payment_addr = shielded_ctx + .ctx + .wallet + .find_payment_addr(BERTHA_PAYMENT_ADDRESS) + .unwrap() + .to_owned(); + + // Shield some tokens for Albert + let shield_tx = shielded_ctx.generate_masp_tx( + amount, + TransferSource::Address(defaults::albert_address()), + TransferTarget::PaymentAddress(albert_payment_addr), + ); + shielded_ctx.shell.execute_tx(&shield_tx); + shielded_ctx.shell.wl_storage.commit_tx(); + shielded_ctx.shell.commit(); + + let signed_tx = match bench_name { + "transparent" => shielded_ctx.generate_masp_tx( + amount, + TransferSource::Address(defaults::albert_address()), + TransferTarget::Address(defaults::bertha_address()), + ), + "shielding" => shielded_ctx.generate_masp_tx( + amount, + TransferSource::Address(defaults::albert_address()), + TransferTarget::PaymentAddress(albert_payment_addr), + ), + "unshielding" => shielded_ctx.generate_masp_tx( + amount, + TransferSource::ExtendedSpendingKey( + albert_spending_key, + ), + TransferTarget::Address(defaults::albert_address()), + ), + "shielded" => shielded_ctx.generate_masp_tx( + amount, + TransferSource::ExtendedSpendingKey( + albert_spending_key, + ), + TransferTarget::PaymentAddress(bertha_payment_addr), + ), + _ => panic!("Unexpected bench test"), + }; + + (shielded_ctx, signed_tx) + }, + |(shielded_ctx, signed_tx)| { + shielded_ctx.shell.execute_tx(signed_tx); + }, + criterion::BatchSize::LargeInput, + ) + }); + } + + group.finish(); +} + +fn bond(c: &mut Criterion) { + let mut group = c.benchmark_group("bond"); + + let bond = generate_tx( + TX_BOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::whole(1000), + source: Some(defaults::albert_address()), + }, + &defaults::albert_keypair(), + ); + + let self_bond = generate_tx( + TX_BOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::whole(1000), + source: None, + }, + &defaults::validator_keypair(), + ); + + for (signed_tx, bench_name) in + [bond, self_bond].iter().zip(["bond", "self_bond"]) + { + group.bench_function(bench_name, |b| { + b.iter_batched_ref( + BenchShell::default, + |shell| shell.execute_tx(signed_tx), + criterion::BatchSize::LargeInput, + ) + }); + } + + group.finish(); +} + +fn unbond(c: &mut Criterion) { + let mut group = c.benchmark_group("unbond"); + + let unbond = generate_tx( + TX_UNBOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::whole(1000), + source: Some(defaults::albert_address()), + }, + &defaults::albert_keypair(), + ); + + let self_unbond = generate_tx( + TX_UNBOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::whole(1000), + source: None, + }, + &defaults::validator_keypair(), + ); + + for (signed_tx, bench_name) in + [unbond, self_unbond].iter().zip(["unbond", "self_unbond"]) + { + group.bench_function(bench_name, |b| { + b.iter_batched_ref( + BenchShell::default, + |shell| shell.execute_tx(signed_tx), + criterion::BatchSize::LargeInput, + ) + }); + } + + group.finish(); +} + +fn withdraw(c: &mut Criterion) { + let mut group = c.benchmark_group("withdraw"); + + let withdraw = generate_tx( + TX_WITHDRAW_WASM, + Withdraw { + validator: defaults::validator_address(), + source: Some(defaults::albert_address()), + }, + &defaults::albert_keypair(), + ); + + let self_withdraw = generate_tx( + TX_WITHDRAW_WASM, + Withdraw { + validator: defaults::validator_address(), + source: None, + }, + &defaults::validator_keypair(), + ); + + for (signed_tx, bench_name) in [withdraw, self_withdraw] + .iter() + .zip(["withdraw", "self_withdraw"]) + { + group.bench_function(bench_name, |b| { + b.iter_batched_ref( + || { + let mut shell = BenchShell::default(); + + // Unbond funds + let unbond_tx = match bench_name { + "withdraw" => generate_tx( + TX_UNBOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::whole(1000), + source: Some(defaults::albert_address()), + }, + &defaults::albert_keypair(), + ), + "self_withdraw" => generate_tx( + TX_UNBOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::whole(1000), + source: None, + }, + &defaults::validator_keypair(), + ), + _ => panic!("Unexpected bench test"), + }; + + shell.execute_tx(&unbond_tx); + shell.wl_storage.commit_tx(); + + // Advance Epoch for pipeline and unbonding length + let params = + proof_of_stake::read_pos_params(&shell.wl_storage) + .unwrap(); + let advance_epochs = + params.pipeline_len + params.unbonding_len; + + for _ in 0..=advance_epochs { + shell.advance_epoch(); + } + + shell + }, + |shell| shell.execute_tx(signed_tx), + criterion::BatchSize::LargeInput, + ) + }); + } + + group.finish(); +} + +fn reveal_pk(c: &mut Criterion) { + let mut csprng = rand::rngs::OsRng {}; + let new_implicit_account: common::SecretKey = + ed25519::SigScheme::generate(&mut csprng) + .try_to_sk() + .unwrap(); + + let tx = Tx::new( + wasm_loader::read_wasm_or_exit(WASM_DIR, TX_REVEAL_PK_WASM), + Some(new_implicit_account.to_public().try_to_vec().unwrap()), + ChainId("bench".to_string()), + None, + ); + + c.bench_function("reveal_pk", |b| { + b.iter_batched_ref( + BenchShell::default, + |shell| shell.execute_tx(&tx), + criterion::BatchSize::LargeInput, + ) + }); +} + +fn update_vp(c: &mut Criterion) { + let shell = BenchShell::default(); + let vp_code_hash: Hash = shell + .read_storage_key(&Key::wasm_hash(VP_VALIDATOR_WASM)) + .unwrap(); + let signed_tx = generate_tx( + TX_UPDATE_VP_WASM, + UpdateVp { + addr: defaults::albert_address(), + vp_code_hash, + }, + &defaults::albert_keypair(), + ); + + c.bench_function("update_vp", |b| { + b.iter_batched_ref( + BenchShell::default, + |shell| shell.execute_tx(&signed_tx), + criterion::BatchSize::LargeInput, + ) + }); +} + +fn init_account(c: &mut Criterion) { + let mut csprng = rand::rngs::OsRng {}; + let new_account: common::SecretKey = + ed25519::SigScheme::generate(&mut csprng) + .try_to_sk() + .unwrap(); + + let shell = BenchShell::default(); + let vp_code_hash: Hash = shell + .read_storage_key(&Key::wasm_hash(VP_VALIDATOR_WASM)) + .unwrap(); + let signed_tx = generate_tx( + TX_INIT_ACCOUNT_WASM, + InitAccount { + public_key: new_account.to_public(), + vp_code_hash, + }, + &defaults::albert_keypair(), + ); + + c.bench_function("init_account", |b| { + b.iter_batched_ref( + BenchShell::default, + |shell| shell.execute_tx(&signed_tx), + criterion::BatchSize::LargeInput, + ) + }); +} + +fn init_proposal(c: &mut Criterion) { + let mut group = c.benchmark_group("init_proposal"); + + for bench_name in ["minimal_proposal", "complete_proposal"] { + group.bench_function(bench_name, |b| { + b.iter_batched_ref( + || { + let shell = BenchShell::default(); + + let signed_tx = match bench_name { + "minimal_proposal" => generate_tx( + TX_INIT_PROPOSAL_WASM, + InitProposalData { + id: None, + content: vec![], + author: defaults::albert_address(), + r#type: ProposalType::Default(None), + voting_start_epoch: 12.into(), + voting_end_epoch: 15.into(), + grace_epoch: 18.into(), + }, + &defaults::albert_keypair(), + ), + "complete_proposal" => { + let max_code_size_key = + governance::storage::get_max_proposal_code_size_key(); + let max_proposal_content_key = + governance::storage::get_max_proposal_content_key(); + let max_code_size = shell + .wl_storage + .read(&max_code_size_key) + .expect("Error while reading from storage") + .expect( + "Missing max_code_size parameter in \ + storage", + ); + let max_proposal_content_size = shell + .wl_storage + .read(&max_proposal_content_key) + .expect("Error while reading from storage") + .expect( + "Missing max_proposal_content parameter \ + in storage", + ); + + generate_tx( + TX_INIT_PROPOSAL_WASM, + InitProposalData { + id: Some(1), + content: vec![0; max_proposal_content_size], + author: defaults::albert_address(), + r#type: ProposalType::Default(Some( + vec![0; max_code_size], + )), + voting_start_epoch: 12.into(), + voting_end_epoch: 15.into(), + grace_epoch: 18.into(), + }, + &defaults::albert_keypair(), + ) + } + _ => panic!("unexpected bench test"), + }; + + (shell, signed_tx) + }, + |(shell, signed_tx)| shell.execute_tx(signed_tx), + criterion::BatchSize::LargeInput, + ) + }); + } + + group.finish(); +} + +fn vote_proposal(c: &mut Criterion) { + let mut group = c.benchmark_group("vote_proposal"); + let delegator_vote = generate_tx( + TX_VOTE_PROPOSAL_WASM, + VoteProposalData { + id: 0, + vote: ProposalVote::Yay(VoteType::Default), + voter: defaults::albert_address(), + delegations: vec![defaults::validator_address()], + }, + &defaults::albert_keypair(), + ); + + let validator_vote = generate_tx( + TX_VOTE_PROPOSAL_WASM, + VoteProposalData { + id: 0, + vote: ProposalVote::Nay, + voter: defaults::validator_address(), + delegations: vec![], + }, + &defaults::validator_keypair(), + ); + + for (signed_tx, bench_name) in [delegator_vote, validator_vote] + .iter() + .zip(["delegator_vote", "validator_vote"]) + { + group.bench_function(bench_name, |b| { + b.iter_batched_ref( + BenchShell::default, + |shell| shell.execute_tx(signed_tx), + criterion::BatchSize::LargeInput, + ) + }); + } + + group.finish(); +} + +fn init_validator(c: &mut Criterion) { + let mut csprng = rand::rngs::OsRng {}; + let consensus_key: common::PublicKey = + secp256k1::SigScheme::generate(&mut csprng) + .try_to_sk::() + .unwrap() + .to_public(); + + let protocol_key: common::PublicKey = + secp256k1::SigScheme::generate(&mut csprng) + .try_to_sk::() + .unwrap() + .to_public(); + + let dkg_key = ferveo_common::Keypair::::new( + &mut StdRng::from_entropy(), + ) + .public() + .into(); + + let shell = BenchShell::default(); + let validator_vp_code_hash: Hash = shell + .read_storage_key(&Key::wasm_hash(VP_VALIDATOR_WASM)) + .unwrap(); + let signed_tx = generate_tx( + TX_INIT_VALIDATOR_WASM, + InitValidator { + account_key: defaults::albert_keypair().to_public(), + consensus_key, + protocol_key, + dkg_key, + commission_rate: Decimal::default(), + max_commission_rate_change: Decimal::default(), + validator_vp_code_hash, + }, + &defaults::albert_keypair(), + ); + + c.bench_function("init_validator", |b| { + b.iter_batched_ref( + BenchShell::default, + |shell| shell.execute_tx(&signed_tx), + criterion::BatchSize::LargeInput, + ) + }); +} + +fn change_validator_commission(c: &mut Criterion) { + let signed_tx = generate_tx( + TX_CHANGE_VALIDATOR_COMMISSION_WASM, + CommissionChange { + validator: defaults::validator_address(), + new_rate: Decimal::new(6, 2), + }, + &defaults::validator_keypair(), + ); + + c.bench_function("change_validator_commission", |b| { + b.iter_batched_ref( + BenchShell::default, + |shell| shell.execute_tx(&signed_tx), + criterion::BatchSize::LargeInput, + ) + }); +} + +fn ibc(c: &mut Criterion) { + let signed_tx = generate_ibc_transfer_tx(); + + c.bench_function("ibc_transfer", |b| { + b.iter_batched_ref( + || { + let mut shell = BenchShell::default(); + shell.init_ibc_channel(); + + shell + }, + |shell| shell.execute_tx(&signed_tx), + criterion::BatchSize::LargeInput, + ) + }); +} + +criterion_group!( + whitelisted_txs, + transfer, + bond, + unbond, + withdraw, + reveal_pk, + update_vp, + init_account, + init_proposal, + vote_proposal, + init_validator, + change_validator_commission, + ibc +); +criterion_main!(whitelisted_txs); diff --git a/benches/vps.rs b/benches/vps.rs new file mode 100644 index 0000000000..e11d8ebc69 --- /dev/null +++ b/benches/vps.rs @@ -0,0 +1,585 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use borsh::BorshSerialize; +use criterion::{criterion_group, criterion_main, Criterion}; +use namada::core::types::address::{self, Address}; +use namada::core::types::key::{ + common, SecretKey as SecretKeyInterface, SigScheme, +}; +use namada::core::types::token::{Amount, Transfer}; +use namada::ledger::gas::VpGasMeter; +use namada::proto::Tx; +use namada::types::chain::ChainId; +use namada::types::governance::{ProposalVote, VoteType}; +use namada::types::hash::Hash; +use namada::types::key::ed25519; +use namada::types::masp::{TransferSource, TransferTarget}; +use namada::types::storage::{Key, TxIndex}; +use namada::types::transaction::governance::VoteProposalData; +use namada::types::transaction::pos::{Bond, CommissionChange}; +use namada::types::transaction::UpdateVp; +use namada::vm::wasm::run; +use namada_apps::wallet::defaults; +use namada_apps::wasm_loader; +use namada_benches::{ + generate_foreign_key_tx, generate_tx, BenchShell, BenchShieldedCtx, + ALBERT_PAYMENT_ADDRESS, ALBERT_SPENDING_KEY, BERTHA_PAYMENT_ADDRESS, + TX_BOND_WASM, TX_CHANGE_VALIDATOR_COMMISSION_WASM, TX_REVEAL_PK_WASM, + TX_TRANSFER_WASM, TX_UNBOND_WASM, TX_UPDATE_VP_WASM, TX_VOTE_PROPOSAL_WASM, + VP_VALIDATOR_WASM, WASM_DIR, +}; +use rust_decimal::Decimal; + +const VP_USER_WASM: &str = "vp_user.wasm"; +const VP_TOKEN_WASM: &str = "vp_token.wasm"; +const VP_IMPLICIT_WASM: &str = "vp_implicit.wasm"; +const VP_MASP_WASM: &str = "vp_masp.wasm"; + +fn vp_user(c: &mut Criterion) { + let mut group = c.benchmark_group("vp_user"); + + let foreign_key_write = + generate_foreign_key_tx(&defaults::albert_keypair()); + + let transfer = generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: defaults::albert_address(), + target: defaults::bertha_address(), + token: address::nam(), + sub_prefix: None, + amount: Amount::whole(1000), + key: None, + shielded: None, + }, + &defaults::albert_keypair(), + ); + + let received_transfer = generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: defaults::bertha_address(), + target: defaults::albert_address(), + token: address::nam(), + sub_prefix: None, + amount: Amount::whole(1000), + key: None, + shielded: None, + }, + &defaults::bertha_keypair(), + ); + + let shell = BenchShell::default(); + let vp_validator_hash = shell + .read_storage_key(&Key::wasm_hash(VP_VALIDATOR_WASM)) + .unwrap(); + let vp = generate_tx( + TX_UPDATE_VP_WASM, + UpdateVp { + addr: defaults::albert_address(), + vp_code_hash: vp_validator_hash, + }, + &defaults::albert_keypair(), + ); + + let vote = generate_tx( + TX_VOTE_PROPOSAL_WASM, + VoteProposalData { + id: 0, + vote: ProposalVote::Yay(VoteType::Default), + voter: defaults::albert_address(), + delegations: vec![defaults::validator_address()], + }, + &defaults::albert_keypair(), + ); + + let pos = generate_tx( + TX_UNBOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::whole(1000), + source: Some(defaults::albert_address()), + }, + &defaults::albert_keypair(), + ); + + for (signed_tx, bench_name) in [ + foreign_key_write, + transfer, + received_transfer, + vote, + pos, + vp, + ] + .iter() + .zip([ + "foreign_key_write", + "transfer", + "received_transfer", + "governance_vote", + "pos", + "vp", + ]) { + let mut shell = BenchShell::default(); + let vp_code_hash: Hash = shell + .read_storage_key(&Key::wasm_hash(VP_USER_WASM)) + .unwrap(); + shell.execute_tx(signed_tx); + let (verifiers, keys_changed) = shell + .wl_storage + .write_log + .verifiers_and_changed_keys(&BTreeSet::default()); + + group.bench_function(bench_name, |b| { + b.iter(|| { + assert!( + run::vp( + &vp_code_hash, + signed_tx, + &TxIndex(0), + &defaults::albert_address(), + &shell.wl_storage.storage, + &shell.wl_storage.write_log, + &mut VpGasMeter::new(u64::MAX, 0), + &BTreeMap::default(), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + false, + ) + .unwrap() + ); + }) + }); + } + + group.finish(); +} + +fn vp_implicit(c: &mut Criterion) { + let mut group = c.benchmark_group("vp_implicit"); + + let mut csprng = rand::rngs::OsRng {}; + let implicit_account: common::SecretKey = + ed25519::SigScheme::generate(&mut csprng) + .try_to_sk() + .unwrap(); + + let foreign_key_write = + generate_foreign_key_tx(&defaults::albert_keypair()); + + let transfer = generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: Address::from(&implicit_account.to_public()), + target: defaults::bertha_address(), + token: address::nam(), + sub_prefix: None, + amount: Amount::whole(500), + key: None, + shielded: None, + }, + &implicit_account, + ); + + let received_transfer = generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: defaults::bertha_address(), + target: Address::from(&implicit_account.to_public()), + token: address::nam(), + sub_prefix: None, + amount: Amount::whole(1000), + key: None, + shielded: None, + }, + &defaults::bertha_keypair(), + ); + + let reveal_pk = Tx::new( + wasm_loader::read_wasm_or_exit(WASM_DIR, TX_REVEAL_PK_WASM), + Some(implicit_account.to_public().try_to_vec().unwrap()), + ChainId("bench".to_string()), + None, + ); + + let pos = generate_tx( + TX_BOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::whole(1000), + source: Some(Address::from(&implicit_account.to_public())), + }, + &implicit_account, + ); + + let vote = generate_tx( + TX_VOTE_PROPOSAL_WASM, + VoteProposalData { + id: 0, + vote: ProposalVote::Yay(VoteType::Default), + voter: Address::from(&implicit_account.to_public()), + delegations: vec![], /* NOTE: no need to bond tokens because the + * implicit vp doesn't check that */ + }, + &implicit_account, + ); + + for (tx, bench_name) in [ + &foreign_key_write, + &reveal_pk, + &transfer, + &received_transfer, + &pos, + &vote, + ] + .into_iter() + .zip([ + "foreign_key_write", + "reveal_pk", + "transfer", + "received_transfer", + "pos", + "governance_vote", + ]) { + let mut shell = BenchShell::default(); + let vp_code_hash: Hash = shell + .read_storage_key(&Key::wasm_hash(VP_IMPLICIT_WASM)) + .unwrap(); + + if bench_name != "reveal_pk" { + // Reveal publick key + shell.execute_tx(&reveal_pk); + shell.wl_storage.commit_tx(); + shell.commit(); + } + + if bench_name == "transfer" { + // Transfer some tokens to the implicit address + shell.execute_tx(&received_transfer); + shell.wl_storage.commit_tx(); + shell.commit(); + } + + // Run the tx to validate + shell.execute_tx(tx); + let (verifiers, keys_changed) = shell + .wl_storage + .write_log + .verifiers_and_changed_keys(&BTreeSet::default()); + + group.bench_function(bench_name, |b| { + b.iter(|| { + assert!( + run::vp( + &vp_code_hash, + tx, + &TxIndex(0), + &Address::from(&implicit_account.to_public()), + &shell.wl_storage.storage, + &shell.wl_storage.write_log, + &mut VpGasMeter::new(u64::MAX, 0), + &BTreeMap::default(), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + false, + ) + .unwrap() + ) + }) + }); + } + + group.finish(); +} + +fn vp_validator(c: &mut Criterion) { + let shell = BenchShell::default(); + let vp_code_hash: Hash = shell + .read_storage_key(&Key::wasm_hash(VP_VALIDATOR_WASM)) + .unwrap(); + let mut group = c.benchmark_group("vp_validator"); + + let foreign_key_write = + generate_foreign_key_tx(&defaults::albert_keypair()); + + let transfer = generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: defaults::validator_address(), + target: defaults::bertha_address(), + token: address::nam(), + sub_prefix: None, + amount: Amount::whole(1000), + key: None, + shielded: None, + }, + &defaults::validator_keypair(), + ); + + let received_transfer = generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: defaults::bertha_address(), + target: defaults::validator_address(), + token: address::nam(), + sub_prefix: None, + amount: Amount::whole(1000), + key: None, + shielded: None, + }, + &defaults::bertha_keypair(), + ); + + let vp = generate_tx( + TX_UPDATE_VP_WASM, + UpdateVp { + addr: defaults::validator_address(), + vp_code_hash: vp_code_hash.clone(), + }, + &defaults::validator_keypair(), + ); + + let commission_rate = generate_tx( + TX_CHANGE_VALIDATOR_COMMISSION_WASM, + CommissionChange { + validator: defaults::validator_address(), + new_rate: Decimal::new(6, 2), + }, + &defaults::validator_keypair(), + ); + + let vote = generate_tx( + TX_VOTE_PROPOSAL_WASM, + VoteProposalData { + id: 0, + vote: ProposalVote::Yay(VoteType::Default), + voter: defaults::validator_address(), + delegations: vec![], + }, + &defaults::validator_keypair(), + ); + + let pos = generate_tx( + TX_UNBOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::whole(1000), + source: None, + }, + &defaults::validator_keypair(), + ); + + for (signed_tx, bench_name) in [ + foreign_key_write, + transfer, + received_transfer, + vote, + pos, + commission_rate, + vp, + ] + .iter() + .zip([ + "foreign_key_write", + "transfer", + "received_transfer", + "governance_vote", + "pos", + "commission_rate", + "vp", + ]) { + let mut shell = BenchShell::default(); + + shell.execute_tx(signed_tx); + let (verifiers, keys_changed) = shell + .wl_storage + .write_log + .verifiers_and_changed_keys(&BTreeSet::default()); + + group.bench_function(bench_name, |b| { + b.iter(|| { + assert!( + run::vp( + &vp_code_hash, + signed_tx, + &TxIndex(0), + &defaults::validator_address(), + &shell.wl_storage.storage, + &shell.wl_storage.write_log, + &mut VpGasMeter::new(u64::MAX, 0), + &BTreeMap::default(), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + false, + ) + .unwrap() + ); + }) + }); + } + + group.finish(); +} + +fn vp_token(c: &mut Criterion) { + let mut group = c.benchmark_group("vp_token"); + + let foreign_key_write = + generate_foreign_key_tx(&defaults::albert_keypair()); + + let transfer = generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: defaults::albert_address(), + target: defaults::bertha_address(), + token: address::nam(), + sub_prefix: None, + amount: Amount::whole(1000), + key: None, + shielded: None, + }, + &defaults::albert_keypair(), + ); + + for (signed_tx, bench_name) in [foreign_key_write, transfer] + .iter() + .zip(["foreign_key_write", "transfer"]) + { + let mut shell = BenchShell::default(); + let vp_code_hash: Hash = shell + .read_storage_key(&Key::wasm_hash(VP_TOKEN_WASM)) + .unwrap(); + shell.execute_tx(signed_tx); + let (verifiers, keys_changed) = shell + .wl_storage + .write_log + .verifiers_and_changed_keys(&BTreeSet::default()); + + group.bench_function(bench_name, |b| { + b.iter(|| { + assert!( + run::vp( + &vp_code_hash, + signed_tx, + &TxIndex(0), + &defaults::albert_address(), + &shell.wl_storage.storage, + &shell.wl_storage.write_log, + &mut VpGasMeter::new(u64::MAX, 0), + &BTreeMap::default(), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + false, + ) + .unwrap() + ); + }) + }); + } +} + +fn vp_masp(c: &mut Criterion) { + let mut group = c.benchmark_group("vp_masp"); + + let amount = Amount::whole(500); + + for bench_name in ["shielding", "unshielding", "shielded"] { + group.bench_function(bench_name, |b| { + let mut shielded_ctx = BenchShieldedCtx::default(); + let vp_code_hash: Hash = shielded_ctx + .shell + .read_storage_key(&Key::wasm_hash(VP_MASP_WASM)) + .unwrap(); + + let albert_spending_key = shielded_ctx + .ctx + .wallet + .find_spending_key(ALBERT_SPENDING_KEY) + .unwrap() + .to_owned(); + let albert_payment_addr = shielded_ctx + .ctx + .wallet + .find_payment_addr(ALBERT_PAYMENT_ADDRESS) + .unwrap() + .to_owned(); + let bertha_payment_addr = shielded_ctx + .ctx + .wallet + .find_payment_addr(BERTHA_PAYMENT_ADDRESS) + .unwrap() + .to_owned(); + + // Shield some tokens for Albert + let shield_tx = shielded_ctx.generate_masp_tx( + amount, + TransferSource::Address(defaults::albert_address()), + TransferTarget::PaymentAddress(albert_payment_addr), + ); + shielded_ctx.shell.execute_tx(&shield_tx); + shielded_ctx.shell.wl_storage.commit_tx(); + shielded_ctx.shell.commit(); + + let signed_tx = match bench_name { + "shielding" => shielded_ctx.generate_masp_tx( + amount, + TransferSource::Address(defaults::albert_address()), + TransferTarget::PaymentAddress(albert_payment_addr), + ), + "unshielding" => shielded_ctx.generate_masp_tx( + amount, + TransferSource::ExtendedSpendingKey(albert_spending_key), + TransferTarget::Address(defaults::albert_address()), + ), + "shielded" => shielded_ctx.generate_masp_tx( + amount, + TransferSource::ExtendedSpendingKey(albert_spending_key), + TransferTarget::PaymentAddress(bertha_payment_addr), + ), + _ => panic!("Unexpected bench test"), + }; + shielded_ctx.shell.execute_tx(&signed_tx); + let (verifiers, keys_changed) = shielded_ctx + .shell + .wl_storage + .write_log + .verifiers_and_changed_keys(&BTreeSet::default()); + + b.iter(|| { + assert!( + run::vp( + &vp_code_hash, + &signed_tx, + &TxIndex(0), + &defaults::validator_address(), + &shielded_ctx.shell.wl_storage.storage, + &shielded_ctx.shell.wl_storage.write_log, + &mut VpGasMeter::new(u64::MAX, 0), + &BTreeMap::default(), + &keys_changed, + &verifiers, + shielded_ctx.shell.vp_wasm_cache.clone(), + false, + ) + .unwrap() + ); + }) + }); + } + + group.finish(); +} + +criterion_group!( + whitelisted_vps, + vp_user, + vp_implicit, + vp_validator, + vp_masp, + vp_token +); +criterion_main!(whitelisted_vps); diff --git a/core/src/ledger/gas.rs b/core/src/ledger/gas.rs index e51c9f78ef..4b93cee35d 100644 --- a/core/src/ledger/gas.rs +++ b/core/src/ledger/gas.rs @@ -1,8 +1,6 @@ //! Gas accounting module to track the gas usage in a block for transactions and //! validity predicates triggered by transactions. -use std::convert::TryFrom; - use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use thiserror::Error; @@ -15,34 +13,75 @@ pub enum Error { BlockGasExceeded, #[error("Overflow during gas operations")] GasOverflow, + #[error("Error converting to u64")] + ConversionError, } +const TX_SIZE_GAS_PER_BYTE: u64 = 10; const COMPILE_GAS_PER_BYTE: u64 = 1; -const BASE_TRANSACTION_FEE: u64 = 2; const PARALLEL_GAS_DIVIDER: u64 = 10; -/// The maximum value should be less or equal to i64::MAX -/// to avoid the gas overflow when sending this to ABCI -const BLOCK_GAS_LIMIT: u64 = 10_000_000_000_000; -const TRANSACTION_GAS_LIMIT: u64 = 10_000_000_000; - /// The minimum gas cost for accessing the storage pub const MIN_STORAGE_GAS: u64 = 1; +/// The gas cost for verifying the signature of a transaction +pub const VERIFY_TX_SIG_GAS_COST: u64 = 10; +/// The gas cost for validating wasm vp code +pub const WASM_VALIDATION_GAS_PER_BYTE: u64 = 1; +/// The cost for writing a byte to storage +pub const STORAGE_WRITE_GAS_PER_BYTE: u64 = 100; /// Gas module result for functions that may fail pub type Result = std::result::Result; -/// Gas metering in a block. Tracks the gas in a current block and a current -/// transaction. -#[derive(Debug, Default, Clone)] +/// Trait to share gas operations for transactions and validity predicates +pub trait TxVpGasMetering { + /// Add gas cost. It will return error when the + /// consumed gas exceeds the provided transaction gas limit, but the state + /// will still be updated + fn add(&mut self, gas: u64) -> Result<()>; + + /// Add the compiling cost proportionate to the code length + fn add_compiling_gas(&mut self, bytes_len: u64) -> Result<()> { + self.add( + bytes_len + .checked_mul(COMPILE_GAS_PER_BYTE) + .ok_or(Error::GasOverflow)?, + ) + } + + /// Add the gas for loading the wasm code from storage + fn add_wasm_load_from_storage_gas(&mut self, bytes_len: u64) -> Result<()> { + self.add( + bytes_len + .checked_mul(MIN_STORAGE_GAS) + .ok_or(Error::GasOverflow)?, + ) + } +} + +/// 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 + pub block_gas_limit: u64, block_gas: u64, +} + +/// Gas metering in a transaction +#[derive(Debug)] +pub struct TxGasMeter { + /// The gas limit for a transaction + pub tx_gas_limit: u64, transaction_gas: u64, } /// Gas metering in a validity predicate -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct VpGasMeter { + /// The transaction gas limit + tx_gas_limit: u64, /// The gas used in the transaction before the VP run initial_gas: u64, /// The current gas usage in the VP @@ -59,119 +98,153 @@ pub struct VpsGas { } impl BlockGasMeter { - /// Add gas cost for the current transaction. It will return error when the - /// consumed gas exceeds the transaction gas limit, but the state will still - /// be updated. - pub fn add(&mut self, gas: u64) -> Result<()> { - self.transaction_gas = self - .transaction_gas - .checked_add(gas) + /// Build a new instance. Requires the block_gas_limit protocol parameter. + pub fn new(block_gas_limit: u64) -> Self { + Self { + block_gas_limit, + block_gas: 0, + } + } + + /// 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( + &mut self, + tx_gas_meter: TxGasMeter, + ) -> Result<()> { + self.block_gas = self + .block_gas + .checked_add(tx_gas_meter.tx_gas_limit) .ok_or(Error::GasOverflow)?; - if self.transaction_gas > TRANSACTION_GAS_LIMIT { - return Err(Error::TransactionGasExceededError); + if self.block_gas > self.block_gas_limit { + return Err(Error::BlockGasExceeded); } Ok(()) } - /// Add the base transaction fee and the fee per transaction byte that's - /// charged the moment we try to apply the transaction. - pub fn add_base_transaction_fee(&mut self, bytes_len: usize) -> Result<()> { - tracing::trace!("add_base_transaction_fee {}", bytes_len); - self.add(BASE_TRANSACTION_FEE) - } + /// 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, + tx_gas_meter: TxGasMeter, + ) -> Result<()> { + let updated_gas = self + .block_gas + .checked_add(tx_gas_meter.tx_gas_limit) + .ok_or(Error::GasOverflow)?; - /// Add the compiling cost proportionate to the code length - pub fn add_compiling_fee(&mut self, bytes_len: usize) -> Result<()> { - self.add(bytes_len as u64 * COMPILE_GAS_PER_BYTE) + if updated_gas > self.block_gas_limit { + return Err(Error::BlockGasExceeded); + } + + self.block_gas = updated_gas; + + Ok(()) } +} - /// Add the transaction gas to the block's total gas. Returns the - /// transaction's gas cost and resets the transaction meter. It will return - /// error when the consumed gas exceeds the block gas limit, but the state - /// will still be updated. - pub fn finalize_transaction(&mut self) -> Result { - self.block_gas = self - .block_gas - .checked_add(self.transaction_gas) +impl TxVpGasMetering for TxGasMeter { + fn add(&mut self, gas: u64) -> Result<()> { + self.transaction_gas = self + .transaction_gas + .checked_add(gas) .ok_or(Error::GasOverflow)?; - let transaction_gas = self.transaction_gas; - self.transaction_gas = 0; - if self.block_gas > BLOCK_GAS_LIMIT { - return Err(Error::BlockGasExceeded); + if self.transaction_gas > self.tx_gas_limit { + return Err(Error::TransactionGasExceededError); } - Ok(transaction_gas) + + Ok(()) } +} - /// Reset the gas meter. - pub fn reset(&mut self) { - self.transaction_gas = 0; - self.block_gas = 0; +impl TxGasMeter { + /// Initialize a new Tx gas meter. Requires the gas limit for the specific + /// transaction + pub fn new(tx_gas_limit: u64) -> Self { + Self { + tx_gas_limit, + transaction_gas: 0, + } } - /// Get the total gas used in the current transaction. - pub fn get_current_transaction_gas(&self) -> u64 { - self.transaction_gas + /// Add the gas for the space that the transaction requires in the block + pub fn add_tx_size_gas( + &mut self, + bytes_len: impl TryInto, + ) -> Result<()> { + let bytes_len = + bytes_len.try_into().map_err(|_| Error::ConversionError)?; + self.add( + bytes_len + .checked_mul(TX_SIZE_GAS_PER_BYTE) + .ok_or(Error::GasOverflow)?, + ) } /// Add the gas cost used in validity predicates to the current transaction. pub fn add_vps_gas(&mut self, vps_gas: &VpsGas) -> Result<()> { self.add(vps_gas.get_current_gas()?) } -} -impl VpGasMeter { - /// Initialize a new VP gas meter, starting with the gas consumed in the - /// transaction so far. - pub fn new(initial_gas: u64) -> Self { - Self { - initial_gas, - current_gas: 0, - } + /// Get the total gas used in the current transaction. + pub fn get_current_transaction_gas(&self) -> u64 { + self.transaction_gas } +} - /// Consume gas in a validity predicate. It will return error when the - /// consumed gas exceeds the transaction gas limit, but the state will still - /// be updated. - pub fn add(&mut self, gas: u64) -> Result<()> { - let gas = self +impl TxVpGasMetering for VpGasMeter { + fn add(&mut self, gas: u64) -> Result<()> { + self.current_gas = self .current_gas .checked_add(gas) .ok_or(Error::GasOverflow)?; - self.current_gas = gas; - let current_total = self .initial_gas .checked_add(self.current_gas) .ok_or(Error::GasOverflow)?; - if current_total > TRANSACTION_GAS_LIMIT { + if current_total > self.tx_gas_limit { return Err(Error::TransactionGasExceededError); } + Ok(()) } +} - /// Add the compiling cost proportionate to the code length - pub fn add_compiling_fee(&mut self, bytes_len: usize) -> Result<()> { - self.add(bytes_len as u64 * COMPILE_GAS_PER_BYTE) +impl VpGasMeter { + /// Initialize a new VP gas meter, starting with the gas consumed in the + /// transaction so far. Also requires the transaction gas limit. + pub fn new(tx_gas_limit: u64, initial_gas: u64) -> Self { + Self { + tx_gas_limit, + initial_gas, + current_gas: 0, + } } } impl VpsGas { - /// Set the gas cost from a single VP run. - pub fn set(&mut self, vp_gas_meter: &VpGasMeter) -> Result<()> { + /// Set the gas cost from a single VP run. It consumes the [`VpGasMeter`] + /// instance which shouldn't be accessed passed this point. + pub fn set(&mut self, vp_gas_meter: VpGasMeter) -> Result<()> { debug_assert_eq!(self.max, None); debug_assert!(self.rest.is_empty()); self.max = Some(vp_gas_meter.current_gas); - self.check_limit(vp_gas_meter.initial_gas) + self.check_limit(vp_gas_meter.tx_gas_limit, vp_gas_meter.initial_gas) } /// Merge validity predicates gas meters from parallelized runs. pub fn merge( &mut self, other: &mut VpsGas, + tx_gas_limit: u64, initial_gas: u64, ) -> Result<()> { match (self.max, other.max) { @@ -190,15 +263,15 @@ impl VpsGas { } self.rest.append(&mut other.rest); - self.check_limit(initial_gas) + self.check_limit(tx_gas_limit, initial_gas) } - fn check_limit(&self, initial_gas: u64) -> Result<()> { + fn check_limit(&self, tx_gas_limit: u64, initial_gas: u64) -> Result<()> { let total = initial_gas .checked_add(self.get_current_gas()?) .ok_or(Error::GasOverflow)?; - if total > TRANSACTION_GAS_LIMIT { - return Err(Error::GasOverflow); + if total > tx_gas_limit { + return Err(Error::TransactionGasExceededError); } Ok(()) } @@ -213,38 +286,34 @@ impl VpsGas { } } -/// Convert the gas from signed to unsigned int. This will panic on overflow, -/// but it should never occur for our gas limits (see -/// `tests::gas_limits_cannot_overflow_i64`). -pub fn as_i64(gas: u64) -> i64 { - i64::try_from(gas).expect("Gas should never overflow i64") -} - #[cfg(test)] mod tests { use proptest::prelude::*; use super::*; + const BLOCK_GAS_LIMIT: u64 = 10_000_000_000; + const TX_GAS_LIMIT: u64 = 1_000_000; proptest! { #[test] - fn test_vp_gas_meter_add(gas in 0..TRANSACTION_GAS_LIMIT) { - let mut meter = VpGasMeter::new(0); + fn test_vp_gas_meter_add(gas in 0..BLOCK_GAS_LIMIT) { + let mut meter = VpGasMeter::new(BLOCK_GAS_LIMIT, 0); meter.add(gas).expect("cannot add the gas"); } #[test] - fn test_block_gas_meter_add(gas in 0..TRANSACTION_GAS_LIMIT) { - let mut meter = BlockGasMeter::default(); - meter.add(gas).expect("cannot add the gas"); - let result = meter.finalize_transaction().expect("cannot finalize the tx"); - assert_eq!(result, gas); + 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 ); + 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, BLOCK_GAS_LIMIT); } } #[test] fn test_vp_gas_overflow() { - let mut meter = VpGasMeter::new(1); + let mut meter = VpGasMeter::new(BLOCK_GAS_LIMIT, 1); assert_matches!( meter.add(u64::MAX).expect_err("unexpectedly succeeded"), Error::GasOverflow @@ -253,18 +322,16 @@ mod tests { #[test] fn test_vp_gas_limit() { - let mut meter = VpGasMeter::new(1); + let mut meter = VpGasMeter::new(TX_GAS_LIMIT, 1); assert_matches!( - meter - .add(TRANSACTION_GAS_LIMIT) - .expect_err("unexpectedly succeeded"), + meter.add(TX_GAS_LIMIT).expect_err("unexpectedly succeeded"), Error::TransactionGasExceededError ); } #[test] fn test_tx_gas_overflow() { - let mut meter = BlockGasMeter::default(); + let mut meter = TxGasMeter::new(BLOCK_GAS_LIMIT); meter.add(1).expect("cannot add the gas"); assert_matches!( meter.add(u64::MAX).expect_err("unexpectedly succeeded"), @@ -274,10 +341,10 @@ mod tests { #[test] fn test_tx_gas_limit() { - let mut meter = BlockGasMeter::default(); + let mut meter = TxGasMeter::new(TX_GAS_LIMIT); assert_matches!( meter - .add(TRANSACTION_GAS_LIMIT + 1) + .add(TX_GAS_LIMIT + 1) .expect_err("unexpectedly succeeded"), Error::TransactionGasExceededError ); @@ -285,36 +352,24 @@ mod tests { #[test] fn test_block_gas_limit() { - let mut meter = BlockGasMeter::default(); + let mut meter = BlockGasMeter::new(BLOCK_GAS_LIMIT); + let transaction_gas = 10_000_u64; // add the maximum tx gas - for _ in 0..(BLOCK_GAS_LIMIT / TRANSACTION_GAS_LIMIT) { - meter - .add(TRANSACTION_GAS_LIMIT) - .expect("over the tx gas limit"); + for _ in 0..(BLOCK_GAS_LIMIT / transaction_gas) { + let tx_gas_meter = TxGasMeter::new(transaction_gas); meter - .finalize_transaction() + .finalize_transaction(tx_gas_meter) .expect("over the block gas limit"); } - meter - .add(TRANSACTION_GAS_LIMIT) - .expect("over the tx gas limit"); + let tx_gas_meter = TxGasMeter::new(transaction_gas); match meter - .finalize_transaction() + .finalize_transaction(tx_gas_meter) .expect_err("unexpectedly succeeded") { Error::BlockGasExceeded => {} _ => panic!("unexpected error happened"), } } - - /// Test that the function [`as_i64`] cannot fail for transaction and block - /// gas limit + some "tolerance" for gas exhaustion. - #[test] - fn gas_limits_cannot_overflow_i64() { - let tolerance = 10_000; - as_i64(BLOCK_GAS_LIMIT + tolerance); - as_i64(TRANSACTION_GAS_LIMIT + tolerance); - } } diff --git a/core/src/ledger/parameters/mod.rs b/core/src/ledger/parameters/mod.rs index bb9ae99577..df4ca4e41e 100644 --- a/core/src/ledger/parameters/mod.rs +++ b/core/src/ledger/parameters/mod.rs @@ -1,13 +1,15 @@ //! Protocol parameters pub mod storage; +use std::collections::BTreeMap; + use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use rust_decimal::Decimal; use thiserror::Error; use super::storage::types; use super::storage_api::{self, ResultExt, StorageRead, StorageWrite}; -use crate::ledger::storage::{self as ledger_storage}; +use crate::ledger::storage as ledger_storage; use crate::types::address::{Address, InternalAddress}; use crate::types::chain::ProposalBytes; use crate::types::hash::Hash; @@ -36,6 +38,8 @@ pub struct Parameters { pub max_expected_time_per_block: DurationSecs, /// Max payload size, in bytes, for a tx batch proposal. pub max_proposal_bytes: ProposalBytes, + /// Max gas for block + pub max_block_gas: u64, /// Whitelisted validity predicate hashes (read only) pub vp_whitelist: Vec, /// Whitelisted tx hashes (read only) @@ -58,6 +62,8 @@ pub struct Parameters { #[cfg(not(feature = "mainnet"))] /// Fixed fees for a wrapper tx to be accepted pub wrapper_tx_fees: Option, + /// Gas table + pub gas_table: BTreeMap, } /// Epoch duration. A new epoch begins as soon as both the `min_num_of_blocks` @@ -111,6 +117,7 @@ impl Parameters { epoch_duration, max_expected_time_per_block, max_proposal_bytes, + max_block_gas, vp_whitelist, tx_whitelist, implicit_vp_code_hash, @@ -123,16 +130,29 @@ impl Parameters { faucet_account, #[cfg(not(feature = "mainnet"))] wrapper_tx_fees, + gas_table, } = self; // write max proposal bytes parameter let max_proposal_bytes_key = storage::get_max_proposal_bytes_key(); storage.write(&max_proposal_bytes_key, max_proposal_bytes)?; + // write max block gas parameter + let max_block_gas_key = storage::get_max_block_gas_key(); + storage.write(&max_block_gas_key, max_block_gas)?; + // write epoch parameters let epoch_key = storage::get_epoch_duration_storage_key(); storage.write(&epoch_key, epoch_duration)?; + // write gas table + let gas_table_key = storage::get_gas_table_storage_key(); + let gas_table = gas_table + .iter() + .map(|(k, v)| (k.to_lowercase(), *v)) + .collect::>(); + storage.write(&gas_table_key, gas_table)?; + // write vp whitelist parameter let vp_whitelist_key = storage::get_vp_whitelist_storage_key(); let vp_whitelist = vp_whitelist @@ -392,6 +412,15 @@ where .into_storage_result()? }; + // read max block gas + let max_block_gas: u64 = { + let key = storage::get_max_block_gas_key(); + let value = storage.read(&key)?; + value + .ok_or(ReadError::ParametersMissing) + .into_storage_result()? + }; + // read epoch duration let epoch_duration = read_epoch_duration_parameter(storage)?; @@ -425,6 +454,13 @@ where let implicit_vp_code_hash = Hash::try_from(&value[..]).into_storage_result()?; + // read gas table + let gas_table_key = storage::get_gas_table_storage_key(); + let value = storage.read(&gas_table_key)?; + let gas_table: BTreeMap = value + .ok_or(ReadError::ParametersMissing) + .into_storage_result()?; + // read epochs per year let epochs_per_year_key = storage::get_epochs_per_year_key(); let value = storage.read(&epochs_per_year_key)?; @@ -472,6 +508,7 @@ where epoch_duration, max_expected_time_per_block, max_proposal_bytes, + max_block_gas, vp_whitelist, tx_whitelist, implicit_vp_code_hash, @@ -484,5 +521,6 @@ where faucet_account, #[cfg(not(feature = "mainnet"))] wrapper_tx_fees, + gas_table, }) } diff --git a/core/src/ledger/parameters/storage.rs b/core/src/ledger/parameters/storage.rs index 178b0860eb..581f0e4b6d 100644 --- a/core/src/ledger/parameters/storage.rs +++ b/core/src/ledger/parameters/storage.rs @@ -19,8 +19,10 @@ struct Keys { tx_whitelist: &'static str, vp_whitelist: &'static str, max_proposal_bytes: &'static str, + max_block_gas: &'static str, faucet_account: &'static str, wrapper_tx_fees: &'static str, + gas_table: &'static str, } /// Returns if the key is a parameter key. @@ -159,6 +161,16 @@ pub fn get_tx_whitelist_storage_key() -> Key { } } +/// Storage key used for gas cost. +pub fn get_gas_table_storage_key() -> Key { + Key { + segments: vec![ + DbKeySeg::AddressSeg(ADDRESS), + DbKeySeg::StringSeg(Keys::VALUES.gas_table.to_string()), + ], + } +} + /// Storage key used for max_epected_time_per_block parameter. pub fn get_max_expected_time_per_block_key() -> Key { Key { @@ -241,6 +253,16 @@ pub fn get_max_proposal_bytes_key() -> Key { } } +/// Storage key used for the max block gas. +pub fn get_max_block_gas_key() -> Key { + Key { + segments: vec![ + DbKeySeg::AddressSeg(ADDRESS), + DbKeySeg::StringSeg(Keys::VALUES.max_block_gas.to_string()), + ], + } +} + /// Storage key used for faucet account. pub fn get_faucet_account_key() -> Key { Key { diff --git a/core/src/ledger/storage/mod.rs b/core/src/ledger/storage/mod.rs index cb3f9299df..e5f6cac5a8 100644 --- a/core/src/ledger/storage/mod.rs +++ b/core/src/ledger/storage/mod.rs @@ -996,6 +996,8 @@ pub mod testing { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use chrono::{TimeZone, Utc}; use proptest::prelude::*; use proptest::test_runner::Config; @@ -1076,6 +1078,7 @@ mod tests { }; let mut parameters = Parameters { max_proposal_bytes: Default::default(), + max_block_gas: 0, epoch_duration: epoch_duration.clone(), max_expected_time_per_block: Duration::seconds(max_expected_time_per_block).into(), vp_whitelist: vec![], @@ -1090,6 +1093,7 @@ mod tests { faucet_account: None, #[cfg(not(feature = "mainnet"))] wrapper_tx_fees: None, + gas_table: BTreeMap::default() }; parameters.init_storage(&mut wl_storage).unwrap(); diff --git a/core/src/ledger/storage/wl_storage.rs b/core/src/ledger/storage/wl_storage.rs index 99df4534e4..588bea9c88 100644 --- a/core/src/ledger/storage/wl_storage.rs +++ b/core/src/ledger/storage/wl_storage.rs @@ -507,7 +507,7 @@ mod tests { })] #[test] fn test_prefix_iters( - key_vals in arb_key_vals(50), + key_vals in arb_key_vals(30), ) { test_prefix_iters_aux(key_vals) } diff --git a/core/src/ledger/storage/write_log.rs b/core/src/ledger/storage/write_log.rs index bf4881aab0..861f83bbe8 100644 --- a/core/src/ledger/storage/write_log.rs +++ b/core/src/ledger/storage/write_log.rs @@ -7,6 +7,7 @@ use itertools::Itertools; use thiserror::Error; use crate::ledger; +use crate::ledger::gas::STORAGE_WRITE_GAS_PER_BYTE; use crate::ledger::storage::{Storage, StorageHasher}; use crate::types::address::{Address, EstablishedAddressGen}; use crate::types::hash::Hash; @@ -191,7 +192,7 @@ impl WriteLog { // the previous value exists on the storage None => len as i64, }; - Ok((gas as _, size_diff)) + Ok((gas as u64 * STORAGE_WRITE_GAS_PER_BYTE, size_diff)) } /// Write a key and a value. @@ -557,7 +558,10 @@ mod tests { // insert a value let inserted = "inserted".as_bytes().to_vec(); let (gas, diff) = write_log.write(&key, inserted.clone()).unwrap(); - assert_eq!(gas, (key.len() + inserted.len()) as u64); + assert_eq!( + gas, + (key.len() + inserted.len()) as u64 * STORAGE_WRITE_GAS_PER_BYTE + ); assert_eq!(diff, inserted.len() as i64); // read the value @@ -573,7 +577,10 @@ mod tests { // update the value let updated = "updated".as_bytes().to_vec(); let (gas, diff) = write_log.write(&key, updated.clone()).unwrap(); - assert_eq!(gas, (key.len() + updated.len()) as u64); + assert_eq!( + gas, + (key.len() + updated.len()) as u64 * STORAGE_WRITE_GAS_PER_BYTE + ); assert_eq!(diff, updated.len() as i64 - inserted.len() as i64); // delete the key @@ -597,7 +604,10 @@ mod tests { // insert again let reinserted = "reinserted".as_bytes().to_vec(); let (gas, diff) = write_log.write(&key, reinserted.clone()).unwrap(); - assert_eq!(gas, (key.len() + reinserted.len()) as u64); + assert_eq!( + gas, + (key.len() + reinserted.len()) as u64 * STORAGE_WRITE_GAS_PER_BYTE + ); assert_eq!(diff, reinserted.len() as i64); } diff --git a/core/src/types/address.rs b/core/src/types/address.rs index 20e3e6f372..c7530e8236 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -575,17 +575,6 @@ pub fn masp() -> Address { Address::decode("atest1v4ehgw36xaryysfsx5unvve4g5my2vjz89p52sjxxgenzd348yuyyv3hg3pnjs35g5unvde4ca36y5").expect("The token address decoding shouldn't fail") } -/// Sentinel secret key to indicate a MASP source -pub fn masp_tx_key() -> crate::types::key::common::SecretKey { - use crate::types::key::common; - let bytes = [ - 0, 27, 238, 157, 32, 131, 242, 184, 142, 146, 189, 24, 249, 68, 165, - 205, 71, 213, 158, 25, 253, 52, 217, 87, 52, 171, 225, 110, 131, 238, - 58, 94, 56, - ]; - common::SecretKey::try_from_slice(bytes.as_ref()).unwrap() -} - /// Temporary helper for testing, a hash map of tokens addresses with their /// MASP XAN incentive schedules. If the reward is (a, b) then a rewarded tokens /// are dispensed for every b possessed tokens. diff --git a/core/src/types/internal.rs b/core/src/types/internal.rs index d13d392381..3987b18e95 100644 --- a/core/src/types/internal.rs +++ b/core/src/types/internal.rs @@ -54,6 +54,8 @@ mod tx_queue { pub struct WrapperTxInQueue { /// Wrapper tx pub tx: crate::types::transaction::WrapperTx, + /// The available gas remaining for the inner tx (for gas accounting) + pub gas: u64, #[cfg(not(feature = "mainnet"))] /// A PoW solution can be used to allow zero-fee testnet /// transactions. diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index 9ceddecf15..9064463139 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -58,6 +58,8 @@ pub const RESERVED_VP_KEY: &str = "?"; pub const WASM_KEY_PREFIX: &str = "wasm"; /// The reserved storage key prefix for wasm codes pub const WASM_CODE_PREFIX: &str = "code"; +/// The reserved storage key prefix for wasm codes' length +pub const WASM_CODE_LEN_PREFIX: &str = "len"; /// The reserved storage key prefix for wasm code hashes pub const WASM_HASH_PREFIX: &str = "hash"; @@ -549,6 +551,15 @@ impl Key { Key { segments } } + /// Returns a key of the wasm code's length of the given hash + pub fn wasm_code_len(code_hash: &Hash) -> Self { + let mut segments = + Self::from(WASM_KEY_PREFIX.to_owned().to_db_key()).segments; + segments.push(DbKeySeg::StringSeg(WASM_CODE_LEN_PREFIX.to_owned())); + segments.push(DbKeySeg::StringSeg(code_hash.to_string())); + Key { segments } + } + /// Returns a key of the wasm code hash of the given code path pub fn wasm_hash(code_path: impl AsRef) -> Self { let mut segments = diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index 5de138bacd..8b74acf05f 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -4,6 +4,8 @@ #[cfg(feature = "ferveo-tpke")] pub mod wrapper_tx { use std::convert::TryFrom; + use std::num::ParseIntError; + use std::str::FromStr; pub use ark_bls12_381::Bls12_381 as EllipticCurve; pub use ark_ec::{AffineCurve, PairingEngine}; @@ -58,7 +60,7 @@ pub mod wrapper_tx { Deserialize, )] pub struct Fee { - /// amount of the fee + /// amount of fee per gas unit pub amount: Amount, /// address of the token pub token: Address, @@ -122,14 +124,6 @@ pub mod wrapper_tx { } } - /// Round the input number up to the next highest multiple - /// of GAS_LIMIT_RESOLUTION - impl From for GasLimit { - fn from(amount: Amount) -> GasLimit { - GasLimit::from(u64::from(amount)) - } - } - /// Get back the gas limit as a raw number impl From<&GasLimit> for u64 { fn from(limit: &GasLimit) -> u64 { @@ -144,10 +138,14 @@ pub mod wrapper_tx { } } - /// Get back the gas limit as a raw number, viewed as an Amount - impl From for Amount { - fn from(limit: GasLimit) -> Amount { - Amount::from(limit.multiplier * GAS_LIMIT_RESOLUTION) + impl FromStr for GasLimit { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + // Expect input to be the multiplier + Ok(Self { + multiplier: s.parse()?, + }) } } diff --git a/genesis/dev.toml b/genesis/dev.toml index 0aff92206f..fb6c5eaaf0 100644 --- a/genesis/dev.toml +++ b/genesis/dev.toml @@ -148,6 +148,8 @@ max_expected_time_per_block = 30 epochs_per_year = 525_600 # Max payload size, in bytes, for a tx batch proposal. max_proposal_bytes = 22020096 +# Max amount of gas per block +max_block_gas = 1000000 # Proof of stake parameters. [pos_params] diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index 6a1cc634f3..57f2d51936 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -155,6 +155,10 @@ min_num_of_blocks = 4 max_expected_time_per_block = 30 # Max payload size, in bytes, for a tx batch proposal. max_proposal_bytes = 22020096 +# Max amount of gas per block +max_block_gas = 1000000000 +# Gas table +gas_table = {} # vp whitelist vp_whitelist = [] # tx whitelist diff --git a/shared/src/ledger/ibc/vp/mod.rs b/shared/src/ledger/ibc/vp/mod.rs index dc0a021b6f..9f11914b21 100644 --- a/shared/src/ledger/ibc/vp/mod.rs +++ b/shared/src/ledger/ibc/vp/mod.rs @@ -359,6 +359,7 @@ mod tests { const ADDRESS: Address = Address::Internal(InternalAddress::Ibc); const COMMITMENT_PREFIX: &[u8] = b"ibc"; + const TX_GAS_LIMIT: u64 = 1_000_000; fn get_client_id() -> ClientId { let id = format!("{}-0", MOCK_CLIENT_TYPE); @@ -717,7 +718,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -749,6 +750,7 @@ mod tests { #[test] fn test_create_client_fail() { let mut wl_storage = TestWlStorage::default(); + let mut keys_changed = BTreeSet::new(); // initialize the storage @@ -798,7 +800,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -934,7 +936,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1044,7 +1046,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1142,7 +1144,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1269,7 +1271,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1380,7 +1382,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1468,7 +1470,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1592,7 +1594,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1717,7 +1719,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1826,7 +1828,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1933,7 +1935,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2073,7 +2075,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2251,7 +2253,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2398,7 +2400,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2550,7 +2552,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2702,7 +2704,7 @@ mod tests { None, ) .sign(&keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); diff --git a/shared/src/ledger/native_vp/mod.rs b/shared/src/ledger/native_vp/mod.rs index bfd14e7c1b..a71146b573 100644 --- a/shared/src/ledger/native_vp/mod.rs +++ b/shared/src/ledger/native_vp/mod.rs @@ -537,7 +537,13 @@ where pk: &crate::types::key::common::PublicKey, sig: &crate::types::key::common::Signature, ) -> Result { - Ok(self.tx.verify_sig(pk, sig).is_ok()) + vp_host_fns::verify_tx_signature( + &mut self.gas_meter.borrow_mut(), + self.tx, + pk, + sig, + ) + .into_storage_result() } fn verify_masp(&self, _tx: Vec) -> Result { diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index 4090c05a4d..a52f1ce9c6 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -1,12 +1,14 @@ //! The ledger's protocol -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::panic; +use namada_core::ledger::gas::TxGasMeter; +use namada_core::types::hash::Hash; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use thiserror::Error; use crate::ledger::eth_bridge::vp::EthBridge; -use crate::ledger::gas::{self, BlockGasMeter, VpGasMeter}; +use crate::ledger::gas::{self, TxVpGasMetering, VpGasMeter}; use crate::ledger::ibc::vp::{Ibc, IbcToken}; use crate::ledger::native_vp::governance::GovernanceVp; use crate::ledger::native_vp::parameters::{self, ParametersVp}; @@ -18,7 +20,6 @@ use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{DBIter, Storage, StorageHasher, DB}; use crate::proto::{self, Tx}; use crate::types::address::{Address, InternalAddress}; -use crate::types::hash::Hash; use crate::types::storage; use crate::types::storage::TxIndex; use crate::types::transaction::{DecryptedTx, TxResult, TxType, VpsResult}; @@ -38,6 +39,8 @@ pub enum Error { TxTypeError, #[error("Gas error: {0}")] GasError(gas::Error), + #[error("Insufficient balance to pay fee")] + FeeError, #[error("Error executing VP for addresses: {0:?}")] VpRunnerError(vm::wasm::run::Error), #[error("The address {0} doesn't exist")] @@ -64,6 +67,12 @@ pub enum Error { ), #[error("Access to an internal address {0} is forbidden")] AccessForbidden(InternalAddress), + #[error("Error while converting the transaction code's hash")] + TxCodeHashConversion, + #[error("Could not retrieve wasm code from storage for hash {0}")] + MissingWasmCodeInStorage(Hash), + #[error("Failed type conversion: {0}")] + ConversionError(String), } /// Result of applying a transaction @@ -79,9 +88,9 @@ pub type Result = std::result::Result; #[allow(clippy::too_many_arguments)] pub fn apply_tx( tx: TxType, - tx_length: usize, tx_index: TxIndex, - block_gas_meter: &mut BlockGasMeter, + tx_gas_meter: &mut TxGasMeter, + gas_table: &BTreeMap, write_log: &mut WriteLog, storage: &Storage, vp_wasm_cache: &mut VpCache, @@ -92,10 +101,6 @@ where H: 'static + StorageHasher + Sync, CA: 'static + WasmCacheAccess + Sync, { - // Base gas cost for applying the tx - block_gas_meter - .add_base_transaction_fee(tx_length) - .map_err(Error::GasError)?; match tx { TxType::Raw(_) => Err(Error::TxTypeError), TxType::Decrypted(DecryptedTx::Decrypted { @@ -107,7 +112,8 @@ where &tx, &tx_index, storage, - block_gas_meter, + tx_gas_meter, + gas_table, write_log, vp_wasm_cache, tx_wasm_cache, @@ -117,7 +123,8 @@ where &tx, &tx_index, storage, - block_gas_meter, + tx_gas_meter, + gas_table, write_log, &verifiers, vp_wasm_cache, @@ -125,9 +132,7 @@ where has_valid_pow, )?; - let gas_used = block_gas_meter - .finalize_transaction() - .map_err(Error::GasError)?; + let gas_used = tx_gas_meter.get_current_transaction_gas(); let initialized_accounts = write_log.get_initialized_accounts(); let changed_keys = write_log.get_keys(); let ibc_events = write_log.take_ibc_events(); @@ -140,24 +145,18 @@ where ibc_events, }) } - _ => { - let gas_used = block_gas_meter - .finalize_transaction() - .map_err(Error::GasError)?; - Ok(TxResult { - gas_used, - ..Default::default() - }) - } + _ => Ok(TxResult::default()), } } /// Execute a transaction code. Returns verifiers requested by the transaction. +#[allow(clippy::too_many_arguments)] fn execute_tx( tx: &Tx, tx_index: &TxIndex, storage: &Storage, - gas_meter: &mut BlockGasMeter, + tx_gas_meter: &mut TxGasMeter, + gas_table: &BTreeMap, write_log: &mut WriteLog, vp_wasm_cache: &mut VpCache, tx_wasm_cache: &mut TxCache, @@ -172,14 +171,21 @@ where wasm::run::tx( storage, write_log, - gas_meter, + tx_gas_meter, + gas_table, tx_index, &tx.code_or_hash, tx_data, vp_wasm_cache, tx_wasm_cache, ) - .map_err(Error::TxRunnerError) + .map_err(|e| { + if let wasm::run::Error::GasError(gas_error) = e { + Error::GasError(gas_error) + } else { + Error::TxRunnerError(e) + } + }) } /// Check the acceptance of a transaction by validity predicates @@ -188,7 +194,8 @@ fn check_vps( tx: &Tx, tx_index: &TxIndex, storage: &Storage, - gas_meter: &mut BlockGasMeter, + tx_gas_meter: &mut TxGasMeter, + gas_table: &BTreeMap, write_log: &WriteLog, verifiers_from_tx: &BTreeSet
, vp_wasm_cache: &mut VpCache, @@ -205,8 +212,6 @@ where let (verifiers, keys_changed) = write_log.verifiers_and_changed_keys(verifiers_from_tx); - let initial_gas = gas_meter.get_current_transaction_gas(); - let vps_result = execute_vps( verifiers, keys_changed, @@ -214,14 +219,16 @@ where tx_index, storage, write_log, - initial_gas, + tx_gas_meter.tx_gas_limit, + tx_gas_meter.get_current_transaction_gas(), + gas_table, vp_wasm_cache, #[cfg(not(feature = "mainnet"))] has_valid_pow, )?; tracing::debug!("Total VPs gas cost {:?}", vps_result.gas_used); - gas_meter + tx_gas_meter .add_vps_gas(&vps_result.gas_used) .map_err(Error::GasError)?; @@ -237,7 +244,9 @@ fn execute_vps( tx_index: &TxIndex, storage: &Storage, write_log: &WriteLog, + tx_gas_limit: u64, initial_gas: u64, + gas_table: &BTreeMap, vp_wasm_cache: &mut VpCache, #[cfg(not(feature = "mainnet"))] // This is true when the wrapper of this tx contained a valid @@ -252,7 +261,7 @@ where verifiers .par_iter() .try_fold(VpsResult::default, |mut result, addr| { - let mut gas_meter = VpGasMeter::new(initial_gas); + let mut gas_meter = VpGasMeter::new(tx_gas_limit, initial_gas); let accept = match &addr { Address::Implicit(_) | Address::Established(_) => { let (vp_hash, gas) = storage @@ -267,6 +276,10 @@ where } }; + // NOTE: because of the whitelisted gas and the gas metering + // for the exposed vm env functions, + // the first signature verification (if any) is accounted + // twice wasm::run::vp( &vp_code_hash, tx, @@ -275,6 +288,7 @@ where storage, write_log, &mut gas_meter, + gas_table, &keys_changed, &verifiers, vp_wasm_cache.clone(), @@ -410,30 +424,17 @@ where }; // Returning error from here will short-circuit the VP parallel - // execution. It's important that we only short-circuit gas - // errors to get deterministic gas costs - result.gas_used.set(&gas_meter).map_err(Error::GasError)?; - match accept { - Ok(accepted) => { - if !accepted { - result.rejected_vps.insert(addr.clone()); - } else { - result.accepted_vps.insert(addr.clone()); - } - Ok(result) - } - Err(err) => match err { - Error::GasError(_) => Err(err), - _ => { - result.rejected_vps.insert(addr.clone()); - result.errors.push((addr.clone(), err.to_string())); - Ok(result) - } - }, + // execution. + result.gas_used.set(gas_meter).map_err(Error::GasError)?; + if accept? { + result.accepted_vps.insert(addr.clone()); + } else { + result.rejected_vps.insert(addr.clone()); } + Ok(result) }) .try_reduce(VpsResult::default, |a, b| { - merge_vp_results(a, b, initial_gas) + merge_vp_results(a, b, tx_gas_limit, initial_gas) }) } @@ -441,6 +442,7 @@ where fn merge_vp_results( a: VpsResult, mut b: VpsResult, + tx_gas_limit: u64, initial_gas: u64, ) -> Result { let mut accepted_vps = a.accepted_vps; @@ -456,7 +458,7 @@ fn merge_vp_results( // gas costs gas_used - .merge(&mut b.gas_used, initial_gas) + .merge(&mut b.gas_used, tx_gas_limit, initial_gas) .map_err(Error::GasError)?; Ok(VpsResult { diff --git a/shared/src/ledger/queries/mod.rs b/shared/src/ledger/queries/mod.rs index 565100b81b..b608572acb 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -155,6 +155,8 @@ pub mod tm { /// Queries testing helpers #[cfg(any(test, feature = "testing"))] mod testing { + use std::collections::BTreeMap; + use tempfile::TempDir; use super::*; @@ -193,7 +195,37 @@ mod testing { /// Initialize a test client for the given root RPC router pub fn new(rpc: RPC) -> Self { // Initialize the `TestClient` - let wl_storage = TestWlStorage::default(); + let mut wl_storage = TestWlStorage::default(); + + // Initialize mock gas table and gas limit + let gas_table: BTreeMap = BTreeMap::default(); + let gas_table_key = + namada_core::ledger::parameters::storage::get_gas_table_storage_key(); + 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( + ); + 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", + ); let event_log = EventLog::default(); let (vp_wasm_cache, vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 14bc0a8806..b2c329e1ce 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -69,14 +69,29 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - use crate::ledger::gas::BlockGasMeter; + use std::collections::BTreeMap; + + use namada_core::ledger::gas::TxGasMeter; + use namada_core::ledger::parameters; + use crate::ledger::protocol; use crate::ledger::storage::write_log::WriteLog; use crate::proto::Tx; use crate::types::storage::TxIndex; use crate::types::transaction::{DecryptedTx, TxType}; - let mut gas_meter = BlockGasMeter::default(); + let gas_table: BTreeMap = ctx + .wl_storage + .read(¶meters::storage::get_gas_table_storage_key()) + .expect("Error while reading storage") + .expect("Missing gas table in storage"); + + let mut tx_gas_meter = TxGasMeter::new( + ctx.wl_storage + .read(¶meters::storage::get_max_block_gas_key()) + .expect("Error while reading storage key") + .expect("Missing parameter in storage"), + ); let mut write_log = WriteLog::default(); let tx = Tx::try_from(&request.data[..]).into_storage_result()?; let tx = TxType::Decrypted(DecryptedTx::Decrypted { @@ -86,9 +101,9 @@ where }); let data = protocol::apply_tx( tx, - request.data.len(), TxIndex(0), - &mut gas_meter, + &mut tx_gas_meter, + &gas_table, &mut write_log, &ctx.wl_storage.storage, &mut ctx.vp_wasm_cache, @@ -343,7 +358,7 @@ where #[cfg(test)] mod test { - use borsh::BorshDeserialize; + use borsh::{BorshDeserialize, BorshSerialize}; use namada_test_utils::TestWasms; use crate::ledger::queries::testing::TestClient; @@ -384,7 +399,13 @@ mod test { let tx_no_op = TestWasms::TxNoOp.read_bytes(); let tx_hash = Hash::sha256(&tx_no_op); let key = Key::wasm_code(&tx_hash); + let len_key = Key::wasm_code_len(&tx_hash); client.wl_storage.storage.write(&key, &tx_no_op).unwrap(); + client + .wl_storage + .storage + .write(&len_key, (tx_no_op.len() as u64).try_to_vec().unwrap()) + .unwrap(); // Request last committed epoch let read_epoch = RPC.shell().epoch(&client).await.unwrap(); diff --git a/shared/src/ledger/queries/types.rs b/shared/src/ledger/queries/types.rs index fd65edcfd9..fcd43e081c 100644 --- a/shared/src/ledger/queries/types.rs +++ b/shared/src/ledger/queries/types.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use namada_core::ledger::storage::WlStorage; use crate::ledger::events::log::EventLog; @@ -75,7 +77,7 @@ pub trait Router { pub trait Client { /// `std::io::Error` can happen in decoding with /// `BorshDeserialize::try_from_slice` - type Error: From; + type Error: From + Display; /// Send a simple query request at the given path. For more options, use the /// `request` method. diff --git a/shared/src/ledger/vp_host_fns.rs b/shared/src/ledger/vp_host_fns.rs index 84b3e6f357..fe927a3e7f 100644 --- a/shared/src/ledger/vp_host_fns.rs +++ b/shared/src/ledger/vp_host_fns.rs @@ -2,8 +2,10 @@ use std::num::TryFromIntError; +use namada_core::ledger::gas::VERIFY_TX_SIG_GAS_COST; use namada_core::types::address::Address; use namada_core::types::hash::{Hash, HASH_LENGTH}; +use namada_core::types::key::common; use namada_core::types::storage::{ BlockHash, BlockHeight, Epoch, Header, Key, TxIndex, }; @@ -11,7 +13,7 @@ use thiserror::Error; use super::gas::MIN_STORAGE_GAS; use crate::ledger::gas; -use crate::ledger::gas::VpGasMeter; +use crate::ledger::gas::{TxVpGasMetering, VpGasMeter}; use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{self, write_log, Storage, StorageHasher}; use crate::proto::Tx; @@ -383,3 +385,15 @@ where } Ok(None) } + +/// Verify the signature of a transaction +pub fn verify_tx_signature( + gas_meter: &mut VpGasMeter, + tx: &Tx, + pk: &common::PublicKey, + sig: &common::Signature, +) -> EnvResult { + add_gas(gas_meter, VERIFY_TX_SIG_GAS_COST)?; + + Ok(tx.verify_sig(pk, sig).is_ok()) +} diff --git a/shared/src/vm/host_env.rs b/shared/src/vm/host_env.rs index 49625b9097..cebd9d007c 100644 --- a/shared/src/vm/host_env.rs +++ b/shared/src/vm/host_env.rs @@ -5,6 +5,7 @@ use std::convert::TryInto; use std::num::TryFromIntError; use borsh::{BorshDeserialize, BorshSerialize}; +use namada_core::ledger::gas::{TxGasMeter, TxVpGasMetering}; use namada_core::types::internal::KeyVal; use thiserror::Error; @@ -13,7 +14,9 @@ use super::wasm::TxCache; #[cfg(feature = "wasm-runtime")] use super::wasm::VpCache; use super::WasmCacheAccess; -use crate::ledger::gas::{self, BlockGasMeter, VpGasMeter, MIN_STORAGE_GAS}; +use crate::ledger::gas::{ + self, VpGasMeter, MIN_STORAGE_GAS, WASM_VALIDATION_GAS_PER_BYTE, +}; use crate::ledger::storage::write_log::{self, WriteLog}; use crate::ledger::storage::{self, Storage, StorageHasher}; use crate::ledger::vp_host_fns; @@ -28,9 +31,6 @@ use crate::vm::memory::VmMemory; use crate::vm::prefix_iter::{PrefixIteratorId, PrefixIterators}; use crate::vm::{HostRef, MutHostRef}; -const VERIFY_TX_SIG_GAS_COST: u64 = 1000; -const WASM_VALIDATION_GAS_PER_BYTE: u64 = 1; - /// These runtime errors will abort tx WASM execution immediately #[allow(missing_docs)] #[derive(Error, Debug)] @@ -91,7 +91,7 @@ where /// Storage prefix iterators. pub iterators: MutHostRef<'a, &'a PrefixIterators<'a, DB>>, /// Transaction gas meter. - pub gas_meter: MutHostRef<'a, &'a BlockGasMeter>, + pub gas_meter: MutHostRef<'a, &'a TxGasMeter>, /// The transaction index is used to identify a shielded transaction's /// parent pub tx_index: HostRef<'a, &'a TxIndex>, @@ -131,7 +131,7 @@ where storage: &Storage, write_log: &mut WriteLog, iterators: &mut PrefixIterators<'a, DB>, - gas_meter: &mut BlockGasMeter, + gas_meter: &mut TxGasMeter, tx_index: &TxIndex, verifiers: &mut BTreeSet
, result_buffer: &mut Option>, @@ -287,7 +287,7 @@ pub trait VpEvaluator { fn eval( &self, ctx: VpCtx<'static, Self::Db, Self::H, Self::Eval, Self::CA>, - vp_code: Vec, + vp_code_hash: Vec, input_data: Vec, ) -> HostEnvResult; } @@ -1783,9 +1783,12 @@ where let sig: common::Signature = BorshDeserialize::try_from_slice(&sig) .map_err(vp_host_fns::RuntimeError::EncodingError)?; - vp_host_fns::add_gas(gas_meter, VERIFY_TX_SIG_GAS_COST)?; let tx = unsafe { env.ctx.tx.get() }; - Ok(HostEnvResult::from(tx.verify_sig(&pk, &sig).is_ok()).to_i64()) + + Ok(HostEnvResult::from(vp_host_fns::verify_tx_signature( + gas_meter, tx, &pk, &sig, + )?) + .to_i64()) } /// Verify a ShieldedTransaction. @@ -1881,8 +1884,8 @@ where /// Evaluate a validity predicate with the given input data. pub fn vp_eval( env: &VpVmEnv<'static, MEM, DB, H, EVAL, CA>, - vp_code_ptr: u64, - vp_code_len: u64, + vp_code_hash_ptr: u64, + vp_code_hash_len: u64, input_data_ptr: u64, input_data_len: u64, ) -> vp_host_fns::EnvResult @@ -1893,10 +1896,10 @@ where EVAL: VpEvaluator, CA: WasmCacheAccess, { - let (vp_code, gas) = - env.memory - .read_bytes(vp_code_ptr, vp_code_len as _) - .map_err(|e| vp_host_fns::RuntimeError::MemoryError(Box::new(e)))?; + let (vp_code, gas) = env + .memory + .read_bytes(vp_code_hash_ptr, vp_code_hash_len as _) + .map_err(|e| vp_host_fns::RuntimeError::MemoryError(Box::new(e)))?; let gas_meter = unsafe { env.ctx.gas_meter.get() }; vp_host_fns::add_gas(gas_meter, gas)?; @@ -2001,7 +2004,7 @@ pub mod testing { write_log: &mut WriteLog, iterators: &mut PrefixIterators<'static, DB>, verifiers: &mut BTreeSet
, - gas_meter: &mut BlockGasMeter, + gas_meter: &mut TxGasMeter, tx_index: &TxIndex, result_buffer: &mut Option>, #[cfg(feature = "wasm-runtime")] vp_wasm_cache: &mut VpCache, diff --git a/shared/src/vm/wasm/run.rs b/shared/src/vm/wasm/run.rs index 35e7875af2..fa22e3295e 100644 --- a/shared/src/vm/wasm/run.rs +++ b/shared/src/vm/wasm/run.rs @@ -1,8 +1,11 @@ //! Wasm runners -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::marker::PhantomData; +use borsh::BorshDeserialize; +use namada_core::ledger::gas::{self, TxGasMeter, TxVpGasMetering}; +use namada_core::ledger::storage::write_log::StorageModification; use parity_wasm::elements; use pwasm_utils::{self, rules}; use thiserror::Error; @@ -10,7 +13,7 @@ use wasmer::{BaseTunables, Module, Store}; use super::memory::{Limit, WasmMemory}; use super::TxCache; -use crate::ledger::gas::{BlockGasMeter, VpGasMeter}; +use crate::ledger::gas::VpGasMeter; use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{self, Storage, StorageHasher}; use crate::proto::Tx; @@ -36,7 +39,7 @@ const WASM_STACK_LIMIT: u32 = u16::MAX as u32; pub enum Error { #[error("Memory error: {0}")] MemoryError(memory::Error), - #[error("Unable to inject gas meter")] + #[error("Unable to inject stack limiter")] StackLimiterInjection, #[error("Wasm deserialization error: {0}")] DeserializationError(elements::Error), @@ -71,18 +74,28 @@ pub enum Error { LoadWasmCode(String), #[error("Unable to find compiled wasm code")] NoCompiledWasmCode, + #[error("Error while accounting for gas: {0}")] + GasError(#[from] gas::Error), + #[error("Failed type conversion: {0}")] + ConversionError(String), } /// Result for functions that may fail pub type Result = std::result::Result; +enum WasmPayload<'fetch> { + Hash(&'fetch Hash), + Code(&'fetch [u8]), +} + /// Execute a transaction code. Returns the set verifiers addresses requested by /// the transaction. #[allow(clippy::too_many_arguments)] pub fn tx( storage: &Storage, write_log: &mut WriteLog, - gas_meter: &mut BlockGasMeter, + gas_meter: &mut TxGasMeter, + gas_table: &BTreeMap, tx_index: &TxIndex, tx_code: impl AsRef<[u8]>, tx_data: impl AsRef<[u8]>, @@ -94,18 +107,40 @@ where H: 'static + StorageHasher, CA: 'static + WasmCacheAccess, { - let (module, store) = if tx_code.as_ref().len() == HASH_LENGTH { + let (tx_hash, code) = if tx_code.as_ref().len() == HASH_LENGTH { // we assume that there is no wasm code with HASH_LENGTH - let code_hash = + let tx_hash = Hash::try_from(tx_code.as_ref()).map_err(Error::CodeHash)?; - fetch_or_compile(tx_wasm_cache, &code_hash, write_log, storage)? + + (tx_hash, None) } else { - match tx_wasm_cache.compile_or_fetch(tx_code)? { - Some((module, store)) => (module, store), - None => return Err(Error::NoCompiledWasmCode), - } + let tx_hash = Hash::sha256(tx_code.as_ref()); + (tx_hash, Some(tx_code.as_ref())) + }; + + let code_or_hash = match code { + Some(code) => WasmPayload::Code(code), + None => WasmPayload::Hash(&tx_hash), }; + let (module, store) = fetch_or_compile( + tx_wasm_cache, + code_or_hash, + write_log, + storage, + gas_meter, + )?; + + let tx_gas_required = + match gas_table.get(&tx_hash.to_string().to_ascii_lowercase()) { + Some(gas) => gas.to_owned(), + #[cfg(test)] + None => 1_000, + #[cfg(not(test))] + None => 0, // VPs will reject the non-whitelisted tx + }; + gas_meter.add(tx_gas_required)?; + let mut iterators: PrefixIterators<'_, DB> = PrefixIterators::default(); let mut verifiers = BTreeSet::new(); let mut result_buffer: Option> = None; @@ -177,6 +212,7 @@ pub fn vp( storage: &Storage, write_log: &WriteLog, gas_meter: &mut VpGasMeter, + gas_table: &BTreeMap, keys_changed: &BTreeSet, verifiers: &BTreeSet
, mut vp_wasm_cache: VpCache, @@ -193,8 +229,13 @@ where }; // Compile the wasm module - let (module, store) = - fetch_or_compile(&mut vp_wasm_cache, vp_code_hash, write_log, storage)?; + let (module, store) = fetch_or_compile( + &mut vp_wasm_cache, + WasmPayload::Hash(vp_code_hash), + write_log, + storage, + gas_meter, + )?; let mut iterators: PrefixIterators<'_, DB> = PrefixIterators::default(); let mut result_buffer: Option> = None; @@ -229,21 +270,39 @@ where run_vp( module, imports, + vp_code_hash, input_data, address, keys_changed, verifiers, + gas_meter, + gas_table, ) } +#[allow(clippy::too_many_arguments)] fn run_vp( module: wasmer::Module, vp_imports: wasmer::ImportObject, + vp_code_hash: &Hash, input_data: &[u8], address: &Address, keys_changed: &BTreeSet, verifiers: &BTreeSet
, + gas_meter: &mut VpGasMeter, + gas_table: &BTreeMap, ) -> Result { + let vp_gas_required = + match gas_table.get(&vp_code_hash.to_string().to_ascii_lowercase()) { + Some(gas) => gas.to_owned(), + #[cfg(test)] + None => 1_000, + #[cfg(not(test))] + None => 0, + }; + + gas_meter.add(vp_gas_required).map_err(Error::GasError)?; + let input: VpInput = VpInput { addr: address, data: input_data, @@ -367,14 +426,43 @@ where let vp_wasm_cache = unsafe { ctx.vp_wasm_cache.get() }; let write_log = unsafe { ctx.write_log.get() }; let storage = unsafe { ctx.storage.get() }; + let gas_meter = unsafe { ctx.gas_meter.get() }; let env = VpVmEnv { memory: WasmMemory::default(), ctx, }; + let gas_table_key = &namada_core::ledger::parameters::storage::get_gas_table_storage_key(); + let gas_table = match write_log.read(gas_table_key).0 { + Some(StorageModification::Write { value }) => { + BTreeMap::try_from_slice(value) + .map_err(|e| Error::ConversionError(e.to_string())) + } + _ => match storage + .read(gas_table_key) + .map_err(|e| { + Error::LoadWasmCode(format!( + "Read gas table from storage failed: {}", + e + )) + })? + .0 + { + Some(v) => BTreeMap::try_from_slice(&v) + .map_err(|e| Error::ConversionError(e.to_string())), + None => Err(Error::LoadWasmCode( + "No gas table in storage".to_string(), + )), + }, + }?; // Compile the wasm module - let (module, store) = - fetch_or_compile(vp_wasm_cache, &vp_code_hash, write_log, storage)?; + let (module, store) = fetch_or_compile( + vp_wasm_cache, + WasmPayload::Hash(&vp_code_hash), + write_log, + storage, + gas_meter, + )?; let initial_memory = memory::prepare_vp_memory(&store).map_err(Error::MemoryError)?; @@ -384,10 +472,13 @@ where run_vp( module, imports, + &vp_code_hash, &input_data[..], address, keys_changed, verifiers, + gas_meter, + &gas_table, ) } } @@ -415,12 +506,14 @@ pub fn prepare_wasm_code>(code: T) -> Result> { elements::serialize(module).map_err(Error::SerializationError) } -// Fetch or compile a WASM code from the cache or storage +// Fetch or compile a WASM code from the cache or storage. Account for the +// loading and code compilation gas costs. fn fetch_or_compile( wasm_cache: &mut Cache, - code_hash: &Hash, + code_or_hash: WasmPayload, write_log: &WriteLog, storage: &Storage, + gas_meter: &mut dyn TxVpGasMetering, ) -> Result<(Module, Store)> where DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, @@ -428,36 +521,87 @@ where CN: 'static + CacheName, CA: 'static + WasmCacheAccess, { - use crate::core::ledger::storage::write_log::StorageModification; - match wasm_cache.fetch(code_hash)? { - Some((module, store)) => Ok((module, store)), - None => { - let key = Key::wasm_code(code_hash); - let code = match write_log.read(&key).0 { - Some(StorageModification::Write { value }) => value.clone(), - _ => match storage - .read(&key) - .map_err(|e| { - Error::LoadWasmCode(format!( - "Read wasm code failed from storage: key {}, \ - error {}", - key, e - )) - })? - .0 - { - Some(v) => v, - None => { - return Err(Error::LoadWasmCode(format!( - "No wasm code in storage: key {}", - key - ))); + match code_or_hash { + WasmPayload::Hash(code_hash) => { + let (module, store, tx_len) = match wasm_cache.fetch(code_hash)? { + Some((module, store)) => { + // Gas accounting even if the compiled module is in cache + let key = Key::wasm_code_len(code_hash); + let tx_len = match write_log.read(&key).0 { + Some(StorageModification::Write { value }) => { + u64::try_from_slice(value).map_err(|e| { + Error::ConversionError(e.to_string()) + }) + } + _ => match storage + .read(&key) + .map_err(|e| { + Error::LoadWasmCode(format!( + "Read wasm code length failed from \ + storage: key {}, error {}", + key, e + )) + })? + .0 + { + Some(v) => u64::try_from_slice(&v).map_err(|e| { + Error::ConversionError(e.to_string()) + }), + None => Err(Error::LoadWasmCode(format!( + "No wasm code length in storage: key {}", + key + ))), + }, + }?; + + (module, store, tx_len) + } + None => { + let key = Key::wasm_code(code_hash); + let code = match write_log.read(&key).0 { + Some(StorageModification::Write { value }) => { + value.clone() + } + _ => match storage + .read(&key) + .map_err(|e| { + Error::LoadWasmCode(format!( + "Read wasm code failed from storage: key \ + {}, error {}", + key, e + )) + })? + .0 + { + Some(v) => v, + None => { + return Err(Error::LoadWasmCode(format!( + "No wasm code in storage: key {}", + key + ))); + } + }, + }; + let tx_len = u64::try_from(code.len()) + .map_err(|e| Error::ConversionError(e.to_string()))?; + + match wasm_cache.compile_or_fetch(code)? { + Some((module, store)) => (module, store, tx_len), + None => return Err(Error::NoCompiledWasmCode), } - }, + } }; - validate_untrusted_wasm(&code).map_err(Error::ValidationError)?; - + gas_meter.add_wasm_load_from_storage_gas(tx_len)?; + gas_meter.add_compiling_gas(tx_len)?; + Ok((module, store)) + } + WasmPayload::Code(code) => { + gas_meter.add_compiling_gas( + u64::try_from(code.as_ref().len()) + .map_err(|e| Error::ConversionError(e.to_string()))?, + )?; + validate_untrusted_wasm(code).map_err(Error::ValidationError)?; match wasm_cache.compile_or_fetch(code)? { Some((module, store)) => Ok((module, store)), None => Err(Error::NoCompiledWasmCode), @@ -485,6 +629,8 @@ mod tests { use crate::types::validity_predicate::EvalVp; use crate::vm::wasm; + const TX_GAS_LIMIT: u64 = 100_000_000; + /// Test that when a transaction wasm goes over the stack-height limit, the /// execution is aborted. #[test] @@ -531,7 +677,8 @@ mod tests { fn test_tx_memory_limiter_in_guest() { let storage = TestStorage::default(); let mut write_log = WriteLog::default(); - let mut gas_meter = BlockGasMeter::default(); + let mut gas_meter = TxGasMeter::new(TX_GAS_LIMIT); + let gas_table = BTreeMap::default(); let tx_index = TxIndex::default(); // This code will allocate memory of the given size @@ -539,7 +686,10 @@ mod tests { // store the wasm code let code_hash = Hash::sha256(&tx_code); let key = Key::wasm_code(&code_hash); + let len_key = Key::wasm_code_len(&code_hash); + let code_len = (tx_code.len() as u64).try_to_vec().unwrap(); write_log.write(&key, tx_code).unwrap(); + write_log.write(&len_key, code_len).unwrap(); // Assuming 200 pages, 12.8 MiB limit assert_eq!(memory::TX_MEMORY_MAX_PAGES, 200); @@ -555,6 +705,7 @@ mod tests { &storage, &mut write_log, &mut gas_meter, + &gas_table, &tx_index, &code_hash, tx_data, @@ -570,6 +721,7 @@ mod tests { &storage, &mut write_log, &mut gas_meter, + &gas_table, &tx_index, &code_hash, tx_data, @@ -589,7 +741,8 @@ mod tests { let mut storage = TestStorage::default(); let addr = storage.address_gen.generate_address("rng seed"); let write_log = WriteLog::default(); - let mut gas_meter = VpGasMeter::new(0); + let mut gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); + let gas_table = BTreeMap::default(); let keys_changed = BTreeSet::new(); let verifiers = BTreeSet::new(); let tx_index = TxIndex::default(); @@ -599,13 +752,23 @@ mod tests { // store the wasm code let code_hash = Hash::sha256(&vp_eval); let key = Key::wasm_code(&code_hash); + let len_key = Key::wasm_code_len(&code_hash); + let code_len = (vp_eval.len() as u64).try_to_vec().unwrap(); storage.write(&key, vp_eval).unwrap(); + storage.write(&len_key, code_len).unwrap(); // This code will allocate memory of the given size let vp_memory_limit = TestWasms::VpMemoryLimit.read_bytes(); // store the wasm code let limit_code_hash = Hash::sha256(&vp_memory_limit); let key = Key::wasm_code(&limit_code_hash); + let len_key = Key::wasm_code_len(&limit_code_hash); + let code_len = (vp_memory_limit.len() as u64).try_to_vec().unwrap(); storage.write(&key, vp_memory_limit).unwrap(); + storage.write(&len_key, code_len).unwrap(); + let gas_table_key = &namada_core::ledger::parameters::storage::get_gas_table_storage_key(); + storage + .write(gas_table_key, gas_table.try_to_vec().unwrap()) + .unwrap(); // Assuming 200 pages, 12.8 MiB limit assert_eq!(memory::VP_MEMORY_MAX_PAGES, 200); @@ -630,6 +793,7 @@ mod tests { &storage, &write_log, &mut gas_meter, + &gas_table, &keys_changed, &verifiers, vp_cache.clone(), @@ -659,6 +823,7 @@ mod tests { &storage, &write_log, &mut gas_meter, + &gas_table, &keys_changed, &verifiers, vp_cache, @@ -677,7 +842,8 @@ mod tests { let mut storage = TestStorage::default(); let addr = storage.address_gen.generate_address("rng seed"); let write_log = WriteLog::default(); - let mut gas_meter = VpGasMeter::new(0); + let mut gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); + let gas_table = BTreeMap::default(); let keys_changed = BTreeSet::new(); let verifiers = BTreeSet::new(); let tx_index = TxIndex::default(); @@ -686,8 +852,11 @@ mod tests { let vp_code = TestWasms::VpMemoryLimit.read_bytes(); // store the wasm code let code_hash = Hash::sha256(&vp_code); + let code_len = (vp_code.len() as u64).try_to_vec().unwrap(); let key = Key::wasm_code(&code_hash); + let len_key = Key::wasm_code_len(&code_hash); storage.write(&key, vp_code).unwrap(); + storage.write(&len_key, code_len).unwrap(); // Assuming 200 pages, 12.8 MiB limit assert_eq!(memory::VP_MEMORY_MAX_PAGES, 200); @@ -705,6 +874,7 @@ mod tests { &storage, &write_log, &mut gas_meter, + &gas_table, &keys_changed, &verifiers, vp_cache.clone(), @@ -725,6 +895,7 @@ mod tests { &storage, &write_log, &mut gas_meter, + &gas_table, &keys_changed, &verifiers, vp_cache, @@ -742,14 +913,18 @@ mod tests { fn test_tx_memory_limiter_in_host_input() { let storage = TestStorage::default(); let mut write_log = WriteLog::default(); - let mut gas_meter = BlockGasMeter::default(); + let mut gas_meter = TxGasMeter::new(TX_GAS_LIMIT); + let gas_table = BTreeMap::default(); let tx_index = TxIndex::default(); let tx_no_op = TestWasms::TxNoOp.read_bytes(); // store the wasm code let code_hash = Hash::sha256(&tx_no_op); let key = Key::wasm_code(&code_hash); + let len_key = Key::wasm_code_len(&code_hash); + let code_len = (tx_no_op.len() as u64).try_to_vec().unwrap(); write_log.write(&key, tx_no_op).unwrap(); + write_log.write(&len_key, code_len).unwrap(); // Assuming 200 pages, 12.8 MiB limit assert_eq!(memory::TX_MEMORY_MAX_PAGES, 200); @@ -766,6 +941,7 @@ mod tests { &storage, &mut write_log, &mut gas_meter, + &gas_table, &tx_index, code_hash, tx_data, @@ -798,7 +974,8 @@ mod tests { let mut storage = TestStorage::default(); let addr = storage.address_gen.generate_address("rng seed"); let write_log = WriteLog::default(); - let mut gas_meter = VpGasMeter::new(0); + let mut gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); + let gas_table = BTreeMap::default(); let keys_changed = BTreeSet::new(); let verifiers = BTreeSet::new(); let tx_index = TxIndex::default(); @@ -807,7 +984,10 @@ mod tests { // store the wasm code let code_hash = Hash::sha256(&vp_code); let key = Key::wasm_code(&code_hash); + let len_key = Key::wasm_code_len(&code_hash); + let code_len = (vp_code.len() as u64).try_to_vec().unwrap(); storage.write(&key, vp_code).unwrap(); + storage.write(&len_key, code_len).unwrap(); // Assuming 200 pages, 12.8 MiB limit assert_eq!(memory::VP_MEMORY_MAX_PAGES, 200); @@ -826,6 +1006,7 @@ mod tests { &storage, &write_log, &mut gas_meter, + &gas_table, &keys_changed, &verifiers, vp_cache, @@ -860,14 +1041,18 @@ mod tests { fn test_tx_memory_limiter_in_host_env() { let mut storage = TestStorage::default(); let mut write_log = WriteLog::default(); - let mut gas_meter = BlockGasMeter::default(); + let mut gas_meter = TxGasMeter::new(TX_GAS_LIMIT); + let gas_table = BTreeMap::default(); let tx_index = TxIndex::default(); let tx_read_key = TestWasms::TxReadStorageKey.read_bytes(); // store the wasm code let code_hash = Hash::sha256(&tx_read_key); + let code_len = (tx_read_key.len() as u64).try_to_vec().unwrap(); let key = Key::wasm_code(&code_hash); + let len_key = Key::wasm_code_len(&code_hash); write_log.write(&key, tx_read_key).unwrap(); + write_log.write(&len_key, code_len).unwrap(); // Allocating `2^24` (16 MiB) for a value in storage that the tx // attempts to read should be above the memory limit and should @@ -889,6 +1074,7 @@ mod tests { &storage, &mut write_log, &mut gas_meter, + &gas_table, &tx_index, code_hash, tx_data, @@ -908,7 +1094,8 @@ mod tests { let mut storage = TestStorage::default(); let addr = storage.address_gen.generate_address("rng seed"); let write_log = WriteLog::default(); - let mut gas_meter = VpGasMeter::new(0); + let mut gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); + let gas_table = BTreeMap::default(); let keys_changed = BTreeSet::new(); let verifiers = BTreeSet::new(); let tx_index = TxIndex::default(); @@ -916,8 +1103,11 @@ mod tests { let vp_read_key = TestWasms::VpReadStorageKey.read_bytes(); // store the wasm code let code_hash = Hash::sha256(&vp_read_key); + let code_len = (vp_read_key.len() as u64).try_to_vec().unwrap(); let key = Key::wasm_code(&code_hash); + let len_key = Key::wasm_code_len(&code_hash); storage.write(&key, vp_read_key).unwrap(); + storage.write(&len_key, code_len).unwrap(); // Allocating `2^24` (16 MiB) for a value in storage that the tx // attempts to read should be above the memory limit and should @@ -941,6 +1131,7 @@ mod tests { &storage, &write_log, &mut gas_meter, + &gas_table, &keys_changed, &verifiers, vp_cache, @@ -961,7 +1152,8 @@ mod tests { let mut storage = TestStorage::default(); let addr = storage.address_gen.generate_address("rng seed"); let write_log = WriteLog::default(); - let mut gas_meter = VpGasMeter::new(0); + let mut gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); + let gas_table = BTreeMap::default(); let keys_changed = BTreeSet::new(); let verifiers = BTreeSet::new(); let tx_index = TxIndex::default(); @@ -970,14 +1162,20 @@ mod tests { let vp_eval = TestWasms::VpEval.read_bytes(); // store the wasm code let code_hash = Hash::sha256(&vp_eval); + let code_len = (vp_eval.len() as u64).try_to_vec().unwrap(); let key = Key::wasm_code(&code_hash); + let len_key = Key::wasm_code_len(&code_hash); storage.write(&key, vp_eval).unwrap(); + storage.write(&len_key, code_len).unwrap(); // This code will read value from the storage let vp_read_key = TestWasms::VpReadStorageKey.read_bytes(); // store the wasm code let read_code_hash = Hash::sha256(&vp_read_key); + let code_len = (vp_read_key.len() as u64).try_to_vec().unwrap(); let key = Key::wasm_code(&read_code_hash); + let len_key = Key::wasm_code_len(&read_code_hash); storage.write(&key, vp_read_key).unwrap(); + storage.write(&len_key, code_len).unwrap(); // Allocating `2^24` (16 MiB) for a value in storage that the tx // attempts to read should be above the memory limit and should @@ -1006,6 +1204,7 @@ mod tests { &storage, &write_log, &mut gas_meter, + &gas_table, &keys_changed, &verifiers, vp_cache, @@ -1055,20 +1254,25 @@ mod tests { let tx_index = TxIndex::default(); let storage = TestStorage::default(); let mut write_log = WriteLog::default(); - let mut gas_meter = BlockGasMeter::default(); + let mut gas_meter = TxGasMeter::new(TX_GAS_LIMIT); + let gas_table = BTreeMap::default(); let (mut vp_cache, _) = wasm::compilation_cache::common::testing::cache(); let (mut tx_cache, _) = wasm::compilation_cache::common::testing::cache(); // store the tx code let code_hash = Hash::sha256(&tx_code); + let code_len = (tx_code.len() as u64).try_to_vec().unwrap(); let key = Key::wasm_code(&code_hash); + let len_key = Key::wasm_code_len(&code_hash); write_log.write(&key, tx_code).unwrap(); + write_log.write(&len_key, code_len).unwrap(); tx( &storage, &mut write_log, &mut gas_meter, + &gas_table, &tx_index, code_hash, tx_data, @@ -1111,14 +1315,18 @@ mod tests { let mut storage = TestStorage::default(); let addr = storage.address_gen.generate_address("rng seed"); let write_log = WriteLog::default(); - let mut gas_meter = VpGasMeter::new(0); + let mut gas_meter = VpGasMeter::new(TX_GAS_LIMIT, 0); + let gas_table = BTreeMap::default(); let keys_changed = BTreeSet::new(); let verifiers = BTreeSet::new(); let (vp_cache, _) = wasm::compilation_cache::common::testing::cache(); // store the vp code let code_hash = Hash::sha256(&vp_code); + let code_len = (vp_code.len() as u64).try_to_vec().unwrap(); let key = Key::wasm_code(&code_hash); + let len_key = Key::wasm_code_len(&code_hash); storage.write(&key, vp_code).unwrap(); + storage.write(&len_key, code_len).unwrap(); vp( &code_hash, @@ -1128,6 +1336,7 @@ mod tests { &storage, &write_log, &mut gas_meter, + &gas_table, &keys_changed, &verifiers, vp_cache, diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index ea6261aed1..1022a2cb73 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -765,11 +765,11 @@ fn transfer_received_token( &sub_prefix, "--amount", "50000", - "--gas-amount", + "--fee-amount", "0", "--gas-limit", - "0", - "--gas-token", + "100", + "--fee-token", NAM, "--node", &rpc, @@ -982,11 +982,11 @@ fn submit_ibc_tx( &data_path, "--signer", signer, - "--gas-amount", + "--fee-amount", "0", "--gas-limit", - "0", - "--gas-token", + "100", + "--fee-token", NAM, "--node", &rpc @@ -1030,6 +1030,8 @@ fn transfer( &channel_id, "--port-id", &port_id, + "--gas-limit", + "100", "--node", &rpc, ]; diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index a8f9adfb48..e7f86b9196 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -32,7 +32,6 @@ use serde_json::json; use setup::constants::*; use super::helpers::{get_height, is_debug_mode, wait_for_block_height}; -use super::setup::get_all_wasms_hashes; use crate::e2e::helpers::{ epoch_sleep, find_address, find_bonded_stake, get_actor_rpc, get_epoch, }; @@ -113,12 +112,8 @@ fn test_node_connectivity_and_consensus() -> Result<()> { NAM, "--amount", "10.1", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -388,12 +383,8 @@ fn ledger_txs_and_queries() -> Result<()> { NAM, "--amount", "10.1", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ], @@ -408,12 +399,8 @@ fn ledger_txs_and_queries() -> Result<()> { NAM, "--amount", "10.1", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ], @@ -425,12 +412,8 @@ fn ledger_txs_and_queries() -> Result<()> { BERTHA, "--code-path", VP_USER_WASM, - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ], @@ -443,12 +426,8 @@ fn ledger_txs_and_queries() -> Result<()> { TX_TRANSFER_WASM, "--data-path", &tx_data_path, - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc ], @@ -464,12 +443,8 @@ fn ledger_txs_and_queries() -> Result<()> { VP_USER_WASM, "--alias", "Test-Account", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ], @@ -488,6 +463,8 @@ fn ledger_txs_and_queries() -> Result<()> { // Faucet withdrawal requires an explicit signer "--signer", ALBERT, + "--gas-limit", + "100", "--node", &validator_one_rpc, ], @@ -625,6 +602,8 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "10", + "--gas-limit", + "100", "--node", &validator_one_rpc, ], @@ -642,6 +621,8 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "15", + "--gas-limit", + "100", "--node", &validator_one_rpc, ], @@ -659,6 +640,8 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "20", + "--gas-limit", + "100", "--node", &validator_one_rpc, ], @@ -678,6 +661,8 @@ fn masp_txs_and_queries() -> Result<()> { "10", "--signer", ALBERT, + "--gas-limit", + "100", "--node", &validator_one_rpc, ], @@ -697,6 +682,8 @@ fn masp_txs_and_queries() -> Result<()> { "7", "--signer", ALBERT, + "--gas-limit", + "100", "--node", &validator_one_rpc, ], @@ -716,6 +703,8 @@ fn masp_txs_and_queries() -> Result<()> { "7", "--signer", ALBERT, + "--gas-limit", + "100", "--node", &validator_one_rpc, ], @@ -735,6 +724,8 @@ fn masp_txs_and_queries() -> Result<()> { "7", "--signer", ALBERT, + "--gas-limit", + "100", "--node", &validator_one_rpc, ], @@ -754,6 +745,8 @@ fn masp_txs_and_queries() -> Result<()> { "6", "--signer", ALBERT, + "--gas-limit", + "100", "--node", &validator_one_rpc, ], @@ -810,6 +803,8 @@ fn masp_txs_and_queries() -> Result<()> { "20", "--signer", BERTHA, + "--gas-limit", + "150", "--node", &validator_one_rpc, ], @@ -934,6 +929,8 @@ fn masp_pinned_txs() -> Result<()> { BTC, "--amount", "20", + "--gas-limit", + "100", "--node", &validator_one_rpc ], @@ -1060,6 +1057,8 @@ fn masp_incentives() -> Result<()> { BTC, "--amount", "20", + "--gas-limit", + "100", "--node", &validator_one_rpc ], @@ -1252,6 +1251,8 @@ fn masp_incentives() -> Result<()> { ETH, "--amount", "30", + "--gas-limit", + "100", "--node", &validator_one_rpc ], @@ -1380,6 +1381,8 @@ fn masp_incentives() -> Result<()> { "30", "--signer", BERTHA, + "--gas-limit", + "150", "--node", &validator_one_rpc ], @@ -1472,6 +1475,8 @@ fn masp_incentives() -> Result<()> { "20", "--signer", ALBERT, + "--gas-limit", + "150", "--node", &validator_one_rpc ], @@ -1630,6 +1635,8 @@ fn masp_incentives() -> Result<()> { &((amt30 * masp_rewards[ð()]).0 * (ep5.0 - ep3.0)).to_string(), "--signer", BERTHA, + "--gas-limit", + "150", "--node", &validator_one_rpc ], @@ -1657,6 +1664,8 @@ fn masp_incentives() -> Result<()> { &((amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0)).to_string(), "--signer", ALBERT, + "--gas-limit", + "100", "--node", &validator_one_rpc ], @@ -1757,12 +1766,8 @@ fn invalid_transactions() -> Result<()> { NAM, "--amount", "1", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -1771,7 +1776,7 @@ fn invalid_transactions() -> Result<()> { client.exp_string("Transaction accepted")?; client.exp_string("Transaction applied")?; client.exp_string("Transaction is invalid")?; - client.exp_string(r#""code": "4"#)?; + client.exp_string(r#""code": "5"#)?; client.assert_success(); let mut ledger = bg_ledger.foreground(); @@ -1812,12 +1817,8 @@ fn invalid_transactions() -> Result<()> { BERTHA, "--amount", "1_000_000.1", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", // Force to ignore client check that fails on the balance check of the // source address "--force", @@ -1831,7 +1832,7 @@ fn invalid_transactions() -> Result<()> { client.exp_string("Error trying to apply a transaction")?; - client.exp_string(r#""code": "3"#)?; + client.exp_string(r#""code": "4"#)?; client.assert_success(); Ok(()) @@ -1890,12 +1891,8 @@ fn pos_bonds() -> Result<()> { "validator-0", "--amount", "10000.0", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -1913,12 +1910,8 @@ fn pos_bonds() -> Result<()> { BERTHA, "--amount", "5000.0", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -1933,12 +1926,8 @@ fn pos_bonds() -> Result<()> { "validator-0", "--amount", "5100.0", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -1956,12 +1945,8 @@ fn pos_bonds() -> Result<()> { BERTHA, "--amount", "3200.", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -2000,12 +1985,8 @@ fn pos_bonds() -> Result<()> { "withdraw", "--validator", "validator-0", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -2021,12 +2002,8 @@ fn pos_bonds() -> Result<()> { "validator-0", "--source", BERTHA, - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -2238,16 +2215,29 @@ fn test_bond_queries() -> Result<()> { "bond", "--validator", validator_alias, + "--amount", + "100", + "--gas-limit", + "100", + "--ledger-address", + &validator_one_rpc, + ]; + let mut client = + run_as!(test, Who::Validator(0), Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // 3. Submit a delegation to the genesis validator + let tx_args = vec![ + "bond", + "--validator", + "validator-0", "--source", BERTHA, "--amount", "200", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--ledger-address", &validator_one_rpc, ]; @@ -2277,12 +2267,8 @@ fn test_bond_queries() -> Result<()> { BERTHA, "--amount", "300", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--ledger-address", &validator_one_rpc, ]; @@ -2299,12 +2285,8 @@ fn test_bond_queries() -> Result<()> { BERTHA, "--amount", "412", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--ledger-address", &validator_one_rpc, ]; @@ -2394,12 +2376,8 @@ fn pos_init_validator() -> Result<()> { "--source", BERTHA, "--unsafe-dont-encrypt", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--commission-rate", "0.05", "--max-commission-rate-change", @@ -2423,12 +2401,8 @@ fn pos_init_validator() -> Result<()> { NAM, "--amount", "0.5", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -2444,12 +2418,8 @@ fn pos_init_validator() -> Result<()> { BERTHA, "--amount", "1000.5", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -2468,12 +2438,8 @@ fn pos_init_validator() -> Result<()> { NAM, "--amount", "10999.5", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -2488,12 +2454,8 @@ fn pos_init_validator() -> Result<()> { new_validator, "--amount", "10000", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -2562,12 +2524,8 @@ fn ledger_many_txs_in_a_block() -> Result<()> { NAM, "--amount", "1.01", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", ]); @@ -2619,8 +2577,6 @@ fn ledger_many_txs_in_a_block() -> Result<()> { /// 13. Check governance address funds are 0 #[test] fn proposal_submission() -> Result<()> { - let working_dir = setup::working_dir(); - let test = setup::network( |genesis| { let parameters = ParametersConfig { @@ -2628,16 +2584,6 @@ fn proposal_submission() -> Result<()> { max_proposal_bytes: Default::default(), min_num_of_blocks: 4, max_expected_time_per_block: 1, - vp_whitelist: Some(get_all_wasms_hashes( - &working_dir, - Some("vp_"), - )), - // Enable tx whitelist to test the execution of a - // non-whitelisted tx by governance - tx_whitelist: Some(get_all_wasms_hashes( - &working_dir, - Some("tx_"), - )), ..genesis.parameters }; @@ -2674,12 +2620,8 @@ fn proposal_submission() -> Result<()> { BERTHA, "--amount", "900", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -2705,6 +2647,8 @@ fn proposal_submission() -> Result<()> { "init-proposal", "--data-path", valid_proposal_json_path.to_str().unwrap(), + "--gas-limit", + "100", "--node", &validator_one_rpc, ]; @@ -2806,6 +2750,8 @@ fn proposal_submission() -> Result<()> { "init-proposal", "--data-path", invalid_proposal_json_path.to_str().unwrap(), + "--gas-limit", + "100", "--node", &validator_one_rpc, ]; @@ -2860,6 +2806,8 @@ fn proposal_submission() -> Result<()> { "yay", "--signer", "validator-0", + "--gas-limit", + "100", "--node", &validator_one_rpc, ]; @@ -2882,6 +2830,8 @@ fn proposal_submission() -> Result<()> { "nay", "--signer", BERTHA, + "--gas-limit", + "100", "--node", &validator_one_rpc, ]; @@ -2900,6 +2850,8 @@ fn proposal_submission() -> Result<()> { "yay", "--signer", ALBERT, + "--gas-limit", + "100", "--node", &validator_one_rpc, ]; @@ -3027,12 +2979,8 @@ fn eth_governance_proposal() -> Result<()> { BERTHA, "--amount", "900", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--ledger-address", &validator_one_rpc, ]; @@ -3050,6 +2998,8 @@ fn eth_governance_proposal() -> Result<()> { "init-proposal", "--data-path", valid_proposal_json_path.to_str().unwrap(), + "--gas-limit", + "100", "--ledger-address", &validator_one_rpc, ]; @@ -3128,6 +3078,8 @@ fn eth_governance_proposal() -> Result<()> { &vote_arg, "--signer", BERTHA, + "--gas-limit", + "100", "--ledger-address", &validator_one_rpc, ]; @@ -3149,6 +3101,8 @@ fn eth_governance_proposal() -> Result<()> { &vote_arg, "--signer", "validator-0", + "--gas-limit", + "100", "--ledger-address", &validator_one_rpc, ]; @@ -3252,12 +3206,8 @@ fn pgf_governance_proposal() -> Result<()> { BERTHA, "--amount", "900", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--ledger-address", &validator_one_rpc, ]; @@ -3275,6 +3225,8 @@ fn pgf_governance_proposal() -> Result<()> { "init-proposal", "--data-path", valid_proposal_json_path.to_str().unwrap(), + "--gas-limit", + "100", "--ledger-address", &validator_one_rpc, ]; @@ -3291,6 +3243,8 @@ fn pgf_governance_proposal() -> Result<()> { "init-proposal", "--data-path", valid_proposal_json_path.to_str().unwrap(), + "--gas-limit", + "100", "--ledger-address", &validator_one_rpc, ]; @@ -3373,6 +3327,8 @@ fn pgf_governance_proposal() -> Result<()> { &arg_vote, "--signer", "validator-0", + "--gas-limit", + "100", "--ledger-address", &validator_one_rpc, ]; @@ -3399,6 +3355,8 @@ fn pgf_governance_proposal() -> Result<()> { &different_vote, "--signer", BERTHA, + "--gas-limit", + "100", "--ledger-address", &validator_one_rpc, ]; @@ -3418,6 +3376,8 @@ fn pgf_governance_proposal() -> Result<()> { &different_vote, "--signer", BERTHA, + "--gas-limit", + "100", "--ledger-address", &validator_one_rpc, ]; @@ -3528,12 +3488,8 @@ fn proposal_offline() -> Result<()> { ALBERT, "--amount", "900", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -3986,12 +3942,8 @@ fn test_genesis_validators() -> Result<()> { NAM, "--amount", "10.1", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; @@ -4162,12 +4114,8 @@ fn double_signing_gets_slashed() -> Result<()> { NAM, "--amount", "10.1", - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", - NAM, + "100", "--node", &validator_one_rpc, ]; diff --git a/tests/src/e2e/multitoken_tests/helpers.rs b/tests/src/e2e/multitoken_tests/helpers.rs index 7008910b5e..2d600a2b2c 100644 --- a/tests/src/e2e/multitoken_tests/helpers.rs +++ b/tests/src/e2e/multitoken_tests/helpers.rs @@ -46,11 +46,11 @@ pub fn init_multitoken_vp(test: &Test, rpc_addr: &str) -> Result { &multitoken_vp_wasm_path, "--alias", multitoken_alias, - "--gas-amount", + "--fee-amount", "0", "--gas-limit", - "0", - "--gas-token", + "100", + "--fee-token", NAM, "--ledger-address", rpc_addr, @@ -153,6 +153,8 @@ pub fn attempt_red_tokens_transfer( signer, "--amount", &amount, + "--gas-limit", + "100", "--ledger-address", rpc_addr, ]; diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index 8066910f81..61485593e2 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::ffi::OsStr; use std::fmt::Display; use std::fs::{File, OpenOptions}; @@ -127,6 +127,7 @@ pub fn network( Some(get_all_wasms_hashes(&working_dir, Some("vp_"))); genesis.parameters.tx_whitelist = Some(get_all_wasms_hashes(&working_dir, Some("tx_"))); + genesis.parameters.gas_table = Some(get_all_wasms_gas(&working_dir)); // Run the provided function on it let genesis = update_genesis(genesis); @@ -889,17 +890,18 @@ pub fn get_all_wasms_hashes( ) -> Vec { let checksums_path = working_dir.join("wasm/checksums.json"); let checksums_content = fs::read_to_string(checksums_path).unwrap(); - let checksums: HashMap = + let checksums: HashMap> = serde_json::from_str(&checksums_content).unwrap(); let filter_prefix = filter.unwrap_or_default(); + checksums - .values() - .filter_map(|wasm| { - if wasm.contains(filter_prefix) { + .iter() + .filter_map(|(name, info)| { + if name.contains(filter_prefix) { Some( - wasm.split('.').collect::>()[1] - .to_owned() - .to_lowercase(), + info.get("hash") + .expect("Missing hash in checksum") + .to_owned(), ) } else { None @@ -907,3 +909,20 @@ pub fn get_all_wasms_hashes( }) .collect() } + +pub fn get_all_wasms_gas(working_dir: &Path) -> BTreeMap { + let checksums_path = working_dir.join("wasm/checksums.json"); + let checksums: HashMap> = + serde_json::from_reader(fs::File::open(checksums_path).unwrap()) + .unwrap(); + + checksums + .values() + .map(|map| { + ( + map.get("hash").unwrap().to_lowercase(), + map.get("gas").unwrap().parse::().unwrap(), + ) + }) + .collect() +} diff --git a/tests/src/native_vp/mod.rs b/tests/src/native_vp/mod.rs index 6baf214a3b..c521b79a90 100644 --- a/tests/src/native_vp/mod.rs +++ b/tests/src/native_vp/mod.rs @@ -1,7 +1,9 @@ pub mod pos; +use std::cell::RefCell; use std::collections::BTreeSet; +use namada::ledger::gas::VpGasMeter; use namada::ledger::native_vp::{Ctx, NativeVp}; use namada::ledger::storage::mockdb::MockDB; use namada::ledger::storage::Sha256Hasher; @@ -48,7 +50,10 @@ impl TestNativeVpEnv { { let ctx = Ctx { iterators: Default::default(), - gas_meter: Default::default(), + gas_meter: RefCell::new(VpGasMeter::new( + self.tx_env.gas_meter.tx_gas_limit, + self.tx_env.gas_meter.get_current_transaction_gas(), + )), storage: &self.tx_env.wl_storage.storage, write_log: &self.tx_env.wl_storage.write_log, tx: &self.tx_env.tx, diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index b60e627804..7cea4d53b4 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -562,6 +562,7 @@ pub mod testing { use derivative::Derivative; use itertools::Either; + use namada::ledger::gas::TxGasMeter; use namada::proof_of_stake::epoched::DynEpochOffset; use namada::proof_of_stake::parameters::testing::arb_rate; use namada::proof_of_stake::parameters::PosParams; @@ -843,7 +844,7 @@ pub mod testing { let current_epoch = tx_host_env::with(|env| { // Reset the gas meter on each change, so that we never run // out in this test - env.gas_meter.reset(); + env.gas_meter = TxGasMeter::new(env.gas_meter.tx_gas_limit); env.wl_storage.storage.block.epoch }); println!("Current epoch {}", current_epoch); diff --git a/tests/src/vm_host_env/ibc.rs b/tests/src/vm_host_env/ibc.rs index 47ff3808fd..b2906fee60 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -157,7 +157,7 @@ pub fn validate_ibc_vp_from_tx<'a>( &tx_env.wl_storage.write_log, tx, &TxIndex(0), - VpGasMeter::new(0), + VpGasMeter::new(1_000_000, 0), &keys_changed, &verifiers, vp_wasm_cache, @@ -193,7 +193,7 @@ pub fn validate_token_vp_from_tx<'a>( &tx_env.wl_storage.write_log, tx, &TxIndex(0), - VpGasMeter::new(0), + VpGasMeter::new(1_000_000, 0), &keys_changed, &verifiers, vp_wasm_cache, diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index 1ad4b22599..6ba6ab2f39 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -18,6 +18,7 @@ pub mod vp; #[cfg(test)] mod tests { + use std::collections::BTreeMap; use std::panic; use itertools::Itertools; @@ -539,12 +540,20 @@ mod tests { assert!(!result); // evaluating the VP template which always returns `true` should pass + let gas_table: BTreeMap = BTreeMap::default(); let code = TestWasms::VpAlwaysTrue.read_bytes(); let code_hash = Hash::sha256(&code); + let code_len = (code.len() as u64).try_to_vec().unwrap(); vp_host_env::with(|env| { // store wasm codes let key = Key::wasm_code(&code_hash); + let len_key = Key::wasm_code_len(&code_hash); env.wl_storage.storage.write(&key, code.clone()).unwrap(); + env.wl_storage + .storage + .write(&len_key, code_len.clone()) + .unwrap(); + env.wl_storage.storage.write(&namada::ledger::parameters::storage::get_gas_table_storage_key(), gas_table.clone().try_to_vec().unwrap()).unwrap(); }); let input_data = vec![]; let result = vp::CTX.eval(code_hash, input_data).unwrap(); @@ -554,10 +563,17 @@ mod tests { // pass let code = TestWasms::VpAlwaysFalse.read_bytes(); let code_hash = Hash::sha256(&code); + let code_len = (code.len() as u64).try_to_vec().unwrap(); vp_host_env::with(|env| { // store wasm codes let key = Key::wasm_code(&code_hash); + let len_key = Key::wasm_code_len(&code_hash); env.wl_storage.storage.write(&key, code.clone()).unwrap(); + env.wl_storage + .storage + .write(&len_key, code_len.clone()) + .unwrap(); + env.wl_storage.storage.write(&namada::ledger::parameters::storage::get_gas_table_storage_key(), gas_table.clone().try_to_vec().unwrap()).unwrap(); }); let input_data = vec![]; let result = vp::CTX.eval(code_hash, input_data).unwrap(); diff --git a/tests/src/vm_host_env/tx.rs b/tests/src/vm_host_env/tx.rs index b67cac2416..de70c14b54 100644 --- a/tests/src/vm_host_env/tx.rs +++ b/tests/src/vm_host_env/tx.rs @@ -1,7 +1,7 @@ use std::borrow::Borrow; -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; -use namada::ledger::gas::BlockGasMeter; +use namada::ledger::gas::TxGasMeter; use namada::ledger::parameters::{self, EpochDuration}; use namada::ledger::storage::mockdb::MockDB; use namada::ledger::storage::testing::TestStorage; @@ -47,7 +47,7 @@ pub struct TestTxEnv { pub wl_storage: WlStorage, pub iterators: PrefixIterators<'static, MockDB>, pub verifiers: BTreeSet
, - pub gas_meter: BlockGasMeter, + pub gas_meter: TxGasMeter, pub tx_index: TxIndex, pub result_buffer: Option>, pub vp_wasm_cache: VpCache, @@ -71,7 +71,7 @@ impl Default for TestTxEnv { Self { wl_storage, iterators: PrefixIterators::default(), - gas_meter: BlockGasMeter::default(), + gas_meter: TxGasMeter::new(100_000_000), tx_index: TxIndex::default(), verifiers: BTreeSet::default(), result_buffer: None, @@ -168,7 +168,6 @@ impl TestTxEnv { .ok(); self.iterators = PrefixIterators::default(); self.verifiers = BTreeSet::default(); - self.gas_meter = BlockGasMeter::default(); } /// Credit tokens to the target account. @@ -213,6 +212,7 @@ impl TestTxEnv { &self.wl_storage.storage, &mut self.wl_storage.write_log, &mut self.gas_meter, + &BTreeMap::default(), &self.tx_index, &self.tx.code_or_hash, self.tx.data.as_ref().unwrap_or(&empty_data), diff --git a/tests/src/vm_host_env/vp.rs b/tests/src/vm_host_env/vp.rs index 8220ce4c64..db66247b62 100644 --- a/tests/src/vm_host_env/vp.rs +++ b/tests/src/vm_host_env/vp.rs @@ -73,7 +73,7 @@ impl Default for TestVpEnv { addr: address::testing::established_address_1(), wl_storage, iterators: PrefixIterators::default(), - gas_meter: VpGasMeter::default(), + gas_meter: VpGasMeter::new(10_000_000, 0), tx: Tx::new(vec![], None, chain_id, None), tx_index: TxIndex::default(), keys_changed: BTreeSet::default(), @@ -233,7 +233,7 @@ mod native_vp_host_env { fn eval( &self, _ctx: VpCtx<'static, Self::Db, Self::H, Self::Eval, Self::CA>, - _vp_code: Vec, + _vp_code_hash: Vec, _input_data: Vec, ) -> namada::types::internal::HostEnvResult { unimplemented!( diff --git a/vm_env/src/lib.rs b/vm_env/src/lib.rs index 9df39d6271..3b67a3c944 100644 --- a/vm_env/src/lib.rs +++ b/vm_env/src/lib.rs @@ -196,8 +196,8 @@ pub mod vp { pub fn namada_vp_log_string(str_ptr: u64, str_len: u64); pub fn namada_vp_eval( - vp_code_ptr: u64, - vp_code_len: u64, + vp_code_hash_ptr: u64, + vp_code_hash_len: u64, input_data_ptr: u64, input_data_len: u64, ) -> i64; diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index 57ac5a388f..b487fe8d54 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -272,11 +272,15 @@ impl<'view> VpEnv<'view> for Ctx { iter_prefix_pre_impl(prefix) } - fn eval(&self, vp_code: Hash, input_data: Vec) -> Result { + fn eval( + &self, + vp_code_hash: Hash, + input_data: Vec, + ) -> Result { let result = unsafe { namada_vp_eval( - vp_code.as_ptr() as _, - vp_code.len() as _, + vp_code_hash.as_ptr() as _, + vp_code_hash.len() as _, input_data.as_ptr() as _, input_data.len() as _, ) diff --git a/wasm/checksums.json b/wasm/checksums.json index 09154499f0..90e6f150b4 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,74 @@ { - "tx_bond.wasm": "tx_bond.1fac5f79b23232f25370160cc71f3ae9c6e6b35a929d79922826d0853288be6a.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.fc6b3d798cbda1833e1ca9be03fe0cab998048e0a829ae127ac1282b300e7f0e.wasm", - "tx_ibc.wasm": "tx_ibc.9a8291d8a249f19d17608d8c1ff5d13a8eea25b9cf1629299707553121ff78fc.wasm", - "tx_init_account.wasm": "tx_init_account.016e2ff541a09b2e4709d7d1b8d36d336412ccb57ab7390fdfb5726324dec8a5.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.32b350ea3377b679bb81035bb19be9f0dd7d678e4de4e8ddb24f8050c5564ba5.wasm", - "tx_init_validator.wasm": "tx_init_validator.c400691869282c6302a2772e11ab086798bbbdcc60d497c3dc279096bd2ad4bd.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.1981f185eae27de9135244b8238fd388206268e69dd511f246751aeaf8137b2a.wasm", - "tx_transfer.wasm": "tx_transfer.54cee6bef28d0ff33281788c124eaabf49486414a0a3eba1a66bfba33347c824.wasm", - "tx_unbond.wasm": "tx_unbond.486ed4ed2324c783a1715b94dc8dcda6fbb3ab7827d66945f8d9179de743fd79.wasm", - "tx_update_vp.wasm": "tx_update_vp.bfcf8672a62187858551fc914b675a9dbfd581d65daffbbbebcedcc28f7aa339.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.9a170b3265c9d6e58dc4f7889c7426698f3f72a3797bbe4c5cbb21f24dfffb70.wasm", - "tx_withdraw.wasm": "tx_withdraw.b77e8e47d787dfc7a311f4bb4c4772573064e5cdd0251af2c4bc6b807ef4f362.wasm", - "vp_implicit.wasm": "vp_implicit.835dc08ccebdb356d0d757cd9813a56a40a98bc688f12c6f8fed74310b2c6d13.wasm", - "vp_masp.wasm": "vp_masp.5cd63d431ffde6c33f0e1fb2a85b940721a924656998e187fb10dc2064676b1d.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.cf0b5c725807c847a271f72711aa4e1d059539cbc6e2d569d5e6e9fcc14253f1.wasm", - "vp_token.wasm": "vp_token.0b0152e2974bfa3f15d3fc30ee8d9687b5418055c67314cedbf50ce972b763c5.wasm", - "vp_user.wasm": "vp_user.0ed07e207a6e1f29fdf113a39f9032d0cbec04a6e92a040970d96bb9c3ddbed8.wasm", - "vp_validator.wasm": "vp_validator.bfa5e9a46f6d722a16f2ded4bb6c05dde382e6de6e882847714720804b2c7661.wasm" + "tx_bond.wasm": { + "gas": "160", + "hash": "2c7bee49a52513a403fb35694ebfd238b9d4b96aeaa6d43cc67f5a81d378e080" + }, + "tx_change_validator_commission.wasm": { + "gas": "220", + "hash": "149ad6f2ad711282b043f36727fe4af9c0d3a51887e683c2b3032fe4ce34281b" + }, + "tx_ibc.wasm": { + "gas": "1240", + "hash": "74c8041170ef6944c9e2fa3bf87cdccdd1c80108e6f7108277c521f25e422aba" + }, + "tx_init_account.wasm": { + "gas": "230", + "hash": "4d975d0f4c7959a4906c47f98d257d003a034074f6af52791e737bfb3b469ef2" + }, + "tx_init_proposal.wasm": { + "gas": "40", + "hash": "508b7acdb8e4031d339adc70cca707fc8318481a2f63178b1c865d28d153c82d" + }, + "tx_init_validator.wasm": { + "gas": "730", + "hash": "eb8a6c44d87c330f78cb552fd3a6778831964d76c2780f9bfe52fd52be27a71f" + }, + "tx_reveal_pk.wasm": { + "gas": "170", + "hash": "be878e819c5a50a057818ca36f9f803967f2750e9defeb1122c6a07292c4f10b" + }, + "tx_transfer.wasm": { + "gas": "110", + "hash": "22e7ee45debf9e826058090665373a77d6df6d4739116692c86a2f7e4d879703" + }, + "tx_unbond.wasm": { + "gas": "430", + "hash": "a1ccdf15828ed0de597528e0cedfcc1572f824310b60cbd564ec4ff13f8c16b1" + }, + "tx_update_vp.wasm": { + "gas": "140", + "hash": "000833cf971f5e9d5d7842ad474b6ae939e46d20b18924af116b54af7433cd64" + }, + "tx_vote_proposal.wasm": { + "gas": "120", + "hash": "636e971ecbd6ed708ee6e69d15020b2f08a6794d8b2ddabf7c64c7406be235ca" + }, + "tx_withdraw.wasm": { + "gas": "260", + "hash": "a88f0fe4fbcad6dc0ae27fba8b5fd6fb415e074252f778aa93306a58041e865f" + }, + "vp_implicit.wasm": { + "gas": "40", + "hash": "186f8912945cb9c5f36dbda8402bcef0073e714bf89e229a9e2ed26de8401cb9" + }, + "vp_masp.wasm": { + "gas": "8030", + "hash": "e50ca2a72df71bb71d0e405a003aa4f7210f3c336f7d3310089cf4e87ebbea30" + }, + "vp_testnet_faucet.wasm": { + "gas": "0", + "hash": "a26ae96c2700063c043fc180aaf6114a3f2621759b3518193f374ac5beebe6c4" + }, + "vp_token.wasm": { + "gas": "30", + "hash": "f5dd59c0c21e4e0bbfa22a87a556a0d0178b1ec33aa357641ab25215d2fd8813" + }, + "vp_user.wasm": { + "gas": "60", + "hash": "ea9589a9ef40fd0f94dfc0b081e37efc0d9e91b6f7ba34153d61f02f372c4650" + }, + "vp_validator.wasm": { + "gas": "50", + "hash": "98c2ded76ae76eb0ec25980a7fbd9c827a8a2e2186532f1c03edb4a469eff671" + } } \ No newline at end of file diff --git a/wasm/checksums.py b/wasm/checksums.py index 4b532b2ee8..aa5a94164d 100644 --- a/wasm/checksums.py +++ b/wasm/checksums.py @@ -3,21 +3,30 @@ import hashlib import os +gas = json.load(open("wasm/gas.json")) checksums = {} +updated_wasms = [] + for wasm in sorted(glob.glob("wasm/*.wasm")): basename = os.path.basename(wasm) - file_name = os.path.splitext(basename)[0] if wasm.count( - ".") == 1 else os.path.splitext(basename)[0].split('.')[0] - checksums["{}.wasm".format(file_name)] = "{}.{}.wasm".format( - file_name, hashlib.sha256(open(wasm, "rb").read()).hexdigest()) - os.rename(wasm, 'wasm/{}'.format(checksums["{}.wasm".format(file_name)])) + file_name = ( + os.path.splitext(basename)[0] + if wasm.count(".") == 1 + else os.path.splitext(basename)[0].split(".")[0] + ) + file_key = "{}.wasm".format(file_name) + file_hash = hashlib.sha256(open(wasm, "rb").read()).hexdigest() + checksums[file_key] = {"hash": file_hash, "gas": str(gas[file_key])} -updated_wasms = list(checksums.values()) + extended_file_name = "{}.{}.wasm".format( + file_name, hashlib.sha256(open(wasm, "rb").read()).hexdigest() + ) + os.rename(wasm, "wasm/{}".format(extended_file_name)) + updated_wasms.append(extended_file_name) for wasm in sorted(glob.glob("wasm/*.wasm")): basename = os.path.basename(wasm) - if not basename in updated_wasms: + if basename not in updated_wasms: os.remove(wasm) -json.dump(checksums, open("wasm/checksums.json", "w"), - indent=4, sort_keys=True) +json.dump(checksums, open("wasm/checksums.json", "w"), indent=4, sort_keys=True) diff --git a/wasm/gas.json b/wasm/gas.json new file mode 100644 index 0000000000..7ab4018276 --- /dev/null +++ b/wasm/gas.json @@ -0,0 +1,20 @@ +{ + "tx_bond.wasm": 160, + "tx_change_validator_commission.wasm": 220, + "tx_ibc.wasm": 1240, + "tx_init_account.wasm": 230, + "tx_init_proposal.wasm": 40, + "tx_init_validator.wasm": 730, + "tx_reveal_pk.wasm": 170, + "tx_transfer.wasm": 110, + "tx_unbond.wasm": 430, + "tx_update_vp.wasm": 140, + "tx_vote_proposal.wasm": 120, + "tx_withdraw.wasm": 260, + "vp_implicit.wasm": 40, + "vp_masp.wasm": 8030, + "vp_testnet_faucet.wasm": 0, + "vp_token.wasm": 30, + "vp_user.wasm": 60, + "vp_validator.wasm": 50 +} \ No newline at end of file