diff --git a/.changelog/unreleased/features/1327-variable-fees.md b/.changelog/unreleased/features/1327-variable-fees.md new file mode 100644 index 0000000000..8a2d0e8d2c --- /dev/null +++ b/.changelog/unreleased/features/1327-variable-fees.md @@ -0,0 +1,2 @@ +- Implemented the runtime gas and fee system. + ([\#1327](https://github.com/anoma/namada/pull/1327)) \ No newline at end of file diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index 15b871e61b..db0a714588 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -19,6 +19,7 @@ "e2e::ledger_tests::test_genesis_validators": 14, "e2e::ledger_tests::test_node_connectivity_and_consensus": 28, "e2e::ledger_tests::test_epoch_sleep": 12, + "e2e::ledger_tests::wrapper_disposable_signer": 28, "e2e::wallet_tests::wallet_address_cmds": 1, "e2e::wallet_tests::wallet_encrypted_key_cmds": 1, "e2e::wallet_tests::wallet_encrypted_key_cmds_env_var": 1, diff --git a/Cargo.lock b/Cargo.lock index 3a069b0956..8ef2826d9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,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" @@ -363,6 +369,17 @@ dependencies = [ "rustc_version 0.4.0", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "auto_impl" version = "1.1.0" @@ -949,6 +966,12 @@ dependencies = [ "thiserror", ] +[[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.79" @@ -1032,6 +1055,33 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a" +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde 1.0.163", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.3.0" @@ -1072,6 +1122,18 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "bitflags 1.2.1", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "textwrap", +] + [[package]] name = "clap" version = "4.3.5" @@ -1090,10 +1152,19 @@ dependencies = [ "anstream", "anstyle", "bitflags 1.2.1", - "clap_lex", + "clap_lex 0.5.0", "strsim", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clap_lex" version = "0.5.0" @@ -1358,6 +1429,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.25", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits 0.2.15", + "oorandom", + "plotters", + "rayon", + "regex", + "serde 1.0.163", + "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" @@ -2910,6 +3017,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.2.6" @@ -4042,7 +4158,7 @@ dependencies = [ "byte-unit", "byteorder", "bytes", - "clap", + "clap 4.3.5", "color-eyre", "config", "data-encoding", @@ -4114,6 +4230,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "namada_benchmarks" +version = "0.21.1" +dependencies = [ + "async-trait", + "borsh 0.9.4", + "criterion", + "ferveo-common", + "masp_primitives", + "masp_proofs", + "namada", + "namada_apps", + "namada_test_utils", + "prost", + "rand 0.8.5", + "rand_core 0.6.4", + "sha2 0.9.9", + "tempfile", + "tokio", +] + [[package]] name = "namada_core" version = "0.21.1" @@ -4250,7 +4387,7 @@ dependencies = [ "async-trait", "borsh 0.9.4", "chrono", - "clap", + "clap 4.3.5", "color-eyre", "concat-idents", "data-encoding", @@ -4619,6 +4756,12 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[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" @@ -4712,6 +4855,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "os_str_bytes" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" + [[package]] name = "output_vt100" version = "0.1.3" @@ -4986,6 +5135,34 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits 0.2.15", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -6605,6 +6782,12 @@ dependencies = [ "syn 1.0.109", ] +[[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.40" @@ -6724,6 +6907,16 @@ dependencies = [ "log", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde 1.0.163", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 2d50daf0b6..ea7071f111 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "apps", + "benches", "core", "ethereum_bridge", "proof_of_stake", diff --git a/Makefile b/Makefile index 0b6345a50d..90623e784f 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ crates += namada_vm_env crates += namada_vp_prelude build: - $(cargo) build $(jobs) + $(cargo) build $(jobs) --workspace --exclude namada_benchmarks build-test: $(cargo) +$(nightly) build --tests $(jobs) @@ -64,7 +64,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 && \ make -C $(wasms) check && \ make -C $(wasms_for_tests) check && \ $(foreach wasm,$(wasm_templates),$(check-wasm) && ) true @@ -149,7 +150,7 @@ test-integration: # Clear pre-built proofs, run integration tests and save the new proofs test-integration-save-proofs: # Clear old proofs first - rm --force test_fixtures/masp_proofs/*.bin || true + rm -f test_fixtures/masp_proofs/*.bin || true NAMADA_MASP_TEST_SEED=$(NAMADA_MASP_TEST_SEED) \ NAMADA_MASP_TEST_PROOFS=save \ TEST_FILTER=masp \ @@ -220,6 +221,9 @@ watch: clean: $(cargo) clean +bench: + $(cargo) bench + build-doc: $(cargo) doc --no-deps @@ -273,4 +277,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 +.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 bench diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index ae62212975..283ac65c69 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -238,11 +238,16 @@ pub async fn main() -> Result<()> { let tx = bridge_pool::build_bridge_pool_tx( &client, args.clone(), - signing_data.gas_payer.clone(), + signing_data.fee_payer.clone(), ) - .await?; - - signing::generate_test_vector(&client, &mut ctx.wallet, &tx).await; + .await?; + + signing::generate_test_vector( + &client, + &mut ctx.wallet, + &tx, + ) + .await; if args.tx.dump_tx { dump_tx(&args.tx, tx); diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 1ed2275f2c..7ffb67eb0c 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2377,6 +2377,7 @@ pub mod args { use namada::types::time::DateTimeUtc; use namada::types::token; use namada::types::token::NATIVE_MAX_DECIMAL_PLACES; + use namada::types::transaction::GasLimit; use super::context::*; use super::utils::*; @@ -2398,7 +2399,6 @@ pub mod args { pub const TX_TRANSFER_WASM: &str = "tx_transfer.wasm"; pub const TX_UNBOND_WASM: &str = "tx_unbond.wasm"; pub const TX_UNJAIL_VALIDATOR_WASM: &str = "tx_unjail_validator.wasm"; - pub const TX_UPDATE_VP_WASM: &str = "tx_update_vp.wasm"; pub const TX_VOTE_PROPOSAL: &str = "tx_vote_proposal.wasm"; pub const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm"; @@ -2440,9 +2440,11 @@ pub mod args { pub const DATA_PATH_OPT: ArgOpt = arg_opt("data-path"); pub const DATA_PATH: Arg = arg("data-path"); pub const DECRYPT: ArgFlag = flag("decrypt"); + pub const DISPOSABLE_SIGNING_KEY: ArgFlag = flag("disposable-gas-payer"); pub const DONT_ARCHIVE: ArgFlag = flag("dont-archive"); pub const DONT_PREFETCH_WASM: ArgFlag = flag("dont-prefetch-wasm"); pub const DRY_RUN_TX: ArgFlag = flag("dry-run"); + pub const DRY_RUN_WRAPPER_TX: ArgFlag = flag("dry-run-wrapper"); pub const DUMP_TX: ArgFlag = flag("dump-tx"); pub const EPOCH: ArgOpt = arg_opt("epoch"); pub const ERC20: Arg = arg("erc20"); @@ -2457,32 +2459,17 @@ pub mod args { ); pub const ETH_SYNC: ArgFlag = flag("sync"); pub const EXPIRATION_OPT: ArgOpt = arg_opt("expiration"); + pub const FEE_UNSHIELD_SPENDING_KEY: ArgOpt = + arg_opt("gas-spending-key"); + pub const FEE_AMOUNT_OPT: ArgOpt = + arg_opt("gas-price"); + pub const FEE_PAYER_OPT: ArgOpt = arg_opt("gas-payer"); pub const FORCE: ArgFlag = flag("force"); - pub const GAS_PAYER: ArgOpt = arg("gas-payer").opt(); - pub const GAS_AMOUNT: ArgDefault = arg_default( - "gas-amount", - DefaultFn(|| token::DenominatedAmount { - amount: token::Amount::default(), - denom: NATIVE_MAX_DECIMAL_PLACES.into(), - }), - ); - pub const GAS_LIMIT: ArgDefault = arg_default( - "gas-limit", - DefaultFn(|| token::DenominatedAmount { - amount: token::Amount::default(), - denom: NATIVE_MAX_DECIMAL_PLACES.into(), - }), - ); - pub const GAS_TOKEN: ArgDefaultFromCtx = + pub const GAS_LIMIT: ArgDefault = + arg_default("gas-limit", DefaultFn(|| GasLimit::from(20_000))); + pub const FEE_TOKEN: ArgDefaultFromCtx = arg_default_from_ctx("gas-token", DefaultFn(|| "NAM".parse().unwrap())); - pub const FEE_PAYER: Arg = arg("fee-payer"); - pub const FEE_AMOUNT: ArgDefault = arg_default( - "fee-amount", - DefaultFn(|| token::DenominatedAmount { - amount: token::Amount::default(), - denom: NATIVE_MAX_DECIMAL_PLACES.into(), - }), - ); + pub const BRIDGE_GAS_PAYER: Arg = arg("bridge-gas-payer"); pub const GENESIS_PATH: Arg = arg("genesis-path"); pub const GENESIS_VALIDATOR: ArgOpt = arg("genesis-validator").opt(); @@ -2806,8 +2793,12 @@ pub mod args { let recipient = ETH_ADDRESS.parse(matches); let sender = ADDRESS.parse(matches); let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); - let fee_amount = FEE_AMOUNT.parse(matches).amount; - let fee_payer = FEE_PAYER.parse(matches); + let fee_amount = FEE_AMOUNT_OPT + .parse(matches) + .map_or_else(token::Amount::default, |denom_amount| { + denom_amount.amount + }); + let fee_payer = BRIDGE_GAS_PAYER.parse(matches); let code_path = PathBuf::from(TX_BRIDGE_POOL_WASM); Self { tx, @@ -2843,15 +2834,14 @@ pub mod args { "The amount of tokens being sent across the bridge.", ), ) - .arg(FEE_AMOUNT.def().help( + .arg(FEE_AMOUNT_OPT.def().help( "The amount of NAM you wish to pay to have this transfer \ relayed to Ethereum.", )) - .arg( - FEE_PAYER.def().help( - "The Namada address of the account paying the fee.", - ), - ) + .arg(BRIDGE_GAS_PAYER.def().help( + "The Namada address of the account paying the fee for the \ + Ethereum transaction.", + )) } } @@ -4583,6 +4573,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> Tx { Tx:: { dry_run: self.dry_run, + dry_run_wrapper: self.dry_run_wrapper, dump_tx: self.dump_tx, output_folder: self.output_folder, force: self.force, @@ -4590,9 +4581,11 @@ pub mod args { ledger_address: (), initialized_account_alias: self.initialized_account_alias, wallet_alias_force: self.wallet_alias_force, - gas_payer: ctx.get_opt_cached(&self.gas_payer), - gas_amount: self.gas_amount, - gas_token: ctx.get(&self.gas_token), + fee_amount: self.fee_amount, + fee_token: ctx.get(&self.fee_token), + fee_unshield: self + .fee_unshield + .map(|ref fee_unshield| ctx.get_cached(fee_unshield)), gas_limit: self.gas_limit, signing_keys: self .signing_keys @@ -4607,12 +4600,16 @@ pub mod args { verification_key: self .verification_key .map(|public_key| ctx.get_cached(&public_key)), + disposable_signing_key: self.disposable_signing_key, tx_reveal_code_path: self.tx_reveal_code_path, password: self.password, expiration: self.expiration, chain_id: self .chain_id .or_else(|| Some(ctx.config.ledger.chain_id.clone())), + wrapper_fee_payer: self + .wrapper_fee_payer + .map(|x| ctx.get_cached(&x)), } } } @@ -4622,7 +4619,17 @@ pub mod args { app.arg( DRY_RUN_TX .def() - .help("Simulate the transaction application."), + .help("Simulate the transaction application.") + .conflicts_with(DRY_RUN_WRAPPER_TX.name), + ) + .arg( + DRY_RUN_WRAPPER_TX + .def() + .help( + "Simulate the complete transaction application. This \ + estimates the gas cost of the transaction.", + ) + .conflicts_with(DRY_RUN_TX.name), ) .arg(DUMP_TX.def().help("Dump transaction bytes to a file.")) .arg(FORCE.def().help( @@ -4645,28 +4652,38 @@ pub mod args { initialized, the alias will be the prefix of each new \ address joined with a number.", )) - .arg(WALLET_ALIAS_FORCE.def().help( - "Override the alias without confirmation if it already exists.", + .arg(FEE_AMOUNT_OPT.def().help( + "The amount being paid, per gas unit, for the inclusion of \ + this transaction", )) - .arg(GAS_PAYER.def().help( - "The implicit address of the gas payer. It defaults to the \ - address associated to the first key passed to --signing-keys.", + .arg(FEE_TOKEN.def().help("The token for paying the gas")) + .arg(FEE_UNSHIELD_SPENDING_KEY.def().help( + "The spending key to be used for fee unshielding. If none is \ + provided, fee will be payed from the unshielded balance only.", )) - .arg(GAS_AMOUNT.def().help( - "The amount being paid for the inclusion of this transaction", + .arg(GAS_LIMIT.def().help( + "The multiplier of the gas limit resolution defining the \ + maximum amount of gas needed to run transaction.", + )) + .arg(WALLET_ALIAS_FORCE.def().help( + "Override the alias without confirmation if it already exists.", )) - .arg(GAS_TOKEN.def().help("The token for paying the gas")) - .arg( - GAS_LIMIT.def().help( - "The maximum amount of gas needed to run transaction", - ), - ) .arg(EXPIRATION_OPT.def().help( "The expiration datetime of the transaction, after which the \ tx won't be accepted anymore. All of these examples are \ equivalent:\n2012-12-12T12:12:12Z\n2012-12-12 \ 12:12:12Z\n2012- 12-12T12: 12:12Z", )) + .arg( + DISPOSABLE_SIGNING_KEY + .def() + .help( + "Generates an ephemeral, disposable keypair to sign \ + the wrapper transaction. This keypair will be \ + immediately discarded after use.", + ) + .requires(FEE_UNSHIELD_SPENDING_KEY.name), + ) .arg( SIGNING_KEYS .def() @@ -4692,7 +4709,7 @@ pub mod args { SIGNING_KEYS.name, VERIFICATION_KEY.name, ]) - .requires(GAS_PAYER.name), + .requires(FEE_PAYER_OPT.name), ) .arg(OUTPUT_FOLDER_PATH.def().help( "The output folder path where the artifact will be stored.", @@ -4708,48 +4725,65 @@ pub mod args { .conflicts_with_all([SIGNING_KEYS.name, SIGNATURES.name]), ) .arg(CHAIN_ID_OPT.def().help("The chain ID.")) + .arg( + FEE_PAYER_OPT + .def() + .help( + "The implicit address of the gas payer. It defaults \ + to the address associated to the first key passed to \ + --signing-keys.", + ) + .conflicts_with(DISPOSABLE_SIGNING_KEY.name), + ) } fn parse(matches: &ArgMatches) -> Self { let dry_run = DRY_RUN_TX.parse(matches); + let dry_run_wrapper = DRY_RUN_WRAPPER_TX.parse(matches); let dump_tx = DUMP_TX.parse(matches); let force = FORCE.parse(matches); 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 = + FEE_AMOUNT_OPT.parse(matches).map(InputAmount::Unvalidated); + let fee_token = FEE_TOKEN.parse(matches); + let fee_unshield = FEE_UNSHIELD_SPENDING_KEY.parse(matches); + let _wallet_alias_force = WALLET_ALIAS_FORCE.parse(matches); + let gas_limit = GAS_LIMIT.parse(matches); let wallet_alias_force = WALLET_ALIAS_FORCE.parse(matches); - let gas_payer = GAS_PAYER.parse(matches); - let gas_amount = - InputAmount::Unvalidated(GAS_AMOUNT.parse(matches)); - let gas_token = GAS_TOKEN.parse(matches); - let gas_limit = GAS_LIMIT.parse(matches).amount.into(); let expiration = EXPIRATION_OPT.parse(matches); + let disposable_signing_key = DISPOSABLE_SIGNING_KEY.parse(matches); let signing_keys = SIGNING_KEYS.parse(matches); let signatures = SIGNATURES.parse(matches); let verification_key = VERIFICATION_KEY.parse(matches); let tx_reveal_code_path = PathBuf::from(TX_REVEAL_PK); let chain_id = CHAIN_ID_OPT.parse(matches); let password = None; + let wrapper_fee_payer = FEE_PAYER_OPT.parse(matches); let output_folder = OUTPUT_FOLDER_PATH.parse(matches); Self { dry_run, + dry_run_wrapper, dump_tx, force, broadcast_only, ledger_address, initialized_account_alias, wallet_alias_force, - gas_payer, - gas_amount, - gas_token, + fee_amount, + fee_token, + fee_unshield, gas_limit, expiration, + disposable_signing_key, signing_keys, signatures, verification_key, tx_reveal_code_path, password, chain_id, + wrapper_fee_payer, output_folder, } } diff --git a/apps/src/lib/cli/client.rs b/apps/src/lib/cli/client.rs index 8649049269..30e83bc81d 100644 --- a/apps/src/lib/cli/client.rs +++ b/apps/src/lib/cli/client.rs @@ -39,7 +39,8 @@ impl CliApi { .await .proceed_or_else(error)?; let args = args.to_sdk(&mut ctx); - let dry_run = args.tx.dry_run; + let dry_run = + args.tx.dry_run || args.tx.dry_run_wrapper; tx::submit_custom(&client, &mut ctx, args).await?; if !dry_run { crate::wallet::save(&ctx.wallet) @@ -102,7 +103,8 @@ impl CliApi { .await .proceed_or_else(error)?; let args = args.to_sdk(&mut ctx); - let dry_run = args.tx.dry_run; + let dry_run = + args.tx.dry_run || args.tx.dry_run_wrapper; tx::submit_init_account(&client, &mut ctx, args) .await?; if !dry_run { @@ -240,7 +242,7 @@ impl CliApi { let tx_args = args.tx.clone(); let default_signer = Some(args.sender.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = tx::aux_signing_data( &client, &mut ctx.wallet, &args.tx, @@ -249,12 +251,15 @@ impl CliApi { ) .await?; - let mut tx = bridge_pool::build_bridge_pool_tx( - &client, - args.clone(), - signing_data.gas_payer.clone(), - ) - .await?; + let (mut tx, _epoch) = + bridge_pool::build_bridge_pool_tx( + &client, + &mut ctx.wallet, + &mut ctx.shielded, + args.clone(), + signing_data.fee_payer.clone(), + ) + .await?; signing::generate_test_vector( &client, diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index ee3a4a8dfa..8a24af54b7 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -15,6 +15,8 @@ use namada::types::masp::*; use super::args; use crate::client::tx::CLIShieldedUtils; +#[cfg(any(test, feature = "dev"))] +use crate::config::genesis; use crate::config::genesis::genesis_config; use crate::config::global::GlobalConfig; use crate::config::{self, Config}; @@ -96,16 +98,35 @@ impl Context { 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); - let native_token = genesis.native_token; - let default_genesis = - genesis_config::open_genesis_config(genesis_file_path)?; - let wallet = crate::wallet::load_or_new_from_genesis( - &chain_dir, - default_genesis, - ); + // NOTE: workaround to make this function work both in integration tests + // and benchmarks + let (wallet, native_token) = if genesis_file_path.is_file() { + let genesis = + genesis_config::read_genesis_config(&genesis_file_path); + + let default_genesis = + genesis_config::open_genesis_config(genesis_file_path)?; + + ( + crate::wallet::load_or_new_from_genesis( + &chain_dir, + default_genesis, + ), + genesis.native_token, + ) // If the WASM dir specified, put it in the config + } else { + #[cfg(not(any(test, feature = "testing")))] + panic!("Missing genesis file"); + #[cfg(any(test, feature = "testing"))] + { + let default_genesis = genesis::genesis(1); + ( + crate::wallet::load_or_new(&genesis_file_path), + default_genesis.native_token, + ) + } + }; - // If the WASM dir specified, put it in the config match global_args.wasm_dir.as_ref() { Some(wasm_dir) => { config.wasm_dir = wasm_dir.clone(); diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index cf802f5706..dfcf3dd53a 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -1078,6 +1078,43 @@ pub async fn query_protocol_parameters< .expect("Parameter should be defined."); println!("{:4}Transactions whitelist: {:?}", "", tx_whitelist); + let key = param_storage::get_max_block_gas_key(); + let max_block_gas = query_storage_value::(client, &key) + .await + .expect("Parameter should be defined."); + println!("{:4}Max block gas: {:?}", "", max_block_gas); + + let key = param_storage::get_fee_unshielding_gas_limit_key(); + let fee_unshielding_gas_limit = query_storage_value::(client, &key) + .await + .expect("Parameter should be defined."); + println!( + "{:4}Fee unshielding gas limit: {:?}", + "", fee_unshielding_gas_limit + ); + + let key = param_storage::get_fee_unshielding_descriptions_limit_key(); + let fee_unshielding_descriptions_limit = + query_storage_value::(client, &key) + .await + .expect("Parameter should be defined."); + println!( + "{:4}Fee unshielding descriptions limit: {:?}", + "", fee_unshielding_descriptions_limit + ); + + let key = param_storage::get_gas_cost_key(); + let gas_cost_table = query_storage_value::< + C, + BTreeMap, + >(client, &key) + .await + .expect("Parameter should be defined."); + println!("{:4}Gas cost table:", ""); + for (token, gas_cost) in gas_cost_table { + println!("{:8}{}: {:?}", "", token, gas_cost); + } + println!("PoS parameters"); let pos_params = query_pos_parameters(client).await; println!( diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 93ebe5f114..90b88e3828 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -1,6 +1,10 @@ //! Helpers for making digital signatures using cryptographic keys from the //! wallet. +use std::borrow::Cow; + +use namada::core::types::token::Amount; +use namada::ledger::masp::{ShieldedContext, ShieldedUtils}; use namada::ledger::rpc::TxBroadcastData; use namada::ledger::signing::TxSigningKey; use namada::ledger::tx; @@ -34,13 +38,22 @@ where pub async fn tx_signers( client: &C, wallet: &mut Wallet, + _shielded: &mut ShieldedContext, args: &args::Tx, default: TxSigningKey, -) -> Result<(Option
, common::PublicKey), tx::Error> +) -> Result< + ( + Option
, + common::PublicKey, + Option, + ), + tx::Error, +> where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, U: WalletUtils, + V: ShieldedUtils, { namada::ledger::signing::tx_signers::(client, wallet, args, default) .await @@ -54,44 +67,55 @@ where /// hashes needed for monitoring the tx on chain. /// /// If it is a dry run, it is not put in a wrapper, but returned as is. +/// +/// If the tx fee is to be unshielded, it also returns the unshielding epoch. pub async fn sign_tx( wallet: &mut Wallet, tx: &mut Tx, args: &args::Tx, default: &common::PublicKey, + wrapper_signer: Option<&common::PublicKey>, ) -> Result<(), tx::Error> where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, U: WalletUtils, { - namada::ledger::signing::sign_tx(wallet, tx, args, default).await + namada::ledger::signing::sign_tx(wallet, tx, args, default, wrapper_signer) + .await } /// Create a wrapper tx from a normal tx. Get the hash of the /// wrapper and its payload which is needed for monitoring its -/// progress on chain. -pub async fn sign_wrapper( +/// progress on chain. Accepts an optional balance reflecting any modification +/// applied to it by the inner tx for a correct fee validation. +#[allow(clippy::too_many_arguments)] +pub async fn sign_wrapper<'key, C, U, V>( client: &C, wallet: &mut Wallet, + shielded: &mut ShieldedContext, args: &args::Tx, epoch: Epoch, tx: Tx, - keypair: &common::SecretKey, + keypair: Cow<'key, common::SecretKey>, + updated_balance: Option, #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> TxBroadcastData +) -> (TxBroadcastData, Option) where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, U: WalletUtils, + V: ShieldedUtils, { namada::ledger::signing::sign_wrapper( client, wallet, + shielded, args, epoch, tx, keypair, + updated_balance, #[cfg(not(feature = "mainnet"))] requires_pow, ) diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 125fe30588..4e80816ea9 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -13,12 +13,12 @@ use namada::core::ledger::governance::cli::offline::{ use namada::core::ledger::governance::cli::onchain::{ DefaultProposal, PgfFundingProposal, PgfStewardProposal, ProposalVote, }; -use namada::ledger::queries::Client; use namada::ledger::rpc::{TxBroadcastData, TxResponse}; use namada::ledger::wallet::{Wallet, WalletUtils}; use namada::ledger::{masp, pos, signing, tx}; use namada::proof_of_stake::parameters::PosParams; use namada::proto::Tx; +use namada::tendermint_rpc::HttpClient; use namada::types::address::{Address, ImplicitAddress}; use namada::types::dec::Dec; use namada::types::key::{self, *}; @@ -31,7 +31,41 @@ use crate::client::tx::tx::ProcessTxResponse; use crate::config::TendermintMode; use crate::facade::tendermint_rpc::endpoint::broadcast::tx_sync::Response; use crate::node::ledger::tendermint_node; -use crate::wallet::{gen_validator_keys, read_and_confirm_encryption_password}; +use crate::wallet::{ + gen_validator_keys, read_and_confirm_encryption_password, CliWalletUtils, +}; + +/// Wrapper around `signing::aux_signing_data` that stores the optional +/// disposable address to the wallet +pub async fn aux_signing_data( + client: &C, + wallet: &mut Wallet, + args: &args::Tx, + owner: &Option
, + default_signer: Option
, +) -> Result { + let signing_data = + signing::aux_signing_data(client, wallet, args, owner, default_signer) + .await?; + + if args.disposable_signing_key { + if !(args.dry_run || args.dry_run_wrapper) { + // Store the generated signing key to wallet in case of need + crate::wallet::save(wallet).map_err(|_| { + tx::Error::Other( + "Failed to save disposable address to wallet".to_string(), + ) + })?; + } else { + println!( + "Transaction dry run. The disposable address will not be \ + saved to wallet." + ) + } + } + + Ok(signing_data) +} // Build a transaction to reveal the signer of the given transaction. pub async fn submit_reveal_aux( @@ -52,21 +86,18 @@ pub async fn submit_reveal_aux( let public_key = key.ref_to(); if tx::is_reveal_pk_needed::(client, address, args.force).await? { - let signing_data = signing::aux_signing_data( - client, - &mut ctx.wallet, - &args, - &None, - None, - ) - .await?; + let signing_data = + aux_signing_data(client, &mut ctx.wallet, &args, &None, None) + .await?; - let mut tx = tx::build_reveal_pk::( + let (mut tx, _epoch) = tx::build_reveal_pk( client, + &mut ctx.wallet, + &mut ctx.shielded, &args, address, &public_key, - &signing_data.gas_payer, + &signing_data.fee_payer, ) .await?; @@ -81,17 +112,16 @@ pub async fn submit_reveal_aux( Ok(()) } -pub async fn submit_custom( +pub async fn submit_custom( client: &C, ctx: &mut Context, args: args::TxCustom, ) -> Result<(), tx::Error> where - C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { let default_signer = Some(args.owner.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -102,8 +132,14 @@ where submit_reveal_aux(client, ctx, args.tx.clone(), &args.owner).await?; - let mut tx = - tx::build_custom(client, args.clone(), &signing_data.gas_payer).await?; + let (mut tx, _epoch) = tx::build_custom( + client, + &mut ctx.wallet, + &mut ctx.shielded, + args.clone(), + &signing_data.fee_payer, + ) + .await?; signing::generate_test_vector(client, &mut ctx.wallet, &tx).await; @@ -127,7 +163,7 @@ where C::Error: std::fmt::Display, { let default_signer = Some(args.addr.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -136,9 +172,14 @@ where ) .await?; - let mut tx = - tx::build_update_account(client, args.clone(), &signing_data.gas_payer) - .await?; + let (mut tx, _epoch) = tx::build_update_account( + client, + &mut ctx.wallet, + &mut ctx.shielded, + args.clone(), + signing_data.fee_payer.clone(), + ) + .await?; signing::generate_test_vector(client, &mut ctx.wallet, &tx).await; @@ -152,28 +193,27 @@ where Ok(()) } -pub async fn submit_init_account( +pub async fn submit_init_account( client: &C, ctx: &mut Context, args: args::TxInitAccount, ) -> Result<(), tx::Error> where - C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let signing_data = signing::aux_signing_data( + let signing_data = + aux_signing_data(client, &mut ctx.wallet, &args.tx, &None, None) + .await?; + + let (mut tx, _epoch) = tx::build_init_account( client, &mut ctx.wallet, - &args.tx, - &None, - None, + &mut ctx.shielded, + args.clone(), + &signing_data.fee_payer, ) .await?; - let mut tx = - tx::build_init_account(client, args.clone(), &signing_data.gas_payer) - .await?; - signing::generate_test_vector(client, &mut ctx.wallet, &tx).await; if args.tx.dump_tx { @@ -186,7 +226,9 @@ where Ok(()) } -pub async fn submit_init_validator( +pub async fn submit_init_validator< + C: namada::ledger::queries::Client + Sync, +>( client: &C, mut ctx: Context, args::TxInitValidator { @@ -204,11 +246,7 @@ pub async fn submit_init_validator( unsafe_dont_encrypt, tx_code_path: _, }: args::TxInitValidator, -) -> Result<(), tx::Error> -where - C: namada::ledger::queries::Client + Sync, - C::Error: std::fmt::Display, -{ +) -> Result<(), tx::Error> { let tx_args = args::Tx { chain_id: tx_args .clone() @@ -336,12 +374,10 @@ where .expect("DKG sessions keys should have been created") .public(); - let validator_vp_code_hash = query_wasm_code_hash::( - client, - validator_vp_code_path.to_str().unwrap(), - ) - .await - .unwrap(); + let validator_vp_code_hash = + query_wasm_code_hash(client, validator_vp_code_path.to_str().unwrap()) + .await + .unwrap(); // Validate the commission rate data if commission_rate > Dec::one() || commission_rate < Dec::zero() { @@ -391,24 +427,22 @@ where tx.add_code_from_hash(tx_code_hash).add_data(data); - let signing_data = signing::aux_signing_data( - client, - &mut ctx.wallet, - &tx_args, - &None, - None, - ) - .await?; + let signing_data = + aux_signing_data(client, &mut ctx.wallet, &tx_args, &None, None) + .await?; tx::prepare_tx( client, + &mut ctx.wallet, + &mut ctx.shielded, &tx_args, &mut tx, - signing_data.gas_payer.clone(), + signing_data.fee_payer.clone(), + None, #[cfg(not(feature = "mainnet"))] false, ) - .await; + .await?; signing::generate_test_vector(client, &mut ctx.wallet, &tx).await; @@ -601,14 +635,14 @@ impl masp::ShieldedUtils for CLIShieldedUtils { } } -pub async fn submit_transfer( +pub async fn submit_transfer( client: &C, mut ctx: Context, args: args::TxTransfer, ) -> Result<(), tx::Error> { for _ in 0..2 { let default_signer = Some(args.source.effective_address()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -628,9 +662,10 @@ pub async fn submit_transfer( let arg = args.clone(); let (mut tx, tx_epoch) = tx::build_transfer( client, + &mut ctx.wallet, &mut ctx.shielded, arg, - &signing_data.gas_payer, + signing_data.fee_payer.clone(), ) .await?; signing::generate_test_vector(client, &mut ctx.wallet, &tx).await; @@ -670,17 +705,16 @@ pub async fn submit_transfer( Ok(()) } -pub async fn submit_ibc_transfer( +pub async fn submit_ibc_transfer( client: &C, mut ctx: Context, args: args::TxIbcTransfer, ) -> Result<(), tx::Error> where - C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { let default_signer = Some(args.source.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -691,9 +725,14 @@ where submit_reveal_aux(client, &mut ctx, args.tx.clone(), &args.source).await?; - let mut tx = - tx::build_ibc_transfer(client, args.clone(), &signing_data.gas_payer) - .await?; + let (mut tx, _epoch) = tx::build_ibc_transfer( + client, + &mut ctx.wallet, + &mut ctx.shielded, + args.clone(), + signing_data.fee_payer.clone(), + ) + .await?; signing::generate_test_vector(client, &mut ctx.wallet, &tx).await; if args.tx.dump_tx { @@ -706,24 +745,25 @@ where Ok(()) } -pub async fn submit_init_proposal( +pub async fn submit_init_proposal( client: &C, mut ctx: Context, args: args::InitProposal, ) -> Result<(), tx::Error> where - C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { let current_epoch = rpc::query_and_print_epoch(client).await; let governance_parameters = rpc::query_governance_parameters(client).await; - let (mut tx_builder, signing_data) = if args.is_offline { + let ((mut tx_builder, _fee_unshield_epoch), signing_data) = if args + .is_offline + { let proposal = namada::core::ledger::governance::cli::offline::OfflineProposal::try_from(args.proposal_data.as_ref()).map_err(|e| tx::Error::FailedGovernaneProposalDeserialize(e.to_string()))?.validate(current_epoch) .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; let default_signer = Some(proposal.author.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -754,7 +794,7 @@ where .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; let default_signer = Some(proposal.proposal.author.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -774,9 +814,11 @@ where ( tx::build_pgf_funding_proposal( client, + &mut ctx.wallet, + &mut ctx.shielded, args.clone(), proposal, - &signing_data.gas_payer, + signing_data.fee_payer.clone(), ) .await?, signing_data, @@ -799,7 +841,7 @@ where .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; let default_signer = Some(proposal.proposal.author.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -819,9 +861,11 @@ where ( tx::build_pgf_stewards_proposal( client, + &mut ctx.wallet, + &mut ctx.shielded, args.clone(), proposal, - &signing_data.gas_payer, + signing_data.fee_payer.clone(), ) .await?, signing_data, @@ -842,7 +886,7 @@ where .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; let default_signer = Some(proposal.proposal.author.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -862,9 +906,11 @@ where ( tx::build_default_proposal( client, + &mut ctx.wallet, + &mut ctx.shielded, args.clone(), proposal, - &signing_data.gas_payer, + signing_data.fee_payer.clone(), ) .await?, signing_data, @@ -886,7 +932,7 @@ where Ok(()) } -pub async fn submit_vote_proposal( +pub async fn submit_vote_proposal( client: &C, mut ctx: Context, args: args::VoteProposal, @@ -898,7 +944,7 @@ where let current_epoch = rpc::query_and_print_epoch(client).await; let default_signer = Some(args.voter.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -907,7 +953,7 @@ where ) .await?; - let mut tx_builder = if args.is_offline { + let (mut tx_builder, _fee_unshield_epoch) = if args.is_offline { let proposal_vote = ProposalVote::try_from(args.vote) .map_err(|_| tx::Error::InvalidProposalVote)?; @@ -950,9 +996,11 @@ where } else { tx::build_vote_proposal( client, + &mut ctx.wallet, + &mut ctx.shielded, args.clone(), current_epoch, - &signing_data.gas_payer, + signing_data.fee_payer.clone(), ) .await? }; @@ -993,7 +1041,7 @@ where }; let default_signer = Some(owner.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &tx_args, @@ -1057,13 +1105,12 @@ where Ok(()) } -pub async fn submit_reveal_pk( +pub async fn submit_reveal_pk( client: &C, ctx: &mut Context, args: args::RevealPk, ) -> Result<(), tx::Error> where - C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { submit_reveal_aux(client, ctx, args.tx, &(&args.public_key).into()).await?; @@ -1082,7 +1129,7 @@ where { let default_address = args.source.clone().unwrap_or(args.validator.clone()); let default_signer = Some(default_address.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -1093,9 +1140,14 @@ where submit_reveal_aux(client, ctx, args.tx.clone(), &default_address).await?; - let mut tx = - tx::build_bond::(client, args.clone(), &signing_data.gas_payer) - .await?; + let (mut tx, _fee_unshield_epoch) = tx::build_bond( + client, + &mut ctx.wallet, + &mut ctx.shielded, + args.clone(), + signing_data.fee_payer.clone(), + ) + .await?; signing::generate_test_vector(client, &mut ctx.wallet, &tx).await; if args.tx.dump_tx { @@ -1109,18 +1161,17 @@ where Ok(()) } -pub async fn submit_unbond( +pub async fn submit_unbond( client: &C, ctx: &mut Context, args: args::Unbond, ) -> Result<(), tx::Error> where - C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { let default_address = args.source.clone().unwrap_or(args.validator.clone()); let default_signer = Some(default_address.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -1129,13 +1180,15 @@ where ) .await?; - let (mut tx, latest_withdrawal_pre) = tx::build_unbond( - client, - &mut ctx.wallet, - args.clone(), - &signing_data.gas_payer, - ) - .await?; + let (mut tx, _fee_unshield_epoch, latest_withdrawal_pre) = + tx::build_unbond( + client, + &mut ctx.wallet, + &mut ctx.shielded, + args.clone(), + signing_data.fee_payer.clone(), + ) + .await?; signing::generate_test_vector(client, &mut ctx.wallet, &tx).await; if args.tx.dump_tx { @@ -1151,18 +1204,17 @@ where Ok(()) } -pub async fn submit_withdraw( +pub async fn submit_withdraw( client: &C, mut ctx: Context, args: args::Withdraw, ) -> Result<(), tx::Error> where - C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { let default_address = args.source.clone().unwrap_or(args.validator.clone()); let default_signer = Some(default_address.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -1171,9 +1223,14 @@ where ) .await?; - let mut tx = - tx::build_withdraw(client, args.clone(), &signing_data.gas_payer) - .await?; + let (mut tx, _fee_unshield_epoch) = tx::build_withdraw( + client, + &mut ctx.wallet, + &mut ctx.shielded, + args.clone(), + signing_data.fee_payer.clone(), + ) + .await?; signing::generate_test_vector(client, &mut ctx.wallet, &tx).await; if args.tx.dump_tx { @@ -1187,17 +1244,18 @@ where Ok(()) } -pub async fn submit_validator_commission_change( +pub async fn submit_validator_commission_change< + C: namada::ledger::queries::Client + Sync, +>( client: &C, mut ctx: Context, args: args::CommissionRateChange, ) -> Result<(), tx::Error> where - C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { let default_signer = Some(args.validator.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -1206,10 +1264,12 @@ where ) .await?; - let mut tx = tx::build_validator_commission_change( + let (mut tx, _fee_unshield_epoch) = tx::build_validator_commission_change( client, + &mut ctx.wallet, + &mut ctx.shielded, args.clone(), - &signing_data.gas_payer, + signing_data.fee_payer.clone(), ) .await?; signing::generate_test_vector(client, &mut ctx.wallet, &tx).await; @@ -1233,11 +1293,10 @@ pub async fn submit_unjail_validator< args: args::TxUnjailValidator, ) -> Result<(), tx::Error> where - C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { let default_signer = Some(args.validator.clone()); - let signing_data = signing::aux_signing_data( + let signing_data = aux_signing_data( client, &mut ctx.wallet, &args.tx, @@ -1246,10 +1305,12 @@ where ) .await?; - let mut tx = tx::build_unjail_validator( + let (mut tx, _fee_unshield_epoch) = tx::build_unjail_validator( client, + &mut ctx.wallet, + &mut ctx.shielded, args.clone(), - &signing_data.gas_payer, + signing_data.fee_payer.clone(), ) .await?; signing::generate_test_vector(client, &mut ctx.wallet, &tx).await; @@ -1278,14 +1339,10 @@ pub async fn save_initialized_accounts( /// the tx has been successfully included into the mempool of a validator /// /// In the case of errors in any of those stages, an error message is returned -pub async fn broadcast_tx( - rpc_cli: &C, +pub async fn broadcast_tx( + rpc_cli: &HttpClient, to_broadcast: &TxBroadcastData, -) -> Result -where - C: namada::ledger::queries::Client + Sync, - C::Error: std::fmt::Display, -{ +) -> Result { tx::broadcast_tx(rpc_cli, to_broadcast).await } @@ -1297,14 +1354,10 @@ where /// 3. The decrypted payload of the tx has been included on the blockchain. /// /// In the case of errors in any of those stages, an error message is returned -pub async fn submit_tx( - client: &C, +pub async fn submit_tx( + client: &HttpClient, to_broadcast: TxBroadcastData, -) -> Result -where - C: namada::ledger::queries::Client + Sync, - C::Error: std::fmt::Display, -{ +) -> Result { tx::submit_tx(client, to_broadcast).await } diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index e9abf6c26d..16fca7a834 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}; use borsh::{BorshDeserialize, BorshSerialize}; use derivative::Derivative; @@ -23,7 +23,7 @@ use namada::types::{storage, token}; /// Genesis configuration file format pub mod genesis_config { use std::array::TryFromSliceError; - use std::collections::{BTreeSet, HashMap}; + use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::convert::TryInto; use std::path::Path; use std::str::FromStr; @@ -260,6 +260,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. pub min_num_of_blocks: u64, /// Maximum duration per block (in seconds). @@ -281,9 +283,13 @@ pub mod genesis_config { pub pos_gain_p: Dec, /// PoS gain d pub pos_gain_d: Dec, - #[cfg(not(feature = "mainnet"))] - /// Fix wrapper tx fees - pub wrapper_tx_fees: Option, + /// Fee unshielding gas limit + pub fee_unshielding_gas_limit: u64, + /// Fee unshielding descriptions limit + pub fee_unshielding_descriptions_limit: u64, + /// Map of the cost per gas unit for every token allowed for fee + /// payment + pub gas_cost: BTreeMap, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -599,6 +605,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, @@ -613,6 +620,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, @@ -624,8 +632,10 @@ pub mod genesis_config { pos_gain_d: parameters.pos_gain_d, staked_ratio: Dec::zero(), pos_inflation_amount: token::Amount::zero(), - #[cfg(not(feature = "mainnet"))] - wrapper_tx_fees: parameters.wrapper_tx_fees, + gas_cost: parameters.gas_cost, + fee_unshielding_gas_limit: parameters.fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit: parameters + .fee_unshielding_descriptions_limit, }; let GovernanceParamsConfig { @@ -868,6 +878,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 @@ -892,9 +904,12 @@ pub struct Parameters { pub staked_ratio: Dec, /// PoS inflation amount from the last epoch (read + write for every epoch) pub pos_inflation_amount: token::Amount, - /// Fixed Wrapper tx fees - #[cfg(not(feature = "mainnet"))] - pub wrapper_tx_fees: Option, + /// Fee unshielding gas limit + pub fee_unshielding_gas_limit: u64, + /// Fee unshielding descriptions limit + pub fee_unshielding_descriptions_limit: u64, + /// Map of the cost per gas unit for every token allowed for fee payment + pub gas_cost: BTreeMap, } #[cfg(not(any(test, feature = "dev")))] @@ -1001,6 +1016,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: 20_000_000, vp_whitelist: vec![], tx_whitelist: vec![], implicit_vp_code_path: vp_implicit_path.into(), @@ -1012,8 +1028,9 @@ pub fn genesis(num_validators: u64) -> Genesis { pos_gain_d: Dec::new(1, 1).expect("This can't fail"), staked_ratio: Dec::zero(), pos_inflation_amount: token::Amount::zero(), - #[cfg(not(feature = "mainnet"))] - wrapper_tx_fees: Some(token::Amount::native_whole(0)), + gas_cost: [(nam(), token::Amount::from(1))].into_iter().collect(), + fee_unshielding_gas_limit: 20_000, + fee_unshielding_descriptions_limit: 15, }; let albert = EstablishedAccount { address: wallet::defaults::albert_address(), diff --git a/apps/src/lib/node/ledger/shell/block_alloc.rs b/apps/src/lib/node/ledger/shell/block_alloc.rs new file mode 100644 index 0000000000..03273194c6 --- /dev/null +++ b/apps/src/lib/node/ledger/shell/block_alloc.rs @@ -0,0 +1,581 @@ +//! Primitives that facilitate keeping track of the number +//! of bytes utilized by some Tendermint consensus round's proposal. +//! +//! This is important, because Tendermint places an upper bound +//! on the size of a block, rejecting blocks whose size exceeds +//! the limit stated in [`RequestPrepareProposal`]. +//! +//! The code in this module doesn't perform any deserializing to +//! verify if we are, in fact, allocating space for the correct +//! kind of tx for the current [`BlockAllocator`] state. It +//! is up to `PrepareProposal` to dispatch the correct kind of tx +//! into the current state of the allocator. +//! +//! # How space is allocated +//! +//! In the current implementation, we allocate space for transactions +//! in the following order of preference: +//! +//! - First, we allot space for DKG encrypted txs. We allow DKG encrypted txs to +//! take up at most 1/3 of the total block space. +//! - Next, we allot space for DKG decrypted txs. Decrypted txs take up as much +//! space as needed. We will see, shortly, why in practice this is fine. +//! - Finally, we allot space for protocol txs. Protocol txs get half of the +//! remaining block space allotted to them. +//! +//! Since at some fixed height `H` decrypted txs only take up as +//! much space as the encrypted txs from height `H - 1`, and we +//! restrict the space of encrypted txs to at most 1/3 of the +//! total block space, we roughly divide the Tendermint block +//! space in 3, for each major type of tx. +//! +//! # How gas is allocated +//! +//! Gas is only relevant to DKG encrypted txs. Every encrypted tx defines its +//! gas limit. We take this entire gas limit as the amount of gas requested by +//! the tx. + +pub mod states; + +// TODO: what if a tx has a size greater than the threshold for +// its bin? how do we handle this? if we keep it in the mempool +// forever, it'll be a DoS vec, as we can make nodes run out of +// memory! maybe we should allow block decisions for txs that are +// too big to fit in their respective bin? in these special block +// decisions, we would only decide proposals with "large" txs?? +// +// MAYBE: in the state machine impl, reset to beginning state, and +// and alloc space for large tx right at the start. the problem with +// this is that then we may not have enough space for decrypted txs + +// TODO: panic if we don't have enough space reserved for a +// decrypted tx; in theory, we should always have enough space +// reserved for decrypted txs, given the invariants of the state +// machine + +use std::marker::PhantomData; + +use namada::core::ledger::storage::{self, WlStorage}; +use namada::proof_of_stake::pos_queries::PosQueries; + +#[allow(unused_imports)] +use crate::facade::tendermint_proto::abci::RequestPrepareProposal; + +/// Block allocation failure status responses. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum AllocFailure { + /// The transaction can only be included in an upcoming block. + /// + /// We return the resource left in the tx bin for logging purposes. + Rejected { bin_resource_left: u64 }, + /// The transaction would overflow the allotted bin resource, + /// therefore it needs to be handled separately. + /// + /// We return the resource allotted to the tx bin for logging purposes. + OverflowsBin { bin_resource: u64 }, +} + +/// The block resources that need to be allocated +pub struct BlockResources<'tx> { + tx: &'tx [u8], + gas: u64, +} + +impl<'tx> BlockResources<'tx> { + /// Generates a new block resource instance + pub fn new(tx: &'tx [u8], gas: u64) -> Self { + Self { tx, gas } + } +} + +/// Marker type for the block space +#[derive(Debug, Default, Clone, Copy)] +pub struct BlockSpace; +/// Marker type for the block gas +#[derive(Debug, Default, Clone, Copy)] +pub struct BlockGas; + +pub trait Resource { + type Input<'r>; + + fn usage_of(input: Self::Input<'_>) -> u64; +} + +impl Resource for BlockSpace { + type Input<'r> = &'r [u8]; + + fn usage_of(input: Self::Input<'_>) -> u64 { + input.len() as u64 + } +} + +impl Resource for BlockGas { + type Input<'r> = u64; + + fn usage_of(input: Self::Input<'_>) -> u64 { + input + } +} + +/// Allotted resources for a batch of transactions in some proposed block. +/// +/// We keep track of the current space utilized by: +/// +/// - DKG encrypted transactions. +/// - DKG decrypted transactions. +/// - Protocol transactions. +/// +/// Gas usage of DKG encrypted txs is also tracked. +#[derive(Debug, Default)] +pub struct BlockAllocator { + /// The current state of the [`BlockAllocator`] state machine. + _state: PhantomData<*const State>, + /// The total space Tendermint has allotted to the + /// application for the current block height. + block: TxBin, + /// The current space utilized by protocol transactions. + protocol_txs: TxBin, + /// The current space and gas utilized by DKG encrypted transactions. + encrypted_txs: EncryptedTxsBins, + /// The current space utilized by DKG decrypted transactions. + decrypted_txs: TxBin, +} + +impl From<&WlStorage> + for BlockAllocator> +where + D: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, + H: 'static + storage::StorageHasher, +{ + #[inline] + fn from(storage: &WlStorage) -> Self { + Self::init( + storage.pos_queries().get_max_proposal_bytes().get(), + namada::core::ledger::gas::get_max_block_gas(storage).unwrap(), + ) + } +} + +impl BlockAllocator> { + /// Construct a new [`BlockAllocator`], with an upper bound + /// on the max size of all txs in a block defined by Tendermint and an upper + /// bound on the max gas in a block. + #[inline] + pub fn init( + tendermint_max_block_space_in_bytes: u64, + max_block_gas: u64, + ) -> Self { + let max = tendermint_max_block_space_in_bytes; + Self { + _state: PhantomData, + block: TxBin::init(max), + protocol_txs: TxBin::default(), + encrypted_txs: EncryptedTxsBins::new(max, max_block_gas), + decrypted_txs: TxBin::default(), + } + } +} + +impl BlockAllocator { + /// Return the amount of space left to initialize in all + /// [`TxBin`] instances. + /// + /// This is calculated based on the difference between the Tendermint + /// block space for a given round and the sum of the allotted space + /// to each [`TxBin`] instance in a [`BlockAllocator`]. + #[inline] + fn uninitialized_space_in_bytes(&self) -> u64 { + let total_bin_space = self.protocol_txs.allotted + + self.encrypted_txs.space.allotted + + self.decrypted_txs.allotted; + self.block.allotted - total_bin_space + } +} + +/// Allotted resource for a batch of transactions of the same kind in some +/// proposed block. At the moment this is used to track two resources of the +/// block: space and gas. Space is measured in bytes while gas in gas units. +#[derive(Debug, Copy, Clone, Default)] +pub struct TxBin { + /// The current resource utilization of the batch of transactions. + occupied: u64, + /// The maximum resource amount the batch of transactions may occupy. + allotted: u64, + /// The resource that this bin is tracking + _resource: PhantomData, +} + +impl TxBin { + /// Return the amount of resource left in this [`TxBin`]. + #[inline] + pub fn resource_left(&self) -> u64 { + self.allotted - self.occupied + } + + /// Construct a new [`TxBin`], with a capacity of `max_capacity`. + #[inline] + pub fn init(max_capacity: u64) -> Self { + Self { + allotted: max_capacity, + occupied: 0, + _resource: PhantomData, + } + } + + /// Shrink the allotted resource of this [`TxBin`] to whatever + /// amount is currently being utilized. + #[inline] + pub fn shrink_to_fit(&mut self) { + self.allotted = self.occupied; + } + + /// Try to dump a new transaction into this [`TxBin`]. + /// + /// Signal the caller if the tx requires more resource than its max + /// allotted. + pub fn try_dump( + &mut self, + resource: R::Input<'_>, + ) -> Result<(), AllocFailure> { + let resource = R::usage_of(resource); + if resource > self.allotted { + let bin_size = self.allotted; + return Err(AllocFailure::OverflowsBin { + bin_resource: bin_size, + }); + } + let occupied = self.occupied + resource; + if occupied <= self.allotted { + self.occupied = occupied; + Ok(()) + } else { + let bin_resource_left = self.resource_left(); + Err(AllocFailure::Rejected { bin_resource_left }) + } + } +} + +#[derive(Debug, Default)] +pub struct EncryptedTxsBins { + space: TxBin, + gas: TxBin, +} + +impl EncryptedTxsBins { + pub fn new(max_bytes: u64, max_gas: u64) -> Self { + let allotted_space_in_bytes = threshold::ONE_THIRD.over(max_bytes); + Self { + space: TxBin::init(allotted_space_in_bytes), + gas: TxBin::init(max_gas), + } + } + + pub fn try_dump(&mut self, tx: &[u8], gas: u64) -> Result<(), String> { + self.space.try_dump(tx).map_err(|e| match e { + AllocFailure::Rejected { .. } => { + "No more space left in the block for wrapper txs".to_string() + } + AllocFailure::OverflowsBin { .. } => "The given wrapper tx is \ + larger than 1/3 of the \ + available block space" + .to_string(), + })?; + self.gas.try_dump(gas).map_err(|e| match e { + AllocFailure::Rejected { .. } => { + "No more gas left in the block for wrapper txs".to_string() + } + AllocFailure::OverflowsBin { .. } => { + "The given wrapper tx requires more gas than available to the \ + entire block" + .to_string() + } + }) + } +} + +pub mod threshold { + //! Transaction allotment thresholds. + + use num_rational::Ratio; + + /// Threshold over a portion of block space. + #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] + pub struct Threshold(Ratio); + + impl Threshold { + /// Return a new [`Threshold`]. + const fn new(numer: u64, denom: u64) -> Self { + // constrain ratio to a max of 1 + let numer = if numer > denom { denom } else { numer }; + Self(Ratio::new_raw(numer, denom)) + } + + /// Return a [`Threshold`] over some free space. + pub fn over(self, free_space_in_bytes: u64) -> u64 { + (self.0 * free_space_in_bytes).to_integer() + } + } + + /// Divide free space in three. + pub const ONE_THIRD: Threshold = Threshold::new(1, 3); +} + +#[cfg(test)] +mod tests { + use std::cell::RefCell; + + use assert_matches::assert_matches; + use proptest::prelude::*; + + use super::states::{ + BuildingEncryptedTxBatch, NextState, TryAlloc, WithEncryptedTxs, + WithoutEncryptedTxs, + }; + use super::*; + use crate::node::ledger::shims::abcipp_shim_types::shim::TxBytes; + + /// Convenience alias for a block space allocator at a state with encrypted + /// txs. + type BsaWrapperTxs = + BlockAllocator>; + + /// Convenience alias for a block space allocator at a state without + /// encrypted txs. + type BsaNoWrapperTxs = + BlockAllocator>; + + /// Proptest generated txs. + #[derive(Debug)] + struct PropTx { + tendermint_max_block_space_in_bytes: u64, + max_block_gas: u64, + protocol_txs: Vec, + encrypted_txs: Vec, + decrypted_txs: Vec, + } + + /// Check that at most 1/3 of the block space is + /// reserved for each kind of tx type, in the + /// allocator's common path. + #[test] + fn test_txs_are_evenly_split_across_block() { + const BLOCK_SIZE: u64 = 60; + const BLOCK_GAS: u64 = 1_000; + + // reserve block space for encrypted txs + let mut alloc = BsaWrapperTxs::init(BLOCK_SIZE, BLOCK_GAS); + + // allocate ~1/3 of the block space to encrypted txs + assert!(alloc.try_alloc(BlockResources::new(&[0; 18], 0)).is_ok()); + + // reserve block space for decrypted txs + let mut alloc = alloc.next_state(); + + // the space we allotted to encrypted txs was shrunk to + // the total space we actually used up + assert_eq!(alloc.encrypted_txs.space.allotted, 18); + + // check that the allotted space for decrypted txs is correct + assert_eq!(alloc.decrypted_txs.allotted, BLOCK_SIZE - 18); + + // add about ~1/3 worth of decrypted txs + assert!(alloc.try_alloc(&[0; 17]).is_ok()); + + // reserve block space for protocol txs + let mut alloc = alloc.next_state(); + + // check that space was shrunk + assert_eq!(alloc.protocol_txs.allotted, BLOCK_SIZE - (18 + 17)); + + // add protocol txs to the block space allocator + assert!(alloc.try_alloc(&[0; 25]).is_ok()); + + // the block should be full at this point + assert_matches!( + alloc.try_alloc(&[0; 1]), + Err(AllocFailure::Rejected { .. }) + ); + } + + // Test that we cannot include encrypted txs in a block + // when the state invariants banish them from inclusion. + #[test] + fn test_encrypted_txs_are_rejected() { + let mut alloc = BsaNoWrapperTxs::init(1234, 1_000); + assert_matches!( + alloc.try_alloc(BlockResources::new(&[0; 1], 0)), + Err(AllocFailure::Rejected { .. }) + ); + } + + proptest! { + /// Check if we reject a tx when its respective bin + /// capacity has been reached on a [`BlockAllocator`]. + #[test] + fn test_reject_tx_on_bin_cap_reached(max in prop::num::u64::ANY) { + proptest_reject_tx_on_bin_cap_reached(max) + } + + /// Check if the initial bin capcity of the [`BlockAllocator`] + /// is correct. + #[test] + fn test_initial_bin_capacity(max in prop::num::u64::ANY) { + proptest_initial_bin_capacity(max) + } + + /// Test that dumping txs whose total combined size + /// is less than the bin cap does not fill up the bin. + #[test] + fn test_tx_dump_doesnt_fill_up_bin(args in arb_transactions()) { + proptest_tx_dump_doesnt_fill_up_bin(args) + } + } + + /// Implementation of [`test_reject_tx_on_bin_cap_reached`]. + fn proptest_reject_tx_on_bin_cap_reached( + tendermint_max_block_space_in_bytes: u64, + ) { + let mut bins = + BsaWrapperTxs::init(tendermint_max_block_space_in_bytes, 1_000); + + // fill the entire bin of encrypted txs + bins.encrypted_txs.space.occupied = bins.encrypted_txs.space.allotted; + + // make sure we can't dump any new encrypted txs in the bin + assert_matches!( + bins.try_alloc(BlockResources::new(b"arbitrary tx bytes", 0)), + Err(AllocFailure::Rejected { .. }) + ); + + // Reset space bin + bins.encrypted_txs.space.occupied = 0; + // Fill the entire gas bin + bins.encrypted_txs.gas.occupied = bins.encrypted_txs.gas.allotted; + + // Make sure we can't dump any new wncrypted txs in the bin + assert_matches!( + bins.try_alloc(BlockResources::new(b"arbitrary tx bytes", 1)), + Err(AllocFailure::Rejected { .. }) + ) + } + + /// Implementation of [`test_initial_bin_capacity`]. + fn proptest_initial_bin_capacity(tendermint_max_block_space_in_bytes: u64) { + let bins = + BsaWrapperTxs::init(tendermint_max_block_space_in_bytes, 1_000); + let expected = tendermint_max_block_space_in_bytes + - threshold::ONE_THIRD.over(tendermint_max_block_space_in_bytes); + assert_eq!(expected, bins.uninitialized_space_in_bytes()); + } + + /// Implementation of [`test_tx_dump_doesnt_fill_up_bin`]. + fn proptest_tx_dump_doesnt_fill_up_bin(args: PropTx) { + let PropTx { + tendermint_max_block_space_in_bytes, + max_block_gas, + protocol_txs, + encrypted_txs, + decrypted_txs, + } = args; + + // produce new txs until the moment we would have + // filled up the bins. + // + // iterate over the produced txs to make sure we can keep + // dumping new txs without filling up the bins + + let bins = RefCell::new(BsaWrapperTxs::init( + tendermint_max_block_space_in_bytes, + max_block_gas, + )); + let encrypted_txs = encrypted_txs.into_iter().take_while(|tx| { + let bin = bins.borrow().encrypted_txs.space; + let new_size = bin.occupied + tx.len() as u64; + new_size < bin.allotted + }); + for tx in encrypted_txs { + assert!( + bins.borrow_mut() + .try_alloc(BlockResources::new(&tx, 0)) + .is_ok() + ); + } + + let bins = RefCell::new(bins.into_inner().next_state()); + let decrypted_txs = decrypted_txs.into_iter().take_while(|tx| { + let bin = bins.borrow().decrypted_txs; + let new_size = bin.occupied + tx.len() as u64; + new_size < bin.allotted + }); + for tx in decrypted_txs { + assert!(bins.borrow_mut().try_alloc(&tx).is_ok()); + } + + let bins = RefCell::new(bins.into_inner().next_state()); + let protocol_txs = protocol_txs.into_iter().take_while(|tx| { + let bin = bins.borrow().protocol_txs; + let new_size = bin.occupied + tx.len() as u64; + new_size < bin.allotted + }); + for tx in protocol_txs { + assert!(bins.borrow_mut().try_alloc(&tx).is_ok()); + } + } + + prop_compose! { + /// Generate arbitrarily sized txs of different kinds. + fn arb_transactions() + // create base strategies + ( + (tendermint_max_block_space_in_bytes, max_block_gas, protocol_tx_max_bin_size, encrypted_tx_max_bin_size, + decrypted_tx_max_bin_size) in arb_max_bin_sizes(), + ) + // compose strategies + ( + tendermint_max_block_space_in_bytes in Just(tendermint_max_block_space_in_bytes), + max_block_gas in Just(max_block_gas), + protocol_txs in arb_tx_list(protocol_tx_max_bin_size), + encrypted_txs in arb_tx_list(encrypted_tx_max_bin_size), + decrypted_txs in arb_tx_list(decrypted_tx_max_bin_size), + ) + -> PropTx { + PropTx { + tendermint_max_block_space_in_bytes, + max_block_gas, + protocol_txs, + encrypted_txs, + decrypted_txs, + } + } + } + + /// Return random bin sizes for a [`BlockAllocator`]. + fn arb_max_bin_sizes() + -> impl Strategy { + const MAX_BLOCK_SIZE_BYTES: u64 = 1000; + (1..=MAX_BLOCK_SIZE_BYTES).prop_map( + |tendermint_max_block_space_in_bytes| { + ( + tendermint_max_block_space_in_bytes, + tendermint_max_block_space_in_bytes, + threshold::ONE_THIRD + .over(tendermint_max_block_space_in_bytes) + as usize, + threshold::ONE_THIRD + .over(tendermint_max_block_space_in_bytes) + as usize, + threshold::ONE_THIRD + .over(tendermint_max_block_space_in_bytes) + as usize, + ) + }, + ) + } + + /// Return a list of txs. + fn arb_tx_list(max_bin_size: usize) -> impl Strategy>> { + const MAX_TX_NUM: usize = 64; + let tx = prop::collection::vec(prop::num::u8::ANY, 0..=max_bin_size); + prop::collection::vec(tx, 0..=MAX_TX_NUM) + } +} diff --git a/apps/src/lib/node/ledger/shell/block_alloc/states.rs b/apps/src/lib/node/ledger/shell/block_alloc/states.rs new file mode 100644 index 0000000000..c5e0343ccf --- /dev/null +++ b/apps/src/lib/node/ledger/shell/block_alloc/states.rs @@ -0,0 +1,123 @@ +//! All the states of the [`BlockAllocator`] state machine, +//! over the extent of a Tendermint consensus round +//! block proposal. +//! +//! # States +//! +//! The state machine moves through the following state DAG: +//! +//! 1. [`BuildingEncryptedTxBatch`] - the initial state. In +//! this state, we populate a block with DKG encrypted txs. +//! This state supports two modes of operation, which you can +//! think of as two sub-states: +//! * [`WithoutEncryptedTxs`] - When this mode is active, no encrypted txs are +//! included in a block proposal. +//! * [`WithEncryptedTxs`] - When this mode is active, we are able to include +//! encrypted txs in a block proposal. +//! 2. [`BuildingDecryptedTxBatch`] - the second state. In +//! this state, we populate a block with DKG decrypted txs. +//! 3. [`BuildingProtocolTxBatch`] - the third state. In +//! this state, we populate a block with protocol txs. + +mod decrypted_txs; +mod encrypted_txs; +mod protocol_txs; + +use super::{AllocFailure, BlockAllocator}; + +/// Convenience wrapper for a [`BlockAllocator`] state that allocates +/// encrypted transactions. +#[allow(dead_code)] +pub enum EncryptedTxBatchAllocator { + WithEncryptedTxs( + BlockAllocator>, + ), + WithoutEncryptedTxs( + BlockAllocator>, + ), +} + +/// 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::block_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::block_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::block_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::block_alloc::states`]. +pub enum WithEncryptedTxs {} + +/// Prohibit block proposals from including encrypted txs. +/// +/// For more info, read the module docs of +/// [`crate::node::ledger::shell::block_alloc::states`]. +pub enum WithoutEncryptedTxs {} + +/// Try to allocate a new transaction on a [`BlockAllocator`] state. +/// +/// For more info, read the module docs of +/// [`crate::node::ledger::shell::block_alloc::states`]. +pub trait TryAlloc { + type Resources<'tx>; + + /// Try to allocate resources for a new transaction. + fn try_alloc( + &mut self, + resource_required: Self::Resources<'_>, + ) -> Result<(), AllocFailure>; +} + +/// Represents a state transition in the [`BlockAllocator`] state machine. +/// +/// This trait should not be used directly. Instead, consider using +/// [`NextState`]. +/// +/// For more info, read the module docs of +/// [`crate::node::ledger::shell::block_alloc::states`]. +pub trait NextStateImpl { + /// The next state in the [`BlockAllocator`] state machine. + type Next; + + /// Transition to the next state in the [`BlockAllocator`] state + /// machine. + fn next_state_impl(self) -> Self::Next; +} + +/// 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::block_alloc::states`]. +pub trait NextState: NextStateImpl { + /// Transition to the next state in the [`BlockAllocator`] state, + /// using a null transiiton function. + #[inline] + fn next_state(self) -> Self::Next + where + Self: Sized, + { + self.next_state_impl() + } +} + +impl NextState for S where S: NextStateImpl {} diff --git a/apps/src/lib/node/ledger/shell/block_alloc/states/decrypted_txs.rs b/apps/src/lib/node/ledger/shell/block_alloc/states/decrypted_txs.rs new file mode 100644 index 0000000000..7d7cc51d90 --- /dev/null +++ b/apps/src/lib/node/ledger/shell/block_alloc/states/decrypted_txs.rs @@ -0,0 +1,48 @@ +use std::marker::PhantomData; + +use super::super::{AllocFailure, BlockAllocator, TxBin}; +use super::{ + BuildingDecryptedTxBatch, BuildingProtocolTxBatch, NextStateImpl, TryAlloc, +}; + +impl TryAlloc for BlockAllocator { + type Resources<'tx> = &'tx [u8]; + + #[inline] + fn try_alloc( + &mut self, + tx: Self::Resources<'_>, + ) -> Result<(), AllocFailure> { + self.decrypted_txs.try_dump(tx) + } +} + +impl NextStateImpl for BlockAllocator { + type Next = BlockAllocator; + + #[inline] + fn next_state_impl(mut self) -> Self::Next { + self.decrypted_txs.shrink_to_fit(); + + // the remaining space is allocated to protocol txs + let remaining_free_space = self.uninitialized_space_in_bytes(); + self.protocol_txs = TxBin::init(remaining_free_space); + + // cast state + let Self { + block, + protocol_txs, + encrypted_txs, + decrypted_txs, + .. + } = self; + + BlockAllocator { + _state: PhantomData, + block, + protocol_txs, + encrypted_txs, + decrypted_txs, + } + } +} diff --git a/apps/src/lib/node/ledger/shell/block_alloc/states/encrypted_txs.rs b/apps/src/lib/node/ledger/shell/block_alloc/states/encrypted_txs.rs new file mode 100644 index 0000000000..05f74d1d56 --- /dev/null +++ b/apps/src/lib/node/ledger/shell/block_alloc/states/encrypted_txs.rs @@ -0,0 +1,127 @@ +use std::marker::PhantomData; + +use super::super::{AllocFailure, BlockAllocator, TxBin}; +use super::{ + BuildingDecryptedTxBatch, BuildingEncryptedTxBatch, + EncryptedTxBatchAllocator, NextStateImpl, TryAlloc, WithEncryptedTxs, + WithoutEncryptedTxs, +}; +use crate::node::ledger::shell::block_alloc::BlockResources; + +impl TryAlloc for BlockAllocator> { + type Resources<'tx> = BlockResources<'tx>; + + #[inline] + fn try_alloc( + &mut self, + resource_required: Self::Resources<'_>, + ) -> Result<(), AllocFailure> { + self.encrypted_txs.space.try_dump(resource_required.tx)?; + self.encrypted_txs.gas.try_dump(resource_required.gas) + } +} + +impl NextStateImpl + for BlockAllocator> +{ + type Next = BlockAllocator; + + #[inline] + fn next_state_impl(self) -> Self::Next { + next_state(self) + } +} + +impl TryAlloc + for BlockAllocator> +{ + type Resources<'tx> = BlockResources<'tx>; + + #[inline] + fn try_alloc( + &mut self, + _resource_required: Self::Resources<'_>, + ) -> Result<(), AllocFailure> { + Err(AllocFailure::Rejected { + bin_resource_left: 0, + }) + } +} + +impl NextStateImpl + for BlockAllocator> +{ + type Next = BlockAllocator; + + #[inline] + fn next_state_impl(self) -> Self::Next { + next_state(self) + } +} + +#[inline] +fn next_state( + mut alloc: BlockAllocator>, +) -> BlockAllocator { + alloc.encrypted_txs.space.shrink_to_fit(); + + // decrypted txs can use as much space as they need - which + // in practice will only be, at most, 1/3 of the block space + // used by encrypted txs at the prev height + let remaining_free_space = alloc.uninitialized_space_in_bytes(); + alloc.decrypted_txs = TxBin::init(remaining_free_space); + + // cast state + let BlockAllocator { + block, + protocol_txs, + encrypted_txs, + decrypted_txs, + .. + } = alloc; + + BlockAllocator { + _state: PhantomData, + block, + protocol_txs, + encrypted_txs, + decrypted_txs, + } +} + +impl TryAlloc for EncryptedTxBatchAllocator { + type Resources<'tx> = BlockResources<'tx>; + + #[inline] + fn try_alloc( + &mut self, + resource_required: Self::Resources<'_>, + ) -> Result<(), AllocFailure> { + match self { + EncryptedTxBatchAllocator::WithEncryptedTxs(state) => { + state.try_alloc(resource_required) + } + EncryptedTxBatchAllocator::WithoutEncryptedTxs(state) => { + // NOTE: this operation will cause the allocator to + // run out of memory immediately + state.try_alloc(resource_required) + } + } + } +} + +impl NextStateImpl for EncryptedTxBatchAllocator { + type Next = BlockAllocator; + + #[inline] + fn next_state_impl(self) -> Self::Next { + match self { + EncryptedTxBatchAllocator::WithEncryptedTxs(state) => { + state.next_state_impl() + } + EncryptedTxBatchAllocator::WithoutEncryptedTxs(state) => { + state.next_state_impl() + } + } + } +} diff --git a/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs b/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs new file mode 100644 index 0000000000..aba289113e --- /dev/null +++ b/apps/src/lib/node/ledger/shell/block_alloc/states/protocol_txs.rs @@ -0,0 +1,14 @@ +use super::super::{AllocFailure, BlockAllocator}; +use super::{BuildingProtocolTxBatch, TryAlloc}; + +impl TryAlloc for BlockAllocator { + type Resources<'tx> = &'tx [u8]; + + #[inline] + fn try_alloc( + &mut self, + tx: Self::Resources<'_>, + ) -> Result<(), AllocFailure> { + self.protocol_txs.try_dump(tx) + } +} diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 8af2d1a746..e979f9b1fb 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -5,6 +5,8 @@ use std::collections::HashMap; use data_encoding::HEXUPPER; use namada::core::ledger::pgf::storage::keys as pgf_storage; use namada::core::ledger::pgf::ADDRESS as pgf_address; +use namada::ledger::events::EventType; +use namada::ledger::gas::{GasMetering, TxGasMeter}; use namada::ledger::parameters::storage as params_storage; use namada::ledger::pos::{namada_proof_of_stake, staking_token_address}; use namada::ledger::storage::EPOCH_SWITCH_BLOCKS_DELAY; @@ -64,9 +66,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 @@ -132,9 +131,19 @@ where self.process_slashes(); } - let wrapper_fees = self.get_wrapper_tx_fees(); let mut stats = InternalStats::default(); + let native_block_proposer_address = { + let tm_raw_hash_string = + tm_raw_hash_to_string(req.proposer_address); + find_validator_by_raw_hash(&self.wl_storage, tm_raw_hash_string) + .unwrap() + .expect( + "Unable to find native validator address of block \ + proposer from tendermint raw hash", + ) + }; + // Tracks the accepted transactions self.wl_storage.storage.block.results = BlockResults::default(); for (tx_index, processed_tx) in req.txs.iter().enumerate() { @@ -148,7 +157,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() @@ -176,16 +184,14 @@ where continue; } - let tx = if tx.validate_tx().is_ok() { - tx - } else { + if tx.validate_tx().is_err() { tracing::error!( "Internal logic error: FinalizeBlock received tx that \ could not be deserialized to a valid TxType" ); continue; }; - let tx_type = tx.header(); + let tx_header = tx.header(); // If [`process_proposal`] rejected a Tx, emit an event here and // move on to next tx if ErrorCodes::from_u32(processed_tx.result.code).unwrap() @@ -200,7 +206,7 @@ where // if the rejected tx was decrypted, remove it // from the queue of txs to be processed and remove the hash // from storage - if let TxType::Decrypted(_) = &tx_type.tx_type { + if let TxType::Decrypted(_) = &tx_header.tx_type { let tx_hash = self .wl_storage .storage @@ -217,101 +223,98 @@ 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_header.tx_type { + // Charge fee if wrapper transaction went out of gas or + // failed because of fees + let error_code = + ErrorCodes::from_u32(processed_tx.result.code).unwrap(); + if (error_code == ErrorCodes::TxGasLimit) + | (error_code == ErrorCodes::FeeError) + { + let masp_transaction = wrapper + .unshield_section_hash + .map(|ref hash| { + tx.get_section(hash) + .map(|section| { + if let Section::MaspTx(transaction) = + section + { + Some(transaction.to_owned()) + } else { + None + } + }) + .flatten() + }) + .flatten(); + #[cfg(not(feature = "mainnet"))] + let has_valid_pow = + self.invalidate_pow_solution_if_valid(wrapper); + if let Err(msg) = protocol::charge_fee( + wrapper, + masp_transaction, + ShellParams::new( + TxGasMeter::new_from_sub_limit(u64::MAX), + &mut self.wl_storage, + &mut self.vp_wasm_cache, + &mut self.tx_wasm_cache, + ), + #[cfg(not(feature = "mainnet"))] + has_valid_pow, + Some(&native_block_proposer_address), + &mut BTreeSet::default(), + ) { + self.wl_storage.write_log.drop_tx(); + tracing::error!( + "Rejected wrapper tx {} could not pay fee: {}", + Hash::sha256( + tx::try_from(processed_tx.as_ref()) + .unwrap() + ), + msg + ) + } + } + } + continue; } - let (mut tx_event, tx_unsigned_hash) = match &tx_type.tx_type { + let ( + mut tx_event, + tx_unsigned_hash, + mut tx_gas_meter, + has_valid_pow, + wrapper, + ) = match &tx_header.tx_type { TxType::Wrapper(wrapper) => { stats.increment_wrapper_txs(); - let mut tx_event = Event::new_tx_event(&tx, height.0); - - // Writes both txs hash to storage - let processed_tx = - Tx::try_from(processed_tx.tx.as_ref()).unwrap(); - let wrapper_tx_hash_key = - replay_protection::get_tx_hash_key(&hash::Hash( - processed_tx.header_hash().0, - )); - self.wl_storage - .write_bytes(&wrapper_tx_hash_key, vec![]) - .expect("Error while writing tx hash to storage"); - - let inner_tx_hash_key = replay_protection::get_tx_hash_key( - &tx.clone().update_header(TxType::Raw).header_hash(), - ); - self.wl_storage - .write_bytes(&inner_tx_hash_key, vec![]) - .expect("Error while writing tx hash to storage"); - + let tx_event = Event::new_tx_event(&tx, height.0); #[cfg(not(feature = "mainnet"))] let has_valid_pow = self.invalidate_pow_solution_if_valid(wrapper); - // Charge fee - let gas_payer = - if wrapper.pk != address::masp_tx_key().ref_to() { - wrapper.gas_payer() - } else { - address::masp() - }; - - let balance_key = - token::balance_key(&wrapper.fee.token, &gas_payer); - let balance: token::Amount = self - .wl_storage - .read(&balance_key) - .expect("must be able to read") - .unwrap_or_default(); - - match balance.checked_sub(wrapper_fees) { - Some(amount) => { - self.wl_storage - .write(&balance_key, amount) - .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 - .write( - &balance_key, - Amount::native_whole(0), - ) - .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; - } - } - } + let gas_meter = TxGasMeter::new(wrapper.gas_limit); - self.wl_storage.storage.tx_queue.push(TxInQueue { - tx: processed_tx.clone(), + ( + tx_event, + None, + gas_meter, #[cfg(not(feature = "mainnet"))] has_valid_pow, - }); - (tx_event, None) + Some(tx.clone()), + ) } TxType::Decrypted(inner) => { // We remove the corresponding wrapper tx from the queue - let wrapper_hash = self + let mut tx_in_queue = self .wl_storage .storage .tx_queue .pop() - .expect("Missing wrapper tx in queue") - .tx - .clone() - .update_header(TxType::Raw) - .header_hash(); + .expect("Missing wrapper tx in queue"); let mut event = Event::new_tx_event(&tx, height.0); match inner { @@ -328,7 +331,7 @@ where DecryptedTx::Undecryptable => { tracing::info!( "Tx with hash {} was un-decryptable", - wrapper_hash + tx_in_queue.tx.header_hash() ); event["info"] = "Transaction is invalid.".into(); event["log"] = @@ -337,7 +340,20 @@ where continue; } } - (event, Some(wrapper_hash)) + + ( + event, + Some( + tx_in_queue + .tx + .update_header(TxType::Raw) + .header_hash(), + ), + TxGasMeter::new_from_sub_limit(tx_in_queue.gas), + #[cfg(not(feature = "mainnet"))] + false, + None, + ) } TxType::Raw => { tracing::error!( @@ -350,9 +366,14 @@ where ProtocolTxType::BridgePoolVext | ProtocolTxType::BridgePool | ProtocolTxType::ValSetUpdateVext - | ProtocolTxType::ValidatorSetUpdate => { - (Event::new_tx_event(&tx, height.0), None) - } + | ProtocolTxType::ValidatorSetUpdate => ( + Event::new_tx_event(&tx, height.0), + None, + TxGasMeter::new_from_sub_limit(0.into()), + #[cfg(not(feature = "mainnet"))] + false, + None, + ), ProtocolTxType::EthEventsVext => { let ext = ethereum_tx_data_variants::EthEventsVext::try_from( @@ -371,7 +392,14 @@ where self.mode.dequeue_eth_event(event); } } - (Event::new_tx_event(&tx, height.0), None) + ( + Event::new_tx_event(&tx, height.0), + None, + TxGasMeter::new_from_sub_limit(0.into()), + #[cfg(not(feature = "mainnet"))] + false, + None, + ) } ProtocolTxType::EthereumEvents => { let digest = @@ -393,7 +421,14 @@ where } } } - (Event::new_tx_event(&tx, height.0), None) + ( + Event::new_tx_event(&tx, height.0), + None, + TxGasMeter::new_from_sub_limit(0.into()), + #[cfg(not(feature = "mainnet"))] + false, + None, + ) } ref protocol_tx_type => { tracing::error!( @@ -408,29 +443,46 @@ where }; match protocol::dispatch_tx( - tx.clone(), - tx_length, + tx, + processed_tx.tx.as_ref(), TxIndex( tx_index .try_into() .expect("transaction index out of bounds"), ), - &mut self.gas_meter, + &mut tx_gas_meter, &mut self.wl_storage, &mut self.vp_wasm_cache, &mut self.tx_wasm_cache, + Some(&native_block_proposer_address), + #[cfg(not(feature = "mainnet"))] + has_valid_pow, ) .map_err(Error::TxApply) { Ok(result) => { if result.is_accepted() { - tracing::trace!( - "all VPs accepted transaction {} storage \ - modification {:#?}", - tx_event["hash"], - result - ); - stats.increment_successful_txs(); + if let EventType::Accepted = tx_event.event_type { + // Wrapper transaction + tracing::trace!( + "Wrapper transaction {} was accepted", + tx_event["hash"] + ); + self.wl_storage.storage.tx_queue.push(TxInQueue { + tx: wrapper.expect("Missing expected wrapper"), + gas: tx_gas_meter.get_available_gas(), + #[cfg(not(feature = "mainnet"))] + has_valid_pow, + }); + } else { + tracing::trace!( + "all VPs accepted transaction {} storage \ + modification {:#?}", + tx_event["hash"], + result + ); + stats.increment_successful_txs(); + } self.wl_storage.commit_tx(); if !tx_event.contains_key("code") { tx_event["code"] = ErrorCodes::Ok.into(); @@ -489,28 +541,26 @@ 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 = replay_protection::get_tx_hash_key(&hash); - self.wl_storage - .delete(&tx_hash_key) - .expect( - "Error while deleting tx hash key from storage", - ); - // Apply only to remove its hash, - // since all other changes have already been dropped - self.wl_storage.commit_tx(); + self.wl_storage.delete(&tx_hash_key).expect( + "Error while deleting tx hash key from storage", + ); } } - tx_event["gas_used"] = self - .gas_meter - .get_current_transaction_gas() - .to_string(); + tx_event["gas_used"] = + tx_gas_meter.get_tx_consumed_gas().to_string(); tx_event["info"] = msg.to_string(); - tx_event["code"] = ErrorCodes::WasmRuntimeError.into(); + if let EventType::Accepted = tx_event.event_type { + // If wrapper, invalid tx error code + tx_event["code"] = ErrorCodes::InvalidTx.into(); + } else { + tx_event["code"] = ErrorCodes::WasmRuntimeError.into(); + } } } response.events.push(tx_event); @@ -535,28 +585,10 @@ where self.update_eth_oracle(); } - if !req.proposer_address.is_empty() { - let tm_raw_hash_string = - tm_raw_hash_to_string(req.proposer_address); - let native_proposer_address = find_validator_by_raw_hash( - &self.wl_storage, - tm_raw_hash_string, - ) - .unwrap() - .expect( - "Unable to find native validator address of block proposer \ - from tendermint raw hash", - ); - write_last_block_proposer_address( - &mut self.wl_storage, - native_proposer_address, - )?; - } - - let _ = self - .gas_meter - .finalize_transaction() - .map_err(|_| Error::GasOverflow)?; + write_last_block_proposer_address( + &mut self.wl_storage, + native_block_proposer_address, + )?; self.event_log_mut().log_events(response.events.clone()); tracing::debug!("End finalize_block {height} of epoch {current_epoch}"); @@ -577,8 +609,6 @@ where ) -> (BlockHeight, bool) { let height = self.wl_storage.storage.get_last_block_height() + 1; - self.gas_meter.reset(); - self.wl_storage .storage .begin_block(hash, height) @@ -1022,7 +1052,6 @@ mod test_finalize_block { use namada::ledger::storage_api; use namada::ledger::storage_api::StorageWrite; use namada::proof_of_stake::btree_set::BTreeSetShims; - use namada::proof_of_stake::parameters::PosParams; use namada::proof_of_stake::storage::{ is_validator_slashes_key, slashes_prefix, }; @@ -1051,7 +1080,7 @@ mod test_finalize_block { InitProposalData, VoteProposalData, }; use namada::types::transaction::protocol::EthereumTxData; - use namada::types::transaction::{Fee, WrapperTx, MIN_FEE_AMOUNT}; + use namada::types::transaction::{Fee, WrapperTx}; use namada::types::uint::Uint; use namada::types::vote_extensions::ethereum_events; use namada_test_utils::TestWasms; @@ -1067,6 +1096,8 @@ mod test_finalize_block { FinalizeBlock, ProcessedTx, }; + const GAS_LIMIT_MULTIPLIER: u64 = 300_000; + /// Make a wrapper tx and a processed tx from the wrapped tx that can be /// added to `FinalizeBlock` request. fn mk_wrapper_tx( @@ -1076,14 +1107,15 @@ mod test_finalize_block { let mut wrapper_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: MIN_FEE_AMOUNT, + amount_per_gas_unit: 1.into(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper_tx.header.chain_id = shell.chain_id.clone(); wrapper_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -1117,21 +1149,26 @@ mod test_finalize_block { let mut outer_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: MIN_FEE_AMOUNT, + amount_per_gas_unit: 1.into(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); outer_tx.header.chain_id = shell.chain_id.clone(); outer_tx.set_code(Code::new(tx_code)); outer_tx.set_data(Data::new( "Decrypted transaction data".as_bytes().to_owned(), )); - shell.enqueue_tx(outer_tx.clone()); + let gas_limit = + Gas::from(outer_tx.header().wrapper().unwrap().gas_limit) + .checked_sub(Gas::from(outer_tx.to_bytes().len() as u64)) + .unwrap(); + shell.enqueue_tx(outer_tx.clone(), gas_limit); outer_tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted { #[cfg(not(feature = "mainnet"))] has_valid_pow: false, @@ -1179,7 +1216,18 @@ mod test_finalize_block { u32::try_from(i.rem_euclid(2)).unwrap(); processed_txs.push(processed_tx); } else { - shell.enqueue_tx(wrapper.clone()); + let wrapper_info = + if let TxType::Wrapper(w) = wrapper.header().tx_type { + w + } else { + panic!("Unexpected tx type"); + }; + shell.enqueue_tx( + wrapper.clone(), + Gas::from(wrapper_info.gas_limit) + .checked_sub(Gas::from(wrapper.to_bytes().len() as u64)) + .unwrap(), + ); } if i != 3 { @@ -1216,7 +1264,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] @@ -1226,21 +1274,26 @@ mod test_finalize_block { let mut outer_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Default::default(), + amount_per_gas_unit: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); outer_tx.header.chain_id = shell.chain_id.clone(); outer_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); outer_tx.set_data(Data::new( String::from("transaction data").as_bytes().to_owned(), )); - shell.enqueue_tx(outer_tx.clone()); + let gas_limit = + Gas::from(outer_tx.header().wrapper().unwrap().gas_limit) + .checked_sub(Gas::from(outer_tx.to_bytes().len() as u64)) + .unwrap(); + shell.enqueue_tx(outer_tx.clone(), gas_limit); outer_tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted { #[cfg(not(feature = "mainnet"))] @@ -1278,17 +1331,18 @@ mod test_finalize_block { let keypair = crate::wallet::defaults::daewon_keypair(); // not valid tx bytes - let wrapper = WrapperTx { - fee: Fee { - amount: Default::default(), + let wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 0.into(), token: shell.wl_storage.storage.native_token.clone(), }, - pk: keypair.ref_to(), - epoch: Epoch(0), - gas_limit: Default::default(), + keypair.ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] - pow_solution: None, - }; + None, + None, + )))); let processed_tx = ProcessedTx { tx: Tx::from_type(TxType::Decrypted(DecryptedTx::Undecryptable)) .to_bytes(), @@ -1298,8 +1352,11 @@ mod test_finalize_block { }, }; - let tx = Tx::from_type(TxType::Wrapper(Box::new(wrapper))); - shell.enqueue_tx(tx); + let gas_limit = + Gas::from(wrapper.header().wrapper().unwrap().gas_limit) + .checked_sub(Gas::from(wrapper.to_bytes().len() as u64)) + .unwrap(); + shell.enqueue_tx(wrapper, gas_limit); // check that correct error message is returned for event in shell @@ -2215,34 +2272,6 @@ mod test_finalize_block { } } - fn next_block_for_inflation( - shell: &mut TestShell, - proposer_address: Vec, - votes: Vec, - byzantine_validators: Option>, - ) { - // Let the header time be always ahead of the next epoch min start time - let header = Header { - time: shell - .wl_storage - .storage - .next_epoch_min_start_time - .next_second(), - ..Default::default() - }; - let mut req = FinalizeBlock { - header, - proposer_address, - votes, - ..Default::default() - }; - if let Some(byz_vals) = byzantine_validators { - req.byzantine_validators = byz_vals; - } - shell.finalize_block(req).unwrap(); - shell.commit(); - } - /// Test that if a decrypted transaction fails because of out-of-gas, its /// hash is removed from storage to allow rewrapping it #[test] @@ -2257,14 +2286,15 @@ mod test_finalize_block { let mut wrapper_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::zero(), + amount_per_gas_unit: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper_tx.header.chain_id = shell.chain_id.clone(); wrapper_tx.set_code(Code::new(tx_code)); @@ -2295,11 +2325,11 @@ mod test_finalize_block { info: "".into(), }, }; - shell.enqueue_tx(wrapper_tx); + shell.enqueue_tx(wrapper_tx, Gas::default()); // merkle tree root before finalize_block let root_pre = shell.shell.wl_storage.storage.block.tree.root(); - let _event = &shell + let event = &shell .finalize_block(FinalizeBlock { txs: vec![processed_tx], ..Default::default() @@ -2310,20 +2340,257 @@ mod test_finalize_block { let root_post = shell.shell.wl_storage.storage.block.tree.root(); assert_eq!(root_pre.0, root_post.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()); + // 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 - // .storage - // .has_key(&inner_hash_key) - // .expect("Test failed") - // .0 - // ) + assert!( + !shell + .wl_storage + .has_key(&inner_hash_key) + .expect("Test failed") + ) + } + + #[test] + /// Test that the hash of the wrapper transaction is committed to storage + /// even if the wrapper tx fails. The inner transaction hash must instead be + /// removed + fn test_commits_hash_if_wrapper_failure() { + let (mut shell, _, _, _) = setup(); + let keypair = gen_keypair(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 0.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + keypair.ref_to(), + Epoch(0), + 0.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper.set_data(Data::new( + "Encrypted transaction data".as_bytes().to_owned(), + )); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &keypair, + ))); + + let wrapper_hash_key = + replay_protection::get_tx_hash_key(&wrapper.header_hash()); + let inner_hash_key = replay_protection::get_tx_hash_key( + &wrapper.clone().update_header(TxType::Raw).header_hash(), + ); + + let processed_tx = ProcessedTx { + tx: wrapper.to_bytes(), + result: TxResult { + code: ErrorCodes::Ok.into(), + info: "".into(), + }, + }; + + let event = &shell + .finalize_block(FinalizeBlock { + txs: vec![processed_tx], + ..Default::default() + }) + .expect("Test failed")[0]; + + // Check wrapper hash has been committed to storage even if it failed. + // Check that, instead, the inner hash has been removed + 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::InvalidTx).as_str()); + + assert!( + shell + .wl_storage + .has_key(&wrapper_hash_key) + .expect("Test failed") + ); + assert!( + !shell + .wl_storage + .has_key(&inner_hash_key) + .expect("Test failed") + ) + } + + // Test that if the fee payer doesn't have enough funds for fee payment the + // ledger drains their balance. Note that because of the checks in process + // proposal this scenario should never happen + #[test] + fn test_fee_payment_if_insufficient_balance() { + let (mut shell, _, _, _) = setup(); + let keypair = gen_keypair(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + keypair.ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper.set_data(Data::new( + "Encrypted transaction data".as_bytes().to_owned(), + )); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &keypair, + ))); + + let processed_tx = ProcessedTx { + tx: wrapper.to_bytes(), + result: TxResult { + code: ErrorCodes::Ok.into(), + info: "".into(), + }, + }; + + let event = &shell + .finalize_block(FinalizeBlock { + txs: vec![processed_tx], + ..Default::default() + }) + .expect("Test failed")[0]; + + // Check balance of fee payer is 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::InvalidTx).as_str()); + let balance_key = namada::core::types::token::balance_key( + &shell.wl_storage.storage.native_token, + &Address::from(&keypair.to_public()), + ); + let balance: Amount = shell + .wl_storage + .read(&balance_key) + .unwrap() + .unwrap_or_default(); + + assert_eq!(balance, 0.into()) + } + + // Test that the fees collected from a block are withdrew from the wrapper + // signer and credited to the block proposer + #[test] + fn test_fee_payment_to_block_proposer() { + let (mut shell, _, _, _) = setup(); + + let validator = shell.mode.get_validator_address().unwrap().to_owned(); + let pos_params = + namada_proof_of_stake::read_pos_params(&shell.wl_storage).unwrap(); + let consensus_key = + namada_proof_of_stake::validator_consensus_key_handle(&validator) + .get(&shell.wl_storage, Epoch::default(), &pos_params) + .unwrap() + .unwrap(); + let proposer_address = HEXUPPER + .decode(consensus_key.tm_raw_hash().as_bytes()) + .unwrap(); + + let proposer_balance = storage_api::token::read_balance( + &shell.wl_storage, + &shell.wl_storage.storage.native_token, + &validator, + ) + .unwrap(); + + 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 mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 1.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + 5_000_000.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new(tx_code)); + wrapper.set_data(Data::new( + "Enxrypted transaction data".as_bytes().to_owned(), + )); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + let fee_amount = + wrapper.header().wrapper().unwrap().get_tx_fee().unwrap(); + + let signer_balance = storage_api::token::read_balance( + &shell.wl_storage, + &shell.wl_storage.storage.native_token, + &wrapper.header().wrapper().unwrap().fee_payer(), + ) + .unwrap(); + + let processed_tx = ProcessedTx { + tx: wrapper.to_bytes(), + result: TxResult { + code: ErrorCodes::Ok.into(), + info: "".into(), + }, + }; + + let event = &shell + .finalize_block(FinalizeBlock { + txs: vec![processed_tx], + proposer_address, + ..Default::default() + }) + .expect("Test failed")[0]; + + // Check fee payment + assert_eq!(event.event_type.to_string(), String::from("accepted")); + let code = event.attributes.get("code").expect("Test failed").as_str(); + assert_eq!(code, String::from(ErrorCodes::Ok).as_str()); + + let new_proposer_balance = storage_api::token::read_balance( + &shell.wl_storage, + &shell.wl_storage.storage.native_token, + &validator, + ) + .unwrap(); + assert_eq!( + new_proposer_balance, + proposer_balance.checked_add(fee_amount).unwrap() + ); + + let new_signer_balance = storage_api::token::read_balance( + &shell.wl_storage, + &shell.wl_storage.storage.native_token, + &wrapper.header().wrapper().unwrap().fee_payer(), + ) + .unwrap(); + assert_eq!( + new_signer_balance, + signer_balance.checked_sub(fee_amount).unwrap() + ) } #[test] @@ -3551,23 +3818,6 @@ mod test_finalize_block { shell.wl_storage.storage.block.epoch } - fn get_pkh_from_address( - storage: &S, - params: &PosParams, - address: Address, - epoch: Epoch, - ) -> Vec - where - S: StorageRead, - { - let ck = validator_consensus_key_handle(&address) - .get(storage, epoch, params) - .unwrap() - .unwrap(); - let hash_string = tm_consensus_key_raw_hash(&ck); - HEXUPPER.decode(hash_string.as_bytes()).unwrap() - } - /// Test that updating the ethereum bridge params via governance works. #[tokio::test] async fn test_eth_bridge_param_updates() { @@ -3587,7 +3837,9 @@ mod test_finalize_block { .wl_storage .write(&min_confirmations_key(), new_min_confirmations) .expect("Test failed"); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()), + ); let keys_changed = BTreeSet::from([min_confirmations_key()]); let verifiers = BTreeSet::default(); let ctx = namada::ledger::native_vp::Ctx::new( @@ -3617,10 +3869,36 @@ mod test_finalize_block { } shell.finalize_block(req).expect("Test failed"); shell.commit(); + + let consensus_set: Vec = + read_consensus_validator_set_addresses_with_stake( + &shell.wl_storage, + Epoch::default(), + ) + .unwrap() + .into_iter() + .collect(); + + let params = read_pos_params(&shell.wl_storage).unwrap(); + let val1 = consensus_set[0].clone(); + let pkh1 = get_pkh_from_address( + &shell.wl_storage, + ¶ms, + val1.address.clone(), + Epoch::default(), + ); + let _ = control_receiver.recv().await.expect("Test failed"); - let mut req = FinalizeBlock::default(); - req.header.time = namada::types::time::DateTimeUtc::now(); - shell.finalize_block(req).expect("Test failed"); + // Finalize block 2 + let votes = vec![VoteInfo { + validator: Some(Validator { + address: pkh1.clone(), + power: u128::try_from(val1.bonded_stake).expect("Test failed") + as i64, + }), + signed_last_block: true, + }]; + next_block_for_inflation(&mut shell, pkh1.clone(), votes, None); let Command::UpdateConfig(cmd) = control_receiver.recv().await.expect("Test failed"); assert_eq!(u64::from(cmd.min_confirmations), 42); diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index 09f0c9dac3..3c04c20f87 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -276,19 +276,19 @@ where tx.set_data(Data::new(encode(&id))); tx.set_code(Code::new(code)); - // 0 parameter is used to compute the fee - // based on the code size. We dont - // need it here. let tx_result = protocol::dispatch_tx( tx, - 0, /* this is used to compute the fee - * based on the code size. We dont - * need it here. */ + &[], /* 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_from_sub_limit(u64::MAX.into()), /* No gas limit for governance proposal */ &mut shell.wl_storage, &mut shell.vp_wasm_cache, &mut shell.tx_wasm_cache, + None, + #[cfg(not(feature = "mainnet"))] + false, ); shell .wl_storage diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 8ad61ee45e..b650e2d730 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -84,6 +84,7 @@ where let genesis::Parameters { epoch_duration, max_proposal_bytes, + max_block_gas, max_expected_time_per_block, vp_whitelist, tx_whitelist, @@ -95,7 +96,9 @@ where pos_gain_d, staked_ratio, pos_inflation_amount, - wrapper_tx_fees, + gas_cost, + fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit, } = genesis.parameters; #[cfg(not(feature = "mainnet"))] // Try to find a faucet account @@ -121,6 +124,8 @@ where 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(|| { @@ -145,10 +150,14 @@ 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); let hash_key = Key::wasm_hash(name); + let code_name_key = Key::wasm_code_name(name.to_owned()); + + self.wl_storage.write_bytes(&code_key, code)?; + self.wl_storage.write(&code_len_key, code_len)?; self.wl_storage.write_bytes(&hash_key, code_hash)?; + self.wl_storage.write_bytes(&code_name_key, code_hash)?; } else { tracing::warn!("The wasm {name} isn't whitelisted."); } @@ -178,6 +187,7 @@ where let parameters = Parameters { epoch_duration, max_proposal_bytes, + max_block_gas, max_expected_time_per_block, vp_whitelist, tx_whitelist, @@ -190,8 +200,9 @@ where pos_inflation_amount, #[cfg(not(feature = "mainnet"))] faucet_account, - #[cfg(not(feature = "mainnet"))] - wrapper_tx_fees, + gas_cost, + fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit, }; 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 c65fbb3f99..5804b6ee79 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -5,12 +5,12 @@ //! 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_alloc; mod finalize_block; mod governance; mod init_chain; -mod prepare_proposal; -mod process_proposal; +pub mod prepare_proposal; +pub mod process_proposal; pub(super) mod queries; mod stats; #[cfg(any(test, feature = "testing"))] @@ -27,42 +27,42 @@ use std::path::{Path, PathBuf}; use std::rc::Rc; use borsh::{BorshDeserialize, BorshSerialize}; +use masp_primitives::transaction::Transaction; use namada::core::ledger::eth_bridge; use namada::ledger::eth_bridge::{EthBridgeQueries, EthereumBridgeConfig}; use namada::ledger::events::log::EventLog; use namada::ledger::events::Event; -use namada::ledger::gas::BlockGasMeter; +use namada::ledger::gas::{Gas, TxGasMeter}; use namada::ledger::pos::into_tm_voting_power; use namada::ledger::pos::namada_proof_of_stake::types::{ ConsensusValidator, ValidatorSetUpdate, }; -use namada::ledger::protocol::ShellParams; +use namada::ledger::protocol::{ + apply_wasm_tx, get_transfer_hash_from_storage, ShellParams, +}; use namada::ledger::storage::write_log::WriteLog; use namada::ledger::storage::{ DBIter, Sha256Hasher, Storage, StorageHasher, TempWlStorage, WlStorage, DB, EPOCH_SWITCH_BLOCKS_DELAY, }; -use namada::ledger::storage_api::{self, StorageRead, StorageWrite}; -use namada::ledger::{pos, protocol, replay_protection}; +use namada::ledger::storage_api::{self, StorageRead}; +use namada::ledger::{parameters, pos, protocol, replay_protection}; use namada::proof_of_stake::{self, process_slashes, read_pos_params, slash}; use namada::proto::{self, Section, Tx}; -use namada::types::address::{masp, masp_tx_key, Address}; +use namada::types::address::Address; use namada::types::chain::ChainId; use namada::types::ethereum_events::EthereumEvent; use namada::types::internal::TxInQueue; use namada::types::key::*; use namada::types::storage::{BlockHeight, Key, TxIndex}; use namada::types::time::DateTimeUtc; -use namada::types::token::{self}; -#[cfg(not(feature = "mainnet"))] -use namada::types::transaction::MIN_FEE; use namada::types::transaction::{ hash_tx, verify_decrypted_correctly, AffineCurve, DecryptedTx, - EllipticCurve, PairingEngine, TxType, + EllipticCurve, PairingEngine, TxType, WrapperTx, }; -use namada::types::{address, hash}; +use namada::types::{address, hash, token}; use namada::vm::wasm::{TxCache, VpCache}; -use namada::vm::WasmCacheRwAccess; +use namada::vm::{WasmCacheAccess, WasmCacheRwAccess}; use num_derive::{FromPrimitive, ToPrimitive}; use num_traits::{FromPrimitive, ToPrimitive}; use thiserror::Error; @@ -108,8 +108,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("{0}")] @@ -147,17 +145,20 @@ 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, - InvalidVoteExtension = 13, + DecryptedTxGasLimit = 3, + WasmRuntimeError = 4, + InvalidTx = 5, + InvalidSig = 6, + InvalidOrder = 7, + ExtraTxs = 8, + Undecryptable = 9, + AllocationError = 10, + ReplayTx = 11, + InvalidChainId = 12, + ExpiredTx = 13, + TxGasLimit = 14, + FeeError = 15, + InvalidVoteExtension = 16, } impl ErrorCodes { @@ -171,10 +172,11 @@ impl ErrorCodes { Ok | InvalidDecryptedChainId | ExpiredDecryptedTx - | WasmRuntimeError => true, + | WasmRuntimeError + | DecryptedTxGasLimit => true, InvalidTx | InvalidSig | InvalidOrder | ExtraTxs | Undecryptable | AllocationError | ReplayTx | InvalidChainId - | ExpiredTx | InvalidVoteExtension => false, + | ExpiredTx | TxGasLimit | FeeError | InvalidVoteExtension => false, } } } @@ -375,9 +377,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, @@ -390,9 +390,9 @@ where #[allow(dead_code)] mode: ShellMode, /// VP WASM compilation cache - pub(super) vp_wasm_cache: VpCache, + pub vp_wasm_cache: VpCache, /// Tx WASM compilation cache - pub(super) 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. @@ -531,7 +531,6 @@ where let mut shell = Self { chain_id, wl_storage, - gas_meter: BlockGasMeter::default(), byzantine_validators: vec![], base_dir, wasm_dir, @@ -900,9 +899,10 @@ where ))); } - // Write inner hash to WAL + // Write inner hash to tx WAL temp_wl_storage - .write(&inner_hash_key, ()) + .write_log + .write(&inner_hash_key, vec![]) .expect("Couldn't write inner transaction hash to write log"); let tx = @@ -920,9 +920,10 @@ where ))); } - // Write wrapper hash to WAL + // Write wrapper hash to tx WAL temp_wl_storage - .write(&wrapper_hash_key, ()) + .write_log + .write(&wrapper_hash_key, vec![]) .expect("Couldn't write wrapper tx hash to write log"); Ok(()) @@ -1053,19 +1054,22 @@ where } }; + let tx_chain_id = tx.header.chain_id.clone(); + let tx_expiration = tx.header.expiration; + // Tx chain id - if tx.header.chain_id != self.chain_id { + if tx_chain_id != self.chain_id { response.code = ErrorCodes::InvalidChainId.into(); response.log = format!( "{INVALID_MSG}: Tx carries a wrong chain id: expected {}, \ found {}", - self.chain_id, tx.header.chain_id + self.chain_id, tx_chain_id ); return response; } // Tx expiration - if let Some(exp) = tx.header.expiration { + if let Some(exp) = tx_expiration { let last_block_timestamp = self.get_block_timestamp(None); if last_block_timestamp > exp { @@ -1198,6 +1202,31 @@ where } }, TxType::Wrapper(wrapper) => { + // Tx gas limit + let mut gas_meter = TxGasMeter::new(wrapper.gas_limit); + if gas_meter.add_tx_size_gas(tx_bytes).is_err() { + response.code = ErrorCodes::TxGasLimit.into(); + response.log = "{INVALID_MSG}: Wrapper transactions \ + exceeds its gas limit" + .to_string(); + return response; + } + + // Max block gas + let block_gas_limit: Gas = Gas::from_whole_units( + namada::core::ledger::gas::get_max_block_gas( + &self.wl_storage, + ) + .unwrap(), + ); + if gas_meter.tx_gas_limit > block_gas_limit { + response.code = ErrorCodes::AllocationError.into(); + response.log = "{INVALID_MSG}: Wrapper transaction \ + exceeds the maximum block gas limit" + .to_string(); + return response; + } + // Replay protection check let mut inner_tx = tx; inner_tx.update_header(TxType::Raw); @@ -1213,8 +1242,9 @@ where { response.code = ErrorCodes::ReplayTx.into(); response.log = format!( - "{INVALID_MSG}: Inner transaction hash \ - {inner_tx_hash} already in storage, replay attempt", + "{INVALID_MSG}: Inner transaction hash {} already in \ + storage, replay attempt", + inner_tx_hash ); return response; } @@ -1242,28 +1272,27 @@ where return response; } - // Check balance for fee - let gas_payer = if wrapper.pk != masp_tx_key().ref_to() { - wrapper.gas_payer() - } else { - masp() - }; - // check that the fee payer has sufficient balance - let balance = self.get_balance(&wrapper.fee.token, &gas_payer); - - // In testnets with a faucet, tx is allowed to skip fees if - // it includes a valid PoW - #[cfg(not(feature = "mainnet"))] - let has_valid_pow = self.has_valid_pow_solution(&wrapper); - #[cfg(feature = "mainnet")] - let has_valid_pow = false; - - if !has_valid_pow && self.get_wrapper_tx_fees() > balance { - response.code = ErrorCodes::InvalidTx.into(); - response.log = format!( - "{INVALID_MSG}: The given address does not have a \ - sufficient balance to pay fee", - ); + let fee_unshield = wrapper + .unshield_section_hash + .and_then(|ref hash| tx.get_section(hash)) + .and_then(|section| { + if let Section::MaspTx(transaction) = section.as_ref() { + Some(transaction.to_owned()) + } else { + None + } + }); + // Validate wrapper fees + if let Err(e) = self.wrapper_fee_check( + &wrapper, + fee_unshield, + &mut TempWlStorage::new(&self.wl_storage.storage), + &mut self.vp_wasm_cache.clone(), + &mut self.tx_wasm_cache.clone(), + None, + ) { + response.code = ErrorCodes::FeeError.into(); + response.log = format!("{INVALID_MSG}: {e}"); return response; } } @@ -1311,16 +1340,6 @@ where false } - #[cfg(not(feature = "mainnet"))] - /// Get fixed amount of fees for wrapper tx - fn get_wrapper_tx_fees(&self) -> token::Amount { - let fees = namada::ledger::parameters::read_wrapper_tx_fees_parameter( - &self.wl_storage, - ) - .expect("Must be able to read wrapper tx fees parameter"); - fees.unwrap_or_else(|| token::Amount::native_whole(MIN_FEE)) - } - #[cfg(not(feature = "mainnet"))] /// Check if the tx has a valid PoW solution and if so invalidate it to /// prevent replay. @@ -1348,6 +1367,129 @@ where false } + /// Check that the Wrapper's signer has enough funds to pay fees. If a block + /// proposer is provided, updates the balance of the fee payer + #[allow(clippy::too_many_arguments)] + pub fn wrapper_fee_check( + &self, + wrapper: &WrapperTx, + masp_transaction: Option, + temp_wl_storage: &mut TempWlStorage, + vp_wasm_cache: &mut VpCache, + tx_wasm_cache: &mut TxCache, + block_proposer: Option<&Address>, + ) -> Result<()> + where + CA: 'static + WasmCacheAccess + Sync, + { + // Check that fee token is an allowed one + let gas_cost = namada::ledger::parameters::read_gas_cost( + &self.wl_storage, + &wrapper.fee.token, + ) + .expect("Must be able to read gas cost parameter") + .ok_or(Error::TxApply(protocol::Error::FeeError(format!( + "The provided {} token is not allowed for fee payment", + wrapper.fee.token + ))))?; + + if wrapper.fee.amount_per_gas_unit < gas_cost { + // The fees do not match the minimum required + return Err(Error::TxApply(protocol::Error::FeeError(format!( + "Fee amount {:?} do not match the minimum required amount \ + {:?} for token {}", + wrapper.fee.amount_per_gas_unit, gas_cost, wrapper.fee.token + )))); + } + + if let Some(transaction) = masp_transaction { + // Validation of the commitment to this section is done when + // checking the aggregated signature of the wrapper, no need for + // further validation + + // Validate data and generate unshielding tx + let transfer_code_hash = + get_transfer_hash_from_storage(temp_wl_storage); + + let descriptions_limit = self.wl_storage.read(¶meters::storage::get_fee_unshielding_descriptions_limit_key()).expect("Error reading the storage").expect("Missing fee unshielding descriptions limit param in storage"); + + let unshield = wrapper + .check_and_generate_fee_unshielding( + transfer_code_hash, + descriptions_limit, + transaction, + ) + .map_err(|e| { + Error::TxApply(protocol::Error::FeeUnshieldingError(e)) + })?; + + let fee_unshielding_gas_limit = temp_wl_storage + .read(¶meters::storage::get_fee_unshielding_gas_limit_key()) + .expect("Error reading from storage") + .expect("Missing fee unshielding gas limit in storage"); + + // Runtime check + // NOTE: A clean tx write log must be provided to this call for a + // correct vp validation. Block write log, instead, should contain + // any prior changes (if any). This is to simulate the + // unshielding tx (to prevent the already written keys + // from being passed/triggering VPs) but we cannot + // commit the tx write log yet cause the tx could still + // be invalid. + temp_wl_storage.write_log.precommit_tx(); + + match apply_wasm_tx( + unshield, + &TxIndex::default(), + ShellParams::new( + &mut TxGasMeter::new(fee_unshielding_gas_limit), + temp_wl_storage, + vp_wasm_cache, + tx_wasm_cache, + ), + #[cfg(not(feature = "mainnet"))] + false, + ) { + Ok(result) => { + if !result.is_accepted() { + return Err(Error::TxApply( + protocol::Error::FeeUnshieldingError(namada::types::transaction::WrapperTxErr::InvalidUnshield(format!( + "Some VPs rejected fee unshielding: {:#?}", + result.vps_result.rejected_vps + ))), + )); + } + } + Err(e) => { + return Err(Error::TxApply( + protocol::Error::FeeUnshieldingError(namada::types::transaction::WrapperTxErr::InvalidUnshield(format!( + "Wasm run failed: {}", + e + ))), + )); + } + } + } + + let result = match block_proposer { + Some(proposer) => protocol::transfer_fee( + temp_wl_storage, + proposer, + #[cfg(not(feature = "mainnet"))] + self.has_valid_pow_solution(wrapper), + wrapper, + ), + None => protocol::check_fees( + temp_wl_storage, + #[cfg(not(feature = "mainnet"))] + self.has_valid_pow_solution(wrapper), + wrapper, + ), + }; + + result.map_err(Error::TxApply) + } + fn get_abci_validator_updates( &self, is_genesis: bool, @@ -1402,36 +1544,23 @@ where } } -impl<'a, D, H> From<&'a mut Shell> - for ShellParams<'a, D, H, namada::vm::WasmCacheRwAccess> -where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, -{ - fn from(shell: &'a mut Shell) -> Self { - ShellParams::Mutating { - block_gas_meter: &mut shell.gas_meter, - wl_storage: &mut shell.wl_storage, - vp_wasm_cache: &mut shell.vp_wasm_cache, - tx_wasm_cache: &mut shell.tx_wasm_cache, - } - } -} - -/// Helper functions and types for writing unit tests /// for the shell #[cfg(test)] mod test_utils { use std::ops::{Deref, DerefMut}; use std::path::PathBuf; + use data_encoding::HEXUPPER; use namada::core::ledger::storage::EPOCH_SWITCH_BLOCKS_DELAY; use namada::ledger::storage::mockdb::MockDB; use namada::ledger::storage::{ update_allowed_conversions, LastBlock, Sha256Hasher, }; use namada::ledger::storage_api::StorageWrite; + use namada::proof_of_stake::parameters::PosParams; + use namada::proof_of_stake::validator_consensus_key_handle; use namada::proto::{Code, Data}; + use namada::tendermint_proto::abci::VoteInfo; use namada::types::address; use namada::types::chain::ChainId; use namada::types::ethereum_events::Uint; @@ -1447,9 +1576,11 @@ mod test_utils { use super::*; use crate::config::ethereum_bridge::ledger::ORACLE_CHANNEL_BUFFER_SIZE; use crate::facade::tendermint_proto::abci::{ - RequestInitChain, RequestProcessProposal, + Misbehavior, RequestInitChain, RequestPrepareProposal, + RequestProcessProposal, }; use crate::facade::tendermint_proto::google::protobuf::Timestamp; + use crate::node::ledger::shims::abcipp_shim_types; use crate::node::ledger::shims::abcipp_shim_types::shim::request::{ FinalizeBlock, ProcessedTx, }; @@ -1648,11 +1779,19 @@ mod test_utils { /// Forward a ProcessProposal request and extract the relevant /// response data to return pub fn process_proposal( - &mut self, + &self, req: ProcessProposal, ) -> std::result::Result, TestError> { let resp = self.shell.process_proposal(RequestProcessProposal { txs: req.txs.clone(), + proposer_address: HEXUPPER + .decode( + crate::wallet::defaults::validator_keypair() + .to_public() + .tm_raw_hash() + .as_bytes(), + ) + .unwrap(), ..Default::default() }); let results = resp @@ -1683,12 +1822,30 @@ mod test_utils { } } + /// Forward a PrepareProposal request + pub fn prepare_proposal( + &self, + mut req: RequestPrepareProposal, + ) -> abcipp_shim_types::shim::response::PrepareProposal { + req.proposer_address = HEXUPPER + .decode( + crate::wallet::defaults::validator_keypair() + .to_public() + .tm_raw_hash() + .as_bytes(), + ) + .unwrap(); + self.shell.prepare_proposal(req) + } + /// 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, tx: Tx) { + pub fn enqueue_tx(&mut self, tx: Tx, inner_tx_gas: Gas) { self.shell.wl_storage.storage.tx_queue.push(TxInQueue { tx, + gas: inner_tx_gas, #[cfg(not(feature = "mainnet"))] has_valid_pow: false, }); @@ -1704,15 +1861,15 @@ mod test_utils { /// Simultaneously call the `FinalizeBlock` and /// `Commit` handlers. - pub fn finalize_and_commit(&mut self) { - let mut req = FinalizeBlock::default(); + pub fn finalize_and_commit(&mut self, req: Option) { + let mut req = req.unwrap_or_default(); req.header.time = DateTimeUtc::now(); self.finalize_block(req).expect("Test failed"); self.commit(); } /// Immediately change to the next epoch. - pub fn start_new_epoch(&mut self) -> Epoch { + pub fn start_new_epoch(&mut self, req: Option) -> Epoch { self.start_new_epoch_in(1); let next_epoch_min_start_height = @@ -1722,10 +1879,10 @@ mod test_utils { { *height = next_epoch_min_start_height; } - self.finalize_and_commit(); + self.finalize_and_commit(req.clone()); for _i in 0..EPOCH_SWITCH_BLOCKS_DELAY { - self.finalize_and_commit(); + self.finalize_and_commit(req.clone()); } self.wl_storage.storage.get_current_epoch().0 } @@ -1831,7 +1988,14 @@ mod test_utils { }, byzantine_validators: vec![], txs: vec![], - proposer_address: vec![], + proposer_address: HEXUPPER + .decode( + crate::wallet::defaults::validator_keypair() + .to_public() + .tm_raw_hash() + .as_bytes(), + ) + .unwrap(), votes: vec![], } } @@ -1894,14 +2058,15 @@ mod test_utils { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Default::default(), + amount_per_gas_unit: Default::default(), token: native_token, }, keypair.ref_to(), Epoch(0), - Default::default(), + 300_000.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -1909,6 +2074,7 @@ mod test_utils { shell.wl_storage.storage.tx_queue.push(TxInQueue { tx: wrapper, + gas: u64::MAX.into(), #[cfg(not(feature = "mainnet"))] has_valid_pow: false, }); @@ -1953,6 +2119,51 @@ mod test_utils { ); assert!(!shell.wl_storage.storage.tx_queue.is_empty()); } + + pub(super) fn get_pkh_from_address( + storage: &S, + params: &PosParams, + address: Address, + epoch: Epoch, + ) -> Vec + where + S: StorageRead, + { + let ck = validator_consensus_key_handle(&address) + .get(storage, epoch, params) + .unwrap() + .unwrap(); + let hash_string = tm_consensus_key_raw_hash(&ck); + HEXUPPER.decode(hash_string.as_bytes()).unwrap() + } + + pub(super) fn next_block_for_inflation( + shell: &mut TestShell, + proposer_address: Vec, + votes: Vec, + byzantine_validators: Option>, + ) { + // Let the header time be always ahead of the next epoch min start time + let header = Header { + time: shell + .wl_storage + .storage + .next_epoch_min_start_time + .next_second(), + ..Default::default() + }; + let mut req = FinalizeBlock { + header, + proposer_address, + votes, + ..Default::default() + }; + if let Some(byz_vals) = byzantine_validators { + req.byzantine_validators = byz_vals; + } + shell.finalize_block(req).unwrap(); + shell.commit(); + } } #[cfg(all(test, not(feature = "abcipp")))] @@ -2116,6 +2327,8 @@ mod test_mempool_validate { use super::*; + const GAS_LIMIT_MULTIPLIER: u64 = 100_000; + /// Mempool validation must reject unsigned wrappers #[test] fn test_missing_signature() { @@ -2126,7 +2339,7 @@ mod test_mempool_validate { let mut unsigned_wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: token::Amount::from_uint(100, 0) + amount_per_gas_unit: token::Amount::from_uint(100, 0) .expect("This can't fail"), token: shell.wl_storage.storage.native_token.clone(), }, @@ -2135,6 +2348,7 @@ mod test_mempool_validate { Default::default(), #[cfg(not(feature = "mainnet"))] None, + None, )))); unsigned_wrapper.header.chain_id = shell.chain_id.clone(); unsigned_wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -2163,7 +2377,7 @@ mod test_mempool_validate { let mut invalid_wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: token::Amount::from_uint(100, 0) + amount_per_gas_unit: token::Amount::from_uint(100, 0) .expect("This can't fail"), token: shell.wl_storage.storage.native_token.clone(), }, @@ -2172,6 +2386,7 @@ mod test_mempool_validate { Default::default(), #[cfg(not(feature = "mainnet"))] None, + None, )))); invalid_wrapper.header.chain_id = shell.chain_id.clone(); invalid_wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -2185,7 +2400,7 @@ mod test_mempool_validate { // we mount a malleability attack to try and remove the fee let mut new_wrapper = invalid_wrapper.header().wrapper().expect("Test failed"); - new_wrapper.fee.amount = Default::default(); + new_wrapper.fee.amount_per_gas_unit = Default::default(); invalid_wrapper.update_header(TxType::Wrapper(Box::new(new_wrapper))); let mut result = shell.mempool_validate( @@ -2231,15 +2446,16 @@ mod test_mempool_validate { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: token::Amount::from_uint(100, 0) + amount_per_gas_unit: token::Amount::from_uint(100, 0) .expect("This can't fail"), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -2375,4 +2591,210 @@ 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, _recv, _, _) = test_utils::setup(); + + let block_gas_limit = + namada::core::ledger::gas::get_max_block_gas(&shell.wl_storage) + .unwrap(); + let keypair = super::test_utils::gen_keypair(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + keypair.ref_to(), + Epoch(0), + (block_gas_limit + 1).into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &keypair, + ))); + + let result = shell.mempool_validate( + wrapper.to_bytes().as_ref(), + MempoolTxType::NewTransaction, + ); + assert_eq!(result.code, u32::from(ErrorCodes::AllocationError)); + } + + // Check that a tx requiring more gas than its limit gets rejected + #[test] + fn test_exceeding_gas_limit_tx() { + let (shell, _recv, _, _) = test_utils::setup(); + let keypair = super::test_utils::gen_keypair(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + keypair.ref_to(), + Epoch(0), + 0.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &keypair, + ))); + + let result = shell.mempool_validate( + wrapper.to_bytes().as_ref(), + MempoolTxType::NewTransaction, + ); + assert_eq!(result.code, u32::from(ErrorCodes::TxGasLimit)); + } + + // Check that a wrapper using a non-whitelisted token for fee payment is + // rejected + #[test] + fn test_fee_non_whitelisted_token() { + let (shell, _recv, _, _) = test_utils::setup(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 100.into(), + token: address::btc(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + + let result = shell.mempool_validate( + wrapper.to_bytes().as_ref(), + MempoolTxType::NewTransaction, + ); + assert_eq!(result.code, u32::from(ErrorCodes::FeeError)); + } + + // Check that a wrapper setting a fee amount lower than the minimum required + // is rejected + #[test] + fn test_fee_wrong_minimum_amount() { + let (shell, _recv, _, _) = test_utils::setup(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 0.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + + let result = shell.mempool_validate( + wrapper.to_bytes().as_ref(), + MempoolTxType::NewTransaction, + ); + assert_eq!(result.code, u32::from(ErrorCodes::FeeError)); + } + + // Check that a wrapper transactions whose fees cannot be paid is rejected + #[test] + fn test_insufficient_balance_for_fee() { + let (shell, _recv, _, _) = test_utils::setup(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 1_000_000_000.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + 150_000.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + + let result = shell.mempool_validate( + wrapper.to_bytes().as_ref(), + MempoolTxType::NewTransaction, + ); + assert_eq!(result.code, u32::from(ErrorCodes::FeeError)); + } + + // Check that a fee overflow in the wrapper transaction is rejected + #[test] + fn test_wrapper_fee_overflow() { + let (shell, _recv, _, _) = test_utils::setup(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: token::Amount::max(), + token: shell.wl_storage.storage.native_token.clone(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + + let result = shell.mempool_validate( + wrapper.to_bytes().as_ref(), + MempoolTxType::NewTransaction, + ); + assert_eq!(result.code, u32::from(ErrorCodes::FeeError)); + } } diff --git a/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/apps/src/lib/node/ledger/shell/prepare_proposal.rs index 4c9356f44a..e609b1157d 100644 --- a/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -1,12 +1,16 @@ //! Implementation of the [`RequestPrepareProposal`] ABCI++ method for the Shell use namada::core::hints; +use namada::core::ledger::gas::TxGasMeter; #[cfg(feature = "abcipp")] use namada::ledger::eth_bridge::{EthBridgeQueries, SendValsetUpd}; use namada::ledger::pos::PosQueries; use namada::ledger::storage::{DBIter, StorageHasher, TempWlStorage, DB}; -use namada::proto::Tx; +use namada::proof_of_stake::find_validator_by_raw_hash; +use namada::proto::{Section, Tx}; +use namada::types::address::Address; use namada::types::internal::TxInQueue; +use namada::types::key::tm_raw_hash_to_string; use namada::types::time::DateTimeUtc; use namada::types::transaction::wrapper::wrapper_tx::PairingEngine; use namada::types::transaction::{ @@ -14,13 +18,15 @@ use namada::types::transaction::{ }; #[cfg(feature = "abcipp")] use namada::types::vote_extensions::VoteExtensionDigest; +use namada::vm::wasm::{TxCache, VpCache}; +use namada::vm::WasmCacheAccess; use super::super::*; -use super::block_space_alloc::states::{ +use super::block_alloc::states::{ BuildingDecryptedTxBatch, BuildingProtocolTxBatch, EncryptedTxBatchAllocator, NextState, TryAlloc, }; -use super::block_space_alloc::{AllocFailure, BlockSpaceAllocator}; +use super::block_alloc::{AllocFailure, BlockAllocator, BlockResources}; #[cfg(feature = "abcipp")] use crate::facade::tendermint_proto::abci::ExtendedCommitInfo; use crate::facade::tendermint_proto::abci::RequestPrepareProposal; @@ -39,8 +45,8 @@ where { /// Begin a new block. /// - /// Block construction is documented in `block_space_alloc` - /// and `block_space_alloc::states` (private modules). + /// Block construction is documented in `block_alloc` + /// and `block_alloc::states` (private modules). /// /// INVARIANT: Any changes applied in this method must be reverted if /// the proposal is rejected (unless we can simply overwrite @@ -52,11 +58,26 @@ where let txs = if let ShellMode::Validator { .. } = self.mode { // start counting allotted space for txs let alloc = self.get_encrypted_txs_allocator(); + // add encrypted txs - let (encrypted_txs, alloc) = - self.build_encrypted_txs(alloc, &req.txs, req.time); + let tm_raw_hash_string = + tm_raw_hash_to_string(req.proposer_address); + let block_proposer = find_validator_by_raw_hash( + &self.wl_storage, + tm_raw_hash_string, + ) + .unwrap() + .expect( + "Unable to find native validator address of block proposer \ + from tendermint raw hash", + ); + let (encrypted_txs, alloc) = self.build_encrypted_txs( + alloc, + &req.txs, + req.time, + &block_proposer, + ); let mut txs = encrypted_txs; - // decrypt the wrapper txs included in the previous block let (mut decrypted_txs, alloc) = self.build_decrypted_txs(alloc); txs.append(&mut decrypted_txs); @@ -127,50 +148,52 @@ where mut alloc: EncryptedTxBatchAllocator, txs: &[TxBytes], block_time: Option, - ) -> (Vec, BlockSpaceAllocator) { - let mut temp_wl_storage = TempWlStorage::new(&self.wl_storage.storage); + block_proposer: &Address, + ) -> (Vec, BlockAllocator) { let pos_queries = self.wl_storage.pos_queries(); let block_time = block_time.and_then(|block_time| { // If error in conversion, default to last block datetime, it's // valid because of mempool check TryInto::::try_into(block_time).ok() }); + let mut temp_wl_storage = TempWlStorage::new(&self.wl_storage.storage); + let mut vp_wasm_cache = self.vp_wasm_cache.clone(); + let mut tx_wasm_cache = self.tx_wasm_cache.clone(); + let txs = txs .iter() .filter_map(|tx_bytes| { - if let Ok(tx) = Tx::try_from(tx_bytes.as_slice()) { - // If tx doesn't have an expiration it is valid. If time cannot be - // retrieved from block default to last block datetime which has - // already been checked by mempool_validate, so it's valid - if let (Some(block_time), Some(exp)) = (block_time.as_ref(), &tx.header.expiration) { - if block_time > exp { return None } - } - if tx.validate_tx().is_ok() && tx.header().wrapper().is_some() && self.replay_protection_checks(&tx, tx_bytes.as_slice(), &mut temp_wl_storage).is_ok() { - return Some(tx_bytes.clone()); + match self.validate_wrapper_bytes(tx_bytes, block_time, &mut temp_wl_storage, &mut vp_wasm_cache, &mut tx_wasm_cache, block_proposer) { + Ok(gas) => { + temp_wl_storage.write_log.commit_tx(); + Some((tx_bytes.to_owned(), gas)) + }, + Err(()) => { + temp_wl_storage.write_log.drop_tx(); + None } } - None }) - .take_while(|tx_bytes| { - alloc.try_alloc(&tx_bytes[..]) + .take_while(|(tx_bytes, tx_gas)| { + alloc.try_alloc(BlockResources::new(&tx_bytes[..], tx_gas.to_owned())) .map_or_else( |status| match status { - AllocFailure::Rejected { bin_space_left } => { + AllocFailure::Rejected { bin_resource_left} => { tracing::debug!( ?tx_bytes, - bin_space_left, + bin_resource_left, proposal_height = ?pos_queries.get_current_decision_height(), "Dropping encrypted tx from the current proposal", ); false } - AllocFailure::OverflowsBin { bin_size } => { + AllocFailure::OverflowsBin { bin_resource} => { // TODO: handle tx whose size is greater // than bin size tracing::warn!( ?tx_bytes, - bin_size, + bin_resource, proposal_height = ?pos_queries.get_current_decision_height(), "Dropping large encrypted tx from the current proposal", @@ -181,12 +204,77 @@ where |()| true, ) }) + .map(|(tx, _)| tx) .collect(); let alloc = alloc.next_state(); (txs, alloc) } + /// Validity checks on a wrapper tx + #[allow(clippy::too_many_arguments)] + fn validate_wrapper_bytes( + &self, + tx_bytes: &[u8], + block_time: Option, + temp_wl_storage: &mut TempWlStorage, + vp_wasm_cache: &mut VpCache, + tx_wasm_cache: &mut TxCache, + block_proposer: &Address, + ) -> Result + where + CA: 'static + WasmCacheAccess + Sync, + { + let tx = Tx::try_from(tx_bytes).map_err(|_| ())?; + + // If tx doesn't have an expiration it is valid. If time cannot be + // retrieved from block default to last block datetime which has + // already been checked by mempool_validate, so it's valid + if let (Some(block_time), Some(exp)) = + (block_time.as_ref(), &tx.header().expiration) + { + if block_time > exp { + return Err(()); + } + } + + tx.validate_tx().map_err(|_| ())?; + if let TxType::Wrapper(wrapper) = tx.header().tx_type { + // Check tx gas limit for tx size + let mut tx_gas_meter = TxGasMeter::new(wrapper.gas_limit); + tx_gas_meter.add_tx_size_gas(tx_bytes).map_err(|_| ())?; + + // Check replay protection + self.replay_protection_checks(&tx, tx_bytes, temp_wl_storage) + .map_err(|_| ())?; + + // Check fees + let fee_unshield = + wrapper.unshield_section_hash.and_then(|ref hash| { + tx.get_section(hash).and_then(|section| { + if let Section::MaspTx(transaction) = section.as_ref() { + Some(transaction.to_owned()) + } else { + None + } + }) + }); + match self.wrapper_fee_check( + &wrapper, + fee_unshield, + temp_wl_storage, + vp_wasm_cache, + tx_wasm_cache, + Some(block_proposer), + ) { + Ok(()) => Ok(u64::from(wrapper.gas_limit)), + Err(_) => Err(()), + } + } else { + Err(()) + } + } + /// Builds a batch of DKG decrypted transactions. // NOTE: we won't have frontrunning protection until V2 of the // Anoma protocol; Namada runs V1, therefore this method is @@ -197,8 +285,8 @@ where // - https://github.com/anoma/ferveo fn build_decrypted_txs( &self, - mut alloc: BlockSpaceAllocator, - ) -> (Vec, BlockSpaceAllocator) { + mut alloc: BlockAllocator, + ) -> (Vec, BlockAllocator) { // TODO: This should not be hardcoded let privkey = ::G2Affine::prime_subgroup_generator(); @@ -211,6 +299,7 @@ where .map( |TxInQueue { tx, + gas: _, #[cfg(not(feature = "mainnet"))] has_valid_pow, }| { @@ -239,7 +328,7 @@ where .take_while(|tx_bytes| { alloc.try_alloc(&tx_bytes[..]).map_or_else( |status| match status { - AllocFailure::Rejected { bin_space_left } => { + AllocFailure::Rejected { bin_resource_left: bin_space_left } => { tracing::warn!( ?tx_bytes, bin_space_left, @@ -249,7 +338,7 @@ where ); false } - AllocFailure::OverflowsBin { bin_size } => { + AllocFailure::OverflowsBin { bin_resource: bin_size } => { tracing::warn!( ?tx_bytes, bin_size, @@ -273,7 +362,7 @@ where #[cfg(feature = "abcipp")] fn build_protocol_txs( &self, - _alloc: BlockSpaceAllocator, + _alloc: BlockAllocator, local_last_commit: Option, ) -> Vec { // genesis should not contain vote extensions. @@ -344,7 +433,7 @@ where #[cfg(not(feature = "abcipp"))] fn build_protocol_txs( &self, - mut alloc: BlockSpaceAllocator, + mut alloc: BlockAllocator, txs: &[TxBytes], ) -> Vec { if self.wl_storage.storage.last_block.is_none() { @@ -363,7 +452,7 @@ where alloc.try_alloc(&tx_bytes[..]) .map_or_else( |status| match status { - AllocFailure::Rejected { bin_space_left } => { + AllocFailure::Rejected { bin_resource_left} => { // TODO: maybe we should find a way to include // validator set updates all the time. for instance, // we could have recursive bins -> bin space within @@ -374,19 +463,19 @@ where // changes (issue #367) tracing::debug!( ?tx_bytes, - bin_space_left, + bin_resource_left, proposal_height = ?pos_queries.get_current_decision_height(), "Dropping protocol tx from the current proposal", ); false } - AllocFailure::OverflowsBin { bin_size } => { + AllocFailure::OverflowsBin { bin_resource} => { // TODO: handle tx whose size is greater // than bin size tracing::warn!( ?tx_bytes, - bin_size, + bin_resource, proposal_height = ?pos_queries.get_current_decision_height(), "Dropping large protocol tx from the current proposal", @@ -414,6 +503,7 @@ const fn not_enough_voting_power_msg() -> &'static str { // TODO: write tests for validator set update vote extensions in // prepare proposals mod test_prepare_proposal { + use std::collections::BTreeSet; #[cfg(feature = "abcipp")] use std::collections::{BTreeSet, HashMap}; @@ -421,17 +511,25 @@ mod test_prepare_proposal { use namada::core::ledger::storage_api::collections::lazy_map::{ NestedSubKey, SubKey, }; + use namada::ledger::gas::Gas; use namada::ledger::pos::PosQueries; use namada::ledger::replay_protection; - use namada::proof_of_stake::{consensus_validator_set_handle, Epoch}; + use namada::proof_of_stake::btree_set::BTreeSetShims; + use namada::proof_of_stake::types::WeightedValidator; + use namada::proof_of_stake::{ + consensus_validator_set_handle, + read_consensus_validator_set_addresses_with_stake, Epoch, + }; #[cfg(feature = "abcipp")] use namada::proto::SignableEthMessage; use namada::proto::{Code, Data, Header, Section, Signature, Signed}; + use namada::types::address::{self, Address}; use namada::types::ethereum_events::EthereumEvent; #[cfg(feature = "abcipp")] use namada::types::key::common; use namada::types::key::RefTo; use namada::types::storage::BlockHeight; + use namada::types::token; use namada::types::token::Amount; use namada::types::transaction::protocol::EthereumTxData; use namada::types::transaction::{Fee, TxType, WrapperTx}; @@ -453,8 +551,9 @@ mod test_prepare_proposal { #[cfg(feature = "abcipp")] use crate::node::ledger::shell::test_utils::setup_at_height; use crate::node::ledger::shell::test_utils::{ - self, gen_keypair, TestShell, + self, gen_keypair, get_pkh_from_address, TestShell, }; + use crate::node::ledger::shims::abcipp_shim_types::shim::request::FinalizeBlock; use crate::wallet; #[cfg(feature = "abcipp")] @@ -537,6 +636,8 @@ mod test_prepare_proposal { } } + const GAS_LIMIT_MULTIPLIER: u64 = 300_000; + /// Test that if a tx from the mempool is not a /// WrapperTx type, it is not included in the /// proposed block. @@ -565,7 +666,7 @@ mod test_prepare_proposal { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Default::default(), + amount_per_gas_unit: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), @@ -573,6 +674,7 @@ mod test_prepare_proposal { Default::default(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -787,14 +889,21 @@ mod test_prepare_proposal { should_panic(expected = "A Tendermint quorum should never") )] fn test_prepare_proposal_vext_insufficient_voting_power() { + use crate::facade::tendermint_proto::abci::{Validator, VoteInfo}; + const FIRST_HEIGHT: BlockHeight = BlockHeight(1); const LAST_HEIGHT: BlockHeight = BlockHeight(FIRST_HEIGHT.0 + 11); let (mut shell, _recv, _, _oracle_control_recv) = - test_utils::setup_at_height(FIRST_HEIGHT); + test_utils::setup_with_cfg(test_utils::SetupCfg { + last_height: FIRST_HEIGHT, + num_validators: 2, + }); + + let params = shell.wl_storage.pos_queries().get_pos_params(); // artificially change the voting power of the default validator to - // zero, change the block height, and commit a dummy block, + // one, change the block height, and commit a dummy block, // to move to a new epoch let events_epoch = shell .wl_storage @@ -817,18 +926,71 @@ mod test_prepare_proposal { (stake, position, address) }) .collect::>(); + + let mut consensus_set: BTreeSet = + read_consensus_validator_set_addresses_with_stake( + &shell.wl_storage, + Epoch::default(), + ) + .unwrap() + .into_iter() + .collect(); + let val1 = consensus_set.pop_first_shim().unwrap(); + let val2 = consensus_set.pop_first_shim().unwrap(); + let pkh1 = get_pkh_from_address( + &shell.wl_storage, + ¶ms, + val1.address.clone(), + Epoch::default(), + ); + let pkh2 = get_pkh_from_address( + &shell.wl_storage, + ¶ms, + val2.address.clone(), + Epoch::default(), + ); + for (val_stake, val_position, address) in consensus_in_mem.into_iter() { - validators_handle - .at(&val_stake) - .remove(&mut shell.wl_storage, &val_position) - .expect("Test failed"); - validators_handle - .at(&0.into()) - .insert(&mut shell.wl_storage, val_position, address) - .expect("Test failed"); + if address == wallet::defaults::validator_address() { + validators_handle + .at(&val_stake) + .remove(&mut shell.wl_storage, &val_position) + .expect("Test failed"); + validators_handle + .at(&1.into()) + .insert(&mut shell.wl_storage, val_position, address) + .expect("Test failed"); + } } - - shell.start_new_epoch(); + // Insert some stake for the second validator to prevent total stake + // from going to 0 + + let votes = vec![ + VoteInfo { + validator: Some(Validator { + address: pkh1.clone(), + power: u128::try_from(val1.bonded_stake) + .expect("Test failed") + as i64, + }), + signed_last_block: true, + }, + VoteInfo { + validator: Some(Validator { + address: pkh2, + power: u128::try_from(val2.bonded_stake) + .expect("Test failed") + as i64, + }), + signed_last_block: true, + }, + ]; + let req = FinalizeBlock { + proposer_address: pkh1, + votes, + ..Default::default() + }; + shell.start_new_epoch(Some(req)); assert_eq!( shell.wl_storage.pos_queries().get_epoch( shell.wl_storage.pos_queries().get_current_decision_height() @@ -932,6 +1094,20 @@ mod test_prepare_proposal { let mut expected_wrapper = vec![]; let mut expected_decrypted = vec![]; + // Load some tokens to tx signer to pay fees + let balance_key = token::balance_key( + &shell.wl_storage.storage.native_token, + &Address::from(&keypair.ref_to()), + ); + shell + .wl_storage + .storage + .write( + &balance_key, + Amount::native_whole(1_000).try_to_vec().unwrap(), + ) + .unwrap(); + let mut req = RequestPrepareProposal { txs: vec![], ..Default::default() @@ -942,14 +1118,15 @@ mod test_prepare_proposal { let mut tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Default::default(), + amount_per_gas_unit: 1.into(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); tx.header.chain_id = shell.chain_id.clone(); tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -961,7 +1138,12 @@ mod test_prepare_proposal { &keypair, ))); - shell.enqueue_tx(tx.clone()); + let gas = Gas::from( + tx.header().wrapper().expect("Wrong tx type").gas_limit, + ) + .checked_sub(Gas::from(tx.to_bytes().len() as u64)) + .unwrap(); + shell.enqueue_tx(tx.clone(), gas); expected_wrapper.push(tx.clone()); req.txs.push(tx.to_bytes()); tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted { @@ -1011,7 +1193,7 @@ mod test_prepare_proposal { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount_per_gas_unit: 0.into(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), @@ -1019,6 +1201,7 @@ mod test_prepare_proposal { Default::default(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -1063,14 +1246,15 @@ mod test_prepare_proposal { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount_per_gas_unit: 1.into(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -1104,7 +1288,7 @@ mod test_prepare_proposal { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::zero(), + amount_per_gas_unit: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), @@ -1112,6 +1296,7 @@ mod test_prepare_proposal { Default::default(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -1157,44 +1342,42 @@ mod test_prepare_proposal { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount_per_gas_unit: 1.into(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); let tx_code = Code::new("wasm_code".as_bytes().to_owned()); - wrapper.set_code(tx_code.clone()); + wrapper.set_code(tx_code); let tx_data = Data::new("transaction data".as_bytes().to_owned()); - wrapper.set_data(tx_data.clone()); + wrapper.set_data(tx_data); + let mut new_wrapper = wrapper.clone(); wrapper.add_section(Section::Signature(Signature::new( wrapper.sechashes(), &keypair, ))); - let mut new_wrapper = - Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( - Fee { - amount: 0.into(), - token: shell.wl_storage.storage.native_token.clone(), - }, - keypair_2.ref_to(), - Epoch(0), - Default::default(), - #[cfg(not(feature = "mainnet"))] - None, - )))); - new_wrapper.header.chain_id = shell.chain_id.clone(); - new_wrapper.header.timestamp = wrapper.header.timestamp; - new_wrapper.set_code(tx_code); - new_wrapper.set_data(tx_data); + new_wrapper.update_header(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 1.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + keypair_2.ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); new_wrapper.add_section(Section::Signature(Signature::new( wrapper.sechashes(), - &keypair, + &keypair_2, ))); let req = RequestPrepareProposal { @@ -1216,11 +1399,10 @@ mod test_prepare_proposal { fn test_expired_wrapper_tx() { let (shell, _recv, _, _) = test_utils::setup(); let keypair = gen_keypair(); - let tx_time = DateTimeUtc::now(); let mut wrapper_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount_per_gas_unit: 1.into(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), @@ -1228,9 +1410,10 @@ mod test_prepare_proposal { Default::default(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper_tx.header.chain_id = shell.chain_id.clone(); - wrapper_tx.header.expiration = Some(tx_time); + wrapper_tx.header.expiration = Some(DateTimeUtc::default()); wrapper_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); wrapper_tx .set_data(Data::new("transaction data".as_bytes().to_owned())); @@ -1255,4 +1438,244 @@ mod test_prepare_proposal { eprintln!("Proposal: {:?}", result.txs); assert_eq!(result.txs.len(), 0); } + + /// 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, _recv, _, _) = test_utils::setup(); + + let block_gas_limit = + namada::core::ledger::gas::get_max_block_gas(&shell.wl_storage) + .unwrap(); + let keypair = gen_keypair(); + + let wrapper = WrapperTx::new( + Fee { + amount_per_gas_unit: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + keypair.ref_to(), + Epoch(0), + (block_gas_limit + 1).into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + ); + let mut wrapper_tx = Tx::from_type(TxType::Wrapper(Box::new(wrapper))); + wrapper_tx.header.chain_id = shell.chain_id.clone(); + wrapper_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper_tx + .set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper_tx.add_section(Section::Signature(Signature::new( + wrapper_tx.sechashes(), + &keypair, + ))); + + let req = RequestPrepareProposal { + txs: vec![wrapper_tx.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, _recv, _, _) = test_utils::setup(); + let keypair = gen_keypair(); + + let wrapper = WrapperTx::new( + Fee { + amount_per_gas_unit: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + keypair.ref_to(), + Epoch(0), + 0.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + ); + + let mut wrapper_tx = Tx::from_type(TxType::Wrapper(Box::new(wrapper))); + wrapper_tx.header.chain_id = shell.chain_id.clone(); + wrapper_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper_tx + .set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper_tx.add_section(Section::Signature(Signature::new( + wrapper_tx.sechashes(), + &keypair, + ))); + + let req = RequestPrepareProposal { + txs: vec![wrapper_tx.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 using a non-whitelisted token for fee payment is not + // included in the block + #[test] + fn test_fee_non_whitelisted_token() { + let (shell, _recv, _, _) = test_utils::setup(); + + let wrapper = WrapperTx::new( + Fee { + amount_per_gas_unit: 100.into(), + token: address::btc(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + ); + + let mut wrapper_tx = Tx::from_type(TxType::Wrapper(Box::new(wrapper))); + wrapper_tx.header.chain_id = shell.chain_id.clone(); + wrapper_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper_tx + .set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper_tx.add_section(Section::Signature(Signature::new( + wrapper_tx.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + + let req = RequestPrepareProposal { + txs: vec![wrapper_tx.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 setting a fee amount lower than the minimum required + // is not included in the block + #[test] + fn test_fee_wrong_minimum_amount() { + let (shell, _recv, _, _) = test_utils::setup(); + + let wrapper = WrapperTx::new( + Fee { + amount_per_gas_unit: 0.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + ); + let mut wrapper_tx = Tx::from_type(TxType::Wrapper(Box::new(wrapper))); + wrapper_tx.header.chain_id = shell.chain_id.clone(); + wrapper_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper_tx + .set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper_tx.add_section(Section::Signature(Signature::new( + wrapper_tx.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + + let req = RequestPrepareProposal { + txs: vec![wrapper_tx.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 transactions whose fees cannot be paid is rejected + #[test] + fn test_insufficient_balance_for_fee() { + let (shell, _recv, _, _) = test_utils::setup(); + + let wrapper = WrapperTx::new( + Fee { + amount_per_gas_unit: 1_000_000_000.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + ); + let mut wrapper_tx = Tx::from_type(TxType::Wrapper(Box::new(wrapper))); + wrapper_tx.header.chain_id = shell.chain_id.clone(); + wrapper_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper_tx + .set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper_tx.add_section(Section::Signature(Signature::new( + wrapper_tx.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + + let req = RequestPrepareProposal { + txs: vec![wrapper_tx.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 fee overflow in the wrapper transaction is rejected + #[test] + fn test_wrapper_fee_overflow() { + let (shell, _recv, _, _) = test_utils::setup(); + + let wrapper = WrapperTx::new( + Fee { + amount_per_gas_unit: token::Amount::max(), + token: shell.wl_storage.storage.native_token.clone(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + ); + let mut wrapper_tx = Tx::from_type(TxType::Wrapper(Box::new(wrapper))); + wrapper_tx.header.chain_id = shell.chain_id.clone(); + wrapper_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper_tx + .set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper_tx.add_section(Section::Signature(Signature::new( + wrapper_tx.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + + let req = RequestPrepareProposal { + txs: vec![wrapper_tx.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 5010a4f211..f4ffda1601 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -7,6 +7,7 @@ use namada::core::ledger::storage::WlStorage; use namada::ledger::eth_bridge::{EthBridgeQueries, SendValsetUpd}; use namada::ledger::pos::PosQueries; use namada::ledger::storage::TempWlStorage; +use namada::proof_of_stake::find_validator_by_raw_hash; use namada::types::internal::TxInQueue; use namada::types::transaction::protocol::{ ethereum_tx_data_variants, ProtocolTxType, @@ -14,12 +15,11 @@ use namada::types::transaction::protocol::{ #[cfg(feature = "abcipp")] use namada::types::voting_power::FractionalVotingPower; +use super::block_alloc::{BlockSpace, EncryptedTxsBins}; use super::*; use crate::facade::tendermint_proto::abci::response_process_proposal::ProposalStatus; use crate::facade::tendermint_proto::abci::RequestProcessProposal; -use crate::node::ledger::shell::block_space_alloc::{ - threshold, AllocFailure, TxBin, -}; +use crate::node::ledger::shell::block_alloc::{AllocFailure, TxBin}; use crate::node::ledger::shims::abcipp_shim_types::shim::response::ProcessProposal; use crate::node::ledger::shims::abcipp_shim_types::shim::TxBytes; @@ -27,13 +27,13 @@ use crate::node::ledger::shims::abcipp_shim_types::shim::TxBytes; /// transaction numbers, in a block proposal. #[derive(Default)] pub struct ValidationMeta { + /// Space and gas utilized by encrypted txs. + pub encrypted_txs_bins: EncryptedTxsBins, /// Vote extension digest counters. #[cfg(feature = "abcipp")] pub digests: DigestCounters, - /// Space utilized by encrypted txs. - pub encrypted_txs_bin: TxBin, /// Space utilized by all txs. - pub txs_bin: TxBin, + pub txs_bin: TxBin, /// Check if the decrypted tx queue has any elements /// left. /// @@ -52,15 +52,17 @@ where fn from(wl_storage: &WlStorage) -> Self { let max_proposal_bytes = wl_storage.pos_queries().get_max_proposal_bytes().get(); + let max_block_gas = + namada::core::ledger::gas::get_max_block_gas(wl_storage).unwrap(); let encrypted_txs_bin = - TxBin::init_over_ratio(max_proposal_bytes, threshold::ONE_THIRD); + EncryptedTxsBins::new(max_proposal_bytes, max_block_gas); let txs_bin = TxBin::init(max_proposal_bytes); Self { #[cfg(feature = "abcipp")] digests: DigestCounters::default(), decrypted_queue_has_remaining_txs: false, has_decrypted_txs: false, - encrypted_txs_bin, + encrypted_txs_bins: encrypted_txs_bin, txs_bin, } } @@ -101,6 +103,14 @@ where &self, req: RequestProcessProposal, ) -> ProcessProposal { + let tm_raw_hash_string = tm_raw_hash_to_string(&req.proposer_address); + let block_proposer = + find_validator_by_raw_hash(&self.wl_storage, tm_raw_hash_string) + .unwrap() + .expect( + "Unable to find native validator address of block \ + proposer from tendermint raw hash", + ); tracing::info!( proposer = ?HEXUPPER.encode(&req.proposer_address), height = req.height, @@ -219,8 +229,22 @@ where n_txs = req.txs.len(), "Received block proposal", ); - let (tx_results, meta) = - self.process_txs(&req.txs, self.get_block_timestamp(req.time)); + let native_block_proposer_address = { + let tm_raw_hash_string = + tm_raw_hash_to_string(&req.proposer_address); + find_validator_by_raw_hash(&self.wl_storage, tm_raw_hash_string) + .unwrap() + .expect( + "Unable to find native validator address of block \ + proposer from tendermint raw hash", + ) + }; + + let (tx_results, meta) = self.process_txs( + &req.txs, + self.get_block_timestamp(req.time), + &native_block_proposer_address, + ); // Erroneous transactions were detected when processing // the leader's proposal. We allow txs that do not @@ -278,10 +302,14 @@ where &self, txs: &[TxBytes], block_time: DateTimeUtc, + block_proposer: &Address, ) -> (Vec, ValidationMeta) { 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 vp_wasm_cache = self.vp_wasm_cache.clone(); + let mut tx_wasm_cache = self.tx_wasm_cache.clone(); + let tx_results: Vec<_> = txs .iter() .map(|tx_bytes| { @@ -291,6 +319,9 @@ where &mut metadata, &mut temp_wl_storage, block_time, + &mut vp_wasm_cache, + &mut tx_wasm_cache, + block_proposer, ); let error_code = ErrorCodes::from_u32(result.code).unwrap(); if let ErrorCodes::Ok = error_code { @@ -410,14 +441,21 @@ 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 check_proposal_tx<'a>( + #[allow(clippy::too_many_arguments)] + pub fn check_proposal_tx<'a, CA>( &self, tx_bytes: &[u8], tx_queue_iter: &mut impl Iterator, metadata: &mut ValidationMeta, temp_wl_storage: &mut TempWlStorage, block_time: DateTimeUtc, - ) -> TxResult { + vp_wasm_cache: &mut VpCache, + tx_wasm_cache: &mut TxCache, + block_proposer: &Address, + ) -> TxResult + where + CA: 'static + WasmCacheAccess + Sync, + { // try to allocate space for this tx if let Err(e) = metadata.txs_bin.try_dump(tx_bytes) { return TxResult { @@ -682,7 +720,7 @@ where metadata.has_decrypted_txs = true; match tx_queue_iter.next() { Some(wrapper) => { - let mut inner_tx = tx; + let mut inner_tx = tx.clone(); inner_tx.update_header(TxType::Raw); if wrapper .tx @@ -731,14 +769,14 @@ where }; } } + TxResult { code: ErrorCodes::Ok.into(), info: "Process Proposal accepted this \ - transaction" + tranasction" .into(), } } else { - // Wrong inner tx commitment TxResult { code: ErrorCodes::InvalidTx.into(), info: "The encrypted payload of tx was \ @@ -755,6 +793,36 @@ where } } TxType::Wrapper(wrapper) => { + // Account for gas and space. This is done even if the + // transaction is later deemed invalid, to + // incentivize the proposer to include only + // valid transaction and avoid wasting block + // resources (ABCI only) + let mut tx_gas_meter = TxGasMeter::new(wrapper.gas_limit); + if tx_gas_meter.add_tx_size_gas(tx_bytes).is_err() { + // Account for the tx's resources even in case of an error. + // Ignore any allocation error + let _ = metadata + .encrypted_txs_bins + .try_dump(tx_bytes, u64::from(wrapper.gas_limit)); + + return TxResult { + code: ErrorCodes::TxGasLimit.into(), + info: "Wrapper transactions exceeds its gas limit" + .to_string(), + }; + } + + // try to allocate space and gas for this encrypted tx + if let Err(e) = metadata + .encrypted_txs_bins + .try_dump(tx_bytes, u64::from(wrapper.gas_limit)) + { + return TxResult { + code: ErrorCodes::AllocationError.into(), + info: e, + }; + } // decrypted txs shouldn't show up before wrapper txs if metadata.has_decrypted_txs { return TxResult { @@ -764,23 +832,6 @@ where .into(), }; } - // try to allocate space for this encrypted tx - if let Err(e) = metadata.encrypted_txs_bin.try_dump(tx_bytes) { - return TxResult { - code: ErrorCodes::AllocationError.into(), - info: match e { - AllocFailure::Rejected { .. } => { - "No more space left in the block for wrapper \ - txs" - } - AllocFailure::OverflowsBin { .. } => { - "The given wrapper tx is larger than 1/3 of \ - the available block space" - } - } - .into(), - }; - } if hints::unlikely(self.encrypted_txs_not_allowed()) { return TxResult { code: ErrorCodes::AllocationError.into(), @@ -837,39 +888,37 @@ where }; } - // If the public key corresponds to the MASP sentinel - // transaction key, then the fee payer is effectively - // the MASP, otherwise derive - // the payer from public key. - let gas_payer = if wrapper.pk != masp_tx_key().ref_to() { - wrapper.gas_payer() - } else { - masp() - }; - // check that the fee payer has sufficient balance - let balance = - self.get_balance(&wrapper.fee.token, &gas_payer); - - // In testnets, tx is allowed to skip fees if it - // includes a valid PoW - #[cfg(not(feature = "mainnet"))] - let has_valid_pow = self.has_valid_pow_solution(&wrapper); - #[cfg(feature = "mainnet")] - let has_valid_pow = false; + // Check that the fee payer has sufficient balance. + let fee_unshield = + wrapper.unshield_section_hash.and_then(|ref hash| { + tx.get_section(hash).and_then(|section| { + if let Section::MaspTx(transaction) = + section.as_ref() + { + Some(transaction.to_owned()) + } else { + None + } + }) + }); - if has_valid_pow || self.get_wrapper_tx_fees() <= balance { - TxResult { + match self.wrapper_fee_check( + &wrapper, + fee_unshield, + temp_wl_storage, + vp_wasm_cache, + tx_wasm_cache, + Some(block_proposer), + ) { + Ok(()) => TxResult { code: ErrorCodes::Ok.into(), info: "Process proposal accepted this transaction" .into(), - } - } else { - TxResult { - code: ErrorCodes::InvalidTx.into(), - info: "The address given does not have sufficient \ - balance to pay fee" - .into(), - } + }, + Err(e) => TxResult { + code: ErrorCodes::FeeError.into(), + info: e.to_string(), + }, } } } @@ -951,7 +1000,7 @@ mod test_process_proposal { #[cfg(feature = "abcipp")] use assert_matches::assert_matches; - use namada::ledger::parameters::storage::get_wrapper_tx_fees_key; + use namada::ledger::storage_api::StorageWrite; use namada::proto::{ Code, Data, Section, SignableEthMessage, Signature, Signed, }; @@ -963,7 +1012,7 @@ mod test_process_proposal { use namada::types::token; use namada::types::token::Amount; use namada::types::transaction::protocol::EthereumTxData; - use namada::types::transaction::{Fee, WrapperTx, MIN_FEE}; + use namada::types::transaction::{Fee, WrapperTx}; #[cfg(feature = "abcipp")] use namada::types::vote_extensions::bridge_pool_roots::MultiSignedVext; #[cfg(feature = "abcipp")] @@ -980,6 +1029,8 @@ mod test_process_proposal { use crate::node::ledger::shims::abcipp_shim_types::shim::TxBytes; use crate::wallet; + const GAS_LIMIT_MULTIPLIER: u64 = 100_000; + #[cfg(feature = "abcipp")] fn get_empty_eth_ev_digest(shell: &TestShell) -> TxBytes { let protocol_key = shell.mode.get_protocol_key().expect("Test failed"); @@ -1535,20 +1586,21 @@ mod test_process_proposal { /// by [`process_proposal`]. #[test] fn test_unsigned_wrapper_rejected() { - let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); + let (shell, _recv, _, _) = test_utils::setup_at_height(3u64); let keypair = gen_keypair(); let public_key = keypair.ref_to(); let mut outer_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Default::default(), + amount_per_gas_unit: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, public_key, Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); outer_tx.header.chain_id = shell.chain_id.clone(); outer_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -1587,19 +1639,21 @@ mod test_process_proposal { /// rejected #[test] fn test_wrapper_bad_signature_rejected() { - let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); + let (shell, _recv, _, _) = test_utils::setup_at_height(3u64); let keypair = gen_keypair(); let mut outer_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::from_uint(100, 0).expect("Test failed"), + amount_per_gas_unit: Amount::from_uint(100, 0) + .expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); outer_tx.header.chain_id = shell.chain_id.clone(); outer_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -1611,7 +1665,7 @@ mod test_process_proposal { let mut new_tx = outer_tx.clone(); if let TxType::Wrapper(wrapper) = &mut new_tx.header.tx_type { // we mount a malleability attack to try and remove the fee - wrapper.fee.amount = Default::default(); + wrapper.fee.amount_per_gas_unit = Default::default(); } else { panic!("Test failed") }; @@ -1649,26 +1703,30 @@ mod test_process_proposal { #[test] fn test_wrapper_unknown_address() { let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); + 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 - .storage - .write( - &get_wrapper_tx_fees_key(), - token::Amount::native_whole(MIN_FEE).try_to_vec().unwrap(), - ) + .write(&balance_key, Amount::native_whole(99)) .unwrap(); let keypair = gen_keypair(); let mut outer_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::from_uint(1, 0).expect("Test failed"), + amount_per_gas_unit: Amount::from_uint(1, 0) + .expect("Test failed"), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); outer_tx.header.chain_id = shell.chain_id.clone(); outer_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -1694,11 +1752,14 @@ mod test_process_proposal { panic!("Test failed") } }; - assert_eq!(response.result.code, u32::from(ErrorCodes::InvalidTx)); + assert_eq!(response.result.code, u32::from(ErrorCodes::FeeError)); assert_eq!( response.result.info, - "The address given does not have sufficient balance to pay fee" - .to_string(), + String::from( + "Error trying to apply a transaction: Error while processing \ + transaction's fees: Insufficient transparent balance to pay \ + fees" + ) ); } @@ -1709,36 +1770,29 @@ mod test_process_proposal { fn test_wrapper_insufficient_balance_address() { let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); 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::native_whole(99).try_to_vec().unwrap()) - .unwrap(); - shell - .wl_storage - .write_log - .write( - &get_wrapper_tx_fees_key(), - token::Amount::native_whole(MIN_FEE).try_to_vec().unwrap(), - ) + .write(&balance_key, Amount::native_whole(99)) .unwrap(); + shell.commit(); let mut outer_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::native_whole(1_000_100), + amount_per_gas_unit: Amount::native_whole(1_000_100), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); outer_tx.header.chain_id = shell.chain_id.clone(); outer_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -1764,11 +1818,13 @@ mod test_process_proposal { panic!("Test failed") } }; - assert_eq!(response.result.code, u32::from(ErrorCodes::InvalidTx)); + assert_eq!(response.result.code, u32::from(ErrorCodes::FeeError)); assert_eq!( response.result.info, String::from( - "The address given does not have sufficient balance to pay fee" + "Error trying to apply a transaction: Error while processing \ + transaction's fees: Insufficient transparent balance to pay \ + fees" ) ); } @@ -1784,21 +1840,26 @@ mod test_process_proposal { let mut outer_tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::native_whole(i as u64), + amount_per_gas_unit: Amount::native_whole(i as u64), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); outer_tx.header.chain_id = shell.chain_id.clone(); outer_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); outer_tx.set_data(Data::new( format!("transaction data: {}", i).as_bytes().to_owned(), )); - shell.enqueue_tx(outer_tx.clone()); + let gas_limit = + Gas::from(outer_tx.header().wrapper().unwrap().gas_limit) + .checked_sub(Gas::from(outer_tx.to_bytes().len() as u64)) + .unwrap(); + shell.enqueue_tx(outer_tx.clone(), gas_limit); outer_tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted { #[cfg(not(feature = "mainnet"))] @@ -1862,19 +1923,23 @@ mod test_process_proposal { let mut tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Default::default(), + amount_per_gas_unit: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); tx.header.chain_id = shell.chain_id.clone(); tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); tx.set_data(Data::new("transaction data".as_bytes().to_owned())); - shell.enqueue_tx(tx.clone()); + let gas_limit = Gas::from(tx.header().wrapper().unwrap().gas_limit) + .checked_sub(Gas::from(tx.to_bytes().len() as u64)) + .unwrap(); + shell.enqueue_tx(tx.clone(), gas_limit); tx.header.tx_type = TxType::Decrypted(DecryptedTx::Undecryptable); @@ -1914,14 +1979,15 @@ mod test_process_proposal { let mut tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Default::default(), + amount_per_gas_unit: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); tx.header.chain_id = shell.chain_id.clone(); tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -1929,7 +1995,11 @@ mod test_process_proposal { tx.set_code_sechash(Hash([0u8; 32])); tx.set_data_sechash(Hash([0u8; 32])); - shell.enqueue_tx(tx.clone()); + let gas_limit = Gas::from(tx.header().wrapper().unwrap().gas_limit) + .checked_sub(Gas::from(tx.to_bytes().len() as u64)) + .unwrap(); + shell.enqueue_tx(tx.clone(), gas_limit); + tx.header.tx_type = TxType::Decrypted(DecryptedTx::Undecryptable); let response = { @@ -1959,21 +2029,25 @@ mod test_process_proposal { // not valid tx bytes let wrapper = WrapperTx { fee: Fee { - amount: Default::default(), + amount_per_gas_unit: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, pk: keypair.ref_to(), epoch: Epoch(0), - gas_limit: Default::default(), + gas_limit: GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] pow_solution: None, + unshield_section_hash: None, }; let tx = Tx::from_type(TxType::Wrapper(Box::new(wrapper))); let mut decrypted = tx.clone(); decrypted.update_header(TxType::Decrypted(DecryptedTx::Undecryptable)); - shell.enqueue_tx(tx); + let gas_limit = Gas::from(tx.header().wrapper().unwrap().gas_limit) + .checked_sub(Gas::from(tx.to_bytes().len() as u64)) + .unwrap(); + shell.enqueue_tx(tx, gas_limit); let response = { let request = ProcessProposal { @@ -1996,7 +2070,7 @@ mod test_process_proposal { /// [`process_proposal`] than expected, they are rejected #[test] fn test_too_many_decrypted_txs() { - let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); + let (shell, _recv, _, _) = test_utils::setup_at_height(3u64); let mut tx = Tx::from_type(TxType::Decrypted(DecryptedTx::Decrypted { #[cfg(not(feature = "mainnet"))] has_valid_pow: false, @@ -2029,7 +2103,7 @@ mod test_process_proposal { /// Process Proposal should reject a block containing a RawTx, but not panic #[test] fn test_raw_tx_rejected() { - let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); + let (shell, _recv, _, _) = test_utils::setup_at_height(3u64); let keypair = crate::wallet::defaults::daewon_keypair(); @@ -2075,14 +2149,15 @@ mod test_process_proposal { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::zero(), + amount_per_gas_unit: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); @@ -2150,14 +2225,15 @@ mod test_process_proposal { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::zero(), + amount_per_gas_unit: 1.into(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -2208,14 +2284,15 @@ mod test_process_proposal { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::zero(), + amount_per_gas_unit: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -2262,50 +2339,23 @@ mod test_process_proposal { /// rejected #[test] fn test_inner_tx_hash_same_block() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let (shell, _recv, _, _) = test_utils::setup(); let keypair = crate::wallet::defaults::daewon_keypair(); let keypair_2 = crate::wallet::defaults::daewon_keypair(); - // Add unshielded balance for fee payment - let balance_key = token::balance_key( - &shell.wl_storage.storage.native_token, - &Address::from(&keypair.ref_to()), - ); - shell - .wl_storage - .storage - .write( - &balance_key, - Amount::native_whole(1000).try_to_vec().unwrap(), - ) - .unwrap(); - - // Add unshielded balance for fee payment - let balance_key = token::balance_key( - &shell.wl_storage.storage.native_token, - &Address::from(&keypair_2.ref_to()), - ); - shell - .wl_storage - .storage - .write( - &balance_key, - Amount::native_whole(1000).try_to_vec().unwrap(), - ) - .unwrap(); - let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::zero(), + amount_per_gas_unit: 1.into(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -2320,18 +2370,19 @@ mod test_process_proposal { new_wrapper.update_header(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::zero(), + amount_per_gas_unit: 1.into(), token: shell.wl_storage.storage.native_token.clone(), }, keypair_2.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); new_wrapper.add_section(Section::Signature(Signature::new( new_wrapper.sechashes(), - &keypair, + &keypair_2, ))); // Run validation @@ -2362,20 +2413,21 @@ mod test_process_proposal { /// causes the entire block to be rejected #[test] fn test_wrong_chain_id() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let (shell, _recv, _, _) = test_utils::setup(); let keypair = crate::wallet::defaults::daewon_keypair(); let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::zero(), + amount_per_gas_unit: Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); let wrong_chain_id = ChainId("Wrong chain id".to_string()); wrapper.header.chain_id = wrong_chain_id.clone(); @@ -2431,14 +2483,15 @@ mod test_process_proposal { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: token::Amount::zero(), + amount_per_gas_unit: token::Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = wrong_chain_id.clone(); wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); @@ -2453,8 +2506,12 @@ mod test_process_proposal { decrypted.sechashes(), &keypair, ))); + let gas_limit = Gas::from(wrapper.header.wrapper().unwrap().gas_limit) + .checked_sub(Gas::from(wrapper.to_bytes().len() as u64)) + .unwrap(); let wrapper_in_queue = TxInQueue { tx: wrapper, + gas: gas_limit, has_valid_pow: false, }; shell.wl_storage.storage.tx_queue.push(wrapper_in_queue); @@ -2486,20 +2543,21 @@ mod test_process_proposal { /// Test that an expired wrapper transaction causes a block rejection #[test] fn test_expired_wrapper() { - let (mut shell, _recv, _, _) = test_utils::setup(); + let (shell, _recv, _, _) = test_utils::setup(); let keypair = crate::wallet::defaults::daewon_keypair(); let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: token::Amount::zero(), + amount_per_gas_unit: 1.into(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.header.expiration = Some(DateTimeUtc::default()); @@ -2535,14 +2593,15 @@ mod test_process_proposal { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: token::Amount::zero(), + amount_per_gas_unit: token::Amount::zero(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.header.expiration = Some(DateTimeUtc::default()); @@ -2558,8 +2617,12 @@ mod test_process_proposal { decrypted.sechashes(), &keypair, ))); + let gas_limit = Gas::from(wrapper.header.wrapper().unwrap().gas_limit) + .checked_sub(Gas::from(wrapper.to_bytes().len() as u64)) + .unwrap(); let wrapper_in_queue = TxInQueue { tx: wrapper, + gas: gas_limit, has_valid_pow: false, }; shell.wl_storage.storage.tx_queue.push(wrapper_in_queue); @@ -2580,6 +2643,264 @@ mod test_process_proposal { } } + /// Check that a tx requiring more gas than the block limit causes a block + /// rejection + #[test] + fn test_exceeding_max_block_gas_tx() { + let (shell, _recv, _, _) = test_utils::setup(); + + let block_gas_limit = + namada::core::ledger::gas::get_max_block_gas(&shell.wl_storage) + .unwrap(); + let keypair = super::test_utils::gen_keypair(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + keypair.ref_to(), + Epoch(0), + (block_gas_limit + 1).into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &keypair, + ))); + + // 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::AllocationError) + ); + } + } + } + + // Check that a wrapper requiring more gas than its limit causes a block + // rejection + #[test] + fn test_exceeding_gas_limit_wrapper() { + let (shell, _recv, _, _) = test_utils::setup(); + let keypair = super::test_utils::gen_keypair(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 100.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + keypair.ref_to(), + Epoch(0), + 0.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &keypair, + ))); + + // 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) + ); + } + } + } + + // Check that a wrapper using a non-whitelisted token for fee payment causes + // a block rejection + #[test] + fn test_fee_non_whitelisted_token() { + let (shell, _recv, _, _) = test_utils::setup(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 100.into(), + token: address::btc(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + + // 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::FeeError) + ); + } + } + } + + // Check that a wrapper setting a fee amount lower than the minimum required + // causes a block rejection + #[test] + fn test_fee_wrong_minimum_amount() { + let (shell, _recv, _, _) = test_utils::setup(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 0.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm code".as_bytes().to_owned())); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + + // 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::FeeError) + ); + } + } + } + + // Check that a wrapper transactions whose fees cannot be paid causes a + // block rejection + #[test] + fn test_insufficient_balance_for_fee() { + let (shell, _recv, _, _) = test_utils::setup(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: 1_000_000_000.into(), + token: shell.wl_storage.storage.native_token.clone(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + 150_000.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm code".as_bytes().to_owned())); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + + // 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::FeeError) + ); + } + } + } + + // Check that a fee overflow in the wrapper transaction causes a block + // rejection + #[test] + fn test_wrapper_fee_overflow() { + let (shell, _recv, _, _) = test_utils::setup(); + + let mut wrapper = + Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( + Fee { + amount_per_gas_unit: token::Amount::max(), + token: shell.wl_storage.storage.native_token.clone(), + }, + crate::wallet::defaults::albert_keypair().ref_to(), + Epoch(0), + GAS_LIMIT_MULTIPLIER.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + )))); + wrapper.header.chain_id = shell.chain_id.clone(); + wrapper.set_code(Code::new("wasm code".as_bytes().to_owned())); + wrapper.set_data(Data::new("transaction data".as_bytes().to_owned())); + wrapper.add_section(Section::Signature(Signature::new( + wrapper.sechashes(), + &crate::wallet::defaults::albert_keypair(), + ))); + + // 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::FeeError) + ); + } + } + } + /// Test if we reject wrapper txs when they shouldn't be included in blocks. /// /// Currently, the conditions to reject wrapper @@ -2592,14 +2913,15 @@ mod test_process_proposal { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: 0.into(), + amount_per_gas_unit: 0.into(), token: shell.wl_storage.storage.native_token.clone(), }, keypair.ref_to(), Epoch(0), - Default::default(), + GAS_LIMIT_MULTIPLIER.into(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.header.chain_id = shell.chain_id.clone(); wrapper.set_code(Code::new("wasm_code".as_bytes().to_owned())); diff --git a/apps/src/lib/node/ledger/shell/queries.rs b/apps/src/lib/node/ledger/shell/queries.rs index 6455a44665..a62c3ec4b4 100644 --- a/apps/src/lib/node/ledger/shell/queries.rs +++ b/apps/src/lib/node/ledger/shell/queries.rs @@ -139,10 +139,14 @@ mod test_queries { use namada::core::ledger::storage::EPOCH_SWITCH_BLOCKS_DELAY; use namada::ledger::eth_bridge::{EthBridgeQueries, SendValsetUpd}; use namada::ledger::pos::PosQueries; + use namada::proof_of_stake::types::WeightedValidator; use namada::types::storage::Epoch; use super::*; + use crate::facade::tendermint_proto::abci::VoteInfo; use crate::node::ledger::shell::test_utils; + use crate::node::ledger::shell::test_utils::get_pkh_from_address; + use crate::node::ledger::shims::abcipp_shim_types::shim::request::FinalizeBlock; macro_rules! test_must_send_valset_upd { (epoch_assertions: $epoch_assertions:expr $(,)?) => { @@ -216,7 +220,41 @@ mod test_queries { // ); // } // ``` - shell.finalize_and_commit(); + let params = + shell.wl_storage.pos_queries().get_pos_params(); + let consensus_set: Vec = + read_consensus_validator_set_addresses_with_stake( + &shell.wl_storage, + Epoch::default(), + ) + .unwrap() + .into_iter() + .collect(); + + let val1 = consensus_set[0].clone(); + let pkh1 = get_pkh_from_address( + &shell.wl_storage, + ¶ms, + val1.address.clone(), + Epoch::default(), + ); + let votes = vec![VoteInfo { + validator: Some( + namada::tendermint_proto::abci::Validator { + address: pkh1.clone(), + power: u128::try_from(val1.bonded_stake) + .expect("Test failed") + as i64, + }, + ), + signed_last_block: true, + }]; + let req = FinalizeBlock { + proposer_address: pkh1, + votes, + ..Default::default() + }; + shell.finalize_and_commit(Some(req)); } } }; diff --git a/apps/src/lib/node/ledger/shell/testing/node.rs b/apps/src/lib/node/ledger/shell/testing/node.rs index a66d33cb78..cebb970f62 100644 --- a/apps/src/lib/node/ledger/shell/testing/node.rs +++ b/apps/src/lib/node/ledger/shell/testing/node.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use color_eyre::eyre::{Report, Result}; +use data_encoding::HEXUPPER; use lazy_static::lazy_static; use namada::ledger::events::log::dumb_queries; use namada::ledger::queries::{ @@ -12,9 +13,17 @@ use namada::ledger::queries::{ use namada::ledger::storage::{ LastBlock, Sha256Hasher, EPOCH_SWITCH_BLOCKS_DELAY, }; +use namada::proof_of_stake::pos_queries::PosQueries; +use namada::proof_of_stake::types::WeightedValidator; +use namada::proof_of_stake::{ + read_consensus_validator_set_addresses_with_stake, + validator_consensus_key_handle, +}; +use namada::tendermint_proto::abci::VoteInfo; use namada::tendermint_rpc::endpoint::abci_info; use namada::tendermint_rpc::SimpleRequest; use namada::types::hash::Hash; +use namada::types::key::tm_consensus_key_raw_hash; use namada::types::storage::{BlockHash, BlockHeight, Epoch, Header}; use namada::types::time::DateTimeUtc; use num_traits::cast::FromPrimitive; @@ -22,7 +31,9 @@ use regex::Regex; use tokio::sync::mpsc::UnboundedReceiver; use crate::facade::tendermint_proto::abci::response_process_proposal::ProposalStatus; -use crate::facade::tendermint_proto::abci::RequestProcessProposal; +use crate::facade::tendermint_proto::abci::{ + RequestPrepareProposal, RequestProcessProposal, +}; use crate::facade::tendermint_rpc::endpoint::abci_info::AbciInfo; use crate::facade::tendermint_rpc::error::Error as RpcError; use crate::facade::{tendermint, tendermint_rpc}; @@ -126,9 +137,47 @@ impl MockNode { .0 } + /// Get the address of the block proposer and the votes for the block + fn prepare_request(&self) -> (Vec, Vec) { + let (val1, ck) = { + let locked = self.shell.lock().unwrap(); + let params = locked.wl_storage.pos_queries().get_pos_params(); + let current_epoch = locked.wl_storage.storage.get_current_epoch().0; + let consensus_set: Vec = + read_consensus_validator_set_addresses_with_stake( + &locked.wl_storage, + current_epoch, + ) + .unwrap() + .into_iter() + .collect(); + + let val1 = consensus_set[0].clone(); + let ck = validator_consensus_key_handle(&val1.address) + .get(&locked.wl_storage, current_epoch, ¶ms) + .unwrap() + .unwrap(); + (val1, ck) + }; + + let hash_string = tm_consensus_key_raw_hash(&ck); + let pkh1 = HEXUPPER.decode(hash_string.as_bytes()).unwrap(); + let votes = vec![VoteInfo { + validator: Some(namada::tendermint_proto::abci::Validator { + address: pkh1.clone(), + power: u128::try_from(val1.bonded_stake).unwrap() as i64, + }), + signed_last_block: true, + }]; + + (pkh1, votes) + } + /// Simultaneously call the `FinalizeBlock` and /// `Commit` handlers. pub fn finalize_and_commit(&self) { + let (proposer_address, votes) = self.prepare_request(); + let mut req = FinalizeBlock { hash: BlockHash([0u8; 32]), header: Header { @@ -138,8 +187,8 @@ impl MockNode { }, byzantine_validators: vec![], txs: vec![], - proposer_address: vec![], - votes: vec![], + proposer_address, + votes, }; req.header.time = DateTimeUtc::now(); let mut locked = self.shell.lock().unwrap(); @@ -164,13 +213,16 @@ impl MockNode { /// Send a tx through Process Proposal and Finalize Block /// and register the results. fn submit_tx(&self, tx_bytes: Vec) { + // The block space allocator disallows txs in certain blocks. + // Advance to block height that allows txs. + self.advance_to_allowed_block(); + let (proposer_address, votes) = self.prepare_request(); + let req = RequestProcessProposal { txs: vec![tx_bytes.clone()], + proposer_address: proposer_address.clone(), ..Default::default() }; - // The block space allocator disallows txs in certain blocks. - // Advance to block height that allows txs. - self.advance_to_allowed_block(); let mut locked = self.shell.lock().unwrap(); let mut result = locked.process_proposal(req); let mut errors: Vec<_> = result @@ -202,8 +254,8 @@ impl MockNode { tx: tx_bytes, result: result.tx_results.remove(0), }], - proposer_address: vec![], - votes: vec![], + proposer_address, + votes, }; // process the results @@ -356,9 +408,14 @@ impl<'a> Client for &'a MockNode { } else { self.clear_results(); } + let (proposer_address, _) = self.prepare_request(); + let req = RequestPrepareProposal { + proposer_address, + ..Default::default() + }; let tx_bytes = { let locked = self.shell.lock().unwrap(); - locked.prepare_proposal(Default::default()).txs.remove(0) + locked.prepare_proposal(req).txs.remove(0) }; self.submit_tx(tx_bytes); Ok(resp) diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs b/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs index edd83cdb3a..c1eba89a2f 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs @@ -280,11 +280,16 @@ mod test_bp_vote_extensions { use namada::ledger::eth_bridge::EthBridgeQueries; use namada::ledger::pos::PosQueries; use namada::ledger::storage_api::StorageWrite; - use namada::proof_of_stake::types::Position as ValidatorPosition; + use namada::proof_of_stake::types::{ + Position as ValidatorPosition, WeightedValidator, + }; use namada::proof_of_stake::{ - become_validator, consensus_validator_set_handle, BecomeValidator, + become_validator, consensus_validator_set_handle, + read_consensus_validator_set_addresses_with_stake, BecomeValidator, + Epoch, }; use namada::proto::{SignableEthMessage, Signed}; + use namada::tendermint_proto::abci::VoteInfo; #[cfg(not(feature = "abcipp"))] use namada::types::ethereum_events::Uint; #[cfg(not(feature = "abcipp"))] @@ -301,6 +306,7 @@ mod test_bp_vote_extensions { use tower_abci_abcipp::request; use crate::node::ledger::shell::test_utils::*; + use crate::node::ledger::shims::abcipp_shim_types::shim::request::FinalizeBlock; use crate::wallet::defaults::{bertha_address, bertha_keypair}; /// Make Bertha a validator. @@ -353,7 +359,36 @@ mod test_bp_vote_extensions { .expect("Test failed"); // we advance forward to the next epoch - assert_eq!(shell.start_new_epoch().0, 1); + let consensus_set: Vec = + read_consensus_validator_set_addresses_with_stake( + &shell.wl_storage, + Epoch::default(), + ) + .unwrap() + .into_iter() + .collect(); + + let val1 = consensus_set[0].clone(); + let pkh1 = get_pkh_from_address( + &shell.wl_storage, + ¶ms, + val1.address.clone(), + Epoch::default(), + ); + let votes = vec![VoteInfo { + validator: Some(namada::tendermint_proto::abci::Validator { + address: pkh1.clone(), + power: u128::try_from(val1.bonded_stake).expect("Test failed") + as i64, + }), + signed_last_block: true, + }]; + let req = FinalizeBlock { + proposer_address: pkh1, + votes, + ..Default::default() + }; + assert_eq!(shell.start_new_epoch(Some(req)).0, 1); // Check that Bertha's vote extensions pass validation. let to_sign = get_bp_bytes_to_sign(); diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs index 891a403f90..5901ce510f 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs @@ -459,9 +459,14 @@ mod test_vote_extensions { }; use namada::eth_bridge::storage::bridge_pool; use namada::ledger::pos::PosQueries; - use namada::proof_of_stake::consensus_validator_set_handle; + use namada::proof_of_stake::types::WeightedValidator; + use namada::proof_of_stake::{ + consensus_validator_set_handle, + read_consensus_validator_set_addresses_with_stake, + }; #[cfg(feature = "abcipp")] use namada::proto::{SignableEthMessage, Signed}; + use namada::tendermint_proto::abci::VoteInfo; use namada::types::address::testing::gen_established_address; #[cfg(feature = "abcipp")] use namada::types::eth_abi::Encode; @@ -485,6 +490,7 @@ mod test_vote_extensions { #[cfg(feature = "abcipp")] use crate::facade::tower_abci::request; use crate::node::ledger::shell::test_utils::*; + use crate::node::ledger::shims::abcipp_shim_types::shim::request::FinalizeBlock; /// Test validating Ethereum events. #[test] @@ -857,7 +863,37 @@ mod test_vote_extensions { .expect("Test failed"); } // we advance forward to the next epoch - assert_eq!(shell.start_new_epoch().0, 1); + let consensus_set: Vec = + read_consensus_validator_set_addresses_with_stake( + &shell.wl_storage, + Epoch::default(), + ) + .unwrap() + .into_iter() + .collect(); + + let params = shell.wl_storage.pos_queries().get_pos_params(); + let val1 = consensus_set[0].clone(); + let pkh1 = get_pkh_from_address( + &shell.wl_storage, + ¶ms, + val1.address.clone(), + Epoch::default(), + ); + let votes = vec![VoteInfo { + validator: Some(namada::tendermint_proto::abci::Validator { + address: pkh1.clone(), + power: u128::try_from(val1.bonded_stake).expect("Test failed") + as i64, + }), + signed_last_block: true, + }]; + let req = FinalizeBlock { + proposer_address: pkh1, + votes, + ..Default::default() + }; + assert_eq!(shell.start_new_epoch(Some(req)).0, 1); assert!( shell .wl_storage diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/val_set_update.rs b/apps/src/lib/node/ledger/shell/vote_extensions/val_set_update.rs index aa8183d9aa..03843b4717 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/val_set_update.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/val_set_update.rs @@ -313,9 +313,14 @@ mod test_vote_extensions { }; use namada::ledger::eth_bridge::EthBridgeQueries; use namada::ledger::pos::PosQueries; - use namada::proof_of_stake::consensus_validator_set_handle; + use namada::proof_of_stake::types::WeightedValidator; + use namada::proof_of_stake::{ + consensus_validator_set_handle, + read_consensus_validator_set_addresses_with_stake, Epoch, + }; #[cfg(feature = "abcipp")] use namada::proto::{SignableEthMessage, Signed}; + use namada::tendermint_proto::abci::VoteInfo; #[cfg(feature = "abcipp")] use namada::types::eth_abi::Encode; #[cfg(feature = "abcipp")] @@ -337,7 +342,8 @@ mod test_vote_extensions { use crate::facade::tendermint_proto::abci::response_verify_vote_extension::VerifyStatus; #[cfg(feature = "abcipp")] use crate::facade::tower_abci::request; - use crate::node::ledger::shell::test_utils; + use crate::node::ledger::shell::test_utils::{self, get_pkh_from_address}; + use crate::node::ledger::shims::abcipp_shim_types::shim::request::FinalizeBlock; use crate::wallet; /// Test if a [`validator_set_update::Vext`] that incorrectly labels what @@ -582,7 +588,37 @@ mod test_vote_extensions { .expect("Test failed"); } // we advance forward to the next epoch - assert_eq!(shell.start_new_epoch().0, 1); + let params = shell.wl_storage.pos_queries().get_pos_params(); + let consensus_set: Vec = + read_consensus_validator_set_addresses_with_stake( + &shell.wl_storage, + Epoch::default(), + ) + .unwrap() + .into_iter() + .collect(); + + let val1 = consensus_set[0].clone(); + let pkh1 = get_pkh_from_address( + &shell.wl_storage, + ¶ms, + val1.address.clone(), + Epoch::default(), + ); + let votes = vec![VoteInfo { + validator: Some(namada::tendermint_proto::abci::Validator { + address: pkh1.clone(), + power: u128::try_from(val1.bonded_stake).expect("Test failed") + as i64, + }), + signed_last_block: true, + }]; + let req = FinalizeBlock { + proposer_address: pkh1, + votes, + ..Default::default() + }; + assert_eq!(shell.start_new_epoch(Some(req)).0, 1); assert!( shell .wl_storage diff --git a/apps/src/lib/node/ledger/shims/abcipp_shim.rs b/apps/src/lib/node/ledger/shims/abcipp_shim.rs index a30b4e59c3..0ca7ef39fc 100644 --- a/apps/src/lib/node/ledger/shims/abcipp_shim.rs +++ b/apps/src/lib/node/ledger/shims/abcipp_shim.rs @@ -5,10 +5,12 @@ use std::pin::Pin; use std::task::{Context, Poll}; use futures::future::FutureExt; +use namada::proof_of_stake::find_validator_by_raw_hash; use namada::proto::Tx; use namada::types::address::Address; #[cfg(not(feature = "abcipp"))] use namada::types::hash::Hash; +use namada::types::key::tm_raw_hash_to_string; #[cfg(not(feature = "abcipp"))] use namada::types::storage::BlockHash; use namada::types::storage::BlockHeight; @@ -174,9 +176,28 @@ impl AbcippShim { .and_then(|header| header.time.to_owned()), ); - let (processing_results, _) = self - .service - .process_txs(&self.delivered_txs, block_time); + let tm_raw_hash_string = tm_raw_hash_to_string( + &begin_block_request + .header + .as_ref() + .expect("Missing block header") + .proposer_address, + ); + let block_proposer = find_validator_by_raw_hash( + &self.service.wl_storage, + tm_raw_hash_string, + ) + .unwrap() + .expect( + "Unable to find native validator address of block \ + proposer from tendermint raw hash", + ); + + let (processing_results, _) = self.service.process_txs( + &self.delivered_txs, + block_time, + &block_proposer, + ); let mut txs = Vec::with_capacity(self.delivered_txs.len()); let mut delivered = vec![]; std::mem::swap(&mut self.delivered_txs, &mut delivered); diff --git a/benches/Cargo.toml b/benches/Cargo.toml new file mode 100644 index 0000000000..244363f9b7 --- /dev/null +++ b/benches/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "namada_benchmarks" +description = "Namada benchmarks" +resolver = "2" +authors.workspace = true +edition.workspace = true +documentation.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +version.workspace = true + +[lib] +name = "namada_benches" +path = "lib.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.workspace = true +borsh.workspace = true +ferveo-common.workspace = true +masp_primitives.workspace = true +masp_proofs.workspace = true +namada = { path = "../shared" } +namada_apps = { path = "../apps" } +namada_test_utils = { path = "../test_utils" } +prost.workspace = true +rand.workspace = true +rand_core.workspace = true +sha2.workspace = true +tokio.workspace = true +tempfile.workspace = true + +[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..6a3cc6e37b --- /dev/null +++ b/benches/host_env.rs @@ -0,0 +1,44 @@ +use borsh::BorshSerialize; +use criterion::{criterion_group, criterion_main, Criterion}; +use namada::core::types::account::AccountPublicKeysMap; +use namada::core::types::address; +use namada::core::types::token::{Amount, Transfer}; +use namada::proto::{Data, MultiSignature, Section}; +use namada_apps::wallet::defaults; + +/// Benchmarks the validation of a single signature on a single `Section` of a +/// transaction +fn tx_section_signature_validation(c: &mut Criterion) { + let transfer_data = Transfer { + source: defaults::albert_address(), + target: defaults::bertha_address(), + token: address::nam(), + amount: Amount::native_whole(500).native_denominated(), + key: None, + shielded: None, + }; + let section = Section::Data(Data::new(transfer_data.try_to_vec().unwrap())); + let section_hash = section.get_hash(); + + let pkim = AccountPublicKeysMap::from_iter([ + defaults::albert_keypair().to_public() + ]); + + let multisig = MultiSignature::new( + vec![section_hash], + &[defaults::albert_keypair()], + &pkim, + ); + let signature_index = multisig.signatures.first().unwrap().clone(); + + c.bench_function("tx_section_signature_validation", |b| { + b.iter(|| { + signature_index + .verify(&pkim, &multisig.get_raw_hash()) + .unwrap() + }) + }); +} + +criterion_group!(host_env, tx_section_signature_validation); +criterion_main!(host_env); diff --git a/benches/lib.rs b/benches/lib.rs new file mode 100644 index 0000000000..3fee5830cb --- /dev/null +++ b/benches/lib.rs @@ -0,0 +1,820 @@ +//! 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::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::ops::{Deref, DerefMut}; +use std::path::PathBuf; + +use borsh::{BorshDeserialize, BorshSerialize}; +use masp_primitives::transaction::Transaction; +use masp_primitives::zip32::ExtendedFullViewingKey; +use masp_proofs::prover::LocalTxProver; +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::applications::transfer::packet::PacketData; +use namada::ibc::applications::transfer::PrefixedCoin; +use namada::ibc::clients::ics07_tendermint::client_state::{ + AllowUpdate, ClientState, +}; +use namada::ibc::clients::ics07_tendermint::consensus_state::ConsensusState; +use namada::ibc::clients::ics07_tendermint::trust_threshold::TrustThreshold; +use namada::ibc::core::ics02_client::client_type::ClientType; +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 as NamadaChannelId, ChannelId, ClientId, + ConnectionId, ConnectionId as NamadaConnectionId, PortId as NamadaPortId, + PortId, +}; +use namada::ibc::core::ics24_host::path::Path as IbcPath; +use namada::ibc::core::timestamp::Timestamp as IbcTimestamp; +use namada::ibc::core::Msg; +use namada::ibc::Height as IbcHeight; +use namada::ibc_proto::google::protobuf::Any; +use namada::ibc_proto::protobuf::Protobuf; +use namada::ledger::args::InputAmount; +use namada::ledger::gas::TxGasMeter; +use namada::ledger::ibc::storage::{channel_key, connection_key}; +use namada::ledger::masp::{ + self, ShieldedContext, ShieldedTransfer, ShieldedUtils, +}; +use namada::ledger::queries::{ + Client, EncodedResponseQuery, RequestCtx, RequestQuery, Router, RPC, +}; +use namada::ledger::wallet::Wallet; +use namada::proof_of_stake; +use namada::proto::{Code, Data, Section, Signature, Tx}; +use namada::tendermint::Hash; +use namada::tendermint_rpc::{self}; +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::token::DenominatedAmount; +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::config::TendermintMode; +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, CliWalletUtils}; +use namada_apps::{config, wasm_loader}; +use namada_test_utils::tx_data::TxWriteData; +use rand_core::OsRng; +use sha2::{Digest, Sha256}; +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_ACCOUNT_WASM: &str = "tx_update_account.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 TX_UNJAIL_VALIDATOR_WASM: &str = "tx_unjail_validator.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"; + +const FILE_NAME: &str = "shielded.dat"; +const TMP_FILE_NAME: &str = "shielded.tmp"; + +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::Full), + WASM_DIR.into(), + sender, + None, + 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() + }, + 2, + ) + .unwrap(); + + // Bond from Albert to validator + let bond = Bond { + validator: defaults::validator_address(), + amount: Amount::native_whole(1000), + source: Some(defaults::albert_address()), + }; + let signed_tx = generate_tx( + TX_BOND_WASM, + bond, + None, + None, + Some(&defaults::albert_keypair()), + ); + + let params = + proof_of_stake::read_pos_params(&shell.wl_storage).unwrap(); + let mut bench_shell = BenchShell { + inner: shell, + tempdir, + }; + + bench_shell.execute_tx(&signed_tx); + bench_shell.wl_storage.commit_tx(); + + // Initialize governance proposal + let content_section = Section::ExtraData(Code::new(vec![])); + let signed_tx = generate_tx( + TX_INIT_PROPOSAL_WASM, + InitProposalData { + id: None, + content: content_section.get_hash(), + author: defaults::albert_address(), + r#type: ProposalType::Default(None), + voting_start_epoch: 12.into(), + voting_end_epoch: 15.into(), + grace_epoch: 18.into(), + }, + None, + Some(vec![content_section]), + Some(&defaults::albert_keypair()), + ); + + bench_shell.execute_tx(&signed_tx); + bench_shell.wl_storage.commit_tx(); + bench_shell.inner.commit(); + + // Advance epoch for pos benches + for _ in 0..=(params.pipeline_len + params.unbonding_len) { + 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_from_sub_limit(u64::MAX.into()), + &TxIndex(0), + tx, + &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, + ) + .unwrap(); + } + + pub fn init_ibc_channel(&mut self) { + // Set connection open + let client_id = ClientId::new( + ClientType::new("01-tendermint".to_string()).unwrap(), + 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), + ) + .unwrap(); + + let addr_key = + Key::from(Address::Internal(InternalAddress::Ibc).to_db_key()); + + let connection_key = connection_key(&NamadaConnectionId::new(1)); + self.wl_storage + .storage + .write(&connection_key, connection.encode_vec()) + .unwrap(); + + // Set port + let port_key = port_key(&NamadaPortId::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()), + ) + .unwrap(); + let channel_key = + channel_key(&NamadaPortId::transfer(), &NamadaChannelId::new(5)); + self.wl_storage + .storage + .write(&channel_key, channel.encode_vec()) + .unwrap(); + + // Set client state + let client_id = ClientId::new( + ClientType::new("01-tendermint".to_string()).unwrap(), + 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, + }, + ) + .unwrap(); + let bytes = >::encode_vec(&client_state); + self.wl_storage + .storage + .write(&client_state_key, bytes) + .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]), + }; + + let bytes = + >::encode_vec(&consensus_state); + self.wl_storage + .storage + .write(&consensus_key, bytes) + .unwrap(); + } +} + +pub fn generate_tx( + wasm_code_path: &str, + data: impl BorshSerialize, + shielded: Option, + extra_section: Option>, + signer: Option<&SecretKey>, +) -> Tx { + let mut tx = Tx::from_type(namada::types::transaction::TxType::Decrypted( + namada::types::transaction::DecryptedTx::Decrypted { + #[cfg(not(feature = "mainnet"))] + has_valid_pow: true, + }, + )); + + // NOTE: don't use the hash to avoid computing the cost of loading the wasm + // code + tx.set_code(Code::new(wasm_loader::read_wasm_or_exit( + WASM_DIR, + wasm_code_path, + ))); + tx.set_data(Data::new(data.try_to_vec().unwrap())); + + if let Some(transaction) = shielded { + tx.add_section(Section::MaspTx(transaction)); + } + + if let Some(sections) = extra_section { + for section in sections { + if let Section::ExtraData(_) = section { + tx.add_section(section); + } + } + } + + if let Some(signer) = signer { + tx.add_section(Section::Signature(Signature::new( + tx.sechashes(), + signer, + ))); + } + + tx +} + +pub fn generate_ibc_tx(wasm_code_path: &str, msg: impl Msg) -> Tx { + // This function avoid serializaing the tx data with Borsh + let mut tx = Tx::from_type(namada::types::transaction::TxType::Decrypted( + namada::types::transaction::DecryptedTx::Decrypted { + #[cfg(not(feature = "mainnet"))] + has_valid_pow: true, + }, + )); + tx.set_code(Code::new(wasm_loader::read_wasm_or_exit( + WASM_DIR, + wasm_code_path, + ))); + + let mut data = vec![]; + prost::Message::encode(&msg.to_any(), &mut data).unwrap(); + tx.set_data(Data::new(data)); + + // NOTE: the Ibc VP doesn't actually check the signature + tx +} + +pub fn generate_foreign_key_tx(signer: &SecretKey) -> Tx { + let wasm_code = std::fs::read("../wasm_for_tests/tx_write.wasm").unwrap(); + + let mut tx = Tx::from_type(namada::types::transaction::TxType::Decrypted( + namada::types::transaction::DecryptedTx::Decrypted { + #[cfg(not(feature = "mainnet"))] + has_valid_pow: true, + }, + )); + tx.set_code(Code::new(wasm_code)); + tx.set_data(Data::new( + TxWriteData { + key: Key::from("bench_foreign_key".to_string().to_db_key()), + value: vec![0; 64], + } + .try_to_vec() + .unwrap(), + )); + tx.add_section(Section::Signature(Signature::new(tx.sechashes(), signer))); + + tx +} + +pub fn generate_ibc_transfer_tx() -> Tx { + let token = PrefixedCoin { + denom: address::nam().to_string().parse().unwrap(), + amount: Amount::native_whole(1000) + .to_string_native() + .split('.') + .next() + .unwrap() + .to_string() + .parse() + .unwrap(), + }; + + 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), + packet_data: PacketData { + token, + sender: defaults::albert_address().to_string().into(), + receiver: defaults::bertha_address().to_string().into(), + memo: "".parse().unwrap(), + }, + timeout_height_on_b: timeout_height, + timeout_timestamp_on_b: timeout_timestamp, + }; + + generate_ibc_tx(TX_IBC_WASM, msg) +} + +pub struct BenchShieldedCtx { + pub shielded: ShieldedContext, + pub shell: BenchShell, + pub wallet: Wallet, +} + +#[derive(Debug)] +struct WrapperTempDir(TempDir); + +// Mock the required traits for ShieldedUtils + +impl Default for WrapperTempDir { + fn default() -> Self { + Self(TempDir::new().unwrap()) + } +} + +impl Clone for WrapperTempDir { + fn clone(&self) -> Self { + Self(TempDir::new().unwrap()) + } +} + +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, Default)] +pub struct BenchShieldedUtils { + #[borsh_skip] + context_dir: WrapperTempDir, +} + +#[async_trait::async_trait(?Send)] +impl ShieldedUtils for BenchShieldedUtils { + fn local_tx_prover(&self) -> LocalTxProver { + if let Ok(params_dir) = std::env::var(masp::ENV_VAR_MASP_PARAMS_DIR) { + let params_dir = PathBuf::from(params_dir); + let spend_path = params_dir.join(masp::SPEND_NAME); + let convert_path = params_dir.join(masp::CONVERT_NAME); + let output_path = params_dir.join(masp::OUTPUT_NAME); + LocalTxProver::new(&spend_path, &output_path, &convert_path) + } else { + LocalTxProver::with_default_location() + .expect("unable to load MASP Parameters") + } + } + + /// Try to load the last saved shielded context from the given context + /// directory. If this fails, then leave the current context unchanged. + async fn load(self) -> std::io::Result> { + // Try to load shielded context from file + let mut ctx_file = File::open( + self.context_dir.0.path().to_path_buf().join(FILE_NAME), + )?; + let mut bytes = Vec::new(); + ctx_file.read_to_end(&mut bytes)?; + let mut new_ctx = ShieldedContext::deserialize(&mut &bytes[..])?; + // Associate the originating context directory with the + // shielded context under construction + new_ctx.utils = self; + Ok(new_ctx) + } + + /// Save this shielded context into its associated context directory + async fn save(&self, ctx: &ShieldedContext) -> std::io::Result<()> { + let tmp_path = + self.context_dir.0.path().to_path_buf().join(TMP_FILE_NAME); + { + // First serialize the shielded context into a temporary file. + // Inability to create this file implies a simultaneuous write is in + // progress. In this case, immediately fail. This is unproblematic + // because the data intended to be stored can always be re-fetched + // from the blockchain. + let mut ctx_file = OpenOptions::new() + .write(true) + .create_new(true) + .open(tmp_path.clone())?; + let mut bytes = Vec::new(); + ctx.serialize(&mut bytes) + .expect("cannot serialize shielded context"); + ctx_file.write_all(&bytes[..])?; + } + // Atomically update the old shielded context file with new data. + // Atomicity is required to prevent other client instances from reading + // corrupt data. + std::fs::rename( + tmp_path.clone(), + self.context_dir.0.path().to_path_buf().join(FILE_NAME), + )?; + // Finally, remove our temporary file to allow future saving of shielded + // contexts. + std::fs::remove_file(tmp_path)?; + Ok(()) + } +} + +#[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)) + } + + async fn perform( + &self, + _request: R, + ) -> Result + where + R: tendermint_rpc::SimpleRequest, + { + tendermint_rpc::Response::from_string("MOCK RESPONSE") + } +} + +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: Some(WASM_DIR.into()), + }) + .unwrap(); + + // Generate spending key for Albert and Bertha + ctx.wallet.gen_spending_key( + ALBERT_SPENDING_KEY.to_string(), + None, + true, + ); + ctx.wallet.gen_spending_key( + BERTHA_SPENDING_KEY.to_string(), + None, + true, + ); + namada_apps::wallet::save(&ctx.wallet).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) = + namada::ledger::masp::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), + true, + ) + .unwrap(); + } + + namada_apps::wallet::save(&ctx.wallet).unwrap(); + namada::ledger::storage::update_allowed_conversions( + &mut shell.wl_storage, + ) + .unwrap(); + + Self { + shielded: ShieldedContext::default(), + shell, + wallet: ctx.wallet, + } + } +} + +impl BenchShieldedCtx { + pub fn generate_masp_tx( + &mut self, + amount: Amount, + source: TransferSource, + target: TransferTarget, + ) -> Tx { + let mock_args = TxArgs { + dry_run: false, + dry_run_wrapper: false, + dump_tx: false, + force: false, + broadcast_only: false, + ledger_address: (), + initialized_account_alias: None, + fee_amount: None, + fee_token: address::nam(), + fee_unshield: None, + gas_limit: GasLimit::from(u64::MAX), + expiration: None, + disposable_signing_key: false, + signing_keys: vec![defaults::albert_keypair()], + signatures: vec![], + wallet_alias_force: true, + chain_id: None, + tx_reveal_code_path: TX_REVEAL_PK_WASM.into(), + verification_key: None, + password: None, + wrapper_fee_payer: None, + output_folder: None, + }; + + let args = TxTransfer { + tx: mock_args, + source: source.clone(), + target: target.clone(), + token: address::nam(), + amount: InputAmount::Validated(DenominatedAmount { + amount, + denom: 0.into(), + }), + native_token: self.shell.wl_storage.storage.native_token.clone(), + tx_code_path: TX_TRANSFER_WASM.into(), + }; + + let async_runtime = tokio::runtime::Runtime::new().unwrap(); + let spending_key = self + .wallet + .find_spending_key(ALBERT_SPENDING_KEY, None) + .unwrap(); + async_runtime.block_on(self.shielded.fetch( + &self.shell, + &[spending_key.into()], + &[], + )); + let shielded = async_runtime + .block_on(self.shielded.gen_shielded_transfer(&self.shell, args)) + .unwrap() + .map( + |ShieldedTransfer { + builder: _, + masp_tx, + metadata: _, + epoch: _, + }| masp_tx, + ); + + let mut hasher = Sha256::new(); + let shielded_section_hash = shielded.clone().map(|transaction| { + namada::core::types::hash::Hash( + Section::MaspTx(transaction) + .hash(&mut hasher) + .finalize_reset() + .into(), + ) + }); + + generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: source.effective_address(), + target: target.effective_address(), + token: address::nam(), + amount: DenominatedAmount::native(amount), + key: None, + shielded: shielded_section_hash, + }, + shielded, + None, + Some(&defaults::albert_keypair()), + ) + } +} diff --git a/benches/native_vps.rs b/benches/native_vps.rs new file mode 100644 index 0000000000..7e0280a837 --- /dev/null +++ b/benches/native_vps.rs @@ -0,0 +1,464 @@ +use std::collections::BTreeSet; +use std::str::FromStr; + +use criterion::{criterion_group, criterion_main, Criterion}; +use namada::core::ledger::governance::storage::vote::{ + StorageProposalVote, VoteType, +}; +use namada::core::types::address::{self, Address}; +use namada::core::types::token::{Amount, Transfer}; +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::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::{ + ClientId, ConnectionId, PortId, +}; +use namada::ledger::gas::{TxGasMeter, VpGasMeter}; +use namada::ledger::governance::GovernanceVp; +use namada::ledger::ibc::vp::Ibc; +use namada::ledger::native_vp::multitoken::MultitokenVp; +use namada::ledger::native_vp::replay_protection::ReplayProtectionVp; +use namada::ledger::native_vp::{Ctx, NativeVp}; +use namada::ledger::storage_api::StorageRead; +use namada::proto::{Code, Section}; +use namada::types::address::InternalAddress; +use namada::types::storage::TxIndex; +use namada::types::transaction::governance::{ + InitProposalData, ProposalType, VoteProposalData, +}; +use namada_apps::wallet::defaults; +use namada_benches::{ + generate_foreign_key_tx, generate_ibc_transfer_tx, generate_ibc_tx, + generate_tx, BenchShell, TX_IBC_WASM, TX_INIT_PROPOSAL_WASM, + TX_TRANSFER_WASM, TX_VOTE_PROPOSAL_WASM, +}; + +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_from_tx_meter(&TxGasMeter::new_from_sub_limit( + u64::MAX.into(), + )), + &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, + 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: StorageProposalVote::Yay(VoteType::Default), + voter: defaults::albert_address(), + delegations: vec![defaults::validator_address()], + }, + None, + None, + Some(&defaults::albert_keypair()), + ), + "validator_vote" => generate_tx( + TX_VOTE_PROPOSAL_WASM, + VoteProposalData { + id: 0, + vote: StorageProposalVote::Nay, + voter: defaults::validator_address(), + delegations: vec![], + }, + None, + None, + Some(&defaults::validator_keypair()), + ), + "minimal_proposal" => { + let content_section = Section::ExtraData(Code::new(vec![])); + generate_tx( + TX_INIT_PROPOSAL_WASM, + InitProposalData { + id: None, + content: content_section.get_hash(), + author: defaults::albert_address(), + r#type: ProposalType::Default(None), + voting_start_epoch: 12.into(), + voting_end_epoch: 15.into(), + grace_epoch: 18.into(), + }, + None, + Some(vec![content_section]), + Some(&defaults::albert_keypair()), + ) + } + "complete_proposal" => { + let max_code_size_key = + namada::core::ledger::governance::storage::keys::get_max_proposal_code_size_key(); + let max_proposal_content_key = + namada::core::ledger::governance::storage::keys::get_max_proposal_content_key(); + let max_code_size: u64 = 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: u64 = shell + .wl_storage + .read(&max_proposal_content_key) + .expect("Error while reading from storage") + .expect( + "Missing max_proposal_content parameter in storage", + ); + let content_section = Section::ExtraData(Code::new(vec![ + 0; + max_proposal_content_size + as _ + ])); + let wasm_code_section = + Section::ExtraData(Code::new(vec![0; max_code_size as _])); + + generate_tx( + TX_INIT_PROPOSAL_WASM, + InitProposalData { + id: Some(1), + content: content_section.get_hash(), + author: defaults::albert_address(), + r#type: ProposalType::Default(Some( + wasm_code_section.get_hash(), + )), + voting_start_epoch: 12.into(), + voting_end_epoch: 15.into(), + grace_epoch: 18.into(), + }, + None, + Some(vec![content_section, wasm_code_section]), + Some(&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 = GovernanceVp { + ctx: Ctx::new( + &Address::Internal(InternalAddress::Governance), + &shell.wl_storage.storage, + &shell.wl_storage.write_log, + &signed_tx, + &TxIndex(0), + VpGasMeter::new_from_tx_meter(&TxGasMeter::new_from_sub_limit( + u64::MAX.into(), + )), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + ), + }; + + group.bench_function(bench_name, |b| { + b.iter(|| { + assert!( + governance + .validate_tx( + &signed_tx, + governance.ctx.keys_changed, + governance.ctx.verifiers, + ) + .unwrap() + ) + }) + }); + } + + group.finish(); +} + +// TODO: missing native vps +// - pos +// - parameters +// - eth bridge +// - eth bridge pool + +// TODO: uncomment when SlashFund internal address is brought back +// 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 content_section = Section::ExtraData(Code::new(vec![])); +// let governance_proposal = generate_tx( +// TX_INIT_PROPOSAL_WASM, +// InitProposalData { +// id: None, +// content: content_section.get_hash(), +// author: defaults::albert_address(), +// r#type: ProposalType::Default(None), +// voting_start_epoch: 12.into(), +// voting_end_epoch: 15.into(), +// grace_epoch: 18.into(), +// }, +// None, +// Some(vec![content_section]), +// Some(&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_from_tx_meter(&TxGasMeter::new_from_sub_limit( +// u64::MAX.into(), )), +// &keys_changed, +// &verifiers, +// shell.vp_wasm_cache.clone(), +// ), +// }; + +// group.bench_function(bench_name, |b| { +// b.iter(|| { +// assert!( +// slash_fund +// .validate_tx( +// &tx, +// 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()).unwrap(), + 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: defaults::albert_address().to_string().into(), + }; + let open_connection = generate_ibc_tx(TX_IBC_WASM, msg); + + // 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: defaults::albert_address().to_string().into(), + version_proposal: ChannelVersion::new("ics20-1".to_string()), + }; + + // Avoid serializing the data again with borsh + let open_channel = generate_ibc_tx(TX_IBC_WASM, msg); + + // 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_from_tx_meter(&TxGasMeter::new_from_sub_limit( + u64::MAX.into(), + )), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + ), + }; + + group.bench_function(bench_name, |b| { + b.iter(|| { + assert!( + ibc.validate_tx( + signed_tx, + ibc.ctx.keys_changed, + ibc.ctx.verifiers, + ) + .unwrap() + ) + }) + }); + } + + group.finish(); +} + +fn vp_multitoken(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(), + amount: Amount::native_whole(1000).native_denominated(), + key: None, + shielded: None, + }, + None, + None, + Some(&defaults::albert_keypair()), + ); + + for (signed_tx, bench_name) in [foreign_key_write, transfer] + .iter() + .zip(["foreign_key_write", "transfer"]) + { + 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()); + + let multitoken = MultitokenVp { + ctx: Ctx::new( + &Address::Internal(InternalAddress::Multitoken), + &shell.wl_storage.storage, + &shell.wl_storage.write_log, + signed_tx, + &TxIndex(0), + VpGasMeter::new_from_tx_meter(&TxGasMeter::new_from_sub_limit( + u64::MAX.into(), + )), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + ), + }; + + group.bench_function(bench_name, |b| { + b.iter(|| { + assert!( + multitoken + .validate_tx( + signed_tx, + multitoken.ctx.keys_changed, + multitoken.ctx.verifiers, + ) + .unwrap() + ) + }) + }); + } +} + +criterion_group!( + native_vps, + replay_protection, + governance, + // slash_fund, + ibc, + vp_multitoken +); +criterion_main!(native_vps); diff --git a/benches/process_wrapper.rs b/benches/process_wrapper.rs new file mode 100644 index 0000000000..72b26e0cb1 --- /dev/null +++ b/benches/process_wrapper.rs @@ -0,0 +1,102 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use namada::core::types::address; +use namada::core::types::token::{Amount, Transfer}; +use namada::ledger::storage::TempWlStorage; +use namada::proto::Signature; +use namada::types::key::RefTo; +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_block.as_mut().unwrap().height = + BlockHeight(2); + + let mut tx = generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: defaults::albert_address(), + target: defaults::bertha_address(), + token: address::nam(), + amount: Amount::native_whole(1).native_denominated(), + key: None, + shielded: None, + }, + None, + None, + Some(&defaults::albert_keypair()), + ); + + tx.update_header(namada::types::transaction::TxType::Wrapper(Box::new( + WrapperTx::new( + Fee { + token: address::nam(), + amount_per_gas_unit: 1.into(), + }, + defaults::albert_keypair().ref_to(), + 0.into(), + 1000.into(), + #[cfg(not(feature = "mainnet"))] + None, + None, + ), + ))); + tx.add_section(namada::proto::Section::Signature(Signature::new( + tx.sechashes(), + &defaults::albert_keypair(), + ))); + let wrapper = tx.to_bytes(); + + let datetime = DateTimeUtc::now(); + + 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), + ValidationMeta::from(&shell.wl_storage), + shell.vp_wasm_cache.clone(), + shell.tx_wasm_cache.clone(), + defaults::daewon_address(), + ) + }, + |( + tx_queue, + mut temp_wl_storage, + mut validation_meta, + mut vp_wasm_cache, + mut tx_wasm_cache, + block_proposer, + )| { + assert_eq!( + // Assert that the wrapper transaction was valid + shell + .check_proposal_tx( + &wrapper, + &mut tx_queue.iter(), + &mut validation_meta, + &mut temp_wl_storage, + datetime, + &mut vp_wasm_cache, + &mut tx_wasm_cache, + &block_proposer + ) + .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..8c4e2988c7 --- /dev/null +++ b/benches/txs.rs @@ -0,0 +1,699 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use namada::core::ledger::governance::storage::vote::{ + StorageProposalVote, VoteType, +}; +use namada::core::types::key::{ + common, SecretKey as SecretKeyInterface, SigScheme, +}; +use namada::core::types::token::Amount; +use namada::core::types::transaction::account::{InitAccount, UpdateAccount}; +use namada::core::types::transaction::pos::InitValidator; +use namada::ledger::storage_api::StorageRead; +use namada::proof_of_stake::types::SlashType; +use namada::proof_of_stake::{self, read_pos_params}; +use namada::proto::{Code, Section}; +use namada::types::hash::Hash; +use namada::types::key::{ed25519, secp256k1, PublicKey, RefTo}; +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; +use namada_apps::wallet::defaults; +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_UNJAIL_VALIDATOR_WASM, + TX_UPDATE_ACCOUNT_WASM, TX_VOTE_PROPOSAL_WASM, VP_VALIDATOR_WASM, +}; +use rand::rngs::StdRng; +use rand::SeedableRng; +use sha2::Digest; + +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"; + +// TODO: need to benchmark tx_bridge_pool.wasm +fn transfer(c: &mut Criterion) { + let mut group = c.benchmark_group("transfer"); + let amount = Amount::native_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 + .wallet + .find_spending_key(ALBERT_SPENDING_KEY, None) + .unwrap() + .to_owned(); + let albert_payment_addr = shielded_ctx + .wallet + .find_payment_addr(ALBERT_PAYMENT_ADDRESS) + .unwrap() + .to_owned(); + let bertha_payment_addr = shielded_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::native_whole(1000), + source: Some(defaults::albert_address()), + }, + None, + None, + Some(&defaults::albert_keypair()), + ); + + let self_bond = generate_tx( + TX_BOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::native_whole(1000), + source: None, + }, + None, + None, + Some(&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::native_whole(1000), + source: Some(defaults::albert_address()), + }, + None, + None, + Some(&defaults::albert_keypair()), + ); + + let self_unbond = generate_tx( + TX_UNBOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::native_whole(1000), + source: None, + }, + None, + None, + Some(&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()), + }, + None, + None, + Some(&defaults::albert_keypair()), + ); + + let self_withdraw = generate_tx( + TX_WITHDRAW_WASM, + Withdraw { + validator: defaults::validator_address(), + source: None, + }, + None, + None, + Some(&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::native_whole(1000), + source: Some(defaults::albert_address()), + }, + None, + None, + Some(&defaults::albert_keypair()), + ), + "self_withdraw" => generate_tx( + TX_UNBOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::native_whole(1000), + source: None, + }, + None, + None, + Some(&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 = generate_tx( + TX_REVEAL_PK_WASM, + new_implicit_account.to_public(), + None, + None, + 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 extra_section = Section::ExtraData(Code::from_hash(vp_code_hash)); + let data = UpdateAccount { + addr: defaults::albert_address(), + vp_code_hash: Some(Hash( + extra_section + .hash(&mut sha2::Sha256::new()) + .finalize_reset() + .into(), + )), + public_keys: vec![defaults::albert_keypair().ref_to()], + threshold: None, + }; + let vp = generate_tx( + TX_UPDATE_ACCOUNT_WASM, + data, + None, + Some(vec![extra_section]), + Some(&defaults::albert_keypair()), + ); + + c.bench_function("update_vp", |b| { + b.iter_batched_ref( + BenchShell::default, + |shell| shell.execute_tx(&vp), + 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 extra_section = Section::ExtraData(Code::from_hash(vp_code_hash)); + let extra_hash = Hash( + extra_section + .hash(&mut sha2::Sha256::new()) + .finalize_reset() + .into(), + ); + let data = InitAccount { + public_keys: vec![new_account.to_public()], + vp_code_hash: extra_hash, + threshold: 1, + }; + let tx = generate_tx( + TX_INIT_ACCOUNT_WASM, + data, + None, + Some(vec![extra_section]), + Some(&defaults::albert_keypair()), + ); + + c.bench_function("init_account", |b| { + b.iter_batched_ref( + BenchShell::default, + |shell| shell.execute_tx(&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" => { + let content_section = + Section::ExtraData(Code::new(vec![])); + generate_tx( + TX_INIT_PROPOSAL_WASM, + InitProposalData { + id: None, + content: content_section.get_hash(), + author: defaults::albert_address(), + r#type: ProposalType::Default(None), + voting_start_epoch: 12.into(), + voting_end_epoch: 15.into(), + grace_epoch: 18.into(), + }, + None, + Some(vec![content_section]), + Some(&defaults::albert_keypair()), + ) + } + "complete_proposal" => { + let max_code_size_key = + namada::core::ledger::governance::storage::keys::get_max_proposal_code_size_key(); + let max_proposal_content_key = + namada::core::ledger::governance::storage::keys::get_max_proposal_content_key(); + let max_code_size: u64 = 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: u64 = shell + .wl_storage + .read(&max_proposal_content_key) + .expect("Error while reading from storage") + .expect( + "Missing max_proposal_content parameter \ + in storage", + ); + let content_section = + Section::ExtraData(Code::new(vec![ + 0; + max_proposal_content_size + as _ + ])); + let wasm_code_section = + Section::ExtraData(Code::new(vec![ + 0; + max_code_size + as _ + ])); + + generate_tx( + TX_INIT_PROPOSAL_WASM, + InitProposalData { + id: Some(1), + content: content_section.get_hash(), + author: defaults::albert_address(), + r#type: ProposalType::Default(Some( + wasm_code_section.get_hash(), + )), + voting_start_epoch: 12.into(), + voting_end_epoch: 15.into(), + grace_epoch: 18.into(), + }, + None, + Some(vec![content_section, wasm_code_section]), + Some(&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: StorageProposalVote::Yay(VoteType::Default), + voter: defaults::albert_address(), + delegations: vec![defaults::validator_address()], + }, + None, + None, + Some(&defaults::albert_keypair()), + ); + + let validator_vote = generate_tx( + TX_VOTE_PROPOSAL_WASM, + VoteProposalData { + id: 0, + vote: StorageProposalVote::Nay, + voter: defaults::validator_address(), + delegations: vec![], + }, + None, + None, + Some(&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 eth_cold_key = secp256k1::PublicKey::try_from_pk( + &secp256k1::SigScheme::generate(&mut csprng) + .try_to_sk::() + .unwrap() + .to_public(), + ) + .unwrap(); + let eth_hot_key = secp256k1::PublicKey::try_from_pk( + &secp256k1::SigScheme::generate(&mut csprng) + .try_to_sk::() + .unwrap() + .to_public(), + ) + .unwrap(); + 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 extra_section = + Section::ExtraData(Code::from_hash(validator_vp_code_hash)); + let extra_hash = Hash( + extra_section + .hash(&mut sha2::Sha256::new()) + .finalize_reset() + .into(), + ); + let data = InitValidator { + account_keys: vec![defaults::albert_keypair().to_public()], + threshold: 1, + consensus_key, + eth_cold_key, + eth_hot_key, + protocol_key, + dkg_key, + commission_rate: namada::types::dec::Dec::default(), + max_commission_rate_change: namada::types::dec::Dec::default(), + validator_vp_code_hash: extra_hash, + }; + let tx = generate_tx( + TX_INIT_VALIDATOR_WASM, + data, + None, + Some(vec![extra_section]), + Some(&defaults::albert_keypair()), + ); + + c.bench_function("init_validator", |b| { + b.iter_batched_ref( + BenchShell::default, + |shell| shell.execute_tx(&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: namada::types::dec::Dec::new(6, 2).unwrap(), + }, + None, + None, + Some(&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, + ) + }); +} + +fn unjail_validator(c: &mut Criterion) { + let signed_tx = generate_tx( + TX_UNJAIL_VALIDATOR_WASM, + defaults::validator_address(), + None, + None, + Some(&defaults::validator_keypair()), + ); + + c.bench_function("unjail_validator", |b| { + b.iter_batched_ref( + || { + let mut shell = BenchShell::default(); + + // Jail the validator + let pos_params = read_pos_params(&shell.wl_storage).unwrap(); + let current_epoch = shell.wl_storage.storage.block.epoch; + let evidence_epoch = current_epoch.prev(); + proof_of_stake::slash( + &mut shell.wl_storage, + &pos_params, + current_epoch, + evidence_epoch, + 0u64, + SlashType::DuplicateVote, + &defaults::validator_address(), + current_epoch.next(), + ) + .unwrap(); + + shell.wl_storage.commit_tx(); + shell.commit(); + // Advance by slash epoch offset epochs + for _ in 0..=pos_params.slash_processing_epoch_offset() { + shell.advance_epoch(); + } + + 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, + unjail_validator +); +criterion_main!(whitelisted_txs); diff --git a/benches/vps.rs b/benches/vps.rs new file mode 100644 index 0000000000..20e6055885 --- /dev/null +++ b/benches/vps.rs @@ -0,0 +1,566 @@ +use std::collections::BTreeSet; + +use criterion::{criterion_group, criterion_main, Criterion}; +use namada::core::ledger::governance::storage::vote::{ + StorageProposalVote, VoteType, +}; +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::core::types::transaction::account::UpdateAccount; +use namada::ledger::gas::{TxGasMeter, VpGasMeter}; +use namada::proto::{Code, Section}; +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::vm::wasm::run; +use namada_apps::wallet::defaults; +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_ACCOUNT_WASM, + TX_VOTE_PROPOSAL_WASM, VP_VALIDATOR_WASM, +}; +use sha2::Digest; + +const VP_USER_WASM: &str = "vp_user.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 shell = BenchShell::default(); + let vp_code_hash: Hash = shell + .read_storage_key(&Key::wasm_hash(VP_USER_WASM)) + .unwrap(); + + 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(), + amount: Amount::native_whole(1000).native_denominated(), + key: None, + shielded: None, + }, + None, + None, + Some(&defaults::albert_keypair()), + ); + + let received_transfer = generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: defaults::bertha_address(), + target: defaults::albert_address(), + token: address::nam(), + amount: Amount::native_whole(1000).native_denominated(), + key: None, + shielded: None, + }, + None, + None, + Some(&defaults::bertha_keypair()), + ); + + let vp_validator_hash = shell + .read_storage_key(&Key::wasm_hash(VP_VALIDATOR_WASM)) + .unwrap(); + let extra_section = Section::ExtraData(Code::from_hash(vp_validator_hash)); + let data = UpdateAccount { + addr: defaults::albert_address(), + vp_code_hash: Some(Hash( + extra_section + .hash(&mut sha2::Sha256::new()) + .finalize_reset() + .into(), + )), + public_keys: vec![defaults::albert_keypair().to_public()], + threshold: None, + }; + let vp = generate_tx( + TX_UPDATE_ACCOUNT_WASM, + data, + None, + Some(vec![extra_section]), + Some(&defaults::albert_keypair()), + ); + + let vote = generate_tx( + TX_VOTE_PROPOSAL_WASM, + VoteProposalData { + id: 0, + vote: StorageProposalVote::Yay(VoteType::Default), + voter: defaults::albert_address(), + delegations: vec![defaults::validator_address()], + }, + None, + None, + Some(&defaults::albert_keypair()), + ); + + let pos = generate_tx( + TX_UNBOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::native_whole(1000), + source: Some(defaults::albert_address()), + }, + None, + None, + Some(&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(); + 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_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()) + ), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + #[cfg(not(feature = "mainnet"))] + 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(), + amount: Amount::native_whole(500).native_denominated(), + key: None, + shielded: None, + }, + None, + None, + Some(&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(), + amount: Amount::native_whole(1000).native_denominated(), + key: None, + shielded: None, + }, + None, + None, + Some(&defaults::bertha_keypair()), + ); + + let reveal_pk = generate_tx( + TX_REVEAL_PK_WASM, + &implicit_account.to_public(), + None, + None, + None, + ); + + let pos = generate_tx( + TX_BOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::native_whole(1000), + source: Some(Address::from(&implicit_account.to_public())), + }, + None, + None, + Some(&implicit_account), + ); + + let vote = generate_tx( + TX_VOTE_PROPOSAL_WASM, + VoteProposalData { + id: 0, + vote: StorageProposalVote::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 */ + }, + None, + None, + Some(&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_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()) + ), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + #[cfg(not(feature = "mainnet"))] + 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(), + amount: Amount::native_whole(1000).native_denominated(), + key: None, + shielded: None, + }, + None, + None, + Some(&defaults::validator_keypair()), + ); + + let received_transfer = generate_tx( + TX_TRANSFER_WASM, + Transfer { + source: defaults::bertha_address(), + target: defaults::validator_address(), + token: address::nam(), + amount: Amount::native_whole(1000).native_denominated(), + key: None, + shielded: None, + }, + None, + None, + Some(&defaults::bertha_keypair()), + ); + + let extra_section = Section::ExtraData(Code::from_hash(vp_code_hash)); + let data = UpdateAccount { + addr: defaults::validator_address(), + vp_code_hash: Some(Hash( + extra_section + .hash(&mut sha2::Sha256::new()) + .finalize_reset() + .into(), + )), + public_keys: vec![defaults::validator_keypair().to_public()], + threshold: None, + }; + let vp = generate_tx( + TX_UPDATE_ACCOUNT_WASM, + data, + None, + Some(vec![extra_section]), + Some(&defaults::validator_keypair()), + ); + + let commission_rate = generate_tx( + TX_CHANGE_VALIDATOR_COMMISSION_WASM, + CommissionChange { + validator: defaults::validator_address(), + new_rate: namada::types::dec::Dec::new(6, 2).unwrap(), + }, + None, + None, + Some(&defaults::validator_keypair()), + ); + + let vote = generate_tx( + TX_VOTE_PROPOSAL_WASM, + VoteProposalData { + id: 0, + vote: StorageProposalVote::Yay(VoteType::Default), + voter: defaults::validator_address(), + delegations: vec![], + }, + None, + None, + Some(&defaults::validator_keypair()), + ); + + let pos = generate_tx( + TX_UNBOND_WASM, + Bond { + validator: defaults::validator_address(), + amount: Amount::native_whole(1000), + source: None, + }, + None, + None, + Some(&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_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()) + ), + &keys_changed, + &verifiers, + shell.vp_wasm_cache.clone(), + #[cfg(not(feature = "mainnet"))] + false, + ) + .unwrap() + ); + }) + }); + } + + group.finish(); +} + +fn vp_masp(c: &mut Criterion) { + let mut group = c.benchmark_group("vp_masp"); + + let amount = Amount::native_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 + .wallet + .find_spending_key(ALBERT_SPENDING_KEY, None) + .unwrap() + .to_owned(); + let albert_payment_addr = shielded_ctx + .wallet + .find_payment_addr(ALBERT_PAYMENT_ADDRESS) + .unwrap() + .to_owned(); + let bertha_payment_addr = shielded_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_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()) + ), + &keys_changed, + &verifiers, + shielded_ctx.shell.vp_wasm_cache.clone(), + #[cfg(not(feature = "mainnet"))] + false, + ) + .unwrap() + ); + }) + }); + } + + group.finish(); +} + +criterion_group!(whitelisted_vps, vp_user, vp_implicit, vp_validator, vp_masp,); +criterion_main!(whitelisted_vps); diff --git a/core/src/ledger/gas.rs b/core/src/ledger/gas.rs index e51c9f78ef..3b00a30cb5 100644 --- a/core/src/ledger/gas.rs +++ b/core/src/ledger/gas.rs @@ -1,11 +1,16 @@ //! Gas accounting module to track the gas usage in a block for transactions and //! validity predicates triggered by transactions. -use std::convert::TryFrom; +use std::fmt::Display; +use std::ops::Div; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use thiserror::Error; +use super::parameters; +use super::storage_api::{self, StorageRead}; +use crate::types::transaction::wrapper::GasLimit; + #[allow(missing_docs)] #[derive(Error, Debug, Clone, PartialEq, Eq)] pub enum Error { @@ -15,38 +20,177 @@ 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 cost of accessing the storage, per byte +pub const STORAGE_ACCESS_GAS_PER_BYTE: u64 = 1; +/// The cost of writing to storage, per byte +pub const STORAGE_WRITE_GAS_PER_BYTE: u64 = 100; +/// The cost of verifying a signle signature of a transaction +pub const VERIFY_TX_SIG_GAS_COST: u64 = 10; +/// The cost of accessing the WASM memory, per byte +pub const VM_MEMORY_ACCESS_GAS_PER_BYTE: u64 = 1; +/// The cost for requesting one more page in wasm (64KB) +pub const WASM_MEMORY_PAGE_GAS_COST: u32 = 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)] -pub struct BlockGasMeter { - block_gas: u64, - transaction_gas: u64, +/// Decimal scale of Gas units +const SCALE: u64 = 1_000; + +/// Helper function to retrieve the `max_block_gas` protocol parameter from +/// storage +pub fn get_max_block_gas( + storage: &impl StorageRead, +) -> std::result::Result { + storage + .read(¶meters::storage::get_max_block_gas_key())? + .ok_or(storage_api::Error::SimpleMessage( + "Missing max_block_gas parameter from storage", + )) +} + +/// Representation of gas in sub-units. This effectively decouples gas metering +/// from fee payment, allowing higher resolution when accounting for gas while, +/// at the same time, providing a contained gas value when paying fees. +#[derive( + Clone, + Copy, + Debug, + Default, + PartialEq, + PartialOrd, + BorshDeserialize, + BorshSerialize, + BorshSchema, +)] +pub struct Gas { + sub: u64, +} + +impl Gas { + /// Checked add of `Gas`. Returns `None` on overflow + pub fn checked_add(&self, rhs: Self) -> Option { + self.sub.checked_add(rhs.sub).map(|sub| Self { sub }) + } + + /// Checked sub of `Gas`. Returns `None` on underflow + pub fn checked_sub(&self, rhs: Self) -> Option { + self.sub.checked_sub(rhs.sub).map(|sub| Self { sub }) + } + + /// Converts the sub gas units to whole ones. If the sub units are not a + /// multiple of the `SCALE` than ceil the quotient + fn get_whole_gas_units(&self) -> u64 { + let quotient = self.sub / SCALE; + if self.sub % SCALE == 0 { + quotient + } else { + quotient + 1 + } + } + + /// Generates a `Gas` instance from a whole amount + pub fn from_whole_units(whole: u64) -> Self { + Self { sub: whole * SCALE } + } +} + +impl Div for Gas { + type Output = Gas; + + fn div(self, rhs: u64) -> Self::Output { + Self { + sub: self.sub / rhs, + } + } +} + +impl From for Gas { + fn from(sub: u64) -> Self { + Self { sub } + } +} + +impl From for u64 { + fn from(gas: Gas) -> Self { + gas.sub + } +} + +impl Display for Gas { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Display the gas in whole amounts + write!(f, "{}", self.get_whole_gas_units()) + } +} + +impl From for Gas { + // Derive a Gas instance with a sub amount which is exaclty a whole amount + // since the limit represents gas in whole units + fn from(value: GasLimit) -> Self { + Self { + sub: u64::from(value) * SCALE, + } + } +} + +/// Trait to share gas operations for transactions and validity predicates +pub trait GasMetering { + /// 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 consume(&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.consume( + 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.consume( + bytes_len + .checked_mul(STORAGE_ACCESS_GAS_PER_BYTE) + .ok_or(Error::GasOverflow)?, + ) + } + + /// Get the gas consumed by the tx alone + fn get_tx_consumed_gas(&self) -> Gas; + + /// Get the gas limit + fn get_gas_limit(&self) -> Gas; +} + +/// Gas metering in a transaction +#[derive(Debug)] +pub struct TxGasMeter { + /// The gas limit for a transaction + pub tx_gas_limit: Gas, + transaction_gas: Gas, } /// Gas metering in a validity predicate -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct VpGasMeter { - /// The gas used in the transaction before the VP run - initial_gas: u64, + /// The transaction gas limit + tx_gas_limit: Gas, + /// The gas consumed by the transaction before the Vp + initial_gas: Gas, /// The current gas usage in the VP - pub current_gas: u64, + current_gas: Gas, } /// Gas meter for VPs parallel runs @@ -54,125 +198,132 @@ pub struct VpGasMeter { Clone, Debug, Default, BorshSerialize, BorshDeserialize, BorshSchema, )] pub struct VpsGas { - max: Option, - rest: Vec, + max: Option, + rest: Vec, } -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<()> { +impl GasMetering for TxGasMeter { + fn consume(&mut self, gas: u64) -> Result<()> { self.transaction_gas = self .transaction_gas - .checked_add(gas) + .checked_add(gas.into()) .ok_or(Error::GasOverflow)?; - if self.transaction_gas > TRANSACTION_GAS_LIMIT { + if self.transaction_gas > self.tx_gas_limit { return Err(Error::TransactionGasExceededError); } + 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) + fn get_tx_consumed_gas(&self) -> Gas { + self.transaction_gas } - /// 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) - } - - /// 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) - .ok_or(Error::GasOverflow)?; + fn get_gas_limit(&self) -> Gas { + self.tx_gas_limit + } +} - let transaction_gas = self.transaction_gas; - self.transaction_gas = 0; - if self.block_gas > BLOCK_GAS_LIMIT { - return Err(Error::BlockGasExceeded); +impl TxGasMeter { + /// Initialize a new Tx gas meter. Requires the `GasLimit` for the specific + /// wrapper transaction + pub fn new(tx_gas_limit: GasLimit) -> Self { + Self { + tx_gas_limit: tx_gas_limit.into(), + transaction_gas: Gas::default(), } - Ok(transaction_gas) } - /// Reset the gas meter. - pub fn reset(&mut self) { - self.transaction_gas = 0; - self.block_gas = 0; + /// Initialize a new gas meter. Requires the gas limit expressed in sub + /// units + pub fn new_from_sub_limit(tx_gas_limit: Gas) -> Self { + Self { + tx_gas_limit, + transaction_gas: Gas::default(), + } } - /// 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, tx_bytes: &[u8]) -> Result<()> { + let bytes_len: u64 = tx_bytes + .len() + .try_into() + .map_err(|_| Error::ConversionError)?; + self.consume( + 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()?) + self.consume(vps_gas.get_current_gas()?.into()) } -} -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 amount of gas still available to the transaction + pub fn get_available_gas(&self) -> Gas { + self.tx_gas_limit + .checked_sub(self.transaction_gas) + .unwrap_or_default() } +} - /// 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 GasMetering for VpGasMeter { + fn consume(&mut self, gas: u64) -> Result<()> { + self.current_gas = self .current_gas - .checked_add(gas) + .checked_add(gas.into()) .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) + fn get_tx_consumed_gas(&self) -> Gas { + self.initial_gas + } + + fn get_gas_limit(&self) -> Gas { + self.tx_gas_limit + } +} + +impl VpGasMeter { + /// Initialize a new VP gas meter from the `TxGasMeter` + pub fn new_from_tx_meter(tx_gas_meter: &TxGasMeter) -> Self { + Self { + tx_gas_limit: tx_gas_meter.tx_gas_limit, + initial_gas: tx_gas_meter.transaction_gas, + current_gas: Gas::default(), + } } } 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) } /// Merge validity predicates gas meters from parallelized runs. pub fn merge( &mut self, other: &mut VpsGas, - initial_gas: u64, + tx_gas_meter: &TxGasMeter, ) -> Result<()> { match (self.max, other.max) { (None, Some(_)) => { @@ -190,22 +341,26 @@ impl VpsGas { } self.rest.append(&mut other.rest); - self.check_limit(initial_gas) + self.check_limit(tx_gas_meter) } - fn check_limit(&self, initial_gas: u64) -> Result<()> { - let total = initial_gas + fn check_limit(&self, gas_meter: &impl GasMetering) -> Result<()> { + let total = gas_meter + .get_tx_consumed_gas() .checked_add(self.get_current_gas()?) .ok_or(Error::GasOverflow)?; - if total > TRANSACTION_GAS_LIMIT { - return Err(Error::GasOverflow); + if total > gas_meter.get_gas_limit() { + return Err(Error::TransactionGasExceededError); } Ok(()) } /// Get the gas consumed by the parallelized VPs - fn get_current_gas(&self) -> Result { - let parallel_gas = self.rest.iter().sum::() / PARALLEL_GAS_DIVIDER; + fn get_current_gas(&self) -> Result { + let parallel_gas = + self.rest.iter().try_fold(Gas::default(), |acc, gas| { + acc.checked_add(*gas).ok_or(Error::GasOverflow) + })? / PARALLEL_GAS_DIVIDER; self.max .unwrap_or_default() .checked_add(parallel_gas) @@ -213,50 +368,50 @@ 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); - meter.add(gas).expect("cannot add the gas"); + fn test_vp_gas_meter_add(gas in 0..BLOCK_GAS_LIMIT) { + let tx_gas_meter = TxGasMeter { + tx_gas_limit: BLOCK_GAS_LIMIT.into(), + transaction_gas: Gas::default(), + }; + let mut meter = VpGasMeter::new_from_tx_meter(&tx_gas_meter); + meter.consume(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); - } } #[test] fn test_vp_gas_overflow() { - let mut meter = VpGasMeter::new(1); + let tx_gas_meter = TxGasMeter { + tx_gas_limit: BLOCK_GAS_LIMIT.into(), + transaction_gas: (TX_GAS_LIMIT - 1).into(), + }; + let mut meter = VpGasMeter::new_from_tx_meter(&tx_gas_meter); assert_matches!( - meter.add(u64::MAX).expect_err("unexpectedly succeeded"), + meter.consume(u64::MAX).expect_err("unexpectedly succeeded"), Error::GasOverflow ); } #[test] fn test_vp_gas_limit() { - let mut meter = VpGasMeter::new(1); + let tx_gas_meter = TxGasMeter { + tx_gas_limit: TX_GAS_LIMIT.into(), + transaction_gas: (TX_GAS_LIMIT - 1).into(), + }; + let mut meter = VpGasMeter::new_from_tx_meter(&tx_gas_meter); assert_matches!( meter - .add(TRANSACTION_GAS_LIMIT) + .consume(TX_GAS_LIMIT) .expect_err("unexpectedly succeeded"), Error::TransactionGasExceededError ); @@ -264,57 +419,22 @@ mod tests { #[test] fn test_tx_gas_overflow() { - let mut meter = BlockGasMeter::default(); - meter.add(1).expect("cannot add the gas"); + let mut meter = TxGasMeter::new_from_sub_limit(BLOCK_GAS_LIMIT.into()); + meter.consume(1).expect("cannot add the gas"); assert_matches!( - meter.add(u64::MAX).expect_err("unexpectedly succeeded"), + meter.consume(u64::MAX).expect_err("unexpectedly succeeded"), Error::GasOverflow ); } #[test] fn test_tx_gas_limit() { - let mut meter = BlockGasMeter::default(); + let mut meter = TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()); assert_matches!( meter - .add(TRANSACTION_GAS_LIMIT + 1) + .consume(TX_GAS_LIMIT + 1) .expect_err("unexpectedly succeeded"), Error::TransactionGasExceededError ); } - - #[test] - fn test_block_gas_limit() { - let mut meter = BlockGasMeter::default(); - - // 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"); - meter - .finalize_transaction() - .expect("over the block gas limit"); - } - - meter - .add(TRANSACTION_GAS_LIMIT) - .expect("over the tx gas limit"); - match meter - .finalize_transaction() - .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 03d27fa2da..5be01fde56 100644 --- a/core/src/ledger/parameters/mod.rs +++ b/core/src/ledger/parameters/mod.rs @@ -1,12 +1,15 @@ //! Protocol parameters pub mod storage; +use std::collections::BTreeMap; + use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use thiserror::Error; use super::storage::types; +use super::storage_api::token::Amount; 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::dec::Dec; @@ -38,6 +41,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) @@ -59,9 +64,12 @@ pub struct Parameters { #[cfg(not(feature = "mainnet"))] /// Faucet account for free token withdrawal pub faucet_account: Option
, - #[cfg(not(feature = "mainnet"))] - /// Fixed fees for a wrapper tx to be accepted - pub wrapper_tx_fees: Option, + /// Fee unshielding gas limit + pub fee_unshielding_gas_limit: u64, + /// Fee unshielding descriptions limit + pub fee_unshielding_descriptions_limit: u64, + /// Map of the cost per gas unit for every token allowed for fee payment + pub gas_cost: BTreeMap, } /// Epoch duration. A new epoch begins as soon as both the `min_num_of_blocks` @@ -115,6 +123,7 @@ impl Parameters { epoch_duration, max_expected_time_per_block, max_proposal_bytes, + max_block_gas, vp_whitelist, tx_whitelist, implicit_vp_code_hash, @@ -126,18 +135,37 @@ impl Parameters { pos_inflation_amount, #[cfg(not(feature = "mainnet"))] faucet_account, - #[cfg(not(feature = "mainnet"))] - wrapper_tx_fees, + gas_cost, + fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit, } = 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 fee unshielding gas limit + let fee_unshielding_gas_limit_key = + storage::get_fee_unshielding_gas_limit_key(); + storage + .write(&fee_unshielding_gas_limit_key, fee_unshielding_gas_limit)?; + + // write fee unshielding descriptions limit + let fee_unshielding_descriptions_limit_key = + storage::get_fee_unshielding_descriptions_limit_key(); + storage.write( + &fee_unshielding_descriptions_limit_key, + fee_unshielding_descriptions_limit, + )?; + // write vp whitelist parameter let vp_whitelist_key = storage::get_vp_whitelist_storage_key(); let vp_whitelist = vp_whitelist @@ -196,13 +224,9 @@ impl Parameters { storage.write(&faucet_account_key, faucet_account)?; } - #[cfg(not(feature = "mainnet"))] - { - let wrapper_tx_fees_key = storage::get_wrapper_tx_fees_key(); - let wrapper_tx_fees = - wrapper_tx_fees.unwrap_or(token::Amount::native_whole(100)); - storage.write(&wrapper_tx_fees_key, wrapper_tx_fees)?; - } + let gas_cost_key = storage::get_gas_cost_key(); + storage.write(&gas_cost_key, gas_cost)?; + Ok(()) } } @@ -402,16 +426,19 @@ where storage.read(&faucet_account_key) } -#[cfg(not(feature = "mainnet"))] -/// Read the wrapper tx fees amount, if any -pub fn read_wrapper_tx_fees_parameter( +/// Read the cost per unit of gas for the provided token +pub fn read_gas_cost( storage: &S, -) -> storage_api::Result> + token: &Address, +) -> storage_api::Result> where S: StorageRead, { - let wrapper_tx_fees_key = storage::get_wrapper_tx_fees_key(); - storage.read(&wrapper_tx_fees_key) + let gas_cost_table: BTreeMap = storage + .read(&storage::get_gas_cost_key())? + .ok_or(ReadError::ParametersMissing) + .into_storage_result()?; + Ok(gas_cost_table.get(token).map(|amount| amount.to_owned())) } /// Read all the parameters from storage. Returns the parameters and gas @@ -429,6 +456,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)?; @@ -462,6 +498,22 @@ where let implicit_vp_code_hash = Hash::try_from(&value[..]).into_storage_result()?; + // read fee unshielding gas limit + let fee_unshielding_gas_limit_key = + storage::get_fee_unshielding_gas_limit_key(); + let value = storage.read(&fee_unshielding_gas_limit_key)?; + let fee_unshielding_gas_limit: u64 = value + .ok_or(ReadError::ParametersMissing) + .into_storage_result()?; + + // read fee unshielding descriptions limit + let fee_unshielding_descriptions_limit_key = + storage::get_fee_unshielding_descriptions_limit_key(); + let value = storage.read(&fee_unshielding_descriptions_limit_key)?; + let fee_unshielding_descriptions_limit: u64 = 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)?; @@ -510,14 +562,18 @@ where #[cfg(not(feature = "mainnet"))] let faucet_account = read_faucet_account_parameter(storage)?; - // read faucet account - #[cfg(not(feature = "mainnet"))] - let wrapper_tx_fees = read_wrapper_tx_fees_parameter(storage)?; + // read gas cost + let gas_cost_key = storage::get_gas_cost_key(); + let value = storage.read(&gas_cost_key)?; + let gas_cost: BTreeMap = value + .ok_or(ReadError::ParametersMissing) + .into_storage_result()?; Ok(Parameters { epoch_duration, max_expected_time_per_block, max_proposal_bytes, + max_block_gas, vp_whitelist, tx_whitelist, implicit_vp_code_hash, @@ -529,7 +585,8 @@ where pos_inflation_amount, #[cfg(not(feature = "mainnet"))] faucet_account, - #[cfg(not(feature = "mainnet"))] - wrapper_tx_fees, + gas_cost, + fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit, }) } diff --git a/core/src/ledger/parameters/storage.rs b/core/src/ledger/parameters/storage.rs index d32b1d221f..3e87799016 100644 --- a/core/src/ledger/parameters/storage.rs +++ b/core/src/ledger/parameters/storage.rs @@ -42,8 +42,11 @@ 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_cost: &'static str, + fee_unshielding_gas_limit: &'static str, + fee_unshielding_descriptions_limit: &'static str, max_signatures_per_transaction: &'static str, } @@ -135,6 +138,16 @@ pub fn get_tx_whitelist_storage_key() -> Key { get_tx_whitelist_key_at_addr(ADDRESS) } +/// Storage key used for the fee unshielding gas limit +pub fn get_fee_unshielding_gas_limit_key() -> Key { + get_fee_unshielding_gas_limit_key_at_addr(ADDRESS) +} + +/// Storage key used for the fee unshielding descriptions limit +pub fn get_fee_unshielding_descriptions_limit_key() -> Key { + get_fee_unshielding_descriptions_limit_key_at_addr(ADDRESS) +} + /// Storage key used for max_epected_time_per_block parameter. pub fn get_max_expected_time_per_block_key() -> Key { get_max_expected_time_per_block_key_at_addr(ADDRESS) @@ -175,14 +188,19 @@ pub fn get_max_proposal_bytes_key() -> Key { get_max_proposal_bytes_key_at_addr(ADDRESS) } +/// Storage key used for the max block gas. +pub fn get_max_block_gas_key() -> Key { + get_max_block_gas_key_at_addr(ADDRESS) +} + /// Storage key used for faucet account. pub fn get_faucet_account_key() -> Key { get_faucet_account_key_at_addr(ADDRESS) } -/// Storage key used for staked ratio parameter. -pub fn get_wrapper_tx_fees_key() -> Key { - get_wrapper_tx_fees_key_at_addr(ADDRESS) +/// Storage key used for the gas cost table +pub fn get_gas_cost_key() -> Key { + get_gas_cost_key_at_addr(ADDRESS) } /// Storage key used for the max signatures per transaction key diff --git a/core/src/ledger/storage/mod.rs b/core/src/ledger/storage/mod.rs index 94669a34f4..bf447fc64e 100644 --- a/core/src/ledger/storage/mod.rs +++ b/core/src/ledger/storage/mod.rs @@ -7,7 +7,7 @@ pub mod merkle_tree; pub mod mockdb; pub mod traits; pub mod types; -mod wl_storage; +pub mod wl_storage; pub mod write_log; use core::fmt::Debug; @@ -28,7 +28,9 @@ pub use wl_storage::{ pub use self::masp_conversions::update_allowed_conversions; pub use self::masp_conversions::{encode_asset_type, ConversionState}; use crate::ledger::eth_bridge::storage::bridge_pool::is_pending_transfer_key; -use crate::ledger::gas::MIN_STORAGE_GAS; +use crate::ledger::gas::{ + STORAGE_ACCESS_GAS_PER_BYTE, STORAGE_WRITE_GAS_PER_BYTE, +}; use crate::ledger::parameters::{self, EpochDuration, Parameters}; use crate::ledger::storage::merkle_tree::{ Error as MerkleTreeError, MerkleRoot, @@ -559,7 +561,10 @@ where /// Check if the given key is present in storage. Returns the result and the /// gas cost. pub fn has_key(&self, key: &Key) -> Result<(bool, u64)> { - Ok((self.block.tree.has_key(key)?, key.len() as _)) + Ok(( + self.block.tree.has_key(key)?, + key.len() as u64 * STORAGE_ACCESS_GAS_PER_BYTE, + )) } /// Returns a value from the specified subspace and the gas cost @@ -572,10 +577,11 @@ where match self.db.read_subspace_val(key)? { Some(v) => { - let gas = key.len() + v.len(); - Ok((Some(v), gas as _)) + let gas = + (key.len() + v.len()) as u64 * STORAGE_ACCESS_GAS_PER_BYTE; + Ok((Some(v), gas)) } - None => Ok((None, key.len() as _)), + None => Ok((None, key.len() as u64 * STORAGE_ACCESS_GAS_PER_BYTE)), } } @@ -595,10 +601,13 @@ where self.get_last_block_height(), )? { Some(v) => { - let gas = key.len() + v.len(); - Ok((Some(v), gas as _)) + let gas = (key.len() + v.len()) as u64 + * STORAGE_ACCESS_GAS_PER_BYTE; + Ok((Some(v), gas)) + } + None => { + Ok((None, key.len() as u64 * STORAGE_ACCESS_GAS_PER_BYTE)) } - None => Ok((None, key.len() as _)), } } } @@ -612,7 +621,10 @@ where &self, prefix: &Key, ) -> (>::PrefixIter, u64) { - (self.db.iter_prefix(Some(prefix)), prefix.len() as _) + ( + self.db.iter_prefix(Some(prefix)), + prefix.len() as u64 * STORAGE_ACCESS_GAS_PER_BYTE, + ) } /// Returns a prefix iterator and the gas cost @@ -642,10 +654,10 @@ where } let len = value.len(); - let gas = key.len() + len; + let gas = (key.len() + len) as u64 * STORAGE_WRITE_GAS_PER_BYTE; let size_diff = self.db.write_subspace_val(self.block.height, key, value)?; - Ok((gas as _, size_diff)) + Ok((gas, size_diff)) } /// Delete the specified subspace and returns the gas cost and the size @@ -659,8 +671,9 @@ where deleted_bytes_len = self.db.delete_subspace_val(self.block.height, key)?; } - let gas = key.len() + deleted_bytes_len as usize; - Ok((gas as _, deleted_bytes_len)) + let gas = (key.len() + deleted_bytes_len as usize) as u64 + * STORAGE_WRITE_GAS_PER_BYTE; + Ok((gas, deleted_bytes_len)) } /// Set the block header. @@ -713,17 +726,23 @@ where /// Get the chain ID as a raw string pub fn get_chain_id(&self) -> (String, u64) { - (self.chain_id.to_string(), CHAIN_ID_LENGTH as _) + ( + self.chain_id.to_string(), + CHAIN_ID_LENGTH as u64 * STORAGE_ACCESS_GAS_PER_BYTE, + ) } /// Get the block height pub fn get_block_height(&self) -> (BlockHeight, u64) { - (self.block.height, MIN_STORAGE_GAS) + (self.block.height, STORAGE_ACCESS_GAS_PER_BYTE) } /// Get the block hash pub fn get_block_hash(&self) -> (BlockHash, u64) { - (self.block.hash.clone(), BLOCK_HASH_LENGTH as _) + ( + self.block.hash.clone(), + BLOCK_HASH_LENGTH as u64 * STORAGE_ACCESS_GAS_PER_BYTE, + ) } /// Get the Merkle tree with stores and diffs in the DB @@ -889,12 +908,12 @@ where /// Get the current (yet to be committed) block epoch pub fn get_current_epoch(&self) -> (Epoch, u64) { - (self.block.epoch, MIN_STORAGE_GAS) + (self.block.epoch, STORAGE_ACCESS_GAS_PER_BYTE) } /// Get the epoch of the last committed block pub fn get_last_epoch(&self) -> (Epoch, u64) { - (self.last_epoch, MIN_STORAGE_GAS) + (self.last_epoch, STORAGE_ACCESS_GAS_PER_BYTE) } /// Initialize the first epoch. The first epoch begins at genesis time. @@ -920,16 +939,17 @@ where ) -> Result<(Option
, u64)> { match height { Some(h) if h == self.get_block_height().0 => { - Ok((self.header.clone(), MIN_STORAGE_GAS)) + Ok((self.header.clone(), STORAGE_ACCESS_GAS_PER_BYTE)) } Some(h) => match self.db.read_block_header(h)? { Some(header) => { - let gas = header.encoded_len() as u64; + let gas = header.encoded_len() as u64 + * STORAGE_ACCESS_GAS_PER_BYTE; Ok((Some(header), gas)) } - None => Ok((None, MIN_STORAGE_GAS)), + None => Ok((None, STORAGE_ACCESS_GAS_PER_BYTE)), }, - None => Ok((self.header.clone(), MIN_STORAGE_GAS)), + None => Ok((self.header.clone(), STORAGE_ACCESS_GAS_PER_BYTE)), } } @@ -1139,6 +1159,8 @@ pub mod testing { #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use chrono::{TimeZone, Utc}; use proptest::prelude::*; use proptest::test_runner::Config; @@ -1219,6 +1241,7 @@ mod tests { }; let mut parameters = Parameters { max_proposal_bytes: Default::default(), + max_block_gas: 20_000_000, epoch_duration: epoch_duration.clone(), max_expected_time_per_block: Duration::seconds(max_expected_time_per_block).into(), vp_whitelist: vec![], @@ -1232,8 +1255,9 @@ mod tests { pos_inflation_amount: token::Amount::zero(), #[cfg(not(feature = "mainnet"))] faucet_account: None, - #[cfg(not(feature = "mainnet"))] - wrapper_tx_fees: None, + fee_unshielding_gas_limit: 20_000, + fee_unshielding_descriptions_limit: 15, + gas_cost: 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 4fb2490ab9..4151108f38 100644 --- a/core/src/ledger/storage/wl_storage.rs +++ b/core/src/ledger/storage/wl_storage.rs @@ -59,10 +59,10 @@ where /// Common trait for [`WlStorage`] and [`TempWlStorage`], used to implement /// storage_api traits. -trait WriteLogAndStorage { - // DB type +pub trait WriteLogAndStorage { + /// DB type type D: DB + for<'iter> DBIter<'iter>; - // DB hasher type + /// DB hasher type type H: StorageHasher; /// Borrow `WriteLog` @@ -73,6 +73,11 @@ trait WriteLogAndStorage { /// Borrow `Storage` fn storage(&self) -> &Storage; + + /// Splitting borrow to get immutable reference to the `Storage` and mutable + /// reference to `WriteLog` when in need of both (avoids complain from the + /// borrow checker) + fn split_borrow(&mut self) -> (&mut WriteLog, &Storage); } impl WriteLogAndStorage for WlStorage @@ -94,6 +99,10 @@ where fn storage(&self) -> &Storage { &self.storage } + + fn split_borrow(&mut self) -> (&mut WriteLog, &Storage) { + (&mut self.write_log, &self.storage) + } } impl WriteLogAndStorage for TempWlStorage<'_, D, H> @@ -115,6 +124,10 @@ where fn storage(&self) -> &Storage { self.storage } + + fn split_borrow(&mut self) -> (&mut WriteLog, &Storage) { + (&mut self.write_log, (self.storage)) + } } impl WlStorage @@ -248,7 +261,7 @@ where storage_iter, write_log_iter, }, - gas::MIN_STORAGE_GAS, + gas::STORAGE_ACCESS_GAS_PER_BYTE, ) } @@ -273,7 +286,7 @@ where storage_iter, write_log_iter, }, - gas::MIN_STORAGE_GAS, + gas::STORAGE_ACCESS_GAS_PER_BYTE, ) } @@ -516,7 +529,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 641fa7fc19..9cf19e3eaa 100644 --- a/core/src/ledger/storage/write_log.rs +++ b/core/src/ledger/storage/write_log.rs @@ -7,6 +7,9 @@ use itertools::Itertools; use thiserror::Error; use crate::ledger; +use crate::ledger::gas::{ + STORAGE_ACCESS_GAS_PER_BYTE, STORAGE_WRITE_GAS_PER_BYTE, +}; use crate::ledger::storage::traits::StorageHasher; use crate::ledger::storage::Storage; use crate::types::address::{Address, EstablishedAddressGen, InternalAddress}; @@ -73,6 +76,15 @@ pub struct WriteLog { block_write_log: HashMap, /// The storage modifications for the current transaction tx_write_log: HashMap, + /// A precommit bucket for the `tx_write_log`. This is useful for + /// validation when a clean `tx_write_log` is needed without committing any + /// modification already in there. These modifications can be temporarely + /// stored here and then discarded or committed to the `block_write_log`, + /// together with th content of `tx_write_log`. No direct key + /// write/update/delete should ever happen on this field, this log should + /// only be populated through a dump of the `tx_write_log` and should be + /// cleaned either when committing or dumping the `tx_write_log` + tx_precommit_write_log: HashMap, /// The IBC events for the current transaction ibc_events: BTreeSet, } @@ -99,6 +111,7 @@ impl Default for WriteLog { address_gen: None, block_write_log: HashMap::with_capacity(100_000), tx_write_log: HashMap::with_capacity(100), + tx_precommit_write_log: HashMap::with_capacity(100), ibc_events: BTreeSet::new(), } } @@ -112,10 +125,17 @@ impl WriteLog { key: &storage::Key, ) -> (Option<&StorageModification>, u64) { // try to read from tx write log first - match self.tx_write_log.get(key).or_else(|| { - // if not found, then try to read from block write log - self.block_write_log.get(key) - }) { + match self + .tx_write_log + .get(key) + .or_else(|| { + // If not found, then try to read from tx precommit write log + self.tx_precommit_write_log.get(key) + }) + .or_else(|| { + // if not found, then try to read from block write log + self.block_write_log.get(key) + }) { Some(v) => { let gas = match v { StorageModification::Write { ref value } => { @@ -129,9 +149,9 @@ impl WriteLog { key.len() + value.len() } }; - (Some(v), gas as _) + (Some(v), gas as u64 * STORAGE_ACCESS_GAS_PER_BYTE) } - None => (None, key.len() as _), + None => (None, key.len() as u64 * STORAGE_ACCESS_GAS_PER_BYTE), } } @@ -157,9 +177,9 @@ impl WriteLog { key.len() + value.len() } }; - (Some(v), gas as _) + (Some(v), gas as u64 * STORAGE_ACCESS_GAS_PER_BYTE) } - None => (None, key.len() as _), + None => (None, key.len() as u64 * STORAGE_ACCESS_GAS_PER_BYTE), } } @@ -195,7 +215,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. @@ -260,7 +280,9 @@ impl WriteLog { // the previous value exists on the storage None => len as i64, }; - Ok((gas as _, size_diff)) + // Temp writes are not propagated to db so just charge the cost of + // accessing storage + Ok((gas as u64 * STORAGE_ACCESS_GAS_PER_BYTE, size_diff)) } /// Delete a key and its value, and return the gas cost and the size @@ -288,7 +310,7 @@ impl WriteLog { None => 0, }; let gas = key.len() + size_diff as usize; - Ok((gas as _, -size_diff)) + Ok((gas as u64 * STORAGE_WRITE_GAS_PER_BYTE, -size_diff)) } /// Delete a key and its value. @@ -327,7 +349,8 @@ impl WriteLog { let addr = address_gen.generate_address("TODO more randomness".as_bytes()); let key = storage::Key::validity_predicate(&addr); - let gas = (key.len() + vp_code_hash.len()) as _; + let gas = (key.len() + vp_code_hash.len()) as u64 + * STORAGE_WRITE_GAS_PER_BYTE; self.tx_write_log .insert(key, StorageModification::InitAccount { vp_code_hash }); (addr, gas) @@ -340,16 +363,28 @@ impl WriteLog { .iter() .fold(0, |acc, (k, v)| acc + k.len() + v.len()); self.ibc_events.insert(event); - len as _ + len as u64 * STORAGE_ACCESS_GAS_PER_BYTE } /// Get the storage keys changed and accounts keys initialized in the /// current transaction. The account keys point to the validity predicates - /// of the newly created accounts. + /// of the newly created accounts. The keys in the precommit are not + /// included in the result of this function. pub fn get_keys(&self) -> BTreeSet { self.tx_write_log.keys().cloned().collect() } + /// Get the storage keys changed and accounts keys initialized in the + /// current transaction and precommit. The account keys point to the + /// validity predicates of the newly created accounts. + pub fn get_keys_with_precommit(&self) -> BTreeSet { + self.tx_precommit_write_log + .keys() + .chain(self.tx_write_log.keys()) + .cloned() + .collect() + } + /// Get the storage keys changed in the current transaction (left) and /// the addresses of accounts initialized in the current transaction /// (right). The first vector excludes keys of validity predicates of @@ -395,24 +430,50 @@ impl WriteLog { &self.ibc_events } - /// Commit the current transaction's write log to the block when it's - /// accepted by all the triggered validity predicates. Starts a new - /// transaction write log. + /// Add the entire content of the tx write log to the precommit one. The tx + /// log gets reset in the process. + pub fn precommit_tx(&mut self) { + let tx_log = std::mem::replace( + &mut self.tx_write_log, + HashMap::with_capacity(100), + ); + + self.tx_precommit_write_log.extend(tx_log) + } + + /// Commit the current transaction's write log and precommit log to the + /// block when it's accepted by all the triggered validity predicates. + /// Starts a new transaction write log. pub fn commit_tx(&mut self) { - self.tx_write_log.retain(|_, v| { + // First precommit everything + self.precommit_tx(); + + // Then commit to block + self.tx_precommit_write_log.retain(|_, v| { !matches!(v, StorageModification::Temp { value: _ }) }); - let tx_write_log = std::mem::replace( - &mut self.tx_write_log, + let tx_precommit_write_log = std::mem::replace( + &mut self.tx_precommit_write_log, HashMap::with_capacity(100), ); - self.block_write_log.extend(tx_write_log); + + self.block_write_log.extend(tx_precommit_write_log); self.take_ibc_events(); } - /// Drop the current transaction's write log when it's declined by any of - /// the triggered validity predicates. Starts a new transaction write log. + /// Drop the current transaction's write log and precommit when it's + /// declined by any of the triggered validity predicates. Starts a new + /// transaction write log. pub fn drop_tx(&mut self) { + self.tx_precommit_write_log.clear(); + self.tx_write_log.clear(); + } + + /// Drop the current transaction's write log but keep the precommit one. + /// This is useful only when a part of a transaction failed but it can still + /// be valid and we want to keep the changes applied before the failed + /// section. + pub fn drop_tx_keep_precommit(&mut self) { self.tx_write_log.clear(); } @@ -563,13 +624,16 @@ mod tests { // delete a non-existing key let (gas, diff) = write_log.delete(&key).unwrap(); - assert_eq!(gas, key.len() as u64); + assert_eq!(gas, key.len() as u64 * STORAGE_WRITE_GAS_PER_BYTE); assert_eq!(diff, 0); // 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 @@ -585,17 +649,23 @@ 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 let (gas, diff) = write_log.delete(&key).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)); // delete the deleted key again let (gas, diff) = write_log.delete(&key).unwrap(); - assert_eq!(gas, key.len() as u64); + assert_eq!(gas, key.len() as u64 * STORAGE_WRITE_GAS_PER_BYTE); assert_eq!(diff, 0); // read the deleted key @@ -604,12 +674,15 @@ mod tests { StorageModification::Delete => {} _ => panic!("unexpected result"), } - assert_eq!(gas, key.len() as u64); + assert_eq!(gas, key.len() as u64 * STORAGE_ACCESS_GAS_PER_BYTE); // 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); } @@ -623,7 +696,10 @@ mod tests { let vp_hash = Hash::sha256(init_vp); let (addr, gas) = write_log.init_account(&address_gen, vp_hash); let vp_key = storage::Key::validity_predicate(&addr); - assert_eq!(gas, (vp_key.len() + vp_hash.len()) as u64); + assert_eq!( + gas, + (vp_key.len() + vp_hash.len()) as u64 * STORAGE_WRITE_GAS_PER_BYTE + ); // read let (value, gas) = write_log.read(&vp_key); @@ -633,7 +709,10 @@ mod tests { } _ => panic!("unexpected result"), } - assert_eq!(gas, (vp_key.len() + vp_hash.len()) as u64); + assert_eq!( + gas, + (vp_key.len() + vp_hash.len()) as u64 * STORAGE_ACCESS_GAS_PER_BYTE + ); // get all let (_changed_keys, init_accounts) = write_log.get_partitioned_keys(); diff --git a/core/src/ledger/tx_env.rs b/core/src/ledger/tx_env.rs index c53d38d4b2..70bd194226 100644 --- a/core/src/ledger/tx_env.rs +++ b/core/src/ledger/tx_env.rs @@ -55,4 +55,7 @@ pub trait TxEnv: StorageRead + StorageWrite { &mut self, event: &IbcEvent, ) -> Result<(), storage_api::Error>; + + /// Request to charge the provided amount of gas for the current transaction + fn charge_gas(&mut self, used_gas: u64) -> Result<(), storage_api::Error>; } diff --git a/core/src/ledger/vp_env.rs b/core/src/ledger/vp_env.rs index b9ce0caced..969452b241 100644 --- a/core/src/ledger/vp_env.rs +++ b/core/src/ledger/vp_env.rs @@ -100,6 +100,9 @@ where /// Verify a MASP transaction fn verify_masp(&self, tx: Vec) -> Result; + /// Charge the provided gas for the current vp + fn charge_gas(&self, used_gas: u64) -> Result<(), storage_api::Error>; + // ---- Methods below have default implementation via `pre/post` ---- /// Storage read prior state Borsh encoded value (before tx execution). It diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index 59c035561e..55e3ff5ec6 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -23,6 +23,7 @@ use sha2::{Digest, Sha256}; use thiserror::Error; use super::generated::types; +use crate::ledger::gas::{GasMetering, VpGasMeter, VERIFY_TX_SIG_GAS_COST}; use crate::ledger::storage::{KeccakHasher, Sha256Hasher, StorageHasher}; use crate::ledger::testnet_pow; #[cfg(any(feature = "tendermint", feature = "tendermint-abcipp"))] @@ -71,6 +72,8 @@ pub enum Error { InvalidJSONDeserialization(String), #[error("The wrapper signature is invalid.")] InvalidWrapperSignature, + #[error("Signature verification went out of gas")] + OutOfGas, } pub type Result = std::result::Result; @@ -1346,6 +1349,7 @@ impl Tx { public_keys_index_map: AccountPublicKeysMap, threshold: u8, max_signatures: Option, + gas_meter: &mut VpGasMeter, ) -> std::result::Result<(), Error> { let max_signatures = max_signatures.unwrap_or(u8::MAX); let mut valid_signatures = 0; @@ -1387,6 +1391,9 @@ impl Tx { &signatures.get_raw_hash(), ) .is_ok(); + gas_meter + .consume(VERIFY_TX_SIG_GAS_COST) + .map_err(|_| Error::OutOfGas)?; if is_valid_signature { valid_signatures += 1; } @@ -1490,6 +1497,7 @@ impl Tx { /// signatures over it #[cfg(feature = "ferveo-tpke")] pub fn encrypt(&mut self, pubkey: &EncryptionKey) -> &mut Self { + use crate::types::hash::Hash; let header_hash = self.header_hash(); let mut plaintexts = vec![]; // Iterate backwrds to sidestep the effects of deletion on indexing @@ -1497,6 +1505,28 @@ impl Tx { match &self.sections[i] { Section::Signature(sig) if sig.targets.contains(&header_hash) => {} + Section::MaspTx(_) => { + // Do NOT encrypt the fee unshielding transaction + if let Some(unshield_section_hash) = self + .header() + .wrapper() + .expect("Tried to encrypt a non-wrapper tx") + .unshield_section_hash + { + if unshield_section_hash + == Hash( + self.sections[i] + .hash(&mut Sha256::new()) + .finalize_reset() + .into(), + ) + { + continue; + } + } + + plaintexts.push(self.sections.remove(i)) + } // Add eligible section to the list of sections to encrypt _ => plaintexts.push(self.sections.remove(i)), } @@ -1660,20 +1690,22 @@ impl Tx { pub fn add_wrapper( &mut self, fee: Fee, - gas_payer: common::PublicKey, + fee_payer: common::PublicKey, epoch: Epoch, gas_limit: GasLimit, #[cfg(not(feature = "mainnet"))] requires_pow: Option< testnet_pow::Solution, >, + fee_unshield_hash: Option, ) -> &mut Self { self.header.tx_type = TxType::Wrapper(Box::new(WrapperTx::new( fee, - gas_payer, + fee_payer, epoch, gas_limit, #[cfg(not(feature = "mainnet"))] requires_pow, + fee_unshield_hash, ))); self } diff --git a/core/src/types/internal.rs b/core/src/types/internal.rs index a15efc8105..b5a2c43092 100644 --- a/core/src/types/internal.rs +++ b/core/src/types/internal.rs @@ -48,6 +48,7 @@ impl From for HostEnvResult { mod tx_queue { use borsh::{BorshDeserialize, BorshSerialize}; + use crate::ledger::gas::Gas; use crate::proto::Tx; /// A wrapper for `crate::types::transaction::WrapperTx` to conditionally @@ -56,6 +57,10 @@ mod tx_queue { pub struct TxInQueue { /// Wrapper tx pub tx: Tx, + /// The available gas remaining for the inner tx (for gas accounting). + /// This allows for a more detailed logging about the gas used by the + /// wrapper and that used by the inner + pub gas: Gas, #[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 0f0a6032f0..96c3d776a6 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -65,6 +65,10 @@ 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' name +pub const WASM_CODE_NAME_PREFIX: &str = "name"; +/// 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"; @@ -554,6 +558,24 @@ impl Key { Key { segments } } + /// Returns a key of wasm code's hash of the given name + pub fn wasm_code_name(code_name: String) -> Self { + let mut segments = + Self::from(WASM_KEY_PREFIX.to_owned().to_db_key()).segments; + segments.push(DbKeySeg::StringSeg(WASM_CODE_NAME_PREFIX.to_owned())); + segments.push(DbKeySeg::StringSeg(code_name)); + 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/governance.rs b/core/src/types/transaction/governance.rs index 0d653c44cf..cde2c15511 100644 --- a/core/src/types/transaction/governance.rs +++ b/core/src/types/transaction/governance.rs @@ -7,9 +7,8 @@ use thiserror::Error; use crate::ledger::governance::cli::onchain::{ DefaultProposal, PgfFundingProposal, PgfStewardProposal, }; -use crate::ledger::governance::storage::proposal::{ - AddRemove, PGFAction, ProposalType, -}; +pub use crate::ledger::governance::storage::proposal::ProposalType; +use crate::ledger::governance::storage::proposal::{AddRemove, PGFAction}; use crate::ledger::governance::storage::vote::StorageProposalVote; use crate::types::address::Address; use crate::types::hash::Hash; diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 112bc5de4b..fa583d6514 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -28,7 +28,7 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; pub use wrapper::*; -use crate::ledger::gas::VpsGas; +use crate::ledger::gas::{Gas, VpsGas}; use crate::types::address::Address; use crate::types::hash::Hash; use crate::types::ibc::IbcEvent; @@ -47,7 +47,7 @@ pub fn hash_tx(tx_bytes: &[u8]) -> Hash { #[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize)] pub struct TxResult { /// Total gas used by the transaction (includes the gas used by VPs) - pub gas_used: u64, + pub gas_used: Gas, /// Storage keys touched by the transaction pub changed_keys: BTreeSet, /// The results of all the triggered validity predicates by the transaction @@ -156,7 +156,7 @@ pub enum TxType { } impl TxType { - /// Produce a SHA-256 hash of this header + /// Produce a SHA-256 hash of this header pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { hasher.update(self.try_to_vec().expect("unable to serialize header")); hasher @@ -254,7 +254,8 @@ mod test_process_tx { // the signed tx let mut tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::from_uint(10, 0).expect("Test failed"), + amount_per_gas_unit: Amount::from_uint(10, 0) + .expect("Test failed"), token: nam(), }, keypair.ref_to(), @@ -262,6 +263,7 @@ mod test_process_tx { Default::default(), #[cfg(not(feature = "mainnet"))] None, + None, )))); tx.set_code(Code::new("wasm code".as_bytes().to_owned())); tx.set_data(Data::new("transaction data".as_bytes().to_owned())); @@ -289,7 +291,8 @@ mod test_process_tx { // the signed tx let mut tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::from_uint(10, 0).expect("Test failed"), + amount_per_gas_unit: Amount::from_uint(10, 0) + .expect("Test failed"), token: nam(), }, keypair.ref_to(), @@ -297,6 +300,7 @@ mod test_process_tx { Default::default(), #[cfg(not(feature = "mainnet"))] None, + None, )))); tx.set_code(Code::new("wasm code".as_bytes().to_owned())); tx.set_data(Data::new("transaction data".as_bytes().to_owned())); diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index 321435687b..bda7142220 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -2,34 +2,30 @@ /// to enable encrypted txs inside of normal txs. /// *Not wasm compatible* pub mod wrapper_tx { - use std::fmt::Formatter; + + use std::num::ParseIntError; + use std::str::FromStr; pub use ark_bls12_381::Bls12_381 as EllipticCurve; #[cfg(feature = "ferveo-tpke")] pub use ark_ec::{AffineCurve, PairingEngine}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; - use serde::de::Error; - use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use masp_primitives::transaction::Transaction; + use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use thiserror::Error; use crate::ledger::testnet_pow; - use crate::types::address::Address; + use crate::proto::{Code, Data, Section, Tx}; + use crate::types::address::{masp, Address}; + use crate::types::hash::Hash; use crate::types::key::*; use crate::types::storage::Epoch; - use crate::types::token::Amount; + use crate::types::token::{Amount, DenominatedAmount, Transfer}; use crate::types::uint::Uint; - /// Minimum fee amount in micro NAMs, repesented - /// with a [`u64`] type. - pub const MIN_FEE: u64 = 100; - - /// Minimum fee amount in micro NAMs, repesented - /// with an [`Amount`] type. - pub const MIN_FEE_AMOUNT: Amount = Amount::from_u64(MIN_FEE); - - // TODO: Determine a sane number for this - const GAS_LIMIT_RESOLUTION: u64 = 1_000_000; + /// TODO: Determine a sane number for this + const GAS_LIMIT_RESOLUTION: u64 = 1; /// Errors relating to decrypting a wrapper tx and its /// encrypted payload from a Tx type @@ -49,6 +45,12 @@ pub mod wrapper_tx { differs from that in the WrapperTx" )] InvalidKeyPair, + #[error("The provided unshielding tx is invalid: {0}")] + InvalidUnshield(String), + #[error("The given Tx fee amount overflowed")] + OverflowingFee, + #[error("Error while converting the denominated fee amount")] + DenominatedFeeConversion, } /// A fee is an amount of a specified token @@ -64,8 +66,8 @@ pub mod wrapper_tx { Eq, )] pub struct Fee { - /// amount of the fee - pub amount: Amount, + /// amount of fee per gas unit + pub amount_per_gas_unit: Amount, /// address of the token /// TODO: This should support multi-tokens pub token: Address, @@ -82,73 +84,32 @@ pub mod wrapper_tx { Default, Debug, Clone, + Copy, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema, + Serialize, + Deserialize, Eq, )] pub struct GasLimit { - multiplier: Uint, - } - - impl Serialize for GasLimit { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let limit = Uint::from(self).to_string(); - Serialize::serialize(&limit, serializer) - } - } - - impl<'de> Deserialize<'de> for GasLimit { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct GasLimitVisitor; - - impl<'a> serde::de::Visitor<'a> for GasLimitVisitor { - type Value = GasLimit; - - fn expecting( - &self, - formatter: &mut Formatter, - ) -> std::fmt::Result { - formatter.write_str( - "A string representing 256-bit unsigned integer", - ) - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - let uint = Uint::from_dec_str(v) - .map_err(|e| E::custom(e.to_string()))?; - Ok(GasLimit::from(uint)) - } - } - deserializer.deserialize_any(GasLimitVisitor) - } + multiplier: u64, } impl GasLimit { /// We refund unused gas up to GAS_LIMIT_RESOLUTION - pub fn refund_amount(&self, used_gas: Uint) -> Amount { + pub fn refund_amount(self, used_gas: u64) -> Amount { Amount::from_uint( - if used_gas - < (Uint::from(self) - Uint::from(GAS_LIMIT_RESOLUTION)) - { + if used_gas < (u64::from(self) - GAS_LIMIT_RESOLUTION) { // we refund only up to GAS_LIMIT_RESOLUTION Uint::from(GAS_LIMIT_RESOLUTION) - } else if used_gas >= Uint::from(self) { + } else if used_gas >= u64::from(self) { // Gas limit was under estimated, no refund - Uint::from(0) + Uint::zero() } else { // compute refund - Uint::from(self) - used_gas + Uint::from(u64::from(self)) - used_gas }, 0, ) @@ -158,40 +119,41 @@ 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: Uint) -> GasLimit { - let gas_limit_resolution = Uint::from(GAS_LIMIT_RESOLUTION); - if gas_limit_resolution * (amount / gas_limit_resolution) < amount { + impl From for GasLimit { + fn from(amount: u64) -> GasLimit { + if GAS_LIMIT_RESOLUTION * (amount / GAS_LIMIT_RESOLUTION) < amount { GasLimit { - multiplier: (amount / gas_limit_resolution) + 1, + multiplier: (amount / GAS_LIMIT_RESOLUTION) + 1, } } else { GasLimit { - multiplier: (amount / gas_limit_resolution), + multiplier: (amount / GAS_LIMIT_RESOLUTION), } } } } - /// 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(Uint::from(amount)) - } - } - /// Get back the gas limit as a raw number - impl From<&GasLimit> for Uint { - fn from(limit: &GasLimit) -> Uint { + impl From for u64 { + fn from(limit: GasLimit) -> u64 { limit.multiplier * GAS_LIMIT_RESOLUTION } } - /// Get back the gas limit as a raw number impl From for Uint { - fn from(limit: GasLimit) -> Uint { - limit.multiplier * GAS_LIMIT_RESOLUTION + fn from(limit: GasLimit) -> Self { + Uint::from_u64(limit.into()) + } + } + + impl FromStr for GasLimit { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + // Expect input to be the multiplier + Ok(Self { + multiplier: s.parse()?, + }) } } @@ -203,9 +165,9 @@ pub mod wrapper_tx { } } - /// A transaction with an encrypted payload as well - /// as some non-encrypted metadata for inclusion - /// and / or verification purposes + /// A transaction with an encrypted payload, an optional shielded pool + /// unshielding tx for fee payment and some non-encrypted metadata for + /// inclusion and / or verification purposes #[derive( Debug, Clone, @@ -218,13 +180,17 @@ pub mod wrapper_tx { pub struct WrapperTx { /// The fee to be payed for including the tx pub fee: Fee, - /// Used to determine an implicit account of the fee payer + /// Used for signature verification and to determine an implicit + /// account of the fee payer pub pk: common::PublicKey, /// The epoch in which the tx is to be submitted. This determines /// which decryption key will be used pub epoch: Epoch, /// Max amount of gas that can be used when executing the inner tx pub gas_limit: GasLimit, + /// The hash of the optional, unencrypted, unshielding transaction for + /// fee payment + pub unshield_section_hash: Option, #[cfg(not(feature = "mainnet"))] /// A PoW solution can be used to allow zero-fee testnet transactions pub pow_solution: Option, @@ -232,9 +198,10 @@ pub mod wrapper_tx { impl WrapperTx { /// Create a new wrapper tx from unencrypted tx, the personal keypair, - /// and the metadata surrounding the inclusion of the tx. This method - /// constructs the signature of relevant data and encrypts the - /// transaction + /// an optional unshielding tx, and the metadata surrounding the + /// inclusion of the tx. This method constructs the signature of + /// relevant data and encrypts the transaction + #[allow(clippy::too_many_arguments)] pub fn new( fee: Fee, pk: common::PublicKey, @@ -243,12 +210,14 @@ pub mod wrapper_tx { #[cfg(not(feature = "mainnet"))] pow_solution: Option< testnet_pow::Solution, >, + unshield_hash: Option, ) -> WrapperTx { Self { fee, pk, epoch, gas_limit, + unshield_section_hash: unshield_hash, #[cfg(not(feature = "mainnet"))] pow_solution, } @@ -256,7 +225,10 @@ pub mod wrapper_tx { /// Get the address of the implicit account associated /// with the public key - pub fn gas_payer(&self) -> Address { + /// NOTE: this is safe in case someone tried to use the masp address to + /// pay fees. All of the masp funds are kept in the established address, + /// while the implicit one has no funds leading to a tx failure + pub fn fee_payer(&self) -> Address { Address::from(&self.pk) } @@ -267,6 +239,104 @@ pub mod wrapper_tx { ); hasher } + + /// Performs validation on the optional fee unshielding data carried by + /// the wrapper and generates the tx for execution. + pub fn check_and_generate_fee_unshielding( + &self, + transfer_code_hash: Hash, + descriptions_limit: u64, + unshield: Transaction, + ) -> Result { + // Check that the number of descriptions is within a certain limit + // to avoid a possible DoS vector + let sapling_bundle = unshield.sapling_bundle().ok_or( + WrapperTxErr::InvalidUnshield( + "Missing required sapling bundle".to_string(), + ), + )?; + let spends = sapling_bundle.shielded_spends.len(); + let converts = sapling_bundle.shielded_converts.len(); + let outs = sapling_bundle.shielded_outputs.len(); + + let descriptions = spends + .checked_add(converts) + .ok_or_else(|| { + WrapperTxErr::InvalidUnshield( + "Descriptions overflow".to_string(), + ) + })? + .checked_add(outs) + .ok_or_else(|| { + WrapperTxErr::InvalidUnshield( + "Descriptions overflow".to_string(), + ) + })?; + + if u64::try_from(descriptions) + .map_err(|e| WrapperTxErr::InvalidUnshield(e.to_string()))? + > descriptions_limit + { + return Err(WrapperTxErr::InvalidUnshield( + "Descriptions exceed the maximum amount allowed" + .to_string(), + )); + } + self.generate_fee_unshielding(transfer_code_hash, unshield) + } + + /// Generates the fee unshielding tx for execution. + pub fn generate_fee_unshielding( + &self, + transfer_code_hash: Hash, + unshield: Transaction, + ) -> Result { + let mut tx = + Tx::from_type(crate::types::transaction::TxType::Decrypted( + crate::types::transaction::DecryptedTx::Decrypted { + #[cfg(not(feature = "mainnet"))] + has_valid_pow: false, + }, + )); + let masp_section = tx.add_section(Section::MaspTx(unshield)); + let masp_hash = Hash( + masp_section + .hash(&mut Sha256::new()) + .finalize_reset() + .into(), + ); + + let transfer = Transfer { + source: masp(), + target: self.fee_payer(), + token: self.fee.token.clone(), + amount: DenominatedAmount { + amount: self.get_tx_fee()?, + denom: 0.into(), + }, + key: None, + shielded: Some(masp_hash), + }; + let data = transfer.try_to_vec().map_err(|_| { + WrapperTxErr::InvalidUnshield( + "Error while serializing the unshield transfer data" + .to_string(), + ) + })?; + tx.set_data(Data::new(data)); + tx.set_code(Code::from_hash(transfer_code_hash)); + + Ok(tx) + } + + /// Get the [`Amount`] of fees to be paid by the given wrapper. Returns + /// an error if the amount overflows + pub fn get_tx_fee(&self) -> Result { + self.fee + .amount_per_gas_unit + .checked_mul(self.gas_limit.into()) + .ok_or(WrapperTxErr::OverflowingFee) + } } #[cfg(test)] @@ -277,15 +347,18 @@ pub mod wrapper_tx { /// Test that serializing converts GasLimit to u64 correctly #[test] fn test_gas_limit_roundtrip() { - let limit = GasLimit { - multiplier: 1.into(), - }; + let limit = GasLimit { multiplier: 1 }; // Test serde roundtrip - let js = serde_json::to_string(&limit).expect("Test failed"); - assert_eq!(js, format!(r#""{}""#, GAS_LIMIT_RESOLUTION)); - let new_limit: GasLimit = + let js = serde_json::to_string(&1).expect("Test failed"); + assert_eq!(js, format!(r#"{}"#, GAS_LIMIT_RESOLUTION)); + let new_limit: u64 = serde_json::from_str(&js).expect("Test failed"); - assert_eq!(new_limit, limit); + assert_eq!( + GasLimit { + multiplier: new_limit + }, + limit + ); // Test borsh roundtrip let borsh = limit.try_to_vec().expect("Test failed"); @@ -301,36 +374,27 @@ pub mod wrapper_tx { /// multiple #[test] fn test_deserialize_not_multiple_of_resolution() { - let js = format!(r#""{}""#, &(GAS_LIMIT_RESOLUTION + 1)); - let limit: GasLimit = - serde_json::from_str(&js).expect("Test failed"); + let js = format!(r#"{}"#, &(GAS_LIMIT_RESOLUTION + 1)); + let limit: u64 = serde_json::from_str(&js).expect("Test failed"); assert_eq!( - limit, - GasLimit { - multiplier: 2.into() - } + GasLimit { multiplier: limit }, + GasLimit { multiplier: 2 } ); } /// Test that refund is calculated correctly #[test] fn test_gas_limit_refund() { - let limit = GasLimit { - multiplier: 1.into(), - }; - let refund = - limit.refund_amount(Uint::from(GAS_LIMIT_RESOLUTION - 1)); + let limit = GasLimit { multiplier: 1 }; + let refund = limit.refund_amount(GAS_LIMIT_RESOLUTION - 1); assert_eq!(refund, Amount::from_uint(1, 0).expect("Test failed")); } /// Test that we don't refund more than GAS_LIMIT_RESOLUTION #[test] fn test_gas_limit_too_high_no_refund() { - let limit = GasLimit { - multiplier: 2.into(), - }; - let refund = - limit.refund_amount(Uint::from(GAS_LIMIT_RESOLUTION - 1)); + let limit = GasLimit { multiplier: 2 }; + let refund = limit.refund_amount(GAS_LIMIT_RESOLUTION - 1); assert_eq!( refund, Amount::from_uint(GAS_LIMIT_RESOLUTION, 0) @@ -341,11 +405,8 @@ pub mod wrapper_tx { /// Test that if gas usage was underestimated, we issue no refund #[test] fn test_gas_limit_too_low_no_refund() { - let limit = GasLimit { - multiplier: 1.into(), - }; - let refund = - limit.refund_amount(Uint::from(GAS_LIMIT_RESOLUTION + 1)); + let limit = GasLimit { multiplier: 1 }; + let refund = limit.refund_amount(GAS_LIMIT_RESOLUTION + 1); assert_eq!(refund, Amount::default()); } } @@ -373,7 +434,8 @@ pub mod wrapper_tx { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::from_uint(10, 0).expect("Test failed"), + amount_per_gas_unit: Amount::from_uint(10, 0) + .expect("Test failed"), token: nam(), }, keypair.ref_to(), @@ -381,6 +443,7 @@ pub mod wrapper_tx { Default::default(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.set_code(Code::new("wasm code".as_bytes().to_owned())); wrapper @@ -406,7 +469,8 @@ pub mod wrapper_tx { let mut wrapper = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::from_uint(10, 0).expect("Test failed"), + amount_per_gas_unit: Amount::from_uint(10, 0) + .expect("Test failed"), token: nam(), }, keypair.ref_to(), @@ -414,6 +478,7 @@ pub mod wrapper_tx { Default::default(), #[cfg(not(feature = "mainnet"))] None, + None, )))); wrapper.set_code(Code::new("wasm code".as_bytes().to_owned())); wrapper @@ -442,7 +507,8 @@ pub mod wrapper_tx { let mut tx = Tx::from_type(TxType::Wrapper(Box::new(WrapperTx::new( Fee { - amount: Amount::from_uint(10, 0).expect("Test failed"), + amount_per_gas_unit: Amount::from_uint(10, 0) + .expect("Test failed"), token: nam(), }, keypair.ref_to(), @@ -450,6 +516,7 @@ pub mod wrapper_tx { Default::default(), #[cfg(not(feature = "mainnet"))] None, + None, )))); tx.set_code(Code::new("wasm code".as_bytes().to_owned())); diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs index d3cd32972d..a2b73f9f00 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs @@ -431,7 +431,8 @@ mod tests { }; assert_eq!( - tx_result.gas_used, 0, + tx_result.gas_used, + 0.into(), "No gas should be used for a derived transaction" ); let eth_msg_keys = vote_tallies::Keys::from(&event); diff --git a/genesis/dev.toml b/genesis/dev.toml index 9121bc1a10..f704f5883f 100644 --- a/genesis/dev.toml +++ b/genesis/dev.toml @@ -184,6 +184,12 @@ 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 = 20000000 +# Fee unshielding gas limit +fee_unshielding_gas_limit = 20000 +# Fee unshielding descriptions limit +fee_unshielding_descriptions_limit = 15 # vp whitelist vp_whitelist = [] # tx whitelist @@ -199,6 +205,9 @@ pos_gain_d = "0.1" # The maximum number of signatures allowed per transaction max_signatures_per_transaction = 15 +[parameters.gas_cost] +"atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" = "0.000001" + # Proof of stake parameters. [pos_params] # Maximum number of consensus validators. diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index 45df21e09a..0af2a23b86 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -160,6 +160,12 @@ 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 = 20000000 +# Fee unshielding gas limit +fee_unshielding_gas_limit = 20000 +# Fee unshielding descriptions limit +fee_unshielding_descriptions_limit = 15 # vp whitelist vp_whitelist = [] # tx whitelist @@ -177,6 +183,9 @@ pos_gain_d = "0.1" # The maximum number of signatures allowed per transaction max_signatures_per_transaction = 15 +[parameters.gas_cost] +"atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" = "0.000001" + # Proof of stake parameters. [pos_params] # Maximum number of consensus validators. diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 53e25aa425..3bdc56b160 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -10,10 +10,14 @@ use proc_macro::TokenStream; use proc_macro2::{Span as Span2, TokenStream as TokenStream2}; use quote::{quote, ToTokens}; use syn::punctuated::Punctuated; -use syn::{parse_macro_input, ItemFn, ItemStruct}; +use syn::{parse_macro_input, ExprAssign, FnArg, ItemFn, ItemStruct, Pat}; /// Generate WASM binding for a transaction main entrypoint function. /// +/// It expects an attribute in the form: `gas = u64`, so that a call to the gas +/// meter can be injected as the first instruction of the transaction to account +/// for the whitelisted gas amount. +/// /// This macro expects a function with signature: /// /// ```compiler_fail @@ -23,15 +27,38 @@ use syn::{parse_macro_input, ItemFn, ItemStruct}; /// ) -> TxResult /// ``` #[proc_macro_attribute] -pub fn transaction(_attr: TokenStream, input: TokenStream) -> TokenStream { +pub fn transaction(attr: TokenStream, input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as ItemFn); - let ident = &ast.sig.ident; + let ItemFn { + attrs, + vis, + sig, + block, + } = ast; + let stmts = &block.stmts; + let ident = &sig.ident; + let attr_ast = parse_macro_input!(attr as ExprAssign); + let gas = attr_ast.right; + let ctx = match sig.inputs.first() { + Some(FnArg::Typed(pat_type)) => { + if let Pat::Ident(pat_ident) = pat_type.pat.as_ref() { + &pat_ident.ident + } else { + panic!("Unexpected token, expected ctx ident") + } + } + _ => panic!("Unexpected token, expected ctx ident"), + }; let gen = quote! { // Use `wee_alloc` as the global allocator. #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; - #ast + #(#attrs)* #vis #sig { + // Consume the whitelisted gas + #ctx.charge_gas(#gas)?; + #(#stmts)* + } // The module entrypoint callable by wasm runtime #[no_mangle] @@ -63,6 +90,10 @@ pub fn transaction(_attr: TokenStream, input: TokenStream) -> TokenStream { /// Generate WASM binding for validity predicate main entrypoint function. /// +/// It expects an attribute in the form: `gas = u64`, so that a call to the gas +/// meter can be injected as the first instruction of the validity predicate to +/// account for the whitelisted gas amount. +/// /// This macro expects a function with signature: /// /// ```compiler_fail @@ -76,17 +107,40 @@ pub fn transaction(_attr: TokenStream, input: TokenStream) -> TokenStream { /// ``` #[proc_macro_attribute] pub fn validity_predicate( - _attr: TokenStream, + attr: TokenStream, input: TokenStream, ) -> TokenStream { let ast = parse_macro_input!(input as ItemFn); - let ident = &ast.sig.ident; + let ItemFn { + attrs, + vis, + sig, + block, + } = ast; + let stmts = &block.stmts; + let ident = &sig.ident; + let attr_ast = parse_macro_input!(attr as ExprAssign); + let gas = attr_ast.right; + let ctx = match sig.inputs.first() { + Some(FnArg::Typed(pat_type)) => { + if let Pat::Ident(pat_ident) = pat_type.pat.as_ref() { + &pat_ident.ident + } else { + panic!("Unexpected token, expected ctx ident") + } + } + _ => panic!("Unexpected token, expected ctx ident"), + }; let gen = quote! { // Use `wee_alloc` as the global allocator. #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; - #ast + #(#attrs)* #vis #sig { + // Consume the whitelisted gas + #ctx.charge_gas(#gas)?; + #(#stmts)* + } // The module entrypoint callable by wasm runtime #[no_mangle] diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 2d426d5510..d72fdd8629 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -510,6 +510,8 @@ pub struct QueryRawBytes { pub struct Tx { /// Simulate applying the transaction pub dry_run: bool, + /// Simulate applying both the wrapper and inner transactions + pub dry_run_wrapper: bool, /// Dump the transaction bytes to file pub dump_tx: bool, /// The output directory path to where serialize the data @@ -526,16 +528,21 @@ pub struct Tx { /// Whether to force overwrite the above alias, if it is provided, in the /// wallet. pub wallet_alias_force: bool, + /// The amount being payed (for gas unit) to include the transaction + pub fee_amount: Option, /// The fee payer signing key - pub gas_payer: Option, - /// The amount being payed to include the transaction - pub gas_amount: InputAmount, + pub wrapper_fee_payer: Option, /// The token in which the fee is being paid - pub gas_token: C::Address, + pub fee_token: C::Address, + /// The optional spending key for fee unshielding + pub fee_unshield: Option, /// The max amount of gas used to process tx pub gas_limit: GasLimit, /// The optional expiration of the transaction pub expiration: Option, + /// Generate an ephimeral signing key to be used only once to sign a + /// wrapper tx + pub disposable_signing_key: bool, /// The chain id for which the transaction is intended pub chain_id: Option, /// Sign the tx with the key for the given alias from your wallet diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 5b8a8eaa5e..fcbe799a73 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -10,6 +10,7 @@ use ethbridge_bridge_contract::Bridge; use ethers::providers::Middleware; use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; use namada_core::types::key::common; +use namada_core::types::storage::Epoch; use owo_colors::OwoColorize; use serde::{Deserialize, Serialize}; @@ -17,9 +18,11 @@ use super::{block_on_eth_sync, eth_sync_or_exit, BlockOnEthSync}; use crate::eth_bridge::ethers::abi::AbiDecode; use crate::eth_bridge::structs::RelayProof; use crate::ledger::args; +use crate::ledger::masp::{ShieldedContext, ShieldedUtils}; use crate::ledger::queries::{Client, RPC}; use crate::ledger::rpc::{query_wasm_code_hash, validate_amount}; use crate::ledger::tx::{prepare_tx, Error}; +use crate::ledger::wallet::{Wallet, WalletUtils}; use crate::proto::Tx; use crate::types::address::Address; use crate::types::control_flow::time::{Duration, Instant}; @@ -35,8 +38,14 @@ use crate::types::token::{Amount, DenominatedAmount}; use crate::types::voting_power::FractionalVotingPower; /// Craft a transaction that adds a transfer to the Ethereum bridge pool. -pub async fn build_bridge_pool_tx( +pub async fn build_bridge_pool_tx< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, +>( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::EthereumBridgePool { tx: tx_args, asset, @@ -47,8 +56,8 @@ pub async fn build_bridge_pool_tx( fee_payer, code_path, }: args::EthereumBridgePool, - gas_payer: common::PublicKey, -) -> Result { + wrapper_fee_payer: common::PublicKey, +) -> Result<(Tx, Option), Error> { let DenominatedAmount { amount, .. } = validate_amount(client, amount, &BRIDGE_ADDRESS, tx_args.force) .await @@ -76,16 +85,22 @@ pub async fn build_bridge_pool_tx( let mut tx = Tx::new(chain_id, tx_args.expiration); tx.add_code_from_hash(tx_code_hash).add_data(transfer); - prepare_tx::( + // TODO: validate balance of sender and fee payer + + let epoch = prepare_tx::( client, + wallet, + shielded, &tx_args, &mut tx, - gas_payer.clone(), + wrapper_fee_payer, + None, #[cfg(not(feature = "mainnet"))] false, ) - .await; - Ok(tx) + .await?; + + Ok((tx, epoch)) } /// A json serializable representation of the Ethereum diff --git a/shared/src/ledger/ibc/vp/mod.rs b/shared/src/ledger/ibc/vp/mod.rs index ec10688066..4e02cb229a 100644 --- a/shared/src/ledger/ibc/vp/mod.rs +++ b/shared/src/ledger/ibc/vp/mod.rs @@ -292,6 +292,7 @@ mod tests { use std::str::FromStr; use borsh::BorshSerialize; + use namada_core::ledger::gas::TxGasMeter; use prost::Message; use sha2::Digest; @@ -396,6 +397,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); @@ -710,7 +712,9 @@ mod tests { let tx_code = vec![]; let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -746,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 @@ -793,7 +798,9 @@ mod tests { .add_serialized_data(tx_data) .sign_wrapper(keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -922,7 +929,9 @@ mod tests { .add_serialized_data(tx_data) .sign_wrapper(keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1030,7 +1039,9 @@ mod tests { vec![*outer_tx.code_sechash(), *outer_tx.data_sechash()], &keypair_1(), ))); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1124,7 +1135,9 @@ mod tests { .add_serialized_data(tx_data) .sign_wrapper(keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1244,13 +1257,14 @@ mod tests { let tx_code = vec![]; let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(wl_storage.storage.chain_id.clone(), None); tx.add_code(tx_code) .add_serialized_data(tx_data) .sign_wrapper(keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1358,7 +1372,9 @@ mod tests { vec![*outer_tx.code_sechash(), *outer_tx.data_sechash()], &keypair_1(), ))); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1443,7 +1459,9 @@ mod tests { vec![*outer_tx.code_sechash(), *outer_tx.data_sechash()], &keypair_1(), ))); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1565,7 +1583,9 @@ mod tests { vec![*outer_tx.code_sechash(), *outer_tx.data_sechash()], &keypair_1(), ))); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1686,7 +1706,9 @@ mod tests { vec![*outer_tx.code_sechash(), *outer_tx.data_sechash()], &keypair_1(), ))); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1792,7 +1814,9 @@ mod tests { vec![*outer_tx.code_sechash(), *outer_tx.data_sechash()], &keypair_1(), ))); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1894,7 +1918,9 @@ mod tests { .add_serialized_data(tx_data) .sign_wrapper(keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2034,7 +2060,9 @@ mod tests { .add_serialized_data(tx_data) .sign_wrapper(keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2220,7 +2248,9 @@ mod tests { .add_serialized_data(tx_data) .sign_wrapper(keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2365,7 +2395,9 @@ mod tests { .add_serialized_data(tx_data) .sign_wrapper(keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2514,7 +2546,9 @@ mod tests { .add_serialized_data(tx_data) .sign_wrapper(keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2664,7 +2698,9 @@ mod tests { .add_serialized_data(tx_data) .sign_wrapper(keypair_1()); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); diff --git a/shared/src/ledger/masp.rs b/shared/src/ledger/masp.rs index d5322e9bf6..90a9c4a889 100644 --- a/shared/src/ledger/masp.rs +++ b/shared/src/ledger/masp.rs @@ -1386,8 +1386,7 @@ impl ShieldedContext { pub async fn gen_shielded_transfer( &mut self, client: &C, - args: &args::TxTransfer, - shielded_gas: bool, + args: args::TxTransfer, ) -> Result< Option, builder::Error, @@ -1452,28 +1451,14 @@ impl ShieldedContext { let (asset_types, amount) = convert_amount(epoch, &args.token, amt.amount); - let tx_fee = // If there are shielded inputs if let Some(sk) = spending_key { - let InputAmount::Validated(fee) = args.tx.gas_amount else { - unreachable!("The function `gen_shielded_transfer` is only called by `submit_tx` which validates amounts.") - }; - // Transaction fees need to match the amount in the wrapper Transfer - // when MASP source is used - let (_, shielded_fee) = - convert_amount(epoch, &args.tx.gas_token, fee.amount); - let required_amt = if shielded_gas { - amount + shielded_fee.clone() - } else { - amount - }; - // Locate unspent notes that can help us meet the transaction amount let (_, unspent_notes, used_convs) = self .collect_unspent_notes( client, &to_viewing_key(&sk).vk, - required_amt, + amount, epoch, ) .await; @@ -1486,15 +1471,15 @@ impl ShieldedContext { // Commit the conversion notes used during summation for (conv, wit, value) in used_convs.values() { if value.is_positive() { - builder.add_sapling_convert( - conv.clone(), - *value as u64, - wit.clone(), - ) - .map_err(builder::Error::SaplingBuild)?; + builder + .add_sapling_convert( + conv.clone(), + *value as u64, + wit.clone(), + ) + .map_err(builder::Error::SaplingBuild)?; } } - shielded_fee } else { // We add a dummy UTXO to our transaction, but only the source of // the parent Transfer object is used to validate fund @@ -1509,7 +1494,8 @@ impl ShieldedContext { source_enc.as_ref(), )); let script = TransparentAddress(hash.into()); - for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) { + for (denom, asset_type) in MaspDenom::iter().zip(asset_types.iter()) + { builder .add_transparent_input(TxOut { asset_type: *asset_type, @@ -1518,10 +1504,7 @@ impl ShieldedContext { }) .map_err(builder::Error::TransparentBuild)?; } - // No transfer fees come from the shielded transaction for non-MASP - // sources - Amount::zero() - }; + } // Now handle the outputs of this transaction // If there is a shielded output @@ -1570,12 +1553,11 @@ impl ShieldedContext { if let Some(sk) = spending_key { // Represents the amount of inputs we are short by let mut additional = Amount::zero(); - // The change left over from this transaction - let value_balance = builder + for (asset_type, amt) in builder .value_balance() - .expect("unable to compute value balance") - - tx_fee.clone(); - for (asset_type, amt) in value_balance.components() { + .expect("Unable to compute value balance") + .components() + { if *amt >= 0 { // Send the change in this asset type back to the sender builder @@ -1659,7 +1641,8 @@ impl ShieldedContext { // Build and return the constructed transaction let (masp_tx, metadata) = builder.build( &self.utils.local_tx_prover(), - &FeeRule::non_standard(tx_fee), + // Fees are always paid outside of MASP + &FeeRule::non_standard(Amount::zero()), )?; let built = ShieldedTransfer { builder: builder_clone, diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 323633b563..5a5a084716 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -343,6 +343,7 @@ mod test_bridge_pool_vp { use borsh::BorshSerialize; use namada_core::ledger::eth_bridge::storage::bridge_pool::get_signed_root_key; + use namada_core::ledger::gas::TxGasMeter; use namada_core::types::address; use namada_ethereum_bridge::parameters::{ Contracts, EthereumBridgeConfig, UpgradeableContract, @@ -538,7 +539,9 @@ mod test_bridge_pool_vp { write_log, tx, &TxIndex(0), - VpGasMeter::new(0u64), + VpGasMeter::new_from_tx_meter(&TxGasMeter::new_from_sub_limit( + u64::MAX.into(), + )), keys_changed, verifiers, VpCache::new(temp_dir(), 100usize), diff --git a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs index 5fd7aa6cd1..1941975740 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs @@ -334,6 +334,7 @@ mod tests { use borsh::BorshSerialize; use namada_core::ledger::eth_bridge; use namada_core::ledger::eth_bridge::storage::bridge_pool::BRIDGE_POOL_ADDRESS; + use namada_core::ledger::gas::TxGasMeter; use namada_core::ledger::storage_api::StorageWrite; use namada_ethereum_bridge::parameters::{ Contracts, EthereumBridgeConfig, UpgradeableContract, @@ -426,7 +427,9 @@ mod tests { write_log, tx, &TxIndex(0), - VpGasMeter::new(0u64), + VpGasMeter::new_from_tx_meter(&TxGasMeter::new_from_sub_limit( + u64::MAX.into(), + )), keys_changed, verifiers, VpCache::new(temp_dir(), 100usize), diff --git a/shared/src/ledger/native_vp/mod.rs b/shared/src/ledger/native_vp/mod.rs index 02a3435397..f6dbb82dba 100644 --- a/shared/src/ledger/native_vp/mod.rs +++ b/shared/src/ledger/native_vp/mod.rs @@ -148,14 +148,6 @@ where } } - /// Add a gas cost incured in a validity predicate - pub fn add_gas( - &self, - used_gas: u64, - ) -> Result<(), vp_host_fns::RuntimeError> { - vp_host_fns::add_gas(&mut self.gas_meter.borrow_mut(), used_gas) - } - /// Read access to the prior storage (state before tx execution) /// via [`trait@StorageRead`]. pub fn pre<'view>(&'view self) -> CtxPreStorageRead<'view, 'a, DB, H, CA> { @@ -535,6 +527,10 @@ where unimplemented!("no masp native vp") } + fn charge_gas(&self, _used_gas: u64) -> Result<(), storage_api::Error> { + unimplemented!("Native vps don't consume whitelisted gas") + } + fn get_tx_code_hash(&self) -> Result, storage_api::Error> { vp_host_fns::get_tx_code_hash(&mut self.gas_meter.borrow_mut(), self.tx) .into_storage_result() diff --git a/shared/src/ledger/native_vp/multitoken.rs b/shared/src/ledger/native_vp/multitoken.rs index 3a15da97aa..0d42e86eaa 100644 --- a/shared/src/ledger/native_vp/multitoken.rs +++ b/shared/src/ledger/native_vp/multitoken.rs @@ -140,6 +140,7 @@ mod tests { use std::collections::BTreeSet; use borsh::BorshSerialize; + use namada_core::ledger::gas::TxGasMeter; use super::*; use crate::core::ledger::storage::testing::TestWlStorage; @@ -206,7 +207,9 @@ mod tests { let tx_index = TxIndex::default(); let tx = dummy_tx(&wl_storage); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm_cache(); let mut verifiers = BTreeSet::new(); verifiers.insert(sender); @@ -261,7 +264,9 @@ mod tests { let tx_index = TxIndex::default(); let tx = dummy_tx(&wl_storage); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm_cache(); let verifiers = BTreeSet::new(); let ctx = Ctx::new( @@ -319,7 +324,9 @@ mod tests { let tx_index = TxIndex::default(); let tx = dummy_tx(&wl_storage); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm_cache(); let mut verifiers = BTreeSet::new(); // for the minter @@ -377,7 +384,9 @@ mod tests { let tx_index = TxIndex::default(); let tx = dummy_tx(&wl_storage); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm_cache(); let mut verifiers = BTreeSet::new(); // for the minter @@ -430,7 +439,9 @@ mod tests { let tx_index = TxIndex::default(); let tx = dummy_tx(&wl_storage); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm_cache(); let verifiers = BTreeSet::new(); let ctx = Ctx::new( @@ -488,7 +499,9 @@ mod tests { let tx_index = TxIndex::default(); let tx = dummy_tx(&wl_storage); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm_cache(); let mut verifiers = BTreeSet::new(); // for the minter @@ -528,7 +541,9 @@ mod tests { let tx_index = TxIndex::default(); let tx = dummy_tx(&wl_storage); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm_cache(); let mut verifiers = BTreeSet::new(); // for the minter @@ -571,7 +586,9 @@ mod tests { let tx_index = TxIndex::default(); let tx = dummy_tx(&wl_storage); - let gas_meter = VpGasMeter::new(0); + let gas_meter = VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()), + ); let (vp_wasm_cache, _vp_cache_dir) = wasm_cache(); let verifiers = BTreeSet::new(); let ctx = Ctx::new( diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index 7a0244932a..f032ff3be0 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -1,13 +1,22 @@ //! The ledger's protocol - use std::collections::BTreeSet; use std::panic; +use borsh::BorshSerialize; use eyre::{eyre, WrapErr}; +use masp_primitives::transaction::Transaction; +use namada_core::ledger::gas::TxGasMeter; +use namada_core::ledger::storage::wl_storage::WriteLogAndStorage; +use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; +use namada_core::proto::Section; +use namada_core::types::hash::Hash; +use namada_core::types::storage::Key; +use namada_core::types::token::Amount; +use namada_core::types::transaction::WrapperTx; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use thiserror::Error; -use crate::ledger::gas::{self, BlockGasMeter, VpGasMeter}; +use crate::ledger::gas::{self, GasMetering, VpGasMeter}; use crate::ledger::governance::GovernanceVp; use crate::ledger::ibc::vp::Ibc; use crate::ledger::native_vp::ethereum_bridge::bridge_pool_vp::BridgePoolVp; @@ -20,12 +29,13 @@ use crate::ledger::pgf::PgfVp; use crate::ledger::pos::{self, PosVP}; use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{DBIter, Storage, StorageHasher, WlStorage, DB}; +use crate::ledger::{replay_protection, storage_api}; use crate::proto::{self, Tx}; use crate::types::address::{Address, InternalAddress}; -use crate::types::storage; use crate::types::storage::TxIndex; use crate::types::transaction::protocol::{EthereumTxData, ProtocolTxType}; use crate::types::transaction::{DecryptedTx, TxResult, TxType, VpsResult}; +use crate::types::{hash, storage}; use crate::vm::wasm::{TxCache, VpCache}; use crate::vm::{self, wasm, WasmCacheAccess}; @@ -44,8 +54,12 @@ pub enum Error { ProtocolTxError(#[from] eyre::Error), #[error("Txs must either be encrypted or a decryption of an encrypted tx")] TxTypeError, - #[error("Gas error: {0}")] - GasError(gas::Error), + #[error("Fee ushielding error: {0}")] + FeeUnshieldingError(crate::types::transaction::WrapperTxErr), + #[error("{0}")] + GasError(#[from] gas::Error), + #[error("Error while processing transaction's fees: {0}")] + FeeError(String), #[error("Error executing VP for addresses: {0:?}")] VpRunnerError(vm::wasm::run::Error), #[error("The address {0} doesn't exist")] @@ -78,25 +92,36 @@ pub enum Error { /// Shell parameters for running wasm transactions. #[allow(missing_docs)] -pub enum ShellParams<'a, D, H, CA> +pub struct ShellParams<'a, CA, WLS> where - D: 'static + DB + for<'iter> DBIter<'iter> + Sync, - H: 'static + StorageHasher + Sync, CA: 'static + WasmCacheAccess + Sync, + WLS: WriteLogAndStorage + StorageRead, { - /// Parameters passed to dry ran txs. - DryRun { - storage: &'a Storage, - vp_wasm_cache: &'a mut VpCache, - tx_wasm_cache: &'a mut TxCache, - }, - /// Parameters passed to mutating tx executions. - Mutating { - block_gas_meter: &'a mut BlockGasMeter, - wl_storage: &'a mut WlStorage, + tx_gas_meter: &'a mut TxGasMeter, + wl_storage: &'a mut WLS, + vp_wasm_cache: &'a mut VpCache, + tx_wasm_cache: &'a mut TxCache, +} + +impl<'a, CA, WLS> ShellParams<'a, CA, WLS> +where + CA: 'static + WasmCacheAccess + Sync, + WLS: WriteLogAndStorage + StorageRead, +{ + /// Create a new instance of `ShellParams` + pub fn new( + tx_gas_meter: &'a mut TxGasMeter, + wl_storage: &'a mut WLS, vp_wasm_cache: &'a mut VpCache, tx_wasm_cache: &'a mut TxCache, - }, + ) -> Self { + Self { + tx_gas_meter, + wl_storage, + vp_wasm_cache, + tx_wasm_cache, + } + } } /// Result of applying a transaction @@ -112,12 +137,14 @@ pub type Result = std::result::Result; #[allow(clippy::too_many_arguments)] pub fn dispatch_tx<'a, D, H, CA>( tx: Tx, - tx_length: usize, + tx_bytes: &'a [u8], tx_index: TxIndex, - block_gas_meter: &'a mut BlockGasMeter, + tx_gas_meter: &'a mut TxGasMeter, wl_storage: &'a mut WlStorage, vp_wasm_cache: &'a mut VpCache, tx_wasm_cache: &'a mut TxCache, + block_proposer: Option<&'a Address>, + #[cfg(not(feature = "mainnet"))] has_valid_pow: bool, ) -> Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, @@ -131,10 +158,9 @@ where has_valid_pow, }) => apply_wasm_tx( tx, - tx_length, &tx_index, - ShellParams::Mutating { - block_gas_meter, + ShellParams { + tx_gas_meter, wl_storage, vp_wasm_cache, tx_wasm_cache, @@ -145,77 +171,474 @@ where TxType::Protocol(protocol_tx) => { apply_protocol_tx(protocol_tx.tx, tx.data(), wl_storage) } - TxType::Wrapper(_) | TxType::Decrypted(DecryptedTx::Undecryptable) => { - // do not apply db updates, but charge gas anyway. - // 1) we can only apply state updates on encrypted txs - // at the next block height - // 2) undecryptable txs should not perform any state - // updates either. errors are emitted at a layer above, - // in `Shell::finalize_block()`. - let gas_used = block_gas_meter - .finalize_transaction() - .map_err(Error::GasError)?; + TxType::Wrapper(ref wrapper) => { + let masp_transaction = + wrapper.unshield_section_hash.and_then(|ref hash| { + tx.get_section(hash).and_then(|section| { + if let Section::MaspTx(transaction) = section.as_ref() { + Some(transaction.to_owned()) + } else { + None + } + }) + }); + + let changed_keys = apply_wrapper_tx( + wrapper, + masp_transaction, + tx_bytes, + ShellParams { + tx_gas_meter, + wl_storage, + vp_wasm_cache, + tx_wasm_cache, + }, + block_proposer, + #[cfg(not(feature = "mainnet"))] + has_valid_pow, + )?; Ok(TxResult { - gas_used, - ..Default::default() + gas_used: tx_gas_meter.get_tx_consumed_gas(), + changed_keys, + vps_result: VpsResult::default(), + initialized_accounts: vec![], + ibc_events: BTreeSet::default(), }) } + TxType::Decrypted(DecryptedTx::Undecryptable) => { + Ok(TxResult::default()) + } + } +} + +/// Load the wasm hash for a transfer from storage. +/// +/// # Panics +/// If the transaction hash is not found in storage +pub fn get_transfer_hash_from_storage(storage: &S) -> Hash +where + S: StorageRead, +{ + let transfer_code_name_key = + Key::wasm_code_name("tx_transfer.wasm".to_string()); + storage + .read(&transfer_code_name_key) + .expect("Could not read the storage") + .expect("Expected tx transfer hash in storage") +} + +/// Performs the required operation on a wrapper transaction: +/// - replay protection +/// - fee payment +/// - gas accounting +/// +/// Returns the set of changed storage keys. +pub(crate) fn apply_wrapper_tx<'a, D, H, CA, WLS>( + wrapper: &WrapperTx, + fee_unshield_transaction: Option, + tx_bytes: &[u8], + mut shell_params: ShellParams<'a, CA, WLS>, + block_proposer: Option<&Address>, + #[cfg(not(feature = "mainnet"))] has_valid_pow: bool, +) -> Result> +where + CA: 'static + WasmCacheAccess + Sync, + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, + WLS: WriteLogAndStorage, +{ + let mut changed_keys = BTreeSet::default(); + let mut tx: Tx = tx_bytes.try_into().unwrap(); + + // Writes wrapper tx hash to block write log (changes must be persisted even + // in case of failure) + let wrapper_hash_key = + replay_protection::get_tx_hash_key(&hash::Hash(tx.header_hash().0)); + shell_params + .wl_storage + .write(&wrapper_hash_key, ()) + .expect("Error while writing tx hash to storage"); + changed_keys.insert(wrapper_hash_key); + + // Charge fee before performing any fallible operations + charge_fee( + wrapper, + fee_unshield_transaction, + &mut shell_params, + #[cfg(not(feature = "mainnet"))] + has_valid_pow, + block_proposer, + &mut changed_keys, + )?; + + // Account for gas + shell_params.tx_gas_meter.add_tx_size_gas(tx_bytes)?; + + // If wrapper was succesful, write inner tx hash to storage + let inner_hash_key = replay_protection::get_tx_hash_key(&hash::Hash( + tx.update_header(TxType::Raw).header_hash().0, + )); + shell_params + .wl_storage + .write(&inner_hash_key, ()) + .expect("Error while writing tx hash to storage"); + changed_keys.insert(inner_hash_key); + + Ok(changed_keys) +} + +/// Charge fee for the provided wrapper transaction. In ABCI returns an error if +/// the balance of the block proposer overflows. In ABCI plus returns error if: +/// - The unshielding fails +/// - Fee amount overflows +/// - Not enough funds are available to pay the entire amount of the fee +/// - The accumulated fee amount to be credited to the block proposer overflows +pub fn charge_fee<'a, D, H, CA, WLS>( + wrapper: &WrapperTx, + masp_transaction: Option, + shell_params: &mut ShellParams<'a, CA, WLS>, + #[cfg(not(feature = "mainnet"))] has_valid_pow: bool, + block_proposer: Option<&Address>, + changed_keys: &mut BTreeSet, +) -> Result<()> +where + CA: 'static + WasmCacheAccess + Sync, + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, + WLS: WriteLogAndStorage, +{ + let ShellParams { + tx_gas_meter: _, + wl_storage, + vp_wasm_cache, + tx_wasm_cache, + } = shell_params; + + // Unshield funds if requested + if let Some(transaction) = masp_transaction { + // The unshielding tx does not charge gas, instantiate a + // custom gas meter for this step + let mut tx_gas_meter = + TxGasMeter::new( + wl_storage + .read::( + &namada_core::ledger::parameters::storage::get_fee_unshielding_gas_limit_key( + ), + ) + .expect("Error reading the storage") + .expect("Missing fee unshielding gas limit in storage").into(), + ); + + // If it fails, do not return early + // from this function but try to take the funds from the unshielded + // balance + match wrapper.generate_fee_unshielding( + get_transfer_hash_from_storage(*wl_storage), + transaction, + ) { + Ok(fee_unshielding_tx) => { + // NOTE: A clean tx write log must be provided to this call + // for a correct vp validation. Block write log, instead, + // should contain any prior changes (if any) + wl_storage.write_log_mut().precommit_tx(); + match apply_wasm_tx( + fee_unshielding_tx, + &TxIndex::default(), + ShellParams { + tx_gas_meter: &mut tx_gas_meter, + wl_storage: *wl_storage, + vp_wasm_cache, + tx_wasm_cache, + }, + #[cfg(not(feature = "mainnet"))] + false, + ) { + Ok(result) => { + // NOTE: do not commit yet cause this could be + // exploited to get free unshieldings + if !result.is_accepted() { + wl_storage.write_log_mut().drop_tx_keep_precommit(); + tracing::error!( + "The unshielding tx is invalid, some VPs \ + rejected it: {:#?}", + result.vps_result.rejected_vps + ); + } + } + Err(e) => { + wl_storage.write_log_mut().drop_tx_keep_precommit(); + tracing::error!( + "The unshielding tx is invalid, wasm run failed: \ + {}", + e + ); + } + } + } + Err(e) => tracing::error!("{}", e), + } + } + + // Charge or check fees + match block_proposer { + Some(proposer) => transfer_fee( + *wl_storage, + proposer, + #[cfg(not(feature = "mainnet"))] + has_valid_pow, + wrapper, + )?, + None => check_fees( + *wl_storage, + #[cfg(not(feature = "mainnet"))] + has_valid_pow, + wrapper, + )?, + } + + changed_keys.extend(wl_storage.write_log_mut().get_keys_with_precommit()); + + // Commit tx write log even in case of subsequent errors + wl_storage.write_log_mut().commit_tx(); + + Ok(()) +} + +/// Perform the actual transfer of fess from the fee payer to the block +/// proposer. +pub fn transfer_fee( + wl_storage: &mut WLS, + block_proposer: &Address, + #[cfg(not(feature = "mainnet"))] has_valid_pow: bool, + wrapper: &WrapperTx, +) -> Result<()> +where + WLS: WriteLogAndStorage + StorageRead, +{ + let balance = storage_api::token::read_balance( + wl_storage, + &wrapper.fee.token, + &wrapper.fee_payer(), + ) + .unwrap(); + + match wrapper.get_tx_fee() { + Ok(fees) => { + if balance.checked_sub(fees).is_some() { + token_transfer( + wl_storage, + &wrapper.fee.token, + &wrapper.fee_payer(), + block_proposer, + fees, + ) + .map_err(|e| Error::FeeError(e.to_string())) + } else { + // Balance was insufficient for fee payment + #[cfg(not(feature = "mainnet"))] + let reject = !has_valid_pow; + #[cfg(feature = "mainnet")] + let reject = true; + + if reject { + #[cfg(not(any(feature = "abciplus", feature = "abcipp")))] + { + // Move all the available funds in the transparent + // balance of the fee payer + token_transfer( + wl_storage, + &wrapper.fee.token, + &wrapper.fee_payer(), + block_proposer, + balance, + ) + .map_err(|e| Error::FeeError(e.to_string()))?; + + return Err(Error::FeeError( + "Transparent balance of wrapper's signer was \ + insufficient to pay fee. All the available \ + transparent funds have been moved to the block \ + proposer" + .to_string(), + )); + } + #[cfg(any(feature = "abciplus", feature = "abcipp"))] + return Err(Error::FeeError( + "Insufficient transparent balance to pay fees" + .to_string(), + )); + } else { + tracing::debug!( + "Balance was insufficient for fee payment but a valid \ + PoW was provided" + ); + Ok(()) + } + } + } + Err(e) => { + // Fee overflow + #[cfg(not(any(feature = "abciplus", feature = "abcipp")))] + { + // Move all the available funds in the transparent balance of + // the fee payer + token_transfer( + wl_storage, + &wrapper.fee.token, + &wrapper.fee_payer(), + block_proposer, + balance, + ) + .map_err(|e| Error::FeeError(e.to_string()))?; + + return Err(Error::FeeError(format!( + "{}. All the available transparent funds have been moved \ + to the block proposer", + e + ))); + } + + #[cfg(any(feature = "abciplus", feature = "abcipp"))] + return Err(Error::FeeError(e.to_string())); + } + } +} + +/// Transfer `token` from `src` to `dest`. Returns an `Err` if `src` has +/// insufficient balance or if the transfer the `dest` would overflow (This can +/// only happen if the total supply does't fit in `token::Amount`). Contrary to +/// `storage_api::token::transfer` this function updates the tx write log and +/// not the block write log. +fn token_transfer( + wl_storage: &mut WLS, + token: &Address, + src: &Address, + dest: &Address, + amount: Amount, +) -> Result<()> +where + WLS: WriteLogAndStorage + StorageRead, +{ + let src_key = namada_core::types::token::balance_key(token, src); + let src_balance = namada_core::ledger::storage_api::token::read_balance( + wl_storage, token, src, + ) + .expect("Token balance read in protocol must not fail"); + match src_balance.checked_sub(amount) { + Some(new_src_balance) => { + if src == dest { + return Ok(()); + } + let dest_key = namada_core::types::token::balance_key(token, dest); + let dest_balance = + namada_core::ledger::storage_api::token::read_balance( + wl_storage, token, dest, + ) + .expect("Token balance read in protocol must not fail"); + match dest_balance.checked_add(amount) { + Some(new_dest_balance) => { + wl_storage + .write_log_mut() + .write(&src_key, new_src_balance.try_to_vec().unwrap()) + .map_err(|e| Error::FeeError(e.to_string()))?; + match wl_storage.write_log_mut().write( + &dest_key, + new_dest_balance.try_to_vec().unwrap(), + ) { + Ok(_) => Ok(()), + Err(e) => Err(Error::FeeError(e.to_string())), + } + } + None => Err(Error::FeeError( + "The transfer would overflow destination balance" + .to_string(), + )), + } + } + None => Err(Error::FeeError("Insufficient source balance".to_string())), + } +} + +/// Check if the fee payer has enough transparent balance to pay fees +pub fn check_fees( + wl_storage: &WLS, + #[cfg(not(feature = "mainnet"))] has_valid_pow: bool, + wrapper: &WrapperTx, +) -> Result<()> +where + WLS: WriteLogAndStorage + StorageRead, +{ + let balance = storage_api::token::read_balance( + wl_storage, + &wrapper.fee.token, + &wrapper.fee_payer(), + ) + .unwrap(); + + let fees = wrapper + .get_tx_fee() + .map_err(|e| Error::FeeError(e.to_string()))?; + + if balance.checked_sub(fees).is_some() { + Ok(()) + } else { + // Balance was insufficient for fee payment + #[cfg(not(feature = "mainnet"))] + let reject = !has_valid_pow; + #[cfg(feature = "mainnet")] + let reject = true; + + if reject { + Err(Error::FeeError( + "Insufficient transparent balance to pay fees".to_string(), + )) + } else { + tracing::debug!( + "Balance was insufficient for fee payment but a valid PoW was \ + provided" + ); + Ok(()) + } } } /// Apply a transaction going via the wasm environment. Gas will be metered and /// validity predicates will be triggered in the normal way. -pub(crate) fn apply_wasm_tx<'a, D, H, CA>( +pub fn apply_wasm_tx<'a, D, H, CA, WLS>( tx: Tx, - tx_length: usize, tx_index: &TxIndex, - shell_params: ShellParams<'a, D, H, CA>, + shell_params: ShellParams<'a, CA, WLS>, #[cfg(not(feature = "mainnet"))] has_valid_pow: bool, ) -> Result where + CA: 'static + WasmCacheAccess + Sync, D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, - CA: 'static + WasmCacheAccess + Sync, + WLS: WriteLogAndStorage, { - let mut default_gas_meter = Default::default(); - let mut default_write_log = Default::default(); + let ShellParams { + tx_gas_meter, + wl_storage, + vp_wasm_cache, + tx_wasm_cache, + } = shell_params; - let (block_gas_meter, storage, write_log, vp_wasm_cache, tx_wasm_cache) = - match shell_params { - ShellParams::Mutating { - block_gas_meter, - wl_storage, - vp_wasm_cache, - tx_wasm_cache, - } => ( - block_gas_meter, - &wl_storage.storage, - &mut wl_storage.write_log, - vp_wasm_cache, - tx_wasm_cache, - ), - ShellParams::DryRun { - storage, - vp_wasm_cache, - tx_wasm_cache, - } => ( - &mut default_gas_meter, - storage, - &mut default_write_log, - vp_wasm_cache, - tx_wasm_cache, - ), - }; + let (tx_gas_meter, storage, write_log, vp_wasm_cache, tx_wasm_cache) = { + let (write_log, storage) = wl_storage.split_borrow(); + ( + tx_gas_meter, + storage, + write_log, + vp_wasm_cache, + tx_wasm_cache, + ) + }; - // Base gas cost for applying the tx - block_gas_meter - .add_base_transaction_fee(tx_length) - .map_err(Error::GasError)?; let verifiers = execute_tx( &tx, tx_index, storage, - block_gas_meter, + tx_gas_meter, write_log, vp_wasm_cache, tx_wasm_cache, @@ -225,7 +648,7 @@ where tx: &tx, tx_index, storage, - gas_meter: block_gas_meter, + tx_gas_meter, write_log, verifiers_from_tx: &verifiers, vp_wasm_cache, @@ -233,9 +656,7 @@ where has_valid_pow, })?; - let gas_used = block_gas_meter - .finalize_transaction() - .map_err(Error::GasError)?; + let gas_used = tx_gas_meter.get_tx_consumed_gas(); let initialized_accounts = write_log.get_initialized_accounts(); let changed_keys = write_log.get_keys(); let ibc_events = write_log.take_ibc_events(); @@ -326,11 +747,12 @@ where } /// 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, write_log: &mut WriteLog, vp_wasm_cache: &mut VpCache, tx_wasm_cache: &mut TxCache, @@ -343,13 +765,19 @@ where wasm::run::tx( storage, write_log, - gas_meter, + tx_gas_meter, tx_index, tx, 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) + } + }) } /// Arguments to [`check_vps`]. @@ -362,7 +790,7 @@ where tx: &'a Tx, tx_index: &'a TxIndex, storage: &'a Storage, - gas_meter: &'a mut BlockGasMeter, + tx_gas_meter: &'a mut TxGasMeter, write_log: &'a WriteLog, verifiers_from_tx: &'a BTreeSet
, vp_wasm_cache: &'a mut VpCache, @@ -376,7 +804,7 @@ fn check_vps( tx, tx_index, storage, - gas_meter, + tx_gas_meter, write_log, verifiers_from_tx, vp_wasm_cache, @@ -392,8 +820,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, @@ -401,15 +827,13 @@ where tx_index, storage, write_log, - initial_gas, + tx_gas_meter, vp_wasm_cache, has_valid_pow, )?; tracing::debug!("Total VPs gas cost {:?}", vps_result.gas_used); - gas_meter - .add_vps_gas(&vps_result.gas_used) - .map_err(Error::GasError)?; + tx_gas_meter.add_vps_gas(&vps_result.gas_used)?; Ok(vps_result) } @@ -423,7 +847,7 @@ fn execute_vps( tx_index: &TxIndex, storage: &Storage, write_log: &WriteLog, - initial_gas: u64, + tx_gas_meter: &TxGasMeter, vp_wasm_cache: &mut VpCache, #[cfg(not(feature = "mainnet"))] // This is true when the wrapper of this tx contained a valid @@ -438,17 +862,21 @@ 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_from_tx_meter(tx_gas_meter); let accept = match &addr { Address::Implicit(_) | Address::Established(_) => { let (vp_hash, gas) = storage .validity_predicate(addr) .map_err(Error::StorageError)?; - gas_meter.add(gas).map_err(Error::GasError)?; + gas_meter.consume(gas).map_err(Error::GasError)?; let Some(vp_code_hash) = vp_hash else { return Err(Error::MissingAddress(addr.clone())); }; + // 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, @@ -600,30 +1028,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_meter) }) } @@ -631,7 +1046,7 @@ where fn merge_vp_results( a: VpsResult, mut b: VpsResult, - initial_gas: u64, + tx_gas_meter: &TxGasMeter, ) -> Result { let mut accepted_vps = a.accepted_vps; let mut rejected_vps = a.rejected_vps; @@ -641,13 +1056,7 @@ fn merge_vp_results( errors.append(&mut b.errors); let mut gas_used = a.gas_used; - // 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 - - gas_used - .merge(&mut b.gas_used, initial_gas) - .map_err(Error::GasError)?; + gas_used.merge(&mut b.gas_used, tx_gas_meter)?; Ok(VpsResult { accepted_vps, diff --git a/shared/src/ledger/queries/mod.rs b/shared/src/ledger/queries/mod.rs index ce689e6325..e7793275d2 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -94,6 +94,7 @@ pub fn require_no_data(request: &RequestQuery) -> storage_api::Result<()> { /// Queries testing helpers #[cfg(any(test, feature = "testing"))] mod testing { + use tempfile::TempDir; use tendermint_rpc::Response; @@ -134,7 +135,23 @@ 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 limit + 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( + &20_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 94412f1a15..f0d62f7ae4 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -100,32 +100,106 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { + use namada_core::ledger::gas::{Gas, GasMetering, TxGasMeter}; + use namada_core::ledger::storage::TempWlStorage; + use namada_core::types::transaction::DecryptedTx; + use crate::ledger::protocol::{self, ShellParams}; use crate::proto::Tx; use crate::types::storage::TxIndex; - use crate::types::transaction::decrypted::DecryptedTx; - use crate::types::transaction::TxType; + use crate::types::transaction::wrapper::wrapper_tx::PairingEngine; + use crate::types::transaction::{AffineCurve, EllipticCurve, TxType}; let mut tx = Tx::try_from(&request.data[..]).into_storage_result()?; - tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted { - #[cfg(not(feature = "mainnet"))] - // To be able to dry-run testnet faucet withdrawal, pretend - // that we got a valid PoW - has_valid_pow: true, - })); - let data = protocol::apply_wasm_tx( + tx.validate_tx().into_storage_result()?; + + let mut temp_wl_storage = TempWlStorage::new(&ctx.wl_storage.storage); + let mut cumulated_gas = Gas::default(); + + // Wrapper dry run to allow estimating the gas cost of a transaction + let mut tx_gas_meter = match tx.header().tx_type { + TxType::Wrapper(wrapper) => { + let mut tx_gas_meter = + TxGasMeter::new(wrapper.gas_limit.to_owned()); + protocol::apply_wrapper_tx( + &wrapper, + None, + &request.data, + ShellParams::new( + &mut tx_gas_meter, + &mut temp_wl_storage, + &mut ctx.vp_wasm_cache, + &mut ctx.tx_wasm_cache, + ), + None, + #[cfg(not(feature = "mainnet"))] + false, + ) + .into_storage_result()?; + + temp_wl_storage.write_log.commit_tx(); + cumulated_gas = tx_gas_meter.get_tx_consumed_gas(); + + // NOTE: the encryption key for a dry-run should always be an + // hardcoded, dummy one + let _privkey = + ::G2Affine::prime_subgroup_generator(); + tx.update_header(TxType::Decrypted( + DecryptedTx::Decrypted { #[cfg(not(feature = "mainnet"))] + // To be able to dry-run testnet faucet withdrawal, pretend + // that we got a valid PoW + has_valid_pow: true }, + )); + TxGasMeter::new_from_sub_limit(tx_gas_meter.get_available_gas()) + } + TxType::Protocol(_) | TxType::Decrypted(_) => { + // If dry run only the inner tx, use the max block gas as the gas + // limit + TxGasMeter::new( + namada_core::ledger::gas::get_max_block_gas(ctx.wl_storage) + .unwrap() + .into(), + ) + } + TxType::Raw => { + // Cast tx to a decrypted for execution + tx.update_header(TxType::Decrypted(DecryptedTx::Decrypted { + #[cfg(not(feature = "mainnet"))] + has_valid_pow: true, + })); + + // If dry run only the inner tx, use the max block gas as the gas + // limit + TxGasMeter::new( + namada_core::ledger::gas::get_max_block_gas(ctx.wl_storage) + .unwrap() + .into(), + ) + } + }; + + let mut data = protocol::apply_wasm_tx( tx, - request.data.len(), &TxIndex(0), - ShellParams::DryRun { - storage: &ctx.wl_storage.storage, - vp_wasm_cache: &mut ctx.vp_wasm_cache, - tx_wasm_cache: &mut ctx.tx_wasm_cache, - }, + ShellParams::new( + &mut tx_gas_meter, + &mut temp_wl_storage, + &mut ctx.vp_wasm_cache, + &mut ctx.tx_wasm_cache, + ), #[cfg(not(feature = "mainnet"))] true, ) .into_storage_result()?; + cumulated_gas = cumulated_gas + .checked_add(tx_gas_meter.get_tx_consumed_gas()) + .ok_or(namada_core::ledger::storage_api::Error::SimpleMessage( + "Overflow in gas", + ))?; + // Account gas for both inner and wrapper (if available) + data.gas_used = cumulated_gas; + // NOTE: the keys changed by the wrapper transaction (if any) are not + // returned from this function let data = data.try_to_vec().into_storage_result()?; Ok(EncodedResponseQuery { data, @@ -487,8 +561,7 @@ where #[cfg(test)] mod test { - - use borsh::BorshDeserialize; + use borsh::{BorshDeserialize, BorshSerialize}; use namada_test_utils::TestWasms; use crate::ledger::queries::testing::TestClient; @@ -531,7 +604,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/rpc.rs b/shared/src/ledger/rpc.rs index d0fad687be..8d64441e0d 100644 --- a/shared/src/ledger/rpc.rs +++ b/shared/src/ledger/rpc.rs @@ -464,8 +464,8 @@ pub async fn dry_run_tx( pub enum TxBroadcastData { /// Dry run broadcast data DryRun(Tx), - /// Wrapper broadcast data - Wrapper { + /// Live broadcast data + Live { /// Transaction to broadcast tx: Tx, /// Hash of the wrapper transaction diff --git a/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs index 5c1e6a975c..28f931e6e4 100644 --- a/shared/src/ledger/signing.rs +++ b/shared/src/ledger/signing.rs @@ -1,6 +1,8 @@ //! Functions to sign transactions -use std::collections::HashMap; + +use std::collections::{BTreeMap, HashMap}; use std::io::ErrorKind; +use std::path::PathBuf; use borsh::{BorshDeserialize, BorshSerialize}; use data_encoding::HEXLOWER; @@ -19,8 +21,11 @@ use namada_core::types::token::{self, Amount, DenominatedAmount, MaspDenom}; use namada_core::types::transaction::pos; use prost::Message; use serde::{Deserialize, Serialize}; +use sha2::Digest; use zeroize::Zeroizing; +use super::masp::{ShieldedContext, ShieldedTransfer, ShieldedUtils}; +use super::rpc::validate_amount; use crate::ibc::applications::transfer::msgs::transfer::MsgTransfer; use crate::ibc_proto::google::protobuf::Any; use crate::ledger::masp::make_asset_type; @@ -64,7 +69,7 @@ pub struct SigningTxData { /// The public keys to index map associated to an account pub account_public_keys_map: Option, /// The public keys of the fee payer - pub gas_payer: common::PublicKey, + pub fee_payer: common::PublicKey, } /// Find the public key for the given address and try to load the keypair @@ -163,6 +168,7 @@ pub async fn tx_signers< // Now actually fetch the signing key and apply it match signer { Some(signer) if signer == masp() => Ok(vec![masp_tx_key().ref_to()]), + Some(signer) => Ok(vec![ find_pk::(client, wallet, &signer, args.password.clone()) .await?, @@ -179,6 +185,9 @@ pub async fn tx_signers< /// If no explicit signer given, use the `default`. If no `default` is given, /// Error. /// +/// It also takes a second, optional keypair to sign the wrapper header +/// separately. +/// /// If this is not a dry run, the tx is put in a wrapper and returned along with /// hashes needed for monitoring the tx on chain. /// @@ -213,7 +222,7 @@ pub fn sign_tx( } let fee_payer_keypair = - find_key_by_pk(wallet, args, &signing_data.gas_payer).expect(""); + find_key_by_pk(wallet, args, &signing_data.fee_payer).expect(""); tx.sign_wrapper(fee_payer_keypair); } @@ -229,7 +238,7 @@ pub async fn aux_signing_data< owner: &Option
, default_signer: Option
, ) -> Result { - let public_keys = if owner.is_some() || args.gas_payer.is_none() { + let public_keys = if owner.is_some() || args.wrapper_fee_payer.is_none() { tx_signers::(client, wallet, args, default_signer.clone()).await? } else { vec![] @@ -254,16 +263,27 @@ pub async fn aux_signing_data< None => (None, 0u8), }; - let gas_payer = match &args.gas_payer { - Some(keypair) => keypair.ref_to(), - None => public_keys.get(0).ok_or(Error::InvalidFeePayer)?.clone(), + let fee_payer = if args.disposable_signing_key { + wallet.generate_disposable_signing_key().to_public() + } else { + match &args.wrapper_fee_payer { + Some(keypair) => keypair.to_public(), + None => public_keys.get(0).ok_or(Error::InvalidFeePayer)?.clone(), + } }; + if fee_payer == masp_tx_key().to_public() { + panic!( + "The gas payer cannot be the MASP, please provide a different gas \ + payer." + ); + } + Ok(SigningTxData { public_keys, threshold, account_public_keys_map, - gas_payer, + fee_payer, }) } @@ -273,40 +293,24 @@ pub async fn aux_signing_data< pub async fn solve_pow_challenge( client: &C, args: &args::Tx, - keypair: &common::PublicKey, requires_pow: bool, -) -> (Option, Fee) { - let wrapper_tx_fees_key = parameter_storage::get_wrapper_tx_fees_key(); - let gas_amount = rpc::query_storage_value::( - client, - &wrapper_tx_fees_key, - ) - .await - .unwrap_or_default(); - let gas_token = &args.gas_token; - let source = Address::from(keypair); - let balance_key = token::balance_key(gas_token, &source); - let balance = - rpc::query_storage_value::(client, &balance_key) - .await - .unwrap_or_default(); - let is_bal_sufficient = gas_amount <= balance; + total_fee: Amount, + balance: Amount, + source: Address, +) -> Option { + let is_bal_sufficient = total_fee <= balance; if !is_bal_sufficient { - let token_addr = args.gas_token.clone(); + let token_addr = args.fee_token.clone(); let err_msg = format!( "The wrapper transaction source doesn't have enough balance to \ pay fee {}, got {}.", - format_denominated_amount(client, &token_addr, gas_amount).await, + format_denominated_amount(client, &token_addr, total_fee).await, format_denominated_amount(client, &token_addr, balance).await, ); if !args.force && cfg!(feature = "mainnet") { panic!("{}", err_msg); } } - let fee = Fee { - amount: gas_amount, - token: gas_token.clone(), - }; // A PoW solution can be used to allow zero-fee testnet transactions // If the address derived from the keypair doesn't have enough balance // to pay for the fee, allow to find a PoW solution instead. @@ -317,9 +321,9 @@ pub async fn solve_pow_challenge( // Solve the solution, this blocks until a solution is found let solution = challenge.solve(); - (Some(solution), fee) + Some(solution) } else { - (None, fee) + None } } @@ -329,40 +333,342 @@ pub async fn update_pow_challenge( client: &C, args: &args::Tx, tx: &mut Tx, - keypair: &common::PublicKey, requires_pow: bool, + source: Address, ) { + let gas_cost_key = parameter_storage::get_gas_cost_key(); + let minimum_fee = match rpc::query_storage_value::< + C, + BTreeMap, + >(client, &gas_cost_key) + .await + .and_then(|map| map.get(&args.fee_token).map(ToOwned::to_owned)) + { + Some(amount) => amount, + None => { + eprintln!( + "Could not retrieve the gas cost for token {}", + args.fee_token + ); + if !args.force { + panic!(); + } else { + token::Amount::default() + } + } + }; + let fee_amount = match args.fee_amount { + Some(amount) => { + let validated_fee_amount = + validate_amount(client, amount, &args.fee_token, args.force) + .await + .expect("Expected to be able to validate fee"); + + let amount = + Amount::from_uint(validated_fee_amount.amount, 0).unwrap(); + + if amount >= minimum_fee { + amount + } else if !args.force { + // Update the fee amount if it's not enough + println!( + "The provided gas price {} is less than the minimum \ + amount required {}, changing it to match the minimum", + amount.to_string_native(), + minimum_fee.to_string_native() + ); + minimum_fee + } else { + amount + } + } + None => minimum_fee, + }; + let total_fee = fee_amount * u64::from(args.gas_limit); + + let balance_key = token::balance_key(&args.fee_token, &source); + let balance = + rpc::query_storage_value::(client, &balance_key) + .await + .unwrap_or_default(); + if let TxType::Wrapper(wrapper) = &mut tx.header.tx_type { - let (pow_solution, fee) = - solve_pow_challenge(client, args, keypair, requires_pow).await; - wrapper.fee = fee; + let pow_solution = solve_pow_challenge( + client, + args, + requires_pow, + total_fee, + balance, + source, + ) + .await; + wrapper.fee = Fee { + amount_per_gas_unit: fee_amount, + token: args.fee_token.clone(), + }; wrapper.pow_solution = pow_solution; } } +/// Informations about the post-tx balance of the tx's source. Used to correctly +/// handle fee validation in the wrapper tx +pub struct TxSourcePostBalance { + /// The balance of the tx source after the tx has been applied + pub post_balance: Amount, + /// The source address of the tx + pub source: Address, + /// The token of the tx + pub token: Address, +} + /// Create a wrapper tx from a normal tx. Get the hash of the /// wrapper and its payload which is needed for monitoring its /// progress on chain. -pub async fn wrap_tx( +#[allow(clippy::too_many_arguments)] +pub async fn wrap_tx< + C: crate::ledger::queries::Client + Sync, + V: ShieldedUtils, +>( client: &C, + shielded: &mut ShieldedContext, tx: &mut Tx, args: &args::Tx, + tx_source_balance: Option, epoch: Epoch, - gas_payer: common::PublicKey, + fee_payer: common::PublicKey, #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) { +) -> Option { + let fee_payer_address = Address::from(&fee_payer); + // Validate fee amount and token + let gas_cost_key = parameter_storage::get_gas_cost_key(); + let minimum_fee = match rpc::query_storage_value::< + C, + BTreeMap, + >(client, &gas_cost_key) + .await + .and_then(|map| map.get(&args.fee_token).map(ToOwned::to_owned)) + { + Some(amount) => amount, + None => { + eprintln!( + "Could not retrieve the gas cost for token {}", + args.fee_token + ); + if !args.force { + panic!(); + } else { + token::Amount::default() + } + } + }; + let fee_amount = match args.fee_amount { + Some(amount) => { + let validated_fee_amount = + validate_amount(client, amount, &args.fee_token, args.force) + .await + .expect("Expected to be able to validate fee"); + + let amount = + Amount::from_uint(validated_fee_amount.amount, 0).unwrap(); + + if amount >= minimum_fee { + amount + } else if !args.force { + // Update the fee amount if it's not enough + println!( + "The provided gas price {} is less than the minimum \ + amount required {}, changing it to match the minimum", + amount.to_string_native(), + minimum_fee.to_string_native() + ); + minimum_fee + } else { + amount + } + } + None => minimum_fee, + }; + + let mut updated_balance = match tx_source_balance { + Some(TxSourcePostBalance { + post_balance: balance, + source, + token, + }) if token == args.fee_token && source == fee_payer_address => balance, + _ => { + let balance_key = + token::balance_key(&args.fee_token, &fee_payer_address); + + rpc::query_storage_value::(client, &balance_key) + .await + .unwrap_or_default() + } + }; + + let total_fee = fee_amount * u64::from(args.gas_limit); + + let (unshield, unshielding_epoch) = match total_fee + .checked_sub(updated_balance) + { + Some(diff) if !diff.is_zero() => { + if let Some(spending_key) = args.fee_unshield.clone() { + // Unshield funds for fee payment + let transfer_args = args::TxTransfer { + tx: args.to_owned(), + source: spending_key, + target: namada_core::types::masp::TransferTarget::Address( + fee_payer_address.clone(), + ), + token: args.fee_token.clone(), + amount: args::InputAmount::Validated(DenominatedAmount { + // NOTE: must unshield the total fee amount, not the + // diff, because the ledger evaluates the transaction in + // reverse (wrapper first, inner second) and cannot know + // ahead of time if the inner will modify the balance of + // the gas payer + amount: total_fee, + denom: 0.into(), + }), + // These last two fields are not used in the function, mock + // them + native_token: args.fee_token.clone(), + tx_code_path: PathBuf::new(), + }; + + match shielded + .gen_shielded_transfer(client, transfer_args) + .await + { + Ok(Some(ShieldedTransfer { + builder: _, + masp_tx: transaction, + metadata: _data, + epoch: unshielding_epoch, + })) => { + let spends = transaction + .sapling_bundle() + .unwrap() + .shielded_spends + .len(); + let converts = transaction + .sapling_bundle() + .unwrap() + .shielded_converts + .len(); + let outs = transaction + .sapling_bundle() + .unwrap() + .shielded_outputs + .len(); + + let descriptions = spends + converts + outs; + + let descriptions_limit_key= parameter_storage::get_fee_unshielding_descriptions_limit_key(); + let descriptions_limit = + rpc::query_storage_value::( + client, + &descriptions_limit_key, + ) + .await + .unwrap(); + + if u64::try_from(descriptions).unwrap() + > descriptions_limit + && !args.force + && cfg!(feature = "mainnet") + { + panic!( + "Fee unshielding descriptions exceed the limit" + ); + } + + updated_balance += total_fee; + (Some(transaction), Some(unshielding_epoch)) + } + Ok(None) => { + eprintln!("Missing unshielding transaction"); + if !args.force && cfg!(feature = "mainnet") { + panic!(); + } + + (None, None) + } + Err(e) => { + eprintln!("Error in fee unshielding generation: {}", e); + if !args.force && cfg!(feature = "mainnet") { + panic!(); + } + + (None, None) + } + } + } else { + let token_addr = args.fee_token.clone(); + let err_msg = format!( + "The wrapper transaction source doesn't have enough \ + balance to pay fee {}, balance: {}.", + format_denominated_amount(client, &token_addr, total_fee) + .await, + format_denominated_amount( + client, + &token_addr, + updated_balance + ) + .await, + ); + eprintln!("{}", err_msg); + if !args.force && cfg!(feature = "mainnet") { + panic!("{}", err_msg); + } + + (None, None) + } + } + _ => { + if args.fee_unshield.is_some() { + println!( + "Enough transparent balance to pay fees: the fee \ + unshielding spending key will be ignored" + ); + } + (None, None) + } + }; + + let unshield_section_hash = unshield.map(|masp_tx| { + let section = Section::MaspTx(masp_tx); + let mut hasher = sha2::Sha256::new(); + section.hash(&mut hasher); + tx.add_section(section); + namada_core::types::hash::Hash(hasher.finalize().into()) + }); + #[cfg(not(feature = "mainnet"))] - let (pow_solution, fee) = - solve_pow_challenge(client, args, &gas_payer, requires_pow).await; + let pow_solution = solve_pow_challenge( + client, + args, + requires_pow, + total_fee, + updated_balance, + fee_payer_address, + ) + .await; tx.add_wrapper( - fee, - gas_payer, + Fee { + amount_per_gas_unit: fee_amount, + token: args.fee_token.clone(), + }, + fee_payer, epoch, - args.gas_limit.clone(), + // TODO: partially validate the gas limit in client + args.gas_limit, #[cfg(not(feature = "mainnet"))] pow_solution, + unshield_section_hash, ); + + unshielding_epoch } #[allow(clippy::result_large_err)] @@ -1100,9 +1406,12 @@ pub async fn to_ledger_vector< Amount::from(wrapper.gas_limit), ) .await; - let gas_amount = - format_denominated_amount(client, &gas_token, wrapper.fee.amount) - .await; + let fee_amount_per_gas_unit = format_denominated_amount( + client, + &gas_token, + wrapper.fee.amount_per_gas_unit, + ) + .await; tv.output_expert.extend(vec![ format!("Timestamp : {}", tx.header.timestamp.0), format!("PK : {}", wrapper.pk), @@ -1111,11 +1420,15 @@ pub async fn to_ledger_vector< format!("Fee token : {}", gas_token), ]); if let Some(token) = tokens.get(&wrapper.fee.token) { - tv.output_expert - .push(format!("Fee amount : {} {}", token, gas_amount)); + tv.output_expert.push(format!( + "Fee amount per gas unit : {} {}", + token, fee_amount_per_gas_unit + )); } else { - tv.output_expert - .push(format!("Fee amount : {}", gas_amount)); + tv.output_expert.push(format!( + "Fee amount per gas unit : {}", + fee_amount_per_gas_unit + )); } } diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index aa9050519e..a1f01d58f2 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -32,7 +32,7 @@ use prost::EncodeError; use thiserror::Error; use super::rpc::query_wasm_code_hash; -use super::signing; +use super::signing::{self, TxSourcePostBalance}; use crate::ibc::applications::transfer::msgs::transfer::MsgTransfer; use crate::ibc::applications::transfer::packet::PacketData; use crate::ibc::applications::transfer::PrefixedCoin; @@ -109,9 +109,9 @@ pub enum Error { instead: {0:?}" )] ExpectDryRun(Tx), - /// Expect a wrapped encrypted running transaction + /// Expect a live running transaction #[error("Cannot broadcast a dry-run transaction")] - ExpectWrappedRun(Tx), + ExpectLiveRun(Tx), /// Error during broadcasting a transaction #[error("Encountered error while broadcasting transaction: {0}")] TxBroadcast(RpcError), @@ -184,16 +184,9 @@ pub enum Error { /// Negative balance after transfer #[error( "The balance of the source {0} is lower than the amount to be \ - transferred and fees. Amount to transfer is {1} {2} and fees are {3} \ - {4}." + transferred. Amount to transfer is {1} {2}" )] - NegativeBalanceAfterTransfer( - Box
, - String, - Box
, - String, - Box
, - ), + NegativeBalanceAfterTransfer(Box
, String, Box
), /// No Balance found for token #[error("{0}")] MaspError(builder::Error), @@ -300,25 +293,38 @@ pub fn dump_tx(args: &args::Tx, tx: Tx) { /// Prepare a transaction for signing and submission by adding a wrapper header /// to it. -pub async fn prepare_tx( +#[allow(clippy::too_many_arguments)] +pub async fn prepare_tx< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, +>( client: &C, + _wallet: &mut Wallet, + shielded: &mut ShieldedContext, args: &args::Tx, tx: &mut Tx, - gas_payer: common::PublicKey, + fee_payer: common::PublicKey, + tx_source_balance: Option, #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) { +) -> Result, Error> { if !args.dry_run { let epoch = rpc::query_epoch(client).await; - signing::wrap_tx( + + Ok(signing::wrap_tx( client, + shielded, tx, args, + tx_source_balance, epoch, - gas_payer, + fee_payer, #[cfg(not(feature = "mainnet"))] requires_pow, ) - .await + .await) + } else { + Ok(None) } } @@ -343,7 +349,7 @@ pub async fn process_tx< // let request_body = request.into_json(); // println!("HTTP request body: {}", request_body); - if args.dry_run { + if args.dry_run || args.dry_run_wrapper { expect_dry_broadcast(TxBroadcastData::DryRun(tx), client).await } else { // We use this to determine when the wrapper tx makes it on-chain @@ -355,13 +361,14 @@ pub async fn process_tx< .update_header(TxType::Raw) .header_hash() .to_string(); - let to_broadcast = TxBroadcastData::Wrapper { + let to_broadcast = TxBroadcastData::Live { tx, wrapper_hash, decrypted_hash, }; - // Either broadcast or submit transaction and collect result into - // sum type + // TODO: implement the code to resubmit the wrapper if it fails because + // of masp epoch Either broadcast or submit transaction and + // collect result into sum type if args.broadcast_only { broadcast_tx(client, &to_broadcast) .await @@ -405,13 +412,19 @@ pub async fn has_revealed_pk( } /// Submit transaction to reveal the given public key -pub async fn build_reveal_pk( +pub async fn build_reveal_pk< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, +>( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args: &args::Tx, address: &Address, public_key: &common::PublicKey, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: &common::PublicKey, +) -> Result<(Tx, Option), Error> { println!( "Submitting a tx to reveal the public key for address {address}..." ); @@ -428,16 +441,20 @@ pub async fn build_reveal_pk( let mut tx = Tx::new(chain_id, args.expiration); tx.add_code_from_hash(tx_code_hash).add_data(public_key); - prepare_tx::( + let epoch = prepare_tx::( client, + wallet, + shielded, args, &mut tx, - gas_payer.clone(), + fee_payer.clone(), + None, #[cfg(not(feature = "mainnet"))] false, ) - .await; - Ok(tx) + .await?; + + Ok((tx, epoch)) } /// Broadcast a transaction to be included in the blockchain and checks that @@ -449,12 +466,12 @@ pub async fn broadcast_tx( to_broadcast: &TxBroadcastData, ) -> Result { let (tx, wrapper_tx_hash, decrypted_tx_hash) = match to_broadcast { - TxBroadcastData::Wrapper { + TxBroadcastData::Live { tx, wrapper_hash, decrypted_hash, } => Ok((tx, wrapper_hash, decrypted_hash)), - TxBroadcastData::DryRun(tx) => Err(Error::ExpectWrappedRun(tx.clone())), + TxBroadcastData::DryRun(tx) => Err(Error::ExpectLiveRun(tx.clone())), }?; tracing::debug!( @@ -500,12 +517,12 @@ where C: crate::ledger::queries::Client + Sync, { let (_, wrapper_hash, decrypted_hash) = match &to_broadcast { - TxBroadcastData::Wrapper { + TxBroadcastData::Live { tx, wrapper_hash, decrypted_hash, } => Ok((tx, wrapper_hash, decrypted_hash)), - TxBroadcastData::DryRun(tx) => Err(Error::ExpectWrappedRun(tx.clone())), + TxBroadcastData::DryRun(tx) => Err(Error::ExpectLiveRun(tx.clone())), }?; // Broadcast the supplied transaction @@ -635,16 +652,20 @@ pub async fn save_initialized_accounts( /// Submit validator comission rate change pub async fn build_validator_commission_change< C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, >( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::CommissionRateChange { tx: tx_args, validator, rate, tx_code_path, }: args::CommissionRateChange, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: common::PublicKey, +) -> Result<(Tx, Option), Error> { let epoch = rpc::query_epoch(client).await; let tx_code_hash = @@ -710,30 +731,38 @@ pub async fn build_validator_commission_change< let mut tx = Tx::new(chain_id, tx_args.expiration); tx.add_code_from_hash(tx_code_hash).add_data(data); - prepare_tx::( + let epoch = prepare_tx::( client, + wallet, + shielded, &tx_args, &mut tx, - gas_payer.clone(), + fee_payer, + None, #[cfg(not(feature = "mainnet"))] false, ) - .await; - Ok(tx) + .await?; + + Ok((tx, epoch)) } /// Submit transaction to unjail a jailed validator pub async fn build_unjail_validator< C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, >( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::TxUnjailValidator { tx: tx_args, validator, tx_code_path, }: args::TxUnjailValidator, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: common::PublicKey, +) -> Result<(Tx, Option), Error> { if !rpc::is_validator(client, &validator).await { eprintln!("The given address {} is not a validator.", &validator); if !tx_args.force { @@ -797,29 +826,39 @@ pub async fn build_unjail_validator< tx.add_code_from_hash(tx_code_hash) .add_data(validator.clone()); - prepare_tx( + let epoch = prepare_tx( client, + wallet, + shielded, &tx_args, &mut tx, - gas_payer.clone(), + fee_payer, + None, #[cfg(not(feature = "mainnet"))] false, ) - .await; - Ok(tx) + .await?; + + Ok((tx, epoch)) } /// Submit transaction to withdraw an unbond -pub async fn build_withdraw( +pub async fn build_withdraw< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, +>( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::Withdraw { tx: tx_args, validator, source, tx_code_path, }: args::Withdraw, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: common::PublicKey, +) -> Result<(Tx, Option), Error> { let epoch = rpc::query_epoch(client).await; let validator = @@ -867,25 +906,31 @@ pub async fn build_withdraw( let mut tx = Tx::new(chain_id, tx_args.expiration); tx.add_code_from_hash(tx_code_hash).add_data(data); - prepare_tx::( + let epoch = prepare_tx::( client, + wallet, + shielded, &tx_args, &mut tx, - gas_payer.clone(), + fee_payer, + None, #[cfg(not(feature = "mainnet"))] false, ) - .await; - Ok(tx) + .await?; + + Ok((tx, epoch)) } /// Submit a transaction to unbond pub async fn build_unbond< C: crate::ledger::queries::Client + Sync, U: WalletUtils, + V: ShieldedUtils, >( client: &C, - _wallet: &mut Wallet, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::Unbond { tx: tx_args, validator, @@ -893,8 +938,8 @@ pub async fn build_unbond< source, tx_code_path, }: args::Unbond, - gas_payer: &common::PublicKey, -) -> Result<(Tx, Option<(Epoch, token::Amount)>), Error> { + fee_payer: common::PublicKey, +) -> Result<(Tx, Option, Option<(Epoch, token::Amount)>), Error> { let source = source.clone(); // Check the source's current bond amount let bond_source = source.clone().unwrap_or_else(|| validator.clone()); @@ -954,17 +999,20 @@ pub async fn build_unbond< let mut tx = Tx::new(chain_id, tx_args.expiration); tx.add_code_from_hash(tx_code_hash).add_data(data); - prepare_tx::( + let fee_unshield_epoch = prepare_tx::( client, + wallet, + shielded, &tx_args, &mut tx, - gas_payer.clone(), + fee_payer, + None, #[cfg(not(feature = "mainnet"))] false, ) - .await; + .await?; - Ok((tx, latest_withdrawal_pre)) + Ok((tx, fee_unshield_epoch, latest_withdrawal_pre)) } /// Query the unbonds post-tx @@ -1030,8 +1078,14 @@ pub async fn query_unbonds( } /// Submit a transaction to bond -pub async fn build_bond( +pub async fn build_bond< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, +>( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::Bond { tx: tx_args, validator, @@ -1040,19 +1094,18 @@ pub async fn build_bond( native_token, tx_code_path, }: args::Bond, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: common::PublicKey, +) -> Result<(Tx, Option), Error> { let validator = known_validator_or_err(validator.clone(), tx_args.force, client) .await?; // Check that the source address exists on chain - let source = source.clone(); let source = match source.clone() { Some(source) => source_exists_or_err(source, tx_args.force, client) .await .map(Some), - None => Ok(source), + None => Ok(source.clone()), }?; // Check bond's source (source for delegation or validator for self-bonds) // balance @@ -1060,7 +1113,7 @@ pub async fn build_bond( let balance_key = token::balance_key(&native_token, bond_source); // TODO Should we state the same error message for the native token? - check_balance_too_low_err( + let post_balance = check_balance_too_low_err( &native_token, bond_source, amount, @@ -1069,6 +1122,11 @@ pub async fn build_bond( client, ) .await?; + let tx_source_balance = Some(TxSourcePostBalance { + post_balance, + source: bond_source.clone(), + token: native_token, + }); let tx_code_hash = query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) @@ -1085,23 +1143,31 @@ pub async fn build_bond( let mut tx = Tx::new(chain_id, tx_args.expiration); tx.add_code_from_hash(tx_code_hash).add_data(data); - prepare_tx::( + let epoch = prepare_tx( client, + wallet, + shielded, &tx_args, &mut tx, - gas_payer.clone(), + fee_payer, + tx_source_balance, #[cfg(not(feature = "mainnet"))] false, ) - .await; - Ok(tx) + .await?; + + Ok((tx, epoch)) } /// Build a default proposal governance pub async fn build_default_proposal< C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, >( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::InitProposal { tx, proposal_data: _, @@ -1112,8 +1178,8 @@ pub async fn build_default_proposal< tx_code_path, }: args::InitProposal, proposal: DefaultProposal, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: common::PublicKey, +) -> Result<(Tx, Option), Error> { let mut init_proposal_data = InitProposalData::try_from(proposal.clone()) .map_err(|e| Error::InvalidProposal(e.to_string()))?; @@ -1142,22 +1208,31 @@ pub async fn build_default_proposal< .add_code_from_hash(tx_code_hash) .add_data(init_proposal_data); - prepare_tx::( + let epoch = prepare_tx::( client, + wallet, + shielded, &tx, &mut tx_builder, - gas_payer.clone(), + fee_payer, + None, // TODO: need to pay the fee to submit a proposal #[cfg(not(feature = "mainnet"))] false, ) - .await; + .await?; - Ok(tx_builder) + Ok((tx_builder, epoch)) } /// Build a proposal vote -pub async fn build_vote_proposal( +pub async fn build_vote_proposal< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, +>( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::VoteProposal { tx, proposal_id, @@ -1168,8 +1243,8 @@ pub async fn build_vote_proposal( tx_code_path, }: args::VoteProposal, epoch: Epoch, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: common::PublicKey, +) -> Result<(Tx, Option), Error> { let proposal_vote = ProposalVote::try_from(vote).map_err(|_| Error::InvalidProposalVote)?; @@ -1219,24 +1294,31 @@ pub async fn build_vote_proposal( let mut tx_builder = Tx::new(chain_id, tx.expiration); tx_builder.add_code_from_hash(tx_code_hash).add_data(data); - prepare_tx::( + let epoch = prepare_tx::( client, + wallet, + shielded, &tx, &mut tx_builder, - gas_payer.clone(), + fee_payer, + None, #[cfg(not(feature = "mainnet"))] false, ) - .await; + .await?; - Ok(tx_builder) + Ok((tx_builder, epoch)) } /// Build a pgf funding proposal governance pub async fn build_pgf_funding_proposal< C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, >( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::InitProposal { tx, proposal_data: _, @@ -1247,8 +1329,8 @@ pub async fn build_pgf_funding_proposal< tx_code_path, }: args::InitProposal, proposal: PgfFundingProposal, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: common::PublicKey, +) -> Result<(Tx, Option), Error> { let mut init_proposal_data = InitProposalData::try_from(proposal.clone()) .map_err(|e| Error::InvalidProposal(e.to_string()))?; @@ -1270,24 +1352,31 @@ pub async fn build_pgf_funding_proposal< .add_code_from_hash(tx_code_hash) .add_data(init_proposal_data); - prepare_tx::( + let epoch = prepare_tx::( client, + wallet, + shielded, &tx, &mut tx_builder, - gas_payer.clone(), + fee_payer, + None, // TODO: need to pay the fee to submit a proposal #[cfg(not(feature = "mainnet"))] false, ) - .await; + .await?; - Ok(tx_builder) + Ok((tx_builder, epoch)) } /// Build a pgf funding proposal governance pub async fn build_pgf_stewards_proposal< C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, >( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::InitProposal { tx, proposal_data: _, @@ -1298,8 +1387,8 @@ pub async fn build_pgf_stewards_proposal< tx_code_path, }: args::InitProposal, proposal: PgfStewardProposal, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: common::PublicKey, +) -> Result<(Tx, Option), Error> { let mut init_proposal_data = InitProposalData::try_from(proposal.clone()) .map_err(|e| Error::InvalidProposal(e.to_string()))?; @@ -1321,22 +1410,31 @@ pub async fn build_pgf_stewards_proposal< .add_code_from_hash(tx_code_hash) .add_data(init_proposal_data); - prepare_tx::( + let epoch = prepare_tx::( client, + wallet, + shielded, &tx, &mut tx_builder, - gas_payer.clone(), + fee_payer, + None, // TODO: need to pay the fee to submit a proposal #[cfg(not(feature = "mainnet"))] false, ) - .await; + .await?; - Ok(tx_builder) + Ok((tx_builder, epoch)) } /// Submit an IBC transfer -pub async fn build_ibc_transfer( +pub async fn build_ibc_transfer< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, +>( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::TxIbcTransfer { tx: tx_args, source, @@ -1350,8 +1448,8 @@ pub async fn build_ibc_transfer( memo, tx_code_path, }: args::TxIbcTransfer, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: common::PublicKey, +) -> Result<(Tx, Option), Error> { // Check that the source address exists on chain let source = source_exists_or_err(source.clone(), tx_args.force, client).await?; @@ -1360,7 +1458,7 @@ pub async fn build_ibc_transfer( // Check source balance let balance_key = token::balance_key(&token, &source); - check_balance_too_low_err( + let post_balance = check_balance_too_low_err( &token, &source, amount, @@ -1369,6 +1467,11 @@ pub async fn build_ibc_transfer( client, ) .await?; + let tx_source_balance = Some(TxSourcePostBalance { + post_balance, + source: source.clone(), + token: token.clone(), + }); let tx_code_hash = query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) @@ -1438,16 +1541,20 @@ pub async fn build_ibc_transfer( tx.add_code_from_hash(tx_code_hash) .add_serialized_data(data); - prepare_tx::( + let epoch = prepare_tx::( client, + wallet, + shielded, &tx_args, &mut tx, - gas_payer.clone(), + fee_payer, + tx_source_balance, #[cfg(not(feature = "mainnet"))] false, ) - .await; - Ok(tx) + .await?; + + Ok((tx, epoch)) } /// Try to decode the given asset type and add its decoding to the supplied set. @@ -1526,12 +1633,14 @@ async fn used_asset_types< /// Submit an ordinary transfer pub async fn build_transfer< C: crate::ledger::queries::Client + Sync, - U: ShieldedUtils, + U: WalletUtils, + V: ShieldedUtils, >( client: &C, - shielded: &mut ShieldedContext, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, mut args: args::TxTransfer, - gas_payer: &common::PublicKey, + fee_payer: common::PublicKey, ) -> Result<(Tx, Option), Error> { let source = args.source.effective_address(); let target = args.target.effective_address(); @@ -1549,18 +1658,9 @@ pub async fn build_transfer< validate_amount(client, args.amount, &token, args.tx.force) .await .expect("expected to validate amount"); - let validate_fee = validate_amount( - client, - args.tx.gas_amount, - &args.tx.gas_token, - args.tx.force, - ) - .await - .expect("expected to be able to validate fee"); args.amount = InputAmount::Validated(validated_amount); - args.tx.gas_amount = InputAmount::Validated(validate_fee); - check_balance_too_low_err::( + let post_balance = check_balance_too_low_err::( &token, &source, validated_amount.amount, @@ -1569,20 +1669,24 @@ pub async fn build_transfer< client, ) .await?; + let tx_source_balance = Some(TxSourcePostBalance { + post_balance, + source: source.clone(), + token: token.clone(), + }); 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 // 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 (_amount, token, shielded_gas) = - if source == masp_addr && target == masp_addr { - // TODO Refactor me, we shouldn't rely on any specific token here. - (token::Amount::default(), args.native_token.clone(), true) - } else { - (validated_amount.amount, token, false) - }; - + let (_amount, token) = if source == masp_addr && target == masp_addr { + // TODO Refactor me, we shouldn't rely on any specific token here. + (token::Amount::default(), args.native_token.clone()) + } else { + (validated_amount.amount, token) + }; // Determine whether to pin this transaction to a storage key let key = match &args.target { TransferTarget::PaymentAddress(pa) if pa.is_pinned() => Some(pa.hash()), @@ -1600,9 +1704,7 @@ pub async fn build_transfer< .unwrap(); // Construct the shielded part of the transaction, if any - let stx_result = shielded - .gen_shielded_transfer(client, &args, shielded_gas) - .await; + let stx_result = shielded.gen_shielded_transfer(client, args.clone()).await; let shielded_parts = match stx_result { Ok(stx) => Ok(stx), @@ -1611,8 +1713,6 @@ pub async fn build_transfer< Box::new(source.clone()), validated_amount.amount.to_string_native(), Box::new(token.clone()), - validate_fee.amount.to_string_native(), - Box::new(args.tx.gas_token.clone()), )) } Err(err) => Err(Error::MaspError(err)), @@ -1669,22 +1769,49 @@ pub async fn build_transfer< tx.add_code_from_hash(tx_code_hash).add_data(transfer); // Dry-run/broadcast/submit the transaction - prepare_tx::( + let unshielding_epoch = prepare_tx::( client, + wallet, + shielded, &args.tx, &mut tx, - gas_payer.clone(), + fee_payer, + tx_source_balance, #[cfg(not(feature = "mainnet"))] is_source_faucet, ) - .await; - - Ok((tx, shielded_tx_epoch)) + .await?; + // Manage the two masp epochs + let masp_epoch = match (unshielding_epoch, shielded_tx_epoch) { + (Some(fee_unshield_epoch), Some(transfer_unshield_epoch)) => { + // If the two masp epochs are different, either the wrapper or the + // inner tx will fail, so abort tx creation + if fee_unshield_epoch != transfer_unshield_epoch && !args.tx.force { + return Err(Error::Other( + "Fee unshilding masp tx and inner tx masp transaction \ + were crafted on an epoch boundary" + .to_string(), + )); + } + // Take the smaller of the two epochs + Some(fee_unshield_epoch.min(transfer_unshield_epoch)) + } + (Some(_fee_unshielding_epoch), None) => unshielding_epoch, + (None, Some(_transfer_unshield_epoch)) => shielded_tx_epoch, + (None, None) => None, + }; + Ok((tx, masp_epoch)) } /// Submit a transaction to initialize an account -pub async fn build_init_account( +pub async fn build_init_account< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, +>( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::TxInitAccount { tx: tx_args, vp_code_path, @@ -1692,8 +1819,8 @@ pub async fn build_init_account( public_keys, threshold, }: args::TxInitAccount, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: &common::PublicKey, +) -> Result<(Tx, Option), Error> { let vp_code_hash = query_wasm_code_hash(client, vp_code_path.to_str().unwrap()) .await @@ -1725,21 +1852,31 @@ pub async fn build_init_account( }; tx.add_code_from_hash(tx_code_hash).add_data(data); - prepare_tx::( + let epoch = prepare_tx::( client, + wallet, + shielded, &tx_args, &mut tx, - gas_payer.clone(), + fee_payer.clone(), + None, #[cfg(not(feature = "mainnet"))] false, ) - .await; - Ok(tx) + .await?; + + Ok((tx, epoch)) } /// Submit a transaction to update a VP -pub async fn build_update_account( +pub async fn build_update_account< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, +>( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::TxUpdateAccount { tx: tx_args, vp_code_path, @@ -1748,8 +1885,8 @@ pub async fn build_update_account( public_keys, threshold, }: args::TxUpdateAccount, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: common::PublicKey, +) -> Result<(Tx, Option), Error> { let addr = if let Some(account) = rpc::get_account_info(client, &addr).await { account.address @@ -1789,21 +1926,31 @@ pub async fn build_update_account( tx.add_code_from_hash(tx_code_hash).add_data(data); - prepare_tx::( + let epoch = prepare_tx::( client, + wallet, + shielded, &tx_args, &mut tx, - gas_payer.clone(), + fee_payer, + None, #[cfg(not(feature = "mainnet"))] false, ) - .await; - Ok(tx) + .await?; + + Ok((tx, epoch)) } /// Submit a custom transaction -pub async fn build_custom( +pub async fn build_custom< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, + V: ShieldedUtils, +>( client: &C, + wallet: &mut Wallet, + shielded: &mut ShieldedContext, args::TxCustom { tx: tx_args, code_path, @@ -1811,8 +1958,8 @@ pub async fn build_custom( serialized_tx, owner: _, }: args::TxCustom, - gas_payer: &common::PublicKey, -) -> Result { + fee_payer: &common::PublicKey, +) -> Result<(Tx, Option), Error> { let mut tx = if let Some(serialized_tx) = serialized_tx { Tx::deserialize(serialized_tx.as_ref()).map_err(|_| { Error::Other("Invalid tx deserialization.".to_string()) @@ -1829,16 +1976,20 @@ pub async fn build_custom( tx }; - prepare_tx::( + let epoch = prepare_tx::( client, + wallet, + shielded, &tx_args, &mut tx, - gas_payer.clone(), + fee_payer.clone(), + None, #[cfg(not(feature = "mainnet"))] false, ) - .await; - Ok(tx) + .await?; + + Ok((tx, epoch)) } async fn expect_dry_broadcast( @@ -1850,7 +2001,7 @@ async fn expect_dry_broadcast( rpc::dry_run_tx(client, tx.to_bytes()).await; Ok(ProcessTxResponse::DryRun) } - TxBroadcastData::Wrapper { + TxBroadcastData::Live { tx, wrapper_hash: _, decrypted_hash: _, @@ -1954,9 +2105,9 @@ async fn target_exists_or_err( .await } -/// checks the balance at the given address is enough to transfer the -/// given amount, along with the balance even existing. force -/// overrides this +/// Checks the balance at the given address is enough to transfer the +/// given amount, along with the balance even existing. Force +/// overrides this. Returns the updated balance for fee check if necessary async fn check_balance_too_low_err( token: &Address, source: &Address, @@ -1964,12 +2115,13 @@ async fn check_balance_too_low_err( balance_key: storage::Key, force: bool, client: &C, -) -> Result<(), Error> { +) -> Result { match rpc::query_storage_value::(client, &balance_key) .await { - Some(balance) => { - if balance < amount { + Some(balance) => match balance.checked_sub(amount) { + Some(diff) => Ok(diff), + None => { if force { eprintln!( "The balance of the source {} of token {} is lower \ @@ -1980,7 +2132,7 @@ async fn check_balance_too_low_err( format_denominated_amount(client, token, amount).await, format_denominated_amount(client, token, balance).await, ); - Ok(()) + Ok(token::Amount::default()) } else { Err(Error::BalanceTooLow( source.clone(), @@ -1989,17 +2141,15 @@ async fn check_balance_too_low_err( balance.to_string_native(), )) } - } else { - Ok(()) } - } + }, None => { if force { eprintln!( "No balance found for the source {} of token {}", source, token ); - Ok(()) + Ok(token::Amount::default()) } else { Err(Error::NoBalanceForToken(source.clone(), token.clone())) } diff --git a/shared/src/ledger/vp_host_fns.rs b/shared/src/ledger/vp_host_fns.rs index 92c8e19e1c..a9aaa7eb16 100644 --- a/shared/src/ledger/vp_host_fns.rs +++ b/shared/src/ledger/vp_host_fns.rs @@ -9,9 +9,9 @@ use namada_core::types::storage::{ }; use thiserror::Error; -use super::gas::MIN_STORAGE_GAS; +use super::gas::STORAGE_ACCESS_GAS_PER_BYTE; use crate::ledger::gas; -use crate::ledger::gas::VpGasMeter; +use crate::ledger::gas::{GasMetering, VpGasMeter}; use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{self, write_log, Storage, StorageHasher}; use crate::proto::{Section, Tx}; @@ -45,7 +45,7 @@ pub type EnvResult = std::result::Result; /// Add a gas cost incured in a validity predicate pub fn add_gas(gas_meter: &mut VpGasMeter, used_gas: u64) -> EnvResult<()> { - let result = gas_meter.add(used_gas).map_err(RuntimeError::OutOfGas); + let result = gas_meter.consume(used_gas).map_err(RuntimeError::OutOfGas); if let Err(err) = &result { tracing::info!("Stopping VP execution because of gas error: {}", err); } @@ -291,7 +291,7 @@ pub fn get_tx_code_hash( .get_section(tx.code_sechash()) .and_then(|x| Section::code_sec(x.as_ref())) .map(|x| x.code.hash()); - add_gas(gas_meter, MIN_STORAGE_GAS)?; + add_gas(gas_meter, STORAGE_ACCESS_GAS_PER_BYTE)?; Ok(hash) } @@ -316,7 +316,7 @@ pub fn get_tx_index( gas_meter: &mut VpGasMeter, tx_index: &TxIndex, ) -> EnvResult { - add_gas(gas_meter, MIN_STORAGE_GAS)?; + add_gas(gas_meter, STORAGE_ACCESS_GAS_PER_BYTE)?; Ok(*tx_index) } @@ -329,7 +329,7 @@ where DB: storage::DB + for<'iter> storage::DBIter<'iter>, H: StorageHasher, { - add_gas(gas_meter, MIN_STORAGE_GAS)?; + add_gas(gas_meter, STORAGE_ACCESS_GAS_PER_BYTE)?; Ok(storage.native_token.clone()) } diff --git a/shared/src/ledger/wallet/mod.rs b/shared/src/ledger/wallet/mod.rs index 4b554aac28..371c97806b 100644 --- a/shared/src/ledger/wallet/mod.rs +++ b/shared/src/ledger/wallet/mod.rs @@ -265,6 +265,30 @@ impl Wallet { )) } + /// Generate a disposable signing key for fee payment and store it under the + /// precomputed alias in the wallet. This is simply a wrapper around + /// `gen_key` to manage the alias + pub fn generate_disposable_signing_key(&mut self) -> common::SecretKey { + // Create the alias + let mut ctr = 1; + let mut alias = format!("disposable_{ctr}"); + + while self.store().contains_alias(&Alias::from(&alias)) { + ctr += 1; + alias = format!("disposable_{ctr}"); + } + // Generate a disposable keypair to sign the wrapper if requested + // TODO: once the wrapper transaction has been accepted, this key can be + // deleted from wallet + let (alias, disposable_keypair) = self + .gen_key(SchemeType::Ed25519, Some(alias), false, None, None) + .expect("Failed to initialize disposable keypair") + .expect("Missing alias and secret key"); + + println!("Created disposable keypair with alias {alias}"); + disposable_keypair + } + /// Generate a spending key and store it under the given alias in the wallet pub fn gen_spending_key( &mut self, diff --git a/shared/src/ledger/wallet/store.rs b/shared/src/ledger/wallet/store.rs index dc93dc515a..09e04ed836 100644 --- a/shared/src/ledger/wallet/store.rs +++ b/shared/src/ledger/wallet/store.rs @@ -444,7 +444,7 @@ impl Store { } /// Check if any map of the wallet contains the given alias - fn contains_alias(&self, alias: &Alias) -> bool { + pub fn contains_alias(&self, alias: &Alias) -> bool { self.payment_addrs.contains_key(alias) || self.view_keys.contains_key(alias) || self.spend_keys.contains_key(alias) diff --git a/shared/src/vm/host_env.rs b/shared/src/vm/host_env.rs index f290e484ff..abc8095fea 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::{GasMetering, TxGasMeter}; use namada_core::types::internal::KeyVal; use thiserror::Error; @@ -13,7 +14,7 @@ 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, STORAGE_ACCESS_GAS_PER_BYTE}; use crate::ledger::storage::write_log::{self, WriteLog}; use crate::ledger::storage::{self, Storage, StorageHasher}; use crate::ledger::vp_host_fns; @@ -30,8 +31,6 @@ use crate::vm::memory::VmMemory; use crate::vm::prefix_iter::{PrefixIteratorId, PrefixIterators}; use crate::vm::{HostRef, MutHostRef}; -const WASM_VALIDATION_GAS_PER_BYTE: u64 = 1; - /// These runtime errors will abort tx WASM execution immediately #[allow(missing_docs)] #[derive(Error, Debug)] @@ -92,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 code is used for signature verification pub tx: HostRef<'a, &'a Tx>, /// The transaction index is used to identify a shielded transaction's @@ -134,7 +133,7 @@ where storage: &Storage, write_log: &mut WriteLog, iterators: &mut PrefixIterators<'a, DB>, - gas_meter: &mut BlockGasMeter, + gas_meter: &mut TxGasMeter, tx: &Tx, tx_index: &TxIndex, verifiers: &mut BTreeSet
, @@ -464,27 +463,8 @@ where } } -/// Called from tx wasm to request to use the given gas amount -pub fn tx_charge_gas( - env: &TxVmEnv, - used_gas: i64, -) -> TxResult<()> -where - MEM: VmMemory, - DB: storage::DB + for<'iter> storage::DBIter<'iter>, - H: StorageHasher, - CA: WasmCacheAccess, -{ - tx_add_gas( - env, - used_gas - .try_into() - .map_err(TxRuntimeError::NumConversionError)?, - ) -} - /// Add a gas cost incured in a transaction -pub fn tx_add_gas( +pub fn tx_charge_gas( env: &TxVmEnv, used_gas: u64, ) -> TxResult<()> @@ -496,7 +476,9 @@ where { let gas_meter = unsafe { env.ctx.gas_meter.get() }; // if we run out of gas, we need to stop the execution - let result = gas_meter.add(used_gas).map_err(TxRuntimeError::OutOfGas); + let result = gas_meter + .consume(used_gas) + .map_err(TxRuntimeError::OutOfGas); if let Err(err) = &result { tracing::info!( "Stopping transaction execution because of gas error: {}", @@ -509,7 +491,7 @@ where /// Called from VP wasm to request to use the given gas amount pub fn vp_charge_gas( env: &VpVmEnv, - used_gas: i64, + used_gas: u64, ) -> vp_host_fns::EnvResult<()> where MEM: VmMemory, @@ -519,12 +501,7 @@ where CA: WasmCacheAccess, { let gas_meter = unsafe { env.ctx.gas_meter.get() }; - vp_host_fns::add_gas( - gas_meter, - used_gas - .try_into() - .map_err(vp_host_fns::RuntimeError::NumConversionError)?, - ) + vp_host_fns::add_gas(gas_meter, used_gas) } /// Storage `has_key` function exposed to the wasm VM Tx environment. It will @@ -544,7 +521,7 @@ where .memory .read_string(key_ptr, key_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; tracing::debug!("tx_has_key {}, key {}", key, key_ptr,); @@ -553,7 +530,7 @@ where // try to read from the write log first let write_log = unsafe { env.ctx.write_log.get() }; let (log_val, gas) = write_log.read(&key); - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; Ok(match log_val { Some(&write_log::StorageModification::Write { .. }) => { HostEnvResult::Success.to_i64() @@ -574,7 +551,7 @@ where let (present, gas) = storage .has_key(&key) .map_err(TxRuntimeError::StorageError)?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; HostEnvResult::from(present).to_i64() } }) @@ -600,7 +577,7 @@ where .memory .read_string(key_ptr, key_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; tracing::debug!("tx_read {}, key {}", key, key_ptr,); @@ -609,7 +586,7 @@ where // try to read from the write log first let write_log = unsafe { env.ctx.write_log.get() }; let (log_val, gas) = write_log.read(&key); - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; Ok(match log_val { Some(write_log::StorageModification::Write { ref value }) => { let len: i64 = value @@ -650,7 +627,7 @@ where let storage = unsafe { env.ctx.storage.get() }; let (value, gas) = storage.read(&key).map_err(TxRuntimeError::StorageError)?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; match value { Some(value) => { let len: i64 = value @@ -691,7 +668,7 @@ where .memory .write_bytes(result_ptr, value) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas) + tx_charge_gas(env, gas) } /// Storage prefix iterator function exposed to the wasm VM Tx environment. @@ -712,7 +689,7 @@ where .memory .read_string(prefix_ptr, prefix_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; tracing::debug!("tx_iter_prefix {}", prefix); @@ -722,7 +699,7 @@ where let write_log = unsafe { env.ctx.write_log.get() }; let storage = unsafe { env.ctx.storage.get() }; let (iter, gas) = storage::iter_prefix_post(write_log, storage, &prefix); - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; let iterators = unsafe { env.ctx.iterators.get() }; Ok(iterators.insert(iter).id()) @@ -754,7 +731,7 @@ where &Key::parse(key.clone()) .map_err(TxRuntimeError::StorageDataError)?, ); - tx_add_gas(env, iter_gas + log_gas)?; + tx_charge_gas(env, iter_gas + log_gas)?; match log_val { Some(write_log::StorageModification::Write { ref value }) => { let key_val = KeyVal { @@ -830,12 +807,12 @@ where .memory .read_string(key_ptr, key_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; let (value, gas) = env .memory .read_bytes(val_ptr, val_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; tracing::debug!("tx_update {}, {:?}", key, value); @@ -850,8 +827,7 @@ where let (gas, _size_diff) = write_log .write(&key, value) .map_err(TxRuntimeError::StorageModificationError)?; - tx_add_gas(env, gas) - // TODO: charge the size diff + tx_charge_gas(env, gas) } /// Temporary storage write function exposed to the wasm VM Tx environment. The @@ -874,12 +850,12 @@ where .memory .read_string(key_ptr, key_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; let (value, gas) = env .memory .read_bytes(val_ptr, val_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; tracing::debug!("tx_write_temp {}, {:?}", key, value); @@ -891,8 +867,7 @@ where let (gas, _size_diff) = write_log .write_temp(&key, value) .map_err(TxRuntimeError::StorageModificationError)?; - tx_add_gas(env, gas) - // TODO: charge the size diff + tx_charge_gas(env, gas) } fn check_address_existence( @@ -925,14 +900,14 @@ where } let vp_key = Key::validity_predicate(&addr); let (vp, gas) = write_log.read(&vp_key); - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; // just check the existence because the write log should not have the // delete log of the VP if vp.is_none() { let (is_present, gas) = storage .has_key(&vp_key) .map_err(TxRuntimeError::StorageError)?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; if !is_present { tracing::info!( "Trying to write into storage with a key containing an \ @@ -965,7 +940,7 @@ where .memory .read_string(key_ptr, key_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; tracing::debug!("tx_delete {}", key); @@ -978,8 +953,7 @@ where let (gas, _size_diff) = write_log .delete(&key) .map_err(TxRuntimeError::StorageModificationError)?; - tx_add_gas(env, gas) - // TODO: charge the size diff + tx_charge_gas(env, gas) } /// Emitting an IBC event function exposed to the wasm VM Tx environment. @@ -999,12 +973,12 @@ where .memory .read_bytes(event_ptr, event_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; let event: IbcEvent = BorshDeserialize::try_from_slice(&event) .map_err(TxRuntimeError::EncodingError)?; let write_log = unsafe { env.ctx.write_log.get() }; let gas = write_log.emit_ibc_event(event); - tx_add_gas(env, gas) + tx_charge_gas(env, gas) } /// Storage read prior state (before tx execution) function exposed to the wasm @@ -1371,7 +1345,7 @@ where .memory .read_string(addr_ptr, addr_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; tracing::debug!("tx_insert_verifier {}, addr_ptr {}", addr, addr_ptr,); @@ -1379,7 +1353,9 @@ where let verifiers = unsafe { env.ctx.verifiers.get() }; verifiers.insert(addr); - tx_add_gas(env, addr_len) + // This is not a storage write, use the same multiplier used for a storage + // read + tx_charge_gas(env, addr_len * STORAGE_ACCESS_GAS_PER_BYTE) } /// Update a validity predicate function exposed to the wasm VM Tx environment @@ -1400,7 +1376,7 @@ where .memory .read_string(addr_ptr, addr_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; let addr = Address::decode(addr).map_err(TxRuntimeError::AddressError)?; tracing::debug!("tx_update_validity_predicate for addr {}", addr); @@ -1410,7 +1386,7 @@ where .memory .read_bytes(code_hash_ptr, code_hash_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; tx_validate_vp_code_hash(env, &code_hash)?; @@ -1418,8 +1394,7 @@ where let (gas, _size_diff) = write_log .write(&key, code_hash) .map_err(TxRuntimeError::StorageModificationError)?; - tx_add_gas(env, gas) - // TODO: charge the size diff + tx_charge_gas(env, gas) } /// Initialize a new account established address. @@ -1439,7 +1414,7 @@ where .memory .read_bytes(code_hash_ptr, code_hash_len as _) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; tx_validate_vp_code_hash(env, &code_hash)?; @@ -1452,12 +1427,12 @@ where let (addr, gas) = write_log.init_account(&storage.address_gen, code_hash); let addr_bytes = addr.try_to_vec().map_err(TxRuntimeError::EncodingError)?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; let gas = env .memory .write_bytes(result_ptr, addr_bytes) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas) + tx_charge_gas(env, gas) } /// Getting the chain ID function exposed to the wasm VM Tx environment. @@ -1473,12 +1448,12 @@ where { let storage = unsafe { env.ctx.storage.get() }; let (chain_id, gas) = storage.get_chain_id(); - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; let gas = env .memory .write_string(result_ptr, chain_id) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas) + tx_charge_gas(env, gas) } /// Getting the block height function exposed to the wasm VM Tx @@ -1495,7 +1470,7 @@ where { let storage = unsafe { env.ctx.storage.get() }; let (height, gas) = storage.get_block_height(); - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; Ok(height.0) } @@ -1512,7 +1487,7 @@ where CA: WasmCacheAccess, { let tx_index = unsafe { env.ctx.tx_index.get() }; - tx_add_gas(env, crate::vm::host_env::gas::MIN_STORAGE_GAS)?; + tx_charge_gas(env, crate::vm::host_env::gas::STORAGE_ACCESS_GAS_PER_BYTE)?; Ok(tx_index.0) } @@ -1549,12 +1524,12 @@ where { let storage = unsafe { env.ctx.storage.get() }; let (hash, gas) = storage.get_block_hash(); - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; let gas = env .memory .write_bytes(result_ptr, hash.0) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas) + tx_charge_gas(env, gas) } /// Getting the block epoch function exposed to the wasm VM Tx @@ -1571,7 +1546,7 @@ where { let storage = unsafe { env.ctx.storage.get() }; let (epoch, gas) = storage.get_current_epoch(); - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; Ok(epoch.0) } @@ -1587,14 +1562,14 @@ where CA: WasmCacheAccess, { let storage = unsafe { env.ctx.storage.get() }; - tx_add_gas(env, MIN_STORAGE_GAS)?; + tx_charge_gas(env, STORAGE_ACCESS_GAS_PER_BYTE)?; let native_token = storage.native_token.clone(); let native_token_string = native_token.encode(); let gas = env .memory .write_string(result_ptr, native_token_string) .map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?; - tx_add_gas(env, gas) + tx_charge_gas(env, gas) } /// Getting the block header function exposed to the wasm VM Tx environment. @@ -1622,7 +1597,7 @@ where .map_err(TxRuntimeError::NumConversionError)?; let result_buffer = unsafe { env.ctx.result_buffer.get() }; result_buffer.replace(value); - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; len } None => HostEnvResult::Fail.to_i64(), @@ -1776,6 +1751,72 @@ where Ok(epoch.0) } +/// Verify a transaction signature +/// TODO: this is just a warkaround to track gas for multiple singature +/// verifications. When the runtime gas meter is implemented, this funcion can +/// be removed +#[allow(clippy::too_many_arguments)] +pub fn vp_verify_tx_section_signature( + env: &VpVmEnv, + hash_list_ptr: u64, + hash_list_len: u64, + public_keys_map_ptr: u64, + public_keys_map_len: u64, + threshold: u8, + max_signatures_ptr: u64, + max_signatures_len: u64, +) -> vp_host_fns::EnvResult +where + MEM: VmMemory, + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: StorageHasher, + EVAL: VpEvaluator, + CA: WasmCacheAccess, +{ + let (hash_list, gas) = env + .memory + .read_bytes(hash_list_ptr, hash_list_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)?; + let hashes = <[Hash; 2]>::try_from_slice(&hash_list) + .map_err(vp_host_fns::RuntimeError::EncodingError)?; + + let (public_keys_map, gas) = env + .memory + .read_bytes(public_keys_map_ptr, public_keys_map_len as _) + .map_err(|e| vp_host_fns::RuntimeError::MemoryError(Box::new(e)))?; + vp_host_fns::add_gas(gas_meter, gas)?; + let public_keys_map = + namada_core::types::account::AccountPublicKeysMap::try_from_slice( + &public_keys_map, + ) + .map_err(vp_host_fns::RuntimeError::EncodingError)?; + + let (max_signatures, gas) = env + .memory + .read_bytes(max_signatures_ptr, max_signatures_len as _) + .map_err(|e| vp_host_fns::RuntimeError::MemoryError(Box::new(e)))?; + vp_host_fns::add_gas(gas_meter, gas)?; + let max_signatures = Option::::try_from_slice(&max_signatures) + .map_err(vp_host_fns::RuntimeError::EncodingError)?; + + let tx = unsafe { env.ctx.tx.get() }; + + Ok(HostEnvResult::from( + tx.verify_section_signatures( + &hashes, + public_keys_map, + threshold, + max_signatures, + gas_meter, + ) + .is_ok(), + ) + .to_i64()) +} + /// Verify a ShieldedTransaction. pub fn vp_verify_masp( env: &VpVmEnv, @@ -1803,6 +1844,9 @@ where .map_err(vp_host_fns::RuntimeError::EncodingError)?; Ok( + // TODO: once the runtime gas meter is implemented we need to benchmark + // this funcion and charge the gas here. For the moment, the cost of + // this is included in the benchmark of the masp vp HostEnvResult::from(crate::ledger::masp::verify_shielded_tx(&shielded)) .to_i64(), ) @@ -1841,19 +1885,18 @@ where H: StorageHasher, CA: WasmCacheAccess, { - tx_add_gas(env, code_hash.len() as u64 * WASM_VALIDATION_GAS_PER_BYTE)?; let hash = Hash::try_from(code_hash) .map_err(|e| TxRuntimeError::InvalidVpCodeHash(e.to_string()))?; let key = Key::wasm_code(&hash); let write_log = unsafe { env.ctx.write_log.get() }; let (result, gas) = write_log.read(&key); - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; if result.is_none() { let storage = unsafe { env.ctx.storage.get() }; let (is_present, gas) = storage .has_key(&key) .map_err(TxRuntimeError::StorageError)?; - tx_add_gas(env, gas)?; + tx_charge_gas(env, gas)?; if !is_present { return Err(TxRuntimeError::InvalidVpCodeHash( "The corresponding VP code doesn't exist".to_string(), @@ -1866,8 +1909,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 @@ -1880,7 +1923,7 @@ where { let (vp_code_hash, gas) = env .memory - .read_bytes(vp_code_ptr, vp_code_len as _) + .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)?; @@ -1995,7 +2038,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: &Tx, tx_index: &TxIndex, result_buffer: &mut Option>, diff --git a/shared/src/vm/mod.rs b/shared/src/vm/mod.rs index 2ca6d81a91..bd83efa86f 100644 --- a/shared/src/vm/mod.rs +++ b/shared/src/vm/mod.rs @@ -25,7 +25,7 @@ const UNTRUSTED_WASM_FEATURES: WasmFeatures = WasmFeatures { relaxed_simd: false, threads: false, tail_call: false, - floats: false, + floats: true, multi_memory: false, exceptions: false, memory64: false, diff --git a/shared/src/vm/wasm/host_env.rs b/shared/src/vm/wasm/host_env.rs index 0899110164..0ac2e081d5 100644 --- a/shared/src/vm/wasm/host_env.rs +++ b/shared/src/vm/wasm/host_env.rs @@ -60,7 +60,10 @@ where // default namespace "env" => { "memory" => initial_memory, + // Wasm middleware gas injectiong hook "gas" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_charge_gas), + // Whitelisted gas exposed function, we need two different functions just because of colliding names in the vm_host_env macro to generate implementations + "namada_tx_charge_gas" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_charge_gas), "namada_tx_read" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_read), "namada_tx_result_buffer" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_result_buffer), "namada_tx_has_key" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_has_key), @@ -102,7 +105,10 @@ where // default namespace "env" => { "memory" => initial_memory, + // Wasm middleware gas injectiong hook "gas" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_charge_gas), + // Whitelisted gas exposed function, we need two different functions just because of colliding names in the vm_host_env macro to generate implementations + "namada_vp_charge_gas" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_charge_gas), "namada_vp_read_pre" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_read_pre), "namada_vp_read_post" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_read_post), "namada_vp_read_temp" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_read_temp), @@ -119,6 +125,7 @@ where "namada_vp_get_block_hash" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_get_block_hash), "namada_vp_get_tx_code_hash" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_get_tx_code_hash), "namada_vp_get_block_epoch" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_get_block_epoch), + "namada_vp_verify_tx_section_signature" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_verify_tx_section_signature), "namada_vp_verify_masp" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_verify_masp), "namada_vp_eval" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_eval), "namada_vp_get_native_token" => Function::new_native_with_env(wasm_store, env.clone(), host_env::vp_get_native_token), diff --git a/shared/src/vm/wasm/memory.rs b/shared/src/vm/wasm/memory.rs index 7e79ab1be0..3e0c7975c9 100644 --- a/shared/src/vm/wasm/memory.rs +++ b/shared/src/vm/wasm/memory.rs @@ -6,6 +6,7 @@ use std::str::Utf8Error; use std::sync::Arc; use borsh::BorshSerialize; +use namada_core::ledger::gas::VM_MEMORY_ACCESS_GAS_PER_BYTE; use thiserror::Error; use wasmer::{ vm, BaseTunables, HostEnvInitError, LazyInit, Memory, MemoryError, @@ -257,16 +258,16 @@ impl VmMemory for WasmMemory { fn read_bytes(&self, offset: u64, len: usize) -> Result<(Vec, u64)> { let memory = self.inner.get_ref().ok_or(Error::UninitializedMemory)?; let bytes = read_memory_bytes(memory, offset, len)?; - let gas = bytes.len(); - Ok((bytes, gas as _)) + let gas = bytes.len() as u64 * VM_MEMORY_ACCESS_GAS_PER_BYTE; + Ok((bytes, gas)) } /// Write bytes into memory at the given offset and return the gas cost fn write_bytes(&self, offset: u64, bytes: impl AsRef<[u8]>) -> Result { - let gas = bytes.as_ref().len(); + let gas = bytes.as_ref().len() as u64 * VM_MEMORY_ACCESS_GAS_PER_BYTE; let memory = self.inner.get_ref().ok_or(Error::UninitializedMemory)?; write_memory_bytes(memory, offset, bytes)?; - Ok(gas as _) + Ok(gas) } /// Read string from memory at the given offset and bytes length, and return @@ -276,7 +277,7 @@ impl VmMemory for WasmMemory { let string = std::str::from_utf8(&bytes) .map_err(Error::InvalidUtf8String)? .to_string(); - Ok((string, gas as _)) + Ok((string, gas)) } /// Write string into memory at the given offset and return the gas cost diff --git a/shared/src/vm/wasm/run.rs b/shared/src/vm/wasm/run.rs index 95cf2afc38..70975af2ed 100644 --- a/shared/src/vm/wasm/run.rs +++ b/shared/src/vm/wasm/run.rs @@ -3,13 +3,18 @@ use std::collections::BTreeSet; use std::marker::PhantomData; +use borsh::BorshDeserialize; +use namada_core::ledger::gas::{ + self, GasMetering, TxGasMeter, WASM_MEMORY_PAGE_GAS_COST, +}; +use namada_core::ledger::storage::write_log::StorageModification; use parity_wasm::elements; use thiserror::Error; 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::{Commitment, Section, Tx}; @@ -37,7 +42,7 @@ pub enum Error { MissingCode, #[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), @@ -72,18 +77,27 @@ 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, tx_index: &TxIndex, tx: &Tx, vp_wasm_cache: &mut VpCache, @@ -98,18 +112,24 @@ where .get_section(tx.code_sechash()) .and_then(|x| Section::code_sec(x.as_ref())) .ok_or(Error::MissingCode)?; - let (module, store) = match tx_code.code { - Commitment::Hash(code_hash) => { - fetch_or_compile(tx_wasm_cache, &code_hash, write_log, storage)? - } - Commitment::Id(tx_code) => { - match tx_wasm_cache.compile_or_fetch(tx_code)? { - Some((module, store)) => (module, store), - None => return Err(Error::NoCompiledWasmCode), - } - } + let (tx_hash, code) = match tx_code.code { + Commitment::Hash(code_hash) => (code_hash, None), + Commitment::Id(tx_code) => (Hash::sha256(&tx_code), Some(tx_code)), + }; + + let code_or_hash = match code { + Some(ref 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 mut iterators: PrefixIterators<'_, DB> = PrefixIterators::default(); let mut verifiers = BTreeSet::new(); let mut result_buffer: Option> = None; @@ -193,8 +213,13 @@ where CA: 'static + WasmCacheAccess, { // 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; @@ -226,16 +251,28 @@ where memory::prepare_vp_memory(&store).map_err(Error::MemoryError)?; let imports = vp_imports(&store, initial_memory, env); - run_vp(module, imports, tx, address, keys_changed, verifiers) + run_vp( + module, + imports, + vp_code_hash, + tx, + address, + keys_changed, + verifiers, + gas_meter, + ) } +#[allow(clippy::too_many_arguments)] fn run_vp( module: wasmer::Module, vp_imports: wasmer::ImportObject, + _vp_code_hash: &Hash, input_data: &Tx, address: &Address, keys_changed: &BTreeSet, verifiers: &BTreeSet
, + _gas_meter: &mut VpGasMeter, ) -> Result { let input: VpInput = VpInput { addr: address, @@ -353,14 +390,20 @@ 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, }; // 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)?; @@ -370,10 +413,12 @@ where run_vp( module, imports, + &vp_code_hash, &input_data, address, keys_changed, verifiers, + gas_meter, ) } } @@ -406,12 +451,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 GasMetering, ) -> Result<(Module, Store)> where DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, @@ -419,36 +466,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.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), @@ -459,9 +557,9 @@ where /// Get the gas rules used to meter wasm operations fn get_gas_rules() -> wasm_instrument::gas_metering::ConstantCostRules { - let instruction_cost = 1; - let memory_grow_cost = 1; - let call_per_local_cost = 1; + let instruction_cost = 0; + let memory_grow_cost = WASM_MEMORY_PAGE_GAS_COST; + let call_per_local_cost = 0; wasm_instrument::gas_metering::ConstantCostRules::new( instruction_cost, memory_grow_cost, @@ -485,6 +583,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 +631,7 @@ 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_from_sub_limit(TX_GAS_LIMIT.into()); let tx_index = TxIndex::default(); // This code will allocate memory of the given size @@ -539,7 +639,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.clone()).unwrap(); + write_log.write(&len_key, code_len).unwrap(); // Assuming 200 pages, 12.8 MiB limit assert_eq!(memory::TX_MEMORY_MAX_PAGES, 200); @@ -593,7 +696,9 @@ 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_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let keys_changed = BTreeSet::new(); let verifiers = BTreeSet::new(); let tx_index = TxIndex::default(); @@ -603,13 +708,19 @@ 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(); // Assuming 200 pages, 12.8 MiB limit assert_eq!(memory::VP_MEMORY_MAX_PAGES, 200); @@ -692,7 +803,9 @@ 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_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let keys_changed = BTreeSet::new(); let verifiers = BTreeSet::new(); let tx_index = TxIndex::default(); @@ -701,8 +814,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); @@ -762,14 +878,17 @@ 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_from_sub_limit(TX_GAS_LIMIT.into()); 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.clone()).unwrap(); + write_log.write(&len_key, code_len).unwrap(); // Assuming 200 pages, 12.8 MiB limit assert_eq!(memory::TX_MEMORY_MAX_PAGES, 200); @@ -820,7 +939,9 @@ 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_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let keys_changed = BTreeSet::new(); let verifiers = BTreeSet::new(); let tx_index = TxIndex::default(); @@ -829,7 +950,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); @@ -885,14 +1009,17 @@ 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_from_sub_limit(TX_GAS_LIMIT.into()); 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.clone()).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 @@ -935,7 +1062,9 @@ 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_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let keys_changed = BTreeSet::new(); let verifiers = BTreeSet::new(); let tx_index = TxIndex::default(); @@ -943,8 +1072,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 @@ -991,7 +1123,9 @@ 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_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); let keys_changed = BTreeSet::new(); let verifiers = BTreeSet::new(); let tx_index = TxIndex::default(); @@ -1000,14 +1134,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 @@ -1091,7 +1231,7 @@ 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_from_sub_limit(TX_GAS_LIMIT.into()); let (mut vp_cache, _) = wasm::compilation_cache::common::testing::cache(); let (mut tx_cache, _) = @@ -1099,8 +1239,11 @@ mod tests { // 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(); let mut outer_tx = Tx::from_type(TxType::Raw); outer_tx.set_code(Code::from_hash(code_hash)); @@ -1151,14 +1294,19 @@ 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_from_tx_meter( + &TxGasMeter::new_from_sub_limit(TX_GAS_LIMIT.into()), + ); 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, diff --git a/test_fixtures/masp_proofs/37332141CB34FC30FF51F4BEE8D76149D3088F539CF8372D404609B89B095EF7.bin b/test_fixtures/masp_proofs/37332141CB34FC30FF51F4BEE8D76149D3088F539CF8372D404609B89B095EF7.bin new file mode 100644 index 0000000000..cc9e1a80d5 Binary files /dev/null and b/test_fixtures/masp_proofs/37332141CB34FC30FF51F4BEE8D76149D3088F539CF8372D404609B89B095EF7.bin differ diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index bbc959908f..ecaddec871 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -710,10 +710,6 @@ fn transfer_received_token( &ibc_token, "--amount", &amount, - "--gas-amount", - "0", - "--gas-limit", - "0", "--gas-token", NAM, "--node", @@ -917,10 +913,6 @@ fn submit_ibc_tx( owner, "--signing-keys", signer, - "--gas-amount", - "0", - "--gas-limit", - "0", "--gas-token", NAM, "--node", diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 2ea7d901f0..88228d3268 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -167,12 +167,8 @@ fn test_node_connectivity_and_consensus() -> Result<()> { NAM, "--amount", "10.1", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, + "--gas-price", + "0.00090", "--signing-keys", BERTHA_KEY, "--node", @@ -476,12 +472,6 @@ fn ledger_txs_and_queries() -> Result<()> { NAM, "--amount", "10.1", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--node", @@ -498,12 +488,6 @@ fn ledger_txs_and_queries() -> Result<()> { NAM, "--amount", "10.1", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", DAEWON, "--node", @@ -520,12 +504,6 @@ fn ledger_txs_and_queries() -> Result<()> { NAM, "--amount", "10.1", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--node", &validator_one_rpc, ], @@ -537,12 +515,6 @@ fn ledger_txs_and_queries() -> Result<()> { BERTHA, "--code-path", VP_USER_WASM, - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--node", @@ -557,16 +529,10 @@ fn ledger_txs_and_queries() -> Result<()> { &tx_data_path, "--owner", BERTHA, - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--node", - &validator_one_rpc + &validator_one_rpc, ], // 5. Submit a tx to initialize a new account vec![ @@ -580,12 +546,6 @@ fn ledger_txs_and_queries() -> Result<()> { VP_USER_WASM, "--alias", "Test-Account", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--node", @@ -602,12 +562,6 @@ fn ledger_txs_and_queries() -> Result<()> { VP_USER_WASM, "--alias", "Test-Account-2", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--node", @@ -631,6 +585,24 @@ fn ledger_txs_and_queries() -> Result<()> { "--node", &validator_one_rpc, ], + // 6. Submit a tx to withdraw from faucet account (requires PoW challenge + // solution) + vec![ + "transfer", + "--source", + "faucet", + "--target", + ALBERT, + "--token", + NAM, + "--amount", + "10.1", + // Faucet withdrawal requires an explicit signer + "--signing-keys", + ALBERT_KEY, + "--node", + &validator_one_rpc, + ], ]; for tx_args in &txs_args { @@ -829,6 +801,8 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", + "--gas-payer", + CHRISTEL_KEY, "--node", &validator_one_rpc, ], @@ -859,6 +833,8 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "5", + "--gas-payer", + CHRISTEL_KEY, "--node", &validator_one_rpc, ], @@ -899,6 +875,116 @@ fn masp_txs_and_queries() -> Result<()> { Ok(()) } +/// Test the optional disposable keypair for wrapper signing +/// +/// 1. Test that a tx requesting a disposable signer with a correct unshielding +/// operation is succesful +/// 2. Test that a tx requesting a disposable signer +/// providing an insufficient unshielding goes through the PoW +#[test] +fn wrapper_disposable_signer() -> Result<()> { + // Download the shielded pool parameters before starting node + let _ = CLIShieldedUtils::new(PathBuf::new()); + // Lengthen epoch to ensure that a transaction can be constructed and + // submitted within the same block. Necessary to ensure that conversion is + // not invalidated. + let test = setup::network( + |genesis| { + let parameters = ParametersConfig { + epochs_per_year: epochs_per_year_from_min_duration(3600), + min_num_of_blocks: 1, + ..genesis.parameters + }; + GenesisConfig { + parameters, + ..genesis + } + }, + None, + )?; + + // 1. Run the ledger node + let _bg_ledger = + start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))? + .background(); + + let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); + + let _ep1 = epoch_sleep(&test, &validator_one_rpc, 720)?; + + let txs_args = vec![ + ( + vec![ + "transfer", + "--source", + ALBERT, + "--target", + AA_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "50", + "--ledger-address", + &validator_one_rpc, + ], + "Transaction is valid", + ), + ( + vec![ + "transfer", + "--source", + ALBERT, + "--target", + BERTHA, + "--token", + NAM, + "--amount", + "1", + "--gas-spending-key", + A_SPENDING_KEY, + "--disposable-gas-payer", + "--ledger-address", + &validator_one_rpc, + ], + "Transaction is valid", + ), + ( + vec![ + "transfer", + "--source", + ALBERT, + "--target", + BERTHA, + "--token", + NAM, + "--amount", + "1", + "--gas-price", + "90000000", + "--gas-spending-key", + A_SPENDING_KEY, + "--disposable-gas-payer", + "--ledger-address", + &validator_one_rpc, + ], + // Not enough funds for fee payment, will use PoW + "Looking for a solution with difficulty", + ), + ]; + + for (tx_args, tx_result) in &txs_args { + let mut client = run!(test, Bin::Client, tx_args, Some(720))?; + + if *tx_result == "Transaction is valid" { + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; + } + client.exp_string(tx_result)?; + } + + Ok(()) +} + /// In this test we: /// 1. Run the ledger node /// 2. Submit an invalid transaction (disallowed by state machine) @@ -936,12 +1022,6 @@ fn invalid_transactions() -> Result<()> { NAM, "--amount", "1", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", ALBERT_KEY, "--node", @@ -953,7 +1033,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(); @@ -996,12 +1076,6 @@ fn invalid_transactions() -> Result<()> { BERTHA, "--amount", "1000000.1", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, // Force to ignore client check that fails on the balance check of the // source address "--force", @@ -1015,7 +1089,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(()) @@ -1079,12 +1153,6 @@ fn pos_bonds() -> Result<()> { "validator-0", "--amount", "10000.0", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", "validator-0-account-key", "--node", @@ -1105,12 +1173,6 @@ fn pos_bonds() -> Result<()> { BERTHA, "--amount", "5000.0", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--node", @@ -1128,12 +1190,6 @@ fn pos_bonds() -> Result<()> { "validator-0", "--amount", "5100.0", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", "validator-0-account-key", "--node", @@ -1154,12 +1210,6 @@ fn pos_bonds() -> Result<()> { BERTHA, "--amount", "3200.", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--node", @@ -1200,12 +1250,6 @@ fn pos_bonds() -> Result<()> { "withdraw", "--validator", "validator-0", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", "validator-0-account-key", "--node", @@ -1224,12 +1268,6 @@ fn pos_bonds() -> Result<()> { "validator-0", "--source", BERTHA, - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--node", @@ -1304,8 +1342,6 @@ fn pos_rewards() -> Result<()> { "10000.0", "--gas-amount", "0", - "--gas-limit", - "0", "--gas-token", NAM, "--signing-keys", @@ -1342,8 +1378,6 @@ fn pos_rewards() -> Result<()> { "30000.0", "--gas-amount", "0", - "--gas-limit", - "0", "--gas-token", NAM, "--signing-keys", @@ -1366,8 +1400,6 @@ fn pos_rewards() -> Result<()> { "25000.0", "--gas-amount", "0", - "--gas-limit", - "0", "--gas-token", NAM, "--signing-keys", @@ -1450,16 +1482,25 @@ fn test_bond_queries() -> Result<()> { "bond", "--validator", validator_alias, + "--amount", + "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, "--signing-keys", BERTHA_KEY, "--ledger-address", @@ -1492,12 +1533,6 @@ fn test_bond_queries() -> Result<()> { BERTHA, "--amount", "300", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--ledger-address", @@ -1517,12 +1552,6 @@ fn test_bond_queries() -> Result<()> { BERTHA, "--amount", "412", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--ledger-address", @@ -1625,12 +1654,6 @@ fn pos_init_validator() -> Result<()> { new_validator, "--account-keys", "bertha-key", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--commission-rate", "0.05", "--max-commission-rate-change", @@ -1656,13 +1679,7 @@ fn pos_init_validator() -> Result<()> { "--token", NAM, "--amount", - "0.5", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, + "10000.5", "--signing-keys", BERTHA_KEY, "--node", @@ -1682,12 +1699,6 @@ fn pos_init_validator() -> Result<()> { BERTHA, "--amount", delegation_str, - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--node", @@ -1709,12 +1720,6 @@ fn pos_init_validator() -> Result<()> { NAM, "--amount", validator_stake_str, - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--node", @@ -1731,12 +1736,6 @@ fn pos_init_validator() -> Result<()> { new_validator, "--amount", validator_stake_str, - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--node", &non_validator_rpc, ]; @@ -1841,12 +1840,6 @@ fn ledger_many_txs_in_a_block() -> Result<()> { NAM, "--amount", "1.01", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--signing-keys", BERTHA_KEY, "--node", @@ -1899,8 +1892,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 { @@ -1908,16 +1899,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 }; @@ -1951,12 +1932,6 @@ fn proposal_submission() -> Result<()> { BERTHA, "--amount", "900", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--node", &validator_0_rpc, ]; @@ -1978,6 +1953,8 @@ fn proposal_submission() -> Result<()> { "init-proposal", "--data-path", valid_proposal_json_path.to_str().unwrap(), + "--gas-limit", + "2000000", "--node", &validator_one_rpc, ]; @@ -2277,12 +2254,6 @@ fn pgf_governance_proposal() -> Result<()> { BERTHA, "--amount", "900", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--ledger-address", &validator_one_rpc, ]; @@ -2535,12 +2506,6 @@ fn proposal_offline() -> Result<()> { ALBERT, "--amount", "900", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--node", &validator_one_rpc, ]; @@ -3009,12 +2974,6 @@ fn test_genesis_validators() -> Result<()> { NAM, "--amount", "10.1", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--node", &validator_one_rpc, ]; @@ -3062,7 +3021,7 @@ fn test_genesis_validators() -> Result<()> { for ledger_rpc in &[validator_0_rpc, validator_1_rpc, non_validator_rpc] { let mut client = run!(test, Bin::Client, query_balance_args(ledger_rpc), Some(40))?; - client.exp_string("nam: 1000000000010.1")?; + client.exp_string(r"nam: 1000000000010.1")?; client.assert_success(); } @@ -3249,12 +3208,6 @@ fn double_signing_gets_slashed() -> Result<()> { NAM, "--amount", "10.1", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--node", &validator_one_rpc, ]; @@ -3338,12 +3291,6 @@ fn double_signing_gets_slashed() -> Result<()> { "unjail-validator", "--validator", "validator-0", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, "--node", &validator_one_rpc, ]; @@ -3469,6 +3416,8 @@ fn implicit_account_reveal_pk() -> Result<()> { valid_proposal_json_path.to_str().unwrap(), "--signing-keys", source, + "--gas-limit", + "2000000", "--node", &validator_0_rpc, ] diff --git a/tests/src/e2e/multitoken_tests/helpers.rs b/tests/src/e2e/multitoken_tests/helpers.rs index 176692c508..7ce90bcc87 100644 --- a/tests/src/e2e/multitoken_tests/helpers.rs +++ b/tests/src/e2e/multitoken_tests/helpers.rs @@ -1,11 +1,11 @@ //! Helpers for use in multitoken tests. use std::path::PathBuf; +use std::str::FromStr; use borsh::BorshSerialize; use color_eyre::eyre::Result; use eyre::Context; use namada_core::types::address::Address; -use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada_core::types::{storage, token}; use namada_test_utils::tx_data::TxWriteData; use namada_test_utils::TestWasms; @@ -15,7 +15,7 @@ use regex::Regex; use super::setup::constants::NAM; use super::setup::{Bin, NamadaCmd, Test}; -use crate::e2e::setup::constants::{ALBERT, ALBERT_KEY}; +use crate::e2e::setup::constants::ALBERT; use crate::run; const MULTITOKEN_KEY_SEGMENT: &str = "tokens"; @@ -24,7 +24,6 @@ const RED_TOKEN_KEY_SEGMENT: &str = "red"; const MULTITOKEN_RED_TOKEN_SUB_PREFIX: &str = "tokens/red"; const ARBITRARY_SIGNER: &str = ALBERT; -const ARBITRARY_SIGNER_KEY: &str = ALBERT_KEY; /// Initializes a VP to represent a multitoken account. pub fn init_multitoken_vp(test: &Test, rpc_addr: &str) -> Result { @@ -47,11 +46,9 @@ pub fn init_multitoken_vp(test: &Test, rpc_addr: &str) -> Result { &multitoken_vp_wasm_path, "--alias", multitoken_alias, - "--gas-amount", - "0", "--gas-limit", - "0", - "--gas-token", + "100", + "--fee-token", NAM, "--ledger-address", rpc_addr, @@ -114,8 +111,8 @@ pub fn mint_red_tokens( let tx_code_path = tx_code_path.to_string_lossy().to_string(); let tx_args = vec![ "tx", - "--signing-keys", - ARBITRARY_SIGNER_KEY, + "--signer", + ARBITRARY_SIGNER, "--code-path", &tx_code_path, "--data-path", @@ -136,10 +133,10 @@ pub fn attempt_red_tokens_transfer( multitoken: &str, from: &str, to: &str, - signing_keys: &str, + signer: &str, amount: &token::Amount, ) -> Result { - let amount = amount.to_string_native(); + let amount = amount.to_string(); let transfer_args = vec![ "transfer", "--token", @@ -150,10 +147,12 @@ pub fn attempt_red_tokens_transfer( from, "--target", to, - "--signing-keys", - signing_keys, + "--signer", + signer, "--amount", &amount, + "--gas-limit", + "100", "--ledger-address", rpc_addr, ]; @@ -185,6 +184,6 @@ pub fn fetch_red_token_balance( println!("Got balance for {}: {}", owner_alias, matched); let decimal = decimal_regex.find(&matched).unwrap().as_str(); client_balance.assert_success(); - token::Amount::from_str(decimal, NATIVE_MAX_DECIMAL_PLACES) + token::Amount::from_str(decimal) .wrap_err(format!("Failed to parse {}", matched)) } diff --git a/tests/src/integration/masp.rs b/tests/src/integration/masp.rs index a8c94f3b15..ac69f08a0d 100644 --- a/tests/src/integration/masp.rs +++ b/tests/src/integration/masp.rs @@ -922,9 +922,10 @@ fn masp_pinned_txs() -> Result<()> { #[test] fn masp_txs_and_queries() -> Result<()> { // Uncomment for better debugging - // let _log_guard = - // namada_apps::logging::init_from_env_or(tracing::level_filters::LevelFilter::INFO)? - // ; This address doesn't matter for tests. But an argument is required. + // let _log_guard = namada_apps::logging::init_from_env_or( + // tracing::level_filters::LevelFilter::INFO, + // )?; + // This address doesn't matter for tests. But an argument is required. let validator_one_rpc = "127.0.0.1:26567"; // Download the shielded pool parameters before starting node let _ = CLIShieldedUtils::new(PathBuf::new()); @@ -949,6 +950,8 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "10", + "--gas-payer", + CHRISTEL, "--node", validator_one_rpc, ], @@ -966,6 +969,8 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "15", + "--gas-payer", + CHRISTEL, "--node", validator_one_rpc, ], @@ -1000,6 +1005,8 @@ fn masp_txs_and_queries() -> Result<()> { ETH, "--amount", "10", + "--gas-payer", + CHRISTEL, "--node", validator_one_rpc, ], @@ -1017,6 +1024,8 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", + "--gas-payer", + CHRISTEL, "--node", validator_one_rpc, ], @@ -1034,6 +1043,8 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", + "--gas-payer", + CHRISTEL, "--node", validator_one_rpc, ], @@ -1051,6 +1062,8 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", + "--gas-payer", + CHRISTEL, "--node", validator_one_rpc, ], @@ -1068,6 +1081,8 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "6", + "--gas-payer", + CHRISTEL, "--node", validator_one_rpc, ], @@ -1122,6 +1137,8 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "20", + "--gas-payer", + CHRISTEL, "--node", validator_one_rpc, ], @@ -1204,3 +1221,103 @@ fn masp_txs_and_queries() -> Result<()> { Ok(()) } + +/// Test the unshielding tx attached to a wrapper: +/// +/// 1. Shield some tokens to reduce the unshielded balance +/// 2. Submit a new wrapper with a valid unshielding tx and assert +/// success +/// 3. Submit a new wrapper with an invalid unshielding tx and assert the +/// failure +#[test] +#[should_panic(expected = "No faucet account found")] +fn wrapper_fee_unshielding() { + // This address doesn't matter for tests. But an argument is required. + let validator_one_rpc = "127.0.0.1:26567"; + // Download the shielded pool parameters before starting node + let _ = CLIShieldedUtils::new(PathBuf::new()); + // Lengthen epoch to ensure that a transaction can be constructed and + // submitted within the same block. Necessary to ensure that conversion is + // not invalidated. + let mut node = setup::setup().unwrap(); + _ = node.next_epoch(); + + // 1. Shield some tokens + run( + &node, + Bin::Client, + vec![ + "transfer", + "--source", + ALBERT, + "--target", + AA_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "500000", + "--gas-price", + "30", // Reduce the balance of the fee payer artificially + "--gas-limit", + "20000", + "--ledger-address", + validator_one_rpc, + ], + ) + .unwrap(); + node.assert_success(); + + _ = node.next_epoch(); + // 2. Valid unshielding + run( + &node, + Bin::Client, + vec![ + "transfer", + "--source", + ALBERT, + "--target", + BERTHA, + "--token", + NAM, + "--amount", + "1", + "--gas-price", + "30", + "--gas-limit", + "20000", + "--gas-spending-key", + A_SPENDING_KEY, + "--ledger-address", + validator_one_rpc, + ], + ) + .unwrap(); + node.assert_success(); + + // 3. Invalid unshielding + // TODO: this test shall panic because of the panic in the sdk. Once the + // panics are removed from there, this test can be updated + run( + &node, + Bin::Client, + vec![ + "transfer", + "--source", + ALBERT, + "--target", + BERTHA, + "--token", + NAM, + "--amount", + "1", + "--gas-price", + "1000", + "--gas-spending-key", + B_SPENDING_KEY, + "--ledger-address", + validator_one_rpc, + ], + ) + .unwrap(); +} diff --git a/tests/src/native_vp/mod.rs b/tests/src/native_vp/mod.rs index 1425b808b3..00070380e6 100644 --- a/tests/src/native_vp/mod.rs +++ b/tests/src/native_vp/mod.rs @@ -1,8 +1,10 @@ pub mod eth_bridge_pool; 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::traits::Sha256Hasher; @@ -49,7 +51,9 @@ impl TestNativeVpEnv { { let ctx = Ctx { iterators: Default::default(), - gas_meter: Default::default(), + gas_meter: RefCell::new(VpGasMeter::new_from_tx_meter( + &self.tx_env.gas_meter, + )), 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 077385dc61..344a75d4e3 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -568,6 +568,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; @@ -853,7 +854,8 @@ 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_from_sub_limit(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 f88be446fe..abb52e54d5 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -82,6 +82,7 @@ use namada::types::storage::{ use namada::types::time::DurationSecs; use namada::types::token::{self, Amount}; use namada::vm::{wasm, WasmCacheRwAccess}; +use namada_core::ledger::gas::TxGasMeter; use namada_test_utils::TestWasms; use namada_tx_prelude::BorshSerialize; @@ -151,7 +152,9 @@ pub fn validate_ibc_vp_from_tx<'a>( &tx_env.wl_storage.write_log, tx, &TxIndex(0), - VpGasMeter::new(0), + VpGasMeter::new_from_tx_meter(&TxGasMeter::new_from_sub_limit( + 1_000_000.into(), + )), &keys_changed, &verifiers, vp_wasm_cache, @@ -187,7 +190,9 @@ pub fn validate_multitoken_vp_from_tx<'a>( &tx_env.wl_storage.write_log, tx, &TxIndex(0), - VpGasMeter::new(0), + VpGasMeter::new_from_tx_meter(&TxGasMeter::new_from_sub_limit( + 1_000_000.into(), + )), &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 878d93aeef..272b0877f3 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -34,6 +34,7 @@ mod tests { use namada::types::time::DateTimeUtc; use namada::types::token::{self, Amount}; use namada::types::{address, key}; + use namada_core::ledger::gas::{TxGasMeter, VpGasMeter}; use namada_core::ledger::ibc::context::transfer_mod::testing::DummyTransferModule; use namada_core::ledger::ibc::Error as IbcActionError; use namada_test_utils::TestWasms; @@ -479,7 +480,10 @@ mod tests { ], pks_map, 1, - None + None, + &mut VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()) + ) ) .is_ok() ); @@ -496,7 +500,10 @@ mod tests { other_keypair.ref_to() ]), 1, - None + None, + &mut VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(u64::MAX.into()) + ) ) .is_err() ); @@ -567,10 +574,16 @@ mod tests { // evaluating the VP template which always returns `true` should pass 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(); }); let mut tx = Tx::new(ChainId::default(), None); tx.add_code_from_hash(code_hash) @@ -584,10 +597,16 @@ 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(); }); let mut tx = Tx::new(ChainId::default(), None); tx.add_code_from_hash(code_hash) diff --git a/tests/src/vm_host_env/tx.rs b/tests/src/vm_host_env/tx.rs index a0e88ab802..eabe3fd315 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 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; @@ -49,7 +49,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, @@ -73,7 +73,7 @@ impl Default for TestTxEnv { Self { wl_storage, iterators: PrefixIterators::default(), - gas_meter: BlockGasMeter::default(), + gas_meter: TxGasMeter::new_from_sub_limit(100_000_000.into()), tx_index: TxIndex::default(), verifiers: BTreeSet::default(), result_buffer: None, @@ -204,7 +204,6 @@ impl TestTxEnv { .ok(); self.iterators = PrefixIterators::default(); self.verifiers = BTreeSet::default(); - self.gas_meter = BlockGasMeter::default(); } /// Credit tokens to the target account. @@ -456,4 +455,5 @@ mod native_tx_host_env { native_host_fn!(tx_get_block_epoch() -> u64); native_host_fn!(tx_get_native_token(result_ptr: u64)); native_host_fn!(tx_log_string(str_ptr: u64, str_len: u64)); + native_host_fn!(tx_charge_gas(used_gas: u64)); } diff --git a/tests/src/vm_host_env/vp.rs b/tests/src/vm_host_env/vp.rs index 890d0681d8..7324fbd90e 100644 --- a/tests/src/vm_host_env/vp.rs +++ b/tests/src/vm_host_env/vp.rs @@ -12,6 +12,7 @@ use namada::types::transaction::TxType; use namada::vm::prefix_iter::PrefixIterators; use namada::vm::wasm::{self, VpCache}; use namada::vm::{self, WasmCacheRwAccess}; +use namada_core::ledger::gas::TxGasMeter; use namada_vp_prelude::Ctx; use tempfile::TempDir; @@ -75,7 +76,9 @@ impl Default for TestVpEnv { addr: address::testing::established_address_1(), wl_storage, iterators: PrefixIterators::default(), - gas_meter: VpGasMeter::default(), + gas_meter: VpGasMeter::new_from_tx_meter( + &TxGasMeter::new_from_sub_limit(10_000_000.into()), + ), tx, tx_index: TxIndex::default(), keys_changed: BTreeSet::default(), @@ -235,7 +238,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!( @@ -371,4 +374,15 @@ mod native_vp_host_env { ) -> i64); native_host_fn!(vp_has_valid_pow() -> i64); native_host_fn!(vp_log_string(str_ptr: u64, str_len: u64)); + native_host_fn!(vp_verify_tx_section_signature( + hash_list_ptr: u64, + hash_list_len: u64, + public_keys_map_ptr: u64, + public_keys_map_len: u64, + threshold: u8, + max_signatures_ptr: u64, + max_signatures_len: u64,) + -> i64 + ); + native_host_fn!(vp_charge_gas(used_gas: u64)); } diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index cd2f5d5f92..16242da8fd 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -324,4 +324,9 @@ impl TxEnv for Ctx { }; Ok(()) } + + fn charge_gas(&mut self, used_gas: u64) -> Result<(), Error> { + unsafe { namada_tx_charge_gas(used_gas) }; + Ok(()) + } } diff --git a/vm_env/src/lib.rs b/vm_env/src/lib.rs index f51d92e62e..678df7ae57 100644 --- a/vm_env/src/lib.rs +++ b/vm_env/src/lib.rs @@ -101,6 +101,9 @@ pub mod tx { // Requires a node running with "Info" log level pub fn namada_tx_log_string(str_ptr: u64, str_len: u64); + + /// Charge the provided amount of gas for the current tx + pub fn namada_tx_charge_gas(used_gas: u64); } } @@ -187,9 +190,20 @@ pub mod vp { // Requires a node running with "Info" log level pub fn namada_vp_log_string(str_ptr: u64, str_len: u64); + // Verify the signatures of a tx + pub fn namada_vp_verify_tx_section_signature( + hash_list_ptr: u64, + hash_list_len: u64, + public_keys_map_ptr: u64, + public_keys_map_len: u64, + threshold: u8, + max_signatures_ptr: u64, + max_signatures_len: u64, + ) -> i64; + 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; @@ -197,6 +211,9 @@ pub mod vp { pub fn namada_vp_verify_masp(tx_ptr: u64, tx_len: u64) -> i64; pub fn namada_vp_has_valid_pow() -> i64; + + /// Charge the provided amount of gas for the current vp + pub fn namada_vp_charge_gas(used_gas: u64); } } diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index c8e555a87b..564668823f 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -87,15 +87,26 @@ pub fn verify_signatures(ctx: &Ctx, tx: &Tx, owner: &Address) -> VpResult { let threshold = storage_api::account::threshold(&ctx.pre(), owner)?.unwrap_or(1); - let targets = &[*tx.data_sechash(), *tx.code_sechash()]; - tx.verify_section_signatures( - targets, - public_keys_index_map, - threshold, - max_signatures_per_transaction, - ) - .map_err(|_e| Error::SimpleMessage("Invalid signatures")) - .map(|_| true) + let targets = [*tx.data_sechash(), *tx.code_sechash()]; + + // Serialize parameters + let max_signatures = max_signatures_per_transaction.try_to_vec().unwrap(); + let public_keys_map = public_keys_index_map.try_to_vec().unwrap(); + let targets = targets.try_to_vec().unwrap(); + + let valid = unsafe { + namada_vp_verify_tx_section_signature( + targets.as_ptr() as _, + targets.len() as _, + public_keys_map.as_ptr() as _, + public_keys_map.len() as _, + threshold, + max_signatures.as_ptr() as _, + max_signatures.len() as _, + ) + }; + + Ok(HostEnvResult::is_success(valid)) } /// Checks whether a transaction is valid, which happens in two cases: @@ -295,12 +306,12 @@ impl<'view> VpEnv<'view> for Ctx { iter_prefix_pre_impl(prefix) } - fn eval(&self, vp_code: Hash, input_data: Tx) -> Result { + fn eval(&self, vp_code_hash: Hash, input_data: Tx) -> Result { let input_data_bytes = BorshSerialize::try_to_vec(&input_data).unwrap(); let result = unsafe { namada_vp_eval( - vp_code.0.as_ptr() as _, - vp_code.0.len() as _, + vp_code_hash.0.as_ptr() as _, + vp_code_hash.0.len() as _, input_data_bytes.as_ptr() as _, input_data_bytes.len() as _, ) @@ -331,6 +342,11 @@ impl<'view> VpEnv<'view> for Ctx { unsafe { namada_vp_verify_masp(tx.as_ptr() as _, tx.len() as _) }; Ok(HostEnvResult::is_success(valid)) } + + fn charge_gas(&self, used_gas: u64) -> Result<(), Error> { + unsafe { namada_vp_charge_gas(used_gas) }; + Ok(()) + } } impl StorageRead for CtxPreStorageRead<'_> { diff --git a/wasm/checksums.json b/wasm/checksums.json index 7741bb762e..3bf6f1f6da 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,21 +1,21 @@ { - "tx_bond.wasm": "tx_bond.8f5bafda7387762a6a7574b125dad6d160331fd7d305c307e8749f5e21b8a04d.wasm", - "tx_bridge_pool.wasm": "tx_bridge_pool.a77799804f6eb9fdc75e60c69f03b8ed5891851f3671ffcb0933b88e64da749c.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.89dd3dd14c539e534f208e105c1fcfde709771ccc700bd1af0400fab19303e65.wasm", - "tx_ibc.wasm": "tx_ibc.6939d8299eb3269e5e80404bd2c3b4265efd5a1820d629b538f3644d48b85a07.wasm", - "tx_init_account.wasm": "tx_init_account.7b772c33c368932de49eec4e78835ad34d6b5c768adb3ba29044ed193a988066.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.48bceb8ec3a3956b89c03598300b0bfd00f311296aa8fb9e779f27bccbb38ab4.wasm", - "tx_init_validator.wasm": "tx_init_validator.dece65164bcde3c2f6bae0663c311ea86d6a735472e80bdff91f12925f476ad1.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.b681948258c9f3c7a0b023ba608f624250b793a3e4a6d2aed8abd60c23f0ce62.wasm", - "tx_transfer.wasm": "tx_transfer.856ce5a52f8b611300d91a612eeb2302ccfe0a3abfacf08aac88bddfa2cb8794.wasm", - "tx_unbond.wasm": "tx_unbond.667849572b95e244ef6562408017129ab75b29ef9eefe5d374b9fc332f9296ad.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.c8e74152789fd6f3754594a570dbb5301792a308b5c7b3018d2b794c655b1d1d.wasm", - "tx_update_account.wasm": "tx_update_account.278c342e43d7822d5fe65c8131ed4f5f98028c62079b8a58be6a45ec2c327475.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.8f330c597fa82dc019754585c1fba07a6a0873a41733d2eed050966c2a3419c9.wasm", - "tx_withdraw.wasm": "tx_withdraw.d09c0327a5a9424e3c7b8b6418ec51b60e3a44337b43e865ad80d24de86341c3.wasm", - "vp_implicit.wasm": "vp_implicit.2ddb2d6105a96a8316b902bec19b3c4b31de6d54c36696d7e45629d8be90acfe.wasm", - "vp_masp.wasm": "vp_masp.2f22a5aef39a4afa1f8348aed394082bb02dc257cc722092ecd2175600ca6466.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.b7f09edf070f6cf8d25d6ec2b72784d05d2385ac87064008c002bbe93a3da556.wasm", - "vp_user.wasm": "vp_user.4b7f2818c48f28d7285b61b10c1d3215ac52f95e77bf6e3ac3ef38e80151e84d.wasm", - "vp_validator.wasm": "vp_validator.21b9c59bc007205ff4a05c00d65bf96b9a1b4f7cd5f1d44aa6a5b38289dbdcb1.wasm" + "tx_bond.wasm": "tx_bond.b776c7f911090648a7380de380c620bbbf18878018054f4f44e60c7b37e734f1.wasm", + "tx_bridge_pool.wasm": "tx_bridge_pool.898d81c54666bdf58a7db17091ebefc7af2e9fbaf2e4e56c02747ad7b6c8a4ad.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.6b3941c0db7e8d4657938177adc2a86915afeda99fb98b13a2be5a4a33513077.wasm", + "tx_ibc.wasm": "tx_ibc.981d4a37696138be3368c3ac66c8df1148a4463c3db51185dcbebeea62a57213.wasm", + "tx_init_account.wasm": "tx_init_account.8fb41dba6bb45b854a06274a27dc34c2db36b6cb0da699ad1f65c0667784e2f7.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.eabbe08625b6daaccede3cb2aa182e2c0ca620d961a52638de5cee6e33090095.wasm", + "tx_init_validator.wasm": "tx_init_validator.b2654f3225a80008e73638ba66ceea7e39f91861a0e0b0356d81d917a6505827.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.5f2c4c7b36e3eec39b49d74ad7be3da984f2e9b96c41af06db871fe00e26d7d0.wasm", + "tx_transfer.wasm": "tx_transfer.c2a5bdf7c9c87767cdb9e855753f7b04e6f6b8576944fdf65ea7b5e5e25ea2b9.wasm", + "tx_unbond.wasm": "tx_unbond.ab8a69d3d085923c7a3c4778b6cf5cec1bf3c91875712a97db04fee707ca73fc.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.f678d459521a5a19292b97102a97d749e7e0e43f8042bdb8e280392ad86f1cc1.wasm", + "tx_update_account.wasm": "tx_update_account.0fcc9e604cee18b974e6bd24ab8185b06e5a574d932712a7927a14c8033546bf.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.3344866294d2b09e65452960a2088267e11bc28ff0f2cf7f631c533a0be7b371.wasm", + "tx_withdraw.wasm": "tx_withdraw.fc9079438b60ea76e602036de268e2ec7ac5d42560052152bdb1e70456c34e6b.wasm", + "vp_implicit.wasm": "vp_implicit.71182d3882cf0bdfb50868d31dc8930304f9db54166db998fb96bc816bd98f54.wasm", + "vp_masp.wasm": "vp_masp.c097e694e4b027c3deb63467cfd9da67fb615ec9226556f389bb4c54df982949.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.5c1583503aba08298321253440ed512144f4b77a1088819d0d7eff49b3e15416.wasm", + "vp_user.wasm": "vp_user.08dc2933d18a1857e703e459927573f4a2943e0c11aa118bbee9df5e35de42e1.wasm", + "vp_validator.wasm": "vp_validator.19705f708d484bea81e19a4dc466195356a9b1b95a6b280d0c16138b7a876a5e.wasm" } \ No newline at end of file diff --git a/wasm/tx_template/src/lib.rs b/wasm/tx_template/src/lib.rs index 5b0f8fb501..4bedb51ecb 100644 --- a/wasm/tx_template/src/lib.rs +++ b/wasm/tx_template/src/lib.rs @@ -1,6 +1,6 @@ use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 1000)] fn apply_tx(_ctx: &mut Ctx, tx_data: Tx) -> TxResult { log_string(format!("apply_tx called with data: {:#?}", tx_data)); Ok(()) diff --git a/wasm/vp_template/src/lib.rs b/wasm/vp_template/src/lib.rs index c4af9f31e0..3a42efc741 100644 --- a/wasm/vp_template/src/lib.rs +++ b/wasm/vp_template/src/lib.rs @@ -1,6 +1,6 @@ use namada_vp_prelude::*; -#[validity_predicate] +#[validity_predicate(gas = 1000)] fn validate_tx( ctx: &Ctx, tx_data: Tx, diff --git a/wasm/wasm_source/Makefile b/wasm/wasm_source/Makefile index a88065355f..9db49d5a5e 100644 --- a/wasm/wasm_source/Makefile +++ b/wasm/wasm_source/Makefile @@ -82,7 +82,7 @@ $(patsubst %,watch_%,$(wasms)): watch_%: $(patsubst %,clippy_%,$(wasms)): clippy_%: $(cargo) +$(nightly) clippy --all-targets --features $* -- -D warnings -clean-wasm = rm ../$(wasm).wasm +clean-wasm = rm ../$(wasm).*.wasm clean: $(foreach wasm,$(wasms),$(clean-wasm) && ) true diff --git a/wasm/wasm_source/src/tx_bond.rs b/wasm/wasm_source/src/tx_bond.rs index f9a6ab9c60..3453747161 100644 --- a/wasm/wasm_source/src/tx_bond.rs +++ b/wasm/wasm_source/src/tx_bond.rs @@ -2,7 +2,7 @@ use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 160000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = signed.data().ok_or_err_msg("Missing data")?; diff --git a/wasm/wasm_source/src/tx_bridge_pool.rs b/wasm/wasm_source/src/tx_bridge_pool.rs index bf73e83f7d..b34966f91e 100644 --- a/wasm/wasm_source/src/tx_bridge_pool.rs +++ b/wasm/wasm_source/src/tx_bridge_pool.rs @@ -5,7 +5,7 @@ use eth_bridge::storage::{bridge_pool, native_erc20_key, wrapped_erc20s}; use eth_bridge_pool::{GasFee, PendingTransfer, TransferToEthereum}; use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 100000)] fn apply_tx(ctx: &mut Ctx, signed: Tx) -> TxResult { let data = signed.data().ok_or_err_msg("Missing data")?; let transfer = PendingTransfer::try_from_slice(&data[..]) diff --git a/wasm/wasm_source/src/tx_change_validator_commission.rs b/wasm/wasm_source/src/tx_change_validator_commission.rs index ee3685b191..c1e1b35226 100644 --- a/wasm/wasm_source/src/tx_change_validator_commission.rs +++ b/wasm/wasm_source/src/tx_change_validator_commission.rs @@ -3,7 +3,7 @@ use namada_tx_prelude::transaction::pos::CommissionChange; use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 220000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = signed.data().ok_or_err_msg("Missing data")?; diff --git a/wasm/wasm_source/src/tx_ibc.rs b/wasm/wasm_source/src/tx_ibc.rs index 08c7678b07..ebcb529842 100644 --- a/wasm/wasm_source/src/tx_ibc.rs +++ b/wasm/wasm_source/src/tx_ibc.rs @@ -5,7 +5,7 @@ use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 1240000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = signed.data().ok_or_err_msg("Missing data")?; diff --git a/wasm/wasm_source/src/tx_init_account.rs b/wasm/wasm_source/src/tx_init_account.rs index 346afb2bec..78a8edb018 100644 --- a/wasm/wasm_source/src/tx_init_account.rs +++ b/wasm/wasm_source/src/tx_init_account.rs @@ -3,7 +3,7 @@ use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 230000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = signed.data().ok_or_err_msg("Missing data")?; diff --git a/wasm/wasm_source/src/tx_init_proposal.rs b/wasm/wasm_source/src/tx_init_proposal.rs index e11ebb7a61..870d53331b 100644 --- a/wasm/wasm_source/src/tx_init_proposal.rs +++ b/wasm/wasm_source/src/tx_init_proposal.rs @@ -2,7 +2,7 @@ use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 40000)] fn apply_tx(ctx: &mut Ctx, tx: Tx) -> TxResult { let data = tx.data().ok_or_err_msg("Missing data")?; let tx_data = diff --git a/wasm/wasm_source/src/tx_init_validator.rs b/wasm/wasm_source/src/tx_init_validator.rs index eb80ec444e..51889d8f00 100644 --- a/wasm/wasm_source/src/tx_init_validator.rs +++ b/wasm/wasm_source/src/tx_init_validator.rs @@ -4,7 +4,7 @@ use namada_tx_prelude::transaction::pos::InitValidator; use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 730000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = signed.data().ok_or_err_msg("Missing data")?; diff --git a/wasm/wasm_source/src/tx_reveal_pk.rs b/wasm/wasm_source/src/tx_reveal_pk.rs index e091b69a4c..189ef2f4b3 100644 --- a/wasm/wasm_source/src/tx_reveal_pk.rs +++ b/wasm/wasm_source/src/tx_reveal_pk.rs @@ -6,7 +6,7 @@ use namada_tx_prelude::key::common; use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 170000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = signed.data().ok_or_err_msg("Missing data")?; diff --git a/wasm/wasm_source/src/tx_transfer.rs b/wasm/wasm_source/src/tx_transfer.rs index 33899640c4..bdc683c339 100644 --- a/wasm/wasm_source/src/tx_transfer.rs +++ b/wasm/wasm_source/src/tx_transfer.rs @@ -4,7 +4,7 @@ use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 110000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = signed.data().ok_or_err_msg("Missing data")?; diff --git a/wasm/wasm_source/src/tx_unbond.rs b/wasm/wasm_source/src/tx_unbond.rs index 90b22734a0..7e08c0dcda 100644 --- a/wasm/wasm_source/src/tx_unbond.rs +++ b/wasm/wasm_source/src/tx_unbond.rs @@ -3,7 +3,7 @@ use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 430000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = signed.data().ok_or_err_msg("Missing data")?; diff --git a/wasm/wasm_source/src/tx_unjail_validator.rs b/wasm/wasm_source/src/tx_unjail_validator.rs index ddbce90f37..b487c44f7b 100644 --- a/wasm/wasm_source/src/tx_unjail_validator.rs +++ b/wasm/wasm_source/src/tx_unjail_validator.rs @@ -3,7 +3,7 @@ use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 340000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = signed.data().ok_or_err_msg("Missing data")?; diff --git a/wasm/wasm_source/src/tx_update_account.rs b/wasm/wasm_source/src/tx_update_account.rs index c2553759e5..d6315f9ace 100644 --- a/wasm/wasm_source/src/tx_update_account.rs +++ b/wasm/wasm_source/src/tx_update_account.rs @@ -5,7 +5,7 @@ use namada_tx_prelude::key::pks_handle; use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 140000)] fn apply_tx(ctx: &mut Ctx, tx: Tx) -> TxResult { let signed = tx; let data = signed.data().ok_or_err_msg("Missing data")?; diff --git a/wasm/wasm_source/src/tx_vote_proposal.rs b/wasm/wasm_source/src/tx_vote_proposal.rs index 3be6685bdc..9bfd4d891b 100644 --- a/wasm/wasm_source/src/tx_vote_proposal.rs +++ b/wasm/wasm_source/src/tx_vote_proposal.rs @@ -2,7 +2,7 @@ use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 120000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = signed.data().ok_or_err_msg("Missing data")?; diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index dd40e044b3..c8fa649c43 100644 --- a/wasm/wasm_source/src/tx_withdraw.rs +++ b/wasm/wasm_source/src/tx_withdraw.rs @@ -3,7 +3,7 @@ use namada_tx_prelude::*; -#[transaction] +#[transaction(gas = 260000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = signed.data().ok_or_err_msg("Missing data")?; diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 9da84d1776..a67b40d357 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -47,7 +47,7 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { } } -#[validity_predicate] +#[validity_predicate(gas = 40000)] fn validate_tx( ctx: &Ctx, tx_data: Tx, @@ -62,8 +62,9 @@ fn validate_tx( verifiers ); - let valid_sig = - Lazy::new(|| verify_signatures(ctx, &tx_data, &addr).is_ok()); + let valid_sig = Lazy::new(|| { + matches!(verify_signatures(ctx, &tx_data, &addr), Ok(true)) + }); if !is_valid_tx(ctx, &tx_data)? { return reject(); diff --git a/wasm/wasm_source/src/vp_masp.rs b/wasm/wasm_source/src/vp_masp.rs index 1e43d93a25..7795007a33 100644 --- a/wasm/wasm_source/src/vp_masp.rs +++ b/wasm/wasm_source/src/vp_masp.rs @@ -72,7 +72,7 @@ fn convert_amount( (asset_type, amount) } -#[validity_predicate] +#[validity_predicate(gas = 8030000)] fn validate_tx( ctx: &Ctx, tx_data: Tx, @@ -257,6 +257,12 @@ fn validate_tx( // transaction value pool MUST be nonnegative. return reject(); } + Some(Ordering::Greater) => { + debug_log!( + "Transaction fees cannot be paid inside MASP transaction." + ); + return reject(); + } _ => {} } // Do the expensive proof verification in the VM at the end. diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index f41909f0c5..44b5adfb06 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -9,7 +9,7 @@ use namada_vp_prelude::*; use once_cell::unsync::Lazy; -#[validity_predicate] +#[validity_predicate(gas = 0)] fn validate_tx( ctx: &Ctx, tx_data: Tx, @@ -25,8 +25,9 @@ fn validate_tx( verifiers ); - let valid_sig = - Lazy::new(|| verify_signatures(ctx, &tx_data, &addr).is_ok()); + let valid_sig = Lazy::new(|| { + matches!(verify_signatures(ctx, &tx_data, &addr), Ok(true)) + }); if !is_valid_tx(ctx, &tx_data)? { return reject(); diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index d68801834a..21b5e58be7 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -45,7 +45,7 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { } } -#[validity_predicate] +#[validity_predicate(gas = 60000)] fn validate_tx( ctx: &Ctx, tx_data: Tx, @@ -60,8 +60,9 @@ fn validate_tx( verifiers ); - let valid_sig = - Lazy::new(|| verify_signatures(ctx, &tx_data, &addr).is_ok()); + let valid_sig = Lazy::new(|| { + matches!(verify_signatures(ctx, &tx_data, &addr), Ok(true)) + }); if !is_valid_tx(ctx, &tx_data)? { return reject(); diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index 9b0de80f3e..2d34519a8c 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -45,7 +45,7 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { } } -#[validity_predicate] +#[validity_predicate(gas = 50000)] fn validate_tx( ctx: &Ctx, tx_data: Tx, @@ -60,8 +60,9 @@ fn validate_tx( verifiers ); - let valid_sig = - Lazy::new(|| verify_signatures(ctx, &tx_data, &addr).is_ok()); + let valid_sig = Lazy::new(|| { + matches!(verify_signatures(ctx, &tx_data, &addr), Ok(true)) + }); if !is_valid_tx(ctx, &tx_data)? { return reject(); diff --git a/wasm_for_tests/wasm_source/src/lib.rs b/wasm_for_tests/wasm_source/src/lib.rs index 790ad1a72c..35198a1a87 100644 --- a/wasm_for_tests/wasm_source/src/lib.rs +++ b/wasm_for_tests/wasm_source/src/lib.rs @@ -3,7 +3,7 @@ pub mod main { use namada_tx_prelude::*; - #[transaction] + #[transaction(gas = 1000)] fn apply_tx(_ctx: &mut Ctx, _tx_data: Tx) -> TxResult { Ok(()) } @@ -14,7 +14,7 @@ pub mod main { pub mod main { use namada_tx_prelude::*; - #[transaction] + #[transaction(gas = 1000)] fn apply_tx(_ctx: &mut Ctx, tx_data: Tx) -> TxResult { let len = usize::try_from_slice(&tx_data.data().as_ref().unwrap()[..]) .unwrap(); @@ -31,7 +31,7 @@ pub mod main { pub mod main { use namada_tx_prelude::*; - #[transaction] + #[transaction(gas = 1000)] fn apply_tx(ctx: &mut Ctx, _tx_data: Tx) -> TxResult { // governance let target_key = gov_storage::keys::get_min_proposal_grace_epoch_key(); @@ -49,7 +49,7 @@ pub mod main { pub mod main { use namada_tx_prelude::*; - #[transaction] + #[transaction(gas = 1000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { // Allocates a memory of size given from the `tx_data (usize)` let key = @@ -67,7 +67,8 @@ pub mod main { use borsh::BorshDeserialize; use namada_test_utils::tx_data::TxWriteData; use namada_tx_prelude::{ - log_string, transaction, Ctx, StorageRead, StorageWrite, Tx, TxResult, + log_string, transaction, Ctx, StorageRead, StorageWrite, Tx, TxEnv, + TxResult, }; const TX_NAME: &str = "tx_write"; @@ -86,7 +87,7 @@ pub mod main { panic!() } - #[transaction] + #[transaction(gas = 1000)] fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = match signed.data() { @@ -132,7 +133,7 @@ pub mod main { pub mod main { use namada_vp_prelude::*; - #[validity_predicate] + #[validity_predicate(gas = 1000)] fn validate_tx( _ctx: &Ctx, _tx_data: Tx, @@ -149,7 +150,7 @@ pub mod main { pub mod main { use namada_vp_prelude::*; - #[validity_predicate] + #[validity_predicate(gas = 1000)] fn validate_tx( _ctx: &Ctx, _tx_data: Tx, @@ -167,7 +168,7 @@ pub mod main { pub mod main { use namada_vp_prelude::*; - #[validity_predicate] + #[validity_predicate(gas = 1000)] fn validate_tx( ctx: &Ctx, tx_data: Tx, @@ -192,7 +193,7 @@ pub mod main { pub mod main { use namada_vp_prelude::*; - #[validity_predicate] + #[validity_predicate(gas = 1000)] fn validate_tx( _ctx: &Ctx, tx_data: Tx, @@ -216,7 +217,7 @@ pub mod main { pub mod main { use namada_vp_prelude::*; - #[validity_predicate] + #[validity_predicate(gas = 1000)] fn validate_tx( ctx: &Ctx, tx_data: Tx,