From 287de766f008bfb4516fb575cca04a768a522e38 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 26 Oct 2023 16:13:03 +0200 Subject: [PATCH 01/47] [feat] Re-implementing new genesis flow in v0.24 --- Cargo.lock | 1 + Makefile | 14 +- README.md | 4 - apps/Cargo.toml | 6 +- apps/build.rs | 9 - apps/src/bin/namada-client/cli.rs | 0 apps/src/bin/namada-node/cli.rs | 33 +- apps/src/bin/namada/cli.rs | 2 +- apps/src/lib/bench_utils.rs | 1 - apps/src/lib/cli.rs | 564 +++++-- apps/src/lib/cli/client.rs | 6 + apps/src/lib/cli/context.rs | 161 +- apps/src/lib/cli/wallet.rs | 148 +- apps/src/lib/client/types.rs | 0 apps/src/lib/client/utils.rs | 1024 ++++-------- apps/src/lib/config/genesis.rs | 1168 +++---------- apps/src/lib/config/genesis/chain.rs | 872 ++++++++++ apps/src/lib/config/genesis/templates.rs | 991 +++++++++++ apps/src/lib/config/genesis/toml_utils.rs | 38 + apps/src/lib/config/genesis/transactions.rs | 1483 +++++++++++++++++ apps/src/lib/config/global.rs | 19 +- apps/src/lib/config/mod.rs | 2 +- apps/src/lib/config/utils.rs | 24 + apps/src/lib/node/ledger/mod.rs | 9 +- .../lib/node/ledger/shell/finalize_block.rs | 8 +- apps/src/lib/node/ledger/shell/init_chain.rs | 808 +++++---- apps/src/lib/node/ledger/shell/mod.rs | 54 +- .../shell/vote_extensions/bridge_pool_vext.rs | 1 + apps/src/lib/node/ledger/shims/abcipp_shim.rs | 3 - apps/src/lib/node/ledger/storage/mod.rs | 2 - apps/src/lib/node/ledger/tendermint_node.rs | 40 +- apps/src/lib/wallet/alias.rs | 0 apps/src/lib/wallet/cli_utils.rs | 517 ------ apps/src/lib/wallet/defaults.rs | 149 +- apps/src/lib/wallet/mod.rs | 41 +- apps/src/lib/wallet/pre_genesis.rs | 2 +- apps/src/lib/wallet/store.rs | 61 +- core/src/ledger/pgf/parameters.rs | 3 + core/src/ledger/storage/masp_conversions.rs | 16 +- core/src/ledger/storage_api/pgf.rs | 4 +- core/src/proto/mod.rs | 5 +- core/src/proto/types.rs | 24 + core/src/types/address.rs | 151 +- core/src/types/chain.rs | 5 - core/src/types/dec.rs | 40 +- core/src/types/key/common.rs | 66 +- core/src/types/key/dkg_session_keys.rs | 32 +- core/src/types/key/mod.rs | 1 - core/src/types/masp.rs | 253 ++- core/src/types/mod.rs | 1 + core/src/types/storage.rs | 2 +- core/src/types/string_encoding.rs | 231 +++ core/src/types/time.rs | 42 +- core/src/types/token.rs | 165 +- core/src/types/uint.rs | 5 + ethereum_bridge/src/parameters.rs | 22 +- ethereum_bridge/src/test_utils.rs | 18 +- genesis/README.md | 153 ++ genesis/dev.toml | 247 --- genesis/e2e-tests-single-node.toml | 260 --- genesis/localnet/README.md | 66 + genesis/localnet/balances.toml | 110 ++ genesis/localnet/parameters.toml | 90 + .../src/pre-genesis/signed-transactions.toml | 126 ++ .../pre-genesis/unsigned-transactions.toml | 114 ++ .../pre-genesis/validator-0/transactions.toml | 44 + .../validator-0/validator-wallet.toml | 11 + genesis/localnet/src/pre-genesis/wallet.toml | 34 + genesis/localnet/tokens.toml | 22 + genesis/localnet/transactions.toml | 180 ++ genesis/localnet/validity-predicates.toml | 17 + genesis/starter/README.md | 71 + genesis/starter/balances.toml | 2 + genesis/starter/parameters.toml | 90 + genesis/starter/tokens.toml | 5 + genesis/starter/transactions.toml | 1 + genesis/starter/validity-predicates.toml | 22 + proof_of_stake/src/epoched.rs | 2 +- proof_of_stake/src/lib.rs | 371 +++-- proof_of_stake/src/tests.rs | 42 +- proof_of_stake/src/tests/state_machine.rs | 4 +- proof_of_stake/src/tests/state_machine_v2.rs | 4 +- sdk/Cargo.toml | 1 + sdk/src/args.rs | 38 + sdk/src/error.rs | 2 +- sdk/src/wallet/alias.rs | 73 +- sdk/src/wallet/mod.rs | 16 +- sdk/src/wallet/store.rs | 101 +- shared/Cargo.toml | 2 - shared/build.rs | 12 - .../ethereum_bridge/bridge_pool_vp.rs | 4 +- .../ledger/native_vp/ethereum_bridge/vp.rs | 4 +- shared/src/ledger/native_vp/ibc/mod.rs | 7 +- shared/src/ledger/pos/mod.rs | 22 +- shared/src/ledger/vp_host_fns.rs | 2 +- tests/src/e2e/eth_bridge_tests.rs | 8 +- tests/src/e2e/eth_bridge_tests/helpers.rs | 4 +- tests/src/e2e/setup.rs | 418 +++-- tests/src/integration/setup.rs | 138 +- tests/src/native_vp/eth_bridge_pool.rs | 4 +- tests/src/native_vp/pos.rs | 2 +- tests/src/vm_host_env/ibc.rs | 8 +- tx_prelude/src/proof_of_stake.rs | 3 +- 103 files changed, 7981 insertions(+), 4327 deletions(-) delete mode 100644 apps/src/bin/namada-client/cli.rs delete mode 100644 apps/src/lib/client/types.rs create mode 100644 apps/src/lib/config/genesis/chain.rs create mode 100644 apps/src/lib/config/genesis/templates.rs create mode 100644 apps/src/lib/config/genesis/toml_utils.rs create mode 100644 apps/src/lib/config/genesis/transactions.rs delete mode 100644 apps/src/lib/wallet/alias.rs delete mode 100644 apps/src/lib/wallet/cli_utils.rs create mode 100644 core/src/types/string_encoding.rs create mode 100644 genesis/README.md delete mode 100644 genesis/dev.toml delete mode 100644 genesis/e2e-tests-single-node.toml create mode 100644 genesis/localnet/README.md create mode 100644 genesis/localnet/balances.toml create mode 100644 genesis/localnet/parameters.toml create mode 100644 genesis/localnet/src/pre-genesis/signed-transactions.toml create mode 100644 genesis/localnet/src/pre-genesis/unsigned-transactions.toml create mode 100644 genesis/localnet/src/pre-genesis/validator-0/transactions.toml create mode 100644 genesis/localnet/src/pre-genesis/validator-0/validator-wallet.toml create mode 100644 genesis/localnet/src/pre-genesis/wallet.toml create mode 100644 genesis/localnet/tokens.toml create mode 100644 genesis/localnet/transactions.toml create mode 100644 genesis/localnet/validity-predicates.toml create mode 100644 genesis/starter/README.md create mode 100644 genesis/starter/balances.toml create mode 100644 genesis/starter/parameters.toml create mode 100644 genesis/starter/tokens.toml create mode 100644 genesis/starter/transactions.toml create mode 100644 genesis/starter/validity-predicates.toml delete mode 100644 shared/build.rs diff --git a/Cargo.lock b/Cargo.lock index 6bb244f7a5..725192ba5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4234,6 +4234,7 @@ version = "0.24.0" dependencies = [ "assert_matches", "async-trait", + "base58 0.2.0", "bimap", "borsh 1.0.0-alpha.4", "borsh-ext", diff --git a/Makefile b/Makefile index dc67c3c431..f535c04cff 100644 --- a/Makefile +++ b/Makefile @@ -54,16 +54,16 @@ build-test: $(cargo) +$(nightly) build --tests $(jobs) build-release: - NAMADA_DEV=false $(cargo) build $(jobs) --release --timings --package namada_apps --manifest-path Cargo.toml + $(cargo) build $(jobs) --release --timings --package namada_apps --manifest-path Cargo.toml build-debug: - NAMADA_DEV=false $(cargo) build --package namada_apps --manifest-path Cargo.toml + $(cargo) build --package namada_apps --manifest-path Cargo.toml install-release: - NAMADA_DEV=false $(cargo) install --path ./apps --locked + $(cargo) install --path ./apps --locked check-release: - NAMADA_DEV=false $(cargo) check --release --package namada_apps + $(cargo) check --release --package namada_apps package: build-release scripts/make-package.sh @@ -88,7 +88,7 @@ check-crates: clippy-wasm = $(cargo) +$(nightly) clippy --manifest-path $(wasm)/Cargo.toml --all-targets -- -D warnings clippy: - NAMADA_DEV=false $(cargo) +$(nightly) clippy $(jobs) --all-targets -- -D warnings && \ + $(cargo) +$(nightly) clippy $(jobs) --all-targets -- -D warnings && \ make -C $(wasms) clippy && \ make -C $(wasms_for_tests) clippy && \ $(foreach wasm,$(wasm_templates),$(clippy-wasm) && ) true @@ -103,7 +103,7 @@ tendermint: ./scripts/get_tendermint.sh install: cometbft - NAMADA_DEV=false $(cargo) install --path ./apps --locked + $(cargo) install --path ./apps --locked cometbft: ./scripts/get_cometbft.sh @@ -167,7 +167,7 @@ test-integration-save-proofs: # Run integration tests without specifiying any pre-built MASP proofs option test-integration-slow: RUST_BACKTRACE=$(RUST_BACKTRACE) \ - $(cargo) +$(nightly) test integration::$(TEST_FILTER) \ + $(cargo) +$(nightly) test integration::$(TEST_FILTER) --features integration \ -Z unstable-options \ -- \ -Z unstable-options --report-time diff --git a/README.md b/README.md index 18d481dc5f..ce85949f3e 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,6 @@ Guide. ```shell # Build the provided validity predicate and transaction wasm modules make build-wasm-scripts-docker - -# Development (debug) build Namada, which includes a validator and some default -# accounts, whose keys and addresses are available in the wallet -NAMADA_DEV=true make ``` ### Before submitting a PR, pls make sure to run the following diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 186b470439..9c6783a5c8 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -54,11 +54,11 @@ default = ["std", "abciplus"] mainnet = [ "namada/mainnet", ] -dev = ["namada/dev"] std = ["ed25519-consensus/std", "rand/std", "rand_core/std", "namada/std", "namada_sdk/std"] # for integration tests and test utilies -testing = ["dev", "namada_test_utils"] - +testing = ["namada_test_utils"] +benches = ["testing", "namada_test_utils"] +integration = [] abciplus = [ "namada/abciplus", "namada/tendermint-rpc", diff --git a/apps/build.rs b/apps/build.rs index 60735e7e4a..6ffb58476f 100644 --- a/apps/build.rs +++ b/apps/build.rs @@ -53,13 +53,4 @@ fn main() { // Tell Cargo that if the given file changes, to rerun this build script. println!("cargo:rerun-if-changed={}", PROTO_SRC); - - // Tell Cargo to build when the `NAMADA_DEV` env var changes - println!("cargo:rerun-if-env-changed=NAMADA_DEV"); - // Enable "dev" feature if `NAMADA_DEV` is trueish - if let Ok(dev) = env::var("NAMADA_DEV") { - if dev.to_ascii_lowercase().trim() == "true" { - println!("cargo:rustc-cfg=feature=\"dev\""); - } - } } diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/src/bin/namada-node/cli.rs b/apps/src/bin/namada-node/cli.rs index 6499a34e9e..7ee24251be 100644 --- a/apps/src/bin/namada-node/cli.rs +++ b/apps/src/bin/namada-node/cli.rs @@ -10,26 +10,31 @@ pub fn main() -> Result<()> { match cmd { cmds::NamadaNode::Ledger(sub) => match sub { cmds::Ledger::Run(cmds::LedgerRun(args)) => { - let wasm_dir = ctx.wasm_dir(); + let chain_ctx = ctx.take_chain_or_exit(); + let wasm_dir = chain_ctx.wasm_dir(); sleep_until(args.start_time); - ledger::run(ctx.config.ledger, wasm_dir); + ledger::run(chain_ctx.config.ledger, wasm_dir); } cmds::Ledger::RunUntil(cmds::LedgerRunUntil(args)) => { - let wasm_dir = ctx.wasm_dir(); + let mut chain_ctx = ctx.take_chain_or_exit(); + let wasm_dir = chain_ctx.wasm_dir(); sleep_until(args.time); - ctx.config.ledger.shell.action_at_height = + chain_ctx.config.ledger.shell.action_at_height = Some(args.action_at_height); - ledger::run(ctx.config.ledger, wasm_dir); + ledger::run(chain_ctx.config.ledger, wasm_dir); } cmds::Ledger::Reset(_) => { - ledger::reset(ctx.config.ledger) + let chain_ctx = ctx.take_chain_or_exit(); + ledger::reset(chain_ctx.config.ledger) .wrap_err("Failed to reset Namada node")?; } cmds::Ledger::DumpDb(cmds::LedgerDumpDb(args)) => { - ledger::dump_db(ctx.config.ledger, args); + let chain_ctx = ctx.take_chain_or_exit(); + ledger::dump_db(chain_ctx.config.ledger, args); } cmds::Ledger::RollBack(_) => { - ledger::rollback(ctx.config.ledger) + let chain_ctx = ctx.take_chain_or_exit(); + ledger::rollback(chain_ctx.config.ledger) .wrap_err("Failed to rollback the Namada node")?; } }, @@ -39,18 +44,18 @@ pub fn main() -> Result<()> { // In here, we just need to overwrite the default chain ID, in // case it's been already set to a different value if let Some(chain_id) = ctx.global_args.chain_id.as_ref() { - ctx.global_config.default_chain_id = chain_id.clone(); + ctx.global_config.default_chain_id = Some(chain_id.clone()); ctx.global_config .write(&ctx.global_args.base_dir) .unwrap_or_else(|err| { - eprintln!("Error writing global config: {}", err); + eprintln!("Error writing global config: {err}"); cli::safe_exit(1) }); + tracing::debug!( + "Generated config and set default chain ID to \ + {chain_id}" + ); } - tracing::debug!( - "Generated config and set default chain ID to {}", - &ctx.global_config.default_chain_id - ); } }, } diff --git a/apps/src/bin/namada/cli.rs b/apps/src/bin/namada/cli.rs index 0259ba525a..c7694117aa 100644 --- a/apps/src/bin/namada/cli.rs +++ b/apps/src/bin/namada/cli.rs @@ -63,7 +63,7 @@ fn handle_command(cmd: cli::cmds::Namada, raw_sub_cmd: String) -> Result<()> { fn handle_subcommand(program: &str, mut sub_args: Vec) -> Result<()> { let env_vars = env::vars_os(); - let cmd_name = if cfg!(feature = "dev") && env::var("CARGO").is_ok() { + let cmd_name = if env::var("CARGO").is_ok() { // When the command is ran from inside `cargo run`, we also want to // call the sub-command via `cargo run` to rebuild if necessary. // We do this by prepending the arguments with `cargo run` arguments. diff --git a/apps/src/lib/bench_utils.rs b/apps/src/lib/bench_utils.rs index db14551a76..5f58a93a58 100644 --- a/apps/src/lib/bench_utils.rs +++ b/apps/src/lib/bench_utils.rs @@ -167,7 +167,6 @@ impl Default for BenchShell { None, 50 * 1024 * 1024, // 50 kiB 50 * 1024 * 1024, // 50 kiB - address::nam(), ); shell diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 51e3b03a73..c7ddefef68 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -712,7 +712,7 @@ pub mod cmds { /// List all known payment addresses #[derive(Clone, Debug)] - pub struct MaspListPayAddrs; + pub struct MaspListPayAddrs(pub args::MaspListPayAddrs); impl SubCmd for MaspListPayAddrs { const CMD: &'static str = "list-addrs"; @@ -720,12 +720,13 @@ pub mod cmds { fn parse(matches: &ArgMatches) -> Option { matches .subcommand_matches(Self::CMD) - .map(|_matches| MaspListPayAddrs) + .map(|matches| Self(args::MaspListPayAddrs::parse(matches))) } fn def() -> App { App::new(Self::CMD) .about("Lists all payment addresses in the wallet") + .add_args::() } } @@ -903,7 +904,7 @@ pub mod cmds { /// List known addresses #[derive(Clone, Debug)] - pub struct AddressList; + pub struct AddressList(pub args::AddressList); impl SubCmd for AddressList { const CMD: &'static str = "list"; @@ -911,11 +912,13 @@ pub mod cmds { fn parse(matches: &ArgMatches) -> Option { matches .subcommand_matches(Self::CMD) - .map(|_matches| AddressList) + .map(|matches| Self(args::AddressList::parse(matches))) } fn def() -> App { - App::new(Self::CMD).about("List all known addresses.") + App::new(Self::CMD) + .about("List all known addresses.") + .add_args::() } } @@ -1959,6 +1962,8 @@ pub mod cmds { PkToTmAddress(PkToTmAddress), DefaultBaseDir(DefaultBaseDir), EpochSleep(EpochSleep), + ValidateGenesisTemplates(ValidateGenesisTemplates), + SignGenesisTx(SignGenesisTx), } impl SubCmd for Utils { @@ -1980,6 +1985,10 @@ pub mod cmds { let default_base_dir = SubCmd::parse(matches).map(Self::DefaultBaseDir); let epoch_sleep = SubCmd::parse(matches).map(Self::EpochSleep); + let validate_genesis_templates = + SubCmd::parse(matches).map(Self::ValidateGenesisTemplates); + let genesis_tx = + SubCmd::parse(matches).map(Self::SignGenesisTx); join_network .or(fetch_wasms) .or(validate_wasm) @@ -1988,6 +1997,8 @@ pub mod cmds { .or(pk_to_tm_address) .or(default_base_dir) .or(epoch_sleep) + .or(validate_genesis_templates) + .or(genesis_tx) }) } @@ -2002,6 +2013,8 @@ pub mod cmds { .subcommand(PkToTmAddress::def()) .subcommand(DefaultBaseDir::def()) .subcommand(EpochSleep::def()) + .subcommand(ValidateGenesisTemplates::def()) + .subcommand(SignGenesisTx::def()) .subcommand_required(true) .arg_required_else_help(true) } @@ -2109,6 +2122,44 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct ValidateGenesisTemplates(pub args::ValidateGenesisTemplates); + + impl SubCmd for ValidateGenesisTemplates { + const CMD: &'static str = "validate-genesis-templates"; + + fn parse(matches: &ArgMatches) -> Option { + matches.subcommand_matches(Self::CMD).map(|matches| { + Self(args::ValidateGenesisTemplates::parse(matches)) + }) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Validate genesis templates.") + .add_args::() + } + } + + #[derive(Clone, Debug)] + pub struct SignGenesisTx(pub args::SignGenesisTx); + + impl SubCmd for SignGenesisTx { + const CMD: &'static str = "sign-genesis-tx"; + + fn parse(matches: &ArgMatches) -> Option { + matches + .subcommand_matches(Self::CMD) + .map(|matches| Self(args::SignGenesisTx::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Sign genesis transaction(s).") + .add_args::() + } + } + /// Used as sub-commands (`SubCmd` instance) in `namadar` binary. #[derive(Clone, Debug)] pub enum EthBridgePool { @@ -2566,6 +2617,7 @@ pub mod args { use std::collections::HashMap; use std::convert::TryFrom; use std::env; + use std::net::SocketAddr; use std::path::PathBuf; use std::str::FromStr; @@ -2587,7 +2639,7 @@ pub mod args { use super::context::*; use super::utils::*; use super::{ArgGroup, ArgMatches}; - use crate::cli::context::FromContext; + use crate::client::utils::PRE_GENESIS_DIR; use crate::config::{self, Action, ActionAtHeight}; use crate::facade::tendermint::Timeout; use crate::facade::tendermint_config::net::Address as TendermintAddress; @@ -2708,6 +2760,7 @@ pub mod args { }), ); pub const GENESIS_PATH: Arg = arg("genesis-path"); + pub const GENESIS_TIME: Arg = arg("genesis-time"); pub const GENESIS_VALIDATOR: ArgOpt = arg("genesis-validator").opt(); pub const HALT_ACTION: ArgFlag = flag("halt"); @@ -2733,20 +2786,23 @@ pub mod args { arg("max-commission-rate-change"); pub const MAX_ETH_GAS: ArgOpt = arg_opt("max_eth-gas"); pub const MODE: ArgOpt = arg_opt("mode"); - pub const NET_ADDRESS: Arg = arg("net-address"); + pub const NET_ADDRESS: Arg = arg("net-address"); pub const NAMADA_START_TIME: ArgOpt = arg_opt("time"); pub const NO_CONVERSIONS: ArgFlag = flag("no-conversions"); pub const NUT: ArgFlag = flag("nut"); pub const OUT_FILE_PATH_OPT: ArgOpt = arg_opt("out-file-path"); + pub const OUTPUT: ArgOpt = arg_opt("output"); pub const OUTPUT_FOLDER_PATH: ArgOpt = arg_opt("output-folder-path"); pub const OWNER: Arg = arg("owner"); pub const OWNER_OPT: ArgOpt = OWNER.opt(); + pub const PATH: Arg = arg("path"); pub const PIN: ArgFlag = flag("pin"); pub const PORT_ID: ArgDefault = arg_default( "port-id", DefaultFn(|| PortId::from_str("transfer").unwrap()), ); + pub const PRE_GENESIS: ArgFlag = flag("pre-genesis"); pub const PROPOSAL_ETH: ArgFlag = flag("eth"); pub const PROPOSAL_PGF_STEWARD: ArgFlag = flag("pgf-stewards"); pub const PROPOSAL_PGF_FUNDING: ArgFlag = flag("pgf-funding"); @@ -2765,12 +2821,18 @@ pub mod args { pub const RAW_PUBLIC_KEY: Arg = arg("public-key"); pub const RAW_PUBLIC_KEY_OPT: ArgOpt = arg_opt("public-key"); + pub const RAW_SOURCE: Arg = arg("source"); pub const RECEIVER: Arg = arg("receiver"); pub const RELAYER: Arg
= arg("relayer"); pub const SAFE_MODE: ArgFlag = flag("safe-mode"); pub const SCHEME: ArgDefault = arg_default("scheme", DefaultFn(|| SchemeType::Ed25519)); + pub const SELF_BOND_AMOUNT: Arg = + arg("self-bond-amount"); pub const SENDER: Arg = arg("sender"); + pub const SIGNER: ArgOpt = arg_opt("signer"); + pub const SIGNING_KEY_OPT: ArgOpt = SIGNING_KEY.opt(); + pub const SIGNING_KEY: Arg = arg("signing-key"); pub const SIGNING_KEYS: ArgMulti = arg_multi("signing-keys"); pub const SIGNATURES: ArgMulti = arg_multi("signatures"); pub const SOURCE: Arg = arg("source"); @@ -2779,11 +2841,14 @@ pub mod args { pub const SOURCE_VALIDATOR: Arg = arg("source-validator"); pub const STORAGE_KEY: Arg = arg("storage-key"); pub const SUSPEND_ACTION: ArgFlag = flag("suspend"); + pub const TEMPLATES_PATH: Arg = arg("templates-path"); pub const TIMEOUT_HEIGHT: ArgOpt = arg_opt("timeout-height"); pub const TIMEOUT_SEC_OFFSET: ArgOpt = arg_opt("timeout-sec-offset"); pub const TM_ADDRESS: Arg = arg("tm-address"); pub const TOKEN_OPT: ArgOpt = TOKEN.opt(); pub const TOKEN: Arg = arg("token"); + pub const TRANSFER_FROM_SOURCE_AMOUNT: Arg = + arg("transfer-from-source-amount"); pub const TRANSFER_SOURCE: Arg = arg("source"); pub const TRANSFER_TARGET: Arg = arg("target"); pub const TX_HASH: Arg = arg("tx-hash"); @@ -3013,16 +3078,20 @@ pub mod args { impl CliToSdk> for EthereumBridgePool { fn to_sdk(self, ctx: &mut Context) -> EthereumBridgePool { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); EthereumBridgePool:: { nut: self.nut, - tx: self.tx.to_sdk(ctx), + tx, asset: self.asset, recipient: self.recipient, - sender: ctx.get(&self.sender), + sender: chain_ctx.get(&self.sender), amount: self.amount, fee_amount: self.fee_amount, - fee_payer: self.fee_payer.map(|fee_payer| ctx.get(&fee_payer)), - fee_token: ctx.get(&self.fee_token), + fee_payer: self + .fee_payer + .map(|fee_payer| chain_ctx.get(&fee_payer)), + fee_token: chain_ctx.get(&self.fee_token), code_path: self.code_path, } } @@ -3096,6 +3165,7 @@ pub mod args { impl CliToSdk> for RecommendBatch { fn to_sdk(self, ctx: &mut Context) -> RecommendBatch { + let chain_ctx = ctx.borrow_chain_or_exit(); RecommendBatch:: { query: self.query.to_sdk_ctxless(), max_gas: self.max_gas, @@ -3115,8 +3185,8 @@ pub mod args { .map(|(token, conversion_rate)| { let token_from_ctx = FromContext::
::new(token); - let address = ctx.get(&token_from_ctx); - let alias = token_from_ctx.into_raw(); + let address = chain_ctx.get(&token_from_ctx); + let alias = token_from_ctx.raw; ( address, BpConversionTableEntry { @@ -3500,7 +3570,7 @@ pub mod args { serialized_tx: self.serialized_tx.map(|path| { std::fs::read(path).expect("Expected a file at given path") }), - owner: ctx.get(&self.owner), + owner: ctx.borrow_chain_or_exit().get(&self.owner), } } } @@ -3558,13 +3628,15 @@ pub mod args { impl CliToSdk> for TxTransfer { fn to_sdk(self, ctx: &mut Context) -> TxTransfer { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); TxTransfer:: { - tx: self.tx.to_sdk(ctx), - source: ctx.get_cached(&self.source), - target: ctx.get(&self.target), - token: ctx.get(&self.token), + tx, + source: chain_ctx.get_cached(&self.source), + target: chain_ctx.get(&self.target), + token: chain_ctx.get(&self.token), amount: self.amount, - native_token: ctx.native_token.clone(), + native_token: chain_ctx.native_token.clone(), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -3606,11 +3678,13 @@ pub mod args { impl CliToSdk> for TxIbcTransfer { fn to_sdk(self, ctx: &mut Context) -> TxIbcTransfer { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); TxIbcTransfer:: { - tx: self.tx.to_sdk(ctx), - source: ctx.get(&self.source), + tx, + source: chain_ctx.get(&self.source), receiver: self.receiver, - token: ctx.get(&self.token), + token: chain_ctx.get(&self.token), amount: self.amount, port_id: self.port_id, channel_id: self.channel_id, @@ -3682,14 +3756,16 @@ pub mod args { impl CliToSdk> for TxInitAccount { fn to_sdk(self, ctx: &mut Context) -> TxInitAccount { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); TxInitAccount:: { - tx: self.tx.to_sdk(ctx), + tx, vp_code_path: self.vp_code_path, tx_code_path: self.tx_code_path, public_keys: self .public_keys .iter() - .map(|pk| ctx.get_cached(pk)) + .map(|pk| chain_ctx.get_cached(pk)) .collect(), threshold: self.threshold, } @@ -3735,19 +3811,27 @@ pub mod args { impl CliToSdk> for TxInitValidator { fn to_sdk(self, ctx: &mut Context) -> TxInitValidator { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); TxInitValidator:: { - tx: self.tx.to_sdk(ctx), + tx, scheme: self.scheme, account_keys: self .account_keys .iter() - .map(|x| ctx.get_cached(x)) + .map(|x| chain_ctx.get_cached(x)) .collect(), threshold: self.threshold, - consensus_key: self.consensus_key.map(|x| ctx.get_cached(&x)), - eth_cold_key: self.eth_cold_key.map(|x| ctx.get_cached(&x)), - eth_hot_key: self.eth_hot_key.map(|x| ctx.get_cached(&x)), - protocol_key: self.protocol_key.map(|x| ctx.get_cached(&x)), + consensus_key: self + .consensus_key + .map(|x| chain_ctx.get_cached(&x)), + eth_cold_key: self + .eth_cold_key + .map(|x| chain_ctx.get_cached(&x)), + eth_hot_key: self.eth_hot_key.map(|x| chain_ctx.get_cached(&x)), + protocol_key: self + .protocol_key + .map(|x| chain_ctx.get_cached(&x)), commission_rate: self.commission_rate, max_commission_rate_change: self.max_commission_rate_change, validator_vp_code_path: self @@ -3854,15 +3938,17 @@ pub mod args { impl CliToSdk> for TxUpdateAccount { fn to_sdk(self, ctx: &mut Context) -> TxUpdateAccount { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); TxUpdateAccount:: { - tx: self.tx.to_sdk(ctx), + tx, vp_code_path: self.vp_code_path, tx_code_path: self.tx_code_path, - addr: ctx.get(&self.addr), + addr: chain_ctx.get(&self.addr), public_keys: self .public_keys .iter() - .map(|pk| ctx.get_cached(pk)) + .map(|pk| chain_ctx.get_cached(pk)) .collect(), threshold: self.threshold, } @@ -3912,12 +3998,14 @@ pub mod args { impl CliToSdk> for Bond { fn to_sdk(self, ctx: &mut Context) -> Bond { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); Bond:: { - tx: self.tx.to_sdk(ctx), - validator: ctx.get(&self.validator), + tx, + validator: chain_ctx.get(&self.validator), amount: self.amount, - source: self.source.map(|x| ctx.get(&x)), - native_token: ctx.native_token.clone(), + source: self.source.map(|x| chain_ctx.get(&x)), + native_token: chain_ctx.native_token.clone(), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -3961,11 +4049,13 @@ pub mod args { impl CliToSdk> for Unbond { fn to_sdk(self, ctx: &mut Context) -> Unbond { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); Unbond:: { - tx: self.tx.to_sdk(ctx), - validator: ctx.get(&self.validator), + tx, + validator: chain_ctx.get(&self.validator), amount: self.amount, - source: self.source.map(|x| ctx.get(&x)), + source: self.source.map(|x| chain_ctx.get(&x)), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -4020,7 +4110,7 @@ pub mod args { ) -> UpdateStewardCommission { UpdateStewardCommission:: { tx: self.tx.to_sdk(ctx), - steward: ctx.get(&self.steward), + steward: ctx.borrow_chain_or_exit().get(&self.steward), commission: std::fs::read(self.commission).expect(""), tx_code_path: self.tx_code_path.to_path_buf(), } @@ -4056,7 +4146,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> ResignSteward { ResignSteward:: { tx: self.tx.to_sdk(ctx), - steward: ctx.get(&self.steward), + steward: ctx.borrow_chain_or_exit().get(&self.steward), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -4082,11 +4172,13 @@ pub mod args { impl CliToSdk> for Redelegate { fn to_sdk(self, ctx: &mut Context) -> Redelegate { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); Redelegate:: { - tx: self.tx.to_sdk(ctx), - src_validator: ctx.get(&self.src_validator), - dest_validator: ctx.get(&self.dest_validator), - owner: ctx.get(&self.owner), + tx, + src_validator: chain_ctx.get(&self.src_validator), + dest_validator: chain_ctx.get(&self.dest_validator), + owner: chain_ctx.get(&self.owner), amount: self.amount, tx_code_path: self.tx_code_path.to_path_buf(), } @@ -4145,7 +4237,7 @@ pub mod args { is_offline: self.is_offline, is_pgf_stewards: self.is_pgf_stewards, is_pgf_funding: self.is_pgf_funding, - native_token: ctx.native_token.clone(), + native_token: ctx.borrow_chain_or_exit().native_token.clone(), tx_code_path: self.tx_code_path, } } @@ -4231,7 +4323,7 @@ pub mod args { tx: self.tx.to_sdk(ctx), proposal_id: self.proposal_id, vote: self.vote, - voter: ctx.get(&self.voter), + voter: ctx.borrow_chain_or_exit().get(&self.voter), is_offline: self.is_offline, proposal_data: self.proposal_data.map(|path| { std::fs::read(path) @@ -4301,9 +4393,11 @@ pub mod args { impl CliToSdk> for RevealPk { fn to_sdk(self, ctx: &mut Context) -> RevealPk { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); RevealPk:: { - tx: self.tx.to_sdk(ctx), - public_key: ctx.get_cached(&self.public_key), + tx, + public_key: chain_ctx.get_cached(&self.public_key), } } } @@ -4464,10 +4558,12 @@ pub mod args { impl CliToSdk> for Withdraw { fn to_sdk(self, ctx: &mut Context) -> Withdraw { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); Withdraw:: { - tx: self.tx.to_sdk(ctx), - validator: ctx.get(&self.validator), - source: self.source.map(|x| ctx.get(&x)), + tx, + validator: chain_ctx.get(&self.validator), + source: self.source.map(|x| chain_ctx.get(&x)), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -4502,7 +4598,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QueryConversions { QueryConversions:: { query: self.query.to_sdk(ctx), - token: self.token.map(|x| ctx.get(&x)), + token: self.token.map(|x| ctx.borrow_chain_or_exit().get(&x)), epoch: self.epoch, } } @@ -4539,7 +4635,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QueryAccount { QueryAccount:: { query: self.query.to_sdk(ctx), - owner: ctx.get(&self.owner), + owner: ctx.borrow_chain_or_exit().get(&self.owner), } } } @@ -4563,10 +4659,12 @@ pub mod args { impl CliToSdk> for QueryBalance { fn to_sdk(self, ctx: &mut Context) -> QueryBalance { + let query = self.query.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); QueryBalance:: { - query: self.query.to_sdk(ctx), - owner: self.owner.map(|x| ctx.get_cached(&x)), - token: self.token.map(|x| ctx.get(&x)), + query, + owner: self.owner.map(|x| chain_ctx.get_cached(&x)), + token: self.token.map(|x| chain_ctx.get(&x)), no_conversions: self.no_conversions, } } @@ -4608,10 +4706,12 @@ pub mod args { impl CliToSdk> for QueryTransfers { fn to_sdk(self, ctx: &mut Context) -> QueryTransfers { + let query = self.query.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); QueryTransfers:: { - query: self.query.to_sdk(ctx), - owner: self.owner.map(|x| ctx.get_cached(&x)), - token: self.token.map(|x| ctx.get(&x)), + query, + owner: self.owner.map(|x| chain_ctx.get_cached(&x)), + token: self.token.map(|x| chain_ctx.get(&x)), } } } @@ -4641,10 +4741,12 @@ pub mod args { impl CliToSdk> for QueryBonds { fn to_sdk(self, ctx: &mut Context) -> QueryBonds { + let query = self.query.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); QueryBonds:: { - query: self.query.to_sdk(ctx), - owner: self.owner.map(|x| ctx.get(&x)), - validator: self.validator.map(|x| ctx.get(&x)), + query, + owner: self.owner.map(|x| chain_ctx.get(&x)), + validator: self.validator.map(|x| chain_ctx.get(&x)), } } } @@ -4680,7 +4782,9 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QueryBondedStake { QueryBondedStake:: { query: self.query.to_sdk(ctx), - validator: self.validator.map(|x| ctx.get(&x)), + validator: self + .validator + .map(|x| ctx.borrow_chain_or_exit().get(&x)), epoch: self.epoch, } } @@ -4714,7 +4818,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QueryValidatorState { QueryValidatorState:: { query: self.query.to_sdk(ctx), - validator: ctx.get(&self.validator), + validator: ctx.borrow_chain_or_exit().get(&self.validator), epoch: self.epoch, } } @@ -4752,7 +4856,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> CommissionRateChange { CommissionRateChange:: { tx: self.tx.to_sdk(ctx), - validator: ctx.get(&self.validator), + validator: ctx.borrow_chain_or_exit().get(&self.validator), rate: self.rate, tx_code_path: self.tx_code_path.to_path_buf(), } @@ -4790,7 +4894,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> TxUnjailValidator { TxUnjailValidator:: { tx: self.tx.to_sdk(ctx), - validator: ctx.get(&self.validator), + validator: ctx.borrow_chain_or_exit().get(&self.validator), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -4822,7 +4926,7 @@ pub mod args { SignTx:: { tx: self.tx.to_sdk(ctx), tx_data: std::fs::read(self.tx_data).expect(""), - owner: ctx.get(&self.owner), + owner: ctx.borrow_chain_or_exit().get(&self.owner), } } } @@ -4857,11 +4961,13 @@ pub mod args { self, ctx: &mut Context, ) -> GenIbcShieldedTransafer { + let query = self.query.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); GenIbcShieldedTransafer:: { query: self.query.to_sdk(ctx), output_folder: self.output_folder, - target: ctx.get(&self.target), - token: ctx.get(&self.token), + target: chain_ctx.get(&self.target), + token: chain_ctx.get(&self.token), amount: self.amount, port_id: self.port_id, channel_id: self.channel_id, @@ -4914,7 +5020,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QueryCommissionRate { QueryCommissionRate:: { query: self.query.to_sdk(ctx), - validator: ctx.get(&self.validator), + validator: ctx.borrow_chain_or_exit().get(&self.validator), epoch: self.epoch, } } @@ -4948,7 +5054,9 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QuerySlashes { QuerySlashes:: { query: self.query.to_sdk(ctx), - validator: self.validator.map(|x| ctx.get(&x)), + validator: self + .validator + .map(|x| ctx.borrow_chain_or_exit().get(&x)), } } } @@ -4989,7 +5097,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QueryDelegations { QueryDelegations:: { query: self.query.to_sdk(ctx), - owner: ctx.get(&self.owner), + owner: ctx.borrow_chain_or_exit().get(&self.owner), } } } @@ -5062,6 +5170,7 @@ pub mod args { impl CliToSdk> for Tx { fn to_sdk(self, ctx: &mut Context) -> Tx { + let ctx = ctx.borrow_mut_chain_or_exit(); Tx:: { dry_run: self.dry_run, dry_run_wrapper: self.dry_run_wrapper, @@ -5308,11 +5417,13 @@ pub mod args { let alias = ALIAS.parse(matches); let alias_force = ALIAS_FORCE.parse(matches); let value = MASP_VALUE.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); Self { alias, alias_force, value, + is_pre_genesis, unsafe_dont_encrypt, } } @@ -5331,6 +5442,10 @@ pub mod args { .def() .help("A spending key, viewing key, or payment address."), ) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) .arg(UNSAFE_DONT_ENCRYPT.def().help( "UNSAFE: Do not encrypt the keypair. Do not use this for keys \ used in a live network.", @@ -5342,10 +5457,12 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); let alias_force = ALIAS_FORCE.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); Self { alias, alias_force, + is_pre_genesis, unsafe_dont_encrypt, } } @@ -5359,6 +5476,10 @@ pub mod args { .arg(ALIAS_FORCE.def().help( "Override the alias without confirmation if it already exists.", )) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) .arg(UNSAFE_DONT_ENCRYPT.def().help( "UNSAFE: Do not encrypt the keypair. Do not use this for keys \ used in a live network.", @@ -5368,11 +5489,35 @@ pub mod args { impl CliToSdk> for MaspPayAddrGen { fn to_sdk(self, ctx: &mut Context) -> MaspPayAddrGen { + use namada_sdk::wallet::Wallet; + + use crate::wallet::CliWalletUtils; + + let find_viewing_key = |w: &mut Wallet| { + w.find_viewing_key(&self.viewing_key.raw) + .map(Clone::clone) + .unwrap_or_else(|_| { + eprintln!( + "Unknown viewing key {}", + self.viewing_key.raw + ); + safe_exit(1) + }) + }; + let viewing_key = if self.is_pre_genesis || ctx.chain.is_none() { + let wallet_path = + ctx.global_args.base_dir.join(PRE_GENESIS_DIR); + let mut wallet = crate::wallet::load_or_new(&wallet_path); + find_viewing_key(&mut wallet) + } else { + find_viewing_key(&mut ctx.borrow_mut_chain_or_exit().wallet) + }; MaspPayAddrGen:: { alias: self.alias, alias_force: self.alias_force, - viewing_key: ctx.get_cached(&self.viewing_key), + viewing_key, pin: self.pin, + is_pre_genesis: self.is_pre_genesis, } } } @@ -5383,11 +5528,13 @@ pub mod args { let alias_force = ALIAS_FORCE.parse(matches); let viewing_key = VIEWING_KEY.parse(matches); let pin = PIN.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); Self { alias, alias_force, viewing_key, pin, + is_pre_genesis, } } @@ -5405,6 +5552,10 @@ pub mod args { "Require that the single transaction to this address be \ pinned.", )) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) } } @@ -5459,12 +5610,14 @@ pub mod args { let scheme = SCHEME.parse(matches); let alias = ALIAS_OPT.parse(matches); let alias_force = ALIAS_FORCE.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); let derivation_path = HD_WALLET_DERIVATION_PATH_OPT.parse(matches); Self { scheme, alias, alias_force, + is_pre_genesis, unsafe_dont_encrypt, derivation_path, } @@ -5483,6 +5636,10 @@ pub mod args { .arg(ALIAS_FORCE.def().help( "Override the alias without confirmation if it already exists.", )) + .arg(PRE_GENESIS.def().help( + "Generate a key for pre-genesis, instead of for the current \ + chain, if any.", + )) .arg(UNSAFE_DONT_ENCRYPT.def().help( "UNSAFE: Do not encrypt the keypair. Do not use this for keys \ used in a live network.", @@ -5504,12 +5661,14 @@ pub mod args { let public_key = RAW_PUBLIC_KEY_OPT.parse(matches); let alias = ALIAS_OPT.parse(matches); let value = VALUE.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_show_secret = UNSAFE_SHOW_SECRET.parse(matches); Self { public_key, alias, value, + is_pre_genesis, unsafe_show_secret, } } @@ -5532,6 +5691,10 @@ pub mod args { .def() .help("A public key or alias associated with the keypair."), ) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) .arg( UNSAFE_SHOW_SECRET .def() @@ -5544,9 +5707,11 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); let unsafe_show_secret = UNSAFE_SHOW_SECRET.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); Self { alias, unsafe_show_secret, + is_pre_genesis, } } @@ -5557,21 +5722,31 @@ pub mod args { .def() .help("UNSAFE: Print the spending key values."), ) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current \ + chain, if any.", + )) } } impl Args for MaspKeysList { fn parse(matches: &ArgMatches) -> Self { let decrypt = DECRYPT.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_show_secret = UNSAFE_SHOW_SECRET.parse(matches); Self { decrypt, + is_pre_genesis, unsafe_show_secret, } } fn def(app: App) -> App { app.arg(DECRYPT.def().help("Decrypt keys that are encrypted.")) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current \ + chain, if any.", + )) .arg( UNSAFE_SHOW_SECRET .def() @@ -5580,18 +5755,38 @@ pub mod args { } } + impl Args for MaspListPayAddrs { + fn parse(matches: &ArgMatches) -> Self { + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { is_pre_genesis } + } + + fn def(app: App) -> App { + app.arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) + } + } + impl Args for KeyList { fn parse(matches: &ArgMatches) -> Self { let decrypt = DECRYPT.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_show_secret = UNSAFE_SHOW_SECRET.parse(matches); Self { decrypt, + is_pre_genesis, unsafe_show_secret, } } fn def(app: App) -> App { app.arg(DECRYPT.def().help("Decrypt keys that are encrypted.")) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current \ + chain, if any.", + )) .arg( UNSAFE_SHOW_SECRET .def() @@ -5603,14 +5798,21 @@ pub mod args { impl Args for KeyExport { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); - - Self { alias } + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { + alias, + is_pre_genesis, + } } fn def(app: App) -> App { app.arg( ALIAS.def().help("The alias of the key you wish to export."), ) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) } } @@ -5618,7 +5820,12 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS_OPT.parse(matches); let address = RAW_ADDRESS_OPT.parse(matches); - Self { alias, address } + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { + alias, + address, + is_pre_genesis, + } } fn def(app: App) -> App { @@ -5632,6 +5839,10 @@ pub mod args { .def() .help("The bech32m encoded address string."), ) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) .group( ArgGroup::new("find_flags") .args([ALIAS_OPT.name, RAW_ADDRESS_OPT.name]) @@ -5640,15 +5851,31 @@ pub mod args { } } + impl Args for AddressList { + fn parse(matches: &ArgMatches) -> Self { + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { is_pre_genesis } + } + + fn def(app: App) -> App { + app.arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) + } + } + impl Args for AddressAdd { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); let alias_force = ALIAS_FORCE.parse(matches); let address = RAW_ADDRESS.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); Self { alias, alias_force, address, + is_pre_genesis, } } @@ -5666,6 +5893,10 @@ pub mod args { .def() .help("The bech32m encoded address string."), ) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) } } @@ -5675,6 +5906,7 @@ pub mod args { pub genesis_validator: Option, pub pre_genesis_path: Option, pub dont_prefetch_wasm: bool, + pub allow_duplicate_ip: bool, } impl Args for JoinNetwork { @@ -5683,11 +5915,13 @@ pub mod args { let genesis_validator = GENESIS_VALIDATOR.parse(matches); let pre_genesis_path = PRE_GENESIS_PATH.parse(matches); let dont_prefetch_wasm = DONT_PREFETCH_WASM.parse(matches); + let allow_duplicate_ip = ALLOW_DUPLICATE_IP.parse(matches); Self { chain_id, genesis_validator, pre_genesis_path, dont_prefetch_wasm, + allow_duplicate_ip, } } @@ -5699,6 +5933,10 @@ pub mod args { .arg(DONT_PREFETCH_WASM.def().help( "Do not pre-fetch WASM.", )) + .arg(ALLOW_DUPLICATE_IP.def().help( + "Toggle to disable guard against peers connecting from the \ + same IP. This option shouldn't be used in mainnet.", + )) } } @@ -5772,48 +6010,41 @@ pub mod args { #[derive(Clone, Debug)] pub struct InitNetwork { - pub genesis_path: PathBuf, + pub templates_path: PathBuf, pub wasm_checksums_path: PathBuf, pub chain_id_prefix: ChainIdPrefix, - pub unsafe_dont_encrypt: bool, + pub genesis_time: DateTimeUtc, pub consensus_timeout_commit: Timeout, - pub localhost: bool, - pub allow_duplicate_ip: bool, pub dont_archive: bool, pub archive_dir: Option, } impl Args for InitNetwork { fn parse(matches: &ArgMatches) -> Self { - let genesis_path = GENESIS_PATH.parse(matches); + let templates_path = TEMPLATES_PATH.parse(matches); let wasm_checksums_path = WASM_CHECKSUMS_PATH.parse(matches); let chain_id_prefix = CHAIN_ID_PREFIX.parse(matches); - let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); + let genesis_time = GENESIS_TIME.parse(matches); let consensus_timeout_commit = CONSENSUS_TIMEOUT_COMMIT.parse(matches); - let localhost = LOCALHOST.parse(matches); - let allow_duplicate_ip = ALLOW_DUPLICATE_IP.parse(matches); let dont_archive = DONT_ARCHIVE.parse(matches); let archive_dir = ARCHIVE_DIR.parse(matches); Self { - genesis_path, + templates_path, wasm_checksums_path, chain_id_prefix, - unsafe_dont_encrypt, + genesis_time, consensus_timeout_commit, - localhost, - allow_duplicate_ip, dont_archive, archive_dir, } } fn def(app: App) -> App { - app.arg( - GENESIS_PATH.def().help( - "Path to the preliminary genesis configuration file.", - ), - ) + app.arg(TEMPLATES_PATH.def().help( + "Path to the directory with genesis templates to be used to \ + initialize the network.", + )) .arg( WASM_CHECKSUMS_PATH .def() @@ -5823,22 +6054,14 @@ pub mod args { "The chain ID prefix. Up to 19 alphanumeric, '.', '-' or '_' \ characters.", )) - .arg(UNSAFE_DONT_ENCRYPT.def().help( - "UNSAFE: Do not encrypt the generated keypairs. Do not use \ - this for keys used in a live network.", + .arg(GENESIS_TIME.def().help( + "The start time of the network in RFC 3339 and ISO 8601 \ + format. For example: \"2021-12-31T00:00:00Z\".", )) .arg(CONSENSUS_TIMEOUT_COMMIT.def().help( "The Tendermint consensus timeout_commit configuration as \ e.g. `1s` or `1000ms`. Defaults to 10 seconds.", )) - .arg(LOCALHOST.def().help( - "Use localhost address for P2P and RPC connections for the \ - validators ledger", - )) - .arg(ALLOW_DUPLICATE_IP.def().help( - "Toggle to disable guard against peers connecting from the \ - same IP. This option shouldn't be used in mainnet.", - )) .arg( DONT_ARCHIVE .def() @@ -5853,16 +6076,20 @@ pub mod args { #[derive(Clone, Debug)] pub struct InitGenesisValidator { + pub source: String, pub alias: String, pub commission_rate: Dec, pub max_commission_rate_change: Dec, - pub net_address: String, + pub net_address: SocketAddr, pub unsafe_dont_encrypt: bool, pub key_scheme: SchemeType, + pub transfer_from_source_amount: token::DenominatedAmount, + pub self_bond_amount: token::DenominatedAmount, } impl Args for InitGenesisValidator { fn parse(matches: &ArgMatches) -> Self { + let source = RAW_SOURCE.parse(matches); let alias = ALIAS.parse(matches); let commission_rate = COMMISSION_RATE.parse(matches); let max_commission_rate_change = @@ -5870,40 +6097,117 @@ pub mod args { let net_address = NET_ADDRESS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); let key_scheme = SCHEME.parse(matches); + // The denomination validation is handled by validating genesis + // files later. + // At this stage, we treat amounts as opaque and pass them on + // verbatim. + let transfer_from_source_amount = + TRANSFER_FROM_SOURCE_AMOUNT.parse(matches); + // this must be an amount of native tokens + let self_bond_amount = SELF_BOND_AMOUNT.parse(matches); Self { + source, alias, net_address, unsafe_dont_encrypt, key_scheme, commission_rate, max_commission_rate_change, + transfer_from_source_amount, + self_bond_amount, } } fn def(app: App) -> App { - app.arg(ALIAS.def().help("The validator address alias.")) - .arg(NET_ADDRESS.def().help( - "Static {host:port} of your validator node's P2P address. \ - Namada uses port `26656` for P2P connections by default, \ - but you can configure a different value.", - )) - .arg(COMMISSION_RATE.def().help( - "The commission rate charged by the validator for \ - delegation rewards. This is a required parameter.", - )) - .arg(MAX_COMMISSION_RATE_CHANGE.def().help( - "The maximum change per epoch in the commission rate \ - charged by the validator for delegation rewards. This is \ - a required parameter.", - )) - .arg(UNSAFE_DONT_ENCRYPT.def().help( - "UNSAFE: Do not encrypt the generated keypairs. Do not \ - use this for keys used in a live network.", - )) - .arg(SCHEME.def().help( - "The key scheme/type used for the validator keys. \ - Currently supports ed25519 and secp256k1.", - )) + app.arg(RAW_SOURCE.def().help( + "The source key for native token transfer from the \ + `balances.toml` genesis template.", + )) + .arg(ALIAS.def().help("The validator address alias.")) + .arg(NET_ADDRESS.def().help( + "Static {host:port} of your validator node's P2P address. \ + Namada uses port `26656` for P2P connections by default, but \ + you can configure a different value.", + )) + .arg(COMMISSION_RATE.def().help( + "The commission rate charged by the validator for delegation \ + rewards. This is a required parameter.", + )) + .arg(MAX_COMMISSION_RATE_CHANGE.def().help( + "The maximum change per epoch in the commission rate charged \ + by the validator for delegation rewards. This is a required \ + parameter.", + )) + .arg(UNSAFE_DONT_ENCRYPT.def().help( + "UNSAFE: Do not encrypt the generated keypairs. Do not use \ + this for keys used in a live network.", + )) + .arg(SCHEME.def().help( + "The key scheme/type used for the validator keys. Currently \ + supports ed25519 and secp256k1.", + )) + .arg(TRANSFER_FROM_SOURCE_AMOUNT.def().help( + "The amount of native token to transfer into the validator \ + account from the `--source`. To self-bond some tokens to the \ + validator at genesis, specify `--self-bond-amount`.", + )) + .arg( + SELF_BOND_AMOUNT + .def() + .help( + "The amount of native token to self-bond in PoS. \ + Because this amount will be bonded from the \ + validator's account, it must be lower or equal to \ + the amount specified in \ + `--transfer-from-source-amount`.", + ) + .requires(TRANSFER_FROM_SOURCE_AMOUNT.name), + ) + } + } + + #[derive(Clone, Debug)] + pub struct ValidateGenesisTemplates { + /// Templates dir + pub path: PathBuf, + } + + impl Args for ValidateGenesisTemplates { + fn parse(matches: &ArgMatches) -> Self { + let path = PATH.parse(matches); + Self { path } + } + + fn def(app: App) -> App { + app.arg( + PATH.def() + .help("Path to the directory with the template files."), + ) + } + } + + #[derive(Clone, Debug)] + pub struct SignGenesisTx { + pub path: PathBuf, + pub output: Option, + } + + impl Args for SignGenesisTx { + fn parse(matches: &ArgMatches) -> Self { + let path = PATH.parse(matches); + let output = OUTPUT.parse(matches); + Self { path, output } + } + + fn def(app: App) -> App { + app.arg( + PATH.def() + .help("Path to the unsigned transactions TOML file."), + ) + .arg(OUTPUT.def().help( + "Save the output to a TOML file. When not supplied, the \ + signed transactions will be printed to stdout instead.", + )) } } } diff --git a/apps/src/lib/cli/client.rs b/apps/src/lib/cli/client.rs index ec60584642..a465d585ce 100644 --- a/apps/src/lib/cli/client.rs +++ b/apps/src/lib/cli/client.rs @@ -538,6 +538,12 @@ impl CliApi { let namada = ctx.to_sdk(&client, io); rpc::epoch_sleep(&namada, args).await; } + Utils::ValidateGenesisTemplates( + ValidateGenesisTemplates(args ) + ) => utils::validate_genesis_templates(global_args, args), + Utils::SignGenesisTx(SignGenesisTx(args)) => { + utils::sign_genesis_tx(global_args, args) + } }, } Ok(()) diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index 64401984ce..494400de16 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -20,13 +20,11 @@ use namada_sdk::wallet::Wallet; use namada_sdk::{Namada, NamadaImpl}; use super::args; -#[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}; use crate::wallet::CliWalletUtils; -use crate::wasm_loader; +use crate::{wallet, wasm_loader}; +use crate::cli::utils; /// Env. var to set chain ID const ENV_VAR_CHAIN_ID: &str = "NAMADA_CHAIN_ID"; @@ -74,10 +72,17 @@ pub type WalletBalanceOwner = FromContext; pub struct Context { /// Global arguments pub global_args: args::Global, - /// The wallet - pub wallet: Wallet, /// The global configuration pub global_config: GlobalConfig, + /// Chain-specific context, if any chain is configured in `global_config` + pub chain: Option, +} + +/// Command execution context with chain-specific data +#[derive(Debug)] +pub struct ChainContext { + /// The wallet + pub wallet: Wallet, /// The ledger configuration for a specific chain ID pub config: Config, /// The context fr shielded operations @@ -89,89 +94,109 @@ pub struct Context { impl Context { pub fn new(global_args: args::Global) -> Result { let global_config = read_or_try_new_global_config(&global_args); - tracing::debug!("Chain ID: {}", global_config.default_chain_id); - - let mut config = Config::load( - &global_args.base_dir, - &global_config.default_chain_id, - None, - ); - - let chain_dir = global_args - .base_dir - .join(global_config.default_chain_id.as_str()); - let genesis_file_path = global_args - .base_dir - .join(format!("{}.toml", global_config.default_chain_id.as_str())); - // 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, - ) - } - }; - match global_args.wasm_dir.as_ref() { - Some(wasm_dir) => { - config.wasm_dir = wasm_dir.clone(); - } - None => { - if let Ok(wasm_dir) = env::var(ENV_VAR_WASM_DIR) { - let wasm_dir: PathBuf = wasm_dir.into(); - config.wasm_dir = wasm_dir; + let chain = match global_config.default_chain_id.as_ref() { + Some(default_chain_id) => { + tracing::info!("Default chain ID: {default_chain_id}"); + let mut config = + Config::load(&global_args.base_dir, default_chain_id, None); + let chain_dir = + global_args.base_dir.join(default_chain_id.as_str()); + let genesis = + genesis::chain::Finalized::read_toml_files(&chain_dir) + .expect("Missing genesis files"); + let native_token = genesis.get_native_token().clone(); + let wallet = if wallet::exists(&chain_dir) { + wallet::load(&chain_dir).unwrap() + } else { + panic!( + "Could not find wallet at {}.", + chain_dir.to_string_lossy() + ); + }; + + // 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(); + } + None => { + if let Ok(wasm_dir) = env::var(ENV_VAR_WASM_DIR) { + let wasm_dir: PathBuf = wasm_dir.into(); + config.wasm_dir = wasm_dir; + } + } } + Some(ChainContext { + wallet, + config, + shielded: FsShieldedUtils::new(chain_dir), + native_token, + }) } - } + None => None, + }; + Ok(Self { global_args, - wallet, global_config, - config, - shielded: FsShieldedUtils::new(chain_dir), - native_token, + chain, }) } + /// Try to take the chain context, or exit the process with an error if no + /// chain is configured. + pub fn take_chain_or_exit(self) -> ChainContext { + self.chain + .unwrap_or_else(|| safe_exit_on_missing_chain_context()) + } + + /// Try to borrow chain context, or exit the process with an error if no + /// chain is configured. + pub fn borrow_chain_or_exit(&self) -> &ChainContext { + self.chain + .as_ref() + .unwrap_or_else(|| safe_exit_on_missing_chain_context()) + } + + /// Try to borrow mutably chain context, or exit the process with an error + /// if no chain is configured. + pub fn borrow_mut_chain_or_exit(&mut self) -> &mut ChainContext { + self.chain + .as_mut() + .unwrap_or_else(|| safe_exit_on_missing_chain_context()) + } + /// Make an implementation of Namada from this object and parameters. pub fn to_sdk<'a, C, IO>( &'a mut self, client: &'a C, io: &'a IO, ) -> impl Namada - where - C: namada::ledger::queries::Client + Sync, - IO: Io, + where + C: namada::ledger::queries::Client + Sync, + IO: Io, { + let chain_ctx = self.borrow_mut_chain_or_exit(); NamadaImpl::native_new( client, - &mut self.wallet, - &mut self.shielded, + &mut chain_ctx.wallet, + &mut chain_ctx.shielded, io, - self.native_token.clone(), + chain_ctx.native_token.clone(), ) } +} + +fn safe_exit_on_missing_chain_context() -> ! { + eprintln!( + "No chain is configured. You may need to run `namada client utils \ + join-network` command." + ); + utils::safe_exit(1) +} +impl ChainContext { /// Parse and/or look-up the value from the context. pub fn get(&self, from_context: &FromContext) -> T where @@ -273,7 +298,7 @@ pub fn read_or_try_new_global_config( /// Argument that can be given raw or found in the [`Context`]. #[derive(Debug, Clone)] pub struct FromContext { - raw: String, + pub(crate) raw: String, phantom: PhantomData, } diff --git a/apps/src/lib/cli/wallet.rs b/apps/src/lib/cli/wallet.rs index 14b9ab9d9a..6d949ac462 100644 --- a/apps/src/lib/cli/wallet.rs +++ b/apps/src/lib/cli/wallet.rs @@ -23,7 +23,8 @@ use crate::cli; use crate::cli::api::CliApi; use crate::cli::args::CliToSdk; use crate::cli::{args, cmds, Context}; -use crate::wallet::{read_and_confirm_encryption_password, CliWalletUtils}; +use crate::client::utils::PRE_GENESIS_DIR; +use crate::wallet::{self, read_and_confirm_encryption_password, CliWalletUtils}; impl CliApi { pub fn handle_wallet_command( @@ -34,57 +35,57 @@ impl CliApi { match cmd { cmds::NamadaWallet::Key(sub) => match sub { cmds::WalletKey::Restore(cmds::KeyRestore(args)) => { - key_and_address_restore(&mut ctx.wallet, io, args) + key_and_address_restore(ctx, io, args) } cmds::WalletKey::Gen(cmds::KeyGen(args)) => { - key_and_address_gen(&mut ctx.wallet, io, &mut OsRng, args) + key_and_address_gen(ctx, io, &mut OsRng, args) } cmds::WalletKey::Find(cmds::KeyFind(args)) => { - key_find(&mut ctx.wallet, io, args) + key_find(ctx, io, args) } cmds::WalletKey::List(cmds::KeyList(args)) => { - key_list(&mut ctx.wallet, io, args) + key_list( ctx, io, args) } cmds::WalletKey::Export(cmds::Export(args)) => { - key_export(&mut ctx.wallet, io, args) + key_export( ctx, io, args) } }, cmds::NamadaWallet::Address(sub) => match sub { cmds::WalletAddress::Gen(cmds::AddressGen(args)) => { - key_and_address_gen(&mut ctx.wallet, io, &mut OsRng, args) + key_and_address_gen(ctx, io, &mut OsRng, args) } cmds::WalletAddress::Restore(cmds::AddressRestore(args)) => { - key_and_address_restore(&mut ctx.wallet, io, args) + key_and_address_restore(&mut ctx.borrow_mut_chain_or_exit().wallet, io, args) } cmds::WalletAddress::Find(cmds::AddressOrAliasFind(args)) => { - address_or_alias_find(&mut ctx.wallet, io, args) + address_or_alias_find(ctx, io, args) } - cmds::WalletAddress::List(cmds::AddressList) => { - address_list(&mut ctx.wallet, io) + cmds::WalletAddress::List(cmds::AddressList(args)) => { + address_list(ctx, io, args) } cmds::WalletAddress::Add(cmds::AddressAdd(args)) => { - address_add(&mut ctx.wallet, io, args) + address_add(ctx, io, args) } }, cmds::NamadaWallet::Masp(sub) => match sub { cmds::WalletMasp::GenSpendKey(cmds::MaspGenSpendKey(args)) => { - spending_key_gen(&mut ctx.wallet, io, args) + spending_key_gen(ctx, io, args) } cmds::WalletMasp::GenPayAddr(cmds::MaspGenPayAddr(args)) => { let args = args.to_sdk(&mut ctx); - payment_address_gen(&mut ctx.wallet, io, args) + payment_address_gen(&mut ctx.borrow_mut_chain_or_exit().wallet, io, args) } cmds::WalletMasp::AddAddrKey(cmds::MaspAddAddrKey(args)) => { - address_key_add(&mut ctx.wallet, io, args) - } - cmds::WalletMasp::ListPayAddrs(cmds::MaspListPayAddrs) => { - payment_addresses_list(&mut ctx.wallet, io) + address_key_add(ctx, io, args) } + cmds::WalletMasp::ListPayAddrs(cmds::MaspListPayAddrs( + args, + )) => payment_addresses_list(ctx, io, args), cmds::WalletMasp::ListKeys(cmds::MaspListKeys(args)) => { - spending_keys_list(&mut ctx.wallet, io, args) + spending_keys_list(ctx, io, args) } cmds::WalletMasp::FindAddrKey(cmds::MaspFindAddrKey(args)) => { - address_key_find(&mut ctx.wallet, io, args) + address_key_find(ctx, io, args) } }, } @@ -94,13 +95,15 @@ impl CliApi { /// Find shielded address or key fn address_key_find( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, args::AddrKeyFind { alias, unsafe_show_secret, + is_pre_genesis, }: args::AddrKeyFind, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); let alias = alias.to_lowercase(); if let Ok(viewing_key) = wallet.find_viewing_key(&alias) { // Check if alias is a viewing key @@ -132,13 +135,15 @@ fn address_key_find( /// List spending keys. fn spending_keys_list( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, args::MaspKeysList { decrypt, + is_pre_genesis, unsafe_show_secret, }: args::MaspKeysList, ) { + let wallet = load_wallet(ctx, is_pre_genesis); let known_view_keys = wallet.get_viewing_keys(); let known_spend_keys = wallet.get_spending_keys(); if known_view_keys.is_empty() { @@ -207,9 +212,11 @@ fn spending_keys_list( /// List payment addresses. fn payment_addresses_list( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, + args::MaspListPayAddrs { is_pre_genesis }: args::MaspListPayAddrs, ) { + let wallet = load_wallet(ctx, is_pre_genesis); let known_addresses = wallet.get_payment_addrs(); if known_addresses.is_empty() { display_line!( @@ -229,14 +236,16 @@ fn payment_addresses_list( /// Generate a spending key. fn spending_key_gen( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, args::MaspSpendKeyGen { alias, alias_force, + is_pre_genesis, unsafe_dont_encrypt, }: args::MaspSpendKeyGen, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); let alias = alias.to_lowercase(); let password = read_and_confirm_encryption_password(unsafe_dont_encrypt); let (alias, _key) = wallet.gen_spending_key(alias, password, alias_force); @@ -257,6 +266,7 @@ fn payment_address_gen( alias_force, viewing_key, pin, + .. }: args::MaspPayAddrGen, ) { let alias = alias.to_lowercase(); @@ -285,16 +295,18 @@ fn payment_address_gen( /// Add a viewing key, spending key, or payment address to wallet. fn address_key_add( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, args::MaspAddrKeyAdd { alias, alias_force, value, + is_pre_genesis, unsafe_dont_encrypt, }: args::MaspAddrKeyAdd, ) { let alias = alias.to_lowercase(); + let mut wallet = load_wallet(ctx, is_pre_genesis); let (alias, typ) = match value { MaspValue::FullViewingKey(viewing_key) => { let alias = wallet @@ -385,20 +397,23 @@ fn key_and_address_restore( /// Generate a new keypair and derive implicit address from it and store them in /// the wallet. fn key_and_address_gen( - wallet: &mut Wallet>, + ctx: Context, io: &impl Io, - rng: &mut R, args::KeyAndAddressGen { scheme, alias, alias_force, + is_pre_genesis, unsafe_dont_encrypt, derivation_path, }: args::KeyAndAddressGen, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); let encryption_password = read_and_confirm_encryption_password(unsafe_dont_encrypt); - let derivation_path_and_mnemonic_rng = derivation_path.map(|p| (p, rng)); + let mut rng = OsRng; + let derivation_path_and_mnemonic_rng = + derivation_path.map(|p| (p, &mut rng)); let (alias, _key, _mnemonic) = wallet .gen_key( scheme, @@ -410,11 +425,11 @@ fn key_and_address_gen( ) .unwrap_or_else(|err| match err { GenRestoreKeyError::KeyStorageError => { - println!("No changes are persisted. Exiting."); + display_line!(io, "No changes are persisted. Exiting."); cli::safe_exit(0); } _ => { - eprintln!("{}", err); + edisplay_line!(io, "{}", err); cli::safe_exit(1); } }); @@ -430,15 +445,17 @@ fn key_and_address_gen( /// Find a keypair in the wallet store. fn key_find( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, args::KeyFind { public_key, alias, value, + is_pre_genesis, unsafe_show_secret, }: args::KeyFind, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); let found_keypair = match public_key { Some(pk) => wallet.find_key_by_pk(&pk, None), None => { @@ -473,13 +490,15 @@ fn key_find( /// List all known keys. fn key_list( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, args::KeyList { decrypt, + is_pre_genesis, unsafe_show_secret, }: args::KeyList, ) { + let wallet = load_wallet(ctx, is_pre_genesis); let known_keys = wallet.get_keys(); if known_keys.is_empty() { display_line!( @@ -538,10 +557,14 @@ fn key_list( /// Export a keypair to a file. fn key_export( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, - args::KeyExport { alias }: args::KeyExport, + args::KeyExport { + alias, + is_pre_genesis, + }: args::KeyExport, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); wallet .find_key(alias.to_lowercase(), None) .map(|keypair| { @@ -560,9 +583,11 @@ fn key_export( /// List all known addresses. fn address_list( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, + args::AddressList { is_pre_genesis }: args::AddressList, ) { + let wallet = load_wallet(ctx, is_pre_genesis); let known_addresses = wallet.get_addresses(); if known_addresses.is_empty() { display_line!( @@ -586,36 +611,40 @@ fn address_list( /// Find address (alias) by its alias (address). fn address_or_alias_find( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, - args: args::AddressOrAliasFind, + args::AddressOrAliasFind { + alias, + address, + is_pre_genesis, + }: args::AddressOrAliasFind, ) { - if args.address.is_some() && args.alias.is_some() { + let wallet = load_wallet(ctx, is_pre_genesis); + if address.is_some() && alias.is_some() { panic!( "This should not be happening: clap should emit its own error \ message." ); - } else if args.alias.is_some() { - if let Some(address) = wallet.find_address(args.alias.as_ref().unwrap()) - { + } else if alias.is_some() { + if let Some(address) = wallet.find_address(alias.as_ref().unwrap()) { display_line!(io, "Found address {}", address.to_pretty_string()); } else { display_line!( io, "No address with alias {} found. Use the command `address \ list` to see all the known addresses.", - args.alias.unwrap().to_lowercase() + alias.unwrap().to_lowercase() ); } - } else if args.address.is_some() { - if let Some(alias) = wallet.find_alias(args.address.as_ref().unwrap()) { + } else if address.is_some() { + if let Some(alias) = wallet.find_alias(address.as_ref().unwrap()) { display_line!(io, "Found alias {}", alias); } else { display_line!( io, "No alias with address {} found. Use the command `address \ list` to see all the known addresses.", - args.address.unwrap() + address.unwrap() ); } } @@ -623,16 +652,18 @@ fn address_or_alias_find( /// Add an address to the wallet. fn address_add( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, - args: args::AddressAdd, + args::AddressAdd { + alias, + alias_force, + address, + is_pre_genesis, + }: args::AddressAdd, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); if wallet - .add_address( - args.alias.clone().to_lowercase(), - args.address, - args.alias_force, - ) + .add_address(alias.to_lowercase(), address, alias_force) .is_none() { edisplay_line!(io, "Address not added"); @@ -644,6 +675,17 @@ fn address_add( display_line!( io, "Successfully added a key and an address with alias: \"{}\"", - args.alias.to_lowercase() + alias.to_lowercase() ); } + +/// Load wallet for chain when `ctx.chain.is_some()` or pre-genesis wallet when +/// `is_pre_genesis || ctx.chain.is_none()`. +fn load_wallet(ctx: Context, is_pre_genesis: bool) -> Wallet { + if is_pre_genesis || ctx.chain.is_none() { + let wallet_path = ctx.global_args.base_dir.join(PRE_GENESIS_DIR); + wallet::load_or_new(&wallet_path) + } else { + ctx.take_chain_or_exit().wallet + } +} diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 16bf625c23..22340cacfc 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -14,7 +14,7 @@ use namada::types::chain::ChainId; use namada::types::dec::Dec; use namada::types::key::*; use namada::vm::validate_untrusted_wasm; -use namada_sdk::wallet::Wallet; +use namada_sdk::wallet::{alias, Wallet}; use prost::bytes::Bytes; use rand::prelude::ThreadRng; use rand::thread_rng; @@ -22,7 +22,7 @@ use serde_json::json; use sha2::{Digest, Sha256}; use crate::cli::context::ENV_VAR_WASM_DIR; -use crate::cli::{self, args, safe_exit}; +use crate::cli::{self, args}; use crate::config::genesis::genesis_config::{ self, GenesisConfig, HexString, ValidatorPreGenesisConfig, }; @@ -56,6 +56,7 @@ pub async fn join_network( genesis_validator, pre_genesis_path, dont_prefetch_wasm, + allow_duplicate_ip, }: args::JoinNetwork, ) { use tokio::fs; @@ -74,7 +75,7 @@ pub async fn join_network( .is_ok() { eprintln!("The chain directory for {} already exists.", chain_id); - cli::safe_exit(1); + safe_exit(1); } } let base_dir_full = fs::canonicalize(&base_dir).await.unwrap(); @@ -109,12 +110,12 @@ pub async fn join_network( let validator_alias_and_pre_genesis_wallet = validator_alias_and_dir.map(|(validator_alias, pre_genesis_dir)| { ( - validator_alias, + alias::Alias::from(validator_alias), pre_genesis::load(&pre_genesis_dir).unwrap_or_else(|err| { eprintln!( "Error loading validator pre-genesis wallet {err}", ); - cli::safe_exit(1) + safe_exit(1) }), ) }); @@ -141,7 +142,7 @@ pub async fn join_network( Ok(contents) => contents, Err(error) => { eprintln!("Error downloading release: {}", error); - cli::safe_exit(1); + safe_exit(1); } }; @@ -190,16 +191,6 @@ pub async fn join_network( .await .unwrap(); - // Move the genesis file - fs::rename( - unpack_dir - .join(config::DEFAULT_BASE_DIR) - .join(format!("{}.toml", chain_id.as_str())), - base_dir_full.join(format!("{}.toml", chain_id.as_str())), - ) - .await - .unwrap(); - // Move the global config fs::rename( unpack_dir @@ -216,6 +207,84 @@ pub async fn join_network( .unwrap(); } + // Read the genesis files + let genesis = genesis::chain::Finalized::read_toml_files(&chain_dir) + .unwrap_or_else(|err| { + eprintln!( + "Failed to read genesis TOML files from {} with {err}.", + chain_dir.to_string_lossy() + ); + safe_exit(1) + }); + + // Try to find validator data when using a pre-genesis validator + let validator_alias = validator_alias_and_pre_genesis_wallet + .as_ref() + .map(|(alias, _wallet)| alias.clone()); + let validator_keys = validator_alias_and_pre_genesis_wallet.as_ref().map( + |(_alias, wallet)| { + let tendermint_node_key: common::SecretKey = wallet + .tendermint_node_key + .try_to_sk() + .unwrap_or_else(|_err| { + eprintln!( + "Tendermint node key must be common (need to change?)" + ); + safe_exit(1) + }); + (tendermint_node_key, wallet.consensus_key.clone()) + }, + ); + let node_mode = if validator_alias.is_some() { + TendermintMode::Validator + } else { + TendermintMode::Full + }; + + // Derive config from genesis + let config = genesis.derive_config( + &chain_dir, + node_mode, + validator_alias, + allow_duplicate_ip, + ); + + // Try to load pre-genesis wallet, if any + let pre_genesis_wallet_path = base_dir.join(PRE_GENESIS_DIR); + let pre_genesis_wallet = crate::wallet::load(&pre_genesis_wallet_path); + // Derive wallet from genesis + let wallet = genesis.derive_wallet( + &chain_dir, + pre_genesis_wallet, + validator_alias_and_pre_genesis_wallet, + ); + + // Save the config and the wallet + config.write(&base_dir, &chain_id, true).unwrap(); + crate::wallet::save(&wallet).unwrap(); + + // Setup the node for a genesis validator, if used + if let Some((tendermint_node_key, consensus_key)) = validator_keys { + println!( + "Setting up validator keys in CometBFT. Consensus key: {}.", + consensus_key.to_public() + ); + let tm_home_dir = chain_dir.join(config::COMETBFT_DIR); + // Write consensus key to tendermint home + tendermint_node::write_validator_key(&tm_home_dir, &consensus_key); + + // Write tendermint node key + write_tendermint_node_key(&tm_home_dir, tendermint_node_key); + + // Pre-initialize tendermint validator state + tendermint_node::write_validator_state(&tm_home_dir); + } else { + println!( + "No validator keys are being used. Make sure you didn't forget to \ + specify `--genesis-validator`?" + ); + } + // Move wasm-dir and update config if it's non-default if let Some(wasm_dir) = wasm_dir.as_ref() { if wasm_dir.to_string_lossy() != config::DEFAULT_WASM_DIR { @@ -242,102 +311,6 @@ pub async fn join_network( } } - // Setup the node for a genesis validator, if used - if let Some((validator_alias, pre_genesis_wallet)) = - validator_alias_and_pre_genesis_wallet - { - let tendermint_node_key: common::SecretKey = pre_genesis_wallet - .tendermint_node_key - .try_to_sk() - .unwrap_or_else(|_err| { - eprintln!( - "Tendermint node key must be common (need to change?)" - ); - cli::safe_exit(1) - }); - - let genesis_file_path = - base_dir.join(format!("{}.toml", chain_id.as_str())); - let genesis_config = - genesis_config::open_genesis_config(genesis_file_path).unwrap(); - - if !is_valid_validator_for_current_chain( - &tendermint_node_key.ref_to(), - &genesis_config, - ) { - eprintln!( - "The current validator is not valid for chain {}.", - chain_id.as_str() - ); - safe_exit(1) - } - - let mut wallet = - crate::wallet::load_or_new_from_genesis(&chain_dir, genesis_config); - - let address = wallet - .find_address(&validator_alias) - .unwrap_or_else(|| { - eprintln!( - "Unable to find validator address for alias \ - {validator_alias}" - ); - cli::safe_exit(1) - }) - .clone(); - - let tm_home_dir = chain_dir.join("cometbft"); - - // Write consensus key to tendermint home - tendermint_node::write_validator_key( - &tm_home_dir, - &pre_genesis_wallet.consensus_key, - ); - - // Derive the node ID from the node key - let node_id = id_from_pk(&tendermint_node_key.ref_to()); - // Write tendermint node key - write_tendermint_node_key(&tm_home_dir, tendermint_node_key); - - // Pre-initialize tendermint validator state - tendermint_node::write_validator_state(&tm_home_dir); - - // Extend the current wallet from the pre-genesis wallet. - // This takes the validator keys to be usable in future commands (e.g. - // to sign a tx from validator account using the account key). - wallet.extend_from_pre_genesis_validator( - address, - validator_alias.into(), - pre_genesis_wallet, - ); - - crate::wallet::save(&wallet).unwrap(); - - // Update the config from the default non-validator settings to - // validator settings - let base_dir = base_dir.clone(); - let chain_id = chain_id.clone(); - tokio::task::spawn_blocking(move || { - let mut config = Config::load(&base_dir, &chain_id, None); - config.ledger.shell.tendermint_mode = TendermintMode::Validator; - - // Remove self from persistent peers - config.ledger.cometbft.p2p.persistent_peers.retain(|peer| { - if let TendermintAddress::Tcp { - peer_id: Some(peer_id), - .. - } = peer - { - node_id != *peer_id - } else { - true - } - }); - config.write(&base_dir, &chain_id, true).unwrap(); - }) - .await - .unwrap(); - } if !dont_prefetch_wasm { fetch_wasms_aux(&base_dir, &chain_id).await; } @@ -369,7 +342,7 @@ pub fn validate_wasm(args::ValidateWasm { code_path }: args::ValidateWasm) { Ok(()) => println!("Wasm code is valid"), Err(e) => { eprintln!("Wasm code is invalid: {e}"); - cli::safe_exit(1) + safe_exit(1) } } } @@ -405,345 +378,115 @@ pub fn id_from_pk(pk: &common::PublicKey) -> TendermintNodeId { pub fn init_network( global_args: args::Global, args::InitNetwork { - genesis_path, + templates_path, wasm_checksums_path, chain_id_prefix, - unsafe_dont_encrypt, + genesis_time, consensus_timeout_commit, - localhost, - allow_duplicate_ip, dont_archive, archive_dir, }: args::InitNetwork, ) { - let mut config = genesis_config::open_genesis_config(genesis_path).unwrap(); - - // Update the WASM checksums - let checksums = - wasm_loader::Checksums::read_checksums_file(&wasm_checksums_path); - config.wasm.iter_mut().for_each(|(name, config)| { - // Find the sha256 from checksums.json - let name = format!("{}.wasm", name); - // Full name in format `{name}.{sha256}.wasm` - let full_name = checksums.0.get(&name).unwrap(); - let hash = full_name - .split_once('.') - .unwrap() - .1 - .split_once('.') - .unwrap() - .0; - config.sha256 = Some(genesis_config::HexString(hash.to_owned())); - }); - - // The `temp_chain_id` gets renamed after we have chain ID. - let temp_chain_id = chain_id_prefix.temp_chain_id(); - let temp_dir = global_args.base_dir.join(temp_chain_id.as_str()); - // The `temp_chain_id` gets renamed after we have chain ID - let accounts_dir = temp_dir.join(NET_ACCOUNTS_DIR); - // Base dir used in account sub-directories - let accounts_temp_dir = - PathBuf::from(config::DEFAULT_BASE_DIR).join(temp_chain_id.as_str()); - - let mut rng: ThreadRng = thread_rng(); - - // Accumulator of validators' Tendermint P2P addresses - let mut persistent_peers: Vec = - Vec::with_capacity(config.validator.len()); - - // Iterate over each validator, generating keys and addresses - config.validator.iter_mut().for_each(|(name, config)| { - let validator_dir = accounts_dir.join(name); - - let chain_dir = validator_dir.join(&accounts_temp_dir); - let tm_home_dir = chain_dir.join("cometbft"); - - // Find or generate tendermint node key - let node_pk = try_parse_public_key( - format!("validator {name} Tendermint node key"), - &config.tendermint_node_key, - ) - .unwrap_or_else(|| { - // Generate a node key with ed25519 as default - let node_sk = common::SecretKey::Ed25519( - ed25519::SigScheme::generate(&mut rng), - ); - - let node_pk = write_tendermint_node_key(&tm_home_dir, node_sk); - - tendermint_node::write_validator_state(&tm_home_dir); - - node_pk - }); - - // Derive the node ID from the node key - let node_id: TendermintNodeId = id_from_pk(&node_pk); - - // Build the list of persistent peers from the validators' node IDs - let peer = TendermintAddress::from_str(&format!( - "{}@{}", - node_id, - config.net_address.as_ref().unwrap(), - )) - .expect("Validator address must be valid"); - persistent_peers.push(peer); - - // Generate account and reward addresses - let address = address::gen_established_address("validator account"); - config.address = Some(address.to_string()); - - // Generate the consensus, account and reward keys, unless they're - // pre-defined. Do not use mnemonic code / HD derivation path. - let mut wallet = crate::wallet::load_or_new(&chain_dir); - - let consensus_pk = try_parse_public_key( - format!("validator {name} consensus key"), - &config.consensus_public_key, - ) + // Load and validate the templates + let templates = genesis::templates::load_and_validate(&templates_path) .unwrap_or_else(|| { - let alias = format!("{}-consensus-key", name); - println!("Generating validator {} consensus key...", name); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Ed25519, - Some(alias), - true, - None, - password, - None, - ) - .expect("Key generation should not fail."); - - // Write consensus key for Tendermint - tendermint_node::write_validator_key(&tm_home_dir, &keypair); - - keypair.ref_to() - }); - - let account_pk = try_parse_public_key( - format!("validator {name} account key"), - &config.account_public_key, - ) - .unwrap_or_else(|| { - let alias = format!("{}-account-key", name); - println!("Generating validator {} account key...", name); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Ed25519, - Some(alias), - true, - None, - password, - None, - ) - .expect("Key generation should not fail."); - keypair.ref_to() - }); - - let protocol_pk = try_parse_public_key( - format!("validator {name} protocol key"), - &config.protocol_public_key, - ) - .unwrap_or_else(|| { - let alias = format!("{}-protocol-key", name); - println!("Generating validator {} protocol signing key...", name); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Ed25519, - Some(alias), - true, - None, - password, - None, - ) - .expect("Key generation should not fail."); - keypair.ref_to() + eprintln!("Invalid templates, aborting."); + safe_exit(1) }); - let eth_hot_pk = try_parse_public_key( - format!("validator {name} eth hot key"), - &config.eth_hot_key, - ) - .unwrap_or_else(|| { - let alias = format!("{}-eth-hot-key", name); - println!("Generating validator {} eth hot key...", name); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Secp256k1, - Some(alias), - true, - None, - password, - None, - ) - .expect("Key generation should not fail."); - keypair.ref_to() - }); + // In addition to standard templates validation, check that there is at + // least one validator account. + if !templates.transactions.has_at_least_one_validator() { + eprintln!("No validator genesis transaction found, aborting."); + safe_exit(1) + } - let eth_cold_pk = try_parse_public_key( - format!("validator {name} eth cold key"), - &config.eth_cold_key, + // Also check that at least one validator account has positive voting power. + let tm_votes_per_token = templates.parameters.pos_params.tm_votes_per_token; + if !templates + .transactions + .has_validator_with_positive_voting_power(tm_votes_per_token) + { + let min_stake = token::Amount::from_uint( + if tm_votes_per_token > Dec::from(1) { + Uint::one() + } else { + (Dec::from(1) / tm_votes_per_token).ceil().abs() + }, + NATIVE_MAX_DECIMAL_PLACES, ) - .unwrap_or_else(|| { - let alias = format!("{}-eth-cold-key", name); - println!("Generating validator {} eth cold key...", name); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Secp256k1, - Some(alias), - true, - None, - password, - None, - ) - .expect("Key generation should not fail."); - keypair.ref_to() - }); - - let dkg_pk = &config - .dkg_public_key - .as_ref() - .map(|key| { - key.to_dkg_public_key().unwrap_or_else(|err| { - let label = format!("validator {name} DKG key"); - eprintln!("Invalid {label} key: {}", err); - cli::safe_exit(1) - }) - }) - .unwrap_or_else(|| { - println!( - "Generating validator {} DKG session keypair...", - name - ); - - let validator_keys = crate::wallet::gen_validator_keys( - &mut wallet, - Some(eth_hot_pk.clone()), - Some(protocol_pk.clone()), - SchemeType::Ed25519, - ) - .expect("Generating new validator keys should not fail"); - let pk = validator_keys.dkg_keypair.as_ref().unwrap().public(); - wallet.add_validator_data(address.clone(), validator_keys); - pk - }); - - // Add the validator public keys to genesis config - config.consensus_public_key = - Some(genesis_config::HexString(consensus_pk.to_string())); - config.account_public_key = - Some(genesis_config::HexString(account_pk.to_string())); - config.eth_cold_key = - Some(genesis_config::HexString(eth_cold_pk.to_string())); - config.eth_hot_key = - Some(genesis_config::HexString(eth_hot_pk.to_string())); - - config.protocol_public_key = - Some(genesis_config::HexString(protocol_pk.to_string())); - config.dkg_public_key = - Some(genesis_config::HexString(dkg_pk.to_string())); - - // Write keypairs to wallet - wallet.add_address(name.clone(), address, true); - - crate::wallet::save(&wallet).unwrap(); - }); - - // Create a wallet for all accounts other than validators. - // Do not use mnemonic code / HD derivation path. - let mut wallet = - crate::wallet::load_or_new(&accounts_dir.join(NET_OTHER_ACCOUNTS_DIR)); - if let Some(established) = &mut config.established { - established.iter_mut().for_each(|(name, config)| { - init_established_account( - name, - &mut wallet, - config, - unsafe_dont_encrypt, - ); - }) + .unwrap(); + eprintln!( + "No validator with positive voting power, aborting. The minimum \ + staked tokens amount required to run the network is {}, because \ + there are {tm_votes_per_token} votes per NAMNAM tokens.", + min_stake.to_string_native(), + ); + safe_exit(1) } - config.token.iter_mut().for_each(|(_name, config)| { - if config.address.is_none() { - let address = address::gen_established_address("token"); - config.address = Some(address.to_string()); - } - if config.vp.is_none() { - config.vp = Some("vp_token".to_string()); - } - }); + // Finalize the genesis config to derive the chain ID + let genesis = genesis::chain::finalize( + templates, + chain_id_prefix, + genesis_time, + consensus_timeout_commit, + ); + let chain_id = &genesis.metadata.chain_id; + let chain_dir = global_args.base_dir.join(chain_id.as_str()); - if let Some(implicit) = &mut config.implicit { - implicit.iter_mut().for_each(|(name, config)| { - if config.public_key.is_none() { - println!( - "Generating implicit account {} key and address ...", - name - ); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Ed25519, - Some(name.clone()), - true, - None, - password, - None, - ) - .expect("Key generation should not fail."); - let public_key = - genesis_config::HexString(keypair.ref_to().to_string()); - config.public_key = Some(public_key); + // Check that chain dir is empty + if chain_dir.exists() && chain_dir.read_dir().unwrap().next().is_some() { + println!( + "The target chain directory {} already exists and is not empty.", + chain_dir.to_string_lossy() + ); + loop { + let mut buffer = String::new(); + print!( + "Do you want to override the chain directory? Will exit \ + otherwise. [y/N]: " + ); + std::io::stdout().flush().unwrap(); + match std::io::stdin().read_line(&mut buffer) { + Ok(size) if size > 0 => { + // Isolate the single character representing the choice + let byte = buffer.chars().next().unwrap(); + buffer.clear(); + match byte { + 'y' | 'Y' => { + fs::remove_dir_all(&chain_dir).unwrap(); + break; + } + 'n' | 'N' => { + println!("Exiting."); + safe_exit(1) + } + // Input is senseless fall through to repeat prompt + _ => { + println!("Unrecognized input."); + } + }; + } + _ => {} } - }) + } } + fs::create_dir_all(&chain_dir).unwrap(); - // Make a copy of genesis config without validator net addresses to - // `write_genesis_config`. Keep the original, because we still need the - // addresses to configure validators. - let mut config_clean = config.clone(); - config_clean - .validator - .iter_mut() - .for_each(|(_name, config)| { - config.net_address = None; - }); - - // Generate the chain ID first - let genesis = genesis_config::load_genesis_config(config_clean.clone()); - let genesis_bytes = genesis.serialize_to_vec(); - let chain_id = ChainId::from_genesis(chain_id_prefix, genesis_bytes); - let chain_dir = global_args.base_dir.join(chain_id.as_str()); - let genesis_path = global_args - .base_dir - .join(format!("{}.toml", chain_id.as_str())); - - // Write the genesis file - genesis_config::write_genesis_config(&config_clean, &genesis_path); - - // Add genesis addresses and save the wallet with other account keys - crate::wallet::add_genesis_addresses(&mut wallet, config_clean.clone()); - crate::wallet::save(&wallet).unwrap(); + // Write the finalized genesis config to the chain dir + genesis.write_toml_files(&chain_dir).unwrap_or_else(|err| { + eprintln!( + "Failed to write finalized genesis TOML files to {} with {err}.", + chain_dir.to_string_lossy() + ); + safe_exit(1) + }); // Write the global config setting the default chain ID let global_config = GlobalConfig::new(chain_id.clone()); global_config.write(&global_args.base_dir).unwrap(); - // Rename the generate chain config dir from `temp_chain_id` to `chain_id` - fs::rename(&temp_dir, &chain_dir).unwrap(); - // Copy the WASM checksums let wasm_dir_full = chain_dir.join(config::DEFAULT_WASM_DIR); fs::create_dir_all(&wasm_dir_full).unwrap(); @@ -753,149 +496,19 @@ pub fn init_network( ) .unwrap(); - config.validator.iter().for_each(|(name, _config)| { - let validator_dir = global_args - .base_dir - .join(chain_id.as_str()) - .join(NET_ACCOUNTS_DIR) - .join(name) - .join(config::DEFAULT_BASE_DIR); - let temp_validator_chain_dir = - validator_dir.join(temp_chain_id.as_str()); - let validator_chain_dir = validator_dir.join(chain_id.as_str()); - // Rename the generated directories for validators from `temp_chain_id` - // to `chain_id` - std::fs::rename(temp_validator_chain_dir, &validator_chain_dir) - .unwrap(); - - // Copy the WASM checksums - let wasm_dir_full = validator_chain_dir.join(config::DEFAULT_WASM_DIR); - fs::create_dir_all(&wasm_dir_full).unwrap(); - fs::copy( - &wasm_checksums_path, - wasm_dir_full.join(config::DEFAULT_WASM_CHECKSUMS_FILE), - ) - .unwrap(); - - // Write the genesis and global config into validator sub-dirs - genesis_config::write_genesis_config( - &config, - validator_dir.join(format!("{}.toml", chain_id.as_str())), - ); - global_config.write(validator_dir).unwrap(); - // Add genesis addresses to the validator's wallet - let mut wallet = crate::wallet::load_or_new(&validator_chain_dir); - crate::wallet::add_genesis_addresses(&mut wallet, config_clean.clone()); - crate::wallet::save(&wallet).unwrap(); - }); - - // Generate the validators' ledger config - config.validator.iter_mut().enumerate().for_each( - |(ix, (name, validator_config))| { - let accounts_dir = chain_dir.join(NET_ACCOUNTS_DIR); - let validator_dir = - accounts_dir.join(name).join(config::DEFAULT_BASE_DIR); - let mut config = Config::load( - &validator_dir, - &chain_id, - Some(TendermintMode::Validator), - ); - - // Configure the ledger - config.ledger.genesis_time = genesis.genesis_time.into(); - // In `config::Ledger`'s `base_dir`, `chain_id` and `tendermint`, - // the paths are prefixed with `validator_dir` given in the first - // parameter. We need to remove this prefix, because - // these sub-directories will be moved to validators' root - // directories. - config.ledger.shell.base_dir = config::DEFAULT_BASE_DIR.into(); - // Add a ledger P2P persistent peers - config.ledger.cometbft.p2p.persistent_peers = persistent_peers - .iter() - .enumerate() - .filter_map(|(index, peer)| - // we do not add the validator in its own persistent peer list - if index != ix { - Some(peer.to_owned()) - } else { - None - }) - .collect(); - - config.ledger.cometbft.consensus.timeout_commit = - consensus_timeout_commit; - config.ledger.cometbft.p2p.allow_duplicate_ip = allow_duplicate_ip; - config.ledger.cometbft.p2p.addr_book_strict = !localhost; - // Clear the net address from the config and use it to set ports - let net_address = validator_config.net_address.take().unwrap(); - let split: Vec<&str> = net_address.split(':').collect(); - let first_port = split[1].parse::().unwrap(); - if localhost { - config.ledger.cometbft.p2p.laddr = TendermintAddress::from_str( - &format!("127.0.0.1:{}", first_port), - ) - .unwrap(); - } else { - config.ledger.cometbft.p2p.laddr = TendermintAddress::from_str( - &format!("0.0.0.0:{}", first_port), - ) - .unwrap(); - } - if localhost { - config.ledger.cometbft.rpc.laddr = TendermintAddress::from_str( - &format!("127.0.0.1:{}", first_port + 1), - ) - .unwrap(); - } else { - config.ledger.cometbft.rpc.laddr = TendermintAddress::from_str( - &format!("0.0.0.0:{}", first_port + 1), - ) - .unwrap(); - } - if localhost { - config.ledger.cometbft.proxy_app = TendermintAddress::from_str( - &format!("127.0.0.1:{}", first_port + 2), - ) - .unwrap(); - } else { - config.ledger.cometbft.proxy_app = TendermintAddress::from_str( - &format!("0.0.0.0:{}", first_port + 2), - ) - .unwrap(); - } - config.write(&validator_dir, &chain_id, true).unwrap(); - }, - ); - - // Update the ledger config persistent peers and save it - let mut config = Config::load(&global_args.base_dir, &chain_id, None); - config.ledger.cometbft.p2p.persistent_peers = persistent_peers; - config.ledger.cometbft.consensus.timeout_commit = consensus_timeout_commit; - config.ledger.cometbft.p2p.allow_duplicate_ip = allow_duplicate_ip; - // Open P2P address - if !localhost { - config.ledger.cometbft.p2p.laddr = - TendermintAddress::from_str("0.0.0.0:26656").unwrap(); - } - config.ledger.cometbft.p2p.addr_book_strict = !localhost; - config.ledger.genesis_time = genesis.genesis_time.into(); - config - .write(&global_args.base_dir, &chain_id, true) - .unwrap(); - println!("Derived chain ID: {}", chain_id); - println!( - "Genesis file generated at {}", - genesis_path.to_string_lossy() - ); + println!("Genesis files stored at {}", chain_dir.to_string_lossy()); // Create a release tarball for anoma-network-config if !dont_archive { + // TODO: remove the `config::DEFAULT_BASE_DIR` and instead just archive + // the chain dir let mut release = tar::Builder::new(Vec::new()); - let release_genesis_path = PathBuf::from(config::DEFAULT_BASE_DIR) - .join(format!("{}.toml", chain_id.as_str())); release - .append_path_with_name(genesis_path, release_genesis_path) + .append_dir_all( + PathBuf::from(config::DEFAULT_BASE_DIR).join(chain_id.as_str()), + &chain_dir, + ) .unwrap(); let global_config_path = GlobalConfig::file_path(&global_args.base_dir); let release_global_config_path = @@ -906,13 +519,6 @@ pub fn init_network( release_global_config_path, ) .unwrap(); - let chain_config_path = - Config::file_path(&global_args.base_dir, &chain_id); - let release_chain_config_path = - Config::file_path(config::DEFAULT_BASE_DIR, &chain_id); - release - .append_path_with_name(chain_config_path, release_chain_config_path) - .unwrap(); let release_wasm_checksums_path = PathBuf::from(config::DEFAULT_BASE_DIR) .join(chain_id.as_str()) @@ -939,39 +545,20 @@ pub fn init_network( release_file.to_string_lossy() ); } -} -fn init_established_account( - name: impl AsRef, - wallet: &mut Wallet, - config: &mut genesis_config::EstablishedAccountConfig, - unsafe_dont_encrypt: bool, -) { - if config.address.is_none() { - let address = address::gen_established_address("established"); - config.address = Some(address.to_string()); - wallet.add_address(&name, address, true); - } - if config.public_key.is_none() { - println!("Generating established account {} key...", name.as_ref()); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Ed25519, - Some(format!("{}-key", name.as_ref())), - true, - None, - password, - None, // do not use mnemonic code / HD derivation path - ) - .expect("Key generation should not fail."); - let public_key = - genesis_config::HexString(keypair.ref_to().to_string()); - config.public_key = Some(public_key); - } - if config.vp.is_none() { - config.vp = Some("vp_user".to_string()); + // After the archive is created, try to copy the built WASM, if they're + // present with the checksums. This is used for local network setup, so + // that we can use a local WASM build. + let checksums = wasm_loader::Checksums::read_checksums(&wasm_dir_full); + for (_, full_name) in checksums.0 { + // try to copy built file from the Namada WASM root dir + let file = std::env::current_dir() + .unwrap() + .join(crate::config::DEFAULT_WASM_DIR) + .join(&full_name); + if file.exists() { + fs::copy(file, wasm_dir_full.join(&full_name)).unwrap(); + } } } @@ -996,34 +583,50 @@ pub fn default_base_dir( } /// Initialize genesis validator's address, consensus key and validator account -/// key and use it in the ledger's node. +/// key into a special "pre-genesis" wallet. pub fn init_genesis_validator( global_args: args::Global, args::InitGenesisValidator { + source, alias, commission_rate, max_commission_rate_change, net_address, unsafe_dont_encrypt, key_scheme, + transfer_from_source_amount, + self_bond_amount, }: args::InitGenesisValidator, ) { + let (mut source_wallet, wallet_file) = + load_pre_genesis_wallet_or_exit(&global_args.base_dir); + + let source_key = + source_wallet.find_key(&source, None).unwrap_or_else(|err| { + eprintln!( + "Couldn't find key for source \"{source}\" in the pre-genesis \ + wallet {}. Failed with {err}.", + wallet_file.to_string_lossy() + ); + safe_exit(1) + }); + // Validate the commission rate data if commission_rate > Dec::one() { eprintln!("The validator commission rate must not exceed 1.0 or 100%"); - cli::safe_exit(1) + safe_exit(1) } if max_commission_rate_change > Dec::one() { eprintln!( "The validator maximum change in commission rate per epoch must \ not exceed 1.0 or 100%" ); - cli::safe_exit(1) + safe_exit(1) } let pre_genesis_dir = validator_pre_genesis_dir(&global_args.base_dir, &alias); println!("Generating validator keys..."); - let pre_genesis = pre_genesis::gen_and_store( + let validator_wallet = pre_genesis::gen_and_store( key_scheme, unsafe_dont_encrypt, &pre_genesis_dir, @@ -1033,78 +636,67 @@ pub fn init_genesis_validator( "Unable to generate the validator pre-genesis wallet: {}", err ); - cli::safe_exit(1) + safe_exit(1) }); println!( "The validator's keys were stored in the wallet at {}", pre_genesis::validator_file_name(&pre_genesis_dir).to_string_lossy() ); - let validator_config = ValidatorPreGenesisConfig { - validator: HashMap::from_iter([( - alias, - genesis_config::ValidatorConfig { - consensus_public_key: Some(HexString( - pre_genesis.consensus_key.ref_to().to_string(), - )), - eth_cold_key: Some(HexString( - pre_genesis.eth_cold_key.ref_to().to_string(), - )), - eth_hot_key: Some(HexString( - pre_genesis.eth_hot_key.ref_to().to_string(), - )), - account_public_key: Some(HexString( - pre_genesis.account_key.ref_to().to_string(), - )), - protocol_public_key: Some(HexString( - pre_genesis - .store - .validator_keys - .protocol_keypair - .ref_to() - .to_string(), - )), - dkg_public_key: Some(HexString( - pre_genesis - .store - .validator_keys - .dkg_keypair - .as_ref() - .unwrap() - .public() - .to_string(), - )), - commission_rate: Some(commission_rate), - max_commission_rate_change: Some(max_commission_rate_change), - tendermint_node_key: Some(HexString( - pre_genesis.tendermint_node_key.ref_to().to_string(), - )), - net_address: Some(net_address), - ..Default::default() - }, - )]), - }; - let genesis_part = toml::to_string(&validator_config).unwrap(); - println!("Your public partial pre-genesis TOML configuration:"); + let transactions = genesis::transactions::init_validator( + genesis::transactions::GenesisValidatorData { + source_key, + alias: alias::Alias::from(alias), + commission_rate, + max_commission_rate_change, + net_address, + transfer_from_source_amount, + self_bond_amount, + }, + &mut source_wallet, + &validator_wallet, + ); + + let genesis_part = toml::to_string(&transactions).unwrap(); + println!("Your public signed pre-genesis transactions TOML:"); println!(); println!("{genesis_part}"); - let file_name = validator_pre_genesis_file(&pre_genesis_dir); + let file_name = validator_pre_genesis_txs_file(&pre_genesis_dir); fs::write(&file_name, genesis_part).unwrap_or_else(|err| { eprintln!( - "Couldn't write partial pre-genesis file to {}. Failed with: {}", + "Couldn't write pre-genesis transactions file to {}. Failed with: \ + {}", file_name.to_string_lossy(), err ); - cli::safe_exit(1) + safe_exit(1) }); println!(); println!( - "Pre-genesis TOML written to {}", + "Pre-genesis transactions TOML written to {}", file_name.to_string_lossy() ); } +/// Try to load a pre-genesis wallet or terminate if it cannot be found. +pub fn load_pre_genesis_wallet_or_exit( + base_dir: &Path, +) -> (Wallet, PathBuf) { + let pre_genesis_dir = base_dir.join(PRE_GENESIS_DIR); + let wallet_file = crate::wallet::wallet_file(&pre_genesis_dir); + ( + crate::wallet::load(&pre_genesis_dir).unwrap_or_else(|| { + eprintln!( + "No pre-genesis wallet found at {}.", + wallet_file.to_string_lossy() + ); + safe_exit(1) + }), + wallet_file, + ) +} + async fn download_file(url: impl AsRef) -> reqwest::Result { let url = url.as_ref(); let response = reqwest::get(url).await?; @@ -1113,19 +705,6 @@ async fn download_file(url: impl AsRef) -> reqwest::Result { Ok(contents) } -fn try_parse_public_key( - label: impl AsRef, - value: &Option, -) -> Option { - let label = label.as_ref(); - value.as_ref().map(|key| { - key.to_public_key().unwrap_or_else(|err| { - eprintln!("Invalid {label} key: {}", err); - cli::safe_exit(1) - }) - }) -} - fn network_configs_url_prefix(chain_id: &ChainId) -> String { std::env::var(ENV_VAR_NETWORK_CONFIGS_SERVER).unwrap_or_else(|_| { format!("{DEFAULT_NETWORK_CONFIGS_SERVER}/{chain_id}") @@ -1173,9 +752,9 @@ pub fn write_tendermint_node_key( node_pk } -/// The default path to a validator pre-genesis file. -pub fn validator_pre_genesis_file(pre_genesis_path: &Path) -> PathBuf { - pre_genesis_path.join("validator.toml") +/// The default path to a validator pre-genesis txs file. +pub fn validator_pre_genesis_txs_file(pre_genesis_path: &Path) -> PathBuf { + pre_genesis_path.join("transactions.toml") } /// The default validator pre-genesis directory @@ -1183,6 +762,76 @@ pub fn validator_pre_genesis_dir(base_dir: &Path, alias: &str) -> PathBuf { base_dir.join(PRE_GENESIS_DIR).join(alias) } +/// Validate genesis templates. Exits process if invalid. +pub fn validate_genesis_templates( + _global_args: args::Global, + args::ValidateGenesisTemplates { path }: args::ValidateGenesisTemplates, +) { + if genesis::templates::load_and_validate(&path).is_none() { + safe_exit(1) + } +} + +/// Sign genesis transactions. +pub fn sign_genesis_tx( + global_args: args::Global, + args::SignGenesisTx { path, output }: args::SignGenesisTx, +) { + let (mut wallet, _wallet_file) = + load_pre_genesis_wallet_or_exit(&global_args.base_dir); + + let contents = fs::read(&path).unwrap_or_else(|err| { + eprintln!( + "Unable to read from file {}. Failed with {err}.", + path.to_string_lossy() + ); + safe_exit(1); + }); + let unsigned = genesis::transactions::parse_unsigned(&contents) + .unwrap_or_else(|err| { + eprintln!( + "Unable to parse the TOML from {}. Failed with {err}.", + path.to_string_lossy() + ); + safe_exit(1); + }); + if unsigned.validator_account.is_some() + && !unsigned.validator_account.as_ref().unwrap().is_empty() + { + eprintln!( + "Validator transactions must be signed with a validator wallet. \ + You can use `namada client utils init-genesis-validator` \ + supplied with the required arguments to generate a validator \ + wallet and sign the validator genesis transactions." + ); + safe_exit(1); + } + let signed = genesis::transactions::sign_txs(unsigned, &mut wallet); + + match output { + Some(output_path) => { + let transactions = toml::to_vec(&signed).unwrap(); + fs::write(&output_path, transactions).unwrap_or_else(|err| { + eprintln!( + "Failed to write output to {} with {err}.", + output_path.to_string_lossy() + ); + safe_exit(1); + }); + println!( + "Your public signed transactions TOML has been written to {}", + output_path.to_string_lossy() + ); + } + None => { + let transactions = toml::to_string(&signed).unwrap(); + println!("Your public signed transactions TOML:"); + println!(); + println!("{transactions}"); + } + } +} + /// Add a spinning wheel to a message for long running commands. /// Can be turned off for E2E tests by setting the `REDUCED_CLI_PRINTING` /// environment variable. @@ -1207,19 +856,6 @@ where task.join().unwrap() } -fn is_valid_validator_for_current_chain( - tendermint_node_pk: &common::PublicKey, - genesis_config: &GenesisConfig, -) -> bool { - genesis_config.validator.iter().any(|(_alias, config)| { - if let Some(tm_node_key) = &config.tendermint_node_key { - tm_node_key.0.eq(&tendermint_node_pk.to_string()) - } else { - false - } - }) -} - /// Replace the contents of `addr` with a dummy address. #[inline] pub fn take_config_address(addr: &mut TendermintAddress) -> TendermintAddress { @@ -1232,3 +868,13 @@ pub fn take_config_address(addr: &mut TendermintAddress) -> TendermintAddress { }, ) } + +#[cfg(not(test))] +fn safe_exit(code: i32) -> ! { + crate::cli::safe_exit(code) +} + +#[cfg(test)] +fn safe_exit(code: i32) -> ! { + panic!("Process exited unsuccesfully with error code: {}", code); +} diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index ed0ffeb788..08a861e423 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -1,11 +1,21 @@ //! The parameters used for the chain's genesis +pub mod chain; +pub mod templates; +pub mod toml_utils; +pub mod transactions; + +use std::array::TryFromSliceError; use std::collections::{BTreeMap, HashMap}; +use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; +use data_encoding::HEXLOWER; use derivative::Derivative; use namada::core::ledger::governance::parameters::GovernanceParameters; use namada::core::ledger::pgf::parameters::PgfParameters; +use namada::core::types::string_encoding; +use namada::ledger::eth_bridge::EthereumBridgeParams; use namada::ledger::parameters::EpochDuration; use namada::ledger::pos::{Dec, GenesisValidator, OwnedPosParams}; use namada::types::address::Address; @@ -15,722 +25,10 @@ use namada::types::key::*; use namada::types::time::{DateTimeUtc, DurationSecs}; use namada::types::token::Denomination; use namada::types::{storage, token}; -use namada_sdk::eth_bridge::EthereumBridgeConfig; - -/// Genesis configuration file format -pub mod genesis_config { - use std::array::TryFromSliceError; - use std::collections::{BTreeMap, BTreeSet, HashMap}; - use std::convert::TryInto; - use std::path::Path; - use std::str::FromStr; - - use data_encoding::HEXLOWER; - use eyre::Context; - use namada::core::ledger::governance::parameters::GovernanceParameters; - use namada::core::ledger::pgf::parameters::PgfParameters; - use namada::ledger::parameters::EpochDuration; - use namada::ledger::pos::{Dec, GenesisValidator, OwnedPosParams}; - use namada::types::address::Address; - use namada::types::chain::ProposalBytes; - use namada::types::key::dkg_session_keys::DkgPublicKey; - use namada::types::key::*; - use namada::types::time::Rfc3339String; - use namada::types::token::Denomination; - use namada::types::{storage, token}; - use serde::{Deserialize, Serialize}; - use thiserror::Error; - - use super::{ - EstablishedAccount, EthereumBridgeConfig, Genesis, ImplicitAccount, - Parameters, TokenAccount, Validator, - }; - use crate::cli; - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct HexString(pub String); - - impl HexString { - pub fn to_bytes(&self) -> Result, HexKeyError> { - let bytes = HEXLOWER.decode(self.0.as_ref())?; - Ok(bytes) - } - - pub fn to_sha256_bytes(&self) -> Result<[u8; 32], HexKeyError> { - let bytes = HEXLOWER.decode(self.0.as_ref())?; - let slice = bytes.as_slice(); - let array: [u8; 32] = slice.try_into()?; - Ok(array) - } - - pub fn to_public_key(&self) -> Result { - let key = common::PublicKey::from_str(&self.0) - .map_err(HexKeyError::InvalidPublicKey)?; - Ok(key) - } - - pub fn to_dkg_public_key(&self) -> Result { - let key = DkgPublicKey::from_str(&self.0)?; - Ok(key) - } - } - - #[derive(Error, Debug)] - pub enum HexKeyError { - #[error("Invalid hex string: {0:?}")] - InvalidHexString(data_encoding::DecodeError), - #[error("Invalid sha256 checksum: {0}")] - InvalidSha256(TryFromSliceError), - #[error("Invalid public key: {0}")] - InvalidPublicKey(ParsePublicKeyError), - } - - impl From for HexKeyError { - fn from(err: data_encoding::DecodeError) -> Self { - Self::InvalidHexString(err) - } - } - - impl From for HexKeyError { - fn from(err: ParsePublicKeyError) -> Self { - Self::InvalidPublicKey(err) - } - } - - impl From for HexKeyError { - fn from(err: TryFromSliceError) -> Self { - Self::InvalidSha256(err) - } - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct GenesisConfig { - // Genesis timestamp - pub genesis_time: Rfc3339String, - // Name of the native token - this must one of the tokens included in - // the `token` field - pub native_token: String, - // Initial validator set - pub validator: HashMap, - // Token accounts present at genesis - pub token: HashMap, - // Established accounts present at genesis - pub established: Option>, - // Implicit accounts present at genesis - pub implicit: Option>, - // Protocol parameters - pub parameters: ParametersConfig, - // PoS parameters - pub pos_params: PosParamsConfig, - // Governance parameters - pub gov_params: GovernanceParamsConfig, - // Pgf parameters - pub pgf_params: PgfParametersConfig, - // Ethereum bridge config - pub ethereum_bridge_params: Option, - // Wasm definitions - pub wasm: HashMap, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct GovernanceParamsConfig { - // Min funds to stake to submit a proposal - pub min_proposal_fund: u64, - // Maximum size of proposal in kibibytes (KiB) - pub max_proposal_code_size: u64, - // Minimum proposal period length in epochs - pub min_proposal_voting_period: u64, - // Maximum proposal period length in epochs - pub max_proposal_period: u64, - // Maximum number of characters in the proposal content - pub max_proposal_content_size: u64, - // Minimum number of epoch between end and grace epoch - pub min_proposal_grace_epochs: u64, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct PgfParametersConfig { - /// The set of stewards - pub stewards: BTreeSet
, - /// The pgf inflation rate - pub pgf_inflation_rate: Dec, - /// The stewards inflation rate - pub stewards_inflation_rate: Dec, - } +use serde::{Deserialize, Serialize}; - /// Validator pre-genesis configuration can be created with client utils - /// `init-genesis-validator` command and added to a genesis for - /// `init-network` cmd and that can be subsequently read by `join-network` - /// cmd to setup a genesis validator node. - #[derive(Serialize, Deserialize, Debug)] - pub struct ValidatorPreGenesisConfig { - pub validator: HashMap, - } - - #[derive(Clone, Default, Debug, Deserialize, Serialize)] - pub struct ValidatorConfig { - // Public key for consensus. (default: generate) - pub consensus_public_key: Option, - // Public key (cold) for eth governance. (default: generate) - pub eth_cold_key: Option, - // Public key (hot) for eth bridge. (default: generate) - pub eth_hot_key: Option, - // Public key for validator account. (default: generate) - pub account_public_key: Option, - // Public protocol signing key for validator account. (default: - // generate) - pub protocol_public_key: Option, - // Public DKG session key for validator account. (default: generate) - pub dkg_public_key: Option, - // Validator address (default: generate). - pub address: Option, - // Total number of tokens held at genesis. - pub tokens: Option, - // Unstaked balance at genesis. - pub non_staked_balance: Option, - /// Commission rate charged on rewards for delegators (bounded inside - /// 0-1) - pub commission_rate: Option, - /// Maximum change in commission rate permitted per epoch - pub max_commission_rate_change: Option, - // Filename of validator VP. (default: default validator VP) - pub validator_vp: Option, - // IP:port of the validator. (used in generation only) - pub net_address: Option, - /// Tendermint node key is used to derive Tendermint node ID for node - /// authentication - pub tendermint_node_key: Option, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct TokenAccountConfig { - // Address of token account (default: generate). - pub address: Option, - // The number of decimal places amounts of this token has - pub denom: Denomination, - // Filename of token account VP. (default: token VP) - pub vp: Option, - // Initial balances held by accounts defined elsewhere. - pub balances: Option>, - // Token parameters - pub parameters: Option, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct EstablishedAccountConfig { - // Address of established account (default: generate). - pub address: Option, - // Filename of established account VP. (default: user VP) - pub vp: Option, - // Public key of established account. (default: generate) - pub public_key: Option, - // Initial storage key values. - pub storage: Option>, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct ImplicitAccountConfig { - // Public key of implicit account (default: generate). - pub public_key: Option, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct ParametersConfig { - /// Max payload size, in bytes, for a tx batch proposal. - /// - /// Block proposers may never return a `PrepareProposal` - /// response containing `txs` with a byte length greater - /// than whatever is configured through this parameter. - /// - /// Note that this parameter's value will always be strictly - /// smaller than a Tendermint block's `MaxBytes` consensus - /// parameter. Currently, we hard cap `max_proposal_bytes` - /// at 90 MiB in Namada, which leaves at least 10 MiB of - /// 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). - // TODO: this is i64 because datetime wants it - pub max_expected_time_per_block: i64, - /// Hashes of whitelisted vps array. `None` value or an empty array - /// disables whitelisting. - pub vp_whitelist: Option>, - /// Hashes of whitelisted txs array. `None` value or an empty array - /// disables whitelisting. - pub tx_whitelist: Option>, - /// Filename of implicit accounts validity predicate WASM code - pub implicit_vp: String, - /// Expected number of epochs per year - pub epochs_per_year: u64, - /// Max signature per transaction - pub max_signatures_per_transaction: u8, - /// PoS gain p - pub pos_gain_p: Dec, - /// PoS gain d - pub pos_gain_d: Dec, - /// 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 minimum_gas_price: BTreeMap, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct PosParamsConfig { - // Maximum number of consensus validators. - pub max_validator_slots: u64, - // Pipeline length (in epochs). - pub pipeline_len: u64, - // Unbonding length (in epochs). - pub unbonding_len: u64, - // Votes per token. - pub tm_votes_per_token: Dec, - // Reward for proposing a block. - pub block_proposer_reward: Dec, - // Reward for voting on a block. - pub block_vote_reward: Dec, - // Maximum staking APY - pub max_inflation_rate: Dec, - // Target ratio of staked NAM tokens to total NAM tokens - pub target_staked_ratio: Dec, - // Portion of a validator's stake that should be slashed on a - // duplicate vote. - pub duplicate_vote_min_slash_rate: Dec, - // Portion of a validator's stake that should be slashed on a - // light client attack. - pub light_client_attack_min_slash_rate: Dec, - /// Number of epochs above and below (separately) the current epoch to - /// consider when doing cubic slashing - pub cubic_slashing_window_length: u64, - /// The minimum amount of bonded tokens that a validator needs to be in - /// either the `consensus` or `below_capacity` validator sets - pub validator_stake_threshold: token::Amount, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct WasmConfig { - filename: String, - pub sha256: Option, - } - - fn load_validator( - config: &ValidatorConfig, - wasm: &HashMap, - ) -> Validator { - let validator_vp_name = config.validator_vp.as_ref().unwrap(); - let validator_vp_config = wasm.get(validator_vp_name).unwrap(); - - Validator { - pos_data: GenesisValidator { - address: Address::decode(config.address.as_ref().unwrap()) - .unwrap(), - tokens: token::Amount::native_whole( - config.tokens.unwrap_or_default(), - ), - consensus_key: config - .consensus_public_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), - protocol_key: config - .protocol_public_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), - eth_cold_key: config - .eth_cold_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), - eth_hot_key: config - .eth_hot_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), - commission_rate: config - .commission_rate - .and_then(|rate| { - if rate <= Dec::one() { Some(rate) } else { None } - }) - .expect("Commission rate must be between 0.0 and 1.0"), - max_commission_rate_change: config - .max_commission_rate_change - .and_then(|rate| { - if rate <= Dec::one() { Some(rate) } else { None } - }) - .expect( - "Max commission rate change must be between 0.0 and \ - 1.0", - ), - }, - account_key: config - .account_public_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), - dkg_public_key: config - .dkg_public_key - .as_ref() - .unwrap() - .to_dkg_public_key() - .unwrap(), - non_staked_balance: token::Amount::native_whole( - config.non_staked_balance.unwrap_or_default(), - ), - validator_vp_code_path: validator_vp_config.filename.to_owned(), - validator_vp_sha256: validator_vp_config - .sha256 - .clone() - .unwrap() - .to_sha256_bytes() - .unwrap(), - } - } - - fn load_token( - config: &TokenAccountConfig, - validators: &HashMap, - established_accounts: &HashMap, - implicit_accounts: &HashMap, - ) -> TokenAccount { - TokenAccount { - last_locked_ratio: Dec::zero(), - last_inflation: token::Amount::zero(), - parameters: config.parameters.as_ref().unwrap().to_owned(), - address: Address::decode(config.address.as_ref().unwrap()).unwrap(), - denom: config.denom, - balances: config - .balances - .as_ref() - .unwrap_or(&HashMap::default()) - .iter() - .map(|(alias_or_address, amount)| { - ( - match Address::decode(alias_or_address) { - Ok(address) => address, - Err(decode_err) => { - if let Some(alias) = - alias_or_address.strip_suffix(".public_key") - { - if let Some(established) = - established_accounts.get(alias) - { - established - .public_key - .as_ref() - .unwrap() - .into() - } else if let Some(validator) = - validators.get(alias) - { - (&validator.account_key).into() - } else { - eprintln!( - "No established or validator \ - account with alias {} found", - alias - ); - cli::safe_exit(1) - } - } else if let Some(established) = - established_accounts.get(alias_or_address) - { - established.address.clone() - } else if let Some(validator) = - validators.get(alias_or_address) - { - validator.pos_data.address.clone() - } else if let Some(implicit) = - implicit_accounts.get(alias_or_address) - { - (&implicit.public_key).into() - } else { - eprintln!( - "{} is unknown alias and not a valid \ - address: {}", - alias_or_address, decode_err - ); - cli::safe_exit(1) - } - } - }, - token::Amount::from_uint(*amount, config.denom).expect( - "expected a balance that fits into 256 bits", - ), - ) - }) - .collect(), - } - } - - fn load_established( - config: &EstablishedAccountConfig, - wasm: &HashMap, - ) -> EstablishedAccount { - let account_vp_name = config.vp.as_ref().unwrap(); - let account_vp_config = wasm.get(account_vp_name).unwrap(); - - EstablishedAccount { - address: Address::decode(config.address.as_ref().unwrap()).unwrap(), - vp_code_path: account_vp_config.filename.to_owned(), - vp_sha256: account_vp_config - .sha256 - .clone() - .unwrap_or_else(|| { - eprintln!("Unknown user VP WASM sha256"); - cli::safe_exit(1); - }) - .to_sha256_bytes() - .unwrap(), - public_key: config - .public_key - .as_ref() - .map(|hex| hex.to_public_key().unwrap()), - storage: config - .storage - .as_ref() - .unwrap_or(&HashMap::default()) - .iter() - .map(|(address, hex)| { - ( - storage::Key::parse(address).unwrap(), - hex.to_bytes().unwrap(), - ) - }) - .collect(), - } - } - - fn load_implicit(config: &ImplicitAccountConfig) -> ImplicitAccount { - ImplicitAccount { - public_key: config - .public_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), - } - } - - pub fn load_genesis_config(config: GenesisConfig) -> Genesis { - let GenesisConfig { - genesis_time, - native_token, - validator, - token, - established, - implicit, - parameters, - pos_params, - gov_params, - pgf_params, - wasm, - ethereum_bridge_params, - } = config; - - let native_token = Address::decode( - token - .get(&native_token) - .expect( - "Native token's alias must be present in the declared \ - tokens", - ) - .address - .as_ref() - .expect("Missing native token address"), - ) - .expect("Invalid address"); - let validators: HashMap = validator - .iter() - .map(|(name, cfg)| (name.clone(), load_validator(cfg, &wasm))) - .collect(); - let established_accounts: HashMap = - established - .unwrap_or_default() - .iter() - .map(|(name, cfg)| (name.clone(), load_established(cfg, &wasm))) - .collect(); - let implicit_accounts: HashMap = implicit - .unwrap_or_default() - .iter() - .map(|(name, cfg)| (name.clone(), load_implicit(cfg))) - .collect(); - #[allow(clippy::iter_kv_map)] - let token_accounts = token - .iter() - .map(|(_name, cfg)| { - load_token( - cfg, - &validators, - &established_accounts, - &implicit_accounts, - ) - }) - .collect(); - - let implicit_vp_config = wasm.get(¶meters.implicit_vp).unwrap(); - let implicit_vp_code_path = implicit_vp_config.filename.to_owned(); - let implicit_vp_sha256 = implicit_vp_config - .sha256 - .clone() - .unwrap_or_else(|| { - eprintln!("Unknown implicit VP WASM sha256"); - cli::safe_exit(1); - }) - .to_sha256_bytes() - .unwrap(); - - 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, - min_duration: namada::types::time::Duration::seconds( - min_duration, - ) - .into(), - }, - max_expected_time_per_block: - namada::types::time::Duration::seconds( - parameters.max_expected_time_per_block, - ) - .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, - implicit_vp_sha256, - epochs_per_year: parameters.epochs_per_year, - max_signatures_per_transaction: parameters - .max_signatures_per_transaction, - pos_gain_p: parameters.pos_gain_p, - pos_gain_d: parameters.pos_gain_d, - staked_ratio: Dec::zero(), - pos_inflation_amount: token::Amount::zero(), - minimum_gas_price: parameters.minimum_gas_price, - fee_unshielding_gas_limit: parameters.fee_unshielding_gas_limit, - fee_unshielding_descriptions_limit: parameters - .fee_unshielding_descriptions_limit, - }; - - let GovernanceParamsConfig { - min_proposal_fund, - max_proposal_code_size, - min_proposal_voting_period, - max_proposal_content_size, - min_proposal_grace_epochs, - max_proposal_period, - } = gov_params; - let gov_params = GovernanceParameters { - min_proposal_fund: token::Amount::native_whole(min_proposal_fund), - max_proposal_code_size, - min_proposal_voting_period, - max_proposal_content_size, - min_proposal_grace_epochs, - max_proposal_period, - }; - - let PgfParametersConfig { - stewards, - pgf_inflation_rate, - stewards_inflation_rate, - } = pgf_params; - let pgf_params = PgfParameters { - stewards, - pgf_inflation_rate, - stewards_inflation_rate, - }; - - let PosParamsConfig { - max_validator_slots, - pipeline_len, - unbonding_len, - tm_votes_per_token, - block_proposer_reward, - block_vote_reward, - max_inflation_rate, - target_staked_ratio, - duplicate_vote_min_slash_rate, - light_client_attack_min_slash_rate, - cubic_slashing_window_length, - validator_stake_threshold, - } = pos_params; - - let pos_params = OwnedPosParams { - max_validator_slots, - pipeline_len, - unbonding_len, - tm_votes_per_token, - block_proposer_reward, - block_vote_reward, - max_inflation_rate, - target_staked_ratio, - duplicate_vote_min_slash_rate, - light_client_attack_min_slash_rate, - cubic_slashing_window_length, - validator_stake_threshold, - }; - - let mut genesis = Genesis { - genesis_time: genesis_time.try_into().unwrap(), - native_token, - validators: validators.into_values().collect(), - token_accounts, - established_accounts: established_accounts.into_values().collect(), - implicit_accounts: implicit_accounts.into_values().collect(), - parameters, - pos_params, - gov_params, - pgf_params, - ethereum_bridge_params, - }; - genesis.init(); - genesis - } - - pub fn open_genesis_config( - path: impl AsRef, - ) -> color_eyre::eyre::Result { - let config_file = - std::fs::read_to_string(&path).wrap_err_with(|| { - format!( - "couldn't read genesis config file from {}", - path.as_ref().to_string_lossy() - ) - })?; - toml::from_str(&config_file).wrap_err_with(|| { - format!( - "couldn't parse TOML from {}", - path.as_ref().to_string_lossy() - ) - }) - } - - pub fn write_genesis_config( - config: &GenesisConfig, - path: impl AsRef, - ) { - let toml = toml::to_string(&config).unwrap(); - std::fs::write(path, toml).unwrap(); - } - - pub fn read_genesis_config(path: impl AsRef) -> Genesis { - load_genesis_config(open_genesis_config(path).unwrap()) - } -} +#[cfg(all(any(test, feature = "benches"), not(feature = "integration")))] +use crate::config::genesis::chain::Finalized; #[derive(Debug, BorshSerialize, BorshDeserialize)] #[borsh(init=init)] @@ -746,7 +44,7 @@ pub struct Genesis { pub gov_params: GovernanceParameters, pub pgf_params: PgfParameters, // Ethereum bridge config - pub ethereum_bridge_params: Option, + pub ethereum_bridge_params: Option, } impl Genesis { @@ -895,254 +193,248 @@ pub struct Parameters { pub minimum_gas_price: BTreeMap, } -#[cfg(not(any(test, feature = "dev")))] -pub fn genesis( - base_dir: impl AsRef, - chain_id: &namada::types::chain::ChainId, -) -> Genesis { - let path = base_dir - .as_ref() - .join(format!("{}.toml", chain_id.as_str())); - genesis_config::read_genesis_config(path) +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialOrd, + Ord, + PartialEq, + Eq, +)] +pub struct HexString(pub String); + +impl HexString { + pub fn to_bytes(&self) -> Result, HexKeyError> { + let bytes = HEXLOWER.decode(self.0.as_ref())?; + Ok(bytes) + } + + pub fn to_sha256_bytes(&self) -> Result<[u8; 32], HexKeyError> { + let bytes = HEXLOWER.decode(self.0.as_ref())?; + let slice = bytes.as_slice(); + let array: [u8; 32] = slice.try_into()?; + Ok(array) + } + + pub fn to_public_key(&self) -> Result { + let key = common::PublicKey::from_str(&self.0) + .map_err(HexKeyError::InvalidPublicKey)?; + Ok(key) + } + + pub fn to_dkg_public_key(&self) -> Result { + let key = DkgPublicKey::from_str(&self.0)?; + Ok(key) + } } -#[cfg(any(test, feature = "dev"))] -pub fn genesis(num_validators: u64) -> Genesis { - use namada::types::address::{ - self, apfel, btc, dot, eth, kartoffel, nam, schnitzel, wnam, - }; - use namada::types::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; - use namada::types::ethereum_events::EthAddress; - use namada::types::uint::Uint; - use namada_sdk::eth_bridge::{ - Contracts, Erc20WhitelistEntry, UpgradeableContract, - }; - use crate::wallet; +#[derive(thiserror::Error, Debug)] +pub enum HexKeyError { + #[error("Invalid hex string: {0:?}")] + InvalidHexString(data_encoding::DecodeError), + #[error("Invalid sha256 checksum: {0}")] + InvalidSha256(TryFromSliceError), + #[error("Invalid public key: {0}")] + InvalidPublicKey(string_encoding::DecodeError), +} + +impl From for HexKeyError { + fn from(err: data_encoding::DecodeError) -> Self { + Self::InvalidHexString(err) + } +} + +impl From for HexKeyError { + fn from(err: string_encoding::DecodeError) -> Self { + Self::InvalidPublicKey(err) + } +} + +impl From for HexKeyError { + fn from(err: TryFromSliceError) -> Self { + Self::InvalidSha256(err) + } +} + +/// Modify the default genesis file (namada/genesis/localnet/) to +/// accommodate testing. +/// +/// This includes adding the Ethereum bridge parameters and +/// adding a specified number of validators. +#[cfg(all(any(test, feature = "benches"), not(feature = "integration")))] +pub fn make_dev_genesis(num_validators: u64) -> Finalized { + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::time::Duration; + + use namada::core::types::string_encoding::StringEncoded; + use namada::ledger::eth_bridge::{Contracts, UpgradeableContract}; + use namada::proto::{standalone_signature, SerializeWithBorsh}; + use namada_sdk::wallet::alias::Alias; + use namada::types::address::wnam; + use namada::types::chain::ChainIdPrefix; + use namada::types::ethereum_events::EthAddress; + use namada::types::token::NATIVE_MAX_DECIMAL_PLACES; - let vp_implicit_path = "vp_implicit.wasm"; - let vp_user_path = "vp_user.wasm"; + use crate::config::genesis::chain::finalize; + use crate::wallet::defaults; - // NOTE When the validator's key changes, tendermint must be reset with - // `namada reset` command. To generate a new validator, use the - // `tests::gen_genesis_validator` below. - let mut validators = Vec::::new(); + let mut current_path = std::env::current_dir() + .expect("Current directory should exist") + .canonicalize() + .expect("Current directory should exist"); + while current_path.file_name().unwrap() != "apps" { + current_path.pop(); + } + current_path.pop(); + let chain_dir = current_path.join("genesis").join("localnet"); + let templates = templates::load_and_validate(&chain_dir) + .expect("Missing genesis files"); + let mut genesis = finalize( + templates, + ChainIdPrefix::from_str("test").unwrap(), + DateTimeUtc::now(), + Duration::from_secs(30).into(), + ); + + // Add Ethereum bridge params. + genesis.parameters.eth_bridge_params = Some(templates::EthBridgeParams { + eth_start_height: Default::default(), + min_confirmations: Default::default(), + contracts: Contracts { + native_erc20: wnam(), + bridge: UpgradeableContract { + address: EthAddress([0; 20]), + version: Default::default(), + }, + }, + erc20_whitelist: vec![], + }); - // Use hard-coded keys for the first validator to avoid breaking other code - let consensus_keypair = wallet::defaults::validator_keypair(); - let account_keypair = wallet::defaults::validator_keypair(); + if let Some(vals) = genesis.transactions.validator_account.as_mut() { + vals[0].address = defaults::validator_address(); + } + let default_addresses: HashMap = + defaults::addresses().into_iter().collect(); + if let Some(accs) = genesis.transactions.established_account.as_mut() { + for acc in accs { + if let Some(addr) = default_addresses.get(&acc.tx.alias) { + acc.address = addr.clone(); + } + } + } + // remove Albert's bond since it messes up existing unit test math + if let Some(bonds) = genesis.transactions.bond.as_mut() { + bonds.retain(|bond| { + bond.source + != transactions::AliasOrPk::Alias( + Alias::from_str("albert").unwrap(), + ) + }) + }; let secp_eth_cold_keypair = secp256k1::SecretKey::try_from_slice(&[ 90, 83, 107, 155, 193, 251, 120, 27, 76, 1, 188, 8, 116, 121, 90, 99, 65, 17, 187, 6, 238, 141, 63, 188, 76, 38, 102, 7, 47, 185, 28, 52, ]) .unwrap(); - - let eth_cold_keypair = - common::SecretKey::try_from_sk(&secp_eth_cold_keypair).unwrap(); - let address = wallet::defaults::validator_address(); - let (protocol_keypair, eth_bridge_keypair, dkg_keypair) = - wallet::defaults::validator_keys(); - let validator = Validator { - pos_data: GenesisValidator { - address, - tokens: token::Amount::native_whole(200_000), - consensus_key: consensus_keypair.ref_to(), - protocol_key: protocol_keypair.ref_to(), - commission_rate: Dec::new(5, 2).expect("This can't fail"), - max_commission_rate_change: Dec::new(1, 2) - .expect("This can't fail"), - eth_cold_key: eth_cold_keypair.ref_to(), - eth_hot_key: eth_bridge_keypair.ref_to(), + let sign_pk = |sk: &common::SecretKey| transactions::SignedPk { + pk: StringEncoded { raw: sk.ref_to() }, + authorization: StringEncoded { + raw: standalone_signature::<_, SerializeWithBorsh>( + sk, + &sk.ref_to(), + ), }, - account_key: account_keypair.ref_to(), - dkg_public_key: dkg_keypair.public(), - non_staked_balance: token::Amount::native_whole(100_000), - // TODO replace with https://github.com/anoma/namada/issues/25) - validator_vp_code_path: vp_user_path.into(), - validator_vp_sha256: Default::default(), }; - validators.push(validator); - // Add other validators with randomly generated keys if needed - for _ in 0..(num_validators - 1) { + for val in 0..(num_validators - 1) { let consensus_keypair: common::SecretKey = testing::gen_keypair::() .try_to_sk() .unwrap(); let account_keypair = consensus_keypair.clone(); - let address = address::gen_established_address("validator account"); + let address = namada::types::address::gen_established_address( + "validator account", + ); let eth_cold_keypair = common::SecretKey::try_from_sk(&secp_eth_cold_keypair).unwrap(); let (protocol_keypair, eth_bridge_keypair, dkg_keypair) = - wallet::defaults::validator_keys(); - let validator = Validator { - pos_data: GenesisValidator { + defaults::validator_keys(); + let alias = Alias::from_str(&format!("validator-{}", val + 1)) + .expect("infallible"); + // add the validator + if let Some(vals) = genesis.transactions.validator_account.as_mut() { + vals.push(chain::FinalizedValidatorAccountTx { address, - tokens: token::Amount::native_whole(200_000), - consensus_key: consensus_keypair.ref_to(), - protocol_key: protocol_keypair.ref_to(), - commission_rate: Dec::new(5, 2).expect("This can't fail"), - max_commission_rate_change: Dec::new(1, 2) - .expect("This can't fail"), - eth_cold_key: eth_cold_keypair.ref_to(), - eth_hot_key: eth_bridge_keypair.ref_to(), - }, - account_key: account_keypair.ref_to(), - dkg_public_key: dkg_keypair.public(), - non_staked_balance: token::Amount::native_whole(100_000), - // TODO replace with https://github.com/anoma/namada/issues/25) - validator_vp_code_path: vp_user_path.into(), - validator_vp_sha256: Default::default(), + tx: transactions::ValidatorAccountTx { + alias: alias.clone(), + dkg_key: StringEncoded { + raw: dkg_keypair.public(), + }, + vp: "vp_validator".to_string(), + commission_rate: Dec::new(5, 2).expect("This can't fail"), + max_commission_rate_change: Dec::new(1, 2) + .expect("This can't fail"), + net_address: SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + 8080, + ), + account_key: sign_pk(&account_keypair), + consensus_key: sign_pk(&consensus_keypair), + protocol_key: sign_pk(&protocol_keypair), + tendermint_node_key: sign_pk(&consensus_keypair), + eth_hot_key: sign_pk(ð_bridge_keypair), + eth_cold_key: sign_pk(ð_cold_keypair), + }, + }) }; - validators.push(validator); - } - - let parameters = Parameters { - epoch_duration: EpochDuration { - min_num_of_blocks: 10, - min_duration: namada::types::time::Duration::seconds(600).into(), - }, - 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(), - implicit_vp_sha256: Default::default(), - max_signatures_per_transaction: 15, - epochs_per_year: 365, /* seconds in yr (60*60*24*365) div seconds - * per epoch (60 = min_duration) */ - pos_gain_p: Dec::new(1, 1).expect("This can't fail"), - pos_gain_d: Dec::new(1, 1).expect("This can't fail"), - staked_ratio: Dec::zero(), - pos_inflation_amount: token::Amount::zero(), - minimum_gas_price: [(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(), - vp_code_path: vp_user_path.into(), - vp_sha256: Default::default(), - public_key: Some(wallet::defaults::albert_keypair().ref_to()), - storage: HashMap::default(), - }; - let bertha = EstablishedAccount { - address: wallet::defaults::bertha_address(), - vp_code_path: vp_user_path.into(), - vp_sha256: Default::default(), - public_key: Some(wallet::defaults::bertha_keypair().ref_to()), - storage: HashMap::default(), - }; - let christel = EstablishedAccount { - address: wallet::defaults::christel_address(), - vp_code_path: vp_user_path.into(), - vp_sha256: Default::default(), - public_key: Some(wallet::defaults::christel_keypair().ref_to()), - storage: HashMap::default(), - }; - let masp = EstablishedAccount { - address: namada::types::address::masp(), - vp_code_path: "vp_masp.wasm".into(), - vp_sha256: Default::default(), - public_key: None, - storage: HashMap::default(), - }; - let implicit_accounts = vec![ - ImplicitAccount { - public_key: wallet::defaults::daewon_keypair().ref_to(), - }, - ImplicitAccount { - public_key: wallet::defaults::ester_keypair().ref_to(), - }, - ]; - let default_user_tokens = Uint::from(1_000_000); - let default_key_tokens = Uint::from(1_000_000); - let mut balances: HashMap = HashMap::from_iter([ - // established accounts' balances - (wallet::defaults::albert_address(), default_user_tokens), - (wallet::defaults::bertha_address(), default_user_tokens), - (wallet::defaults::christel_address(), default_user_tokens), - // implicit accounts' balances - (wallet::defaults::daewon_address(), default_user_tokens), - // implicit accounts derived from public keys balances - ( - bertha.public_key.as_ref().unwrap().into(), - default_key_tokens, - ), - ( - albert.public_key.as_ref().unwrap().into(), - default_key_tokens, - ), - ( - christel.public_key.as_ref().unwrap().into(), - default_key_tokens, - ), - ]); - for validator in &validators { - balances.insert((&validator.account_key).into(), default_key_tokens); - } - - /// Deprecated function, soon to be deleted. Generates default tokens - fn tokens() -> HashMap { - vec![ - (nam(), ("NAM", 6.into())), - (btc(), ("BTC", 8.into())), - (eth(), ("ETH", 18.into())), - (dot(), ("DOT", 10.into())), - (schnitzel(), ("Schnitzel", 6.into())), - (apfel(), ("Apfel", 6.into())), - (kartoffel(), ("Kartoffel", 6.into())), - ] - .into_iter() - .collect() - } - let token_accounts = tokens() - .into_iter() - .map(|(address, (_, denom))| TokenAccount { - address, - denom, - balances: balances - .clone() - .into_iter() - .map(|(k, v)| (k, token::Amount::from_uint(v, denom).unwrap())) - .collect(), - parameters: token::Parameters::default(), - last_inflation: token::Amount::zero(), - last_locked_ratio: Dec::zero(), - }) - .collect(); - Genesis { - genesis_time: DateTimeUtc::now(), - validators, - established_accounts: vec![albert, bertha, christel, masp], - implicit_accounts, - token_accounts, - parameters, - pos_params: OwnedPosParams::default(), - gov_params: GovernanceParameters::default(), - pgf_params: PgfParameters::default(), - ethereum_bridge_params: Some(EthereumBridgeConfig { - erc20_whitelist: vec![Erc20WhitelistEntry { - token_address: DAI_ERC20_ETH_ADDRESS, - token_cap: token::DenominatedAmount { - amount: token::Amount::max(), - denom: 18.into(), + // add the balance to validators implicit key + if let Some(bals) = genesis + .balances + .token + .get_mut(&Alias::from_str("nam").unwrap()) + { + bals.0.insert( + StringEncoded { + raw: account_keypair.ref_to(), }, - }], - eth_start_height: Default::default(), - min_confirmations: Default::default(), - contracts: Contracts { - native_erc20: wnam(), - bridge: UpgradeableContract { - address: EthAddress([0; 20]), - version: Default::default(), + token::DenominatedAmount { + amount: token::Amount::native_whole(200_000), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), }, - }, - }), - native_token: address::nam(), + ); + } + // transfer funds from implicit key to validator + if let Some(trans) = genesis.transactions.transfer.as_mut() { + trans.push(transactions::TransferTx { + token: Alias::from_str("nam").expect("infallible"), + source: StringEncoded { + raw: account_keypair.ref_to(), + }, + target: alias.clone(), + amount: token::DenominatedAmount { + amount: token::Amount::native_whole(200_000), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }, + }) + } + // self bond + if let Some(bonds) = genesis.transactions.bond.as_mut() { + bonds.push(transactions::BondTx { + source: transactions::AliasOrPk::Alias(alias.clone()), + validator: alias, + amount: token::DenominatedAmount { + amount: token::Amount::native_whole(100_000), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }, + }) + } } + + genesis } #[cfg(test)] diff --git a/apps/src/lib/config/genesis/chain.rs b/apps/src/lib/config/genesis/chain.rs new file mode 100644 index 0000000000..0d064789c8 --- /dev/null +++ b/apps/src/lib/config/genesis/chain.rs @@ -0,0 +1,872 @@ +use std::collections::BTreeMap; +use std::path::Path; +use std::str::FromStr; + +use borsh::{BorshDeserialize, BorshSerialize}; +use namada::ledger::parameters::EpochDuration; +use namada_sdk::wallet::store::AddressVpType; +use namada_sdk::wallet::{pre_genesis, Wallet}; +use namada::types::address::{masp, Address, EstablishedAddressGen}; +use namada::types::chain::{ChainId, ChainIdPrefix}; +use namada::types::dec::Dec; +use namada::types::hash::Hash; +use namada::types::time::{DateTimeUtc, DurationNanos, Rfc3339String}; +use namada::types::token::Amount; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use super::toml_utils::{read_toml, write_toml}; +use super::{templates, transactions}; +use crate::config::genesis::templates::Validated; +use crate::config::utils::{set_ip, set_port}; +use crate::config::{Config, TendermintMode}; +use crate::facade::tendermint::node::Id as TendermintNodeId; +use crate::facade::tendermint_config::net::Address as TendermintAddress; +use crate::node::ledger::tendermint_node::id_from_pk; +use crate::wallet::{Alias, CliWalletUtils}; +use crate::wasm_loader; + +pub const METADATA_FILE_NAME: &str = "chain.toml"; + +// Rng source used for generating genesis addresses. Because the process has to +// be deterministic, change of this value is a breaking change for genesis. +const ADDRESS_RNG_SOURCE: &[u8] = &[]; + +impl Finalized { + /// Write all genesis and the chain metadata TOML files to the given + /// directory. + pub fn write_toml_files(&self, output_dir: &Path) -> eyre::Result<()> { + let vps_file = output_dir.join(templates::VPS_FILE_NAME); + let tokens_file = output_dir.join(templates::TOKENS_FILE_NAME); + let balances_file = output_dir.join(templates::BALANCES_FILE_NAME); + let parameters_file = output_dir.join(templates::PARAMETERS_FILE_NAME); + let transactions_file = + output_dir.join(templates::TRANSACTIONS_FILE_NAME); + let metadata_file = output_dir.join(METADATA_FILE_NAME); + + write_toml(&self.vps, &vps_file, "Validity predicates")?; + write_toml(&self.tokens, &tokens_file, "Tokens")?; + write_toml(&self.balances, &balances_file, "Balances")?; + write_toml(&self.parameters, ¶meters_file, "Parameters")?; + write_toml(&self.transactions, &transactions_file, "Transactions")?; + write_toml(&self.metadata, &metadata_file, "Chain metadata")?; + Ok(()) + } + + /// Try to read all genesis and the chain metadata TOML files from the given + /// directory. + pub fn read_toml_files(input_dir: &Path) -> eyre::Result { + let vps_file = input_dir.join(templates::VPS_FILE_NAME); + let tokens_file = input_dir.join(templates::TOKENS_FILE_NAME); + let balances_file = input_dir.join(templates::BALANCES_FILE_NAME); + let parameters_file = input_dir.join(templates::PARAMETERS_FILE_NAME); + let transactions_file = + input_dir.join(templates::TRANSACTIONS_FILE_NAME); + let metadata_file = input_dir.join(METADATA_FILE_NAME); + + let vps = read_toml(&vps_file, "Validity predicates")?; + let tokens = read_toml(&tokens_file, "Tokens")?; + let balances = read_toml(&balances_file, "Balances")?; + let parameters = read_toml(¶meters_file, "Parameters")?; + let transactions = read_toml(&transactions_file, "Transactions")?; + let metadata = read_toml(&metadata_file, "Chain metadata")?; + Ok(Self { + vps, + tokens, + balances, + parameters, + transactions, + metadata, + }) + } + + /// Find the address of the configured native token + pub fn get_native_token(&self) -> &Address { + let alias = &self.parameters.parameters.native_token; + &self + .tokens + .token + .get(alias) + .expect("The native token must exist") + .address + } + + /// Derive Namada wallet from genesis + pub fn derive_wallet( + &self, + base_dir: &Path, + pre_genesis_wallet: Option>, + validator: Option<(Alias, pre_genesis::ValidatorWallet)>, + ) -> Wallet { + let mut wallet = crate::wallet::load_or_new(base_dir); + dbg!(&wallet); + for (alias, config) in &self.tokens.token { + dbg!("add token", alias); + wallet.add_address( + alias.normalize(), + config.address.clone(), + false, + ); + wallet.add_vp_type_to_address( + AddressVpType::Token, + config.address.clone(), + ); + } + if let Some(txs) = &self.transactions.validator_account { + for tx in txs { + wallet.add_address( + tx.tx.alias.normalize(), + tx.address.clone(), + false, + ); + } + } + if let Some(txs) = &self.transactions.established_account { + for tx in txs { + wallet.add_address( + tx.tx.alias.normalize(), + tx.address.clone(), + false, + ); + } + } + if let Some(pre_genesis_wallet) = pre_genesis_wallet { + wallet.extend(pre_genesis_wallet); + } + if let Some((alias, validator_wallet)) = validator { + let address = self + .transactions + .find_validator(&alias) + .map(|tx| tx.address.clone()) + .expect("Validator alias not found in genesis transactions."); + wallet.extend_from_pre_genesis_validator( + address, + alias, + validator_wallet, + ) + } + wallet + } + + /// Derive Namada configuration from genesis + pub fn derive_config( + &self, + base_dir: &Path, + node_mode: TendermintMode, + validator_alias: Option, + allow_duplicate_ip: bool, + ) -> Config { + if node_mode != TendermintMode::Validator && validator_alias.is_some() { + println!( + "Warning: Validator alias used to derive config, but node \ + mode is not validator, it is {node_mode:?}!" + ); + } + let mut config = + Config::new(base_dir, self.metadata.chain_id.clone(), node_mode); + + // Derive persistent peers from genesis + let persistent_peers = self.derive_persistent_peers(); + // If `validator_wallet` is given, find its net_address + let validator_net_and_tm_address = + if let Some(alias) = validator_alias.as_ref() { + self.transactions.find_validator(alias).map(|validator_tx| { + ( + validator_tx.tx.net_address, + validator_tx.derive_tendermint_address(), + ) + }) + } else { + None + }; + // Check if the validators are localhost to automatically turn off + // Tendermint P2P address book strict mode to allow it + let is_localhost = persistent_peers.iter().all(|peer| match peer { + TendermintAddress::Tcp { + peer_id: _, + host, + port: _, + } => matches!(host.as_str(), "127.0.0.1" | "localhost"), + TendermintAddress::Unix { path: _ } => false, + }); + + // Configure the ledger + config.ledger.genesis_time = self.metadata.genesis_time.clone(); + + // Add a ledger P2P persistent peers + config.ledger.cometbft.p2p.persistent_peers = persistent_peers; + config.ledger.cometbft.consensus.timeout_commit = + self.metadata.consensus_timeout_commit.into(); + config.ledger.cometbft.p2p.allow_duplicate_ip = allow_duplicate_ip; + config.ledger.cometbft.p2p.addr_book_strict = !is_localhost; + + if let Some((net_address, tm_address)) = validator_net_and_tm_address { + // Take out address of self from the P2P persistent peers + config.ledger.cometbft.p2p.persistent_peers = config.ledger.cometbft.p2p.persistent_peers.iter() + .filter_map(|peer| + // we do not add the validator in its own persistent peer list + if peer != &tm_address { + Some(peer.to_owned()) + } else { + None + }) + .collect(); + + let first_port = net_address.port(); + if !is_localhost { + set_ip(&mut config.ledger.cometbft.p2p.laddr, "0.0.0.0"); + } + set_port(&mut config.ledger.cometbft.p2p.laddr, first_port); + if !is_localhost { + set_ip(&mut config.ledger.cometbft.rpc.laddr, "0.0.0.0"); + } + set_port(&mut config.ledger.cometbft.rpc.laddr, first_port + 1); + set_port(&mut config.ledger.cometbft.proxy_app, first_port + 2); + + // Validator node should turned off peer exchange reactor + config.ledger.cometbft.p2p.pex = false; + } + + config + } + + /// Derive persistent peers from genesis validators + fn derive_persistent_peers(&self) -> Vec { + self.transactions + .validator_account + .as_ref() + .map(|txs| { + txs.iter() + .map(FinalizedValidatorAccountTx::derive_tendermint_address) + .collect() + }) + .unwrap_or_default() + } + + /// Get the chain parameters set in genesis + pub fn get_chain_parameters( + &self, + wasm_dir: impl AsRef, + ) -> namada::ledger::parameters::Parameters { + let templates::ChainParams { + min_num_of_blocks, + max_expected_time_per_block, + max_proposal_bytes, + vp_whitelist, + tx_whitelist, + implicit_vp, + epochs_per_year, + pos_gain_p, + pos_gain_d, + max_signatures_per_transaction, + fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit, + max_block_gas, + minimum_gas_price, + .. + } = self.parameters.parameters.clone(); + + let implicit_vp_filename = &self + .vps + .wasm + .get(&implicit_vp) + .expect("Implicit VP must be present") + .filename; + let implicit_vp = + wasm_loader::read_wasm(&wasm_dir, implicit_vp_filename) + .expect("Implicit VP WASM code couldn't get read"); + let implicit_vp_code_hash = Hash::sha256(implicit_vp); + + let min_duration: i64 = 60 * 60 * 24 * 365 / (epochs_per_year as i64); + let epoch_duration = EpochDuration { + min_num_of_blocks, + min_duration: namada::types::time::Duration::seconds(min_duration) + .into(), + }; + let max_expected_time_per_block = + namada::types::time::Duration::seconds(max_expected_time_per_block) + .into(); + let vp_whitelist = vp_whitelist.unwrap_or_default(); + let tx_whitelist = tx_whitelist.unwrap_or_default(); + let staked_ratio = Dec::zero(); + let pos_inflation_amount = 0; + + namada::ledger::parameters::Parameters { + epoch_duration, + max_expected_time_per_block, + vp_whitelist, + tx_whitelist, + implicit_vp_code_hash, + epochs_per_year, + pos_gain_p, + pos_gain_d, + staked_ratio, + pos_inflation_amount: Amount::native_whole(pos_inflation_amount), + max_proposal_bytes, + max_signatures_per_transaction, + fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit, + max_block_gas, + minimum_gas_price: minimum_gas_price + .iter() + .map(|(token, amt)| { + ( + self.tokens.token.get(token).cloned().unwrap().address, + amt.amount, + ) + }) + .collect(), + } + } + + pub fn get_pos_params( + &self, + ) -> namada::proof_of_stake::parameters::PosParams { + let templates::PosParams { + max_validator_slots, + pipeline_len, + unbonding_len, + tm_votes_per_token, + block_proposer_reward, + block_vote_reward, + max_inflation_rate, + target_staked_ratio, + duplicate_vote_min_slash_rate, + light_client_attack_min_slash_rate, + cubic_slashing_window_length, + validator_stake_threshold, + } = self.parameters.pos_params.clone(); + + namada::proof_of_stake::parameters::PosParams { + max_validator_slots, + pipeline_len, + unbonding_len, + tm_votes_per_token, + block_proposer_reward, + block_vote_reward, + max_inflation_rate, + target_staked_ratio, + duplicate_vote_min_slash_rate, + light_client_attack_min_slash_rate, + cubic_slashing_window_length, + validator_stake_threshold, + } + } + + pub fn get_gov_params( + &self, + ) -> namada::core::ledger::governance::parameters::GovernanceParameters + { + let templates::GovernanceParams { + min_proposal_fund, + max_proposal_code_size, + min_proposal_voting_period, + max_proposal_period, + max_proposal_content_size, + min_proposal_grace_epochs, + } = self.parameters.gov_params.clone(); + namada::core::ledger::governance::parameters::GovernanceParameters { + min_proposal_fund: Amount::native_whole(min_proposal_fund), + max_proposal_code_size, + max_proposal_period, + max_proposal_content_size, + min_proposal_grace_epochs, + min_proposal_voting_period, + } + } + + pub fn get_pgf_params( + &self, + ) -> namada::core::ledger::pgf::parameters::PgfParameters { + self.parameters.pgf_params.clone() + } + + pub fn get_eth_bridge_params( + &self, + ) -> Option { + if let Some(templates::EthBridgeParams { + eth_start_height, + min_confirmations, + contracts, + erc20_whitelist, + }) = self.parameters.eth_bridge_params.clone() + { + Some(namada::ledger::eth_bridge::EthereumBridgeParams { + eth_start_height, + min_confirmations, + erc20_whitelist, + contracts, + }) + } else { + None + } + } + + pub fn get_token_address(&self, alias: &Alias) -> Option<&Address> { + self.tokens.token.get(alias).map(|token| &token.address) + } + + pub fn get_user_address(&self, alias: &Alias) -> Option
{ + if alias.to_string() == *"masp" { + return Some(masp()); + } + let established = self.transactions.established_account.as_ref()?; + let validators = self.transactions.validator_account.as_ref()?; + established + .iter() + .find_map(|tx| { + (&tx.tx.alias == alias).then_some(tx.address.clone()) + }) + .or_else(|| { + validators.iter().find_map(|tx| { + (&tx.tx.alias == alias).then_some(tx.address.clone()) + }) + }) + } + + pub fn get_validator_address(&self, alias: &Alias) -> Option<&Address> { + let validators = self.transactions.validator_account.as_ref()?; + validators + .iter() + .find_map(|tx| (&tx.tx.alias == alias).then_some(&tx.address)) + } +} + +/// Create the [`Finalized`] chain configuration. Derives the chain ID from the +/// genesis bytes and assigns addresses to tokens and transactions that +/// initialize established accounts. +/// +/// Invariant: The output must deterministic. For the same input this function +/// must return the same output. +pub fn finalize( + templates: templates::All, + chain_id_prefix: ChainIdPrefix, + genesis_time: DateTimeUtc, + consensus_timeout_commit: crate::facade::tendermint::Timeout, +) -> Finalized { + let genesis_time: Rfc3339String = genesis_time.into(); + let consensus_timeout_commit: DurationNanos = + consensus_timeout_commit.into(); + + // Derive seed for address generator + let genesis_to_gen_address = GenesisToGenAddresses { + templates, + metadata: Metadata { + chain_id: chain_id_prefix.clone(), + genesis_time, + consensus_timeout_commit, + address_gen: None, + }, + }; + let genesis_bytes = genesis_to_gen_address.try_to_vec().unwrap(); + let mut addr_gen = established_address_gen(&genesis_bytes); + + // Generate addresses + let templates::All { + vps, + tokens, + balances, + parameters, + transactions, + } = genesis_to_gen_address.templates; + let tokens = FinalizedTokens::finalize_from(tokens, &mut addr_gen); + let transactions = + FinalizedTransactions::finalize_from(transactions, &mut addr_gen); + let parameters = + FinalizedParameters::finalize_from(&transactions, parameters); + + // Store the last state of the address generator in the metadata + let mut metadata = genesis_to_gen_address.metadata; + metadata.address_gen = Some(addr_gen); + + // Derive chain ID + let to_finalize = ToFinalize { + metadata, + vps, + tokens, + balances, + parameters, + transactions, + }; + let to_finalize_bytes = to_finalize.try_to_vec().unwrap(); + let chain_id = ChainId::from_genesis(chain_id_prefix, to_finalize_bytes); + + // Construct the `Finalized` chain + let ToFinalize { + vps, + tokens, + balances, + parameters, + transactions, + metadata, + } = to_finalize; + let Metadata { + chain_id: _, + genesis_time, + consensus_timeout_commit, + address_gen, + } = metadata; + let metadata = Metadata { + chain_id, + genesis_time, + consensus_timeout_commit, + address_gen, + }; + Finalized { + metadata, + vps, + tokens, + balances, + parameters, + transactions, + } +} + +/// Use bytes as a deterministic seed for address generator. +fn established_address_gen(bytes: &[u8]) -> EstablishedAddressGen { + let mut hasher = Sha256::new(); + hasher.update(bytes); + // hex of the first 40 chars of the hash + let hash = format!("{:.width$X}", hasher.finalize(), width = 40); + EstablishedAddressGen::new(hash) +} + +/// Deterministically generate an [`Address`]. +fn gen_address(gen: &mut EstablishedAddressGen) -> Address { + gen.generate_address(ADDRESS_RNG_SOURCE) +} + +/// Chain genesis config to be finalized. This struct is used to derive the +/// chain ID to construct a [`Finalized`] chain genesis config. +#[derive( + Clone, Debug, Deserialize, Serialize, BorshDeserialize, BorshSerialize, +)] +pub struct GenesisToGenAddresses { + /// Filled-in templates + pub templates: templates::All, + /// Chain metadata + pub metadata: Metadata, +} + +/// Chain genesis config to be finalized. This struct is used to derive the +/// chain ID to construct a [`Finalized`] chain genesis config. +pub type ToFinalize = Chain; + +/// Chain genesis config. +pub type Finalized = Chain; + +/// Chain genesis config with generic chain ID. +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Chain { + pub vps: templates::ValidityPredicates, + pub tokens: FinalizedTokens, + pub balances: templates::DenominatedBalances, + pub parameters: FinalizedParameters, + pub transactions: FinalizedTransactions, + /// Chain metadata + pub metadata: Metadata, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct FinalizedTokens { + pub token: BTreeMap, +} + +impl FinalizedTokens { + fn finalize_from( + tokens: templates::Tokens, + addr_gen: &mut EstablishedAddressGen, + ) -> FinalizedTokens { + let templates::Tokens { token } = tokens; + let token = token + .into_iter() + .map(|(key, config)| { + let address = gen_address(addr_gen); + (key, FinalizedTokenConfig { address, config }) + }) + .collect(); + FinalizedTokens { token } + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct FinalizedTokenConfig { + pub address: Address, + #[serde(flatten)] + pub config: templates::TokenConfig, +} + +#[derive( + Clone, + Debug, + Default, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct FinalizedTransactions { + pub established_account: Option>, + pub validator_account: Option>, + pub transfer: Option>>, + pub bond: Option>>, +} + +impl FinalizedTransactions { + fn finalize_from( + transactions: transactions::Transactions, + addr_gen: &mut EstablishedAddressGen, + ) -> FinalizedTransactions { + let transactions::Transactions { + established_account, + validator_account, + transfer, + bond, + } = transactions; + let established_account = established_account.map(|txs| { + txs.into_iter() + .map(|tx| { + let address = gen_address(addr_gen); + FinalizedEstablishedAccountTx { address, tx } + }) + .collect() + }); + let validator_account = validator_account.map(|txs| { + txs.into_iter() + .map(|tx| { + let address = gen_address(addr_gen); + FinalizedValidatorAccountTx { address, tx } + }) + .collect() + }); + FinalizedTransactions { + established_account, + validator_account, + transfer, + bond, + } + } + + fn find_validator( + &self, + alias: &Alias, + ) -> Option<&FinalizedValidatorAccountTx> { + self.validator_account + .as_ref() + .and_then(|txs| txs.iter().find(|tx| &tx.tx.alias == alias)) + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct FinalizedParameters { + pub parameters: templates::ChainParams, + pub pos_params: templates::PosParams, + pub gov_params: templates::GovernanceParams, + pub pgf_params: namada::core::ledger::pgf::parameters::PgfParameters, + pub eth_bridge_params: Option, +} + +impl FinalizedParameters { + fn finalize_from( + txs: &FinalizedTransactions, + templates::Parameters { + parameters, + pos_params, + gov_params, + pgf_params, + eth_bridge_params, + }: templates::Parameters, + ) -> Self { + use namada::core::ledger::pgf::parameters::PgfParameters; + let mut finalized_pgf_params = PgfParameters { + stewards: Default::default(), + pgf_inflation_rate: pgf_params.pgf_inflation_rate, + stewards_inflation_rate: pgf_params.stewards_inflation_rate, + }; + finalized_pgf_params.stewards = pgf_params + .stewards + .into_iter() + .map(|alias| { + let maybe_estbd = txs + .established_account + .as_ref() + .unwrap() + .iter() + .find(|tx| tx.tx.alias == alias) + .map(|tx| tx.address.clone()); + let maybe_validator = txs + .validator_account + .as_ref() + .unwrap() + .iter() + .find(|tx| tx.tx.alias == alias) + .map(|tx| tx.address.clone()); + maybe_estbd.or(maybe_validator).unwrap() + }) + .collect(); + Self { + parameters, + pos_params, + gov_params, + pgf_params: finalized_pgf_params, + eth_bridge_params, + } + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct FinalizedEstablishedAccountTx { + pub address: Address, + #[serde(flatten)] + pub tx: transactions::SignedEstablishedAccountTx, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct FinalizedValidatorAccountTx { + pub address: Address, + #[serde(flatten)] + pub tx: transactions::SignedValidatorAccountTx, +} + +impl FinalizedValidatorAccountTx { + pub fn derive_tendermint_address(&self) -> TendermintAddress { + // Derive the node ID from the node key + let node_id: TendermintNodeId = + id_from_pk(&self.tx.tendermint_node_key.pk.raw); + + // Build the list of persistent peers from the validators' node IDs + TendermintAddress::from_str(&format!( + "{}@{}", + node_id, self.tx.net_address, + )) + .expect("Validator address must be valid") + } +} + +/// Chain metadata +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Metadata { + /// Chain ID in [`Finalized`] or chain ID prefix in + /// [`GenesisToGenAddresses`] and [`ToFinalize`]. + pub chain_id: ID, + // Genesis timestamp + pub genesis_time: Rfc3339String, + /// The Tendermint consensus timeout_commit configuration + pub consensus_timeout_commit: DurationNanos, + /// This generator should be used to initialize the ledger for the + /// next address that will be generated on chain. + /// + /// The value is expected to always be `None` in [`GenesisToGenAddresses`] + /// and `Some` in [`ToFinalize`] and [`Finalized`]. + pub address_gen: Option, +} + +#[cfg(test)] +mod test { + use std::path::PathBuf; + use std::str::FromStr; + + use super::*; + + /// Test that the [`finalize`] returns deterministic output with the same + /// chain ID for the same input. + #[test] + fn test_finalize_is_deterministic() { + // Load the localnet templates + let templates_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("genesis/localnet"); + let templates = templates::load_and_validate(&templates_dir).unwrap(); + + let chain_id_prefix: ChainIdPrefix = + FromStr::from_str("test-prefix").unwrap(); + + let genesis_time = + DateTimeUtc::from_str("2021-12-31T00:00:00Z").unwrap(); + + let consensus_timeout_commit = + crate::facade::tendermint::Timeout::from_str("1s").unwrap(); + + let finalized_0 = finalize( + templates.clone(), + chain_id_prefix.clone(), + genesis_time, + consensus_timeout_commit, + ); + + let finalized_1 = finalize( + templates, + chain_id_prefix, + genesis_time, + consensus_timeout_commit, + ); + + pretty_assertions::assert_eq!(finalized_0, finalized_1); + } +} diff --git a/apps/src/lib/config/genesis/templates.rs b/apps/src/lib/config/genesis/templates.rs new file mode 100644 index 0000000000..df01fc2fdc --- /dev/null +++ b/apps/src/lib/config/genesis/templates.rs @@ -0,0 +1,991 @@ +//! The templates for balances, parameters and VPs used for a chain's genesis. + +use std::collections::{BTreeMap, BTreeSet}; +use std::marker::PhantomData; +use std::path::Path; + +use borsh::{BorshDeserialize, BorshSerialize}; +use namada::core::types::key::common; +use namada::core::types::string_encoding::StringEncoded; +use namada::core::types::{ethereum_structs, token}; +use namada::eth_bridge::parameters::{ + Contracts, Erc20WhitelistEntry, MinimumConfirmations, +}; +use namada::types::chain::ProposalBytes; +use namada::types::dec::Dec; +use namada::types::token::{ + Amount, DenominatedAmount, Denomination, NATIVE_MAX_DECIMAL_PLACES, +}; +use serde::{Deserialize, Serialize}; + +use super::toml_utils::{read_toml, write_toml}; +use super::transactions::{self, Transactions}; +use crate::config::genesis::transactions::{ + BondTx, SignedBondTx, SignedTransferTx, TransferTx, +}; +use crate::wallet::Alias; + +pub const BALANCES_FILE_NAME: &str = "balances.toml"; +pub const PARAMETERS_FILE_NAME: &str = "parameters.toml"; +pub const VPS_FILE_NAME: &str = "validity-predicates.toml"; +pub const TOKENS_FILE_NAME: &str = "tokens.toml"; +pub const TRANSACTIONS_FILE_NAME: &str = "transactions.toml"; + +const MAX_TOKEN_BALANCE_SUM: u64 = i64::MAX as u64; + +/// Note that these balances must be crossed-checked with the token configs +/// to correctly represent the underlying amounts. +pub fn read_balances(path: &Path) -> eyre::Result { + read_toml(path, "Balances") +} + +pub fn read_parameters(path: &Path) -> eyre::Result> { + read_toml(path, "Parameters") +} + +pub fn read_validity_predicates( + path: &Path, +) -> eyre::Result { + read_toml(path, "Validity predicates") +} + +pub fn read_tokens(path: &Path) -> eyre::Result { + read_toml(path, "Tokens") +} + +pub fn read_transactions( + path: &Path, +) -> eyre::Result> { + read_toml(path, "Transactions") +} + +/// Genesis balances of all tokens +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct UndenominatedBalances { + pub token: BTreeMap, +} + +impl UndenominatedBalances { + /// Use the denom in `TokenConfig` to correctly interpret the balances + /// to the right denomination. + pub fn denominate( + self, + tokens: &Tokens, + ) -> eyre::Result { + let mut balances = DenominatedBalances { + token: BTreeMap::new(), + }; + for (alias, bals) in self.token { + let denom = tokens + .token + .get(&alias) + .ok_or_else(|| { + eyre::eyre!( + "A balance of token {} was found, but this token was \ + not found in the `tokens.toml` file", + alias + ) + })? + .denom; + let mut denominated_bals = BTreeMap::new(); + for (pk, bal) in bals.0.into_iter() { + let denominated = bal.increase_precision(denom)?; + denominated_bals.insert(pk, denominated); + } + balances + .token + .insert(alias, TokenBalances(denominated_bals)); + } + Ok(balances) + } +} + +/// Genesis balances of all tokens +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct DenominatedBalances { + pub token: BTreeMap, +} + +/// Genesis balances for a given token +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct RawTokenBalances( + pub BTreeMap, token::DenominatedAmount>, +); + +/// Genesis balances for a given token +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct TokenBalances( + pub BTreeMap, token::DenominatedAmount>, +); + +/// Genesis validity predicates +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct ValidityPredicates { + // Wasm definitions + pub wasm: BTreeMap, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct WasmVpConfig { + pub filename: String, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Tokens { + pub token: BTreeMap, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct TokenConfig { + pub denom: Denomination, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Parameters { + pub parameters: ChainParams, + pub pos_params: PosParams, + pub gov_params: GovernanceParams, + pub pgf_params: PgfParams, + pub eth_bridge_params: Option, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct ChainParams { + /// Name of the native token - this must one of the tokens from + /// `tokens.toml` file + pub native_token: Alias, + /// Minimum number of blocks per epoch. + // TODO: u64 only works with values up to i64::MAX with toml-rs! + pub min_num_of_blocks: u64, + /// Maximum duration per block (in seconds). + // TODO: this is i64 because datetime wants it + pub max_expected_time_per_block: i64, + /// Max payload size, in bytes, for a tx batch proposal. + /// + /// Block proposers may never return a `PrepareProposal` + /// response containing `txs` with a byte length greater + /// than whatever is configured through this parameter. + /// + /// Note that this parameter's value will always be strictly + /// smaller than a Tendermint block's `MaxBytes` consensus + /// parameter. Currently, we hard cap `max_proposal_bytes` + /// at 90 MiB in Namada, which leaves at least 10 MiB of + /// room for header data, evidence and protobuf + /// serialization overhead in Tendermint blocks. + pub max_proposal_bytes: ProposalBytes, + /// Hashes of whitelisted vps array. `None` value or an empty array + /// disables whitelisting. + pub vp_whitelist: Option>, + /// Hashes of whitelisted txs array. `None` value or an empty array + /// disables whitelisting. + pub tx_whitelist: Option>, + /// Filename of implicit accounts validity predicate WASM code + pub implicit_vp: String, + /// Expected number of epochs per year + pub epochs_per_year: u64, + /// PoS gain p + pub pos_gain_p: Dec, + /// PoS gain d + pub pos_gain_d: Dec, + /// Maximum number of signature per transaction + pub max_signatures_per_transaction: u8, + /// Max gas for block + pub max_block_gas: u64, + /// 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 minimum_gas_price: T::GasMinimums, +} + +impl ChainParams { + pub fn denominate( + self, + tokens: &Tokens, + ) -> eyre::Result> { + let ChainParams { + native_token, + min_num_of_blocks, + max_expected_time_per_block, + max_proposal_bytes, + vp_whitelist, + tx_whitelist, + implicit_vp, + epochs_per_year, + pos_gain_p, + pos_gain_d, + max_signatures_per_transaction, + max_block_gas, + fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit, + minimum_gas_price, + } = self; + let mut min_gas_prices = BTreeMap::default(); + for (token, amount) in minimum_gas_price.into_iter() { + let denom = if let Some(TokenConfig { denom, .. }) = + tokens.token.get(&token) + { + *denom + } else { + eprintln!( + "Genesis files contained minimum gas amount of token {}, \ + which is not in the `tokens.toml` file", + token + ); + return Err(eyre::eyre!( + "Genesis files contained minimum gas amount of token {}, \ + which is not in the `tokens.toml` file", + token + )); + }; + let amount = amount.increase_precision(denom).map_err(|e| { + eprintln!( + "A minimum gas amount in the parameters.toml file was \ + incorrectly formatted:\n{}", + e + ); + e + })?; + min_gas_prices.insert(token, amount); + } + + Ok(ChainParams { + native_token, + min_num_of_blocks, + max_expected_time_per_block, + max_proposal_bytes, + vp_whitelist, + tx_whitelist, + implicit_vp, + epochs_per_year, + pos_gain_p, + pos_gain_d, + max_signatures_per_transaction, + max_block_gas, + fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit, + minimum_gas_price: min_gas_prices, + }) + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct PosParams { + /// Maximum number of active validators. + pub max_validator_slots: u64, + /// Pipeline length (in epochs). + pub pipeline_len: u64, + /// Unbonding length (in epochs). + pub unbonding_len: u64, + /// Votes per token. + pub tm_votes_per_token: Dec, + /// Reward for proposing a block. + pub block_proposer_reward: Dec, + /// Reward for voting on a block. + pub block_vote_reward: Dec, + /// Maximum staking APY + pub max_inflation_rate: Dec, + /// Target ratio of staked NAM tokens to total NAM tokens + pub target_staked_ratio: Dec, + /// Portion of a validator's stake that should be slashed on a + /// duplicate vote. + pub duplicate_vote_min_slash_rate: Dec, + /// Portion of a validator's stake that should be slashed on a + /// light client attack. + pub light_client_attack_min_slash_rate: Dec, + /// Number of epochs above and below (separately) the current epoch to + /// consider when doing cubic slashing + pub cubic_slashing_window_length: u64, + /// The minimum amount of bonded tokens that a validator needs to be in + /// either the `consensus` or `below_capacity` validator sets + pub validator_stake_threshold: token::Amount, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct GovernanceParams { + /// Min funds to stake to submit a proposal + pub min_proposal_fund: u64, + /// Maximum size of proposal in kibibytes (KiB) + pub max_proposal_code_size: u64, + /// Minimum proposal period length in epochs + pub min_proposal_voting_period: u64, + /// Maximum proposal period length in epochs + pub max_proposal_period: u64, + /// Maximum number of characters in the proposal content + pub max_proposal_content_size: u64, + /// Minimum number of epoch between end and grace epoch + pub min_proposal_grace_epochs: u64, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct PgfParams { + /// The set of stewards + pub stewards: BTreeSet, + /// The pgf funding inflation rate + pub pgf_inflation_rate: Dec, + /// The pgf stewards inflation rate + pub stewards_inflation_rate: Dec, + #[serde(default)] + #[serde(skip_serializing)] + #[cfg(test)] + pub valid: PhantomData, + #[serde(default)] + #[serde(skip_serializing)] + #[cfg(not(test))] + valid: PhantomData, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct EthBridgeParams { + /// Initial Ethereum block height when events will first be extracted from. + pub eth_start_height: ethereum_structs::BlockHeight, + /// Minimum number of confirmations needed to trust an Ethereum branch. + /// This must be at least one. + pub min_confirmations: MinimumConfirmations, + /// The addresses of the Ethereum contracts that need to be directly known + /// by validators. + pub contracts: Contracts, + /// List of ERC20 token types whitelisted at genesis time. + pub erc20_whitelist: Vec, +} + +impl TokenBalances { + pub fn get(&self, pk: common::PublicKey) -> Option { + let pk = StringEncoded { raw: pk }; + self.0.get(&pk).map(|amt| amt.amount) + } +} +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Unvalidated {} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Validated {} + +pub trait TemplateValidation: Serialize { + type Amount: for<'a> Deserialize<'a> + + Serialize + + Clone + + std::fmt::Debug + + BorshSerialize + + BorshDeserialize + + PartialEq + + Eq; + type Balances: for<'a> Deserialize<'a> + + Serialize + + Clone + + std::fmt::Debug + + BorshSerialize + + BorshDeserialize + + PartialEq + + Eq; + type TransferTx: for<'a> Deserialize<'a> + + Serialize + + Clone + + std::fmt::Debug + + BorshSerialize + + BorshDeserialize + + PartialEq + + Eq; + type BondTx: for<'a> Deserialize<'a> + + Serialize + + Clone + + std::fmt::Debug + + BorshSerialize + + BorshDeserialize + + PartialEq + + Eq; + type GasMinimums: for<'a> Deserialize<'a> + + Serialize + + Clone + + std::fmt::Debug + + BorshSerialize + + BorshDeserialize + + PartialEq + + Eq; +} + +impl TemplateValidation for Unvalidated { + type Amount = token::DenominatedAmount; + type Balances = UndenominatedBalances; + type BondTx = SignedBondTx; + type GasMinimums = BTreeMap; + type TransferTx = SignedTransferTx; +} + +impl TemplateValidation for Validated { + type Amount = token::DenominatedAmount; + type Balances = DenominatedBalances; + type BondTx = BondTx; + type GasMinimums = BTreeMap; + type TransferTx = TransferTx; +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct All { + pub vps: ValidityPredicates, + pub tokens: Tokens, + pub balances: T::Balances, + pub parameters: Parameters, + pub transactions: Transactions, +} + +impl All { + pub fn write_toml_files(&self, output_dir: &Path) -> eyre::Result<()> { + let All { + vps, + tokens, + balances, + parameters, + transactions, + } = self; + + let vps_file = output_dir.join(VPS_FILE_NAME); + let tokens_file = output_dir.join(TOKENS_FILE_NAME); + let balances_file = output_dir.join(BALANCES_FILE_NAME); + let parameters_file = output_dir.join(PARAMETERS_FILE_NAME); + let transactions_file = output_dir.join(TRANSACTIONS_FILE_NAME); + + write_toml(vps, &vps_file, "Validity predicates")?; + write_toml(tokens, &tokens_file, "Tokens")?; + write_toml(balances, &balances_file, "Balances")?; + write_toml(parameters, ¶meters_file, "Parameters")?; + write_toml(transactions, &transactions_file, "Transactions")?; + Ok(()) + } +} + +impl All { + pub fn read_toml_files(input_dir: &Path) -> eyre::Result { + let vps_file = input_dir.join(VPS_FILE_NAME); + let tokens_file = input_dir.join(TOKENS_FILE_NAME); + let balances_file = input_dir.join(BALANCES_FILE_NAME); + let parameters_file = input_dir.join(PARAMETERS_FILE_NAME); + let transactions_file = input_dir.join(TRANSACTIONS_FILE_NAME); + + let vps = read_toml(&vps_file, "Validity predicates")?; + let tokens = read_toml(&tokens_file, "Tokens")?; + let balances = read_toml(&balances_file, "Balances")?; + let parameters = read_toml(¶meters_file, "Parameters")?; + let transactions = read_toml(&transactions_file, "Transactions")?; + Ok(Self { + vps, + tokens, + balances, + parameters, + transactions, + }) + } +} + +/// Load genesis templates from the given directory and validate them. Returns +/// `None` when there are some validation issues. +/// +/// Note that the validation rules for these templates won't enforce that there +/// is at least one validator with positive voting power. This must be checked +/// when the templates are being used to `init-network`. +pub fn load_and_validate(templates_dir: &Path) -> Option> { + let mut is_valid = true; + // We don't reuse `All::read_toml_files` here to allow to validate config + // without all files present. + let vps_file = templates_dir.join(VPS_FILE_NAME); + let tokens_file = templates_dir.join(TOKENS_FILE_NAME); + let balances_file = templates_dir.join(BALANCES_FILE_NAME); + let parameters_file = templates_dir.join(PARAMETERS_FILE_NAME); + let transactions_file = templates_dir.join(TRANSACTIONS_FILE_NAME); + + // Check that all required files are present + let mut check_file_exists = |file: &Path, name: &str| { + if !file.exists() { + is_valid = false; + eprintln!("{name} file is missing at {}", file.to_string_lossy()); + } + }; + check_file_exists(&vps_file, "Validity predicates"); + check_file_exists(&tokens_file, "Tokens"); + check_file_exists(&balances_file, "Balances"); + check_file_exists(¶meters_file, "Parameters"); + check_file_exists(&transactions_file, "Transactions"); + + // Load and parse the files + let vps = read_validity_predicates(&vps_file); + let tokens = read_tokens(&tokens_file); + let balances = read_balances(&balances_file); + let parameters = read_parameters(¶meters_file); + let transactions = read_transactions(&transactions_file); + + let eprintln_invalid_file = |err: &eyre::Report, name: &str| { + eprintln!("{name} file is NOT valid. Failed to read with: {err}"); + }; + + // Check the parsing results + let vps = vps.map_or_else( + |err| { + eprintln_invalid_file(&err, "Validity predicates"); + None + }, + Some, + ); + let tokens = tokens.map_or_else( + |err| { + eprintln_invalid_file(&err, "Tokens"); + None + }, + Some, + ); + let balances = balances.map_or_else( + |err| { + eprintln_invalid_file(&err, "Balances"); + None + }, + Some, + ); + let parameters = parameters.map_or_else( + |err| { + eprintln_invalid_file(&err, "Parameters"); + None + }, + Some, + ); + let transactions = transactions.map_or_else( + |err| { + eprintln_invalid_file(&err, "Transactions"); + None + }, + Some, + ); + + // Validate each file that could be loaded + if let Some(vps) = vps.as_ref() { + if validate_vps(vps) { + println!("Validity predicates file is valid."); + } else { + is_valid = false; + } + } + + let parameters = parameters.and_then(|params| { + let params = + validate_parameters(params, &tokens, &transactions, vps.as_ref()); + if params.is_some() { + println!("Parameters file is valid."); + } else { + is_valid = false + } + params + }); + + let balances = if let Some(tokens) = tokens.as_ref() { + if tokens.token.is_empty() { + is_valid = false; + eprintln!( + "Tokens file is invalid. There has to be at least one token." + ); + } + println!("Tokens file is valid."); + balances + .and_then(|raw| raw.denominate(tokens).ok()) + .and_then(|balances| { + validate_balances(&balances, Some(tokens)).then(|| { + println!("Balances file is valid."); + balances + }) + }) + } else { + None + }; + if balances.is_none() { + is_valid = false; + } + + let txs = if let Some(tokens) = tokens.as_ref() { + if let Some(txs) = transactions.and_then(|txs| { + transactions::validate( + txs, + vps.as_ref(), + balances.as_ref(), + tokens, + parameters.as_ref(), + ) + }) { + println!("Transactions file is valid."); + Some(txs) + } else { + is_valid = false; + None + } + } else { + is_valid = false; + None + }; + + match vps { + Some(vps) if is_valid => Some(All { + vps, + tokens: tokens.unwrap(), + balances: balances.unwrap(), + parameters: parameters.unwrap(), + transactions: txs.unwrap(), + }), + _ => None, + } +} + +pub fn validate_vps(vps: &ValidityPredicates) -> bool { + let mut is_valid = true; + vps.wasm.iter().for_each(|(name, config)| { + if !config.filename.ends_with(".wasm") { + eprintln!( + "Invalid validity predicate \"{name}\" configuration. Only \ + \".wasm\" filenames are currently supported." + ); + is_valid = false; + } + }); + is_valid +} + +pub fn validate_parameters( + parameters: Parameters, + tokens: &Option, + transactions: &Option>, + vps: Option<&ValidityPredicates>, +) -> Option> { + let tokens = tokens.as_ref()?; + let txs = transactions.as_ref()?; + let mut is_valid = true; + let implicit_vp = ¶meters.parameters.implicit_vp; + if !vps + .map(|vps| vps.wasm.contains_key(implicit_vp)) + .unwrap_or_default() + { + eprintln!( + "Implicit VP \"{implicit_vp}\" not found in the Validity \ + predicates files." + ); + is_valid = false; + } + // check that each PGF steward has an established account + for steward in ¶meters.pgf_params.stewards { + let mut found_steward = false; + if let Some(accs) = &txs.established_account { + if accs.iter().any(|acct| acct.alias == *steward) { + found_steward = true; + } + } + + if let Some(accs) = &txs.validator_account { + if accs.iter().any(|acct| acct.alias == *steward) { + found_steward = true; + } + } + is_valid = found_steward && is_valid; + if !is_valid { + eprintln!( + "Could not find an established or validator account \ + associated with the PGF steward {}", + steward + ); + } + } + let Parameters { + parameters, + pos_params, + gov_params, + pgf_params, + eth_bridge_params, + } = parameters; + match parameters.denominate(tokens) { + Err(e) => { + eprintln!("{}", e); + None + } + Ok(parameters) => is_valid.then(|| Parameters { + parameters, + pos_params, + gov_params, + pgf_params: PgfParams { + stewards: pgf_params.stewards, + pgf_inflation_rate: pgf_params.pgf_inflation_rate, + stewards_inflation_rate: pgf_params.stewards_inflation_rate, + valid: Default::default(), + }, + eth_bridge_params, + }), + } +} + +pub fn validate_balances( + balances: &DenominatedBalances, + tokens: Option<&Tokens>, +) -> bool { + let mut is_valid = true; + use std::str::FromStr; + let native_alias = Alias::from_str("nam").expect("Infalllible"); + balances.token.iter().for_each(|(token, next)| { + // Every token alias used in Balances file must be present in + // the Tokens file + if !tokens + .as_ref() + .map(|tokens| tokens.token.contains_key(token)) + .unwrap_or_default() + { + is_valid = false; + eprintln!( + "Token \"{token}\" from the Balances file is not present in \ + the Tokens file." + ) + } + + // Check the sum of balances + let sum = next.0.values().try_fold( + token::Amount::default(), + |acc, amount| { + let res = acc.checked_add(amount.amount); + if res.as_ref().is_none() { + is_valid = false; + eprintln!( + "Balances for token {token} overflow `token::Amount`" + ); + } + res + }, + ); + if sum.is_none() + || (*token == native_alias + && sum.unwrap() + > Amount::from_uint( + MAX_TOKEN_BALANCE_SUM, + NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap()) + { + eprintln!( + "The sum of balances for token {token} is greater than \ + {MAX_TOKEN_BALANCE_SUM}" + ); + is_valid = false; + } + }); + is_valid +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + + use namada::core::types::key; + use namada::types::key::RefTo; + use tempfile::tempdir; + + use super::*; + + /// Validate the `genesis/localnet` genesis templates. + #[test] + fn test_validate_localnet_genesis_templates() { + let templates_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("genesis/localnet"); + assert!( + load_and_validate(&templates_dir).is_some(), + "Localnet genesis templates must be valid" + ); + } + + /// Validate the `genesis/starter` genesis templates. + #[test] + fn test_validate_starter_genesis_templates() { + let templates_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("genesis/starter"); + assert!( + load_and_validate(&templates_dir).is_some(), + "Starter genesis templates must be valid" + ); + } + + #[test] + fn test_read_balances() { + let test_dir = tempdir().unwrap(); + let path = test_dir.path().join(BALANCES_FILE_NAME); + let sk = key::testing::keypair_1(); + let pk = sk.ref_to(); + let balance = token::Amount::from(101_000_001); + let token_alias = Alias::from("Some_token".to_string()); + let contents = format!( + r#" + [token.{token_alias}] + {pk} = "{}" + "#, + balance.to_string_native() + ); + fs::write(&path, contents).unwrap(); + + let balances = read_balances(&path).unwrap(); + let example_balance = balances.token.get(&token_alias).unwrap(); + assert_eq!( + balance, + example_balance + .0 + .get(&StringEncoded { raw: pk }) + .unwrap() + .amount + ); + } +} diff --git a/apps/src/lib/config/genesis/toml_utils.rs b/apps/src/lib/config/genesis/toml_utils.rs new file mode 100644 index 0000000000..e699341951 --- /dev/null +++ b/apps/src/lib/config/genesis/toml_utils.rs @@ -0,0 +1,38 @@ +use std::path::Path; + +use eyre::Context; +use serde::de::DeserializeOwned; +use serde::Serialize; + +pub fn read_toml( + path: &Path, + which_file: &str, +) -> eyre::Result { + let file_contents = std::fs::read_to_string(path).wrap_err_with(|| { + format!( + "Couldn't read {which_file} config file from {}", + path.to_string_lossy() + ) + })?; + toml::from_str(&file_contents).wrap_err_with(|| { + format!( + "Couldn't parse {which_file} TOML from {}", + path.to_string_lossy() + ) + }) +} + +pub fn write_toml( + data: &T, + path: &Path, + which_file: &str, +) -> eyre::Result<()> { + let file_contents = toml::to_vec(data) + .wrap_err_with(|| format!("Couldn't format {which_file} to TOML."))?; + std::fs::write(path, file_contents).wrap_err_with(|| { + format!( + "Couldn't write {which_file} TOML to {}", + path.to_string_lossy() + ) + }) +} diff --git a/apps/src/lib/config/genesis/transactions.rs b/apps/src/lib/config/genesis/transactions.rs new file mode 100644 index 0000000000..021a895c40 --- /dev/null +++ b/apps/src/lib/config/genesis/transactions.rs @@ -0,0 +1,1483 @@ +//! Genesis transactions + +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::fmt::{Debug, Display, Formatter}; +use std::net::SocketAddr; +use std::str::FromStr; + +use borsh::{BorshDeserialize, BorshSerialize}; +use namada::core::types::storage; +use namada::core::types::string_encoding::StringEncoded; +use namada::proto::{ + standalone_signature, verify_standalone_sig, SerializeWithBorsh, +}; +use namada::types::dec::Dec; +use namada::types::key::dkg_session_keys::DkgPublicKey; +use namada::types::key::{common, RefTo, VerifySigError}; +use namada::types::time::{DateTimeUtc, MIN_UTC}; +use namada::types::token; +use namada::types::token::{DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES}; +use namada_sdk::wallet::pre_genesis::ValidatorWallet; +use namada_sdk::wallet::{FindKeyError, Wallet, WalletUtils}; +use serde::{Deserialize, Serialize}; + +use super::templates::{ + DenominatedBalances, Parameters, TokenBalances, ValidityPredicates, +}; +use crate::config::genesis::templates::{ + TemplateValidation, Tokens, Unvalidated, Validated, +}; +use crate::config::genesis::HexString; +use crate::wallet::Alias; + +pub const PRE_GENESIS_TX_TIMESTAMP: DateTimeUtc = MIN_UTC; + +pub struct GenesisValidatorData { + pub source_key: common::SecretKey, + pub alias: Alias, + pub commission_rate: Dec, + pub max_commission_rate_change: Dec, + pub net_address: SocketAddr, + pub transfer_from_source_amount: token::DenominatedAmount, + pub self_bond_amount: token::DenominatedAmount, +} + +/// Panics if given `txs.validator_accounts` is not empty, because validator +/// transactions must be signed with a validator wallet (see +/// `init-genesis-validator` command). +pub fn sign_txs( + txs: UnsignedTransactions, + wallet: &mut Wallet, +) -> Transactions { + let UnsignedTransactions { + established_account, + validator_account, + transfer, + bond, + } = txs; + + // Validate input first + if validator_account.is_some() && !validator_account.unwrap().is_empty() { + panic!( + "Validator transactions must be signed with a validator wallet." + ); + } + + if let Some(bonds) = bond.as_ref() { + for bond in bonds { + if let AliasOrPk::Alias(source) = &bond.source { + if source == &bond.validator { + panic!( + "Validator self-bonds must be signed with a validator \ + wallet." + ) + } + } + } + } + + // Sign all the transactions + let established_account = established_account.map(|tx| { + tx.into_iter() + .map(|tx| sign_established_account_tx(tx, wallet)) + .collect() + }); + let validator_account = None; + let transfer = transfer.map(|tx| { + tx.into_iter() + .map(|tx| sign_transfer_tx(tx, wallet)) + .collect() + }); + let bond = bond.map(|tx| { + tx.into_iter() + .map(|tx| sign_delegation_bond_tx(tx, wallet, &established_account)) + .collect() + }); + + Transactions { + established_account, + validator_account, + transfer, + bond, + } +} + +/// Parse [`UnsignedTransactions`] from bytes. +pub fn parse_unsigned( + bytes: &[u8], +) -> Result { + toml::from_slice(bytes) +} + +/// Create signed [`Transactions`] for a genesis validator. +pub fn init_validator( + GenesisValidatorData { + source_key, + alias, + commission_rate, + max_commission_rate_change, + net_address, + transfer_from_source_amount, + self_bond_amount, + }: GenesisValidatorData, + source_wallet: &mut Wallet, + validator_wallet: &ValidatorWallet, +) -> Transactions { + let unsigned_validator_account_tx = UnsignedValidatorAccountTx { + alias: alias.clone(), + account_key: StringEncoded::new(validator_wallet.account_key.ref_to()), + consensus_key: StringEncoded::new( + validator_wallet.consensus_key.ref_to(), + ), + protocol_key: StringEncoded::new( + validator_wallet + .store + .validator_keys + .protocol_keypair + .ref_to(), + ), + dkg_key: StringEncoded::new( + validator_wallet + .store + .validator_keys + .dkg_keypair + .as_ref() + .expect("Missing validator DKG key") + .public(), + ), + tendermint_node_key: StringEncoded::new( + validator_wallet.tendermint_node_key.ref_to(), + ), + + eth_hot_key: StringEncoded::new(validator_wallet.eth_hot_key.ref_to()), + eth_cold_key: StringEncoded::new( + validator_wallet.eth_cold_key.ref_to(), + ), + // No custom validator VPs yet + vp: "vp_validator".to_string(), + commission_rate, + max_commission_rate_change, + net_address, + }; + let validator_account = Some(vec![sign_validator_account_tx( + unsigned_validator_account_tx, + validator_wallet, + )]); + + let transfer = if transfer_from_source_amount.amount.is_zero() { + None + } else { + let unsigned_transfer_tx = TransferTx { + // Only native token can be staked + token: Alias::from("NAM"), + source: StringEncoded::new(source_key.ref_to()), + target: alias.clone(), + amount: transfer_from_source_amount, + }; + let transfer_tx = sign_transfer_tx(unsigned_transfer_tx, source_wallet); + Some(vec![transfer_tx]) + }; + + let bond = if self_bond_amount.amount.is_zero() { + None + } else { + let unsigned_bond_tx = BondTx { + source: AliasOrPk::Alias(alias.clone()), + validator: alias, + amount: self_bond_amount, + }; + let bond_tx = sign_self_bond_tx(unsigned_bond_tx, validator_wallet); + Some(vec![bond_tx]) + }; + + Transactions { + validator_account, + transfer, + bond, + ..Default::default() + } +} + +pub fn sign_established_account_tx( + unsigned_tx: UnsignedEstablishedAccountTx, + wallet: &mut Wallet, +) -> SignedEstablishedAccountTx { + let key = unsigned_tx.public_key.as_ref().map(|pk| { + let secret = wallet + .find_key_by_pk(pk, None) + .expect("Key for source must be present to sign with it."); + let sig = sign_tx(&unsigned_tx, &secret); + SignedPk { + pk: pk.clone(), + authorization: sig, + } + }); + let UnsignedEstablishedAccountTx { + alias, + vp, + public_key: _, + storage, + } = unsigned_tx; + + SignedEstablishedAccountTx { + alias, + vp, + public_key: key, + storage, + } +} + +pub fn sign_validator_account_tx( + unsigned_tx: UnsignedValidatorAccountTx, + validator_wallet: &ValidatorWallet, +) -> SignedValidatorAccountTx { + // Sign the tx with every validator key to authorize their usage + let account_key_sig = sign_tx(&unsigned_tx, &validator_wallet.account_key); + let consensus_key_sig = + sign_tx(&unsigned_tx, &validator_wallet.consensus_key); + let protocol_key_sig = sign_tx( + &unsigned_tx, + &validator_wallet.store.validator_keys.protocol_keypair, + ); + let eth_hot_key_sig = sign_tx(&unsigned_tx, &validator_wallet.eth_hot_key); + let eth_cold_key_sig = + sign_tx(&unsigned_tx, &validator_wallet.eth_cold_key); + let tendermint_node_key_sig = + sign_tx(&unsigned_tx, &validator_wallet.tendermint_node_key); + + let ValidatorAccountTx { + alias, + account_key, + consensus_key, + protocol_key, + dkg_key, + tendermint_node_key, + vp, + commission_rate, + max_commission_rate_change, + net_address, + eth_hot_key, + eth_cold_key, + } = unsigned_tx; + + let account_key = SignedPk { + pk: account_key, + authorization: account_key_sig, + }; + let consensus_key = SignedPk { + pk: consensus_key, + authorization: consensus_key_sig, + }; + let protocol_key = SignedPk { + pk: protocol_key, + authorization: protocol_key_sig, + }; + let tendermint_node_key = SignedPk { + pk: tendermint_node_key, + authorization: tendermint_node_key_sig, + }; + + let eth_hot_key = SignedPk { + pk: eth_hot_key, + authorization: eth_hot_key_sig, + }; + + let eth_cold_key = SignedPk { + pk: eth_cold_key, + authorization: eth_cold_key_sig, + }; + + SignedValidatorAccountTx { + alias, + account_key, + consensus_key, + protocol_key, + dkg_key, + tendermint_node_key, + vp, + commission_rate, + max_commission_rate_change, + net_address, + eth_hot_key, + eth_cold_key, + } +} + +pub fn sign_transfer_tx( + unsigned_tx: TransferTx, + source_wallet: &mut Wallet, +) -> SignedTransferTx { + let source_key = source_wallet + .find_key_by_pk(&unsigned_tx.source, None) + .expect("Key for source must be present to sign with it."); + unsigned_tx.sign(&source_key) +} + +pub fn sign_self_bond_tx( + unsigned_tx: BondTx, + validator_wallet: &ValidatorWallet, +) -> SignedBondTx { + unsigned_tx.sign(&validator_wallet.account_key) +} + +pub fn sign_delegation_bond_tx( + unsigned_tx: BondTx, + wallet: &mut Wallet, + established_accounts: &Option>>, +) -> SignedBondTx { + let alias = &unsigned_tx.source; + // Try to look-up the source from wallet first - if it's an alias of an + // implicit account that should give us the right key + let found_key = match alias { + AliasOrPk::Alias(alias) => wallet.find_key(&alias.normalize(), None), + AliasOrPk::PublicKey(pk) => wallet.find_key_by_pk(pk, None), + }; + let source_key = match found_key { + Ok(key) => key, + Err(FindKeyError::KeyNotFound) => { + // If it's not in the wallet, it must be an established account + // so we need to look-up its public key first + let pk = established_accounts + .as_ref() + .unwrap_or_else(|| { + panic!( + "Signing a bond failed. Cannot find \"{alias}\" in \ + the wallet and there are no established accounts." + ); + }) + .iter() + .find_map(|account| match alias { + AliasOrPk::Alias(alias) => { + // delegation from established account + if &account.alias == alias { + Some( + &account + .public_key + .as_ref() + .unwrap_or_else(|| { + panic!( + "Signing a bond failed. The \ + established account \"{alias}\" \ + has no public key. Add a public \ + to be able to sign bonds." + ); + }) + .pk + .raw, + ) + } else { + None + } + } + AliasOrPk::PublicKey(pk) => { + // delegation from an implicit account + Some(&pk.raw) + } + }) + .unwrap_or_else(|| { + panic!( + "Signing a bond failed. Cannot find \"{alias}\" in \ + the wallet or in the established accounts." + ); + }); + wallet.find_key_by_pk(pk, None).unwrap_or_else(|err| { + panic!( + "Signing a bond failed. Cannot find key for established \ + account \"{alias}\" in the wallet. Failed with {err}." + ); + }) + } + Err(err) => panic!( + "Signing a bond failed. Failed to read the key for \"{alias}\" \ + from wallet with {err}." + ), + }; + unsigned_tx.sign(&source_key) +} + +fn sign_tx( + tx_data: &T, + keypair: &common::SecretKey, +) -> StringEncoded { + StringEncoded::new(namada::proto::standalone_signature::< + T, + SerializeWithBorsh, + >(keypair, tx_data)) +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Transactions { + pub established_account: Option>, + pub validator_account: Option>, + pub transfer: Option>, + pub bond: Option>, +} + +impl Transactions { + /// Take the union of two sets of transactions + pub fn merge(&mut self, mut other: Self) { + self.established_account = self + .established_account + .take() + .map(|mut txs| { + if let Some(new_txs) = other.established_account.as_mut() { + txs.append(new_txs); + } + txs + }) + .or(other.established_account); + self.validator_account = self + .validator_account + .take() + .map(|mut txs| { + if let Some(new_txs) = other.validator_account.as_mut() { + txs.append(new_txs); + } + txs + }) + .or(other.validator_account); + self.transfer = self + .transfer + .take() + .map(|mut txs| { + if let Some(new_txs) = other.transfer.as_mut() { + txs.append(new_txs); + } + txs + }) + .or(other.transfer); + self.bond = self + .bond + .take() + .map(|mut txs| { + if let Some(new_txs) = other.bond.as_mut() { + txs.append(new_txs); + } + txs + }) + .or(other.bond); + } +} + +impl Default for Transactions { + fn default() -> Self { + Self { + established_account: None, + validator_account: None, + transfer: None, + bond: None, + } + } +} + +impl Transactions { + /// Check that there is at least one validator. + pub fn has_at_least_one_validator(&self) -> bool { + self.validator_account + .as_ref() + .map(|txs| !txs.is_empty()) + .unwrap_or_default() + } + + /// Check if there is at least one validator with positive Tendermint voting + /// power. The voting power is converted from `token::Amount` of the + /// validator's stake using the `tm_votes_per_token` PoS parameter. + pub fn has_validator_with_positive_voting_power( + &self, + votes_per_token: Dec, + ) -> bool { + self.bond + .as_ref() + .map(|txs| { + let mut stakes: BTreeMap<&Alias, token::Amount> = + BTreeMap::new(); + for tx in txs { + let entry = stakes.entry(&tx.validator).or_default(); + *entry += tx.amount.amount; + } + + stakes.into_iter().any(|(_validator, stake)| { + let tendermint_voting_power = + namada::ledger::pos::into_tm_voting_power( + votes_per_token, + stake, + ); + if tendermint_voting_power > 0 { + return true; + } + false + }) + }) + .unwrap_or_default() + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct UnsignedTransactions { + pub established_account: Option>, + pub validator_account: Option>, + pub transfer: Option>>, + pub bond: Option>>, +} + +pub type UnsignedValidatorAccountTx = + ValidatorAccountTx>; + +pub type SignedValidatorAccountTx = ValidatorAccountTx; + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct ValidatorAccountTx { + pub alias: Alias, + pub dkg_key: StringEncoded, + pub vp: String, + /// Commission rate charged on rewards for delegators (bounded inside + /// 0-1) + pub commission_rate: Dec, + /// Maximum change in commission rate permitted per epoch + pub max_commission_rate_change: Dec, + /// P2P IP:port + pub net_address: SocketAddr, + /// PKs have to come last in TOML to avoid `ValueAfterTable` error + pub account_key: PK, + pub consensus_key: PK, + pub protocol_key: PK, + pub tendermint_node_key: PK, + pub eth_hot_key: PK, + pub eth_cold_key: PK, +} + +pub type UnsignedEstablishedAccountTx = + EstablishedAccountTx>; + +pub type SignedEstablishedAccountTx = EstablishedAccountTx; + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct EstablishedAccountTx { + pub alias: Alias, + pub vp: String, + /// PKs have to come last in TOML to avoid `ValueAfterTable` error + pub public_key: Option, + #[serde(default)] + /// Initial storage key values + pub storage: HashMap, +} + +pub type SignedTransferTx = Signed>; + +impl SignedTransferTx { + /// Verify the signature of `TransferTx`. This should not depend + /// on whether the contained amount is denominated or not. + /// + /// Since we denominate amounts as part of validation, we can + /// only verify signatures on [`SignedTransferTx`] + /// types. + pub fn verify_sig(&self) -> Result<(), VerifySigError> { + let Self { data, signature } = self; + verify_standalone_sig::<_, SerializeWithBorsh>( + &data.data_to_sign(), + &data.source.raw, + signature, + ) + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct TransferTx { + pub token: Alias, + pub source: StringEncoded, + pub target: Alias, + pub amount: T::Amount, +} + +impl TransferTx { + /// Add the correct denomination to the contained amount + pub fn denominate( + self, + tokens: &Tokens, + ) -> eyre::Result> { + let TransferTx { + token, + source, + target, + amount, + } = self; + let denom = + if let Some(super::templates::TokenConfig { denom, .. }) = + tokens.token.get(&token) + { + *denom + } else { + eprintln!( + "Genesis files contained transfer of token {}, which is \ + not in the `tokens.toml` file", + token + ); + return Err(eyre::eyre!( + "Genesis files contained transfer of token {}, which is \ + not in the `tokens.toml` file", + token + )); + }; + let amount = amount.increase_precision(denom).map_err(|e| { + eprintln!( + "A bond amount in the transactions.toml file was incorrectly \ + formatted:\n{}", + e + ); + e + })?; + + Ok(TransferTx { + token, + source, + target, + amount, + }) + } + + /// The signable data. This does not include the phantom data. + fn data_to_sign(&self) -> Vec { + [ + self.token.try_to_vec().unwrap(), + self.source.try_to_vec().unwrap(), + self.target.try_to_vec().unwrap(), + self.amount.try_to_vec().unwrap(), + ] + .concat() + } + + /// Sign the transfer. + /// + /// Since we denominate amounts as part of validation, we can + /// only verify signatures on [`SignedTransferTx`] + /// types. Thus we only allow signing of [`TransferTx`] + /// types. + pub fn sign(self, key: &common::SecretKey) -> SignedTransferTx { + let sig = standalone_signature::<_, SerializeWithBorsh>( + key, + &self.data_to_sign(), + ); + SignedTransferTx { + data: self, + signature: StringEncoded { raw: sig }, + } + } +} + +pub type SignedBondTx = Signed>; + +impl SignedBondTx { + /// Verify the signature of `BondTx`. This should not depend + /// on whether the contained amount is denominated or not. + /// + /// Since we denominate amounts as part of validation, we can + /// only verify signatures on [`SignedBondTx`] + /// types. + pub fn verify_sig( + &self, + pk: &common::PublicKey, + ) -> Result<(), VerifySigError> { + let Self { data, signature } = self; + verify_standalone_sig::<_, SerializeWithBorsh>( + &data.data_to_sign(), + pk, + signature, + ) + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct BondTx { + pub source: AliasOrPk, + pub validator: Alias, + pub amount: T::Amount, +} + +impl BondTx { + /// Add the correct denomination to the contained amount + pub fn denominate(self) -> eyre::Result> { + let BondTx { + source, + validator, + amount, + } = self; + let amount = amount + .increase_precision(NATIVE_MAX_DECIMAL_PLACES.into()) + .map_err(|e| { + eprintln!( + "A bond amount in the transactions.toml file was \ + incorrectly formatted:\n{}", + e + ); + e + })?; + Ok(BondTx { + source, + validator, + amount, + }) + } + + /// The signable data. This does not include the phantom data. + fn data_to_sign(&self) -> Vec { + [ + self.source.try_to_vec().unwrap(), + self.validator.try_to_vec().unwrap(), + self.amount.try_to_vec().unwrap(), + ] + .concat() + } + + /// Sign the transfer. + /// + /// Since we denominate amounts as part of validation, we can + /// only verify signatures on [`SignedBondTx`] + /// types. Thus we only allow signing of [`BondTx`] + /// types. + pub fn sign(self, key: &common::SecretKey) -> SignedBondTx { + let sig = standalone_signature::<_, SerializeWithBorsh>( + key, + &self.data_to_sign(), + ); + SignedBondTx { + data: self, + signature: StringEncoded { raw: sig }, + } + } +} + +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +pub enum AliasOrPk { + /// `alias = "value"` in toml (encoded via `AliasSerHelper`) + Alias(Alias), + /// `public_key = "value"` in toml (encoded via `PkSerHelper`) + PublicKey(StringEncoded), +} + +impl Serialize for AliasOrPk { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + AliasOrPk::Alias(alias) => Serialize::serialize(alias, serializer), + AliasOrPk::PublicKey(pk) => Serialize::serialize(pk, serializer), + } + } +} + +impl<'de> Deserialize<'de> for AliasOrPk { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct FieldVisitor; + + impl<'de> serde::de::Visitor<'de> for FieldVisitor { + type Value = AliasOrPk; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str( + "a bech32m encoded `common::PublicKey` or an alias", + ) + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + // Try to deserialize a PK first + let maybe_pk = + StringEncoded::::from_str(value); + match maybe_pk { + Ok(pk) => Ok(AliasOrPk::PublicKey(pk)), + Err(_) => { + // If that doesn't work, use it as an alias + let alias = Alias::from_str(value) + .map_err(serde::de::Error::custom)?; + Ok(AliasOrPk::Alias(alias)) + } + } + } + } + + deserializer.deserialize_str(FieldVisitor) + } +} + +impl Display for AliasOrPk { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AliasOrPk::Alias(alias) => write!(f, "{}", alias), + AliasOrPk::PublicKey(pk) => write!(f, "{}", pk), + } + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct Signed { + #[serde(flatten)] + pub data: T, + pub signature: StringEncoded, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct SignedPk { + pub pk: StringEncoded, + pub authorization: StringEncoded, +} + +pub fn validate( + transactions: Transactions, + vps: Option<&ValidityPredicates>, + balances: Option<&DenominatedBalances>, + tokens: &Tokens, + parameters: Option<&Parameters>, +) -> Option> { + let mut is_valid = true; + + let mut all_used_aliases: BTreeSet = BTreeSet::default(); + let mut established_accounts: BTreeMap> = + BTreeMap::default(); + let mut validator_accounts: BTreeMap = + BTreeMap::default(); + + let Transactions { + ref established_account, + ref validator_account, + ref transfer, + bond, + } = transactions; + + if let Some(txs) = established_account { + for tx in txs { + if !validate_established_account( + tx, + vps, + &mut all_used_aliases, + &mut established_accounts, + ) { + is_valid = false; + } + } + } + + if let Some(txs) = validator_account { + for tx in txs { + if !validate_validator_account( + tx, + vps, + &mut all_used_aliases, + &mut validator_accounts, + ) { + is_valid = false; + } + } + } + + // Make a mutable copy of the balances for tracking changes applied from txs + let mut token_balances: BTreeMap = + balances + .map(|balances| { + balances + .token + .iter() + .map(|(token, token_balances)| { + ( + token.clone(), + TokenBalancesForValidation { + // Add an accumulator for tokens transferred to + // aliases + aliases: BTreeMap::new(), + pks: token_balances.clone(), + }, + ) + }) + .collect() + }) + .unwrap_or_default(); + + let validated_txs = if let Some(txs) = transfer { + let validated_txs: Vec<_> = txs + .iter() + .filter_map(|tx| { + validate_transfer( + tx, + &mut token_balances, + &all_used_aliases, + tokens, + ) + }) + .collect(); + if validated_txs.len() != txs.len() { + is_valid = false; + None + } else { + Some(validated_txs) + } + } else { + None + }; + + let validated_bonds = if let Some(txs) = bond { + if !txs.is_empty() { + match parameters { + Some(parameters) => { + let bond_number = txs.len(); + let validated_bonds: Vec<_> = txs + .into_iter() + .filter_map(|tx| { + validate_bond( + tx, + &mut token_balances, + &established_accounts, + &validator_accounts, + parameters, + ) + }) + .collect(); + if validated_bonds.len() != bond_number { + is_valid = false; + None + } else { + Some(validated_bonds) + } + } + None => { + eprintln!( + "Unable to validate bonds without a valid parameters \ + file." + ); + is_valid = false; + None + } + } + } else { + None + } + } else { + None + }; + + is_valid.then_some(Transactions { + established_account: transactions.established_account, + validator_account: transactions.validator_account, + transfer: validated_txs, + bond: validated_bonds, + }) +} + +fn validate_bond( + tx: SignedBondTx, + balances: &mut BTreeMap, + established_accounts: &BTreeMap>, + validator_accounts: &BTreeMap, + parameters: &Parameters, +) -> Option> { + // Check signature + let mut is_valid = { + let source = &tx.data.source; + if let Some(source_pk) = match source { + AliasOrPk::Alias(alias) => { + // Try to find the source's PK in either established_accounts or + // validator_accounts + established_accounts + .get(alias) + .cloned() + .flatten() + .or_else(|| validator_accounts.get(alias).cloned()) + } + AliasOrPk::PublicKey(pk) => Some(pk.raw.clone()), + } { + if tx.verify_sig(&source_pk).is_err() { + eprintln!("Invalid bond tx signature.",); + false + } else { + true + } + } else { + eprintln!( + "Invalid bond tx. Couldn't verify bond's signature, because \ + the source accounts \"{source}\" public key cannot be found." + ); + false + } + }; + + // Make sure the native token amount is denominated correctly + let validated_bond = tx.data.denominate().ok()?; + let BondTx { + source, + validator, + amount, + .. + } = &validated_bond; + + // Check that the validator exists + if !validator_accounts.contains_key(validator) { + eprintln!( + "Invalid bond tx. The target validator \"{validator}\" account \ + not found." + ); + is_valid = false; + } + + // Check and update token balance of the source + let native_token = ¶meters.parameters.native_token; + match balances.get_mut(native_token) { + Some(balances) => { + let balance = match source { + AliasOrPk::Alias(source) => balances.aliases.get_mut(source), + AliasOrPk::PublicKey(source) => balances.pks.0.get_mut(source), + }; + match balance { + Some(balance) => { + if *balance < *amount { + eprintln!( + "Invalid bond tx. Source {source} doesn't have \ + enough balance of token \"{native_token}\" to \ + transfer {}. Got {}.", + amount, balance, + ); + is_valid = false; + } else { + // Deduct the amount from source + if amount == balance { + match source { + AliasOrPk::Alias(source) => { + balances.aliases.remove(source); + } + AliasOrPk::PublicKey(source) => { + balances.pks.0.remove(source); + } + } + } else { + balance.amount -= amount.amount; + } + } + } + None => { + eprintln!( + "Invalid transfer tx. Source {source} has no balance \ + of token \"{native_token}\"." + ); + is_valid = false; + } + } + } + None => { + eprintln!( + "Invalid bond tx. Token \"{native_token}\" not found in \ + balances." + ); + is_valid = false; + } + } + + is_valid.then_some(validated_bond) +} + +#[derive(Clone, Debug)] +pub struct TokenBalancesForValidation { + /// Accumulator for tokens transferred to aliases + pub aliases: BTreeMap, + /// Token balances from the balances file, associated with PKs + pub pks: TokenBalances, +} + +pub fn validate_established_account( + tx: &SignedEstablishedAccountTx, + vps: Option<&ValidityPredicates>, + all_used_aliases: &mut BTreeSet, + established_accounts: &mut BTreeMap>, +) -> bool { + let mut is_valid = true; + + established_accounts.insert( + tx.alias.clone(), + tx.public_key.as_ref().map(|signed| signed.pk.raw.clone()), + ); + + // Check that alias is unique + if all_used_aliases.contains(&tx.alias) { + eprintln!( + "A duplicate alias \"{}\" found in a `established_account` tx.", + tx.alias + ); + is_valid = false; + } else { + all_used_aliases.insert(tx.alias.clone()); + } + + // Check the VP exists + if !vps + .map(|vps| vps.wasm.contains_key(&tx.vp)) + .unwrap_or_default() + { + eprintln!( + "An `established_account` tx `vp` \"{}\" not found in Validity \ + predicates file.", + tx.vp + ); + is_valid = false; + } + + // If PK is used, check the authorization + if let Some(pk) = tx.public_key.as_ref() { + if !validate_established_account_sig(pk, tx) { + is_valid = false; + } + } + + is_valid +} + +fn validate_established_account_sig( + SignedPk { pk, authorization }: &SignedPk, + tx: &SignedEstablishedAccountTx, +) -> bool { + let unsigned = UnsignedEstablishedAccountTx::from(tx); + validate_signature(&unsigned, &pk.raw, &authorization.raw) +} + +pub fn validate_validator_account( + tx: &ValidatorAccountTx, + vps: Option<&ValidityPredicates>, + all_used_aliases: &mut BTreeSet, + validator_accounts: &mut BTreeMap, +) -> bool { + let mut is_valid = true; + + validator_accounts.insert(tx.alias.clone(), tx.account_key.pk.raw.clone()); + + // Check that alias is unique + if all_used_aliases.contains(&tx.alias) { + eprintln!( + "A duplicate alias \"{}\" found in a `validator_account` tx.", + tx.alias + ); + is_valid = false; + } else { + all_used_aliases.insert(tx.alias.clone()); + } + + // Check the VP exists + if !vps + .map(|vps| vps.wasm.contains_key(&tx.vp)) + .unwrap_or_default() + { + eprintln!( + "A `validator_account` tx `vp` \"{}\" not found in Validity \ + predicates file.", + tx.vp + ); + is_valid = false; + } + + // Check keys authorizations + let unsigned = UnsignedValidatorAccountTx::from(tx); + if !validate_signature( + &unsigned, + &tx.account_key.pk.raw, + &tx.account_key.authorization.raw, + ) { + eprintln!( + "Invalid `account_key` authorization for `validator_account` tx \ + with alias \"{}\".", + tx.alias + ); + is_valid = false; + } + if !validate_signature( + &unsigned, + &tx.consensus_key.pk.raw, + &tx.consensus_key.authorization.raw, + ) { + eprintln!( + "Invalid `consensus_key` authorization for `validator_account` tx \ + with alias \"{}\".", + tx.alias + ); + is_valid = false; + } + if !validate_signature( + &unsigned, + &tx.protocol_key.pk.raw, + &tx.protocol_key.authorization.raw, + ) { + eprintln!( + "Invalid `protocol_key` authorization for `validator_account` tx \ + with alias \"{}\".", + tx.alias + ); + is_valid = false; + } + if !validate_signature( + &unsigned, + &tx.tendermint_node_key.pk.raw, + &tx.tendermint_node_key.authorization.raw, + ) { + eprintln!( + "Invalid `tendermint_node_key` authorization for \ + `validator_account` tx with alias \"{}\".", + tx.alias + ); + is_valid = false; + } + + if !validate_signature( + &unsigned, + &tx.eth_hot_key.pk.raw, + &tx.eth_hot_key.authorization.raw, + ) { + eprintln!( + "Invalid `eth_hot_key` authorization for `validator_account` tx \ + with alias \"{}\".", + tx.alias + ); + is_valid = false; + } + + if !validate_signature( + &unsigned, + &tx.eth_cold_key.pk.raw, + &tx.eth_cold_key.authorization.raw, + ) { + eprintln!( + "Invalid `eth_cold_key` authorization for `validator_account` tx \ + with alias \"{}\".", + tx.alias + ); + is_valid = false; + } + + is_valid +} + +/// Updates the token balances with all the valid transfers applied +pub fn validate_transfer( + tx: &SignedTransferTx, + balances: &mut BTreeMap, + all_used_aliases: &BTreeSet, + tokens: &Tokens, +) -> Option> { + let mut is_valid = true; + // Check signature + if tx.verify_sig().is_err() { + eprintln!("Invalid transfer tx signature.",); + is_valid = false; + } + + let unsigned: TransferTx = tx.into(); + let validated = unsigned.denominate(tokens).ok()?; + let TransferTx { + token, + source, + target, + amount, + .. + } = &validated; + + // Check that the target exists + if !all_used_aliases.contains(target) { + eprintln!( + "Invalid transfer tx. The target alias \"{target}\" no matching \ + account found." + ); + is_valid = false; + } + + // Check token balance of the source and update token balances of the source + // and target + match balances.get_mut(token) { + Some(balances) => match balances.pks.0.get_mut(source) { + Some(balance) => { + if balance.amount < amount.amount { + eprintln!( + "Invalid transfer tx. Source {source} doesn't have \ + enough balance of token \"{token}\" to transfer {}. \ + Got {}.", + amount, balance, + ); + is_valid = false; + } else { + // Deduct the amount from source + if amount.amount == balance.amount { + balances.pks.0.remove(source); + } else { + balance.amount -= amount.amount; + } + + // Add the amount to target + let target_balance = balances + .aliases + .entry(target.clone()) + .or_insert_with(|| DenominatedAmount { + amount: token::Amount::zero(), + denom: amount.denom, + }); + target_balance.amount += amount.amount; + } + } + None => { + eprintln!( + "Invalid transfer tx. Source {source} has no balance of \ + token \"{token}\"." + ); + is_valid = false; + } + }, + None => { + eprintln!( + "Invalid transfer tx. Token \"{token}\" not found in balances." + ); + is_valid = false; + } + } + + is_valid.then_some(validated) +} + +fn validate_signature( + tx_data: &T, + pk: &common::PublicKey, + sig: &common::Signature, +) -> bool { + match verify_standalone_sig::(tx_data, pk, sig) { + Ok(()) => true, + Err(err) => { + eprintln!( + "Invalid tx signature in tx {tx_data:?}, failed with: {err}." + ); + false + } + } +} + +impl From<&SignedEstablishedAccountTx> for UnsignedEstablishedAccountTx { + fn from(tx: &SignedEstablishedAccountTx) -> Self { + let SignedEstablishedAccountTx { + alias, + vp, + public_key, + storage, + } = tx; + Self { + alias: alias.clone(), + vp: vp.clone(), + public_key: public_key.as_ref().map(|signed| signed.pk.clone()), + storage: storage.clone(), + } + } +} + +impl From<&SignedValidatorAccountTx> for UnsignedValidatorAccountTx { + fn from(tx: &SignedValidatorAccountTx) -> Self { + let SignedValidatorAccountTx { + alias, + dkg_key, + vp, + commission_rate, + max_commission_rate_change, + net_address, + account_key, + consensus_key, + protocol_key, + tendermint_node_key, + eth_hot_key, + eth_cold_key, + } = tx; + + Self { + alias: alias.clone(), + dkg_key: dkg_key.clone(), + vp: vp.clone(), + commission_rate: *commission_rate, + max_commission_rate_change: *max_commission_rate_change, + net_address: *net_address, + account_key: account_key.pk.clone(), + consensus_key: consensus_key.pk.clone(), + protocol_key: protocol_key.pk.clone(), + tendermint_node_key: tendermint_node_key.pk.clone(), + eth_hot_key: eth_hot_key.pk.clone(), + eth_cold_key: eth_cold_key.pk.clone(), + } + } +} + +impl From<&SignedTransferTx> for TransferTx { + fn from(tx: &SignedTransferTx) -> Self { + let SignedTransferTx { data, signature: _ } = tx; + data.clone() + } +} + +impl From<&SignedBondTx> for BondTx { + fn from(tx: &SignedBondTx) -> Self { + let SignedBondTx { data, signature: _ } = tx; + data.clone() + } +} diff --git a/apps/src/lib/config/global.rs b/apps/src/lib/config/global.rs index f1d8e5d483..6c9bae0e4a 100644 --- a/apps/src/lib/config/global.rs +++ b/apps/src/lib/config/global.rs @@ -26,29 +26,30 @@ pub enum Error { pub type Result = std::result::Result; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct GlobalConfig { /// The default chain ID - pub default_chain_id: ChainId, + pub default_chain_id: Option, // NOTE: There will be sub-chains in here in future } impl GlobalConfig { pub fn new(default_chain_id: ChainId) -> Self { - Self { default_chain_id } + Self { + default_chain_id: Some(default_chain_id), + } } /// Try to read the global config from a file. pub fn read(base_dir: impl AsRef) -> Result { let file_path = Self::file_path(base_dir.as_ref()); let file_name = file_path.to_str().expect("Expected UTF-8 file path"); - if !file_path.exists() { - return Err(Error::FileNotFound(file_name.to_string())); - }; let mut config = config::Config::new(); - config - .merge(config::File::with_name(file_name)) - .map_err(Error::ReadError)?; + if file_path.exists() { + config + .merge(config::File::with_name(file_name)) + .map_err(Error::ReadError)?; + }; config.try_into().map_err(Error::DeserializationError) } diff --git a/apps/src/lib/config/mod.rs b/apps/src/lib/config/mod.rs index f6307c6bd1..735391d056 100644 --- a/apps/src/lib/config/mod.rs +++ b/apps/src/lib/config/mod.rs @@ -41,7 +41,7 @@ pub struct Config { pub ledger: Ledger, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum TendermintMode { Full, Validator, diff --git a/apps/src/lib/config/utils.rs b/apps/src/lib/config/utils.rs index 03725d112c..41658a000c 100644 --- a/apps/src/lib/config/utils.rs +++ b/apps/src/lib/config/utils.rs @@ -64,6 +64,30 @@ pub fn convert_tm_addr_to_socket_addr( } } +/// Set the IP address of a [`TendermintAddress`] +pub fn set_ip(tm_addr: &mut TendermintAddress, new_host: impl Into) { + match tm_addr { + TendermintAddress::Tcp { host, .. } => { + *host = new_host.into(); + } + TendermintAddress::Unix { path: _ } => { + panic!("Unix addresses aren't currently supported.") + } + } +} + +/// Set the port of a [`TendermintAddress`] +pub fn set_port(tm_addr: &mut TendermintAddress, new_port: impl Into) { + match tm_addr { + TendermintAddress::Tcp { port, .. } => { + *port = new_port.into(); + } + TendermintAddress::Unix { path: _ } => { + panic!("Unix addresses aren't currently supported.") + } + } +} + #[cfg(test)] mod test { use std::panic; diff --git a/apps/src/lib/node/ledger/mod.rs b/apps/src/lib/node/ledger/mod.rs index 3e1dbe0b46..5d4d1845a9 100644 --- a/apps/src/lib/node/ledger/mod.rs +++ b/apps/src/lib/node/ledger/mod.rs @@ -33,7 +33,6 @@ use crate::config::{ethereum_bridge, TendermintMode}; use crate::facade::tendermint_proto::abci::CheckTxType; use crate::facade::tower_abci::{response, split, Server}; use crate::node::ledger::broadcaster::Broadcaster; -use crate::node::ledger::config::genesis; use crate::node::ledger::ethereum_oracle as oracle; use crate::node::ledger::shell::{Error, MempoolTxType, Shell}; use crate::node::ledger::shims::abcipp_shim::AbcippShim; @@ -98,7 +97,7 @@ impl Shell { tracing::debug!("Request InitChain"); self.init_chain( init, - #[cfg(any(test, feature = "dev"))] + #[cfg(any(test, feature = "testing"))] 1, ) .map(Response::InitChain) @@ -469,10 +468,7 @@ fn start_abci_broadcaster_shell( let tendermint_mode = config.shell.tendermint_mode.clone(); let proxy_app_address = convert_tm_addr_to_socket_addr(&config.cometbft.proxy_app); - #[cfg(not(any(test, feature = "dev")))] - let genesis = genesis::genesis(&config.shell.base_dir, &config.chain_id); - #[cfg(any(test, feature = "dev"))] - let genesis = genesis::genesis(1); + let (shell, abci_service, service_handle) = AbcippShim::new( config, wasm_dir, @@ -481,7 +477,6 @@ fn start_abci_broadcaster_shell( &db_cache, vp_wasm_compilation_cache, tx_wasm_compilation_cache, - genesis.native_token, ); // Channel for signalling shut down to ABCI server diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 4fef1fc7ef..3f6fbcd398 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1667,7 +1667,6 @@ mod test_finalize_block { /// Test that adding a new erc20 transfer to the bridge pool /// increments the pool's nonce. fn test_bp_nonce_is_incremented() { - use crate::node::ledger::shell::address::nam; test_bp(|shell: &mut TestShell| { let asset = EthAddress([0xff; 20]); let receiver = EthAddress([0xaa; 20]); @@ -1694,7 +1693,7 @@ mod test_finalize_block { { let amt: Amount = 999_999_u64.into(); let pool_balance_key = token::balance_key( - &nam(), + &shell.wl_storage.storage.native_token, &bridge_pool::BRIDGE_POOL_ADDRESS, ); shell @@ -1717,7 +1716,7 @@ mod test_finalize_block { sender: bertha.clone(), }, gas_fee: GasFee { - token: nam(), + token: shell.wl_storage.storage.native_token.clone(), amount: 10u64.into(), payer: bertha.clone(), }, @@ -3090,6 +3089,7 @@ mod test_finalize_block { &val1.address, del_1_amount, current_epoch, + None, ) .unwrap(); @@ -3190,6 +3190,7 @@ mod test_finalize_block { &val1.address, self_bond_1_amount, current_epoch, + None, ) .unwrap(); @@ -3231,6 +3232,7 @@ mod test_finalize_block { &val1.address, del_2_amount, current_epoch, + None, ) .unwrap(); diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 2554349680..68027e1548 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -2,23 +2,32 @@ use std::collections::HashMap; use std::hash::Hash; -use namada::ledger::parameters::{self, Parameters}; -use namada::ledger::pos::{staking_token_address, OwnedPosParams}; +use namada::ledger::parameters::{Parameters}; +use namada::ledger::pos::OwnedPosParams; use namada::ledger::storage::traits::StorageHasher; use namada::ledger::storage::{DBIter, DB}; use namada::ledger::storage_api::token::{ - credit_tokens, read_balance, read_total_supply, write_denom, + credit_tokens, write_denom, }; use namada::ledger::storage_api::{ResultExt, StorageRead, StorageWrite}; use namada::ledger::{ibc, pos}; -use namada::types::dec::Dec; +use namada::proof_of_stake::BecomeValidator; use namada::types::hash::Hash as CodeHash; use namada::types::key::*; +use namada::types::storage::KeySeg; use namada::types::time::{DateTimeUtc, TimeZone, Utc}; use namada::vm::validate_untrusted_wasm; use namada_sdk::eth_bridge::EthBridgeStatus; use super::*; +use crate::config::genesis::chain::{ + FinalizedEstablishedAccountTx, FinalizedTokenConfig, + FinalizedValidatorAccountTx, +}; +use crate::config::genesis::templates::{TokenBalances, TokenConfig}; +use crate::config::genesis::transactions::{ + BondTx, EstablishedAccountTx, TransferTx, ValidatorAccountTx, +}; use crate::facade::tendermint_proto::google::protobuf; use crate::facade::tower_abci::{request, response}; use crate::wasm_loader; @@ -39,32 +48,41 @@ where pub fn init_chain( &mut self, init: request::InitChain, - #[cfg(any(test, feature = "dev"))] num_validators: u64, + #[cfg(any(test, feature = "testing"))] _num_validators: u64, ) -> Result { - let (current_chain_id, _) = self.wl_storage.storage.get_chain_id(); - if current_chain_id != init.chain_id { + let mut response = response::InitChain::default(); + let chain_id = self.wl_storage.storage.chain_id.as_str(); + if chain_id != init.chain_id.as_str() { return Err(Error::ChainId(format!( "Current chain ID: {}, Tendermint chain ID: {}", - current_chain_id, init.chain_id + chain_id, init.chain_id ))); } - #[cfg(not(any(test, feature = "dev")))] - let genesis = - genesis::genesis(&self.base_dir, &self.wl_storage.storage.chain_id); - #[cfg(not(any(test, feature = "dev")))] + + // Read the genesis files + #[cfg(any( + feature = "integration", + not(any(test, feature = "benches")) + ))] + let genesis = { + let chain_dir = self.base_dir.join(chain_id); + genesis::chain::Finalized::read_toml_files(&chain_dir) + .expect("Missing genesis files") + }; + #[cfg(all( + any(test, feature = "benches"), + not(feature = "integration") + ))] + let genesis = genesis::make_dev_genesis(_num_validators); + #[cfg(all( + any(test, feature = "benches"), + not(feature = "integration") + ))] { - let genesis_bytes = genesis.serialize_to_vec(); - let errors = - self.wl_storage.storage.chain_id.validate(genesis_bytes); - use itertools::Itertools; - assert!( - errors.is_empty(), - "Chain ID validation failed: {}", - errors.into_iter().format(". ") - ); + // update the native token from the genesis file + let native_token = genesis.get_native_token().clone(); + self.wl_storage.storage.native_token = native_token; } - #[cfg(any(test, feature = "dev"))] - let genesis = genesis::genesis(num_validators); let ts: protobuf::Timestamp = init.time.expect("Missing genesis time"); let initial_height = init @@ -79,26 +97,112 @@ where .into(); // Initialize protocol parameters - let genesis::Parameters { - epoch_duration, - max_proposal_bytes, - max_block_gas, - max_expected_time_per_block, - vp_whitelist, + let parameters = genesis.get_chain_parameters(&self.wasm_dir); + self.store_wasms(¶meters)?; + parameters.init_storage(&mut self.wl_storage)?; + + // Initialize governance parameters + let gov_params = genesis.get_gov_params(); + gov_params.init_storage(&mut self.wl_storage)?; + + // configure the Ethereum bridge if the configuration is set. + if let Some(config) = genesis.get_eth_bridge_params() { + tracing::debug!("Initializing Ethereum bridge storage."); + config.init_storage(&mut self.wl_storage); + self.update_eth_oracle(); + } else { + self.wl_storage + .write_bytes( + &namada::eth_bridge::storage::active_key(), + EthBridgeStatus::Disabled.serialize_to_vec(), + ) + .unwrap(); + } + + // Depends on parameters being initialized + self.wl_storage + .storage + .init_genesis_epoch(initial_height, genesis_time, ¶meters) + .expect("Initializing genesis epoch must not fail"); + + // PoS system depends on epoch being initialized + let pos_params = genesis.get_pos_params(); + let (current_epoch, _gas) = self.wl_storage.storage.get_current_epoch(); + pos::namada_proof_of_stake::init_genesis( + &mut self.wl_storage, + &pos_params, + current_epoch, + ) + .expect("Must be able to initialize PoS genesis storage"); + + // PGF parameters + let pgf_params = genesis.get_pgf_params(); + pgf_params + .init_storage(&mut self.wl_storage) + .expect("Should be able to initialized PGF at genesis"); + + // Loaded VP code cache to avoid loading the same files multiple times + let mut vp_cache: HashMap> = HashMap::default(); + self.init_token_accounts(&genesis); + self.init_token_balances(&genesis); + self.apply_genesis_txs_established_account(&genesis, &mut vp_cache); + self.apply_genesis_txs_validator_account( + &genesis, + &mut vp_cache, + &pos_params, + current_epoch, + ); + self.apply_genesis_txs_transfer(&genesis); + self.apply_genesis_txs_bonds(&genesis); + + pos::namada_proof_of_stake::store_total_consensus_stake( + &mut self.wl_storage, + Default::default(), + ) + .expect("Could not compute total consensus stake at genesis"); + // This has to be done after `apply_genesis_txs_validator_account` + pos::namada_proof_of_stake::copy_genesis_validator_sets( + &mut self.wl_storage, + &pos_params, + current_epoch, + ) + .expect("Must be able to copy PoS genesis validator sets"); + + ibc::init_genesis_storage(&mut self.wl_storage); + + // Set the initial validator set + response.validators = self + .get_abci_validator_updates(true) + .expect("Must be able to set genesis validator set"); + debug_assert!(!response.validators.is_empty()); + Ok(response) + } + + /// Look-up WASM code of a genesis VP by its name + fn lookup_vp( + &self, + name: &str, + genesis: &genesis::chain::Finalized, + vp_cache: &mut HashMap>, + ) -> Vec { + let config = + genesis.vps.wasm.get(name).unwrap_or_else(|| { + panic!("Missing validity predicate for {name}") + }); + let vp_filename = &config.filename; + vp_cache.get_or_insert_with(vp_filename.clone(), || { + wasm_loader::read_wasm(&self.wasm_dir, vp_filename).unwrap() + }) + } + + fn store_wasms(&mut self, params: &Parameters) -> Result<()> { + let Parameters { tx_whitelist, - implicit_vp_code_path, - implicit_vp_sha256, - epochs_per_year, - max_signatures_per_transaction, - pos_gain_p, - pos_gain_d, - staked_ratio, - pos_inflation_amount, - minimum_gas_price, - fee_unshielding_gas_limit, - fee_unshielding_descriptions_limit, - } = genesis.parameters; - // Store wasm codes into storage + vp_whitelist, + implicit_vp_code_hash, + .. + } = params; + let mut is_implicit_vp_stored = false; let checksums = wasm_loader::Checksums::read_checksums(&self.wasm_dir); for (name, full_name) in checksums.0.iter() { let code = wasm_loader::read_wasm(&self.wasm_dir, name) @@ -140,6 +244,9 @@ where 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)?; + if &code_hash == implicit_vp_code_hash { + is_implicit_vp_stored = true; + } self.wl_storage.write_bytes(&code_name_key, code_hash)?; } else { tracing::warn!("The wasm {name} isn't whitelisted."); @@ -147,353 +254,354 @@ where } // check if implicit_vp wasm is stored - let implicit_vp_code_hash = - read_wasm_hash(&self.wl_storage, &implicit_vp_code_path)?.ok_or( - Error::LoadingWasm(format!( - "Unknown vp code path: {}", - implicit_vp_code_path - )), - )?; - // In dev, we don't check the hash - #[cfg(feature = "dev")] - let _ = implicit_vp_sha256; - #[cfg(not(feature = "dev"))] - { - assert_eq!( - implicit_vp_code_hash.0.as_slice(), - &implicit_vp_sha256, - "Invalid implicit account's VP sha256 hash for {}", - implicit_vp_code_path - ); - } - - let parameters = Parameters { - epoch_duration, - max_proposal_bytes, - max_block_gas, - max_expected_time_per_block, - vp_whitelist, - tx_whitelist, - implicit_vp_code_hash, - epochs_per_year, - max_signatures_per_transaction, - pos_gain_p, - pos_gain_d, - staked_ratio, - pos_inflation_amount, - minimum_gas_price, - fee_unshielding_gas_limit, - fee_unshielding_descriptions_limit, - }; - parameters - .init_storage(&mut self.wl_storage) - .expect("Initializing chain parameters must not fail"); - - // Initialize governance parameters - genesis - .gov_params - .init_storage(&mut self.wl_storage) - .expect("Initializing chain parameters must not fail"); + assert!( + is_implicit_vp_stored, + "No VP found matching the expected implicit VP sha256 hash: {}", + implicit_vp_code_hash + ); + Ok(()) + } - // Initialize pgf parameters - genesis - .pgf_params - .init_storage(&mut self.wl_storage) - .expect("Initializing chain parameters must not fail"); + /// Init genesis token accounts + fn init_token_accounts(&mut self, genesis: &genesis::chain::Finalized) { + let masp_rewards = address::tokens(); + for (alias, token) in &genesis.tokens.token { + tracing::debug!("Initializing token {alias}"); - // configure the Ethereum bridge if the configuration is set. - if let Some(config) = genesis.ethereum_bridge_params { - tracing::debug!("Initializing Ethereum bridge storage."); - config.init_storage(&mut self.wl_storage); - self.update_eth_oracle(); - } else { - self.wl_storage - .write_bytes( - &namada::eth_bridge::storage::active_key(), - EthBridgeStatus::Disabled.serialize_to_vec(), - ) - .unwrap(); + let FinalizedTokenConfig { + address, + config: TokenConfig { denom }, + } = token; + // associate a token with its denomination. + write_denom(&mut self.wl_storage, address, *denom).unwrap(); + // add token addresses to the masp reward conversions lookup table. + let alias = alias.to_string(); + if masp_rewards.contains_key(&alias.as_str()) { + self.wl_storage + .storage + .conversion_state + .tokens + .insert(alias, address.clone()); + } } - - // Depends on parameters being initialized - self.wl_storage + let key_prefix: Key = address::masp().to_db_key().into(); + // Save the current conversion state + let state_key = key_prefix + .push(&(token::CONVERSION_KEY_PREFIX.to_owned())) + .unwrap(); + let conv_bytes = self + .wl_storage .storage - .init_genesis_epoch(initial_height, genesis_time, ¶meters) - .expect("Initializing genesis epoch must not fail"); - - // Initialize genesis established accounts - self.initialize_established_accounts( - genesis.established_accounts, - &implicit_vp_code_path, - )?; - - // Initialize genesis implicit - self.initialize_implicit_accounts(genesis.implicit_accounts); - - // Initialize genesis token accounts - self.initialize_token_accounts(genesis.token_accounts); - - // Initialize genesis validator accounts - let staking_token = staking_token_address(&self.wl_storage); - self.initialize_validators( - &staking_token, - &genesis.validators, - &implicit_vp_code_path, - ); - // set the initial validators set - self.set_initial_validators( - &staking_token, - genesis.validators, - &genesis.pos_params, - ) + .conversion_state + .serialize_to_vec() + .unwrap(); + self.wl_storage.write_bytes(&state_key, conv_bytes).unwrap(); } - /// Initialize genesis established accounts - fn initialize_established_accounts( - &mut self, - accounts: Vec, - implicit_vp_code_path: &str, - ) -> Result<()> { - for genesis::EstablishedAccount { - address, - vp_code_path, - vp_sha256, - public_key, - storage, - } in accounts - { - let vp_code_hash = read_wasm_hash(&self.wl_storage, &vp_code_path)? - .ok_or(Error::LoadingWasm(format!( - "Unknown vp code path: {}", - implicit_vp_code_path - )))?; - - // In dev, we don't check the hash - #[cfg(feature = "dev")] - let _ = vp_sha256; - #[cfg(not(feature = "dev"))] - { - assert_eq!( - vp_code_hash.0.as_slice(), - &vp_sha256, - "Invalid established account's VP sha256 hash for {}", - vp_code_path - ); - } - - self.wl_storage - .write_bytes(&Key::validity_predicate(&address), vp_code_hash) - .unwrap(); - - if let Some(pk) = public_key { + /// Init genesis token balances + fn init_token_balances(&mut self, genesis: &genesis::chain::Finalized) { + for (token_alias, TokenBalances(balances)) in &genesis.balances.token { + tracing::debug!("Initializing token balances {token_alias}"); + + let token_address = &genesis + .tokens + .token + .get(token_alias) + .expect("Token with configured balance not found in genesis.") + .address; + for (owner_pk, balance) in balances { + let owner = Address::from(&owner_pk.raw); storage_api::account::set_public_key_at( &mut self.wl_storage, - &address, - &pk, + &owner, + &owner_pk.raw, 0, - )?; - } - - for (key, value) in storage { - self.wl_storage.write_bytes(&key, value).unwrap(); + ) + .unwrap(); + tracing::info!( + "Crediting {} {} tokens to {}", + balance, + token_alias, + owner_pk.raw + ); + credit_tokens( + &mut self.wl_storage, + token_address, + &owner, + balance.amount, + ) + .expect("Couldn't credit initial balance"); } } - - Ok(()) } - /// Initialize genesis implicit accounts - fn initialize_implicit_accounts( + /// Apply genesis txs to initialize established accounts + fn apply_genesis_txs_established_account( &mut self, - accounts: Vec, + genesis: &genesis::chain::Finalized, + vp_cache: &mut HashMap>, ) { - // Initialize genesis implicit - for genesis::ImplicitAccount { public_key } in accounts { - let address: address::Address = (&public_key).into(); - storage_api::account::set_public_key_at( - &mut self.wl_storage, - &address, - &public_key, - 0, - ) + let vp_code = self.lookup_vp("vp_masp", genesis, vp_cache); + let code_hash = CodeHash::sha256(&vp_code); + self.wl_storage + .write_bytes(&Key::validity_predicate(&address::masp()), code_hash) .unwrap(); + if let Some(txs) = genesis.transactions.established_account.as_ref() { + for FinalizedEstablishedAccountTx { + address, + tx: + EstablishedAccountTx { + alias, + vp, + public_key, + storage, + }, + } in txs + { + tracing::debug!( + "Applying genesis tx to init an established account \ + {alias}" + ); + let vp_code = self.lookup_vp(vp, genesis, vp_cache); + let code_hash = CodeHash::sha256(&vp_code); + self.wl_storage + .write_bytes(&Key::validity_predicate(address), code_hash) + .unwrap(); + + if let Some(pk) = public_key { + storage_api::account::set_public_key_at( + &mut self.wl_storage, + address, + &pk.pk.raw, + 0, + ) + .unwrap(); + } + + // Place the keys under the owners sub-storage + let sub_key = namada::core::types::storage::Key::from( + address.to_db_key(), + ); + for (key, value) in storage { + self.wl_storage + .write_bytes( + &sub_key.join(key), + value.to_bytes().unwrap(), + ) + .unwrap(); + } + } } } - /// Initialize genesis token accounts - fn initialize_token_accounts( + /// Apply genesis txs to initialize validator accounts + fn apply_genesis_txs_validator_account( &mut self, - accounts: Vec, + genesis: &genesis::chain::Finalized, + vp_cache: &mut HashMap>, + params: &PosParams, + current_epoch: namada::types::storage::Epoch, ) { - // Initialize genesis token accounts - for genesis::TokenAccount { - address, - denom, - balances, - parameters, - last_inflation, - last_locked_ratio, - } in accounts - { - // Init token parameters and last inflation and caching rates - parameters.init_storage(&address, &mut self.wl_storage); - self.wl_storage - .write( - &token::masp_last_inflation_key(&address), - last_inflation, - ) - .unwrap(); - self.wl_storage - .write( - &token::masp_last_locked_ratio_key(&address), - last_locked_ratio, + if let Some(txs) = genesis.transactions.validator_account.as_ref() { + for FinalizedValidatorAccountTx { + address, + tx: + ValidatorAccountTx { + alias, + vp, + dkg_key, + commission_rate, + max_commission_rate_change, + net_address: _, + account_key, + consensus_key, + protocol_key, + tendermint_node_key: _, + eth_hot_key, + eth_cold_key, + }, + } in txs + { + tracing::debug!( + "Applying genesis tx to init a validator account {alias}" + ); + + let vp_code = self.lookup_vp(vp, genesis, vp_cache); + let code_hash = CodeHash::sha256(&vp_code); + self.wl_storage + .write_bytes(&Key::validity_predicate(address), code_hash) + .expect("Unable to write user VP"); + // Validator account key + storage_api::account::set_public_key_at( + &mut self.wl_storage, + address, + &account_key.pk.raw, + 0, ) .unwrap(); - // associate a token with its denomination. - write_denom(&mut self.wl_storage, &address, denom).unwrap(); - let mut total_balance_for_token = token::Amount::default(); - for (owner, amount) in balances { - total_balance_for_token += amount; - credit_tokens(&mut self.wl_storage, &address, &owner, amount) - .unwrap(); + self.wl_storage + .write(&protocol_pk_key(address), &protocol_key.pk.raw) + .expect("Unable to set genesis user protocol public key"); + + self.wl_storage + .write(&dkg_session_keys::dkg_pk_key(address), &dkg_key.raw) + .expect( + "Unable to set genesis user public DKG session key", + ); + + // TODO: replace pos::init_genesis validators arg with + // init_genesis_validator from here + if let Err(err) = pos::namada_proof_of_stake::become_validator( + BecomeValidator { + storage: &mut self.wl_storage, + params, + address, + consensus_key: &consensus_key.pk.raw, + protocol_key: &protocol_key.pk.raw, + eth_cold_key: ð_cold_key.pk.raw, + eth_hot_key: ð_hot_key.pk.raw, + current_epoch, + commission_rate: *commission_rate, + max_commission_rate_change: *max_commission_rate_change, + offset_opt: Some(0), + }, + ) { + tracing::warn!( + "Genesis init genesis validator tx for {alias} failed \ + with {err}. Skipping." + ); + continue; + } } - // Write the total amount of tokens for the ratio - self.wl_storage - .write( - &token::minted_balance_key(&address), - total_balance_for_token, - ) - .unwrap(); } } - /// Initialize genesis validator accounts - fn initialize_validators( + /// Apply genesis txs to transfer tokens + fn apply_genesis_txs_transfer( &mut self, - staking_token: &Address, - validators: &[genesis::Validator], - implicit_vp_code_path: &str, + genesis: &genesis::chain::Finalized, ) { - // Initialize genesis validator accounts - for validator in validators { - let vp_code_hash = read_wasm_hash( - &self.wl_storage, - &validator.validator_vp_code_path, - ) - .unwrap() - .ok_or(Error::LoadingWasm(format!( - "Unknown vp code path: {}", - implicit_vp_code_path - ))) - .expect("Reading wasms should not fail"); - - #[cfg(not(feature = "dev"))] + if let Some(txs) = &genesis.transactions.transfer { + for TransferTx { + token, + source, + target, + amount, + .. + } in txs { - assert_eq!( - vp_code_hash.0.as_slice(), - &validator.validator_vp_sha256, - "Invalid validator VP sha256 hash for {}", - validator.validator_vp_code_path + let token = match genesis.get_token_address(token) { + Some(token) => { + tracing::debug!( + "Applying genesis tx to transfer {} of token \ + {token} from {source} to {target}", + amount + ); + token + } + None => { + tracing::warn!( + "Genesis token transfer tx uses an unknown token \ + alias {token}. Skipping." + ); + continue; + } + }; + let target = match genesis.get_user_address(target) { + Some(target) => target, + None => { + tracing::warn!( + "Genesis token transfer tx uses an unknown target \ + alias {target}. Skipping." + ); + continue; + } + }; + let source: Address = (&source.raw).into(); + tracing::debug!( + "Transfer addresses: token {token} from {source} to \ + {target}" ); - } - - let addr = &validator.pos_data.address; - self.wl_storage - .write_bytes(&Key::validity_predicate(addr), vp_code_hash) - .expect("Unable to write user VP"); - // Validator account key - storage_api::account::set_public_key_at( - &mut self.wl_storage, - addr, - &validator.account_key, - 0, - ) - .unwrap(); - // Balances - // Account balance (tokens not staked in PoS) - credit_tokens( - &mut self.wl_storage, - staking_token, - addr, - validator.non_staked_balance, - ) - .unwrap(); - - self.wl_storage - .write( - &dkg_session_keys::dkg_pk_key(addr), - &validator.dkg_public_key, - ) - .expect("Unable to set genesis user public DKG session key"); + if let Err(err) = storage_api::token::transfer( + &mut self.wl_storage, + token, + &source, + &target, + amount.amount, + ) { + tracing::warn!( + "Genesis token transfer tx failed with: {err}. \ + Skipping." + ); + continue; + }; + } } } - /// Initialize the PoS and set the initial validator set - fn set_initial_validators( - &mut self, - staking_token: &Address, - validators: Vec, - pos_params: &OwnedPosParams, - ) -> Result { - let mut response = response::InitChain::default(); - // PoS system depends on epoch being initialized. Write the total - // genesis staking token balance to storage after - // initialization. + /// Apply genesis txs to transfer tokens + fn apply_genesis_txs_bonds(&mut self, genesis: &genesis::chain::Finalized) { let (current_epoch, _gas) = self.wl_storage.storage.get_current_epoch(); - pos::init_genesis_storage( - &mut self.wl_storage, - pos_params, - validators.into_iter().map(|validator| validator.pos_data), - current_epoch, - ); - - let total_nam = read_total_supply(&self.wl_storage, staking_token)?; - // At this stage in the chain genesis, the PoS address balance is the - // same as the number of staked tokens - let total_staked_nam = - read_balance(&self.wl_storage, staking_token, &address::POS)?; - - tracing::info!( - "Genesis total native tokens: {}.", - total_nam.to_string_native() - ); - tracing::info!( - "Total staked tokens: {}.", - total_staked_nam.to_string_native() - ); - - // Set the ratio of staked to total NAM tokens in the parameters storage - parameters::update_staked_ratio_parameter( - &mut self.wl_storage, - &(Dec::from(total_staked_nam) / Dec::from(total_nam)), - ) - .expect("unable to set staked ratio of NAM in storage"); - - ibc::init_genesis_storage(&mut self.wl_storage); - - // Set the initial validator set - response.validators = self - .get_abci_validator_updates(true) - .expect("Must be able to set genesis validator set"); - debug_assert!(!response.validators.is_empty()); - - Ok(response) - } -} + if let Some(txs) = &genesis.transactions.bond { + for BondTx { + source, + validator, + amount, + .. + } in txs + { + tracing::debug!( + "Applying genesis tx to bond {} native tokens from \ + {source} to {validator}", + amount, + ); -fn read_wasm_hash( - storage: &impl StorageRead, - path: impl AsRef, -) -> storage_api::Result> { - let hash_key = Key::wasm_hash(path); - match storage.read_bytes(&hash_key)? { - Some(value) => { - let hash = CodeHash::try_from(&value[..]).into_storage_result()?; - Ok(Some(hash)) + let source = match source { + genesis::transactions::AliasOrPk::Alias(alias) => { + match genesis.get_user_address(alias) { + Some(addr) => addr, + None => { + tracing::warn!( + "Cannot find bond source address with \ + alias \"{alias}\". Skipping." + ); + continue; + } + } + } + genesis::transactions::AliasOrPk::PublicKey(pk) => { + Address::from(&pk.raw) + } + }; + + let validator = match genesis.get_validator_address(validator) { + Some(addr) => addr, + None => { + tracing::warn!( + "Cannot find bond validator address with alias \ + \"{validator}\". Skipping." + ); + continue; + } + }; + + if let Err(err) = pos::namada_proof_of_stake::bond_tokens( + &mut self.wl_storage, + Some(&source), + validator, + amount.amount, + current_epoch, + Some(0), + ) { + tracing::warn!( + "Genesis bond tx failed with: {err}. Skipping." + ); + continue; + }; + } } - None => Ok(None), } } diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index ab8ebd5b05..1110a6562d 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -439,7 +439,6 @@ where db_cache: Option<&D::Cache>, vp_wasm_compilation_cache: u64, tx_wasm_compilation_cache: u64, - native_token: Address, ) -> Self { let chain_id = config.chain_id; let db_path = config.shell.db_dir(&chain_id); @@ -451,6 +450,16 @@ where std::fs::create_dir(&base_dir) .expect("Creating directory for Namada should not fail"); } + let native_token = if !cfg!(test) { + let chain_dir = base_dir.join(chain_id.as_str()); + let genesis = + genesis::chain::Finalized::read_toml_files(&chain_dir) + .expect("Missing genesis files"); + genesis.get_native_token().clone() + } else { + address::nam() + }; + // load last state from storage let mut storage = Storage::open( db_path, @@ -472,26 +481,19 @@ where // load in keys and address from wallet if mode is set to `Validator` let mode = match mode { TendermintMode::Validator => { - #[cfg(not(feature = "dev"))] + #[cfg(not(test))] { let wallet_path = &base_dir.join(chain_id.as_str()); - let genesis_path = - &base_dir.join(format!("{}.toml", chain_id.as_str())); tracing::debug!( - "{}", - wallet_path.as_path().to_str().unwrap() - ); - let mut wallet = crate::wallet::load_or_new_from_genesis( - wallet_path, - genesis::genesis_config::open_genesis_config( - genesis_path, - ) - .unwrap(), + "Loading wallet from {}", + wallet_path.to_string_lossy() ); + let mut wallet = crate::wallet::load(wallet_path) + .expect("Validator node must have a wallet"); wallet .take_validator_data() .map(|data| ShellMode::Validator { - data: data.clone(), + data, broadcast_sender, eth_oracle, }) @@ -500,14 +502,15 @@ where wallet", ) } - #[cfg(feature = "dev")] + #[cfg(test)] { let (protocol_keypair, eth_bridge_keypair, dkg_keypair) = - wallet::defaults::validator_keys(); + crate::wallet::defaults::validator_keys(); ShellMode::Validator { - data: wallet::ValidatorData { - address: wallet::defaults::validator_address(), - keys: wallet::ValidatorKeys { + data: ValidatorData { + address: crate::wallet::defaults::validator_address( + ), + keys: ValidatorKeys { protocol_keypair, eth_bridge_keypair, dkg_keypair: Some(dkg_keypair), @@ -1689,7 +1692,6 @@ mod test_utils { None, vp_wasm_compilation_cache, tx_wasm_compilation_cache, - address::nam(), ); shell.wl_storage.storage.block.height = height.into(); (Self { shell }, receiver, eth_sender, control_receiver) @@ -1967,7 +1969,6 @@ mod test_utils { /// We test that on shell shutdown, the tx queue gets persisted in a DB, and /// on startup it is read successfully - #[cfg(feature = "testing")] #[test] fn test_tx_queue_persistence() { let base_dir = tempdir().unwrap().as_ref().canonicalize().unwrap(); @@ -1998,14 +1999,12 @@ mod test_utils { None, vp_wasm_compilation_cache, tx_wasm_compilation_cache, - native_token.clone(), ); shell .wl_storage .storage .begin_block(BlockHash::default(), BlockHeight(1)) .expect("begin_block failed"); - token::testing::init_token_storage(&mut shell.wl_storage, 60); let keypair = gen_keypair(); // enqueue a wrapper tx let mut wrapper = @@ -2035,6 +2034,14 @@ mod test_utils { .block .pred_epochs .new_epoch(BlockHeight(1)); + // Insert a map assigning random addresses to each token alias. + // Needed for storage but not for this test. + for (token, _) in address::tokens() { + shell.wl_storage.storage.conversion_state.tokens.insert( + token.to_string(), + address::gen_deterministic_established_address(token), + ); + } update_allowed_conversions(&mut shell.wl_storage) .expect("update conversions failed"); shell.wl_storage.commit_block().expect("commit failed"); @@ -2064,7 +2071,6 @@ mod test_utils { None, vp_wasm_compilation_cache, tx_wasm_compilation_cache, - address::nam(), ); assert!(!shell.wl_storage.storage.tx_queue.is_empty()); } 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 f817b4cd9f..2cefc82bf4 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 @@ -340,6 +340,7 @@ mod test_bp_vote_extensions { current_epoch: 0.into(), commission_rate: Default::default(), max_commission_rate_change: Default::default(), + offset_opt: None, }) .expect("Test failed"); diff --git a/apps/src/lib/node/ledger/shims/abcipp_shim.rs b/apps/src/lib/node/ledger/shims/abcipp_shim.rs index 0ca7ef39fc..3dd5922e92 100644 --- a/apps/src/lib/node/ledger/shims/abcipp_shim.rs +++ b/apps/src/lib/node/ledger/shims/abcipp_shim.rs @@ -7,7 +7,6 @@ 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; @@ -63,7 +62,6 @@ impl AbcippShim { db_cache: &rocksdb::Cache, vp_wasm_compilation_cache: u64, tx_wasm_compilation_cache: u64, - native_token: Address, ) -> (Self, AbciService, broadcast::Sender<()>) { // We can use an unbounded channel here, because tower-abci limits the // the number of requests that can come in @@ -81,7 +79,6 @@ impl AbcippShim { Some(db_cache), vp_wasm_compilation_cache, tx_wasm_compilation_cache, - native_token, ), #[cfg(not(feature = "abcipp"))] begin_block_request: None, diff --git a/apps/src/lib/node/ledger/storage/mod.rs b/apps/src/lib/node/ledger/storage/mod.rs index 43617879c8..659f561cce 100644 --- a/apps/src/lib/node/ledger/storage/mod.rs +++ b/apps/src/lib/node/ledger/storage/mod.rs @@ -118,7 +118,6 @@ mod tests { assert_eq!(result, None); } - #[cfg(feature = "testing")] #[test] fn test_commit_block() { let db_path = @@ -145,7 +144,6 @@ mod tests { storage.block.pred_epochs.new_epoch(BlockHeight(100)); // make wl_storage to update conversion for a new epoch let mut wl_storage = WlStorage::new(WriteLog::default(), storage); - namada::types::token::testing::init_token_storage(&mut wl_storage, 60); update_allowed_conversions(&mut wl_storage) .expect("update conversions failed"); wl_storage.commit_block().expect("commit failed"); diff --git a/apps/src/lib/node/ledger/tendermint_node.rs b/apps/src/lib/node/ledger/tendermint_node.rs index 0833f7c3a7..ace037cd95 100644 --- a/apps/src/lib/node/ledger/tendermint_node.rs +++ b/apps/src/lib/node/ledger/tendermint_node.rs @@ -9,6 +9,7 @@ use namada::types::key::*; use namada::types::storage::BlockHeight; use namada::types::time::DateTimeUtc; use serde_json::json; +use sha2::{Digest, Sha256}; #[cfg(feature = "abcipp")] use tendermint_abcipp::Moniker; use thiserror::Error; @@ -18,12 +19,14 @@ use tokio::process::Command; use crate::cli::namada_version; use crate::config; +use crate::facade::tendermint::node::Id as TendermintNodeId; #[cfg(feature = "abciplus")] use crate::facade::tendermint::Moniker; use crate::facade::tendermint::{block, Genesis}; use crate::facade::tendermint_config::{ Error as TendermintError, TendermintConfig, }; + /// Env. var to output Tendermint log to stdout pub const ENV_VAR_TM_STDOUT: &str = "NAMADA_CMT_STDOUT"; @@ -83,13 +86,6 @@ pub async fn run( let tendermint_path = from_env_or_default()?; let mode = config.shell.tendermint_mode.to_str().to_owned(); - #[cfg(feature = "dev")] - // This has to be checked before we run tendermint init - let has_validator_key = { - let path = home_dir.join("config").join("priv_validator_key.json"); - Path::new(&path).exists() - }; - // init and run a tendermint node child process let output = Command::new(&tendermint_path) .args(["init", &mode, "--home", &home_dir_string]) @@ -100,14 +96,6 @@ pub async fn run( panic!("Tendermint failed to initialize with {:#?}", output); } - #[cfg(feature = "dev")] - { - let consensus_key = crate::wallet::defaults::validator_keypair(); - // write the validator key file if it didn't already exist - if !has_validator_key { - write_validator_key_async(&home_dir, &consensus_key).await; - } - } #[cfg(feature = "abcipp")] write_tm_genesis(&home_dir, chain_id, genesis_time, &config).await; #[cfg(not(feature = "abcipp"))] @@ -342,6 +330,28 @@ pub fn write_validator_state(home_dir: impl AsRef) { .expect("Couldn't write private validator state file"); } +/// Length of a Tendermint Node ID in bytes +const TENDERMINT_NODE_ID_LENGTH: usize = 20; + +/// Derive Tendermint node ID from public key +pub fn id_from_pk(pk: &common::PublicKey) -> TendermintNodeId { + let mut bytes = [0u8; TENDERMINT_NODE_ID_LENGTH]; + + match pk { + common::PublicKey::Ed25519(_) => { + let _pk: ed25519::PublicKey = pk.try_to_pk().unwrap(); + let digest = Sha256::digest(_pk.serialize_to_vec().as_slice()); + bytes.copy_from_slice(&digest[..TENDERMINT_NODE_ID_LENGTH]); + } + common::PublicKey::Secp256k1(_) => { + let _pk: secp256k1::PublicKey = pk.try_to_pk().unwrap(); + let digest = Sha256::digest(_pk.serialize_to_vec().as_slice()); + bytes.copy_from_slice(&digest[..TENDERMINT_NODE_ID_LENGTH]); + } + } + TendermintNodeId::new(bytes) +} + async fn update_tendermint_config( home_dir: impl AsRef, config: TendermintConfig, diff --git a/apps/src/lib/wallet/alias.rs b/apps/src/lib/wallet/alias.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/src/lib/wallet/cli_utils.rs b/apps/src/lib/wallet/cli_utils.rs deleted file mode 100644 index 9908af12bb..0000000000 --- a/apps/src/lib/wallet/cli_utils.rs +++ /dev/null @@ -1,517 +0,0 @@ -use std::fs::File; -use std::io::{self, Write}; - -use borsh_ext::BorshSerializeExt; -use itertools::sorted; -use masp_primitives::zip32::ExtendedFullViewingKey; -use namada::types::key::{PublicKeyHash, RefTo}; -use namada::types::masp::{MaspValue, PaymentAddress}; -use namada_sdk::masp::find_valid_diversifier; -use namada_sdk::wallet::{DecryptionError, FindKeyError, GenRestoreKeyError}; -use rand_core::OsRng; - -use crate::cli; -use crate::cli::{args, Context}; -use crate::wallet::{read_and_confirm_encryption_password, CliWalletUtils}; - -/// Find shielded address or key -pub fn address_key_find( - ctx: Context, - args::AddrKeyFind { - alias, - unsafe_show_secret, - }: args::AddrKeyFind, -) { - let mut wallet = ctx.wallet; - let alias = alias.to_lowercase(); - if let Ok(viewing_key) = wallet.find_viewing_key(&alias) { - // Check if alias is a viewing key - println!("Viewing key: {}", viewing_key); - if unsafe_show_secret { - // Check if alias is also a spending key - match wallet.find_spending_key(&alias, None) { - Ok(spending_key) => println!("Spending key: {}", spending_key), - Err(FindKeyError::KeyNotFound) => {} - Err(err) => eprintln!("{}", err), - } - } - } else if let Some(payment_addr) = wallet.find_payment_addr(&alias) { - // Failing that, check if alias is a payment address - println!("Payment address: {}", payment_addr); - } else { - // Otherwise alias cannot be referring to any shielded value - println!( - "No shielded address or key with alias {} found. Use the commands \ - `masp list-addrs` and `masp list-keys` to see all the known \ - addresses and keys.", - alias.to_lowercase() - ); - } -} - -/// List spending keys. -pub fn spending_keys_list( - ctx: Context, - args::MaspKeysList { - decrypt, - unsafe_show_secret, - }: args::MaspKeysList, -) { - let wallet = ctx.wallet; - let known_view_keys = wallet.get_viewing_keys(); - let known_spend_keys = wallet.get_spending_keys(); - if known_view_keys.is_empty() { - println!( - "No known keys. Try `masp add --alias my-addr --value ...` to add \ - a new key to the wallet." - ); - } else { - let stdout = std::io::stdout(); - let mut w = stdout.lock(); - writeln!(w, "Known keys:").unwrap(); - for (alias, key) in known_view_keys { - write!(w, " Alias \"{}\"", alias).unwrap(); - let spending_key_opt = known_spend_keys.get(&alias); - // If this alias is associated with a spending key, indicate whether - // or not the spending key is encrypted - // TODO: consider turning if let into match - if let Some(spending_key) = spending_key_opt { - if spending_key.is_encrypted() { - writeln!(w, " (encrypted):") - } else { - writeln!(w, " (not encrypted):") - } - .unwrap(); - } else { - writeln!(w, ":").unwrap(); - } - // Always print the corresponding viewing key - writeln!(w, " Viewing Key: {}", key).unwrap(); - // A subset of viewing keys will have corresponding spending keys. - // Print those too if they are available and requested. - if unsafe_show_secret { - if let Some(spending_key) = spending_key_opt { - match spending_key.get::(decrypt, None) { - // Here the spending key is unencrypted or successfully - // decrypted - Ok(spending_key) => { - writeln!(w, " Spending key: {}", spending_key) - .unwrap(); - } - // Here the key is encrypted but decryption has not been - // requested - Err(DecryptionError::NotDecrypting) if !decrypt => { - continue; - } - // Here the key is encrypted but incorrect password has - // been provided - Err(err) => { - writeln!( - w, - " Couldn't decrypt the spending key: {}", - err - ) - .unwrap(); - } - } - } - } - } - } -} - -/// List payment addresses. -pub fn payment_addresses_list(ctx: Context) { - let wallet = ctx.wallet; - let known_addresses = wallet.get_payment_addrs(); - if known_addresses.is_empty() { - println!( - "No known payment addresses. Try `masp gen-addr --alias my-addr` \ - to generate a new payment address." - ); - } else { - let stdout = std::io::stdout(); - let mut w = stdout.lock(); - writeln!(w, "Known payment addresses:").unwrap(); - for (alias, address) in sorted(known_addresses) { - writeln!(w, " \"{}\": {}", alias, address).unwrap(); - } - } -} - -/// Generate a spending key. -pub fn spending_key_gen( - ctx: Context, - args::MaspSpendKeyGen { - alias, - alias_force, - unsafe_dont_encrypt, - }: args::MaspSpendKeyGen, -) { - let mut wallet = ctx.wallet; - let alias = alias.to_lowercase(); - let password = read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (alias, _key) = wallet.gen_spending_key(alias, password, alias_force); - crate::wallet::save(&wallet).unwrap_or_else(|err| eprintln!("{}", err)); - println!( - "Successfully added a spending key with alias: \"{}\"", - alias - ); -} - -/// Generate a shielded payment address from the given key. -pub fn payment_address_gen( - ctx: Context, - args::MaspPayAddrGen { - alias, - alias_force, - viewing_key, - pin, - }: args::MaspPayAddrGen, -) { - let alias = alias.to_lowercase(); - let viewing_key = ExtendedFullViewingKey::from(viewing_key).fvk.vk; - let (div, _g_d) = find_valid_diversifier(&mut OsRng); - let payment_addr = viewing_key - .to_payment_address(div) - .expect("a PaymentAddress"); - let mut wallet = ctx.wallet; - let alias = wallet - .insert_payment_addr( - alias, - PaymentAddress::from(payment_addr).pinned(pin), - alias_force, - ) - .unwrap_or_else(|| { - eprintln!("Payment address not added"); - cli::safe_exit(1); - }); - crate::wallet::save(&wallet).unwrap_or_else(|err| eprintln!("{}", err)); - println!( - "Successfully generated a payment address with the following alias: {}", - alias, - ); -} - -/// Add a viewing key, spending key, or payment address to wallet. -pub fn address_key_add( - mut ctx: Context, - args::MaspAddrKeyAdd { - alias, - alias_force, - value, - unsafe_dont_encrypt, - }: args::MaspAddrKeyAdd, -) { - let alias = alias.to_lowercase(); - let (alias, typ) = match value { - MaspValue::FullViewingKey(viewing_key) => { - let alias = ctx - .wallet - .insert_viewing_key(alias, viewing_key, alias_force) - .unwrap_or_else(|| { - eprintln!("Viewing key not added"); - cli::safe_exit(1); - }); - (alias, "viewing key") - } - MaspValue::ExtendedSpendingKey(spending_key) => { - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let alias = ctx - .wallet - .encrypt_insert_spending_key( - alias, - spending_key, - password, - alias_force, - ) - .unwrap_or_else(|| { - eprintln!("Spending key not added"); - cli::safe_exit(1); - }); - (alias, "spending key") - } - MaspValue::PaymentAddress(payment_addr) => { - let alias = ctx - .wallet - .insert_payment_addr(alias, payment_addr, alias_force) - .unwrap_or_else(|| { - eprintln!("Payment address not added"); - cli::safe_exit(1); - }); - (alias, "payment address") - } - }; - crate::wallet::save(&ctx.wallet).unwrap_or_else(|err| eprintln!("{}", err)); - println!( - "Successfully added a {} with the following alias to wallet: {}", - typ, alias, - ); -} - -/// Restore a keypair and an implicit address from the mnemonic code in the -/// wallet. -pub fn key_and_address_restore( - ctx: Context, - args::KeyAndAddressRestore { - scheme, - alias, - alias_force, - unsafe_dont_encrypt, - derivation_path, - }: args::KeyAndAddressRestore, -) { - let mut wallet = ctx.wallet; - let encryption_password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (alias, _key) = wallet - .derive_key_from_user_mnemonic_code( - scheme, - alias, - alias_force, - derivation_path, - None, - encryption_password, - ) - .unwrap_or_else(|err| { - eprintln!("{}", err); - cli::safe_exit(1) - }) - .unwrap_or_else(|| { - println!("No changes are persisted. Exiting."); - cli::safe_exit(0); - }); - crate::wallet::save(&wallet).unwrap_or_else(|err| eprintln!("{}", err)); - println!( - "Successfully added a key and an address with alias: \"{}\"", - alias - ); -} - -/// Generate a new keypair and derive implicit address from it and store them in -/// the wallet. -pub fn key_and_address_gen( - ctx: Context, - args::KeyAndAddressGen { - scheme, - alias, - alias_force, - unsafe_dont_encrypt, - derivation_path, - }: args::KeyAndAddressGen, -) { - let mut wallet = ctx.wallet; - let encryption_password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let mut rng = OsRng; - let derivation_path_and_mnemonic_rng = - derivation_path.map(|p| (p, &mut rng)); - let (alias, _key, _mnemonic) = wallet - .gen_key( - scheme, - alias, - alias_force, - None, - encryption_password, - derivation_path_and_mnemonic_rng, - ) - .unwrap_or_else(|err| match err { - GenRestoreKeyError::KeyStorageError => { - println!("No changes are persisted. Exiting."); - cli::safe_exit(0); - } - _ => { - eprintln!("{}", err); - cli::safe_exit(1); - } - }); - crate::wallet::save(&wallet).unwrap_or_else(|err| eprintln!("{}", err)); - println!( - "Successfully added a key and an address with alias: \"{}\"", - alias - ); -} - -/// Find a keypair in the wallet store. -pub fn key_find( - ctx: Context, - args::KeyFind { - public_key, - alias, - value, - unsafe_show_secret, - }: args::KeyFind, -) { - let mut wallet = ctx.wallet; - let found_keypair = match public_key { - Some(pk) => wallet.find_key_by_pk(&pk, None), - None => { - let alias = alias.or(value); - match alias { - None => { - eprintln!( - "An alias, public key or public key hash needs to be \ - supplied" - ); - cli::safe_exit(1) - } - Some(alias) => wallet.find_key(alias.to_lowercase(), None), - } - } - }; - match found_keypair { - Ok(keypair) => { - let pkh: PublicKeyHash = (&keypair.ref_to()).into(); - println!("Public key hash: {}", pkh); - println!("Public key: {}", keypair.ref_to()); - if unsafe_show_secret { - println!("Secret key: {}", keypair); - } - } - Err(err) => { - eprintln!("{}", err); - } - } -} - -/// List all known keys. -pub fn key_list( - ctx: Context, - args::KeyList { - decrypt, - unsafe_show_secret, - }: args::KeyList, -) { - let wallet = ctx.wallet; - let known_keys = wallet.get_keys(); - if known_keys.is_empty() { - println!( - "No known keys. Try `key gen --alias my-key` to generate a new \ - key." - ); - } else { - let stdout = io::stdout(); - let mut w = stdout.lock(); - writeln!(w, "Known keys:").unwrap(); - for (alias, (stored_keypair, pkh)) in known_keys { - let encrypted = if stored_keypair.is_encrypted() { - "encrypted" - } else { - "not encrypted" - }; - writeln!(w, " Alias \"{}\" ({}):", alias, encrypted).unwrap(); - if let Some(pkh) = pkh { - writeln!(w, " Public key hash: {}", pkh).unwrap(); - } - match stored_keypair.get::(decrypt, None) { - Ok(keypair) => { - writeln!(w, " Public key: {}", keypair.ref_to()) - .unwrap(); - if unsafe_show_secret { - writeln!(w, " Secret key: {}", keypair).unwrap(); - } - } - Err(DecryptionError::NotDecrypting) if !decrypt => { - continue; - } - Err(err) => { - writeln!(w, " Couldn't decrypt the keypair: {}", err) - .unwrap(); - } - } - } - } -} - -/// Export a keypair to a file. -pub fn key_export(ctx: Context, args::KeyExport { alias }: args::KeyExport) { - let mut wallet = ctx.wallet; - wallet - .find_key(alias.to_lowercase(), None) - .map(|keypair| { - let file_data = keypair.serialize_to_vec(); - let file_name = format!("key_{}", alias.to_lowercase()); - let mut file = File::create(&file_name).unwrap(); - - file.write_all(file_data.as_ref()).unwrap(); - println!("Exported to file {}", file_name); - }) - .unwrap_or_else(|err| { - eprintln!("{}", err); - cli::safe_exit(1) - }) -} - -/// List all known addresses. -pub fn address_list(ctx: Context) { - let wallet = ctx.wallet; - let known_addresses = wallet.get_addresses(); - if known_addresses.is_empty() { - println!( - "No known addresses. Try `address gen --alias my-addr` to \ - generate a new implicit address." - ); - } else { - let stdout = io::stdout(); - let mut w = stdout.lock(); - writeln!(w, "Known addresses:").unwrap(); - for (alias, address) in sorted(known_addresses) { - writeln!(w, " \"{}\": {}", alias, address.to_pretty_string()) - .unwrap(); - } - } -} - -/// Find address (alias) by its alias (address). -pub fn address_or_alias_find(ctx: Context, args: args::AddressOrAliasFind) { - let wallet = ctx.wallet; - if args.address.is_some() && args.alias.is_some() { - panic!( - "This should not be happening: clap should emit its own error \ - message." - ); - } else if args.alias.is_some() { - if let Some(address) = wallet.find_address(args.alias.as_ref().unwrap()) - { - println!("Found address {}", address.to_pretty_string()); - } else { - println!( - "No address with alias {} found. Use the command `address \ - list` to see all the known addresses.", - args.alias.unwrap().to_lowercase() - ); - } - } else if args.address.is_some() { - if let Some(alias) = wallet.find_alias(args.address.as_ref().unwrap()) { - println!("Found alias {}", alias); - } else { - println!( - "No alias with address {} found. Use the command `address \ - list` to see all the known addresses.", - args.address.unwrap() - ); - } - } -} - -/// Add an address to the wallet. -pub fn address_add(ctx: Context, args: args::AddressAdd) { - let mut wallet = ctx.wallet; - if wallet - .add_address( - args.alias.clone().to_lowercase(), - args.address, - args.alias_force, - ) - .is_none() - { - eprintln!("Address not added"); - cli::safe_exit(1); - } - crate::wallet::save(&wallet).unwrap_or_else(|err| eprintln!("{}", err)); - println!( - "Successfully added a key and an address with alias: \"{}\"", - args.alias.to_lowercase() - ); -} diff --git a/apps/src/lib/wallet/defaults.rs b/apps/src/lib/wallet/defaults.rs index 82a9524daa..cbb027b8f8 100644 --- a/apps/src/lib/wallet/defaults.rs +++ b/apps/src/lib/wallet/defaults.rs @@ -1,78 +1,14 @@ //! Default addresses and keys. -#[cfg(any(test, feature = "dev"))] +#[cfg(any(test, feature = "testing", feature = "benches"))] pub use dev::{ addresses, albert_address, albert_keypair, bertha_address, bertha_keypair, christel_address, christel_keypair, daewon_address, daewon_keypair, ester_address, ester_keypair, keys, validator_address, validator_keypair, validator_keys, }; -use namada::core::ledger::eth_bridge::storage::bridge_pool::BRIDGE_POOL_ADDRESS; -use namada::ledger::{governance, pgf, pos}; -use namada::types::address::Address; -use namada::types::key::*; -use namada_sdk::wallet::alias::Alias; -use crate::config::genesis::genesis_config::GenesisConfig; - -/// The default addresses with their aliases. -pub fn addresses_from_genesis(genesis: GenesisConfig) -> Vec<(Alias, Address)> { - // Internal addresses - let mut addresses: Vec<(Alias, Address)> = vec![ - ("pos".into(), pos::ADDRESS), - ("pos_slash_pool".into(), pos::SLASH_POOL_ADDRESS), - ("governance".into(), governance::ADDRESS), - ("eth_bridge".into(), namada_sdk::eth_bridge::ADDRESS), - ("bridge_pool".into(), BRIDGE_POOL_ADDRESS), - ("pgf".into(), pgf::ADDRESS), - ]; - // Genesis validators - let validator_addresses = - genesis.validator.into_iter().map(|(alias, validator)| { - // The address must be set in the genesis config file - ( - alias.into(), - Address::decode(validator.address.unwrap()).unwrap(), - ) - }); - addresses.extend(validator_addresses); - // Genesis tokens - let token_addresses = genesis.token.into_iter().map(|(alias, token)| { - // The address must be set in the genesis config file - ( - alias.into(), - Address::decode(token.address.unwrap()).unwrap(), - ) - }); - addresses.extend(token_addresses); - // Genesis established accounts - if let Some(accounts) = genesis.established { - let est_addresses = accounts.into_iter().map(|(alias, established)| { - // The address must be set in the genesis config file - ( - alias.into(), - Address::decode(established.address.unwrap()).unwrap(), - ) - }); - addresses.extend(est_addresses); - } - // Genesis implicit accounts - if let Some(accounts) = genesis.implicit { - let imp_addresses = - accounts.into_iter().filter_map(|(alias, implicit)| { - // The public key may not be revealed, only add it if it is - implicit.public_key.map(|pk| { - let pk: common::PublicKey = pk.to_public_key().unwrap(); - let addr: Address = (&pk).into(); - (alias.into(), addr) - }) - }); - addresses.extend(imp_addresses); - } - addresses -} - -#[cfg(any(test, feature = "dev"))] +#[cfg(any(test, feature = "testing", feature = "benches"))] mod dev { use std::collections::HashMap; @@ -85,27 +21,30 @@ mod dev { use namada::types::key::*; use namada_sdk::wallet::alias::Alias; - /// Generate a new protocol signing keypair, eth hot key and DKG session - /// keypair + /// N.B. these are the corresponding values from + /// `genesis/pre-genesis/validator-0/validator-wallet.toml`. + /// + /// If that wallet is regenerated, these values must be changed to fix unit + /// tests. pub fn validator_keys() -> (common::SecretKey, common::SecretKey, DkgKeypair) { // ed25519 bytes let bytes: [u8; 33] = [ - 0, 200, 107, 23, 252, 78, 80, 8, 164, 142, 3, 194, 33, 12, 250, - 169, 211, 127, 47, 13, 194, 54, 199, 81, 102, 246, 189, 119, 144, - 25, 27, 113, 222, + 0, 217, 87, 83, 250, 179, 159, 135, 229, 194, 14, 202, 177, 38, + 144, 254, 250, 103, 233, 113, 100, 202, 111, 23, 214, 122, 235, + 165, 8, 131, 185, 61, 222, ]; // secp256k1 bytes let eth_bridge_key_bytes = [ - 1, 117, 93, 118, 129, 202, 67, 51, 62, 202, 196, 130, 244, 5, 44, - 88, 200, 121, 169, 11, 227, 79, 223, 74, 88, 49, 132, 213, 59, 64, - 20, 13, 82, + 1, 38, 59, 91, 81, 119, 89, 252, 48, 195, 171, 237, 19, 144, 123, + 117, 231, 121, 218, 231, 14, 54, 117, 19, 90, 120, 141, 231, 199, + 7, 110, 254, 191, ]; // DkgKeypair let dkg_bytes = [ - 32, 0, 0, 0, 210, 193, 55, 24, 92, 233, 23, 2, 73, 204, 221, 107, - 110, 222, 192, 136, 54, 24, 108, 236, 137, 27, 121, 142, 142, 7, - 193, 248, 155, 56, 51, 21, + 32, 0, 0, 0, 208, 36, 153, 32, 179, 193, 163, 222, 29, 238, 154, + 53, 181, 71, 213, 162, 59, 130, 225, 93, 57, 20, 161, 254, 52, 1, + 172, 184, 112, 189, 160, 102, ]; ( @@ -195,57 +134,57 @@ mod dev { Address::decode("atest1v4ehgw36ggcnsdee8qerswph8y6ry3p5xgunvve3xaqngd3kxc6nqwz9gseyydzzg5unys3ht2n48q").expect("The token address decoding shouldn't fail") } + /// N.B. this is the corresponding value from + /// `genesis/pre-genesis/wallet.toml`. pub fn albert_keypair() -> common::SecretKey { - // generated from - // [`namada::types::key::ed25519::gen_keypair`] let bytes = [ - 115, 191, 32, 247, 18, 101, 5, 106, 26, 203, 48, 145, 39, 41, 41, - 196, 252, 190, 245, 222, 96, 209, 34, 36, 40, 214, 169, 156, 235, - 78, 188, 33, + 131, 49, 140, 204, 234, 198, 192, 138, 1, 119, 102, 120, 64, 180, + 185, 63, 14, 69, 94, 69, 212, 195, 140, 40, 183, 59, 143, 132, 98, + 251, 245, 72, ]; let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); ed_sk.try_to_sk().unwrap() } + /// N.B. this is the corresponding value from + /// `genesis/pre-genesis/wallet.toml`. pub fn bertha_keypair() -> common::SecretKey { - // generated from - // [`namada::types::key::ed25519::gen_keypair`] let bytes = [ - 240, 3, 224, 69, 201, 148, 60, 53, 112, 79, 80, 107, 101, 127, 186, - 6, 176, 162, 113, 224, 62, 8, 183, 187, 124, 234, 244, 251, 92, 36, - 119, 243, + 115, 237, 97, 129, 119, 32, 210, 119, 132, 231, 169, 188, 164, 166, + 6, 104, 215, 99, 166, 247, 236, 172, 45, 69, 237, 31, 36, 26, 165, + 197, 158, 153, ]; let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); ed_sk.try_to_sk().unwrap() } + /// N.B. this is the corresponding value from + /// `genesis/pre-genesis/wallet.toml`. pub fn christel_keypair() -> common::SecretKey { - // generated from - // [`namada::types::key::ed25519::gen_keypair`] let bytes = [ - 65, 198, 96, 145, 237, 227, 84, 182, 107, 55, 209, 235, 115, 105, - 71, 190, 234, 137, 176, 188, 181, 174, 183, 49, 131, 230, 46, 39, - 70, 20, 130, 253, + 54, 37, 185, 57, 165, 142, 246, 4, 2, 215, 207, 143, 192, 66, 80, + 2, 108, 193, 186, 144, 204, 48, 40, 175, 28, 230, 178, 43, 232, 87, + 255, 199, ]; let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); ed_sk.try_to_sk().unwrap() } + /// N.B. this is the corresponding value from + /// `genesis/pre-genesis/wallet.toml`. pub fn daewon_keypair() -> common::SecretKey { - // generated from - // [`namada::types::key::ed25519::gen_keypair`] let bytes = [ - 235, 250, 15, 1, 145, 250, 172, 218, 247, 27, 63, 212, 60, 47, 164, - 57, 187, 156, 182, 144, 107, 174, 38, 81, 37, 40, 19, 142, 68, 135, - 57, 50, + 209, 158, 34, 108, 14, 125, 18, 61, 121, 245, 144, 139, 89, 72, + 212, 196, 97, 182, 106, 95, 138, 169, 86, 0, 194, 139, 85, 171, + 111, 93, 199, 114, ]; let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); ed_sk.try_to_sk().unwrap() } + /// N.B. this is the corresponding value from + /// `genesis/pre-genesis/wallet.toml`. pub fn ester_keypair() -> common::SecretKey { - // generated from - // [`namada::types::key::secp256k1::gen_keypair`] let bytes = [ 54, 144, 147, 226, 3, 93, 132, 247, 42, 126, 90, 23, 200, 155, 122, 147, 139, 93, 8, 204, 135, 178, 40, 152, 5, 227, 175, 204, 102, @@ -255,13 +194,15 @@ mod dev { sk.try_to_sk().unwrap() } + /// N.B. this is the consensus key from + /// `genesis/pre-genesis/validator-0/validator-wallet.toml`. + /// If that wallet is regenerated, this value must be changed to fix unit + /// tests. pub fn validator_keypair() -> common::SecretKey { - // generated from - // [`namada::types::key::ed25519::gen_keypair`] let bytes = [ - 80, 110, 166, 33, 135, 254, 34, 138, 253, 44, 214, 71, 50, 230, 39, - 246, 124, 201, 68, 138, 194, 251, 192, 36, 55, 160, 211, 68, 65, - 189, 121, 217, + 194, 41, 223, 103, 103, 178, 152, 145, 161, 212, 82, 133, 69, 13, + 133, 136, 238, 11, 198, 182, 29, 41, 75, 249, 88, 0, 28, 215, 217, + 63, 234, 78, ]; let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); ed_sk.try_to_sk().unwrap() diff --git a/apps/src/lib/wallet/mod.rs b/apps/src/lib/wallet/mod.rs index 18818daef5..779a137ccd 100644 --- a/apps/src/lib/wallet/mod.rs +++ b/apps/src/lib/wallet/mod.rs @@ -1,4 +1,3 @@ -pub mod cli_utils; pub mod defaults; pub mod pre_genesis; mod store; @@ -9,13 +8,12 @@ use std::str::FromStr; use std::{env, fs}; use namada::bip39::{Language, Mnemonic}; -use namada::types::address::Address; use namada::types::key::*; pub use namada_sdk::wallet::alias::Alias; use namada_sdk::wallet::fs::FsWalletStorage; use namada_sdk::wallet::store::Store; use namada_sdk::wallet::{ - AddressVpType, ConfirmationResponse, FindKeyError, GenRestoreKeyError, + ConfirmationResponse, FindKeyError, GenRestoreKeyError, Wallet, WalletIo, }; pub use namada_sdk::wallet::{ValidatorData, ValidatorKeys}; @@ -24,8 +22,6 @@ pub use store::wallet_file; use zeroize::Zeroizing; use crate::cli; -use crate::config::genesis::genesis_config::GenesisConfig; - #[derive(Debug, Clone)] pub struct CliWalletUtils { store_dir: PathBuf, @@ -239,23 +235,6 @@ where .transpose() } -/// Add addresses from a genesis configuration. -pub fn add_genesis_addresses( - wallet: &mut Wallet, - genesis: GenesisConfig, -) { - for (alias, config) in &genesis.token { - let exp_addr = format!("Genesis token {alias} must have address"); - let address = - Address::from_str(config.address.as_ref().expect(&exp_addr)) - .expect("Valid address"); - wallet.add_vp_type_to_address(AddressVpType::Token, address); - } - for (alias, addr) in defaults::addresses_from_genesis(genesis) { - wallet.add_address(alias.normalize(), addr, true); - } -} - /// Save the wallet store to a file. pub fn save(wallet: &Wallet) -> std::io::Result<()> { wallet @@ -285,20 +264,10 @@ pub fn load_or_new(store_dir: &Path) -> Wallet { wallet } -/// Load a wallet from the store file or create a new one with the default -/// addresses loaded from the genesis file, if not found. -pub fn load_or_new_from_genesis( - store_dir: &Path, - genesis_cfg: GenesisConfig, -) -> Wallet { - let store = self::store::load_or_new_from_genesis(store_dir, genesis_cfg) - .unwrap_or_else(|err| { - eprintln!("Unable to load the wallet: {}", err); - cli::safe_exit(1) - }); - let mut wallet = CliWalletUtils::new(store_dir.to_path_buf()); - *wallet.store_mut() = store; - wallet +/// Check if a wallet exists in the given store dir. +pub fn exists(store_dir: &Path) -> bool { + let file = wallet_file(store_dir); + file.exists() } /// Read the password for encryption from the file/env/stdin, with diff --git a/apps/src/lib/wallet/pre_genesis.rs b/apps/src/lib/wallet/pre_genesis.rs index da12c2dcce..6144c857af 100644 --- a/apps/src/lib/wallet/pre_genesis.rs +++ b/apps/src/lib/wallet/pre_genesis.rs @@ -14,7 +14,7 @@ use crate::wallet::store::gen_validator_keys; use crate::wallet::{read_and_confirm_encryption_password, CliWalletUtils}; /// Validator pre-genesis wallet file name -const VALIDATOR_FILE_NAME: &str = "wallet.toml"; +const VALIDATOR_FILE_NAME: &str = "validator-wallet.toml"; /// Get the path to the validator pre-genesis wallet store. pub fn validator_file_name(store_dir: impl AsRef) -> PathBuf { diff --git a/apps/src/lib/wallet/store.rs b/apps/src/lib/wallet/store.rs index 62eae8ac0e..dffaf48a98 100644 --- a/apps/src/lib/wallet/store.rs +++ b/apps/src/lib/wallet/store.rs @@ -1,20 +1,15 @@ use std::path::{Path, PathBuf}; -#[cfg(not(feature = "dev"))] use std::str::FromStr; use ark_std::rand::prelude::*; use ark_std::rand::SeedableRng; -#[cfg(not(feature = "dev"))] use namada::types::address::Address; use namada::types::key::*; use namada::types::transaction::EllipticCurve; -#[cfg(not(feature = "dev"))] use namada_sdk::wallet::store::AddressVpType; -#[cfg(feature = "dev")] use namada_sdk::wallet::StoredKeypair; use namada_sdk::wallet::{gen_sk_rng, LoadStoreError, Store, ValidatorKeys}; -use crate::config::genesis::genesis_config::GenesisConfig; use crate::wallet::CliWalletUtils; /// Wallet file name @@ -34,28 +29,6 @@ pub fn load_or_new(store_dir: &Path) -> Result { }) } -/// Load the store file or create a new one with the default addresses from -/// the genesis file, if not found. -pub fn load_or_new_from_genesis( - store_dir: &Path, - genesis_cfg: GenesisConfig, -) -> Result { - load(store_dir).or_else(|_| { - #[cfg(not(feature = "dev"))] - let store = new(genesis_cfg); - #[cfg(feature = "dev")] - let store = { - // The function is unused in dev - let _ = genesis_cfg; - new() - }; - let mut wallet = CliWalletUtils::new(store_dir.to_path_buf()); - *wallet.store_mut() = store; - wallet.save()?; - Ok(wallet.into()) - }) -} - /// Attempt to load the store file. pub fn load(store_dir: &Path) -> Result { let mut wallet = CliWalletUtils::new(store_dir.to_path_buf()); @@ -63,38 +36,6 @@ pub fn load(store_dir: &Path) -> Result { Ok(wallet.into()) } -/// Add addresses from a genesis configuration. -#[cfg(not(feature = "dev"))] -pub fn add_genesis_addresses(store: &mut Store, genesis: GenesisConfig) { - for (alias, addr) in - super::defaults::addresses_from_genesis(genesis.clone()) - { - store.insert_address::(alias, addr, true); - } - for (alias, token) in &genesis.token { - if let Some(address) = token.address.as_ref() { - match Address::from_str(address) { - Ok(address) => { - store.add_vp_type_to_address(AddressVpType::Token, address) - } - Err(_) => { - tracing::error!( - "Weird address for token {alias}: {address}" - ) - } - } - } - } -} - -#[cfg(not(feature = "dev"))] -fn new(genesis: GenesisConfig) -> Store { - let mut store = Store::default(); - add_genesis_addresses(&mut store, genesis); - store -} - -#[cfg(feature = "dev")] fn new() -> Store { let mut store = Store::default(); // Pre-load the default keys without encryption @@ -143,7 +84,7 @@ pub fn gen_validator_keys( } } -#[cfg(all(test, feature = "dev"))] +#[cfg(test)] mod test_wallet { use namada::types::address::Address; diff --git a/core/src/ledger/pgf/parameters.rs b/core/src/ledger/pgf/parameters.rs index 6319ec33d0..1d45ea79d2 100644 --- a/core/src/ledger/pgf/parameters.rs +++ b/core/src/ledger/pgf/parameters.rs @@ -1,6 +1,7 @@ use std::collections::BTreeSet; use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; use super::storage::keys as pgf_storage; use super::storage::steward::StewardDetail; @@ -18,6 +19,8 @@ use crate::types::dec::Dec; Hash, BorshSerialize, BorshDeserialize, + Serialize, + Deserialize, )] /// Pgf parameter structure pub struct PgfParameters { diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 830068220e..6b8d542d53 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -27,6 +27,8 @@ pub struct ConversionState { pub normed_inflation: Option, /// The tree currently containing all the conversions pub tree: FrozenCommitmentTree, + /// A map from token alias to actual address. + pub tokens: BTreeMap, /// Map assets to their latest conversion and position in Merkle tree #[allow(clippy::type_complexity)] pub assets: BTreeMap< @@ -215,7 +217,15 @@ where let key_prefix: storage::Key = masp_addr.to_db_key().into(); let tokens = address::tokens(); - let mut masp_reward_keys: Vec<_> = tokens.into_keys().collect(); + let mut masp_reward_keys: Vec<_> = tokens.into_keys() + .map(|k| wl_storage + .storage + .conversion_state + .tokens + .get(k) + .unwrap_or_else(|| panic!("Could not find token alias {} in MASP conversion state.", k)) + .clone() + ).collect(); // Put the native rewards first because other inflation computations depend // on it let native_token = wl_storage.storage.native_token.clone(); @@ -293,7 +303,7 @@ where "MASP reward for {} assumed to be 0 because the \ computed value is too large. Please check the \ inflation parameters.", - *addr + addr ); *normed_inflation }); @@ -339,7 +349,7 @@ where "MASP reward for {} assumed to be 0 because the \ computed value is too large. Please check the \ inflation parameters.", - *addr + addr ); 0u128 }); diff --git a/core/src/ledger/storage_api/pgf.rs b/core/src/ledger/storage_api/pgf.rs index b456cef0f9..91cc17b71e 100644 --- a/core/src/ledger/storage_api/pgf.rs +++ b/core/src/ledger/storage_api/pgf.rs @@ -90,10 +90,10 @@ where let pgf_inflation_rate: Dec = storage .read(&pgf_inflation_rate_key)? - .expect("Parameter should be definied."); + .expect("Parameter should be defined."); let stewards_inflation_rate: Dec = storage .read(&stewards_inflation_rate_key)? - .expect("Parameter should be definied."); + .expect("Parameter should be defined."); Ok(PgfParameters { pgf_inflation_rate, diff --git a/core/src/proto/mod.rs b/core/src/proto/mod.rs index 9dfe32e644..ecf8606008 100644 --- a/core/src/proto/mod.rs +++ b/core/src/proto/mod.rs @@ -4,8 +4,9 @@ pub mod generated; mod types; pub use types::{ - Code, Commitment, CompressedSignature, Data, Dkg, Error, Header, - MaspBuilder, Section, Signable, SignableEthMessage, Signature, + standalone_signature, verify_standalone_sig, Code, Commitment, + CompressedSignature, Data, Dkg, Error, Header, MaspBuilder, Section, + SerializeWithBorsh, Signable, SignableEthMessage, Signature, SignatureIndex, Signed, Signer, Tx, TxError, }; diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index 2702d4285e..b1d2b40cb5 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -246,6 +246,30 @@ impl> Signed { } } +/// Get a signature for data +pub fn standalone_signature>( + keypair: &common::SecretKey, + data: &T, +) -> common::Signature { + let to_sign = S::as_signable(data); + common::SigScheme::sign_with_hasher::(keypair, to_sign) +} + +/// Verify that the input data has been signed by the secret key +/// counterpart of the given public key. +pub fn verify_standalone_sig>( + data: &T, + pk: &common::PublicKey, + sig: &common::Signature, +) -> std::result::Result<(), VerifySigError> { + let signed_data = S::as_signable(data); + common::SigScheme::verify_signature_with_hasher::( + pk, + &signed_data, + sig, + ) +} + /// A section representing transaction data #[derive( Clone, diff --git a/core/src/types/address.rs b/core/src/types/address.rs index f7c63bb470..3493fc6d8f 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -7,31 +7,24 @@ use std::hash::Hash; use std::io::ErrorKind; use std::str::FromStr; -use bech32::{self, FromBase32, ToBase32, Variant}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use borsh_ext::BorshSerializeExt; use data_encoding::HEXUPPER; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use thiserror::Error; use crate::ibc::Signer; +use crate::impl_display_and_from_str_via_format; use crate::types::ethereum_events::EthAddress; -use crate::types::key; use crate::types::key::PublicKeyHash; use crate::types::token::Denomination; +use crate::types::{key, string_encoding}; /// The length of an established [`Address`] encoded with Borsh. pub const ESTABLISHED_ADDRESS_BYTES_LEN: usize = 21; /// The length of [`Address`] encoded with Bech32m. -pub const ADDRESS_LEN: usize = 79 + ADDRESS_HRP.len(); - -/// human-readable part of Bech32m encoded address -// TODO use "a" for live network -const ADDRESS_HRP: &str = "atest"; -/// We're using "Bech32m" variant -pub const BECH32M_VARIANT: bech32::Variant = Variant::Bech32m; +pub const ADDRESS_LEN: usize = 79 + string_encoding::hrp_len::
(); /// Length of a hash of an address as a hexadecimal string pub(crate) const HASH_HEX_LEN: usize = 40; @@ -89,6 +82,12 @@ mod internal { "ano::Pgf "; } +/// Error from decoding address from string +pub type DecodeError = string_encoding::DecodeError; + +/// Result of decoding address from string +pub type Result = std::result::Result; + /// Fixed-length address strings prefix for established addresses. const PREFIX_ESTABLISHED: &str = "est"; /// Fixed-length address strings prefix for implicit addresses. @@ -102,26 +101,6 @@ const PREFIX_ETH: &str = "eth"; /// Fixed-length address strings prefix for Non-Usable-Token addresses. const PREFIX_NUT: &str = "nut"; -#[allow(missing_docs)] -#[derive(Error, Debug, PartialEq, Eq, Clone)] -pub enum DecodeError { - #[error("Error decoding address from Bech32m: {0}")] - DecodeBech32(bech32::Error), - #[error("Error decoding address from base32: {0}")] - DecodeBase32(bech32::Error), - #[error("Unexpected Bech32m human-readable part {0}, expected {1}")] - UnexpectedBech32Prefix(String, String), - #[error("Unexpected Bech32m variant {0:?}, expected {BECH32M_VARIANT:?}")] - UnexpectedBech32Variant(bech32::Variant), - #[error("Invalid address encoding: {0}, {1}")] - InvalidInnerEncoding(ErrorKind, String), - #[error("Invalid address encoding")] - InvalidInnerEncodingStr(String), -} - -/// Result of a function that may fail -pub type Result = std::result::Result; - /// An account's address #[derive( Clone, BorshSerialize, BorshDeserialize, BorshSchema, PartialEq, Eq, Hash, @@ -153,33 +132,12 @@ impl Ord for Address { impl Address { /// Encode an address with Bech32m encoding pub fn encode(&self) -> String { - let bytes = self.to_fixed_len_string(); - bech32::encode(ADDRESS_HRP, bytes.to_base32(), BECH32M_VARIANT) - .unwrap_or_else(|_| { - panic!( - "The human-readable part {} should never cause a failure", - ADDRESS_HRP - ) - }) + string_encoding::Format::encode(self) } /// Decode an address from Bech32m encoding pub fn decode(string: impl AsRef) -> Result { - let (prefix, hash_base32, variant) = bech32::decode(string.as_ref()) - .map_err(DecodeError::DecodeBech32)?; - if prefix != ADDRESS_HRP { - return Err(DecodeError::UnexpectedBech32Prefix( - prefix, - ADDRESS_HRP.into(), - )); - } - match variant { - BECH32M_VARIANT => {} - _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), - } - let bytes: Vec = FromBase32::from_base32(&hash_base32) - .map_err(DecodeError::DecodeBase32)?; - Self::try_from_fixed_len_string(&mut &bytes[..]) + string_encoding::Format::decode(string) } /// Try to get a raw hash of an address, only defined for established and @@ -199,7 +157,7 @@ impl Address { } /// Convert an address to a fixed length 7-bit ascii string bytes - fn to_fixed_len_string(&self) -> Vec { + pub fn to_fixed_len_string(&self) -> Vec { let mut string = match self { Address::Established(EstablishedAddress { hash }) => { // The bech32m's data is a hex of the first 40 chars of the hash @@ -276,7 +234,7 @@ impl Address { let raw = HEXUPPER.decode(hash.as_bytes()).map_err(|e| { DecodeError::InvalidInnerEncoding( - std::io::ErrorKind::InvalidInput, + ErrorKind::InvalidInput, e.to_string(), ) })?; @@ -394,6 +352,20 @@ impl Address { } } +impl string_encoding::Format for Address { + const HRP: &'static str = string_encoding::ADDRESS_HRP; + + fn to_bytes(&self) -> Vec { + Self::to_fixed_len_string(self) + } + + fn decode_bytes(bytes: &[u8]) -> Result { + Self::try_from_fixed_len_string(&mut &bytes[..]) + } +} + +impl_display_and_from_str_via_format!(Address); + impl serde::Serialize for Address { fn serialize( &self, @@ -418,43 +390,18 @@ impl<'de> serde::Deserialize<'de> for Address { } } -impl Display for Address { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.encode()) - } -} - impl Debug for Address { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.pretty_fmt(f) } } -impl FromStr for Address { - type Err = DecodeError; - - fn from_str(s: &str) -> Result { - Address::decode(s) - } -} - /// for IBC signer impl TryFrom for Address { type Error = DecodeError; fn try_from(signer: Signer) -> Result { - // The given address should be an address or payment address. When - // sending a token from a spending key, it has been already - // replaced with the MASP address. - Address::decode(signer.as_ref()).or( - match crate::types::masp::PaymentAddress::from_str(signer.as_ref()) - { - Ok(_) => Ok(masp()), - Err(_) => Err(DecodeError::InvalidInnerEncodingStr(format!( - "Invalid address for IBC transfer: {signer}" - ))), - }, - ) + Address::decode(signer.as_ref()) } } @@ -478,7 +425,17 @@ pub struct EstablishedAddress { } /// A generator of established addresses -#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +#[derive( + Debug, + Default, + Clone, + PartialEq, + Eq, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, +)] pub struct EstablishedAddressGen { last_hash: [u8; SHA_HASH_LEN], } @@ -603,6 +560,20 @@ impl Display for InternalAddress { } } +impl InternalAddress { + /// Certain internal addresses have reserved aliases. + pub fn try_from_alias(alias: &str) -> Option { + match alias { + "pos" => Some(InternalAddress::PoS), + "ibc" => Some(InternalAddress::Ibc), + "ethbridge" => Some(InternalAddress::EthBridge), + "bridgepool" => Some(InternalAddress::EthBridgePool), + "governance" => Some(InternalAddress::Governance), + _ => None, + } + } +} + /// Temporary helper for testing pub fn nam() -> Address { Address::decode("atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5").expect("The token address decoding shouldn't fail") @@ -666,15 +637,15 @@ pub const fn wnam() -> EthAddress { /// Temporary helper for testing, a hash map of tokens addresses with their /// informal currency codes and number of decimal places. -pub fn tokens() -> HashMap { +pub fn tokens() -> HashMap<&'static str, Denomination> { vec![ - (nam(), ("NAM", 6.into())), - (btc(), ("BTC", 8.into())), - (eth(), ("ETH", 18.into())), - (dot(), ("DOT", 10.into())), - (schnitzel(), ("Schnitzel", 6.into())), - (apfel(), ("Apfel", 6.into())), - (kartoffel(), ("Kartoffel", 6.into())), + ("NAM", 6.into()), + ("BTC", 8.into()), + ("ETH", 18.into()), + ("DOT", 10.into()), + ("Schnitzel", 6.into()), + ("Apfel", 6.into()), + ("Kartoffel", 6.into()), ] .into_iter() .collect() diff --git a/core/src/types/chain.rs b/core/src/types/chain.rs index 43977d8812..47f2af8146 100644 --- a/core/src/types/chain.rs +++ b/core/src/types/chain.rs @@ -176,12 +176,7 @@ impl ProposalBytes { } } -/// Development default chain ID. Must be [`CHAIN_ID_LENGTH`] long. -#[cfg(feature = "dev")] -pub const DEFAULT_CHAIN_ID: &str = "namada-devchain.00000000000000"; - /// Release default chain ID. Must be [`CHAIN_ID_LENGTH`] long. -#[cfg(not(feature = "dev"))] pub const DEFAULT_CHAIN_ID: &str = "namada-internal.00000000000000"; /// Chain ID diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index 40494ffad0..406144a0df 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -170,6 +170,20 @@ impl Dec { pub fn is_negative(&self) -> bool { self.0.is_negative() } + + /// Return the integer value of a [`Dec`] by rounding up. + pub fn ceil(&self) -> I256 { + if self.0.is_negative() { + self.to_i256() + } else { + let floor = self.to_i256(); + if (*self - Dec(floor)).is_zero() { + floor + } else { + floor + I256::one() + } + } + } } impl FromStr for Dec { @@ -178,7 +192,7 @@ impl FromStr for Dec { fn from_str(s: &str) -> Result { let ((large, small), is_neg) = if let Some(strip) = s.strip_prefix('-') { - (strip.split_once('.').unwrap_or((s, "0")), true) + (strip.split_once('.').unwrap_or((strip, "0")), true) } else { (s.split_once('.').unwrap_or((s, "0")), false) }; @@ -277,6 +291,12 @@ impl From for Dec { } } +impl From for Dec { + fn from(num: i32) -> Self { + Self::from(num as i128) + } +} + impl TryFrom for Dec { type Error = Box; @@ -615,8 +635,6 @@ mod test_dec { assert!(Dec::from_str("0.").is_err()); // Test that multiple decimal points get caught assert!(Dec::from_str("1.2.3").is_err()); - // Test that negative numbers are rejected - assert!(Dec::from_str("-1").is_err()); // Test that non-numerics are caught assert!(Dec::from_str("DEADBEEF.12").is_err()); assert!(Dec::from_str("23.DEADBEEF").is_err()); @@ -644,6 +662,7 @@ mod test_dec { ); } + /// Test that ordering of [`Dec`] values using more than 64 bits works. #[test] fn test_ordering() { let smaller = Dec::from_str("6483947304.195066085701").unwrap(); @@ -651,6 +670,21 @@ mod test_dec { assert!(smaller < larger); } + /// Test that taking the ceiling of a [`Dec`] works. + #[test] + fn test_ceiling() { + let neg = Dec::from_str("-2.4").expect("Test failed"); + assert_eq!( + neg.ceil(), + Dec::from_str("-2").expect("Test failed").to_i256() + ); + let pos = Dec::from_str("2.4").expect("Test failed"); + assert_eq!( + pos.ceil(), + Dec::from_str("3").expect("Test failed").to_i256() + ); + } + #[test] fn test_dec_display() { let num = Dec::from_str("14000.0000").unwrap(); diff --git a/core/src/types/key/common.rs b/core/src/types/key/common.rs index 9ca0bdaffc..33f300d884 100644 --- a/core/src/types/key/common.rs +++ b/core/src/types/key/common.rs @@ -17,8 +17,10 @@ use super::{ ParseSignatureError, RefTo, SchemeType, SigScheme as SigSchemeTrait, VerifySigError, }; +use crate::impl_display_and_from_str_via_format; use crate::types::ethereum_events::EthAddress; use crate::types::key::{SignableBytes, StorageHasher}; +use crate::types::string_encoding; /// Public key #[derive( @@ -71,24 +73,26 @@ impl super::PublicKey for PublicKey { } } -impl Display for PublicKey { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", HEXLOWER.encode(&self.serialize_to_vec())) - } -} +/// String decoding error +pub type DecodeError = string_encoding::DecodeError; -impl FromStr for PublicKey { - type Err = ParsePublicKeyError; +impl string_encoding::Format for PublicKey { + const HRP: &'static str = string_encoding::COMMON_PK_HRP; - fn from_str(str: &str) -> Result { - let vec = HEXLOWER - .decode(str.as_ref()) - .map_err(ParsePublicKeyError::InvalidHex)?; - Self::try_from_slice(vec.as_slice()) - .map_err(ParsePublicKeyError::InvalidEncoding) + fn to_bytes(&self) -> Vec { + self.serialize_to_vec() + } + + fn decode_bytes( + bytes: &[u8], + ) -> Result { + BorshDeserialize::try_from_slice(bytes) + .map_err(DecodeError::InvalidBytes) } } +impl_display_and_from_str_via_format!(PublicKey); + #[allow(missing_docs)] #[derive(Error, Debug)] pub enum EthAddressConvError { @@ -246,6 +250,23 @@ pub enum Signature { Secp256k1(secp256k1::Signature), } +impl string_encoding::Format for Signature { + const HRP: &'static str = string_encoding::COMMON_SIG_HRP; + + fn to_bytes(&self) -> Vec { + self.serialize_to_vec() + } + + fn decode_bytes( + bytes: &[u8], + ) -> Result { + BorshDeserialize::try_from_slice(bytes) + .map_err(DecodeError::InvalidBytes) + } +} + +impl_display_and_from_str_via_format!(Signature); + impl From for Signature { fn from(sig: ed25519::Signature) -> Self { Signature::Ed25519(sig) @@ -370,3 +391,22 @@ impl super::SigScheme for SigScheme { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::key::ed25519; + + /// Run `cargo test gen_ed25519_keypair -- --nocapture` to generate a + /// new ed25519 keypair wrapped in `common` key types. + #[test] + fn gen_ed25519_keypair() { + let secret_key = + SecretKey::Ed25519(crate::types::key::testing::gen_keypair::< + ed25519::SigScheme, + >()); + let public_key = secret_key.to_public(); + println!("Public key: {}", public_key); + println!("Secret key: {}", secret_key); + } +} diff --git a/core/src/types/key/dkg_session_keys.rs b/core/src/types/key/dkg_session_keys.rs index ccca82aeba..07177f5ec6 100644 --- a/core/src/types/key/dkg_session_keys.rs +++ b/core/src/types/key/dkg_session_keys.rs @@ -2,19 +2,17 @@ use std::cmp::Ordering; use std::collections::BTreeMap; -use std::fmt::Display; use std::io::{Error, ErrorKind, Read}; -use std::str::FromStr; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use borsh_ext::BorshSerializeExt; -use data_encoding::HEXLOWER; use serde::{Deserialize, Serialize}; +use crate::impl_display_and_from_str_via_format; use crate::types::address::Address; -use crate::types::key::ParsePublicKeyError; use crate::types::storage::{DbKeySeg, Key, KeySeg}; +use crate::types::string_encoding; use crate::types::transaction::EllipticCurve; /// A keypair used in the DKG protocol @@ -140,25 +138,23 @@ impl BorshSchema for DkgPublicKey { } } -impl Display for DkgPublicKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let vec = self.serialize_to_vec(); - write!(f, "{}", HEXLOWER.encode(&vec)) - } -} +impl string_encoding::Format for DkgPublicKey { + const HRP: &'static str = string_encoding::DKG_PK_HRP; -impl FromStr for DkgPublicKey { - type Err = ParsePublicKeyError; + fn to_bytes(&self) -> Vec { + self.serialize_to_vec() + } - fn from_str(s: &str) -> Result { - let vec = HEXLOWER - .decode(s.as_ref()) - .map_err(ParsePublicKeyError::InvalidHex)?; - BorshDeserialize::try_from_slice(&vec) - .map_err(ParsePublicKeyError::InvalidEncoding) + fn decode_bytes( + bytes: &[u8], + ) -> Result { + BorshDeserialize::try_from_slice(bytes) + .map_err(string_encoding::DecodeError::InvalidBytes) } } +impl_display_and_from_str_via_format!(DkgPublicKey); + /// Obtain a storage key for user's public dkg session key. pub fn dkg_pk_key(owner: &Address) -> Key { Key::from(owner.to_db_key()) diff --git a/core/src/types/key/mod.rs b/core/src/types/key/mod.rs index f6d226e2f4..52af46d5dc 100644 --- a/core/src/types/key/mod.rs +++ b/core/src/types/key/mod.rs @@ -238,7 +238,6 @@ pub trait PublicKey: + Display + Debug + PartialOrd - + FromStr + Hash + Send + Sync diff --git a/core/src/types/masp.rs b/core/src/types/masp.rs index 9083852a81..a7cc6be9f7 100644 --- a/core/src/types/masp.rs +++ b/core/src/types/masp.rs @@ -1,7 +1,7 @@ //! MASP types use std::fmt::Display; -use std::io::ErrorKind; +use std::io::{Error, ErrorKind}; use std::str::FromStr; use bech32::{FromBase32, ToBase32}; @@ -9,17 +9,14 @@ use borsh::{BorshDeserialize, BorshSerialize}; use borsh_ext::BorshSerializeExt; use sha2::{Digest, Sha256}; -use crate::types::address::{ - masp, Address, DecodeError, BECH32M_VARIANT, HASH_HEX_LEN, +use crate::impl_display_and_from_str_via_format; +use crate::types::address::{masp, Address, DecodeError, HASH_HEX_LEN}; +use crate::types::string_encoding::{ + self, BECH32M_VARIANT, MASP_EXT_FULL_VIEWING_KEY_HRP, + MASP_EXT_SPENDING_KEY_HRP, MASP_PAYMENT_ADDRESS_HRP, + MASP_PINNED_PAYMENT_ADDRESS_HRP, }; -/// human-readable part of Bech32m encoded address -// TODO remove "test" suffix for live network -const EXT_FULL_VIEWING_KEY_HRP: &str = "xfvktest"; -const PAYMENT_ADDRESS_HRP: &str = "patest"; -const PINNED_PAYMENT_ADDRESS_HRP: &str = "ppatest"; -const EXT_SPENDING_KEY_HRP: &str = "xsktest"; - /// Wrapper for masp_primitive's FullViewingKey #[derive( Clone, @@ -35,51 +32,103 @@ const EXT_SPENDING_KEY_HRP: &str = "xsktest"; )] pub struct ExtendedViewingKey(masp_primitives::zip32::ExtendedFullViewingKey); -impl Display for ExtendedViewingKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl ExtendedViewingKey { + /// Encode `Self` to bytes + pub fn to_bytes(&self) -> Vec { let mut bytes = [0; 169]; self.0 .write(&mut bytes[..]) .expect("should be able to serialize an ExtendedFullViewingKey"); - let encoded = bech32::encode( - EXT_FULL_VIEWING_KEY_HRP, - bytes.to_base32(), - BECH32M_VARIANT, + bytes.to_vec() + } + + /// Try to decode `Self` from bytes + pub fn decode_bytes(bytes: &[u8]) -> Result { + masp_primitives::zip32::ExtendedFullViewingKey::read(&mut &bytes[..]) + .map(Self) + } +} + +impl string_encoding::Format for ExtendedViewingKey { + const HRP: &'static str = MASP_EXT_FULL_VIEWING_KEY_HRP; + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn decode_bytes( + bytes: &[u8], + ) -> Result { + Self::decode_bytes(bytes).map_err(DecodeError::InvalidBytes) + } +} + +impl_display_and_from_str_via_format!(ExtendedViewingKey); + +impl string_encoding::Format for PaymentAddress { + const HRP: &'static str = MASP_PAYMENT_ADDRESS_HRP; + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn decode_bytes( + _bytes: &[u8], + ) -> Result { + unimplemented!( + "Cannot determine if the PaymentAddress is pinned from bytes. Use \ + `PaymentAddress::decode_bytes(bytes, is_pinned)` instead." ) - .unwrap_or_else(|_| { + } + + // We override `encode` because we need to determine whether the address + // is pinned from its HRP + fn encode(&self) -> String { + let hrp = if self.is_pinned() { + MASP_PINNED_PAYMENT_ADDRESS_HRP + } else { + MASP_PAYMENT_ADDRESS_HRP + }; + let base32 = self.to_bytes().to_base32(); + bech32::encode(hrp, base32, BECH32M_VARIANT).unwrap_or_else(|_| { panic!( "The human-readable part {} should never cause a failure", - EXT_FULL_VIEWING_KEY_HRP + hrp ) - }); - write!(f, "{encoded}") + }) } -} - -impl FromStr for ExtendedViewingKey { - type Err = DecodeError; - fn from_str(string: &str) -> Result { - let (prefix, base32, variant) = - bech32::decode(string).map_err(DecodeError::DecodeBech32)?; - if prefix != EXT_FULL_VIEWING_KEY_HRP { - return Err(DecodeError::UnexpectedBech32Prefix( + // We override `decode` because we need to use different HRP for pinned and + // non-pinned address + fn decode( + string: impl AsRef, + ) -> Result { + let (prefix, base32, variant) = bech32::decode(string.as_ref()) + .map_err(DecodeError::DecodeBech32)?; + let is_pinned = if prefix == MASP_PAYMENT_ADDRESS_HRP { + false + } else if prefix == MASP_PINNED_PAYMENT_ADDRESS_HRP { + true + } else { + return Err(DecodeError::UnexpectedBech32Hrp( prefix, - EXT_FULL_VIEWING_KEY_HRP.into(), + MASP_PAYMENT_ADDRESS_HRP.into(), )); - } + }; match variant { BECH32M_VARIANT => {} _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), } let bytes: Vec = FromBase32::from_base32(&base32) .map_err(DecodeError::DecodeBase32)?; - masp_primitives::zip32::ExtendedFullViewingKey::read(&mut &bytes[..]) - .map_err(|op| DecodeError::InvalidInnerEncodingStr(op.to_string())) - .map(Self) + + PaymentAddress::decode_bytes(&bytes, is_pinned) + .map_err(DecodeError::InvalidBytes) } } +impl_display_and_from_str_via_format!(PaymentAddress); + impl From for masp_primitives::zip32::ExtendedFullViewingKey { @@ -154,78 +203,45 @@ impl PaymentAddress { // hex of the first 40 chars of the hash format!("{:.width$X}", hasher.finalize(), width = HASH_HEX_LEN) } -} -impl From for masp_primitives::sapling::PaymentAddress { - fn from(addr: PaymentAddress) -> Self { - addr.0 - } -} - -impl From for PaymentAddress { - fn from(addr: masp_primitives::sapling::PaymentAddress) -> Self { - Self(addr, false) - } -} - -impl Display for PaymentAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let bytes = self.0.to_bytes(); - let hrp = if self.1 { - PINNED_PAYMENT_ADDRESS_HRP - } else { - PAYMENT_ADDRESS_HRP - }; - let encoded = bech32::encode(hrp, bytes.to_base32(), BECH32M_VARIANT) - .unwrap_or_else(|_| { - panic!( - "The human-readable part {} should never cause a failure", - PAYMENT_ADDRESS_HRP - ) - }); - write!(f, "{encoded}") + /// Encode `Self` to bytes + pub fn to_bytes(&self) -> Vec { + self.0.to_bytes().to_vec() } -} -impl FromStr for PaymentAddress { - type Err = DecodeError; - - fn from_str(string: &str) -> Result { - let (prefix, base32, variant) = - bech32::decode(string).map_err(DecodeError::DecodeBech32)?; - let pinned = if prefix == PAYMENT_ADDRESS_HRP { - false - } else if prefix == PINNED_PAYMENT_ADDRESS_HRP { - true - } else { - return Err(DecodeError::UnexpectedBech32Prefix( - prefix, - PAYMENT_ADDRESS_HRP.into(), - )); - }; - match variant { - BECH32M_VARIANT => {} - _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), - } + /// Try to decode `Self` from bytes + pub fn decode_bytes( + bytes: &[u8], + is_pinned: bool, + ) -> Result { let addr_len_err = |_| { - DecodeError::InvalidInnerEncoding( + Error::new( ErrorKind::InvalidData, - "expected 43 bytes for the payment address".to_string(), + "expected 43 bytes for the payment address", ) }; let addr_data_err = || { - DecodeError::InvalidInnerEncoding( + Error::new( ErrorKind::InvalidData, - "invalid payment address provided".to_string(), + "invalid payment address provided", ) }; - let bytes: Vec = FromBase32::from_base32(&base32) - .map_err(DecodeError::DecodeBase32)?; - masp_primitives::sapling::PaymentAddress::from_bytes( - &bytes.try_into().map_err(addr_len_err)?, - ) - .ok_or_else(addr_data_err) - .map(|x| Self(x, pinned)) + let bytes: &[u8; 43] = &bytes.try_into().map_err(addr_len_err)?; + masp_primitives::sapling::PaymentAddress::from_bytes(bytes) + .ok_or_else(addr_data_err) + .map(|addr| Self(addr, is_pinned)) + } +} + +impl From for masp_primitives::sapling::PaymentAddress { + fn from(addr: PaymentAddress) -> Self { + addr.0 + } +} + +impl From for PaymentAddress { + fn from(addr: masp_primitives::sapling::PaymentAddress) -> Self { + Self(addr, false) } } @@ -257,51 +273,28 @@ impl<'de> serde::Deserialize<'de> for PaymentAddress { #[derive(Clone, Debug, Copy, BorshSerialize, BorshDeserialize)] pub struct ExtendedSpendingKey(masp_primitives::zip32::ExtendedSpendingKey); -impl Display for ExtendedSpendingKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl string_encoding::Format for ExtendedSpendingKey { + const HRP: &'static str = MASP_EXT_SPENDING_KEY_HRP; + + fn to_bytes(&self) -> Vec { let mut bytes = [0; 169]; self.0 .write(&mut &mut bytes[..]) .expect("should be able to serialize an ExtendedSpendingKey"); - let encoded = bech32::encode( - EXT_SPENDING_KEY_HRP, - bytes.to_base32(), - BECH32M_VARIANT, - ) - .unwrap_or_else(|_| { - panic!( - "The human-readable part {} should never cause a failure", - EXT_SPENDING_KEY_HRP - ) - }); - write!(f, "{encoded}") + bytes.to_vec() } -} -impl FromStr for ExtendedSpendingKey { - type Err = DecodeError; - - fn from_str(string: &str) -> Result { - let (prefix, base32, variant) = - bech32::decode(string).map_err(DecodeError::DecodeBech32)?; - if prefix != EXT_SPENDING_KEY_HRP { - return Err(DecodeError::UnexpectedBech32Prefix( - prefix, - EXT_SPENDING_KEY_HRP.into(), - )); - } - match variant { - BECH32M_VARIANT => {} - _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), - } - let bytes: Vec = FromBase32::from_base32(&base32) - .map_err(DecodeError::DecodeBase32)?; + fn decode_bytes( + bytes: &[u8], + ) -> Result { masp_primitives::zip32::ExtendedSpendingKey::read(&mut &bytes[..]) .map_err(|op| DecodeError::InvalidInnerEncodingStr(op.to_string())) .map(Self) } } +impl_display_and_from_str_via_format!(ExtendedSpendingKey); + impl From for masp_primitives::zip32::ExtendedSpendingKey { fn from(key: ExtendedSpendingKey) -> Self { key.0 @@ -407,7 +400,7 @@ impl TransferTarget { /// Get the contained PaymentAddress, if any pub fn payment_address(&self) -> Option { match self { - Self::PaymentAddress(x) => Some(*x), + Self::PaymentAddress(address) => Some(*address), _ => None, } } @@ -425,7 +418,7 @@ impl Display for TransferTarget { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Address(x) => x.fmt(f), - Self::PaymentAddress(x) => x.fmt(f), + Self::PaymentAddress(address) => address.fmt(f), } } } diff --git a/core/src/types/mod.rs b/core/src/types/mod.rs index 8aee038d9b..904e005f34 100644 --- a/core/src/types/mod.rs +++ b/core/src/types/mod.rs @@ -15,6 +15,7 @@ pub mod keccak; pub mod key; pub mod masp; pub mod storage; +pub mod string_encoding; pub mod time; pub mod token; pub mod transaction; diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index a21de88cbe..5dba69ed36 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -30,7 +30,7 @@ use crate::types::time::DateTimeUtc; pub const IBC_KEY_LIMIT: usize = 240; #[allow(missing_docs)] -#[derive(Error, Debug, Clone)] +#[derive(Error, Debug)] pub enum Error { #[error("Error parsing address: {0}")] ParseAddress(address::DecodeError), diff --git a/core/src/types/string_encoding.rs b/core/src/types/string_encoding.rs new file mode 100644 index 0000000000..9cebaa8451 --- /dev/null +++ b/core/src/types/string_encoding.rs @@ -0,0 +1,231 @@ +//! Namada's standard string encoding for public types. +//! +//! We're using [bech32m](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki), +//! a format with a human-readable, followed by base32 encoding with a limited +//! character set with checksum check. +//! +//! To use this encoding for a new type, add a HRP (human-readable part) const +//! below and use it to `impl string_encoding::Format for YourType`. + +use std::fmt::Display; +use std::io::ErrorKind; +use std::ops::Deref; +use std::str::FromStr; + +use bech32::{self, FromBase32, ToBase32, Variant}; +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// We're using "Bech32m" variant +pub const BECH32M_VARIANT: bech32::Variant = Variant::Bech32m; + +// Human-readable parts of Bech32m encoding +// +// Invariant: HRPs must be unique !!! +// +// TODO: remove "test" suffix for live network +/// `Address` human-readable part +pub const ADDRESS_HRP: &str = "atest"; +/// MASP extended viewing key human-readable part +pub const MASP_EXT_FULL_VIEWING_KEY_HRP: &str = "xfvktest"; +/// MASP payment address (not pinned) human-readable part +pub const MASP_PAYMENT_ADDRESS_HRP: &str = "patest"; +/// MASP pinned payment address human-readable part +pub const MASP_PINNED_PAYMENT_ADDRESS_HRP: &str = "ppatest"; +/// MASP extended spending key human-readable part +pub const MASP_EXT_SPENDING_KEY_HRP: &str = "xsktest"; +/// `common::PublicKey` human-readable part +pub const COMMON_PK_HRP: &str = "pktest"; +/// `DkgPublicKey` human-readable part +pub const DKG_PK_HRP: &str = "dpktest"; +/// `common::Signature` human-readable part +pub const COMMON_SIG_HRP: &str = "sigtest"; + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum DecodeError { + #[error("Error decoding from Bech32m: {0}")] + DecodeBech32(bech32::Error), + #[error("Error decoding from base32: {0}")] + DecodeBase32(bech32::Error), + #[error("Unexpected Bech32m human-readable part {0}, expected {1}")] + UnexpectedBech32Hrp(String, String), + #[error("Unexpected Bech32m variant {0:?}, expected {BECH32M_VARIANT:?}")] + UnexpectedBech32Variant(bech32::Variant), + #[error("Invalid address encoding: {0}, {1}")] + InvalidInnerEncoding(ErrorKind, String), + #[error("Invalid address encoding")] + InvalidInnerEncodingStr(String), + #[error("Invalid bytes: {0}")] + InvalidBytes(std::io::Error), +} + +/// Format to string with bech32m +pub trait Format: Sized { + /// Human-readable part + const HRP: &'static str; + + /// Encode `Self` to a string + fn encode(&self) -> String { + let base32 = self.to_bytes().to_base32(); + bech32::encode(Self::HRP, base32, BECH32M_VARIANT).unwrap_or_else( + |_| { + panic!( + "The human-readable part {} should never cause a failure", + Self::HRP + ) + }, + ) + } + + /// Try to decode `Self` from a string + fn decode(string: impl AsRef) -> Result { + let (hrp, hash_base32, variant) = bech32::decode(string.as_ref()) + .map_err(DecodeError::DecodeBech32)?; + if hrp != Self::HRP { + return Err(DecodeError::UnexpectedBech32Hrp( + hrp, + Self::HRP.into(), + )); + } + match variant { + BECH32M_VARIANT => {} + _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), + } + let bytes: Vec = FromBase32::from_base32(&hash_base32) + .map_err(DecodeError::DecodeBase32)?; + + Self::decode_bytes(&bytes) + } + + /// Encode `Self` to bytes + fn to_bytes(&self) -> Vec; + + /// Try to decode `Self` from bytes + fn decode_bytes(bytes: &[u8]) -> Result; +} + +/// Implement [`std::fmt::Display`] and [`std::str::FromStr`] via +/// [`Format`]. +#[macro_export] +macro_rules! impl_display_and_from_str_via_format { + ($t:path) => { + impl std::fmt::Display for $t { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + $crate::types::string_encoding::Format::encode(self) + ) + } + } + + impl std::str::FromStr for $t { + type Err = $crate::types::string_encoding::DecodeError; + + fn from_str(s: &str) -> std::result::Result { + $crate::types::string_encoding::Format::decode(s) + } + } + }; +} + +/// Get the length of the human-readable part +// Not in the `Format` trait, cause functions in traits cannot be const +pub const fn hrp_len() -> usize { + T::HRP.len() +} + +/// Wrapper for `T` to serde encode via `Display` and decode via `FromStr` +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BorshSerialize, + BorshDeserialize, +)] +#[serde(transparent)] +pub struct StringEncoded +where + T: FromStr + Display, + ::Err: Display, +{ + /// Raw value + #[serde( + serialize_with = "encode_via_display", + deserialize_with = "decode_via_from_str" + )] + pub raw: T, +} + +impl StringEncoded +where + T: FromStr + Display, + ::Err: Display, +{ + /// Wrap to make `T` string encoded + pub fn new(raw: T) -> Self { + Self { raw } + } +} + +impl Deref for StringEncoded +where + T: FromStr + Display, + ::Err: Display, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.raw + } +} + +impl Display for StringEncoded +where + T: FromStr + Display, + ::Err: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.raw.fmt(f) + } +} + +impl FromStr for StringEncoded +where + T: FromStr + Display, + ::Err: Display, +{ + type Err = ::Err; + + fn from_str(s: &str) -> Result { + let raw = T::from_str(s)?; + Ok(Self { raw }) + } +} + +fn encode_via_display(val: &T, serializer: S) -> Result +where + S: serde::Serializer, + T: Display, +{ + let val_str = val.to_string(); + serde::Serialize::serialize(&val_str, serializer) +} + +fn decode_via_from_str<'de, D, T>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, + T: FromStr, + ::Err: Display, +{ + let val_str: String = serde::Deserialize::deserialize(deserializer)?; + FromStr::from_str(&val_str).map_err(serde::de::Error::custom) +} diff --git a/core/src/types/time.rs b/core/src/types/time.rs index 341a81411c..54622e15a6 100644 --- a/core/src/types/time.rs +++ b/core/src/types/time.rs @@ -10,6 +10,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use chrono::ParseError; pub use chrono::{DateTime, Duration, TimeZone, Utc}; +use serde::{Serialize, Deserialize}; /// Check if the given `duration` has passed since the given `start. pub fn duration_passed( @@ -30,6 +31,8 @@ pub fn duration_passed( PartialOrd, Ord, Hash, + Serialize, + Deserialize, BorshSerialize, BorshDeserialize, BorshSchema, @@ -73,6 +76,8 @@ impl Display for DurationSecs { PartialOrd, Ord, Hash, + Serialize, + Deserialize, BorshSerialize, BorshDeserialize, BorshSchema, @@ -93,8 +98,26 @@ impl From for DurationNanos { } } +impl From for std::time::Duration { + fn from(DurationNanos { secs, nanos }: DurationNanos) -> Self { + Self::new(secs, nanos) + } +} + /// An RFC 3339 timestamp (e.g., "1970-01-01T00:00:00Z"). -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, +)] pub struct Rfc3339String(pub String); /// A duration in seconds precision. @@ -114,6 +137,9 @@ pub struct Rfc3339String(pub String); #[serde(try_from = "Rfc3339String", into = "Rfc3339String")] pub struct DateTimeUtc(pub DateTime); +/// The minimum possible DateTime. +pub const MIN_UTC: DateTimeUtc = DateTimeUtc(chrono::DateTime::::MIN_UTC); + impl Display for DateTimeUtc { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.to_rfc3339()) @@ -292,3 +318,17 @@ impl TryFrom for DateTimeUtc { Rfc3339String(t.to_rfc3339()).try_into() } } + +#[cfg(any(feature = "tendermint", feature = "tendermint-abcipp"))] +impl From for DurationNanos { + fn from(val: crate::tendermint::Timeout) -> Self { + Self::from(std::time::Duration::from(val)) + } +} + +#[cfg(any(feature = "tendermint", feature = "tendermint-abcipp"))] +impl From for crate::tendermint::Timeout { + fn from(val: DurationNanos) -> Self { + Self::from(std::time::Duration::from(val)) + } +} diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 9e7c2d0ee7..73a78b5bd9 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1,5 +1,6 @@ //! A basic fungible token +use std::cmp::Ordering; use std::fmt::Display; use std::iter::Sum; use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}; @@ -313,8 +314,6 @@ impl From for u8 { Hash, PartialEq, Eq, - PartialOrd, - Ord, BorshSerialize, BorshDeserialize, BorshSchema, @@ -348,6 +347,10 @@ impl DenominatedAmount { pub fn to_string_precise(&self) -> String { let decimals = self.denom.0 as usize; let mut string = self.amount.raw.to_string(); + // escape hatch if there are no decimal places + if decimals == 0 { + return string; + } if string.len() > decimals { string.insert(string.len() - decimals, '.'); } else { @@ -403,7 +406,11 @@ impl DenominatedAmount { impl Display for DenominatedAmount { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let string = self.to_string_precise(); - let string = string.trim_end_matches(&['0']); + let string = if self.denom.0 > 0 { + string.trim_end_matches(&['0']) + } else { + &string + }; let string = string.trim_end_matches(&['.']); f.write_str(string) } @@ -453,6 +460,50 @@ impl FromStr for DenominatedAmount { } } +impl PartialOrd for DenominatedAmount { + fn partial_cmp(&self, other: &Self) -> Option { + if self.denom < other.denom { + let diff = other.denom.0 - self.denom.0; + let (div, rem) = + other.amount.raw.div_mod(Uint::exp10(diff as usize)); + let div_ceil = if rem.is_zero() { + div + } else { + div + Uint::one() + }; + let ord = self.amount.raw.partial_cmp(&div_ceil); + if let Some(Ordering::Equal) = ord { + if rem.is_zero() { + Some(Ordering::Equal) + } else { + Some(Ordering::Greater) + } + } else { + ord + } + } else { + let diff = self.denom.0 - other.denom.0; + let (div, rem) = + self.amount.raw.div_mod(Uint::exp10(diff as usize)); + let div_ceil = if rem.is_zero() { + div + } else { + div + Uint::one() + }; + let ord = div_ceil.partial_cmp(&other.amount.raw); + if let Some(Ordering::Equal) = ord { + if rem.is_zero() { + Some(Ordering::Equal) + } else { + Some(Ordering::Less) + } + } else { + ord + } + } + } +} + impl serde::Serialize for Amount { fn serialize( &self, @@ -1216,6 +1267,13 @@ mod tests { }; assert_eq!("0.0112", amount.to_string()); assert_eq!("0.01120", amount.to_string_precise()); + + let amount = DenominatedAmount { + amount: Amount::from_uint(200, 0).expect("Test failed"), + denom: 0.into(), + }; + assert_eq!("200", amount.to_string()); + assert_eq!("200", amount.to_string_precise()); } #[test] @@ -1342,6 +1400,62 @@ mod tests { assert_eq!(two.mul_ceil(dec), one); assert_eq!(three.mul_ceil(dec), two); } + + #[test] + fn test_denominated_amt_ord() { + let denom_1 = DenominatedAmount { + amount: Amount::from_uint(15, 0).expect("Test failed"), + denom: 1.into(), + }; + let denom_2 = DenominatedAmount { + amount: Amount::from_uint(1500, 0).expect("Test failed"), + denom: 3.into(), + }; + // The psychedelic case. Partial ordering works on the underlying + // amounts but `Eq` also checks the equality of denoms. + assert_eq!( + denom_1.partial_cmp(&denom_2).expect("Test failed"), + Ordering::Equal + ); + assert_eq!( + denom_2.partial_cmp(&denom_1).expect("Test failed"), + Ordering::Equal + ); + assert_ne!(denom_1, denom_2); + + let denom_1 = DenominatedAmount { + amount: Amount::from_uint(15, 0).expect("Test failed"), + denom: 1.into(), + }; + let denom_2 = DenominatedAmount { + amount: Amount::from_uint(1501, 0).expect("Test failed"), + denom: 3.into(), + }; + assert_eq!( + denom_1.partial_cmp(&denom_2).expect("Test failed"), + Ordering::Less + ); + assert_eq!( + denom_2.partial_cmp(&denom_1).expect("Test failed"), + Ordering::Greater + ); + let denom_1 = DenominatedAmount { + amount: Amount::from_uint(15, 0).expect("Test failed"), + denom: 1.into(), + }; + let denom_2 = DenominatedAmount { + amount: Amount::from_uint(1499, 0).expect("Test failed"), + denom: 3.into(), + }; + assert_eq!( + denom_1.partial_cmp(&denom_2).expect("Test failed"), + Ordering::Greater + ); + assert_eq!( + denom_2.partial_cmp(&denom_1).expect("Test failed"), + Ordering::Less + ); + } } /// Helpers for testing with addresses. @@ -1369,49 +1483,4 @@ pub mod testing { (1..=max).prop_map(|val| Amount::from_uint(val, 0).unwrap()) } - /// init_token_storage is useful when the initialization of the network is - /// not properly made. This properly sets up the storage such that - /// inflation calculations can be ran on the token addresses. We assume - /// a total supply that may not be real - pub fn init_token_storage( - wl_storage: &mut ledger_storage::WlStorage, - epochs_per_year: u64, - ) where - D: 'static - + ledger_storage::DB - + for<'iter> ledger_storage::DBIter<'iter>, - H: 'static + ledger_storage::StorageHasher, - { - use crate::ledger::parameters::storage::get_epochs_per_year_key; - use crate::types::address::tokens; - - let tokens = tokens(); - let masp_reward_keys: Vec<_> = tokens.keys().collect(); - - wl_storage - .write(&get_epochs_per_year_key(), epochs_per_year) - .unwrap(); - let params = Parameters { - max_reward_rate: Dec::from_str("0.1").unwrap(), - kd_gain_nom: Dec::from_str("0.1").unwrap(), - kp_gain_nom: Dec::from_str("0.1").unwrap(), - locked_ratio_target: Dec::zero(), - }; - - for address in masp_reward_keys { - params.init_storage(address, wl_storage); - wl_storage - .write( - &minted_balance_key(address), - Amount::native_whole(5), // arbitrary amount - ) - .unwrap(); - wl_storage - .write(&masp_last_inflation_key(address), Amount::zero()) - .expect("inflation ought to be written"); - wl_storage - .write(&masp_last_locked_ratio_key(address), Dec::zero()) - .expect("last locked set default"); - } - } } diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index e6aeedc252..463fa34383 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -538,6 +538,11 @@ impl I256 { Self(Uint::zero()) } + /// Gives the one value of an I256 + pub fn one() -> I256 { + Self(Uint::one()) + } + /// Get a string representation of `self` as a /// native token amount. pub fn to_string_native(&self) -> String { diff --git a/ethereum_bridge/src/parameters.rs b/ethereum_bridge/src/parameters.rs index 599ef0c5f8..62d1f9a7a1 100644 --- a/ethereum_bridge/src/parameters.rs +++ b/ethereum_bridge/src/parameters.rs @@ -150,7 +150,7 @@ pub struct Contracts { BorshSerialize, BorshDeserialize, )] -pub struct EthereumBridgeConfig { +pub struct EthereumBridgeParams { /// Initial Ethereum block height when events will first be extracted from. pub eth_start_height: ethereum_structs::BlockHeight, /// Minimum number of confirmations needed to trust an Ethereum branch. @@ -163,7 +163,7 @@ pub struct EthereumBridgeConfig { pub contracts: Contracts, } -impl EthereumBridgeConfig { +impl EthereumBridgeParams { /// Initialize the Ethereum bridge parameters in storage. /// /// If these parameters are initialized, the storage subspaces @@ -248,7 +248,7 @@ impl EthereumBridgeConfig { } } -/// Subset of [`EthereumBridgeConfig`], containing only Ethereum +/// Subset of [`EthereumBridgeParams`], containing only Ethereum /// oracle specific parameters. #[derive(Clone, Debug, Eq, PartialEq)] pub struct EthereumOracleConfig { @@ -262,9 +262,9 @@ pub struct EthereumOracleConfig { pub contracts: Contracts, } -impl From for EthereumOracleConfig { - fn from(config: EthereumBridgeConfig) -> Self { - let EthereumBridgeConfig { +impl From for EthereumOracleConfig { + fn from(config: EthereumBridgeParams) -> Self { + let EthereumBridgeParams { eth_start_height, min_confirmations, contracts, @@ -374,7 +374,7 @@ mod tests { use super::*; use crate::parameters::{ - ContractVersion, Contracts, EthereumBridgeConfig, MinimumConfirmations, + ContractVersion, Contracts, EthereumBridgeParams, MinimumConfirmations, UpgradeableContract, }; @@ -383,7 +383,7 @@ mod tests { /// in any of the config structs. #[test] fn test_round_trip_toml_serde() -> Result<()> { - let config = EthereumBridgeConfig { + let config = EthereumBridgeParams { erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::default(), @@ -396,7 +396,7 @@ mod tests { }, }; let serialized = toml::to_string(&config)?; - let deserialized: EthereumBridgeConfig = toml::from_str(&serialized)?; + let deserialized: EthereumBridgeParams = toml::from_str(&serialized)?; assert_eq!(config, deserialized); Ok(()) @@ -405,7 +405,7 @@ mod tests { #[test] fn test_ethereum_bridge_config_read_write_storage() { let mut wl_storage = TestWlStorage::default(); - let config = EthereumBridgeConfig { + let config = EthereumBridgeParams { erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::default(), @@ -437,7 +437,7 @@ mod tests { #[should_panic(expected = "Could not read")] fn test_ethereum_bridge_config_storage_corrupt() { let mut wl_storage = TestWlStorage::default(); - let config = EthereumBridgeConfig { + let config = EthereumBridgeParams { erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::default(), diff --git a/ethereum_bridge/src/test_utils.rs b/ethereum_bridge/src/test_utils.rs index cc5370360d..c4d4967389 100644 --- a/ethereum_bridge/src/test_utils.rs +++ b/ethereum_bridge/src/test_utils.rs @@ -6,7 +6,6 @@ use std::num::NonZeroU64; use borsh_ext::BorshSerializeExt; use namada_core::ledger::eth_bridge::storage::bridge_pool::get_key_from_hash; use namada_core::ledger::eth_bridge::storage::whitelist; -use namada_core::ledger::governance::parameters::GovernanceParameters; use namada_core::ledger::storage::mockdb::MockDBWriteBatch; use namada_core::ledger::storage::testing::{TestStorage, TestWlStorage}; use namada_core::ledger::storage_api::token::credit_tokens; @@ -27,7 +26,7 @@ use namada_proof_of_stake::{ }; use crate::parameters::{ - ContractVersion, Contracts, EthereumBridgeConfig, MinimumConfirmations, + ContractVersion, Contracts, EthereumBridgeParams, MinimumConfirmations, UpgradeableContract, }; @@ -99,12 +98,12 @@ pub fn default_validator() -> (Address, token::Amount) { (addr, voting_power) } -/// Writes a dummy [`EthereumBridgeConfig`] to the given [`TestWlStorage`], and +/// Writes a dummy [`EthereumBridgeParams`] to the given [`TestWlStorage`], and /// returns it. pub fn bootstrap_ethereum_bridge( wl_storage: &mut TestWlStorage, -) -> EthereumBridgeConfig { - let config = EthereumBridgeConfig { +) -> EthereumBridgeParams { + let config = EthereumBridgeParams { // start with empty erc20 whitelist erc20_whitelist: vec![], eth_start_height: Default::default(), @@ -217,11 +216,9 @@ pub fn init_storage_with_validators( }) .collect(); - let gov_params = GovernanceParameters::default(); - gov_params.init_storage(wl_storage).unwrap(); - namada_proof_of_stake::init_genesis( + namada_proof_of_stake::test_utils::test_init_genesis( wl_storage, - &OwnedPosParams::default(), + OwnedPosParams::default(), validators.into_iter(), 0.into(), ) @@ -291,11 +288,12 @@ pub fn append_validators_to_storage( current_epoch, commission_rate: Dec::new(5, 2).unwrap(), max_commission_rate_change: Dec::new(1, 2).unwrap(), + offset_opt: Some(1), }) .expect("Test failed"); credit_tokens(wl_storage, &staking_token, &validator, stake) .expect("Test failed"); - bond_tokens(wl_storage, None, &validator, stake, current_epoch) + bond_tokens(wl_storage, None, &validator, stake, current_epoch, None) .expect("Test failed"); all_keys.insert(validator, keys); diff --git a/genesis/README.md b/genesis/README.md new file mode 100644 index 0000000000..83495cd996 --- /dev/null +++ b/genesis/README.md @@ -0,0 +1,153 @@ +# Genesis templates + +An example setup with a single validator used to run a localnet can be found in [localnet](localnet/README.md) directory. + +[Starter templates](starter/README.md) can be used to configure new networks. + +The required genesis templates to setup a network are: + +- [`validity-predicates.toml`](#validity-predicates) +- [`tokens.toml`](#tokens) +- [`balances.toml`](#balances) +- [`parameters.toml`](#parameters) +- [`transactions.toml`](#transactions) + +## Validity predicates + +The [validity-predicates.toml file](validity-predicates) contains definitions of WASM validity predicates, which can be used in the [tokens](#tokens), [parameters](#parameters) and [transactions.toml](#transactions) files as validity predicates of established accounts. + +## Tokens + +The [tokens.toml file](tokens.toml) contains tokens with their aliases and validity predicates. + +## Balances + +The [balances.toml file](balances.toml) contains token balances associated with the public keys. + +TODO: add shielded balances + +## Parameters + +The [parameters.toml file](parameters.toml) contains the general chain parameters, PoS and governance parameters. + +## Transactions + +The [transactions.toml file](transactions.toml) contains any transactions that can be applied at genesis. These are: + +### Genesis tx `established_account` + +An established account with some `alias`, a validity predicate `vp` and optionally a `public_key`. When a public key is used, the transaction must be [signed](#signing-genesis-txs) with it to authorize its use. + +An unsigned `established_account` tx example: + +```toml +[[established_account]] +alias = "Albert" # Aliases are case-insensitive +vp = "vp_user" +public_key = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +``` + +### Genesis tx `validator_account` + +A validator account with some `alias`, a validity predicate `vp`, various keys and validator variables. Public keys used in the transaction must also [sign](#signing-validator-genesis-txs) the transaction to authorize their use. + +An unsigned `validator_account` tx example: + +```toml +[[validator_account]] +alias = "validator-0" +vp = "vp_validator" +commission_rate = "0.05" +max_commission_rate_change = "0.01" +net_address = "127.0.0.1:27656" +account_key = "pktest1qzjnu45v9uvvz4shwkxrgq44l7l4ncs0ryt9mwt7973fdjvm76tgkkakjmf" +consensus_key = "pktest1qp4dcws0fthlrt69erz854efxxtxvympw9m3npy2w8rphqgxu2ufc476rgt" +protocol_key = "pktest1qqwg6uwuxn70spl9x377v0q6fzr6d29gpkdfc0tmp8uj97p5awnukkeue3k" +tendermint_node_key = "pktest1qzmajsm6a5uamaq7el4kkp6txe9jt0ld3q0jy0er7cuz0u0k2yck6ls5ppm" +dkg_key = "dpktest1vqqqqqqzlgrsdkkjc0yg842xqkffy7g2vwvx3x8389ydprz2qwncruzxr8cg8u939z4yy76wkx6uwfe7qur95yrftsd0r8lu0ayhu4zqsrkf9em3n5zpm7jkcmjtg0a24h2fa5gejvt0ywddwc6xa72f3z8czkcw9ynw66" +``` + +### Genesis tx `transfer` + +A transfer can only be applied from one of the keys used in [Balances file](#balances) as the `source`. The target may be another key or an alias of an account to be created with `established_account` or `validator_account` genesis transactions. + +An unsigned `transfer` tx example: + +```toml +[[transfer]] +token = "NAM" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = 1_000_000 +``` + +### Genesis tx `bond` + +A bond may be either a self-bond when the `source` is the same as `validator` or a delegation otherwise. + +An example of an unsigned delegation `bond` tx from `established_account` with alias "albert": + +```toml +[[bond]] +source = "albert" +validator = "validator-0" # There must be a `validator_account` tx with this alias +amount = 20_000 # in native token NAM +``` + +For a delegation `bond` tx from an implicit account, one can use a public key as the source: + +```toml +[[bond]] +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +validator = "validator-0" +amount = 20_000 # in native token NAM +``` + +Note that for a delegation, the source key must have the sufficient balance assigned in the Balances file. + +An unsigned self-`bond` tx example: + +```toml +[[bond]] +source = "validator-0" +validator = "validator-0" +amount = 90_000_000 # The validator must have this amount of NAM available in account +``` + +### Signing genesis txs + +To sign genesis transactions, the data is borsh-encoded into a `Tx` `data` field. For `code` an empty vec is used and for the timestamp we use the minimum UTC timestamp. The transaction must be constructed in exactly the same way to verify the signatures, which is being done by the ledger when we're initializing the genesis. Any transaction that has invalid signature or cannot be applied for any other reason, such as insufficient funds may fail at genesis initialization and the chain will continue to be initialized without it. + +For non-validator transactions, a helper tool for producing signatures for transactions can be used with e.g.: + +```shell +namada client utils \ + sign-genesis-tx \ + --path "unsigned-tx.toml" \ + --output "signed-txs.toml" +``` + +For validator txs, see [Signing validator genesis txs](#signing-validator-genesis-txs) below. + +#### Signing validator genesis txs + +To generate validator wallet and sign validator transactions, run e.g.: + +```shell +namadac utils \ + init-genesis-validator \ + --source validator-0-key \ + --alias validator-0 \ + --net-address "127.0.0.1:27656" \ + --commission-rate 0.05 \ + --max-commission-rate-change 0.01 \ + --transfer-from-source-amount 1_000_000_000 \ + --self-bond-amount 900_000_000 \ + --unsafe-dont-encrypt +``` + +The `--source` key alias must have already have native token `NAM` in the [Balances files](#balances) and the balance must be greater than or equal to `--transfer-from-source-amount`. + +The `--self-bond-amount` must be lower than or equal to `--transfer-from-source-amount`, but we recommend to keep at least some tokens in the validator account for submitting validator transactions to be able to pay for fees and gas. + +This command will generate a validator pre-genesis wallet and transactions file containing signed `validator_account`, `transfer` and `bond` txs. diff --git a/genesis/dev.toml b/genesis/dev.toml deleted file mode 100644 index dded8358d9..0000000000 --- a/genesis/dev.toml +++ /dev/null @@ -1,247 +0,0 @@ -# Developer network -genesis_time = "2021-12-20T15:00:00.00Z" -native_token = "NAM" - -# Some tokens present at genesis. - -[token.NAM] -address = "atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" -denom = 6 -vp = "vp_token" -[token.NAM.balances] -Albert = "1000000" -"Albert.public_key" = "100" -Bertha = "1000000" -"Bertha.public_key" = "2000" -Christel = "1000000" -"Christel.public_key" = "100" -Daewon = "1000000" -Ester = "1000000" -[token.NAM.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[token.BTC] -address = "atest1v4ehgw36xdzryve5gsc52veeg5cnsv2yx5eygvp38qcrvd29xy6rys6p8yc5xvp4xfpy2v694wgwcp" -denom = 8 -vp = "vp_token" -[token.BTC.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.BTC.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[token.ETH] -address = "atest1v4ehgw36xqmr2d3nx3ryvd2xxgmrq33j8qcns33sxezrgv6zxdzrydjrxveygd2yxumrsdpsf9jc2p" -denom = 18 -vp = "vp_token" -[token.ETH.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.ETH.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[token.DOT] -address = "atest1v4ehgw36gg6nvs2zgfpyxsfjgc65yv6pxy6nwwfsxgungdzrggeyzv35gveyxsjyxymyz335hur2jn" -denom = 10 -vp = "vp_token" -[token.DOT.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.DOT.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[token.Schnitzel] -address = "atest1v4ehgw36xue5xvf5xvuyzvpjx5un2v3k8qeyvd3cxdqns32p89rrxd6xx9zngvpegccnzs699rdnnt" -denom = 6 -vp = "vp_token" -[token.Schnitzel.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.Schnitzel.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[token.Apfel] -address = "atest1v4ehgw36gfryydj9g3p5zv3kg9znyd358ycnzsfcggc5gvecgc6ygs2rxv6ry3zpg4zrwdfeumqcz9" -denom = 6 -vp = "vp_token" -[token.Apfel.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.Apfel.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[token.Kartoffel] -address = "atest1v4ehgw36gep5ysecxq6nyv3jg3zygv3e89qn2vp48pryxsf4xpznvve5gvmy23fs89pryvf5a6ht90" -public_key = "" -denom = 6 -vp = "vp_token" -[token.Kartoffel.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.Kartoffel.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[established.Albert] -vp = "vp_user" - -[established.Bertha] -vp = "vp_user" - -[established.Christel] -vp = "vp_user" - -[established.masp] -address = "atest1v4ehgw36xaryysfsx5unvve4g5my2vjz89p52sjxxgenzd348yuyyv3hg3pnjs35g5unvde4ca36y5" -vp = "vp_masp" - -[implicit.Daewon] - -[implicit.Ester] - -# Wasm VP definitions - -# Wasm VP definitions - -# Implicit VP -[wasm.vp_implicit] -filename = "vp_implicit.wasm" - -# Default user VP in established accounts -[wasm.vp_user] -filename = "vp_user.wasm" - -# Default validator VP -[wasm.vp_validator] -# filename (relative to wasm path used by the node) -filename = "vp_validator.wasm" - -# MASP VP -[wasm.vp_masp] -filename = "vp_masp.wasm" - -# General protocol parameters. -[parameters] -# Minimum number of blocks in an epoch. -min_num_of_blocks = 4 -# Maximum expected time per block (in seconds). -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 -tx_whitelist = [] -# Implicit VP WASM name -implicit_vp = "vp_implicit" -# Expected number of epochs per year (also sets the min duration of an epoch in seconds) -epochs_per_year = 105_120 # 5 minute epochs -# The P gain factor in the Proof of Stake rewards controller -pos_gain_p = "0.1" -# The D gain factor in the Proof of Stake rewards controller -pos_gain_d = "0.1" -# The maximum number of signatures allowed per transaction -max_signatures_per_transaction = 15 - -[parameters.minimum_gas_price] -"atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" = "0.000001" - -# Proof of stake parameters. -[pos_params] -# Maximum number of consensus validators. -max_validator_slots = 128 -# Pipeline length (in epochs). Any change in the validator set made in -# epoch 'n' will become active in epoch 'n + pipeline_len'. -pipeline_len = 2 -# Unbonding length (in epochs). Validators may have their stake slashed -# for a fault in epoch 'n' up through epoch 'n + unbonding_len'. -unbonding_len = 3 -# Votes per fundamental staking token (namnam) -tm_votes_per_token = "0.1" -# Reward for proposing a block. -block_proposer_reward = "0.125" -# Reward for voting on a block. -block_vote_reward = "0.1" -# Maximum inflation rate per annum (10%) -max_inflation_rate = "0.1" -# Targeted ratio of staked tokens to total tokens in the supply -target_staked_ratio = "0.6667" -# Portion of a validator's stake that should be slashed on a duplicate -# vote. -duplicate_vote_min_slash_rate = "0.001" -# Portion of a validator's stake that should be slashed on a light -# client attack. -light_client_attack_min_slash_rate = "0.001" -# Number of epochs above and below (separately) the current epoch to -# consider when doing cubic slashing -cubic_slashing_window_length = 1 -# The minimum amount of bonded tokens that a validator needs to be in -# either the `consensus` or `below_capacity` validator sets -validator_stake_threshold = "1" - -# Governance parameters. -[gov_params] -# minimum amount of nam token to lock -min_proposal_fund = 500 -# proposal code size in bytes -max_proposal_code_size = 500000 -# min proposal voting period length in epochs -min_proposal_voting_period = 3 -# max proposal period length in epochs -max_proposal_period = 27 -# maximum number of characters in the proposal content -max_proposal_content_size = 10000 -# minimum epochs between end and grace epoch -min_proposal_grace_epochs = 6 - -[pgf_params] -# list of steward address at genezis -stewards = [] -# inflation rate for pgf fundings -pgf_inflation_rate = "0.1" -# inflation rate for pgf stewards -stewards_inflation_rate = "0.01" \ No newline at end of file diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml deleted file mode 100644 index 1096be0d28..0000000000 --- a/genesis/e2e-tests-single-node.toml +++ /dev/null @@ -1,260 +0,0 @@ -# Genesis configuration source for E2E tests with: -# - 1 genesis validator -# - User accounts same as the ones in "dev" build (Albert, Bertha, Christel) - -genesis_time = "2021-09-30T10:00:00Z" -native_token = "NAM" - -[validator.validator-0] -# Validator's staked NAM at genesis. -tokens = 200000 -# Amount of the validator's genesis token balance which is not staked. -non_staked_balance = 1000000000000 -# VP for the validator account -validator_vp = "vp_validator" -# Commission rate for rewards -commission_rate = "0.05" -# Maximum change per epoch in the commission rate -max_commission_rate_change = "0.01" -# (Public IP | Hostname):port address. -# We set the port to be the default+1000, so that if a local node was running at -# the same time as the E2E tests, it wouldn't affect them. -net_address = "127.0.0.1:27656" - -# Some tokens present at genesis. - -[token.NAM] -address = "atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" -denom = 6 -[token.NAM.balances] -Albert = "1000000" -"Albert.public_key" = "100" -Bertha = "1000000" -"Bertha.public_key" = "2000" -Christel = "1000000" -"Christel.public_key" = "100" -Daewon = "1000000" -Ester = "1000000" -"validator-0.public_key" = "100" -[token.NAM.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[token.BTC] -address = "atest1v4ehgw36xdzryve5gsc52veeg5cnsv2yx5eygvp38qcrvd29xy6rys6p8yc5xvp4xfpy2v694wgwcp" -denom = 8 -[token.BTC.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.BTC.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[token.ETH] -address = "atest1v4ehgw36xqmr2d3nx3ryvd2xxgmrq33j8qcns33sxezrgv6zxdzrydjrxveygd2yxumrsdpsf9jc2p" -denom = 18 -[token.ETH.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.ETH.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[token.DOT] -address = "atest1v4ehgw36gg6nvs2zgfpyxsfjgc65yv6pxy6nwwfsxgungdzrggeyzv35gveyxsjyxymyz335hur2jn" -denom = 10 -[token.DOT.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.DOT.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[token.Schnitzel] -address = "atest1v4ehgw36xue5xvf5xvuyzvpjx5un2v3k8qeyvd3cxdqns32p89rrxd6xx9zngvpegccnzs699rdnnt" -denom = 6 -[token.Schnitzel.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.Schnitzel.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[token.Apfel] -address = "atest1v4ehgw36gfryydj9g3p5zv3kg9znyd358ycnzsfcggc5gvecgc6ygs2rxv6ry3zpg4zrwdfeumqcz9" -denom = 6 -[token.Apfel.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.Apfel.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[token.Kartoffel] -address = "atest1v4ehgw36gep5ysecxq6nyv3jg3zygv3e89qn2vp48pryxsf4xpznvve5gvmy23fs89pryvf5a6ht90" -public_key = "" -denom = 6 -[token.Kartoffel.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.Kartoffel.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[established.Albert] -vp = "vp_user" - -[established.Bertha] -vp = "vp_user" - -[established.Christel] -vp = "vp_user" - -[established.masp] -address = "atest1v4ehgw36xaryysfsx5unvve4g5my2vjz89p52sjxxgenzd348yuyyv3hg3pnjs35g5unvde4ca36y5" -vp = "vp_masp" - -[implicit.Daewon] - -[implicit.Ester] - -# Wasm VP definitions - -# Implicit VP -[wasm.vp_implicit] -filename = "vp_implicit.wasm" - -# Default user VP in established accounts -[wasm.vp_user] -filename = "vp_user.wasm" - -# Default validator VP -[wasm.vp_validator] -# filename (relative to wasm path used by the node) -filename = "vp_validator.wasm" - -# MASP VP -[wasm.vp_masp] -filename = "vp_masp.wasm" - -# General protocol parameters. -[parameters] -# Minimum number of blocks in an epoch. -min_num_of_blocks = 4 -# Maximum expected time per block (in seconds). -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 -tx_whitelist = [] -# Implicit VP WASM name -implicit_vp = "vp_implicit" -# Expected number of epochs per year (also sets the min duration of an epoch in seconds). -# Remember to set this to a more reasonable number for production networks. Also, expect most masp -# txs to fail due to epoch boundary errors. -epochs_per_year = 31_536_000 -# The P gain factor in the Proof of Stake rewards controller -pos_gain_p = "0.1" -# The D gain factor in the Proof of Stake rewards controller -pos_gain_d = "0.1" -# The maximum number of signatures allowed per transaction -max_signatures_per_transaction = 15 - -[parameters.minimum_gas_price] -"atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" = "0.000001" - -# Proof of stake parameters. -[pos_params] -# Maximum number of consensus validators. -max_validator_slots = 128 -# Pipeline length (in epochs). Any change in the validator set made in -# epoch 'n' will become active in epoch 'n + pipeline_len'. -pipeline_len = 2 -# Unbonding length (in epochs). Validators may have their stake slashed -# for a fault in epoch 'n' up through epoch 'n + unbonding_len'. -unbonding_len = 3 -# Votes per fundamental staking token (namnam) - for testnets this should be 1. For e2e toml, a decimal is used for testing purposes. -tm_votes_per_token = "0.1" -# Reward for proposing a block. -block_proposer_reward = "0.125" -# Reward for voting on a block. -block_vote_reward = "0.1" -# Maximum inflation rate per annum (10%) -max_inflation_rate = "0.1" -# Targeted ratio of staked tokens to total tokens in the supply -target_staked_ratio = "0.6667" -# Portion of a validator's stake that should be slashed on a duplicate -# vote. -duplicate_vote_min_slash_rate = "0.001" -# Portion of a validator's stake that should be slashed on a light -# client attack. -light_client_attack_min_slash_rate = "0.001" -# Number of epochs above and below (separately) the current epoch to -# consider when doing cubic slashing -cubic_slashing_window_length = 1 -# The minimum amount of bonded tokens that a validator needs to be in -# either the `consensus` or `below_capacity` validator sets -validator_stake_threshold = "1" - -# Governance parameters. -[gov_params] -# minimum amount of nam token to lock -min_proposal_fund = 500 -# proposal code size in bytes -max_proposal_code_size = 1000000 -# min proposal period length in epochs -min_proposal_voting_period = 3 -# max proposal period length in epochs -max_proposal_period = 27 -# maximum number of characters in the proposal content -max_proposal_content_size = 10000 -# minimum epochs between end and grace epoch -min_proposal_grace_epochs = 6 - -[pgf_params] -# list of steward address at genezis -stewards = [] -# inflation rate for pgf fundings -pgf_inflation_rate = "0.1" -# inflation rate for pgf stewards -stewards_inflation_rate = "0.01" \ No newline at end of file diff --git a/genesis/localnet/README.md b/genesis/localnet/README.md new file mode 100644 index 0000000000..e483f7aa07 --- /dev/null +++ b/genesis/localnet/README.md @@ -0,0 +1,66 @@ +# Localnet genesis templates + +This directory contains genesis templates for a local network with a single validator. The `src` directory contains generated pre-genesis wallet pre-loaded with unencrypted keys and a single validator `"validator-0" wallet that are being used in the templates. + +If you're modifying any of the files here, you can run this to ensure that the changes are valid: + +```shell +cargo watch -x "test test_validate_localnet_genesis_templates" +``` + +## balances.toml + +The pre-genesis balances wallet is located at [pre-genesis/wallet.toml](pre-genesis/wallet.toml) was used to setup the [balances.toml](balances.toml) and can be re-generated from the repo's root dir with: + +```shell +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key gen \ + --alias albert-key --unsafe-dont-encrypt +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key gen \ + --alias bertha-key --unsafe-dont-encrypt +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key gen \ + --alias christel --unsafe-dont-encrypt +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key gen \ + --alias daewon --unsafe-dont-encrypt +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key gen \ + --alias validator-0-balance-key --unsafe-dont-encrypt +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key gen \ + --alias faucet-key --unsafe-dont-encrypt +``` + +The [balances.toml file](balances.toml) contains token balances associated with the public keys. The public keys from the wallet can be found with: + +```shell +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key list +``` + +## transactions.toml + +The pre-genesis validator wallet used to generate [validator transactions for transactions.toml](src/pre-genesis/validator-0/transactions.toml) is located at [src/pre-genesis/validator-0/validator-wallet.toml](src/pre-genesis/validator-0/validator-wallet.toml) and can be re-generated from the repo's root dir with: + +```shell +cargo run --bin namadac -- --base-dir "genesis/localnet/src" utils \ + init-genesis-validator \ + --source validator-0-balance-key \ + --alias validator-0 \ + --net-address "127.0.0.1:27656" \ + --commission-rate 0.05 \ + --max-commission-rate-change 0.01 \ + --transfer-from-source-amount 200000 \ + --self-bond-amount 100000 \ + --unsafe-dont-encrypt +``` + +The rest of the transactions are generated from [src/pre-genesis/unsigned-transactions.toml](src/pre-genesis/unsigned-transactions.toml) using: + +```shell +cargo run --bin namadac -- --base-dir "genesis/localnet/src" utils \ + sign-genesis-tx \ + --path "genesis/localnet/src/pre-genesis/unsigned-transactions.toml" \ + --output "genesis/localnet/src/pre-genesis/signed-transactions.toml" +``` + +This command produces [src/pre-genesis/signed-transactions.toml](src/pre-genesis/signed-transactions.toml), which is then concatenated in [transactions.toml](transactiosn.toml) with the validator transactions. + +## Validation + +A unit test `test_localnet_genesis_templates` is setup to check validity of the localnet setup. diff --git a/genesis/localnet/balances.toml b/genesis/localnet/balances.toml new file mode 100644 index 0000000000..8b84002267 --- /dev/null +++ b/genesis/localnet/balances.toml @@ -0,0 +1,110 @@ +# Genesis balances. +# +# This files contains the genesis balances of any tokens present at genesis +# associated with public keys. +# +# For example: +# ``` +# [token.NAM] +# some_pk_bech32m_encoding = 10 # genesis tokens, the amount can have up to 6 decimal places +# ``` +# +# The public keys present in here are taken from `pre-genesis/wallet.toml` + +[token.NAM] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "2000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "2000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "2000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "200000" + +[token.BTC] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "1000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "1000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "1000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "1000000" + +[token.ETH] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "1000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "1000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "1000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "1000000" + +[token.DOT] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "1000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "1000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "1000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "1000000" + +[token.Schnitzel] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "1000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "1000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "1000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "1000000" + +[token.Apfel] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "1000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "1000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "1000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "1000000" + +[token.Kartoffel] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "1000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "1000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "1000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "1000000" \ No newline at end of file diff --git a/genesis/localnet/parameters.toml b/genesis/localnet/parameters.toml new file mode 100644 index 0000000000..41fb316f34 --- /dev/null +++ b/genesis/localnet/parameters.toml @@ -0,0 +1,90 @@ +# General protocol parameters. +[parameters] +native_token = "NAM" +# Minimum number of blocks in an epoch. +min_num_of_blocks = 4 +# Maximum expected time per block (in seconds). +max_expected_time_per_block = 30 +# Max payload size, in bytes, for a tx batch proposal. +max_proposal_bytes = 22020096 +# vp whitelist +vp_whitelist = [] +# tx whitelist +tx_whitelist = [] +# Implicit VP WASM name +implicit_vp = "vp_implicit" +# Expected number of epochs per year (also sets the min duration of an epoch in seconds) +epochs_per_year = 31_536_000 +# The P gain factor in the Proof of Stake rewards controller +pos_gain_p = "0.1" +# The D gain factor in the Proof of Stake rewards controller +pos_gain_d = "0.1" +# Maximum number of signature per transaction +max_signatures_per_transaction = 15 +# Max gas for block +max_block_gas = 20000000 +# Fee unshielding gas limit +fee_unshielding_gas_limit = 20000 +# Fee unshielding descriptions limit +fee_unshielding_descriptions_limit = 15 + +# Map of the cost per gas unit for every token allowed for fee payment +[parameters.minimum_gas_price] +nam = "0.000001" + +# Proof of stake parameters. +[pos_params] +# Maximum number of active validators. +max_validator_slots = 128 +# Pipeline length (in epochs). Any change in the validator set made in +# epoch 'n' will become active in epoch 'n + pipeline_len'. +pipeline_len = 2 +# Unbonding length (in epochs). Validators may have their stake slashed +# for a fault in epoch 'n' up through epoch 'n + unbonding_len'. +unbonding_len = 3 +# Votes per fundamental staking token (namnam) +tm_votes_per_token = "1" +# Reward for proposing a block. +block_proposer_reward = "0.125" +# Reward for voting on a block. +block_vote_reward = "0.1" +# Maximum inflation rate per annum (10%) +max_inflation_rate = "0.1" +# Targeted ratio of staked tokens to total tokens in the supply +target_staked_ratio = "0.6667" +# Portion of a validator's stake that should be slashed on a duplicate +# vote. +duplicate_vote_min_slash_rate = "0.001" +# Portion of a validator's stake that should be slashed on a light +# client attack. +light_client_attack_min_slash_rate = "0.001" +# Number of epochs above and below (separately) the current epoch to +# consider when doing cubic slashing +cubic_slashing_window_length = 1 +# The minimum amount of bonded tokens that a validator needs to be in +# either the `consensus` or `below_capacity` validator sets +validator_stake_threshold = "1" + +# Governance parameters. +[gov_params] +# minimum amount of nam token to lock +min_proposal_fund = 500 +# proposal code size in bytes +max_proposal_code_size = 600000 +# min proposal period length in epochs +min_proposal_voting_period = 3 +# max proposal period length in epochs +max_proposal_period = 27 +# maximum number of characters in the proposal content +max_proposal_content_size = 10000 +# minimum epochs between end and grace epoch +min_proposal_grace_epochs = 6 + +# Public goods funding parameters +[pgf_params] +# Initial set of stewards +stewards = ["bertha", "validator-0"] +# The pgf funding inflation rate +pgf_inflation_rate = "0.1" +# The pgf stewards inflation rate +stewards_inflation_rate = "0.01" diff --git a/genesis/localnet/src/pre-genesis/signed-transactions.toml b/genesis/localnet/src/pre-genesis/signed-transactions.toml new file mode 100644 index 0000000000..722e4cc430 --- /dev/null +++ b/genesis/localnet/src/pre-genesis/signed-transactions.toml @@ -0,0 +1,126 @@ +[[established_account]] +alias = "albert" +vp = "vp_user" + +[established_account.public_key] +pk = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +authorization = "sigtest1qzq6dwf2a9dq4hp3nmxfgckh55mulzryxsfkqhr9uvfvmtr9wt38lyyvzvfqryxnat2a4ry6hygv957z683dyngu03gse2uvl5ldfccypktkp6" + +[established_account.storage] + +[[established_account]] +alias = "bertha" +vp = "vp_user" + +[established_account.public_key] +pk = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +authorization = "sigtest1qzt58nd7k2mj647x8x4ydhsjkut7dmsl7yjlnrnwu9kzjdch3cljv6dq05mx2kvwn80kjezh7lz26adc5ksvyn3knufymtkhlmnhg3c8zka3y4" + +[established_account.storage] + +[[established_account]] +alias = "christel" +vp = "vp_user" + +[established_account.public_key] +pk = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +authorization = "sigtest1qrf2l6a5u4ywskryhdueudnm50j3kqhujas3mmktenqm89fmlskjnyeskr2tr7js5swmtqqtenkj6ap9xpelx2w40fjjczc4w9xtdggqjtszy4" + +[established_account.storage] + +[[transfer]] +token = "nam" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qrxfthuz5yzapdt82d8nc656ffrzkxnkfg6fw8w7e6045vcpn8kpr4c536ug4fs2ddz5823dfd3w4qnhcl40qmtasfccaa99a0hvjcqvyy2l02" + +[[transfer]] +token = "btc" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qq826qel8zn5p4dfx25ma45uxeskx0yjlsaxp38uyvehvds6t93c8wkhaydunm4nzz9750dwhkjst00mfrwmu066y3rj6gtk5wjskxq9zvk7ed" + +[[transfer]] +token = "eth" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qzghddk0u6kpqaxhskrj8ah69d3hce95wndgvlpuyvuw0h0fnyrnp026ltvtfmfhpzywkf88yyhv80hzkus57s9nn6vn3ljvmkkf7zgq2nzn40" + +[[transfer]] +token = "dot" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qqlu8kajh0up5zhewd2h44fhq7zdaapqt9xaj8xp7jmdqpv4dl9d5tesw3wyr9su962rvpkf02x27579vqjk4f4advx7t9ejz74cjrc25puna6" + +[[transfer]] +token = "schnitzel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qpukdd6f7pvtgau9v58xxwrnmv5pjra6527aggce9j8cu6fnekzag2cmmf4w0x0cn76gpth2vw6zcsnw37xjuz96hflqfnk5sznxe6cfd5mjnv" + +[[transfer]] +token = "apfel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qprnsyhlruvw4a6jjs98xdd04gxdsgycywzt79gyyak68gn4f50ppdvdm0q6ns44cheu7s43kal0qsldml8s0zexqk3s9zmglu4l6fszwcqswy" + +[[transfer]] +token = "kartoffel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qpg4y6ltax3qfclguym2kpa0ezlkf4hz4u3h8fc2ndmuzm4zfe26ygq9u6nhjjvj93n5v296x4arkvjk22ygee2fx054ce7ap6gvynq2j2k0vf" + +[[transfer]] +token = "nam" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" +signature = "sigtest1qpe3ms76cxvqsa58624ked5ndjr3jr3u7lf9ypalxdh29gajwmsxtzjxzswk0xmza90u47tgksr0d7enpd730ps7fq5u76l7ekxeyvgfnl0qxj" + +[[transfer]] +token = "btc" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" +signature = "sigtest1qqq4yc6s3h4ysezcmgsdgyfsqu8mmrwwc8a358sm864zehx74ums8fmgmkyyr3wz9qqrc5fagwrw0a4mgqkkleumc0u6ynshj5ns0tcdrygupg" + +[[transfer]] +token = "eth" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" +signature = "sigtest1qzyqkdxt3cxf0huv0hzskqmuthw38s3kj2sx50znzc68gzx64hrd3hkyx0g27pruwa9fv42suw3k8gkfaelcp4ymnvfw7ltx0mz2z9qr2lwwcs" + +[[transfer]] +token = "nam" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" +signature = "sigtest1qran2jh0dwuwchh45snhk3p9uyxqym57eg527pcjeekwhvpr5j0fy6dte7x3v2dwwc6vqmv49drpvmk6uhzlt2p8rqx0th3h7smf8rqrcpfxc7" + +[[transfer]] +token = "btc" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" +signature = "sigtest1qrxp6p56qtvsyp7jfs4jru3mkfv6l4t7hpguur72579pqjaexdry9turgmzwzj6qmua3s2y2a4krtuxwaag66j4dpz44aaqksy30tfcxmnhw2k" + +[[transfer]] +token = "eth" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" +signature = "sigtest1qrtrec6mps9p7zhmm7sywxwc7zdm32umt0zdwpuflrkh6952rx74amnsf9hn0yughs4e6czgpdmwsmujvkr4ptk2x24vtc8phz0s2asvtyy9pf" + +[[bond]] +source = "albert" +validator = "validator-0" +amount = "20000" +signature = "sigtest1qpx0kc8rl966vnnswfwhcwxh5vv69p7lev2apc2yephag978gqg5903kuw6dugg9c6pg5jdw048umk72ntympphz0dka74aspldslaqr3q7eqr" diff --git a/genesis/localnet/src/pre-genesis/unsigned-transactions.toml b/genesis/localnet/src/pre-genesis/unsigned-transactions.toml new file mode 100644 index 0000000000..00c82cf511 --- /dev/null +++ b/genesis/localnet/src/pre-genesis/unsigned-transactions.toml @@ -0,0 +1,114 @@ +# This file contains hand-written unsigned transactions for localnet with: +# +# - MASP account +# - Faucet account that allows anyone to withdraw limited amount of tokens +# - 2 established accounts for "Albert" and "Bertha" +# +# Note that 2 more localnet user accounts "Christel" and "Daewon" are left as +# implicit accounts, so their tokens are kept in the accounts derived from their +# keys used in `balances.toml`. +# +# This file is used to produce `signed-transactions.toml` with +# the `sign-genesis-tx` command. + +# Albert +[[established_account]] +alias = "albert" +vp = "vp_user" +public_key = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" + +# Bertha +[[established_account]] +alias = "bertha" +vp = "vp_user" +public_key = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" + +# Christel +[[established_account]] +alias = "christel" +vp = "vp_user" +public_key = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" + +# Transfer all the Albert's tokens into it's established account: +[[transfer]] +token = "NAM" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "BTC" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "ETH" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "DOT" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "Schnitzel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "Apfel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "Kartoffel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "NAM" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" + +[[transfer]] +token = "BTC" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" + +[[transfer]] +token = "ETH" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" + +[[transfer]] +token = "NAM" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" + +[[transfer]] +token = "BTC" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" + +[[transfer]] +token = "ETH" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" + +[[bond]] +source = "albert" +validator = "validator-0" +amount = "20000" \ No newline at end of file diff --git a/genesis/localnet/src/pre-genesis/validator-0/transactions.toml b/genesis/localnet/src/pre-genesis/validator-0/transactions.toml new file mode 100644 index 0000000000..571aa7e523 --- /dev/null +++ b/genesis/localnet/src/pre-genesis/validator-0/transactions.toml @@ -0,0 +1,44 @@ +[[validator_account]] +alias = "validator-0" +dkg_key = "dpktest1vqqqqqqndwmp3cvzfuepzvdh3fmt4dvjwd4vx6ffxk6t4vm6waysng2q8zh3yu8hdnxhwnwq22p8fwaqlyzcggef6k6qv3h3qtyy8c8c686w50v7d0z49ufd6zje46ujzc4ew5z7sdh45d94cvywy2v40yz9jlckh8cksr" +vp = "vp_validator" +commission_rate = "0.05" +max_commission_rate_change = "0.01" +net_address = "127.0.0.1:27656" + +[validator_account.account_key] +pk = "pktest1qpqznlesv6fcgsz9x4rsn7l4dsnp7h5sswvs9atw0zgq3uwp0jll2r42szz" +authorization = "sigtest1qrcy6a095uj6n8ryc9fkfw3hn2jn285g82hdratzrvmg22xp5qdmyr7e9q97t77v4zr8a73x4c3k53pcec3mp4ugt5jqd2w0fvd7nhcp0ajsk4" + +[validator_account.consensus_key] +pk = "pktest1qpk2a5m26cgnrp32l2c2hss03draxwnctu92llhlr6kjesn2kt892k2kklp" +authorization = "sigtest1qzajuey408zf66wgw8x3awd5jx9x249csx63n80clehj238qqpsf9cdr630edqjr8fw95vywlngy368r087sge2xy89qpcsrxvwtm3gr0h6g79" + +[validator_account.protocol_key] +pk = "pktest1qpfv9pj4zmelcg98rtw8wezztzdgpuczuxjmx946klkxajmvvcxjz3vsv63" +authorization = "sigtest1qz0aerskgtzwmgxwgml72zyz4974njfz6s9dl7r399d0v64pzsmctkwe8f28dwyef52hklj6r2fqmeypncvv3qg2x4hg4u3g23kw23qt73uzmy" + +[validator_account.tendermint_node_key] +pk = "pktest1qp7shxxfdkp8ft8xfkgf2vqy7u6effdp82w0cq6aq8575txul33t63dyd68" +authorization = "sigtest1qq87l6zjuswmrfeysxp8j0tu5zlklnswzaqgcuyaq23p49hhvupa57r3rwrh3txfuvq8lh5e7wu7d90js8cn7a7dygs7v5yx2a9ljtqxwl0chr" + +[validator_account.eth_hot_key] +pk = "pktest1qypdzza7uqtklzzs50hhy07h5ru9p0v8y5666wwulqnd3mdpuj0tcxsgck694" +authorization = "sigtest1q9qtzvwlqhnft5jgeyms2w757dlly8rjg4g89593ayhan0fx7uvjzvm7r7ltgsly9tp6783pqpl6ezxty240udrrknxmunymz6lm523qqqwx72ra" + +[validator_account.eth_cold_key] +pk = "pktest1qypzca0n6l890jc83nk5lljuzps04xmfccz3f87xc6ez40t2wvql48s52cx8r" +authorization = "sigtest1q8qmjstc2jtt3dwueepv0njk9te5qpj6z9yj6w0duhagswz80vyws3ywdsy9n49qnjecvnpxwlna3578vn423tgd4rrtss06ejgkj7c5qyv22nm6" + +[[transfer]] +token = "nam" +source = "pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup" +target = "validator-0" +amount = "200000" +signature = "sigtest1qz8ch7n6jp7g7hvnzkhmprhqc5umd0gatarlttm7gpcdx6g8hmgqffdq67fn6vzcflmym5xej3m5jmfw7nap00s4nlx2zxrlwz0wlfgts9kera" + +[[bond]] +source = "validator-0" +validator = "validator-0" +amount = "100000" +signature = "sigtest1qpss2x7vewnr2sr2wxj0ftldcmvv7v7t7qsr26k2l2hx4gls7y9sksrcq0m9vv2ymdqqxgmmw0dmuwazflxuqt4v2p2wng4endtcfcc0v6f6tj" diff --git a/genesis/localnet/src/pre-genesis/validator-0/validator-wallet.toml b/genesis/localnet/src/pre-genesis/validator-0/validator-wallet.toml new file mode 100644 index 0000000000..b6cc8fea5f --- /dev/null +++ b/genesis/localnet/src/pre-genesis/validator-0/validator-wallet.toml @@ -0,0 +1,11 @@ +account_key = "unencrypted:00dba003e446e56fb3382aaea01f2aab00dd7ea10a8c6725245b9542bc22c51519" +consensus_key = "unencrypted:00c229df6767b29891a1d45285450d8588ee0bc6b61d294bf958001cd7d93fea4e" +eth_cold_key = "unencrypted:0173220e1258baf4bd4851bbdd41e3fc5167ceb0cbcd28d3d1d0d250f7563b2350" +tendermint_node_key = "unencrypted:0036389d415fdb91c224fc6d42a301f545cf9ea879f2ebe548fd31179341e4e318" + +[validator_keys] +protocol_keypair = "ED25519_SK_PREFIX00d95753fab39f87e5c20ecab12690fefa67e97164ca6f17d67aeba50883b93dde" +eth_bridge_keypair = "SECP256K1_SK_PREFIX01263b5b517759fc30c3abed13907b75e779dae70e3675135a788de7c7076efebf" + +[validator_keys.dkg_keypair] +decryption_key = [208, 36, 153, 32, 179, 193, 163, 222, 29, 238, 154, 53, 181, 71, 213, 162, 59, 130, 225, 93, 57, 20, 161, 254, 52, 1, 172, 184, 112, 189, 160, 102] diff --git a/genesis/localnet/src/pre-genesis/wallet.toml b/genesis/localnet/src/pre-genesis/wallet.toml new file mode 100644 index 0000000000..2444ba3f73 --- /dev/null +++ b/genesis/localnet/src/pre-genesis/wallet.toml @@ -0,0 +1,34 @@ +[view_keys] + +[spend_keys] + +[payment_addrs] + +[keys] +albert-key = "unencrypted:0083318ccceac6c08a0177667840b4b93f0e455e45d4c38c28b73b8f8462fbf548" +bertha-key = "unencrypted:0073ed61817720d27784e7a9bca4a60668d763a6f7ecac2d45ed1f241aa5c59e99" +christel-key = "unencrypted:003625b939a58ef60402d7cf8fc04250026cc1ba90cc3028af1ce6b22be857ffc7" +daewon = "unencrypted:00d19e226c0e7d123d79f5908b5948d4c461b66a5f8aa95600c28b55ab6f5dc772" +ester = "unencrypted:01369093e2035d84f72a7e5a17c89b7a938b5d08cc87b2289805e3afcc66ef9a42" +faucet-key = "unencrypted:00548aa8393422b88dce5f4be8ee0320638061c3e0649ada1b0dacbec4c0c75bb2" +validator-0-balance-key = "unencrypted:000b9c8cb8f3ad6b8a387b064a11c0e98189814e9733aa7bb1e802425f6356f98a" + +[addresses] +albert-key = "atest1d9khqw36x56rgvphx5erjwp48y6y2s6y89qnwdzygce5xsfkx9rrsd35xerrjdp3gsunz33n3zytee" +bertha-key = "atest1d9khqw36xvm5zwpjxqm5zwz9x56rydp5gscnwsf4geznjdpegvmrwv29xcmnxvp4x5u5vdjp4wcjdf" +christel-key = "atest1d9khqw36gccrsv348qurzvpcx3p52ve3xu6rys3kxc6yxsjrgymyg32xggmnxvj9g5erzv2rn6pwv6" +daewon = "atest1d9khqw36xuc5gsfegsen2sfsx4q5yvz9xq6yv3psgcunj334g9prvdzzxucnyv2zxqm5xve44x97gg" +ester = "atest1d9khqw36xumnzd3cxccyx33k8ymyy3f5x3z5zd3jx5crsdfcgezyyv6rg3z5vw2xxcenqdpez4uxkf" +faucet-key = "atest1d9khqw368ycns3jp8q6nx329g5myg3zyggu523f589zyzs6x8pq5g3zzx9pr2d3h8qmrjdjyvvty45" +validator-0-balance-key = "atest1d9khqw36geqnxw29x9q5xvj98qmnj3fjgscygs6pg4p52s6z89qnzd3exgmnsv34gfqnj33elp2296" + +[pkhs] +37A8207A8E54244D17A5FE949C671E6730559F6A = "bertha-key" +544075298594ECD9A74DF3CA61F8646F941D91F3 = "albert-key" +71DA9D35A05AB0E04FD0F99F5AB64B7121B07C35 = "daewon" +918FA853EEE6DDDB9EE49DACF8ADDB1B5678696D = "faucet-key" +F0825881084CE31742B664CBCA6DEFB732EE211C = "christel-key" +7716860CF696BE44EA6250858FDB3CDEF9F63049 = "ester" +FA39E1AC2E879E2D0DCAECECB9A16927825BA9F9 = "validator-0-balance-key" + +[address_vp_types] diff --git a/genesis/localnet/tokens.toml b/genesis/localnet/tokens.toml new file mode 100644 index 0000000000..3079fab700 --- /dev/null +++ b/genesis/localnet/tokens.toml @@ -0,0 +1,22 @@ +# Token accounts with their validity predicates + +[token.NAM] +denom = 6 + +[token.BTC] +denom = 8 + +[token.ETH] +denom = 18 + +[token.DOT] +denom = 10 + +[token.Schnitzel] +denom = 6 + +[token.Apfel] +denom = 6 + +[token.Kartoffel] +denom = 6 \ No newline at end of file diff --git a/genesis/localnet/transactions.toml b/genesis/localnet/transactions.toml new file mode 100644 index 0000000000..9a4132d95e --- /dev/null +++ b/genesis/localnet/transactions.toml @@ -0,0 +1,180 @@ +# Transactions pasted from: +# +# 1. `src/pre-genesis/validator-0/transactions.toml` +# 2. `src/pre-genesis/signed-transactions.toml` + +# 1. + +[[validator_account]] +alias = "validator-0" +dkg_key = "dpktest1vqqqqqqndwmp3cvzfuepzvdh3fmt4dvjwd4vx6ffxk6t4vm6waysng2q8zh3yu8hdnxhwnwq22p8fwaqlyzcggef6k6qv3h3qtyy8c8c686w50v7d0z49ufd6zje46ujzc4ew5z7sdh45d94cvywy2v40yz9jlckh8cksr" +vp = "vp_validator" +commission_rate = "0.05" +max_commission_rate_change = "0.01" +net_address = "127.0.0.1:27656" + +[validator_account.account_key] +pk = "pktest1qpqznlesv6fcgsz9x4rsn7l4dsnp7h5sswvs9atw0zgq3uwp0jll2r42szz" +authorization = "sigtest1qrcy6a095uj6n8ryc9fkfw3hn2jn285g82hdratzrvmg22xp5qdmyr7e9q97t77v4zr8a73x4c3k53pcec3mp4ugt5jqd2w0fvd7nhcp0ajsk4" + +[validator_account.consensus_key] +pk = "pktest1qpk2a5m26cgnrp32l2c2hss03draxwnctu92llhlr6kjesn2kt892k2kklp" +authorization = "sigtest1qzajuey408zf66wgw8x3awd5jx9x249csx63n80clehj238qqpsf9cdr630edqjr8fw95vywlngy368r087sge2xy89qpcsrxvwtm3gr0h6g79" + +[validator_account.protocol_key] +pk = "pktest1qpfv9pj4zmelcg98rtw8wezztzdgpuczuxjmx946klkxajmvvcxjz3vsv63" +authorization = "sigtest1qz0aerskgtzwmgxwgml72zyz4974njfz6s9dl7r399d0v64pzsmctkwe8f28dwyef52hklj6r2fqmeypncvv3qg2x4hg4u3g23kw23qt73uzmy" + +[validator_account.tendermint_node_key] +pk = "pktest1qp7shxxfdkp8ft8xfkgf2vqy7u6effdp82w0cq6aq8575txul33t63dyd68" +authorization = "sigtest1qq87l6zjuswmrfeysxp8j0tu5zlklnswzaqgcuyaq23p49hhvupa57r3rwrh3txfuvq8lh5e7wu7d90js8cn7a7dygs7v5yx2a9ljtqxwl0chr" + +[validator_account.eth_hot_key] +pk = "pktest1qypdzza7uqtklzzs50hhy07h5ru9p0v8y5666wwulqnd3mdpuj0tcxsgck694" +authorization = "sigtest1q9qtzvwlqhnft5jgeyms2w757dlly8rjg4g89593ayhan0fx7uvjzvm7r7ltgsly9tp6783pqpl6ezxty240udrrknxmunymz6lm523qqqwx72ra" + +[validator_account.eth_cold_key] +pk = "pktest1qypzca0n6l890jc83nk5lljuzps04xmfccz3f87xc6ez40t2wvql48s52cx8r" +authorization = "sigtest1q8qmjstc2jtt3dwueepv0njk9te5qpj6z9yj6w0duhagswz80vyws3ywdsy9n49qnjecvnpxwlna3578vn423tgd4rrtss06ejgkj7c5qyv22nm6" + +[[transfer]] +token = "nam" +source = "pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup" +target = "validator-0" +amount = "200000" +signature = "sigtest1qz8ch7n6jp7g7hvnzkhmprhqc5umd0gatarlttm7gpcdx6g8hmgqffdq67fn6vzcflmym5xej3m5jmfw7nap00s4nlx2zxrlwz0wlfgts9kera" + +[[bond]] +source = "validator-0" +validator = "validator-0" +amount = "100000" +signature = "sigtest1qpss2x7vewnr2sr2wxj0ftldcmvv7v7t7qsr26k2l2hx4gls7y9sksrcq0m9vv2ymdqqxgmmw0dmuwazflxuqt4v2p2wng4endtcfcc0v6f6tj" + +# 2. + +[[established_account]] +alias = "albert" +vp = "vp_user" + +[established_account.public_key] +pk = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +authorization = "sigtest1qzq6dwf2a9dq4hp3nmxfgckh55mulzryxsfkqhr9uvfvmtr9wt38lyyvzvfqryxnat2a4ry6hygv957z683dyngu03gse2uvl5ldfccypktkp6" + +[established_account.storage] + +[[established_account]] +alias = "bertha" +vp = "vp_user" + +[established_account.public_key] +pk = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +authorization = "sigtest1qzt58nd7k2mj647x8x4ydhsjkut7dmsl7yjlnrnwu9kzjdch3cljv6dq05mx2kvwn80kjezh7lz26adc5ksvyn3knufymtkhlmnhg3c8zka3y4" + +[established_account.storage] + +[[established_account]] +alias = "christel" +vp = "vp_user" + +[established_account.public_key] +pk = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +authorization = "sigtest1qrf2l6a5u4ywskryhdueudnm50j3kqhujas3mmktenqm89fmlskjnyeskr2tr7js5swmtqqtenkj6ap9xpelx2w40fjjczc4w9xtdggqjtszy4" + +[established_account.storage] + +[[transfer]] +token = "nam" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qrxfthuz5yzapdt82d8nc656ffrzkxnkfg6fw8w7e6045vcpn8kpr4c536ug4fs2ddz5823dfd3w4qnhcl40qmtasfccaa99a0hvjcqvyy2l02" + +[[transfer]] +token = "btc" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qq826qel8zn5p4dfx25ma45uxeskx0yjlsaxp38uyvehvds6t93c8wkhaydunm4nzz9750dwhkjst00mfrwmu066y3rj6gtk5wjskxq9zvk7ed" + +[[transfer]] +token = "eth" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qzghddk0u6kpqaxhskrj8ah69d3hce95wndgvlpuyvuw0h0fnyrnp026ltvtfmfhpzywkf88yyhv80hzkus57s9nn6vn3ljvmkkf7zgq2nzn40" + +[[transfer]] +token = "dot" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qqlu8kajh0up5zhewd2h44fhq7zdaapqt9xaj8xp7jmdqpv4dl9d5tesw3wyr9su962rvpkf02x27579vqjk4f4advx7t9ejz74cjrc25puna6" + +[[transfer]] +token = "schnitzel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qpukdd6f7pvtgau9v58xxwrnmv5pjra6527aggce9j8cu6fnekzag2cmmf4w0x0cn76gpth2vw6zcsnw37xjuz96hflqfnk5sznxe6cfd5mjnv" + +[[transfer]] +token = "apfel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qprnsyhlruvw4a6jjs98xdd04gxdsgycywzt79gyyak68gn4f50ppdvdm0q6ns44cheu7s43kal0qsldml8s0zexqk3s9zmglu4l6fszwcqswy" + +[[transfer]] +token = "kartoffel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qpg4y6ltax3qfclguym2kpa0ezlkf4hz4u3h8fc2ndmuzm4zfe26ygq9u6nhjjvj93n5v296x4arkvjk22ygee2fx054ce7ap6gvynq2j2k0vf" + +[[transfer]] +token = "nam" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" +signature = "sigtest1qpe3ms76cxvqsa58624ked5ndjr3jr3u7lf9ypalxdh29gajwmsxtzjxzswk0xmza90u47tgksr0d7enpd730ps7fq5u76l7ekxeyvgfnl0qxj" + +[[transfer]] +token = "btc" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" +signature = "sigtest1qqq4yc6s3h4ysezcmgsdgyfsqu8mmrwwc8a358sm864zehx74ums8fmgmkyyr3wz9qqrc5fagwrw0a4mgqkkleumc0u6ynshj5ns0tcdrygupg" + +[[transfer]] +token = "eth" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" +signature = "sigtest1qzyqkdxt3cxf0huv0hzskqmuthw38s3kj2sx50znzc68gzx64hrd3hkyx0g27pruwa9fv42suw3k8gkfaelcp4ymnvfw7ltx0mz2z9qr2lwwcs" + +[[transfer]] +token = "nam" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" +signature = "sigtest1qran2jh0dwuwchh45snhk3p9uyxqym57eg527pcjeekwhvpr5j0fy6dte7x3v2dwwc6vqmv49drpvmk6uhzlt2p8rqx0th3h7smf8rqrcpfxc7" + +[[transfer]] +token = "btc" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" +signature = "sigtest1qrxp6p56qtvsyp7jfs4jru3mkfv6l4t7hpguur72579pqjaexdry9turgmzwzj6qmua3s2y2a4krtuxwaag66j4dpz44aaqksy30tfcxmnhw2k" + +[[transfer]] +token = "eth" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" +signature = "sigtest1qrtrec6mps9p7zhmm7sywxwc7zdm32umt0zdwpuflrkh6952rx74amnsf9hn0yughs4e6czgpdmwsmujvkr4ptk2x24vtc8phz0s2asvtyy9pf" + +[[bond]] +source = "albert" +validator = "validator-0" +amount = "20000" +signature = "sigtest1qpx0kc8rl966vnnswfwhcwxh5vv69p7lev2apc2yephag978gqg5903kuw6dugg9c6pg5jdw048umk72ntympphz0dka74aspldslaqr3q7eqr" diff --git a/genesis/localnet/validity-predicates.toml b/genesis/localnet/validity-predicates.toml new file mode 100644 index 0000000000..a0c2570121 --- /dev/null +++ b/genesis/localnet/validity-predicates.toml @@ -0,0 +1,17 @@ +# WASM Validity predicate that can be used for genesis accounts + +# Implicit VP +[wasm.vp_implicit] +filename = "vp_implicit.wasm" + +# Default user VP in established accounts +[wasm.vp_user] +filename = "vp_user.wasm" + +# Default validator VP +[wasm.vp_validator] +filename = "vp_validator.wasm" + +# MASP VP +[wasm.vp_masp] +filename = "vp_masp.wasm" diff --git a/genesis/starter/README.md b/genesis/starter/README.md new file mode 100644 index 0000000000..ef58236340 --- /dev/null +++ b/genesis/starter/README.md @@ -0,0 +1,71 @@ +# Starter genesis templates + +This directory contains genesis templates for a minimum configuration with a single token account, which can be used as a starter for setting up a new chain. + +If you're modifying any of the files here, you can run this to ensure that the changes are valid: + +```shell +cargo watch -x "test test_validate_starter_genesis_templates" +``` + +In order to be able to run it, the following has to be added: + +1. At least one key with a positive native [token balance](#token-balances) +2. At least one [validator account](#validator-accounts), with some native tokens transferred from the key, some of which have to be self-bonded in PoS to amount to a positive voting power + +## Token balances + +We'll generate a key and give it some token balance. + +To generate a new key in a pre-genesis wallet (before the chain is setup), you can use e.g.: + +```shell +namada wallet key gen --pre-genesis --alias "my-key" +``` + +This will print your public key: + +```shell +Successfully added a key and an address with alias: "my-key". +Public key: pktest1qz5ywdn47sdm8s7rkzjl5dud0k9c9ndd5agn4gu0u0ryrmtmyuxmk948q0p +``` + +The public key can then be given some tokens in the [balances.toml file](balances.toml) with e.g.: + +```toml +[token.NAM] +pktest1qz5ywdn47sdm8s7rkzjl5dud0k9c9ndd5agn4gu0u0ryrmtmyuxmk948q0p = 1_337_707.50 +``` + +## Validator accounts + +For this step, you'll need to have a key with some native [token balance](#token-balances), from which you can sign the validator account creation genesis transaction. + +To generate a new validator pre-genesis wallet and produce signed transactions with it, use e.g.: + +```shell +namada client utils \ + init-genesis-validator \ + --source "my-key" \ + --alias "my-validator" \ + --net-address "127.0.0.1:26656" \ + --commission-rate 0.05 \ + --max-commission-rate-change 0.01 \ + --transfer-from-source-amount 1337707.50 \ + --self-bond-amount 1000000 +``` + +This will print the validator transactions that can be added to the [transactions.toml file](transactions.toml). + +## Initialize the chain + +This is sufficient minimal configuration to initialize the chain with the single genesis validator. All that's left is to pick a chain ID prefix and genesis time: + +```shell +namada client utils \ + init-network \ + --chain-prefix "my-chain" \ + --genesis-time "2021-12-31T00:00:00Z" \ + --templates-path "path/to/templates" \ + --wasm-checksums-path "path/to/wasm/checksums.json" +``` diff --git a/genesis/starter/balances.toml b/genesis/starter/balances.toml new file mode 100644 index 0000000000..2f1aad4022 --- /dev/null +++ b/genesis/starter/balances.toml @@ -0,0 +1,2 @@ +[token.NAM] +# assign balance to public keys diff --git a/genesis/starter/parameters.toml b/genesis/starter/parameters.toml new file mode 100644 index 0000000000..86714f827c --- /dev/null +++ b/genesis/starter/parameters.toml @@ -0,0 +1,90 @@ +# General protocol parameters. +[parameters] +native_token = "NAM" +# Minimum number of blocks in an epoch. +min_num_of_blocks = 4 +# Maximum expected time per block (in seconds). +max_expected_time_per_block = 30 +# Max payload size, in bytes, for a tx batch proposal. +max_proposal_bytes = 22020096 +# vp whitelist +vp_whitelist = [] +# tx whitelist +tx_whitelist = [] +# Implicit VP WASM name +implicit_vp = "vp_implicit" +# Expected number of epochs per year (also sets the min duration of an epoch in seconds) +epochs_per_year = 31_536_000 +# The P gain factor in the Proof of Stake rewards controller +pos_gain_p = "0.1" +# The D gain factor in the Proof of Stake rewards controller +pos_gain_d = "0.1" +# Maximum number of signature per transaction +max_signatures_per_transaction = 15 +# Max gas for block +max_block_gas = 20000000 +# Fee unshielding gas limit +fee_unshielding_gas_limit = 20000 +# Fee unshielding descriptions limit +fee_unshielding_descriptions_limit = 15 + +# Map of the cost per gas unit for every token allowed for fee payment +[parameters.minimum_gas_price] +nam = "0.000001" + +# Proof of stake parameters. +[pos_params] +# Maximum number of active validators. +max_validator_slots = 128 +# Pipeline length (in epochs). Any change in the validator set made in +# epoch 'n' will become active in epoch 'n + pipeline_len'. +pipeline_len = 2 +# Unbonding length (in epochs). Validators may have their stake slashed +# for a fault in epoch 'n' up through epoch 'n + unbonding_len'. +unbonding_len = 3 +# Votes per fundamental staking token (namnam) +tm_votes_per_token = "1" +# Reward for proposing a block. +block_proposer_reward = "0.125" +# Reward for voting on a block. +block_vote_reward = "0.1" +# Maximum inflation rate per annum (10%) +max_inflation_rate = "0.1" +# Targeted ratio of staked tokens to total tokens in the supply +target_staked_ratio = "0.6667" +# Portion of a validator's stake that should be slashed on a duplicate +# vote. +duplicate_vote_min_slash_rate = "0.001" +# Portion of a validator's stake that should be slashed on a light +# client attack. +light_client_attack_min_slash_rate = "0.001" +# Number of epochs above and below (separately) the current epoch to +# consider when doing cubic slashing +cubic_slashing_window_length = 1 +# The minimum amount of bonded tokens that a validator needs to be in +# either the `consensus` or `below_capacity` validator sets +validator_stake_threshold = "1" + +# Governance parameters. +[gov_params] +# minimum amount of nam token to lock +min_proposal_fund = 500 +# proposal code size in bytes +max_proposal_code_size = 300000 +# min proposal period length in epochs +min_proposal_voting_period = 3 +# max proposal period length in epochs +max_proposal_period = 27 +# maximum number of characters in the proposal content +max_proposal_content_size = 10000 +# minimum epochs between end and grace epoch +min_proposal_grace_epochs = 6 + +# Public goods funding parameters +[pgf_params] +# Initial set of stewards +stewards = [] +# The pgf funding inflation rate +pgf_inflation_rate = "0.1" +# The pgf stewards inflation rate +stewards_inflation_rate = "0.01" diff --git a/genesis/starter/tokens.toml b/genesis/starter/tokens.toml new file mode 100644 index 0000000000..612a6901ff --- /dev/null +++ b/genesis/starter/tokens.toml @@ -0,0 +1,5 @@ +# Token accounts with their validity predicates + +[token.NAM] +vp = "vp_token" +denom = 6 \ No newline at end of file diff --git a/genesis/starter/transactions.toml b/genesis/starter/transactions.toml new file mode 100644 index 0000000000..e9406c2f03 --- /dev/null +++ b/genesis/starter/transactions.toml @@ -0,0 +1 @@ +# Genesis transactions \ No newline at end of file diff --git a/genesis/starter/validity-predicates.toml b/genesis/starter/validity-predicates.toml new file mode 100644 index 0000000000..863ec7ec38 --- /dev/null +++ b/genesis/starter/validity-predicates.toml @@ -0,0 +1,22 @@ +# WASM Validity predicate that can be used for genesis accounts + +# Implicit VP +[wasm.vp_implicit] +filename = "vp_implicit.wasm" + +# Default user VP in established accounts +[wasm.vp_user] +filename = "vp_user.wasm" + +# Default validator VP +[wasm.vp_validator] +filename = "vp_validator.wasm" + +# Token VP +[wasm.vp_token] +filename = "vp_token.wasm" + +# MASP VP +[wasm.vp_masp] +filename = "vp_masp.wasm" + diff --git a/proof_of_stake/src/epoched.rs b/proof_of_stake/src/epoched.rs index 796827de1f..c35b310d0c 100644 --- a/proof_of_stake/src/epoched.rs +++ b/proof_of_stake/src/epoched.rs @@ -1399,7 +1399,7 @@ mod test { let mut s = TestWlStorage::default(); let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); gov_params.init_storage(&mut s)?; - crate::init_genesis( + crate::test_utils::init_genesis_helper( &mut s, &PosParams::default(), [GenesisValidator { diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 0aa24142fc..618b20a373 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -60,7 +60,7 @@ use types::{ BondsAndUnbondsDetail, BondsAndUnbondsDetails, CommissionRates, ConsensusValidator, ConsensusValidatorSet, ConsensusValidatorSets, DelegatorRedelegatedBonded, DelegatorRedelegatedUnbonded, - EagerRedelegatedBondsMap, EpochedSlashes, GenesisValidator, + EagerRedelegatedBondsMap, EpochedSlashes, IncomingRedelegations, OutgoingRedelegations, Position, RedelegatedBondsOrUnbonds, RedelegatedTokens, ReverseOrdTokenAmount, RewardsAccumulator, RewardsProducts, Slash, SlashType, SlashedAmount, @@ -298,128 +298,38 @@ pub fn delegator_redelegated_unbonds_handle( pub fn init_genesis( storage: &mut S, params: &OwnedPosParams, - validators: impl Iterator + Clone, - current_epoch: namada_core::types::storage::Epoch, + current_epoch: Epoch, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { tracing::debug!("Initializing PoS genesis"); write_pos_params(storage, params)?; - let params = read_non_pos_owned_params(storage, params.clone())?; - let mut total_bonded = token::Amount::zero(); consensus_validator_set_handle().init(storage, current_epoch)?; below_capacity_validator_set_handle().init(storage, current_epoch)?; validator_set_positions_handle().init(storage, current_epoch)?; validator_addresses_handle().init(storage, current_epoch)?; + tracing::debug!("Finished genesis"); + Ok(()) +} - for GenesisValidator { - address, - tokens, - consensus_key, - protocol_key, - eth_cold_key, - eth_hot_key, - commission_rate, - max_commission_rate_change, - } in validators - { - // This will fail if the key is already being used - the uniqueness must - // be enforced in the genesis configuration to prevent it - try_insert_consensus_key(storage, &consensus_key)?; - - total_bonded += tokens; - - // Insert the validator into a validator set and write its epoched - // validator data - insert_validator_into_validator_set( - storage, - ¶ms, - &address, - tokens, - current_epoch, - 0, - )?; - - validator_addresses_handle() - .at(¤t_epoch) - .insert(storage, address.clone())?; - - // Write other validator data to storage - write_validator_address_raw_hash(storage, &address, &consensus_key)?; - write_validator_max_commission_rate_change( - storage, - &address, - max_commission_rate_change, - )?; - validator_consensus_key_handle(&address).init_at_genesis( - storage, - consensus_key, - current_epoch, - )?; - validator_protocol_key_handle(&address).init_at_genesis( - storage, - protocol_key, - current_epoch, - )?; - validator_eth_hot_key_handle(&address).init_at_genesis( - storage, - eth_hot_key, - current_epoch, - )?; - validator_eth_cold_key_handle(&address).init_at_genesis( - storage, - eth_cold_key, - current_epoch, - )?; - validator_deltas_handle(&address).init_at_genesis( - storage, - tokens.change(), - current_epoch, - )?; - bond_handle(&address, &address).init_at_genesis( - storage, - tokens, - current_epoch, - )?; - total_bonded_handle(&address).init_at_genesis( - storage, - tokens, - current_epoch, - )?; - validator_commission_rate_handle(&address).init_at_genesis( - storage, - commission_rate, - current_epoch, - )?; - } - - // Store the total consensus validator stake to storage - store_total_consensus_stake(storage, current_epoch)?; - - // Write total deltas to storage - total_deltas_handle().init_at_genesis( - storage, - token::Change::from(total_bonded), - current_epoch, - )?; +/// new init genesis +pub fn copy_genesis_validator_sets( + storage: &mut S, + params: &OwnedPosParams, + current_epoch: Epoch, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let params = read_non_pos_owned_params(storage, params.clone())?; - // Credit bonded token amount to the PoS account - let staking_token = staking_token_address(storage); - token::credit_tokens(storage, &staking_token, &ADDRESS, total_bonded)?; - // Copy the genesis validator set into the pipeline epoch as well + // Copy the genesis validator sets up to the pipeline epoch for epoch in (current_epoch.next()).iter_range(params.pipeline_len) { - copy_validator_sets_and_positions( - storage, - ¶ms, - current_epoch, - epoch, - )?; + copy_validator_sets_and_positions(storage, ¶ms, current_epoch, epoch)?; + store_total_consensus_stake(storage, epoch)?; } - - tracing::debug!("Genesis initialized"); - Ok(()) } @@ -600,15 +510,17 @@ where /// Add or remove PoS validator's stake delta value pub fn update_validator_deltas( storage: &mut S, + params: &OwnedPosParams, validator: &Address, delta: token::Change, current_epoch: namada_core::types::storage::Epoch, - offset: u64, + offset_opt: Option, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { let handle = validator_deltas_handle(validator); + let offset = offset_opt.unwrap_or(params.pipeline_len); let val = handle .get_delta_val(storage, current_epoch + offset)? .unwrap_or_default(); @@ -781,14 +693,16 @@ where /// Note: for EpochedDelta, write the value to change storage by pub fn update_total_deltas( storage: &mut S, + params: &OwnedPosParams, delta: token::Change, current_epoch: namada_core::types::storage::Epoch, - offset: u64, + offset_opt: Option, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { let handle = total_deltas_handle(); + let offset = offset_opt.unwrap_or(params.pipeline_len); let val = handle .get_delta_val(storage, current_epoch + offset)? .unwrap_or_default(); @@ -848,6 +762,7 @@ pub fn bond_tokens( validator: &Address, amount: token::Amount, current_epoch: Epoch, + offset_opt: Option, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -861,9 +776,8 @@ where } let params = read_pos_params(storage)?; - let pipeline_epoch = current_epoch + params.pipeline_len; - - // Check that the source is not a validator + let offset = offset_opt.unwrap_or(params.pipeline_len); + let offset_epoch = current_epoch + offset; if let Some(source) = source { if source != validator && is_validator(storage, source)? { return Err( @@ -874,7 +788,7 @@ where // Check that the validator is actually a validator let validator_state_handle = validator_state_handle(validator); - let state = validator_state_handle.get(storage, pipeline_epoch, ¶ms)?; + let state = validator_state_handle.get(storage, offset_epoch, ¶ms)?; if state.is_none() { return Err(BondError::NotAValidator(validator.clone()).into()); } @@ -887,7 +801,7 @@ where // Check that validator is not inactive at anywhere between the current // epoch and pipeline offset - for epoch in current_epoch.iter_range(params.pipeline_len) { + for epoch in current_epoch.iter_range(offset) { if let Some(ValidatorState::Inactive) = validator_state_handle.get(storage, epoch, ¶ms)? { @@ -901,12 +815,12 @@ where } // Initialize or update the bond at the pipeline offset - bond_handle.add(storage, amount, current_epoch, params.pipeline_len)?; + bond_handle.add(storage, amount, current_epoch, offset)?; total_bonded_handle.add( storage, amount, current_epoch, - params.pipeline_len, + offset, )?; if tracing::level_enabled!(tracing::Level::DEBUG) { @@ -919,7 +833,7 @@ where // must be no changes to the validator set. Check at the pipeline epoch. let is_jailed_at_pipeline = matches!( validator_state_handle - .get(storage, pipeline_epoch, ¶ms)? + .get(storage, offset_epoch, ¶ms)? .unwrap(), ValidatorState::Jailed ); @@ -929,24 +843,27 @@ where ¶ms, validator, amount.change(), - pipeline_epoch, + offset_epoch, + offset_opt, )?; } // Update the validator and total deltas update_validator_deltas( storage, + ¶ms, validator, amount.change(), current_epoch, - params.pipeline_len, + offset_opt, )?; update_total_deltas( storage, + ¶ms, amount.change(), current_epoch, - params.pipeline_len, + offset_opt, )?; // Transfer the bonded tokens from the source to PoS @@ -1068,7 +985,8 @@ fn update_validator_set( params: &PosParams, validator: &Address, token_change: token::Change, - epoch: Epoch, + current_epoch: Epoch, + offset: Option, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -1076,7 +994,8 @@ where if token_change.is_zero() { return Ok(()); } - // let pipeline_epoch = current_epoch + params.pipeline_len; + let offset = offset.unwrap_or(params.pipeline_len); + let epoch = current_epoch + offset; tracing::debug!( "Update epoch for validator set: {epoch}, validator: {validator}" ); @@ -1140,8 +1059,8 @@ where validator_state_handle(validator).set( storage, ValidatorState::BelowThreshold, - epoch, - 0, + current_epoch, + offset, )?; // Remove the validator's position from storage @@ -1177,8 +1096,8 @@ where validator_state_handle(&removed_max_below_capacity).set( storage, ValidatorState::Consensus, - epoch, - 0, + current_epoch, + offset, )?; } } else if tokens_post < max_below_capacity_validator_amount { @@ -1212,8 +1131,8 @@ where validator_state_handle(&removed_max_below_capacity).set( storage, ValidatorState::Consensus, - epoch, - 0, + current_epoch, + offset, )?; // Insert the current validator into the below-capacity set @@ -1226,8 +1145,8 @@ where validator_state_handle(validator).set( storage, ValidatorState::BelowCapacity, - epoch, - 0, + current_epoch, + offset, )?; } else { tracing::debug!("Validator remains in consensus set"); @@ -1270,7 +1189,8 @@ where validator, tokens_post, min_consensus_validator_amount, - epoch, + current_epoch, + offset, &consensus_val_handle, &below_capacity_val_handle, )?; @@ -1286,8 +1206,8 @@ where validator_state_handle(validator).set( storage, ValidatorState::BelowCapacity, - epoch, - 0, + current_epoch, + offset, )?; } else { // The current validator is demoted to the below-threshold set @@ -1298,8 +1218,8 @@ where validator_state_handle(validator).set( storage, ValidatorState::BelowThreshold, - epoch, - 0, + current_epoch, + offset, )?; // Remove the validator's position from storage @@ -1309,9 +1229,12 @@ where } } } else { - // If there is no position at pipeline offset, then the validator must - // be in the below-threshold set - debug_assert!(tokens_pre < params.validator_stake_threshold); + // At non-zero offset (0 is genesis only) + if offset > 0 { + // If there is no position at pipeline offset, then the validator + // must be in the below-threshold set + debug_assert!(tokens_pre < params.validator_stake_threshold); + } tracing::debug!("Target validator is below-threshold"); // Move the validator into the appropriate set @@ -1330,8 +1253,8 @@ where validator_state_handle(validator).set( storage, ValidatorState::Consensus, - epoch, - 0, + current_epoch, + offset, )?; } else { let min_consensus_validator_amount = @@ -1352,7 +1275,8 @@ where validator, tokens_post, min_consensus_validator_amount, - epoch, + current_epoch, + offset, &consensus_val_handle, &below_capacity_val_handle, )?; @@ -1371,8 +1295,8 @@ where validator_state_handle(validator).set( storage, ValidatorState::BelowCapacity, - epoch, - 0, + current_epoch, + offset, )?; } } @@ -1387,7 +1311,8 @@ fn insert_into_consensus_and_demote_to_below_cap( validator: &Address, tokens_post: token::Amount, min_consensus_amount: token::Amount, - epoch: Epoch, + current_epoch: Epoch, + offset: u64, consensus_set: &ConsensusValidatorSet, below_capacity_set: &BelowCapacityValidatorSet, ) -> storage_api::Result<()> @@ -1403,35 +1328,35 @@ where .remove(storage, &last_position_of_min_consensus_vals)? .expect("There must be always be at least 1 consensus validator"); - // let pipeline_epoch = current_epoch + params.pipeline_len; + let offset_epoch = current_epoch + offset; // Insert the min consensus validator into the below-capacity // set insert_validator_into_set( &below_capacity_set.at(&min_consensus_amount.into()), storage, - &epoch, + &offset_epoch, &removed_min_consensus, )?; validator_state_handle(&removed_min_consensus).set( storage, ValidatorState::BelowCapacity, - epoch, - 0, + current_epoch, + offset, )?; // Insert the current validator into the consensus set insert_validator_into_set( &consensus_set.at(&tokens_post), storage, - &epoch, + &offset_epoch, validator, )?; validator_state_handle(validator).set( storage, ValidatorState::Consensus, - epoch, - 0, + current_epoch, + offset, )?; Ok(()) } @@ -2048,23 +1973,26 @@ where ¶ms, validator, change_after_slashing, - pipeline_epoch, + current_epoch, + None, )?; } // Update the validator and total deltas at the pipeline offset update_validator_deltas( storage, + ¶ms, validator, change_after_slashing, current_epoch, - params.pipeline_len, + None, )?; update_total_deltas( storage, + ¶ms, change_after_slashing, current_epoch, - params.pipeline_len, + None, )?; if tracing::level_enabled!(tracing::Level::DEBUG) { @@ -2761,6 +2689,8 @@ pub struct BecomeValidator<'a, S> { pub commission_rate: Dec, /// Max commission rate change. pub max_commission_rate_change: Dec, + /// Optional offset to use instead of pipeline offset + pub offset_opt: Option, } /// Initialize data for a new validator. @@ -2781,12 +2711,14 @@ where current_epoch, commission_rate, max_commission_rate_change, + offset_opt, } = args; + let offset = offset_opt.unwrap_or(params.pipeline_len); // This will fail if the key is already being used try_insert_consensus_key(storage, consensus_key)?; - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = current_epoch + offset; validator_addresses_handle() .at(&pipeline_epoch) .insert(storage, address.clone())?; @@ -2804,37 +2736,37 @@ where storage, consensus_key.clone(), current_epoch, - params.pipeline_len, + offset, )?; validator_protocol_key_handle(address).set( storage, protocol_key.clone(), current_epoch, - params.pipeline_len, + offset, )?; validator_eth_hot_key_handle(address).set( storage, eth_hot_key.clone(), current_epoch, - params.pipeline_len, + offset, )?; validator_eth_cold_key_handle(address).set( storage, eth_cold_key.clone(), current_epoch, - params.pipeline_len, + offset, )?; validator_commission_rate_handle(address).set( storage, commission_rate, current_epoch, - params.pipeline_len, + offset, )?; validator_deltas_handle(address).set( storage, token::Change::zero(), current_epoch, - params.pipeline_len, + offset, )?; // The validator's stake at initialization is 0, so its state is immediately @@ -2843,7 +2775,7 @@ where storage, ValidatorState::BelowThreshold, current_epoch, - params.pipeline_len, + offset, )?; Ok(()) @@ -4526,6 +4458,7 @@ where &validator, -slash_amount.change(), epoch, + None, )?; } } @@ -4536,12 +4469,13 @@ where update_validator_deltas( storage, + ¶ms, &validator, -slash_delta.change(), epoch, - 0, + None, )?; - update_total_deltas(storage, -slash_delta.change(), epoch, 0)?; + update_total_deltas(storage, ¶ms, -slash_delta.change(), epoch, None)?; } // TODO: should we clear some storage here as is done in Quint?? @@ -5362,41 +5296,114 @@ where dest_validator, amount_after_slashing.change(), pipeline_epoch, + None, )?; } // Update deltas update_validator_deltas( storage, + ¶ms, dest_validator, amount_after_slashing.change(), current_epoch, - params.pipeline_len, + None, )?; update_total_deltas( storage, + ¶ms, amount_after_slashing.change(), current_epoch, - params.pipeline_len, + None, )?; Ok(()) } -/// Init PoS genesis wrapper helper that also initializes gov params that are -/// used in PoS with default values. #[cfg(any(test, feature = "testing"))] -pub fn test_init_genesis( - storage: &mut S, - owned: OwnedPosParams, - validators: impl Iterator + Clone, - current_epoch: namada_core::types::storage::Epoch, -) -> storage_api::Result -where - S: StorageRead + StorageWrite, -{ - let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); - gov_params.init_storage(storage)?; - crate::init_genesis(storage, &owned, validators, current_epoch)?; - crate::read_non_pos_owned_params(storage, owned) +/// PoS related utility functions to help set up tests. +pub mod test_utils { + use namada_core::ledger::storage_api; + use namada_core::ledger::storage_api::token::credit_tokens; + use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; + + use super::*; + use crate::parameters::PosParams; + use crate::types::GenesisValidator; + + /// Helper function to intialize storage with PoS data + /// about validators for tests. + pub fn init_genesis_helper( + storage: &mut S, + params: &PosParams, + validators: impl Iterator, + current_epoch: namada_core::types::storage::Epoch, + ) -> storage_api::Result<()> + where + S: StorageRead + StorageWrite, + { + init_genesis(storage, params, current_epoch)?; + for GenesisValidator { + address, + consensus_key, + protocol_key, eth_cold_key, + eth_hot_key, + commission_rate, + max_commission_rate_change, + tokens, + } in validators + { + become_validator(BecomeValidator { + storage, + params, + address: &address, + consensus_key: &consensus_key, + protocol_key: &protocol_key, + eth_cold_key: ð_cold_key, + eth_hot_key: ð_hot_key, + current_epoch, + commission_rate, + max_commission_rate_change, + offset_opt: Some(0), + })?; + // Credit token amount to be bonded to the validator address so it + // can be bonded + let staking_token = staking_token_address(storage); + credit_tokens(storage, &staking_token, &address, tokens)?; + + bond_tokens( + storage, + None, + &address, + tokens, + current_epoch, + Some(0), + )?; + } + // Store the total consensus validator stake to storage + store_total_consensus_stake(storage, current_epoch)?; + + // Copy validator sets and positions + copy_genesis_validator_sets(storage, params, current_epoch)?; + + Ok(()) + } + + /// Init PoS genesis wrapper helper that also initializes gov params that are + /// used in PoS with default values. + pub fn test_init_genesis( + storage: &mut S, + owned: OwnedPosParams, + validators: impl Iterator + Clone, + current_epoch: namada_core::types::storage::Epoch, + ) -> storage_api::Result + where + S: StorageRead + StorageWrite, + { + let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); + gov_params.init_storage(storage)?; + let params = crate::read_non_pos_owned_params(storage, owned)?; + init_genesis_helper(storage, ¶ms, validators, current_epoch)?; + Ok(params) + } } diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index ad0a2cccd9..b75c3f207c 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -64,7 +64,7 @@ use crate::{ read_consensus_validator_set_addresses_with_stake, read_total_stake, read_validator_deltas_value, read_validator_stake, slash, slash_redelegation, slash_validator, slash_validator_redelegation, - staking_token_address, store_total_consensus_stake, test_init_genesis, + staking_token_address, store_total_consensus_stake, test_utils::test_init_genesis, total_bonded_handle, total_deltas_handle, total_unbonded_handle, unbond_handle, unbond_tokens, unjail_validator, update_validator_deltas, update_validator_set, validator_consensus_key_handle, @@ -390,6 +390,7 @@ fn test_bonds_aux(params: OwnedPosParams, validators: Vec) { &validator.address, amount_self_bond, current_epoch, + None, ) .unwrap(); @@ -501,6 +502,7 @@ fn test_bonds_aux(params: OwnedPosParams, validators: Vec) { &validator.address, amount_del, current_epoch, + None, ) .unwrap(); let val_stake_pre = read_validator_stake( @@ -949,6 +951,7 @@ fn test_become_validator_aux( commission_rate: Dec::new(5, 2).expect("Dec creation failed"), max_commission_rate_change: Dec::new(5, 2) .expect("Dec creation failed"), + offset_opt: None, }) .unwrap(); assert!(is_validator(&s, &new_validator).unwrap()); @@ -967,7 +970,7 @@ fn test_become_validator_aux( let staking_token = staking_token_address(&s); let amount = token::Amount::from_uint(100_500_000, 0).unwrap(); credit_tokens(&mut s, &staking_token, &new_validator, amount).unwrap(); - bond_tokens(&mut s, None, &new_validator, amount, current_epoch).unwrap(); + bond_tokens(&mut s, None, &new_validator, amount, current_epoch, None).unwrap(); // Check the bond delta let bond_handle = bond_handle(&new_validator, &new_validator); @@ -1320,10 +1323,11 @@ fn test_validator_sets() { update_validator_deltas( s, + ¶ms, addr, stake.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); @@ -1553,14 +1557,16 @@ fn test_validator_sets() { &val1, -unbond.change(), pipeline_epoch, + None, ) .unwrap(); update_validator_deltas( &mut s, + ¶ms, &val1, -unbond.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); // Epoch 6 @@ -1748,14 +1754,15 @@ fn test_validator_sets() { let bond = token::Amount::from_uint(500_000, 0).unwrap(); let stake6 = stake6 + bond; println!("val6 {val6} new stake {}", stake6.to_string_native()); - update_validator_set(&mut s, ¶ms, &val6, bond.change(), pipeline_epoch) + update_validator_set(&mut s, ¶ms, &val6, bond.change(), pipeline_epoch, None) .unwrap(); update_validator_deltas( &mut s, + ¶ms, &val6, bond.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); let val6_bond_epoch = pipeline_epoch; @@ -2001,10 +2008,11 @@ fn test_validator_sets_swap() { update_validator_deltas( s, + ¶ms, addr, stake.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); @@ -2041,14 +2049,16 @@ fn test_validator_sets_swap() { &val2, bond2.change(), pipeline_epoch, + None, ) .unwrap(); update_validator_deltas( &mut s, + ¶ms, &val2, bond2.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); @@ -2058,14 +2068,16 @@ fn test_validator_sets_swap() { &val3, bond3.change(), pipeline_epoch, + None, ) .unwrap(); update_validator_deltas( &mut s, + ¶ms, &val3, bond3.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); @@ -2090,14 +2102,16 @@ fn test_validator_sets_swap() { &val2, bonds.change(), pipeline_epoch, + None, ) .unwrap(); update_validator_deltas( &mut s, + ¶ms, &val2, bonds.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); @@ -2107,14 +2121,16 @@ fn test_validator_sets_swap() { &val3, bonds.change(), pipeline_epoch, + None, ) .unwrap(); update_validator_deltas( &mut s, + ¶ms, &val3, bonds.change(), epoch, - params.pipeline_len, +None, ) .unwrap(); @@ -4373,6 +4389,7 @@ fn test_simple_redelegation_aux( &src_validator, amount_delegate, current_epoch, + None, ) .unwrap(); @@ -4768,6 +4785,7 @@ fn test_redelegation_with_slashing_aux( &src_validator, amount_delegate, current_epoch, + None, ) .unwrap(); @@ -5163,6 +5181,7 @@ fn test_chain_redelegations_aux(mut validators: Vec) { &src_validator, bond_amount, current_epoch, + None, ) .unwrap(); @@ -5643,6 +5662,7 @@ fn test_overslashing_aux(mut validators: Vec) { &validator, amount_del, current_epoch, + None, ) .unwrap(); diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index a067037232..21be643af6 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -217,7 +217,7 @@ impl StateMachineTest for ConcretePosState { .collect::>() ); let mut s = TestWlStorage::default(); - crate::test_init_genesis( + crate::test_utils::test_init_genesis( &mut s, initial_state.params.owned.clone(), initial_state.genesis_validators.clone().into_iter(), @@ -275,6 +275,7 @@ impl StateMachineTest for ConcretePosState { current_epoch, commission_rate, max_commission_rate_change, + offset_opt: None, }) .unwrap(); @@ -338,6 +339,7 @@ impl StateMachineTest for ConcretePosState { &id.validator, amount, current_epoch, + None, ) .unwrap(); diff --git a/proof_of_stake/src/tests/state_machine_v2.rs b/proof_of_stake/src/tests/state_machine_v2.rs index 02df1b39a0..81f8226ac9 100644 --- a/proof_of_stake/src/tests/state_machine_v2.rs +++ b/proof_of_stake/src/tests/state_machine_v2.rs @@ -1929,7 +1929,7 @@ impl StateMachineTest for ConcretePosState { .collect::>() ); let mut s = TestWlStorage::default(); - crate::init_genesis( + crate::test_utils::init_genesis_helper( &mut s, &initial_state.params, initial_state.genesis_validators.clone().into_iter(), @@ -2003,6 +2003,7 @@ impl StateMachineTest for ConcretePosState { current_epoch, commission_rate, max_commission_rate_change, + offset_opt: None, }) .unwrap(); @@ -2066,6 +2067,7 @@ impl StateMachineTest for ConcretePosState { &id.validator, amount, current_epoch, + None, ) .unwrap(); diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index f9df559c8a..7162f92772 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -118,5 +118,6 @@ wasmtimer = "0.2.0" [dev-dependencies] assert_matches.workspace = true +base58.workspace = true namada_test_utils = {path = "../test_utils"} tempfile.workspace = true diff --git a/sdk/src/args.rs b/sdk/src/args.rs index 1ef3aff0be..b89c6b4209 100644 --- a/sdk/src/args.rs +++ b/sdk/src/args.rs @@ -1638,6 +1638,9 @@ pub struct MaspAddrKeyAdd { pub alias_force: bool, /// Any MASP value pub value: MaspValue, + /// Add a MASP key / address pre-genesis instead + /// of a current chain + pub is_pre_genesis: bool, /// Don't encrypt the keypair pub unsafe_dont_encrypt: bool, } @@ -1649,6 +1652,8 @@ pub struct MaspSpendKeyGen { pub alias: String, /// Whether to force overwrite the alias pub alias_force: bool, + /// Generate spending key pre-genesis instead of a current chain + pub is_pre_genesis: bool, /// Don't encrypt the keypair pub unsafe_dont_encrypt: bool, } @@ -1664,6 +1669,8 @@ pub struct MaspPayAddrGen { pub viewing_key: C::ViewingKey, /// Pin pub pin: bool, + /// Generate an address pre-genesis instead of a current chain + pub is_pre_genesis: bool, } /// Wallet generate key and implicit address arguments @@ -1675,6 +1682,8 @@ pub struct KeyAndAddressGen { pub alias: Option, /// Whether to force overwrite the alias, if provided pub alias_force: bool, + /// Generate a key for pre-genesis, instead of a current chain + pub is_pre_genesis: bool, /// Don't encrypt the keypair pub unsafe_dont_encrypt: bool, /// BIP44 derivation path @@ -1705,6 +1714,8 @@ pub struct KeyFind { pub alias: Option, /// Public key hash to lookup keypair with pub value: Option, + /// Find a key pre-genesis instead of a current chain + pub is_pre_genesis: bool, /// Show secret keys to user pub unsafe_show_secret: bool, } @@ -1716,6 +1727,8 @@ pub struct AddrKeyFind { pub alias: String, /// Show secret keys to user pub unsafe_show_secret: bool, + /// Find shielded address / key pre-genesis instead of a current chain + pub is_pre_genesis: bool, } /// Wallet list shielded keys arguments @@ -1723,15 +1736,27 @@ pub struct AddrKeyFind { pub struct MaspKeysList { /// Don't decrypt spending keys pub decrypt: bool, + /// List shielded keys pre-genesis instead of a current chain + pub is_pre_genesis: bool, /// Show secret keys to user pub unsafe_show_secret: bool, } +/// Wallet list shielded payment addresses arguments +#[derive(Clone, Debug)] +pub struct MaspListPayAddrs { + /// List sheilded payment address pre-genesis instead + /// of a current chain + pub is_pre_genesis: bool, +} + /// Wallet list keys arguments #[derive(Clone, Debug)] pub struct KeyList { /// Don't decrypt keypairs pub decrypt: bool, + /// List keys pre-genesis instead of a current chain + pub is_pre_genesis: bool, /// Show secret keys to user pub unsafe_show_secret: bool, } @@ -1741,6 +1766,8 @@ pub struct KeyList { pub struct KeyExport { /// Key alias pub alias: String, + /// Export key pre-genesis instead of a current chain + pub is_pre_genesis: bool, } /// Wallet address lookup arguments @@ -1750,6 +1777,15 @@ pub struct AddressOrAliasFind { pub alias: Option, /// Address to find pub address: Option
, + /// Lookup address pre-genesis instead of a current chain + pub is_pre_genesis: bool, +} + +/// List wallet address +#[derive(Clone, Debug)] +pub struct AddressList { + /// List addresses pre-genesis instead of current chain + pub is_pre_genesis: bool, } /// Wallet address add arguments @@ -1761,6 +1797,8 @@ pub struct AddressAdd { pub alias_force: bool, /// Address to add pub address: Address, + /// Add an address pre-genesis instead of current chain + pub is_pre_genesis: bool, } /// Bridge pool batch recommendation. diff --git a/sdk/src/error.rs b/sdk/src/error.rs index 9f84195cc2..aa4c2c9842 100644 --- a/sdk/src/error.rs +++ b/sdk/src/error.rs @@ -19,7 +19,7 @@ pub type Result = std::result::Result; /// /// The general mentality should be that this error type should cover all /// possible errors that one may face. -#[derive(Clone, Error, Debug)] +#[derive( Error, Debug)] pub enum Error { /// Errors that are caused by trying to retrieve a pinned transaction #[error("Error in retrieving pinned balance: {0}")] diff --git a/sdk/src/wallet/alias.rs b/sdk/src/wallet/alias.rs index 13d977b852..61ba36d74e 100644 --- a/sdk/src/wallet/alias.rs +++ b/sdk/src/wallet/alias.rs @@ -3,15 +3,17 @@ use std::convert::Infallible; use std::fmt::Display; use std::hash::Hash; +use std::io::Read; use std::str::FromStr; +use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; +use namada_core::types::address::{Address, InternalAddress}; /// Aliases created from raw strings are kept in-memory as given, but their /// `Serialize` and `Display` instance converts them to lowercase. Their /// `PartialEq` instance is case-insensitive. -#[derive(Clone, Debug, Default, Deserialize, PartialOrd, Ord, Eq)] -#[serde(transparent)] +#[derive(Clone, Debug, Default, Eq)] pub struct Alias(String); impl Alias { @@ -29,14 +31,51 @@ impl Alias { pub fn is_empty(&self) -> bool { self.0.is_empty() } + + /// If the alias is reserved for an internal address, + /// return that address + pub fn is_reserved(alias: impl AsRef) -> Option
{ + InternalAddress::try_from_alias(alias.as_ref()).map(Address::Internal) + } +} + +impl BorshSerialize for Alias { + fn serialize( + &self, + writer: &mut W, + ) -> std::io::Result<()> { + BorshSerialize::serialize(&self.normalize(), writer) + } +} + +impl BorshDeserialize for Alias { + fn deserialize(buf: &mut &[u8]) -> std::io::Result { + let raw: String = BorshDeserialize::deserialize(buf)?; + Ok(Self::from(raw)) + } + + fn deserialize_reader(reader: &mut R) -> std::io::Result { + let raw: String = BorshDeserialize::deserialize_reader(reader)?; + Ok(Self::from(raw)) + } } impl Serialize for Alias { fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, + where + S: serde::Serializer, { - self.normalize().serialize(serializer) + Serialize::serialize(&self.normalize(), serializer) + } +} + +impl<'de> Deserialize<'de> for Alias { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw: String = Deserialize::deserialize(deserializer)?; + Ok(Self::from(raw)) } } @@ -46,6 +85,19 @@ impl PartialEq for Alias { } } + +impl PartialOrd for Alias { + fn partial_cmp(&self, other: &Self) -> Option { + self.normalize().partial_cmp(&other.normalize()) + } +} + +impl Ord for Alias { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.normalize().cmp(&other.normalize()) + } +} + impl Hash for Alias { fn hash(&self, state: &mut H) { self.normalize().hash(state); @@ -57,7 +109,7 @@ where T: AsRef, { fn from(raw: T) -> Self { - Self(raw.as_ref().to_owned()) + Self(raw.as_ref().to_lowercase()) } } @@ -83,7 +135,14 @@ impl FromStr for Alias { type Err = Infallible; fn from_str(s: &str) -> Result { - Ok(Self(s.into())) + Ok(Self::from(s)) + } +} + + +impl AsRef for &Alias { + fn as_ref(&self) -> &str { + &self.0 } } diff --git a/sdk/src/wallet/mod.rs b/sdk/src/wallet/mod.rs index 67d5a11f86..8715b02b7c 100644 --- a/sdk/src/wallet/mod.rs +++ b/sdk/src/wallet/mod.rs @@ -346,8 +346,14 @@ impl Wallet { } /// Find the stored address by an alias. - pub fn find_address(&self, alias: impl AsRef) -> Option<&Address> { - self.store.find_address(alias) + pub fn find_address(&self, alias: impl AsRef) -> Option> { + Alias::is_reserved(alias.as_ref()) + .map(std::borrow::Cow::Owned) + .or_else(|| { + self.store + .find_address(alias) + .map(std::borrow::Cow::Borrowed) + }) } /// Find an alias by the address if it's in the wallet. @@ -883,4 +889,10 @@ impl Wallet { .insert_payment_addr::(alias.into(), payment_addr, force_alias) .map(Into::into) } + + /// Extend this wallet from another wallet (typically pre-genesis). + /// Note that this method ignores `store.validator_data` if any. + pub fn extend(&mut self, wallet: Self) { + self.store.extend(wallet.store) + } } diff --git a/sdk/src/wallet/store.rs b/sdk/src/wallet/store.rs index 201cc885a4..ddc5c5e03e 100644 --- a/sdk/src/wallet/store.rs +++ b/sdk/src/wallet/store.rs @@ -1,10 +1,10 @@ //! Wallet Store information -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashSet}; use std::fmt::Display; use std::str::FromStr; -use bimap::BiHashMap; +use bimap::BiBTreeMap; use bip39::Seed; use itertools::Itertools; use masp_primitives::zip32::ExtendedFullViewingKey; @@ -67,26 +67,26 @@ pub struct ValidatorData { #[derive(Serialize, Deserialize, Debug, Default)] pub struct Store { /// Known viewing keys - view_keys: HashMap, + view_keys: BTreeMap, /// Known spending keys - spend_keys: HashMap>, + spend_keys: BTreeMap>, /// Known payment addresses - payment_addrs: HashMap, + payment_addrs: BTreeMap, /// Cryptographic keypairs - keys: HashMap>, + keys: BTreeMap>, /// Namada address book - addresses: BiHashMap, + addresses: BiBTreeMap, /// Known mappings of public key hashes to their aliases in the `keys` /// field. Used for look-up by a public key. - pkhs: HashMap, + pkhs: BTreeMap, /// Special keys if the wallet belongs to a validator pub(crate) validator_data: Option, /// Namada address vp type - address_vp_types: HashMap>, + address_vp_types: BTreeMap>, } /// Grouping of addresses by validity predicate. -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)] pub enum AddressVpType { /// The Token Token, @@ -174,11 +174,11 @@ impl Store { /// Get all known keys by their alias, paired with PKH, if known. pub fn get_keys( &self, - ) -> HashMap< + ) -> BTreeMap< Alias, (&StoredKeypair, Option<&PublicKeyHash>), > { - let mut keys: HashMap< + let mut keys: BTreeMap< Alias, (&StoredKeypair, Option<&PublicKeyHash>), > = self @@ -198,24 +198,24 @@ impl Store { } /// Get all known addresses by their alias. - pub fn get_addresses(&self) -> &BiHashMap { + pub fn get_addresses(&self) -> &BiBTreeMap { &self.addresses } /// Get all known payment addresses by their alias. - pub fn get_payment_addrs(&self) -> &HashMap { + pub fn get_payment_addrs(&self) -> &BTreeMap { &self.payment_addrs } /// Get all known viewing keys by their alias. - pub fn get_viewing_keys(&self) -> &HashMap { + pub fn get_viewing_keys(&self) -> &BTreeMap { &self.view_keys } /// Get all known spending keys by their alias. pub fn get_spending_keys( &self, - ) -> &HashMap> { + ) -> &BTreeMap> { &self.spend_keys } @@ -247,6 +247,10 @@ impl Store { seed_and_derivation_path: Option<(Seed, DerivationPath)>, password: Option>, ) -> Option<(Alias, common::SecretKey)> { + // We cannot generate keys for reserved aliases + if alias.as_ref().and_then(Alias::is_reserved).is_some() { + return None; + } let sk = if let Some((seed, derivation_path)) = seed_and_derivation_path { gen_sk_from_seed_and_derivation_path( @@ -283,6 +287,13 @@ impl Store { password: Option>, force_alias: bool, ) -> (Alias, ExtendedSpendingKey) { + if Alias::is_reserved(&alias).is_some() { + panic!( + "Tried to generated spending key with reserved alias: {}. \ + Action cancelled, no changes persisted.", + alias + ); + } let spendkey = Self::generate_spending_key(); let viewkey = ExtendedFullViewingKey::from(&spendkey.into()).into(); let (spendkey_to_store, _raw_spendkey) = @@ -348,6 +359,12 @@ impl Store { return None; } + // abort if the alias is reserved + if Alias::is_reserved(&alias).is_some() { + println!("The alias {} is reserved", alias); + return None; + } + if alias.is_empty() { println!( "Empty alias given, defaulting to {}.", @@ -395,6 +412,12 @@ impl Store { viewkey: ExtendedViewingKey, force: bool, ) -> Option { + // abort if the alias is reserved + if Alias::is_reserved(&alias).is_some() { + println!("The alias {} is reserved", alias); + return None; + } + if alias.is_empty() { eprintln!("Empty alias given."); return None; @@ -424,6 +447,12 @@ impl Store { viewkey: ExtendedViewingKey, force: bool, ) -> Option { + // abort if the alias is reserved + if Alias::is_reserved(&alias).is_some() { + println!("The alias {} is reserved", alias); + return None; + } + if alias.is_empty() { eprintln!("Empty alias given."); return None; @@ -469,6 +498,12 @@ impl Store { payment_addr: PaymentAddress, force: bool, ) -> Option { + // abort if the alias is reserved + if Alias::is_reserved(&alias).is_some() { + println!("The alias {} is reserved", alias); + return None; + } + if alias.is_empty() { eprintln!("Empty alias given."); return None; @@ -499,6 +534,11 @@ impl Store { key: Option>, pkh: Option, ) { + // abort if the alias is reserved + if Alias::is_reserved(&alias).is_some() { + println!("The alias {} is reserved", alias); + return ; + } key.map(|x| self.keys.insert(alias.clone(), x)); pkh.map(|x| self.pkhs.insert(x, alias.clone())); } @@ -513,6 +553,11 @@ impl Store { address: Address, force: bool, ) -> Option { + // abort if the alias is reserved + if Alias::is_reserved(&alias).is_some() { + println!("The alias {} is reserved", alias); + return None; + } // abort if the address already exists in the wallet if self.addresses.contains_right(&address) && !force { println!( @@ -571,6 +616,28 @@ impl Store { Some(alias) } + /// Extend this store from another store (typically pre-genesis). + /// Note that this method ignores `validator_data` if any. + pub fn extend(&mut self, store: Store) { + let Self { + view_keys, + spend_keys, + payment_addrs, + keys, + addresses, + pkhs, + validator_data: _, + address_vp_types, + } = self; + view_keys.extend(store.view_keys); + spend_keys.extend(store.spend_keys); + payment_addrs.extend(store.payment_addrs); + keys.extend(store.keys); + addresses.extend(store.addresses); + pkhs.extend(store.pkhs); + address_vp_types.extend(store.address_vp_types); + } + /// Extend this store from pre-genesis validator wallet. pub fn extend_from_pre_genesis_validator( &mut self, @@ -750,7 +817,7 @@ impl<'de> Deserialize<'de> for AddressVpType { } } -#[cfg(all(test, feature = "dev"))] +#[cfg(test)] mod test_wallet { use base58::{self, FromBase58}; use bip39::{Language, Mnemonic}; diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 648f7e0b39..0795ca3c39 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -20,8 +20,6 @@ mainnet = [ "namada_core/mainnet", ] std = ["fd-lock"] -# NOTE "dev" features that shouldn't be used in live networks are enabled by default for now -dev = [] ferveo-tpke = [ "namada_core/ferveo-tpke", "namada_sdk/ferveo-tpke", diff --git a/shared/build.rs b/shared/build.rs deleted file mode 100644 index b97290123c..0000000000 --- a/shared/build.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::env; - -fn main() { - // Tell Cargo to build when the `NAMADA_DEV` env var changes - println!("cargo:rerun-if-env-changed=NAMADA_DEV"); - // Enable "dev" feature if `NAMADA_DEV` is trueish - if let Ok(dev) = env::var("NAMADA_DEV") { - if dev.to_ascii_lowercase().trim() == "true" { - println!("cargo:rustc-cfg=feature=\"dev\""); - } - } -} 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 6d07f5a613..01671ea882 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 @@ -647,7 +647,7 @@ mod test_bridge_pool_vp { use namada_core::ledger::gas::TxGasMeter; use namada_core::types::address; use namada_ethereum_bridge::parameters::{ - Contracts, EthereumBridgeConfig, UpgradeableContract, + Contracts, EthereumBridgeParams, UpgradeableContract, }; use super::*; @@ -901,7 +901,7 @@ mod test_bridge_pool_vp { /// Initialize some dummy storage for testing fn setup_storage() -> WlStorage { // a dummy config for testing - let config = EthereumBridgeConfig { + let config = EthereumBridgeParams { erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), diff --git a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs index 9f5f6dd19c..88c1f07044 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs @@ -171,7 +171,7 @@ mod tests { use namada_core::ledger::gas::TxGasMeter; use namada_core::ledger::storage_api::StorageWrite; use namada_ethereum_bridge::parameters::{ - Contracts, EthereumBridgeConfig, UpgradeableContract, + Contracts, EthereumBridgeParams, UpgradeableContract, }; use rand::Rng; @@ -225,7 +225,7 @@ mod tests { .expect("Test failed"); // a dummy config for testing - let config = EthereumBridgeConfig { + let config = EthereumBridgeParams { erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), diff --git a/shared/src/ledger/native_vp/ibc/mod.rs b/shared/src/ledger/native_vp/ibc/mod.rs index b0901f6fec..429643256e 100644 --- a/shared/src/ledger/native_vp/ibc/mod.rs +++ b/shared/src/ledger/native_vp/ibc/mod.rs @@ -389,7 +389,6 @@ mod tests { }; use crate::ledger::parameters::EpochDuration; use crate::ledger::{ibc, pos}; - use crate::proof_of_stake::parameters::PosParams; use crate::proto::{Code, Data, Section, Signature, Tx}; use crate::tendermint::time::Time as TmTime; use crate::tendermint_proto::Protobuf as TmProtobuf; @@ -416,12 +415,12 @@ mod tests { ibc::init_genesis_storage(&mut wl_storage); let gov_params = GovernanceParameters::default(); gov_params.init_storage(&mut wl_storage).unwrap(); - pos::init_genesis_storage( + pos::test_utils::test_init_genesis( &mut wl_storage, - &PosParams::default(), + namada_proof_of_stake::OwnedPosParams::default(), vec![get_dummy_genesis_validator()].into_iter(), Epoch(1), - ); + ).unwrap(); // epoch duration let epoch_duration_key = get_epoch_duration_storage_key(); let epoch_duration = EpochDuration { diff --git a/shared/src/ledger/pos/mod.rs b/shared/src/ledger/pos/mod.rs index d47e5cc884..3987b9b533 100644 --- a/shared/src/ledger/pos/mod.rs +++ b/shared/src/ledger/pos/mod.rs @@ -5,7 +5,6 @@ pub mod vp; use std::convert::TryFrom; pub use namada_core::ledger::storage_api; -use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; use namada_core::types::address; pub use namada_core::types::dec::Dec; pub use namada_core::types::key::common; @@ -15,10 +14,11 @@ pub use namada_proof_of_stake::parameters::{OwnedPosParams, PosParams}; pub use namada_proof_of_stake::pos_queries::*; pub use namada_proof_of_stake::storage::*; pub use namada_proof_of_stake::{staking_token_address, types}; +#[cfg(any(test, feature = "testing"))] +pub use namada_proof_of_stake::test_utils; pub use vp::PosVP; use crate::types::address::{Address, InternalAddress}; -use crate::types::storage::Epoch; /// Address of the PoS account implemented as a native VP pub const ADDRESS: Address = address::POS; @@ -39,24 +39,6 @@ pub fn into_tm_voting_power( i64::try_from(res).expect("Invalid validator voting power (i64)") } -/// Initialize storage in the genesis block. -pub fn init_genesis_storage( - storage: &mut S, - params: &OwnedPosParams, - validators: impl Iterator + Clone, - current_epoch: Epoch, -) where - S: StorageRead + StorageWrite, -{ - namada_proof_of_stake::init_genesis( - storage, - params, - validators, - current_epoch, - ) - .expect("Initialize PoS genesis storage"); -} - /// Alias for a PoS type with the same name with concrete type parameters pub type BondId = namada_proof_of_stake::types::BondId; diff --git a/shared/src/ledger/vp_host_fns.rs b/shared/src/ledger/vp_host_fns.rs index ebe32d4f72..ef8831ae88 100644 --- a/shared/src/ledger/vp_host_fns.rs +++ b/shared/src/ledger/vp_host_fns.rs @@ -321,7 +321,7 @@ pub fn get_tx_index( Ok(*tx_index) } -/// Getting the chain ID. +/// Getting the native token's address. pub fn get_native_token( gas_meter: &mut VpGasMeter, storage: &Storage, diff --git a/tests/src/e2e/eth_bridge_tests.rs b/tests/src/e2e/eth_bridge_tests.rs index d85b344f45..538a5d4ae5 100644 --- a/tests/src/e2e/eth_bridge_tests.rs +++ b/tests/src/e2e/eth_bridge_tests.rs @@ -10,7 +10,7 @@ use expectrl::ControlCode; use namada::eth_bridge::oracle; use namada::eth_bridge::storage::vote_tallies; use namada::ledger::eth_bridge::{ - ContractVersion, Contracts, EthereumBridgeConfig, MinimumConfirmations, + ContractVersion, Contracts, EthereumBridgeParams, MinimumConfirmations, UpgradeableContract, }; use namada::types::address::wnam; @@ -292,7 +292,7 @@ async fn test_bridge_pool_e2e() { let wnam_address = wnam().to_canonical(); let test = setup::network( |mut genesis| { - genesis.ethereum_bridge_params = Some(EthereumBridgeConfig { + genesis.ethereum_bridge_params = Some(EthereumBridgeParams { eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { @@ -483,7 +483,7 @@ async fn test_bridge_pool_e2e() { /// other ERC20 transfers. #[tokio::test] async fn test_wnam_transfer() -> Result<()> { - let ethereum_bridge_params = EthereumBridgeConfig { + let ethereum_bridge_params = EthereumBridgeParams { eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::from(unsafe { // SAFETY: The only way the API contract of `NonZeroU64` can @@ -590,7 +590,7 @@ async fn test_wnam_transfer() -> Result<()> { /// storage, if the Ethereum bridge has been bootstrapped for the Namada chain. #[test] fn test_configure_oracle_from_storage() -> Result<()> { - let ethereum_bridge_params = EthereumBridgeConfig { + let ethereum_bridge_params = EthereumBridgeParams { eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::from(unsafe { // SAFETY: The only way the API contract of `NonZeroU64` can diff --git a/tests/src/e2e/eth_bridge_tests/helpers.rs b/tests/src/e2e/eth_bridge_tests/helpers.rs index 04047a1722..9fbc467443 100644 --- a/tests/src/e2e/eth_bridge_tests/helpers.rs +++ b/tests/src/e2e/eth_bridge_tests/helpers.rs @@ -8,7 +8,7 @@ use eyre::{eyre, Context, Result}; use hyper::client::HttpConnector; use hyper::{Body, Client, Method, Request, StatusCode}; use namada::ledger::eth_bridge::{ - wrapped_erc20s, ContractVersion, Contracts, EthereumBridgeConfig, + wrapped_erc20s, ContractVersion, Contracts, EthereumBridgeParams, MinimumConfirmations, UpgradeableContract, }; use namada::types::address::{wnam, Address}; @@ -80,7 +80,7 @@ impl EventsEndpointClient { /// validator that is exposing an endpoint for submission of fake Ethereum /// events. pub fn setup_single_validator_test() -> Result<(Test, NamadaBgCmd)> { - let ethereum_bridge_params = EthereumBridgeConfig { + let ethereum_bridge_params = EthereumBridgeParams { eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::from(unsafe { // SAFETY: The only way the API contract of `NonZeroU64` can diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index 9a58230072..e98a14735c 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::ffi::OsStr; use std::fmt::Display; -use std::fs::{File, OpenOptions}; +use std::fs::{create_dir_all, File, OpenOptions}; use std::path::{Path, PathBuf}; use std::process::Command; use std::str::FromStr; @@ -18,11 +18,20 @@ use expectrl::stream::log::LogStream; use expectrl::{ControlCode, Eof, WaitStatus}; use eyre::{eyre, Context}; use itertools::{Either, Itertools}; +use namada_sdk::wallet::alias::Alias; use namada::types::chain::ChainId; -use namada_apps::client::utils; -use namada_apps::config::genesis::genesis_config::{self, GenesisConfig}; -use namada_apps::config::{ethereum_bridge, Config}; +use namada_apps::client::utils::{ + self, validator_pre_genesis_dir, validator_pre_genesis_txs_file, +}; +use namada_apps::config::genesis::toml_utils::read_toml; +use namada_apps::config::genesis::{chain, templates}; +use namada_apps::config::{ethereum_bridge, genesis, Config}; use namada_apps::{config, wallet}; +use namada_core::types::key::{RefTo, SchemeType}; +use namada_core::types::string_encoding::StringEncoded; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; +use namada_tx_prelude::token; +use namada_vp_prelude::HashSet; use once_cell::sync::Lazy; use rand::Rng; use serde_json; @@ -54,9 +63,10 @@ pub const ENV_VAR_USE_PREBUILT_BINARIES: &str = /// This file must contain a single validator with alias "validator-0". /// To add more validators, use the [`set_validators`] function in the call to /// setup the [`network`]. -pub const SINGLE_NODE_NET_GENESIS: &str = "genesis/e2e-tests-single-node.toml"; +#[allow(dead_code)] +pub const SINGLE_NODE_NET_GENESIS: &str = "genesis/localnet"; /// An E2E test network. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Network { pub chain_id: ChainId, } @@ -88,6 +98,13 @@ pub fn update_actor_config( .unwrap(); } +/// Configure validator p2p settings to allow duplicat ips +pub fn allow_duplicate_ips(test: &Test, chain_id: &ChainId, who: &Who) { + update_actor_config(test, chain_id, who, |config| { + config.ledger.cometbft.p2p.allow_duplicate_ip = true; + }); +} + /// Configures the Ethereum bridge mode of `who`. This should be done before /// `who` starts running. pub fn set_ethereum_bridge_mode( @@ -112,40 +129,146 @@ pub fn set_ethereum_bridge_mode( /// INVARIANT: Do not call this function more than once on the same config. pub fn set_validators( num: u8, - mut genesis: GenesisConfig, + mut genesis: templates::All, + base_dir: &Path, port_offset: F, -) -> GenesisConfig +) -> templates::All where F: Fn(u8) -> u16, { - let validator_0 = genesis.validator.get_mut("validator-0").unwrap(); - // Clone the first validator before modifying it - let other_validators = validator_0.clone(); - let validator_0_target = validator_0.net_address.clone().unwrap(); - let split: Vec<&str> = validator_0_target.split(':').collect(); - let (net_target_0, net_address_port_0) = - (split[0], split[1].parse::().unwrap()); - for ix in 0..num { - let mut validator = other_validators.clone(); - let mut net_target = net_target_0.to_string(); - // 6 ports for each validator - let first_port = net_address_port_0 + port_offset(ix); - net_target = format!("{}:{}", net_target, first_port); - validator.net_address = Some(net_target.to_string()); - let name = format!("validator-{}", ix); - genesis.validator.insert(name, validator); + // for each validator: + // - generate a balance key + // - assign balance to the key + // - invoke `init-genesis-validator` signed by balance key to generate + // validator pre-genesis wallet signed genesis txs + // - add txs to genesis templates + let wallet_path = base_dir.join("pre-genesis"); + for val in 0..num { + // generate a balance key + let mut wallet = wallet::load(&wallet_path) + .expect("Could not locate pre-genesis wallet used for e2e tests."); + let alias = format!("validator-{}-balance-key", val); + let (alias, sk) = wallet + .gen_key(SchemeType::Ed25519, Some(alias), true, None, None) + .unwrap_or_else(|_| { + panic!("Could not generate new key for validator-{}", val) + }) + .unwrap(); + wallet::save(&wallet).unwrap(); + // assign balance to the key + genesis + .balances + .token + .get_mut(&Alias::from_str("nam").expect("Infallible")) + .expect("NAM balances should exist in pre-genesis wallet already") + .0 + .insert( + StringEncoded::new(sk.ref_to()), + token::DenominatedAmount { + amount: token::Amount::from_uint( + 3000000, + NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }, + ); + // invoke `init-genesis-validator` signed by balance key to generate + // validator pre-genesis wallet signed genesis txs + let validator_alias = format!("validator-{}", val); + let net_addr = format!("127.0.0.1:{}", 27656 + port_offset(val)); + let args = vec![ + "utils", + "init-genesis-validator", + "--source", + &alias, + "--alias", + &validator_alias, + "--net-address", + &net_addr, + "--commission-rate", + "0.05", + "--max-commission-rate-change", + "0.01", + "--transfer-from-source-amount", + "2000000", + "--self-bond-amount", + "100000", + "--unsafe-dont-encrypt", + ]; + let validator_alias = format!("validator-{}", val); + // initialize the validator + let mut init_genesis_validator = run_cmd( + Bin::Client, + args, + Some(5), + &working_dir(), + base_dir, + format!("{}:{}", std::file!(), std::line!()), + ) + .unwrap(); + init_genesis_validator.assert_success(); + // add generated txs to genesis + let pre_genesis_path = + validator_pre_genesis_dir(base_dir, &validator_alias); + let pre_genesis_tx_path = + validator_pre_genesis_txs_file(&pre_genesis_path); + let pre_genesis_txs = + read_toml(&pre_genesis_tx_path, "transactions.toml").unwrap(); + genesis.transactions.merge(pre_genesis_txs); + // move validators generated files to their own base dir + let validator_base_dir = base_dir + .join(utils::NET_ACCOUNTS_DIR) + .join(&validator_alias); + let src_path = validator_pre_genesis_dir(base_dir, &validator_alias); + let dest_path = + validator_pre_genesis_dir(&validator_base_dir, &validator_alias); + println!( + "{} for {validator_alias} from {} to {}.", + "Copying pre-genesis validator-wallet".yellow(), + src_path.to_string_lossy(), + dest_path.to_string_lossy(), + ); + fs::create_dir_all(&dest_path).unwrap(); + fs::rename(src_path, dest_path).unwrap(); } genesis } +/// Remove self-bonds from default templates. They will be +/// regenerated later. +fn remove_self_bonds(genesis: &mut templates::All) { + let bonds = genesis.transactions.bond.take().unwrap(); + genesis.transactions.bond = Some( + bonds + .into_iter() + .filter(|bond| { + if let genesis::transactions::AliasOrPk::Alias(alias) = + &bond.data.source + { + *alias != bond.data.validator + } else { + true + } + }) + .collect(), + ); +} + /// Setup a network with a single genesis validator node. pub fn single_node_net() -> Result { - network(|genesis| genesis, None) + network( + |genesis, base_dir: &_| set_validators(1, genesis, base_dir, |_| 0u16), + None, + ) } /// Setup a configurable network. pub fn network( - mut update_genesis: impl FnMut(GenesisConfig) -> GenesisConfig, + mut update_genesis: impl FnMut( + templates::All, + &Path, + ) -> templates::All, consensus_timeout_commit: Option<&'static str>, ) -> Result { INIT.call_once(|| { @@ -156,41 +279,86 @@ pub fn network( let working_dir = working_dir(); let test_dir = TestDir::new(); - // Open the source genesis file - let mut genesis = genesis_config::open_genesis_config( - working_dir.join(SINGLE_NODE_NET_GENESIS), - )?; - - genesis.parameters.vp_whitelist = + // Open the source genesis file templates + let templates_dir = working_dir.join("genesis").join("localnet"); + println!( + "{} {}.", + "Loading genesis templates from".yellow(), + templates_dir.to_string_lossy() + ); + let mut templates = + genesis::templates::All::read_toml_files(&templates_dir) + .unwrap_or_else(|_| { + panic!( + "Missing genesis templates files at {}", + templates_dir.to_string_lossy() + ) + }); + // clear existing validator txs from genesis + templates.transactions.validator_account = None; + // remove self-bonds from genesis + remove_self_bonds(&mut templates); + + // Update the templates as needed + templates.parameters.parameters.vp_whitelist = Some(get_all_wasms_hashes(&working_dir, Some("vp_"))); - genesis.parameters.tx_whitelist = + templates.parameters.parameters.tx_whitelist = Some(get_all_wasms_hashes(&working_dir, Some("tx_"))); + // Copy the main wallet from templates dir into the base dir. + { + let base_dir = test_dir.path(); + let src_path = + wallet::wallet_file(&templates_dir.join("src").join("pre-genesis")); + let dest_dir = base_dir.join("pre-genesis"); + let dest_path = wallet::wallet_file(&dest_dir); + println!( + "{} from {} to {}.", + "Copying main pre-genesis wallet into a default non-validator \ + base dir" + .yellow(), + src_path.to_string_lossy(), + dest_path.to_string_lossy(), + ); + fs::create_dir_all(&dest_dir)?; + fs::copy(&src_path, &dest_path)?; + } // Run the provided function on it - let genesis = update_genesis(genesis); + let templates = update_genesis(templates, test_dir.path()); + + // Write the updated genesis templates to the test dir + let updated_templates_dir = test_dir.path().join("templates"); + create_dir_all(&updated_templates_dir)?; + println!( + "{} {}.", + "Writing updated genesis templates to".yellow(), + updated_templates_dir.to_string_lossy() + ); + templates.write_toml_files(&updated_templates_dir)?; - // Run `init-network` to generate the finalized genesis config, keys and - // addresses and update WASM checksums - let genesis_file = test_dir.path().join("e2e-test-genesis-src.toml"); - genesis_config::write_genesis_config(&genesis, &genesis_file); - let genesis_path = genesis_file.to_string_lossy(); + // Run `init-network` on the updated templates to generate the finalized + // genesis config and addresses and update WASM checksums + let templates_path = updated_templates_dir.to_string_lossy().into_owned(); + println!("{}", "Finalizing network from genesis templates.".yellow()); let checksums_path = working_dir .join("wasm/checksums.json") .to_string_lossy() .into_owned(); + let genesis_dir = test_dir.path().join("genesis"); + let archive_dir = genesis_dir.to_string_lossy().to_string(); let mut args = vec![ "utils", "init-network", - "--unsafe-dont-encrypt", - "--genesis-path", - &genesis_path, + "--templates-path", + &templates_path, "--chain-prefix", "e2e-test", - "--localhost", - "--dont-archive", - "--allow-duplicate-ip", "--wasm-checksums-path", &checksums_path, + "--genesis-time", + "2023-08-30T00:00:00Z", + "--archive-dir", + &archive_dir, ]; if let Some(consensus_timeout_commit) = consensus_timeout_commit { args.push("--consensus-timeout-commit"); @@ -201,7 +369,7 @@ pub fn network( args, Some(5), &working_dir, - &test_dir, + &genesis_dir, format!("{}:{}", std::file!(), std::line!()), )?; @@ -211,38 +379,121 @@ pub fn network( let chain_id_raw = matched.trim().split_once("Derived chain ID: ").unwrap().1; let chain_id = ChainId::from_str(chain_id_raw.trim())?; - println!("'init-network' output: {}", unread); + println!("'init-network' unread output: {}", unread); let net = Network { chain_id }; + init_network.assert_success(); - // release lock on wallet by dropping the - // child process drop(init_network); - // Move the "others" accounts wallet in the main base dir, so that we can - // use them with `Who::NonValidator` - let chain_dir = test_dir.path().join(net.chain_id.as_str()); - std::fs::rename( - wallet::wallet_file( - chain_dir - .join(utils::NET_ACCOUNTS_DIR) - .join(utils::NET_OTHER_ACCOUNTS_DIR), - ), - wallet::wallet_file(chain_dir.clone()), + // Host the network archive to make it available for `join-network` commands + // TODO: allow to unpack from a file instead of having to host it + let network_archive_server = file_serve::Server::new(&archive_dir); + let network_archive_addr = network_archive_server.addr().to_owned(); + std::thread::spawn(move || { + network_archive_server.serve().unwrap(); + }); + std::env::set_var( + namada_apps::client::utils::ENV_VAR_NETWORK_CONFIGS_SERVER, + format!("http://{network_archive_addr}"), + ); + + let genesis_new = chain::Finalized::read_toml_files( + &genesis_dir.join(net.chain_id.as_str()), ) .unwrap(); + let validator_aliases = genesis_new + .transactions + .validator_account + .as_ref() + .map(|txs| { + txs.iter().fold(HashSet::new(), |mut acc, finalized| { + acc.insert(finalized.tx.alias.to_string()); + acc + }) + }) + .unwrap_or_default(); - copy_wasm_to_chain_dir( - &working_dir, - &chain_dir, - &net.chain_id, - genesis.validator.keys(), - ); + // Setup a dir for every validator and non-validator using their + // pre-genesis wallets + for alias in &validator_aliases { + let validator_base_dir = + test_dir.path().join(utils::NET_ACCOUNTS_DIR).join(alias); + + // Copy the main wallet from templates dir into validator's base dir. + { + let dest_dir = validator_base_dir.join("pre-genesis"); + let dest_path = wallet::wallet_file(&dest_dir); + let base_dir = test_dir.path(); + let src_dir = base_dir.join("pre-genesis"); + let src_path = wallet::wallet_file(&src_dir); + println!( + "{} for {alias} from {} to {}.", + "Copying main pre-genesis wallet".yellow(), + src_path.to_string_lossy(), + dest_path.to_string_lossy(), + ); + fs::create_dir_all(&dest_dir)?; + fs::copy(&src_path, &dest_path)?; + } + println!("{} {}.", "Joining network with ".yellow(), alias); + let validator_base_dir = + test_dir.path().join(utils::NET_ACCOUNTS_DIR).join(alias); + let mut join_network = run_cmd( + Bin::Client, + [ + "utils", + "join-network", + "--chain-id", + net.chain_id.as_str(), + "--genesis-validator", + alias, + "--dont-prefetch-wasm", + ], + Some(5), + &working_dir, + &validator_base_dir, + format!("{}:{}", std::file!(), std::line!()), + )?; + join_network.exp_string("Successfully configured for chain")?; + join_network.assert_success(); + copy_wasm_to_chain_dir( + &working_dir, + &validator_base_dir, + &net.chain_id, + ); + } + + // Setup a dir for a non-validator using the pre-genesis wallet + { + let base_dir = test_dir.path(); + println!( + "{}.", + "Joining network with a default non-validator node".yellow() + ); + let mut join_network = run_cmd( + Bin::Client, + [ + "utils", + "join-network", + "--chain-id", + net.chain_id.as_str(), + "--dont-prefetch-wasm", + ], + Some(5), + &working_dir, + base_dir, + format!("{}:{}", std::file!(), std::line!()), + )?; + join_network.exp_string("Successfully configured for chain")?; + join_network.assert_success(); + } + + copy_wasm_to_chain_dir(&working_dir, test_dir.path(), &net.chain_id); Ok(Test { working_dir, test_dir, net, - genesis, async_runtime: Default::default(), }) } @@ -265,7 +516,6 @@ pub struct Test { /// Namada cmds pub test_dir: TestDir, pub net: Network, - pub genesis: GenesisConfig, pub async_runtime: LazyAsyncRuntime, } @@ -455,10 +705,8 @@ impl Test { Who::Validator(index) => self .test_dir .path() - .join(self.net.chain_id.as_str()) .join(utils::NET_ACCOUNTS_DIR) - .join(format!("validator-{}", index)) - .join(config::DEFAULT_BASE_DIR), + .join(format!("validator-{}", index)), } } @@ -883,11 +1131,11 @@ pub mod constants { } /// Copy WASM files from the `wasm` directory to every node's chain dir. -pub fn copy_wasm_to_chain_dir<'a>( +pub fn copy_wasm_to_chain_dir( working_dir: &Path, - chain_dir: &Path, + test_dir: &Path, chain_id: &ChainId, - genesis_validator_keys: impl Iterator, + // genesis_validator_keys: impl Iterator, ) { // Copy the built WASM files from "wasm" directory in the root of the // project. @@ -911,6 +1159,7 @@ pub fn copy_wasm_to_chain_dir<'a>( built_wasm_dir.to_string_lossy() ); } + let chain_dir = test_dir.join(chain_id.as_str()); let target_wasm_dir = chain_dir.join(config::DEFAULT_WASM_DIR); for file in &wasm_files { std::fs::copy( @@ -919,29 +1168,6 @@ pub fn copy_wasm_to_chain_dir<'a>( ) .unwrap(); } - - // Copy the built WASM files from "wasm" directory to each validator dir - for validator_name in genesis_validator_keys { - let target_wasm_dir = chain_dir - .join(utils::NET_ACCOUNTS_DIR) - .join(validator_name) - .join(config::DEFAULT_BASE_DIR) - .join(chain_id.as_str()) - .join(config::DEFAULT_WASM_DIR); - for file in &wasm_files { - let src = working_dir.join("wasm").join(file); - let dst = target_wasm_dir.join(file); - std::fs::copy(&src, &dst) - .wrap_err_with(|| { - format!( - "copying {} to {}", - &src.to_string_lossy(), - &dst.to_string_lossy(), - ) - }) - .unwrap(); - } - } } pub fn get_all_wasms_hashes( diff --git a/tests/src/integration/setup.rs b/tests/src/integration/setup.rs index 9bbd00eef9..6e8d953bcb 100644 --- a/tests/src/integration/setup.rs +++ b/tests/src/integration/setup.rs @@ -30,60 +30,65 @@ const ENV_VAR_KEEP_TEMP: &str = "NAMADA_INT_KEEP_TEMP"; /// Setup a network with a single genesis validator node. pub fn setup() -> Result<(MockNode, MockServicesController)> { - initialize_genesis(|genesis| genesis) + initialize_genesis() } /// Setup folders with genesis, configs, wasm, etc. -pub fn initialize_genesis( - mut update_genesis: impl FnMut(GenesisConfig) -> GenesisConfig, -) -> Result<(MockNode, MockServicesController)> { +pub fn initialize_genesis() -> Result<(MockNode, MockServicesController)> { let working_dir = std::fs::canonicalize("..").unwrap(); let keep_temp = match std::env::var(ENV_VAR_KEEP_TEMP) { Ok(val) => val.to_ascii_lowercase() != "false", _ => false, }; let test_dir = TestDir::new(); + let template_dir = working_dir.join(SINGLE_NODE_NET_GENESIS); - // Open the source genesis file - let mut genesis = genesis_config::open_genesis_config( - working_dir.join(SINGLE_NODE_NET_GENESIS), - )?; - - genesis.parameters.vp_whitelist = - Some(get_all_wasms_hashes(&working_dir, Some("vp_"))); - genesis.parameters.tx_whitelist = - Some(get_all_wasms_hashes(&working_dir, Some("tx_"))); + // Copy genesis files to test directory. + let templates = templates::All::read_toml_files(&template_dir) + .expect("Missing genesis files"); + let genesis_path = test_dir.path().join("int-test-genesis-src"); + std::fs::create_dir(&genesis_path) + .expect("Could not create test chain directory."); + templates + .write_toml_files(&genesis_path) + .expect("Could not write genesis files into test chain directory."); - // Run the provided function on it - let genesis = update_genesis(genesis); + // Finalize the genesis config to derive the chain ID + let templates = load_and_validate(&template_dir) + .expect("Missing or invalid genesis files"); + let genesis_time = Default::default(); + let chain_id_prefix = ChainIdPrefix::from_str("integration-test").unwrap(); + let genesis = config::genesis::chain::finalize( + templates, + chain_id_prefix.clone(), + genesis_time, + Timeout::from_str("30s").unwrap(), + ); + let chain_id = &genesis.metadata.chain_id; // Run `init-network` to generate the finalized genesis config, keys and // addresses and update WASM checksums - let genesis_path = test_dir.path().join("e2e-test-genesis-src.toml"); - genesis_config::write_genesis_config(&genesis, &genesis_path); let wasm_checksums_path = working_dir.join("wasm/checksums.json"); - + let global_args = args::Global { + chain_id: Some(chain_id.clone()), + base_dir: test_dir.path().to_path_buf(), + wasm_dir: Some(test_dir.path().join(chain_id.as_str()).join("wasm")), + }; // setup genesis file namada_apps::client::utils::init_network( - args::Global { - chain_id: None, - base_dir: test_dir.path().to_path_buf(), - wasm_dir: None, - }, + global_args.clone(), args::InitNetwork { - genesis_path, + templates_path: genesis_path, wasm_checksums_path, - chain_id_prefix: ChainIdPrefix::from_str("integration-test") - .unwrap(), - unsafe_dont_encrypt: true, - consensus_timeout_commit: Timeout::from_str("1s").unwrap(), - localhost: true, - allow_duplicate_ip: true, + chain_id_prefix, + consensus_timeout_commit: Timeout::from_str("30s").unwrap(), dont_archive: true, archive_dir: None, + genesis_time, }, ); + finalize_wallet(&template_dir, &global_args, genesis); let auto_drive_services = { // NB: for now, the only condition that // dictates whether mock services should @@ -100,42 +105,60 @@ pub fn initialize_genesis( auto_drive_services, enable_eth_oracle, }; - create_node(test_dir, &genesis, keep_temp, services_cfg) + create_node(test_dir, global_args, keep_temp, services_cfg) +} + +/// Add the address from the finalized genesis to the wallet. +/// Additionally add the validator keys to the wallet. +fn finalize_wallet( + template_dir: &Path, + global_args: &args::Global, + genesis: Finalized, +) { + let pre_genesis_path = template_dir.join("src").join(PRE_GENESIS_DIR); + let validator_alias_and_dir = + Some(("validator-0", pre_genesis_path.join("validator-0"))); + // Pre-load the validator pre-genesis wallet and its keys to validate that + // everything is in place + let validator_alias_and_pre_genesis_wallet = + validator_alias_and_dir.map(|(validator_alias, pre_genesis_dir)| { + ( + Alias::from(validator_alias), + pre_genesis::load(&pre_genesis_dir).unwrap_or_else(|err| { + panic!("Error loading validator pre-genesis wallet {err}") + }), + ) + }); + + // Try to load pre-genesis wallet + let pre_genesis_wallet = namada_apps::wallet::load(&pre_genesis_path); + let chain_dir = global_args + .base_dir + .join(global_args.chain_id.as_ref().unwrap().as_str()); + // Derive wallet from genesis + let wallet = genesis.derive_wallet( + &chain_dir, + pre_genesis_wallet, + validator_alias_and_pre_genesis_wallet, + ); + namada_apps::wallet::save(&wallet).unwrap(); } /// Create a mock ledger node. fn create_node( - base_dir: TestDir, - genesis: &GenesisConfig, + test_dir: TestDir, + global_args: args::Global, keep_temp: bool, services_cfg: MockServicesCfg, ) -> Result<(MockNode, MockServicesController)> { // look up the chain id from the global file. - let chain_id = if let toml::Value::String(chain_id) = - toml::from_str::( - &std::fs::read_to_string( - base_dir.path().join("global-config.toml"), - ) - .unwrap(), - ) - .unwrap() - .get("default_chain_id") - .unwrap() - { - chain_id.to_string() - } else { - return Err(eyre!("Could not read chain id from global-config.toml")); - }; + let chain_id = global_args.chain_id.unwrap_or_default(); - // the directory holding compiled wasm - let wasm_dir = base_dir.path().join(Path::new(&chain_id)).join("wasm"); // copy compiled wasms into the wasm directory - let chain_id = ChainId::from_str(&chain_id).unwrap(); copy_wasm_to_chain_dir( &std::fs::canonicalize("..").unwrap(), - &base_dir.path().join(Path::new(&chain_id.to_string())), + &global_args.base_dir, &chain_id, - genesis.validator.keys(), ); // instantiate and initialize the ledger node. @@ -148,19 +171,20 @@ fn create_node( let node = MockNode { shell: Arc::new(Mutex::new(Shell::new( config::Ledger::new( - base_dir.path(), + global_args.base_dir, chain_id.clone(), TendermintMode::Validator, ), - wasm_dir, + global_args + .wasm_dir + .expect("Wasm path not provided to integration test setup."), shell_handlers.tx_broadcaster, shell_handlers.eth_oracle_channels, None, 50 * 1024 * 1024, // 50 kiB 50 * 1024 * 1024, // 50 kiB - nam(), ))), - test_dir: ManuallyDrop::new(base_dir), + test_dir: ManuallyDrop::new(test_dir), keep_temp, services: Arc::new(services), results: Arc::new(Mutex::new(vec![])), diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index 3de46fc84b..ba69c6e380 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -18,7 +18,7 @@ mod test_bridge_pool_vp { use namada_apps::wallet::defaults::{albert_address, bertha_address}; use namada_apps::wasm_loader; use namada_sdk::eth_bridge::{ - wrapped_erc20s, Contracts, Erc20WhitelistEntry, EthereumBridgeConfig, + wrapped_erc20s, Contracts, Erc20WhitelistEntry, EthereumBridgeParams, UpgradeableContract, }; @@ -65,7 +65,7 @@ mod test_bridge_pool_vp { tx, ..Default::default() }; - let config = EthereumBridgeConfig { + let config = EthereumBridgeParams { erc20_whitelist: vec![Erc20WhitelistEntry { token_address: wnam(), token_cap: Amount::from_u64(TOKEN_CAP).native_denominated(), diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index 233d999861..1a3236ca40 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -96,7 +96,7 @@ //! - add rewards use namada::proof_of_stake::parameters::{OwnedPosParams, PosParams}; -use namada::proof_of_stake::test_init_genesis as init_genesis; +use namada::proof_of_stake::test_utils::test_init_genesis as init_genesis; use namada::proof_of_stake::types::GenesisValidator; use namada::types::storage::Epoch; diff --git a/tests/src/vm_host_env/ibc.rs b/tests/src/vm_host_env/ibc.rs index 583c58ada3..88f88820a5 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -71,7 +71,7 @@ use namada::ledger::storage::mockdb::MockDB; use namada::ledger::storage::traits::Sha256Hasher; use namada::ledger::tx_env::TxEnv; use namada::ledger::{ibc, pos}; -use namada::proof_of_stake::parameters::PosParams; +use namada::proof_of_stake::OwnedPosParams; use namada::proto::Tx; use namada::tendermint::time::Time as TmTime; use namada::tendermint_proto::Protobuf as TmProtobuf; @@ -214,12 +214,12 @@ pub fn init_storage() -> (Address, Address) { ibc::init_genesis_storage(&mut env.wl_storage); let gov_params = GovernanceParameters::default(); gov_params.init_storage(&mut env.wl_storage).unwrap(); - pos::init_genesis_storage( + pos::test_utils::test_init_genesis( &mut env.wl_storage, - &PosParams::default(), + OwnedPosParams::default(), vec![get_dummy_genesis_validator()].into_iter(), Epoch(1), - ); + ).unwrap(); // store wasm code let key = Key::wasm_code(&code_hash); env.wl_storage.storage.write(&key, code.clone()).unwrap(); diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 1f365f1b9c..2582287af2 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -25,7 +25,7 @@ impl Ctx { amount: token::Amount, ) -> TxResult { let current_epoch = self.get_block_epoch()?; - bond_tokens(self, source, validator, amount, current_epoch) + bond_tokens(self, source, validator, amount, current_epoch, None) } /// Unbond self-bonded tokens from a validator when `source` is `None` @@ -132,6 +132,7 @@ impl Ctx { current_epoch, commission_rate, max_commission_rate_change, + offset_opt: None, })?; Ok(validator_address) From f5312a57d5ba776468f5126a4fc121231491ba36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 26 Oct 2023 17:42:52 +0200 Subject: [PATCH 02/47] fixup! [feat] Re-implementing new genesis flow in v0.24 --- apps/src/lib/cli/client.rs | 24 +++++++------------- apps/src/lib/cli/context.rs | 12 +++++----- apps/src/lib/client/utils.rs | 12 +++++----- apps/src/lib/config/genesis/transactions.rs | 24 ++++++++++---------- apps/src/lib/node/ledger/shell/init_chain.rs | 15 ++++-------- apps/src/lib/wallet/store.rs | 24 +++----------------- 6 files changed, 40 insertions(+), 71 deletions(-) diff --git a/apps/src/lib/cli/client.rs b/apps/src/lib/cli/client.rs index a465d585ce..c16c885c34 100644 --- a/apps/src/lib/cli/client.rs +++ b/apps/src/lib/cli/client.rs @@ -114,19 +114,11 @@ impl CliApi { }); client.wait_until_node_is_synced(io).await?; let args = args.to_sdk(&mut ctx); - let namada = NamadaImpl::native_new( - &client, - &mut ctx.wallet, - &mut ctx.shielded, - io, - ctx.native_token, - ); - tx::submit_init_validator( - &namada, - &mut ctx.config, - args, - ) - .await?; + let mut config = + ctx.borrow_chain_or_exit().config.clone(); + let namada = ctx.to_sdk(&client, io); + tx::submit_init_validator(&namada, &mut config, args) + .await?; } Sub::TxInitProposal(TxInitProposal(mut args)) => { let client = client.unwrap_or_else(|| { @@ -538,9 +530,9 @@ impl CliApi { let namada = ctx.to_sdk(&client, io); rpc::epoch_sleep(&namada, args).await; } - Utils::ValidateGenesisTemplates( - ValidateGenesisTemplates(args ) - ) => utils::validate_genesis_templates(global_args, args), + Utils::ValidateGenesisTemplates(ValidateGenesisTemplates( + args, + )) => utils::validate_genesis_templates(global_args, args), Utils::SignGenesisTx(SignGenesisTx(args)) => { utils::sign_genesis_tx(global_args, args) } diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index 494400de16..58ef80975d 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -20,11 +20,11 @@ use namada_sdk::wallet::Wallet; use namada_sdk::{Namada, NamadaImpl}; use super::args; -use crate::config::genesis; -use crate::config::{self, Config}; +use crate::cli::utils; +use crate::config::global::GlobalConfig; +use crate::config::{self, genesis, Config}; use crate::wallet::CliWalletUtils; use crate::{wallet, wasm_loader}; -use crate::cli::utils; /// Env. var to set chain ID const ENV_VAR_CHAIN_ID: &str = "NAMADA_CHAIN_ID"; @@ -173,9 +173,9 @@ impl Context { client: &'a C, io: &'a IO, ) -> impl Namada - where - C: namada::ledger::queries::Client + Sync, - IO: Io, + where + C: namada::ledger::queries::Client + Sync, + IO: Io, { let chain_ctx = self.borrow_mut_chain_or_exit(); NamadaImpl::native_new( diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 22340cacfc..59e697042e 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -9,10 +9,11 @@ use borsh_ext::BorshSerializeExt; use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; -use namada::types::address; use namada::types::chain::ChainId; use namada::types::dec::Dec; use namada::types::key::*; +use namada::types::uint::Uint; +use namada::types::{address, token}; use namada::vm::validate_untrusted_wasm; use namada_sdk::wallet::{alias, Wallet}; use prost::bytes::Bytes; @@ -23,11 +24,10 @@ use sha2::{Digest, Sha256}; use crate::cli::context::ENV_VAR_WASM_DIR; use crate::cli::{self, args}; -use crate::config::genesis::genesis_config::{ - self, GenesisConfig, HexString, ValidatorPreGenesisConfig, -}; use crate::config::global::GlobalConfig; -use crate::config::{self, get_default_namada_folder, Config, TendermintMode}; +use crate::config::{ + self, genesis, get_default_namada_folder, Config, TendermintMode, +}; use crate::facade::tendermint::node::Id as TendermintNodeId; use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::node::ledger::tendermint_node; @@ -413,7 +413,7 @@ pub fn init_network( } else { (Dec::from(1) / tm_votes_per_token).ceil().abs() }, - NATIVE_MAX_DECIMAL_PLACES, + token::NATIVE_MAX_DECIMAL_PLACES, ) .unwrap(); eprintln!( diff --git a/apps/src/lib/config/genesis/transactions.rs b/apps/src/lib/config/genesis/transactions.rs index 021a895c40..c66accb918 100644 --- a/apps/src/lib/config/genesis/transactions.rs +++ b/apps/src/lib/config/genesis/transactions.rs @@ -18,7 +18,7 @@ use namada::types::time::{DateTimeUtc, MIN_UTC}; use namada::types::token; use namada::types::token::{DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES}; use namada_sdk::wallet::pre_genesis::ValidatorWallet; -use namada_sdk::wallet::{FindKeyError, Wallet, WalletUtils}; +use namada_sdk::wallet::{FindKeyError, Wallet}; use serde::{Deserialize, Serialize}; use super::templates::{ @@ -28,7 +28,7 @@ use crate::config::genesis::templates::{ TemplateValidation, Tokens, Unvalidated, Validated, }; use crate::config::genesis::HexString; -use crate::wallet::Alias; +use crate::wallet::{Alias, CliWalletUtils}; pub const PRE_GENESIS_TX_TIMESTAMP: DateTimeUtc = MIN_UTC; @@ -45,9 +45,9 @@ pub struct GenesisValidatorData { /// Panics if given `txs.validator_accounts` is not empty, because validator /// transactions must be signed with a validator wallet (see /// `init-genesis-validator` command). -pub fn sign_txs( +pub fn sign_txs( txs: UnsignedTransactions, - wallet: &mut Wallet, + wallet: &mut Wallet, ) -> Transactions { let UnsignedTransactions { established_account, @@ -110,7 +110,7 @@ pub fn parse_unsigned( } /// Create signed [`Transactions`] for a genesis validator. -pub fn init_validator( +pub fn init_validator( GenesisValidatorData { source_key, alias, @@ -120,7 +120,7 @@ pub fn init_validator( transfer_from_source_amount, self_bond_amount, }: GenesisValidatorData, - source_wallet: &mut Wallet, + source_wallet: &mut Wallet, validator_wallet: &ValidatorWallet, ) -> Transactions { let unsigned_validator_account_tx = UnsignedValidatorAccountTx { @@ -198,9 +198,9 @@ pub fn init_validator( } } -pub fn sign_established_account_tx( +pub fn sign_established_account_tx( unsigned_tx: UnsignedEstablishedAccountTx, - wallet: &mut Wallet, + wallet: &mut Wallet, ) -> SignedEstablishedAccountTx { let key = unsigned_tx.public_key.as_ref().map(|pk| { let secret = wallet @@ -303,9 +303,9 @@ pub fn sign_validator_account_tx( } } -pub fn sign_transfer_tx( +pub fn sign_transfer_tx( unsigned_tx: TransferTx, - source_wallet: &mut Wallet, + source_wallet: &mut Wallet, ) -> SignedTransferTx { let source_key = source_wallet .find_key_by_pk(&unsigned_tx.source, None) @@ -320,9 +320,9 @@ pub fn sign_self_bond_tx( unsigned_tx.sign(&validator_wallet.account_key) } -pub fn sign_delegation_bond_tx( +pub fn sign_delegation_bond_tx( unsigned_tx: BondTx, - wallet: &mut Wallet, + wallet: &mut Wallet, established_accounts: &Option>>, ) -> SignedBondTx { let alias = &unsigned_tx.source; diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 68027e1548..caa4234263 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -2,13 +2,11 @@ use std::collections::HashMap; use std::hash::Hash; -use namada::ledger::parameters::{Parameters}; +use namada::ledger::parameters::Parameters; use namada::ledger::pos::OwnedPosParams; use namada::ledger::storage::traits::StorageHasher; use namada::ledger::storage::{DBIter, DB}; -use namada::ledger::storage_api::token::{ - credit_tokens, write_denom, -}; +use namada::ledger::storage_api::token::{credit_tokens, write_denom}; use namada::ledger::storage_api::{ResultExt, StorageRead, StorageWrite}; use namada::ledger::{ibc, pos}; use namada::proof_of_stake::BecomeValidator; @@ -18,6 +16,7 @@ use namada::types::storage::KeySeg; use namada::types::time::{DateTimeUtc, TimeZone, Utc}; use namada::vm::validate_untrusted_wasm; use namada_sdk::eth_bridge::EthBridgeStatus; +use namada_sdk::proof_of_stake::PosParams; use super::*; use crate::config::genesis::chain::{ @@ -289,12 +288,8 @@ where let state_key = key_prefix .push(&(token::CONVERSION_KEY_PREFIX.to_owned())) .unwrap(); - let conv_bytes = self - .wl_storage - .storage - .conversion_state - .serialize_to_vec() - .unwrap(); + let conv_bytes = + self.wl_storage.storage.conversion_state.serialize_to_vec(); self.wl_storage.write_bytes(&state_key, conv_bytes).unwrap(); } diff --git a/apps/src/lib/wallet/store.rs b/apps/src/lib/wallet/store.rs index dffaf48a98..78515d5b4a 100644 --- a/apps/src/lib/wallet/store.rs +++ b/apps/src/lib/wallet/store.rs @@ -7,8 +7,9 @@ use namada::types::address::Address; use namada::types::key::*; use namada::types::transaction::EllipticCurve; use namada_sdk::wallet::store::AddressVpType; -use namada_sdk::wallet::StoredKeypair; -use namada_sdk::wallet::{gen_sk_rng, LoadStoreError, Store, ValidatorKeys}; +use namada_sdk::wallet::{ + gen_sk_rng, LoadStoreError, Store, StoredKeypair, ValidatorKeys, +}; use crate::wallet::CliWalletUtils; @@ -36,25 +37,6 @@ pub fn load(store_dir: &Path) -> Result { Ok(wallet.into()) } -fn new() -> Store { - let mut store = Store::default(); - // Pre-load the default keys without encryption - let no_password = None; - for (alias, keypair) in super::defaults::keys() { - let pkh: PublicKeyHash = (&keypair.ref_to()).into(); - store.insert_keypair::( - alias, - StoredKeypair::new(keypair, no_password.clone()).0, - pkh, - true, - ); - } - for (alias, addr) in super::defaults::addresses() { - store.insert_address::(alias, addr, true); - } - store -} - /// Generate keypair for signing protocol txs and for the DKG /// A protocol keypair may be optionally provided /// From 741e9ea70975e29d36c5d8dab11925a79f7bb0e8 Mon Sep 17 00:00:00 2001 From: satan Date: Fri, 27 Oct 2023 16:55:45 +0200 Subject: [PATCH 03/47] [fix]: Compiling now. Tests need fixing --- Cargo.lock | 11 + apps/Cargo.toml | 1 + apps/src/lib/bench_utils.rs | 27 +- apps/src/lib/cli.rs | 2 +- apps/src/lib/cli/client.rs | 16 +- apps/src/lib/cli/context.rs | 62 +- apps/src/lib/cli/wallet.rs | 33 +- apps/src/lib/client/rpc.rs | 4 +- apps/src/lib/client/utils.rs | 11 +- apps/src/lib/config/genesis.rs | 2 +- apps/src/lib/config/genesis/chain.rs | 36 +- apps/src/lib/config/genesis/templates.rs | 2 + apps/src/lib/config/genesis/transactions.rs | 15 +- .../lib/node/ledger/shell/finalize_block.rs | 2 +- apps/src/lib/node/ledger/shell/init_chain.rs | 16 +- apps/src/lib/node/ledger/storage/mod.rs | 21 +- apps/src/lib/wallet/mod.rs | 4 +- apps/src/lib/wallet/store.rs | 11 +- core/src/ledger/storage/masp_conversions.rs | 27 +- core/src/types/address.rs | 14 +- core/src/types/time.rs | 2 +- core/src/types/token.rs | 13 +- genesis/localnet/tokens.toml | 44 +- genesis/starter/tokens.toml | 8 +- proof_of_stake/src/lib.rs | 60 +- proof_of_stake/src/tests.rs | 47 +- sdk/src/error.rs | 2 +- sdk/src/wallet/alias.rs | 14 +- sdk/src/wallet/mod.rs | 5 +- sdk/src/wallet/store.rs | 2 +- shared/src/ledger/native_vp/ibc/mod.rs | 3 +- shared/src/ledger/pos/mod.rs | 2 +- tests/Cargo.toml | 2 + tests/src/e2e/helpers.rs | 29 +- tests/src/e2e/ibc_tests.rs | 104 +- tests/src/e2e/ledger_tests.rs | 1154 +++++------------ tests/src/e2e/setup.rs | 11 +- tests/src/integration/setup.rs | 23 +- tests/src/vm_host_env/ibc.rs | 3 +- wasm/Cargo.lock | 10 + wasm/checksums.json | 42 +- wasm_for_tests/wasm_source/Cargo.lock | 10 + 42 files changed, 830 insertions(+), 1077 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 725192ba5c..9c150d5178 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1305,6 +1305,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "copy_dir" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3" +dependencies = [ + "walkdir", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -4049,6 +4058,7 @@ dependencies = [ "num_cpus", "once_cell", "orion", + "pretty_assertions", "proptest", "prost", "prost-types", @@ -4298,6 +4308,7 @@ dependencies = [ "clap", "color-eyre", "concat-idents", + "copy_dir", "data-encoding", "derivative", "escargot", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 9c6783a5c8..272aef9679 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -111,6 +111,7 @@ num-rational.workspace = true num-traits.workspace = true once_cell.workspace = true orion.workspace = true +pretty_assertions.workspace = true prost-types.workspace = true prost.workspace = true rand_core.workspace = true diff --git a/apps/src/lib/bench_utils.rs b/apps/src/lib/bench_utils.rs index 5f58a93a58..ef2791ac7a 100644 --- a/apps/src/lib/bench_utils.rs +++ b/apps/src/lib/bench_utils.rs @@ -682,25 +682,26 @@ impl Default for BenchShieldedCtx { fn default() -> Self { let mut shell = BenchShell::default(); - let mut ctx = Context::new::(crate::cli::args::Global { + let ctx = Context::new::(crate::cli::args::Global { chain_id: None, base_dir: shell.tempdir.as_ref().canonicalize().unwrap(), wasm_dir: Some(WASM_DIR.into()), }) .unwrap(); + let mut chain_ctx = ctx.take_chain_or_exit(); // Generate spending key for Albert and Bertha - ctx.wallet.gen_spending_key( + chain_ctx.wallet.gen_spending_key( ALBERT_SPENDING_KEY.to_string(), None, true, ); - ctx.wallet.gen_spending_key( + chain_ctx.wallet.gen_spending_key( BERTHA_SPENDING_KEY.to_string(), None, true, ); - crate::wallet::save(&ctx.wallet).unwrap(); + crate::wallet::save(&chain_ctx.wallet).unwrap(); // Generate payment addresses for both Albert and Bertha for (alias, viewing_alias) in [ @@ -710,19 +711,21 @@ impl Default for BenchShieldedCtx { .map(|(p, s)| (p.to_owned(), s.to_owned())) { let viewing_key: FromContext = FromContext::new( - ctx.wallet + chain_ctx + .wallet .find_viewing_key(viewing_alias) .unwrap() .to_string(), ); - let viewing_key = - ExtendedFullViewingKey::from(ctx.get_cached(&viewing_key)) - .fvk - .vk; + let viewing_key = ExtendedFullViewingKey::from( + chain_ctx.get_cached(&viewing_key), + ) + .fvk + .vk; let (div, _g_d) = namada_sdk::masp::find_valid_diversifier(&mut OsRng); let payment_addr = viewing_key.to_payment_address(div).unwrap(); - let _ = ctx + let _ = chain_ctx .wallet .insert_payment_addr( alias, @@ -732,7 +735,7 @@ impl Default for BenchShieldedCtx { .unwrap(); } - crate::wallet::save(&ctx.wallet).unwrap(); + crate::wallet::save(&chain_ctx.wallet).unwrap(); namada::ledger::storage::update_allowed_conversions( &mut shell.wl_storage, ) @@ -741,7 +744,7 @@ impl Default for BenchShieldedCtx { Self { shielded: ShieldedContext::default(), shell, - wallet: ctx.wallet, + wallet: chain_ctx.wallet, } } } diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index c7ddefef68..b9b4d15780 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -4964,7 +4964,7 @@ pub mod args { let query = self.query.to_sdk(ctx); let chain_ctx = ctx.borrow_chain_or_exit(); GenIbcShieldedTransafer:: { - query: self.query.to_sdk(ctx), + query, output_folder: self.output_folder, target: chain_ctx.get(&self.target), token: chain_ctx.get(&self.token), diff --git a/apps/src/lib/cli/client.rs b/apps/src/lib/cli/client.rs index c16c885c34..9a88626561 100644 --- a/apps/src/lib/cli/client.rs +++ b/apps/src/lib/cli/client.rs @@ -114,9 +114,19 @@ impl CliApi { }); client.wait_until_node_is_synced(io).await?; let args = args.to_sdk(&mut ctx); - let mut config = - ctx.borrow_chain_or_exit().config.clone(); - let namada = ctx.to_sdk(&client, io); + let cli::context::ChainContext { + mut wallet, + mut config, + mut shielded, + native_token, + } = ctx.take_chain_or_exit(); + let namada = NamadaImpl::native_new( + &client, + &mut wallet, + &mut shielded, + io, + native_token, + ); tx::submit_init_validator(&namada, &mut config, args) .await?; } diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index 58ef80975d..3b8d942040 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -241,20 +241,7 @@ impl ChainContext { /// /// Note that in "dev" build, this may be the root `wasm` dir. pub fn wasm_dir(&self) -> PathBuf { - let wasm_dir = - self.config.ledger.chain_dir().join(&self.config.wasm_dir); - - // In dev-mode with dev chain (the default), load wasm directly from the - // root wasm dir instead of the chain dir - #[cfg(feature = "dev")] - let wasm_dir = - if self.global_config.default_chain_id == ChainId::default() { - "wasm".into() - } else { - wasm_dir - }; - - wasm_dir + self.config.ledger.chain_dir().join(&self.config.wasm_dir) } /// Read the given WASM file from the WASM directory or an absolute path. @@ -309,10 +296,6 @@ impl FromContext { phantom: PhantomData, } } - - pub fn into_raw(self) -> String { - self.raw - } } impl FromContext { @@ -360,8 +343,8 @@ impl FromContext where T: ArgFromContext, { - /// Parse and/or look-up the value from the context. - fn arg_from_ctx(&self, ctx: &Context) -> Result { + /// Parse and/or look-up the value from the chain context. + fn arg_from_ctx(&self, ctx: &ChainContext) -> Result { T::arg_from_ctx(ctx, &self.raw) } } @@ -370,32 +353,32 @@ impl FromContext where T: ArgFromMutContext, { - /// Parse and/or look-up the value from the mutable context. - fn arg_from_mut_ctx(&self, ctx: &mut Context) -> Result { + /// Parse and/or look-up the value from the mutable chain context. + fn arg_from_mut_ctx(&self, ctx: &mut ChainContext) -> Result { T::arg_from_mut_ctx(ctx, &self.raw) } } -/// CLI argument that found via the [`Context`]. +/// CLI argument that found via the [`ChainContext`]. pub trait ArgFromContext: Sized { fn arg_from_ctx( - ctx: &Context, + ctx: &ChainContext, raw: impl AsRef, ) -> Result; } -/// CLI argument that found via the [`Context`] and cached (as in case of an -/// encrypted keypair that has been decrypted), hence using mutable context. +/// CLI argument that found via the [`ChainContext`] and cached (as in case of +/// an encrypted keypair that has been decrypted), hence using mutable context. pub trait ArgFromMutContext: Sized { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result; } impl ArgFromContext for Address { fn arg_from_ctx( - ctx: &Context, + ctx: &ChainContext, raw: impl AsRef, ) -> Result { struct Skip; @@ -429,14 +412,19 @@ impl ArgFromContext for Address { .ok_or(Skip) }) // Or it can be an alias that may be found in the wallet - .or_else(|_| ctx.wallet.find_address(raw).cloned().ok_or(Skip)) + .or_else(|_| { + ctx.wallet + .find_address(raw) + .map(|x| x.into_owned()) + .ok_or(Skip) + }) .map_err(|_| format!("Unknown address {raw}")) } } impl ArgFromMutContext for common::SecretKey { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -452,7 +440,7 @@ impl ArgFromMutContext for common::SecretKey { impl ArgFromMutContext for common::PublicKey { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -477,7 +465,7 @@ impl ArgFromMutContext for common::PublicKey { impl ArgFromMutContext for ExtendedSpendingKey { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -493,7 +481,7 @@ impl ArgFromMutContext for ExtendedSpendingKey { impl ArgFromMutContext for ExtendedViewingKey { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -510,7 +498,7 @@ impl ArgFromMutContext for ExtendedViewingKey { impl ArgFromContext for PaymentAddress { fn arg_from_ctx( - ctx: &Context, + ctx: &ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -527,7 +515,7 @@ impl ArgFromContext for PaymentAddress { impl ArgFromMutContext for TransferSource { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -543,7 +531,7 @@ impl ArgFromMutContext for TransferSource { impl ArgFromContext for TransferTarget { fn arg_from_ctx( - ctx: &Context, + ctx: &ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -558,7 +546,7 @@ impl ArgFromContext for TransferTarget { impl ArgFromMutContext for BalanceOwner { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); diff --git a/apps/src/lib/cli/wallet.rs b/apps/src/lib/cli/wallet.rs index 6d949ac462..3b43e09b74 100644 --- a/apps/src/lib/cli/wallet.rs +++ b/apps/src/lib/cli/wallet.rs @@ -16,7 +16,6 @@ use namada_sdk::wallet::{ WalletStorage, }; use namada_sdk::{display, display_line, edisplay_line}; -use rand::RngCore; use rand_core::OsRng; use crate::cli; @@ -24,7 +23,9 @@ use crate::cli::api::CliApi; use crate::cli::args::CliToSdk; use crate::cli::{args, cmds, Context}; use crate::client::utils::PRE_GENESIS_DIR; -use crate::wallet::{self, read_and_confirm_encryption_password, CliWalletUtils}; +use crate::wallet::{ + self, read_and_confirm_encryption_password, CliWalletUtils, +}; impl CliApi { pub fn handle_wallet_command( @@ -35,27 +36,35 @@ impl CliApi { match cmd { cmds::NamadaWallet::Key(sub) => match sub { cmds::WalletKey::Restore(cmds::KeyRestore(args)) => { - key_and_address_restore(ctx, io, args) + key_and_address_restore( + &mut ctx.borrow_mut_chain_or_exit().wallet, + io, + args, + ) } cmds::WalletKey::Gen(cmds::KeyGen(args)) => { - key_and_address_gen(ctx, io, &mut OsRng, args) + key_and_address_gen(ctx, io, args) } cmds::WalletKey::Find(cmds::KeyFind(args)) => { key_find(ctx, io, args) } cmds::WalletKey::List(cmds::KeyList(args)) => { - key_list( ctx, io, args) + key_list(ctx, io, args) } cmds::WalletKey::Export(cmds::Export(args)) => { - key_export( ctx, io, args) + key_export(ctx, io, args) } }, cmds::NamadaWallet::Address(sub) => match sub { cmds::WalletAddress::Gen(cmds::AddressGen(args)) => { - key_and_address_gen(ctx, io, &mut OsRng, args) + key_and_address_gen(ctx, io, args) } cmds::WalletAddress::Restore(cmds::AddressRestore(args)) => { - key_and_address_restore(&mut ctx.borrow_mut_chain_or_exit().wallet, io, args) + key_and_address_restore( + &mut ctx.borrow_mut_chain_or_exit().wallet, + io, + args, + ) } cmds::WalletAddress::Find(cmds::AddressOrAliasFind(args)) => { address_or_alias_find(ctx, io, args) @@ -73,7 +82,11 @@ impl CliApi { } cmds::WalletMasp::GenPayAddr(cmds::MaspGenPayAddr(args)) => { let args = args.to_sdk(&mut ctx); - payment_address_gen(&mut ctx.borrow_mut_chain_or_exit().wallet, io, args) + payment_address_gen( + &mut ctx.borrow_mut_chain_or_exit().wallet, + io, + args, + ) } cmds::WalletMasp::AddAddrKey(cmds::MaspAddAddrKey(args)) => { address_key_add(ctx, io, args) @@ -396,7 +409,7 @@ fn key_and_address_restore( /// Generate a new keypair and derive implicit address from it and store them in /// the wallet. -fn key_and_address_gen( +fn key_and_address_gen( ctx: Context, io: &impl Io, args::KeyAndAddressGen { diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index e6ddbff2e9..0231e3731d 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -410,9 +410,9 @@ pub async fn query_pinned_balance<'a>( .collect(); let _ = context.shielded_mut().await.load().await; // Print the token balances by payment address - let pinned_error = Err(Error::from(PinnedBalanceError::InvalidViewingKey)); for owner in owners { - let mut balance = pinned_error.clone(); + let mut balance = + Err(Error::from(PinnedBalanceError::InvalidViewingKey)); // Find the viewing key that can recognize payments the current payment // address for vk in &viewing_keys { diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 59e697042e..cb17d4bc62 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::env; use std::fs::{self, File, OpenOptions}; use std::io::Write; @@ -12,18 +11,16 @@ use flate2::Compression; use namada::types::chain::ChainId; use namada::types::dec::Dec; use namada::types::key::*; +use namada::types::token; use namada::types::uint::Uint; -use namada::types::{address, token}; use namada::vm::validate_untrusted_wasm; use namada_sdk::wallet::{alias, Wallet}; use prost::bytes::Bytes; -use rand::prelude::ThreadRng; -use rand::thread_rng; use serde_json::json; use sha2::{Digest, Sha256}; +use crate::cli::args; use crate::cli::context::ENV_VAR_WASM_DIR; -use crate::cli::{self, args}; use crate::config::global::GlobalConfig; use crate::config::{ self, genesis, get_default_namada_folder, Config, TendermintMode, @@ -31,9 +28,7 @@ use crate::config::{ use crate::facade::tendermint::node::Id as TendermintNodeId; use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::node::ledger::tendermint_node; -use crate::wallet::{ - pre_genesis, read_and_confirm_encryption_password, CliWalletUtils, -}; +use crate::wallet::{pre_genesis, CliWalletUtils}; use crate::wasm_loader; pub const NET_ACCOUNTS_DIR: &str = "setup"; diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 08a861e423..659617d99a 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -273,11 +273,11 @@ pub fn make_dev_genesis(num_validators: u64) -> Finalized { use namada::core::types::string_encoding::StringEncoded; use namada::ledger::eth_bridge::{Contracts, UpgradeableContract}; use namada::proto::{standalone_signature, SerializeWithBorsh}; - use namada_sdk::wallet::alias::Alias; use namada::types::address::wnam; use namada::types::chain::ChainIdPrefix; use namada::types::ethereum_events::EthAddress; use namada::types::token::NATIVE_MAX_DECIMAL_PLACES; + use namada_sdk::wallet::alias::Alias; use crate::config::genesis::chain::finalize; use crate::wallet::defaults; diff --git a/apps/src/lib/config/genesis/chain.rs b/apps/src/lib/config/genesis/chain.rs index 0d064789c8..69dd23f658 100644 --- a/apps/src/lib/config/genesis/chain.rs +++ b/apps/src/lib/config/genesis/chain.rs @@ -3,15 +3,16 @@ use std::path::Path; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; +use borsh_ext::BorshSerializeExt; use namada::ledger::parameters::EpochDuration; -use namada_sdk::wallet::store::AddressVpType; -use namada_sdk::wallet::{pre_genesis, Wallet}; use namada::types::address::{masp, Address, EstablishedAddressGen}; use namada::types::chain::{ChainId, ChainIdPrefix}; use namada::types::dec::Dec; use namada::types::hash::Hash; use namada::types::time::{DateTimeUtc, DurationNanos, Rfc3339String}; use namada::types::token::Amount; +use namada_sdk::wallet::store::AddressVpType; +use namada_sdk::wallet::{pre_genesis, Wallet}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -338,18 +339,21 @@ impl Finalized { } = self.parameters.pos_params.clone(); namada::proof_of_stake::parameters::PosParams { - max_validator_slots, - pipeline_len, - unbonding_len, - tm_votes_per_token, - block_proposer_reward, - block_vote_reward, - max_inflation_rate, - target_staked_ratio, - duplicate_vote_min_slash_rate, - light_client_attack_min_slash_rate, - cubic_slashing_window_length, - validator_stake_threshold, + owned: namada::proof_of_stake::parameters::OwnedPosParams { + max_validator_slots, + pipeline_len, + unbonding_len, + tm_votes_per_token, + block_proposer_reward, + block_vote_reward, + max_inflation_rate, + target_staked_ratio, + duplicate_vote_min_slash_rate, + light_client_attack_min_slash_rate, + cubic_slashing_window_length, + validator_stake_threshold, + }, + max_proposal_period: self.parameters.gov_params.max_proposal_period, } } @@ -458,7 +462,7 @@ pub fn finalize( address_gen: None, }, }; - let genesis_bytes = genesis_to_gen_address.try_to_vec().unwrap(); + let genesis_bytes = genesis_to_gen_address.serialize_to_vec(); let mut addr_gen = established_address_gen(&genesis_bytes); // Generate addresses @@ -488,7 +492,7 @@ pub fn finalize( parameters, transactions, }; - let to_finalize_bytes = to_finalize.try_to_vec().unwrap(); + let to_finalize_bytes = to_finalize.serialize_to_vec(); let chain_id = ChainId::from_genesis(chain_id_prefix, to_finalize_bytes); // Construct the `Finalized` chain diff --git a/apps/src/lib/config/genesis/templates.rs b/apps/src/lib/config/genesis/templates.rs index df01fc2fdc..15defbd17f 100644 --- a/apps/src/lib/config/genesis/templates.rs +++ b/apps/src/lib/config/genesis/templates.rs @@ -210,8 +210,10 @@ pub struct Tokens { )] pub struct TokenConfig { pub denom: Denomination, + pub parameters: token::Parameters } + #[derive( Clone, Debug, diff --git a/apps/src/lib/config/genesis/transactions.rs b/apps/src/lib/config/genesis/transactions.rs index c66accb918..8595db5f18 100644 --- a/apps/src/lib/config/genesis/transactions.rs +++ b/apps/src/lib/config/genesis/transactions.rs @@ -6,6 +6,7 @@ use std::net::SocketAddr; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; +use borsh_ext::BorshSerializeExt; use namada::core::types::storage; use namada::core::types::string_encoding::StringEncoded; use namada::proto::{ @@ -674,10 +675,10 @@ impl TransferTx { /// The signable data. This does not include the phantom data. fn data_to_sign(&self) -> Vec { [ - self.token.try_to_vec().unwrap(), - self.source.try_to_vec().unwrap(), - self.target.try_to_vec().unwrap(), - self.amount.try_to_vec().unwrap(), + self.token.serialize_to_vec(), + self.source.serialize_to_vec(), + self.target.serialize_to_vec(), + self.amount.serialize_to_vec(), ] .concat() } @@ -766,9 +767,9 @@ impl BondTx { /// The signable data. This does not include the phantom data. fn data_to_sign(&self) -> Vec { [ - self.source.try_to_vec().unwrap(), - self.validator.try_to_vec().unwrap(), - self.amount.try_to_vec().unwrap(), + self.source.serialize_to_vec(), + self.validator.serialize_to_vec(), + self.amount.serialize_to_vec(), ] .concat() } diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 3f6fbcd398..2202f7a157 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -3094,7 +3094,7 @@ mod test_finalize_block { .unwrap(); // Self-unbond - let self_unbond_1_amount = token::Amount::native_whole(154_654); + let self_unbond_1_amount = token::Amount::native_whole(54_654); namada_proof_of_stake::unbond_tokens( &mut shell.wl_storage, None, diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index caa4234263..2b36a4e7a8 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -3,11 +3,10 @@ use std::collections::HashMap; use std::hash::Hash; use namada::ledger::parameters::Parameters; -use namada::ledger::pos::OwnedPosParams; use namada::ledger::storage::traits::StorageHasher; use namada::ledger::storage::{DBIter, DB}; use namada::ledger::storage_api::token::{credit_tokens, write_denom}; -use namada::ledger::storage_api::{ResultExt, StorageRead, StorageWrite}; +use namada::ledger::storage_api::StorageWrite; use namada::ledger::{ibc, pos}; use namada::proof_of_stake::BecomeValidator; use namada::types::hash::Hash as CodeHash; @@ -269,10 +268,11 @@ where let FinalizedTokenConfig { address, - config: TokenConfig { denom }, + config: TokenConfig { denom, parameters }, } = token; // associate a token with its denomination. write_denom(&mut self.wl_storage, address, *denom).unwrap(); + parameters.init_storage(address, &mut self.wl_storage); // add token addresses to the masp reward conversions lookup table. let alias = alias.to_string(); if masp_rewards.contains_key(&alias.as_str()) { @@ -304,6 +304,7 @@ where .get(token_alias) .expect("Token with configured balance not found in genesis.") .address; + let mut total_token_balance = token::Amount::zero(); for (owner_pk, balance) in balances { let owner = Address::from(&owner_pk.raw); storage_api::account::set_public_key_at( @@ -326,7 +327,16 @@ where balance.amount, ) .expect("Couldn't credit initial balance"); + total_token_balance += balance.amount; } + // Write the total amount of tokens for the ratio + self.wl_storage + .write( + &token::minted_balance_key(token_address), + total_token_balance, + ) + .unwrap(); + } } diff --git a/apps/src/lib/node/ledger/storage/mod.rs b/apps/src/lib/node/ledger/storage/mod.rs index 659f561cce..329d0a7bd8 100644 --- a/apps/src/lib/node/ledger/storage/mod.rs +++ b/apps/src/lib/node/ledger/storage/mod.rs @@ -62,7 +62,7 @@ mod tests { use namada::types::chain::ChainId; use namada::types::hash::Hash; use namada::types::storage::{BlockHash, BlockHeight, Key}; - use namada::types::{address, storage}; + use namada::types::{address, storage, token}; use proptest::collection::vec; use proptest::prelude::*; use proptest::test_runner::Config; @@ -144,6 +144,25 @@ mod tests { storage.block.pred_epochs.new_epoch(BlockHeight(100)); // make wl_storage to update conversion for a new epoch let mut wl_storage = WlStorage::new(WriteLog::default(), storage); + let token_params = token::Parameters { + max_reward_rate: Default::default(), + kd_gain_nom: Default::default(), + kp_gain_nom: Default::default(), + locked_ratio_target: Default::default(), + }; + // Insert a map assigning random addresses to each token alias. + // Needed for storage but not for this test. + for (token, _) in address::tokens() { + let addr = address::gen_deterministic_established_address(token); + token_params.init_storage(&addr, &mut wl_storage); + wl_storage.write(&token::minted_balance_key(&addr), token::Amount::zero()).unwrap(); + wl_storage.storage.conversion_state.tokens.insert( + token.to_string(), + addr, + ); + } + token_params.init_storage(&wl_storage.storage.native_token.clone(), &mut wl_storage); + wl_storage.write(&token::minted_balance_key(&wl_storage.storage.native_token.clone()), token::Amount::zero()).unwrap(); update_allowed_conversions(&mut wl_storage) .expect("update conversions failed"); wl_storage.commit_block().expect("commit failed"); diff --git a/apps/src/lib/wallet/mod.rs b/apps/src/lib/wallet/mod.rs index 779a137ccd..4cf55df158 100644 --- a/apps/src/lib/wallet/mod.rs +++ b/apps/src/lib/wallet/mod.rs @@ -4,7 +4,6 @@ mod store; use std::io::{self, Write}; use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::{env, fs}; use namada::bip39::{Language, Mnemonic}; @@ -13,8 +12,7 @@ pub use namada_sdk::wallet::alias::Alias; use namada_sdk::wallet::fs::FsWalletStorage; use namada_sdk::wallet::store::Store; use namada_sdk::wallet::{ - ConfirmationResponse, FindKeyError, GenRestoreKeyError, - Wallet, WalletIo, + ConfirmationResponse, FindKeyError, GenRestoreKeyError, Wallet, WalletIo, }; pub use namada_sdk::wallet::{ValidatorData, ValidatorKeys}; use rand_core::OsRng; diff --git a/apps/src/lib/wallet/store.rs b/apps/src/lib/wallet/store.rs index 78515d5b4a..b358c886b1 100644 --- a/apps/src/lib/wallet/store.rs +++ b/apps/src/lib/wallet/store.rs @@ -1,15 +1,10 @@ use std::path::{Path, PathBuf}; -use std::str::FromStr; use ark_std::rand::prelude::*; use ark_std::rand::SeedableRng; -use namada::types::address::Address; use namada::types::key::*; use namada::types::transaction::EllipticCurve; -use namada_sdk::wallet::store::AddressVpType; -use namada_sdk::wallet::{ - gen_sk_rng, LoadStoreError, Store, StoredKeypair, ValidatorKeys, -}; +use namada_sdk::wallet::{gen_sk_rng, LoadStoreError, Store, ValidatorKeys}; use crate::wallet::CliWalletUtils; @@ -74,7 +69,7 @@ mod test_wallet { #[test] fn test_toml_roundtrip_ed25519() { - let mut store = new(); + let mut store = Store::default(); let validator_keys = gen_validator_keys(None, None, SchemeType::Ed25519); store.add_validator_data( @@ -87,7 +82,7 @@ mod test_wallet { #[test] fn test_toml_roundtrip_secp256k1() { - let mut store = new(); + let mut store = Store::default(); let validator_keys = gen_validator_keys(None, None, SchemeType::Secp256k1); store.add_validator_data( diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 6b8d542d53..5cbbca570a 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -217,15 +217,24 @@ where let key_prefix: storage::Key = masp_addr.to_db_key().into(); let tokens = address::tokens(); - let mut masp_reward_keys: Vec<_> = tokens.into_keys() - .map(|k| wl_storage - .storage - .conversion_state - .tokens - .get(k) - .unwrap_or_else(|| panic!("Could not find token alias {} in MASP conversion state.", k)) - .clone() - ).collect(); + let mut masp_reward_keys: Vec<_> = tokens + .into_keys() + .map(|k| { + wl_storage + .storage + .conversion_state + .tokens + .get(k) + .unwrap_or_else(|| { + panic!( + "Could not find token alias {} in MASP conversion \ + state.", + k + ) + }) + .clone() + }) + .collect(); // Put the native rewards first because other inflation computations depend // on it let native_token = wl_storage.storage.native_token.clone(); diff --git a/core/src/types/address.rs b/core/src/types/address.rs index 3493fc6d8f..c9a9396187 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -639,13 +639,13 @@ pub const fn wnam() -> EthAddress { /// informal currency codes and number of decimal places. pub fn tokens() -> HashMap<&'static str, Denomination> { vec![ - ("NAM", 6.into()), - ("BTC", 8.into()), - ("ETH", 18.into()), - ("DOT", 10.into()), - ("Schnitzel", 6.into()), - ("Apfel", 6.into()), - ("Kartoffel", 6.into()), + ("nam", 6.into()), + ("btc", 8.into()), + ("eth", 18.into()), + ("dot", 10.into()), + ("schnitzel", 6.into()), + ("apfel", 6.into()), + ("kartoffel", 6.into()), ] .into_iter() .collect() diff --git a/core/src/types/time.rs b/core/src/types/time.rs index 54622e15a6..948bf6373f 100644 --- a/core/src/types/time.rs +++ b/core/src/types/time.rs @@ -10,7 +10,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use chrono::ParseError; pub use chrono::{DateTime, Duration, TimeZone, Utc}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; /// Check if the given `duration` has passed since the given `start. pub fn duration_passed( diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 73a78b5bd9..343fc3e3e5 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1026,6 +1026,18 @@ impl Parameters { kp_gain_nom, locked_ratio_target: locked_target, } = self; + wl_storage + .write( + &masp_last_inflation_key(&address), + Amount::zero(), + ) + .expect("last inflation key for the given asset must be initialized"); + wl_storage + .write( + &masp_last_locked_ratio_key(&address), + Dec::zero(), + ) + .expect("last locked ratio key for the given asset must be initialized"); wl_storage .write(&masp_max_reward_rate_key(address), max_rate) .expect("max reward rate for the given asset must be initialized"); @@ -1482,5 +1494,4 @@ pub mod testing { ) -> impl Strategy { (1..=max).prop_map(|val| Amount::from_uint(val, 0).unwrap()) } - } diff --git a/genesis/localnet/tokens.toml b/genesis/localnet/tokens.toml index 3079fab700..db1da7df9d 100644 --- a/genesis/localnet/tokens.toml +++ b/genesis/localnet/tokens.toml @@ -3,20 +3,62 @@ [token.NAM] denom = 6 +[token.NAM.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" + [token.BTC] denom = 8 +[token.BTC.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" + [token.ETH] denom = 18 +[token.ETH.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" + [token.DOT] denom = 10 +[token.DOT.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" + [token.Schnitzel] denom = 6 +[token.Schnitzel.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" + [token.Apfel] denom = 6 +[token.Apfel.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" + [token.Kartoffel] -denom = 6 \ No newline at end of file +denom = 6 + +[token.Kartoffel.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" \ No newline at end of file diff --git a/genesis/starter/tokens.toml b/genesis/starter/tokens.toml index 612a6901ff..d0f9d44d44 100644 --- a/genesis/starter/tokens.toml +++ b/genesis/starter/tokens.toml @@ -2,4 +2,10 @@ [token.NAM] vp = "vp_token" -denom = 6 \ No newline at end of file +denom = 6 + +[token.NAM.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" \ No newline at end of file diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 618b20a373..72b58150a5 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -60,11 +60,11 @@ use types::{ BondsAndUnbondsDetail, BondsAndUnbondsDetails, CommissionRates, ConsensusValidator, ConsensusValidatorSet, ConsensusValidatorSets, DelegatorRedelegatedBonded, DelegatorRedelegatedUnbonded, - EagerRedelegatedBondsMap, EpochedSlashes, - IncomingRedelegations, OutgoingRedelegations, Position, - RedelegatedBondsOrUnbonds, RedelegatedTokens, ReverseOrdTokenAmount, - RewardsAccumulator, RewardsProducts, Slash, SlashType, SlashedAmount, - Slashes, TotalConsensusStakes, TotalDeltas, TotalRedelegatedBonded, + EagerRedelegatedBondsMap, EpochedSlashes, IncomingRedelegations, + OutgoingRedelegations, Position, RedelegatedBondsOrUnbonds, + RedelegatedTokens, ReverseOrdTokenAmount, RewardsAccumulator, + RewardsProducts, Slash, SlashType, SlashedAmount, Slashes, + TotalConsensusStakes, TotalDeltas, TotalRedelegatedBonded, TotalRedelegatedUnbonded, UnbondDetails, Unbonds, ValidatorAddresses, ValidatorConsensusKeys, ValidatorDeltas, ValidatorEthColdKeys, ValidatorEthHotKeys, ValidatorPositionAddresses, ValidatorProtocolKeys, @@ -327,7 +327,12 @@ where // Copy the genesis validator sets up to the pipeline epoch for epoch in (current_epoch.next()).iter_range(params.pipeline_len) { - copy_validator_sets_and_positions(storage, ¶ms, current_epoch, epoch)?; + copy_validator_sets_and_positions( + storage, + ¶ms, + current_epoch, + epoch, + )?; store_total_consensus_stake(storage, epoch)?; } Ok(()) @@ -816,12 +821,7 @@ where // Initialize or update the bond at the pipeline offset bond_handle.add(storage, amount, current_epoch, offset)?; - total_bonded_handle.add( - storage, - amount, - current_epoch, - offset, - )?; + total_bonded_handle.add(storage, amount, current_epoch, offset)?; if tracing::level_enabled!(tracing::Level::DEBUG) { let bonds = find_bonds(storage, source, validator)?; @@ -843,7 +843,7 @@ where ¶ms, validator, amount.change(), - offset_epoch, + current_epoch, offset_opt, )?; } @@ -2778,6 +2778,15 @@ where offset, )?; + insert_validator_into_validator_set( + storage, + ¶ms, + &address, + token::Amount::zero(), + current_epoch, + offset, + )?; + Ok(()) } @@ -4473,9 +4482,15 @@ where &validator, -slash_delta.change(), epoch, - None, + Some(0), + )?; + update_total_deltas( + storage, + ¶ms, + -slash_delta.change(), + epoch, + Some(0), )?; - update_total_deltas(storage, ¶ms, -slash_delta.change(), epoch, None)?; } // TODO: should we clear some storage here as is done in Quint?? @@ -5339,14 +5354,15 @@ pub mod test_utils { validators: impl Iterator, current_epoch: namada_core::types::storage::Epoch, ) -> storage_api::Result<()> - where - S: StorageRead + StorageWrite, + where + S: StorageRead + StorageWrite, { init_genesis(storage, params, current_epoch)?; for GenesisValidator { address, consensus_key, - protocol_key, eth_cold_key, + protocol_key, + eth_cold_key, eth_hot_key, commission_rate, max_commission_rate_change, @@ -5389,16 +5405,16 @@ pub mod test_utils { Ok(()) } - /// Init PoS genesis wrapper helper that also initializes gov params that are - /// used in PoS with default values. + /// Init PoS genesis wrapper helper that also initializes gov params that + /// are used in PoS with default values. pub fn test_init_genesis( storage: &mut S, owned: OwnedPosParams, validators: impl Iterator + Clone, current_epoch: namada_core::types::storage::Epoch, ) -> storage_api::Result - where - S: StorageRead + StorageWrite, + where + S: StorageRead + StorageWrite, { let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); gov_params.init_storage(storage)?; diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index b75c3f207c..070f93aeaf 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -41,6 +41,7 @@ use test_log::test; use crate::epoched::DEFAULT_NUM_PAST_EPOCHS; use crate::parameters::testing::arb_pos_params; use crate::parameters::{OwnedPosParams, PosParams}; +use crate::test_utils::test_init_genesis; use crate::types::{ into_tm_voting_power, BondDetails, BondId, BondsAndUnbondsDetails, ConsensusValidator, EagerRedelegatedBondsMap, GenesisValidator, Position, @@ -64,11 +65,10 @@ use crate::{ read_consensus_validator_set_addresses_with_stake, read_total_stake, read_validator_deltas_value, read_validator_stake, slash, slash_redelegation, slash_validator, slash_validator_redelegation, - staking_token_address, store_total_consensus_stake, test_utils::test_init_genesis, - total_bonded_handle, total_deltas_handle, total_unbonded_handle, - unbond_handle, unbond_tokens, unjail_validator, update_validator_deltas, - update_validator_set, validator_consensus_key_handle, - validator_incoming_redelegations_handle, + staking_token_address, store_total_consensus_stake, total_bonded_handle, + total_deltas_handle, total_unbonded_handle, unbond_handle, unbond_tokens, + unjail_validator, update_validator_deltas, update_validator_set, + validator_consensus_key_handle, validator_incoming_redelegations_handle, validator_outgoing_redelegations_handle, validator_set_positions_handle, validator_set_update_tendermint, validator_slashes_handle, validator_state_handle, validator_total_redelegated_bonded_handle, @@ -970,7 +970,8 @@ fn test_become_validator_aux( let staking_token = staking_token_address(&s); let amount = token::Amount::from_uint(100_500_000, 0).unwrap(); credit_tokens(&mut s, &staking_token, &new_validator, amount).unwrap(); - bond_tokens(&mut s, None, &new_validator, amount, current_epoch, None).unwrap(); + bond_tokens(&mut s, None, &new_validator, amount, current_epoch, None) + .unwrap(); // Check the bond delta let bond_handle = bond_handle(&new_validator, &new_validator); @@ -1321,15 +1322,8 @@ fn test_validator_sets() { ) .unwrap(); - update_validator_deltas( - s, - ¶ms, - addr, - stake.change(), - epoch, - None, - ) - .unwrap(); + update_validator_deltas(s, ¶ms, addr, stake.change(), epoch, None) + .unwrap(); // Set their consensus key (needed for // `validator_set_update_tendermint` fn) @@ -1754,17 +1748,17 @@ fn test_validator_sets() { let bond = token::Amount::from_uint(500_000, 0).unwrap(); let stake6 = stake6 + bond; println!("val6 {val6} new stake {}", stake6.to_string_native()); - update_validator_set(&mut s, ¶ms, &val6, bond.change(), pipeline_epoch, None) - .unwrap(); - update_validator_deltas( + update_validator_set( &mut s, ¶ms, &val6, bond.change(), - epoch, + pipeline_epoch, None, ) .unwrap(); + update_validator_deltas(&mut s, ¶ms, &val6, bond.change(), epoch, None) + .unwrap(); let val6_bond_epoch = pipeline_epoch; let consensus_vals: Vec<_> = consensus_validator_set_handle() @@ -2006,15 +2000,8 @@ fn test_validator_sets_swap() { ) .unwrap(); - update_validator_deltas( - s, - ¶ms, - addr, - stake.change(), - epoch, - None, - ) - .unwrap(); + update_validator_deltas(s, ¶ms, addr, stake.change(), epoch, None) + .unwrap(); // Set their consensus key (needed for // `validator_set_update_tendermint` fn) @@ -2068,7 +2055,7 @@ fn test_validator_sets_swap() { &val3, bond3.change(), pipeline_epoch, - None, + None, ) .unwrap(); update_validator_deltas( @@ -2130,7 +2117,7 @@ fn test_validator_sets_swap() { &val3, bonds.change(), epoch, -None, + None, ) .unwrap(); diff --git a/sdk/src/error.rs b/sdk/src/error.rs index aa4c2c9842..3512d1a9f2 100644 --- a/sdk/src/error.rs +++ b/sdk/src/error.rs @@ -19,7 +19,7 @@ pub type Result = std::result::Result; /// /// The general mentality should be that this error type should cover all /// possible errors that one may face. -#[derive( Error, Debug)] +#[derive(Error, Debug)] pub enum Error { /// Errors that are caused by trying to retrieve a pinned transaction #[error("Error in retrieving pinned balance: {0}")] diff --git a/sdk/src/wallet/alias.rs b/sdk/src/wallet/alias.rs index 61ba36d74e..48ab4a9fa0 100644 --- a/sdk/src/wallet/alias.rs +++ b/sdk/src/wallet/alias.rs @@ -5,10 +5,10 @@ use std::fmt::Display; use std::hash::Hash; use std::io::Read; use std::str::FromStr; -use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; +use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::types::address::{Address, InternalAddress}; +use serde::{Deserialize, Serialize}; /// Aliases created from raw strings are kept in-memory as given, but their /// `Serialize` and `Display` instance converts them to lowercase. Their @@ -62,8 +62,8 @@ impl BorshDeserialize for Alias { impl Serialize for Alias { fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, + where + S: serde::Serializer, { Serialize::serialize(&self.normalize(), serializer) } @@ -71,8 +71,8 @@ impl Serialize for Alias { impl<'de> Deserialize<'de> for Alias { fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, + where + D: serde::Deserializer<'de>, { let raw: String = Deserialize::deserialize(deserializer)?; Ok(Self::from(raw)) @@ -85,7 +85,6 @@ impl PartialEq for Alias { } } - impl PartialOrd for Alias { fn partial_cmp(&self, other: &Self) -> Option { self.normalize().partial_cmp(&other.normalize()) @@ -139,7 +138,6 @@ impl FromStr for Alias { } } - impl AsRef for &Alias { fn as_ref(&self) -> &str { &self.0 diff --git a/sdk/src/wallet/mod.rs b/sdk/src/wallet/mod.rs index 8715b02b7c..4570c8a283 100644 --- a/sdk/src/wallet/mod.rs +++ b/sdk/src/wallet/mod.rs @@ -346,7 +346,10 @@ impl Wallet { } /// Find the stored address by an alias. - pub fn find_address(&self, alias: impl AsRef) -> Option> { + pub fn find_address( + &self, + alias: impl AsRef, + ) -> Option> { Alias::is_reserved(alias.as_ref()) .map(std::borrow::Cow::Owned) .or_else(|| { diff --git a/sdk/src/wallet/store.rs b/sdk/src/wallet/store.rs index ddc5c5e03e..4b13291e03 100644 --- a/sdk/src/wallet/store.rs +++ b/sdk/src/wallet/store.rs @@ -537,7 +537,7 @@ impl Store { // abort if the alias is reserved if Alias::is_reserved(&alias).is_some() { println!("The alias {} is reserved", alias); - return ; + return; } key.map(|x| self.keys.insert(alias.clone(), x)); pkh.map(|x| self.pkhs.insert(x, alias.clone())); diff --git a/shared/src/ledger/native_vp/ibc/mod.rs b/shared/src/ledger/native_vp/ibc/mod.rs index 429643256e..902bef0a5b 100644 --- a/shared/src/ledger/native_vp/ibc/mod.rs +++ b/shared/src/ledger/native_vp/ibc/mod.rs @@ -420,7 +420,8 @@ mod tests { namada_proof_of_stake::OwnedPosParams::default(), vec![get_dummy_genesis_validator()].into_iter(), Epoch(1), - ).unwrap(); + ) + .unwrap(); // epoch duration let epoch_duration_key = get_epoch_duration_storage_key(); let epoch_duration = EpochDuration { diff --git a/shared/src/ledger/pos/mod.rs b/shared/src/ledger/pos/mod.rs index 3987b9b533..620d0108d3 100644 --- a/shared/src/ledger/pos/mod.rs +++ b/shared/src/ledger/pos/mod.rs @@ -13,9 +13,9 @@ pub use namada_proof_of_stake; pub use namada_proof_of_stake::parameters::{OwnedPosParams, PosParams}; pub use namada_proof_of_stake::pos_queries::*; pub use namada_proof_of_stake::storage::*; -pub use namada_proof_of_stake::{staking_token_address, types}; #[cfg(any(test, feature = "testing"))] pub use namada_proof_of_stake::test_utils; +pub use namada_proof_of_stake::{staking_token_address, types}; pub use vp::PosVP; use crate::types::address::{Address, InternalAddress}; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 2c21ea9d97..f37cc681b7 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -27,6 +27,7 @@ abciplus = [ ] wasm-runtime = ["namada/wasm-runtime"] +integration = ["namada_apps/integration"] [dependencies] namada = {path = "../shared", features = ["testing"]} @@ -39,6 +40,7 @@ async-trait.workspace = true chrono.workspace = true clap.workspace = true concat-idents.workspace = true +copy_dir = "0.1.3" derivative.workspace = true hyper = {version = "0.14.20", features = ["full"]} lazy_static.workspace = true diff --git a/tests/src/e2e/helpers.rs b/tests/src/e2e/helpers.rs index 5e4edce6aa..09ec4ab808 100644 --- a/tests/src/e2e/helpers.rs +++ b/tests/src/e2e/helpers.rs @@ -19,7 +19,7 @@ use namada::types::address::Address; use namada::types::key::*; use namada::types::storage::Epoch; use namada::types::token; -use namada_apps::config::genesis::genesis_config; +use namada_apps::config::genesis::chain; use namada_apps::config::utils::convert_tm_addr_to_socket_addr; use namada_apps::config::{Config, TendermintMode}; use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; @@ -178,17 +178,19 @@ pub fn get_validator_pk(test: &Test, who: &Who) -> Option { Who::NonValidator => return None, Who::Validator(i) => i, }; - let file = format!("{}.toml", test.net.chain_id.as_str()); - let path = test.test_dir.path().join(file); - let config = genesis_config::open_genesis_config(path).unwrap(); - let pk = config - .validator - .get(&format!("validator-{}", index)) - .unwrap() - .account_public_key - .as_ref() - .unwrap(); - Some(pk.to_public_key().unwrap()) + let path = test.test_dir.path().join("genesis"); + let genesis = chain::Finalized::read_toml_files( + &path.join(test.net.chain_id.as_str()), + ) + .unwrap(); + genesis + .transactions + .validator_account? + .iter() + .find(|val_tx| { + val_tx.tx.alias.to_string() == format!("validator-{}", index) + }) + .map(|val_tx| val_tx.tx.account_key.pk.raw.clone()) } /// Find the address of an account by its alias from the wallet @@ -364,9 +366,6 @@ pub fn generate_bin_command(bin_name: &str, manifest_path: &Path) -> Command { let build_cmd = CargoBuild::new() .package(APPS_PACKAGE) .manifest_path(manifest_path) - // Explicitly disable dev, in case it's enabled when a test is - // invoked - .env("NAMADA_DEV", "false") .bin(bin_name); let build_cmd = if run_debug { diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index 9e8e8d4eba..433770bfa5 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -67,8 +67,9 @@ use namada_apps::client::rpc::{ query_pos_parameters, query_storage_value, query_storage_value_bytes, }; use namada_apps::client::utils::id_from_pk; -use namada_apps::config::ethereum_bridge; -use namada_apps::config::genesis::genesis_config::GenesisConfig; +use namada_apps::config::genesis::{chain, templates}; +use namada_apps::config::utils::set_port; +use namada_apps::config::{ethereum_bridge, TendermintMode}; use namada_apps::facade::tendermint::block::Header as TmHeader; use namada_apps::facade::tendermint::merkle::proof::Proof as TmProof; use namada_apps::facade::tendermint_config::net::Address as TendermintAddress; @@ -82,7 +83,9 @@ use super::setup::set_ethereum_bridge_mode; use crate::e2e::helpers::{ find_address, get_actor_rpc, get_validator_pk, wait_for_wasm_pre_compile, }; -use crate::e2e::setup::{self, sleep, Bin, NamadaCmd, Test, Who}; +use crate::e2e::setup::{ + self, sleep, working_dir, Bin, NamadaCmd, Test, TestDir, Who, +}; use crate::{run, run_as}; #[test] @@ -206,25 +209,92 @@ fn run_ledger_ibc() -> Result<()> { Ok(()) } +/// Set up two Namada chains to talk to each other via IBC. fn setup_two_single_node_nets() -> Result<(Test, Test)> { + const ANOTHER_PROXY_APP: u16 = 27659u16; + const ANOTHER_RPC: u16 = 27660u16; + const ANOTHER_P2P: u16 = 26655u16; // Download the shielded pool parameters before starting node let _ = FsShieldedUtils::new(PathBuf::new()); - // epoch per 100 seconds - let update_genesis = |mut genesis: GenesisConfig| { - genesis.parameters.epochs_per_year = 31536; - genesis.parameters.min_num_of_blocks = 1; - genesis - }; - let update_genesis_b = |mut genesis: GenesisConfig| { - genesis.parameters.epochs_per_year = 31536; - genesis.parameters.min_num_of_blocks = 1; - setup::set_validators(1, genesis, |_| setup::ANOTHER_CHAIN_PORT_OFFSET) + let update_genesis = + |mut genesis: templates::All, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = 315_360; + setup::set_validators(1, genesis, base_dir, |_| 0) + }; + let test_a = setup::network(update_genesis, None)?; + let test_b = Test { + working_dir: working_dir(), + test_dir: TestDir::new(), + net: test_a.net.clone(), + async_runtime: Default::default(), }; - Ok(( - setup::network(update_genesis, None)?, - setup::network(update_genesis_b, None)?, - )) + for entry in std::fs::read_dir(test_a.test_dir.path()).unwrap() { + let entry = entry.unwrap(); + if entry.path().is_dir() { + copy_dir::copy_dir( + entry.path(), + test_b.test_dir.path().join(entry.file_name()), + ) + .map_err(|e| { + eyre!( + "Failed copying directory from test_a to test_b with {}", + e + ) + })?; + } else { + std::fs::copy( + entry.path(), + test_b.test_dir.path().join(entry.file_name()), + ) + .map_err(|e| { + eyre!("Failed copying file from test_a to test_b with {}", e) + })?; + } + } + let genesis_b_dir = test_b + .test_dir + .path() + .join(namada_apps::client::utils::NET_ACCOUNTS_DIR) + .join("validator-0"); + let mut genesis_b = chain::Finalized::read_toml_files( + &genesis_b_dir.join(test_a.net.chain_id.as_str()), + ) + .map_err(|_| eyre!("Could not read genesis files from test b"))?; + // chain b's validator needs to listen on a different port than chain a's + // validator + let validator_tx = genesis_b + .transactions + .validator_account + .as_mut() + .unwrap() + .iter_mut() + .find(|val| val.tx.alias.to_string() == *"validator-0") + .unwrap(); + let new_port = + validator_tx.tx.net_address.port() + setup::ANOTHER_CHAIN_PORT_OFFSET; + validator_tx.tx.net_address.set_port(new_port); + genesis_b + .write_toml_files(&genesis_b_dir.join(test_a.net.chain_id.as_str())) + .map_err(|_| eyre!("Could not write genesis toml files for test_b"))?; + // modify chain b to use different ports for cometbft + let mut config = namada_apps::config::Config::load( + &genesis_b_dir, + &test_a.net.chain_id, + Some(TendermintMode::Validator), + ); + let proxy_app = &mut config.ledger.cometbft.proxy_app; + set_port(proxy_app, ANOTHER_PROXY_APP); + let rpc_addr = &mut config.ledger.cometbft.rpc.laddr; + set_port(rpc_addr, ANOTHER_RPC); + let p2p_addr = &mut config.ledger.cometbft.p2p.laddr; + set_port(p2p_addr, ANOTHER_P2P); + config + .write(&genesis_b_dir, &test_a.net.chain_id, true) + .map_err(|e| { + eyre!("Unable to modify chain b's config file due to {}", e) + })?; + Ok((test_a, test_b)) } fn create_client(test_a: &Test, test_b: &Test) -> Result<(ClientId, ClientId)> { diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 2ee44c62ae..224c3d767f 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -23,15 +23,14 @@ use namada::types::address::Address; use namada::types::storage::Epoch; use namada::types::token; use namada_apps::config::ethereum_bridge; -use namada_apps::config::genesis::genesis_config::{ - GenesisConfig, ParametersConfig, PgfParametersConfig, PosParamsConfig, -}; use namada_apps::config::utils::convert_tm_addr_to_socket_addr; use namada_apps::facade::tendermint_config::net::Address as TendermintAddress; use namada_core::ledger::governance::cli::onchain::{ PgfFunding, PgfFundingTarget, StewardsUpdate, }; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada_sdk::masp::fs::FsShieldedUtils; +use namada_sdk::wallet::alias::Alias; use namada_test_utils::TestWasms; use namada_vp_prelude::BTreeSet; use serde_json::json; @@ -47,7 +46,10 @@ use crate::e2e::helpers::{ epoch_sleep, find_address, find_bonded_stake, get_actor_rpc, get_epoch, is_debug_mode, parse_reached_epoch, }; -use crate::e2e::setup::{self, default_port_offset, sleep, Bin, Who}; +use crate::e2e::setup::{ + self, allow_duplicate_ips, default_port_offset, set_validators, sleep, Bin, + Who, +}; use crate::{run, run_as}; fn start_namada_ledger_node( @@ -125,10 +127,15 @@ fn run_ledger() -> Result<()> { fn test_node_connectivity_and_consensus() -> Result<()> { // Setup 2 genesis validator nodes let test = setup::network( - |genesis| setup::set_validators(2, genesis, default_port_offset), + |genesis, base_dir| { + setup::set_validators(2, genesis, base_dir, default_port_offset) + }, None, )?; + allow_duplicate_ips(&test, &test.net.chain_id, &Who::Validator(0)); + allow_duplicate_ips(&test, &test.net.chain_id, &Who::Validator(1)); + set_ethereum_bridge_mode( &test, &test.net.chain_id, @@ -210,7 +217,7 @@ fn test_node_connectivity_and_consensus() -> 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: 1000010.1")?; + client.exp_string("nam: 980010.1")?; client.assert_success(); } @@ -409,8 +416,7 @@ fn stop_ledger_at_height() -> Result<()> { /// 8. Query the raw bytes of a storage key #[test] fn ledger_txs_and_queries() -> Result<()> { - let test = setup::network(|genesis| genesis, None)?; - + let test = setup::single_node_net()?; set_ethereum_bridge_mode( &test, &test.net.chain_id, @@ -525,7 +531,7 @@ fn ledger_txs_and_queries() -> Result<()> { "init-account", "--public-keys", // Value obtained from `namada::types::key::ed25519::tests::gen_keypair` - "001be519a321e29020fa3cbfbfd01bd5e92db134305609270b71dace25b5a21168", + "pktest1qpqfzxu3gt05jx2mvg82f4anf90psqerkwqhjey4zlqv0qfgwuvkzpklrjh", "--threshold", "1", "--code-path", @@ -594,7 +600,7 @@ fn ledger_txs_and_queries() -> Result<()> { ), // Unspecified token expect all tokens from wallet derived from genesis ( - vec!["balance", "--owner", BERTHA, "--node", &validator_one_rpc], + vec!["balance", "--owner", ALBERT, "--node", &validator_one_rpc], // expect all genesis tokens, sorted by alias vec![ r"apfel: \d+(\.\d+)?", @@ -676,155 +682,6 @@ fn ledger_txs_and_queries() -> Result<()> { Ok(()) } -/// We test shielding, shielded to shielded and unshielding transfers: -/// 1. Run the ledger node -/// 2. Send 20 BTC from Albert to PA(A) -/// 3. Send 7 BTC from SK(A) to PA(B) -/// 4. Assert BTC balance at VK(A) is 13 -/// 5. Send 5 BTC from SK(B) to Bertha -/// 6. Assert BTC balance at VK(B) is 2 -#[test] -fn masp_txs_and_queries() -> Result<()> { - // Download the shielded pool parameters before starting node - let _ = FsShieldedUtils::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, - )?; - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - &Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - 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![ - // 2. Send 20 BTC from Albert to PA(A) - ( - vec![ - "transfer", - "--source", - ALBERT, - "--target", - AA_PAYMENT_ADDRESS, - "--token", - BTC, - "--amount", - "20", - "--node", - &validator_one_rpc, - ], - "Transaction is valid", - ), - // 3. Send 7 BTC from SK(A) to PA(B) - ( - vec![ - "transfer", - "--source", - A_SPENDING_KEY, - "--target", - AB_PAYMENT_ADDRESS, - "--token", - BTC, - "--amount", - "7", - "--gas-payer", - CHRISTEL_KEY, - "--node", - &validator_one_rpc, - ], - "Transaction is valid", - ), - // 4. Assert BTC balance at VK(A) is 13 - ( - vec![ - "balance", - "--owner", - AA_VIEWING_KEY, - "--token", - BTC, - "--node", - &validator_one_rpc, - ], - "btc: 13", - ), - // 5. Send 5 BTC from SK(B) to Bertha - ( - vec![ - "transfer", - "--source", - B_SPENDING_KEY, - "--target", - BERTHA, - "--token", - BTC, - "--amount", - "5", - "--gas-payer", - CHRISTEL_KEY, - "--node", - &validator_one_rpc, - ], - "Transaction is valid", - ), - // 6. Assert BTC balance at VK(B) is 2 - ( - vec![ - "balance", - "--owner", - AB_VIEWING_KEY, - "--token", - BTC, - "--node", - &validator_one_rpc, - ], - "btc: 2", - ), - ]; - - for (tx_args, tx_result) in &txs_args { - for &dry_run in &[true, false] { - let tx_args = if dry_run && tx_args[0] == "transfer" { - vec![tx_args.clone(), vec!["--dry-run"]].concat() - } else { - tx_args.clone() - }; - let mut client = run!(test, Bin::Client, tx_args, Some(720))?; - - if *tx_result == "Transaction is valid" && !dry_run { - client.exp_string("Transaction accepted")?; - client.exp_string("Transaction applied")?; - } - client.exp_string(tx_result)?; - } - } - - Ok(()) -} - /// Test the optional disposable keypair for wrapper signing /// /// 1. Test that a tx requesting a disposable signer with a correct unshielding @@ -839,16 +696,11 @@ fn wrapper_disposable_signer() -> Result<()> { // 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 - } + |mut genesis, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(120); + genesis.parameters.parameters.min_num_of_blocks = 1; + set_validators(1, genesis, base_dir, default_port_offset) }, None, )?; @@ -862,76 +714,69 @@ fn wrapper_disposable_signer() -> Result<()> { 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, - "--force", - ], - // Not enough funds for fee payment, will use PoW - "Error while processing transaction's fees", - ), + let tx_args = vec![ + "transfer", + "--source", + ALBERT, + "--target", + AA_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "50", + "--ledger-address", + &validator_one_rpc, ]; + let mut client = run!(test, Bin::Client, tx_args, Some(720))?; - for (tx_args, tx_result) in &txs_args { - let mut client = run!(test, Bin::Client, tx_args, Some(720))?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; + client.exp_string("Transaction is valid")?; - if *tx_result == "Transaction is valid" { - client.exp_string("Transaction accepted")?; - client.exp_string("Transaction applied")?; - } - client.exp_string(tx_result)?; - } + let _ep1 = epoch_sleep(&test, &validator_one_rpc, 720)?; + let tx_args = vec![ + "transfer", + "--source", + ALBERT, + "--target", + BERTHA, + "--token", + NAM, + "--amount", + "1", + "--gas-spending-key", + A_SPENDING_KEY, + "--disposable-gas-payer", + "--ledger-address", + &validator_one_rpc, + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(720))?; + + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; + client.exp_string("Transaction is valid")?; + let _ep1 = epoch_sleep(&test, &validator_one_rpc, 720)?; + let tx_args = 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, + "--force", + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(720))?; + client.exp_string("Error while processing transaction's fees")?; Ok(()) } @@ -1064,29 +909,13 @@ fn pos_bonds() -> Result<()> { let pipeline_len = 2; let unbonding_len = 4; let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - min_num_of_blocks: 6, - max_expected_time_per_block: 1, - epochs_per_year: 31_536_000, - ..genesis.parameters - }; - let pos_params = PosParamsConfig { - pipeline_len, - unbonding_len, - ..genesis.pos_params - }; - let genesis = GenesisConfig { - parameters, - pos_params, - ..genesis - }; - let mut genesis = - setup::set_validators(2, genesis, default_port_offset); - // Remove stake from the 2nd validator so chain can run with a - // single node - genesis.validator.get_mut("validator-1").unwrap().tokens = None; - genesis + |mut genesis, base_dir: &_| { + genesis.parameters.pos_params.pipeline_len = pipeline_len; + genesis.parameters.pos_params.unbonding_len = unbonding_len; + genesis.parameters.parameters.min_num_of_blocks = 6; + genesis.parameters.parameters.max_expected_time_per_block = 1; + genesis.parameters.parameters.epochs_per_year = 31_536_000; + setup::set_validators(1, genesis, base_dir, default_port_offset) }, None, )?; @@ -1106,6 +935,27 @@ fn pos_bonds() -> Result<()> { let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); + // put money in the validator account from its balance account so that it + // can self-bond + let tx_args = vec![ + "transfer", + "--source", + "validator-0-balance-key", + "--target", + "validator-0-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &validator_0_rpc, + ]; + let mut client = + run_as!(test, Who::Validator(0), Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction applied with result:")?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + // 2. Submit a self-bond for the first genesis validator let tx_args = vec![ "bond", @@ -1114,7 +964,7 @@ fn pos_bonds() -> Result<()> { "--amount", "10000.0", "--signing-keys", - "validator-0-account-key", + "validator-0-validator-key", "--node", &validator_0_rpc, ]; @@ -1172,7 +1022,7 @@ fn pos_bonds() -> Result<()> { "--amount", "5100.0", "--signing-keys", - "validator-0-account-key", + "validator-0-validator-key", "--node", &validator_0_rpc, ]; @@ -1251,7 +1101,7 @@ fn pos_bonds() -> Result<()> { "--validator", "validator-0", "--signing-keys", - "validator-0-account-key", + "validator-0-validator-key", "--node", &validator_0_rpc, ]; @@ -1302,24 +1152,13 @@ fn pos_bonds() -> Result<()> { #[test] fn pos_rewards() -> Result<()> { let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - min_num_of_blocks: 4, - epochs_per_year: 31_536_000, - max_expected_time_per_block: 1, - ..genesis.parameters - }; - let pos_params = PosParamsConfig { - pipeline_len: 2, - unbonding_len: 4, - ..genesis.pos_params - }; - let genesis = GenesisConfig { - parameters, - pos_params, - ..genesis - }; - setup::set_validators(3, genesis, default_port_offset) + |mut genesis, base_dir| { + genesis.parameters.parameters.max_expected_time_per_block = 4; + genesis.parameters.parameters.epochs_per_year = 31_536_000; + genesis.parameters.parameters.max_expected_time_per_block = 1; + genesis.parameters.pos_params.pipeline_len = 2; + genesis.parameters.pos_params.unbonding_len = 4; + setup::set_validators(3, genesis, base_dir, default_port_offset) }, None, )?; @@ -1347,7 +1186,6 @@ fn pos_rewards() -> Result<()> { let validator_zero_rpc = get_actor_rpc(&test, &Who::Validator(0)); let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(1)); - let validator_two_rpc = get_actor_rpc(&test, &Who::Validator(2)); // Submit a delegation from Bertha to validator-0 let tx_args = vec![ @@ -1386,7 +1224,26 @@ fn pos_rewards() -> Result<()> { let _bg_validator_0 = validator_0.background(); let _bg_validator_1 = validator_1.background(); let _bg_validator_2 = validator_2.background(); - + // put money in the validator account from its balance account so that it + // can self-bond + let tx_args = vec![ + "transfer", + "--source", + "validator-1-balance-key", + "--target", + "validator-1-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &validator_one_rpc, + ]; + let mut client = + run_as!(test, Who::Validator(1), Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction applied with result:")?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); // Let validator-1 self-bond let tx_args = vec![ "bond", @@ -1399,7 +1256,7 @@ fn pos_rewards() -> Result<()> { "--gas-token", NAM, "--signing-keys", - "validator-1-account-key", + "validator-1-validator-key", "--ledger-address", &validator_one_rpc, ]; @@ -1408,6 +1265,26 @@ fn pos_rewards() -> Result<()> { client.exp_string("Transaction applied with result:")?; client.exp_string("Transaction is valid.")?; client.assert_success(); + // put money in the validator account from its balance account so that it + // can self-bond + let tx_args = vec![ + "transfer", + "--source", + "validator-2-balance-key", + "--target", + "validator-2-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &validator_one_rpc, + ]; + let mut client = + run_as!(test, Who::Validator(2), Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction applied with result:")?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); // Let validator-2 self-bond let tx_args = vec![ @@ -1421,9 +1298,9 @@ fn pos_rewards() -> Result<()> { "--gas-token", NAM, "--signing-keys", - "validator-2-account-key", + "validator-2-validator-key", "--ledger-address", - &validator_two_rpc, + &validator_zero_rpc, ]; let mut client = run_as!(test, Who::Validator(2), Bin::Client, tx_args, Some(40))?; @@ -1466,23 +1343,13 @@ fn test_bond_queries() -> Result<()> { let pipeline_len = 2; let unbonding_len = 4; let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - min_num_of_blocks: 2, - max_expected_time_per_block: 1, - epochs_per_year: 31_536_000, - ..genesis.parameters - }; - let pos_params = PosParamsConfig { - pipeline_len, - unbonding_len, - ..genesis.pos_params - }; - GenesisConfig { - parameters, - pos_params, - ..genesis - } + |mut genesis, base_dir: &_| { + genesis.parameters.parameters.min_num_of_blocks = 2; + genesis.parameters.parameters.max_expected_time_per_block = 1; + genesis.parameters.parameters.epochs_per_year = 31_536_000; + genesis.parameters.pos_params.pipeline_len = pipeline_len; + genesis.parameters.pos_params.unbonding_len = unbonding_len; + setup::set_validators(1, genesis, base_dir, default_port_offset) }, None, )?; @@ -1495,6 +1362,26 @@ fn test_bond_queries() -> Result<()> { let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); let validator_alias = "validator-0"; + // put money in the validator account from its balance account so that it + // can pay gas fees + let tx_args = vec![ + "transfer", + "--source", + "validator-0-balance-key", + "--target", + "validator-0-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &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(); + // 2. Submit a delegation to the genesis validator let tx_args = vec![ "bond", @@ -1597,8 +1484,8 @@ fn test_bond_queries() -> Result<()> { let tx_args = vec!["bonds", "--ledger-address", &validator_one_rpc]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; client.exp_string( - "All bonds total active: 200088.000000\r -All bonds total: 200088.000000\r + "All bonds total active: 120188.000000\r +All bonds total: 120188.000000\r All unbonds total active: 412.000000\r All unbonds total: 412.000000\r All unbonds total withdrawable: 412.000000\r", @@ -1620,31 +1507,40 @@ All unbonds total withdrawable: 412.000000\r", #[test] fn pos_init_validator() -> Result<()> { let pipeline_len = 1; - let validator_stake = 200000_u64; + let validator_stake = token::Amount::native_whole(20000_u64); let test = setup::network( - |genesis| { + |mut genesis, base_dir: &_| { + let stake = genesis + .transactions + .bond + .as_ref() + .unwrap() + .iter() + .filter_map(|bond| { + (bond.data.validator.to_string() == *"validator-0").then( + || { + bond.data + .amount + .increase_precision( + NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap() + .amount + }, + ) + }) + .sum::(); assert_eq!( - genesis.validator.get("validator-0").unwrap().tokens, - Some(validator_stake), + stake, validator_stake, "Assuming this stake, we give the same amount to the new \ validator to have half of voting power", ); - let parameters = ParametersConfig { - min_num_of_blocks: 4, - epochs_per_year: 31_536_000, - max_expected_time_per_block: 1, - ..genesis.parameters - }; - let pos_params = PosParamsConfig { - pipeline_len, - unbonding_len: 2, - ..genesis.pos_params - }; - GenesisConfig { - parameters, - pos_params, - ..genesis - } + genesis.parameters.parameters.min_num_of_blocks = 4; + genesis.parameters.parameters.epochs_per_year = 31_536_000; + genesis.parameters.parameters.max_expected_time_per_block = 1; + genesis.parameters.pos_params.pipeline_len = pipeline_len; + genesis.parameters.pos_params.unbonding_len = 2; + setup::set_validators(1, genesis, base_dir, default_port_offset) }, None, )?; @@ -1727,7 +1623,7 @@ fn pos_init_validator() -> Result<()> { client.assert_success(); // 4. Transfer some NAM to the new validator - let validator_stake_str = &validator_stake.to_string(); + let validator_stake_str = &validator_stake.to_string_native(); let tx_args = vec![ "transfer", "--source", @@ -1814,11 +1710,12 @@ fn pos_init_validator() -> Result<()> { find_bonded_stake(&test, new_validator, &non_validator_rpc)?; assert_eq!( bonded_stake, - token::Amount::native_whole(validator_stake + delegation) + token::Amount::native_whole(delegation) + validator_stake ); Ok(()) } + /// Test that multiple txs submitted in the same block all get the tx result. /// /// In this test we: @@ -1827,7 +1724,9 @@ fn pos_init_validator() -> Result<()> { #[test] fn ledger_many_txs_in_a_block() -> Result<()> { let test = Arc::new(setup::network( - |genesis| genesis, + |genesis, base_dir: &_| { + setup::set_validators(1, genesis, base_dir, |_| 0) + }, // Set 10s consensus timeout to have more time to submit txs Some("10s"), )?); @@ -1911,22 +1810,20 @@ fn ledger_many_txs_in_a_block() -> Result<()> { #[test] fn proposal_submission() -> Result<()> { let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - epochs_per_year: epochs_per_year_from_min_duration(1), - max_proposal_bytes: Default::default(), - min_num_of_blocks: 4, - max_expected_time_per_block: 1, - ..genesis.parameters - }; - - GenesisConfig { - parameters, - ..genesis - } + |mut genesis, base_dir: &_| { + genesis.parameters.gov_params.max_proposal_code_size = 600000; + genesis.parameters.parameters.max_expected_time_per_block = 1; + setup::set_validators(1, genesis, base_dir, |_| 0u16) }, None, )?; + set_ethereum_bridge_mode( + &test, + &test.net.chain_id, + &Who::Validator(0), + ethereum_bridge::ledger::Mode::Off, + None, + ); let namadac_help = vec!["--help"]; @@ -1941,6 +1838,26 @@ fn proposal_submission() -> Result<()> { let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); + // put money in the validator account from its balance account so that it + // can pay gas fees + let tx_args = vec![ + "transfer", + "--source", + "validator-0-balance-key", + "--target", + "validator-0-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &validator_0_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(); + // 1.1 Delegate some token let tx_args = vec![ "bond", @@ -1977,6 +1894,7 @@ fn proposal_submission() -> Result<()> { &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid.")?; client.assert_success(); @@ -2010,7 +1928,7 @@ fn proposal_submission() -> Result<()> { ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("nam: 999500")?; + client.exp_string("nam: 979500")?; client.assert_success(); // 5. Query token balance governance @@ -2077,7 +1995,7 @@ fn proposal_submission() -> Result<()> { ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("nam: 999500")?; + client.exp_string("nam: 979500")?; client.assert_success(); // 9. Send a yay vote from a validator @@ -2163,7 +2081,7 @@ fn proposal_submission() -> Result<()> { let mut client = run!(test, Bin::Client, query_proposal, Some(15))?; client.exp_string("Proposal Id: 0")?; client.exp_string( - "passed with 200900.000000 yay votes and 0.000000 nay votes (0.%)", + "passed with 120900.000000 yay votes and 0.000000 nay votes (0.%)", )?; client.assert_success(); @@ -2185,7 +2103,7 @@ fn proposal_submission() -> Result<()> { ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("nam: 1000000")?; + client.exp_string("nam: 980000")?; client.assert_success(); // 13. Check if governance funds are 0 @@ -2225,24 +2143,16 @@ fn proposal_submission() -> Result<()> { #[test] fn pgf_governance_proposal() -> Result<()> { let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - epochs_per_year: epochs_per_year_from_min_duration(1), - max_proposal_bytes: Default::default(), - min_num_of_blocks: 4, - max_expected_time_per_block: 1, - ..genesis.parameters - }; - let pgf_params = PgfParametersConfig { - stewards: BTreeSet::from_iter(Address::from_str("atest1v4ehgw36xguyzsejx5m5xvehxqmnyvejgdzygv6rgcurgdzxxsunzs3nxuc5vwfkg3pnxdf4u4p9h9")), - ..genesis.pgf_params - }; - - GenesisConfig { - parameters, - pgf_params, - ..genesis - } + |mut genesis, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(1); + genesis.parameters.parameters.max_proposal_bytes = + Default::default(); + genesis.parameters.parameters.min_num_of_blocks = 4; + genesis.parameters.parameters.max_expected_time_per_block = 1; + genesis.parameters.pgf_params.stewards = + BTreeSet::from_iter(Alias::from_str("albert")); + setup::set_validators(1, genesis, base_dir, |_| 0) }, None, )?; @@ -2268,6 +2178,26 @@ fn pgf_governance_proposal() -> Result<()> { let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); + // put money in the validator account from its balance account so that it + // can pay gas fees + let tx_args = vec![ + "transfer", + "--source", + "validator-0-balance-key", + "--target", + "validator-0-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &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(); + // Delegate some token let tx_args = vec![ "bond", @@ -2334,7 +2264,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("nam: 999500")?; + client.exp_string("nam: 979500")?; client.assert_success(); // Query token balance governance @@ -2439,7 +2369,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("nam: 1000000")?; + client.exp_string("nam: 980000")?; client.assert_success(); // Check if governance funds are 0 @@ -2543,29 +2473,20 @@ fn pgf_governance_proposal() -> Result<()> { fn proposal_offline() -> Result<()> { let working_dir = setup::working_dir(); let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - epochs_per_year: epochs_per_year_from_min_duration(1), - 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 - }; - - GenesisConfig { - parameters, - ..genesis - } + |mut genesis, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(1); + genesis.parameters.parameters.max_proposal_bytes = + Default::default(); + genesis.parameters.parameters.min_num_of_blocks = 4; + genesis.parameters.parameters.max_expected_time_per_block = 1; + genesis.parameters.parameters.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 + genesis.parameters.parameters.tx_whitelist = + Some(get_all_wasms_hashes(&working_dir, Some("tx_"))); + setup::set_validators(1, genesis, base_dir, |_| 0) }, None, )?; @@ -2695,7 +2616,7 @@ fn proposal_offline() -> Result<()> { let mut client = run!(test, Bin::Client, tally_offline, Some(15))?; client.exp_string("Parsed 1 votes")?; - client.exp_string("rejected with 900.000000 yay votes")?; + client.exp_string("rejected with 20900.000000 yay votes")?; client.assert_success(); Ok(()) @@ -2715,407 +2636,6 @@ fn generate_proposal_json_file( serde_json::to_writer(intent_writer, proposal_content).unwrap(); } -/// In this test we: -/// 1. Setup 2 genesis validators -/// 2. Initialize a new network with the 2 validators -/// 3. Setup and start the 2 genesis validator nodes and a non-validator node -/// 4. Submit a valid token transfer tx from one validator to the other -/// 5. Check that all the nodes processed the tx with the same result -#[test] -fn test_genesis_validators() -> Result<()> { - use std::collections::HashMap; - use std::net::SocketAddr; - use std::str::FromStr; - - use namada::types::chain::ChainId; - use namada_apps::config::genesis::genesis_config::{ - self, ValidatorPreGenesisConfig, - }; - use namada_apps::config::Config; - - // This test is not using the `setup::network`, because we're setting up - // custom genesis validators - setup::INIT.call_once(|| { - if let Err(err) = color_eyre::install() { - eprintln!("Failed setting up colorful error reports {}", err); - } - }); - - let working_dir = setup::working_dir(); - let test_dir = setup::TestDir::new(); - let checksums_path = working_dir - .join("wasm/checksums.json") - .to_string_lossy() - .into_owned(); - - // Same as in `genesis/e2e-tests-single-node.toml` for `validator-0` - let net_address_0 = SocketAddr::from_str("127.0.0.1:27656").unwrap(); - let net_address_port_0 = net_address_0.port(); - // Find the first port (ledger P2P) that should be used for a validator at - // the given index - let get_first_port = |ix: u8| net_address_port_0 + 6 * (ix as u16 + 1); - - // 1. Setup 2 genesis validators, one with ed25519 keys (0) and one with - // secp256k1 keys (1) - let validator_0_alias = "validator-0"; - let validator_1_alias = "validator-1"; - - let mut init_genesis_validator_0 = setup::run_cmd( - Bin::Client, - [ - "utils", - "init-genesis-validator", - "--unsafe-dont-encrypt", - "--alias", - validator_0_alias, - "--scheme", - "ed25519", - "--commission-rate", - "0.05", - "--max-commission-rate-change", - "0.01", - "--net-address", - &format!("127.0.0.1:{}", get_first_port(0)), - ], - Some(5), - &working_dir, - &test_dir, - format!("{}:{}", std::file!(), std::line!()), - )?; - init_genesis_validator_0.assert_success(); - let validator_0_pre_genesis_dir = - namada_apps::client::utils::validator_pre_genesis_dir( - test_dir.path(), - validator_0_alias, - ); - let config = std::fs::read_to_string( - namada_apps::client::utils::validator_pre_genesis_file( - &validator_0_pre_genesis_dir, - ), - ) - .unwrap(); - let mut validator_0_config: ValidatorPreGenesisConfig = - toml::from_str(&config).unwrap(); - let validator_0_config = validator_0_config - .validator - .remove(validator_0_alias) - .unwrap(); - - let mut init_genesis_validator_1 = setup::run_cmd( - Bin::Client, - [ - "utils", - "init-genesis-validator", - "--unsafe-dont-encrypt", - "--alias", - validator_1_alias, - "--scheme", - "secp256k1", - "--commission-rate", - "0.05", - "--max-commission-rate-change", - "0.01", - "--net-address", - &format!("127.0.0.1:{}", get_first_port(1)), - ], - Some(5), - &working_dir, - &test_dir, - format!("{}:{}", std::file!(), std::line!()), - )?; - init_genesis_validator_1.assert_success(); - let validator_1_pre_genesis_dir = - namada_apps::client::utils::validator_pre_genesis_dir( - test_dir.path(), - validator_1_alias, - ); - let config = std::fs::read_to_string( - namada_apps::client::utils::validator_pre_genesis_file( - &validator_1_pre_genesis_dir, - ), - ) - .unwrap(); - let mut validator_1_config: ValidatorPreGenesisConfig = - toml::from_str(&config).unwrap(); - let validator_1_config = validator_1_config - .validator - .remove(validator_1_alias) - .unwrap(); - - // 2. Initialize a new network with the 2 validators - let mut genesis = genesis_config::open_genesis_config( - working_dir.join(setup::SINGLE_NODE_NET_GENESIS), - )?; - let update_validator_config = - |ix: u8, mut config: genesis_config::ValidatorConfig| { - // Setup tokens balances and validity predicates - config.tokens = Some(200000); - config.non_staked_balance = Some(1000000000000); - config.validator_vp = Some("vp_user".into()); - // Setup the validator ports same as what - // `setup::set_validators` would do - let mut net_address = net_address_0; - // 6 ports for each validator - let first_port = get_first_port(ix); - net_address.set_port(first_port); - config.net_address = Some(net_address.to_string()); - config - }; - genesis.validator = HashMap::from_iter([ - ( - validator_0_alias.to_owned(), - update_validator_config(0, validator_0_config), - ), - ( - validator_1_alias.to_owned(), - update_validator_config(1, validator_1_config), - ), - ]); - let genesis_file = test_dir.path().join("e2e-test-genesis-src.toml"); - genesis_config::write_genesis_config(&genesis, &genesis_file); - let genesis_path = genesis_file.to_string_lossy(); - - let archive_dir = test_dir.path().to_string_lossy().to_string(); - let args = vec![ - "utils", - "init-network", - "--unsafe-dont-encrypt", - "--genesis-path", - &genesis_path, - "--chain-prefix", - "e2e-test", - "--localhost", - "--allow-duplicate-ip", - "--wasm-checksums-path", - &checksums_path, - "--archive-dir", - &archive_dir, - ]; - let mut init_network = setup::run_cmd( - Bin::Client, - args, - Some(5), - &working_dir, - &test_dir, - format!("{}:{}", std::file!(), std::line!()), - )?; - - // Get the generated chain_id` from result of the last command - let (unread, matched) = - init_network.exp_regex(r"Derived chain ID: .*\n")?; - let chain_id_raw = - matched.trim().split_once("Derived chain ID: ").unwrap().1; - let chain_id = ChainId::from_str(chain_id_raw.trim())?; - println!("'init-network' output: {}", unread); - let net = setup::Network { - chain_id: chain_id.clone(), - }; - let test = setup::Test { - working_dir: working_dir.clone(), - test_dir, - net, - genesis, - async_runtime: Default::default(), - }; - - // Host the network archive to make it available for `join-network` commands - let network_archive_server = file_serve::Server::new(&working_dir); - let network_archive_addr = network_archive_server.addr().to_owned(); - std::thread::spawn(move || { - network_archive_server.serve().unwrap(); - }); - - // 3. Setup and start the 2 genesis validator nodes and a non-validator node - - // Clean-up the chain dir from the existing validator dir that were created - // by `init-network`, because we want to set them up with `join-network` - // instead - let validator_0_base_dir = test.get_base_dir(&Who::Validator(0)); - let validator_1_base_dir = test.get_base_dir(&Who::Validator(1)); - std::fs::remove_dir_all(&validator_0_base_dir).unwrap(); - std::fs::remove_dir_all(&validator_1_base_dir).unwrap(); - - std::env::set_var( - namada_apps::client::utils::ENV_VAR_NETWORK_CONFIGS_SERVER, - format!("http://{network_archive_addr}/{}", archive_dir), - ); - let pre_genesis_path = validator_0_pre_genesis_dir.to_string_lossy(); - let mut join_network_val_0 = run_as!( - test, - Who::Validator(0), - Bin::Client, - [ - "utils", - "join-network", - "--chain-id", - chain_id.as_str(), - "--pre-genesis-path", - pre_genesis_path.as_ref(), - "--dont-prefetch-wasm", - ], - Some(5) - )?; - join_network_val_0.exp_string("Successfully configured for chain")?; - - let pre_genesis_path = validator_1_pre_genesis_dir.to_string_lossy(); - let mut join_network_val_1 = run_as!( - test, - Who::Validator(1), - Bin::Client, - [ - "utils", - "join-network", - "--chain-id", - chain_id.as_str(), - "--pre-genesis-path", - pre_genesis_path.as_ref(), - "--dont-prefetch-wasm", - ], - Some(5) - )?; - join_network_val_1.exp_string("Successfully configured for chain")?; - - // We have to update the ports in the configs again, because the ones from - // `join-network` use the defaults - // - // TODO: use `update_actor_config` from `setup`, instead - let update_config = |ix: u8, mut config: Config| { - let first_port = net_address_port_0 + 6 * (ix as u16 + 1); - let p2p_addr = - convert_tm_addr_to_socket_addr(&config.ledger.cometbft.p2p.laddr) - .ip() - .to_string(); - - config.ledger.cometbft.p2p.laddr = TendermintAddress::from_str( - &format!("{}:{}", p2p_addr, first_port), - ) - .unwrap(); - let rpc_addr = - convert_tm_addr_to_socket_addr(&config.ledger.cometbft.rpc.laddr) - .ip() - .to_string(); - config.ledger.cometbft.rpc.laddr = TendermintAddress::from_str( - &format!("{}:{}", rpc_addr, first_port + 1), - ) - .unwrap(); - let proxy_app_addr = - convert_tm_addr_to_socket_addr(&config.ledger.cometbft.proxy_app) - .ip() - .to_string(); - config.ledger.cometbft.proxy_app = TendermintAddress::from_str( - &format!("{}:{}", proxy_app_addr, first_port + 2), - ) - .unwrap(); - config - }; - - let validator_0_config = update_config( - 0, - Config::load(&validator_0_base_dir, &test.net.chain_id, None), - ); - validator_0_config - .write(&validator_0_base_dir, &chain_id, true) - .unwrap(); - - let validator_1_config = update_config( - 1, - Config::load(&validator_1_base_dir, &test.net.chain_id, None), - ); - validator_1_config - .write(&validator_1_base_dir, &chain_id, true) - .unwrap(); - - // Copy WASMs to each node's chain dir - let chain_dir = test.test_dir.path().join(chain_id.as_str()); - setup::copy_wasm_to_chain_dir( - &working_dir, - &chain_dir, - &chain_id, - test.genesis.validator.keys(), - ); - - let mut validator_0 = - start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))?; - let mut validator_1 = - start_namada_ledger_node_wait_wasm(&test, Some(1), Some(40))?; - let mut non_validator = - start_namada_ledger_node_wait_wasm(&test, None, Some(40))?; - - // Wait for a first block - validator_0.exp_string("Committed block hash")?; - validator_1.exp_string("Committed block hash")?; - non_validator.exp_string("Committed block hash")?; - - let bg_validator_0 = validator_0.background(); - let bg_validator_1 = validator_1.background(); - let _bg_non_validator = non_validator.background(); - - // 4. Submit a valid token transfer tx - let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); - let tx_args = [ - "transfer", - "--source", - validator_0_alias, - "--target", - validator_1_alias, - "--token", - NAM, - "--amount", - "10.1", - "--node", - &validator_one_rpc, - ]; - let mut client = - run_as!(test, Who::Validator(0), Bin::Client, tx_args, Some(40))?; - client.exp_string("Transaction applied with result:")?; - client.exp_string("Transaction is valid.")?; - client.assert_success(); - - // 3. Check that all the nodes processed the tx with the same result - let mut validator_0 = bg_validator_0.foreground(); - let mut validator_1 = bg_validator_1.foreground(); - - let expected_result = "successful inner txs: 1"; - // We cannot check this on non-validator node as it might sync without - // applying the tx itself, but its state should be the same, checked below. - validator_0.exp_string(expected_result)?; - validator_1.exp_string(expected_result)?; - let _bg_validator_0 = validator_0.background(); - let _bg_validator_1 = validator_1.background(); - - let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); - let validator_1_rpc = get_actor_rpc(&test, &Who::Validator(1)); - let non_validator_rpc = get_actor_rpc(&test, &Who::NonValidator); - - // Find the block height on the validator - let after_tx_height = get_height(&test, &validator_0_rpc)?; - - // Wait for the second validator and non-validator to be synced to at least - // the same height - wait_for_block_height(&test, &validator_1_rpc, after_tx_height, 10)?; - wait_for_block_height(&test, &non_validator_rpc, after_tx_height, 10)?; - - let query_balance_args = |ledger_rpc| { - vec![ - "balance", - "--owner", - validator_1_alias, - "--token", - NAM, - "--node", - ledger_rpc, - ] - }; - 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(r"nam: 1000000000010.1")?; - client.assert_success(); - } - - Ok(()) -} - /// In this test we intentionally make a validator node double sign blocks /// to test that slashing evidence is received and processed by the ledger /// correctly: @@ -3141,21 +2661,22 @@ fn double_signing_gets_slashed() -> Result<()> { // Setup 2 genesis validator nodes let test = setup::network( - |genesis| { + |mut genesis, base_dir| { (pipeline_len, unbonding_len, cubic_offset) = ( - genesis.pos_params.pipeline_len, - genesis.pos_params.unbonding_len, - genesis.pos_params.cubic_slashing_window_length, + genesis.parameters.pos_params.pipeline_len, + genesis.parameters.pos_params.unbonding_len, + genesis.parameters.pos_params.cubic_slashing_window_length, ); - let mut genesis = - setup::set_validators(4, genesis, default_port_offset); // Make faster epochs to be more likely to discover boundary issues - genesis.parameters.min_num_of_blocks = 2; - genesis + genesis.parameters.parameters.min_num_of_blocks = 2; + setup::set_validators(4, genesis, base_dir, default_port_offset) }, None, )?; + allow_duplicate_ips(&test, &test.net.chain_id, &Who::Validator(0)); + allow_duplicate_ips(&test, &test.net.chain_id, &Who::Validator(1)); + set_ethereum_bridge_mode( &test, &test.net.chain_id, @@ -3192,6 +2713,28 @@ fn double_signing_gets_slashed() -> Result<()> { validator_3.exp_string("This node is a validator")?; let _bg_validator_3 = validator_3.background(); + let validator_zero_rpc = get_actor_rpc(&test, &Who::Validator(0)); + // put money in the validator account from its balance account so that it + // can pay gas fees + let tx_args = vec![ + "transfer", + "--source", + "validator-0-balance-key", + "--target", + "validator-0-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &validator_zero_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(); + // 2. Copy the first genesis validator base-dir let validator_0_base_dir = test.get_base_dir(&Who::Validator(0)); let validator_0_base_dir_copy = test @@ -3437,7 +2980,7 @@ fn double_signing_gets_slashed() -> Result<()> { /// 2d. Submit same tx again, this time the client shouldn't reveal again. #[test] fn implicit_account_reveal_pk() -> Result<()> { - let test = setup::network(|genesis| genesis, None)?; + let test = setup::single_node_net()?; // 1. Run the ledger node let _bg_ledger = @@ -3568,16 +3111,11 @@ fn implicit_account_reveal_pk() -> Result<()> { fn test_epoch_sleep() -> Result<()> { // Use slightly longer epochs to give us time to sleep let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - epochs_per_year: epochs_per_year_from_min_duration(30), - min_num_of_blocks: 1, - ..genesis.parameters - }; - GenesisConfig { - parameters, - ..genesis - } + |mut genesis, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(30); + genesis.parameters.parameters.min_num_of_blocks = 1; + setup::set_validators(1, genesis, base_dir, |_| 0) }, None, )?; diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index e98a14735c..233f4be1b2 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -16,9 +16,8 @@ use expectrl::process::unix::{PtyStream, UnixProcess}; use expectrl::session::Session; use expectrl::stream::log::LogStream; use expectrl::{ControlCode, Eof, WaitStatus}; -use eyre::{eyre, Context}; +use eyre::eyre; use itertools::{Either, Itertools}; -use namada_sdk::wallet::alias::Alias; use namada::types::chain::ChainId; use namada_apps::client::utils::{ self, validator_pre_genesis_dir, validator_pre_genesis_txs_file, @@ -30,6 +29,7 @@ use namada_apps::{config, wallet}; use namada_core::types::key::{RefTo, SchemeType}; use namada_core::types::string_encoding::StringEncoded; use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; +use namada_sdk::wallet::alias::Alias; use namada_tx_prelude::token; use namada_vp_prelude::HashSet; use once_cell::sync::Lazy; @@ -148,12 +148,11 @@ where let mut wallet = wallet::load(&wallet_path) .expect("Could not locate pre-genesis wallet used for e2e tests."); let alias = format!("validator-{}-balance-key", val); - let (alias, sk) = wallet - .gen_key(SchemeType::Ed25519, Some(alias), true, None, None) + let (alias, sk, _mnemonic) = wallet + .gen_key(SchemeType::Ed25519, Some(alias), true, None, None, None) .unwrap_or_else(|_| { panic!("Could not generate new key for validator-{}", val) - }) - .unwrap(); + }); wallet::save(&wallet).unwrap(); // assign balance to the key genesis diff --git a/tests/src/integration/setup.rs b/tests/src/integration/setup.rs index 6e8d953bcb..3a5739176d 100644 --- a/tests/src/integration/setup.rs +++ b/tests/src/integration/setup.rs @@ -5,9 +5,11 @@ use std::sync::{Arc, Mutex}; use color_eyre::eyre::{eyre, Result}; use namada_apps::cli::args; +use namada_apps::client::utils::PRE_GENESIS_DIR; use namada_apps::config; -use namada_apps::config::genesis::genesis_config; -use namada_apps::config::genesis::genesis_config::GenesisConfig; +use namada_apps::config::genesis::chain::Finalized; +use namada_apps::config::genesis::templates; +use namada_apps::config::genesis::templates::load_and_validate; use namada_apps::config::TendermintMode; use namada_apps::facade::tendermint::Timeout; use namada_apps::facade::tendermint_proto::google::protobuf::Timestamp; @@ -17,13 +19,11 @@ use namada_apps::node::ledger::shell::testing::node::{ }; use namada_apps::node::ledger::shell::testing::utils::TestDir; use namada_apps::node::ledger::shell::Shell; -use namada_core::types::address::nam; -use namada_core::types::chain::{ChainId, ChainIdPrefix}; -use toml::value::Table; +use namada_apps::wallet::pre_genesis; +use namada_core::types::chain::ChainIdPrefix; +use namada_sdk::wallet::alias::Alias; -use crate::e2e::setup::{ - copy_wasm_to_chain_dir, get_all_wasms_hashes, SINGLE_NODE_NET_GENESIS, -}; +use crate::e2e::setup::{copy_wasm_to_chain_dir, SINGLE_NODE_NET_GENESIS}; /// Env. var for keeping temporary files created by the integration tests const ENV_VAR_KEEP_TEMP: &str = "NAMADA_INT_KEEP_TEMP"; @@ -88,23 +88,24 @@ pub fn initialize_genesis() -> Result<(MockNode, MockServicesController)> { }, ); - finalize_wallet(&template_dir, &global_args, genesis); + let eth_bridge_params = genesis.get_eth_bridge_params(); let auto_drive_services = { // NB: for now, the only condition that // dictates whether mock services should // be enabled is if the Ethereum bridge // is enabled at genesis - genesis.ethereum_bridge_params.is_some() + eth_bridge_params.is_some() }; let enable_eth_oracle = { // NB: we only enable the oracle if the // Ethereum bridge is enabled at genesis - genesis.ethereum_bridge_params.is_some() + eth_bridge_params.is_some() }; let services_cfg = MockServicesCfg { auto_drive_services, enable_eth_oracle, }; + finalize_wallet(&template_dir, &global_args, genesis); create_node(test_dir, global_args, keep_temp, services_cfg) } diff --git a/tests/src/vm_host_env/ibc.rs b/tests/src/vm_host_env/ibc.rs index 88f88820a5..052b5b5e77 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -219,7 +219,8 @@ pub fn init_storage() -> (Address, Address) { OwnedPosParams::default(), vec![get_dummy_genesis_validator()].into_iter(), Epoch(1), - ).unwrap(); + ) + .unwrap(); // store wasm code let key = Key::wasm_code(&code_hash); env.wl_storage.storage.write(&key, code.clone()).unwrap(); diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index d8daa460d4..fe18edd852 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1005,6 +1005,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "copy_dir" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3" +dependencies = [ + "walkdir", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -3468,6 +3477,7 @@ dependencies = [ "chrono", "clap", "concat-idents", + "copy_dir", "derivative", "hyper", "lazy_static", diff --git a/wasm/checksums.json b/wasm/checksums.json index ba49a6a111..a46ac8e119 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,23 +1,23 @@ { - "tx_bond.wasm": "tx_bond.bec1efd37d88876be4176d1afc0b6fa784901efe18cf9ec30293313be855d814.wasm", - "tx_bridge_pool.wasm": "tx_bridge_pool.dee27236443e5e1fe6bf16385f59376a40b824a1865c88dd19964779cf8d19a8.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.6cf329120204bf10b217b8113f93011fc2ea27a09c0246a866710fa9f8b68f79.wasm", - "tx_ibc.wasm": "tx_ibc.58bd568dfa94973c798b3868326197fa5d8687a6f6aee77c395eaaa1d01e2c19.wasm", - "tx_init_account.wasm": "tx_init_account.f0061157377454c314ad7c9e3855221d49da7425ffef6db6b7d28f5242423281.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.6f63dc0ee534b74cec728b9f76dae383388f7306df66b827fa940e210a7c11ca.wasm", - "tx_init_validator.wasm": "tx_init_validator.b3b48166976e0259db4eab1e72ace4c92368b34fd5a06f7ccd41b4ae4c88ca1d.wasm", - "tx_redelegate.wasm": "tx_redelegate.7216e581fdb10b7563847d831f7783c67c2b7e3a0eec917f9d1ea4b2f30022dc.wasm", - "tx_resign_steward.wasm": "tx_resign_steward.24d4c8549698b5309017f2610a152c6f0e3cb9f29961f76aadb2db392ab478e4.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.152efebe3954ac8f2d5760ec18fae1197c9e02a161491375516cfc40cba0a1e8.wasm", - "tx_transfer.wasm": "tx_transfer.111a5b61d839f5b974f774762e74c98db8fc389d2795cbd61a2c44e5ab46c5a1.wasm", - "tx_unbond.wasm": "tx_unbond.4730c6c302f0a572c09d2b671ae4c40deee8cf46fc168c8eba57fadef88b73de.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.0a7624e9884b681e0af8708961b4e0c98a025ef571676cfbb408b77a3195cd15.wasm", - "tx_update_account.wasm": "tx_update_account.c3e25b73b94a226c3e9a5990fb91864c1515cf2bcb8e28e4b3e403c41a370932.wasm", - "tx_update_steward_commission.wasm": "tx_update_steward_commission.18ada5bdea4846fbce630bf2a8444ae4e927d8b5d08bd0605463cba11c07681a.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.92ed2a568021d8c015e275cc39936750e10b51a2ee950f213fb21a76ccc72147.wasm", - "tx_withdraw.wasm": "tx_withdraw.649353ad4c9b5acefe52b5e67eada78b6f83e206ed71dc935ddec70467c3063b.wasm", - "vp_implicit.wasm": "vp_implicit.973bbd890ebd233a8fc347a8b2b59f7bc0243d5a4d61760f27c94b5e4117912e.wasm", - "vp_masp.wasm": "vp_masp.8d78d43dcc5044d3e4e9a25cb35181e33382b7df9728cde6ab814d9561fc55d1.wasm", - "vp_user.wasm": "vp_user.3b9e4d777c9420bbbf730e1549252f1a091bc01535eb1f37e7a7e3d1bace7576.wasm", - "vp_validator.wasm": "vp_validator.33c1232c3d6fb3fbd554fe1015dddf80582bc6c92253cc8ea0068b465436ddf7.wasm" + "tx_bond.wasm": "tx_bond.9fdaf63c3052a44195c8280d749e1682654ee2bad6edd25c6551bdc8197f7512.wasm", + "tx_bridge_pool.wasm": "tx_bridge_pool.605e9b7e8b0ff27863567b56d3f5e757783f5c75c596260d7c0805a47b7a877e.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.f036ef369e754cb6c75453f9b380f95be588d330f993284b08b477231d1ed0eb.wasm", + "tx_ibc.wasm": "tx_ibc.95832c0d92bb83ac2cde7b3c7cde9edae7c21f8dd5b32d93a9368093d58b5f15.wasm", + "tx_init_account.wasm": "tx_init_account.f525992552827cfd7693d568879dfd1f54406af9906b8827bc86e4beb21a0efe.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.9c3cf15da2b14e2c9898f5910cbfcffb80ef14dfcd0082dc9039b170cb3cc208.wasm", + "tx_init_validator.wasm": "tx_init_validator.1b695e5869532bbaebe5d99d48a267ae0891e9c4470abbbad5d0b37118e35ca6.wasm", + "tx_redelegate.wasm": "tx_redelegate.63f6c6b2f1582b357870ca9b779e88139c1e859b18fe7ada71dfdf5842fd83c7.wasm", + "tx_resign_steward.wasm": "tx_resign_steward.c21e812ba4a095d4b5eca562a94a2ef5071522a95f87a641c5f6e2bb2a4c890d.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.e5b1b047b44cde0cc8f9f89ba15f611dbefe8a3c060c4012363fdf07e92c6875.wasm", + "tx_transfer.wasm": "tx_transfer.c34a0a0b15ae448db7bc455bab8c59e6510c5fe9cb2d6386897030c37a189e42.wasm", + "tx_unbond.wasm": "tx_unbond.e846fd512237bdee232edc8d87e637ab3631942e3e1eda635d595e96d52bf489.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.1c8db6285b60d8ce80ce5c1ff7d43755e50cd3749fad5fb286f66592112ba4cd.wasm", + "tx_update_account.wasm": "tx_update_account.9f9493f2fb39d0a5f0fa3327a78828e387ba15690ad86e96d4b8cdf509cf1efc.wasm", + "tx_update_steward_commission.wasm": "tx_update_steward_commission.92bcf758cbb58be93ae5d7fde616a3974016f0d41c09b7333bc6041bc23e8d30.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.825ffa0c7c7c588cb668af4ff6ba3ce5c5d1c37c0728c18a7eb807a02da52158.wasm", + "tx_withdraw.wasm": "tx_withdraw.0225f3c8ac6e4f2d4636568048035156c936d9a8bc12d4c0d619598089af6f47.wasm", + "vp_implicit.wasm": "vp_implicit.e8dd924d43f11854724350c3e082a35898dc03e21f1543c117cff6763d881d2b.wasm", + "vp_masp.wasm": "vp_masp.64553be3434516e8de6b3e0a445582dce7d7975bd9765f4fbd9875bdbf0f9feb.wasm", + "vp_user.wasm": "vp_user.37fee9afa11b5899c824b6b8e1f0a626f5fd05c9813af454081a797772ca3de6.wasm", + "vp_validator.wasm": "vp_validator.e6283c764480d04942c0fa2fb3d6e3d1cd18565b235b09e6a6deb7918c808f31.wasm" } \ No newline at end of file diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 716c75b919..f5a457e183 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1005,6 +1005,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "copy_dir" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3" +dependencies = [ + "walkdir", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -3468,6 +3477,7 @@ dependencies = [ "chrono", "clap", "concat-idents", + "copy_dir", "derivative", "hyper", "lazy_static", From 430d82c6242286eb52b6a32f51b86e774b6c4b23 Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 30 Oct 2023 11:48:20 +0100 Subject: [PATCH 04/47] [fix]: Fixed unit tests --- apps/src/lib/node/ledger/shell/mod.rs | 42 +++++++++++++++++++++++-- apps/src/lib/node/ledger/storage/mod.rs | 36 ++++++++++++++++++--- core/src/ledger/inflation.rs | 6 +++- proof_of_stake/src/lib.rs | 4 +-- proof_of_stake/src/tests.rs | 12 +++---- 5 files changed, 84 insertions(+), 16 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 1110a6562d..8a282b5052 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -1514,13 +1514,15 @@ mod test_utils { use namada::types::keccak::KeccakHash; use namada::types::key::*; use namada::types::storage::{BlockHash, Epoch, Header}; - use namada::types::time::DateTimeUtc; + use namada::types::time::{DateTimeUtc, DurationSecs}; use namada::types::transaction::{Fee, TxType, WrapperTx}; use tempfile::tempdir; use tokio::sync::mpsc::{Sender, UnboundedReceiver}; + use namada::ledger::parameters::{EpochDuration, Parameters}; use super::*; use crate::config::ethereum_bridge::ledger::ORACLE_CHANNEL_BUFFER_SIZE; + use crate::facade::tendermint_proto::abci::{ Misbehavior, RequestInitChain, RequestPrepareProposal, RequestProcessProposal, @@ -2034,14 +2036,50 @@ mod test_utils { .block .pred_epochs .new_epoch(BlockHeight(1)); + // initialize parameter storage + let params = Parameters { + epoch_duration: EpochDuration { min_num_of_blocks: 1, min_duration: DurationSecs(3600) }, + max_expected_time_per_block: DurationSecs(3600), + max_proposal_bytes: Default::default(), + max_block_gas: 100, + vp_whitelist: vec![], + tx_whitelist: vec![], + implicit_vp_code_hash: Default::default(), + epochs_per_year: 365, + max_signatures_per_transaction: 10, + pos_gain_p: Default::default(), + pos_gain_d: Default::default(), + staked_ratio: Default::default(), + pos_inflation_amount: Default::default(), + fee_unshielding_gas_limit: 0, + fee_unshielding_descriptions_limit: 0, + minimum_gas_price: Default::default(), + }; + params.init_storage(&mut shell.wl_storage).expect("Test failed"); + // make wl_storage to update conversion for a new epoch + let token_params = token::Parameters { + max_reward_rate: Default::default(), + kd_gain_nom: Default::default(), + kp_gain_nom: Default::default(), + locked_ratio_target: Default::default(), + }; // Insert a map assigning random addresses to each token alias. // Needed for storage but not for this test. for (token, _) in address::tokens() { + let addr = address::gen_deterministic_established_address(token); + token_params.init_storage(&addr, &mut shell.wl_storage); + shell.wl_storage.write(&token::minted_balance_key(&addr), token::Amount::zero()).unwrap(); shell.wl_storage.storage.conversion_state.tokens.insert( token.to_string(), - address::gen_deterministic_established_address(token), + addr, ); } + shell.wl_storage.storage.conversion_state.tokens.insert("nam".to_string(), shell.wl_storage.storage.native_token.clone()); + token_params.init_storage(&shell.wl_storage.storage.native_token.clone(), &mut shell.wl_storage); + // final adjustments so that updating allowed conversions doesn't panic with + // divide by zero + shell.wl_storage.write(&token::minted_balance_key(&shell.wl_storage.storage.native_token.clone()), token::Amount::zero()).unwrap(); + shell.wl_storage.storage.conversion_state.normed_inflation = Some(1); update_allowed_conversions(&mut shell.wl_storage) .expect("update conversions failed"); shell.wl_storage.commit_block().expect("commit failed"); diff --git a/apps/src/lib/node/ledger/storage/mod.rs b/apps/src/lib/node/ledger/storage/mod.rs index 329d0a7bd8..1a6a880063 100644 --- a/apps/src/lib/node/ledger/storage/mod.rs +++ b/apps/src/lib/node/ledger/storage/mod.rs @@ -67,6 +67,8 @@ mod tests { use proptest::prelude::*; use proptest::test_runner::Config; use tempfile::TempDir; + use namada::ledger::parameters::{EpochDuration, Parameters}; + use namada::types::time::DurationSecs; use super::*; @@ -135,15 +137,36 @@ mod tests { let key = Key::parse("key").expect("cannot parse the key string"); let value: u64 = 1; let value_bytes = types::encode(&value); - + let mut wl_storage = WlStorage::new(WriteLog::default(), storage); + // initialize parameter storage + let params = Parameters { + epoch_duration: EpochDuration { min_num_of_blocks: 1, min_duration: DurationSecs(3600) }, + max_expected_time_per_block: DurationSecs(3600), + max_proposal_bytes: Default::default(), + max_block_gas: 100, + vp_whitelist: vec![], + tx_whitelist: vec![], + implicit_vp_code_hash: Default::default(), + epochs_per_year: 365, + max_signatures_per_transaction: 10, + pos_gain_p: Default::default(), + pos_gain_d: Default::default(), + staked_ratio: Default::default(), + pos_inflation_amount: Default::default(), + fee_unshielding_gas_limit: 0, + fee_unshielding_descriptions_limit: 0, + minimum_gas_price: Default::default(), + }; + params.init_storage(&mut wl_storage).expect("Test failed"); // insert and commit - storage + wl_storage + .storage .write(&key, value_bytes.clone()) .expect("write failed"); - storage.block.epoch = storage.block.epoch.next(); - storage.block.pred_epochs.new_epoch(BlockHeight(100)); + wl_storage.storage.block.epoch = wl_storage.storage.block.epoch.next(); + wl_storage.storage.block.pred_epochs.new_epoch(BlockHeight(100)); // make wl_storage to update conversion for a new epoch - let mut wl_storage = WlStorage::new(WriteLog::default(), storage); + let token_params = token::Parameters { max_reward_rate: Default::default(), kd_gain_nom: Default::default(), @@ -161,8 +184,11 @@ mod tests { addr, ); } + wl_storage.storage.conversion_state.tokens.insert("nam".to_string(), wl_storage.storage.native_token.clone()); token_params.init_storage(&wl_storage.storage.native_token.clone(), &mut wl_storage); + wl_storage.write(&token::minted_balance_key(&wl_storage.storage.native_token.clone()), token::Amount::zero()).unwrap(); + wl_storage.storage.conversion_state.normed_inflation = Some(1); update_allowed_conversions(&mut wl_storage) .expect("update conversions failed"); wl_storage.commit_block().expect("commit failed"); diff --git a/core/src/ledger/inflation.rs b/core/src/ledger/inflation.rs index 3e1902a445..477eb7ade9 100644 --- a/core/src/ledger/inflation.rs +++ b/core/src/ledger/inflation.rs @@ -73,7 +73,11 @@ impl RewardsController { .expect("Should not fail to convert Uint to Dec"); let epochs_py: Dec = epochs_per_year.into(); - let locked_ratio = locked / total; + let locked_ratio = if total.is_zero() { + Dec::one() + } else { + locked / total + }; let max_inflation = total_native * max_reward_rate / epochs_py; let p_gain = p_gain_nom * max_inflation; let d_gain = d_gain_nom * max_inflation; diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 72b58150a5..ffe85f0358 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -4467,7 +4467,7 @@ where &validator, -slash_amount.change(), epoch, - None, + Some(0), )?; } } @@ -5310,7 +5310,7 @@ where ¶ms, dest_validator, amount_after_slashing.change(), - pipeline_epoch, + current_epoch, None, )?; } diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 070f93aeaf..7d5f79f099 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -1550,7 +1550,7 @@ fn test_validator_sets() { ¶ms, &val1, -unbond.change(), - pipeline_epoch, + epoch, None, ) .unwrap(); @@ -1753,7 +1753,7 @@ fn test_validator_sets() { ¶ms, &val6, bond.change(), - pipeline_epoch, + epoch, None, ) .unwrap(); @@ -2035,7 +2035,7 @@ fn test_validator_sets_swap() { ¶ms, &val2, bond2.change(), - pipeline_epoch, + epoch, None, ) .unwrap(); @@ -2054,7 +2054,7 @@ fn test_validator_sets_swap() { ¶ms, &val3, bond3.change(), - pipeline_epoch, + epoch, None, ) .unwrap(); @@ -2088,7 +2088,7 @@ fn test_validator_sets_swap() { ¶ms, &val2, bonds.change(), - pipeline_epoch, + epoch, None, ) .unwrap(); @@ -2107,7 +2107,7 @@ fn test_validator_sets_swap() { ¶ms, &val3, bonds.change(), - pipeline_epoch, + epoch, None, ) .unwrap(); From 24ec2df0a6e77b9e9ab43b4981f4001beab67524 Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 30 Oct 2023 16:58:55 +0100 Subject: [PATCH 05/47] More fixes --- apps/src/lib/config/genesis/templates.rs | 3 +- apps/src/lib/node/ledger/shell/init_chain.rs | 1 - apps/src/lib/node/ledger/shell/mod.rs | 51 ++++++++++---- apps/src/lib/node/ledger/storage/mod.rs | 48 +++++++++---- core/src/types/token.rs | 18 +++-- proof_of_stake/src/lib.rs | 4 +- proof_of_stake/src/tests.rs | 66 ++++-------------- ...B3D80CA615B4BBA75BDD5AA773E1E33792217D.bin | Bin 0 -> 7448 bytes ...FF8BF07596031CD460FEBFAEA4F75AF65D5402.bin | Bin 9208 -> 0 bytes ...D76149D3088F539CF8372D404609B89B095EF7.bin | Bin 7448 -> 0 bytes ...EB66F886B3A3B6C71D33F456B859D01DA47ADD.bin | Bin 9208 -> 0 bytes ...6EF25580F3F5916126CAC201AD67B3D644691.bin} | Bin 7448 -> 7448 bytes ...FC3DA2C57E4FD8FF0811D9CB129887F0F9F706.bin | Bin 25031 -> 0 bytes ...8B6780B6F18A312AE3909BEA19D16FCFE837DC.bin | Bin 18792 -> 0 bytes ...E72A01F0B169F946835583DC2C71B550315603.bin | Bin 19947 -> 0 bytes ...CFE8EEC08E2D8512695A667D294AE1A4A8D4E6.bin | Bin 9649 -> 0 bytes ...8DC6BBB31619C7E93A1A5A2E64B694DBE1BD6E.bin | Bin 7448 -> 0 bytes ...FA3F379DB351AB4AE081207ABFDFC429C9FA48.bin | Bin 17018 -> 0 bytes ...344FFFAA6CA273027CD480AEA68DDED57D88CA.bin | Bin 7448 -> 0 bytes ...B827EEEDA858AB983D16024AAA415579A68953.bin | Bin 9649 -> 0 bytes ...5A3F8ED350401B95715A6AF9BDD8CD1C367BA.bin} | Bin 7448 -> 7448 bytes tests/src/e2e/ibc_tests.rs | 4 +- tests/src/integration/masp.rs | 30 ++++---- 23 files changed, 111 insertions(+), 114 deletions(-) create mode 100644 test_fixtures/masp_proofs/00E4DF5E2DCAD1EC5923537C64B3D80CA615B4BBA75BDD5AA773E1E33792217D.bin delete mode 100644 test_fixtures/masp_proofs/29AC8DE3B07495BEABEAF50FE8FF8BF07596031CD460FEBFAEA4F75AF65D5402.bin delete mode 100644 test_fixtures/masp_proofs/37332141CB34FC30FF51F4BEE8D76149D3088F539CF8372D404609B89B095EF7.bin delete mode 100644 test_fixtures/masp_proofs/52984E26D4A044A259B441C1DAEB66F886B3A3B6C71D33F456B859D01DA47ADD.bin rename test_fixtures/masp_proofs/{1362F1CF9B836CF8B05D8189EA9CB1712CCA85B0E96A3330A63BE7CD9E5ECD22.bin => 5A70C79871C7B5DF84C5CF3C4D86EF25580F3F5916126CAC201AD67B3D644691.bin} (54%) delete mode 100644 test_fixtures/masp_proofs/9883C2EF7971504BB1CF651BAFFC3DA2C57E4FD8FF0811D9CB129887F0F9F706.bin delete mode 100644 test_fixtures/masp_proofs/99393E3AC8046F86ABA05519568B6780B6F18A312AE3909BEA19D16FCFE837DC.bin delete mode 100644 test_fixtures/masp_proofs/BA4FED83467B6FEE522748C6F7E72A01F0B169F946835583DC2C71B550315603.bin delete mode 100644 test_fixtures/masp_proofs/C7ECE8C02C2E764EFD5B6A0756CFE8EEC08E2D8512695A667D294AE1A4A8D4E6.bin delete mode 100644 test_fixtures/masp_proofs/EE7C912B7E21F07494D58AA6668DC6BBB31619C7E93A1A5A2E64B694DBE1BD6E.bin delete mode 100644 test_fixtures/masp_proofs/EEB91EB873807EC77BBCA95D4CFA3F379DB351AB4AE081207ABFDFC429C9FA48.bin delete mode 100644 test_fixtures/masp_proofs/F068FDF05B8F25DD923E667215344FFFAA6CA273027CD480AEA68DDED57D88CA.bin delete mode 100644 test_fixtures/masp_proofs/F36A8353F15FD6D8158DBC67DDB827EEEDA858AB983D16024AAA415579A68953.bin rename test_fixtures/masp_proofs/{8B29BC2E1A96DF331C7C3A2B227C98D1E5AAAA9988F26B1A47090ACCE693572F.bin => F39ADA174E1FB9F45BFFB0C68625A3F8ED350401B95715A6AF9BDD8CD1C367BA.bin} (52%) diff --git a/apps/src/lib/config/genesis/templates.rs b/apps/src/lib/config/genesis/templates.rs index 15defbd17f..0296907a46 100644 --- a/apps/src/lib/config/genesis/templates.rs +++ b/apps/src/lib/config/genesis/templates.rs @@ -210,10 +210,9 @@ pub struct Tokens { )] pub struct TokenConfig { pub denom: Denomination, - pub parameters: token::Parameters + pub parameters: token::Parameters, } - #[derive( Clone, Debug, diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 2b36a4e7a8..2038ff30fb 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -336,7 +336,6 @@ where total_token_balance, ) .unwrap(); - } } diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 8a282b5052..5e3c6ac2a3 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -1498,6 +1498,7 @@ mod test_utils { use data_encoding::HEXUPPER; use namada::core::ledger::storage::EPOCH_SWITCH_BLOCKS_DELAY; + use namada::ledger::parameters::{EpochDuration, Parameters}; use namada::ledger::storage::mockdb::MockDB; use namada::ledger::storage::{ update_allowed_conversions, LastBlock, Sha256Hasher, @@ -1518,11 +1519,9 @@ mod test_utils { use namada::types::transaction::{Fee, TxType, WrapperTx}; use tempfile::tempdir; use tokio::sync::mpsc::{Sender, UnboundedReceiver}; - use namada::ledger::parameters::{EpochDuration, Parameters}; use super::*; use crate::config::ethereum_bridge::ledger::ORACLE_CHANNEL_BUFFER_SIZE; - use crate::facade::tendermint_proto::abci::{ Misbehavior, RequestInitChain, RequestPrepareProposal, RequestProcessProposal, @@ -2038,7 +2037,10 @@ mod test_utils { .new_epoch(BlockHeight(1)); // initialize parameter storage let params = Parameters { - epoch_duration: EpochDuration { min_num_of_blocks: 1, min_duration: DurationSecs(3600) }, + epoch_duration: EpochDuration { + min_num_of_blocks: 1, + min_duration: DurationSecs(3600), + }, max_expected_time_per_block: DurationSecs(3600), max_proposal_bytes: Default::default(), max_block_gas: 100, @@ -2055,7 +2057,9 @@ mod test_utils { fee_unshielding_descriptions_limit: 0, minimum_gas_price: Default::default(), }; - params.init_storage(&mut shell.wl_storage).expect("Test failed"); + params + .init_storage(&mut shell.wl_storage) + .expect("Test failed"); // make wl_storage to update conversion for a new epoch let token_params = token::Parameters { max_reward_rate: Default::default(), @@ -2068,17 +2072,36 @@ mod test_utils { for (token, _) in address::tokens() { let addr = address::gen_deterministic_established_address(token); token_params.init_storage(&addr, &mut shell.wl_storage); - shell.wl_storage.write(&token::minted_balance_key(&addr), token::Amount::zero()).unwrap(); - shell.wl_storage.storage.conversion_state.tokens.insert( - token.to_string(), - addr, - ); + shell + .wl_storage + .write(&token::minted_balance_key(&addr), token::Amount::zero()) + .unwrap(); + shell + .wl_storage + .storage + .conversion_state + .tokens + .insert(token.to_string(), addr); } - shell.wl_storage.storage.conversion_state.tokens.insert("nam".to_string(), shell.wl_storage.storage.native_token.clone()); - token_params.init_storage(&shell.wl_storage.storage.native_token.clone(), &mut shell.wl_storage); - // final adjustments so that updating allowed conversions doesn't panic with - // divide by zero - shell.wl_storage.write(&token::minted_balance_key(&shell.wl_storage.storage.native_token.clone()), token::Amount::zero()).unwrap(); + shell.wl_storage.storage.conversion_state.tokens.insert( + "nam".to_string(), + shell.wl_storage.storage.native_token.clone(), + ); + token_params.init_storage( + &shell.wl_storage.storage.native_token.clone(), + &mut shell.wl_storage, + ); + // final adjustments so that updating allowed conversions doesn't panic + // with divide by zero + shell + .wl_storage + .write( + &token::minted_balance_key( + &shell.wl_storage.storage.native_token.clone(), + ), + token::Amount::zero(), + ) + .unwrap(); shell.wl_storage.storage.conversion_state.normed_inflation = Some(1); update_allowed_conversions(&mut shell.wl_storage) .expect("update conversions failed"); diff --git a/apps/src/lib/node/ledger/storage/mod.rs b/apps/src/lib/node/ledger/storage/mod.rs index 1a6a880063..7a1f44e6f9 100644 --- a/apps/src/lib/node/ledger/storage/mod.rs +++ b/apps/src/lib/node/ledger/storage/mod.rs @@ -54,6 +54,7 @@ mod tests { use std::collections::HashMap; use itertools::Itertools; + use namada::ledger::parameters::{EpochDuration, Parameters}; use namada::ledger::storage::write_log::WriteLog; use namada::ledger::storage::{ types, update_allowed_conversions, WlStorage, @@ -62,13 +63,12 @@ mod tests { use namada::types::chain::ChainId; use namada::types::hash::Hash; use namada::types::storage::{BlockHash, BlockHeight, Key}; + use namada::types::time::DurationSecs; use namada::types::{address, storage, token}; use proptest::collection::vec; use proptest::prelude::*; use proptest::test_runner::Config; use tempfile::TempDir; - use namada::ledger::parameters::{EpochDuration, Parameters}; - use namada::types::time::DurationSecs; use super::*; @@ -140,7 +140,10 @@ mod tests { let mut wl_storage = WlStorage::new(WriteLog::default(), storage); // initialize parameter storage let params = Parameters { - epoch_duration: EpochDuration { min_num_of_blocks: 1, min_duration: DurationSecs(3600) }, + epoch_duration: EpochDuration { + min_num_of_blocks: 1, + min_duration: DurationSecs(3600), + }, max_expected_time_per_block: DurationSecs(3600), max_proposal_bytes: Default::default(), max_block_gas: 100, @@ -164,7 +167,11 @@ mod tests { .write(&key, value_bytes.clone()) .expect("write failed"); wl_storage.storage.block.epoch = wl_storage.storage.block.epoch.next(); - wl_storage.storage.block.pred_epochs.new_epoch(BlockHeight(100)); + wl_storage + .storage + .block + .pred_epochs + .new_epoch(BlockHeight(100)); // make wl_storage to update conversion for a new epoch let token_params = token::Parameters { @@ -178,16 +185,33 @@ mod tests { for (token, _) in address::tokens() { let addr = address::gen_deterministic_established_address(token); token_params.init_storage(&addr, &mut wl_storage); - wl_storage.write(&token::minted_balance_key(&addr), token::Amount::zero()).unwrap(); - wl_storage.storage.conversion_state.tokens.insert( - token.to_string(), - addr, - ); + wl_storage + .write(&token::minted_balance_key(&addr), token::Amount::zero()) + .unwrap(); + wl_storage + .storage + .conversion_state + .tokens + .insert(token.to_string(), addr); } - wl_storage.storage.conversion_state.tokens.insert("nam".to_string(), wl_storage.storage.native_token.clone()); - token_params.init_storage(&wl_storage.storage.native_token.clone(), &mut wl_storage); + wl_storage + .storage + .conversion_state + .tokens + .insert("nam".to_string(), wl_storage.storage.native_token.clone()); + token_params.init_storage( + &wl_storage.storage.native_token.clone(), + &mut wl_storage, + ); - wl_storage.write(&token::minted_balance_key(&wl_storage.storage.native_token.clone()), token::Amount::zero()).unwrap(); + wl_storage + .write( + &token::minted_balance_key( + &wl_storage.storage.native_token.clone(), + ), + token::Amount::zero(), + ) + .unwrap(); wl_storage.storage.conversion_state.normed_inflation = Some(1); update_allowed_conversions(&mut wl_storage) .expect("update conversions failed"); diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 343fc3e3e5..372cd9a2c0 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1027,17 +1027,15 @@ impl Parameters { locked_ratio_target: locked_target, } = self; wl_storage - .write( - &masp_last_inflation_key(&address), - Amount::zero(), - ) - .expect("last inflation key for the given asset must be initialized"); + .write(&masp_last_inflation_key(address), Amount::zero()) + .expect( + "last inflation key for the given asset must be initialized", + ); wl_storage - .write( - &masp_last_locked_ratio_key(&address), - Dec::zero(), - ) - .expect("last locked ratio key for the given asset must be initialized"); + .write(&masp_last_locked_ratio_key(address), Dec::zero()) + .expect( + "last locked ratio key for the given asset must be initialized", + ); wl_storage .write(&masp_max_reward_rate_key(address), max_rate) .expect("max reward rate for the given asset must be initialized"); diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index ffe85f0358..f977ff4a0c 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -2780,8 +2780,8 @@ where insert_validator_into_validator_set( storage, - ¶ms, - &address, + params, + address, token::Amount::zero(), current_epoch, offset, diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 7d5f79f099..35c1d04fae 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -1545,15 +1545,8 @@ fn test_validator_sets() { // Because `update_validator_set` and `update_validator_deltas` are // effective from pipeline offset, we use pipeline epoch for the rest of the // checks - update_validator_set( - &mut s, - ¶ms, - &val1, - -unbond.change(), - epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val1, -unbond.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -1748,15 +1741,8 @@ fn test_validator_sets() { let bond = token::Amount::from_uint(500_000, 0).unwrap(); let stake6 = stake6 + bond; println!("val6 {val6} new stake {}", stake6.to_string_native()); - update_validator_set( - &mut s, - ¶ms, - &val6, - bond.change(), - epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val6, bond.change(), epoch, None) + .unwrap(); update_validator_deltas(&mut s, ¶ms, &val6, bond.change(), epoch, None) .unwrap(); let val6_bond_epoch = pipeline_epoch; @@ -2030,15 +2016,8 @@ fn test_validator_sets_swap() { assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake2), 0); assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake3), 0); - update_validator_set( - &mut s, - ¶ms, - &val2, - bond2.change(), - epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val2, bond2.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -2049,15 +2028,8 @@ fn test_validator_sets_swap() { ) .unwrap(); - update_validator_set( - &mut s, - ¶ms, - &val3, - bond3.change(), - epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val3, bond3.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -2083,15 +2055,8 @@ fn test_validator_sets_swap() { into_tm_voting_power(params.tm_votes_per_token, stake3) ); - update_validator_set( - &mut s, - ¶ms, - &val2, - bonds.change(), - epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val2, bonds.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -2102,15 +2067,8 @@ fn test_validator_sets_swap() { ) .unwrap(); - update_validator_set( - &mut s, - ¶ms, - &val3, - bonds.change(), - epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val3, bonds.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, diff --git a/test_fixtures/masp_proofs/00E4DF5E2DCAD1EC5923537C64B3D80CA615B4BBA75BDD5AA773E1E33792217D.bin b/test_fixtures/masp_proofs/00E4DF5E2DCAD1EC5923537C64B3D80CA615B4BBA75BDD5AA773E1E33792217D.bin new file mode 100644 index 0000000000000000000000000000000000000000..648bc521605b4d51b77d4d6163cdf12b860706a7 GIT binary patch literal 7448 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 diff --git a/test_fixtures/masp_proofs/37332141CB34FC30FF51F4BEE8D76149D3088F539CF8372D404609B89B095EF7.bin b/test_fixtures/masp_proofs/37332141CB34FC30FF51F4BEE8D76149D3088F539CF8372D404609B89B095EF7.bin deleted file mode 100644 index 18f83d0543674902e112707b9a890c8baf916c7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7448 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 diff --git a/test_fixtures/masp_proofs/52984E26D4A044A259B441C1DAEB66F886B3A3B6C71D33F456B859D01DA47ADD.bin b/test_fixtures/masp_proofs/52984E26D4A044A259B441C1DAEB66F886B3A3B6C71D33F456B859D01DA47ADD.bin deleted file mode 100644 index c51b5ed0d978ab920e1c513daaffdee73adc4aed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9208 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 Od;j+x0s`nCH~$7@I|zCJ diff --git a/test_fixtures/masp_proofs/1362F1CF9B836CF8B05D8189EA9CB1712CCA85B0E96A3330A63BE7CD9E5ECD22.bin b/test_fixtures/masp_proofs/5A70C79871C7B5DF84C5CF3C4D86EF25580F3F5916126CAC201AD67B3D644691.bin similarity index 54% rename from test_fixtures/masp_proofs/1362F1CF9B836CF8B05D8189EA9CB1712CCA85B0E96A3330A63BE7CD9E5ECD22.bin rename to test_fixtures/masp_proofs/5A70C79871C7B5DF84C5CF3C4D86EF25580F3F5916126CAC201AD67B3D644691.bin index 741f4d5106a2c0107b1dbbcb346b1da4d04cc5fa..f1c8aa5c63490eff0eb4bc67a0feabd687ccc7dd 100644 GIT binary patch delta 2142 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 Yx6j}GyTc!+KWFv+m;Lqmb9t421Kf{AI{*Lx diff --git a/test_fixtures/masp_proofs/99393E3AC8046F86ABA05519568B6780B6F18A312AE3909BEA19D16FCFE837DC.bin b/test_fixtures/masp_proofs/99393E3AC8046F86ABA05519568B6780B6F18A312AE3909BEA19D16FCFE837DC.bin deleted file mode 100644 index f456d94d7d508b81413aada12461d083d9b29c0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18792 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 diff --git a/test_fixtures/masp_proofs/BA4FED83467B6FEE522748C6F7E72A01F0B169F946835583DC2C71B550315603.bin b/test_fixtures/masp_proofs/BA4FED83467B6FEE522748C6F7E72A01F0B169F946835583DC2C71B550315603.bin deleted file mode 100644 index 565d189c0ce9f5b3fc605e1d63a57a4b3ff949e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19947 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 NzCZiK~)dy{2)v!?Iv~r|L{bjoXQNN0??% zC)8Vfc>Y$J&x{-d+Kqsv=!>+iXWZ12965yPK5KsQ!Sn5Ox!ZdvZIec2Ga4kYC^i$2 zk&B`9p4}LlZ=fEqnnS*-T*6Y%f}cwcvMC| z@%BTx&l6L4c#XrgR+HY3({7o!Oi3XPv`UtMh8LgM5}2Vbha(y~DQ#F7t;?RjZijwS z+k~fy>ly|$vBMJ!JK*@+bW_>Ue;PZ$j4kb9m! zqo)CaQPv?815&S=46xFM-=Tz_CC70hx7T61=({Un@gP2XW-u%Otcc$XIUnjl4e&qV z2Nn)_m{ou6MID(wS?)Yt&Q*Ov1uC#_>f!t(*uH~tr-t3}72onzEicT20WJXAM9RdJ z8Hji=6=?r31b?+%w zH)6x3thvA>JyU|rydeMzq==QGrc@o-VcY6x{9fjE`FVI=m8CsMj(%z`=xl@n1XS

uE-H%Yk6E~GN6&ybD%-m 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 diff --git a/test_fixtures/masp_proofs/EE7C912B7E21F07494D58AA6668DC6BBB31619C7E93A1A5A2E64B694DBE1BD6E.bin b/test_fixtures/masp_proofs/EE7C912B7E21F07494D58AA6668DC6BBB31619C7E93A1A5A2E64B694DBE1BD6E.bin deleted file mode 100644 index fee4361a2b54e7e9b59cdc715893a58d59f9ada9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7448 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 diff --git a/test_fixtures/masp_proofs/F36A8353F15FD6D8158DBC67DDB827EEEDA858AB983D16024AAA415579A68953.bin b/test_fixtures/masp_proofs/F36A8353F15FD6D8158DBC67DDB827EEEDA858AB983D16024AAA415579A68953.bin deleted file mode 100644 index 755c32011e4d8f737561ac85b8cb3357c052506c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9649 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 S<8^*Kf>!4E`4(MeJ1o diff --git a/test_fixtures/masp_proofs/8B29BC2E1A96DF331C7C3A2B227C98D1E5AAAA9988F26B1A47090ACCE693572F.bin b/test_fixtures/masp_proofs/F39ADA174E1FB9F45BFFB0C68625A3F8ED350401B95715A6AF9BDD8CD1C367BA.bin similarity index 52% rename from test_fixtures/masp_proofs/8B29BC2E1A96DF331C7C3A2B227C98D1E5AAAA9988F26B1A47090ACCE693572F.bin rename to test_fixtures/masp_proofs/F39ADA174E1FB9F45BFFB0C68625A3F8ED350401B95715A6AF9BDD8CD1C367BA.bin index afcad7c65449afcb2942ccb2e2db2169ed4ffc23..b04b1f1f04a611396d4fa9e38ba6cd92e118182c 100644 GIT binary patch delta 2133 zcmc(geKZsLAIE2R9+#1^@Jq;S$RZD0o1Zq%RE&Gey&fy%QvDioiDV2RB~0j&8a8bn zW+wJaG!G%dMNjhBRA{-^G`ShM)BWe3d(XLk=luS-pL0Hcec!M1`F`H-?>XNhToH~e z0{{Rtqz#fbZ%MdR5`0AW5~;RE3qJr>YKMBiEi7S4FsI^ z)+M)Ib2dkltjg-Cd{*c9nEmGT-?0F|9_jI>1)IIq`cz{XH~3bfO^@>Aow#E%hk<%q znL_0#x^Z(rQsq_-d{H{dOO;#g%jGC00?Gf&H8-94Qyl+Klz&W;{0~G40P(Lao1H0* zDemt>PnpV>`O#68mNS%A6{0!mWl1&=Z@j;IUV`P+e>_+xy5+i%=kL;X;p?Q-)f!8; z$z($90T4A<*-Gy?(Z~G?rPcekh4?(j)l<)U+lQ5@l%hGmqh8HGyR{W=ld>LA^b`JP_*B(+wra^r4mDxkSPH`c~8Y~y}DuC~c^&NVX z1M+#S+E{v26o`*9(h#gNGK*43&-K;tte|&`FFw`E3Ji+hPiIw#tc=uW$~zhDlNQu0 zsFmJheh1xH9A_8-{>rcW^A1s!$pm{&=hi4(aQU0eeR0v;DX4Tl{ixS$bziM7qAS)5 zBp5$TRe^aD;+tQa+&`E23WRfMxS{V9XtCX}ooTgOj0mtFT6)K;fu z_%#FHmiodmv}<%QwCSG7-R-AQnAG;NP@2Q~fVpIJtt8fbZK{p!#3W^DYSl1!=CWguhmKds*@ z4q<)8Shwf|HHhu8N++Prm;phM&=MQuBb)&f7QWvb8NzF?`Z*54L7DG85)uQtZq|`A zH+c_hM{$lUtFnXUJ;9RMEQ`p|q2YZm2MRaD#F6e5W8&3BlmIDTm+=;6(inA)cb0PU)Gq^ybJ>ckVK=bcfjSIiqIk#~-Jb29zyBmAQ) z+dNE$iz5ZB3^yuM4r3HHWAjSu@`-_=O2-#Q-q>XWiWKwwb3burB zfo`IC;0=V%EU4q}K_uALR|FRlF0<%$UECFS^iEoSlL;J%HEB^669r^IEtow|uh&5iVB zS!S6AV3#>fR~DcMHMos2(md*9$jCU+T({r#!d+g0b_be*k8Av0onlrmFPKE)7i1Wr(|~KVs)j}pWR3Z48>Q6VCrUG1 zL}t}GfO=lrq}!{-db8Rx!_F==BIk2MRl*Oay}$|iYWU8JbU@})Fdx3}F1>5A>`O#hXEnypB|4mwaZ=amfD-4JKz;J0r zO$3bPz`LLo7$6=vdKPUhtBM-DlKDh>Wtz($KvRC^&}rL$t&1FWtP62iEvJE( zSBAAUZIKhtc3VV^$W7f98QE4;A)VcO87ZxVx20u?_VO9&N}K=4W`U>+JNNtsyU5x* delta 2133 zcmbPXHN&c&k%56h6o^@Xc!AEJ>w$YXE~hT5W zF)arwW7ra~HB_^`>PUs-qG+B&W(g*tSAHfv;#}KTGkf;soB4i$QOvRI#j#=s<7GVW z9cp|Qq?dgYqz?qp^wxtl8;Gk<-@U%7uwkYSOHC7VvgzY5f#j=Ipuo?<0s- z49#G@B^HNviR^r-xT9vBpz{Ww=9jzQelUEqx9Xa)=o9bM!bAAA86V3s5DY5CE%mWe2JxtZ+`k>{r{>PA2bsvg0&Igu>7Ah32Qd`Dbq5zo%wv-tm_?l8w)&c-wwB z6S#tL&2o>W=fo5ymkXFE=>F8xiP>g4QU3iW;n+St7MY_ zE)uk+MSkI&>_cmI_Z*)z;nhSnjrv)MmVHS}Ua!m8nEaa|4^xLl|oGrY5aDvdClGiUX0?7uv3CUM&9pm~d2ZR?6%> z*JiQldR%J}=t8FKE@@Si`K$w%}~_|ID=o?OA39u7>}*7#s41A8{#$@09kGdz$z8*6Q;c z?y?$%)amuU(z|?ke|q7&C$+c2HU8P=JLdGxYBgWl%FoCZe}E}w$N%!k8{ZvHevnAh z2-18R!QCNRq9NsbAZ$^dt#C-*wc{O&f0lAA>V5ntlDRQr`GQ8b`rjJY{yMDN&{fKxJ4#+#g3lM5WmK*ap`+(@t32)lUDHV+hicX$~%qq%l#X%ex5r{dMpYM zlEzkIvWOmd|)(zpcS3YD>(k!}OG&uI_jZ;D_HMu)d z@`H|M7aB9ak13nDTV!?O+%<=nAGVoV@-;Xy!#3fboL_)i4q(7C`K^pL0As4% Ag#Z8m diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index 433770bfa5..0b93d0df94 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -1411,7 +1411,7 @@ fn check_balances( ); client.exp_string(&expected)?; // Check the source balance - let expected = ": 900000, owned by albert".to_string(); + let expected = ": 880000, owned by albert".to_string(); client.exp_string(&expected)?; client.assert_success(); @@ -1475,7 +1475,7 @@ fn check_balances_after_back( ); client.exp_string(&expected)?; // Check the source balance - let expected = ": 950000, owned by albert".to_string(); + let expected = ": 930000, owned by albert".to_string(); client.exp_string(&expected)?; client.assert_success(); diff --git a/tests/src/integration/masp.rs b/tests/src/integration/masp.rs index f01b97c01f..d644e5e8db 100644 --- a/tests/src/integration/masp.rs +++ b/tests/src/integration/masp.rs @@ -8,11 +8,7 @@ use namada_sdk::masp::fs::FsShieldedUtils; use test_log::test; use super::setup; -use crate::e2e::setup::constants::{ - AA_PAYMENT_ADDRESS, AA_VIEWING_KEY, AB_PAYMENT_ADDRESS, AB_VIEWING_KEY, - AC_PAYMENT_ADDRESS, AC_VIEWING_KEY, ALBERT, A_SPENDING_KEY, - BB_PAYMENT_ADDRESS, BERTHA, BTC, B_SPENDING_KEY, CHRISTEL, ETH, MASP, NAM, -}; +use crate::e2e::setup::constants::{AA_PAYMENT_ADDRESS, AA_VIEWING_KEY, AB_PAYMENT_ADDRESS, AB_VIEWING_KEY, AC_PAYMENT_ADDRESS, AC_VIEWING_KEY, ALBERT, A_SPENDING_KEY, BB_PAYMENT_ADDRESS, BERTHA, BTC, B_SPENDING_KEY, CHRISTEL, ETH, MASP, NAM, BERTHA_KEY, ALBERT_KEY, CHRISTEL_KEY}; /// In this test we verify that users of the MASP receive the correct rewards /// for leaving their assets in the pool for varying periods of time. @@ -347,7 +343,7 @@ fn masp_incentives() -> Result<()> { "--amount", "10", "--signing-keys", - BERTHA, + BERTHA_KEY, "--node", validator_one_rpc, ], @@ -433,7 +429,7 @@ fn masp_incentives() -> Result<()> { "--amount", "20", "--signing-keys", - ALBERT, + ALBERT_KEY, "--node", validator_one_rpc, ], @@ -578,7 +574,7 @@ fn masp_incentives() -> Result<()> { "--amount", "141.49967", "--signing-keys", - BERTHA, + BERTHA_KEY, "--node", validator_one_rpc, ], @@ -603,7 +599,7 @@ fn masp_incentives() -> Result<()> { "--amount", "1980.356", "--signing-keys", - ALBERT, + ALBERT_KEY, "--node", validator_one_rpc, ], @@ -867,7 +863,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "10", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -886,7 +882,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "15", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -922,7 +918,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "10", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -941,7 +937,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "7", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -960,7 +956,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "7", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -979,7 +975,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "7", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -998,7 +994,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "6", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -1054,7 +1050,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "20", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], From fa976ca6bf8c1fa1a768c49546aa09521bbb8f33 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 26 Oct 2023 16:13:03 +0200 Subject: [PATCH 06/47] [feat] Re-implementing new genesis flow in v0.24 --- Cargo.lock | 1 + Makefile | 14 +- README.md | 4 - apps/Cargo.toml | 6 +- apps/build.rs | 9 - apps/src/bin/namada-client/cli.rs | 0 apps/src/bin/namada-node/cli.rs | 33 +- apps/src/bin/namada/cli.rs | 2 +- apps/src/lib/bench_utils.rs | 1 - apps/src/lib/cli.rs | 564 +++++-- apps/src/lib/cli/client.rs | 6 + apps/src/lib/cli/context.rs | 161 +- apps/src/lib/cli/wallet.rs | 148 +- apps/src/lib/client/types.rs | 0 apps/src/lib/client/utils.rs | 1024 ++++-------- apps/src/lib/config/genesis.rs | 1168 +++---------- apps/src/lib/config/genesis/chain.rs | 872 ++++++++++ apps/src/lib/config/genesis/templates.rs | 991 +++++++++++ apps/src/lib/config/genesis/toml_utils.rs | 38 + apps/src/lib/config/genesis/transactions.rs | 1483 +++++++++++++++++ apps/src/lib/config/global.rs | 19 +- apps/src/lib/config/mod.rs | 2 +- apps/src/lib/config/utils.rs | 24 + apps/src/lib/node/ledger/mod.rs | 9 +- .../lib/node/ledger/shell/finalize_block.rs | 8 +- apps/src/lib/node/ledger/shell/init_chain.rs | 808 +++++---- apps/src/lib/node/ledger/shell/mod.rs | 54 +- .../shell/vote_extensions/bridge_pool_vext.rs | 1 + apps/src/lib/node/ledger/shims/abcipp_shim.rs | 3 - apps/src/lib/node/ledger/storage/mod.rs | 2 - apps/src/lib/node/ledger/tendermint_node.rs | 40 +- apps/src/lib/wallet/alias.rs | 0 apps/src/lib/wallet/cli_utils.rs | 517 ------ apps/src/lib/wallet/defaults.rs | 149 +- apps/src/lib/wallet/mod.rs | 41 +- apps/src/lib/wallet/pre_genesis.rs | 2 +- apps/src/lib/wallet/store.rs | 61 +- core/src/ledger/pgf/parameters.rs | 3 + core/src/ledger/storage/masp_conversions.rs | 16 +- core/src/ledger/storage_api/pgf.rs | 4 +- core/src/proto/mod.rs | 5 +- core/src/proto/types.rs | 24 + core/src/types/address.rs | 151 +- core/src/types/chain.rs | 5 - core/src/types/dec.rs | 40 +- core/src/types/key/common.rs | 66 +- core/src/types/key/dkg_session_keys.rs | 32 +- core/src/types/key/mod.rs | 1 - core/src/types/masp.rs | 253 ++- core/src/types/mod.rs | 1 + core/src/types/storage.rs | 2 +- core/src/types/string_encoding.rs | 231 +++ core/src/types/time.rs | 42 +- core/src/types/token.rs | 165 +- core/src/types/uint.rs | 5 + ethereum_bridge/src/parameters.rs | 22 +- ethereum_bridge/src/test_utils.rs | 18 +- genesis/README.md | 153 ++ genesis/dev.toml | 247 --- genesis/e2e-tests-single-node.toml | 260 --- genesis/localnet/README.md | 66 + genesis/localnet/balances.toml | 110 ++ genesis/localnet/parameters.toml | 90 + .../src/pre-genesis/signed-transactions.toml | 126 ++ .../pre-genesis/unsigned-transactions.toml | 114 ++ .../pre-genesis/validator-0/transactions.toml | 44 + .../validator-0/validator-wallet.toml | 11 + genesis/localnet/src/pre-genesis/wallet.toml | 34 + genesis/localnet/tokens.toml | 22 + genesis/localnet/transactions.toml | 180 ++ genesis/localnet/validity-predicates.toml | 17 + genesis/starter/README.md | 71 + genesis/starter/balances.toml | 2 + genesis/starter/parameters.toml | 90 + genesis/starter/tokens.toml | 5 + genesis/starter/transactions.toml | 1 + genesis/starter/validity-predicates.toml | 22 + proof_of_stake/src/epoched.rs | 2 +- proof_of_stake/src/lib.rs | 371 +++-- proof_of_stake/src/tests.rs | 42 +- proof_of_stake/src/tests/state_machine.rs | 4 +- proof_of_stake/src/tests/state_machine_v2.rs | 4 +- sdk/Cargo.toml | 1 + sdk/src/args.rs | 38 + sdk/src/error.rs | 2 +- sdk/src/wallet/alias.rs | 73 +- sdk/src/wallet/mod.rs | 16 +- sdk/src/wallet/store.rs | 101 +- shared/Cargo.toml | 2 - shared/build.rs | 12 - .../ethereum_bridge/bridge_pool_vp.rs | 4 +- .../ledger/native_vp/ethereum_bridge/vp.rs | 4 +- shared/src/ledger/native_vp/ibc/mod.rs | 7 +- shared/src/ledger/pos/mod.rs | 22 +- shared/src/ledger/vp_host_fns.rs | 2 +- tests/src/e2e/eth_bridge_tests.rs | 8 +- tests/src/e2e/eth_bridge_tests/helpers.rs | 4 +- tests/src/e2e/setup.rs | 418 +++-- tests/src/integration/setup.rs | 138 +- tests/src/native_vp/eth_bridge_pool.rs | 4 +- tests/src/native_vp/pos.rs | 2 +- tests/src/vm_host_env/ibc.rs | 8 +- tx_prelude/src/proof_of_stake.rs | 3 +- 103 files changed, 7981 insertions(+), 4327 deletions(-) delete mode 100644 apps/src/bin/namada-client/cli.rs delete mode 100644 apps/src/lib/client/types.rs create mode 100644 apps/src/lib/config/genesis/chain.rs create mode 100644 apps/src/lib/config/genesis/templates.rs create mode 100644 apps/src/lib/config/genesis/toml_utils.rs create mode 100644 apps/src/lib/config/genesis/transactions.rs delete mode 100644 apps/src/lib/wallet/alias.rs delete mode 100644 apps/src/lib/wallet/cli_utils.rs create mode 100644 core/src/types/string_encoding.rs create mode 100644 genesis/README.md delete mode 100644 genesis/dev.toml delete mode 100644 genesis/e2e-tests-single-node.toml create mode 100644 genesis/localnet/README.md create mode 100644 genesis/localnet/balances.toml create mode 100644 genesis/localnet/parameters.toml create mode 100644 genesis/localnet/src/pre-genesis/signed-transactions.toml create mode 100644 genesis/localnet/src/pre-genesis/unsigned-transactions.toml create mode 100644 genesis/localnet/src/pre-genesis/validator-0/transactions.toml create mode 100644 genesis/localnet/src/pre-genesis/validator-0/validator-wallet.toml create mode 100644 genesis/localnet/src/pre-genesis/wallet.toml create mode 100644 genesis/localnet/tokens.toml create mode 100644 genesis/localnet/transactions.toml create mode 100644 genesis/localnet/validity-predicates.toml create mode 100644 genesis/starter/README.md create mode 100644 genesis/starter/balances.toml create mode 100644 genesis/starter/parameters.toml create mode 100644 genesis/starter/tokens.toml create mode 100644 genesis/starter/transactions.toml create mode 100644 genesis/starter/validity-predicates.toml delete mode 100644 shared/build.rs diff --git a/Cargo.lock b/Cargo.lock index f439e19e62..e171ac3ad9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4234,6 +4234,7 @@ version = "0.24.1" dependencies = [ "assert_matches", "async-trait", + "base58 0.2.0", "bimap", "borsh 1.0.0-alpha.4", "borsh-ext", diff --git a/Makefile b/Makefile index dc67c3c431..f535c04cff 100644 --- a/Makefile +++ b/Makefile @@ -54,16 +54,16 @@ build-test: $(cargo) +$(nightly) build --tests $(jobs) build-release: - NAMADA_DEV=false $(cargo) build $(jobs) --release --timings --package namada_apps --manifest-path Cargo.toml + $(cargo) build $(jobs) --release --timings --package namada_apps --manifest-path Cargo.toml build-debug: - NAMADA_DEV=false $(cargo) build --package namada_apps --manifest-path Cargo.toml + $(cargo) build --package namada_apps --manifest-path Cargo.toml install-release: - NAMADA_DEV=false $(cargo) install --path ./apps --locked + $(cargo) install --path ./apps --locked check-release: - NAMADA_DEV=false $(cargo) check --release --package namada_apps + $(cargo) check --release --package namada_apps package: build-release scripts/make-package.sh @@ -88,7 +88,7 @@ check-crates: clippy-wasm = $(cargo) +$(nightly) clippy --manifest-path $(wasm)/Cargo.toml --all-targets -- -D warnings clippy: - NAMADA_DEV=false $(cargo) +$(nightly) clippy $(jobs) --all-targets -- -D warnings && \ + $(cargo) +$(nightly) clippy $(jobs) --all-targets -- -D warnings && \ make -C $(wasms) clippy && \ make -C $(wasms_for_tests) clippy && \ $(foreach wasm,$(wasm_templates),$(clippy-wasm) && ) true @@ -103,7 +103,7 @@ tendermint: ./scripts/get_tendermint.sh install: cometbft - NAMADA_DEV=false $(cargo) install --path ./apps --locked + $(cargo) install --path ./apps --locked cometbft: ./scripts/get_cometbft.sh @@ -167,7 +167,7 @@ test-integration-save-proofs: # Run integration tests without specifiying any pre-built MASP proofs option test-integration-slow: RUST_BACKTRACE=$(RUST_BACKTRACE) \ - $(cargo) +$(nightly) test integration::$(TEST_FILTER) \ + $(cargo) +$(nightly) test integration::$(TEST_FILTER) --features integration \ -Z unstable-options \ -- \ -Z unstable-options --report-time diff --git a/README.md b/README.md index 18d481dc5f..ce85949f3e 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,6 @@ Guide. ```shell # Build the provided validity predicate and transaction wasm modules make build-wasm-scripts-docker - -# Development (debug) build Namada, which includes a validator and some default -# accounts, whose keys and addresses are available in the wallet -NAMADA_DEV=true make ``` ### Before submitting a PR, pls make sure to run the following diff --git a/apps/Cargo.toml b/apps/Cargo.toml index ea94db19ac..22f2caf683 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -54,11 +54,11 @@ default = ["std", "abciplus"] mainnet = [ "namada/mainnet", ] -dev = ["namada/dev"] std = ["ed25519-consensus/std", "rand/std", "rand_core/std", "namada/std", "namada_sdk/std"] # for integration tests and test utilies -testing = ["dev", "namada_test_utils"] - +testing = ["namada_test_utils"] +benches = ["testing", "namada_test_utils"] +integration = [] abciplus = [ "namada/abciplus", "namada/tendermint-rpc", diff --git a/apps/build.rs b/apps/build.rs index 60735e7e4a..6ffb58476f 100644 --- a/apps/build.rs +++ b/apps/build.rs @@ -53,13 +53,4 @@ fn main() { // Tell Cargo that if the given file changes, to rerun this build script. println!("cargo:rerun-if-changed={}", PROTO_SRC); - - // Tell Cargo to build when the `NAMADA_DEV` env var changes - println!("cargo:rerun-if-env-changed=NAMADA_DEV"); - // Enable "dev" feature if `NAMADA_DEV` is trueish - if let Ok(dev) = env::var("NAMADA_DEV") { - if dev.to_ascii_lowercase().trim() == "true" { - println!("cargo:rustc-cfg=feature=\"dev\""); - } - } } diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/src/bin/namada-node/cli.rs b/apps/src/bin/namada-node/cli.rs index 6499a34e9e..7ee24251be 100644 --- a/apps/src/bin/namada-node/cli.rs +++ b/apps/src/bin/namada-node/cli.rs @@ -10,26 +10,31 @@ pub fn main() -> Result<()> { match cmd { cmds::NamadaNode::Ledger(sub) => match sub { cmds::Ledger::Run(cmds::LedgerRun(args)) => { - let wasm_dir = ctx.wasm_dir(); + let chain_ctx = ctx.take_chain_or_exit(); + let wasm_dir = chain_ctx.wasm_dir(); sleep_until(args.start_time); - ledger::run(ctx.config.ledger, wasm_dir); + ledger::run(chain_ctx.config.ledger, wasm_dir); } cmds::Ledger::RunUntil(cmds::LedgerRunUntil(args)) => { - let wasm_dir = ctx.wasm_dir(); + let mut chain_ctx = ctx.take_chain_or_exit(); + let wasm_dir = chain_ctx.wasm_dir(); sleep_until(args.time); - ctx.config.ledger.shell.action_at_height = + chain_ctx.config.ledger.shell.action_at_height = Some(args.action_at_height); - ledger::run(ctx.config.ledger, wasm_dir); + ledger::run(chain_ctx.config.ledger, wasm_dir); } cmds::Ledger::Reset(_) => { - ledger::reset(ctx.config.ledger) + let chain_ctx = ctx.take_chain_or_exit(); + ledger::reset(chain_ctx.config.ledger) .wrap_err("Failed to reset Namada node")?; } cmds::Ledger::DumpDb(cmds::LedgerDumpDb(args)) => { - ledger::dump_db(ctx.config.ledger, args); + let chain_ctx = ctx.take_chain_or_exit(); + ledger::dump_db(chain_ctx.config.ledger, args); } cmds::Ledger::RollBack(_) => { - ledger::rollback(ctx.config.ledger) + let chain_ctx = ctx.take_chain_or_exit(); + ledger::rollback(chain_ctx.config.ledger) .wrap_err("Failed to rollback the Namada node")?; } }, @@ -39,18 +44,18 @@ pub fn main() -> Result<()> { // In here, we just need to overwrite the default chain ID, in // case it's been already set to a different value if let Some(chain_id) = ctx.global_args.chain_id.as_ref() { - ctx.global_config.default_chain_id = chain_id.clone(); + ctx.global_config.default_chain_id = Some(chain_id.clone()); ctx.global_config .write(&ctx.global_args.base_dir) .unwrap_or_else(|err| { - eprintln!("Error writing global config: {}", err); + eprintln!("Error writing global config: {err}"); cli::safe_exit(1) }); + tracing::debug!( + "Generated config and set default chain ID to \ + {chain_id}" + ); } - tracing::debug!( - "Generated config and set default chain ID to {}", - &ctx.global_config.default_chain_id - ); } }, } diff --git a/apps/src/bin/namada/cli.rs b/apps/src/bin/namada/cli.rs index 0259ba525a..c7694117aa 100644 --- a/apps/src/bin/namada/cli.rs +++ b/apps/src/bin/namada/cli.rs @@ -63,7 +63,7 @@ fn handle_command(cmd: cli::cmds::Namada, raw_sub_cmd: String) -> Result<()> { fn handle_subcommand(program: &str, mut sub_args: Vec) -> Result<()> { let env_vars = env::vars_os(); - let cmd_name = if cfg!(feature = "dev") && env::var("CARGO").is_ok() { + let cmd_name = if env::var("CARGO").is_ok() { // When the command is ran from inside `cargo run`, we also want to // call the sub-command via `cargo run` to rebuild if necessary. // We do this by prepending the arguments with `cargo run` arguments. diff --git a/apps/src/lib/bench_utils.rs b/apps/src/lib/bench_utils.rs index db14551a76..5f58a93a58 100644 --- a/apps/src/lib/bench_utils.rs +++ b/apps/src/lib/bench_utils.rs @@ -167,7 +167,6 @@ impl Default for BenchShell { None, 50 * 1024 * 1024, // 50 kiB 50 * 1024 * 1024, // 50 kiB - address::nam(), ); shell diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 51e3b03a73..c7ddefef68 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -712,7 +712,7 @@ pub mod cmds { /// List all known payment addresses #[derive(Clone, Debug)] - pub struct MaspListPayAddrs; + pub struct MaspListPayAddrs(pub args::MaspListPayAddrs); impl SubCmd for MaspListPayAddrs { const CMD: &'static str = "list-addrs"; @@ -720,12 +720,13 @@ pub mod cmds { fn parse(matches: &ArgMatches) -> Option { matches .subcommand_matches(Self::CMD) - .map(|_matches| MaspListPayAddrs) + .map(|matches| Self(args::MaspListPayAddrs::parse(matches))) } fn def() -> App { App::new(Self::CMD) .about("Lists all payment addresses in the wallet") + .add_args::() } } @@ -903,7 +904,7 @@ pub mod cmds { /// List known addresses #[derive(Clone, Debug)] - pub struct AddressList; + pub struct AddressList(pub args::AddressList); impl SubCmd for AddressList { const CMD: &'static str = "list"; @@ -911,11 +912,13 @@ pub mod cmds { fn parse(matches: &ArgMatches) -> Option { matches .subcommand_matches(Self::CMD) - .map(|_matches| AddressList) + .map(|matches| Self(args::AddressList::parse(matches))) } fn def() -> App { - App::new(Self::CMD).about("List all known addresses.") + App::new(Self::CMD) + .about("List all known addresses.") + .add_args::() } } @@ -1959,6 +1962,8 @@ pub mod cmds { PkToTmAddress(PkToTmAddress), DefaultBaseDir(DefaultBaseDir), EpochSleep(EpochSleep), + ValidateGenesisTemplates(ValidateGenesisTemplates), + SignGenesisTx(SignGenesisTx), } impl SubCmd for Utils { @@ -1980,6 +1985,10 @@ pub mod cmds { let default_base_dir = SubCmd::parse(matches).map(Self::DefaultBaseDir); let epoch_sleep = SubCmd::parse(matches).map(Self::EpochSleep); + let validate_genesis_templates = + SubCmd::parse(matches).map(Self::ValidateGenesisTemplates); + let genesis_tx = + SubCmd::parse(matches).map(Self::SignGenesisTx); join_network .or(fetch_wasms) .or(validate_wasm) @@ -1988,6 +1997,8 @@ pub mod cmds { .or(pk_to_tm_address) .or(default_base_dir) .or(epoch_sleep) + .or(validate_genesis_templates) + .or(genesis_tx) }) } @@ -2002,6 +2013,8 @@ pub mod cmds { .subcommand(PkToTmAddress::def()) .subcommand(DefaultBaseDir::def()) .subcommand(EpochSleep::def()) + .subcommand(ValidateGenesisTemplates::def()) + .subcommand(SignGenesisTx::def()) .subcommand_required(true) .arg_required_else_help(true) } @@ -2109,6 +2122,44 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct ValidateGenesisTemplates(pub args::ValidateGenesisTemplates); + + impl SubCmd for ValidateGenesisTemplates { + const CMD: &'static str = "validate-genesis-templates"; + + fn parse(matches: &ArgMatches) -> Option { + matches.subcommand_matches(Self::CMD).map(|matches| { + Self(args::ValidateGenesisTemplates::parse(matches)) + }) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Validate genesis templates.") + .add_args::() + } + } + + #[derive(Clone, Debug)] + pub struct SignGenesisTx(pub args::SignGenesisTx); + + impl SubCmd for SignGenesisTx { + const CMD: &'static str = "sign-genesis-tx"; + + fn parse(matches: &ArgMatches) -> Option { + matches + .subcommand_matches(Self::CMD) + .map(|matches| Self(args::SignGenesisTx::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Sign genesis transaction(s).") + .add_args::() + } + } + /// Used as sub-commands (`SubCmd` instance) in `namadar` binary. #[derive(Clone, Debug)] pub enum EthBridgePool { @@ -2566,6 +2617,7 @@ pub mod args { use std::collections::HashMap; use std::convert::TryFrom; use std::env; + use std::net::SocketAddr; use std::path::PathBuf; use std::str::FromStr; @@ -2587,7 +2639,7 @@ pub mod args { use super::context::*; use super::utils::*; use super::{ArgGroup, ArgMatches}; - use crate::cli::context::FromContext; + use crate::client::utils::PRE_GENESIS_DIR; use crate::config::{self, Action, ActionAtHeight}; use crate::facade::tendermint::Timeout; use crate::facade::tendermint_config::net::Address as TendermintAddress; @@ -2708,6 +2760,7 @@ pub mod args { }), ); pub const GENESIS_PATH: Arg = arg("genesis-path"); + pub const GENESIS_TIME: Arg = arg("genesis-time"); pub const GENESIS_VALIDATOR: ArgOpt = arg("genesis-validator").opt(); pub const HALT_ACTION: ArgFlag = flag("halt"); @@ -2733,20 +2786,23 @@ pub mod args { arg("max-commission-rate-change"); pub const MAX_ETH_GAS: ArgOpt = arg_opt("max_eth-gas"); pub const MODE: ArgOpt = arg_opt("mode"); - pub const NET_ADDRESS: Arg = arg("net-address"); + pub const NET_ADDRESS: Arg = arg("net-address"); pub const NAMADA_START_TIME: ArgOpt = arg_opt("time"); pub const NO_CONVERSIONS: ArgFlag = flag("no-conversions"); pub const NUT: ArgFlag = flag("nut"); pub const OUT_FILE_PATH_OPT: ArgOpt = arg_opt("out-file-path"); + pub const OUTPUT: ArgOpt = arg_opt("output"); pub const OUTPUT_FOLDER_PATH: ArgOpt = arg_opt("output-folder-path"); pub const OWNER: Arg = arg("owner"); pub const OWNER_OPT: ArgOpt = OWNER.opt(); + pub const PATH: Arg = arg("path"); pub const PIN: ArgFlag = flag("pin"); pub const PORT_ID: ArgDefault = arg_default( "port-id", DefaultFn(|| PortId::from_str("transfer").unwrap()), ); + pub const PRE_GENESIS: ArgFlag = flag("pre-genesis"); pub const PROPOSAL_ETH: ArgFlag = flag("eth"); pub const PROPOSAL_PGF_STEWARD: ArgFlag = flag("pgf-stewards"); pub const PROPOSAL_PGF_FUNDING: ArgFlag = flag("pgf-funding"); @@ -2765,12 +2821,18 @@ pub mod args { pub const RAW_PUBLIC_KEY: Arg = arg("public-key"); pub const RAW_PUBLIC_KEY_OPT: ArgOpt = arg_opt("public-key"); + pub const RAW_SOURCE: Arg = arg("source"); pub const RECEIVER: Arg = arg("receiver"); pub const RELAYER: Arg

= arg("relayer"); pub const SAFE_MODE: ArgFlag = flag("safe-mode"); pub const SCHEME: ArgDefault = arg_default("scheme", DefaultFn(|| SchemeType::Ed25519)); + pub const SELF_BOND_AMOUNT: Arg = + arg("self-bond-amount"); pub const SENDER: Arg = arg("sender"); + pub const SIGNER: ArgOpt = arg_opt("signer"); + pub const SIGNING_KEY_OPT: ArgOpt = SIGNING_KEY.opt(); + pub const SIGNING_KEY: Arg = arg("signing-key"); pub const SIGNING_KEYS: ArgMulti = arg_multi("signing-keys"); pub const SIGNATURES: ArgMulti = arg_multi("signatures"); pub const SOURCE: Arg = arg("source"); @@ -2779,11 +2841,14 @@ pub mod args { pub const SOURCE_VALIDATOR: Arg = arg("source-validator"); pub const STORAGE_KEY: Arg = arg("storage-key"); pub const SUSPEND_ACTION: ArgFlag = flag("suspend"); + pub const TEMPLATES_PATH: Arg = arg("templates-path"); pub const TIMEOUT_HEIGHT: ArgOpt = arg_opt("timeout-height"); pub const TIMEOUT_SEC_OFFSET: ArgOpt = arg_opt("timeout-sec-offset"); pub const TM_ADDRESS: Arg = arg("tm-address"); pub const TOKEN_OPT: ArgOpt = TOKEN.opt(); pub const TOKEN: Arg = arg("token"); + pub const TRANSFER_FROM_SOURCE_AMOUNT: Arg = + arg("transfer-from-source-amount"); pub const TRANSFER_SOURCE: Arg = arg("source"); pub const TRANSFER_TARGET: Arg = arg("target"); pub const TX_HASH: Arg = arg("tx-hash"); @@ -3013,16 +3078,20 @@ pub mod args { impl CliToSdk> for EthereumBridgePool { fn to_sdk(self, ctx: &mut Context) -> EthereumBridgePool { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); EthereumBridgePool:: { nut: self.nut, - tx: self.tx.to_sdk(ctx), + tx, asset: self.asset, recipient: self.recipient, - sender: ctx.get(&self.sender), + sender: chain_ctx.get(&self.sender), amount: self.amount, fee_amount: self.fee_amount, - fee_payer: self.fee_payer.map(|fee_payer| ctx.get(&fee_payer)), - fee_token: ctx.get(&self.fee_token), + fee_payer: self + .fee_payer + .map(|fee_payer| chain_ctx.get(&fee_payer)), + fee_token: chain_ctx.get(&self.fee_token), code_path: self.code_path, } } @@ -3096,6 +3165,7 @@ pub mod args { impl CliToSdk> for RecommendBatch { fn to_sdk(self, ctx: &mut Context) -> RecommendBatch { + let chain_ctx = ctx.borrow_chain_or_exit(); RecommendBatch:: { query: self.query.to_sdk_ctxless(), max_gas: self.max_gas, @@ -3115,8 +3185,8 @@ pub mod args { .map(|(token, conversion_rate)| { let token_from_ctx = FromContext::
::new(token); - let address = ctx.get(&token_from_ctx); - let alias = token_from_ctx.into_raw(); + let address = chain_ctx.get(&token_from_ctx); + let alias = token_from_ctx.raw; ( address, BpConversionTableEntry { @@ -3500,7 +3570,7 @@ pub mod args { serialized_tx: self.serialized_tx.map(|path| { std::fs::read(path).expect("Expected a file at given path") }), - owner: ctx.get(&self.owner), + owner: ctx.borrow_chain_or_exit().get(&self.owner), } } } @@ -3558,13 +3628,15 @@ pub mod args { impl CliToSdk> for TxTransfer { fn to_sdk(self, ctx: &mut Context) -> TxTransfer { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); TxTransfer:: { - tx: self.tx.to_sdk(ctx), - source: ctx.get_cached(&self.source), - target: ctx.get(&self.target), - token: ctx.get(&self.token), + tx, + source: chain_ctx.get_cached(&self.source), + target: chain_ctx.get(&self.target), + token: chain_ctx.get(&self.token), amount: self.amount, - native_token: ctx.native_token.clone(), + native_token: chain_ctx.native_token.clone(), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -3606,11 +3678,13 @@ pub mod args { impl CliToSdk> for TxIbcTransfer { fn to_sdk(self, ctx: &mut Context) -> TxIbcTransfer { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); TxIbcTransfer:: { - tx: self.tx.to_sdk(ctx), - source: ctx.get(&self.source), + tx, + source: chain_ctx.get(&self.source), receiver: self.receiver, - token: ctx.get(&self.token), + token: chain_ctx.get(&self.token), amount: self.amount, port_id: self.port_id, channel_id: self.channel_id, @@ -3682,14 +3756,16 @@ pub mod args { impl CliToSdk> for TxInitAccount { fn to_sdk(self, ctx: &mut Context) -> TxInitAccount { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); TxInitAccount:: { - tx: self.tx.to_sdk(ctx), + tx, vp_code_path: self.vp_code_path, tx_code_path: self.tx_code_path, public_keys: self .public_keys .iter() - .map(|pk| ctx.get_cached(pk)) + .map(|pk| chain_ctx.get_cached(pk)) .collect(), threshold: self.threshold, } @@ -3735,19 +3811,27 @@ pub mod args { impl CliToSdk> for TxInitValidator { fn to_sdk(self, ctx: &mut Context) -> TxInitValidator { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); TxInitValidator:: { - tx: self.tx.to_sdk(ctx), + tx, scheme: self.scheme, account_keys: self .account_keys .iter() - .map(|x| ctx.get_cached(x)) + .map(|x| chain_ctx.get_cached(x)) .collect(), threshold: self.threshold, - consensus_key: self.consensus_key.map(|x| ctx.get_cached(&x)), - eth_cold_key: self.eth_cold_key.map(|x| ctx.get_cached(&x)), - eth_hot_key: self.eth_hot_key.map(|x| ctx.get_cached(&x)), - protocol_key: self.protocol_key.map(|x| ctx.get_cached(&x)), + consensus_key: self + .consensus_key + .map(|x| chain_ctx.get_cached(&x)), + eth_cold_key: self + .eth_cold_key + .map(|x| chain_ctx.get_cached(&x)), + eth_hot_key: self.eth_hot_key.map(|x| chain_ctx.get_cached(&x)), + protocol_key: self + .protocol_key + .map(|x| chain_ctx.get_cached(&x)), commission_rate: self.commission_rate, max_commission_rate_change: self.max_commission_rate_change, validator_vp_code_path: self @@ -3854,15 +3938,17 @@ pub mod args { impl CliToSdk> for TxUpdateAccount { fn to_sdk(self, ctx: &mut Context) -> TxUpdateAccount { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); TxUpdateAccount:: { - tx: self.tx.to_sdk(ctx), + tx, vp_code_path: self.vp_code_path, tx_code_path: self.tx_code_path, - addr: ctx.get(&self.addr), + addr: chain_ctx.get(&self.addr), public_keys: self .public_keys .iter() - .map(|pk| ctx.get_cached(pk)) + .map(|pk| chain_ctx.get_cached(pk)) .collect(), threshold: self.threshold, } @@ -3912,12 +3998,14 @@ pub mod args { impl CliToSdk> for Bond { fn to_sdk(self, ctx: &mut Context) -> Bond { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); Bond:: { - tx: self.tx.to_sdk(ctx), - validator: ctx.get(&self.validator), + tx, + validator: chain_ctx.get(&self.validator), amount: self.amount, - source: self.source.map(|x| ctx.get(&x)), - native_token: ctx.native_token.clone(), + source: self.source.map(|x| chain_ctx.get(&x)), + native_token: chain_ctx.native_token.clone(), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -3961,11 +4049,13 @@ pub mod args { impl CliToSdk> for Unbond { fn to_sdk(self, ctx: &mut Context) -> Unbond { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); Unbond:: { - tx: self.tx.to_sdk(ctx), - validator: ctx.get(&self.validator), + tx, + validator: chain_ctx.get(&self.validator), amount: self.amount, - source: self.source.map(|x| ctx.get(&x)), + source: self.source.map(|x| chain_ctx.get(&x)), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -4020,7 +4110,7 @@ pub mod args { ) -> UpdateStewardCommission { UpdateStewardCommission:: { tx: self.tx.to_sdk(ctx), - steward: ctx.get(&self.steward), + steward: ctx.borrow_chain_or_exit().get(&self.steward), commission: std::fs::read(self.commission).expect(""), tx_code_path: self.tx_code_path.to_path_buf(), } @@ -4056,7 +4146,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> ResignSteward { ResignSteward:: { tx: self.tx.to_sdk(ctx), - steward: ctx.get(&self.steward), + steward: ctx.borrow_chain_or_exit().get(&self.steward), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -4082,11 +4172,13 @@ pub mod args { impl CliToSdk> for Redelegate { fn to_sdk(self, ctx: &mut Context) -> Redelegate { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); Redelegate:: { - tx: self.tx.to_sdk(ctx), - src_validator: ctx.get(&self.src_validator), - dest_validator: ctx.get(&self.dest_validator), - owner: ctx.get(&self.owner), + tx, + src_validator: chain_ctx.get(&self.src_validator), + dest_validator: chain_ctx.get(&self.dest_validator), + owner: chain_ctx.get(&self.owner), amount: self.amount, tx_code_path: self.tx_code_path.to_path_buf(), } @@ -4145,7 +4237,7 @@ pub mod args { is_offline: self.is_offline, is_pgf_stewards: self.is_pgf_stewards, is_pgf_funding: self.is_pgf_funding, - native_token: ctx.native_token.clone(), + native_token: ctx.borrow_chain_or_exit().native_token.clone(), tx_code_path: self.tx_code_path, } } @@ -4231,7 +4323,7 @@ pub mod args { tx: self.tx.to_sdk(ctx), proposal_id: self.proposal_id, vote: self.vote, - voter: ctx.get(&self.voter), + voter: ctx.borrow_chain_or_exit().get(&self.voter), is_offline: self.is_offline, proposal_data: self.proposal_data.map(|path| { std::fs::read(path) @@ -4301,9 +4393,11 @@ pub mod args { impl CliToSdk> for RevealPk { fn to_sdk(self, ctx: &mut Context) -> RevealPk { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); RevealPk:: { - tx: self.tx.to_sdk(ctx), - public_key: ctx.get_cached(&self.public_key), + tx, + public_key: chain_ctx.get_cached(&self.public_key), } } } @@ -4464,10 +4558,12 @@ pub mod args { impl CliToSdk> for Withdraw { fn to_sdk(self, ctx: &mut Context) -> Withdraw { + let tx = self.tx.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); Withdraw:: { - tx: self.tx.to_sdk(ctx), - validator: ctx.get(&self.validator), - source: self.source.map(|x| ctx.get(&x)), + tx, + validator: chain_ctx.get(&self.validator), + source: self.source.map(|x| chain_ctx.get(&x)), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -4502,7 +4598,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QueryConversions { QueryConversions:: { query: self.query.to_sdk(ctx), - token: self.token.map(|x| ctx.get(&x)), + token: self.token.map(|x| ctx.borrow_chain_or_exit().get(&x)), epoch: self.epoch, } } @@ -4539,7 +4635,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QueryAccount { QueryAccount:: { query: self.query.to_sdk(ctx), - owner: ctx.get(&self.owner), + owner: ctx.borrow_chain_or_exit().get(&self.owner), } } } @@ -4563,10 +4659,12 @@ pub mod args { impl CliToSdk> for QueryBalance { fn to_sdk(self, ctx: &mut Context) -> QueryBalance { + let query = self.query.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); QueryBalance:: { - query: self.query.to_sdk(ctx), - owner: self.owner.map(|x| ctx.get_cached(&x)), - token: self.token.map(|x| ctx.get(&x)), + query, + owner: self.owner.map(|x| chain_ctx.get_cached(&x)), + token: self.token.map(|x| chain_ctx.get(&x)), no_conversions: self.no_conversions, } } @@ -4608,10 +4706,12 @@ pub mod args { impl CliToSdk> for QueryTransfers { fn to_sdk(self, ctx: &mut Context) -> QueryTransfers { + let query = self.query.to_sdk(ctx); + let chain_ctx = ctx.borrow_mut_chain_or_exit(); QueryTransfers:: { - query: self.query.to_sdk(ctx), - owner: self.owner.map(|x| ctx.get_cached(&x)), - token: self.token.map(|x| ctx.get(&x)), + query, + owner: self.owner.map(|x| chain_ctx.get_cached(&x)), + token: self.token.map(|x| chain_ctx.get(&x)), } } } @@ -4641,10 +4741,12 @@ pub mod args { impl CliToSdk> for QueryBonds { fn to_sdk(self, ctx: &mut Context) -> QueryBonds { + let query = self.query.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); QueryBonds:: { - query: self.query.to_sdk(ctx), - owner: self.owner.map(|x| ctx.get(&x)), - validator: self.validator.map(|x| ctx.get(&x)), + query, + owner: self.owner.map(|x| chain_ctx.get(&x)), + validator: self.validator.map(|x| chain_ctx.get(&x)), } } } @@ -4680,7 +4782,9 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QueryBondedStake { QueryBondedStake:: { query: self.query.to_sdk(ctx), - validator: self.validator.map(|x| ctx.get(&x)), + validator: self + .validator + .map(|x| ctx.borrow_chain_or_exit().get(&x)), epoch: self.epoch, } } @@ -4714,7 +4818,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QueryValidatorState { QueryValidatorState:: { query: self.query.to_sdk(ctx), - validator: ctx.get(&self.validator), + validator: ctx.borrow_chain_or_exit().get(&self.validator), epoch: self.epoch, } } @@ -4752,7 +4856,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> CommissionRateChange { CommissionRateChange:: { tx: self.tx.to_sdk(ctx), - validator: ctx.get(&self.validator), + validator: ctx.borrow_chain_or_exit().get(&self.validator), rate: self.rate, tx_code_path: self.tx_code_path.to_path_buf(), } @@ -4790,7 +4894,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> TxUnjailValidator { TxUnjailValidator:: { tx: self.tx.to_sdk(ctx), - validator: ctx.get(&self.validator), + validator: ctx.borrow_chain_or_exit().get(&self.validator), tx_code_path: self.tx_code_path.to_path_buf(), } } @@ -4822,7 +4926,7 @@ pub mod args { SignTx:: { tx: self.tx.to_sdk(ctx), tx_data: std::fs::read(self.tx_data).expect(""), - owner: ctx.get(&self.owner), + owner: ctx.borrow_chain_or_exit().get(&self.owner), } } } @@ -4857,11 +4961,13 @@ pub mod args { self, ctx: &mut Context, ) -> GenIbcShieldedTransafer { + let query = self.query.to_sdk(ctx); + let chain_ctx = ctx.borrow_chain_or_exit(); GenIbcShieldedTransafer:: { query: self.query.to_sdk(ctx), output_folder: self.output_folder, - target: ctx.get(&self.target), - token: ctx.get(&self.token), + target: chain_ctx.get(&self.target), + token: chain_ctx.get(&self.token), amount: self.amount, port_id: self.port_id, channel_id: self.channel_id, @@ -4914,7 +5020,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QueryCommissionRate { QueryCommissionRate:: { query: self.query.to_sdk(ctx), - validator: ctx.get(&self.validator), + validator: ctx.borrow_chain_or_exit().get(&self.validator), epoch: self.epoch, } } @@ -4948,7 +5054,9 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QuerySlashes { QuerySlashes:: { query: self.query.to_sdk(ctx), - validator: self.validator.map(|x| ctx.get(&x)), + validator: self + .validator + .map(|x| ctx.borrow_chain_or_exit().get(&x)), } } } @@ -4989,7 +5097,7 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> QueryDelegations { QueryDelegations:: { query: self.query.to_sdk(ctx), - owner: ctx.get(&self.owner), + owner: ctx.borrow_chain_or_exit().get(&self.owner), } } } @@ -5062,6 +5170,7 @@ pub mod args { impl CliToSdk> for Tx { fn to_sdk(self, ctx: &mut Context) -> Tx { + let ctx = ctx.borrow_mut_chain_or_exit(); Tx:: { dry_run: self.dry_run, dry_run_wrapper: self.dry_run_wrapper, @@ -5308,11 +5417,13 @@ pub mod args { let alias = ALIAS.parse(matches); let alias_force = ALIAS_FORCE.parse(matches); let value = MASP_VALUE.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); Self { alias, alias_force, value, + is_pre_genesis, unsafe_dont_encrypt, } } @@ -5331,6 +5442,10 @@ pub mod args { .def() .help("A spending key, viewing key, or payment address."), ) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) .arg(UNSAFE_DONT_ENCRYPT.def().help( "UNSAFE: Do not encrypt the keypair. Do not use this for keys \ used in a live network.", @@ -5342,10 +5457,12 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); let alias_force = ALIAS_FORCE.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); Self { alias, alias_force, + is_pre_genesis, unsafe_dont_encrypt, } } @@ -5359,6 +5476,10 @@ pub mod args { .arg(ALIAS_FORCE.def().help( "Override the alias without confirmation if it already exists.", )) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) .arg(UNSAFE_DONT_ENCRYPT.def().help( "UNSAFE: Do not encrypt the keypair. Do not use this for keys \ used in a live network.", @@ -5368,11 +5489,35 @@ pub mod args { impl CliToSdk> for MaspPayAddrGen { fn to_sdk(self, ctx: &mut Context) -> MaspPayAddrGen { + use namada_sdk::wallet::Wallet; + + use crate::wallet::CliWalletUtils; + + let find_viewing_key = |w: &mut Wallet| { + w.find_viewing_key(&self.viewing_key.raw) + .map(Clone::clone) + .unwrap_or_else(|_| { + eprintln!( + "Unknown viewing key {}", + self.viewing_key.raw + ); + safe_exit(1) + }) + }; + let viewing_key = if self.is_pre_genesis || ctx.chain.is_none() { + let wallet_path = + ctx.global_args.base_dir.join(PRE_GENESIS_DIR); + let mut wallet = crate::wallet::load_or_new(&wallet_path); + find_viewing_key(&mut wallet) + } else { + find_viewing_key(&mut ctx.borrow_mut_chain_or_exit().wallet) + }; MaspPayAddrGen:: { alias: self.alias, alias_force: self.alias_force, - viewing_key: ctx.get_cached(&self.viewing_key), + viewing_key, pin: self.pin, + is_pre_genesis: self.is_pre_genesis, } } } @@ -5383,11 +5528,13 @@ pub mod args { let alias_force = ALIAS_FORCE.parse(matches); let viewing_key = VIEWING_KEY.parse(matches); let pin = PIN.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); Self { alias, alias_force, viewing_key, pin, + is_pre_genesis, } } @@ -5405,6 +5552,10 @@ pub mod args { "Require that the single transaction to this address be \ pinned.", )) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) } } @@ -5459,12 +5610,14 @@ pub mod args { let scheme = SCHEME.parse(matches); let alias = ALIAS_OPT.parse(matches); let alias_force = ALIAS_FORCE.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); let derivation_path = HD_WALLET_DERIVATION_PATH_OPT.parse(matches); Self { scheme, alias, alias_force, + is_pre_genesis, unsafe_dont_encrypt, derivation_path, } @@ -5483,6 +5636,10 @@ pub mod args { .arg(ALIAS_FORCE.def().help( "Override the alias without confirmation if it already exists.", )) + .arg(PRE_GENESIS.def().help( + "Generate a key for pre-genesis, instead of for the current \ + chain, if any.", + )) .arg(UNSAFE_DONT_ENCRYPT.def().help( "UNSAFE: Do not encrypt the keypair. Do not use this for keys \ used in a live network.", @@ -5504,12 +5661,14 @@ pub mod args { let public_key = RAW_PUBLIC_KEY_OPT.parse(matches); let alias = ALIAS_OPT.parse(matches); let value = VALUE.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_show_secret = UNSAFE_SHOW_SECRET.parse(matches); Self { public_key, alias, value, + is_pre_genesis, unsafe_show_secret, } } @@ -5532,6 +5691,10 @@ pub mod args { .def() .help("A public key or alias associated with the keypair."), ) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) .arg( UNSAFE_SHOW_SECRET .def() @@ -5544,9 +5707,11 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); let unsafe_show_secret = UNSAFE_SHOW_SECRET.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); Self { alias, unsafe_show_secret, + is_pre_genesis, } } @@ -5557,21 +5722,31 @@ pub mod args { .def() .help("UNSAFE: Print the spending key values."), ) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current \ + chain, if any.", + )) } } impl Args for MaspKeysList { fn parse(matches: &ArgMatches) -> Self { let decrypt = DECRYPT.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_show_secret = UNSAFE_SHOW_SECRET.parse(matches); Self { decrypt, + is_pre_genesis, unsafe_show_secret, } } fn def(app: App) -> App { app.arg(DECRYPT.def().help("Decrypt keys that are encrypted.")) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current \ + chain, if any.", + )) .arg( UNSAFE_SHOW_SECRET .def() @@ -5580,18 +5755,38 @@ pub mod args { } } + impl Args for MaspListPayAddrs { + fn parse(matches: &ArgMatches) -> Self { + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { is_pre_genesis } + } + + fn def(app: App) -> App { + app.arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) + } + } + impl Args for KeyList { fn parse(matches: &ArgMatches) -> Self { let decrypt = DECRYPT.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); let unsafe_show_secret = UNSAFE_SHOW_SECRET.parse(matches); Self { decrypt, + is_pre_genesis, unsafe_show_secret, } } fn def(app: App) -> App { app.arg(DECRYPT.def().help("Decrypt keys that are encrypted.")) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current \ + chain, if any.", + )) .arg( UNSAFE_SHOW_SECRET .def() @@ -5603,14 +5798,21 @@ pub mod args { impl Args for KeyExport { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); - - Self { alias } + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { + alias, + is_pre_genesis, + } } fn def(app: App) -> App { app.arg( ALIAS.def().help("The alias of the key you wish to export."), ) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) } } @@ -5618,7 +5820,12 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS_OPT.parse(matches); let address = RAW_ADDRESS_OPT.parse(matches); - Self { alias, address } + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { + alias, + address, + is_pre_genesis, + } } fn def(app: App) -> App { @@ -5632,6 +5839,10 @@ pub mod args { .def() .help("The bech32m encoded address string."), ) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) .group( ArgGroup::new("find_flags") .args([ALIAS_OPT.name, RAW_ADDRESS_OPT.name]) @@ -5640,15 +5851,31 @@ pub mod args { } } + impl Args for AddressList { + fn parse(matches: &ArgMatches) -> Self { + let is_pre_genesis = PRE_GENESIS.parse(matches); + Self { is_pre_genesis } + } + + fn def(app: App) -> App { + app.arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) + } + } + impl Args for AddressAdd { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); let alias_force = ALIAS_FORCE.parse(matches); let address = RAW_ADDRESS.parse(matches); + let is_pre_genesis = PRE_GENESIS.parse(matches); Self { alias, alias_force, address, + is_pre_genesis, } } @@ -5666,6 +5893,10 @@ pub mod args { .def() .help("The bech32m encoded address string."), ) + .arg(PRE_GENESIS.def().help( + "Use pre-genesis wallet, instead of for the current chain, if \ + any.", + )) } } @@ -5675,6 +5906,7 @@ pub mod args { pub genesis_validator: Option, pub pre_genesis_path: Option, pub dont_prefetch_wasm: bool, + pub allow_duplicate_ip: bool, } impl Args for JoinNetwork { @@ -5683,11 +5915,13 @@ pub mod args { let genesis_validator = GENESIS_VALIDATOR.parse(matches); let pre_genesis_path = PRE_GENESIS_PATH.parse(matches); let dont_prefetch_wasm = DONT_PREFETCH_WASM.parse(matches); + let allow_duplicate_ip = ALLOW_DUPLICATE_IP.parse(matches); Self { chain_id, genesis_validator, pre_genesis_path, dont_prefetch_wasm, + allow_duplicate_ip, } } @@ -5699,6 +5933,10 @@ pub mod args { .arg(DONT_PREFETCH_WASM.def().help( "Do not pre-fetch WASM.", )) + .arg(ALLOW_DUPLICATE_IP.def().help( + "Toggle to disable guard against peers connecting from the \ + same IP. This option shouldn't be used in mainnet.", + )) } } @@ -5772,48 +6010,41 @@ pub mod args { #[derive(Clone, Debug)] pub struct InitNetwork { - pub genesis_path: PathBuf, + pub templates_path: PathBuf, pub wasm_checksums_path: PathBuf, pub chain_id_prefix: ChainIdPrefix, - pub unsafe_dont_encrypt: bool, + pub genesis_time: DateTimeUtc, pub consensus_timeout_commit: Timeout, - pub localhost: bool, - pub allow_duplicate_ip: bool, pub dont_archive: bool, pub archive_dir: Option, } impl Args for InitNetwork { fn parse(matches: &ArgMatches) -> Self { - let genesis_path = GENESIS_PATH.parse(matches); + let templates_path = TEMPLATES_PATH.parse(matches); let wasm_checksums_path = WASM_CHECKSUMS_PATH.parse(matches); let chain_id_prefix = CHAIN_ID_PREFIX.parse(matches); - let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); + let genesis_time = GENESIS_TIME.parse(matches); let consensus_timeout_commit = CONSENSUS_TIMEOUT_COMMIT.parse(matches); - let localhost = LOCALHOST.parse(matches); - let allow_duplicate_ip = ALLOW_DUPLICATE_IP.parse(matches); let dont_archive = DONT_ARCHIVE.parse(matches); let archive_dir = ARCHIVE_DIR.parse(matches); Self { - genesis_path, + templates_path, wasm_checksums_path, chain_id_prefix, - unsafe_dont_encrypt, + genesis_time, consensus_timeout_commit, - localhost, - allow_duplicate_ip, dont_archive, archive_dir, } } fn def(app: App) -> App { - app.arg( - GENESIS_PATH.def().help( - "Path to the preliminary genesis configuration file.", - ), - ) + app.arg(TEMPLATES_PATH.def().help( + "Path to the directory with genesis templates to be used to \ + initialize the network.", + )) .arg( WASM_CHECKSUMS_PATH .def() @@ -5823,22 +6054,14 @@ pub mod args { "The chain ID prefix. Up to 19 alphanumeric, '.', '-' or '_' \ characters.", )) - .arg(UNSAFE_DONT_ENCRYPT.def().help( - "UNSAFE: Do not encrypt the generated keypairs. Do not use \ - this for keys used in a live network.", + .arg(GENESIS_TIME.def().help( + "The start time of the network in RFC 3339 and ISO 8601 \ + format. For example: \"2021-12-31T00:00:00Z\".", )) .arg(CONSENSUS_TIMEOUT_COMMIT.def().help( "The Tendermint consensus timeout_commit configuration as \ e.g. `1s` or `1000ms`. Defaults to 10 seconds.", )) - .arg(LOCALHOST.def().help( - "Use localhost address for P2P and RPC connections for the \ - validators ledger", - )) - .arg(ALLOW_DUPLICATE_IP.def().help( - "Toggle to disable guard against peers connecting from the \ - same IP. This option shouldn't be used in mainnet.", - )) .arg( DONT_ARCHIVE .def() @@ -5853,16 +6076,20 @@ pub mod args { #[derive(Clone, Debug)] pub struct InitGenesisValidator { + pub source: String, pub alias: String, pub commission_rate: Dec, pub max_commission_rate_change: Dec, - pub net_address: String, + pub net_address: SocketAddr, pub unsafe_dont_encrypt: bool, pub key_scheme: SchemeType, + pub transfer_from_source_amount: token::DenominatedAmount, + pub self_bond_amount: token::DenominatedAmount, } impl Args for InitGenesisValidator { fn parse(matches: &ArgMatches) -> Self { + let source = RAW_SOURCE.parse(matches); let alias = ALIAS.parse(matches); let commission_rate = COMMISSION_RATE.parse(matches); let max_commission_rate_change = @@ -5870,40 +6097,117 @@ pub mod args { let net_address = NET_ADDRESS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); let key_scheme = SCHEME.parse(matches); + // The denomination validation is handled by validating genesis + // files later. + // At this stage, we treat amounts as opaque and pass them on + // verbatim. + let transfer_from_source_amount = + TRANSFER_FROM_SOURCE_AMOUNT.parse(matches); + // this must be an amount of native tokens + let self_bond_amount = SELF_BOND_AMOUNT.parse(matches); Self { + source, alias, net_address, unsafe_dont_encrypt, key_scheme, commission_rate, max_commission_rate_change, + transfer_from_source_amount, + self_bond_amount, } } fn def(app: App) -> App { - app.arg(ALIAS.def().help("The validator address alias.")) - .arg(NET_ADDRESS.def().help( - "Static {host:port} of your validator node's P2P address. \ - Namada uses port `26656` for P2P connections by default, \ - but you can configure a different value.", - )) - .arg(COMMISSION_RATE.def().help( - "The commission rate charged by the validator for \ - delegation rewards. This is a required parameter.", - )) - .arg(MAX_COMMISSION_RATE_CHANGE.def().help( - "The maximum change per epoch in the commission rate \ - charged by the validator for delegation rewards. This is \ - a required parameter.", - )) - .arg(UNSAFE_DONT_ENCRYPT.def().help( - "UNSAFE: Do not encrypt the generated keypairs. Do not \ - use this for keys used in a live network.", - )) - .arg(SCHEME.def().help( - "The key scheme/type used for the validator keys. \ - Currently supports ed25519 and secp256k1.", - )) + app.arg(RAW_SOURCE.def().help( + "The source key for native token transfer from the \ + `balances.toml` genesis template.", + )) + .arg(ALIAS.def().help("The validator address alias.")) + .arg(NET_ADDRESS.def().help( + "Static {host:port} of your validator node's P2P address. \ + Namada uses port `26656` for P2P connections by default, but \ + you can configure a different value.", + )) + .arg(COMMISSION_RATE.def().help( + "The commission rate charged by the validator for delegation \ + rewards. This is a required parameter.", + )) + .arg(MAX_COMMISSION_RATE_CHANGE.def().help( + "The maximum change per epoch in the commission rate charged \ + by the validator for delegation rewards. This is a required \ + parameter.", + )) + .arg(UNSAFE_DONT_ENCRYPT.def().help( + "UNSAFE: Do not encrypt the generated keypairs. Do not use \ + this for keys used in a live network.", + )) + .arg(SCHEME.def().help( + "The key scheme/type used for the validator keys. Currently \ + supports ed25519 and secp256k1.", + )) + .arg(TRANSFER_FROM_SOURCE_AMOUNT.def().help( + "The amount of native token to transfer into the validator \ + account from the `--source`. To self-bond some tokens to the \ + validator at genesis, specify `--self-bond-amount`.", + )) + .arg( + SELF_BOND_AMOUNT + .def() + .help( + "The amount of native token to self-bond in PoS. \ + Because this amount will be bonded from the \ + validator's account, it must be lower or equal to \ + the amount specified in \ + `--transfer-from-source-amount`.", + ) + .requires(TRANSFER_FROM_SOURCE_AMOUNT.name), + ) + } + } + + #[derive(Clone, Debug)] + pub struct ValidateGenesisTemplates { + /// Templates dir + pub path: PathBuf, + } + + impl Args for ValidateGenesisTemplates { + fn parse(matches: &ArgMatches) -> Self { + let path = PATH.parse(matches); + Self { path } + } + + fn def(app: App) -> App { + app.arg( + PATH.def() + .help("Path to the directory with the template files."), + ) + } + } + + #[derive(Clone, Debug)] + pub struct SignGenesisTx { + pub path: PathBuf, + pub output: Option, + } + + impl Args for SignGenesisTx { + fn parse(matches: &ArgMatches) -> Self { + let path = PATH.parse(matches); + let output = OUTPUT.parse(matches); + Self { path, output } + } + + fn def(app: App) -> App { + app.arg( + PATH.def() + .help("Path to the unsigned transactions TOML file."), + ) + .arg(OUTPUT.def().help( + "Save the output to a TOML file. When not supplied, the \ + signed transactions will be printed to stdout instead.", + )) } } } diff --git a/apps/src/lib/cli/client.rs b/apps/src/lib/cli/client.rs index ec60584642..a465d585ce 100644 --- a/apps/src/lib/cli/client.rs +++ b/apps/src/lib/cli/client.rs @@ -538,6 +538,12 @@ impl CliApi { let namada = ctx.to_sdk(&client, io); rpc::epoch_sleep(&namada, args).await; } + Utils::ValidateGenesisTemplates( + ValidateGenesisTemplates(args ) + ) => utils::validate_genesis_templates(global_args, args), + Utils::SignGenesisTx(SignGenesisTx(args)) => { + utils::sign_genesis_tx(global_args, args) + } }, } Ok(()) diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index 64401984ce..494400de16 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -20,13 +20,11 @@ use namada_sdk::wallet::Wallet; use namada_sdk::{Namada, NamadaImpl}; use super::args; -#[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}; use crate::wallet::CliWalletUtils; -use crate::wasm_loader; +use crate::{wallet, wasm_loader}; +use crate::cli::utils; /// Env. var to set chain ID const ENV_VAR_CHAIN_ID: &str = "NAMADA_CHAIN_ID"; @@ -74,10 +72,17 @@ pub type WalletBalanceOwner = FromContext; pub struct Context { /// Global arguments pub global_args: args::Global, - /// The wallet - pub wallet: Wallet, /// The global configuration pub global_config: GlobalConfig, + /// Chain-specific context, if any chain is configured in `global_config` + pub chain: Option, +} + +/// Command execution context with chain-specific data +#[derive(Debug)] +pub struct ChainContext { + /// The wallet + pub wallet: Wallet, /// The ledger configuration for a specific chain ID pub config: Config, /// The context fr shielded operations @@ -89,89 +94,109 @@ pub struct Context { impl Context { pub fn new(global_args: args::Global) -> Result { let global_config = read_or_try_new_global_config(&global_args); - tracing::debug!("Chain ID: {}", global_config.default_chain_id); - - let mut config = Config::load( - &global_args.base_dir, - &global_config.default_chain_id, - None, - ); - - let chain_dir = global_args - .base_dir - .join(global_config.default_chain_id.as_str()); - let genesis_file_path = global_args - .base_dir - .join(format!("{}.toml", global_config.default_chain_id.as_str())); - // 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, - ) - } - }; - match global_args.wasm_dir.as_ref() { - Some(wasm_dir) => { - config.wasm_dir = wasm_dir.clone(); - } - None => { - if let Ok(wasm_dir) = env::var(ENV_VAR_WASM_DIR) { - let wasm_dir: PathBuf = wasm_dir.into(); - config.wasm_dir = wasm_dir; + let chain = match global_config.default_chain_id.as_ref() { + Some(default_chain_id) => { + tracing::info!("Default chain ID: {default_chain_id}"); + let mut config = + Config::load(&global_args.base_dir, default_chain_id, None); + let chain_dir = + global_args.base_dir.join(default_chain_id.as_str()); + let genesis = + genesis::chain::Finalized::read_toml_files(&chain_dir) + .expect("Missing genesis files"); + let native_token = genesis.get_native_token().clone(); + let wallet = if wallet::exists(&chain_dir) { + wallet::load(&chain_dir).unwrap() + } else { + panic!( + "Could not find wallet at {}.", + chain_dir.to_string_lossy() + ); + }; + + // 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(); + } + None => { + if let Ok(wasm_dir) = env::var(ENV_VAR_WASM_DIR) { + let wasm_dir: PathBuf = wasm_dir.into(); + config.wasm_dir = wasm_dir; + } + } } + Some(ChainContext { + wallet, + config, + shielded: FsShieldedUtils::new(chain_dir), + native_token, + }) } - } + None => None, + }; + Ok(Self { global_args, - wallet, global_config, - config, - shielded: FsShieldedUtils::new(chain_dir), - native_token, + chain, }) } + /// Try to take the chain context, or exit the process with an error if no + /// chain is configured. + pub fn take_chain_or_exit(self) -> ChainContext { + self.chain + .unwrap_or_else(|| safe_exit_on_missing_chain_context()) + } + + /// Try to borrow chain context, or exit the process with an error if no + /// chain is configured. + pub fn borrow_chain_or_exit(&self) -> &ChainContext { + self.chain + .as_ref() + .unwrap_or_else(|| safe_exit_on_missing_chain_context()) + } + + /// Try to borrow mutably chain context, or exit the process with an error + /// if no chain is configured. + pub fn borrow_mut_chain_or_exit(&mut self) -> &mut ChainContext { + self.chain + .as_mut() + .unwrap_or_else(|| safe_exit_on_missing_chain_context()) + } + /// Make an implementation of Namada from this object and parameters. pub fn to_sdk<'a, C, IO>( &'a mut self, client: &'a C, io: &'a IO, ) -> impl Namada - where - C: namada::ledger::queries::Client + Sync, - IO: Io, + where + C: namada::ledger::queries::Client + Sync, + IO: Io, { + let chain_ctx = self.borrow_mut_chain_or_exit(); NamadaImpl::native_new( client, - &mut self.wallet, - &mut self.shielded, + &mut chain_ctx.wallet, + &mut chain_ctx.shielded, io, - self.native_token.clone(), + chain_ctx.native_token.clone(), ) } +} + +fn safe_exit_on_missing_chain_context() -> ! { + eprintln!( + "No chain is configured. You may need to run `namada client utils \ + join-network` command." + ); + utils::safe_exit(1) +} +impl ChainContext { /// Parse and/or look-up the value from the context. pub fn get(&self, from_context: &FromContext) -> T where @@ -273,7 +298,7 @@ pub fn read_or_try_new_global_config( /// Argument that can be given raw or found in the [`Context`]. #[derive(Debug, Clone)] pub struct FromContext { - raw: String, + pub(crate) raw: String, phantom: PhantomData, } diff --git a/apps/src/lib/cli/wallet.rs b/apps/src/lib/cli/wallet.rs index 14b9ab9d9a..6d949ac462 100644 --- a/apps/src/lib/cli/wallet.rs +++ b/apps/src/lib/cli/wallet.rs @@ -23,7 +23,8 @@ use crate::cli; use crate::cli::api::CliApi; use crate::cli::args::CliToSdk; use crate::cli::{args, cmds, Context}; -use crate::wallet::{read_and_confirm_encryption_password, CliWalletUtils}; +use crate::client::utils::PRE_GENESIS_DIR; +use crate::wallet::{self, read_and_confirm_encryption_password, CliWalletUtils}; impl CliApi { pub fn handle_wallet_command( @@ -34,57 +35,57 @@ impl CliApi { match cmd { cmds::NamadaWallet::Key(sub) => match sub { cmds::WalletKey::Restore(cmds::KeyRestore(args)) => { - key_and_address_restore(&mut ctx.wallet, io, args) + key_and_address_restore(ctx, io, args) } cmds::WalletKey::Gen(cmds::KeyGen(args)) => { - key_and_address_gen(&mut ctx.wallet, io, &mut OsRng, args) + key_and_address_gen(ctx, io, &mut OsRng, args) } cmds::WalletKey::Find(cmds::KeyFind(args)) => { - key_find(&mut ctx.wallet, io, args) + key_find(ctx, io, args) } cmds::WalletKey::List(cmds::KeyList(args)) => { - key_list(&mut ctx.wallet, io, args) + key_list( ctx, io, args) } cmds::WalletKey::Export(cmds::Export(args)) => { - key_export(&mut ctx.wallet, io, args) + key_export( ctx, io, args) } }, cmds::NamadaWallet::Address(sub) => match sub { cmds::WalletAddress::Gen(cmds::AddressGen(args)) => { - key_and_address_gen(&mut ctx.wallet, io, &mut OsRng, args) + key_and_address_gen(ctx, io, &mut OsRng, args) } cmds::WalletAddress::Restore(cmds::AddressRestore(args)) => { - key_and_address_restore(&mut ctx.wallet, io, args) + key_and_address_restore(&mut ctx.borrow_mut_chain_or_exit().wallet, io, args) } cmds::WalletAddress::Find(cmds::AddressOrAliasFind(args)) => { - address_or_alias_find(&mut ctx.wallet, io, args) + address_or_alias_find(ctx, io, args) } - cmds::WalletAddress::List(cmds::AddressList) => { - address_list(&mut ctx.wallet, io) + cmds::WalletAddress::List(cmds::AddressList(args)) => { + address_list(ctx, io, args) } cmds::WalletAddress::Add(cmds::AddressAdd(args)) => { - address_add(&mut ctx.wallet, io, args) + address_add(ctx, io, args) } }, cmds::NamadaWallet::Masp(sub) => match sub { cmds::WalletMasp::GenSpendKey(cmds::MaspGenSpendKey(args)) => { - spending_key_gen(&mut ctx.wallet, io, args) + spending_key_gen(ctx, io, args) } cmds::WalletMasp::GenPayAddr(cmds::MaspGenPayAddr(args)) => { let args = args.to_sdk(&mut ctx); - payment_address_gen(&mut ctx.wallet, io, args) + payment_address_gen(&mut ctx.borrow_mut_chain_or_exit().wallet, io, args) } cmds::WalletMasp::AddAddrKey(cmds::MaspAddAddrKey(args)) => { - address_key_add(&mut ctx.wallet, io, args) - } - cmds::WalletMasp::ListPayAddrs(cmds::MaspListPayAddrs) => { - payment_addresses_list(&mut ctx.wallet, io) + address_key_add(ctx, io, args) } + cmds::WalletMasp::ListPayAddrs(cmds::MaspListPayAddrs( + args, + )) => payment_addresses_list(ctx, io, args), cmds::WalletMasp::ListKeys(cmds::MaspListKeys(args)) => { - spending_keys_list(&mut ctx.wallet, io, args) + spending_keys_list(ctx, io, args) } cmds::WalletMasp::FindAddrKey(cmds::MaspFindAddrKey(args)) => { - address_key_find(&mut ctx.wallet, io, args) + address_key_find(ctx, io, args) } }, } @@ -94,13 +95,15 @@ impl CliApi { /// Find shielded address or key fn address_key_find( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, args::AddrKeyFind { alias, unsafe_show_secret, + is_pre_genesis, }: args::AddrKeyFind, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); let alias = alias.to_lowercase(); if let Ok(viewing_key) = wallet.find_viewing_key(&alias) { // Check if alias is a viewing key @@ -132,13 +135,15 @@ fn address_key_find( /// List spending keys. fn spending_keys_list( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, args::MaspKeysList { decrypt, + is_pre_genesis, unsafe_show_secret, }: args::MaspKeysList, ) { + let wallet = load_wallet(ctx, is_pre_genesis); let known_view_keys = wallet.get_viewing_keys(); let known_spend_keys = wallet.get_spending_keys(); if known_view_keys.is_empty() { @@ -207,9 +212,11 @@ fn spending_keys_list( /// List payment addresses. fn payment_addresses_list( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, + args::MaspListPayAddrs { is_pre_genesis }: args::MaspListPayAddrs, ) { + let wallet = load_wallet(ctx, is_pre_genesis); let known_addresses = wallet.get_payment_addrs(); if known_addresses.is_empty() { display_line!( @@ -229,14 +236,16 @@ fn payment_addresses_list( /// Generate a spending key. fn spending_key_gen( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, args::MaspSpendKeyGen { alias, alias_force, + is_pre_genesis, unsafe_dont_encrypt, }: args::MaspSpendKeyGen, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); let alias = alias.to_lowercase(); let password = read_and_confirm_encryption_password(unsafe_dont_encrypt); let (alias, _key) = wallet.gen_spending_key(alias, password, alias_force); @@ -257,6 +266,7 @@ fn payment_address_gen( alias_force, viewing_key, pin, + .. }: args::MaspPayAddrGen, ) { let alias = alias.to_lowercase(); @@ -285,16 +295,18 @@ fn payment_address_gen( /// Add a viewing key, spending key, or payment address to wallet. fn address_key_add( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, args::MaspAddrKeyAdd { alias, alias_force, value, + is_pre_genesis, unsafe_dont_encrypt, }: args::MaspAddrKeyAdd, ) { let alias = alias.to_lowercase(); + let mut wallet = load_wallet(ctx, is_pre_genesis); let (alias, typ) = match value { MaspValue::FullViewingKey(viewing_key) => { let alias = wallet @@ -385,20 +397,23 @@ fn key_and_address_restore( /// Generate a new keypair and derive implicit address from it and store them in /// the wallet. fn key_and_address_gen( - wallet: &mut Wallet>, + ctx: Context, io: &impl Io, - rng: &mut R, args::KeyAndAddressGen { scheme, alias, alias_force, + is_pre_genesis, unsafe_dont_encrypt, derivation_path, }: args::KeyAndAddressGen, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); let encryption_password = read_and_confirm_encryption_password(unsafe_dont_encrypt); - let derivation_path_and_mnemonic_rng = derivation_path.map(|p| (p, rng)); + let mut rng = OsRng; + let derivation_path_and_mnemonic_rng = + derivation_path.map(|p| (p, &mut rng)); let (alias, _key, _mnemonic) = wallet .gen_key( scheme, @@ -410,11 +425,11 @@ fn key_and_address_gen( ) .unwrap_or_else(|err| match err { GenRestoreKeyError::KeyStorageError => { - println!("No changes are persisted. Exiting."); + display_line!(io, "No changes are persisted. Exiting."); cli::safe_exit(0); } _ => { - eprintln!("{}", err); + edisplay_line!(io, "{}", err); cli::safe_exit(1); } }); @@ -430,15 +445,17 @@ fn key_and_address_gen( /// Find a keypair in the wallet store. fn key_find( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, args::KeyFind { public_key, alias, value, + is_pre_genesis, unsafe_show_secret, }: args::KeyFind, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); let found_keypair = match public_key { Some(pk) => wallet.find_key_by_pk(&pk, None), None => { @@ -473,13 +490,15 @@ fn key_find( /// List all known keys. fn key_list( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, args::KeyList { decrypt, + is_pre_genesis, unsafe_show_secret, }: args::KeyList, ) { + let wallet = load_wallet(ctx, is_pre_genesis); let known_keys = wallet.get_keys(); if known_keys.is_empty() { display_line!( @@ -538,10 +557,14 @@ fn key_list( /// Export a keypair to a file. fn key_export( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, - args::KeyExport { alias }: args::KeyExport, + args::KeyExport { + alias, + is_pre_genesis, + }: args::KeyExport, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); wallet .find_key(alias.to_lowercase(), None) .map(|keypair| { @@ -560,9 +583,11 @@ fn key_export( /// List all known addresses. fn address_list( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, + args::AddressList { is_pre_genesis }: args::AddressList, ) { + let wallet = load_wallet(ctx, is_pre_genesis); let known_addresses = wallet.get_addresses(); if known_addresses.is_empty() { display_line!( @@ -586,36 +611,40 @@ fn address_list( /// Find address (alias) by its alias (address). fn address_or_alias_find( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, - args: args::AddressOrAliasFind, + args::AddressOrAliasFind { + alias, + address, + is_pre_genesis, + }: args::AddressOrAliasFind, ) { - if args.address.is_some() && args.alias.is_some() { + let wallet = load_wallet(ctx, is_pre_genesis); + if address.is_some() && alias.is_some() { panic!( "This should not be happening: clap should emit its own error \ message." ); - } else if args.alias.is_some() { - if let Some(address) = wallet.find_address(args.alias.as_ref().unwrap()) - { + } else if alias.is_some() { + if let Some(address) = wallet.find_address(alias.as_ref().unwrap()) { display_line!(io, "Found address {}", address.to_pretty_string()); } else { display_line!( io, "No address with alias {} found. Use the command `address \ list` to see all the known addresses.", - args.alias.unwrap().to_lowercase() + alias.unwrap().to_lowercase() ); } - } else if args.address.is_some() { - if let Some(alias) = wallet.find_alias(args.address.as_ref().unwrap()) { + } else if address.is_some() { + if let Some(alias) = wallet.find_alias(address.as_ref().unwrap()) { display_line!(io, "Found alias {}", alias); } else { display_line!( io, "No alias with address {} found. Use the command `address \ list` to see all the known addresses.", - args.address.unwrap() + address.unwrap() ); } } @@ -623,16 +652,18 @@ fn address_or_alias_find( /// Add an address to the wallet. fn address_add( - wallet: &mut Wallet, + ctx: Context, io: &impl Io, - args: args::AddressAdd, + args::AddressAdd { + alias, + alias_force, + address, + is_pre_genesis, + }: args::AddressAdd, ) { + let mut wallet = load_wallet(ctx, is_pre_genesis); if wallet - .add_address( - args.alias.clone().to_lowercase(), - args.address, - args.alias_force, - ) + .add_address(alias.to_lowercase(), address, alias_force) .is_none() { edisplay_line!(io, "Address not added"); @@ -644,6 +675,17 @@ fn address_add( display_line!( io, "Successfully added a key and an address with alias: \"{}\"", - args.alias.to_lowercase() + alias.to_lowercase() ); } + +/// Load wallet for chain when `ctx.chain.is_some()` or pre-genesis wallet when +/// `is_pre_genesis || ctx.chain.is_none()`. +fn load_wallet(ctx: Context, is_pre_genesis: bool) -> Wallet { + if is_pre_genesis || ctx.chain.is_none() { + let wallet_path = ctx.global_args.base_dir.join(PRE_GENESIS_DIR); + wallet::load_or_new(&wallet_path) + } else { + ctx.take_chain_or_exit().wallet + } +} diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 16bf625c23..22340cacfc 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -14,7 +14,7 @@ use namada::types::chain::ChainId; use namada::types::dec::Dec; use namada::types::key::*; use namada::vm::validate_untrusted_wasm; -use namada_sdk::wallet::Wallet; +use namada_sdk::wallet::{alias, Wallet}; use prost::bytes::Bytes; use rand::prelude::ThreadRng; use rand::thread_rng; @@ -22,7 +22,7 @@ use serde_json::json; use sha2::{Digest, Sha256}; use crate::cli::context::ENV_VAR_WASM_DIR; -use crate::cli::{self, args, safe_exit}; +use crate::cli::{self, args}; use crate::config::genesis::genesis_config::{ self, GenesisConfig, HexString, ValidatorPreGenesisConfig, }; @@ -56,6 +56,7 @@ pub async fn join_network( genesis_validator, pre_genesis_path, dont_prefetch_wasm, + allow_duplicate_ip, }: args::JoinNetwork, ) { use tokio::fs; @@ -74,7 +75,7 @@ pub async fn join_network( .is_ok() { eprintln!("The chain directory for {} already exists.", chain_id); - cli::safe_exit(1); + safe_exit(1); } } let base_dir_full = fs::canonicalize(&base_dir).await.unwrap(); @@ -109,12 +110,12 @@ pub async fn join_network( let validator_alias_and_pre_genesis_wallet = validator_alias_and_dir.map(|(validator_alias, pre_genesis_dir)| { ( - validator_alias, + alias::Alias::from(validator_alias), pre_genesis::load(&pre_genesis_dir).unwrap_or_else(|err| { eprintln!( "Error loading validator pre-genesis wallet {err}", ); - cli::safe_exit(1) + safe_exit(1) }), ) }); @@ -141,7 +142,7 @@ pub async fn join_network( Ok(contents) => contents, Err(error) => { eprintln!("Error downloading release: {}", error); - cli::safe_exit(1); + safe_exit(1); } }; @@ -190,16 +191,6 @@ pub async fn join_network( .await .unwrap(); - // Move the genesis file - fs::rename( - unpack_dir - .join(config::DEFAULT_BASE_DIR) - .join(format!("{}.toml", chain_id.as_str())), - base_dir_full.join(format!("{}.toml", chain_id.as_str())), - ) - .await - .unwrap(); - // Move the global config fs::rename( unpack_dir @@ -216,6 +207,84 @@ pub async fn join_network( .unwrap(); } + // Read the genesis files + let genesis = genesis::chain::Finalized::read_toml_files(&chain_dir) + .unwrap_or_else(|err| { + eprintln!( + "Failed to read genesis TOML files from {} with {err}.", + chain_dir.to_string_lossy() + ); + safe_exit(1) + }); + + // Try to find validator data when using a pre-genesis validator + let validator_alias = validator_alias_and_pre_genesis_wallet + .as_ref() + .map(|(alias, _wallet)| alias.clone()); + let validator_keys = validator_alias_and_pre_genesis_wallet.as_ref().map( + |(_alias, wallet)| { + let tendermint_node_key: common::SecretKey = wallet + .tendermint_node_key + .try_to_sk() + .unwrap_or_else(|_err| { + eprintln!( + "Tendermint node key must be common (need to change?)" + ); + safe_exit(1) + }); + (tendermint_node_key, wallet.consensus_key.clone()) + }, + ); + let node_mode = if validator_alias.is_some() { + TendermintMode::Validator + } else { + TendermintMode::Full + }; + + // Derive config from genesis + let config = genesis.derive_config( + &chain_dir, + node_mode, + validator_alias, + allow_duplicate_ip, + ); + + // Try to load pre-genesis wallet, if any + let pre_genesis_wallet_path = base_dir.join(PRE_GENESIS_DIR); + let pre_genesis_wallet = crate::wallet::load(&pre_genesis_wallet_path); + // Derive wallet from genesis + let wallet = genesis.derive_wallet( + &chain_dir, + pre_genesis_wallet, + validator_alias_and_pre_genesis_wallet, + ); + + // Save the config and the wallet + config.write(&base_dir, &chain_id, true).unwrap(); + crate::wallet::save(&wallet).unwrap(); + + // Setup the node for a genesis validator, if used + if let Some((tendermint_node_key, consensus_key)) = validator_keys { + println!( + "Setting up validator keys in CometBFT. Consensus key: {}.", + consensus_key.to_public() + ); + let tm_home_dir = chain_dir.join(config::COMETBFT_DIR); + // Write consensus key to tendermint home + tendermint_node::write_validator_key(&tm_home_dir, &consensus_key); + + // Write tendermint node key + write_tendermint_node_key(&tm_home_dir, tendermint_node_key); + + // Pre-initialize tendermint validator state + tendermint_node::write_validator_state(&tm_home_dir); + } else { + println!( + "No validator keys are being used. Make sure you didn't forget to \ + specify `--genesis-validator`?" + ); + } + // Move wasm-dir and update config if it's non-default if let Some(wasm_dir) = wasm_dir.as_ref() { if wasm_dir.to_string_lossy() != config::DEFAULT_WASM_DIR { @@ -242,102 +311,6 @@ pub async fn join_network( } } - // Setup the node for a genesis validator, if used - if let Some((validator_alias, pre_genesis_wallet)) = - validator_alias_and_pre_genesis_wallet - { - let tendermint_node_key: common::SecretKey = pre_genesis_wallet - .tendermint_node_key - .try_to_sk() - .unwrap_or_else(|_err| { - eprintln!( - "Tendermint node key must be common (need to change?)" - ); - cli::safe_exit(1) - }); - - let genesis_file_path = - base_dir.join(format!("{}.toml", chain_id.as_str())); - let genesis_config = - genesis_config::open_genesis_config(genesis_file_path).unwrap(); - - if !is_valid_validator_for_current_chain( - &tendermint_node_key.ref_to(), - &genesis_config, - ) { - eprintln!( - "The current validator is not valid for chain {}.", - chain_id.as_str() - ); - safe_exit(1) - } - - let mut wallet = - crate::wallet::load_or_new_from_genesis(&chain_dir, genesis_config); - - let address = wallet - .find_address(&validator_alias) - .unwrap_or_else(|| { - eprintln!( - "Unable to find validator address for alias \ - {validator_alias}" - ); - cli::safe_exit(1) - }) - .clone(); - - let tm_home_dir = chain_dir.join("cometbft"); - - // Write consensus key to tendermint home - tendermint_node::write_validator_key( - &tm_home_dir, - &pre_genesis_wallet.consensus_key, - ); - - // Derive the node ID from the node key - let node_id = id_from_pk(&tendermint_node_key.ref_to()); - // Write tendermint node key - write_tendermint_node_key(&tm_home_dir, tendermint_node_key); - - // Pre-initialize tendermint validator state - tendermint_node::write_validator_state(&tm_home_dir); - - // Extend the current wallet from the pre-genesis wallet. - // This takes the validator keys to be usable in future commands (e.g. - // to sign a tx from validator account using the account key). - wallet.extend_from_pre_genesis_validator( - address, - validator_alias.into(), - pre_genesis_wallet, - ); - - crate::wallet::save(&wallet).unwrap(); - - // Update the config from the default non-validator settings to - // validator settings - let base_dir = base_dir.clone(); - let chain_id = chain_id.clone(); - tokio::task::spawn_blocking(move || { - let mut config = Config::load(&base_dir, &chain_id, None); - config.ledger.shell.tendermint_mode = TendermintMode::Validator; - - // Remove self from persistent peers - config.ledger.cometbft.p2p.persistent_peers.retain(|peer| { - if let TendermintAddress::Tcp { - peer_id: Some(peer_id), - .. - } = peer - { - node_id != *peer_id - } else { - true - } - }); - config.write(&base_dir, &chain_id, true).unwrap(); - }) - .await - .unwrap(); - } if !dont_prefetch_wasm { fetch_wasms_aux(&base_dir, &chain_id).await; } @@ -369,7 +342,7 @@ pub fn validate_wasm(args::ValidateWasm { code_path }: args::ValidateWasm) { Ok(()) => println!("Wasm code is valid"), Err(e) => { eprintln!("Wasm code is invalid: {e}"); - cli::safe_exit(1) + safe_exit(1) } } } @@ -405,345 +378,115 @@ pub fn id_from_pk(pk: &common::PublicKey) -> TendermintNodeId { pub fn init_network( global_args: args::Global, args::InitNetwork { - genesis_path, + templates_path, wasm_checksums_path, chain_id_prefix, - unsafe_dont_encrypt, + genesis_time, consensus_timeout_commit, - localhost, - allow_duplicate_ip, dont_archive, archive_dir, }: args::InitNetwork, ) { - let mut config = genesis_config::open_genesis_config(genesis_path).unwrap(); - - // Update the WASM checksums - let checksums = - wasm_loader::Checksums::read_checksums_file(&wasm_checksums_path); - config.wasm.iter_mut().for_each(|(name, config)| { - // Find the sha256 from checksums.json - let name = format!("{}.wasm", name); - // Full name in format `{name}.{sha256}.wasm` - let full_name = checksums.0.get(&name).unwrap(); - let hash = full_name - .split_once('.') - .unwrap() - .1 - .split_once('.') - .unwrap() - .0; - config.sha256 = Some(genesis_config::HexString(hash.to_owned())); - }); - - // The `temp_chain_id` gets renamed after we have chain ID. - let temp_chain_id = chain_id_prefix.temp_chain_id(); - let temp_dir = global_args.base_dir.join(temp_chain_id.as_str()); - // The `temp_chain_id` gets renamed after we have chain ID - let accounts_dir = temp_dir.join(NET_ACCOUNTS_DIR); - // Base dir used in account sub-directories - let accounts_temp_dir = - PathBuf::from(config::DEFAULT_BASE_DIR).join(temp_chain_id.as_str()); - - let mut rng: ThreadRng = thread_rng(); - - // Accumulator of validators' Tendermint P2P addresses - let mut persistent_peers: Vec = - Vec::with_capacity(config.validator.len()); - - // Iterate over each validator, generating keys and addresses - config.validator.iter_mut().for_each(|(name, config)| { - let validator_dir = accounts_dir.join(name); - - let chain_dir = validator_dir.join(&accounts_temp_dir); - let tm_home_dir = chain_dir.join("cometbft"); - - // Find or generate tendermint node key - let node_pk = try_parse_public_key( - format!("validator {name} Tendermint node key"), - &config.tendermint_node_key, - ) - .unwrap_or_else(|| { - // Generate a node key with ed25519 as default - let node_sk = common::SecretKey::Ed25519( - ed25519::SigScheme::generate(&mut rng), - ); - - let node_pk = write_tendermint_node_key(&tm_home_dir, node_sk); - - tendermint_node::write_validator_state(&tm_home_dir); - - node_pk - }); - - // Derive the node ID from the node key - let node_id: TendermintNodeId = id_from_pk(&node_pk); - - // Build the list of persistent peers from the validators' node IDs - let peer = TendermintAddress::from_str(&format!( - "{}@{}", - node_id, - config.net_address.as_ref().unwrap(), - )) - .expect("Validator address must be valid"); - persistent_peers.push(peer); - - // Generate account and reward addresses - let address = address::gen_established_address("validator account"); - config.address = Some(address.to_string()); - - // Generate the consensus, account and reward keys, unless they're - // pre-defined. Do not use mnemonic code / HD derivation path. - let mut wallet = crate::wallet::load_or_new(&chain_dir); - - let consensus_pk = try_parse_public_key( - format!("validator {name} consensus key"), - &config.consensus_public_key, - ) + // Load and validate the templates + let templates = genesis::templates::load_and_validate(&templates_path) .unwrap_or_else(|| { - let alias = format!("{}-consensus-key", name); - println!("Generating validator {} consensus key...", name); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Ed25519, - Some(alias), - true, - None, - password, - None, - ) - .expect("Key generation should not fail."); - - // Write consensus key for Tendermint - tendermint_node::write_validator_key(&tm_home_dir, &keypair); - - keypair.ref_to() - }); - - let account_pk = try_parse_public_key( - format!("validator {name} account key"), - &config.account_public_key, - ) - .unwrap_or_else(|| { - let alias = format!("{}-account-key", name); - println!("Generating validator {} account key...", name); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Ed25519, - Some(alias), - true, - None, - password, - None, - ) - .expect("Key generation should not fail."); - keypair.ref_to() - }); - - let protocol_pk = try_parse_public_key( - format!("validator {name} protocol key"), - &config.protocol_public_key, - ) - .unwrap_or_else(|| { - let alias = format!("{}-protocol-key", name); - println!("Generating validator {} protocol signing key...", name); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Ed25519, - Some(alias), - true, - None, - password, - None, - ) - .expect("Key generation should not fail."); - keypair.ref_to() + eprintln!("Invalid templates, aborting."); + safe_exit(1) }); - let eth_hot_pk = try_parse_public_key( - format!("validator {name} eth hot key"), - &config.eth_hot_key, - ) - .unwrap_or_else(|| { - let alias = format!("{}-eth-hot-key", name); - println!("Generating validator {} eth hot key...", name); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Secp256k1, - Some(alias), - true, - None, - password, - None, - ) - .expect("Key generation should not fail."); - keypair.ref_to() - }); + // In addition to standard templates validation, check that there is at + // least one validator account. + if !templates.transactions.has_at_least_one_validator() { + eprintln!("No validator genesis transaction found, aborting."); + safe_exit(1) + } - let eth_cold_pk = try_parse_public_key( - format!("validator {name} eth cold key"), - &config.eth_cold_key, + // Also check that at least one validator account has positive voting power. + let tm_votes_per_token = templates.parameters.pos_params.tm_votes_per_token; + if !templates + .transactions + .has_validator_with_positive_voting_power(tm_votes_per_token) + { + let min_stake = token::Amount::from_uint( + if tm_votes_per_token > Dec::from(1) { + Uint::one() + } else { + (Dec::from(1) / tm_votes_per_token).ceil().abs() + }, + NATIVE_MAX_DECIMAL_PLACES, ) - .unwrap_or_else(|| { - let alias = format!("{}-eth-cold-key", name); - println!("Generating validator {} eth cold key...", name); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Secp256k1, - Some(alias), - true, - None, - password, - None, - ) - .expect("Key generation should not fail."); - keypair.ref_to() - }); - - let dkg_pk = &config - .dkg_public_key - .as_ref() - .map(|key| { - key.to_dkg_public_key().unwrap_or_else(|err| { - let label = format!("validator {name} DKG key"); - eprintln!("Invalid {label} key: {}", err); - cli::safe_exit(1) - }) - }) - .unwrap_or_else(|| { - println!( - "Generating validator {} DKG session keypair...", - name - ); - - let validator_keys = crate::wallet::gen_validator_keys( - &mut wallet, - Some(eth_hot_pk.clone()), - Some(protocol_pk.clone()), - SchemeType::Ed25519, - ) - .expect("Generating new validator keys should not fail"); - let pk = validator_keys.dkg_keypair.as_ref().unwrap().public(); - wallet.add_validator_data(address.clone(), validator_keys); - pk - }); - - // Add the validator public keys to genesis config - config.consensus_public_key = - Some(genesis_config::HexString(consensus_pk.to_string())); - config.account_public_key = - Some(genesis_config::HexString(account_pk.to_string())); - config.eth_cold_key = - Some(genesis_config::HexString(eth_cold_pk.to_string())); - config.eth_hot_key = - Some(genesis_config::HexString(eth_hot_pk.to_string())); - - config.protocol_public_key = - Some(genesis_config::HexString(protocol_pk.to_string())); - config.dkg_public_key = - Some(genesis_config::HexString(dkg_pk.to_string())); - - // Write keypairs to wallet - wallet.add_address(name.clone(), address, true); - - crate::wallet::save(&wallet).unwrap(); - }); - - // Create a wallet for all accounts other than validators. - // Do not use mnemonic code / HD derivation path. - let mut wallet = - crate::wallet::load_or_new(&accounts_dir.join(NET_OTHER_ACCOUNTS_DIR)); - if let Some(established) = &mut config.established { - established.iter_mut().for_each(|(name, config)| { - init_established_account( - name, - &mut wallet, - config, - unsafe_dont_encrypt, - ); - }) + .unwrap(); + eprintln!( + "No validator with positive voting power, aborting. The minimum \ + staked tokens amount required to run the network is {}, because \ + there are {tm_votes_per_token} votes per NAMNAM tokens.", + min_stake.to_string_native(), + ); + safe_exit(1) } - config.token.iter_mut().for_each(|(_name, config)| { - if config.address.is_none() { - let address = address::gen_established_address("token"); - config.address = Some(address.to_string()); - } - if config.vp.is_none() { - config.vp = Some("vp_token".to_string()); - } - }); + // Finalize the genesis config to derive the chain ID + let genesis = genesis::chain::finalize( + templates, + chain_id_prefix, + genesis_time, + consensus_timeout_commit, + ); + let chain_id = &genesis.metadata.chain_id; + let chain_dir = global_args.base_dir.join(chain_id.as_str()); - if let Some(implicit) = &mut config.implicit { - implicit.iter_mut().for_each(|(name, config)| { - if config.public_key.is_none() { - println!( - "Generating implicit account {} key and address ...", - name - ); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Ed25519, - Some(name.clone()), - true, - None, - password, - None, - ) - .expect("Key generation should not fail."); - let public_key = - genesis_config::HexString(keypair.ref_to().to_string()); - config.public_key = Some(public_key); + // Check that chain dir is empty + if chain_dir.exists() && chain_dir.read_dir().unwrap().next().is_some() { + println!( + "The target chain directory {} already exists and is not empty.", + chain_dir.to_string_lossy() + ); + loop { + let mut buffer = String::new(); + print!( + "Do you want to override the chain directory? Will exit \ + otherwise. [y/N]: " + ); + std::io::stdout().flush().unwrap(); + match std::io::stdin().read_line(&mut buffer) { + Ok(size) if size > 0 => { + // Isolate the single character representing the choice + let byte = buffer.chars().next().unwrap(); + buffer.clear(); + match byte { + 'y' | 'Y' => { + fs::remove_dir_all(&chain_dir).unwrap(); + break; + } + 'n' | 'N' => { + println!("Exiting."); + safe_exit(1) + } + // Input is senseless fall through to repeat prompt + _ => { + println!("Unrecognized input."); + } + }; + } + _ => {} } - }) + } } + fs::create_dir_all(&chain_dir).unwrap(); - // Make a copy of genesis config without validator net addresses to - // `write_genesis_config`. Keep the original, because we still need the - // addresses to configure validators. - let mut config_clean = config.clone(); - config_clean - .validator - .iter_mut() - .for_each(|(_name, config)| { - config.net_address = None; - }); - - // Generate the chain ID first - let genesis = genesis_config::load_genesis_config(config_clean.clone()); - let genesis_bytes = genesis.serialize_to_vec(); - let chain_id = ChainId::from_genesis(chain_id_prefix, genesis_bytes); - let chain_dir = global_args.base_dir.join(chain_id.as_str()); - let genesis_path = global_args - .base_dir - .join(format!("{}.toml", chain_id.as_str())); - - // Write the genesis file - genesis_config::write_genesis_config(&config_clean, &genesis_path); - - // Add genesis addresses and save the wallet with other account keys - crate::wallet::add_genesis_addresses(&mut wallet, config_clean.clone()); - crate::wallet::save(&wallet).unwrap(); + // Write the finalized genesis config to the chain dir + genesis.write_toml_files(&chain_dir).unwrap_or_else(|err| { + eprintln!( + "Failed to write finalized genesis TOML files to {} with {err}.", + chain_dir.to_string_lossy() + ); + safe_exit(1) + }); // Write the global config setting the default chain ID let global_config = GlobalConfig::new(chain_id.clone()); global_config.write(&global_args.base_dir).unwrap(); - // Rename the generate chain config dir from `temp_chain_id` to `chain_id` - fs::rename(&temp_dir, &chain_dir).unwrap(); - // Copy the WASM checksums let wasm_dir_full = chain_dir.join(config::DEFAULT_WASM_DIR); fs::create_dir_all(&wasm_dir_full).unwrap(); @@ -753,149 +496,19 @@ pub fn init_network( ) .unwrap(); - config.validator.iter().for_each(|(name, _config)| { - let validator_dir = global_args - .base_dir - .join(chain_id.as_str()) - .join(NET_ACCOUNTS_DIR) - .join(name) - .join(config::DEFAULT_BASE_DIR); - let temp_validator_chain_dir = - validator_dir.join(temp_chain_id.as_str()); - let validator_chain_dir = validator_dir.join(chain_id.as_str()); - // Rename the generated directories for validators from `temp_chain_id` - // to `chain_id` - std::fs::rename(temp_validator_chain_dir, &validator_chain_dir) - .unwrap(); - - // Copy the WASM checksums - let wasm_dir_full = validator_chain_dir.join(config::DEFAULT_WASM_DIR); - fs::create_dir_all(&wasm_dir_full).unwrap(); - fs::copy( - &wasm_checksums_path, - wasm_dir_full.join(config::DEFAULT_WASM_CHECKSUMS_FILE), - ) - .unwrap(); - - // Write the genesis and global config into validator sub-dirs - genesis_config::write_genesis_config( - &config, - validator_dir.join(format!("{}.toml", chain_id.as_str())), - ); - global_config.write(validator_dir).unwrap(); - // Add genesis addresses to the validator's wallet - let mut wallet = crate::wallet::load_or_new(&validator_chain_dir); - crate::wallet::add_genesis_addresses(&mut wallet, config_clean.clone()); - crate::wallet::save(&wallet).unwrap(); - }); - - // Generate the validators' ledger config - config.validator.iter_mut().enumerate().for_each( - |(ix, (name, validator_config))| { - let accounts_dir = chain_dir.join(NET_ACCOUNTS_DIR); - let validator_dir = - accounts_dir.join(name).join(config::DEFAULT_BASE_DIR); - let mut config = Config::load( - &validator_dir, - &chain_id, - Some(TendermintMode::Validator), - ); - - // Configure the ledger - config.ledger.genesis_time = genesis.genesis_time.into(); - // In `config::Ledger`'s `base_dir`, `chain_id` and `tendermint`, - // the paths are prefixed with `validator_dir` given in the first - // parameter. We need to remove this prefix, because - // these sub-directories will be moved to validators' root - // directories. - config.ledger.shell.base_dir = config::DEFAULT_BASE_DIR.into(); - // Add a ledger P2P persistent peers - config.ledger.cometbft.p2p.persistent_peers = persistent_peers - .iter() - .enumerate() - .filter_map(|(index, peer)| - // we do not add the validator in its own persistent peer list - if index != ix { - Some(peer.to_owned()) - } else { - None - }) - .collect(); - - config.ledger.cometbft.consensus.timeout_commit = - consensus_timeout_commit; - config.ledger.cometbft.p2p.allow_duplicate_ip = allow_duplicate_ip; - config.ledger.cometbft.p2p.addr_book_strict = !localhost; - // Clear the net address from the config and use it to set ports - let net_address = validator_config.net_address.take().unwrap(); - let split: Vec<&str> = net_address.split(':').collect(); - let first_port = split[1].parse::().unwrap(); - if localhost { - config.ledger.cometbft.p2p.laddr = TendermintAddress::from_str( - &format!("127.0.0.1:{}", first_port), - ) - .unwrap(); - } else { - config.ledger.cometbft.p2p.laddr = TendermintAddress::from_str( - &format!("0.0.0.0:{}", first_port), - ) - .unwrap(); - } - if localhost { - config.ledger.cometbft.rpc.laddr = TendermintAddress::from_str( - &format!("127.0.0.1:{}", first_port + 1), - ) - .unwrap(); - } else { - config.ledger.cometbft.rpc.laddr = TendermintAddress::from_str( - &format!("0.0.0.0:{}", first_port + 1), - ) - .unwrap(); - } - if localhost { - config.ledger.cometbft.proxy_app = TendermintAddress::from_str( - &format!("127.0.0.1:{}", first_port + 2), - ) - .unwrap(); - } else { - config.ledger.cometbft.proxy_app = TendermintAddress::from_str( - &format!("0.0.0.0:{}", first_port + 2), - ) - .unwrap(); - } - config.write(&validator_dir, &chain_id, true).unwrap(); - }, - ); - - // Update the ledger config persistent peers and save it - let mut config = Config::load(&global_args.base_dir, &chain_id, None); - config.ledger.cometbft.p2p.persistent_peers = persistent_peers; - config.ledger.cometbft.consensus.timeout_commit = consensus_timeout_commit; - config.ledger.cometbft.p2p.allow_duplicate_ip = allow_duplicate_ip; - // Open P2P address - if !localhost { - config.ledger.cometbft.p2p.laddr = - TendermintAddress::from_str("0.0.0.0:26656").unwrap(); - } - config.ledger.cometbft.p2p.addr_book_strict = !localhost; - config.ledger.genesis_time = genesis.genesis_time.into(); - config - .write(&global_args.base_dir, &chain_id, true) - .unwrap(); - println!("Derived chain ID: {}", chain_id); - println!( - "Genesis file generated at {}", - genesis_path.to_string_lossy() - ); + println!("Genesis files stored at {}", chain_dir.to_string_lossy()); // Create a release tarball for anoma-network-config if !dont_archive { + // TODO: remove the `config::DEFAULT_BASE_DIR` and instead just archive + // the chain dir let mut release = tar::Builder::new(Vec::new()); - let release_genesis_path = PathBuf::from(config::DEFAULT_BASE_DIR) - .join(format!("{}.toml", chain_id.as_str())); release - .append_path_with_name(genesis_path, release_genesis_path) + .append_dir_all( + PathBuf::from(config::DEFAULT_BASE_DIR).join(chain_id.as_str()), + &chain_dir, + ) .unwrap(); let global_config_path = GlobalConfig::file_path(&global_args.base_dir); let release_global_config_path = @@ -906,13 +519,6 @@ pub fn init_network( release_global_config_path, ) .unwrap(); - let chain_config_path = - Config::file_path(&global_args.base_dir, &chain_id); - let release_chain_config_path = - Config::file_path(config::DEFAULT_BASE_DIR, &chain_id); - release - .append_path_with_name(chain_config_path, release_chain_config_path) - .unwrap(); let release_wasm_checksums_path = PathBuf::from(config::DEFAULT_BASE_DIR) .join(chain_id.as_str()) @@ -939,39 +545,20 @@ pub fn init_network( release_file.to_string_lossy() ); } -} -fn init_established_account( - name: impl AsRef, - wallet: &mut Wallet, - config: &mut genesis_config::EstablishedAccountConfig, - unsafe_dont_encrypt: bool, -) { - if config.address.is_none() { - let address = address::gen_established_address("established"); - config.address = Some(address.to_string()); - wallet.add_address(&name, address, true); - } - if config.public_key.is_none() { - println!("Generating established account {} key...", name.as_ref()); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (_alias, keypair, _mnemonic) = wallet - .gen_key( - SchemeType::Ed25519, - Some(format!("{}-key", name.as_ref())), - true, - None, - password, - None, // do not use mnemonic code / HD derivation path - ) - .expect("Key generation should not fail."); - let public_key = - genesis_config::HexString(keypair.ref_to().to_string()); - config.public_key = Some(public_key); - } - if config.vp.is_none() { - config.vp = Some("vp_user".to_string()); + // After the archive is created, try to copy the built WASM, if they're + // present with the checksums. This is used for local network setup, so + // that we can use a local WASM build. + let checksums = wasm_loader::Checksums::read_checksums(&wasm_dir_full); + for (_, full_name) in checksums.0 { + // try to copy built file from the Namada WASM root dir + let file = std::env::current_dir() + .unwrap() + .join(crate::config::DEFAULT_WASM_DIR) + .join(&full_name); + if file.exists() { + fs::copy(file, wasm_dir_full.join(&full_name)).unwrap(); + } } } @@ -996,34 +583,50 @@ pub fn default_base_dir( } /// Initialize genesis validator's address, consensus key and validator account -/// key and use it in the ledger's node. +/// key into a special "pre-genesis" wallet. pub fn init_genesis_validator( global_args: args::Global, args::InitGenesisValidator { + source, alias, commission_rate, max_commission_rate_change, net_address, unsafe_dont_encrypt, key_scheme, + transfer_from_source_amount, + self_bond_amount, }: args::InitGenesisValidator, ) { + let (mut source_wallet, wallet_file) = + load_pre_genesis_wallet_or_exit(&global_args.base_dir); + + let source_key = + source_wallet.find_key(&source, None).unwrap_or_else(|err| { + eprintln!( + "Couldn't find key for source \"{source}\" in the pre-genesis \ + wallet {}. Failed with {err}.", + wallet_file.to_string_lossy() + ); + safe_exit(1) + }); + // Validate the commission rate data if commission_rate > Dec::one() { eprintln!("The validator commission rate must not exceed 1.0 or 100%"); - cli::safe_exit(1) + safe_exit(1) } if max_commission_rate_change > Dec::one() { eprintln!( "The validator maximum change in commission rate per epoch must \ not exceed 1.0 or 100%" ); - cli::safe_exit(1) + safe_exit(1) } let pre_genesis_dir = validator_pre_genesis_dir(&global_args.base_dir, &alias); println!("Generating validator keys..."); - let pre_genesis = pre_genesis::gen_and_store( + let validator_wallet = pre_genesis::gen_and_store( key_scheme, unsafe_dont_encrypt, &pre_genesis_dir, @@ -1033,78 +636,67 @@ pub fn init_genesis_validator( "Unable to generate the validator pre-genesis wallet: {}", err ); - cli::safe_exit(1) + safe_exit(1) }); println!( "The validator's keys were stored in the wallet at {}", pre_genesis::validator_file_name(&pre_genesis_dir).to_string_lossy() ); - let validator_config = ValidatorPreGenesisConfig { - validator: HashMap::from_iter([( - alias, - genesis_config::ValidatorConfig { - consensus_public_key: Some(HexString( - pre_genesis.consensus_key.ref_to().to_string(), - )), - eth_cold_key: Some(HexString( - pre_genesis.eth_cold_key.ref_to().to_string(), - )), - eth_hot_key: Some(HexString( - pre_genesis.eth_hot_key.ref_to().to_string(), - )), - account_public_key: Some(HexString( - pre_genesis.account_key.ref_to().to_string(), - )), - protocol_public_key: Some(HexString( - pre_genesis - .store - .validator_keys - .protocol_keypair - .ref_to() - .to_string(), - )), - dkg_public_key: Some(HexString( - pre_genesis - .store - .validator_keys - .dkg_keypair - .as_ref() - .unwrap() - .public() - .to_string(), - )), - commission_rate: Some(commission_rate), - max_commission_rate_change: Some(max_commission_rate_change), - tendermint_node_key: Some(HexString( - pre_genesis.tendermint_node_key.ref_to().to_string(), - )), - net_address: Some(net_address), - ..Default::default() - }, - )]), - }; - let genesis_part = toml::to_string(&validator_config).unwrap(); - println!("Your public partial pre-genesis TOML configuration:"); + let transactions = genesis::transactions::init_validator( + genesis::transactions::GenesisValidatorData { + source_key, + alias: alias::Alias::from(alias), + commission_rate, + max_commission_rate_change, + net_address, + transfer_from_source_amount, + self_bond_amount, + }, + &mut source_wallet, + &validator_wallet, + ); + + let genesis_part = toml::to_string(&transactions).unwrap(); + println!("Your public signed pre-genesis transactions TOML:"); println!(); println!("{genesis_part}"); - let file_name = validator_pre_genesis_file(&pre_genesis_dir); + let file_name = validator_pre_genesis_txs_file(&pre_genesis_dir); fs::write(&file_name, genesis_part).unwrap_or_else(|err| { eprintln!( - "Couldn't write partial pre-genesis file to {}. Failed with: {}", + "Couldn't write pre-genesis transactions file to {}. Failed with: \ + {}", file_name.to_string_lossy(), err ); - cli::safe_exit(1) + safe_exit(1) }); println!(); println!( - "Pre-genesis TOML written to {}", + "Pre-genesis transactions TOML written to {}", file_name.to_string_lossy() ); } +/// Try to load a pre-genesis wallet or terminate if it cannot be found. +pub fn load_pre_genesis_wallet_or_exit( + base_dir: &Path, +) -> (Wallet, PathBuf) { + let pre_genesis_dir = base_dir.join(PRE_GENESIS_DIR); + let wallet_file = crate::wallet::wallet_file(&pre_genesis_dir); + ( + crate::wallet::load(&pre_genesis_dir).unwrap_or_else(|| { + eprintln!( + "No pre-genesis wallet found at {}.", + wallet_file.to_string_lossy() + ); + safe_exit(1) + }), + wallet_file, + ) +} + async fn download_file(url: impl AsRef) -> reqwest::Result { let url = url.as_ref(); let response = reqwest::get(url).await?; @@ -1113,19 +705,6 @@ async fn download_file(url: impl AsRef) -> reqwest::Result { Ok(contents) } -fn try_parse_public_key( - label: impl AsRef, - value: &Option, -) -> Option { - let label = label.as_ref(); - value.as_ref().map(|key| { - key.to_public_key().unwrap_or_else(|err| { - eprintln!("Invalid {label} key: {}", err); - cli::safe_exit(1) - }) - }) -} - fn network_configs_url_prefix(chain_id: &ChainId) -> String { std::env::var(ENV_VAR_NETWORK_CONFIGS_SERVER).unwrap_or_else(|_| { format!("{DEFAULT_NETWORK_CONFIGS_SERVER}/{chain_id}") @@ -1173,9 +752,9 @@ pub fn write_tendermint_node_key( node_pk } -/// The default path to a validator pre-genesis file. -pub fn validator_pre_genesis_file(pre_genesis_path: &Path) -> PathBuf { - pre_genesis_path.join("validator.toml") +/// The default path to a validator pre-genesis txs file. +pub fn validator_pre_genesis_txs_file(pre_genesis_path: &Path) -> PathBuf { + pre_genesis_path.join("transactions.toml") } /// The default validator pre-genesis directory @@ -1183,6 +762,76 @@ pub fn validator_pre_genesis_dir(base_dir: &Path, alias: &str) -> PathBuf { base_dir.join(PRE_GENESIS_DIR).join(alias) } +/// Validate genesis templates. Exits process if invalid. +pub fn validate_genesis_templates( + _global_args: args::Global, + args::ValidateGenesisTemplates { path }: args::ValidateGenesisTemplates, +) { + if genesis::templates::load_and_validate(&path).is_none() { + safe_exit(1) + } +} + +/// Sign genesis transactions. +pub fn sign_genesis_tx( + global_args: args::Global, + args::SignGenesisTx { path, output }: args::SignGenesisTx, +) { + let (mut wallet, _wallet_file) = + load_pre_genesis_wallet_or_exit(&global_args.base_dir); + + let contents = fs::read(&path).unwrap_or_else(|err| { + eprintln!( + "Unable to read from file {}. Failed with {err}.", + path.to_string_lossy() + ); + safe_exit(1); + }); + let unsigned = genesis::transactions::parse_unsigned(&contents) + .unwrap_or_else(|err| { + eprintln!( + "Unable to parse the TOML from {}. Failed with {err}.", + path.to_string_lossy() + ); + safe_exit(1); + }); + if unsigned.validator_account.is_some() + && !unsigned.validator_account.as_ref().unwrap().is_empty() + { + eprintln!( + "Validator transactions must be signed with a validator wallet. \ + You can use `namada client utils init-genesis-validator` \ + supplied with the required arguments to generate a validator \ + wallet and sign the validator genesis transactions." + ); + safe_exit(1); + } + let signed = genesis::transactions::sign_txs(unsigned, &mut wallet); + + match output { + Some(output_path) => { + let transactions = toml::to_vec(&signed).unwrap(); + fs::write(&output_path, transactions).unwrap_or_else(|err| { + eprintln!( + "Failed to write output to {} with {err}.", + output_path.to_string_lossy() + ); + safe_exit(1); + }); + println!( + "Your public signed transactions TOML has been written to {}", + output_path.to_string_lossy() + ); + } + None => { + let transactions = toml::to_string(&signed).unwrap(); + println!("Your public signed transactions TOML:"); + println!(); + println!("{transactions}"); + } + } +} + /// Add a spinning wheel to a message for long running commands. /// Can be turned off for E2E tests by setting the `REDUCED_CLI_PRINTING` /// environment variable. @@ -1207,19 +856,6 @@ where task.join().unwrap() } -fn is_valid_validator_for_current_chain( - tendermint_node_pk: &common::PublicKey, - genesis_config: &GenesisConfig, -) -> bool { - genesis_config.validator.iter().any(|(_alias, config)| { - if let Some(tm_node_key) = &config.tendermint_node_key { - tm_node_key.0.eq(&tendermint_node_pk.to_string()) - } else { - false - } - }) -} - /// Replace the contents of `addr` with a dummy address. #[inline] pub fn take_config_address(addr: &mut TendermintAddress) -> TendermintAddress { @@ -1232,3 +868,13 @@ pub fn take_config_address(addr: &mut TendermintAddress) -> TendermintAddress { }, ) } + +#[cfg(not(test))] +fn safe_exit(code: i32) -> ! { + crate::cli::safe_exit(code) +} + +#[cfg(test)] +fn safe_exit(code: i32) -> ! { + panic!("Process exited unsuccesfully with error code: {}", code); +} diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index ed0ffeb788..08a861e423 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -1,11 +1,21 @@ //! The parameters used for the chain's genesis +pub mod chain; +pub mod templates; +pub mod toml_utils; +pub mod transactions; + +use std::array::TryFromSliceError; use std::collections::{BTreeMap, HashMap}; +use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; +use data_encoding::HEXLOWER; use derivative::Derivative; use namada::core::ledger::governance::parameters::GovernanceParameters; use namada::core::ledger::pgf::parameters::PgfParameters; +use namada::core::types::string_encoding; +use namada::ledger::eth_bridge::EthereumBridgeParams; use namada::ledger::parameters::EpochDuration; use namada::ledger::pos::{Dec, GenesisValidator, OwnedPosParams}; use namada::types::address::Address; @@ -15,722 +25,10 @@ use namada::types::key::*; use namada::types::time::{DateTimeUtc, DurationSecs}; use namada::types::token::Denomination; use namada::types::{storage, token}; -use namada_sdk::eth_bridge::EthereumBridgeConfig; - -/// Genesis configuration file format -pub mod genesis_config { - use std::array::TryFromSliceError; - use std::collections::{BTreeMap, BTreeSet, HashMap}; - use std::convert::TryInto; - use std::path::Path; - use std::str::FromStr; - - use data_encoding::HEXLOWER; - use eyre::Context; - use namada::core::ledger::governance::parameters::GovernanceParameters; - use namada::core::ledger::pgf::parameters::PgfParameters; - use namada::ledger::parameters::EpochDuration; - use namada::ledger::pos::{Dec, GenesisValidator, OwnedPosParams}; - use namada::types::address::Address; - use namada::types::chain::ProposalBytes; - use namada::types::key::dkg_session_keys::DkgPublicKey; - use namada::types::key::*; - use namada::types::time::Rfc3339String; - use namada::types::token::Denomination; - use namada::types::{storage, token}; - use serde::{Deserialize, Serialize}; - use thiserror::Error; - - use super::{ - EstablishedAccount, EthereumBridgeConfig, Genesis, ImplicitAccount, - Parameters, TokenAccount, Validator, - }; - use crate::cli; - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct HexString(pub String); - - impl HexString { - pub fn to_bytes(&self) -> Result, HexKeyError> { - let bytes = HEXLOWER.decode(self.0.as_ref())?; - Ok(bytes) - } - - pub fn to_sha256_bytes(&self) -> Result<[u8; 32], HexKeyError> { - let bytes = HEXLOWER.decode(self.0.as_ref())?; - let slice = bytes.as_slice(); - let array: [u8; 32] = slice.try_into()?; - Ok(array) - } - - pub fn to_public_key(&self) -> Result { - let key = common::PublicKey::from_str(&self.0) - .map_err(HexKeyError::InvalidPublicKey)?; - Ok(key) - } - - pub fn to_dkg_public_key(&self) -> Result { - let key = DkgPublicKey::from_str(&self.0)?; - Ok(key) - } - } - - #[derive(Error, Debug)] - pub enum HexKeyError { - #[error("Invalid hex string: {0:?}")] - InvalidHexString(data_encoding::DecodeError), - #[error("Invalid sha256 checksum: {0}")] - InvalidSha256(TryFromSliceError), - #[error("Invalid public key: {0}")] - InvalidPublicKey(ParsePublicKeyError), - } - - impl From for HexKeyError { - fn from(err: data_encoding::DecodeError) -> Self { - Self::InvalidHexString(err) - } - } - - impl From for HexKeyError { - fn from(err: ParsePublicKeyError) -> Self { - Self::InvalidPublicKey(err) - } - } - - impl From for HexKeyError { - fn from(err: TryFromSliceError) -> Self { - Self::InvalidSha256(err) - } - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct GenesisConfig { - // Genesis timestamp - pub genesis_time: Rfc3339String, - // Name of the native token - this must one of the tokens included in - // the `token` field - pub native_token: String, - // Initial validator set - pub validator: HashMap, - // Token accounts present at genesis - pub token: HashMap, - // Established accounts present at genesis - pub established: Option>, - // Implicit accounts present at genesis - pub implicit: Option>, - // Protocol parameters - pub parameters: ParametersConfig, - // PoS parameters - pub pos_params: PosParamsConfig, - // Governance parameters - pub gov_params: GovernanceParamsConfig, - // Pgf parameters - pub pgf_params: PgfParametersConfig, - // Ethereum bridge config - pub ethereum_bridge_params: Option, - // Wasm definitions - pub wasm: HashMap, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct GovernanceParamsConfig { - // Min funds to stake to submit a proposal - pub min_proposal_fund: u64, - // Maximum size of proposal in kibibytes (KiB) - pub max_proposal_code_size: u64, - // Minimum proposal period length in epochs - pub min_proposal_voting_period: u64, - // Maximum proposal period length in epochs - pub max_proposal_period: u64, - // Maximum number of characters in the proposal content - pub max_proposal_content_size: u64, - // Minimum number of epoch between end and grace epoch - pub min_proposal_grace_epochs: u64, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct PgfParametersConfig { - /// The set of stewards - pub stewards: BTreeSet
, - /// The pgf inflation rate - pub pgf_inflation_rate: Dec, - /// The stewards inflation rate - pub stewards_inflation_rate: Dec, - } +use serde::{Deserialize, Serialize}; - /// Validator pre-genesis configuration can be created with client utils - /// `init-genesis-validator` command and added to a genesis for - /// `init-network` cmd and that can be subsequently read by `join-network` - /// cmd to setup a genesis validator node. - #[derive(Serialize, Deserialize, Debug)] - pub struct ValidatorPreGenesisConfig { - pub validator: HashMap, - } - - #[derive(Clone, Default, Debug, Deserialize, Serialize)] - pub struct ValidatorConfig { - // Public key for consensus. (default: generate) - pub consensus_public_key: Option, - // Public key (cold) for eth governance. (default: generate) - pub eth_cold_key: Option, - // Public key (hot) for eth bridge. (default: generate) - pub eth_hot_key: Option, - // Public key for validator account. (default: generate) - pub account_public_key: Option, - // Public protocol signing key for validator account. (default: - // generate) - pub protocol_public_key: Option, - // Public DKG session key for validator account. (default: generate) - pub dkg_public_key: Option, - // Validator address (default: generate). - pub address: Option, - // Total number of tokens held at genesis. - pub tokens: Option, - // Unstaked balance at genesis. - pub non_staked_balance: Option, - /// Commission rate charged on rewards for delegators (bounded inside - /// 0-1) - pub commission_rate: Option, - /// Maximum change in commission rate permitted per epoch - pub max_commission_rate_change: Option, - // Filename of validator VP. (default: default validator VP) - pub validator_vp: Option, - // IP:port of the validator. (used in generation only) - pub net_address: Option, - /// Tendermint node key is used to derive Tendermint node ID for node - /// authentication - pub tendermint_node_key: Option, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct TokenAccountConfig { - // Address of token account (default: generate). - pub address: Option, - // The number of decimal places amounts of this token has - pub denom: Denomination, - // Filename of token account VP. (default: token VP) - pub vp: Option, - // Initial balances held by accounts defined elsewhere. - pub balances: Option>, - // Token parameters - pub parameters: Option, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct EstablishedAccountConfig { - // Address of established account (default: generate). - pub address: Option, - // Filename of established account VP. (default: user VP) - pub vp: Option, - // Public key of established account. (default: generate) - pub public_key: Option, - // Initial storage key values. - pub storage: Option>, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct ImplicitAccountConfig { - // Public key of implicit account (default: generate). - pub public_key: Option, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct ParametersConfig { - /// Max payload size, in bytes, for a tx batch proposal. - /// - /// Block proposers may never return a `PrepareProposal` - /// response containing `txs` with a byte length greater - /// than whatever is configured through this parameter. - /// - /// Note that this parameter's value will always be strictly - /// smaller than a Tendermint block's `MaxBytes` consensus - /// parameter. Currently, we hard cap `max_proposal_bytes` - /// at 90 MiB in Namada, which leaves at least 10 MiB of - /// 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). - // TODO: this is i64 because datetime wants it - pub max_expected_time_per_block: i64, - /// Hashes of whitelisted vps array. `None` value or an empty array - /// disables whitelisting. - pub vp_whitelist: Option>, - /// Hashes of whitelisted txs array. `None` value or an empty array - /// disables whitelisting. - pub tx_whitelist: Option>, - /// Filename of implicit accounts validity predicate WASM code - pub implicit_vp: String, - /// Expected number of epochs per year - pub epochs_per_year: u64, - /// Max signature per transaction - pub max_signatures_per_transaction: u8, - /// PoS gain p - pub pos_gain_p: Dec, - /// PoS gain d - pub pos_gain_d: Dec, - /// 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 minimum_gas_price: BTreeMap, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct PosParamsConfig { - // Maximum number of consensus validators. - pub max_validator_slots: u64, - // Pipeline length (in epochs). - pub pipeline_len: u64, - // Unbonding length (in epochs). - pub unbonding_len: u64, - // Votes per token. - pub tm_votes_per_token: Dec, - // Reward for proposing a block. - pub block_proposer_reward: Dec, - // Reward for voting on a block. - pub block_vote_reward: Dec, - // Maximum staking APY - pub max_inflation_rate: Dec, - // Target ratio of staked NAM tokens to total NAM tokens - pub target_staked_ratio: Dec, - // Portion of a validator's stake that should be slashed on a - // duplicate vote. - pub duplicate_vote_min_slash_rate: Dec, - // Portion of a validator's stake that should be slashed on a - // light client attack. - pub light_client_attack_min_slash_rate: Dec, - /// Number of epochs above and below (separately) the current epoch to - /// consider when doing cubic slashing - pub cubic_slashing_window_length: u64, - /// The minimum amount of bonded tokens that a validator needs to be in - /// either the `consensus` or `below_capacity` validator sets - pub validator_stake_threshold: token::Amount, - } - - #[derive(Clone, Debug, Deserialize, Serialize)] - pub struct WasmConfig { - filename: String, - pub sha256: Option, - } - - fn load_validator( - config: &ValidatorConfig, - wasm: &HashMap, - ) -> Validator { - let validator_vp_name = config.validator_vp.as_ref().unwrap(); - let validator_vp_config = wasm.get(validator_vp_name).unwrap(); - - Validator { - pos_data: GenesisValidator { - address: Address::decode(config.address.as_ref().unwrap()) - .unwrap(), - tokens: token::Amount::native_whole( - config.tokens.unwrap_or_default(), - ), - consensus_key: config - .consensus_public_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), - protocol_key: config - .protocol_public_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), - eth_cold_key: config - .eth_cold_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), - eth_hot_key: config - .eth_hot_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), - commission_rate: config - .commission_rate - .and_then(|rate| { - if rate <= Dec::one() { Some(rate) } else { None } - }) - .expect("Commission rate must be between 0.0 and 1.0"), - max_commission_rate_change: config - .max_commission_rate_change - .and_then(|rate| { - if rate <= Dec::one() { Some(rate) } else { None } - }) - .expect( - "Max commission rate change must be between 0.0 and \ - 1.0", - ), - }, - account_key: config - .account_public_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), - dkg_public_key: config - .dkg_public_key - .as_ref() - .unwrap() - .to_dkg_public_key() - .unwrap(), - non_staked_balance: token::Amount::native_whole( - config.non_staked_balance.unwrap_or_default(), - ), - validator_vp_code_path: validator_vp_config.filename.to_owned(), - validator_vp_sha256: validator_vp_config - .sha256 - .clone() - .unwrap() - .to_sha256_bytes() - .unwrap(), - } - } - - fn load_token( - config: &TokenAccountConfig, - validators: &HashMap, - established_accounts: &HashMap, - implicit_accounts: &HashMap, - ) -> TokenAccount { - TokenAccount { - last_locked_ratio: Dec::zero(), - last_inflation: token::Amount::zero(), - parameters: config.parameters.as_ref().unwrap().to_owned(), - address: Address::decode(config.address.as_ref().unwrap()).unwrap(), - denom: config.denom, - balances: config - .balances - .as_ref() - .unwrap_or(&HashMap::default()) - .iter() - .map(|(alias_or_address, amount)| { - ( - match Address::decode(alias_or_address) { - Ok(address) => address, - Err(decode_err) => { - if let Some(alias) = - alias_or_address.strip_suffix(".public_key") - { - if let Some(established) = - established_accounts.get(alias) - { - established - .public_key - .as_ref() - .unwrap() - .into() - } else if let Some(validator) = - validators.get(alias) - { - (&validator.account_key).into() - } else { - eprintln!( - "No established or validator \ - account with alias {} found", - alias - ); - cli::safe_exit(1) - } - } else if let Some(established) = - established_accounts.get(alias_or_address) - { - established.address.clone() - } else if let Some(validator) = - validators.get(alias_or_address) - { - validator.pos_data.address.clone() - } else if let Some(implicit) = - implicit_accounts.get(alias_or_address) - { - (&implicit.public_key).into() - } else { - eprintln!( - "{} is unknown alias and not a valid \ - address: {}", - alias_or_address, decode_err - ); - cli::safe_exit(1) - } - } - }, - token::Amount::from_uint(*amount, config.denom).expect( - "expected a balance that fits into 256 bits", - ), - ) - }) - .collect(), - } - } - - fn load_established( - config: &EstablishedAccountConfig, - wasm: &HashMap, - ) -> EstablishedAccount { - let account_vp_name = config.vp.as_ref().unwrap(); - let account_vp_config = wasm.get(account_vp_name).unwrap(); - - EstablishedAccount { - address: Address::decode(config.address.as_ref().unwrap()).unwrap(), - vp_code_path: account_vp_config.filename.to_owned(), - vp_sha256: account_vp_config - .sha256 - .clone() - .unwrap_or_else(|| { - eprintln!("Unknown user VP WASM sha256"); - cli::safe_exit(1); - }) - .to_sha256_bytes() - .unwrap(), - public_key: config - .public_key - .as_ref() - .map(|hex| hex.to_public_key().unwrap()), - storage: config - .storage - .as_ref() - .unwrap_or(&HashMap::default()) - .iter() - .map(|(address, hex)| { - ( - storage::Key::parse(address).unwrap(), - hex.to_bytes().unwrap(), - ) - }) - .collect(), - } - } - - fn load_implicit(config: &ImplicitAccountConfig) -> ImplicitAccount { - ImplicitAccount { - public_key: config - .public_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), - } - } - - pub fn load_genesis_config(config: GenesisConfig) -> Genesis { - let GenesisConfig { - genesis_time, - native_token, - validator, - token, - established, - implicit, - parameters, - pos_params, - gov_params, - pgf_params, - wasm, - ethereum_bridge_params, - } = config; - - let native_token = Address::decode( - token - .get(&native_token) - .expect( - "Native token's alias must be present in the declared \ - tokens", - ) - .address - .as_ref() - .expect("Missing native token address"), - ) - .expect("Invalid address"); - let validators: HashMap = validator - .iter() - .map(|(name, cfg)| (name.clone(), load_validator(cfg, &wasm))) - .collect(); - let established_accounts: HashMap = - established - .unwrap_or_default() - .iter() - .map(|(name, cfg)| (name.clone(), load_established(cfg, &wasm))) - .collect(); - let implicit_accounts: HashMap = implicit - .unwrap_or_default() - .iter() - .map(|(name, cfg)| (name.clone(), load_implicit(cfg))) - .collect(); - #[allow(clippy::iter_kv_map)] - let token_accounts = token - .iter() - .map(|(_name, cfg)| { - load_token( - cfg, - &validators, - &established_accounts, - &implicit_accounts, - ) - }) - .collect(); - - let implicit_vp_config = wasm.get(¶meters.implicit_vp).unwrap(); - let implicit_vp_code_path = implicit_vp_config.filename.to_owned(); - let implicit_vp_sha256 = implicit_vp_config - .sha256 - .clone() - .unwrap_or_else(|| { - eprintln!("Unknown implicit VP WASM sha256"); - cli::safe_exit(1); - }) - .to_sha256_bytes() - .unwrap(); - - 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, - min_duration: namada::types::time::Duration::seconds( - min_duration, - ) - .into(), - }, - max_expected_time_per_block: - namada::types::time::Duration::seconds( - parameters.max_expected_time_per_block, - ) - .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, - implicit_vp_sha256, - epochs_per_year: parameters.epochs_per_year, - max_signatures_per_transaction: parameters - .max_signatures_per_transaction, - pos_gain_p: parameters.pos_gain_p, - pos_gain_d: parameters.pos_gain_d, - staked_ratio: Dec::zero(), - pos_inflation_amount: token::Amount::zero(), - minimum_gas_price: parameters.minimum_gas_price, - fee_unshielding_gas_limit: parameters.fee_unshielding_gas_limit, - fee_unshielding_descriptions_limit: parameters - .fee_unshielding_descriptions_limit, - }; - - let GovernanceParamsConfig { - min_proposal_fund, - max_proposal_code_size, - min_proposal_voting_period, - max_proposal_content_size, - min_proposal_grace_epochs, - max_proposal_period, - } = gov_params; - let gov_params = GovernanceParameters { - min_proposal_fund: token::Amount::native_whole(min_proposal_fund), - max_proposal_code_size, - min_proposal_voting_period, - max_proposal_content_size, - min_proposal_grace_epochs, - max_proposal_period, - }; - - let PgfParametersConfig { - stewards, - pgf_inflation_rate, - stewards_inflation_rate, - } = pgf_params; - let pgf_params = PgfParameters { - stewards, - pgf_inflation_rate, - stewards_inflation_rate, - }; - - let PosParamsConfig { - max_validator_slots, - pipeline_len, - unbonding_len, - tm_votes_per_token, - block_proposer_reward, - block_vote_reward, - max_inflation_rate, - target_staked_ratio, - duplicate_vote_min_slash_rate, - light_client_attack_min_slash_rate, - cubic_slashing_window_length, - validator_stake_threshold, - } = pos_params; - - let pos_params = OwnedPosParams { - max_validator_slots, - pipeline_len, - unbonding_len, - tm_votes_per_token, - block_proposer_reward, - block_vote_reward, - max_inflation_rate, - target_staked_ratio, - duplicate_vote_min_slash_rate, - light_client_attack_min_slash_rate, - cubic_slashing_window_length, - validator_stake_threshold, - }; - - let mut genesis = Genesis { - genesis_time: genesis_time.try_into().unwrap(), - native_token, - validators: validators.into_values().collect(), - token_accounts, - established_accounts: established_accounts.into_values().collect(), - implicit_accounts: implicit_accounts.into_values().collect(), - parameters, - pos_params, - gov_params, - pgf_params, - ethereum_bridge_params, - }; - genesis.init(); - genesis - } - - pub fn open_genesis_config( - path: impl AsRef, - ) -> color_eyre::eyre::Result { - let config_file = - std::fs::read_to_string(&path).wrap_err_with(|| { - format!( - "couldn't read genesis config file from {}", - path.as_ref().to_string_lossy() - ) - })?; - toml::from_str(&config_file).wrap_err_with(|| { - format!( - "couldn't parse TOML from {}", - path.as_ref().to_string_lossy() - ) - }) - } - - pub fn write_genesis_config( - config: &GenesisConfig, - path: impl AsRef, - ) { - let toml = toml::to_string(&config).unwrap(); - std::fs::write(path, toml).unwrap(); - } - - pub fn read_genesis_config(path: impl AsRef) -> Genesis { - load_genesis_config(open_genesis_config(path).unwrap()) - } -} +#[cfg(all(any(test, feature = "benches"), not(feature = "integration")))] +use crate::config::genesis::chain::Finalized; #[derive(Debug, BorshSerialize, BorshDeserialize)] #[borsh(init=init)] @@ -746,7 +44,7 @@ pub struct Genesis { pub gov_params: GovernanceParameters, pub pgf_params: PgfParameters, // Ethereum bridge config - pub ethereum_bridge_params: Option, + pub ethereum_bridge_params: Option, } impl Genesis { @@ -895,254 +193,248 @@ pub struct Parameters { pub minimum_gas_price: BTreeMap, } -#[cfg(not(any(test, feature = "dev")))] -pub fn genesis( - base_dir: impl AsRef, - chain_id: &namada::types::chain::ChainId, -) -> Genesis { - let path = base_dir - .as_ref() - .join(format!("{}.toml", chain_id.as_str())); - genesis_config::read_genesis_config(path) +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialOrd, + Ord, + PartialEq, + Eq, +)] +pub struct HexString(pub String); + +impl HexString { + pub fn to_bytes(&self) -> Result, HexKeyError> { + let bytes = HEXLOWER.decode(self.0.as_ref())?; + Ok(bytes) + } + + pub fn to_sha256_bytes(&self) -> Result<[u8; 32], HexKeyError> { + let bytes = HEXLOWER.decode(self.0.as_ref())?; + let slice = bytes.as_slice(); + let array: [u8; 32] = slice.try_into()?; + Ok(array) + } + + pub fn to_public_key(&self) -> Result { + let key = common::PublicKey::from_str(&self.0) + .map_err(HexKeyError::InvalidPublicKey)?; + Ok(key) + } + + pub fn to_dkg_public_key(&self) -> Result { + let key = DkgPublicKey::from_str(&self.0)?; + Ok(key) + } } -#[cfg(any(test, feature = "dev"))] -pub fn genesis(num_validators: u64) -> Genesis { - use namada::types::address::{ - self, apfel, btc, dot, eth, kartoffel, nam, schnitzel, wnam, - }; - use namada::types::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; - use namada::types::ethereum_events::EthAddress; - use namada::types::uint::Uint; - use namada_sdk::eth_bridge::{ - Contracts, Erc20WhitelistEntry, UpgradeableContract, - }; - use crate::wallet; +#[derive(thiserror::Error, Debug)] +pub enum HexKeyError { + #[error("Invalid hex string: {0:?}")] + InvalidHexString(data_encoding::DecodeError), + #[error("Invalid sha256 checksum: {0}")] + InvalidSha256(TryFromSliceError), + #[error("Invalid public key: {0}")] + InvalidPublicKey(string_encoding::DecodeError), +} + +impl From for HexKeyError { + fn from(err: data_encoding::DecodeError) -> Self { + Self::InvalidHexString(err) + } +} + +impl From for HexKeyError { + fn from(err: string_encoding::DecodeError) -> Self { + Self::InvalidPublicKey(err) + } +} + +impl From for HexKeyError { + fn from(err: TryFromSliceError) -> Self { + Self::InvalidSha256(err) + } +} + +/// Modify the default genesis file (namada/genesis/localnet/) to +/// accommodate testing. +/// +/// This includes adding the Ethereum bridge parameters and +/// adding a specified number of validators. +#[cfg(all(any(test, feature = "benches"), not(feature = "integration")))] +pub fn make_dev_genesis(num_validators: u64) -> Finalized { + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::time::Duration; + + use namada::core::types::string_encoding::StringEncoded; + use namada::ledger::eth_bridge::{Contracts, UpgradeableContract}; + use namada::proto::{standalone_signature, SerializeWithBorsh}; + use namada_sdk::wallet::alias::Alias; + use namada::types::address::wnam; + use namada::types::chain::ChainIdPrefix; + use namada::types::ethereum_events::EthAddress; + use namada::types::token::NATIVE_MAX_DECIMAL_PLACES; - let vp_implicit_path = "vp_implicit.wasm"; - let vp_user_path = "vp_user.wasm"; + use crate::config::genesis::chain::finalize; + use crate::wallet::defaults; - // NOTE When the validator's key changes, tendermint must be reset with - // `namada reset` command. To generate a new validator, use the - // `tests::gen_genesis_validator` below. - let mut validators = Vec::::new(); + let mut current_path = std::env::current_dir() + .expect("Current directory should exist") + .canonicalize() + .expect("Current directory should exist"); + while current_path.file_name().unwrap() != "apps" { + current_path.pop(); + } + current_path.pop(); + let chain_dir = current_path.join("genesis").join("localnet"); + let templates = templates::load_and_validate(&chain_dir) + .expect("Missing genesis files"); + let mut genesis = finalize( + templates, + ChainIdPrefix::from_str("test").unwrap(), + DateTimeUtc::now(), + Duration::from_secs(30).into(), + ); + + // Add Ethereum bridge params. + genesis.parameters.eth_bridge_params = Some(templates::EthBridgeParams { + eth_start_height: Default::default(), + min_confirmations: Default::default(), + contracts: Contracts { + native_erc20: wnam(), + bridge: UpgradeableContract { + address: EthAddress([0; 20]), + version: Default::default(), + }, + }, + erc20_whitelist: vec![], + }); - // Use hard-coded keys for the first validator to avoid breaking other code - let consensus_keypair = wallet::defaults::validator_keypair(); - let account_keypair = wallet::defaults::validator_keypair(); + if let Some(vals) = genesis.transactions.validator_account.as_mut() { + vals[0].address = defaults::validator_address(); + } + let default_addresses: HashMap = + defaults::addresses().into_iter().collect(); + if let Some(accs) = genesis.transactions.established_account.as_mut() { + for acc in accs { + if let Some(addr) = default_addresses.get(&acc.tx.alias) { + acc.address = addr.clone(); + } + } + } + // remove Albert's bond since it messes up existing unit test math + if let Some(bonds) = genesis.transactions.bond.as_mut() { + bonds.retain(|bond| { + bond.source + != transactions::AliasOrPk::Alias( + Alias::from_str("albert").unwrap(), + ) + }) + }; let secp_eth_cold_keypair = secp256k1::SecretKey::try_from_slice(&[ 90, 83, 107, 155, 193, 251, 120, 27, 76, 1, 188, 8, 116, 121, 90, 99, 65, 17, 187, 6, 238, 141, 63, 188, 76, 38, 102, 7, 47, 185, 28, 52, ]) .unwrap(); - - let eth_cold_keypair = - common::SecretKey::try_from_sk(&secp_eth_cold_keypair).unwrap(); - let address = wallet::defaults::validator_address(); - let (protocol_keypair, eth_bridge_keypair, dkg_keypair) = - wallet::defaults::validator_keys(); - let validator = Validator { - pos_data: GenesisValidator { - address, - tokens: token::Amount::native_whole(200_000), - consensus_key: consensus_keypair.ref_to(), - protocol_key: protocol_keypair.ref_to(), - commission_rate: Dec::new(5, 2).expect("This can't fail"), - max_commission_rate_change: Dec::new(1, 2) - .expect("This can't fail"), - eth_cold_key: eth_cold_keypair.ref_to(), - eth_hot_key: eth_bridge_keypair.ref_to(), + let sign_pk = |sk: &common::SecretKey| transactions::SignedPk { + pk: StringEncoded { raw: sk.ref_to() }, + authorization: StringEncoded { + raw: standalone_signature::<_, SerializeWithBorsh>( + sk, + &sk.ref_to(), + ), }, - account_key: account_keypair.ref_to(), - dkg_public_key: dkg_keypair.public(), - non_staked_balance: token::Amount::native_whole(100_000), - // TODO replace with https://github.com/anoma/namada/issues/25) - validator_vp_code_path: vp_user_path.into(), - validator_vp_sha256: Default::default(), }; - validators.push(validator); - // Add other validators with randomly generated keys if needed - for _ in 0..(num_validators - 1) { + for val in 0..(num_validators - 1) { let consensus_keypair: common::SecretKey = testing::gen_keypair::() .try_to_sk() .unwrap(); let account_keypair = consensus_keypair.clone(); - let address = address::gen_established_address("validator account"); + let address = namada::types::address::gen_established_address( + "validator account", + ); let eth_cold_keypair = common::SecretKey::try_from_sk(&secp_eth_cold_keypair).unwrap(); let (protocol_keypair, eth_bridge_keypair, dkg_keypair) = - wallet::defaults::validator_keys(); - let validator = Validator { - pos_data: GenesisValidator { + defaults::validator_keys(); + let alias = Alias::from_str(&format!("validator-{}", val + 1)) + .expect("infallible"); + // add the validator + if let Some(vals) = genesis.transactions.validator_account.as_mut() { + vals.push(chain::FinalizedValidatorAccountTx { address, - tokens: token::Amount::native_whole(200_000), - consensus_key: consensus_keypair.ref_to(), - protocol_key: protocol_keypair.ref_to(), - commission_rate: Dec::new(5, 2).expect("This can't fail"), - max_commission_rate_change: Dec::new(1, 2) - .expect("This can't fail"), - eth_cold_key: eth_cold_keypair.ref_to(), - eth_hot_key: eth_bridge_keypair.ref_to(), - }, - account_key: account_keypair.ref_to(), - dkg_public_key: dkg_keypair.public(), - non_staked_balance: token::Amount::native_whole(100_000), - // TODO replace with https://github.com/anoma/namada/issues/25) - validator_vp_code_path: vp_user_path.into(), - validator_vp_sha256: Default::default(), + tx: transactions::ValidatorAccountTx { + alias: alias.clone(), + dkg_key: StringEncoded { + raw: dkg_keypair.public(), + }, + vp: "vp_validator".to_string(), + commission_rate: Dec::new(5, 2).expect("This can't fail"), + max_commission_rate_change: Dec::new(1, 2) + .expect("This can't fail"), + net_address: SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + 8080, + ), + account_key: sign_pk(&account_keypair), + consensus_key: sign_pk(&consensus_keypair), + protocol_key: sign_pk(&protocol_keypair), + tendermint_node_key: sign_pk(&consensus_keypair), + eth_hot_key: sign_pk(ð_bridge_keypair), + eth_cold_key: sign_pk(ð_cold_keypair), + }, + }) }; - validators.push(validator); - } - - let parameters = Parameters { - epoch_duration: EpochDuration { - min_num_of_blocks: 10, - min_duration: namada::types::time::Duration::seconds(600).into(), - }, - 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(), - implicit_vp_sha256: Default::default(), - max_signatures_per_transaction: 15, - epochs_per_year: 365, /* seconds in yr (60*60*24*365) div seconds - * per epoch (60 = min_duration) */ - pos_gain_p: Dec::new(1, 1).expect("This can't fail"), - pos_gain_d: Dec::new(1, 1).expect("This can't fail"), - staked_ratio: Dec::zero(), - pos_inflation_amount: token::Amount::zero(), - minimum_gas_price: [(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(), - vp_code_path: vp_user_path.into(), - vp_sha256: Default::default(), - public_key: Some(wallet::defaults::albert_keypair().ref_to()), - storage: HashMap::default(), - }; - let bertha = EstablishedAccount { - address: wallet::defaults::bertha_address(), - vp_code_path: vp_user_path.into(), - vp_sha256: Default::default(), - public_key: Some(wallet::defaults::bertha_keypair().ref_to()), - storage: HashMap::default(), - }; - let christel = EstablishedAccount { - address: wallet::defaults::christel_address(), - vp_code_path: vp_user_path.into(), - vp_sha256: Default::default(), - public_key: Some(wallet::defaults::christel_keypair().ref_to()), - storage: HashMap::default(), - }; - let masp = EstablishedAccount { - address: namada::types::address::masp(), - vp_code_path: "vp_masp.wasm".into(), - vp_sha256: Default::default(), - public_key: None, - storage: HashMap::default(), - }; - let implicit_accounts = vec![ - ImplicitAccount { - public_key: wallet::defaults::daewon_keypair().ref_to(), - }, - ImplicitAccount { - public_key: wallet::defaults::ester_keypair().ref_to(), - }, - ]; - let default_user_tokens = Uint::from(1_000_000); - let default_key_tokens = Uint::from(1_000_000); - let mut balances: HashMap = HashMap::from_iter([ - // established accounts' balances - (wallet::defaults::albert_address(), default_user_tokens), - (wallet::defaults::bertha_address(), default_user_tokens), - (wallet::defaults::christel_address(), default_user_tokens), - // implicit accounts' balances - (wallet::defaults::daewon_address(), default_user_tokens), - // implicit accounts derived from public keys balances - ( - bertha.public_key.as_ref().unwrap().into(), - default_key_tokens, - ), - ( - albert.public_key.as_ref().unwrap().into(), - default_key_tokens, - ), - ( - christel.public_key.as_ref().unwrap().into(), - default_key_tokens, - ), - ]); - for validator in &validators { - balances.insert((&validator.account_key).into(), default_key_tokens); - } - - /// Deprecated function, soon to be deleted. Generates default tokens - fn tokens() -> HashMap { - vec![ - (nam(), ("NAM", 6.into())), - (btc(), ("BTC", 8.into())), - (eth(), ("ETH", 18.into())), - (dot(), ("DOT", 10.into())), - (schnitzel(), ("Schnitzel", 6.into())), - (apfel(), ("Apfel", 6.into())), - (kartoffel(), ("Kartoffel", 6.into())), - ] - .into_iter() - .collect() - } - let token_accounts = tokens() - .into_iter() - .map(|(address, (_, denom))| TokenAccount { - address, - denom, - balances: balances - .clone() - .into_iter() - .map(|(k, v)| (k, token::Amount::from_uint(v, denom).unwrap())) - .collect(), - parameters: token::Parameters::default(), - last_inflation: token::Amount::zero(), - last_locked_ratio: Dec::zero(), - }) - .collect(); - Genesis { - genesis_time: DateTimeUtc::now(), - validators, - established_accounts: vec![albert, bertha, christel, masp], - implicit_accounts, - token_accounts, - parameters, - pos_params: OwnedPosParams::default(), - gov_params: GovernanceParameters::default(), - pgf_params: PgfParameters::default(), - ethereum_bridge_params: Some(EthereumBridgeConfig { - erc20_whitelist: vec![Erc20WhitelistEntry { - token_address: DAI_ERC20_ETH_ADDRESS, - token_cap: token::DenominatedAmount { - amount: token::Amount::max(), - denom: 18.into(), + // add the balance to validators implicit key + if let Some(bals) = genesis + .balances + .token + .get_mut(&Alias::from_str("nam").unwrap()) + { + bals.0.insert( + StringEncoded { + raw: account_keypair.ref_to(), }, - }], - eth_start_height: Default::default(), - min_confirmations: Default::default(), - contracts: Contracts { - native_erc20: wnam(), - bridge: UpgradeableContract { - address: EthAddress([0; 20]), - version: Default::default(), + token::DenominatedAmount { + amount: token::Amount::native_whole(200_000), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), }, - }, - }), - native_token: address::nam(), + ); + } + // transfer funds from implicit key to validator + if let Some(trans) = genesis.transactions.transfer.as_mut() { + trans.push(transactions::TransferTx { + token: Alias::from_str("nam").expect("infallible"), + source: StringEncoded { + raw: account_keypair.ref_to(), + }, + target: alias.clone(), + amount: token::DenominatedAmount { + amount: token::Amount::native_whole(200_000), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }, + }) + } + // self bond + if let Some(bonds) = genesis.transactions.bond.as_mut() { + bonds.push(transactions::BondTx { + source: transactions::AliasOrPk::Alias(alias.clone()), + validator: alias, + amount: token::DenominatedAmount { + amount: token::Amount::native_whole(100_000), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }, + }) + } } + + genesis } #[cfg(test)] diff --git a/apps/src/lib/config/genesis/chain.rs b/apps/src/lib/config/genesis/chain.rs new file mode 100644 index 0000000000..0d064789c8 --- /dev/null +++ b/apps/src/lib/config/genesis/chain.rs @@ -0,0 +1,872 @@ +use std::collections::BTreeMap; +use std::path::Path; +use std::str::FromStr; + +use borsh::{BorshDeserialize, BorshSerialize}; +use namada::ledger::parameters::EpochDuration; +use namada_sdk::wallet::store::AddressVpType; +use namada_sdk::wallet::{pre_genesis, Wallet}; +use namada::types::address::{masp, Address, EstablishedAddressGen}; +use namada::types::chain::{ChainId, ChainIdPrefix}; +use namada::types::dec::Dec; +use namada::types::hash::Hash; +use namada::types::time::{DateTimeUtc, DurationNanos, Rfc3339String}; +use namada::types::token::Amount; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use super::toml_utils::{read_toml, write_toml}; +use super::{templates, transactions}; +use crate::config::genesis::templates::Validated; +use crate::config::utils::{set_ip, set_port}; +use crate::config::{Config, TendermintMode}; +use crate::facade::tendermint::node::Id as TendermintNodeId; +use crate::facade::tendermint_config::net::Address as TendermintAddress; +use crate::node::ledger::tendermint_node::id_from_pk; +use crate::wallet::{Alias, CliWalletUtils}; +use crate::wasm_loader; + +pub const METADATA_FILE_NAME: &str = "chain.toml"; + +// Rng source used for generating genesis addresses. Because the process has to +// be deterministic, change of this value is a breaking change for genesis. +const ADDRESS_RNG_SOURCE: &[u8] = &[]; + +impl Finalized { + /// Write all genesis and the chain metadata TOML files to the given + /// directory. + pub fn write_toml_files(&self, output_dir: &Path) -> eyre::Result<()> { + let vps_file = output_dir.join(templates::VPS_FILE_NAME); + let tokens_file = output_dir.join(templates::TOKENS_FILE_NAME); + let balances_file = output_dir.join(templates::BALANCES_FILE_NAME); + let parameters_file = output_dir.join(templates::PARAMETERS_FILE_NAME); + let transactions_file = + output_dir.join(templates::TRANSACTIONS_FILE_NAME); + let metadata_file = output_dir.join(METADATA_FILE_NAME); + + write_toml(&self.vps, &vps_file, "Validity predicates")?; + write_toml(&self.tokens, &tokens_file, "Tokens")?; + write_toml(&self.balances, &balances_file, "Balances")?; + write_toml(&self.parameters, ¶meters_file, "Parameters")?; + write_toml(&self.transactions, &transactions_file, "Transactions")?; + write_toml(&self.metadata, &metadata_file, "Chain metadata")?; + Ok(()) + } + + /// Try to read all genesis and the chain metadata TOML files from the given + /// directory. + pub fn read_toml_files(input_dir: &Path) -> eyre::Result { + let vps_file = input_dir.join(templates::VPS_FILE_NAME); + let tokens_file = input_dir.join(templates::TOKENS_FILE_NAME); + let balances_file = input_dir.join(templates::BALANCES_FILE_NAME); + let parameters_file = input_dir.join(templates::PARAMETERS_FILE_NAME); + let transactions_file = + input_dir.join(templates::TRANSACTIONS_FILE_NAME); + let metadata_file = input_dir.join(METADATA_FILE_NAME); + + let vps = read_toml(&vps_file, "Validity predicates")?; + let tokens = read_toml(&tokens_file, "Tokens")?; + let balances = read_toml(&balances_file, "Balances")?; + let parameters = read_toml(¶meters_file, "Parameters")?; + let transactions = read_toml(&transactions_file, "Transactions")?; + let metadata = read_toml(&metadata_file, "Chain metadata")?; + Ok(Self { + vps, + tokens, + balances, + parameters, + transactions, + metadata, + }) + } + + /// Find the address of the configured native token + pub fn get_native_token(&self) -> &Address { + let alias = &self.parameters.parameters.native_token; + &self + .tokens + .token + .get(alias) + .expect("The native token must exist") + .address + } + + /// Derive Namada wallet from genesis + pub fn derive_wallet( + &self, + base_dir: &Path, + pre_genesis_wallet: Option>, + validator: Option<(Alias, pre_genesis::ValidatorWallet)>, + ) -> Wallet { + let mut wallet = crate::wallet::load_or_new(base_dir); + dbg!(&wallet); + for (alias, config) in &self.tokens.token { + dbg!("add token", alias); + wallet.add_address( + alias.normalize(), + config.address.clone(), + false, + ); + wallet.add_vp_type_to_address( + AddressVpType::Token, + config.address.clone(), + ); + } + if let Some(txs) = &self.transactions.validator_account { + for tx in txs { + wallet.add_address( + tx.tx.alias.normalize(), + tx.address.clone(), + false, + ); + } + } + if let Some(txs) = &self.transactions.established_account { + for tx in txs { + wallet.add_address( + tx.tx.alias.normalize(), + tx.address.clone(), + false, + ); + } + } + if let Some(pre_genesis_wallet) = pre_genesis_wallet { + wallet.extend(pre_genesis_wallet); + } + if let Some((alias, validator_wallet)) = validator { + let address = self + .transactions + .find_validator(&alias) + .map(|tx| tx.address.clone()) + .expect("Validator alias not found in genesis transactions."); + wallet.extend_from_pre_genesis_validator( + address, + alias, + validator_wallet, + ) + } + wallet + } + + /// Derive Namada configuration from genesis + pub fn derive_config( + &self, + base_dir: &Path, + node_mode: TendermintMode, + validator_alias: Option, + allow_duplicate_ip: bool, + ) -> Config { + if node_mode != TendermintMode::Validator && validator_alias.is_some() { + println!( + "Warning: Validator alias used to derive config, but node \ + mode is not validator, it is {node_mode:?}!" + ); + } + let mut config = + Config::new(base_dir, self.metadata.chain_id.clone(), node_mode); + + // Derive persistent peers from genesis + let persistent_peers = self.derive_persistent_peers(); + // If `validator_wallet` is given, find its net_address + let validator_net_and_tm_address = + if let Some(alias) = validator_alias.as_ref() { + self.transactions.find_validator(alias).map(|validator_tx| { + ( + validator_tx.tx.net_address, + validator_tx.derive_tendermint_address(), + ) + }) + } else { + None + }; + // Check if the validators are localhost to automatically turn off + // Tendermint P2P address book strict mode to allow it + let is_localhost = persistent_peers.iter().all(|peer| match peer { + TendermintAddress::Tcp { + peer_id: _, + host, + port: _, + } => matches!(host.as_str(), "127.0.0.1" | "localhost"), + TendermintAddress::Unix { path: _ } => false, + }); + + // Configure the ledger + config.ledger.genesis_time = self.metadata.genesis_time.clone(); + + // Add a ledger P2P persistent peers + config.ledger.cometbft.p2p.persistent_peers = persistent_peers; + config.ledger.cometbft.consensus.timeout_commit = + self.metadata.consensus_timeout_commit.into(); + config.ledger.cometbft.p2p.allow_duplicate_ip = allow_duplicate_ip; + config.ledger.cometbft.p2p.addr_book_strict = !is_localhost; + + if let Some((net_address, tm_address)) = validator_net_and_tm_address { + // Take out address of self from the P2P persistent peers + config.ledger.cometbft.p2p.persistent_peers = config.ledger.cometbft.p2p.persistent_peers.iter() + .filter_map(|peer| + // we do not add the validator in its own persistent peer list + if peer != &tm_address { + Some(peer.to_owned()) + } else { + None + }) + .collect(); + + let first_port = net_address.port(); + if !is_localhost { + set_ip(&mut config.ledger.cometbft.p2p.laddr, "0.0.0.0"); + } + set_port(&mut config.ledger.cometbft.p2p.laddr, first_port); + if !is_localhost { + set_ip(&mut config.ledger.cometbft.rpc.laddr, "0.0.0.0"); + } + set_port(&mut config.ledger.cometbft.rpc.laddr, first_port + 1); + set_port(&mut config.ledger.cometbft.proxy_app, first_port + 2); + + // Validator node should turned off peer exchange reactor + config.ledger.cometbft.p2p.pex = false; + } + + config + } + + /// Derive persistent peers from genesis validators + fn derive_persistent_peers(&self) -> Vec { + self.transactions + .validator_account + .as_ref() + .map(|txs| { + txs.iter() + .map(FinalizedValidatorAccountTx::derive_tendermint_address) + .collect() + }) + .unwrap_or_default() + } + + /// Get the chain parameters set in genesis + pub fn get_chain_parameters( + &self, + wasm_dir: impl AsRef, + ) -> namada::ledger::parameters::Parameters { + let templates::ChainParams { + min_num_of_blocks, + max_expected_time_per_block, + max_proposal_bytes, + vp_whitelist, + tx_whitelist, + implicit_vp, + epochs_per_year, + pos_gain_p, + pos_gain_d, + max_signatures_per_transaction, + fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit, + max_block_gas, + minimum_gas_price, + .. + } = self.parameters.parameters.clone(); + + let implicit_vp_filename = &self + .vps + .wasm + .get(&implicit_vp) + .expect("Implicit VP must be present") + .filename; + let implicit_vp = + wasm_loader::read_wasm(&wasm_dir, implicit_vp_filename) + .expect("Implicit VP WASM code couldn't get read"); + let implicit_vp_code_hash = Hash::sha256(implicit_vp); + + let min_duration: i64 = 60 * 60 * 24 * 365 / (epochs_per_year as i64); + let epoch_duration = EpochDuration { + min_num_of_blocks, + min_duration: namada::types::time::Duration::seconds(min_duration) + .into(), + }; + let max_expected_time_per_block = + namada::types::time::Duration::seconds(max_expected_time_per_block) + .into(); + let vp_whitelist = vp_whitelist.unwrap_or_default(); + let tx_whitelist = tx_whitelist.unwrap_or_default(); + let staked_ratio = Dec::zero(); + let pos_inflation_amount = 0; + + namada::ledger::parameters::Parameters { + epoch_duration, + max_expected_time_per_block, + vp_whitelist, + tx_whitelist, + implicit_vp_code_hash, + epochs_per_year, + pos_gain_p, + pos_gain_d, + staked_ratio, + pos_inflation_amount: Amount::native_whole(pos_inflation_amount), + max_proposal_bytes, + max_signatures_per_transaction, + fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit, + max_block_gas, + minimum_gas_price: minimum_gas_price + .iter() + .map(|(token, amt)| { + ( + self.tokens.token.get(token).cloned().unwrap().address, + amt.amount, + ) + }) + .collect(), + } + } + + pub fn get_pos_params( + &self, + ) -> namada::proof_of_stake::parameters::PosParams { + let templates::PosParams { + max_validator_slots, + pipeline_len, + unbonding_len, + tm_votes_per_token, + block_proposer_reward, + block_vote_reward, + max_inflation_rate, + target_staked_ratio, + duplicate_vote_min_slash_rate, + light_client_attack_min_slash_rate, + cubic_slashing_window_length, + validator_stake_threshold, + } = self.parameters.pos_params.clone(); + + namada::proof_of_stake::parameters::PosParams { + max_validator_slots, + pipeline_len, + unbonding_len, + tm_votes_per_token, + block_proposer_reward, + block_vote_reward, + max_inflation_rate, + target_staked_ratio, + duplicate_vote_min_slash_rate, + light_client_attack_min_slash_rate, + cubic_slashing_window_length, + validator_stake_threshold, + } + } + + pub fn get_gov_params( + &self, + ) -> namada::core::ledger::governance::parameters::GovernanceParameters + { + let templates::GovernanceParams { + min_proposal_fund, + max_proposal_code_size, + min_proposal_voting_period, + max_proposal_period, + max_proposal_content_size, + min_proposal_grace_epochs, + } = self.parameters.gov_params.clone(); + namada::core::ledger::governance::parameters::GovernanceParameters { + min_proposal_fund: Amount::native_whole(min_proposal_fund), + max_proposal_code_size, + max_proposal_period, + max_proposal_content_size, + min_proposal_grace_epochs, + min_proposal_voting_period, + } + } + + pub fn get_pgf_params( + &self, + ) -> namada::core::ledger::pgf::parameters::PgfParameters { + self.parameters.pgf_params.clone() + } + + pub fn get_eth_bridge_params( + &self, + ) -> Option { + if let Some(templates::EthBridgeParams { + eth_start_height, + min_confirmations, + contracts, + erc20_whitelist, + }) = self.parameters.eth_bridge_params.clone() + { + Some(namada::ledger::eth_bridge::EthereumBridgeParams { + eth_start_height, + min_confirmations, + erc20_whitelist, + contracts, + }) + } else { + None + } + } + + pub fn get_token_address(&self, alias: &Alias) -> Option<&Address> { + self.tokens.token.get(alias).map(|token| &token.address) + } + + pub fn get_user_address(&self, alias: &Alias) -> Option
{ + if alias.to_string() == *"masp" { + return Some(masp()); + } + let established = self.transactions.established_account.as_ref()?; + let validators = self.transactions.validator_account.as_ref()?; + established + .iter() + .find_map(|tx| { + (&tx.tx.alias == alias).then_some(tx.address.clone()) + }) + .or_else(|| { + validators.iter().find_map(|tx| { + (&tx.tx.alias == alias).then_some(tx.address.clone()) + }) + }) + } + + pub fn get_validator_address(&self, alias: &Alias) -> Option<&Address> { + let validators = self.transactions.validator_account.as_ref()?; + validators + .iter() + .find_map(|tx| (&tx.tx.alias == alias).then_some(&tx.address)) + } +} + +/// Create the [`Finalized`] chain configuration. Derives the chain ID from the +/// genesis bytes and assigns addresses to tokens and transactions that +/// initialize established accounts. +/// +/// Invariant: The output must deterministic. For the same input this function +/// must return the same output. +pub fn finalize( + templates: templates::All, + chain_id_prefix: ChainIdPrefix, + genesis_time: DateTimeUtc, + consensus_timeout_commit: crate::facade::tendermint::Timeout, +) -> Finalized { + let genesis_time: Rfc3339String = genesis_time.into(); + let consensus_timeout_commit: DurationNanos = + consensus_timeout_commit.into(); + + // Derive seed for address generator + let genesis_to_gen_address = GenesisToGenAddresses { + templates, + metadata: Metadata { + chain_id: chain_id_prefix.clone(), + genesis_time, + consensus_timeout_commit, + address_gen: None, + }, + }; + let genesis_bytes = genesis_to_gen_address.try_to_vec().unwrap(); + let mut addr_gen = established_address_gen(&genesis_bytes); + + // Generate addresses + let templates::All { + vps, + tokens, + balances, + parameters, + transactions, + } = genesis_to_gen_address.templates; + let tokens = FinalizedTokens::finalize_from(tokens, &mut addr_gen); + let transactions = + FinalizedTransactions::finalize_from(transactions, &mut addr_gen); + let parameters = + FinalizedParameters::finalize_from(&transactions, parameters); + + // Store the last state of the address generator in the metadata + let mut metadata = genesis_to_gen_address.metadata; + metadata.address_gen = Some(addr_gen); + + // Derive chain ID + let to_finalize = ToFinalize { + metadata, + vps, + tokens, + balances, + parameters, + transactions, + }; + let to_finalize_bytes = to_finalize.try_to_vec().unwrap(); + let chain_id = ChainId::from_genesis(chain_id_prefix, to_finalize_bytes); + + // Construct the `Finalized` chain + let ToFinalize { + vps, + tokens, + balances, + parameters, + transactions, + metadata, + } = to_finalize; + let Metadata { + chain_id: _, + genesis_time, + consensus_timeout_commit, + address_gen, + } = metadata; + let metadata = Metadata { + chain_id, + genesis_time, + consensus_timeout_commit, + address_gen, + }; + Finalized { + metadata, + vps, + tokens, + balances, + parameters, + transactions, + } +} + +/// Use bytes as a deterministic seed for address generator. +fn established_address_gen(bytes: &[u8]) -> EstablishedAddressGen { + let mut hasher = Sha256::new(); + hasher.update(bytes); + // hex of the first 40 chars of the hash + let hash = format!("{:.width$X}", hasher.finalize(), width = 40); + EstablishedAddressGen::new(hash) +} + +/// Deterministically generate an [`Address`]. +fn gen_address(gen: &mut EstablishedAddressGen) -> Address { + gen.generate_address(ADDRESS_RNG_SOURCE) +} + +/// Chain genesis config to be finalized. This struct is used to derive the +/// chain ID to construct a [`Finalized`] chain genesis config. +#[derive( + Clone, Debug, Deserialize, Serialize, BorshDeserialize, BorshSerialize, +)] +pub struct GenesisToGenAddresses { + /// Filled-in templates + pub templates: templates::All, + /// Chain metadata + pub metadata: Metadata, +} + +/// Chain genesis config to be finalized. This struct is used to derive the +/// chain ID to construct a [`Finalized`] chain genesis config. +pub type ToFinalize = Chain; + +/// Chain genesis config. +pub type Finalized = Chain; + +/// Chain genesis config with generic chain ID. +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Chain { + pub vps: templates::ValidityPredicates, + pub tokens: FinalizedTokens, + pub balances: templates::DenominatedBalances, + pub parameters: FinalizedParameters, + pub transactions: FinalizedTransactions, + /// Chain metadata + pub metadata: Metadata, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct FinalizedTokens { + pub token: BTreeMap, +} + +impl FinalizedTokens { + fn finalize_from( + tokens: templates::Tokens, + addr_gen: &mut EstablishedAddressGen, + ) -> FinalizedTokens { + let templates::Tokens { token } = tokens; + let token = token + .into_iter() + .map(|(key, config)| { + let address = gen_address(addr_gen); + (key, FinalizedTokenConfig { address, config }) + }) + .collect(); + FinalizedTokens { token } + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct FinalizedTokenConfig { + pub address: Address, + #[serde(flatten)] + pub config: templates::TokenConfig, +} + +#[derive( + Clone, + Debug, + Default, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct FinalizedTransactions { + pub established_account: Option>, + pub validator_account: Option>, + pub transfer: Option>>, + pub bond: Option>>, +} + +impl FinalizedTransactions { + fn finalize_from( + transactions: transactions::Transactions, + addr_gen: &mut EstablishedAddressGen, + ) -> FinalizedTransactions { + let transactions::Transactions { + established_account, + validator_account, + transfer, + bond, + } = transactions; + let established_account = established_account.map(|txs| { + txs.into_iter() + .map(|tx| { + let address = gen_address(addr_gen); + FinalizedEstablishedAccountTx { address, tx } + }) + .collect() + }); + let validator_account = validator_account.map(|txs| { + txs.into_iter() + .map(|tx| { + let address = gen_address(addr_gen); + FinalizedValidatorAccountTx { address, tx } + }) + .collect() + }); + FinalizedTransactions { + established_account, + validator_account, + transfer, + bond, + } + } + + fn find_validator( + &self, + alias: &Alias, + ) -> Option<&FinalizedValidatorAccountTx> { + self.validator_account + .as_ref() + .and_then(|txs| txs.iter().find(|tx| &tx.tx.alias == alias)) + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct FinalizedParameters { + pub parameters: templates::ChainParams, + pub pos_params: templates::PosParams, + pub gov_params: templates::GovernanceParams, + pub pgf_params: namada::core::ledger::pgf::parameters::PgfParameters, + pub eth_bridge_params: Option, +} + +impl FinalizedParameters { + fn finalize_from( + txs: &FinalizedTransactions, + templates::Parameters { + parameters, + pos_params, + gov_params, + pgf_params, + eth_bridge_params, + }: templates::Parameters, + ) -> Self { + use namada::core::ledger::pgf::parameters::PgfParameters; + let mut finalized_pgf_params = PgfParameters { + stewards: Default::default(), + pgf_inflation_rate: pgf_params.pgf_inflation_rate, + stewards_inflation_rate: pgf_params.stewards_inflation_rate, + }; + finalized_pgf_params.stewards = pgf_params + .stewards + .into_iter() + .map(|alias| { + let maybe_estbd = txs + .established_account + .as_ref() + .unwrap() + .iter() + .find(|tx| tx.tx.alias == alias) + .map(|tx| tx.address.clone()); + let maybe_validator = txs + .validator_account + .as_ref() + .unwrap() + .iter() + .find(|tx| tx.tx.alias == alias) + .map(|tx| tx.address.clone()); + maybe_estbd.or(maybe_validator).unwrap() + }) + .collect(); + Self { + parameters, + pos_params, + gov_params, + pgf_params: finalized_pgf_params, + eth_bridge_params, + } + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct FinalizedEstablishedAccountTx { + pub address: Address, + #[serde(flatten)] + pub tx: transactions::SignedEstablishedAccountTx, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct FinalizedValidatorAccountTx { + pub address: Address, + #[serde(flatten)] + pub tx: transactions::SignedValidatorAccountTx, +} + +impl FinalizedValidatorAccountTx { + pub fn derive_tendermint_address(&self) -> TendermintAddress { + // Derive the node ID from the node key + let node_id: TendermintNodeId = + id_from_pk(&self.tx.tendermint_node_key.pk.raw); + + // Build the list of persistent peers from the validators' node IDs + TendermintAddress::from_str(&format!( + "{}@{}", + node_id, self.tx.net_address, + )) + .expect("Validator address must be valid") + } +} + +/// Chain metadata +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Metadata { + /// Chain ID in [`Finalized`] or chain ID prefix in + /// [`GenesisToGenAddresses`] and [`ToFinalize`]. + pub chain_id: ID, + // Genesis timestamp + pub genesis_time: Rfc3339String, + /// The Tendermint consensus timeout_commit configuration + pub consensus_timeout_commit: DurationNanos, + /// This generator should be used to initialize the ledger for the + /// next address that will be generated on chain. + /// + /// The value is expected to always be `None` in [`GenesisToGenAddresses`] + /// and `Some` in [`ToFinalize`] and [`Finalized`]. + pub address_gen: Option, +} + +#[cfg(test)] +mod test { + use std::path::PathBuf; + use std::str::FromStr; + + use super::*; + + /// Test that the [`finalize`] returns deterministic output with the same + /// chain ID for the same input. + #[test] + fn test_finalize_is_deterministic() { + // Load the localnet templates + let templates_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("genesis/localnet"); + let templates = templates::load_and_validate(&templates_dir).unwrap(); + + let chain_id_prefix: ChainIdPrefix = + FromStr::from_str("test-prefix").unwrap(); + + let genesis_time = + DateTimeUtc::from_str("2021-12-31T00:00:00Z").unwrap(); + + let consensus_timeout_commit = + crate::facade::tendermint::Timeout::from_str("1s").unwrap(); + + let finalized_0 = finalize( + templates.clone(), + chain_id_prefix.clone(), + genesis_time, + consensus_timeout_commit, + ); + + let finalized_1 = finalize( + templates, + chain_id_prefix, + genesis_time, + consensus_timeout_commit, + ); + + pretty_assertions::assert_eq!(finalized_0, finalized_1); + } +} diff --git a/apps/src/lib/config/genesis/templates.rs b/apps/src/lib/config/genesis/templates.rs new file mode 100644 index 0000000000..df01fc2fdc --- /dev/null +++ b/apps/src/lib/config/genesis/templates.rs @@ -0,0 +1,991 @@ +//! The templates for balances, parameters and VPs used for a chain's genesis. + +use std::collections::{BTreeMap, BTreeSet}; +use std::marker::PhantomData; +use std::path::Path; + +use borsh::{BorshDeserialize, BorshSerialize}; +use namada::core::types::key::common; +use namada::core::types::string_encoding::StringEncoded; +use namada::core::types::{ethereum_structs, token}; +use namada::eth_bridge::parameters::{ + Contracts, Erc20WhitelistEntry, MinimumConfirmations, +}; +use namada::types::chain::ProposalBytes; +use namada::types::dec::Dec; +use namada::types::token::{ + Amount, DenominatedAmount, Denomination, NATIVE_MAX_DECIMAL_PLACES, +}; +use serde::{Deserialize, Serialize}; + +use super::toml_utils::{read_toml, write_toml}; +use super::transactions::{self, Transactions}; +use crate::config::genesis::transactions::{ + BondTx, SignedBondTx, SignedTransferTx, TransferTx, +}; +use crate::wallet::Alias; + +pub const BALANCES_FILE_NAME: &str = "balances.toml"; +pub const PARAMETERS_FILE_NAME: &str = "parameters.toml"; +pub const VPS_FILE_NAME: &str = "validity-predicates.toml"; +pub const TOKENS_FILE_NAME: &str = "tokens.toml"; +pub const TRANSACTIONS_FILE_NAME: &str = "transactions.toml"; + +const MAX_TOKEN_BALANCE_SUM: u64 = i64::MAX as u64; + +/// Note that these balances must be crossed-checked with the token configs +/// to correctly represent the underlying amounts. +pub fn read_balances(path: &Path) -> eyre::Result { + read_toml(path, "Balances") +} + +pub fn read_parameters(path: &Path) -> eyre::Result> { + read_toml(path, "Parameters") +} + +pub fn read_validity_predicates( + path: &Path, +) -> eyre::Result { + read_toml(path, "Validity predicates") +} + +pub fn read_tokens(path: &Path) -> eyre::Result { + read_toml(path, "Tokens") +} + +pub fn read_transactions( + path: &Path, +) -> eyre::Result> { + read_toml(path, "Transactions") +} + +/// Genesis balances of all tokens +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct UndenominatedBalances { + pub token: BTreeMap, +} + +impl UndenominatedBalances { + /// Use the denom in `TokenConfig` to correctly interpret the balances + /// to the right denomination. + pub fn denominate( + self, + tokens: &Tokens, + ) -> eyre::Result { + let mut balances = DenominatedBalances { + token: BTreeMap::new(), + }; + for (alias, bals) in self.token { + let denom = tokens + .token + .get(&alias) + .ok_or_else(|| { + eyre::eyre!( + "A balance of token {} was found, but this token was \ + not found in the `tokens.toml` file", + alias + ) + })? + .denom; + let mut denominated_bals = BTreeMap::new(); + for (pk, bal) in bals.0.into_iter() { + let denominated = bal.increase_precision(denom)?; + denominated_bals.insert(pk, denominated); + } + balances + .token + .insert(alias, TokenBalances(denominated_bals)); + } + Ok(balances) + } +} + +/// Genesis balances of all tokens +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct DenominatedBalances { + pub token: BTreeMap, +} + +/// Genesis balances for a given token +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct RawTokenBalances( + pub BTreeMap, token::DenominatedAmount>, +); + +/// Genesis balances for a given token +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct TokenBalances( + pub BTreeMap, token::DenominatedAmount>, +); + +/// Genesis validity predicates +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct ValidityPredicates { + // Wasm definitions + pub wasm: BTreeMap, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct WasmVpConfig { + pub filename: String, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Tokens { + pub token: BTreeMap, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct TokenConfig { + pub denom: Denomination, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Parameters { + pub parameters: ChainParams, + pub pos_params: PosParams, + pub gov_params: GovernanceParams, + pub pgf_params: PgfParams, + pub eth_bridge_params: Option, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct ChainParams { + /// Name of the native token - this must one of the tokens from + /// `tokens.toml` file + pub native_token: Alias, + /// Minimum number of blocks per epoch. + // TODO: u64 only works with values up to i64::MAX with toml-rs! + pub min_num_of_blocks: u64, + /// Maximum duration per block (in seconds). + // TODO: this is i64 because datetime wants it + pub max_expected_time_per_block: i64, + /// Max payload size, in bytes, for a tx batch proposal. + /// + /// Block proposers may never return a `PrepareProposal` + /// response containing `txs` with a byte length greater + /// than whatever is configured through this parameter. + /// + /// Note that this parameter's value will always be strictly + /// smaller than a Tendermint block's `MaxBytes` consensus + /// parameter. Currently, we hard cap `max_proposal_bytes` + /// at 90 MiB in Namada, which leaves at least 10 MiB of + /// room for header data, evidence and protobuf + /// serialization overhead in Tendermint blocks. + pub max_proposal_bytes: ProposalBytes, + /// Hashes of whitelisted vps array. `None` value or an empty array + /// disables whitelisting. + pub vp_whitelist: Option>, + /// Hashes of whitelisted txs array. `None` value or an empty array + /// disables whitelisting. + pub tx_whitelist: Option>, + /// Filename of implicit accounts validity predicate WASM code + pub implicit_vp: String, + /// Expected number of epochs per year + pub epochs_per_year: u64, + /// PoS gain p + pub pos_gain_p: Dec, + /// PoS gain d + pub pos_gain_d: Dec, + /// Maximum number of signature per transaction + pub max_signatures_per_transaction: u8, + /// Max gas for block + pub max_block_gas: u64, + /// 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 minimum_gas_price: T::GasMinimums, +} + +impl ChainParams { + pub fn denominate( + self, + tokens: &Tokens, + ) -> eyre::Result> { + let ChainParams { + native_token, + min_num_of_blocks, + max_expected_time_per_block, + max_proposal_bytes, + vp_whitelist, + tx_whitelist, + implicit_vp, + epochs_per_year, + pos_gain_p, + pos_gain_d, + max_signatures_per_transaction, + max_block_gas, + fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit, + minimum_gas_price, + } = self; + let mut min_gas_prices = BTreeMap::default(); + for (token, amount) in minimum_gas_price.into_iter() { + let denom = if let Some(TokenConfig { denom, .. }) = + tokens.token.get(&token) + { + *denom + } else { + eprintln!( + "Genesis files contained minimum gas amount of token {}, \ + which is not in the `tokens.toml` file", + token + ); + return Err(eyre::eyre!( + "Genesis files contained minimum gas amount of token {}, \ + which is not in the `tokens.toml` file", + token + )); + }; + let amount = amount.increase_precision(denom).map_err(|e| { + eprintln!( + "A minimum gas amount in the parameters.toml file was \ + incorrectly formatted:\n{}", + e + ); + e + })?; + min_gas_prices.insert(token, amount); + } + + Ok(ChainParams { + native_token, + min_num_of_blocks, + max_expected_time_per_block, + max_proposal_bytes, + vp_whitelist, + tx_whitelist, + implicit_vp, + epochs_per_year, + pos_gain_p, + pos_gain_d, + max_signatures_per_transaction, + max_block_gas, + fee_unshielding_gas_limit, + fee_unshielding_descriptions_limit, + minimum_gas_price: min_gas_prices, + }) + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct PosParams { + /// Maximum number of active validators. + pub max_validator_slots: u64, + /// Pipeline length (in epochs). + pub pipeline_len: u64, + /// Unbonding length (in epochs). + pub unbonding_len: u64, + /// Votes per token. + pub tm_votes_per_token: Dec, + /// Reward for proposing a block. + pub block_proposer_reward: Dec, + /// Reward for voting on a block. + pub block_vote_reward: Dec, + /// Maximum staking APY + pub max_inflation_rate: Dec, + /// Target ratio of staked NAM tokens to total NAM tokens + pub target_staked_ratio: Dec, + /// Portion of a validator's stake that should be slashed on a + /// duplicate vote. + pub duplicate_vote_min_slash_rate: Dec, + /// Portion of a validator's stake that should be slashed on a + /// light client attack. + pub light_client_attack_min_slash_rate: Dec, + /// Number of epochs above and below (separately) the current epoch to + /// consider when doing cubic slashing + pub cubic_slashing_window_length: u64, + /// The minimum amount of bonded tokens that a validator needs to be in + /// either the `consensus` or `below_capacity` validator sets + pub validator_stake_threshold: token::Amount, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct GovernanceParams { + /// Min funds to stake to submit a proposal + pub min_proposal_fund: u64, + /// Maximum size of proposal in kibibytes (KiB) + pub max_proposal_code_size: u64, + /// Minimum proposal period length in epochs + pub min_proposal_voting_period: u64, + /// Maximum proposal period length in epochs + pub max_proposal_period: u64, + /// Maximum number of characters in the proposal content + pub max_proposal_content_size: u64, + /// Minimum number of epoch between end and grace epoch + pub min_proposal_grace_epochs: u64, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct PgfParams { + /// The set of stewards + pub stewards: BTreeSet, + /// The pgf funding inflation rate + pub pgf_inflation_rate: Dec, + /// The pgf stewards inflation rate + pub stewards_inflation_rate: Dec, + #[serde(default)] + #[serde(skip_serializing)] + #[cfg(test)] + pub valid: PhantomData, + #[serde(default)] + #[serde(skip_serializing)] + #[cfg(not(test))] + valid: PhantomData, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct EthBridgeParams { + /// Initial Ethereum block height when events will first be extracted from. + pub eth_start_height: ethereum_structs::BlockHeight, + /// Minimum number of confirmations needed to trust an Ethereum branch. + /// This must be at least one. + pub min_confirmations: MinimumConfirmations, + /// The addresses of the Ethereum contracts that need to be directly known + /// by validators. + pub contracts: Contracts, + /// List of ERC20 token types whitelisted at genesis time. + pub erc20_whitelist: Vec, +} + +impl TokenBalances { + pub fn get(&self, pk: common::PublicKey) -> Option { + let pk = StringEncoded { raw: pk }; + self.0.get(&pk).map(|amt| amt.amount) + } +} +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Unvalidated {} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Validated {} + +pub trait TemplateValidation: Serialize { + type Amount: for<'a> Deserialize<'a> + + Serialize + + Clone + + std::fmt::Debug + + BorshSerialize + + BorshDeserialize + + PartialEq + + Eq; + type Balances: for<'a> Deserialize<'a> + + Serialize + + Clone + + std::fmt::Debug + + BorshSerialize + + BorshDeserialize + + PartialEq + + Eq; + type TransferTx: for<'a> Deserialize<'a> + + Serialize + + Clone + + std::fmt::Debug + + BorshSerialize + + BorshDeserialize + + PartialEq + + Eq; + type BondTx: for<'a> Deserialize<'a> + + Serialize + + Clone + + std::fmt::Debug + + BorshSerialize + + BorshDeserialize + + PartialEq + + Eq; + type GasMinimums: for<'a> Deserialize<'a> + + Serialize + + Clone + + std::fmt::Debug + + BorshSerialize + + BorshDeserialize + + PartialEq + + Eq; +} + +impl TemplateValidation for Unvalidated { + type Amount = token::DenominatedAmount; + type Balances = UndenominatedBalances; + type BondTx = SignedBondTx; + type GasMinimums = BTreeMap; + type TransferTx = SignedTransferTx; +} + +impl TemplateValidation for Validated { + type Amount = token::DenominatedAmount; + type Balances = DenominatedBalances; + type BondTx = BondTx; + type GasMinimums = BTreeMap; + type TransferTx = TransferTx; +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct All { + pub vps: ValidityPredicates, + pub tokens: Tokens, + pub balances: T::Balances, + pub parameters: Parameters, + pub transactions: Transactions, +} + +impl All { + pub fn write_toml_files(&self, output_dir: &Path) -> eyre::Result<()> { + let All { + vps, + tokens, + balances, + parameters, + transactions, + } = self; + + let vps_file = output_dir.join(VPS_FILE_NAME); + let tokens_file = output_dir.join(TOKENS_FILE_NAME); + let balances_file = output_dir.join(BALANCES_FILE_NAME); + let parameters_file = output_dir.join(PARAMETERS_FILE_NAME); + let transactions_file = output_dir.join(TRANSACTIONS_FILE_NAME); + + write_toml(vps, &vps_file, "Validity predicates")?; + write_toml(tokens, &tokens_file, "Tokens")?; + write_toml(balances, &balances_file, "Balances")?; + write_toml(parameters, ¶meters_file, "Parameters")?; + write_toml(transactions, &transactions_file, "Transactions")?; + Ok(()) + } +} + +impl All { + pub fn read_toml_files(input_dir: &Path) -> eyre::Result { + let vps_file = input_dir.join(VPS_FILE_NAME); + let tokens_file = input_dir.join(TOKENS_FILE_NAME); + let balances_file = input_dir.join(BALANCES_FILE_NAME); + let parameters_file = input_dir.join(PARAMETERS_FILE_NAME); + let transactions_file = input_dir.join(TRANSACTIONS_FILE_NAME); + + let vps = read_toml(&vps_file, "Validity predicates")?; + let tokens = read_toml(&tokens_file, "Tokens")?; + let balances = read_toml(&balances_file, "Balances")?; + let parameters = read_toml(¶meters_file, "Parameters")?; + let transactions = read_toml(&transactions_file, "Transactions")?; + Ok(Self { + vps, + tokens, + balances, + parameters, + transactions, + }) + } +} + +/// Load genesis templates from the given directory and validate them. Returns +/// `None` when there are some validation issues. +/// +/// Note that the validation rules for these templates won't enforce that there +/// is at least one validator with positive voting power. This must be checked +/// when the templates are being used to `init-network`. +pub fn load_and_validate(templates_dir: &Path) -> Option> { + let mut is_valid = true; + // We don't reuse `All::read_toml_files` here to allow to validate config + // without all files present. + let vps_file = templates_dir.join(VPS_FILE_NAME); + let tokens_file = templates_dir.join(TOKENS_FILE_NAME); + let balances_file = templates_dir.join(BALANCES_FILE_NAME); + let parameters_file = templates_dir.join(PARAMETERS_FILE_NAME); + let transactions_file = templates_dir.join(TRANSACTIONS_FILE_NAME); + + // Check that all required files are present + let mut check_file_exists = |file: &Path, name: &str| { + if !file.exists() { + is_valid = false; + eprintln!("{name} file is missing at {}", file.to_string_lossy()); + } + }; + check_file_exists(&vps_file, "Validity predicates"); + check_file_exists(&tokens_file, "Tokens"); + check_file_exists(&balances_file, "Balances"); + check_file_exists(¶meters_file, "Parameters"); + check_file_exists(&transactions_file, "Transactions"); + + // Load and parse the files + let vps = read_validity_predicates(&vps_file); + let tokens = read_tokens(&tokens_file); + let balances = read_balances(&balances_file); + let parameters = read_parameters(¶meters_file); + let transactions = read_transactions(&transactions_file); + + let eprintln_invalid_file = |err: &eyre::Report, name: &str| { + eprintln!("{name} file is NOT valid. Failed to read with: {err}"); + }; + + // Check the parsing results + let vps = vps.map_or_else( + |err| { + eprintln_invalid_file(&err, "Validity predicates"); + None + }, + Some, + ); + let tokens = tokens.map_or_else( + |err| { + eprintln_invalid_file(&err, "Tokens"); + None + }, + Some, + ); + let balances = balances.map_or_else( + |err| { + eprintln_invalid_file(&err, "Balances"); + None + }, + Some, + ); + let parameters = parameters.map_or_else( + |err| { + eprintln_invalid_file(&err, "Parameters"); + None + }, + Some, + ); + let transactions = transactions.map_or_else( + |err| { + eprintln_invalid_file(&err, "Transactions"); + None + }, + Some, + ); + + // Validate each file that could be loaded + if let Some(vps) = vps.as_ref() { + if validate_vps(vps) { + println!("Validity predicates file is valid."); + } else { + is_valid = false; + } + } + + let parameters = parameters.and_then(|params| { + let params = + validate_parameters(params, &tokens, &transactions, vps.as_ref()); + if params.is_some() { + println!("Parameters file is valid."); + } else { + is_valid = false + } + params + }); + + let balances = if let Some(tokens) = tokens.as_ref() { + if tokens.token.is_empty() { + is_valid = false; + eprintln!( + "Tokens file is invalid. There has to be at least one token." + ); + } + println!("Tokens file is valid."); + balances + .and_then(|raw| raw.denominate(tokens).ok()) + .and_then(|balances| { + validate_balances(&balances, Some(tokens)).then(|| { + println!("Balances file is valid."); + balances + }) + }) + } else { + None + }; + if balances.is_none() { + is_valid = false; + } + + let txs = if let Some(tokens) = tokens.as_ref() { + if let Some(txs) = transactions.and_then(|txs| { + transactions::validate( + txs, + vps.as_ref(), + balances.as_ref(), + tokens, + parameters.as_ref(), + ) + }) { + println!("Transactions file is valid."); + Some(txs) + } else { + is_valid = false; + None + } + } else { + is_valid = false; + None + }; + + match vps { + Some(vps) if is_valid => Some(All { + vps, + tokens: tokens.unwrap(), + balances: balances.unwrap(), + parameters: parameters.unwrap(), + transactions: txs.unwrap(), + }), + _ => None, + } +} + +pub fn validate_vps(vps: &ValidityPredicates) -> bool { + let mut is_valid = true; + vps.wasm.iter().for_each(|(name, config)| { + if !config.filename.ends_with(".wasm") { + eprintln!( + "Invalid validity predicate \"{name}\" configuration. Only \ + \".wasm\" filenames are currently supported." + ); + is_valid = false; + } + }); + is_valid +} + +pub fn validate_parameters( + parameters: Parameters, + tokens: &Option, + transactions: &Option>, + vps: Option<&ValidityPredicates>, +) -> Option> { + let tokens = tokens.as_ref()?; + let txs = transactions.as_ref()?; + let mut is_valid = true; + let implicit_vp = ¶meters.parameters.implicit_vp; + if !vps + .map(|vps| vps.wasm.contains_key(implicit_vp)) + .unwrap_or_default() + { + eprintln!( + "Implicit VP \"{implicit_vp}\" not found in the Validity \ + predicates files." + ); + is_valid = false; + } + // check that each PGF steward has an established account + for steward in ¶meters.pgf_params.stewards { + let mut found_steward = false; + if let Some(accs) = &txs.established_account { + if accs.iter().any(|acct| acct.alias == *steward) { + found_steward = true; + } + } + + if let Some(accs) = &txs.validator_account { + if accs.iter().any(|acct| acct.alias == *steward) { + found_steward = true; + } + } + is_valid = found_steward && is_valid; + if !is_valid { + eprintln!( + "Could not find an established or validator account \ + associated with the PGF steward {}", + steward + ); + } + } + let Parameters { + parameters, + pos_params, + gov_params, + pgf_params, + eth_bridge_params, + } = parameters; + match parameters.denominate(tokens) { + Err(e) => { + eprintln!("{}", e); + None + } + Ok(parameters) => is_valid.then(|| Parameters { + parameters, + pos_params, + gov_params, + pgf_params: PgfParams { + stewards: pgf_params.stewards, + pgf_inflation_rate: pgf_params.pgf_inflation_rate, + stewards_inflation_rate: pgf_params.stewards_inflation_rate, + valid: Default::default(), + }, + eth_bridge_params, + }), + } +} + +pub fn validate_balances( + balances: &DenominatedBalances, + tokens: Option<&Tokens>, +) -> bool { + let mut is_valid = true; + use std::str::FromStr; + let native_alias = Alias::from_str("nam").expect("Infalllible"); + balances.token.iter().for_each(|(token, next)| { + // Every token alias used in Balances file must be present in + // the Tokens file + if !tokens + .as_ref() + .map(|tokens| tokens.token.contains_key(token)) + .unwrap_or_default() + { + is_valid = false; + eprintln!( + "Token \"{token}\" from the Balances file is not present in \ + the Tokens file." + ) + } + + // Check the sum of balances + let sum = next.0.values().try_fold( + token::Amount::default(), + |acc, amount| { + let res = acc.checked_add(amount.amount); + if res.as_ref().is_none() { + is_valid = false; + eprintln!( + "Balances for token {token} overflow `token::Amount`" + ); + } + res + }, + ); + if sum.is_none() + || (*token == native_alias + && sum.unwrap() + > Amount::from_uint( + MAX_TOKEN_BALANCE_SUM, + NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap()) + { + eprintln!( + "The sum of balances for token {token} is greater than \ + {MAX_TOKEN_BALANCE_SUM}" + ); + is_valid = false; + } + }); + is_valid +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + + use namada::core::types::key; + use namada::types::key::RefTo; + use tempfile::tempdir; + + use super::*; + + /// Validate the `genesis/localnet` genesis templates. + #[test] + fn test_validate_localnet_genesis_templates() { + let templates_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("genesis/localnet"); + assert!( + load_and_validate(&templates_dir).is_some(), + "Localnet genesis templates must be valid" + ); + } + + /// Validate the `genesis/starter` genesis templates. + #[test] + fn test_validate_starter_genesis_templates() { + let templates_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("genesis/starter"); + assert!( + load_and_validate(&templates_dir).is_some(), + "Starter genesis templates must be valid" + ); + } + + #[test] + fn test_read_balances() { + let test_dir = tempdir().unwrap(); + let path = test_dir.path().join(BALANCES_FILE_NAME); + let sk = key::testing::keypair_1(); + let pk = sk.ref_to(); + let balance = token::Amount::from(101_000_001); + let token_alias = Alias::from("Some_token".to_string()); + let contents = format!( + r#" + [token.{token_alias}] + {pk} = "{}" + "#, + balance.to_string_native() + ); + fs::write(&path, contents).unwrap(); + + let balances = read_balances(&path).unwrap(); + let example_balance = balances.token.get(&token_alias).unwrap(); + assert_eq!( + balance, + example_balance + .0 + .get(&StringEncoded { raw: pk }) + .unwrap() + .amount + ); + } +} diff --git a/apps/src/lib/config/genesis/toml_utils.rs b/apps/src/lib/config/genesis/toml_utils.rs new file mode 100644 index 0000000000..e699341951 --- /dev/null +++ b/apps/src/lib/config/genesis/toml_utils.rs @@ -0,0 +1,38 @@ +use std::path::Path; + +use eyre::Context; +use serde::de::DeserializeOwned; +use serde::Serialize; + +pub fn read_toml( + path: &Path, + which_file: &str, +) -> eyre::Result { + let file_contents = std::fs::read_to_string(path).wrap_err_with(|| { + format!( + "Couldn't read {which_file} config file from {}", + path.to_string_lossy() + ) + })?; + toml::from_str(&file_contents).wrap_err_with(|| { + format!( + "Couldn't parse {which_file} TOML from {}", + path.to_string_lossy() + ) + }) +} + +pub fn write_toml( + data: &T, + path: &Path, + which_file: &str, +) -> eyre::Result<()> { + let file_contents = toml::to_vec(data) + .wrap_err_with(|| format!("Couldn't format {which_file} to TOML."))?; + std::fs::write(path, file_contents).wrap_err_with(|| { + format!( + "Couldn't write {which_file} TOML to {}", + path.to_string_lossy() + ) + }) +} diff --git a/apps/src/lib/config/genesis/transactions.rs b/apps/src/lib/config/genesis/transactions.rs new file mode 100644 index 0000000000..021a895c40 --- /dev/null +++ b/apps/src/lib/config/genesis/transactions.rs @@ -0,0 +1,1483 @@ +//! Genesis transactions + +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::fmt::{Debug, Display, Formatter}; +use std::net::SocketAddr; +use std::str::FromStr; + +use borsh::{BorshDeserialize, BorshSerialize}; +use namada::core::types::storage; +use namada::core::types::string_encoding::StringEncoded; +use namada::proto::{ + standalone_signature, verify_standalone_sig, SerializeWithBorsh, +}; +use namada::types::dec::Dec; +use namada::types::key::dkg_session_keys::DkgPublicKey; +use namada::types::key::{common, RefTo, VerifySigError}; +use namada::types::time::{DateTimeUtc, MIN_UTC}; +use namada::types::token; +use namada::types::token::{DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES}; +use namada_sdk::wallet::pre_genesis::ValidatorWallet; +use namada_sdk::wallet::{FindKeyError, Wallet, WalletUtils}; +use serde::{Deserialize, Serialize}; + +use super::templates::{ + DenominatedBalances, Parameters, TokenBalances, ValidityPredicates, +}; +use crate::config::genesis::templates::{ + TemplateValidation, Tokens, Unvalidated, Validated, +}; +use crate::config::genesis::HexString; +use crate::wallet::Alias; + +pub const PRE_GENESIS_TX_TIMESTAMP: DateTimeUtc = MIN_UTC; + +pub struct GenesisValidatorData { + pub source_key: common::SecretKey, + pub alias: Alias, + pub commission_rate: Dec, + pub max_commission_rate_change: Dec, + pub net_address: SocketAddr, + pub transfer_from_source_amount: token::DenominatedAmount, + pub self_bond_amount: token::DenominatedAmount, +} + +/// Panics if given `txs.validator_accounts` is not empty, because validator +/// transactions must be signed with a validator wallet (see +/// `init-genesis-validator` command). +pub fn sign_txs( + txs: UnsignedTransactions, + wallet: &mut Wallet, +) -> Transactions { + let UnsignedTransactions { + established_account, + validator_account, + transfer, + bond, + } = txs; + + // Validate input first + if validator_account.is_some() && !validator_account.unwrap().is_empty() { + panic!( + "Validator transactions must be signed with a validator wallet." + ); + } + + if let Some(bonds) = bond.as_ref() { + for bond in bonds { + if let AliasOrPk::Alias(source) = &bond.source { + if source == &bond.validator { + panic!( + "Validator self-bonds must be signed with a validator \ + wallet." + ) + } + } + } + } + + // Sign all the transactions + let established_account = established_account.map(|tx| { + tx.into_iter() + .map(|tx| sign_established_account_tx(tx, wallet)) + .collect() + }); + let validator_account = None; + let transfer = transfer.map(|tx| { + tx.into_iter() + .map(|tx| sign_transfer_tx(tx, wallet)) + .collect() + }); + let bond = bond.map(|tx| { + tx.into_iter() + .map(|tx| sign_delegation_bond_tx(tx, wallet, &established_account)) + .collect() + }); + + Transactions { + established_account, + validator_account, + transfer, + bond, + } +} + +/// Parse [`UnsignedTransactions`] from bytes. +pub fn parse_unsigned( + bytes: &[u8], +) -> Result { + toml::from_slice(bytes) +} + +/// Create signed [`Transactions`] for a genesis validator. +pub fn init_validator( + GenesisValidatorData { + source_key, + alias, + commission_rate, + max_commission_rate_change, + net_address, + transfer_from_source_amount, + self_bond_amount, + }: GenesisValidatorData, + source_wallet: &mut Wallet, + validator_wallet: &ValidatorWallet, +) -> Transactions { + let unsigned_validator_account_tx = UnsignedValidatorAccountTx { + alias: alias.clone(), + account_key: StringEncoded::new(validator_wallet.account_key.ref_to()), + consensus_key: StringEncoded::new( + validator_wallet.consensus_key.ref_to(), + ), + protocol_key: StringEncoded::new( + validator_wallet + .store + .validator_keys + .protocol_keypair + .ref_to(), + ), + dkg_key: StringEncoded::new( + validator_wallet + .store + .validator_keys + .dkg_keypair + .as_ref() + .expect("Missing validator DKG key") + .public(), + ), + tendermint_node_key: StringEncoded::new( + validator_wallet.tendermint_node_key.ref_to(), + ), + + eth_hot_key: StringEncoded::new(validator_wallet.eth_hot_key.ref_to()), + eth_cold_key: StringEncoded::new( + validator_wallet.eth_cold_key.ref_to(), + ), + // No custom validator VPs yet + vp: "vp_validator".to_string(), + commission_rate, + max_commission_rate_change, + net_address, + }; + let validator_account = Some(vec![sign_validator_account_tx( + unsigned_validator_account_tx, + validator_wallet, + )]); + + let transfer = if transfer_from_source_amount.amount.is_zero() { + None + } else { + let unsigned_transfer_tx = TransferTx { + // Only native token can be staked + token: Alias::from("NAM"), + source: StringEncoded::new(source_key.ref_to()), + target: alias.clone(), + amount: transfer_from_source_amount, + }; + let transfer_tx = sign_transfer_tx(unsigned_transfer_tx, source_wallet); + Some(vec![transfer_tx]) + }; + + let bond = if self_bond_amount.amount.is_zero() { + None + } else { + let unsigned_bond_tx = BondTx { + source: AliasOrPk::Alias(alias.clone()), + validator: alias, + amount: self_bond_amount, + }; + let bond_tx = sign_self_bond_tx(unsigned_bond_tx, validator_wallet); + Some(vec![bond_tx]) + }; + + Transactions { + validator_account, + transfer, + bond, + ..Default::default() + } +} + +pub fn sign_established_account_tx( + unsigned_tx: UnsignedEstablishedAccountTx, + wallet: &mut Wallet, +) -> SignedEstablishedAccountTx { + let key = unsigned_tx.public_key.as_ref().map(|pk| { + let secret = wallet + .find_key_by_pk(pk, None) + .expect("Key for source must be present to sign with it."); + let sig = sign_tx(&unsigned_tx, &secret); + SignedPk { + pk: pk.clone(), + authorization: sig, + } + }); + let UnsignedEstablishedAccountTx { + alias, + vp, + public_key: _, + storage, + } = unsigned_tx; + + SignedEstablishedAccountTx { + alias, + vp, + public_key: key, + storage, + } +} + +pub fn sign_validator_account_tx( + unsigned_tx: UnsignedValidatorAccountTx, + validator_wallet: &ValidatorWallet, +) -> SignedValidatorAccountTx { + // Sign the tx with every validator key to authorize their usage + let account_key_sig = sign_tx(&unsigned_tx, &validator_wallet.account_key); + let consensus_key_sig = + sign_tx(&unsigned_tx, &validator_wallet.consensus_key); + let protocol_key_sig = sign_tx( + &unsigned_tx, + &validator_wallet.store.validator_keys.protocol_keypair, + ); + let eth_hot_key_sig = sign_tx(&unsigned_tx, &validator_wallet.eth_hot_key); + let eth_cold_key_sig = + sign_tx(&unsigned_tx, &validator_wallet.eth_cold_key); + let tendermint_node_key_sig = + sign_tx(&unsigned_tx, &validator_wallet.tendermint_node_key); + + let ValidatorAccountTx { + alias, + account_key, + consensus_key, + protocol_key, + dkg_key, + tendermint_node_key, + vp, + commission_rate, + max_commission_rate_change, + net_address, + eth_hot_key, + eth_cold_key, + } = unsigned_tx; + + let account_key = SignedPk { + pk: account_key, + authorization: account_key_sig, + }; + let consensus_key = SignedPk { + pk: consensus_key, + authorization: consensus_key_sig, + }; + let protocol_key = SignedPk { + pk: protocol_key, + authorization: protocol_key_sig, + }; + let tendermint_node_key = SignedPk { + pk: tendermint_node_key, + authorization: tendermint_node_key_sig, + }; + + let eth_hot_key = SignedPk { + pk: eth_hot_key, + authorization: eth_hot_key_sig, + }; + + let eth_cold_key = SignedPk { + pk: eth_cold_key, + authorization: eth_cold_key_sig, + }; + + SignedValidatorAccountTx { + alias, + account_key, + consensus_key, + protocol_key, + dkg_key, + tendermint_node_key, + vp, + commission_rate, + max_commission_rate_change, + net_address, + eth_hot_key, + eth_cold_key, + } +} + +pub fn sign_transfer_tx( + unsigned_tx: TransferTx, + source_wallet: &mut Wallet, +) -> SignedTransferTx { + let source_key = source_wallet + .find_key_by_pk(&unsigned_tx.source, None) + .expect("Key for source must be present to sign with it."); + unsigned_tx.sign(&source_key) +} + +pub fn sign_self_bond_tx( + unsigned_tx: BondTx, + validator_wallet: &ValidatorWallet, +) -> SignedBondTx { + unsigned_tx.sign(&validator_wallet.account_key) +} + +pub fn sign_delegation_bond_tx( + unsigned_tx: BondTx, + wallet: &mut Wallet, + established_accounts: &Option>>, +) -> SignedBondTx { + let alias = &unsigned_tx.source; + // Try to look-up the source from wallet first - if it's an alias of an + // implicit account that should give us the right key + let found_key = match alias { + AliasOrPk::Alias(alias) => wallet.find_key(&alias.normalize(), None), + AliasOrPk::PublicKey(pk) => wallet.find_key_by_pk(pk, None), + }; + let source_key = match found_key { + Ok(key) => key, + Err(FindKeyError::KeyNotFound) => { + // If it's not in the wallet, it must be an established account + // so we need to look-up its public key first + let pk = established_accounts + .as_ref() + .unwrap_or_else(|| { + panic!( + "Signing a bond failed. Cannot find \"{alias}\" in \ + the wallet and there are no established accounts." + ); + }) + .iter() + .find_map(|account| match alias { + AliasOrPk::Alias(alias) => { + // delegation from established account + if &account.alias == alias { + Some( + &account + .public_key + .as_ref() + .unwrap_or_else(|| { + panic!( + "Signing a bond failed. The \ + established account \"{alias}\" \ + has no public key. Add a public \ + to be able to sign bonds." + ); + }) + .pk + .raw, + ) + } else { + None + } + } + AliasOrPk::PublicKey(pk) => { + // delegation from an implicit account + Some(&pk.raw) + } + }) + .unwrap_or_else(|| { + panic!( + "Signing a bond failed. Cannot find \"{alias}\" in \ + the wallet or in the established accounts." + ); + }); + wallet.find_key_by_pk(pk, None).unwrap_or_else(|err| { + panic!( + "Signing a bond failed. Cannot find key for established \ + account \"{alias}\" in the wallet. Failed with {err}." + ); + }) + } + Err(err) => panic!( + "Signing a bond failed. Failed to read the key for \"{alias}\" \ + from wallet with {err}." + ), + }; + unsigned_tx.sign(&source_key) +} + +fn sign_tx( + tx_data: &T, + keypair: &common::SecretKey, +) -> StringEncoded { + StringEncoded::new(namada::proto::standalone_signature::< + T, + SerializeWithBorsh, + >(keypair, tx_data)) +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, +)] +pub struct Transactions { + pub established_account: Option>, + pub validator_account: Option>, + pub transfer: Option>, + pub bond: Option>, +} + +impl Transactions { + /// Take the union of two sets of transactions + pub fn merge(&mut self, mut other: Self) { + self.established_account = self + .established_account + .take() + .map(|mut txs| { + if let Some(new_txs) = other.established_account.as_mut() { + txs.append(new_txs); + } + txs + }) + .or(other.established_account); + self.validator_account = self + .validator_account + .take() + .map(|mut txs| { + if let Some(new_txs) = other.validator_account.as_mut() { + txs.append(new_txs); + } + txs + }) + .or(other.validator_account); + self.transfer = self + .transfer + .take() + .map(|mut txs| { + if let Some(new_txs) = other.transfer.as_mut() { + txs.append(new_txs); + } + txs + }) + .or(other.transfer); + self.bond = self + .bond + .take() + .map(|mut txs| { + if let Some(new_txs) = other.bond.as_mut() { + txs.append(new_txs); + } + txs + }) + .or(other.bond); + } +} + +impl Default for Transactions { + fn default() -> Self { + Self { + established_account: None, + validator_account: None, + transfer: None, + bond: None, + } + } +} + +impl Transactions { + /// Check that there is at least one validator. + pub fn has_at_least_one_validator(&self) -> bool { + self.validator_account + .as_ref() + .map(|txs| !txs.is_empty()) + .unwrap_or_default() + } + + /// Check if there is at least one validator with positive Tendermint voting + /// power. The voting power is converted from `token::Amount` of the + /// validator's stake using the `tm_votes_per_token` PoS parameter. + pub fn has_validator_with_positive_voting_power( + &self, + votes_per_token: Dec, + ) -> bool { + self.bond + .as_ref() + .map(|txs| { + let mut stakes: BTreeMap<&Alias, token::Amount> = + BTreeMap::new(); + for tx in txs { + let entry = stakes.entry(&tx.validator).or_default(); + *entry += tx.amount.amount; + } + + stakes.into_iter().any(|(_validator, stake)| { + let tendermint_voting_power = + namada::ledger::pos::into_tm_voting_power( + votes_per_token, + stake, + ); + if tendermint_voting_power > 0 { + return true; + } + false + }) + }) + .unwrap_or_default() + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct UnsignedTransactions { + pub established_account: Option>, + pub validator_account: Option>, + pub transfer: Option>>, + pub bond: Option>>, +} + +pub type UnsignedValidatorAccountTx = + ValidatorAccountTx>; + +pub type SignedValidatorAccountTx = ValidatorAccountTx; + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct ValidatorAccountTx { + pub alias: Alias, + pub dkg_key: StringEncoded, + pub vp: String, + /// Commission rate charged on rewards for delegators (bounded inside + /// 0-1) + pub commission_rate: Dec, + /// Maximum change in commission rate permitted per epoch + pub max_commission_rate_change: Dec, + /// P2P IP:port + pub net_address: SocketAddr, + /// PKs have to come last in TOML to avoid `ValueAfterTable` error + pub account_key: PK, + pub consensus_key: PK, + pub protocol_key: PK, + pub tendermint_node_key: PK, + pub eth_hot_key: PK, + pub eth_cold_key: PK, +} + +pub type UnsignedEstablishedAccountTx = + EstablishedAccountTx>; + +pub type SignedEstablishedAccountTx = EstablishedAccountTx; + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct EstablishedAccountTx { + pub alias: Alias, + pub vp: String, + /// PKs have to come last in TOML to avoid `ValueAfterTable` error + pub public_key: Option, + #[serde(default)] + /// Initial storage key values + pub storage: HashMap, +} + +pub type SignedTransferTx = Signed>; + +impl SignedTransferTx { + /// Verify the signature of `TransferTx`. This should not depend + /// on whether the contained amount is denominated or not. + /// + /// Since we denominate amounts as part of validation, we can + /// only verify signatures on [`SignedTransferTx`] + /// types. + pub fn verify_sig(&self) -> Result<(), VerifySigError> { + let Self { data, signature } = self; + verify_standalone_sig::<_, SerializeWithBorsh>( + &data.data_to_sign(), + &data.source.raw, + signature, + ) + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct TransferTx { + pub token: Alias, + pub source: StringEncoded, + pub target: Alias, + pub amount: T::Amount, +} + +impl TransferTx { + /// Add the correct denomination to the contained amount + pub fn denominate( + self, + tokens: &Tokens, + ) -> eyre::Result> { + let TransferTx { + token, + source, + target, + amount, + } = self; + let denom = + if let Some(super::templates::TokenConfig { denom, .. }) = + tokens.token.get(&token) + { + *denom + } else { + eprintln!( + "Genesis files contained transfer of token {}, which is \ + not in the `tokens.toml` file", + token + ); + return Err(eyre::eyre!( + "Genesis files contained transfer of token {}, which is \ + not in the `tokens.toml` file", + token + )); + }; + let amount = amount.increase_precision(denom).map_err(|e| { + eprintln!( + "A bond amount in the transactions.toml file was incorrectly \ + formatted:\n{}", + e + ); + e + })?; + + Ok(TransferTx { + token, + source, + target, + amount, + }) + } + + /// The signable data. This does not include the phantom data. + fn data_to_sign(&self) -> Vec { + [ + self.token.try_to_vec().unwrap(), + self.source.try_to_vec().unwrap(), + self.target.try_to_vec().unwrap(), + self.amount.try_to_vec().unwrap(), + ] + .concat() + } + + /// Sign the transfer. + /// + /// Since we denominate amounts as part of validation, we can + /// only verify signatures on [`SignedTransferTx`] + /// types. Thus we only allow signing of [`TransferTx`] + /// types. + pub fn sign(self, key: &common::SecretKey) -> SignedTransferTx { + let sig = standalone_signature::<_, SerializeWithBorsh>( + key, + &self.data_to_sign(), + ); + SignedTransferTx { + data: self, + signature: StringEncoded { raw: sig }, + } + } +} + +pub type SignedBondTx = Signed>; + +impl SignedBondTx { + /// Verify the signature of `BondTx`. This should not depend + /// on whether the contained amount is denominated or not. + /// + /// Since we denominate amounts as part of validation, we can + /// only verify signatures on [`SignedBondTx`] + /// types. + pub fn verify_sig( + &self, + pk: &common::PublicKey, + ) -> Result<(), VerifySigError> { + let Self { data, signature } = self; + verify_standalone_sig::<_, SerializeWithBorsh>( + &data.data_to_sign(), + pk, + signature, + ) + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct BondTx { + pub source: AliasOrPk, + pub validator: Alias, + pub amount: T::Amount, +} + +impl BondTx { + /// Add the correct denomination to the contained amount + pub fn denominate(self) -> eyre::Result> { + let BondTx { + source, + validator, + amount, + } = self; + let amount = amount + .increase_precision(NATIVE_MAX_DECIMAL_PLACES.into()) + .map_err(|e| { + eprintln!( + "A bond amount in the transactions.toml file was \ + incorrectly formatted:\n{}", + e + ); + e + })?; + Ok(BondTx { + source, + validator, + amount, + }) + } + + /// The signable data. This does not include the phantom data. + fn data_to_sign(&self) -> Vec { + [ + self.source.try_to_vec().unwrap(), + self.validator.try_to_vec().unwrap(), + self.amount.try_to_vec().unwrap(), + ] + .concat() + } + + /// Sign the transfer. + /// + /// Since we denominate amounts as part of validation, we can + /// only verify signatures on [`SignedBondTx`] + /// types. Thus we only allow signing of [`BondTx`] + /// types. + pub fn sign(self, key: &common::SecretKey) -> SignedBondTx { + let sig = standalone_signature::<_, SerializeWithBorsh>( + key, + &self.data_to_sign(), + ); + SignedBondTx { + data: self, + signature: StringEncoded { raw: sig }, + } + } +} + +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +pub enum AliasOrPk { + /// `alias = "value"` in toml (encoded via `AliasSerHelper`) + Alias(Alias), + /// `public_key = "value"` in toml (encoded via `PkSerHelper`) + PublicKey(StringEncoded), +} + +impl Serialize for AliasOrPk { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + AliasOrPk::Alias(alias) => Serialize::serialize(alias, serializer), + AliasOrPk::PublicKey(pk) => Serialize::serialize(pk, serializer), + } + } +} + +impl<'de> Deserialize<'de> for AliasOrPk { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct FieldVisitor; + + impl<'de> serde::de::Visitor<'de> for FieldVisitor { + type Value = AliasOrPk; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str( + "a bech32m encoded `common::PublicKey` or an alias", + ) + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + // Try to deserialize a PK first + let maybe_pk = + StringEncoded::::from_str(value); + match maybe_pk { + Ok(pk) => Ok(AliasOrPk::PublicKey(pk)), + Err(_) => { + // If that doesn't work, use it as an alias + let alias = Alias::from_str(value) + .map_err(serde::de::Error::custom)?; + Ok(AliasOrPk::Alias(alias)) + } + } + } + } + + deserializer.deserialize_str(FieldVisitor) + } +} + +impl Display for AliasOrPk { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AliasOrPk::Alias(alias) => write!(f, "{}", alias), + AliasOrPk::PublicKey(pk) => write!(f, "{}", pk), + } + } +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct Signed { + #[serde(flatten)] + pub data: T, + pub signature: StringEncoded, +} + +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, + PartialEq, + Eq, +)] +pub struct SignedPk { + pub pk: StringEncoded, + pub authorization: StringEncoded, +} + +pub fn validate( + transactions: Transactions, + vps: Option<&ValidityPredicates>, + balances: Option<&DenominatedBalances>, + tokens: &Tokens, + parameters: Option<&Parameters>, +) -> Option> { + let mut is_valid = true; + + let mut all_used_aliases: BTreeSet = BTreeSet::default(); + let mut established_accounts: BTreeMap> = + BTreeMap::default(); + let mut validator_accounts: BTreeMap = + BTreeMap::default(); + + let Transactions { + ref established_account, + ref validator_account, + ref transfer, + bond, + } = transactions; + + if let Some(txs) = established_account { + for tx in txs { + if !validate_established_account( + tx, + vps, + &mut all_used_aliases, + &mut established_accounts, + ) { + is_valid = false; + } + } + } + + if let Some(txs) = validator_account { + for tx in txs { + if !validate_validator_account( + tx, + vps, + &mut all_used_aliases, + &mut validator_accounts, + ) { + is_valid = false; + } + } + } + + // Make a mutable copy of the balances for tracking changes applied from txs + let mut token_balances: BTreeMap = + balances + .map(|balances| { + balances + .token + .iter() + .map(|(token, token_balances)| { + ( + token.clone(), + TokenBalancesForValidation { + // Add an accumulator for tokens transferred to + // aliases + aliases: BTreeMap::new(), + pks: token_balances.clone(), + }, + ) + }) + .collect() + }) + .unwrap_or_default(); + + let validated_txs = if let Some(txs) = transfer { + let validated_txs: Vec<_> = txs + .iter() + .filter_map(|tx| { + validate_transfer( + tx, + &mut token_balances, + &all_used_aliases, + tokens, + ) + }) + .collect(); + if validated_txs.len() != txs.len() { + is_valid = false; + None + } else { + Some(validated_txs) + } + } else { + None + }; + + let validated_bonds = if let Some(txs) = bond { + if !txs.is_empty() { + match parameters { + Some(parameters) => { + let bond_number = txs.len(); + let validated_bonds: Vec<_> = txs + .into_iter() + .filter_map(|tx| { + validate_bond( + tx, + &mut token_balances, + &established_accounts, + &validator_accounts, + parameters, + ) + }) + .collect(); + if validated_bonds.len() != bond_number { + is_valid = false; + None + } else { + Some(validated_bonds) + } + } + None => { + eprintln!( + "Unable to validate bonds without a valid parameters \ + file." + ); + is_valid = false; + None + } + } + } else { + None + } + } else { + None + }; + + is_valid.then_some(Transactions { + established_account: transactions.established_account, + validator_account: transactions.validator_account, + transfer: validated_txs, + bond: validated_bonds, + }) +} + +fn validate_bond( + tx: SignedBondTx, + balances: &mut BTreeMap, + established_accounts: &BTreeMap>, + validator_accounts: &BTreeMap, + parameters: &Parameters, +) -> Option> { + // Check signature + let mut is_valid = { + let source = &tx.data.source; + if let Some(source_pk) = match source { + AliasOrPk::Alias(alias) => { + // Try to find the source's PK in either established_accounts or + // validator_accounts + established_accounts + .get(alias) + .cloned() + .flatten() + .or_else(|| validator_accounts.get(alias).cloned()) + } + AliasOrPk::PublicKey(pk) => Some(pk.raw.clone()), + } { + if tx.verify_sig(&source_pk).is_err() { + eprintln!("Invalid bond tx signature.",); + false + } else { + true + } + } else { + eprintln!( + "Invalid bond tx. Couldn't verify bond's signature, because \ + the source accounts \"{source}\" public key cannot be found." + ); + false + } + }; + + // Make sure the native token amount is denominated correctly + let validated_bond = tx.data.denominate().ok()?; + let BondTx { + source, + validator, + amount, + .. + } = &validated_bond; + + // Check that the validator exists + if !validator_accounts.contains_key(validator) { + eprintln!( + "Invalid bond tx. The target validator \"{validator}\" account \ + not found." + ); + is_valid = false; + } + + // Check and update token balance of the source + let native_token = ¶meters.parameters.native_token; + match balances.get_mut(native_token) { + Some(balances) => { + let balance = match source { + AliasOrPk::Alias(source) => balances.aliases.get_mut(source), + AliasOrPk::PublicKey(source) => balances.pks.0.get_mut(source), + }; + match balance { + Some(balance) => { + if *balance < *amount { + eprintln!( + "Invalid bond tx. Source {source} doesn't have \ + enough balance of token \"{native_token}\" to \ + transfer {}. Got {}.", + amount, balance, + ); + is_valid = false; + } else { + // Deduct the amount from source + if amount == balance { + match source { + AliasOrPk::Alias(source) => { + balances.aliases.remove(source); + } + AliasOrPk::PublicKey(source) => { + balances.pks.0.remove(source); + } + } + } else { + balance.amount -= amount.amount; + } + } + } + None => { + eprintln!( + "Invalid transfer tx. Source {source} has no balance \ + of token \"{native_token}\"." + ); + is_valid = false; + } + } + } + None => { + eprintln!( + "Invalid bond tx. Token \"{native_token}\" not found in \ + balances." + ); + is_valid = false; + } + } + + is_valid.then_some(validated_bond) +} + +#[derive(Clone, Debug)] +pub struct TokenBalancesForValidation { + /// Accumulator for tokens transferred to aliases + pub aliases: BTreeMap, + /// Token balances from the balances file, associated with PKs + pub pks: TokenBalances, +} + +pub fn validate_established_account( + tx: &SignedEstablishedAccountTx, + vps: Option<&ValidityPredicates>, + all_used_aliases: &mut BTreeSet, + established_accounts: &mut BTreeMap>, +) -> bool { + let mut is_valid = true; + + established_accounts.insert( + tx.alias.clone(), + tx.public_key.as_ref().map(|signed| signed.pk.raw.clone()), + ); + + // Check that alias is unique + if all_used_aliases.contains(&tx.alias) { + eprintln!( + "A duplicate alias \"{}\" found in a `established_account` tx.", + tx.alias + ); + is_valid = false; + } else { + all_used_aliases.insert(tx.alias.clone()); + } + + // Check the VP exists + if !vps + .map(|vps| vps.wasm.contains_key(&tx.vp)) + .unwrap_or_default() + { + eprintln!( + "An `established_account` tx `vp` \"{}\" not found in Validity \ + predicates file.", + tx.vp + ); + is_valid = false; + } + + // If PK is used, check the authorization + if let Some(pk) = tx.public_key.as_ref() { + if !validate_established_account_sig(pk, tx) { + is_valid = false; + } + } + + is_valid +} + +fn validate_established_account_sig( + SignedPk { pk, authorization }: &SignedPk, + tx: &SignedEstablishedAccountTx, +) -> bool { + let unsigned = UnsignedEstablishedAccountTx::from(tx); + validate_signature(&unsigned, &pk.raw, &authorization.raw) +} + +pub fn validate_validator_account( + tx: &ValidatorAccountTx, + vps: Option<&ValidityPredicates>, + all_used_aliases: &mut BTreeSet, + validator_accounts: &mut BTreeMap, +) -> bool { + let mut is_valid = true; + + validator_accounts.insert(tx.alias.clone(), tx.account_key.pk.raw.clone()); + + // Check that alias is unique + if all_used_aliases.contains(&tx.alias) { + eprintln!( + "A duplicate alias \"{}\" found in a `validator_account` tx.", + tx.alias + ); + is_valid = false; + } else { + all_used_aliases.insert(tx.alias.clone()); + } + + // Check the VP exists + if !vps + .map(|vps| vps.wasm.contains_key(&tx.vp)) + .unwrap_or_default() + { + eprintln!( + "A `validator_account` tx `vp` \"{}\" not found in Validity \ + predicates file.", + tx.vp + ); + is_valid = false; + } + + // Check keys authorizations + let unsigned = UnsignedValidatorAccountTx::from(tx); + if !validate_signature( + &unsigned, + &tx.account_key.pk.raw, + &tx.account_key.authorization.raw, + ) { + eprintln!( + "Invalid `account_key` authorization for `validator_account` tx \ + with alias \"{}\".", + tx.alias + ); + is_valid = false; + } + if !validate_signature( + &unsigned, + &tx.consensus_key.pk.raw, + &tx.consensus_key.authorization.raw, + ) { + eprintln!( + "Invalid `consensus_key` authorization for `validator_account` tx \ + with alias \"{}\".", + tx.alias + ); + is_valid = false; + } + if !validate_signature( + &unsigned, + &tx.protocol_key.pk.raw, + &tx.protocol_key.authorization.raw, + ) { + eprintln!( + "Invalid `protocol_key` authorization for `validator_account` tx \ + with alias \"{}\".", + tx.alias + ); + is_valid = false; + } + if !validate_signature( + &unsigned, + &tx.tendermint_node_key.pk.raw, + &tx.tendermint_node_key.authorization.raw, + ) { + eprintln!( + "Invalid `tendermint_node_key` authorization for \ + `validator_account` tx with alias \"{}\".", + tx.alias + ); + is_valid = false; + } + + if !validate_signature( + &unsigned, + &tx.eth_hot_key.pk.raw, + &tx.eth_hot_key.authorization.raw, + ) { + eprintln!( + "Invalid `eth_hot_key` authorization for `validator_account` tx \ + with alias \"{}\".", + tx.alias + ); + is_valid = false; + } + + if !validate_signature( + &unsigned, + &tx.eth_cold_key.pk.raw, + &tx.eth_cold_key.authorization.raw, + ) { + eprintln!( + "Invalid `eth_cold_key` authorization for `validator_account` tx \ + with alias \"{}\".", + tx.alias + ); + is_valid = false; + } + + is_valid +} + +/// Updates the token balances with all the valid transfers applied +pub fn validate_transfer( + tx: &SignedTransferTx, + balances: &mut BTreeMap, + all_used_aliases: &BTreeSet, + tokens: &Tokens, +) -> Option> { + let mut is_valid = true; + // Check signature + if tx.verify_sig().is_err() { + eprintln!("Invalid transfer tx signature.",); + is_valid = false; + } + + let unsigned: TransferTx = tx.into(); + let validated = unsigned.denominate(tokens).ok()?; + let TransferTx { + token, + source, + target, + amount, + .. + } = &validated; + + // Check that the target exists + if !all_used_aliases.contains(target) { + eprintln!( + "Invalid transfer tx. The target alias \"{target}\" no matching \ + account found." + ); + is_valid = false; + } + + // Check token balance of the source and update token balances of the source + // and target + match balances.get_mut(token) { + Some(balances) => match balances.pks.0.get_mut(source) { + Some(balance) => { + if balance.amount < amount.amount { + eprintln!( + "Invalid transfer tx. Source {source} doesn't have \ + enough balance of token \"{token}\" to transfer {}. \ + Got {}.", + amount, balance, + ); + is_valid = false; + } else { + // Deduct the amount from source + if amount.amount == balance.amount { + balances.pks.0.remove(source); + } else { + balance.amount -= amount.amount; + } + + // Add the amount to target + let target_balance = balances + .aliases + .entry(target.clone()) + .or_insert_with(|| DenominatedAmount { + amount: token::Amount::zero(), + denom: amount.denom, + }); + target_balance.amount += amount.amount; + } + } + None => { + eprintln!( + "Invalid transfer tx. Source {source} has no balance of \ + token \"{token}\"." + ); + is_valid = false; + } + }, + None => { + eprintln!( + "Invalid transfer tx. Token \"{token}\" not found in balances." + ); + is_valid = false; + } + } + + is_valid.then_some(validated) +} + +fn validate_signature( + tx_data: &T, + pk: &common::PublicKey, + sig: &common::Signature, +) -> bool { + match verify_standalone_sig::(tx_data, pk, sig) { + Ok(()) => true, + Err(err) => { + eprintln!( + "Invalid tx signature in tx {tx_data:?}, failed with: {err}." + ); + false + } + } +} + +impl From<&SignedEstablishedAccountTx> for UnsignedEstablishedAccountTx { + fn from(tx: &SignedEstablishedAccountTx) -> Self { + let SignedEstablishedAccountTx { + alias, + vp, + public_key, + storage, + } = tx; + Self { + alias: alias.clone(), + vp: vp.clone(), + public_key: public_key.as_ref().map(|signed| signed.pk.clone()), + storage: storage.clone(), + } + } +} + +impl From<&SignedValidatorAccountTx> for UnsignedValidatorAccountTx { + fn from(tx: &SignedValidatorAccountTx) -> Self { + let SignedValidatorAccountTx { + alias, + dkg_key, + vp, + commission_rate, + max_commission_rate_change, + net_address, + account_key, + consensus_key, + protocol_key, + tendermint_node_key, + eth_hot_key, + eth_cold_key, + } = tx; + + Self { + alias: alias.clone(), + dkg_key: dkg_key.clone(), + vp: vp.clone(), + commission_rate: *commission_rate, + max_commission_rate_change: *max_commission_rate_change, + net_address: *net_address, + account_key: account_key.pk.clone(), + consensus_key: consensus_key.pk.clone(), + protocol_key: protocol_key.pk.clone(), + tendermint_node_key: tendermint_node_key.pk.clone(), + eth_hot_key: eth_hot_key.pk.clone(), + eth_cold_key: eth_cold_key.pk.clone(), + } + } +} + +impl From<&SignedTransferTx> for TransferTx { + fn from(tx: &SignedTransferTx) -> Self { + let SignedTransferTx { data, signature: _ } = tx; + data.clone() + } +} + +impl From<&SignedBondTx> for BondTx { + fn from(tx: &SignedBondTx) -> Self { + let SignedBondTx { data, signature: _ } = tx; + data.clone() + } +} diff --git a/apps/src/lib/config/global.rs b/apps/src/lib/config/global.rs index f1d8e5d483..6c9bae0e4a 100644 --- a/apps/src/lib/config/global.rs +++ b/apps/src/lib/config/global.rs @@ -26,29 +26,30 @@ pub enum Error { pub type Result = std::result::Result; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct GlobalConfig { /// The default chain ID - pub default_chain_id: ChainId, + pub default_chain_id: Option, // NOTE: There will be sub-chains in here in future } impl GlobalConfig { pub fn new(default_chain_id: ChainId) -> Self { - Self { default_chain_id } + Self { + default_chain_id: Some(default_chain_id), + } } /// Try to read the global config from a file. pub fn read(base_dir: impl AsRef) -> Result { let file_path = Self::file_path(base_dir.as_ref()); let file_name = file_path.to_str().expect("Expected UTF-8 file path"); - if !file_path.exists() { - return Err(Error::FileNotFound(file_name.to_string())); - }; let mut config = config::Config::new(); - config - .merge(config::File::with_name(file_name)) - .map_err(Error::ReadError)?; + if file_path.exists() { + config + .merge(config::File::with_name(file_name)) + .map_err(Error::ReadError)?; + }; config.try_into().map_err(Error::DeserializationError) } diff --git a/apps/src/lib/config/mod.rs b/apps/src/lib/config/mod.rs index f6307c6bd1..735391d056 100644 --- a/apps/src/lib/config/mod.rs +++ b/apps/src/lib/config/mod.rs @@ -41,7 +41,7 @@ pub struct Config { pub ledger: Ledger, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum TendermintMode { Full, Validator, diff --git a/apps/src/lib/config/utils.rs b/apps/src/lib/config/utils.rs index 03725d112c..41658a000c 100644 --- a/apps/src/lib/config/utils.rs +++ b/apps/src/lib/config/utils.rs @@ -64,6 +64,30 @@ pub fn convert_tm_addr_to_socket_addr( } } +/// Set the IP address of a [`TendermintAddress`] +pub fn set_ip(tm_addr: &mut TendermintAddress, new_host: impl Into) { + match tm_addr { + TendermintAddress::Tcp { host, .. } => { + *host = new_host.into(); + } + TendermintAddress::Unix { path: _ } => { + panic!("Unix addresses aren't currently supported.") + } + } +} + +/// Set the port of a [`TendermintAddress`] +pub fn set_port(tm_addr: &mut TendermintAddress, new_port: impl Into) { + match tm_addr { + TendermintAddress::Tcp { port, .. } => { + *port = new_port.into(); + } + TendermintAddress::Unix { path: _ } => { + panic!("Unix addresses aren't currently supported.") + } + } +} + #[cfg(test)] mod test { use std::panic; diff --git a/apps/src/lib/node/ledger/mod.rs b/apps/src/lib/node/ledger/mod.rs index 3e1dbe0b46..5d4d1845a9 100644 --- a/apps/src/lib/node/ledger/mod.rs +++ b/apps/src/lib/node/ledger/mod.rs @@ -33,7 +33,6 @@ use crate::config::{ethereum_bridge, TendermintMode}; use crate::facade::tendermint_proto::abci::CheckTxType; use crate::facade::tower_abci::{response, split, Server}; use crate::node::ledger::broadcaster::Broadcaster; -use crate::node::ledger::config::genesis; use crate::node::ledger::ethereum_oracle as oracle; use crate::node::ledger::shell::{Error, MempoolTxType, Shell}; use crate::node::ledger::shims::abcipp_shim::AbcippShim; @@ -98,7 +97,7 @@ impl Shell { tracing::debug!("Request InitChain"); self.init_chain( init, - #[cfg(any(test, feature = "dev"))] + #[cfg(any(test, feature = "testing"))] 1, ) .map(Response::InitChain) @@ -469,10 +468,7 @@ fn start_abci_broadcaster_shell( let tendermint_mode = config.shell.tendermint_mode.clone(); let proxy_app_address = convert_tm_addr_to_socket_addr(&config.cometbft.proxy_app); - #[cfg(not(any(test, feature = "dev")))] - let genesis = genesis::genesis(&config.shell.base_dir, &config.chain_id); - #[cfg(any(test, feature = "dev"))] - let genesis = genesis::genesis(1); + let (shell, abci_service, service_handle) = AbcippShim::new( config, wasm_dir, @@ -481,7 +477,6 @@ fn start_abci_broadcaster_shell( &db_cache, vp_wasm_compilation_cache, tx_wasm_compilation_cache, - genesis.native_token, ); // Channel for signalling shut down to ABCI server diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 4fef1fc7ef..3f6fbcd398 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1667,7 +1667,6 @@ mod test_finalize_block { /// Test that adding a new erc20 transfer to the bridge pool /// increments the pool's nonce. fn test_bp_nonce_is_incremented() { - use crate::node::ledger::shell::address::nam; test_bp(|shell: &mut TestShell| { let asset = EthAddress([0xff; 20]); let receiver = EthAddress([0xaa; 20]); @@ -1694,7 +1693,7 @@ mod test_finalize_block { { let amt: Amount = 999_999_u64.into(); let pool_balance_key = token::balance_key( - &nam(), + &shell.wl_storage.storage.native_token, &bridge_pool::BRIDGE_POOL_ADDRESS, ); shell @@ -1717,7 +1716,7 @@ mod test_finalize_block { sender: bertha.clone(), }, gas_fee: GasFee { - token: nam(), + token: shell.wl_storage.storage.native_token.clone(), amount: 10u64.into(), payer: bertha.clone(), }, @@ -3090,6 +3089,7 @@ mod test_finalize_block { &val1.address, del_1_amount, current_epoch, + None, ) .unwrap(); @@ -3190,6 +3190,7 @@ mod test_finalize_block { &val1.address, self_bond_1_amount, current_epoch, + None, ) .unwrap(); @@ -3231,6 +3232,7 @@ mod test_finalize_block { &val1.address, del_2_amount, current_epoch, + None, ) .unwrap(); diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 2554349680..68027e1548 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -2,23 +2,32 @@ use std::collections::HashMap; use std::hash::Hash; -use namada::ledger::parameters::{self, Parameters}; -use namada::ledger::pos::{staking_token_address, OwnedPosParams}; +use namada::ledger::parameters::{Parameters}; +use namada::ledger::pos::OwnedPosParams; use namada::ledger::storage::traits::StorageHasher; use namada::ledger::storage::{DBIter, DB}; use namada::ledger::storage_api::token::{ - credit_tokens, read_balance, read_total_supply, write_denom, + credit_tokens, write_denom, }; use namada::ledger::storage_api::{ResultExt, StorageRead, StorageWrite}; use namada::ledger::{ibc, pos}; -use namada::types::dec::Dec; +use namada::proof_of_stake::BecomeValidator; use namada::types::hash::Hash as CodeHash; use namada::types::key::*; +use namada::types::storage::KeySeg; use namada::types::time::{DateTimeUtc, TimeZone, Utc}; use namada::vm::validate_untrusted_wasm; use namada_sdk::eth_bridge::EthBridgeStatus; use super::*; +use crate::config::genesis::chain::{ + FinalizedEstablishedAccountTx, FinalizedTokenConfig, + FinalizedValidatorAccountTx, +}; +use crate::config::genesis::templates::{TokenBalances, TokenConfig}; +use crate::config::genesis::transactions::{ + BondTx, EstablishedAccountTx, TransferTx, ValidatorAccountTx, +}; use crate::facade::tendermint_proto::google::protobuf; use crate::facade::tower_abci::{request, response}; use crate::wasm_loader; @@ -39,32 +48,41 @@ where pub fn init_chain( &mut self, init: request::InitChain, - #[cfg(any(test, feature = "dev"))] num_validators: u64, + #[cfg(any(test, feature = "testing"))] _num_validators: u64, ) -> Result { - let (current_chain_id, _) = self.wl_storage.storage.get_chain_id(); - if current_chain_id != init.chain_id { + let mut response = response::InitChain::default(); + let chain_id = self.wl_storage.storage.chain_id.as_str(); + if chain_id != init.chain_id.as_str() { return Err(Error::ChainId(format!( "Current chain ID: {}, Tendermint chain ID: {}", - current_chain_id, init.chain_id + chain_id, init.chain_id ))); } - #[cfg(not(any(test, feature = "dev")))] - let genesis = - genesis::genesis(&self.base_dir, &self.wl_storage.storage.chain_id); - #[cfg(not(any(test, feature = "dev")))] + + // Read the genesis files + #[cfg(any( + feature = "integration", + not(any(test, feature = "benches")) + ))] + let genesis = { + let chain_dir = self.base_dir.join(chain_id); + genesis::chain::Finalized::read_toml_files(&chain_dir) + .expect("Missing genesis files") + }; + #[cfg(all( + any(test, feature = "benches"), + not(feature = "integration") + ))] + let genesis = genesis::make_dev_genesis(_num_validators); + #[cfg(all( + any(test, feature = "benches"), + not(feature = "integration") + ))] { - let genesis_bytes = genesis.serialize_to_vec(); - let errors = - self.wl_storage.storage.chain_id.validate(genesis_bytes); - use itertools::Itertools; - assert!( - errors.is_empty(), - "Chain ID validation failed: {}", - errors.into_iter().format(". ") - ); + // update the native token from the genesis file + let native_token = genesis.get_native_token().clone(); + self.wl_storage.storage.native_token = native_token; } - #[cfg(any(test, feature = "dev"))] - let genesis = genesis::genesis(num_validators); let ts: protobuf::Timestamp = init.time.expect("Missing genesis time"); let initial_height = init @@ -79,26 +97,112 @@ where .into(); // Initialize protocol parameters - let genesis::Parameters { - epoch_duration, - max_proposal_bytes, - max_block_gas, - max_expected_time_per_block, - vp_whitelist, + let parameters = genesis.get_chain_parameters(&self.wasm_dir); + self.store_wasms(¶meters)?; + parameters.init_storage(&mut self.wl_storage)?; + + // Initialize governance parameters + let gov_params = genesis.get_gov_params(); + gov_params.init_storage(&mut self.wl_storage)?; + + // configure the Ethereum bridge if the configuration is set. + if let Some(config) = genesis.get_eth_bridge_params() { + tracing::debug!("Initializing Ethereum bridge storage."); + config.init_storage(&mut self.wl_storage); + self.update_eth_oracle(); + } else { + self.wl_storage + .write_bytes( + &namada::eth_bridge::storage::active_key(), + EthBridgeStatus::Disabled.serialize_to_vec(), + ) + .unwrap(); + } + + // Depends on parameters being initialized + self.wl_storage + .storage + .init_genesis_epoch(initial_height, genesis_time, ¶meters) + .expect("Initializing genesis epoch must not fail"); + + // PoS system depends on epoch being initialized + let pos_params = genesis.get_pos_params(); + let (current_epoch, _gas) = self.wl_storage.storage.get_current_epoch(); + pos::namada_proof_of_stake::init_genesis( + &mut self.wl_storage, + &pos_params, + current_epoch, + ) + .expect("Must be able to initialize PoS genesis storage"); + + // PGF parameters + let pgf_params = genesis.get_pgf_params(); + pgf_params + .init_storage(&mut self.wl_storage) + .expect("Should be able to initialized PGF at genesis"); + + // Loaded VP code cache to avoid loading the same files multiple times + let mut vp_cache: HashMap> = HashMap::default(); + self.init_token_accounts(&genesis); + self.init_token_balances(&genesis); + self.apply_genesis_txs_established_account(&genesis, &mut vp_cache); + self.apply_genesis_txs_validator_account( + &genesis, + &mut vp_cache, + &pos_params, + current_epoch, + ); + self.apply_genesis_txs_transfer(&genesis); + self.apply_genesis_txs_bonds(&genesis); + + pos::namada_proof_of_stake::store_total_consensus_stake( + &mut self.wl_storage, + Default::default(), + ) + .expect("Could not compute total consensus stake at genesis"); + // This has to be done after `apply_genesis_txs_validator_account` + pos::namada_proof_of_stake::copy_genesis_validator_sets( + &mut self.wl_storage, + &pos_params, + current_epoch, + ) + .expect("Must be able to copy PoS genesis validator sets"); + + ibc::init_genesis_storage(&mut self.wl_storage); + + // Set the initial validator set + response.validators = self + .get_abci_validator_updates(true) + .expect("Must be able to set genesis validator set"); + debug_assert!(!response.validators.is_empty()); + Ok(response) + } + + /// Look-up WASM code of a genesis VP by its name + fn lookup_vp( + &self, + name: &str, + genesis: &genesis::chain::Finalized, + vp_cache: &mut HashMap>, + ) -> Vec { + let config = + genesis.vps.wasm.get(name).unwrap_or_else(|| { + panic!("Missing validity predicate for {name}") + }); + let vp_filename = &config.filename; + vp_cache.get_or_insert_with(vp_filename.clone(), || { + wasm_loader::read_wasm(&self.wasm_dir, vp_filename).unwrap() + }) + } + + fn store_wasms(&mut self, params: &Parameters) -> Result<()> { + let Parameters { tx_whitelist, - implicit_vp_code_path, - implicit_vp_sha256, - epochs_per_year, - max_signatures_per_transaction, - pos_gain_p, - pos_gain_d, - staked_ratio, - pos_inflation_amount, - minimum_gas_price, - fee_unshielding_gas_limit, - fee_unshielding_descriptions_limit, - } = genesis.parameters; - // Store wasm codes into storage + vp_whitelist, + implicit_vp_code_hash, + .. + } = params; + let mut is_implicit_vp_stored = false; let checksums = wasm_loader::Checksums::read_checksums(&self.wasm_dir); for (name, full_name) in checksums.0.iter() { let code = wasm_loader::read_wasm(&self.wasm_dir, name) @@ -140,6 +244,9 @@ where 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)?; + if &code_hash == implicit_vp_code_hash { + is_implicit_vp_stored = true; + } self.wl_storage.write_bytes(&code_name_key, code_hash)?; } else { tracing::warn!("The wasm {name} isn't whitelisted."); @@ -147,353 +254,354 @@ where } // check if implicit_vp wasm is stored - let implicit_vp_code_hash = - read_wasm_hash(&self.wl_storage, &implicit_vp_code_path)?.ok_or( - Error::LoadingWasm(format!( - "Unknown vp code path: {}", - implicit_vp_code_path - )), - )?; - // In dev, we don't check the hash - #[cfg(feature = "dev")] - let _ = implicit_vp_sha256; - #[cfg(not(feature = "dev"))] - { - assert_eq!( - implicit_vp_code_hash.0.as_slice(), - &implicit_vp_sha256, - "Invalid implicit account's VP sha256 hash for {}", - implicit_vp_code_path - ); - } - - let parameters = Parameters { - epoch_duration, - max_proposal_bytes, - max_block_gas, - max_expected_time_per_block, - vp_whitelist, - tx_whitelist, - implicit_vp_code_hash, - epochs_per_year, - max_signatures_per_transaction, - pos_gain_p, - pos_gain_d, - staked_ratio, - pos_inflation_amount, - minimum_gas_price, - fee_unshielding_gas_limit, - fee_unshielding_descriptions_limit, - }; - parameters - .init_storage(&mut self.wl_storage) - .expect("Initializing chain parameters must not fail"); - - // Initialize governance parameters - genesis - .gov_params - .init_storage(&mut self.wl_storage) - .expect("Initializing chain parameters must not fail"); + assert!( + is_implicit_vp_stored, + "No VP found matching the expected implicit VP sha256 hash: {}", + implicit_vp_code_hash + ); + Ok(()) + } - // Initialize pgf parameters - genesis - .pgf_params - .init_storage(&mut self.wl_storage) - .expect("Initializing chain parameters must not fail"); + /// Init genesis token accounts + fn init_token_accounts(&mut self, genesis: &genesis::chain::Finalized) { + let masp_rewards = address::tokens(); + for (alias, token) in &genesis.tokens.token { + tracing::debug!("Initializing token {alias}"); - // configure the Ethereum bridge if the configuration is set. - if let Some(config) = genesis.ethereum_bridge_params { - tracing::debug!("Initializing Ethereum bridge storage."); - config.init_storage(&mut self.wl_storage); - self.update_eth_oracle(); - } else { - self.wl_storage - .write_bytes( - &namada::eth_bridge::storage::active_key(), - EthBridgeStatus::Disabled.serialize_to_vec(), - ) - .unwrap(); + let FinalizedTokenConfig { + address, + config: TokenConfig { denom }, + } = token; + // associate a token with its denomination. + write_denom(&mut self.wl_storage, address, *denom).unwrap(); + // add token addresses to the masp reward conversions lookup table. + let alias = alias.to_string(); + if masp_rewards.contains_key(&alias.as_str()) { + self.wl_storage + .storage + .conversion_state + .tokens + .insert(alias, address.clone()); + } } - - // Depends on parameters being initialized - self.wl_storage + let key_prefix: Key = address::masp().to_db_key().into(); + // Save the current conversion state + let state_key = key_prefix + .push(&(token::CONVERSION_KEY_PREFIX.to_owned())) + .unwrap(); + let conv_bytes = self + .wl_storage .storage - .init_genesis_epoch(initial_height, genesis_time, ¶meters) - .expect("Initializing genesis epoch must not fail"); - - // Initialize genesis established accounts - self.initialize_established_accounts( - genesis.established_accounts, - &implicit_vp_code_path, - )?; - - // Initialize genesis implicit - self.initialize_implicit_accounts(genesis.implicit_accounts); - - // Initialize genesis token accounts - self.initialize_token_accounts(genesis.token_accounts); - - // Initialize genesis validator accounts - let staking_token = staking_token_address(&self.wl_storage); - self.initialize_validators( - &staking_token, - &genesis.validators, - &implicit_vp_code_path, - ); - // set the initial validators set - self.set_initial_validators( - &staking_token, - genesis.validators, - &genesis.pos_params, - ) + .conversion_state + .serialize_to_vec() + .unwrap(); + self.wl_storage.write_bytes(&state_key, conv_bytes).unwrap(); } - /// Initialize genesis established accounts - fn initialize_established_accounts( - &mut self, - accounts: Vec, - implicit_vp_code_path: &str, - ) -> Result<()> { - for genesis::EstablishedAccount { - address, - vp_code_path, - vp_sha256, - public_key, - storage, - } in accounts - { - let vp_code_hash = read_wasm_hash(&self.wl_storage, &vp_code_path)? - .ok_or(Error::LoadingWasm(format!( - "Unknown vp code path: {}", - implicit_vp_code_path - )))?; - - // In dev, we don't check the hash - #[cfg(feature = "dev")] - let _ = vp_sha256; - #[cfg(not(feature = "dev"))] - { - assert_eq!( - vp_code_hash.0.as_slice(), - &vp_sha256, - "Invalid established account's VP sha256 hash for {}", - vp_code_path - ); - } - - self.wl_storage - .write_bytes(&Key::validity_predicate(&address), vp_code_hash) - .unwrap(); - - if let Some(pk) = public_key { + /// Init genesis token balances + fn init_token_balances(&mut self, genesis: &genesis::chain::Finalized) { + for (token_alias, TokenBalances(balances)) in &genesis.balances.token { + tracing::debug!("Initializing token balances {token_alias}"); + + let token_address = &genesis + .tokens + .token + .get(token_alias) + .expect("Token with configured balance not found in genesis.") + .address; + for (owner_pk, balance) in balances { + let owner = Address::from(&owner_pk.raw); storage_api::account::set_public_key_at( &mut self.wl_storage, - &address, - &pk, + &owner, + &owner_pk.raw, 0, - )?; - } - - for (key, value) in storage { - self.wl_storage.write_bytes(&key, value).unwrap(); + ) + .unwrap(); + tracing::info!( + "Crediting {} {} tokens to {}", + balance, + token_alias, + owner_pk.raw + ); + credit_tokens( + &mut self.wl_storage, + token_address, + &owner, + balance.amount, + ) + .expect("Couldn't credit initial balance"); } } - - Ok(()) } - /// Initialize genesis implicit accounts - fn initialize_implicit_accounts( + /// Apply genesis txs to initialize established accounts + fn apply_genesis_txs_established_account( &mut self, - accounts: Vec, + genesis: &genesis::chain::Finalized, + vp_cache: &mut HashMap>, ) { - // Initialize genesis implicit - for genesis::ImplicitAccount { public_key } in accounts { - let address: address::Address = (&public_key).into(); - storage_api::account::set_public_key_at( - &mut self.wl_storage, - &address, - &public_key, - 0, - ) + let vp_code = self.lookup_vp("vp_masp", genesis, vp_cache); + let code_hash = CodeHash::sha256(&vp_code); + self.wl_storage + .write_bytes(&Key::validity_predicate(&address::masp()), code_hash) .unwrap(); + if let Some(txs) = genesis.transactions.established_account.as_ref() { + for FinalizedEstablishedAccountTx { + address, + tx: + EstablishedAccountTx { + alias, + vp, + public_key, + storage, + }, + } in txs + { + tracing::debug!( + "Applying genesis tx to init an established account \ + {alias}" + ); + let vp_code = self.lookup_vp(vp, genesis, vp_cache); + let code_hash = CodeHash::sha256(&vp_code); + self.wl_storage + .write_bytes(&Key::validity_predicate(address), code_hash) + .unwrap(); + + if let Some(pk) = public_key { + storage_api::account::set_public_key_at( + &mut self.wl_storage, + address, + &pk.pk.raw, + 0, + ) + .unwrap(); + } + + // Place the keys under the owners sub-storage + let sub_key = namada::core::types::storage::Key::from( + address.to_db_key(), + ); + for (key, value) in storage { + self.wl_storage + .write_bytes( + &sub_key.join(key), + value.to_bytes().unwrap(), + ) + .unwrap(); + } + } } } - /// Initialize genesis token accounts - fn initialize_token_accounts( + /// Apply genesis txs to initialize validator accounts + fn apply_genesis_txs_validator_account( &mut self, - accounts: Vec, + genesis: &genesis::chain::Finalized, + vp_cache: &mut HashMap>, + params: &PosParams, + current_epoch: namada::types::storage::Epoch, ) { - // Initialize genesis token accounts - for genesis::TokenAccount { - address, - denom, - balances, - parameters, - last_inflation, - last_locked_ratio, - } in accounts - { - // Init token parameters and last inflation and caching rates - parameters.init_storage(&address, &mut self.wl_storage); - self.wl_storage - .write( - &token::masp_last_inflation_key(&address), - last_inflation, - ) - .unwrap(); - self.wl_storage - .write( - &token::masp_last_locked_ratio_key(&address), - last_locked_ratio, + if let Some(txs) = genesis.transactions.validator_account.as_ref() { + for FinalizedValidatorAccountTx { + address, + tx: + ValidatorAccountTx { + alias, + vp, + dkg_key, + commission_rate, + max_commission_rate_change, + net_address: _, + account_key, + consensus_key, + protocol_key, + tendermint_node_key: _, + eth_hot_key, + eth_cold_key, + }, + } in txs + { + tracing::debug!( + "Applying genesis tx to init a validator account {alias}" + ); + + let vp_code = self.lookup_vp(vp, genesis, vp_cache); + let code_hash = CodeHash::sha256(&vp_code); + self.wl_storage + .write_bytes(&Key::validity_predicate(address), code_hash) + .expect("Unable to write user VP"); + // Validator account key + storage_api::account::set_public_key_at( + &mut self.wl_storage, + address, + &account_key.pk.raw, + 0, ) .unwrap(); - // associate a token with its denomination. - write_denom(&mut self.wl_storage, &address, denom).unwrap(); - let mut total_balance_for_token = token::Amount::default(); - for (owner, amount) in balances { - total_balance_for_token += amount; - credit_tokens(&mut self.wl_storage, &address, &owner, amount) - .unwrap(); + self.wl_storage + .write(&protocol_pk_key(address), &protocol_key.pk.raw) + .expect("Unable to set genesis user protocol public key"); + + self.wl_storage + .write(&dkg_session_keys::dkg_pk_key(address), &dkg_key.raw) + .expect( + "Unable to set genesis user public DKG session key", + ); + + // TODO: replace pos::init_genesis validators arg with + // init_genesis_validator from here + if let Err(err) = pos::namada_proof_of_stake::become_validator( + BecomeValidator { + storage: &mut self.wl_storage, + params, + address, + consensus_key: &consensus_key.pk.raw, + protocol_key: &protocol_key.pk.raw, + eth_cold_key: ð_cold_key.pk.raw, + eth_hot_key: ð_hot_key.pk.raw, + current_epoch, + commission_rate: *commission_rate, + max_commission_rate_change: *max_commission_rate_change, + offset_opt: Some(0), + }, + ) { + tracing::warn!( + "Genesis init genesis validator tx for {alias} failed \ + with {err}. Skipping." + ); + continue; + } } - // Write the total amount of tokens for the ratio - self.wl_storage - .write( - &token::minted_balance_key(&address), - total_balance_for_token, - ) - .unwrap(); } } - /// Initialize genesis validator accounts - fn initialize_validators( + /// Apply genesis txs to transfer tokens + fn apply_genesis_txs_transfer( &mut self, - staking_token: &Address, - validators: &[genesis::Validator], - implicit_vp_code_path: &str, + genesis: &genesis::chain::Finalized, ) { - // Initialize genesis validator accounts - for validator in validators { - let vp_code_hash = read_wasm_hash( - &self.wl_storage, - &validator.validator_vp_code_path, - ) - .unwrap() - .ok_or(Error::LoadingWasm(format!( - "Unknown vp code path: {}", - implicit_vp_code_path - ))) - .expect("Reading wasms should not fail"); - - #[cfg(not(feature = "dev"))] + if let Some(txs) = &genesis.transactions.transfer { + for TransferTx { + token, + source, + target, + amount, + .. + } in txs { - assert_eq!( - vp_code_hash.0.as_slice(), - &validator.validator_vp_sha256, - "Invalid validator VP sha256 hash for {}", - validator.validator_vp_code_path + let token = match genesis.get_token_address(token) { + Some(token) => { + tracing::debug!( + "Applying genesis tx to transfer {} of token \ + {token} from {source} to {target}", + amount + ); + token + } + None => { + tracing::warn!( + "Genesis token transfer tx uses an unknown token \ + alias {token}. Skipping." + ); + continue; + } + }; + let target = match genesis.get_user_address(target) { + Some(target) => target, + None => { + tracing::warn!( + "Genesis token transfer tx uses an unknown target \ + alias {target}. Skipping." + ); + continue; + } + }; + let source: Address = (&source.raw).into(); + tracing::debug!( + "Transfer addresses: token {token} from {source} to \ + {target}" ); - } - - let addr = &validator.pos_data.address; - self.wl_storage - .write_bytes(&Key::validity_predicate(addr), vp_code_hash) - .expect("Unable to write user VP"); - // Validator account key - storage_api::account::set_public_key_at( - &mut self.wl_storage, - addr, - &validator.account_key, - 0, - ) - .unwrap(); - // Balances - // Account balance (tokens not staked in PoS) - credit_tokens( - &mut self.wl_storage, - staking_token, - addr, - validator.non_staked_balance, - ) - .unwrap(); - - self.wl_storage - .write( - &dkg_session_keys::dkg_pk_key(addr), - &validator.dkg_public_key, - ) - .expect("Unable to set genesis user public DKG session key"); + if let Err(err) = storage_api::token::transfer( + &mut self.wl_storage, + token, + &source, + &target, + amount.amount, + ) { + tracing::warn!( + "Genesis token transfer tx failed with: {err}. \ + Skipping." + ); + continue; + }; + } } } - /// Initialize the PoS and set the initial validator set - fn set_initial_validators( - &mut self, - staking_token: &Address, - validators: Vec, - pos_params: &OwnedPosParams, - ) -> Result { - let mut response = response::InitChain::default(); - // PoS system depends on epoch being initialized. Write the total - // genesis staking token balance to storage after - // initialization. + /// Apply genesis txs to transfer tokens + fn apply_genesis_txs_bonds(&mut self, genesis: &genesis::chain::Finalized) { let (current_epoch, _gas) = self.wl_storage.storage.get_current_epoch(); - pos::init_genesis_storage( - &mut self.wl_storage, - pos_params, - validators.into_iter().map(|validator| validator.pos_data), - current_epoch, - ); - - let total_nam = read_total_supply(&self.wl_storage, staking_token)?; - // At this stage in the chain genesis, the PoS address balance is the - // same as the number of staked tokens - let total_staked_nam = - read_balance(&self.wl_storage, staking_token, &address::POS)?; - - tracing::info!( - "Genesis total native tokens: {}.", - total_nam.to_string_native() - ); - tracing::info!( - "Total staked tokens: {}.", - total_staked_nam.to_string_native() - ); - - // Set the ratio of staked to total NAM tokens in the parameters storage - parameters::update_staked_ratio_parameter( - &mut self.wl_storage, - &(Dec::from(total_staked_nam) / Dec::from(total_nam)), - ) - .expect("unable to set staked ratio of NAM in storage"); - - ibc::init_genesis_storage(&mut self.wl_storage); - - // Set the initial validator set - response.validators = self - .get_abci_validator_updates(true) - .expect("Must be able to set genesis validator set"); - debug_assert!(!response.validators.is_empty()); - - Ok(response) - } -} + if let Some(txs) = &genesis.transactions.bond { + for BondTx { + source, + validator, + amount, + .. + } in txs + { + tracing::debug!( + "Applying genesis tx to bond {} native tokens from \ + {source} to {validator}", + amount, + ); -fn read_wasm_hash( - storage: &impl StorageRead, - path: impl AsRef, -) -> storage_api::Result> { - let hash_key = Key::wasm_hash(path); - match storage.read_bytes(&hash_key)? { - Some(value) => { - let hash = CodeHash::try_from(&value[..]).into_storage_result()?; - Ok(Some(hash)) + let source = match source { + genesis::transactions::AliasOrPk::Alias(alias) => { + match genesis.get_user_address(alias) { + Some(addr) => addr, + None => { + tracing::warn!( + "Cannot find bond source address with \ + alias \"{alias}\". Skipping." + ); + continue; + } + } + } + genesis::transactions::AliasOrPk::PublicKey(pk) => { + Address::from(&pk.raw) + } + }; + + let validator = match genesis.get_validator_address(validator) { + Some(addr) => addr, + None => { + tracing::warn!( + "Cannot find bond validator address with alias \ + \"{validator}\". Skipping." + ); + continue; + } + }; + + if let Err(err) = pos::namada_proof_of_stake::bond_tokens( + &mut self.wl_storage, + Some(&source), + validator, + amount.amount, + current_epoch, + Some(0), + ) { + tracing::warn!( + "Genesis bond tx failed with: {err}. Skipping." + ); + continue; + }; + } } - None => Ok(None), } } diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index e647d1eebb..38b7456e87 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -439,7 +439,6 @@ where db_cache: Option<&D::Cache>, vp_wasm_compilation_cache: u64, tx_wasm_compilation_cache: u64, - native_token: Address, ) -> Self { let chain_id = config.chain_id; let db_path = config.shell.db_dir(&chain_id); @@ -451,6 +450,16 @@ where std::fs::create_dir(&base_dir) .expect("Creating directory for Namada should not fail"); } + let native_token = if !cfg!(test) { + let chain_dir = base_dir.join(chain_id.as_str()); + let genesis = + genesis::chain::Finalized::read_toml_files(&chain_dir) + .expect("Missing genesis files"); + genesis.get_native_token().clone() + } else { + address::nam() + }; + // load last state from storage let mut storage = Storage::open( db_path, @@ -472,26 +481,19 @@ where // load in keys and address from wallet if mode is set to `Validator` let mode = match mode { TendermintMode::Validator => { - #[cfg(not(feature = "dev"))] + #[cfg(not(test))] { let wallet_path = &base_dir.join(chain_id.as_str()); - let genesis_path = - &base_dir.join(format!("{}.toml", chain_id.as_str())); tracing::debug!( - "{}", - wallet_path.as_path().to_str().unwrap() - ); - let mut wallet = crate::wallet::load_or_new_from_genesis( - wallet_path, - genesis::genesis_config::open_genesis_config( - genesis_path, - ) - .unwrap(), + "Loading wallet from {}", + wallet_path.to_string_lossy() ); + let mut wallet = crate::wallet::load(wallet_path) + .expect("Validator node must have a wallet"); wallet .take_validator_data() .map(|data| ShellMode::Validator { - data: data.clone(), + data, broadcast_sender, eth_oracle, }) @@ -500,14 +502,15 @@ where wallet", ) } - #[cfg(feature = "dev")] + #[cfg(test)] { let (protocol_keypair, eth_bridge_keypair, dkg_keypair) = - wallet::defaults::validator_keys(); + crate::wallet::defaults::validator_keys(); ShellMode::Validator { - data: wallet::ValidatorData { - address: wallet::defaults::validator_address(), - keys: wallet::ValidatorKeys { + data: ValidatorData { + address: crate::wallet::defaults::validator_address( + ), + keys: ValidatorKeys { protocol_keypair, eth_bridge_keypair, dkg_keypair: Some(dkg_keypair), @@ -1690,7 +1693,6 @@ mod test_utils { None, vp_wasm_compilation_cache, tx_wasm_compilation_cache, - address::nam(), ); shell.wl_storage.storage.block.height = height.into(); (Self { shell }, receiver, eth_sender, control_receiver) @@ -1968,7 +1970,6 @@ mod test_utils { /// We test that on shell shutdown, the tx queue gets persisted in a DB, and /// on startup it is read successfully - #[cfg(feature = "testing")] #[test] fn test_tx_queue_persistence() { let base_dir = tempdir().unwrap().as_ref().canonicalize().unwrap(); @@ -1999,14 +2000,12 @@ mod test_utils { None, vp_wasm_compilation_cache, tx_wasm_compilation_cache, - native_token.clone(), ); shell .wl_storage .storage .begin_block(BlockHash::default(), BlockHeight(1)) .expect("begin_block failed"); - token::testing::init_token_storage(&mut shell.wl_storage, 60); let keypair = gen_keypair(); // enqueue a wrapper tx let mut wrapper = @@ -2036,6 +2035,14 @@ mod test_utils { .block .pred_epochs .new_epoch(BlockHeight(1)); + // Insert a map assigning random addresses to each token alias. + // Needed for storage but not for this test. + for (token, _) in address::tokens() { + shell.wl_storage.storage.conversion_state.tokens.insert( + token.to_string(), + address::gen_deterministic_established_address(token), + ); + } update_allowed_conversions(&mut shell.wl_storage) .expect("update conversions failed"); shell.wl_storage.commit_block().expect("commit failed"); @@ -2065,7 +2072,6 @@ mod test_utils { None, vp_wasm_compilation_cache, tx_wasm_compilation_cache, - address::nam(), ); assert!(!shell.wl_storage.storage.tx_queue.is_empty()); } 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 f817b4cd9f..2cefc82bf4 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 @@ -340,6 +340,7 @@ mod test_bp_vote_extensions { current_epoch: 0.into(), commission_rate: Default::default(), max_commission_rate_change: Default::default(), + offset_opt: None, }) .expect("Test failed"); diff --git a/apps/src/lib/node/ledger/shims/abcipp_shim.rs b/apps/src/lib/node/ledger/shims/abcipp_shim.rs index 0ca7ef39fc..3dd5922e92 100644 --- a/apps/src/lib/node/ledger/shims/abcipp_shim.rs +++ b/apps/src/lib/node/ledger/shims/abcipp_shim.rs @@ -7,7 +7,6 @@ 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; @@ -63,7 +62,6 @@ impl AbcippShim { db_cache: &rocksdb::Cache, vp_wasm_compilation_cache: u64, tx_wasm_compilation_cache: u64, - native_token: Address, ) -> (Self, AbciService, broadcast::Sender<()>) { // We can use an unbounded channel here, because tower-abci limits the // the number of requests that can come in @@ -81,7 +79,6 @@ impl AbcippShim { Some(db_cache), vp_wasm_compilation_cache, tx_wasm_compilation_cache, - native_token, ), #[cfg(not(feature = "abcipp"))] begin_block_request: None, diff --git a/apps/src/lib/node/ledger/storage/mod.rs b/apps/src/lib/node/ledger/storage/mod.rs index 43617879c8..659f561cce 100644 --- a/apps/src/lib/node/ledger/storage/mod.rs +++ b/apps/src/lib/node/ledger/storage/mod.rs @@ -118,7 +118,6 @@ mod tests { assert_eq!(result, None); } - #[cfg(feature = "testing")] #[test] fn test_commit_block() { let db_path = @@ -145,7 +144,6 @@ mod tests { storage.block.pred_epochs.new_epoch(BlockHeight(100)); // make wl_storage to update conversion for a new epoch let mut wl_storage = WlStorage::new(WriteLog::default(), storage); - namada::types::token::testing::init_token_storage(&mut wl_storage, 60); update_allowed_conversions(&mut wl_storage) .expect("update conversions failed"); wl_storage.commit_block().expect("commit failed"); diff --git a/apps/src/lib/node/ledger/tendermint_node.rs b/apps/src/lib/node/ledger/tendermint_node.rs index 0833f7c3a7..ace037cd95 100644 --- a/apps/src/lib/node/ledger/tendermint_node.rs +++ b/apps/src/lib/node/ledger/tendermint_node.rs @@ -9,6 +9,7 @@ use namada::types::key::*; use namada::types::storage::BlockHeight; use namada::types::time::DateTimeUtc; use serde_json::json; +use sha2::{Digest, Sha256}; #[cfg(feature = "abcipp")] use tendermint_abcipp::Moniker; use thiserror::Error; @@ -18,12 +19,14 @@ use tokio::process::Command; use crate::cli::namada_version; use crate::config; +use crate::facade::tendermint::node::Id as TendermintNodeId; #[cfg(feature = "abciplus")] use crate::facade::tendermint::Moniker; use crate::facade::tendermint::{block, Genesis}; use crate::facade::tendermint_config::{ Error as TendermintError, TendermintConfig, }; + /// Env. var to output Tendermint log to stdout pub const ENV_VAR_TM_STDOUT: &str = "NAMADA_CMT_STDOUT"; @@ -83,13 +86,6 @@ pub async fn run( let tendermint_path = from_env_or_default()?; let mode = config.shell.tendermint_mode.to_str().to_owned(); - #[cfg(feature = "dev")] - // This has to be checked before we run tendermint init - let has_validator_key = { - let path = home_dir.join("config").join("priv_validator_key.json"); - Path::new(&path).exists() - }; - // init and run a tendermint node child process let output = Command::new(&tendermint_path) .args(["init", &mode, "--home", &home_dir_string]) @@ -100,14 +96,6 @@ pub async fn run( panic!("Tendermint failed to initialize with {:#?}", output); } - #[cfg(feature = "dev")] - { - let consensus_key = crate::wallet::defaults::validator_keypair(); - // write the validator key file if it didn't already exist - if !has_validator_key { - write_validator_key_async(&home_dir, &consensus_key).await; - } - } #[cfg(feature = "abcipp")] write_tm_genesis(&home_dir, chain_id, genesis_time, &config).await; #[cfg(not(feature = "abcipp"))] @@ -342,6 +330,28 @@ pub fn write_validator_state(home_dir: impl AsRef) { .expect("Couldn't write private validator state file"); } +/// Length of a Tendermint Node ID in bytes +const TENDERMINT_NODE_ID_LENGTH: usize = 20; + +/// Derive Tendermint node ID from public key +pub fn id_from_pk(pk: &common::PublicKey) -> TendermintNodeId { + let mut bytes = [0u8; TENDERMINT_NODE_ID_LENGTH]; + + match pk { + common::PublicKey::Ed25519(_) => { + let _pk: ed25519::PublicKey = pk.try_to_pk().unwrap(); + let digest = Sha256::digest(_pk.serialize_to_vec().as_slice()); + bytes.copy_from_slice(&digest[..TENDERMINT_NODE_ID_LENGTH]); + } + common::PublicKey::Secp256k1(_) => { + let _pk: secp256k1::PublicKey = pk.try_to_pk().unwrap(); + let digest = Sha256::digest(_pk.serialize_to_vec().as_slice()); + bytes.copy_from_slice(&digest[..TENDERMINT_NODE_ID_LENGTH]); + } + } + TendermintNodeId::new(bytes) +} + async fn update_tendermint_config( home_dir: impl AsRef, config: TendermintConfig, diff --git a/apps/src/lib/wallet/alias.rs b/apps/src/lib/wallet/alias.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/src/lib/wallet/cli_utils.rs b/apps/src/lib/wallet/cli_utils.rs deleted file mode 100644 index 9908af12bb..0000000000 --- a/apps/src/lib/wallet/cli_utils.rs +++ /dev/null @@ -1,517 +0,0 @@ -use std::fs::File; -use std::io::{self, Write}; - -use borsh_ext::BorshSerializeExt; -use itertools::sorted; -use masp_primitives::zip32::ExtendedFullViewingKey; -use namada::types::key::{PublicKeyHash, RefTo}; -use namada::types::masp::{MaspValue, PaymentAddress}; -use namada_sdk::masp::find_valid_diversifier; -use namada_sdk::wallet::{DecryptionError, FindKeyError, GenRestoreKeyError}; -use rand_core::OsRng; - -use crate::cli; -use crate::cli::{args, Context}; -use crate::wallet::{read_and_confirm_encryption_password, CliWalletUtils}; - -/// Find shielded address or key -pub fn address_key_find( - ctx: Context, - args::AddrKeyFind { - alias, - unsafe_show_secret, - }: args::AddrKeyFind, -) { - let mut wallet = ctx.wallet; - let alias = alias.to_lowercase(); - if let Ok(viewing_key) = wallet.find_viewing_key(&alias) { - // Check if alias is a viewing key - println!("Viewing key: {}", viewing_key); - if unsafe_show_secret { - // Check if alias is also a spending key - match wallet.find_spending_key(&alias, None) { - Ok(spending_key) => println!("Spending key: {}", spending_key), - Err(FindKeyError::KeyNotFound) => {} - Err(err) => eprintln!("{}", err), - } - } - } else if let Some(payment_addr) = wallet.find_payment_addr(&alias) { - // Failing that, check if alias is a payment address - println!("Payment address: {}", payment_addr); - } else { - // Otherwise alias cannot be referring to any shielded value - println!( - "No shielded address or key with alias {} found. Use the commands \ - `masp list-addrs` and `masp list-keys` to see all the known \ - addresses and keys.", - alias.to_lowercase() - ); - } -} - -/// List spending keys. -pub fn spending_keys_list( - ctx: Context, - args::MaspKeysList { - decrypt, - unsafe_show_secret, - }: args::MaspKeysList, -) { - let wallet = ctx.wallet; - let known_view_keys = wallet.get_viewing_keys(); - let known_spend_keys = wallet.get_spending_keys(); - if known_view_keys.is_empty() { - println!( - "No known keys. Try `masp add --alias my-addr --value ...` to add \ - a new key to the wallet." - ); - } else { - let stdout = std::io::stdout(); - let mut w = stdout.lock(); - writeln!(w, "Known keys:").unwrap(); - for (alias, key) in known_view_keys { - write!(w, " Alias \"{}\"", alias).unwrap(); - let spending_key_opt = known_spend_keys.get(&alias); - // If this alias is associated with a spending key, indicate whether - // or not the spending key is encrypted - // TODO: consider turning if let into match - if let Some(spending_key) = spending_key_opt { - if spending_key.is_encrypted() { - writeln!(w, " (encrypted):") - } else { - writeln!(w, " (not encrypted):") - } - .unwrap(); - } else { - writeln!(w, ":").unwrap(); - } - // Always print the corresponding viewing key - writeln!(w, " Viewing Key: {}", key).unwrap(); - // A subset of viewing keys will have corresponding spending keys. - // Print those too if they are available and requested. - if unsafe_show_secret { - if let Some(spending_key) = spending_key_opt { - match spending_key.get::(decrypt, None) { - // Here the spending key is unencrypted or successfully - // decrypted - Ok(spending_key) => { - writeln!(w, " Spending key: {}", spending_key) - .unwrap(); - } - // Here the key is encrypted but decryption has not been - // requested - Err(DecryptionError::NotDecrypting) if !decrypt => { - continue; - } - // Here the key is encrypted but incorrect password has - // been provided - Err(err) => { - writeln!( - w, - " Couldn't decrypt the spending key: {}", - err - ) - .unwrap(); - } - } - } - } - } - } -} - -/// List payment addresses. -pub fn payment_addresses_list(ctx: Context) { - let wallet = ctx.wallet; - let known_addresses = wallet.get_payment_addrs(); - if known_addresses.is_empty() { - println!( - "No known payment addresses. Try `masp gen-addr --alias my-addr` \ - to generate a new payment address." - ); - } else { - let stdout = std::io::stdout(); - let mut w = stdout.lock(); - writeln!(w, "Known payment addresses:").unwrap(); - for (alias, address) in sorted(known_addresses) { - writeln!(w, " \"{}\": {}", alias, address).unwrap(); - } - } -} - -/// Generate a spending key. -pub fn spending_key_gen( - ctx: Context, - args::MaspSpendKeyGen { - alias, - alias_force, - unsafe_dont_encrypt, - }: args::MaspSpendKeyGen, -) { - let mut wallet = ctx.wallet; - let alias = alias.to_lowercase(); - let password = read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (alias, _key) = wallet.gen_spending_key(alias, password, alias_force); - crate::wallet::save(&wallet).unwrap_or_else(|err| eprintln!("{}", err)); - println!( - "Successfully added a spending key with alias: \"{}\"", - alias - ); -} - -/// Generate a shielded payment address from the given key. -pub fn payment_address_gen( - ctx: Context, - args::MaspPayAddrGen { - alias, - alias_force, - viewing_key, - pin, - }: args::MaspPayAddrGen, -) { - let alias = alias.to_lowercase(); - let viewing_key = ExtendedFullViewingKey::from(viewing_key).fvk.vk; - let (div, _g_d) = find_valid_diversifier(&mut OsRng); - let payment_addr = viewing_key - .to_payment_address(div) - .expect("a PaymentAddress"); - let mut wallet = ctx.wallet; - let alias = wallet - .insert_payment_addr( - alias, - PaymentAddress::from(payment_addr).pinned(pin), - alias_force, - ) - .unwrap_or_else(|| { - eprintln!("Payment address not added"); - cli::safe_exit(1); - }); - crate::wallet::save(&wallet).unwrap_or_else(|err| eprintln!("{}", err)); - println!( - "Successfully generated a payment address with the following alias: {}", - alias, - ); -} - -/// Add a viewing key, spending key, or payment address to wallet. -pub fn address_key_add( - mut ctx: Context, - args::MaspAddrKeyAdd { - alias, - alias_force, - value, - unsafe_dont_encrypt, - }: args::MaspAddrKeyAdd, -) { - let alias = alias.to_lowercase(); - let (alias, typ) = match value { - MaspValue::FullViewingKey(viewing_key) => { - let alias = ctx - .wallet - .insert_viewing_key(alias, viewing_key, alias_force) - .unwrap_or_else(|| { - eprintln!("Viewing key not added"); - cli::safe_exit(1); - }); - (alias, "viewing key") - } - MaspValue::ExtendedSpendingKey(spending_key) => { - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let alias = ctx - .wallet - .encrypt_insert_spending_key( - alias, - spending_key, - password, - alias_force, - ) - .unwrap_or_else(|| { - eprintln!("Spending key not added"); - cli::safe_exit(1); - }); - (alias, "spending key") - } - MaspValue::PaymentAddress(payment_addr) => { - let alias = ctx - .wallet - .insert_payment_addr(alias, payment_addr, alias_force) - .unwrap_or_else(|| { - eprintln!("Payment address not added"); - cli::safe_exit(1); - }); - (alias, "payment address") - } - }; - crate::wallet::save(&ctx.wallet).unwrap_or_else(|err| eprintln!("{}", err)); - println!( - "Successfully added a {} with the following alias to wallet: {}", - typ, alias, - ); -} - -/// Restore a keypair and an implicit address from the mnemonic code in the -/// wallet. -pub fn key_and_address_restore( - ctx: Context, - args::KeyAndAddressRestore { - scheme, - alias, - alias_force, - unsafe_dont_encrypt, - derivation_path, - }: args::KeyAndAddressRestore, -) { - let mut wallet = ctx.wallet; - let encryption_password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let (alias, _key) = wallet - .derive_key_from_user_mnemonic_code( - scheme, - alias, - alias_force, - derivation_path, - None, - encryption_password, - ) - .unwrap_or_else(|err| { - eprintln!("{}", err); - cli::safe_exit(1) - }) - .unwrap_or_else(|| { - println!("No changes are persisted. Exiting."); - cli::safe_exit(0); - }); - crate::wallet::save(&wallet).unwrap_or_else(|err| eprintln!("{}", err)); - println!( - "Successfully added a key and an address with alias: \"{}\"", - alias - ); -} - -/// Generate a new keypair and derive implicit address from it and store them in -/// the wallet. -pub fn key_and_address_gen( - ctx: Context, - args::KeyAndAddressGen { - scheme, - alias, - alias_force, - unsafe_dont_encrypt, - derivation_path, - }: args::KeyAndAddressGen, -) { - let mut wallet = ctx.wallet; - let encryption_password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - let mut rng = OsRng; - let derivation_path_and_mnemonic_rng = - derivation_path.map(|p| (p, &mut rng)); - let (alias, _key, _mnemonic) = wallet - .gen_key( - scheme, - alias, - alias_force, - None, - encryption_password, - derivation_path_and_mnemonic_rng, - ) - .unwrap_or_else(|err| match err { - GenRestoreKeyError::KeyStorageError => { - println!("No changes are persisted. Exiting."); - cli::safe_exit(0); - } - _ => { - eprintln!("{}", err); - cli::safe_exit(1); - } - }); - crate::wallet::save(&wallet).unwrap_or_else(|err| eprintln!("{}", err)); - println!( - "Successfully added a key and an address with alias: \"{}\"", - alias - ); -} - -/// Find a keypair in the wallet store. -pub fn key_find( - ctx: Context, - args::KeyFind { - public_key, - alias, - value, - unsafe_show_secret, - }: args::KeyFind, -) { - let mut wallet = ctx.wallet; - let found_keypair = match public_key { - Some(pk) => wallet.find_key_by_pk(&pk, None), - None => { - let alias = alias.or(value); - match alias { - None => { - eprintln!( - "An alias, public key or public key hash needs to be \ - supplied" - ); - cli::safe_exit(1) - } - Some(alias) => wallet.find_key(alias.to_lowercase(), None), - } - } - }; - match found_keypair { - Ok(keypair) => { - let pkh: PublicKeyHash = (&keypair.ref_to()).into(); - println!("Public key hash: {}", pkh); - println!("Public key: {}", keypair.ref_to()); - if unsafe_show_secret { - println!("Secret key: {}", keypair); - } - } - Err(err) => { - eprintln!("{}", err); - } - } -} - -/// List all known keys. -pub fn key_list( - ctx: Context, - args::KeyList { - decrypt, - unsafe_show_secret, - }: args::KeyList, -) { - let wallet = ctx.wallet; - let known_keys = wallet.get_keys(); - if known_keys.is_empty() { - println!( - "No known keys. Try `key gen --alias my-key` to generate a new \ - key." - ); - } else { - let stdout = io::stdout(); - let mut w = stdout.lock(); - writeln!(w, "Known keys:").unwrap(); - for (alias, (stored_keypair, pkh)) in known_keys { - let encrypted = if stored_keypair.is_encrypted() { - "encrypted" - } else { - "not encrypted" - }; - writeln!(w, " Alias \"{}\" ({}):", alias, encrypted).unwrap(); - if let Some(pkh) = pkh { - writeln!(w, " Public key hash: {}", pkh).unwrap(); - } - match stored_keypair.get::(decrypt, None) { - Ok(keypair) => { - writeln!(w, " Public key: {}", keypair.ref_to()) - .unwrap(); - if unsafe_show_secret { - writeln!(w, " Secret key: {}", keypair).unwrap(); - } - } - Err(DecryptionError::NotDecrypting) if !decrypt => { - continue; - } - Err(err) => { - writeln!(w, " Couldn't decrypt the keypair: {}", err) - .unwrap(); - } - } - } - } -} - -/// Export a keypair to a file. -pub fn key_export(ctx: Context, args::KeyExport { alias }: args::KeyExport) { - let mut wallet = ctx.wallet; - wallet - .find_key(alias.to_lowercase(), None) - .map(|keypair| { - let file_data = keypair.serialize_to_vec(); - let file_name = format!("key_{}", alias.to_lowercase()); - let mut file = File::create(&file_name).unwrap(); - - file.write_all(file_data.as_ref()).unwrap(); - println!("Exported to file {}", file_name); - }) - .unwrap_or_else(|err| { - eprintln!("{}", err); - cli::safe_exit(1) - }) -} - -/// List all known addresses. -pub fn address_list(ctx: Context) { - let wallet = ctx.wallet; - let known_addresses = wallet.get_addresses(); - if known_addresses.is_empty() { - println!( - "No known addresses. Try `address gen --alias my-addr` to \ - generate a new implicit address." - ); - } else { - let stdout = io::stdout(); - let mut w = stdout.lock(); - writeln!(w, "Known addresses:").unwrap(); - for (alias, address) in sorted(known_addresses) { - writeln!(w, " \"{}\": {}", alias, address.to_pretty_string()) - .unwrap(); - } - } -} - -/// Find address (alias) by its alias (address). -pub fn address_or_alias_find(ctx: Context, args: args::AddressOrAliasFind) { - let wallet = ctx.wallet; - if args.address.is_some() && args.alias.is_some() { - panic!( - "This should not be happening: clap should emit its own error \ - message." - ); - } else if args.alias.is_some() { - if let Some(address) = wallet.find_address(args.alias.as_ref().unwrap()) - { - println!("Found address {}", address.to_pretty_string()); - } else { - println!( - "No address with alias {} found. Use the command `address \ - list` to see all the known addresses.", - args.alias.unwrap().to_lowercase() - ); - } - } else if args.address.is_some() { - if let Some(alias) = wallet.find_alias(args.address.as_ref().unwrap()) { - println!("Found alias {}", alias); - } else { - println!( - "No alias with address {} found. Use the command `address \ - list` to see all the known addresses.", - args.address.unwrap() - ); - } - } -} - -/// Add an address to the wallet. -pub fn address_add(ctx: Context, args: args::AddressAdd) { - let mut wallet = ctx.wallet; - if wallet - .add_address( - args.alias.clone().to_lowercase(), - args.address, - args.alias_force, - ) - .is_none() - { - eprintln!("Address not added"); - cli::safe_exit(1); - } - crate::wallet::save(&wallet).unwrap_or_else(|err| eprintln!("{}", err)); - println!( - "Successfully added a key and an address with alias: \"{}\"", - args.alias.to_lowercase() - ); -} diff --git a/apps/src/lib/wallet/defaults.rs b/apps/src/lib/wallet/defaults.rs index 82a9524daa..cbb027b8f8 100644 --- a/apps/src/lib/wallet/defaults.rs +++ b/apps/src/lib/wallet/defaults.rs @@ -1,78 +1,14 @@ //! Default addresses and keys. -#[cfg(any(test, feature = "dev"))] +#[cfg(any(test, feature = "testing", feature = "benches"))] pub use dev::{ addresses, albert_address, albert_keypair, bertha_address, bertha_keypair, christel_address, christel_keypair, daewon_address, daewon_keypair, ester_address, ester_keypair, keys, validator_address, validator_keypair, validator_keys, }; -use namada::core::ledger::eth_bridge::storage::bridge_pool::BRIDGE_POOL_ADDRESS; -use namada::ledger::{governance, pgf, pos}; -use namada::types::address::Address; -use namada::types::key::*; -use namada_sdk::wallet::alias::Alias; -use crate::config::genesis::genesis_config::GenesisConfig; - -/// The default addresses with their aliases. -pub fn addresses_from_genesis(genesis: GenesisConfig) -> Vec<(Alias, Address)> { - // Internal addresses - let mut addresses: Vec<(Alias, Address)> = vec![ - ("pos".into(), pos::ADDRESS), - ("pos_slash_pool".into(), pos::SLASH_POOL_ADDRESS), - ("governance".into(), governance::ADDRESS), - ("eth_bridge".into(), namada_sdk::eth_bridge::ADDRESS), - ("bridge_pool".into(), BRIDGE_POOL_ADDRESS), - ("pgf".into(), pgf::ADDRESS), - ]; - // Genesis validators - let validator_addresses = - genesis.validator.into_iter().map(|(alias, validator)| { - // The address must be set in the genesis config file - ( - alias.into(), - Address::decode(validator.address.unwrap()).unwrap(), - ) - }); - addresses.extend(validator_addresses); - // Genesis tokens - let token_addresses = genesis.token.into_iter().map(|(alias, token)| { - // The address must be set in the genesis config file - ( - alias.into(), - Address::decode(token.address.unwrap()).unwrap(), - ) - }); - addresses.extend(token_addresses); - // Genesis established accounts - if let Some(accounts) = genesis.established { - let est_addresses = accounts.into_iter().map(|(alias, established)| { - // The address must be set in the genesis config file - ( - alias.into(), - Address::decode(established.address.unwrap()).unwrap(), - ) - }); - addresses.extend(est_addresses); - } - // Genesis implicit accounts - if let Some(accounts) = genesis.implicit { - let imp_addresses = - accounts.into_iter().filter_map(|(alias, implicit)| { - // The public key may not be revealed, only add it if it is - implicit.public_key.map(|pk| { - let pk: common::PublicKey = pk.to_public_key().unwrap(); - let addr: Address = (&pk).into(); - (alias.into(), addr) - }) - }); - addresses.extend(imp_addresses); - } - addresses -} - -#[cfg(any(test, feature = "dev"))] +#[cfg(any(test, feature = "testing", feature = "benches"))] mod dev { use std::collections::HashMap; @@ -85,27 +21,30 @@ mod dev { use namada::types::key::*; use namada_sdk::wallet::alias::Alias; - /// Generate a new protocol signing keypair, eth hot key and DKG session - /// keypair + /// N.B. these are the corresponding values from + /// `genesis/pre-genesis/validator-0/validator-wallet.toml`. + /// + /// If that wallet is regenerated, these values must be changed to fix unit + /// tests. pub fn validator_keys() -> (common::SecretKey, common::SecretKey, DkgKeypair) { // ed25519 bytes let bytes: [u8; 33] = [ - 0, 200, 107, 23, 252, 78, 80, 8, 164, 142, 3, 194, 33, 12, 250, - 169, 211, 127, 47, 13, 194, 54, 199, 81, 102, 246, 189, 119, 144, - 25, 27, 113, 222, + 0, 217, 87, 83, 250, 179, 159, 135, 229, 194, 14, 202, 177, 38, + 144, 254, 250, 103, 233, 113, 100, 202, 111, 23, 214, 122, 235, + 165, 8, 131, 185, 61, 222, ]; // secp256k1 bytes let eth_bridge_key_bytes = [ - 1, 117, 93, 118, 129, 202, 67, 51, 62, 202, 196, 130, 244, 5, 44, - 88, 200, 121, 169, 11, 227, 79, 223, 74, 88, 49, 132, 213, 59, 64, - 20, 13, 82, + 1, 38, 59, 91, 81, 119, 89, 252, 48, 195, 171, 237, 19, 144, 123, + 117, 231, 121, 218, 231, 14, 54, 117, 19, 90, 120, 141, 231, 199, + 7, 110, 254, 191, ]; // DkgKeypair let dkg_bytes = [ - 32, 0, 0, 0, 210, 193, 55, 24, 92, 233, 23, 2, 73, 204, 221, 107, - 110, 222, 192, 136, 54, 24, 108, 236, 137, 27, 121, 142, 142, 7, - 193, 248, 155, 56, 51, 21, + 32, 0, 0, 0, 208, 36, 153, 32, 179, 193, 163, 222, 29, 238, 154, + 53, 181, 71, 213, 162, 59, 130, 225, 93, 57, 20, 161, 254, 52, 1, + 172, 184, 112, 189, 160, 102, ]; ( @@ -195,57 +134,57 @@ mod dev { Address::decode("atest1v4ehgw36ggcnsdee8qerswph8y6ry3p5xgunvve3xaqngd3kxc6nqwz9gseyydzzg5unys3ht2n48q").expect("The token address decoding shouldn't fail") } + /// N.B. this is the corresponding value from + /// `genesis/pre-genesis/wallet.toml`. pub fn albert_keypair() -> common::SecretKey { - // generated from - // [`namada::types::key::ed25519::gen_keypair`] let bytes = [ - 115, 191, 32, 247, 18, 101, 5, 106, 26, 203, 48, 145, 39, 41, 41, - 196, 252, 190, 245, 222, 96, 209, 34, 36, 40, 214, 169, 156, 235, - 78, 188, 33, + 131, 49, 140, 204, 234, 198, 192, 138, 1, 119, 102, 120, 64, 180, + 185, 63, 14, 69, 94, 69, 212, 195, 140, 40, 183, 59, 143, 132, 98, + 251, 245, 72, ]; let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); ed_sk.try_to_sk().unwrap() } + /// N.B. this is the corresponding value from + /// `genesis/pre-genesis/wallet.toml`. pub fn bertha_keypair() -> common::SecretKey { - // generated from - // [`namada::types::key::ed25519::gen_keypair`] let bytes = [ - 240, 3, 224, 69, 201, 148, 60, 53, 112, 79, 80, 107, 101, 127, 186, - 6, 176, 162, 113, 224, 62, 8, 183, 187, 124, 234, 244, 251, 92, 36, - 119, 243, + 115, 237, 97, 129, 119, 32, 210, 119, 132, 231, 169, 188, 164, 166, + 6, 104, 215, 99, 166, 247, 236, 172, 45, 69, 237, 31, 36, 26, 165, + 197, 158, 153, ]; let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); ed_sk.try_to_sk().unwrap() } + /// N.B. this is the corresponding value from + /// `genesis/pre-genesis/wallet.toml`. pub fn christel_keypair() -> common::SecretKey { - // generated from - // [`namada::types::key::ed25519::gen_keypair`] let bytes = [ - 65, 198, 96, 145, 237, 227, 84, 182, 107, 55, 209, 235, 115, 105, - 71, 190, 234, 137, 176, 188, 181, 174, 183, 49, 131, 230, 46, 39, - 70, 20, 130, 253, + 54, 37, 185, 57, 165, 142, 246, 4, 2, 215, 207, 143, 192, 66, 80, + 2, 108, 193, 186, 144, 204, 48, 40, 175, 28, 230, 178, 43, 232, 87, + 255, 199, ]; let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); ed_sk.try_to_sk().unwrap() } + /// N.B. this is the corresponding value from + /// `genesis/pre-genesis/wallet.toml`. pub fn daewon_keypair() -> common::SecretKey { - // generated from - // [`namada::types::key::ed25519::gen_keypair`] let bytes = [ - 235, 250, 15, 1, 145, 250, 172, 218, 247, 27, 63, 212, 60, 47, 164, - 57, 187, 156, 182, 144, 107, 174, 38, 81, 37, 40, 19, 142, 68, 135, - 57, 50, + 209, 158, 34, 108, 14, 125, 18, 61, 121, 245, 144, 139, 89, 72, + 212, 196, 97, 182, 106, 95, 138, 169, 86, 0, 194, 139, 85, 171, + 111, 93, 199, 114, ]; let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); ed_sk.try_to_sk().unwrap() } + /// N.B. this is the corresponding value from + /// `genesis/pre-genesis/wallet.toml`. pub fn ester_keypair() -> common::SecretKey { - // generated from - // [`namada::types::key::secp256k1::gen_keypair`] let bytes = [ 54, 144, 147, 226, 3, 93, 132, 247, 42, 126, 90, 23, 200, 155, 122, 147, 139, 93, 8, 204, 135, 178, 40, 152, 5, 227, 175, 204, 102, @@ -255,13 +194,15 @@ mod dev { sk.try_to_sk().unwrap() } + /// N.B. this is the consensus key from + /// `genesis/pre-genesis/validator-0/validator-wallet.toml`. + /// If that wallet is regenerated, this value must be changed to fix unit + /// tests. pub fn validator_keypair() -> common::SecretKey { - // generated from - // [`namada::types::key::ed25519::gen_keypair`] let bytes = [ - 80, 110, 166, 33, 135, 254, 34, 138, 253, 44, 214, 71, 50, 230, 39, - 246, 124, 201, 68, 138, 194, 251, 192, 36, 55, 160, 211, 68, 65, - 189, 121, 217, + 194, 41, 223, 103, 103, 178, 152, 145, 161, 212, 82, 133, 69, 13, + 133, 136, 238, 11, 198, 182, 29, 41, 75, 249, 88, 0, 28, 215, 217, + 63, 234, 78, ]; let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); ed_sk.try_to_sk().unwrap() diff --git a/apps/src/lib/wallet/mod.rs b/apps/src/lib/wallet/mod.rs index 18818daef5..779a137ccd 100644 --- a/apps/src/lib/wallet/mod.rs +++ b/apps/src/lib/wallet/mod.rs @@ -1,4 +1,3 @@ -pub mod cli_utils; pub mod defaults; pub mod pre_genesis; mod store; @@ -9,13 +8,12 @@ use std::str::FromStr; use std::{env, fs}; use namada::bip39::{Language, Mnemonic}; -use namada::types::address::Address; use namada::types::key::*; pub use namada_sdk::wallet::alias::Alias; use namada_sdk::wallet::fs::FsWalletStorage; use namada_sdk::wallet::store::Store; use namada_sdk::wallet::{ - AddressVpType, ConfirmationResponse, FindKeyError, GenRestoreKeyError, + ConfirmationResponse, FindKeyError, GenRestoreKeyError, Wallet, WalletIo, }; pub use namada_sdk::wallet::{ValidatorData, ValidatorKeys}; @@ -24,8 +22,6 @@ pub use store::wallet_file; use zeroize::Zeroizing; use crate::cli; -use crate::config::genesis::genesis_config::GenesisConfig; - #[derive(Debug, Clone)] pub struct CliWalletUtils { store_dir: PathBuf, @@ -239,23 +235,6 @@ where .transpose() } -/// Add addresses from a genesis configuration. -pub fn add_genesis_addresses( - wallet: &mut Wallet, - genesis: GenesisConfig, -) { - for (alias, config) in &genesis.token { - let exp_addr = format!("Genesis token {alias} must have address"); - let address = - Address::from_str(config.address.as_ref().expect(&exp_addr)) - .expect("Valid address"); - wallet.add_vp_type_to_address(AddressVpType::Token, address); - } - for (alias, addr) in defaults::addresses_from_genesis(genesis) { - wallet.add_address(alias.normalize(), addr, true); - } -} - /// Save the wallet store to a file. pub fn save(wallet: &Wallet) -> std::io::Result<()> { wallet @@ -285,20 +264,10 @@ pub fn load_or_new(store_dir: &Path) -> Wallet { wallet } -/// Load a wallet from the store file or create a new one with the default -/// addresses loaded from the genesis file, if not found. -pub fn load_or_new_from_genesis( - store_dir: &Path, - genesis_cfg: GenesisConfig, -) -> Wallet { - let store = self::store::load_or_new_from_genesis(store_dir, genesis_cfg) - .unwrap_or_else(|err| { - eprintln!("Unable to load the wallet: {}", err); - cli::safe_exit(1) - }); - let mut wallet = CliWalletUtils::new(store_dir.to_path_buf()); - *wallet.store_mut() = store; - wallet +/// Check if a wallet exists in the given store dir. +pub fn exists(store_dir: &Path) -> bool { + let file = wallet_file(store_dir); + file.exists() } /// Read the password for encryption from the file/env/stdin, with diff --git a/apps/src/lib/wallet/pre_genesis.rs b/apps/src/lib/wallet/pre_genesis.rs index da12c2dcce..6144c857af 100644 --- a/apps/src/lib/wallet/pre_genesis.rs +++ b/apps/src/lib/wallet/pre_genesis.rs @@ -14,7 +14,7 @@ use crate::wallet::store::gen_validator_keys; use crate::wallet::{read_and_confirm_encryption_password, CliWalletUtils}; /// Validator pre-genesis wallet file name -const VALIDATOR_FILE_NAME: &str = "wallet.toml"; +const VALIDATOR_FILE_NAME: &str = "validator-wallet.toml"; /// Get the path to the validator pre-genesis wallet store. pub fn validator_file_name(store_dir: impl AsRef) -> PathBuf { diff --git a/apps/src/lib/wallet/store.rs b/apps/src/lib/wallet/store.rs index 62eae8ac0e..dffaf48a98 100644 --- a/apps/src/lib/wallet/store.rs +++ b/apps/src/lib/wallet/store.rs @@ -1,20 +1,15 @@ use std::path::{Path, PathBuf}; -#[cfg(not(feature = "dev"))] use std::str::FromStr; use ark_std::rand::prelude::*; use ark_std::rand::SeedableRng; -#[cfg(not(feature = "dev"))] use namada::types::address::Address; use namada::types::key::*; use namada::types::transaction::EllipticCurve; -#[cfg(not(feature = "dev"))] use namada_sdk::wallet::store::AddressVpType; -#[cfg(feature = "dev")] use namada_sdk::wallet::StoredKeypair; use namada_sdk::wallet::{gen_sk_rng, LoadStoreError, Store, ValidatorKeys}; -use crate::config::genesis::genesis_config::GenesisConfig; use crate::wallet::CliWalletUtils; /// Wallet file name @@ -34,28 +29,6 @@ pub fn load_or_new(store_dir: &Path) -> Result { }) } -/// Load the store file or create a new one with the default addresses from -/// the genesis file, if not found. -pub fn load_or_new_from_genesis( - store_dir: &Path, - genesis_cfg: GenesisConfig, -) -> Result { - load(store_dir).or_else(|_| { - #[cfg(not(feature = "dev"))] - let store = new(genesis_cfg); - #[cfg(feature = "dev")] - let store = { - // The function is unused in dev - let _ = genesis_cfg; - new() - }; - let mut wallet = CliWalletUtils::new(store_dir.to_path_buf()); - *wallet.store_mut() = store; - wallet.save()?; - Ok(wallet.into()) - }) -} - /// Attempt to load the store file. pub fn load(store_dir: &Path) -> Result { let mut wallet = CliWalletUtils::new(store_dir.to_path_buf()); @@ -63,38 +36,6 @@ pub fn load(store_dir: &Path) -> Result { Ok(wallet.into()) } -/// Add addresses from a genesis configuration. -#[cfg(not(feature = "dev"))] -pub fn add_genesis_addresses(store: &mut Store, genesis: GenesisConfig) { - for (alias, addr) in - super::defaults::addresses_from_genesis(genesis.clone()) - { - store.insert_address::(alias, addr, true); - } - for (alias, token) in &genesis.token { - if let Some(address) = token.address.as_ref() { - match Address::from_str(address) { - Ok(address) => { - store.add_vp_type_to_address(AddressVpType::Token, address) - } - Err(_) => { - tracing::error!( - "Weird address for token {alias}: {address}" - ) - } - } - } - } -} - -#[cfg(not(feature = "dev"))] -fn new(genesis: GenesisConfig) -> Store { - let mut store = Store::default(); - add_genesis_addresses(&mut store, genesis); - store -} - -#[cfg(feature = "dev")] fn new() -> Store { let mut store = Store::default(); // Pre-load the default keys without encryption @@ -143,7 +84,7 @@ pub fn gen_validator_keys( } } -#[cfg(all(test, feature = "dev"))] +#[cfg(test)] mod test_wallet { use namada::types::address::Address; diff --git a/core/src/ledger/pgf/parameters.rs b/core/src/ledger/pgf/parameters.rs index 6319ec33d0..1d45ea79d2 100644 --- a/core/src/ledger/pgf/parameters.rs +++ b/core/src/ledger/pgf/parameters.rs @@ -1,6 +1,7 @@ use std::collections::BTreeSet; use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; use super::storage::keys as pgf_storage; use super::storage::steward::StewardDetail; @@ -18,6 +19,8 @@ use crate::types::dec::Dec; Hash, BorshSerialize, BorshDeserialize, + Serialize, + Deserialize, )] /// Pgf parameter structure pub struct PgfParameters { diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 830068220e..6b8d542d53 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -27,6 +27,8 @@ pub struct ConversionState { pub normed_inflation: Option, /// The tree currently containing all the conversions pub tree: FrozenCommitmentTree, + /// A map from token alias to actual address. + pub tokens: BTreeMap, /// Map assets to their latest conversion and position in Merkle tree #[allow(clippy::type_complexity)] pub assets: BTreeMap< @@ -215,7 +217,15 @@ where let key_prefix: storage::Key = masp_addr.to_db_key().into(); let tokens = address::tokens(); - let mut masp_reward_keys: Vec<_> = tokens.into_keys().collect(); + let mut masp_reward_keys: Vec<_> = tokens.into_keys() + .map(|k| wl_storage + .storage + .conversion_state + .tokens + .get(k) + .unwrap_or_else(|| panic!("Could not find token alias {} in MASP conversion state.", k)) + .clone() + ).collect(); // Put the native rewards first because other inflation computations depend // on it let native_token = wl_storage.storage.native_token.clone(); @@ -293,7 +303,7 @@ where "MASP reward for {} assumed to be 0 because the \ computed value is too large. Please check the \ inflation parameters.", - *addr + addr ); *normed_inflation }); @@ -339,7 +349,7 @@ where "MASP reward for {} assumed to be 0 because the \ computed value is too large. Please check the \ inflation parameters.", - *addr + addr ); 0u128 }); diff --git a/core/src/ledger/storage_api/pgf.rs b/core/src/ledger/storage_api/pgf.rs index b456cef0f9..91cc17b71e 100644 --- a/core/src/ledger/storage_api/pgf.rs +++ b/core/src/ledger/storage_api/pgf.rs @@ -90,10 +90,10 @@ where let pgf_inflation_rate: Dec = storage .read(&pgf_inflation_rate_key)? - .expect("Parameter should be definied."); + .expect("Parameter should be defined."); let stewards_inflation_rate: Dec = storage .read(&stewards_inflation_rate_key)? - .expect("Parameter should be definied."); + .expect("Parameter should be defined."); Ok(PgfParameters { pgf_inflation_rate, diff --git a/core/src/proto/mod.rs b/core/src/proto/mod.rs index 9dfe32e644..ecf8606008 100644 --- a/core/src/proto/mod.rs +++ b/core/src/proto/mod.rs @@ -4,8 +4,9 @@ pub mod generated; mod types; pub use types::{ - Code, Commitment, CompressedSignature, Data, Dkg, Error, Header, - MaspBuilder, Section, Signable, SignableEthMessage, Signature, + standalone_signature, verify_standalone_sig, Code, Commitment, + CompressedSignature, Data, Dkg, Error, Header, MaspBuilder, Section, + SerializeWithBorsh, Signable, SignableEthMessage, Signature, SignatureIndex, Signed, Signer, Tx, TxError, }; diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index 2702d4285e..b1d2b40cb5 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -246,6 +246,30 @@ impl> Signed { } } +/// Get a signature for data +pub fn standalone_signature>( + keypair: &common::SecretKey, + data: &T, +) -> common::Signature { + let to_sign = S::as_signable(data); + common::SigScheme::sign_with_hasher::(keypair, to_sign) +} + +/// Verify that the input data has been signed by the secret key +/// counterpart of the given public key. +pub fn verify_standalone_sig>( + data: &T, + pk: &common::PublicKey, + sig: &common::Signature, +) -> std::result::Result<(), VerifySigError> { + let signed_data = S::as_signable(data); + common::SigScheme::verify_signature_with_hasher::( + pk, + &signed_data, + sig, + ) +} + /// A section representing transaction data #[derive( Clone, diff --git a/core/src/types/address.rs b/core/src/types/address.rs index f7c63bb470..3493fc6d8f 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -7,31 +7,24 @@ use std::hash::Hash; use std::io::ErrorKind; use std::str::FromStr; -use bech32::{self, FromBase32, ToBase32, Variant}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use borsh_ext::BorshSerializeExt; use data_encoding::HEXUPPER; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use thiserror::Error; use crate::ibc::Signer; +use crate::impl_display_and_from_str_via_format; use crate::types::ethereum_events::EthAddress; -use crate::types::key; use crate::types::key::PublicKeyHash; use crate::types::token::Denomination; +use crate::types::{key, string_encoding}; /// The length of an established [`Address`] encoded with Borsh. pub const ESTABLISHED_ADDRESS_BYTES_LEN: usize = 21; /// The length of [`Address`] encoded with Bech32m. -pub const ADDRESS_LEN: usize = 79 + ADDRESS_HRP.len(); - -/// human-readable part of Bech32m encoded address -// TODO use "a" for live network -const ADDRESS_HRP: &str = "atest"; -/// We're using "Bech32m" variant -pub const BECH32M_VARIANT: bech32::Variant = Variant::Bech32m; +pub const ADDRESS_LEN: usize = 79 + string_encoding::hrp_len::
(); /// Length of a hash of an address as a hexadecimal string pub(crate) const HASH_HEX_LEN: usize = 40; @@ -89,6 +82,12 @@ mod internal { "ano::Pgf "; } +/// Error from decoding address from string +pub type DecodeError = string_encoding::DecodeError; + +/// Result of decoding address from string +pub type Result = std::result::Result; + /// Fixed-length address strings prefix for established addresses. const PREFIX_ESTABLISHED: &str = "est"; /// Fixed-length address strings prefix for implicit addresses. @@ -102,26 +101,6 @@ const PREFIX_ETH: &str = "eth"; /// Fixed-length address strings prefix for Non-Usable-Token addresses. const PREFIX_NUT: &str = "nut"; -#[allow(missing_docs)] -#[derive(Error, Debug, PartialEq, Eq, Clone)] -pub enum DecodeError { - #[error("Error decoding address from Bech32m: {0}")] - DecodeBech32(bech32::Error), - #[error("Error decoding address from base32: {0}")] - DecodeBase32(bech32::Error), - #[error("Unexpected Bech32m human-readable part {0}, expected {1}")] - UnexpectedBech32Prefix(String, String), - #[error("Unexpected Bech32m variant {0:?}, expected {BECH32M_VARIANT:?}")] - UnexpectedBech32Variant(bech32::Variant), - #[error("Invalid address encoding: {0}, {1}")] - InvalidInnerEncoding(ErrorKind, String), - #[error("Invalid address encoding")] - InvalidInnerEncodingStr(String), -} - -/// Result of a function that may fail -pub type Result = std::result::Result; - /// An account's address #[derive( Clone, BorshSerialize, BorshDeserialize, BorshSchema, PartialEq, Eq, Hash, @@ -153,33 +132,12 @@ impl Ord for Address { impl Address { /// Encode an address with Bech32m encoding pub fn encode(&self) -> String { - let bytes = self.to_fixed_len_string(); - bech32::encode(ADDRESS_HRP, bytes.to_base32(), BECH32M_VARIANT) - .unwrap_or_else(|_| { - panic!( - "The human-readable part {} should never cause a failure", - ADDRESS_HRP - ) - }) + string_encoding::Format::encode(self) } /// Decode an address from Bech32m encoding pub fn decode(string: impl AsRef) -> Result { - let (prefix, hash_base32, variant) = bech32::decode(string.as_ref()) - .map_err(DecodeError::DecodeBech32)?; - if prefix != ADDRESS_HRP { - return Err(DecodeError::UnexpectedBech32Prefix( - prefix, - ADDRESS_HRP.into(), - )); - } - match variant { - BECH32M_VARIANT => {} - _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), - } - let bytes: Vec = FromBase32::from_base32(&hash_base32) - .map_err(DecodeError::DecodeBase32)?; - Self::try_from_fixed_len_string(&mut &bytes[..]) + string_encoding::Format::decode(string) } /// Try to get a raw hash of an address, only defined for established and @@ -199,7 +157,7 @@ impl Address { } /// Convert an address to a fixed length 7-bit ascii string bytes - fn to_fixed_len_string(&self) -> Vec { + pub fn to_fixed_len_string(&self) -> Vec { let mut string = match self { Address::Established(EstablishedAddress { hash }) => { // The bech32m's data is a hex of the first 40 chars of the hash @@ -276,7 +234,7 @@ impl Address { let raw = HEXUPPER.decode(hash.as_bytes()).map_err(|e| { DecodeError::InvalidInnerEncoding( - std::io::ErrorKind::InvalidInput, + ErrorKind::InvalidInput, e.to_string(), ) })?; @@ -394,6 +352,20 @@ impl Address { } } +impl string_encoding::Format for Address { + const HRP: &'static str = string_encoding::ADDRESS_HRP; + + fn to_bytes(&self) -> Vec { + Self::to_fixed_len_string(self) + } + + fn decode_bytes(bytes: &[u8]) -> Result { + Self::try_from_fixed_len_string(&mut &bytes[..]) + } +} + +impl_display_and_from_str_via_format!(Address); + impl serde::Serialize for Address { fn serialize( &self, @@ -418,43 +390,18 @@ impl<'de> serde::Deserialize<'de> for Address { } } -impl Display for Address { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.encode()) - } -} - impl Debug for Address { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.pretty_fmt(f) } } -impl FromStr for Address { - type Err = DecodeError; - - fn from_str(s: &str) -> Result { - Address::decode(s) - } -} - /// for IBC signer impl TryFrom for Address { type Error = DecodeError; fn try_from(signer: Signer) -> Result { - // The given address should be an address or payment address. When - // sending a token from a spending key, it has been already - // replaced with the MASP address. - Address::decode(signer.as_ref()).or( - match crate::types::masp::PaymentAddress::from_str(signer.as_ref()) - { - Ok(_) => Ok(masp()), - Err(_) => Err(DecodeError::InvalidInnerEncodingStr(format!( - "Invalid address for IBC transfer: {signer}" - ))), - }, - ) + Address::decode(signer.as_ref()) } } @@ -478,7 +425,17 @@ pub struct EstablishedAddress { } /// A generator of established addresses -#[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] +#[derive( + Debug, + Default, + Clone, + PartialEq, + Eq, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, +)] pub struct EstablishedAddressGen { last_hash: [u8; SHA_HASH_LEN], } @@ -603,6 +560,20 @@ impl Display for InternalAddress { } } +impl InternalAddress { + /// Certain internal addresses have reserved aliases. + pub fn try_from_alias(alias: &str) -> Option { + match alias { + "pos" => Some(InternalAddress::PoS), + "ibc" => Some(InternalAddress::Ibc), + "ethbridge" => Some(InternalAddress::EthBridge), + "bridgepool" => Some(InternalAddress::EthBridgePool), + "governance" => Some(InternalAddress::Governance), + _ => None, + } + } +} + /// Temporary helper for testing pub fn nam() -> Address { Address::decode("atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5").expect("The token address decoding shouldn't fail") @@ -666,15 +637,15 @@ pub const fn wnam() -> EthAddress { /// Temporary helper for testing, a hash map of tokens addresses with their /// informal currency codes and number of decimal places. -pub fn tokens() -> HashMap { +pub fn tokens() -> HashMap<&'static str, Denomination> { vec![ - (nam(), ("NAM", 6.into())), - (btc(), ("BTC", 8.into())), - (eth(), ("ETH", 18.into())), - (dot(), ("DOT", 10.into())), - (schnitzel(), ("Schnitzel", 6.into())), - (apfel(), ("Apfel", 6.into())), - (kartoffel(), ("Kartoffel", 6.into())), + ("NAM", 6.into()), + ("BTC", 8.into()), + ("ETH", 18.into()), + ("DOT", 10.into()), + ("Schnitzel", 6.into()), + ("Apfel", 6.into()), + ("Kartoffel", 6.into()), ] .into_iter() .collect() diff --git a/core/src/types/chain.rs b/core/src/types/chain.rs index 43977d8812..47f2af8146 100644 --- a/core/src/types/chain.rs +++ b/core/src/types/chain.rs @@ -176,12 +176,7 @@ impl ProposalBytes { } } -/// Development default chain ID. Must be [`CHAIN_ID_LENGTH`] long. -#[cfg(feature = "dev")] -pub const DEFAULT_CHAIN_ID: &str = "namada-devchain.00000000000000"; - /// Release default chain ID. Must be [`CHAIN_ID_LENGTH`] long. -#[cfg(not(feature = "dev"))] pub const DEFAULT_CHAIN_ID: &str = "namada-internal.00000000000000"; /// Chain ID diff --git a/core/src/types/dec.rs b/core/src/types/dec.rs index 40494ffad0..406144a0df 100644 --- a/core/src/types/dec.rs +++ b/core/src/types/dec.rs @@ -170,6 +170,20 @@ impl Dec { pub fn is_negative(&self) -> bool { self.0.is_negative() } + + /// Return the integer value of a [`Dec`] by rounding up. + pub fn ceil(&self) -> I256 { + if self.0.is_negative() { + self.to_i256() + } else { + let floor = self.to_i256(); + if (*self - Dec(floor)).is_zero() { + floor + } else { + floor + I256::one() + } + } + } } impl FromStr for Dec { @@ -178,7 +192,7 @@ impl FromStr for Dec { fn from_str(s: &str) -> Result { let ((large, small), is_neg) = if let Some(strip) = s.strip_prefix('-') { - (strip.split_once('.').unwrap_or((s, "0")), true) + (strip.split_once('.').unwrap_or((strip, "0")), true) } else { (s.split_once('.').unwrap_or((s, "0")), false) }; @@ -277,6 +291,12 @@ impl From for Dec { } } +impl From for Dec { + fn from(num: i32) -> Self { + Self::from(num as i128) + } +} + impl TryFrom for Dec { type Error = Box; @@ -615,8 +635,6 @@ mod test_dec { assert!(Dec::from_str("0.").is_err()); // Test that multiple decimal points get caught assert!(Dec::from_str("1.2.3").is_err()); - // Test that negative numbers are rejected - assert!(Dec::from_str("-1").is_err()); // Test that non-numerics are caught assert!(Dec::from_str("DEADBEEF.12").is_err()); assert!(Dec::from_str("23.DEADBEEF").is_err()); @@ -644,6 +662,7 @@ mod test_dec { ); } + /// Test that ordering of [`Dec`] values using more than 64 bits works. #[test] fn test_ordering() { let smaller = Dec::from_str("6483947304.195066085701").unwrap(); @@ -651,6 +670,21 @@ mod test_dec { assert!(smaller < larger); } + /// Test that taking the ceiling of a [`Dec`] works. + #[test] + fn test_ceiling() { + let neg = Dec::from_str("-2.4").expect("Test failed"); + assert_eq!( + neg.ceil(), + Dec::from_str("-2").expect("Test failed").to_i256() + ); + let pos = Dec::from_str("2.4").expect("Test failed"); + assert_eq!( + pos.ceil(), + Dec::from_str("3").expect("Test failed").to_i256() + ); + } + #[test] fn test_dec_display() { let num = Dec::from_str("14000.0000").unwrap(); diff --git a/core/src/types/key/common.rs b/core/src/types/key/common.rs index 9ca0bdaffc..33f300d884 100644 --- a/core/src/types/key/common.rs +++ b/core/src/types/key/common.rs @@ -17,8 +17,10 @@ use super::{ ParseSignatureError, RefTo, SchemeType, SigScheme as SigSchemeTrait, VerifySigError, }; +use crate::impl_display_and_from_str_via_format; use crate::types::ethereum_events::EthAddress; use crate::types::key::{SignableBytes, StorageHasher}; +use crate::types::string_encoding; /// Public key #[derive( @@ -71,24 +73,26 @@ impl super::PublicKey for PublicKey { } } -impl Display for PublicKey { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", HEXLOWER.encode(&self.serialize_to_vec())) - } -} +/// String decoding error +pub type DecodeError = string_encoding::DecodeError; -impl FromStr for PublicKey { - type Err = ParsePublicKeyError; +impl string_encoding::Format for PublicKey { + const HRP: &'static str = string_encoding::COMMON_PK_HRP; - fn from_str(str: &str) -> Result { - let vec = HEXLOWER - .decode(str.as_ref()) - .map_err(ParsePublicKeyError::InvalidHex)?; - Self::try_from_slice(vec.as_slice()) - .map_err(ParsePublicKeyError::InvalidEncoding) + fn to_bytes(&self) -> Vec { + self.serialize_to_vec() + } + + fn decode_bytes( + bytes: &[u8], + ) -> Result { + BorshDeserialize::try_from_slice(bytes) + .map_err(DecodeError::InvalidBytes) } } +impl_display_and_from_str_via_format!(PublicKey); + #[allow(missing_docs)] #[derive(Error, Debug)] pub enum EthAddressConvError { @@ -246,6 +250,23 @@ pub enum Signature { Secp256k1(secp256k1::Signature), } +impl string_encoding::Format for Signature { + const HRP: &'static str = string_encoding::COMMON_SIG_HRP; + + fn to_bytes(&self) -> Vec { + self.serialize_to_vec() + } + + fn decode_bytes( + bytes: &[u8], + ) -> Result { + BorshDeserialize::try_from_slice(bytes) + .map_err(DecodeError::InvalidBytes) + } +} + +impl_display_and_from_str_via_format!(Signature); + impl From for Signature { fn from(sig: ed25519::Signature) -> Self { Signature::Ed25519(sig) @@ -370,3 +391,22 @@ impl super::SigScheme for SigScheme { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::key::ed25519; + + /// Run `cargo test gen_ed25519_keypair -- --nocapture` to generate a + /// new ed25519 keypair wrapped in `common` key types. + #[test] + fn gen_ed25519_keypair() { + let secret_key = + SecretKey::Ed25519(crate::types::key::testing::gen_keypair::< + ed25519::SigScheme, + >()); + let public_key = secret_key.to_public(); + println!("Public key: {}", public_key); + println!("Secret key: {}", secret_key); + } +} diff --git a/core/src/types/key/dkg_session_keys.rs b/core/src/types/key/dkg_session_keys.rs index ccca82aeba..07177f5ec6 100644 --- a/core/src/types/key/dkg_session_keys.rs +++ b/core/src/types/key/dkg_session_keys.rs @@ -2,19 +2,17 @@ use std::cmp::Ordering; use std::collections::BTreeMap; -use std::fmt::Display; use std::io::{Error, ErrorKind, Read}; -use std::str::FromStr; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use borsh_ext::BorshSerializeExt; -use data_encoding::HEXLOWER; use serde::{Deserialize, Serialize}; +use crate::impl_display_and_from_str_via_format; use crate::types::address::Address; -use crate::types::key::ParsePublicKeyError; use crate::types::storage::{DbKeySeg, Key, KeySeg}; +use crate::types::string_encoding; use crate::types::transaction::EllipticCurve; /// A keypair used in the DKG protocol @@ -140,25 +138,23 @@ impl BorshSchema for DkgPublicKey { } } -impl Display for DkgPublicKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let vec = self.serialize_to_vec(); - write!(f, "{}", HEXLOWER.encode(&vec)) - } -} +impl string_encoding::Format for DkgPublicKey { + const HRP: &'static str = string_encoding::DKG_PK_HRP; -impl FromStr for DkgPublicKey { - type Err = ParsePublicKeyError; + fn to_bytes(&self) -> Vec { + self.serialize_to_vec() + } - fn from_str(s: &str) -> Result { - let vec = HEXLOWER - .decode(s.as_ref()) - .map_err(ParsePublicKeyError::InvalidHex)?; - BorshDeserialize::try_from_slice(&vec) - .map_err(ParsePublicKeyError::InvalidEncoding) + fn decode_bytes( + bytes: &[u8], + ) -> Result { + BorshDeserialize::try_from_slice(bytes) + .map_err(string_encoding::DecodeError::InvalidBytes) } } +impl_display_and_from_str_via_format!(DkgPublicKey); + /// Obtain a storage key for user's public dkg session key. pub fn dkg_pk_key(owner: &Address) -> Key { Key::from(owner.to_db_key()) diff --git a/core/src/types/key/mod.rs b/core/src/types/key/mod.rs index f6d226e2f4..52af46d5dc 100644 --- a/core/src/types/key/mod.rs +++ b/core/src/types/key/mod.rs @@ -238,7 +238,6 @@ pub trait PublicKey: + Display + Debug + PartialOrd - + FromStr + Hash + Send + Sync diff --git a/core/src/types/masp.rs b/core/src/types/masp.rs index 9083852a81..a7cc6be9f7 100644 --- a/core/src/types/masp.rs +++ b/core/src/types/masp.rs @@ -1,7 +1,7 @@ //! MASP types use std::fmt::Display; -use std::io::ErrorKind; +use std::io::{Error, ErrorKind}; use std::str::FromStr; use bech32::{FromBase32, ToBase32}; @@ -9,17 +9,14 @@ use borsh::{BorshDeserialize, BorshSerialize}; use borsh_ext::BorshSerializeExt; use sha2::{Digest, Sha256}; -use crate::types::address::{ - masp, Address, DecodeError, BECH32M_VARIANT, HASH_HEX_LEN, +use crate::impl_display_and_from_str_via_format; +use crate::types::address::{masp, Address, DecodeError, HASH_HEX_LEN}; +use crate::types::string_encoding::{ + self, BECH32M_VARIANT, MASP_EXT_FULL_VIEWING_KEY_HRP, + MASP_EXT_SPENDING_KEY_HRP, MASP_PAYMENT_ADDRESS_HRP, + MASP_PINNED_PAYMENT_ADDRESS_HRP, }; -/// human-readable part of Bech32m encoded address -// TODO remove "test" suffix for live network -const EXT_FULL_VIEWING_KEY_HRP: &str = "xfvktest"; -const PAYMENT_ADDRESS_HRP: &str = "patest"; -const PINNED_PAYMENT_ADDRESS_HRP: &str = "ppatest"; -const EXT_SPENDING_KEY_HRP: &str = "xsktest"; - /// Wrapper for masp_primitive's FullViewingKey #[derive( Clone, @@ -35,51 +32,103 @@ const EXT_SPENDING_KEY_HRP: &str = "xsktest"; )] pub struct ExtendedViewingKey(masp_primitives::zip32::ExtendedFullViewingKey); -impl Display for ExtendedViewingKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl ExtendedViewingKey { + /// Encode `Self` to bytes + pub fn to_bytes(&self) -> Vec { let mut bytes = [0; 169]; self.0 .write(&mut bytes[..]) .expect("should be able to serialize an ExtendedFullViewingKey"); - let encoded = bech32::encode( - EXT_FULL_VIEWING_KEY_HRP, - bytes.to_base32(), - BECH32M_VARIANT, + bytes.to_vec() + } + + /// Try to decode `Self` from bytes + pub fn decode_bytes(bytes: &[u8]) -> Result { + masp_primitives::zip32::ExtendedFullViewingKey::read(&mut &bytes[..]) + .map(Self) + } +} + +impl string_encoding::Format for ExtendedViewingKey { + const HRP: &'static str = MASP_EXT_FULL_VIEWING_KEY_HRP; + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn decode_bytes( + bytes: &[u8], + ) -> Result { + Self::decode_bytes(bytes).map_err(DecodeError::InvalidBytes) + } +} + +impl_display_and_from_str_via_format!(ExtendedViewingKey); + +impl string_encoding::Format for PaymentAddress { + const HRP: &'static str = MASP_PAYMENT_ADDRESS_HRP; + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn decode_bytes( + _bytes: &[u8], + ) -> Result { + unimplemented!( + "Cannot determine if the PaymentAddress is pinned from bytes. Use \ + `PaymentAddress::decode_bytes(bytes, is_pinned)` instead." ) - .unwrap_or_else(|_| { + } + + // We override `encode` because we need to determine whether the address + // is pinned from its HRP + fn encode(&self) -> String { + let hrp = if self.is_pinned() { + MASP_PINNED_PAYMENT_ADDRESS_HRP + } else { + MASP_PAYMENT_ADDRESS_HRP + }; + let base32 = self.to_bytes().to_base32(); + bech32::encode(hrp, base32, BECH32M_VARIANT).unwrap_or_else(|_| { panic!( "The human-readable part {} should never cause a failure", - EXT_FULL_VIEWING_KEY_HRP + hrp ) - }); - write!(f, "{encoded}") + }) } -} - -impl FromStr for ExtendedViewingKey { - type Err = DecodeError; - fn from_str(string: &str) -> Result { - let (prefix, base32, variant) = - bech32::decode(string).map_err(DecodeError::DecodeBech32)?; - if prefix != EXT_FULL_VIEWING_KEY_HRP { - return Err(DecodeError::UnexpectedBech32Prefix( + // We override `decode` because we need to use different HRP for pinned and + // non-pinned address + fn decode( + string: impl AsRef, + ) -> Result { + let (prefix, base32, variant) = bech32::decode(string.as_ref()) + .map_err(DecodeError::DecodeBech32)?; + let is_pinned = if prefix == MASP_PAYMENT_ADDRESS_HRP { + false + } else if prefix == MASP_PINNED_PAYMENT_ADDRESS_HRP { + true + } else { + return Err(DecodeError::UnexpectedBech32Hrp( prefix, - EXT_FULL_VIEWING_KEY_HRP.into(), + MASP_PAYMENT_ADDRESS_HRP.into(), )); - } + }; match variant { BECH32M_VARIANT => {} _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), } let bytes: Vec = FromBase32::from_base32(&base32) .map_err(DecodeError::DecodeBase32)?; - masp_primitives::zip32::ExtendedFullViewingKey::read(&mut &bytes[..]) - .map_err(|op| DecodeError::InvalidInnerEncodingStr(op.to_string())) - .map(Self) + + PaymentAddress::decode_bytes(&bytes, is_pinned) + .map_err(DecodeError::InvalidBytes) } } +impl_display_and_from_str_via_format!(PaymentAddress); + impl From for masp_primitives::zip32::ExtendedFullViewingKey { @@ -154,78 +203,45 @@ impl PaymentAddress { // hex of the first 40 chars of the hash format!("{:.width$X}", hasher.finalize(), width = HASH_HEX_LEN) } -} -impl From for masp_primitives::sapling::PaymentAddress { - fn from(addr: PaymentAddress) -> Self { - addr.0 - } -} - -impl From for PaymentAddress { - fn from(addr: masp_primitives::sapling::PaymentAddress) -> Self { - Self(addr, false) - } -} - -impl Display for PaymentAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let bytes = self.0.to_bytes(); - let hrp = if self.1 { - PINNED_PAYMENT_ADDRESS_HRP - } else { - PAYMENT_ADDRESS_HRP - }; - let encoded = bech32::encode(hrp, bytes.to_base32(), BECH32M_VARIANT) - .unwrap_or_else(|_| { - panic!( - "The human-readable part {} should never cause a failure", - PAYMENT_ADDRESS_HRP - ) - }); - write!(f, "{encoded}") + /// Encode `Self` to bytes + pub fn to_bytes(&self) -> Vec { + self.0.to_bytes().to_vec() } -} -impl FromStr for PaymentAddress { - type Err = DecodeError; - - fn from_str(string: &str) -> Result { - let (prefix, base32, variant) = - bech32::decode(string).map_err(DecodeError::DecodeBech32)?; - let pinned = if prefix == PAYMENT_ADDRESS_HRP { - false - } else if prefix == PINNED_PAYMENT_ADDRESS_HRP { - true - } else { - return Err(DecodeError::UnexpectedBech32Prefix( - prefix, - PAYMENT_ADDRESS_HRP.into(), - )); - }; - match variant { - BECH32M_VARIANT => {} - _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), - } + /// Try to decode `Self` from bytes + pub fn decode_bytes( + bytes: &[u8], + is_pinned: bool, + ) -> Result { let addr_len_err = |_| { - DecodeError::InvalidInnerEncoding( + Error::new( ErrorKind::InvalidData, - "expected 43 bytes for the payment address".to_string(), + "expected 43 bytes for the payment address", ) }; let addr_data_err = || { - DecodeError::InvalidInnerEncoding( + Error::new( ErrorKind::InvalidData, - "invalid payment address provided".to_string(), + "invalid payment address provided", ) }; - let bytes: Vec = FromBase32::from_base32(&base32) - .map_err(DecodeError::DecodeBase32)?; - masp_primitives::sapling::PaymentAddress::from_bytes( - &bytes.try_into().map_err(addr_len_err)?, - ) - .ok_or_else(addr_data_err) - .map(|x| Self(x, pinned)) + let bytes: &[u8; 43] = &bytes.try_into().map_err(addr_len_err)?; + masp_primitives::sapling::PaymentAddress::from_bytes(bytes) + .ok_or_else(addr_data_err) + .map(|addr| Self(addr, is_pinned)) + } +} + +impl From for masp_primitives::sapling::PaymentAddress { + fn from(addr: PaymentAddress) -> Self { + addr.0 + } +} + +impl From for PaymentAddress { + fn from(addr: masp_primitives::sapling::PaymentAddress) -> Self { + Self(addr, false) } } @@ -257,51 +273,28 @@ impl<'de> serde::Deserialize<'de> for PaymentAddress { #[derive(Clone, Debug, Copy, BorshSerialize, BorshDeserialize)] pub struct ExtendedSpendingKey(masp_primitives::zip32::ExtendedSpendingKey); -impl Display for ExtendedSpendingKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl string_encoding::Format for ExtendedSpendingKey { + const HRP: &'static str = MASP_EXT_SPENDING_KEY_HRP; + + fn to_bytes(&self) -> Vec { let mut bytes = [0; 169]; self.0 .write(&mut &mut bytes[..]) .expect("should be able to serialize an ExtendedSpendingKey"); - let encoded = bech32::encode( - EXT_SPENDING_KEY_HRP, - bytes.to_base32(), - BECH32M_VARIANT, - ) - .unwrap_or_else(|_| { - panic!( - "The human-readable part {} should never cause a failure", - EXT_SPENDING_KEY_HRP - ) - }); - write!(f, "{encoded}") + bytes.to_vec() } -} -impl FromStr for ExtendedSpendingKey { - type Err = DecodeError; - - fn from_str(string: &str) -> Result { - let (prefix, base32, variant) = - bech32::decode(string).map_err(DecodeError::DecodeBech32)?; - if prefix != EXT_SPENDING_KEY_HRP { - return Err(DecodeError::UnexpectedBech32Prefix( - prefix, - EXT_SPENDING_KEY_HRP.into(), - )); - } - match variant { - BECH32M_VARIANT => {} - _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), - } - let bytes: Vec = FromBase32::from_base32(&base32) - .map_err(DecodeError::DecodeBase32)?; + fn decode_bytes( + bytes: &[u8], + ) -> Result { masp_primitives::zip32::ExtendedSpendingKey::read(&mut &bytes[..]) .map_err(|op| DecodeError::InvalidInnerEncodingStr(op.to_string())) .map(Self) } } +impl_display_and_from_str_via_format!(ExtendedSpendingKey); + impl From for masp_primitives::zip32::ExtendedSpendingKey { fn from(key: ExtendedSpendingKey) -> Self { key.0 @@ -407,7 +400,7 @@ impl TransferTarget { /// Get the contained PaymentAddress, if any pub fn payment_address(&self) -> Option { match self { - Self::PaymentAddress(x) => Some(*x), + Self::PaymentAddress(address) => Some(*address), _ => None, } } @@ -425,7 +418,7 @@ impl Display for TransferTarget { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Address(x) => x.fmt(f), - Self::PaymentAddress(x) => x.fmt(f), + Self::PaymentAddress(address) => address.fmt(f), } } } diff --git a/core/src/types/mod.rs b/core/src/types/mod.rs index 8aee038d9b..904e005f34 100644 --- a/core/src/types/mod.rs +++ b/core/src/types/mod.rs @@ -15,6 +15,7 @@ pub mod keccak; pub mod key; pub mod masp; pub mod storage; +pub mod string_encoding; pub mod time; pub mod token; pub mod transaction; diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index a21de88cbe..5dba69ed36 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -30,7 +30,7 @@ use crate::types::time::DateTimeUtc; pub const IBC_KEY_LIMIT: usize = 240; #[allow(missing_docs)] -#[derive(Error, Debug, Clone)] +#[derive(Error, Debug)] pub enum Error { #[error("Error parsing address: {0}")] ParseAddress(address::DecodeError), diff --git a/core/src/types/string_encoding.rs b/core/src/types/string_encoding.rs new file mode 100644 index 0000000000..9cebaa8451 --- /dev/null +++ b/core/src/types/string_encoding.rs @@ -0,0 +1,231 @@ +//! Namada's standard string encoding for public types. +//! +//! We're using [bech32m](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki), +//! a format with a human-readable, followed by base32 encoding with a limited +//! character set with checksum check. +//! +//! To use this encoding for a new type, add a HRP (human-readable part) const +//! below and use it to `impl string_encoding::Format for YourType`. + +use std::fmt::Display; +use std::io::ErrorKind; +use std::ops::Deref; +use std::str::FromStr; + +use bech32::{self, FromBase32, ToBase32, Variant}; +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// We're using "Bech32m" variant +pub const BECH32M_VARIANT: bech32::Variant = Variant::Bech32m; + +// Human-readable parts of Bech32m encoding +// +// Invariant: HRPs must be unique !!! +// +// TODO: remove "test" suffix for live network +/// `Address` human-readable part +pub const ADDRESS_HRP: &str = "atest"; +/// MASP extended viewing key human-readable part +pub const MASP_EXT_FULL_VIEWING_KEY_HRP: &str = "xfvktest"; +/// MASP payment address (not pinned) human-readable part +pub const MASP_PAYMENT_ADDRESS_HRP: &str = "patest"; +/// MASP pinned payment address human-readable part +pub const MASP_PINNED_PAYMENT_ADDRESS_HRP: &str = "ppatest"; +/// MASP extended spending key human-readable part +pub const MASP_EXT_SPENDING_KEY_HRP: &str = "xsktest"; +/// `common::PublicKey` human-readable part +pub const COMMON_PK_HRP: &str = "pktest"; +/// `DkgPublicKey` human-readable part +pub const DKG_PK_HRP: &str = "dpktest"; +/// `common::Signature` human-readable part +pub const COMMON_SIG_HRP: &str = "sigtest"; + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum DecodeError { + #[error("Error decoding from Bech32m: {0}")] + DecodeBech32(bech32::Error), + #[error("Error decoding from base32: {0}")] + DecodeBase32(bech32::Error), + #[error("Unexpected Bech32m human-readable part {0}, expected {1}")] + UnexpectedBech32Hrp(String, String), + #[error("Unexpected Bech32m variant {0:?}, expected {BECH32M_VARIANT:?}")] + UnexpectedBech32Variant(bech32::Variant), + #[error("Invalid address encoding: {0}, {1}")] + InvalidInnerEncoding(ErrorKind, String), + #[error("Invalid address encoding")] + InvalidInnerEncodingStr(String), + #[error("Invalid bytes: {0}")] + InvalidBytes(std::io::Error), +} + +/// Format to string with bech32m +pub trait Format: Sized { + /// Human-readable part + const HRP: &'static str; + + /// Encode `Self` to a string + fn encode(&self) -> String { + let base32 = self.to_bytes().to_base32(); + bech32::encode(Self::HRP, base32, BECH32M_VARIANT).unwrap_or_else( + |_| { + panic!( + "The human-readable part {} should never cause a failure", + Self::HRP + ) + }, + ) + } + + /// Try to decode `Self` from a string + fn decode(string: impl AsRef) -> Result { + let (hrp, hash_base32, variant) = bech32::decode(string.as_ref()) + .map_err(DecodeError::DecodeBech32)?; + if hrp != Self::HRP { + return Err(DecodeError::UnexpectedBech32Hrp( + hrp, + Self::HRP.into(), + )); + } + match variant { + BECH32M_VARIANT => {} + _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), + } + let bytes: Vec = FromBase32::from_base32(&hash_base32) + .map_err(DecodeError::DecodeBase32)?; + + Self::decode_bytes(&bytes) + } + + /// Encode `Self` to bytes + fn to_bytes(&self) -> Vec; + + /// Try to decode `Self` from bytes + fn decode_bytes(bytes: &[u8]) -> Result; +} + +/// Implement [`std::fmt::Display`] and [`std::str::FromStr`] via +/// [`Format`]. +#[macro_export] +macro_rules! impl_display_and_from_str_via_format { + ($t:path) => { + impl std::fmt::Display for $t { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + $crate::types::string_encoding::Format::encode(self) + ) + } + } + + impl std::str::FromStr for $t { + type Err = $crate::types::string_encoding::DecodeError; + + fn from_str(s: &str) -> std::result::Result { + $crate::types::string_encoding::Format::decode(s) + } + } + }; +} + +/// Get the length of the human-readable part +// Not in the `Format` trait, cause functions in traits cannot be const +pub const fn hrp_len() -> usize { + T::HRP.len() +} + +/// Wrapper for `T` to serde encode via `Display` and decode via `FromStr` +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BorshSerialize, + BorshDeserialize, +)] +#[serde(transparent)] +pub struct StringEncoded +where + T: FromStr + Display, + ::Err: Display, +{ + /// Raw value + #[serde( + serialize_with = "encode_via_display", + deserialize_with = "decode_via_from_str" + )] + pub raw: T, +} + +impl StringEncoded +where + T: FromStr + Display, + ::Err: Display, +{ + /// Wrap to make `T` string encoded + pub fn new(raw: T) -> Self { + Self { raw } + } +} + +impl Deref for StringEncoded +where + T: FromStr + Display, + ::Err: Display, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.raw + } +} + +impl Display for StringEncoded +where + T: FromStr + Display, + ::Err: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.raw.fmt(f) + } +} + +impl FromStr for StringEncoded +where + T: FromStr + Display, + ::Err: Display, +{ + type Err = ::Err; + + fn from_str(s: &str) -> Result { + let raw = T::from_str(s)?; + Ok(Self { raw }) + } +} + +fn encode_via_display(val: &T, serializer: S) -> Result +where + S: serde::Serializer, + T: Display, +{ + let val_str = val.to_string(); + serde::Serialize::serialize(&val_str, serializer) +} + +fn decode_via_from_str<'de, D, T>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, + T: FromStr, + ::Err: Display, +{ + let val_str: String = serde::Deserialize::deserialize(deserializer)?; + FromStr::from_str(&val_str).map_err(serde::de::Error::custom) +} diff --git a/core/src/types/time.rs b/core/src/types/time.rs index 341a81411c..54622e15a6 100644 --- a/core/src/types/time.rs +++ b/core/src/types/time.rs @@ -10,6 +10,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use chrono::ParseError; pub use chrono::{DateTime, Duration, TimeZone, Utc}; +use serde::{Serialize, Deserialize}; /// Check if the given `duration` has passed since the given `start. pub fn duration_passed( @@ -30,6 +31,8 @@ pub fn duration_passed( PartialOrd, Ord, Hash, + Serialize, + Deserialize, BorshSerialize, BorshDeserialize, BorshSchema, @@ -73,6 +76,8 @@ impl Display for DurationSecs { PartialOrd, Ord, Hash, + Serialize, + Deserialize, BorshSerialize, BorshDeserialize, BorshSchema, @@ -93,8 +98,26 @@ impl From for DurationNanos { } } +impl From for std::time::Duration { + fn from(DurationNanos { secs, nanos }: DurationNanos) -> Self { + Self::new(secs, nanos) + } +} + /// An RFC 3339 timestamp (e.g., "1970-01-01T00:00:00Z"). -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[derive( + Clone, + Debug, + Deserialize, + Serialize, + BorshDeserialize, + BorshSerialize, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, +)] pub struct Rfc3339String(pub String); /// A duration in seconds precision. @@ -114,6 +137,9 @@ pub struct Rfc3339String(pub String); #[serde(try_from = "Rfc3339String", into = "Rfc3339String")] pub struct DateTimeUtc(pub DateTime); +/// The minimum possible DateTime. +pub const MIN_UTC: DateTimeUtc = DateTimeUtc(chrono::DateTime::::MIN_UTC); + impl Display for DateTimeUtc { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.to_rfc3339()) @@ -292,3 +318,17 @@ impl TryFrom for DateTimeUtc { Rfc3339String(t.to_rfc3339()).try_into() } } + +#[cfg(any(feature = "tendermint", feature = "tendermint-abcipp"))] +impl From for DurationNanos { + fn from(val: crate::tendermint::Timeout) -> Self { + Self::from(std::time::Duration::from(val)) + } +} + +#[cfg(any(feature = "tendermint", feature = "tendermint-abcipp"))] +impl From for crate::tendermint::Timeout { + fn from(val: DurationNanos) -> Self { + Self::from(std::time::Duration::from(val)) + } +} diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 9e7c2d0ee7..73a78b5bd9 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1,5 +1,6 @@ //! A basic fungible token +use std::cmp::Ordering; use std::fmt::Display; use std::iter::Sum; use std::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}; @@ -313,8 +314,6 @@ impl From for u8 { Hash, PartialEq, Eq, - PartialOrd, - Ord, BorshSerialize, BorshDeserialize, BorshSchema, @@ -348,6 +347,10 @@ impl DenominatedAmount { pub fn to_string_precise(&self) -> String { let decimals = self.denom.0 as usize; let mut string = self.amount.raw.to_string(); + // escape hatch if there are no decimal places + if decimals == 0 { + return string; + } if string.len() > decimals { string.insert(string.len() - decimals, '.'); } else { @@ -403,7 +406,11 @@ impl DenominatedAmount { impl Display for DenominatedAmount { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let string = self.to_string_precise(); - let string = string.trim_end_matches(&['0']); + let string = if self.denom.0 > 0 { + string.trim_end_matches(&['0']) + } else { + &string + }; let string = string.trim_end_matches(&['.']); f.write_str(string) } @@ -453,6 +460,50 @@ impl FromStr for DenominatedAmount { } } +impl PartialOrd for DenominatedAmount { + fn partial_cmp(&self, other: &Self) -> Option { + if self.denom < other.denom { + let diff = other.denom.0 - self.denom.0; + let (div, rem) = + other.amount.raw.div_mod(Uint::exp10(diff as usize)); + let div_ceil = if rem.is_zero() { + div + } else { + div + Uint::one() + }; + let ord = self.amount.raw.partial_cmp(&div_ceil); + if let Some(Ordering::Equal) = ord { + if rem.is_zero() { + Some(Ordering::Equal) + } else { + Some(Ordering::Greater) + } + } else { + ord + } + } else { + let diff = self.denom.0 - other.denom.0; + let (div, rem) = + self.amount.raw.div_mod(Uint::exp10(diff as usize)); + let div_ceil = if rem.is_zero() { + div + } else { + div + Uint::one() + }; + let ord = div_ceil.partial_cmp(&other.amount.raw); + if let Some(Ordering::Equal) = ord { + if rem.is_zero() { + Some(Ordering::Equal) + } else { + Some(Ordering::Less) + } + } else { + ord + } + } + } +} + impl serde::Serialize for Amount { fn serialize( &self, @@ -1216,6 +1267,13 @@ mod tests { }; assert_eq!("0.0112", amount.to_string()); assert_eq!("0.01120", amount.to_string_precise()); + + let amount = DenominatedAmount { + amount: Amount::from_uint(200, 0).expect("Test failed"), + denom: 0.into(), + }; + assert_eq!("200", amount.to_string()); + assert_eq!("200", amount.to_string_precise()); } #[test] @@ -1342,6 +1400,62 @@ mod tests { assert_eq!(two.mul_ceil(dec), one); assert_eq!(three.mul_ceil(dec), two); } + + #[test] + fn test_denominated_amt_ord() { + let denom_1 = DenominatedAmount { + amount: Amount::from_uint(15, 0).expect("Test failed"), + denom: 1.into(), + }; + let denom_2 = DenominatedAmount { + amount: Amount::from_uint(1500, 0).expect("Test failed"), + denom: 3.into(), + }; + // The psychedelic case. Partial ordering works on the underlying + // amounts but `Eq` also checks the equality of denoms. + assert_eq!( + denom_1.partial_cmp(&denom_2).expect("Test failed"), + Ordering::Equal + ); + assert_eq!( + denom_2.partial_cmp(&denom_1).expect("Test failed"), + Ordering::Equal + ); + assert_ne!(denom_1, denom_2); + + let denom_1 = DenominatedAmount { + amount: Amount::from_uint(15, 0).expect("Test failed"), + denom: 1.into(), + }; + let denom_2 = DenominatedAmount { + amount: Amount::from_uint(1501, 0).expect("Test failed"), + denom: 3.into(), + }; + assert_eq!( + denom_1.partial_cmp(&denom_2).expect("Test failed"), + Ordering::Less + ); + assert_eq!( + denom_2.partial_cmp(&denom_1).expect("Test failed"), + Ordering::Greater + ); + let denom_1 = DenominatedAmount { + amount: Amount::from_uint(15, 0).expect("Test failed"), + denom: 1.into(), + }; + let denom_2 = DenominatedAmount { + amount: Amount::from_uint(1499, 0).expect("Test failed"), + denom: 3.into(), + }; + assert_eq!( + denom_1.partial_cmp(&denom_2).expect("Test failed"), + Ordering::Greater + ); + assert_eq!( + denom_2.partial_cmp(&denom_1).expect("Test failed"), + Ordering::Less + ); + } } /// Helpers for testing with addresses. @@ -1369,49 +1483,4 @@ pub mod testing { (1..=max).prop_map(|val| Amount::from_uint(val, 0).unwrap()) } - /// init_token_storage is useful when the initialization of the network is - /// not properly made. This properly sets up the storage such that - /// inflation calculations can be ran on the token addresses. We assume - /// a total supply that may not be real - pub fn init_token_storage( - wl_storage: &mut ledger_storage::WlStorage, - epochs_per_year: u64, - ) where - D: 'static - + ledger_storage::DB - + for<'iter> ledger_storage::DBIter<'iter>, - H: 'static + ledger_storage::StorageHasher, - { - use crate::ledger::parameters::storage::get_epochs_per_year_key; - use crate::types::address::tokens; - - let tokens = tokens(); - let masp_reward_keys: Vec<_> = tokens.keys().collect(); - - wl_storage - .write(&get_epochs_per_year_key(), epochs_per_year) - .unwrap(); - let params = Parameters { - max_reward_rate: Dec::from_str("0.1").unwrap(), - kd_gain_nom: Dec::from_str("0.1").unwrap(), - kp_gain_nom: Dec::from_str("0.1").unwrap(), - locked_ratio_target: Dec::zero(), - }; - - for address in masp_reward_keys { - params.init_storage(address, wl_storage); - wl_storage - .write( - &minted_balance_key(address), - Amount::native_whole(5), // arbitrary amount - ) - .unwrap(); - wl_storage - .write(&masp_last_inflation_key(address), Amount::zero()) - .expect("inflation ought to be written"); - wl_storage - .write(&masp_last_locked_ratio_key(address), Dec::zero()) - .expect("last locked set default"); - } - } } diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index e6aeedc252..463fa34383 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -538,6 +538,11 @@ impl I256 { Self(Uint::zero()) } + /// Gives the one value of an I256 + pub fn one() -> I256 { + Self(Uint::one()) + } + /// Get a string representation of `self` as a /// native token amount. pub fn to_string_native(&self) -> String { diff --git a/ethereum_bridge/src/parameters.rs b/ethereum_bridge/src/parameters.rs index 599ef0c5f8..62d1f9a7a1 100644 --- a/ethereum_bridge/src/parameters.rs +++ b/ethereum_bridge/src/parameters.rs @@ -150,7 +150,7 @@ pub struct Contracts { BorshSerialize, BorshDeserialize, )] -pub struct EthereumBridgeConfig { +pub struct EthereumBridgeParams { /// Initial Ethereum block height when events will first be extracted from. pub eth_start_height: ethereum_structs::BlockHeight, /// Minimum number of confirmations needed to trust an Ethereum branch. @@ -163,7 +163,7 @@ pub struct EthereumBridgeConfig { pub contracts: Contracts, } -impl EthereumBridgeConfig { +impl EthereumBridgeParams { /// Initialize the Ethereum bridge parameters in storage. /// /// If these parameters are initialized, the storage subspaces @@ -248,7 +248,7 @@ impl EthereumBridgeConfig { } } -/// Subset of [`EthereumBridgeConfig`], containing only Ethereum +/// Subset of [`EthereumBridgeParams`], containing only Ethereum /// oracle specific parameters. #[derive(Clone, Debug, Eq, PartialEq)] pub struct EthereumOracleConfig { @@ -262,9 +262,9 @@ pub struct EthereumOracleConfig { pub contracts: Contracts, } -impl From for EthereumOracleConfig { - fn from(config: EthereumBridgeConfig) -> Self { - let EthereumBridgeConfig { +impl From for EthereumOracleConfig { + fn from(config: EthereumBridgeParams) -> Self { + let EthereumBridgeParams { eth_start_height, min_confirmations, contracts, @@ -374,7 +374,7 @@ mod tests { use super::*; use crate::parameters::{ - ContractVersion, Contracts, EthereumBridgeConfig, MinimumConfirmations, + ContractVersion, Contracts, EthereumBridgeParams, MinimumConfirmations, UpgradeableContract, }; @@ -383,7 +383,7 @@ mod tests { /// in any of the config structs. #[test] fn test_round_trip_toml_serde() -> Result<()> { - let config = EthereumBridgeConfig { + let config = EthereumBridgeParams { erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::default(), @@ -396,7 +396,7 @@ mod tests { }, }; let serialized = toml::to_string(&config)?; - let deserialized: EthereumBridgeConfig = toml::from_str(&serialized)?; + let deserialized: EthereumBridgeParams = toml::from_str(&serialized)?; assert_eq!(config, deserialized); Ok(()) @@ -405,7 +405,7 @@ mod tests { #[test] fn test_ethereum_bridge_config_read_write_storage() { let mut wl_storage = TestWlStorage::default(); - let config = EthereumBridgeConfig { + let config = EthereumBridgeParams { erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::default(), @@ -437,7 +437,7 @@ mod tests { #[should_panic(expected = "Could not read")] fn test_ethereum_bridge_config_storage_corrupt() { let mut wl_storage = TestWlStorage::default(); - let config = EthereumBridgeConfig { + let config = EthereumBridgeParams { erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::default(), diff --git a/ethereum_bridge/src/test_utils.rs b/ethereum_bridge/src/test_utils.rs index cc5370360d..c4d4967389 100644 --- a/ethereum_bridge/src/test_utils.rs +++ b/ethereum_bridge/src/test_utils.rs @@ -6,7 +6,6 @@ use std::num::NonZeroU64; use borsh_ext::BorshSerializeExt; use namada_core::ledger::eth_bridge::storage::bridge_pool::get_key_from_hash; use namada_core::ledger::eth_bridge::storage::whitelist; -use namada_core::ledger::governance::parameters::GovernanceParameters; use namada_core::ledger::storage::mockdb::MockDBWriteBatch; use namada_core::ledger::storage::testing::{TestStorage, TestWlStorage}; use namada_core::ledger::storage_api::token::credit_tokens; @@ -27,7 +26,7 @@ use namada_proof_of_stake::{ }; use crate::parameters::{ - ContractVersion, Contracts, EthereumBridgeConfig, MinimumConfirmations, + ContractVersion, Contracts, EthereumBridgeParams, MinimumConfirmations, UpgradeableContract, }; @@ -99,12 +98,12 @@ pub fn default_validator() -> (Address, token::Amount) { (addr, voting_power) } -/// Writes a dummy [`EthereumBridgeConfig`] to the given [`TestWlStorage`], and +/// Writes a dummy [`EthereumBridgeParams`] to the given [`TestWlStorage`], and /// returns it. pub fn bootstrap_ethereum_bridge( wl_storage: &mut TestWlStorage, -) -> EthereumBridgeConfig { - let config = EthereumBridgeConfig { +) -> EthereumBridgeParams { + let config = EthereumBridgeParams { // start with empty erc20 whitelist erc20_whitelist: vec![], eth_start_height: Default::default(), @@ -217,11 +216,9 @@ pub fn init_storage_with_validators( }) .collect(); - let gov_params = GovernanceParameters::default(); - gov_params.init_storage(wl_storage).unwrap(); - namada_proof_of_stake::init_genesis( + namada_proof_of_stake::test_utils::test_init_genesis( wl_storage, - &OwnedPosParams::default(), + OwnedPosParams::default(), validators.into_iter(), 0.into(), ) @@ -291,11 +288,12 @@ pub fn append_validators_to_storage( current_epoch, commission_rate: Dec::new(5, 2).unwrap(), max_commission_rate_change: Dec::new(1, 2).unwrap(), + offset_opt: Some(1), }) .expect("Test failed"); credit_tokens(wl_storage, &staking_token, &validator, stake) .expect("Test failed"); - bond_tokens(wl_storage, None, &validator, stake, current_epoch) + bond_tokens(wl_storage, None, &validator, stake, current_epoch, None) .expect("Test failed"); all_keys.insert(validator, keys); diff --git a/genesis/README.md b/genesis/README.md new file mode 100644 index 0000000000..83495cd996 --- /dev/null +++ b/genesis/README.md @@ -0,0 +1,153 @@ +# Genesis templates + +An example setup with a single validator used to run a localnet can be found in [localnet](localnet/README.md) directory. + +[Starter templates](starter/README.md) can be used to configure new networks. + +The required genesis templates to setup a network are: + +- [`validity-predicates.toml`](#validity-predicates) +- [`tokens.toml`](#tokens) +- [`balances.toml`](#balances) +- [`parameters.toml`](#parameters) +- [`transactions.toml`](#transactions) + +## Validity predicates + +The [validity-predicates.toml file](validity-predicates) contains definitions of WASM validity predicates, which can be used in the [tokens](#tokens), [parameters](#parameters) and [transactions.toml](#transactions) files as validity predicates of established accounts. + +## Tokens + +The [tokens.toml file](tokens.toml) contains tokens with their aliases and validity predicates. + +## Balances + +The [balances.toml file](balances.toml) contains token balances associated with the public keys. + +TODO: add shielded balances + +## Parameters + +The [parameters.toml file](parameters.toml) contains the general chain parameters, PoS and governance parameters. + +## Transactions + +The [transactions.toml file](transactions.toml) contains any transactions that can be applied at genesis. These are: + +### Genesis tx `established_account` + +An established account with some `alias`, a validity predicate `vp` and optionally a `public_key`. When a public key is used, the transaction must be [signed](#signing-genesis-txs) with it to authorize its use. + +An unsigned `established_account` tx example: + +```toml +[[established_account]] +alias = "Albert" # Aliases are case-insensitive +vp = "vp_user" +public_key = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +``` + +### Genesis tx `validator_account` + +A validator account with some `alias`, a validity predicate `vp`, various keys and validator variables. Public keys used in the transaction must also [sign](#signing-validator-genesis-txs) the transaction to authorize their use. + +An unsigned `validator_account` tx example: + +```toml +[[validator_account]] +alias = "validator-0" +vp = "vp_validator" +commission_rate = "0.05" +max_commission_rate_change = "0.01" +net_address = "127.0.0.1:27656" +account_key = "pktest1qzjnu45v9uvvz4shwkxrgq44l7l4ncs0ryt9mwt7973fdjvm76tgkkakjmf" +consensus_key = "pktest1qp4dcws0fthlrt69erz854efxxtxvympw9m3npy2w8rphqgxu2ufc476rgt" +protocol_key = "pktest1qqwg6uwuxn70spl9x377v0q6fzr6d29gpkdfc0tmp8uj97p5awnukkeue3k" +tendermint_node_key = "pktest1qzmajsm6a5uamaq7el4kkp6txe9jt0ld3q0jy0er7cuz0u0k2yck6ls5ppm" +dkg_key = "dpktest1vqqqqqqzlgrsdkkjc0yg842xqkffy7g2vwvx3x8389ydprz2qwncruzxr8cg8u939z4yy76wkx6uwfe7qur95yrftsd0r8lu0ayhu4zqsrkf9em3n5zpm7jkcmjtg0a24h2fa5gejvt0ywddwc6xa72f3z8czkcw9ynw66" +``` + +### Genesis tx `transfer` + +A transfer can only be applied from one of the keys used in [Balances file](#balances) as the `source`. The target may be another key or an alias of an account to be created with `established_account` or `validator_account` genesis transactions. + +An unsigned `transfer` tx example: + +```toml +[[transfer]] +token = "NAM" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = 1_000_000 +``` + +### Genesis tx `bond` + +A bond may be either a self-bond when the `source` is the same as `validator` or a delegation otherwise. + +An example of an unsigned delegation `bond` tx from `established_account` with alias "albert": + +```toml +[[bond]] +source = "albert" +validator = "validator-0" # There must be a `validator_account` tx with this alias +amount = 20_000 # in native token NAM +``` + +For a delegation `bond` tx from an implicit account, one can use a public key as the source: + +```toml +[[bond]] +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +validator = "validator-0" +amount = 20_000 # in native token NAM +``` + +Note that for a delegation, the source key must have the sufficient balance assigned in the Balances file. + +An unsigned self-`bond` tx example: + +```toml +[[bond]] +source = "validator-0" +validator = "validator-0" +amount = 90_000_000 # The validator must have this amount of NAM available in account +``` + +### Signing genesis txs + +To sign genesis transactions, the data is borsh-encoded into a `Tx` `data` field. For `code` an empty vec is used and for the timestamp we use the minimum UTC timestamp. The transaction must be constructed in exactly the same way to verify the signatures, which is being done by the ledger when we're initializing the genesis. Any transaction that has invalid signature or cannot be applied for any other reason, such as insufficient funds may fail at genesis initialization and the chain will continue to be initialized without it. + +For non-validator transactions, a helper tool for producing signatures for transactions can be used with e.g.: + +```shell +namada client utils \ + sign-genesis-tx \ + --path "unsigned-tx.toml" \ + --output "signed-txs.toml" +``` + +For validator txs, see [Signing validator genesis txs](#signing-validator-genesis-txs) below. + +#### Signing validator genesis txs + +To generate validator wallet and sign validator transactions, run e.g.: + +```shell +namadac utils \ + init-genesis-validator \ + --source validator-0-key \ + --alias validator-0 \ + --net-address "127.0.0.1:27656" \ + --commission-rate 0.05 \ + --max-commission-rate-change 0.01 \ + --transfer-from-source-amount 1_000_000_000 \ + --self-bond-amount 900_000_000 \ + --unsafe-dont-encrypt +``` + +The `--source` key alias must have already have native token `NAM` in the [Balances files](#balances) and the balance must be greater than or equal to `--transfer-from-source-amount`. + +The `--self-bond-amount` must be lower than or equal to `--transfer-from-source-amount`, but we recommend to keep at least some tokens in the validator account for submitting validator transactions to be able to pay for fees and gas. + +This command will generate a validator pre-genesis wallet and transactions file containing signed `validator_account`, `transfer` and `bond` txs. diff --git a/genesis/dev.toml b/genesis/dev.toml deleted file mode 100644 index dded8358d9..0000000000 --- a/genesis/dev.toml +++ /dev/null @@ -1,247 +0,0 @@ -# Developer network -genesis_time = "2021-12-20T15:00:00.00Z" -native_token = "NAM" - -# Some tokens present at genesis. - -[token.NAM] -address = "atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" -denom = 6 -vp = "vp_token" -[token.NAM.balances] -Albert = "1000000" -"Albert.public_key" = "100" -Bertha = "1000000" -"Bertha.public_key" = "2000" -Christel = "1000000" -"Christel.public_key" = "100" -Daewon = "1000000" -Ester = "1000000" -[token.NAM.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[token.BTC] -address = "atest1v4ehgw36xdzryve5gsc52veeg5cnsv2yx5eygvp38qcrvd29xy6rys6p8yc5xvp4xfpy2v694wgwcp" -denom = 8 -vp = "vp_token" -[token.BTC.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.BTC.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[token.ETH] -address = "atest1v4ehgw36xqmr2d3nx3ryvd2xxgmrq33j8qcns33sxezrgv6zxdzrydjrxveygd2yxumrsdpsf9jc2p" -denom = 18 -vp = "vp_token" -[token.ETH.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.ETH.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[token.DOT] -address = "atest1v4ehgw36gg6nvs2zgfpyxsfjgc65yv6pxy6nwwfsxgungdzrggeyzv35gveyxsjyxymyz335hur2jn" -denom = 10 -vp = "vp_token" -[token.DOT.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.DOT.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[token.Schnitzel] -address = "atest1v4ehgw36xue5xvf5xvuyzvpjx5un2v3k8qeyvd3cxdqns32p89rrxd6xx9zngvpegccnzs699rdnnt" -denom = 6 -vp = "vp_token" -[token.Schnitzel.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.Schnitzel.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[token.Apfel] -address = "atest1v4ehgw36gfryydj9g3p5zv3kg9znyd358ycnzsfcggc5gvecgc6ygs2rxv6ry3zpg4zrwdfeumqcz9" -denom = 6 -vp = "vp_token" -[token.Apfel.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.Apfel.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[token.Kartoffel] -address = "atest1v4ehgw36gep5ysecxq6nyv3jg3zygv3e89qn2vp48pryxsf4xpznvve5gvmy23fs89pryvf5a6ht90" -public_key = "" -denom = 6 -vp = "vp_token" -[token.Kartoffel.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.Kartoffel.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target_key = "0.6667" - -[established.Albert] -vp = "vp_user" - -[established.Bertha] -vp = "vp_user" - -[established.Christel] -vp = "vp_user" - -[established.masp] -address = "atest1v4ehgw36xaryysfsx5unvve4g5my2vjz89p52sjxxgenzd348yuyyv3hg3pnjs35g5unvde4ca36y5" -vp = "vp_masp" - -[implicit.Daewon] - -[implicit.Ester] - -# Wasm VP definitions - -# Wasm VP definitions - -# Implicit VP -[wasm.vp_implicit] -filename = "vp_implicit.wasm" - -# Default user VP in established accounts -[wasm.vp_user] -filename = "vp_user.wasm" - -# Default validator VP -[wasm.vp_validator] -# filename (relative to wasm path used by the node) -filename = "vp_validator.wasm" - -# MASP VP -[wasm.vp_masp] -filename = "vp_masp.wasm" - -# General protocol parameters. -[parameters] -# Minimum number of blocks in an epoch. -min_num_of_blocks = 4 -# Maximum expected time per block (in seconds). -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 -tx_whitelist = [] -# Implicit VP WASM name -implicit_vp = "vp_implicit" -# Expected number of epochs per year (also sets the min duration of an epoch in seconds) -epochs_per_year = 105_120 # 5 minute epochs -# The P gain factor in the Proof of Stake rewards controller -pos_gain_p = "0.1" -# The D gain factor in the Proof of Stake rewards controller -pos_gain_d = "0.1" -# The maximum number of signatures allowed per transaction -max_signatures_per_transaction = 15 - -[parameters.minimum_gas_price] -"atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" = "0.000001" - -# Proof of stake parameters. -[pos_params] -# Maximum number of consensus validators. -max_validator_slots = 128 -# Pipeline length (in epochs). Any change in the validator set made in -# epoch 'n' will become active in epoch 'n + pipeline_len'. -pipeline_len = 2 -# Unbonding length (in epochs). Validators may have their stake slashed -# for a fault in epoch 'n' up through epoch 'n + unbonding_len'. -unbonding_len = 3 -# Votes per fundamental staking token (namnam) -tm_votes_per_token = "0.1" -# Reward for proposing a block. -block_proposer_reward = "0.125" -# Reward for voting on a block. -block_vote_reward = "0.1" -# Maximum inflation rate per annum (10%) -max_inflation_rate = "0.1" -# Targeted ratio of staked tokens to total tokens in the supply -target_staked_ratio = "0.6667" -# Portion of a validator's stake that should be slashed on a duplicate -# vote. -duplicate_vote_min_slash_rate = "0.001" -# Portion of a validator's stake that should be slashed on a light -# client attack. -light_client_attack_min_slash_rate = "0.001" -# Number of epochs above and below (separately) the current epoch to -# consider when doing cubic slashing -cubic_slashing_window_length = 1 -# The minimum amount of bonded tokens that a validator needs to be in -# either the `consensus` or `below_capacity` validator sets -validator_stake_threshold = "1" - -# Governance parameters. -[gov_params] -# minimum amount of nam token to lock -min_proposal_fund = 500 -# proposal code size in bytes -max_proposal_code_size = 500000 -# min proposal voting period length in epochs -min_proposal_voting_period = 3 -# max proposal period length in epochs -max_proposal_period = 27 -# maximum number of characters in the proposal content -max_proposal_content_size = 10000 -# minimum epochs between end and grace epoch -min_proposal_grace_epochs = 6 - -[pgf_params] -# list of steward address at genezis -stewards = [] -# inflation rate for pgf fundings -pgf_inflation_rate = "0.1" -# inflation rate for pgf stewards -stewards_inflation_rate = "0.01" \ No newline at end of file diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml deleted file mode 100644 index 1096be0d28..0000000000 --- a/genesis/e2e-tests-single-node.toml +++ /dev/null @@ -1,260 +0,0 @@ -# Genesis configuration source for E2E tests with: -# - 1 genesis validator -# - User accounts same as the ones in "dev" build (Albert, Bertha, Christel) - -genesis_time = "2021-09-30T10:00:00Z" -native_token = "NAM" - -[validator.validator-0] -# Validator's staked NAM at genesis. -tokens = 200000 -# Amount of the validator's genesis token balance which is not staked. -non_staked_balance = 1000000000000 -# VP for the validator account -validator_vp = "vp_validator" -# Commission rate for rewards -commission_rate = "0.05" -# Maximum change per epoch in the commission rate -max_commission_rate_change = "0.01" -# (Public IP | Hostname):port address. -# We set the port to be the default+1000, so that if a local node was running at -# the same time as the E2E tests, it wouldn't affect them. -net_address = "127.0.0.1:27656" - -# Some tokens present at genesis. - -[token.NAM] -address = "atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" -denom = 6 -[token.NAM.balances] -Albert = "1000000" -"Albert.public_key" = "100" -Bertha = "1000000" -"Bertha.public_key" = "2000" -Christel = "1000000" -"Christel.public_key" = "100" -Daewon = "1000000" -Ester = "1000000" -"validator-0.public_key" = "100" -[token.NAM.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[token.BTC] -address = "atest1v4ehgw36xdzryve5gsc52veeg5cnsv2yx5eygvp38qcrvd29xy6rys6p8yc5xvp4xfpy2v694wgwcp" -denom = 8 -[token.BTC.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.BTC.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[token.ETH] -address = "atest1v4ehgw36xqmr2d3nx3ryvd2xxgmrq33j8qcns33sxezrgv6zxdzrydjrxveygd2yxumrsdpsf9jc2p" -denom = 18 -[token.ETH.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.ETH.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[token.DOT] -address = "atest1v4ehgw36gg6nvs2zgfpyxsfjgc65yv6pxy6nwwfsxgungdzrggeyzv35gveyxsjyxymyz335hur2jn" -denom = 10 -[token.DOT.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.DOT.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[token.Schnitzel] -address = "atest1v4ehgw36xue5xvf5xvuyzvpjx5un2v3k8qeyvd3cxdqns32p89rrxd6xx9zngvpegccnzs699rdnnt" -denom = 6 -[token.Schnitzel.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.Schnitzel.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[token.Apfel] -address = "atest1v4ehgw36gfryydj9g3p5zv3kg9znyd358ycnzsfcggc5gvecgc6ygs2rxv6ry3zpg4zrwdfeumqcz9" -denom = 6 -[token.Apfel.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.Apfel.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[token.Kartoffel] -address = "atest1v4ehgw36gep5ysecxq6nyv3jg3zygv3e89qn2vp48pryxsf4xpznvve5gvmy23fs89pryvf5a6ht90" -public_key = "" -denom = 6 -[token.Kartoffel.balances] -Albert = "1000000" -Bertha = "1000000" -Christel = "1000000" -Daewon = "1000000" -Ester = "1000000" -[token.Kartoffel.parameters] -max_reward_rate = "0.1" -kd_gain_nom = "0.1" -kp_gain_nom = "0.1" -locked_ratio_target = "0.6667" - -[established.Albert] -vp = "vp_user" - -[established.Bertha] -vp = "vp_user" - -[established.Christel] -vp = "vp_user" - -[established.masp] -address = "atest1v4ehgw36xaryysfsx5unvve4g5my2vjz89p52sjxxgenzd348yuyyv3hg3pnjs35g5unvde4ca36y5" -vp = "vp_masp" - -[implicit.Daewon] - -[implicit.Ester] - -# Wasm VP definitions - -# Implicit VP -[wasm.vp_implicit] -filename = "vp_implicit.wasm" - -# Default user VP in established accounts -[wasm.vp_user] -filename = "vp_user.wasm" - -# Default validator VP -[wasm.vp_validator] -# filename (relative to wasm path used by the node) -filename = "vp_validator.wasm" - -# MASP VP -[wasm.vp_masp] -filename = "vp_masp.wasm" - -# General protocol parameters. -[parameters] -# Minimum number of blocks in an epoch. -min_num_of_blocks = 4 -# Maximum expected time per block (in seconds). -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 -tx_whitelist = [] -# Implicit VP WASM name -implicit_vp = "vp_implicit" -# Expected number of epochs per year (also sets the min duration of an epoch in seconds). -# Remember to set this to a more reasonable number for production networks. Also, expect most masp -# txs to fail due to epoch boundary errors. -epochs_per_year = 31_536_000 -# The P gain factor in the Proof of Stake rewards controller -pos_gain_p = "0.1" -# The D gain factor in the Proof of Stake rewards controller -pos_gain_d = "0.1" -# The maximum number of signatures allowed per transaction -max_signatures_per_transaction = 15 - -[parameters.minimum_gas_price] -"atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" = "0.000001" - -# Proof of stake parameters. -[pos_params] -# Maximum number of consensus validators. -max_validator_slots = 128 -# Pipeline length (in epochs). Any change in the validator set made in -# epoch 'n' will become active in epoch 'n + pipeline_len'. -pipeline_len = 2 -# Unbonding length (in epochs). Validators may have their stake slashed -# for a fault in epoch 'n' up through epoch 'n + unbonding_len'. -unbonding_len = 3 -# Votes per fundamental staking token (namnam) - for testnets this should be 1. For e2e toml, a decimal is used for testing purposes. -tm_votes_per_token = "0.1" -# Reward for proposing a block. -block_proposer_reward = "0.125" -# Reward for voting on a block. -block_vote_reward = "0.1" -# Maximum inflation rate per annum (10%) -max_inflation_rate = "0.1" -# Targeted ratio of staked tokens to total tokens in the supply -target_staked_ratio = "0.6667" -# Portion of a validator's stake that should be slashed on a duplicate -# vote. -duplicate_vote_min_slash_rate = "0.001" -# Portion of a validator's stake that should be slashed on a light -# client attack. -light_client_attack_min_slash_rate = "0.001" -# Number of epochs above and below (separately) the current epoch to -# consider when doing cubic slashing -cubic_slashing_window_length = 1 -# The minimum amount of bonded tokens that a validator needs to be in -# either the `consensus` or `below_capacity` validator sets -validator_stake_threshold = "1" - -# Governance parameters. -[gov_params] -# minimum amount of nam token to lock -min_proposal_fund = 500 -# proposal code size in bytes -max_proposal_code_size = 1000000 -# min proposal period length in epochs -min_proposal_voting_period = 3 -# max proposal period length in epochs -max_proposal_period = 27 -# maximum number of characters in the proposal content -max_proposal_content_size = 10000 -# minimum epochs between end and grace epoch -min_proposal_grace_epochs = 6 - -[pgf_params] -# list of steward address at genezis -stewards = [] -# inflation rate for pgf fundings -pgf_inflation_rate = "0.1" -# inflation rate for pgf stewards -stewards_inflation_rate = "0.01" \ No newline at end of file diff --git a/genesis/localnet/README.md b/genesis/localnet/README.md new file mode 100644 index 0000000000..e483f7aa07 --- /dev/null +++ b/genesis/localnet/README.md @@ -0,0 +1,66 @@ +# Localnet genesis templates + +This directory contains genesis templates for a local network with a single validator. The `src` directory contains generated pre-genesis wallet pre-loaded with unencrypted keys and a single validator `"validator-0" wallet that are being used in the templates. + +If you're modifying any of the files here, you can run this to ensure that the changes are valid: + +```shell +cargo watch -x "test test_validate_localnet_genesis_templates" +``` + +## balances.toml + +The pre-genesis balances wallet is located at [pre-genesis/wallet.toml](pre-genesis/wallet.toml) was used to setup the [balances.toml](balances.toml) and can be re-generated from the repo's root dir with: + +```shell +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key gen \ + --alias albert-key --unsafe-dont-encrypt +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key gen \ + --alias bertha-key --unsafe-dont-encrypt +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key gen \ + --alias christel --unsafe-dont-encrypt +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key gen \ + --alias daewon --unsafe-dont-encrypt +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key gen \ + --alias validator-0-balance-key --unsafe-dont-encrypt +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key gen \ + --alias faucet-key --unsafe-dont-encrypt +``` + +The [balances.toml file](balances.toml) contains token balances associated with the public keys. The public keys from the wallet can be found with: + +```shell +cargo run --bin namadaw -- --base-dir "genesis/localnet/src" key list +``` + +## transactions.toml + +The pre-genesis validator wallet used to generate [validator transactions for transactions.toml](src/pre-genesis/validator-0/transactions.toml) is located at [src/pre-genesis/validator-0/validator-wallet.toml](src/pre-genesis/validator-0/validator-wallet.toml) and can be re-generated from the repo's root dir with: + +```shell +cargo run --bin namadac -- --base-dir "genesis/localnet/src" utils \ + init-genesis-validator \ + --source validator-0-balance-key \ + --alias validator-0 \ + --net-address "127.0.0.1:27656" \ + --commission-rate 0.05 \ + --max-commission-rate-change 0.01 \ + --transfer-from-source-amount 200000 \ + --self-bond-amount 100000 \ + --unsafe-dont-encrypt +``` + +The rest of the transactions are generated from [src/pre-genesis/unsigned-transactions.toml](src/pre-genesis/unsigned-transactions.toml) using: + +```shell +cargo run --bin namadac -- --base-dir "genesis/localnet/src" utils \ + sign-genesis-tx \ + --path "genesis/localnet/src/pre-genesis/unsigned-transactions.toml" \ + --output "genesis/localnet/src/pre-genesis/signed-transactions.toml" +``` + +This command produces [src/pre-genesis/signed-transactions.toml](src/pre-genesis/signed-transactions.toml), which is then concatenated in [transactions.toml](transactiosn.toml) with the validator transactions. + +## Validation + +A unit test `test_localnet_genesis_templates` is setup to check validity of the localnet setup. diff --git a/genesis/localnet/balances.toml b/genesis/localnet/balances.toml new file mode 100644 index 0000000000..8b84002267 --- /dev/null +++ b/genesis/localnet/balances.toml @@ -0,0 +1,110 @@ +# Genesis balances. +# +# This files contains the genesis balances of any tokens present at genesis +# associated with public keys. +# +# For example: +# ``` +# [token.NAM] +# some_pk_bech32m_encoding = 10 # genesis tokens, the amount can have up to 6 decimal places +# ``` +# +# The public keys present in here are taken from `pre-genesis/wallet.toml` + +[token.NAM] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "2000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "2000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "2000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "200000" + +[token.BTC] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "1000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "1000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "1000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "1000000" + +[token.ETH] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "1000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "1000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "1000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "1000000" + +[token.DOT] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "1000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "1000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "1000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "1000000" + +[token.Schnitzel] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "1000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "1000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "1000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "1000000" + +[token.Apfel] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "1000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "1000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "1000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "1000000" + +[token.Kartoffel] +# albert-key +pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44 = "1000000" +# bertha-key +pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "1000000" +# christel +pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "1000000" +# daewon +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +# ester +pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" +# validator-0-key +pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup = "1000000" \ No newline at end of file diff --git a/genesis/localnet/parameters.toml b/genesis/localnet/parameters.toml new file mode 100644 index 0000000000..41fb316f34 --- /dev/null +++ b/genesis/localnet/parameters.toml @@ -0,0 +1,90 @@ +# General protocol parameters. +[parameters] +native_token = "NAM" +# Minimum number of blocks in an epoch. +min_num_of_blocks = 4 +# Maximum expected time per block (in seconds). +max_expected_time_per_block = 30 +# Max payload size, in bytes, for a tx batch proposal. +max_proposal_bytes = 22020096 +# vp whitelist +vp_whitelist = [] +# tx whitelist +tx_whitelist = [] +# Implicit VP WASM name +implicit_vp = "vp_implicit" +# Expected number of epochs per year (also sets the min duration of an epoch in seconds) +epochs_per_year = 31_536_000 +# The P gain factor in the Proof of Stake rewards controller +pos_gain_p = "0.1" +# The D gain factor in the Proof of Stake rewards controller +pos_gain_d = "0.1" +# Maximum number of signature per transaction +max_signatures_per_transaction = 15 +# Max gas for block +max_block_gas = 20000000 +# Fee unshielding gas limit +fee_unshielding_gas_limit = 20000 +# Fee unshielding descriptions limit +fee_unshielding_descriptions_limit = 15 + +# Map of the cost per gas unit for every token allowed for fee payment +[parameters.minimum_gas_price] +nam = "0.000001" + +# Proof of stake parameters. +[pos_params] +# Maximum number of active validators. +max_validator_slots = 128 +# Pipeline length (in epochs). Any change in the validator set made in +# epoch 'n' will become active in epoch 'n + pipeline_len'. +pipeline_len = 2 +# Unbonding length (in epochs). Validators may have their stake slashed +# for a fault in epoch 'n' up through epoch 'n + unbonding_len'. +unbonding_len = 3 +# Votes per fundamental staking token (namnam) +tm_votes_per_token = "1" +# Reward for proposing a block. +block_proposer_reward = "0.125" +# Reward for voting on a block. +block_vote_reward = "0.1" +# Maximum inflation rate per annum (10%) +max_inflation_rate = "0.1" +# Targeted ratio of staked tokens to total tokens in the supply +target_staked_ratio = "0.6667" +# Portion of a validator's stake that should be slashed on a duplicate +# vote. +duplicate_vote_min_slash_rate = "0.001" +# Portion of a validator's stake that should be slashed on a light +# client attack. +light_client_attack_min_slash_rate = "0.001" +# Number of epochs above and below (separately) the current epoch to +# consider when doing cubic slashing +cubic_slashing_window_length = 1 +# The minimum amount of bonded tokens that a validator needs to be in +# either the `consensus` or `below_capacity` validator sets +validator_stake_threshold = "1" + +# Governance parameters. +[gov_params] +# minimum amount of nam token to lock +min_proposal_fund = 500 +# proposal code size in bytes +max_proposal_code_size = 600000 +# min proposal period length in epochs +min_proposal_voting_period = 3 +# max proposal period length in epochs +max_proposal_period = 27 +# maximum number of characters in the proposal content +max_proposal_content_size = 10000 +# minimum epochs between end and grace epoch +min_proposal_grace_epochs = 6 + +# Public goods funding parameters +[pgf_params] +# Initial set of stewards +stewards = ["bertha", "validator-0"] +# The pgf funding inflation rate +pgf_inflation_rate = "0.1" +# The pgf stewards inflation rate +stewards_inflation_rate = "0.01" diff --git a/genesis/localnet/src/pre-genesis/signed-transactions.toml b/genesis/localnet/src/pre-genesis/signed-transactions.toml new file mode 100644 index 0000000000..722e4cc430 --- /dev/null +++ b/genesis/localnet/src/pre-genesis/signed-transactions.toml @@ -0,0 +1,126 @@ +[[established_account]] +alias = "albert" +vp = "vp_user" + +[established_account.public_key] +pk = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +authorization = "sigtest1qzq6dwf2a9dq4hp3nmxfgckh55mulzryxsfkqhr9uvfvmtr9wt38lyyvzvfqryxnat2a4ry6hygv957z683dyngu03gse2uvl5ldfccypktkp6" + +[established_account.storage] + +[[established_account]] +alias = "bertha" +vp = "vp_user" + +[established_account.public_key] +pk = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +authorization = "sigtest1qzt58nd7k2mj647x8x4ydhsjkut7dmsl7yjlnrnwu9kzjdch3cljv6dq05mx2kvwn80kjezh7lz26adc5ksvyn3knufymtkhlmnhg3c8zka3y4" + +[established_account.storage] + +[[established_account]] +alias = "christel" +vp = "vp_user" + +[established_account.public_key] +pk = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +authorization = "sigtest1qrf2l6a5u4ywskryhdueudnm50j3kqhujas3mmktenqm89fmlskjnyeskr2tr7js5swmtqqtenkj6ap9xpelx2w40fjjczc4w9xtdggqjtszy4" + +[established_account.storage] + +[[transfer]] +token = "nam" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qrxfthuz5yzapdt82d8nc656ffrzkxnkfg6fw8w7e6045vcpn8kpr4c536ug4fs2ddz5823dfd3w4qnhcl40qmtasfccaa99a0hvjcqvyy2l02" + +[[transfer]] +token = "btc" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qq826qel8zn5p4dfx25ma45uxeskx0yjlsaxp38uyvehvds6t93c8wkhaydunm4nzz9750dwhkjst00mfrwmu066y3rj6gtk5wjskxq9zvk7ed" + +[[transfer]] +token = "eth" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qzghddk0u6kpqaxhskrj8ah69d3hce95wndgvlpuyvuw0h0fnyrnp026ltvtfmfhpzywkf88yyhv80hzkus57s9nn6vn3ljvmkkf7zgq2nzn40" + +[[transfer]] +token = "dot" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qqlu8kajh0up5zhewd2h44fhq7zdaapqt9xaj8xp7jmdqpv4dl9d5tesw3wyr9su962rvpkf02x27579vqjk4f4advx7t9ejz74cjrc25puna6" + +[[transfer]] +token = "schnitzel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qpukdd6f7pvtgau9v58xxwrnmv5pjra6527aggce9j8cu6fnekzag2cmmf4w0x0cn76gpth2vw6zcsnw37xjuz96hflqfnk5sznxe6cfd5mjnv" + +[[transfer]] +token = "apfel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qprnsyhlruvw4a6jjs98xdd04gxdsgycywzt79gyyak68gn4f50ppdvdm0q6ns44cheu7s43kal0qsldml8s0zexqk3s9zmglu4l6fszwcqswy" + +[[transfer]] +token = "kartoffel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qpg4y6ltax3qfclguym2kpa0ezlkf4hz4u3h8fc2ndmuzm4zfe26ygq9u6nhjjvj93n5v296x4arkvjk22ygee2fx054ce7ap6gvynq2j2k0vf" + +[[transfer]] +token = "nam" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" +signature = "sigtest1qpe3ms76cxvqsa58624ked5ndjr3jr3u7lf9ypalxdh29gajwmsxtzjxzswk0xmza90u47tgksr0d7enpd730ps7fq5u76l7ekxeyvgfnl0qxj" + +[[transfer]] +token = "btc" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" +signature = "sigtest1qqq4yc6s3h4ysezcmgsdgyfsqu8mmrwwc8a358sm864zehx74ums8fmgmkyyr3wz9qqrc5fagwrw0a4mgqkkleumc0u6ynshj5ns0tcdrygupg" + +[[transfer]] +token = "eth" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" +signature = "sigtest1qzyqkdxt3cxf0huv0hzskqmuthw38s3kj2sx50znzc68gzx64hrd3hkyx0g27pruwa9fv42suw3k8gkfaelcp4ymnvfw7ltx0mz2z9qr2lwwcs" + +[[transfer]] +token = "nam" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" +signature = "sigtest1qran2jh0dwuwchh45snhk3p9uyxqym57eg527pcjeekwhvpr5j0fy6dte7x3v2dwwc6vqmv49drpvmk6uhzlt2p8rqx0th3h7smf8rqrcpfxc7" + +[[transfer]] +token = "btc" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" +signature = "sigtest1qrxp6p56qtvsyp7jfs4jru3mkfv6l4t7hpguur72579pqjaexdry9turgmzwzj6qmua3s2y2a4krtuxwaag66j4dpz44aaqksy30tfcxmnhw2k" + +[[transfer]] +token = "eth" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" +signature = "sigtest1qrtrec6mps9p7zhmm7sywxwc7zdm32umt0zdwpuflrkh6952rx74amnsf9hn0yughs4e6czgpdmwsmujvkr4ptk2x24vtc8phz0s2asvtyy9pf" + +[[bond]] +source = "albert" +validator = "validator-0" +amount = "20000" +signature = "sigtest1qpx0kc8rl966vnnswfwhcwxh5vv69p7lev2apc2yephag978gqg5903kuw6dugg9c6pg5jdw048umk72ntympphz0dka74aspldslaqr3q7eqr" diff --git a/genesis/localnet/src/pre-genesis/unsigned-transactions.toml b/genesis/localnet/src/pre-genesis/unsigned-transactions.toml new file mode 100644 index 0000000000..00c82cf511 --- /dev/null +++ b/genesis/localnet/src/pre-genesis/unsigned-transactions.toml @@ -0,0 +1,114 @@ +# This file contains hand-written unsigned transactions for localnet with: +# +# - MASP account +# - Faucet account that allows anyone to withdraw limited amount of tokens +# - 2 established accounts for "Albert" and "Bertha" +# +# Note that 2 more localnet user accounts "Christel" and "Daewon" are left as +# implicit accounts, so their tokens are kept in the accounts derived from their +# keys used in `balances.toml`. +# +# This file is used to produce `signed-transactions.toml` with +# the `sign-genesis-tx` command. + +# Albert +[[established_account]] +alias = "albert" +vp = "vp_user" +public_key = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" + +# Bertha +[[established_account]] +alias = "bertha" +vp = "vp_user" +public_key = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" + +# Christel +[[established_account]] +alias = "christel" +vp = "vp_user" +public_key = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" + +# Transfer all the Albert's tokens into it's established account: +[[transfer]] +token = "NAM" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "BTC" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "ETH" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "DOT" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "Schnitzel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "Apfel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "Kartoffel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" + +[[transfer]] +token = "NAM" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" + +[[transfer]] +token = "BTC" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" + +[[transfer]] +token = "ETH" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" + +[[transfer]] +token = "NAM" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" + +[[transfer]] +token = "BTC" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" + +[[transfer]] +token = "ETH" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" + +[[bond]] +source = "albert" +validator = "validator-0" +amount = "20000" \ No newline at end of file diff --git a/genesis/localnet/src/pre-genesis/validator-0/transactions.toml b/genesis/localnet/src/pre-genesis/validator-0/transactions.toml new file mode 100644 index 0000000000..571aa7e523 --- /dev/null +++ b/genesis/localnet/src/pre-genesis/validator-0/transactions.toml @@ -0,0 +1,44 @@ +[[validator_account]] +alias = "validator-0" +dkg_key = "dpktest1vqqqqqqndwmp3cvzfuepzvdh3fmt4dvjwd4vx6ffxk6t4vm6waysng2q8zh3yu8hdnxhwnwq22p8fwaqlyzcggef6k6qv3h3qtyy8c8c686w50v7d0z49ufd6zje46ujzc4ew5z7sdh45d94cvywy2v40yz9jlckh8cksr" +vp = "vp_validator" +commission_rate = "0.05" +max_commission_rate_change = "0.01" +net_address = "127.0.0.1:27656" + +[validator_account.account_key] +pk = "pktest1qpqznlesv6fcgsz9x4rsn7l4dsnp7h5sswvs9atw0zgq3uwp0jll2r42szz" +authorization = "sigtest1qrcy6a095uj6n8ryc9fkfw3hn2jn285g82hdratzrvmg22xp5qdmyr7e9q97t77v4zr8a73x4c3k53pcec3mp4ugt5jqd2w0fvd7nhcp0ajsk4" + +[validator_account.consensus_key] +pk = "pktest1qpk2a5m26cgnrp32l2c2hss03draxwnctu92llhlr6kjesn2kt892k2kklp" +authorization = "sigtest1qzajuey408zf66wgw8x3awd5jx9x249csx63n80clehj238qqpsf9cdr630edqjr8fw95vywlngy368r087sge2xy89qpcsrxvwtm3gr0h6g79" + +[validator_account.protocol_key] +pk = "pktest1qpfv9pj4zmelcg98rtw8wezztzdgpuczuxjmx946klkxajmvvcxjz3vsv63" +authorization = "sigtest1qz0aerskgtzwmgxwgml72zyz4974njfz6s9dl7r399d0v64pzsmctkwe8f28dwyef52hklj6r2fqmeypncvv3qg2x4hg4u3g23kw23qt73uzmy" + +[validator_account.tendermint_node_key] +pk = "pktest1qp7shxxfdkp8ft8xfkgf2vqy7u6effdp82w0cq6aq8575txul33t63dyd68" +authorization = "sigtest1qq87l6zjuswmrfeysxp8j0tu5zlklnswzaqgcuyaq23p49hhvupa57r3rwrh3txfuvq8lh5e7wu7d90js8cn7a7dygs7v5yx2a9ljtqxwl0chr" + +[validator_account.eth_hot_key] +pk = "pktest1qypdzza7uqtklzzs50hhy07h5ru9p0v8y5666wwulqnd3mdpuj0tcxsgck694" +authorization = "sigtest1q9qtzvwlqhnft5jgeyms2w757dlly8rjg4g89593ayhan0fx7uvjzvm7r7ltgsly9tp6783pqpl6ezxty240udrrknxmunymz6lm523qqqwx72ra" + +[validator_account.eth_cold_key] +pk = "pktest1qypzca0n6l890jc83nk5lljuzps04xmfccz3f87xc6ez40t2wvql48s52cx8r" +authorization = "sigtest1q8qmjstc2jtt3dwueepv0njk9te5qpj6z9yj6w0duhagswz80vyws3ywdsy9n49qnjecvnpxwlna3578vn423tgd4rrtss06ejgkj7c5qyv22nm6" + +[[transfer]] +token = "nam" +source = "pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup" +target = "validator-0" +amount = "200000" +signature = "sigtest1qz8ch7n6jp7g7hvnzkhmprhqc5umd0gatarlttm7gpcdx6g8hmgqffdq67fn6vzcflmym5xej3m5jmfw7nap00s4nlx2zxrlwz0wlfgts9kera" + +[[bond]] +source = "validator-0" +validator = "validator-0" +amount = "100000" +signature = "sigtest1qpss2x7vewnr2sr2wxj0ftldcmvv7v7t7qsr26k2l2hx4gls7y9sksrcq0m9vv2ymdqqxgmmw0dmuwazflxuqt4v2p2wng4endtcfcc0v6f6tj" diff --git a/genesis/localnet/src/pre-genesis/validator-0/validator-wallet.toml b/genesis/localnet/src/pre-genesis/validator-0/validator-wallet.toml new file mode 100644 index 0000000000..b6cc8fea5f --- /dev/null +++ b/genesis/localnet/src/pre-genesis/validator-0/validator-wallet.toml @@ -0,0 +1,11 @@ +account_key = "unencrypted:00dba003e446e56fb3382aaea01f2aab00dd7ea10a8c6725245b9542bc22c51519" +consensus_key = "unencrypted:00c229df6767b29891a1d45285450d8588ee0bc6b61d294bf958001cd7d93fea4e" +eth_cold_key = "unencrypted:0173220e1258baf4bd4851bbdd41e3fc5167ceb0cbcd28d3d1d0d250f7563b2350" +tendermint_node_key = "unencrypted:0036389d415fdb91c224fc6d42a301f545cf9ea879f2ebe548fd31179341e4e318" + +[validator_keys] +protocol_keypair = "ED25519_SK_PREFIX00d95753fab39f87e5c20ecab12690fefa67e97164ca6f17d67aeba50883b93dde" +eth_bridge_keypair = "SECP256K1_SK_PREFIX01263b5b517759fc30c3abed13907b75e779dae70e3675135a788de7c7076efebf" + +[validator_keys.dkg_keypair] +decryption_key = [208, 36, 153, 32, 179, 193, 163, 222, 29, 238, 154, 53, 181, 71, 213, 162, 59, 130, 225, 93, 57, 20, 161, 254, 52, 1, 172, 184, 112, 189, 160, 102] diff --git a/genesis/localnet/src/pre-genesis/wallet.toml b/genesis/localnet/src/pre-genesis/wallet.toml new file mode 100644 index 0000000000..2444ba3f73 --- /dev/null +++ b/genesis/localnet/src/pre-genesis/wallet.toml @@ -0,0 +1,34 @@ +[view_keys] + +[spend_keys] + +[payment_addrs] + +[keys] +albert-key = "unencrypted:0083318ccceac6c08a0177667840b4b93f0e455e45d4c38c28b73b8f8462fbf548" +bertha-key = "unencrypted:0073ed61817720d27784e7a9bca4a60668d763a6f7ecac2d45ed1f241aa5c59e99" +christel-key = "unencrypted:003625b939a58ef60402d7cf8fc04250026cc1ba90cc3028af1ce6b22be857ffc7" +daewon = "unencrypted:00d19e226c0e7d123d79f5908b5948d4c461b66a5f8aa95600c28b55ab6f5dc772" +ester = "unencrypted:01369093e2035d84f72a7e5a17c89b7a938b5d08cc87b2289805e3afcc66ef9a42" +faucet-key = "unencrypted:00548aa8393422b88dce5f4be8ee0320638061c3e0649ada1b0dacbec4c0c75bb2" +validator-0-balance-key = "unencrypted:000b9c8cb8f3ad6b8a387b064a11c0e98189814e9733aa7bb1e802425f6356f98a" + +[addresses] +albert-key = "atest1d9khqw36x56rgvphx5erjwp48y6y2s6y89qnwdzygce5xsfkx9rrsd35xerrjdp3gsunz33n3zytee" +bertha-key = "atest1d9khqw36xvm5zwpjxqm5zwz9x56rydp5gscnwsf4geznjdpegvmrwv29xcmnxvp4x5u5vdjp4wcjdf" +christel-key = "atest1d9khqw36gccrsv348qurzvpcx3p52ve3xu6rys3kxc6yxsjrgymyg32xggmnxvj9g5erzv2rn6pwv6" +daewon = "atest1d9khqw36xuc5gsfegsen2sfsx4q5yvz9xq6yv3psgcunj334g9prvdzzxucnyv2zxqm5xve44x97gg" +ester = "atest1d9khqw36xumnzd3cxccyx33k8ymyy3f5x3z5zd3jx5crsdfcgezyyv6rg3z5vw2xxcenqdpez4uxkf" +faucet-key = "atest1d9khqw368ycns3jp8q6nx329g5myg3zyggu523f589zyzs6x8pq5g3zzx9pr2d3h8qmrjdjyvvty45" +validator-0-balance-key = "atest1d9khqw36geqnxw29x9q5xvj98qmnj3fjgscygs6pg4p52s6z89qnzd3exgmnsv34gfqnj33elp2296" + +[pkhs] +37A8207A8E54244D17A5FE949C671E6730559F6A = "bertha-key" +544075298594ECD9A74DF3CA61F8646F941D91F3 = "albert-key" +71DA9D35A05AB0E04FD0F99F5AB64B7121B07C35 = "daewon" +918FA853EEE6DDDB9EE49DACF8ADDB1B5678696D = "faucet-key" +F0825881084CE31742B664CBCA6DEFB732EE211C = "christel-key" +7716860CF696BE44EA6250858FDB3CDEF9F63049 = "ester" +FA39E1AC2E879E2D0DCAECECB9A16927825BA9F9 = "validator-0-balance-key" + +[address_vp_types] diff --git a/genesis/localnet/tokens.toml b/genesis/localnet/tokens.toml new file mode 100644 index 0000000000..3079fab700 --- /dev/null +++ b/genesis/localnet/tokens.toml @@ -0,0 +1,22 @@ +# Token accounts with their validity predicates + +[token.NAM] +denom = 6 + +[token.BTC] +denom = 8 + +[token.ETH] +denom = 18 + +[token.DOT] +denom = 10 + +[token.Schnitzel] +denom = 6 + +[token.Apfel] +denom = 6 + +[token.Kartoffel] +denom = 6 \ No newline at end of file diff --git a/genesis/localnet/transactions.toml b/genesis/localnet/transactions.toml new file mode 100644 index 0000000000..9a4132d95e --- /dev/null +++ b/genesis/localnet/transactions.toml @@ -0,0 +1,180 @@ +# Transactions pasted from: +# +# 1. `src/pre-genesis/validator-0/transactions.toml` +# 2. `src/pre-genesis/signed-transactions.toml` + +# 1. + +[[validator_account]] +alias = "validator-0" +dkg_key = "dpktest1vqqqqqqndwmp3cvzfuepzvdh3fmt4dvjwd4vx6ffxk6t4vm6waysng2q8zh3yu8hdnxhwnwq22p8fwaqlyzcggef6k6qv3h3qtyy8c8c686w50v7d0z49ufd6zje46ujzc4ew5z7sdh45d94cvywy2v40yz9jlckh8cksr" +vp = "vp_validator" +commission_rate = "0.05" +max_commission_rate_change = "0.01" +net_address = "127.0.0.1:27656" + +[validator_account.account_key] +pk = "pktest1qpqznlesv6fcgsz9x4rsn7l4dsnp7h5sswvs9atw0zgq3uwp0jll2r42szz" +authorization = "sigtest1qrcy6a095uj6n8ryc9fkfw3hn2jn285g82hdratzrvmg22xp5qdmyr7e9q97t77v4zr8a73x4c3k53pcec3mp4ugt5jqd2w0fvd7nhcp0ajsk4" + +[validator_account.consensus_key] +pk = "pktest1qpk2a5m26cgnrp32l2c2hss03draxwnctu92llhlr6kjesn2kt892k2kklp" +authorization = "sigtest1qzajuey408zf66wgw8x3awd5jx9x249csx63n80clehj238qqpsf9cdr630edqjr8fw95vywlngy368r087sge2xy89qpcsrxvwtm3gr0h6g79" + +[validator_account.protocol_key] +pk = "pktest1qpfv9pj4zmelcg98rtw8wezztzdgpuczuxjmx946klkxajmvvcxjz3vsv63" +authorization = "sigtest1qz0aerskgtzwmgxwgml72zyz4974njfz6s9dl7r399d0v64pzsmctkwe8f28dwyef52hklj6r2fqmeypncvv3qg2x4hg4u3g23kw23qt73uzmy" + +[validator_account.tendermint_node_key] +pk = "pktest1qp7shxxfdkp8ft8xfkgf2vqy7u6effdp82w0cq6aq8575txul33t63dyd68" +authorization = "sigtest1qq87l6zjuswmrfeysxp8j0tu5zlklnswzaqgcuyaq23p49hhvupa57r3rwrh3txfuvq8lh5e7wu7d90js8cn7a7dygs7v5yx2a9ljtqxwl0chr" + +[validator_account.eth_hot_key] +pk = "pktest1qypdzza7uqtklzzs50hhy07h5ru9p0v8y5666wwulqnd3mdpuj0tcxsgck694" +authorization = "sigtest1q9qtzvwlqhnft5jgeyms2w757dlly8rjg4g89593ayhan0fx7uvjzvm7r7ltgsly9tp6783pqpl6ezxty240udrrknxmunymz6lm523qqqwx72ra" + +[validator_account.eth_cold_key] +pk = "pktest1qypzca0n6l890jc83nk5lljuzps04xmfccz3f87xc6ez40t2wvql48s52cx8r" +authorization = "sigtest1q8qmjstc2jtt3dwueepv0njk9te5qpj6z9yj6w0duhagswz80vyws3ywdsy9n49qnjecvnpxwlna3578vn423tgd4rrtss06ejgkj7c5qyv22nm6" + +[[transfer]] +token = "nam" +source = "pktest1qzp22w6trhmxmp2dx5h883c2l684z0e20a9egusmxaat62wvaa4agcthpup" +target = "validator-0" +amount = "200000" +signature = "sigtest1qz8ch7n6jp7g7hvnzkhmprhqc5umd0gatarlttm7gpcdx6g8hmgqffdq67fn6vzcflmym5xej3m5jmfw7nap00s4nlx2zxrlwz0wlfgts9kera" + +[[bond]] +source = "validator-0" +validator = "validator-0" +amount = "100000" +signature = "sigtest1qpss2x7vewnr2sr2wxj0ftldcmvv7v7t7qsr26k2l2hx4gls7y9sksrcq0m9vv2ymdqqxgmmw0dmuwazflxuqt4v2p2wng4endtcfcc0v6f6tj" + +# 2. + +[[established_account]] +alias = "albert" +vp = "vp_user" + +[established_account.public_key] +pk = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +authorization = "sigtest1qzq6dwf2a9dq4hp3nmxfgckh55mulzryxsfkqhr9uvfvmtr9wt38lyyvzvfqryxnat2a4ry6hygv957z683dyngu03gse2uvl5ldfccypktkp6" + +[established_account.storage] + +[[established_account]] +alias = "bertha" +vp = "vp_user" + +[established_account.public_key] +pk = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +authorization = "sigtest1qzt58nd7k2mj647x8x4ydhsjkut7dmsl7yjlnrnwu9kzjdch3cljv6dq05mx2kvwn80kjezh7lz26adc5ksvyn3knufymtkhlmnhg3c8zka3y4" + +[established_account.storage] + +[[established_account]] +alias = "christel" +vp = "vp_user" + +[established_account.public_key] +pk = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +authorization = "sigtest1qrf2l6a5u4ywskryhdueudnm50j3kqhujas3mmktenqm89fmlskjnyeskr2tr7js5swmtqqtenkj6ap9xpelx2w40fjjczc4w9xtdggqjtszy4" + +[established_account.storage] + +[[transfer]] +token = "nam" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qrxfthuz5yzapdt82d8nc656ffrzkxnkfg6fw8w7e6045vcpn8kpr4c536ug4fs2ddz5823dfd3w4qnhcl40qmtasfccaa99a0hvjcqvyy2l02" + +[[transfer]] +token = "btc" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qq826qel8zn5p4dfx25ma45uxeskx0yjlsaxp38uyvehvds6t93c8wkhaydunm4nzz9750dwhkjst00mfrwmu066y3rj6gtk5wjskxq9zvk7ed" + +[[transfer]] +token = "eth" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qzghddk0u6kpqaxhskrj8ah69d3hce95wndgvlpuyvuw0h0fnyrnp026ltvtfmfhpzywkf88yyhv80hzkus57s9nn6vn3ljvmkkf7zgq2nzn40" + +[[transfer]] +token = "dot" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qqlu8kajh0up5zhewd2h44fhq7zdaapqt9xaj8xp7jmdqpv4dl9d5tesw3wyr9su962rvpkf02x27579vqjk4f4advx7t9ejz74cjrc25puna6" + +[[transfer]] +token = "schnitzel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qpukdd6f7pvtgau9v58xxwrnmv5pjra6527aggce9j8cu6fnekzag2cmmf4w0x0cn76gpth2vw6zcsnw37xjuz96hflqfnk5sznxe6cfd5mjnv" + +[[transfer]] +token = "apfel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qprnsyhlruvw4a6jjs98xdd04gxdsgycywzt79gyyak68gn4f50ppdvdm0q6ns44cheu7s43kal0qsldml8s0zexqk3s9zmglu4l6fszwcqswy" + +[[transfer]] +token = "kartoffel" +source = "pktest1qz0aphcsrw37j8fy742cjwhphu9jwx7esd3ad4xxtxrkwv07ff63wnnul44" +target = "albert" +amount = "1000000" +signature = "sigtest1qpg4y6ltax3qfclguym2kpa0ezlkf4hz4u3h8fc2ndmuzm4zfe26ygq9u6nhjjvj93n5v296x4arkvjk22ygee2fx054ce7ap6gvynq2j2k0vf" + +[[transfer]] +token = "nam" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" +signature = "sigtest1qpe3ms76cxvqsa58624ked5ndjr3jr3u7lf9ypalxdh29gajwmsxtzjxzswk0xmza90u47tgksr0d7enpd730ps7fq5u76l7ekxeyvgfnl0qxj" + +[[transfer]] +token = "btc" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" +signature = "sigtest1qqq4yc6s3h4ysezcmgsdgyfsqu8mmrwwc8a358sm864zehx74ums8fmgmkyyr3wz9qqrc5fagwrw0a4mgqkkleumc0u6ynshj5ns0tcdrygupg" + +[[transfer]] +token = "eth" +source = "pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a" +target = "bertha" +amount = "1000000" +signature = "sigtest1qzyqkdxt3cxf0huv0hzskqmuthw38s3kj2sx50znzc68gzx64hrd3hkyx0g27pruwa9fv42suw3k8gkfaelcp4ymnvfw7ltx0mz2z9qr2lwwcs" + +[[transfer]] +token = "nam" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" +signature = "sigtest1qran2jh0dwuwchh45snhk3p9uyxqym57eg527pcjeekwhvpr5j0fy6dte7x3v2dwwc6vqmv49drpvmk6uhzlt2p8rqx0th3h7smf8rqrcpfxc7" + +[[transfer]] +token = "btc" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" +signature = "sigtest1qrxp6p56qtvsyp7jfs4jru3mkfv6l4t7hpguur72579pqjaexdry9turgmzwzj6qmua3s2y2a4krtuxwaag66j4dpz44aaqksy30tfcxmnhw2k" + +[[transfer]] +token = "eth" +source = "pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2" +target = "christel" +amount = "1000000" +signature = "sigtest1qrtrec6mps9p7zhmm7sywxwc7zdm32umt0zdwpuflrkh6952rx74amnsf9hn0yughs4e6czgpdmwsmujvkr4ptk2x24vtc8phz0s2asvtyy9pf" + +[[bond]] +source = "albert" +validator = "validator-0" +amount = "20000" +signature = "sigtest1qpx0kc8rl966vnnswfwhcwxh5vv69p7lev2apc2yephag978gqg5903kuw6dugg9c6pg5jdw048umk72ntympphz0dka74aspldslaqr3q7eqr" diff --git a/genesis/localnet/validity-predicates.toml b/genesis/localnet/validity-predicates.toml new file mode 100644 index 0000000000..a0c2570121 --- /dev/null +++ b/genesis/localnet/validity-predicates.toml @@ -0,0 +1,17 @@ +# WASM Validity predicate that can be used for genesis accounts + +# Implicit VP +[wasm.vp_implicit] +filename = "vp_implicit.wasm" + +# Default user VP in established accounts +[wasm.vp_user] +filename = "vp_user.wasm" + +# Default validator VP +[wasm.vp_validator] +filename = "vp_validator.wasm" + +# MASP VP +[wasm.vp_masp] +filename = "vp_masp.wasm" diff --git a/genesis/starter/README.md b/genesis/starter/README.md new file mode 100644 index 0000000000..ef58236340 --- /dev/null +++ b/genesis/starter/README.md @@ -0,0 +1,71 @@ +# Starter genesis templates + +This directory contains genesis templates for a minimum configuration with a single token account, which can be used as a starter for setting up a new chain. + +If you're modifying any of the files here, you can run this to ensure that the changes are valid: + +```shell +cargo watch -x "test test_validate_starter_genesis_templates" +``` + +In order to be able to run it, the following has to be added: + +1. At least one key with a positive native [token balance](#token-balances) +2. At least one [validator account](#validator-accounts), with some native tokens transferred from the key, some of which have to be self-bonded in PoS to amount to a positive voting power + +## Token balances + +We'll generate a key and give it some token balance. + +To generate a new key in a pre-genesis wallet (before the chain is setup), you can use e.g.: + +```shell +namada wallet key gen --pre-genesis --alias "my-key" +``` + +This will print your public key: + +```shell +Successfully added a key and an address with alias: "my-key". +Public key: pktest1qz5ywdn47sdm8s7rkzjl5dud0k9c9ndd5agn4gu0u0ryrmtmyuxmk948q0p +``` + +The public key can then be given some tokens in the [balances.toml file](balances.toml) with e.g.: + +```toml +[token.NAM] +pktest1qz5ywdn47sdm8s7rkzjl5dud0k9c9ndd5agn4gu0u0ryrmtmyuxmk948q0p = 1_337_707.50 +``` + +## Validator accounts + +For this step, you'll need to have a key with some native [token balance](#token-balances), from which you can sign the validator account creation genesis transaction. + +To generate a new validator pre-genesis wallet and produce signed transactions with it, use e.g.: + +```shell +namada client utils \ + init-genesis-validator \ + --source "my-key" \ + --alias "my-validator" \ + --net-address "127.0.0.1:26656" \ + --commission-rate 0.05 \ + --max-commission-rate-change 0.01 \ + --transfer-from-source-amount 1337707.50 \ + --self-bond-amount 1000000 +``` + +This will print the validator transactions that can be added to the [transactions.toml file](transactions.toml). + +## Initialize the chain + +This is sufficient minimal configuration to initialize the chain with the single genesis validator. All that's left is to pick a chain ID prefix and genesis time: + +```shell +namada client utils \ + init-network \ + --chain-prefix "my-chain" \ + --genesis-time "2021-12-31T00:00:00Z" \ + --templates-path "path/to/templates" \ + --wasm-checksums-path "path/to/wasm/checksums.json" +``` diff --git a/genesis/starter/balances.toml b/genesis/starter/balances.toml new file mode 100644 index 0000000000..2f1aad4022 --- /dev/null +++ b/genesis/starter/balances.toml @@ -0,0 +1,2 @@ +[token.NAM] +# assign balance to public keys diff --git a/genesis/starter/parameters.toml b/genesis/starter/parameters.toml new file mode 100644 index 0000000000..86714f827c --- /dev/null +++ b/genesis/starter/parameters.toml @@ -0,0 +1,90 @@ +# General protocol parameters. +[parameters] +native_token = "NAM" +# Minimum number of blocks in an epoch. +min_num_of_blocks = 4 +# Maximum expected time per block (in seconds). +max_expected_time_per_block = 30 +# Max payload size, in bytes, for a tx batch proposal. +max_proposal_bytes = 22020096 +# vp whitelist +vp_whitelist = [] +# tx whitelist +tx_whitelist = [] +# Implicit VP WASM name +implicit_vp = "vp_implicit" +# Expected number of epochs per year (also sets the min duration of an epoch in seconds) +epochs_per_year = 31_536_000 +# The P gain factor in the Proof of Stake rewards controller +pos_gain_p = "0.1" +# The D gain factor in the Proof of Stake rewards controller +pos_gain_d = "0.1" +# Maximum number of signature per transaction +max_signatures_per_transaction = 15 +# Max gas for block +max_block_gas = 20000000 +# Fee unshielding gas limit +fee_unshielding_gas_limit = 20000 +# Fee unshielding descriptions limit +fee_unshielding_descriptions_limit = 15 + +# Map of the cost per gas unit for every token allowed for fee payment +[parameters.minimum_gas_price] +nam = "0.000001" + +# Proof of stake parameters. +[pos_params] +# Maximum number of active validators. +max_validator_slots = 128 +# Pipeline length (in epochs). Any change in the validator set made in +# epoch 'n' will become active in epoch 'n + pipeline_len'. +pipeline_len = 2 +# Unbonding length (in epochs). Validators may have their stake slashed +# for a fault in epoch 'n' up through epoch 'n + unbonding_len'. +unbonding_len = 3 +# Votes per fundamental staking token (namnam) +tm_votes_per_token = "1" +# Reward for proposing a block. +block_proposer_reward = "0.125" +# Reward for voting on a block. +block_vote_reward = "0.1" +# Maximum inflation rate per annum (10%) +max_inflation_rate = "0.1" +# Targeted ratio of staked tokens to total tokens in the supply +target_staked_ratio = "0.6667" +# Portion of a validator's stake that should be slashed on a duplicate +# vote. +duplicate_vote_min_slash_rate = "0.001" +# Portion of a validator's stake that should be slashed on a light +# client attack. +light_client_attack_min_slash_rate = "0.001" +# Number of epochs above and below (separately) the current epoch to +# consider when doing cubic slashing +cubic_slashing_window_length = 1 +# The minimum amount of bonded tokens that a validator needs to be in +# either the `consensus` or `below_capacity` validator sets +validator_stake_threshold = "1" + +# Governance parameters. +[gov_params] +# minimum amount of nam token to lock +min_proposal_fund = 500 +# proposal code size in bytes +max_proposal_code_size = 300000 +# min proposal period length in epochs +min_proposal_voting_period = 3 +# max proposal period length in epochs +max_proposal_period = 27 +# maximum number of characters in the proposal content +max_proposal_content_size = 10000 +# minimum epochs between end and grace epoch +min_proposal_grace_epochs = 6 + +# Public goods funding parameters +[pgf_params] +# Initial set of stewards +stewards = [] +# The pgf funding inflation rate +pgf_inflation_rate = "0.1" +# The pgf stewards inflation rate +stewards_inflation_rate = "0.01" diff --git a/genesis/starter/tokens.toml b/genesis/starter/tokens.toml new file mode 100644 index 0000000000..612a6901ff --- /dev/null +++ b/genesis/starter/tokens.toml @@ -0,0 +1,5 @@ +# Token accounts with their validity predicates + +[token.NAM] +vp = "vp_token" +denom = 6 \ No newline at end of file diff --git a/genesis/starter/transactions.toml b/genesis/starter/transactions.toml new file mode 100644 index 0000000000..e9406c2f03 --- /dev/null +++ b/genesis/starter/transactions.toml @@ -0,0 +1 @@ +# Genesis transactions \ No newline at end of file diff --git a/genesis/starter/validity-predicates.toml b/genesis/starter/validity-predicates.toml new file mode 100644 index 0000000000..863ec7ec38 --- /dev/null +++ b/genesis/starter/validity-predicates.toml @@ -0,0 +1,22 @@ +# WASM Validity predicate that can be used for genesis accounts + +# Implicit VP +[wasm.vp_implicit] +filename = "vp_implicit.wasm" + +# Default user VP in established accounts +[wasm.vp_user] +filename = "vp_user.wasm" + +# Default validator VP +[wasm.vp_validator] +filename = "vp_validator.wasm" + +# Token VP +[wasm.vp_token] +filename = "vp_token.wasm" + +# MASP VP +[wasm.vp_masp] +filename = "vp_masp.wasm" + diff --git a/proof_of_stake/src/epoched.rs b/proof_of_stake/src/epoched.rs index 796827de1f..c35b310d0c 100644 --- a/proof_of_stake/src/epoched.rs +++ b/proof_of_stake/src/epoched.rs @@ -1399,7 +1399,7 @@ mod test { let mut s = TestWlStorage::default(); let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); gov_params.init_storage(&mut s)?; - crate::init_genesis( + crate::test_utils::init_genesis_helper( &mut s, &PosParams::default(), [GenesisValidator { diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 0aa24142fc..618b20a373 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -60,7 +60,7 @@ use types::{ BondsAndUnbondsDetail, BondsAndUnbondsDetails, CommissionRates, ConsensusValidator, ConsensusValidatorSet, ConsensusValidatorSets, DelegatorRedelegatedBonded, DelegatorRedelegatedUnbonded, - EagerRedelegatedBondsMap, EpochedSlashes, GenesisValidator, + EagerRedelegatedBondsMap, EpochedSlashes, IncomingRedelegations, OutgoingRedelegations, Position, RedelegatedBondsOrUnbonds, RedelegatedTokens, ReverseOrdTokenAmount, RewardsAccumulator, RewardsProducts, Slash, SlashType, SlashedAmount, @@ -298,128 +298,38 @@ pub fn delegator_redelegated_unbonds_handle( pub fn init_genesis( storage: &mut S, params: &OwnedPosParams, - validators: impl Iterator + Clone, - current_epoch: namada_core::types::storage::Epoch, + current_epoch: Epoch, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { tracing::debug!("Initializing PoS genesis"); write_pos_params(storage, params)?; - let params = read_non_pos_owned_params(storage, params.clone())?; - let mut total_bonded = token::Amount::zero(); consensus_validator_set_handle().init(storage, current_epoch)?; below_capacity_validator_set_handle().init(storage, current_epoch)?; validator_set_positions_handle().init(storage, current_epoch)?; validator_addresses_handle().init(storage, current_epoch)?; + tracing::debug!("Finished genesis"); + Ok(()) +} - for GenesisValidator { - address, - tokens, - consensus_key, - protocol_key, - eth_cold_key, - eth_hot_key, - commission_rate, - max_commission_rate_change, - } in validators - { - // This will fail if the key is already being used - the uniqueness must - // be enforced in the genesis configuration to prevent it - try_insert_consensus_key(storage, &consensus_key)?; - - total_bonded += tokens; - - // Insert the validator into a validator set and write its epoched - // validator data - insert_validator_into_validator_set( - storage, - ¶ms, - &address, - tokens, - current_epoch, - 0, - )?; - - validator_addresses_handle() - .at(¤t_epoch) - .insert(storage, address.clone())?; - - // Write other validator data to storage - write_validator_address_raw_hash(storage, &address, &consensus_key)?; - write_validator_max_commission_rate_change( - storage, - &address, - max_commission_rate_change, - )?; - validator_consensus_key_handle(&address).init_at_genesis( - storage, - consensus_key, - current_epoch, - )?; - validator_protocol_key_handle(&address).init_at_genesis( - storage, - protocol_key, - current_epoch, - )?; - validator_eth_hot_key_handle(&address).init_at_genesis( - storage, - eth_hot_key, - current_epoch, - )?; - validator_eth_cold_key_handle(&address).init_at_genesis( - storage, - eth_cold_key, - current_epoch, - )?; - validator_deltas_handle(&address).init_at_genesis( - storage, - tokens.change(), - current_epoch, - )?; - bond_handle(&address, &address).init_at_genesis( - storage, - tokens, - current_epoch, - )?; - total_bonded_handle(&address).init_at_genesis( - storage, - tokens, - current_epoch, - )?; - validator_commission_rate_handle(&address).init_at_genesis( - storage, - commission_rate, - current_epoch, - )?; - } - - // Store the total consensus validator stake to storage - store_total_consensus_stake(storage, current_epoch)?; - - // Write total deltas to storage - total_deltas_handle().init_at_genesis( - storage, - token::Change::from(total_bonded), - current_epoch, - )?; +/// new init genesis +pub fn copy_genesis_validator_sets( + storage: &mut S, + params: &OwnedPosParams, + current_epoch: Epoch, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let params = read_non_pos_owned_params(storage, params.clone())?; - // Credit bonded token amount to the PoS account - let staking_token = staking_token_address(storage); - token::credit_tokens(storage, &staking_token, &ADDRESS, total_bonded)?; - // Copy the genesis validator set into the pipeline epoch as well + // Copy the genesis validator sets up to the pipeline epoch for epoch in (current_epoch.next()).iter_range(params.pipeline_len) { - copy_validator_sets_and_positions( - storage, - ¶ms, - current_epoch, - epoch, - )?; + copy_validator_sets_and_positions(storage, ¶ms, current_epoch, epoch)?; + store_total_consensus_stake(storage, epoch)?; } - - tracing::debug!("Genesis initialized"); - Ok(()) } @@ -600,15 +510,17 @@ where /// Add or remove PoS validator's stake delta value pub fn update_validator_deltas( storage: &mut S, + params: &OwnedPosParams, validator: &Address, delta: token::Change, current_epoch: namada_core::types::storage::Epoch, - offset: u64, + offset_opt: Option, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { let handle = validator_deltas_handle(validator); + let offset = offset_opt.unwrap_or(params.pipeline_len); let val = handle .get_delta_val(storage, current_epoch + offset)? .unwrap_or_default(); @@ -781,14 +693,16 @@ where /// Note: for EpochedDelta, write the value to change storage by pub fn update_total_deltas( storage: &mut S, + params: &OwnedPosParams, delta: token::Change, current_epoch: namada_core::types::storage::Epoch, - offset: u64, + offset_opt: Option, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, { let handle = total_deltas_handle(); + let offset = offset_opt.unwrap_or(params.pipeline_len); let val = handle .get_delta_val(storage, current_epoch + offset)? .unwrap_or_default(); @@ -848,6 +762,7 @@ pub fn bond_tokens( validator: &Address, amount: token::Amount, current_epoch: Epoch, + offset_opt: Option, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -861,9 +776,8 @@ where } let params = read_pos_params(storage)?; - let pipeline_epoch = current_epoch + params.pipeline_len; - - // Check that the source is not a validator + let offset = offset_opt.unwrap_or(params.pipeline_len); + let offset_epoch = current_epoch + offset; if let Some(source) = source { if source != validator && is_validator(storage, source)? { return Err( @@ -874,7 +788,7 @@ where // Check that the validator is actually a validator let validator_state_handle = validator_state_handle(validator); - let state = validator_state_handle.get(storage, pipeline_epoch, ¶ms)?; + let state = validator_state_handle.get(storage, offset_epoch, ¶ms)?; if state.is_none() { return Err(BondError::NotAValidator(validator.clone()).into()); } @@ -887,7 +801,7 @@ where // Check that validator is not inactive at anywhere between the current // epoch and pipeline offset - for epoch in current_epoch.iter_range(params.pipeline_len) { + for epoch in current_epoch.iter_range(offset) { if let Some(ValidatorState::Inactive) = validator_state_handle.get(storage, epoch, ¶ms)? { @@ -901,12 +815,12 @@ where } // Initialize or update the bond at the pipeline offset - bond_handle.add(storage, amount, current_epoch, params.pipeline_len)?; + bond_handle.add(storage, amount, current_epoch, offset)?; total_bonded_handle.add( storage, amount, current_epoch, - params.pipeline_len, + offset, )?; if tracing::level_enabled!(tracing::Level::DEBUG) { @@ -919,7 +833,7 @@ where // must be no changes to the validator set. Check at the pipeline epoch. let is_jailed_at_pipeline = matches!( validator_state_handle - .get(storage, pipeline_epoch, ¶ms)? + .get(storage, offset_epoch, ¶ms)? .unwrap(), ValidatorState::Jailed ); @@ -929,24 +843,27 @@ where ¶ms, validator, amount.change(), - pipeline_epoch, + offset_epoch, + offset_opt, )?; } // Update the validator and total deltas update_validator_deltas( storage, + ¶ms, validator, amount.change(), current_epoch, - params.pipeline_len, + offset_opt, )?; update_total_deltas( storage, + ¶ms, amount.change(), current_epoch, - params.pipeline_len, + offset_opt, )?; // Transfer the bonded tokens from the source to PoS @@ -1068,7 +985,8 @@ fn update_validator_set( params: &PosParams, validator: &Address, token_change: token::Change, - epoch: Epoch, + current_epoch: Epoch, + offset: Option, ) -> storage_api::Result<()> where S: StorageRead + StorageWrite, @@ -1076,7 +994,8 @@ where if token_change.is_zero() { return Ok(()); } - // let pipeline_epoch = current_epoch + params.pipeline_len; + let offset = offset.unwrap_or(params.pipeline_len); + let epoch = current_epoch + offset; tracing::debug!( "Update epoch for validator set: {epoch}, validator: {validator}" ); @@ -1140,8 +1059,8 @@ where validator_state_handle(validator).set( storage, ValidatorState::BelowThreshold, - epoch, - 0, + current_epoch, + offset, )?; // Remove the validator's position from storage @@ -1177,8 +1096,8 @@ where validator_state_handle(&removed_max_below_capacity).set( storage, ValidatorState::Consensus, - epoch, - 0, + current_epoch, + offset, )?; } } else if tokens_post < max_below_capacity_validator_amount { @@ -1212,8 +1131,8 @@ where validator_state_handle(&removed_max_below_capacity).set( storage, ValidatorState::Consensus, - epoch, - 0, + current_epoch, + offset, )?; // Insert the current validator into the below-capacity set @@ -1226,8 +1145,8 @@ where validator_state_handle(validator).set( storage, ValidatorState::BelowCapacity, - epoch, - 0, + current_epoch, + offset, )?; } else { tracing::debug!("Validator remains in consensus set"); @@ -1270,7 +1189,8 @@ where validator, tokens_post, min_consensus_validator_amount, - epoch, + current_epoch, + offset, &consensus_val_handle, &below_capacity_val_handle, )?; @@ -1286,8 +1206,8 @@ where validator_state_handle(validator).set( storage, ValidatorState::BelowCapacity, - epoch, - 0, + current_epoch, + offset, )?; } else { // The current validator is demoted to the below-threshold set @@ -1298,8 +1218,8 @@ where validator_state_handle(validator).set( storage, ValidatorState::BelowThreshold, - epoch, - 0, + current_epoch, + offset, )?; // Remove the validator's position from storage @@ -1309,9 +1229,12 @@ where } } } else { - // If there is no position at pipeline offset, then the validator must - // be in the below-threshold set - debug_assert!(tokens_pre < params.validator_stake_threshold); + // At non-zero offset (0 is genesis only) + if offset > 0 { + // If there is no position at pipeline offset, then the validator + // must be in the below-threshold set + debug_assert!(tokens_pre < params.validator_stake_threshold); + } tracing::debug!("Target validator is below-threshold"); // Move the validator into the appropriate set @@ -1330,8 +1253,8 @@ where validator_state_handle(validator).set( storage, ValidatorState::Consensus, - epoch, - 0, + current_epoch, + offset, )?; } else { let min_consensus_validator_amount = @@ -1352,7 +1275,8 @@ where validator, tokens_post, min_consensus_validator_amount, - epoch, + current_epoch, + offset, &consensus_val_handle, &below_capacity_val_handle, )?; @@ -1371,8 +1295,8 @@ where validator_state_handle(validator).set( storage, ValidatorState::BelowCapacity, - epoch, - 0, + current_epoch, + offset, )?; } } @@ -1387,7 +1311,8 @@ fn insert_into_consensus_and_demote_to_below_cap( validator: &Address, tokens_post: token::Amount, min_consensus_amount: token::Amount, - epoch: Epoch, + current_epoch: Epoch, + offset: u64, consensus_set: &ConsensusValidatorSet, below_capacity_set: &BelowCapacityValidatorSet, ) -> storage_api::Result<()> @@ -1403,35 +1328,35 @@ where .remove(storage, &last_position_of_min_consensus_vals)? .expect("There must be always be at least 1 consensus validator"); - // let pipeline_epoch = current_epoch + params.pipeline_len; + let offset_epoch = current_epoch + offset; // Insert the min consensus validator into the below-capacity // set insert_validator_into_set( &below_capacity_set.at(&min_consensus_amount.into()), storage, - &epoch, + &offset_epoch, &removed_min_consensus, )?; validator_state_handle(&removed_min_consensus).set( storage, ValidatorState::BelowCapacity, - epoch, - 0, + current_epoch, + offset, )?; // Insert the current validator into the consensus set insert_validator_into_set( &consensus_set.at(&tokens_post), storage, - &epoch, + &offset_epoch, validator, )?; validator_state_handle(validator).set( storage, ValidatorState::Consensus, - epoch, - 0, + current_epoch, + offset, )?; Ok(()) } @@ -2048,23 +1973,26 @@ where ¶ms, validator, change_after_slashing, - pipeline_epoch, + current_epoch, + None, )?; } // Update the validator and total deltas at the pipeline offset update_validator_deltas( storage, + ¶ms, validator, change_after_slashing, current_epoch, - params.pipeline_len, + None, )?; update_total_deltas( storage, + ¶ms, change_after_slashing, current_epoch, - params.pipeline_len, + None, )?; if tracing::level_enabled!(tracing::Level::DEBUG) { @@ -2761,6 +2689,8 @@ pub struct BecomeValidator<'a, S> { pub commission_rate: Dec, /// Max commission rate change. pub max_commission_rate_change: Dec, + /// Optional offset to use instead of pipeline offset + pub offset_opt: Option, } /// Initialize data for a new validator. @@ -2781,12 +2711,14 @@ where current_epoch, commission_rate, max_commission_rate_change, + offset_opt, } = args; + let offset = offset_opt.unwrap_or(params.pipeline_len); // This will fail if the key is already being used try_insert_consensus_key(storage, consensus_key)?; - let pipeline_epoch = current_epoch + params.pipeline_len; + let pipeline_epoch = current_epoch + offset; validator_addresses_handle() .at(&pipeline_epoch) .insert(storage, address.clone())?; @@ -2804,37 +2736,37 @@ where storage, consensus_key.clone(), current_epoch, - params.pipeline_len, + offset, )?; validator_protocol_key_handle(address).set( storage, protocol_key.clone(), current_epoch, - params.pipeline_len, + offset, )?; validator_eth_hot_key_handle(address).set( storage, eth_hot_key.clone(), current_epoch, - params.pipeline_len, + offset, )?; validator_eth_cold_key_handle(address).set( storage, eth_cold_key.clone(), current_epoch, - params.pipeline_len, + offset, )?; validator_commission_rate_handle(address).set( storage, commission_rate, current_epoch, - params.pipeline_len, + offset, )?; validator_deltas_handle(address).set( storage, token::Change::zero(), current_epoch, - params.pipeline_len, + offset, )?; // The validator's stake at initialization is 0, so its state is immediately @@ -2843,7 +2775,7 @@ where storage, ValidatorState::BelowThreshold, current_epoch, - params.pipeline_len, + offset, )?; Ok(()) @@ -4526,6 +4458,7 @@ where &validator, -slash_amount.change(), epoch, + None, )?; } } @@ -4536,12 +4469,13 @@ where update_validator_deltas( storage, + ¶ms, &validator, -slash_delta.change(), epoch, - 0, + None, )?; - update_total_deltas(storage, -slash_delta.change(), epoch, 0)?; + update_total_deltas(storage, ¶ms, -slash_delta.change(), epoch, None)?; } // TODO: should we clear some storage here as is done in Quint?? @@ -5362,41 +5296,114 @@ where dest_validator, amount_after_slashing.change(), pipeline_epoch, + None, )?; } // Update deltas update_validator_deltas( storage, + ¶ms, dest_validator, amount_after_slashing.change(), current_epoch, - params.pipeline_len, + None, )?; update_total_deltas( storage, + ¶ms, amount_after_slashing.change(), current_epoch, - params.pipeline_len, + None, )?; Ok(()) } -/// Init PoS genesis wrapper helper that also initializes gov params that are -/// used in PoS with default values. #[cfg(any(test, feature = "testing"))] -pub fn test_init_genesis( - storage: &mut S, - owned: OwnedPosParams, - validators: impl Iterator + Clone, - current_epoch: namada_core::types::storage::Epoch, -) -> storage_api::Result -where - S: StorageRead + StorageWrite, -{ - let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); - gov_params.init_storage(storage)?; - crate::init_genesis(storage, &owned, validators, current_epoch)?; - crate::read_non_pos_owned_params(storage, owned) +/// PoS related utility functions to help set up tests. +pub mod test_utils { + use namada_core::ledger::storage_api; + use namada_core::ledger::storage_api::token::credit_tokens; + use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; + + use super::*; + use crate::parameters::PosParams; + use crate::types::GenesisValidator; + + /// Helper function to intialize storage with PoS data + /// about validators for tests. + pub fn init_genesis_helper( + storage: &mut S, + params: &PosParams, + validators: impl Iterator, + current_epoch: namada_core::types::storage::Epoch, + ) -> storage_api::Result<()> + where + S: StorageRead + StorageWrite, + { + init_genesis(storage, params, current_epoch)?; + for GenesisValidator { + address, + consensus_key, + protocol_key, eth_cold_key, + eth_hot_key, + commission_rate, + max_commission_rate_change, + tokens, + } in validators + { + become_validator(BecomeValidator { + storage, + params, + address: &address, + consensus_key: &consensus_key, + protocol_key: &protocol_key, + eth_cold_key: ð_cold_key, + eth_hot_key: ð_hot_key, + current_epoch, + commission_rate, + max_commission_rate_change, + offset_opt: Some(0), + })?; + // Credit token amount to be bonded to the validator address so it + // can be bonded + let staking_token = staking_token_address(storage); + credit_tokens(storage, &staking_token, &address, tokens)?; + + bond_tokens( + storage, + None, + &address, + tokens, + current_epoch, + Some(0), + )?; + } + // Store the total consensus validator stake to storage + store_total_consensus_stake(storage, current_epoch)?; + + // Copy validator sets and positions + copy_genesis_validator_sets(storage, params, current_epoch)?; + + Ok(()) + } + + /// Init PoS genesis wrapper helper that also initializes gov params that are + /// used in PoS with default values. + pub fn test_init_genesis( + storage: &mut S, + owned: OwnedPosParams, + validators: impl Iterator + Clone, + current_epoch: namada_core::types::storage::Epoch, + ) -> storage_api::Result + where + S: StorageRead + StorageWrite, + { + let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); + gov_params.init_storage(storage)?; + let params = crate::read_non_pos_owned_params(storage, owned)?; + init_genesis_helper(storage, ¶ms, validators, current_epoch)?; + Ok(params) + } } diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index ad0a2cccd9..b75c3f207c 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -64,7 +64,7 @@ use crate::{ read_consensus_validator_set_addresses_with_stake, read_total_stake, read_validator_deltas_value, read_validator_stake, slash, slash_redelegation, slash_validator, slash_validator_redelegation, - staking_token_address, store_total_consensus_stake, test_init_genesis, + staking_token_address, store_total_consensus_stake, test_utils::test_init_genesis, total_bonded_handle, total_deltas_handle, total_unbonded_handle, unbond_handle, unbond_tokens, unjail_validator, update_validator_deltas, update_validator_set, validator_consensus_key_handle, @@ -390,6 +390,7 @@ fn test_bonds_aux(params: OwnedPosParams, validators: Vec) { &validator.address, amount_self_bond, current_epoch, + None, ) .unwrap(); @@ -501,6 +502,7 @@ fn test_bonds_aux(params: OwnedPosParams, validators: Vec) { &validator.address, amount_del, current_epoch, + None, ) .unwrap(); let val_stake_pre = read_validator_stake( @@ -949,6 +951,7 @@ fn test_become_validator_aux( commission_rate: Dec::new(5, 2).expect("Dec creation failed"), max_commission_rate_change: Dec::new(5, 2) .expect("Dec creation failed"), + offset_opt: None, }) .unwrap(); assert!(is_validator(&s, &new_validator).unwrap()); @@ -967,7 +970,7 @@ fn test_become_validator_aux( let staking_token = staking_token_address(&s); let amount = token::Amount::from_uint(100_500_000, 0).unwrap(); credit_tokens(&mut s, &staking_token, &new_validator, amount).unwrap(); - bond_tokens(&mut s, None, &new_validator, amount, current_epoch).unwrap(); + bond_tokens(&mut s, None, &new_validator, amount, current_epoch, None).unwrap(); // Check the bond delta let bond_handle = bond_handle(&new_validator, &new_validator); @@ -1320,10 +1323,11 @@ fn test_validator_sets() { update_validator_deltas( s, + ¶ms, addr, stake.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); @@ -1553,14 +1557,16 @@ fn test_validator_sets() { &val1, -unbond.change(), pipeline_epoch, + None, ) .unwrap(); update_validator_deltas( &mut s, + ¶ms, &val1, -unbond.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); // Epoch 6 @@ -1748,14 +1754,15 @@ fn test_validator_sets() { let bond = token::Amount::from_uint(500_000, 0).unwrap(); let stake6 = stake6 + bond; println!("val6 {val6} new stake {}", stake6.to_string_native()); - update_validator_set(&mut s, ¶ms, &val6, bond.change(), pipeline_epoch) + update_validator_set(&mut s, ¶ms, &val6, bond.change(), pipeline_epoch, None) .unwrap(); update_validator_deltas( &mut s, + ¶ms, &val6, bond.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); let val6_bond_epoch = pipeline_epoch; @@ -2001,10 +2008,11 @@ fn test_validator_sets_swap() { update_validator_deltas( s, + ¶ms, addr, stake.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); @@ -2041,14 +2049,16 @@ fn test_validator_sets_swap() { &val2, bond2.change(), pipeline_epoch, + None, ) .unwrap(); update_validator_deltas( &mut s, + ¶ms, &val2, bond2.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); @@ -2058,14 +2068,16 @@ fn test_validator_sets_swap() { &val3, bond3.change(), pipeline_epoch, + None, ) .unwrap(); update_validator_deltas( &mut s, + ¶ms, &val3, bond3.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); @@ -2090,14 +2102,16 @@ fn test_validator_sets_swap() { &val2, bonds.change(), pipeline_epoch, + None, ) .unwrap(); update_validator_deltas( &mut s, + ¶ms, &val2, bonds.change(), epoch, - params.pipeline_len, + None, ) .unwrap(); @@ -2107,14 +2121,16 @@ fn test_validator_sets_swap() { &val3, bonds.change(), pipeline_epoch, + None, ) .unwrap(); update_validator_deltas( &mut s, + ¶ms, &val3, bonds.change(), epoch, - params.pipeline_len, +None, ) .unwrap(); @@ -4373,6 +4389,7 @@ fn test_simple_redelegation_aux( &src_validator, amount_delegate, current_epoch, + None, ) .unwrap(); @@ -4768,6 +4785,7 @@ fn test_redelegation_with_slashing_aux( &src_validator, amount_delegate, current_epoch, + None, ) .unwrap(); @@ -5163,6 +5181,7 @@ fn test_chain_redelegations_aux(mut validators: Vec) { &src_validator, bond_amount, current_epoch, + None, ) .unwrap(); @@ -5643,6 +5662,7 @@ fn test_overslashing_aux(mut validators: Vec) { &validator, amount_del, current_epoch, + None, ) .unwrap(); diff --git a/proof_of_stake/src/tests/state_machine.rs b/proof_of_stake/src/tests/state_machine.rs index a067037232..21be643af6 100644 --- a/proof_of_stake/src/tests/state_machine.rs +++ b/proof_of_stake/src/tests/state_machine.rs @@ -217,7 +217,7 @@ impl StateMachineTest for ConcretePosState { .collect::>() ); let mut s = TestWlStorage::default(); - crate::test_init_genesis( + crate::test_utils::test_init_genesis( &mut s, initial_state.params.owned.clone(), initial_state.genesis_validators.clone().into_iter(), @@ -275,6 +275,7 @@ impl StateMachineTest for ConcretePosState { current_epoch, commission_rate, max_commission_rate_change, + offset_opt: None, }) .unwrap(); @@ -338,6 +339,7 @@ impl StateMachineTest for ConcretePosState { &id.validator, amount, current_epoch, + None, ) .unwrap(); diff --git a/proof_of_stake/src/tests/state_machine_v2.rs b/proof_of_stake/src/tests/state_machine_v2.rs index 02df1b39a0..81f8226ac9 100644 --- a/proof_of_stake/src/tests/state_machine_v2.rs +++ b/proof_of_stake/src/tests/state_machine_v2.rs @@ -1929,7 +1929,7 @@ impl StateMachineTest for ConcretePosState { .collect::>() ); let mut s = TestWlStorage::default(); - crate::init_genesis( + crate::test_utils::init_genesis_helper( &mut s, &initial_state.params, initial_state.genesis_validators.clone().into_iter(), @@ -2003,6 +2003,7 @@ impl StateMachineTest for ConcretePosState { current_epoch, commission_rate, max_commission_rate_change, + offset_opt: None, }) .unwrap(); @@ -2066,6 +2067,7 @@ impl StateMachineTest for ConcretePosState { &id.validator, amount, current_epoch, + None, ) .unwrap(); diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index f9df559c8a..7162f92772 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -118,5 +118,6 @@ wasmtimer = "0.2.0" [dev-dependencies] assert_matches.workspace = true +base58.workspace = true namada_test_utils = {path = "../test_utils"} tempfile.workspace = true diff --git a/sdk/src/args.rs b/sdk/src/args.rs index f9b02ebafb..f8454648b5 100644 --- a/sdk/src/args.rs +++ b/sdk/src/args.rs @@ -1695,6 +1695,9 @@ pub struct MaspAddrKeyAdd { pub alias_force: bool, /// Any MASP value pub value: MaspValue, + /// Add a MASP key / address pre-genesis instead + /// of a current chain + pub is_pre_genesis: bool, /// Don't encrypt the keypair pub unsafe_dont_encrypt: bool, } @@ -1706,6 +1709,8 @@ pub struct MaspSpendKeyGen { pub alias: String, /// Whether to force overwrite the alias pub alias_force: bool, + /// Generate spending key pre-genesis instead of a current chain + pub is_pre_genesis: bool, /// Don't encrypt the keypair pub unsafe_dont_encrypt: bool, } @@ -1721,6 +1726,8 @@ pub struct MaspPayAddrGen { pub viewing_key: C::ViewingKey, /// Pin pub pin: bool, + /// Generate an address pre-genesis instead of a current chain + pub is_pre_genesis: bool, } /// Wallet generate key and implicit address arguments @@ -1732,6 +1739,8 @@ pub struct KeyAndAddressGen { pub alias: Option, /// Whether to force overwrite the alias, if provided pub alias_force: bool, + /// Generate a key for pre-genesis, instead of a current chain + pub is_pre_genesis: bool, /// Don't encrypt the keypair pub unsafe_dont_encrypt: bool, /// BIP44 derivation path @@ -1762,6 +1771,8 @@ pub struct KeyFind { pub alias: Option, /// Public key hash to lookup keypair with pub value: Option, + /// Find a key pre-genesis instead of a current chain + pub is_pre_genesis: bool, /// Show secret keys to user pub unsafe_show_secret: bool, } @@ -1773,6 +1784,8 @@ pub struct AddrKeyFind { pub alias: String, /// Show secret keys to user pub unsafe_show_secret: bool, + /// Find shielded address / key pre-genesis instead of a current chain + pub is_pre_genesis: bool, } /// Wallet list shielded keys arguments @@ -1780,15 +1793,27 @@ pub struct AddrKeyFind { pub struct MaspKeysList { /// Don't decrypt spending keys pub decrypt: bool, + /// List shielded keys pre-genesis instead of a current chain + pub is_pre_genesis: bool, /// Show secret keys to user pub unsafe_show_secret: bool, } +/// Wallet list shielded payment addresses arguments +#[derive(Clone, Debug)] +pub struct MaspListPayAddrs { + /// List sheilded payment address pre-genesis instead + /// of a current chain + pub is_pre_genesis: bool, +} + /// Wallet list keys arguments #[derive(Clone, Debug)] pub struct KeyList { /// Don't decrypt keypairs pub decrypt: bool, + /// List keys pre-genesis instead of a current chain + pub is_pre_genesis: bool, /// Show secret keys to user pub unsafe_show_secret: bool, } @@ -1798,6 +1823,8 @@ pub struct KeyList { pub struct KeyExport { /// Key alias pub alias: String, + /// Export key pre-genesis instead of a current chain + pub is_pre_genesis: bool, } /// Wallet address lookup arguments @@ -1807,6 +1834,15 @@ pub struct AddressOrAliasFind { pub alias: Option, /// Address to find pub address: Option
, + /// Lookup address pre-genesis instead of a current chain + pub is_pre_genesis: bool, +} + +/// List wallet address +#[derive(Clone, Debug)] +pub struct AddressList { + /// List addresses pre-genesis instead of current chain + pub is_pre_genesis: bool, } /// Wallet address add arguments @@ -1818,6 +1854,8 @@ pub struct AddressAdd { pub alias_force: bool, /// Address to add pub address: Address, + /// Add an address pre-genesis instead of current chain + pub is_pre_genesis: bool, } /// Bridge pool batch recommendation. diff --git a/sdk/src/error.rs b/sdk/src/error.rs index 9f84195cc2..aa4c2c9842 100644 --- a/sdk/src/error.rs +++ b/sdk/src/error.rs @@ -19,7 +19,7 @@ pub type Result = std::result::Result; /// /// The general mentality should be that this error type should cover all /// possible errors that one may face. -#[derive(Clone, Error, Debug)] +#[derive( Error, Debug)] pub enum Error { /// Errors that are caused by trying to retrieve a pinned transaction #[error("Error in retrieving pinned balance: {0}")] diff --git a/sdk/src/wallet/alias.rs b/sdk/src/wallet/alias.rs index 13d977b852..61ba36d74e 100644 --- a/sdk/src/wallet/alias.rs +++ b/sdk/src/wallet/alias.rs @@ -3,15 +3,17 @@ use std::convert::Infallible; use std::fmt::Display; use std::hash::Hash; +use std::io::Read; use std::str::FromStr; +use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; +use namada_core::types::address::{Address, InternalAddress}; /// Aliases created from raw strings are kept in-memory as given, but their /// `Serialize` and `Display` instance converts them to lowercase. Their /// `PartialEq` instance is case-insensitive. -#[derive(Clone, Debug, Default, Deserialize, PartialOrd, Ord, Eq)] -#[serde(transparent)] +#[derive(Clone, Debug, Default, Eq)] pub struct Alias(String); impl Alias { @@ -29,14 +31,51 @@ impl Alias { pub fn is_empty(&self) -> bool { self.0.is_empty() } + + /// If the alias is reserved for an internal address, + /// return that address + pub fn is_reserved(alias: impl AsRef) -> Option
{ + InternalAddress::try_from_alias(alias.as_ref()).map(Address::Internal) + } +} + +impl BorshSerialize for Alias { + fn serialize( + &self, + writer: &mut W, + ) -> std::io::Result<()> { + BorshSerialize::serialize(&self.normalize(), writer) + } +} + +impl BorshDeserialize for Alias { + fn deserialize(buf: &mut &[u8]) -> std::io::Result { + let raw: String = BorshDeserialize::deserialize(buf)?; + Ok(Self::from(raw)) + } + + fn deserialize_reader(reader: &mut R) -> std::io::Result { + let raw: String = BorshDeserialize::deserialize_reader(reader)?; + Ok(Self::from(raw)) + } } impl Serialize for Alias { fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, + where + S: serde::Serializer, { - self.normalize().serialize(serializer) + Serialize::serialize(&self.normalize(), serializer) + } +} + +impl<'de> Deserialize<'de> for Alias { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw: String = Deserialize::deserialize(deserializer)?; + Ok(Self::from(raw)) } } @@ -46,6 +85,19 @@ impl PartialEq for Alias { } } + +impl PartialOrd for Alias { + fn partial_cmp(&self, other: &Self) -> Option { + self.normalize().partial_cmp(&other.normalize()) + } +} + +impl Ord for Alias { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.normalize().cmp(&other.normalize()) + } +} + impl Hash for Alias { fn hash(&self, state: &mut H) { self.normalize().hash(state); @@ -57,7 +109,7 @@ where T: AsRef, { fn from(raw: T) -> Self { - Self(raw.as_ref().to_owned()) + Self(raw.as_ref().to_lowercase()) } } @@ -83,7 +135,14 @@ impl FromStr for Alias { type Err = Infallible; fn from_str(s: &str) -> Result { - Ok(Self(s.into())) + Ok(Self::from(s)) + } +} + + +impl AsRef for &Alias { + fn as_ref(&self) -> &str { + &self.0 } } diff --git a/sdk/src/wallet/mod.rs b/sdk/src/wallet/mod.rs index 67d5a11f86..8715b02b7c 100644 --- a/sdk/src/wallet/mod.rs +++ b/sdk/src/wallet/mod.rs @@ -346,8 +346,14 @@ impl Wallet { } /// Find the stored address by an alias. - pub fn find_address(&self, alias: impl AsRef) -> Option<&Address> { - self.store.find_address(alias) + pub fn find_address(&self, alias: impl AsRef) -> Option> { + Alias::is_reserved(alias.as_ref()) + .map(std::borrow::Cow::Owned) + .or_else(|| { + self.store + .find_address(alias) + .map(std::borrow::Cow::Borrowed) + }) } /// Find an alias by the address if it's in the wallet. @@ -883,4 +889,10 @@ impl Wallet { .insert_payment_addr::(alias.into(), payment_addr, force_alias) .map(Into::into) } + + /// Extend this wallet from another wallet (typically pre-genesis). + /// Note that this method ignores `store.validator_data` if any. + pub fn extend(&mut self, wallet: Self) { + self.store.extend(wallet.store) + } } diff --git a/sdk/src/wallet/store.rs b/sdk/src/wallet/store.rs index 201cc885a4..ddc5c5e03e 100644 --- a/sdk/src/wallet/store.rs +++ b/sdk/src/wallet/store.rs @@ -1,10 +1,10 @@ //! Wallet Store information -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashSet}; use std::fmt::Display; use std::str::FromStr; -use bimap::BiHashMap; +use bimap::BiBTreeMap; use bip39::Seed; use itertools::Itertools; use masp_primitives::zip32::ExtendedFullViewingKey; @@ -67,26 +67,26 @@ pub struct ValidatorData { #[derive(Serialize, Deserialize, Debug, Default)] pub struct Store { /// Known viewing keys - view_keys: HashMap, + view_keys: BTreeMap, /// Known spending keys - spend_keys: HashMap>, + spend_keys: BTreeMap>, /// Known payment addresses - payment_addrs: HashMap, + payment_addrs: BTreeMap, /// Cryptographic keypairs - keys: HashMap>, + keys: BTreeMap>, /// Namada address book - addresses: BiHashMap, + addresses: BiBTreeMap, /// Known mappings of public key hashes to their aliases in the `keys` /// field. Used for look-up by a public key. - pkhs: HashMap, + pkhs: BTreeMap, /// Special keys if the wallet belongs to a validator pub(crate) validator_data: Option, /// Namada address vp type - address_vp_types: HashMap>, + address_vp_types: BTreeMap>, } /// Grouping of addresses by validity predicate. -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)] pub enum AddressVpType { /// The Token Token, @@ -174,11 +174,11 @@ impl Store { /// Get all known keys by their alias, paired with PKH, if known. pub fn get_keys( &self, - ) -> HashMap< + ) -> BTreeMap< Alias, (&StoredKeypair, Option<&PublicKeyHash>), > { - let mut keys: HashMap< + let mut keys: BTreeMap< Alias, (&StoredKeypair, Option<&PublicKeyHash>), > = self @@ -198,24 +198,24 @@ impl Store { } /// Get all known addresses by their alias. - pub fn get_addresses(&self) -> &BiHashMap { + pub fn get_addresses(&self) -> &BiBTreeMap { &self.addresses } /// Get all known payment addresses by their alias. - pub fn get_payment_addrs(&self) -> &HashMap { + pub fn get_payment_addrs(&self) -> &BTreeMap { &self.payment_addrs } /// Get all known viewing keys by their alias. - pub fn get_viewing_keys(&self) -> &HashMap { + pub fn get_viewing_keys(&self) -> &BTreeMap { &self.view_keys } /// Get all known spending keys by their alias. pub fn get_spending_keys( &self, - ) -> &HashMap> { + ) -> &BTreeMap> { &self.spend_keys } @@ -247,6 +247,10 @@ impl Store { seed_and_derivation_path: Option<(Seed, DerivationPath)>, password: Option>, ) -> Option<(Alias, common::SecretKey)> { + // We cannot generate keys for reserved aliases + if alias.as_ref().and_then(Alias::is_reserved).is_some() { + return None; + } let sk = if let Some((seed, derivation_path)) = seed_and_derivation_path { gen_sk_from_seed_and_derivation_path( @@ -283,6 +287,13 @@ impl Store { password: Option>, force_alias: bool, ) -> (Alias, ExtendedSpendingKey) { + if Alias::is_reserved(&alias).is_some() { + panic!( + "Tried to generated spending key with reserved alias: {}. \ + Action cancelled, no changes persisted.", + alias + ); + } let spendkey = Self::generate_spending_key(); let viewkey = ExtendedFullViewingKey::from(&spendkey.into()).into(); let (spendkey_to_store, _raw_spendkey) = @@ -348,6 +359,12 @@ impl Store { return None; } + // abort if the alias is reserved + if Alias::is_reserved(&alias).is_some() { + println!("The alias {} is reserved", alias); + return None; + } + if alias.is_empty() { println!( "Empty alias given, defaulting to {}.", @@ -395,6 +412,12 @@ impl Store { viewkey: ExtendedViewingKey, force: bool, ) -> Option { + // abort if the alias is reserved + if Alias::is_reserved(&alias).is_some() { + println!("The alias {} is reserved", alias); + return None; + } + if alias.is_empty() { eprintln!("Empty alias given."); return None; @@ -424,6 +447,12 @@ impl Store { viewkey: ExtendedViewingKey, force: bool, ) -> Option { + // abort if the alias is reserved + if Alias::is_reserved(&alias).is_some() { + println!("The alias {} is reserved", alias); + return None; + } + if alias.is_empty() { eprintln!("Empty alias given."); return None; @@ -469,6 +498,12 @@ impl Store { payment_addr: PaymentAddress, force: bool, ) -> Option { + // abort if the alias is reserved + if Alias::is_reserved(&alias).is_some() { + println!("The alias {} is reserved", alias); + return None; + } + if alias.is_empty() { eprintln!("Empty alias given."); return None; @@ -499,6 +534,11 @@ impl Store { key: Option>, pkh: Option, ) { + // abort if the alias is reserved + if Alias::is_reserved(&alias).is_some() { + println!("The alias {} is reserved", alias); + return ; + } key.map(|x| self.keys.insert(alias.clone(), x)); pkh.map(|x| self.pkhs.insert(x, alias.clone())); } @@ -513,6 +553,11 @@ impl Store { address: Address, force: bool, ) -> Option { + // abort if the alias is reserved + if Alias::is_reserved(&alias).is_some() { + println!("The alias {} is reserved", alias); + return None; + } // abort if the address already exists in the wallet if self.addresses.contains_right(&address) && !force { println!( @@ -571,6 +616,28 @@ impl Store { Some(alias) } + /// Extend this store from another store (typically pre-genesis). + /// Note that this method ignores `validator_data` if any. + pub fn extend(&mut self, store: Store) { + let Self { + view_keys, + spend_keys, + payment_addrs, + keys, + addresses, + pkhs, + validator_data: _, + address_vp_types, + } = self; + view_keys.extend(store.view_keys); + spend_keys.extend(store.spend_keys); + payment_addrs.extend(store.payment_addrs); + keys.extend(store.keys); + addresses.extend(store.addresses); + pkhs.extend(store.pkhs); + address_vp_types.extend(store.address_vp_types); + } + /// Extend this store from pre-genesis validator wallet. pub fn extend_from_pre_genesis_validator( &mut self, @@ -750,7 +817,7 @@ impl<'de> Deserialize<'de> for AddressVpType { } } -#[cfg(all(test, feature = "dev"))] +#[cfg(test)] mod test_wallet { use base58::{self, FromBase58}; use bip39::{Language, Mnemonic}; diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 648f7e0b39..0795ca3c39 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -20,8 +20,6 @@ mainnet = [ "namada_core/mainnet", ] std = ["fd-lock"] -# NOTE "dev" features that shouldn't be used in live networks are enabled by default for now -dev = [] ferveo-tpke = [ "namada_core/ferveo-tpke", "namada_sdk/ferveo-tpke", diff --git a/shared/build.rs b/shared/build.rs deleted file mode 100644 index b97290123c..0000000000 --- a/shared/build.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::env; - -fn main() { - // Tell Cargo to build when the `NAMADA_DEV` env var changes - println!("cargo:rerun-if-env-changed=NAMADA_DEV"); - // Enable "dev" feature if `NAMADA_DEV` is trueish - if let Ok(dev) = env::var("NAMADA_DEV") { - if dev.to_ascii_lowercase().trim() == "true" { - println!("cargo:rustc-cfg=feature=\"dev\""); - } - } -} 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 6d07f5a613..01671ea882 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 @@ -647,7 +647,7 @@ mod test_bridge_pool_vp { use namada_core::ledger::gas::TxGasMeter; use namada_core::types::address; use namada_ethereum_bridge::parameters::{ - Contracts, EthereumBridgeConfig, UpgradeableContract, + Contracts, EthereumBridgeParams, UpgradeableContract, }; use super::*; @@ -901,7 +901,7 @@ mod test_bridge_pool_vp { /// Initialize some dummy storage for testing fn setup_storage() -> WlStorage { // a dummy config for testing - let config = EthereumBridgeConfig { + let config = EthereumBridgeParams { erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), diff --git a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs index 9f5f6dd19c..88c1f07044 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs @@ -171,7 +171,7 @@ mod tests { use namada_core::ledger::gas::TxGasMeter; use namada_core::ledger::storage_api::StorageWrite; use namada_ethereum_bridge::parameters::{ - Contracts, EthereumBridgeConfig, UpgradeableContract, + Contracts, EthereumBridgeParams, UpgradeableContract, }; use rand::Rng; @@ -225,7 +225,7 @@ mod tests { .expect("Test failed"); // a dummy config for testing - let config = EthereumBridgeConfig { + let config = EthereumBridgeParams { erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), diff --git a/shared/src/ledger/native_vp/ibc/mod.rs b/shared/src/ledger/native_vp/ibc/mod.rs index b0901f6fec..429643256e 100644 --- a/shared/src/ledger/native_vp/ibc/mod.rs +++ b/shared/src/ledger/native_vp/ibc/mod.rs @@ -389,7 +389,6 @@ mod tests { }; use crate::ledger::parameters::EpochDuration; use crate::ledger::{ibc, pos}; - use crate::proof_of_stake::parameters::PosParams; use crate::proto::{Code, Data, Section, Signature, Tx}; use crate::tendermint::time::Time as TmTime; use crate::tendermint_proto::Protobuf as TmProtobuf; @@ -416,12 +415,12 @@ mod tests { ibc::init_genesis_storage(&mut wl_storage); let gov_params = GovernanceParameters::default(); gov_params.init_storage(&mut wl_storage).unwrap(); - pos::init_genesis_storage( + pos::test_utils::test_init_genesis( &mut wl_storage, - &PosParams::default(), + namada_proof_of_stake::OwnedPosParams::default(), vec![get_dummy_genesis_validator()].into_iter(), Epoch(1), - ); + ).unwrap(); // epoch duration let epoch_duration_key = get_epoch_duration_storage_key(); let epoch_duration = EpochDuration { diff --git a/shared/src/ledger/pos/mod.rs b/shared/src/ledger/pos/mod.rs index d47e5cc884..3987b9b533 100644 --- a/shared/src/ledger/pos/mod.rs +++ b/shared/src/ledger/pos/mod.rs @@ -5,7 +5,6 @@ pub mod vp; use std::convert::TryFrom; pub use namada_core::ledger::storage_api; -use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; use namada_core::types::address; pub use namada_core::types::dec::Dec; pub use namada_core::types::key::common; @@ -15,10 +14,11 @@ pub use namada_proof_of_stake::parameters::{OwnedPosParams, PosParams}; pub use namada_proof_of_stake::pos_queries::*; pub use namada_proof_of_stake::storage::*; pub use namada_proof_of_stake::{staking_token_address, types}; +#[cfg(any(test, feature = "testing"))] +pub use namada_proof_of_stake::test_utils; pub use vp::PosVP; use crate::types::address::{Address, InternalAddress}; -use crate::types::storage::Epoch; /// Address of the PoS account implemented as a native VP pub const ADDRESS: Address = address::POS; @@ -39,24 +39,6 @@ pub fn into_tm_voting_power( i64::try_from(res).expect("Invalid validator voting power (i64)") } -/// Initialize storage in the genesis block. -pub fn init_genesis_storage( - storage: &mut S, - params: &OwnedPosParams, - validators: impl Iterator + Clone, - current_epoch: Epoch, -) where - S: StorageRead + StorageWrite, -{ - namada_proof_of_stake::init_genesis( - storage, - params, - validators, - current_epoch, - ) - .expect("Initialize PoS genesis storage"); -} - /// Alias for a PoS type with the same name with concrete type parameters pub type BondId = namada_proof_of_stake::types::BondId; diff --git a/shared/src/ledger/vp_host_fns.rs b/shared/src/ledger/vp_host_fns.rs index ebe32d4f72..ef8831ae88 100644 --- a/shared/src/ledger/vp_host_fns.rs +++ b/shared/src/ledger/vp_host_fns.rs @@ -321,7 +321,7 @@ pub fn get_tx_index( Ok(*tx_index) } -/// Getting the chain ID. +/// Getting the native token's address. pub fn get_native_token( gas_meter: &mut VpGasMeter, storage: &Storage, diff --git a/tests/src/e2e/eth_bridge_tests.rs b/tests/src/e2e/eth_bridge_tests.rs index d85b344f45..538a5d4ae5 100644 --- a/tests/src/e2e/eth_bridge_tests.rs +++ b/tests/src/e2e/eth_bridge_tests.rs @@ -10,7 +10,7 @@ use expectrl::ControlCode; use namada::eth_bridge::oracle; use namada::eth_bridge::storage::vote_tallies; use namada::ledger::eth_bridge::{ - ContractVersion, Contracts, EthereumBridgeConfig, MinimumConfirmations, + ContractVersion, Contracts, EthereumBridgeParams, MinimumConfirmations, UpgradeableContract, }; use namada::types::address::wnam; @@ -292,7 +292,7 @@ async fn test_bridge_pool_e2e() { let wnam_address = wnam().to_canonical(); let test = setup::network( |mut genesis| { - genesis.ethereum_bridge_params = Some(EthereumBridgeConfig { + genesis.ethereum_bridge_params = Some(EthereumBridgeParams { eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { @@ -483,7 +483,7 @@ async fn test_bridge_pool_e2e() { /// other ERC20 transfers. #[tokio::test] async fn test_wnam_transfer() -> Result<()> { - let ethereum_bridge_params = EthereumBridgeConfig { + let ethereum_bridge_params = EthereumBridgeParams { eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::from(unsafe { // SAFETY: The only way the API contract of `NonZeroU64` can @@ -590,7 +590,7 @@ async fn test_wnam_transfer() -> Result<()> { /// storage, if the Ethereum bridge has been bootstrapped for the Namada chain. #[test] fn test_configure_oracle_from_storage() -> Result<()> { - let ethereum_bridge_params = EthereumBridgeConfig { + let ethereum_bridge_params = EthereumBridgeParams { eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::from(unsafe { // SAFETY: The only way the API contract of `NonZeroU64` can diff --git a/tests/src/e2e/eth_bridge_tests/helpers.rs b/tests/src/e2e/eth_bridge_tests/helpers.rs index 04047a1722..9fbc467443 100644 --- a/tests/src/e2e/eth_bridge_tests/helpers.rs +++ b/tests/src/e2e/eth_bridge_tests/helpers.rs @@ -8,7 +8,7 @@ use eyre::{eyre, Context, Result}; use hyper::client::HttpConnector; use hyper::{Body, Client, Method, Request, StatusCode}; use namada::ledger::eth_bridge::{ - wrapped_erc20s, ContractVersion, Contracts, EthereumBridgeConfig, + wrapped_erc20s, ContractVersion, Contracts, EthereumBridgeParams, MinimumConfirmations, UpgradeableContract, }; use namada::types::address::{wnam, Address}; @@ -80,7 +80,7 @@ impl EventsEndpointClient { /// validator that is exposing an endpoint for submission of fake Ethereum /// events. pub fn setup_single_validator_test() -> Result<(Test, NamadaBgCmd)> { - let ethereum_bridge_params = EthereumBridgeConfig { + let ethereum_bridge_params = EthereumBridgeParams { eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::from(unsafe { // SAFETY: The only way the API contract of `NonZeroU64` can diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index 9a58230072..e98a14735c 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::ffi::OsStr; use std::fmt::Display; -use std::fs::{File, OpenOptions}; +use std::fs::{create_dir_all, File, OpenOptions}; use std::path::{Path, PathBuf}; use std::process::Command; use std::str::FromStr; @@ -18,11 +18,20 @@ use expectrl::stream::log::LogStream; use expectrl::{ControlCode, Eof, WaitStatus}; use eyre::{eyre, Context}; use itertools::{Either, Itertools}; +use namada_sdk::wallet::alias::Alias; use namada::types::chain::ChainId; -use namada_apps::client::utils; -use namada_apps::config::genesis::genesis_config::{self, GenesisConfig}; -use namada_apps::config::{ethereum_bridge, Config}; +use namada_apps::client::utils::{ + self, validator_pre_genesis_dir, validator_pre_genesis_txs_file, +}; +use namada_apps::config::genesis::toml_utils::read_toml; +use namada_apps::config::genesis::{chain, templates}; +use namada_apps::config::{ethereum_bridge, genesis, Config}; use namada_apps::{config, wallet}; +use namada_core::types::key::{RefTo, SchemeType}; +use namada_core::types::string_encoding::StringEncoded; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; +use namada_tx_prelude::token; +use namada_vp_prelude::HashSet; use once_cell::sync::Lazy; use rand::Rng; use serde_json; @@ -54,9 +63,10 @@ pub const ENV_VAR_USE_PREBUILT_BINARIES: &str = /// This file must contain a single validator with alias "validator-0". /// To add more validators, use the [`set_validators`] function in the call to /// setup the [`network`]. -pub const SINGLE_NODE_NET_GENESIS: &str = "genesis/e2e-tests-single-node.toml"; +#[allow(dead_code)] +pub const SINGLE_NODE_NET_GENESIS: &str = "genesis/localnet"; /// An E2E test network. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Network { pub chain_id: ChainId, } @@ -88,6 +98,13 @@ pub fn update_actor_config( .unwrap(); } +/// Configure validator p2p settings to allow duplicat ips +pub fn allow_duplicate_ips(test: &Test, chain_id: &ChainId, who: &Who) { + update_actor_config(test, chain_id, who, |config| { + config.ledger.cometbft.p2p.allow_duplicate_ip = true; + }); +} + /// Configures the Ethereum bridge mode of `who`. This should be done before /// `who` starts running. pub fn set_ethereum_bridge_mode( @@ -112,40 +129,146 @@ pub fn set_ethereum_bridge_mode( /// INVARIANT: Do not call this function more than once on the same config. pub fn set_validators( num: u8, - mut genesis: GenesisConfig, + mut genesis: templates::All, + base_dir: &Path, port_offset: F, -) -> GenesisConfig +) -> templates::All where F: Fn(u8) -> u16, { - let validator_0 = genesis.validator.get_mut("validator-0").unwrap(); - // Clone the first validator before modifying it - let other_validators = validator_0.clone(); - let validator_0_target = validator_0.net_address.clone().unwrap(); - let split: Vec<&str> = validator_0_target.split(':').collect(); - let (net_target_0, net_address_port_0) = - (split[0], split[1].parse::().unwrap()); - for ix in 0..num { - let mut validator = other_validators.clone(); - let mut net_target = net_target_0.to_string(); - // 6 ports for each validator - let first_port = net_address_port_0 + port_offset(ix); - net_target = format!("{}:{}", net_target, first_port); - validator.net_address = Some(net_target.to_string()); - let name = format!("validator-{}", ix); - genesis.validator.insert(name, validator); + // for each validator: + // - generate a balance key + // - assign balance to the key + // - invoke `init-genesis-validator` signed by balance key to generate + // validator pre-genesis wallet signed genesis txs + // - add txs to genesis templates + let wallet_path = base_dir.join("pre-genesis"); + for val in 0..num { + // generate a balance key + let mut wallet = wallet::load(&wallet_path) + .expect("Could not locate pre-genesis wallet used for e2e tests."); + let alias = format!("validator-{}-balance-key", val); + let (alias, sk) = wallet + .gen_key(SchemeType::Ed25519, Some(alias), true, None, None) + .unwrap_or_else(|_| { + panic!("Could not generate new key for validator-{}", val) + }) + .unwrap(); + wallet::save(&wallet).unwrap(); + // assign balance to the key + genesis + .balances + .token + .get_mut(&Alias::from_str("nam").expect("Infallible")) + .expect("NAM balances should exist in pre-genesis wallet already") + .0 + .insert( + StringEncoded::new(sk.ref_to()), + token::DenominatedAmount { + amount: token::Amount::from_uint( + 3000000, + NATIVE_MAX_DECIMAL_PLACES, + ) + .unwrap(), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }, + ); + // invoke `init-genesis-validator` signed by balance key to generate + // validator pre-genesis wallet signed genesis txs + let validator_alias = format!("validator-{}", val); + let net_addr = format!("127.0.0.1:{}", 27656 + port_offset(val)); + let args = vec![ + "utils", + "init-genesis-validator", + "--source", + &alias, + "--alias", + &validator_alias, + "--net-address", + &net_addr, + "--commission-rate", + "0.05", + "--max-commission-rate-change", + "0.01", + "--transfer-from-source-amount", + "2000000", + "--self-bond-amount", + "100000", + "--unsafe-dont-encrypt", + ]; + let validator_alias = format!("validator-{}", val); + // initialize the validator + let mut init_genesis_validator = run_cmd( + Bin::Client, + args, + Some(5), + &working_dir(), + base_dir, + format!("{}:{}", std::file!(), std::line!()), + ) + .unwrap(); + init_genesis_validator.assert_success(); + // add generated txs to genesis + let pre_genesis_path = + validator_pre_genesis_dir(base_dir, &validator_alias); + let pre_genesis_tx_path = + validator_pre_genesis_txs_file(&pre_genesis_path); + let pre_genesis_txs = + read_toml(&pre_genesis_tx_path, "transactions.toml").unwrap(); + genesis.transactions.merge(pre_genesis_txs); + // move validators generated files to their own base dir + let validator_base_dir = base_dir + .join(utils::NET_ACCOUNTS_DIR) + .join(&validator_alias); + let src_path = validator_pre_genesis_dir(base_dir, &validator_alias); + let dest_path = + validator_pre_genesis_dir(&validator_base_dir, &validator_alias); + println!( + "{} for {validator_alias} from {} to {}.", + "Copying pre-genesis validator-wallet".yellow(), + src_path.to_string_lossy(), + dest_path.to_string_lossy(), + ); + fs::create_dir_all(&dest_path).unwrap(); + fs::rename(src_path, dest_path).unwrap(); } genesis } +/// Remove self-bonds from default templates. They will be +/// regenerated later. +fn remove_self_bonds(genesis: &mut templates::All) { + let bonds = genesis.transactions.bond.take().unwrap(); + genesis.transactions.bond = Some( + bonds + .into_iter() + .filter(|bond| { + if let genesis::transactions::AliasOrPk::Alias(alias) = + &bond.data.source + { + *alias != bond.data.validator + } else { + true + } + }) + .collect(), + ); +} + /// Setup a network with a single genesis validator node. pub fn single_node_net() -> Result { - network(|genesis| genesis, None) + network( + |genesis, base_dir: &_| set_validators(1, genesis, base_dir, |_| 0u16), + None, + ) } /// Setup a configurable network. pub fn network( - mut update_genesis: impl FnMut(GenesisConfig) -> GenesisConfig, + mut update_genesis: impl FnMut( + templates::All, + &Path, + ) -> templates::All, consensus_timeout_commit: Option<&'static str>, ) -> Result { INIT.call_once(|| { @@ -156,41 +279,86 @@ pub fn network( let working_dir = working_dir(); let test_dir = TestDir::new(); - // Open the source genesis file - let mut genesis = genesis_config::open_genesis_config( - working_dir.join(SINGLE_NODE_NET_GENESIS), - )?; - - genesis.parameters.vp_whitelist = + // Open the source genesis file templates + let templates_dir = working_dir.join("genesis").join("localnet"); + println!( + "{} {}.", + "Loading genesis templates from".yellow(), + templates_dir.to_string_lossy() + ); + let mut templates = + genesis::templates::All::read_toml_files(&templates_dir) + .unwrap_or_else(|_| { + panic!( + "Missing genesis templates files at {}", + templates_dir.to_string_lossy() + ) + }); + // clear existing validator txs from genesis + templates.transactions.validator_account = None; + // remove self-bonds from genesis + remove_self_bonds(&mut templates); + + // Update the templates as needed + templates.parameters.parameters.vp_whitelist = Some(get_all_wasms_hashes(&working_dir, Some("vp_"))); - genesis.parameters.tx_whitelist = + templates.parameters.parameters.tx_whitelist = Some(get_all_wasms_hashes(&working_dir, Some("tx_"))); + // Copy the main wallet from templates dir into the base dir. + { + let base_dir = test_dir.path(); + let src_path = + wallet::wallet_file(&templates_dir.join("src").join("pre-genesis")); + let dest_dir = base_dir.join("pre-genesis"); + let dest_path = wallet::wallet_file(&dest_dir); + println!( + "{} from {} to {}.", + "Copying main pre-genesis wallet into a default non-validator \ + base dir" + .yellow(), + src_path.to_string_lossy(), + dest_path.to_string_lossy(), + ); + fs::create_dir_all(&dest_dir)?; + fs::copy(&src_path, &dest_path)?; + } // Run the provided function on it - let genesis = update_genesis(genesis); + let templates = update_genesis(templates, test_dir.path()); + + // Write the updated genesis templates to the test dir + let updated_templates_dir = test_dir.path().join("templates"); + create_dir_all(&updated_templates_dir)?; + println!( + "{} {}.", + "Writing updated genesis templates to".yellow(), + updated_templates_dir.to_string_lossy() + ); + templates.write_toml_files(&updated_templates_dir)?; - // Run `init-network` to generate the finalized genesis config, keys and - // addresses and update WASM checksums - let genesis_file = test_dir.path().join("e2e-test-genesis-src.toml"); - genesis_config::write_genesis_config(&genesis, &genesis_file); - let genesis_path = genesis_file.to_string_lossy(); + // Run `init-network` on the updated templates to generate the finalized + // genesis config and addresses and update WASM checksums + let templates_path = updated_templates_dir.to_string_lossy().into_owned(); + println!("{}", "Finalizing network from genesis templates.".yellow()); let checksums_path = working_dir .join("wasm/checksums.json") .to_string_lossy() .into_owned(); + let genesis_dir = test_dir.path().join("genesis"); + let archive_dir = genesis_dir.to_string_lossy().to_string(); let mut args = vec![ "utils", "init-network", - "--unsafe-dont-encrypt", - "--genesis-path", - &genesis_path, + "--templates-path", + &templates_path, "--chain-prefix", "e2e-test", - "--localhost", - "--dont-archive", - "--allow-duplicate-ip", "--wasm-checksums-path", &checksums_path, + "--genesis-time", + "2023-08-30T00:00:00Z", + "--archive-dir", + &archive_dir, ]; if let Some(consensus_timeout_commit) = consensus_timeout_commit { args.push("--consensus-timeout-commit"); @@ -201,7 +369,7 @@ pub fn network( args, Some(5), &working_dir, - &test_dir, + &genesis_dir, format!("{}:{}", std::file!(), std::line!()), )?; @@ -211,38 +379,121 @@ pub fn network( let chain_id_raw = matched.trim().split_once("Derived chain ID: ").unwrap().1; let chain_id = ChainId::from_str(chain_id_raw.trim())?; - println!("'init-network' output: {}", unread); + println!("'init-network' unread output: {}", unread); let net = Network { chain_id }; + init_network.assert_success(); - // release lock on wallet by dropping the - // child process drop(init_network); - // Move the "others" accounts wallet in the main base dir, so that we can - // use them with `Who::NonValidator` - let chain_dir = test_dir.path().join(net.chain_id.as_str()); - std::fs::rename( - wallet::wallet_file( - chain_dir - .join(utils::NET_ACCOUNTS_DIR) - .join(utils::NET_OTHER_ACCOUNTS_DIR), - ), - wallet::wallet_file(chain_dir.clone()), + // Host the network archive to make it available for `join-network` commands + // TODO: allow to unpack from a file instead of having to host it + let network_archive_server = file_serve::Server::new(&archive_dir); + let network_archive_addr = network_archive_server.addr().to_owned(); + std::thread::spawn(move || { + network_archive_server.serve().unwrap(); + }); + std::env::set_var( + namada_apps::client::utils::ENV_VAR_NETWORK_CONFIGS_SERVER, + format!("http://{network_archive_addr}"), + ); + + let genesis_new = chain::Finalized::read_toml_files( + &genesis_dir.join(net.chain_id.as_str()), ) .unwrap(); + let validator_aliases = genesis_new + .transactions + .validator_account + .as_ref() + .map(|txs| { + txs.iter().fold(HashSet::new(), |mut acc, finalized| { + acc.insert(finalized.tx.alias.to_string()); + acc + }) + }) + .unwrap_or_default(); - copy_wasm_to_chain_dir( - &working_dir, - &chain_dir, - &net.chain_id, - genesis.validator.keys(), - ); + // Setup a dir for every validator and non-validator using their + // pre-genesis wallets + for alias in &validator_aliases { + let validator_base_dir = + test_dir.path().join(utils::NET_ACCOUNTS_DIR).join(alias); + + // Copy the main wallet from templates dir into validator's base dir. + { + let dest_dir = validator_base_dir.join("pre-genesis"); + let dest_path = wallet::wallet_file(&dest_dir); + let base_dir = test_dir.path(); + let src_dir = base_dir.join("pre-genesis"); + let src_path = wallet::wallet_file(&src_dir); + println!( + "{} for {alias} from {} to {}.", + "Copying main pre-genesis wallet".yellow(), + src_path.to_string_lossy(), + dest_path.to_string_lossy(), + ); + fs::create_dir_all(&dest_dir)?; + fs::copy(&src_path, &dest_path)?; + } + println!("{} {}.", "Joining network with ".yellow(), alias); + let validator_base_dir = + test_dir.path().join(utils::NET_ACCOUNTS_DIR).join(alias); + let mut join_network = run_cmd( + Bin::Client, + [ + "utils", + "join-network", + "--chain-id", + net.chain_id.as_str(), + "--genesis-validator", + alias, + "--dont-prefetch-wasm", + ], + Some(5), + &working_dir, + &validator_base_dir, + format!("{}:{}", std::file!(), std::line!()), + )?; + join_network.exp_string("Successfully configured for chain")?; + join_network.assert_success(); + copy_wasm_to_chain_dir( + &working_dir, + &validator_base_dir, + &net.chain_id, + ); + } + + // Setup a dir for a non-validator using the pre-genesis wallet + { + let base_dir = test_dir.path(); + println!( + "{}.", + "Joining network with a default non-validator node".yellow() + ); + let mut join_network = run_cmd( + Bin::Client, + [ + "utils", + "join-network", + "--chain-id", + net.chain_id.as_str(), + "--dont-prefetch-wasm", + ], + Some(5), + &working_dir, + base_dir, + format!("{}:{}", std::file!(), std::line!()), + )?; + join_network.exp_string("Successfully configured for chain")?; + join_network.assert_success(); + } + + copy_wasm_to_chain_dir(&working_dir, test_dir.path(), &net.chain_id); Ok(Test { working_dir, test_dir, net, - genesis, async_runtime: Default::default(), }) } @@ -265,7 +516,6 @@ pub struct Test { /// Namada cmds pub test_dir: TestDir, pub net: Network, - pub genesis: GenesisConfig, pub async_runtime: LazyAsyncRuntime, } @@ -455,10 +705,8 @@ impl Test { Who::Validator(index) => self .test_dir .path() - .join(self.net.chain_id.as_str()) .join(utils::NET_ACCOUNTS_DIR) - .join(format!("validator-{}", index)) - .join(config::DEFAULT_BASE_DIR), + .join(format!("validator-{}", index)), } } @@ -883,11 +1131,11 @@ pub mod constants { } /// Copy WASM files from the `wasm` directory to every node's chain dir. -pub fn copy_wasm_to_chain_dir<'a>( +pub fn copy_wasm_to_chain_dir( working_dir: &Path, - chain_dir: &Path, + test_dir: &Path, chain_id: &ChainId, - genesis_validator_keys: impl Iterator, + // genesis_validator_keys: impl Iterator, ) { // Copy the built WASM files from "wasm" directory in the root of the // project. @@ -911,6 +1159,7 @@ pub fn copy_wasm_to_chain_dir<'a>( built_wasm_dir.to_string_lossy() ); } + let chain_dir = test_dir.join(chain_id.as_str()); let target_wasm_dir = chain_dir.join(config::DEFAULT_WASM_DIR); for file in &wasm_files { std::fs::copy( @@ -919,29 +1168,6 @@ pub fn copy_wasm_to_chain_dir<'a>( ) .unwrap(); } - - // Copy the built WASM files from "wasm" directory to each validator dir - for validator_name in genesis_validator_keys { - let target_wasm_dir = chain_dir - .join(utils::NET_ACCOUNTS_DIR) - .join(validator_name) - .join(config::DEFAULT_BASE_DIR) - .join(chain_id.as_str()) - .join(config::DEFAULT_WASM_DIR); - for file in &wasm_files { - let src = working_dir.join("wasm").join(file); - let dst = target_wasm_dir.join(file); - std::fs::copy(&src, &dst) - .wrap_err_with(|| { - format!( - "copying {} to {}", - &src.to_string_lossy(), - &dst.to_string_lossy(), - ) - }) - .unwrap(); - } - } } pub fn get_all_wasms_hashes( diff --git a/tests/src/integration/setup.rs b/tests/src/integration/setup.rs index 9bbd00eef9..6e8d953bcb 100644 --- a/tests/src/integration/setup.rs +++ b/tests/src/integration/setup.rs @@ -30,60 +30,65 @@ const ENV_VAR_KEEP_TEMP: &str = "NAMADA_INT_KEEP_TEMP"; /// Setup a network with a single genesis validator node. pub fn setup() -> Result<(MockNode, MockServicesController)> { - initialize_genesis(|genesis| genesis) + initialize_genesis() } /// Setup folders with genesis, configs, wasm, etc. -pub fn initialize_genesis( - mut update_genesis: impl FnMut(GenesisConfig) -> GenesisConfig, -) -> Result<(MockNode, MockServicesController)> { +pub fn initialize_genesis() -> Result<(MockNode, MockServicesController)> { let working_dir = std::fs::canonicalize("..").unwrap(); let keep_temp = match std::env::var(ENV_VAR_KEEP_TEMP) { Ok(val) => val.to_ascii_lowercase() != "false", _ => false, }; let test_dir = TestDir::new(); + let template_dir = working_dir.join(SINGLE_NODE_NET_GENESIS); - // Open the source genesis file - let mut genesis = genesis_config::open_genesis_config( - working_dir.join(SINGLE_NODE_NET_GENESIS), - )?; - - genesis.parameters.vp_whitelist = - Some(get_all_wasms_hashes(&working_dir, Some("vp_"))); - genesis.parameters.tx_whitelist = - Some(get_all_wasms_hashes(&working_dir, Some("tx_"))); + // Copy genesis files to test directory. + let templates = templates::All::read_toml_files(&template_dir) + .expect("Missing genesis files"); + let genesis_path = test_dir.path().join("int-test-genesis-src"); + std::fs::create_dir(&genesis_path) + .expect("Could not create test chain directory."); + templates + .write_toml_files(&genesis_path) + .expect("Could not write genesis files into test chain directory."); - // Run the provided function on it - let genesis = update_genesis(genesis); + // Finalize the genesis config to derive the chain ID + let templates = load_and_validate(&template_dir) + .expect("Missing or invalid genesis files"); + let genesis_time = Default::default(); + let chain_id_prefix = ChainIdPrefix::from_str("integration-test").unwrap(); + let genesis = config::genesis::chain::finalize( + templates, + chain_id_prefix.clone(), + genesis_time, + Timeout::from_str("30s").unwrap(), + ); + let chain_id = &genesis.metadata.chain_id; // Run `init-network` to generate the finalized genesis config, keys and // addresses and update WASM checksums - let genesis_path = test_dir.path().join("e2e-test-genesis-src.toml"); - genesis_config::write_genesis_config(&genesis, &genesis_path); let wasm_checksums_path = working_dir.join("wasm/checksums.json"); - + let global_args = args::Global { + chain_id: Some(chain_id.clone()), + base_dir: test_dir.path().to_path_buf(), + wasm_dir: Some(test_dir.path().join(chain_id.as_str()).join("wasm")), + }; // setup genesis file namada_apps::client::utils::init_network( - args::Global { - chain_id: None, - base_dir: test_dir.path().to_path_buf(), - wasm_dir: None, - }, + global_args.clone(), args::InitNetwork { - genesis_path, + templates_path: genesis_path, wasm_checksums_path, - chain_id_prefix: ChainIdPrefix::from_str("integration-test") - .unwrap(), - unsafe_dont_encrypt: true, - consensus_timeout_commit: Timeout::from_str("1s").unwrap(), - localhost: true, - allow_duplicate_ip: true, + chain_id_prefix, + consensus_timeout_commit: Timeout::from_str("30s").unwrap(), dont_archive: true, archive_dir: None, + genesis_time, }, ); + finalize_wallet(&template_dir, &global_args, genesis); let auto_drive_services = { // NB: for now, the only condition that // dictates whether mock services should @@ -100,42 +105,60 @@ pub fn initialize_genesis( auto_drive_services, enable_eth_oracle, }; - create_node(test_dir, &genesis, keep_temp, services_cfg) + create_node(test_dir, global_args, keep_temp, services_cfg) +} + +/// Add the address from the finalized genesis to the wallet. +/// Additionally add the validator keys to the wallet. +fn finalize_wallet( + template_dir: &Path, + global_args: &args::Global, + genesis: Finalized, +) { + let pre_genesis_path = template_dir.join("src").join(PRE_GENESIS_DIR); + let validator_alias_and_dir = + Some(("validator-0", pre_genesis_path.join("validator-0"))); + // Pre-load the validator pre-genesis wallet and its keys to validate that + // everything is in place + let validator_alias_and_pre_genesis_wallet = + validator_alias_and_dir.map(|(validator_alias, pre_genesis_dir)| { + ( + Alias::from(validator_alias), + pre_genesis::load(&pre_genesis_dir).unwrap_or_else(|err| { + panic!("Error loading validator pre-genesis wallet {err}") + }), + ) + }); + + // Try to load pre-genesis wallet + let pre_genesis_wallet = namada_apps::wallet::load(&pre_genesis_path); + let chain_dir = global_args + .base_dir + .join(global_args.chain_id.as_ref().unwrap().as_str()); + // Derive wallet from genesis + let wallet = genesis.derive_wallet( + &chain_dir, + pre_genesis_wallet, + validator_alias_and_pre_genesis_wallet, + ); + namada_apps::wallet::save(&wallet).unwrap(); } /// Create a mock ledger node. fn create_node( - base_dir: TestDir, - genesis: &GenesisConfig, + test_dir: TestDir, + global_args: args::Global, keep_temp: bool, services_cfg: MockServicesCfg, ) -> Result<(MockNode, MockServicesController)> { // look up the chain id from the global file. - let chain_id = if let toml::Value::String(chain_id) = - toml::from_str::
( - &std::fs::read_to_string( - base_dir.path().join("global-config.toml"), - ) - .unwrap(), - ) - .unwrap() - .get("default_chain_id") - .unwrap() - { - chain_id.to_string() - } else { - return Err(eyre!("Could not read chain id from global-config.toml")); - }; + let chain_id = global_args.chain_id.unwrap_or_default(); - // the directory holding compiled wasm - let wasm_dir = base_dir.path().join(Path::new(&chain_id)).join("wasm"); // copy compiled wasms into the wasm directory - let chain_id = ChainId::from_str(&chain_id).unwrap(); copy_wasm_to_chain_dir( &std::fs::canonicalize("..").unwrap(), - &base_dir.path().join(Path::new(&chain_id.to_string())), + &global_args.base_dir, &chain_id, - genesis.validator.keys(), ); // instantiate and initialize the ledger node. @@ -148,19 +171,20 @@ fn create_node( let node = MockNode { shell: Arc::new(Mutex::new(Shell::new( config::Ledger::new( - base_dir.path(), + global_args.base_dir, chain_id.clone(), TendermintMode::Validator, ), - wasm_dir, + global_args + .wasm_dir + .expect("Wasm path not provided to integration test setup."), shell_handlers.tx_broadcaster, shell_handlers.eth_oracle_channels, None, 50 * 1024 * 1024, // 50 kiB 50 * 1024 * 1024, // 50 kiB - nam(), ))), - test_dir: ManuallyDrop::new(base_dir), + test_dir: ManuallyDrop::new(test_dir), keep_temp, services: Arc::new(services), results: Arc::new(Mutex::new(vec![])), diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index 3de46fc84b..ba69c6e380 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -18,7 +18,7 @@ mod test_bridge_pool_vp { use namada_apps::wallet::defaults::{albert_address, bertha_address}; use namada_apps::wasm_loader; use namada_sdk::eth_bridge::{ - wrapped_erc20s, Contracts, Erc20WhitelistEntry, EthereumBridgeConfig, + wrapped_erc20s, Contracts, Erc20WhitelistEntry, EthereumBridgeParams, UpgradeableContract, }; @@ -65,7 +65,7 @@ mod test_bridge_pool_vp { tx, ..Default::default() }; - let config = EthereumBridgeConfig { + let config = EthereumBridgeParams { erc20_whitelist: vec![Erc20WhitelistEntry { token_address: wnam(), token_cap: Amount::from_u64(TOKEN_CAP).native_denominated(), diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index 233d999861..1a3236ca40 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -96,7 +96,7 @@ //! - add rewards use namada::proof_of_stake::parameters::{OwnedPosParams, PosParams}; -use namada::proof_of_stake::test_init_genesis as init_genesis; +use namada::proof_of_stake::test_utils::test_init_genesis as init_genesis; use namada::proof_of_stake::types::GenesisValidator; use namada::types::storage::Epoch; diff --git a/tests/src/vm_host_env/ibc.rs b/tests/src/vm_host_env/ibc.rs index 583c58ada3..88f88820a5 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -71,7 +71,7 @@ use namada::ledger::storage::mockdb::MockDB; use namada::ledger::storage::traits::Sha256Hasher; use namada::ledger::tx_env::TxEnv; use namada::ledger::{ibc, pos}; -use namada::proof_of_stake::parameters::PosParams; +use namada::proof_of_stake::OwnedPosParams; use namada::proto::Tx; use namada::tendermint::time::Time as TmTime; use namada::tendermint_proto::Protobuf as TmProtobuf; @@ -214,12 +214,12 @@ pub fn init_storage() -> (Address, Address) { ibc::init_genesis_storage(&mut env.wl_storage); let gov_params = GovernanceParameters::default(); gov_params.init_storage(&mut env.wl_storage).unwrap(); - pos::init_genesis_storage( + pos::test_utils::test_init_genesis( &mut env.wl_storage, - &PosParams::default(), + OwnedPosParams::default(), vec![get_dummy_genesis_validator()].into_iter(), Epoch(1), - ); + ).unwrap(); // store wasm code let key = Key::wasm_code(&code_hash); env.wl_storage.storage.write(&key, code.clone()).unwrap(); diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 1f365f1b9c..2582287af2 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -25,7 +25,7 @@ impl Ctx { amount: token::Amount, ) -> TxResult { let current_epoch = self.get_block_epoch()?; - bond_tokens(self, source, validator, amount, current_epoch) + bond_tokens(self, source, validator, amount, current_epoch, None) } /// Unbond self-bonded tokens from a validator when `source` is `None` @@ -132,6 +132,7 @@ impl Ctx { current_epoch, commission_rate, max_commission_rate_change, + offset_opt: None, })?; Ok(validator_address) From c045f5a437de8ec7130502794f37e58c61268d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 26 Oct 2023 17:42:52 +0200 Subject: [PATCH 07/47] fixup! [feat] Re-implementing new genesis flow in v0.24 --- apps/src/lib/cli/client.rs | 24 +++++++------------- apps/src/lib/cli/context.rs | 12 +++++----- apps/src/lib/client/utils.rs | 12 +++++----- apps/src/lib/config/genesis/transactions.rs | 24 ++++++++++---------- apps/src/lib/node/ledger/shell/init_chain.rs | 15 ++++-------- apps/src/lib/wallet/store.rs | 24 +++----------------- 6 files changed, 40 insertions(+), 71 deletions(-) diff --git a/apps/src/lib/cli/client.rs b/apps/src/lib/cli/client.rs index a465d585ce..c16c885c34 100644 --- a/apps/src/lib/cli/client.rs +++ b/apps/src/lib/cli/client.rs @@ -114,19 +114,11 @@ impl CliApi { }); client.wait_until_node_is_synced(io).await?; let args = args.to_sdk(&mut ctx); - let namada = NamadaImpl::native_new( - &client, - &mut ctx.wallet, - &mut ctx.shielded, - io, - ctx.native_token, - ); - tx::submit_init_validator( - &namada, - &mut ctx.config, - args, - ) - .await?; + let mut config = + ctx.borrow_chain_or_exit().config.clone(); + let namada = ctx.to_sdk(&client, io); + tx::submit_init_validator(&namada, &mut config, args) + .await?; } Sub::TxInitProposal(TxInitProposal(mut args)) => { let client = client.unwrap_or_else(|| { @@ -538,9 +530,9 @@ impl CliApi { let namada = ctx.to_sdk(&client, io); rpc::epoch_sleep(&namada, args).await; } - Utils::ValidateGenesisTemplates( - ValidateGenesisTemplates(args ) - ) => utils::validate_genesis_templates(global_args, args), + Utils::ValidateGenesisTemplates(ValidateGenesisTemplates( + args, + )) => utils::validate_genesis_templates(global_args, args), Utils::SignGenesisTx(SignGenesisTx(args)) => { utils::sign_genesis_tx(global_args, args) } diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index 494400de16..58ef80975d 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -20,11 +20,11 @@ use namada_sdk::wallet::Wallet; use namada_sdk::{Namada, NamadaImpl}; use super::args; -use crate::config::genesis; -use crate::config::{self, Config}; +use crate::cli::utils; +use crate::config::global::GlobalConfig; +use crate::config::{self, genesis, Config}; use crate::wallet::CliWalletUtils; use crate::{wallet, wasm_loader}; -use crate::cli::utils; /// Env. var to set chain ID const ENV_VAR_CHAIN_ID: &str = "NAMADA_CHAIN_ID"; @@ -173,9 +173,9 @@ impl Context { client: &'a C, io: &'a IO, ) -> impl Namada - where - C: namada::ledger::queries::Client + Sync, - IO: Io, + where + C: namada::ledger::queries::Client + Sync, + IO: Io, { let chain_ctx = self.borrow_mut_chain_or_exit(); NamadaImpl::native_new( diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 22340cacfc..59e697042e 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -9,10 +9,11 @@ use borsh_ext::BorshSerializeExt; use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; -use namada::types::address; use namada::types::chain::ChainId; use namada::types::dec::Dec; use namada::types::key::*; +use namada::types::uint::Uint; +use namada::types::{address, token}; use namada::vm::validate_untrusted_wasm; use namada_sdk::wallet::{alias, Wallet}; use prost::bytes::Bytes; @@ -23,11 +24,10 @@ use sha2::{Digest, Sha256}; use crate::cli::context::ENV_VAR_WASM_DIR; use crate::cli::{self, args}; -use crate::config::genesis::genesis_config::{ - self, GenesisConfig, HexString, ValidatorPreGenesisConfig, -}; use crate::config::global::GlobalConfig; -use crate::config::{self, get_default_namada_folder, Config, TendermintMode}; +use crate::config::{ + self, genesis, get_default_namada_folder, Config, TendermintMode, +}; use crate::facade::tendermint::node::Id as TendermintNodeId; use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::node::ledger::tendermint_node; @@ -413,7 +413,7 @@ pub fn init_network( } else { (Dec::from(1) / tm_votes_per_token).ceil().abs() }, - NATIVE_MAX_DECIMAL_PLACES, + token::NATIVE_MAX_DECIMAL_PLACES, ) .unwrap(); eprintln!( diff --git a/apps/src/lib/config/genesis/transactions.rs b/apps/src/lib/config/genesis/transactions.rs index 021a895c40..c66accb918 100644 --- a/apps/src/lib/config/genesis/transactions.rs +++ b/apps/src/lib/config/genesis/transactions.rs @@ -18,7 +18,7 @@ use namada::types::time::{DateTimeUtc, MIN_UTC}; use namada::types::token; use namada::types::token::{DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES}; use namada_sdk::wallet::pre_genesis::ValidatorWallet; -use namada_sdk::wallet::{FindKeyError, Wallet, WalletUtils}; +use namada_sdk::wallet::{FindKeyError, Wallet}; use serde::{Deserialize, Serialize}; use super::templates::{ @@ -28,7 +28,7 @@ use crate::config::genesis::templates::{ TemplateValidation, Tokens, Unvalidated, Validated, }; use crate::config::genesis::HexString; -use crate::wallet::Alias; +use crate::wallet::{Alias, CliWalletUtils}; pub const PRE_GENESIS_TX_TIMESTAMP: DateTimeUtc = MIN_UTC; @@ -45,9 +45,9 @@ pub struct GenesisValidatorData { /// Panics if given `txs.validator_accounts` is not empty, because validator /// transactions must be signed with a validator wallet (see /// `init-genesis-validator` command). -pub fn sign_txs( +pub fn sign_txs( txs: UnsignedTransactions, - wallet: &mut Wallet, + wallet: &mut Wallet, ) -> Transactions { let UnsignedTransactions { established_account, @@ -110,7 +110,7 @@ pub fn parse_unsigned( } /// Create signed [`Transactions`] for a genesis validator. -pub fn init_validator( +pub fn init_validator( GenesisValidatorData { source_key, alias, @@ -120,7 +120,7 @@ pub fn init_validator( transfer_from_source_amount, self_bond_amount, }: GenesisValidatorData, - source_wallet: &mut Wallet, + source_wallet: &mut Wallet, validator_wallet: &ValidatorWallet, ) -> Transactions { let unsigned_validator_account_tx = UnsignedValidatorAccountTx { @@ -198,9 +198,9 @@ pub fn init_validator( } } -pub fn sign_established_account_tx( +pub fn sign_established_account_tx( unsigned_tx: UnsignedEstablishedAccountTx, - wallet: &mut Wallet, + wallet: &mut Wallet, ) -> SignedEstablishedAccountTx { let key = unsigned_tx.public_key.as_ref().map(|pk| { let secret = wallet @@ -303,9 +303,9 @@ pub fn sign_validator_account_tx( } } -pub fn sign_transfer_tx( +pub fn sign_transfer_tx( unsigned_tx: TransferTx, - source_wallet: &mut Wallet, + source_wallet: &mut Wallet, ) -> SignedTransferTx { let source_key = source_wallet .find_key_by_pk(&unsigned_tx.source, None) @@ -320,9 +320,9 @@ pub fn sign_self_bond_tx( unsigned_tx.sign(&validator_wallet.account_key) } -pub fn sign_delegation_bond_tx( +pub fn sign_delegation_bond_tx( unsigned_tx: BondTx, - wallet: &mut Wallet, + wallet: &mut Wallet, established_accounts: &Option>>, ) -> SignedBondTx { let alias = &unsigned_tx.source; diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 68027e1548..caa4234263 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -2,13 +2,11 @@ use std::collections::HashMap; use std::hash::Hash; -use namada::ledger::parameters::{Parameters}; +use namada::ledger::parameters::Parameters; use namada::ledger::pos::OwnedPosParams; use namada::ledger::storage::traits::StorageHasher; use namada::ledger::storage::{DBIter, DB}; -use namada::ledger::storage_api::token::{ - credit_tokens, write_denom, -}; +use namada::ledger::storage_api::token::{credit_tokens, write_denom}; use namada::ledger::storage_api::{ResultExt, StorageRead, StorageWrite}; use namada::ledger::{ibc, pos}; use namada::proof_of_stake::BecomeValidator; @@ -18,6 +16,7 @@ use namada::types::storage::KeySeg; use namada::types::time::{DateTimeUtc, TimeZone, Utc}; use namada::vm::validate_untrusted_wasm; use namada_sdk::eth_bridge::EthBridgeStatus; +use namada_sdk::proof_of_stake::PosParams; use super::*; use crate::config::genesis::chain::{ @@ -289,12 +288,8 @@ where let state_key = key_prefix .push(&(token::CONVERSION_KEY_PREFIX.to_owned())) .unwrap(); - let conv_bytes = self - .wl_storage - .storage - .conversion_state - .serialize_to_vec() - .unwrap(); + let conv_bytes = + self.wl_storage.storage.conversion_state.serialize_to_vec(); self.wl_storage.write_bytes(&state_key, conv_bytes).unwrap(); } diff --git a/apps/src/lib/wallet/store.rs b/apps/src/lib/wallet/store.rs index dffaf48a98..78515d5b4a 100644 --- a/apps/src/lib/wallet/store.rs +++ b/apps/src/lib/wallet/store.rs @@ -7,8 +7,9 @@ use namada::types::address::Address; use namada::types::key::*; use namada::types::transaction::EllipticCurve; use namada_sdk::wallet::store::AddressVpType; -use namada_sdk::wallet::StoredKeypair; -use namada_sdk::wallet::{gen_sk_rng, LoadStoreError, Store, ValidatorKeys}; +use namada_sdk::wallet::{ + gen_sk_rng, LoadStoreError, Store, StoredKeypair, ValidatorKeys, +}; use crate::wallet::CliWalletUtils; @@ -36,25 +37,6 @@ pub fn load(store_dir: &Path) -> Result { Ok(wallet.into()) } -fn new() -> Store { - let mut store = Store::default(); - // Pre-load the default keys without encryption - let no_password = None; - for (alias, keypair) in super::defaults::keys() { - let pkh: PublicKeyHash = (&keypair.ref_to()).into(); - store.insert_keypair::( - alias, - StoredKeypair::new(keypair, no_password.clone()).0, - pkh, - true, - ); - } - for (alias, addr) in super::defaults::addresses() { - store.insert_address::(alias, addr, true); - } - store -} - /// Generate keypair for signing protocol txs and for the DKG /// A protocol keypair may be optionally provided /// From 87c954dfc2f830df60e80628a8b5c66799f9a924 Mon Sep 17 00:00:00 2001 From: satan Date: Fri, 27 Oct 2023 16:55:45 +0200 Subject: [PATCH 08/47] [fix]: Compiling now. Tests need fixing --- Cargo.lock | 11 + apps/Cargo.toml | 1 + apps/src/lib/bench_utils.rs | 27 +- apps/src/lib/cli.rs | 2 +- apps/src/lib/cli/client.rs | 16 +- apps/src/lib/cli/context.rs | 62 +- apps/src/lib/cli/wallet.rs | 33 +- apps/src/lib/client/rpc.rs | 4 +- apps/src/lib/client/utils.rs | 11 +- apps/src/lib/config/genesis.rs | 2 +- apps/src/lib/config/genesis/chain.rs | 36 +- apps/src/lib/config/genesis/templates.rs | 2 + apps/src/lib/config/genesis/transactions.rs | 15 +- .../lib/node/ledger/shell/finalize_block.rs | 2 +- apps/src/lib/node/ledger/shell/init_chain.rs | 16 +- apps/src/lib/node/ledger/storage/mod.rs | 21 +- apps/src/lib/wallet/mod.rs | 4 +- apps/src/lib/wallet/store.rs | 11 +- core/src/ledger/storage/masp_conversions.rs | 27 +- core/src/types/address.rs | 14 +- core/src/types/time.rs | 2 +- core/src/types/token.rs | 13 +- genesis/localnet/tokens.toml | 44 +- genesis/starter/tokens.toml | 8 +- proof_of_stake/src/lib.rs | 60 +- proof_of_stake/src/tests.rs | 47 +- sdk/src/error.rs | 2 +- sdk/src/wallet/alias.rs | 14 +- sdk/src/wallet/mod.rs | 5 +- sdk/src/wallet/store.rs | 2 +- shared/src/ledger/native_vp/ibc/mod.rs | 3 +- shared/src/ledger/pos/mod.rs | 2 +- tests/Cargo.toml | 2 + tests/src/e2e/helpers.rs | 29 +- tests/src/e2e/ibc_tests.rs | 104 +- tests/src/e2e/ledger_tests.rs | 1154 +++++------------ tests/src/e2e/setup.rs | 11 +- tests/src/integration/setup.rs | 23 +- tests/src/vm_host_env/ibc.rs | 3 +- wasm/Cargo.lock | 10 + wasm/checksums.json | 44 +- wasm_for_tests/wasm_source/Cargo.lock | 10 + 42 files changed, 831 insertions(+), 1078 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e171ac3ad9..2f5ac23b68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1305,6 +1305,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "copy_dir" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3" +dependencies = [ + "walkdir", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -4049,6 +4058,7 @@ dependencies = [ "num_cpus", "once_cell", "orion", + "pretty_assertions", "proptest", "prost", "prost-types", @@ -4298,6 +4308,7 @@ dependencies = [ "clap", "color-eyre", "concat-idents", + "copy_dir", "data-encoding", "derivative", "escargot", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 22f2caf683..8a783d4dc5 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -111,6 +111,7 @@ num-rational.workspace = true num-traits.workspace = true once_cell.workspace = true orion.workspace = true +pretty_assertions.workspace = true prost-types.workspace = true prost.workspace = true rand_core.workspace = true diff --git a/apps/src/lib/bench_utils.rs b/apps/src/lib/bench_utils.rs index 5f58a93a58..ef2791ac7a 100644 --- a/apps/src/lib/bench_utils.rs +++ b/apps/src/lib/bench_utils.rs @@ -682,25 +682,26 @@ impl Default for BenchShieldedCtx { fn default() -> Self { let mut shell = BenchShell::default(); - let mut ctx = Context::new::(crate::cli::args::Global { + let ctx = Context::new::(crate::cli::args::Global { chain_id: None, base_dir: shell.tempdir.as_ref().canonicalize().unwrap(), wasm_dir: Some(WASM_DIR.into()), }) .unwrap(); + let mut chain_ctx = ctx.take_chain_or_exit(); // Generate spending key for Albert and Bertha - ctx.wallet.gen_spending_key( + chain_ctx.wallet.gen_spending_key( ALBERT_SPENDING_KEY.to_string(), None, true, ); - ctx.wallet.gen_spending_key( + chain_ctx.wallet.gen_spending_key( BERTHA_SPENDING_KEY.to_string(), None, true, ); - crate::wallet::save(&ctx.wallet).unwrap(); + crate::wallet::save(&chain_ctx.wallet).unwrap(); // Generate payment addresses for both Albert and Bertha for (alias, viewing_alias) in [ @@ -710,19 +711,21 @@ impl Default for BenchShieldedCtx { .map(|(p, s)| (p.to_owned(), s.to_owned())) { let viewing_key: FromContext = FromContext::new( - ctx.wallet + chain_ctx + .wallet .find_viewing_key(viewing_alias) .unwrap() .to_string(), ); - let viewing_key = - ExtendedFullViewingKey::from(ctx.get_cached(&viewing_key)) - .fvk - .vk; + let viewing_key = ExtendedFullViewingKey::from( + chain_ctx.get_cached(&viewing_key), + ) + .fvk + .vk; let (div, _g_d) = namada_sdk::masp::find_valid_diversifier(&mut OsRng); let payment_addr = viewing_key.to_payment_address(div).unwrap(); - let _ = ctx + let _ = chain_ctx .wallet .insert_payment_addr( alias, @@ -732,7 +735,7 @@ impl Default for BenchShieldedCtx { .unwrap(); } - crate::wallet::save(&ctx.wallet).unwrap(); + crate::wallet::save(&chain_ctx.wallet).unwrap(); namada::ledger::storage::update_allowed_conversions( &mut shell.wl_storage, ) @@ -741,7 +744,7 @@ impl Default for BenchShieldedCtx { Self { shielded: ShieldedContext::default(), shell, - wallet: ctx.wallet, + wallet: chain_ctx.wallet, } } } diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index c7ddefef68..b9b4d15780 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -4964,7 +4964,7 @@ pub mod args { let query = self.query.to_sdk(ctx); let chain_ctx = ctx.borrow_chain_or_exit(); GenIbcShieldedTransafer:: { - query: self.query.to_sdk(ctx), + query, output_folder: self.output_folder, target: chain_ctx.get(&self.target), token: chain_ctx.get(&self.token), diff --git a/apps/src/lib/cli/client.rs b/apps/src/lib/cli/client.rs index c16c885c34..9a88626561 100644 --- a/apps/src/lib/cli/client.rs +++ b/apps/src/lib/cli/client.rs @@ -114,9 +114,19 @@ impl CliApi { }); client.wait_until_node_is_synced(io).await?; let args = args.to_sdk(&mut ctx); - let mut config = - ctx.borrow_chain_or_exit().config.clone(); - let namada = ctx.to_sdk(&client, io); + let cli::context::ChainContext { + mut wallet, + mut config, + mut shielded, + native_token, + } = ctx.take_chain_or_exit(); + let namada = NamadaImpl::native_new( + &client, + &mut wallet, + &mut shielded, + io, + native_token, + ); tx::submit_init_validator(&namada, &mut config, args) .await?; } diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index 58ef80975d..3b8d942040 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -241,20 +241,7 @@ impl ChainContext { /// /// Note that in "dev" build, this may be the root `wasm` dir. pub fn wasm_dir(&self) -> PathBuf { - let wasm_dir = - self.config.ledger.chain_dir().join(&self.config.wasm_dir); - - // In dev-mode with dev chain (the default), load wasm directly from the - // root wasm dir instead of the chain dir - #[cfg(feature = "dev")] - let wasm_dir = - if self.global_config.default_chain_id == ChainId::default() { - "wasm".into() - } else { - wasm_dir - }; - - wasm_dir + self.config.ledger.chain_dir().join(&self.config.wasm_dir) } /// Read the given WASM file from the WASM directory or an absolute path. @@ -309,10 +296,6 @@ impl FromContext { phantom: PhantomData, } } - - pub fn into_raw(self) -> String { - self.raw - } } impl FromContext { @@ -360,8 +343,8 @@ impl FromContext where T: ArgFromContext, { - /// Parse and/or look-up the value from the context. - fn arg_from_ctx(&self, ctx: &Context) -> Result { + /// Parse and/or look-up the value from the chain context. + fn arg_from_ctx(&self, ctx: &ChainContext) -> Result { T::arg_from_ctx(ctx, &self.raw) } } @@ -370,32 +353,32 @@ impl FromContext where T: ArgFromMutContext, { - /// Parse and/or look-up the value from the mutable context. - fn arg_from_mut_ctx(&self, ctx: &mut Context) -> Result { + /// Parse and/or look-up the value from the mutable chain context. + fn arg_from_mut_ctx(&self, ctx: &mut ChainContext) -> Result { T::arg_from_mut_ctx(ctx, &self.raw) } } -/// CLI argument that found via the [`Context`]. +/// CLI argument that found via the [`ChainContext`]. pub trait ArgFromContext: Sized { fn arg_from_ctx( - ctx: &Context, + ctx: &ChainContext, raw: impl AsRef, ) -> Result; } -/// CLI argument that found via the [`Context`] and cached (as in case of an -/// encrypted keypair that has been decrypted), hence using mutable context. +/// CLI argument that found via the [`ChainContext`] and cached (as in case of +/// an encrypted keypair that has been decrypted), hence using mutable context. pub trait ArgFromMutContext: Sized { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result; } impl ArgFromContext for Address { fn arg_from_ctx( - ctx: &Context, + ctx: &ChainContext, raw: impl AsRef, ) -> Result { struct Skip; @@ -429,14 +412,19 @@ impl ArgFromContext for Address { .ok_or(Skip) }) // Or it can be an alias that may be found in the wallet - .or_else(|_| ctx.wallet.find_address(raw).cloned().ok_or(Skip)) + .or_else(|_| { + ctx.wallet + .find_address(raw) + .map(|x| x.into_owned()) + .ok_or(Skip) + }) .map_err(|_| format!("Unknown address {raw}")) } } impl ArgFromMutContext for common::SecretKey { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -452,7 +440,7 @@ impl ArgFromMutContext for common::SecretKey { impl ArgFromMutContext for common::PublicKey { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -477,7 +465,7 @@ impl ArgFromMutContext for common::PublicKey { impl ArgFromMutContext for ExtendedSpendingKey { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -493,7 +481,7 @@ impl ArgFromMutContext for ExtendedSpendingKey { impl ArgFromMutContext for ExtendedViewingKey { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -510,7 +498,7 @@ impl ArgFromMutContext for ExtendedViewingKey { impl ArgFromContext for PaymentAddress { fn arg_from_ctx( - ctx: &Context, + ctx: &ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -527,7 +515,7 @@ impl ArgFromContext for PaymentAddress { impl ArgFromMutContext for TransferSource { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -543,7 +531,7 @@ impl ArgFromMutContext for TransferSource { impl ArgFromContext for TransferTarget { fn arg_from_ctx( - ctx: &Context, + ctx: &ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); @@ -558,7 +546,7 @@ impl ArgFromContext for TransferTarget { impl ArgFromMutContext for BalanceOwner { fn arg_from_mut_ctx( - ctx: &mut Context, + ctx: &mut ChainContext, raw: impl AsRef, ) -> Result { let raw = raw.as_ref(); diff --git a/apps/src/lib/cli/wallet.rs b/apps/src/lib/cli/wallet.rs index 6d949ac462..3b43e09b74 100644 --- a/apps/src/lib/cli/wallet.rs +++ b/apps/src/lib/cli/wallet.rs @@ -16,7 +16,6 @@ use namada_sdk::wallet::{ WalletStorage, }; use namada_sdk::{display, display_line, edisplay_line}; -use rand::RngCore; use rand_core::OsRng; use crate::cli; @@ -24,7 +23,9 @@ use crate::cli::api::CliApi; use crate::cli::args::CliToSdk; use crate::cli::{args, cmds, Context}; use crate::client::utils::PRE_GENESIS_DIR; -use crate::wallet::{self, read_and_confirm_encryption_password, CliWalletUtils}; +use crate::wallet::{ + self, read_and_confirm_encryption_password, CliWalletUtils, +}; impl CliApi { pub fn handle_wallet_command( @@ -35,27 +36,35 @@ impl CliApi { match cmd { cmds::NamadaWallet::Key(sub) => match sub { cmds::WalletKey::Restore(cmds::KeyRestore(args)) => { - key_and_address_restore(ctx, io, args) + key_and_address_restore( + &mut ctx.borrow_mut_chain_or_exit().wallet, + io, + args, + ) } cmds::WalletKey::Gen(cmds::KeyGen(args)) => { - key_and_address_gen(ctx, io, &mut OsRng, args) + key_and_address_gen(ctx, io, args) } cmds::WalletKey::Find(cmds::KeyFind(args)) => { key_find(ctx, io, args) } cmds::WalletKey::List(cmds::KeyList(args)) => { - key_list( ctx, io, args) + key_list(ctx, io, args) } cmds::WalletKey::Export(cmds::Export(args)) => { - key_export( ctx, io, args) + key_export(ctx, io, args) } }, cmds::NamadaWallet::Address(sub) => match sub { cmds::WalletAddress::Gen(cmds::AddressGen(args)) => { - key_and_address_gen(ctx, io, &mut OsRng, args) + key_and_address_gen(ctx, io, args) } cmds::WalletAddress::Restore(cmds::AddressRestore(args)) => { - key_and_address_restore(&mut ctx.borrow_mut_chain_or_exit().wallet, io, args) + key_and_address_restore( + &mut ctx.borrow_mut_chain_or_exit().wallet, + io, + args, + ) } cmds::WalletAddress::Find(cmds::AddressOrAliasFind(args)) => { address_or_alias_find(ctx, io, args) @@ -73,7 +82,11 @@ impl CliApi { } cmds::WalletMasp::GenPayAddr(cmds::MaspGenPayAddr(args)) => { let args = args.to_sdk(&mut ctx); - payment_address_gen(&mut ctx.borrow_mut_chain_or_exit().wallet, io, args) + payment_address_gen( + &mut ctx.borrow_mut_chain_or_exit().wallet, + io, + args, + ) } cmds::WalletMasp::AddAddrKey(cmds::MaspAddAddrKey(args)) => { address_key_add(ctx, io, args) @@ -396,7 +409,7 @@ fn key_and_address_restore( /// Generate a new keypair and derive implicit address from it and store them in /// the wallet. -fn key_and_address_gen( +fn key_and_address_gen( ctx: Context, io: &impl Io, args::KeyAndAddressGen { diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index e6ddbff2e9..0231e3731d 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -410,9 +410,9 @@ pub async fn query_pinned_balance<'a>( .collect(); let _ = context.shielded_mut().await.load().await; // Print the token balances by payment address - let pinned_error = Err(Error::from(PinnedBalanceError::InvalidViewingKey)); for owner in owners { - let mut balance = pinned_error.clone(); + let mut balance = + Err(Error::from(PinnedBalanceError::InvalidViewingKey)); // Find the viewing key that can recognize payments the current payment // address for vk in &viewing_keys { diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 59e697042e..cb17d4bc62 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::env; use std::fs::{self, File, OpenOptions}; use std::io::Write; @@ -12,18 +11,16 @@ use flate2::Compression; use namada::types::chain::ChainId; use namada::types::dec::Dec; use namada::types::key::*; +use namada::types::token; use namada::types::uint::Uint; -use namada::types::{address, token}; use namada::vm::validate_untrusted_wasm; use namada_sdk::wallet::{alias, Wallet}; use prost::bytes::Bytes; -use rand::prelude::ThreadRng; -use rand::thread_rng; use serde_json::json; use sha2::{Digest, Sha256}; +use crate::cli::args; use crate::cli::context::ENV_VAR_WASM_DIR; -use crate::cli::{self, args}; use crate::config::global::GlobalConfig; use crate::config::{ self, genesis, get_default_namada_folder, Config, TendermintMode, @@ -31,9 +28,7 @@ use crate::config::{ use crate::facade::tendermint::node::Id as TendermintNodeId; use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::node::ledger::tendermint_node; -use crate::wallet::{ - pre_genesis, read_and_confirm_encryption_password, CliWalletUtils, -}; +use crate::wallet::{pre_genesis, CliWalletUtils}; use crate::wasm_loader; pub const NET_ACCOUNTS_DIR: &str = "setup"; diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 08a861e423..659617d99a 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -273,11 +273,11 @@ pub fn make_dev_genesis(num_validators: u64) -> Finalized { use namada::core::types::string_encoding::StringEncoded; use namada::ledger::eth_bridge::{Contracts, UpgradeableContract}; use namada::proto::{standalone_signature, SerializeWithBorsh}; - use namada_sdk::wallet::alias::Alias; use namada::types::address::wnam; use namada::types::chain::ChainIdPrefix; use namada::types::ethereum_events::EthAddress; use namada::types::token::NATIVE_MAX_DECIMAL_PLACES; + use namada_sdk::wallet::alias::Alias; use crate::config::genesis::chain::finalize; use crate::wallet::defaults; diff --git a/apps/src/lib/config/genesis/chain.rs b/apps/src/lib/config/genesis/chain.rs index 0d064789c8..69dd23f658 100644 --- a/apps/src/lib/config/genesis/chain.rs +++ b/apps/src/lib/config/genesis/chain.rs @@ -3,15 +3,16 @@ use std::path::Path; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; +use borsh_ext::BorshSerializeExt; use namada::ledger::parameters::EpochDuration; -use namada_sdk::wallet::store::AddressVpType; -use namada_sdk::wallet::{pre_genesis, Wallet}; use namada::types::address::{masp, Address, EstablishedAddressGen}; use namada::types::chain::{ChainId, ChainIdPrefix}; use namada::types::dec::Dec; use namada::types::hash::Hash; use namada::types::time::{DateTimeUtc, DurationNanos, Rfc3339String}; use namada::types::token::Amount; +use namada_sdk::wallet::store::AddressVpType; +use namada_sdk::wallet::{pre_genesis, Wallet}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -338,18 +339,21 @@ impl Finalized { } = self.parameters.pos_params.clone(); namada::proof_of_stake::parameters::PosParams { - max_validator_slots, - pipeline_len, - unbonding_len, - tm_votes_per_token, - block_proposer_reward, - block_vote_reward, - max_inflation_rate, - target_staked_ratio, - duplicate_vote_min_slash_rate, - light_client_attack_min_slash_rate, - cubic_slashing_window_length, - validator_stake_threshold, + owned: namada::proof_of_stake::parameters::OwnedPosParams { + max_validator_slots, + pipeline_len, + unbonding_len, + tm_votes_per_token, + block_proposer_reward, + block_vote_reward, + max_inflation_rate, + target_staked_ratio, + duplicate_vote_min_slash_rate, + light_client_attack_min_slash_rate, + cubic_slashing_window_length, + validator_stake_threshold, + }, + max_proposal_period: self.parameters.gov_params.max_proposal_period, } } @@ -458,7 +462,7 @@ pub fn finalize( address_gen: None, }, }; - let genesis_bytes = genesis_to_gen_address.try_to_vec().unwrap(); + let genesis_bytes = genesis_to_gen_address.serialize_to_vec(); let mut addr_gen = established_address_gen(&genesis_bytes); // Generate addresses @@ -488,7 +492,7 @@ pub fn finalize( parameters, transactions, }; - let to_finalize_bytes = to_finalize.try_to_vec().unwrap(); + let to_finalize_bytes = to_finalize.serialize_to_vec(); let chain_id = ChainId::from_genesis(chain_id_prefix, to_finalize_bytes); // Construct the `Finalized` chain diff --git a/apps/src/lib/config/genesis/templates.rs b/apps/src/lib/config/genesis/templates.rs index df01fc2fdc..15defbd17f 100644 --- a/apps/src/lib/config/genesis/templates.rs +++ b/apps/src/lib/config/genesis/templates.rs @@ -210,8 +210,10 @@ pub struct Tokens { )] pub struct TokenConfig { pub denom: Denomination, + pub parameters: token::Parameters } + #[derive( Clone, Debug, diff --git a/apps/src/lib/config/genesis/transactions.rs b/apps/src/lib/config/genesis/transactions.rs index c66accb918..8595db5f18 100644 --- a/apps/src/lib/config/genesis/transactions.rs +++ b/apps/src/lib/config/genesis/transactions.rs @@ -6,6 +6,7 @@ use std::net::SocketAddr; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; +use borsh_ext::BorshSerializeExt; use namada::core::types::storage; use namada::core::types::string_encoding::StringEncoded; use namada::proto::{ @@ -674,10 +675,10 @@ impl TransferTx { /// The signable data. This does not include the phantom data. fn data_to_sign(&self) -> Vec { [ - self.token.try_to_vec().unwrap(), - self.source.try_to_vec().unwrap(), - self.target.try_to_vec().unwrap(), - self.amount.try_to_vec().unwrap(), + self.token.serialize_to_vec(), + self.source.serialize_to_vec(), + self.target.serialize_to_vec(), + self.amount.serialize_to_vec(), ] .concat() } @@ -766,9 +767,9 @@ impl BondTx { /// The signable data. This does not include the phantom data. fn data_to_sign(&self) -> Vec { [ - self.source.try_to_vec().unwrap(), - self.validator.try_to_vec().unwrap(), - self.amount.try_to_vec().unwrap(), + self.source.serialize_to_vec(), + self.validator.serialize_to_vec(), + self.amount.serialize_to_vec(), ] .concat() } diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 3f6fbcd398..2202f7a157 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -3094,7 +3094,7 @@ mod test_finalize_block { .unwrap(); // Self-unbond - let self_unbond_1_amount = token::Amount::native_whole(154_654); + let self_unbond_1_amount = token::Amount::native_whole(54_654); namada_proof_of_stake::unbond_tokens( &mut shell.wl_storage, None, diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index caa4234263..2b36a4e7a8 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -3,11 +3,10 @@ use std::collections::HashMap; use std::hash::Hash; use namada::ledger::parameters::Parameters; -use namada::ledger::pos::OwnedPosParams; use namada::ledger::storage::traits::StorageHasher; use namada::ledger::storage::{DBIter, DB}; use namada::ledger::storage_api::token::{credit_tokens, write_denom}; -use namada::ledger::storage_api::{ResultExt, StorageRead, StorageWrite}; +use namada::ledger::storage_api::StorageWrite; use namada::ledger::{ibc, pos}; use namada::proof_of_stake::BecomeValidator; use namada::types::hash::Hash as CodeHash; @@ -269,10 +268,11 @@ where let FinalizedTokenConfig { address, - config: TokenConfig { denom }, + config: TokenConfig { denom, parameters }, } = token; // associate a token with its denomination. write_denom(&mut self.wl_storage, address, *denom).unwrap(); + parameters.init_storage(address, &mut self.wl_storage); // add token addresses to the masp reward conversions lookup table. let alias = alias.to_string(); if masp_rewards.contains_key(&alias.as_str()) { @@ -304,6 +304,7 @@ where .get(token_alias) .expect("Token with configured balance not found in genesis.") .address; + let mut total_token_balance = token::Amount::zero(); for (owner_pk, balance) in balances { let owner = Address::from(&owner_pk.raw); storage_api::account::set_public_key_at( @@ -326,7 +327,16 @@ where balance.amount, ) .expect("Couldn't credit initial balance"); + total_token_balance += balance.amount; } + // Write the total amount of tokens for the ratio + self.wl_storage + .write( + &token::minted_balance_key(token_address), + total_token_balance, + ) + .unwrap(); + } } diff --git a/apps/src/lib/node/ledger/storage/mod.rs b/apps/src/lib/node/ledger/storage/mod.rs index 659f561cce..329d0a7bd8 100644 --- a/apps/src/lib/node/ledger/storage/mod.rs +++ b/apps/src/lib/node/ledger/storage/mod.rs @@ -62,7 +62,7 @@ mod tests { use namada::types::chain::ChainId; use namada::types::hash::Hash; use namada::types::storage::{BlockHash, BlockHeight, Key}; - use namada::types::{address, storage}; + use namada::types::{address, storage, token}; use proptest::collection::vec; use proptest::prelude::*; use proptest::test_runner::Config; @@ -144,6 +144,25 @@ mod tests { storage.block.pred_epochs.new_epoch(BlockHeight(100)); // make wl_storage to update conversion for a new epoch let mut wl_storage = WlStorage::new(WriteLog::default(), storage); + let token_params = token::Parameters { + max_reward_rate: Default::default(), + kd_gain_nom: Default::default(), + kp_gain_nom: Default::default(), + locked_ratio_target: Default::default(), + }; + // Insert a map assigning random addresses to each token alias. + // Needed for storage but not for this test. + for (token, _) in address::tokens() { + let addr = address::gen_deterministic_established_address(token); + token_params.init_storage(&addr, &mut wl_storage); + wl_storage.write(&token::minted_balance_key(&addr), token::Amount::zero()).unwrap(); + wl_storage.storage.conversion_state.tokens.insert( + token.to_string(), + addr, + ); + } + token_params.init_storage(&wl_storage.storage.native_token.clone(), &mut wl_storage); + wl_storage.write(&token::minted_balance_key(&wl_storage.storage.native_token.clone()), token::Amount::zero()).unwrap(); update_allowed_conversions(&mut wl_storage) .expect("update conversions failed"); wl_storage.commit_block().expect("commit failed"); diff --git a/apps/src/lib/wallet/mod.rs b/apps/src/lib/wallet/mod.rs index 779a137ccd..4cf55df158 100644 --- a/apps/src/lib/wallet/mod.rs +++ b/apps/src/lib/wallet/mod.rs @@ -4,7 +4,6 @@ mod store; use std::io::{self, Write}; use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::{env, fs}; use namada::bip39::{Language, Mnemonic}; @@ -13,8 +12,7 @@ pub use namada_sdk::wallet::alias::Alias; use namada_sdk::wallet::fs::FsWalletStorage; use namada_sdk::wallet::store::Store; use namada_sdk::wallet::{ - ConfirmationResponse, FindKeyError, GenRestoreKeyError, - Wallet, WalletIo, + ConfirmationResponse, FindKeyError, GenRestoreKeyError, Wallet, WalletIo, }; pub use namada_sdk::wallet::{ValidatorData, ValidatorKeys}; use rand_core::OsRng; diff --git a/apps/src/lib/wallet/store.rs b/apps/src/lib/wallet/store.rs index 78515d5b4a..b358c886b1 100644 --- a/apps/src/lib/wallet/store.rs +++ b/apps/src/lib/wallet/store.rs @@ -1,15 +1,10 @@ use std::path::{Path, PathBuf}; -use std::str::FromStr; use ark_std::rand::prelude::*; use ark_std::rand::SeedableRng; -use namada::types::address::Address; use namada::types::key::*; use namada::types::transaction::EllipticCurve; -use namada_sdk::wallet::store::AddressVpType; -use namada_sdk::wallet::{ - gen_sk_rng, LoadStoreError, Store, StoredKeypair, ValidatorKeys, -}; +use namada_sdk::wallet::{gen_sk_rng, LoadStoreError, Store, ValidatorKeys}; use crate::wallet::CliWalletUtils; @@ -74,7 +69,7 @@ mod test_wallet { #[test] fn test_toml_roundtrip_ed25519() { - let mut store = new(); + let mut store = Store::default(); let validator_keys = gen_validator_keys(None, None, SchemeType::Ed25519); store.add_validator_data( @@ -87,7 +82,7 @@ mod test_wallet { #[test] fn test_toml_roundtrip_secp256k1() { - let mut store = new(); + let mut store = Store::default(); let validator_keys = gen_validator_keys(None, None, SchemeType::Secp256k1); store.add_validator_data( diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 6b8d542d53..5cbbca570a 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -217,15 +217,24 @@ where let key_prefix: storage::Key = masp_addr.to_db_key().into(); let tokens = address::tokens(); - let mut masp_reward_keys: Vec<_> = tokens.into_keys() - .map(|k| wl_storage - .storage - .conversion_state - .tokens - .get(k) - .unwrap_or_else(|| panic!("Could not find token alias {} in MASP conversion state.", k)) - .clone() - ).collect(); + let mut masp_reward_keys: Vec<_> = tokens + .into_keys() + .map(|k| { + wl_storage + .storage + .conversion_state + .tokens + .get(k) + .unwrap_or_else(|| { + panic!( + "Could not find token alias {} in MASP conversion \ + state.", + k + ) + }) + .clone() + }) + .collect(); // Put the native rewards first because other inflation computations depend // on it let native_token = wl_storage.storage.native_token.clone(); diff --git a/core/src/types/address.rs b/core/src/types/address.rs index 3493fc6d8f..c9a9396187 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -639,13 +639,13 @@ pub const fn wnam() -> EthAddress { /// informal currency codes and number of decimal places. pub fn tokens() -> HashMap<&'static str, Denomination> { vec![ - ("NAM", 6.into()), - ("BTC", 8.into()), - ("ETH", 18.into()), - ("DOT", 10.into()), - ("Schnitzel", 6.into()), - ("Apfel", 6.into()), - ("Kartoffel", 6.into()), + ("nam", 6.into()), + ("btc", 8.into()), + ("eth", 18.into()), + ("dot", 10.into()), + ("schnitzel", 6.into()), + ("apfel", 6.into()), + ("kartoffel", 6.into()), ] .into_iter() .collect() diff --git a/core/src/types/time.rs b/core/src/types/time.rs index 54622e15a6..948bf6373f 100644 --- a/core/src/types/time.rs +++ b/core/src/types/time.rs @@ -10,7 +10,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use chrono::ParseError; pub use chrono::{DateTime, Duration, TimeZone, Utc}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; /// Check if the given `duration` has passed since the given `start. pub fn duration_passed( diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 73a78b5bd9..343fc3e3e5 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1026,6 +1026,18 @@ impl Parameters { kp_gain_nom, locked_ratio_target: locked_target, } = self; + wl_storage + .write( + &masp_last_inflation_key(&address), + Amount::zero(), + ) + .expect("last inflation key for the given asset must be initialized"); + wl_storage + .write( + &masp_last_locked_ratio_key(&address), + Dec::zero(), + ) + .expect("last locked ratio key for the given asset must be initialized"); wl_storage .write(&masp_max_reward_rate_key(address), max_rate) .expect("max reward rate for the given asset must be initialized"); @@ -1482,5 +1494,4 @@ pub mod testing { ) -> impl Strategy { (1..=max).prop_map(|val| Amount::from_uint(val, 0).unwrap()) } - } diff --git a/genesis/localnet/tokens.toml b/genesis/localnet/tokens.toml index 3079fab700..db1da7df9d 100644 --- a/genesis/localnet/tokens.toml +++ b/genesis/localnet/tokens.toml @@ -3,20 +3,62 @@ [token.NAM] denom = 6 +[token.NAM.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" + [token.BTC] denom = 8 +[token.BTC.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" + [token.ETH] denom = 18 +[token.ETH.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" + [token.DOT] denom = 10 +[token.DOT.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" + [token.Schnitzel] denom = 6 +[token.Schnitzel.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" + [token.Apfel] denom = 6 +[token.Apfel.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" + [token.Kartoffel] -denom = 6 \ No newline at end of file +denom = 6 + +[token.Kartoffel.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" \ No newline at end of file diff --git a/genesis/starter/tokens.toml b/genesis/starter/tokens.toml index 612a6901ff..d0f9d44d44 100644 --- a/genesis/starter/tokens.toml +++ b/genesis/starter/tokens.toml @@ -2,4 +2,10 @@ [token.NAM] vp = "vp_token" -denom = 6 \ No newline at end of file +denom = 6 + +[token.NAM.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target = "0.6667" \ No newline at end of file diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 618b20a373..72b58150a5 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -60,11 +60,11 @@ use types::{ BondsAndUnbondsDetail, BondsAndUnbondsDetails, CommissionRates, ConsensusValidator, ConsensusValidatorSet, ConsensusValidatorSets, DelegatorRedelegatedBonded, DelegatorRedelegatedUnbonded, - EagerRedelegatedBondsMap, EpochedSlashes, - IncomingRedelegations, OutgoingRedelegations, Position, - RedelegatedBondsOrUnbonds, RedelegatedTokens, ReverseOrdTokenAmount, - RewardsAccumulator, RewardsProducts, Slash, SlashType, SlashedAmount, - Slashes, TotalConsensusStakes, TotalDeltas, TotalRedelegatedBonded, + EagerRedelegatedBondsMap, EpochedSlashes, IncomingRedelegations, + OutgoingRedelegations, Position, RedelegatedBondsOrUnbonds, + RedelegatedTokens, ReverseOrdTokenAmount, RewardsAccumulator, + RewardsProducts, Slash, SlashType, SlashedAmount, Slashes, + TotalConsensusStakes, TotalDeltas, TotalRedelegatedBonded, TotalRedelegatedUnbonded, UnbondDetails, Unbonds, ValidatorAddresses, ValidatorConsensusKeys, ValidatorDeltas, ValidatorEthColdKeys, ValidatorEthHotKeys, ValidatorPositionAddresses, ValidatorProtocolKeys, @@ -327,7 +327,12 @@ where // Copy the genesis validator sets up to the pipeline epoch for epoch in (current_epoch.next()).iter_range(params.pipeline_len) { - copy_validator_sets_and_positions(storage, ¶ms, current_epoch, epoch)?; + copy_validator_sets_and_positions( + storage, + ¶ms, + current_epoch, + epoch, + )?; store_total_consensus_stake(storage, epoch)?; } Ok(()) @@ -816,12 +821,7 @@ where // Initialize or update the bond at the pipeline offset bond_handle.add(storage, amount, current_epoch, offset)?; - total_bonded_handle.add( - storage, - amount, - current_epoch, - offset, - )?; + total_bonded_handle.add(storage, amount, current_epoch, offset)?; if tracing::level_enabled!(tracing::Level::DEBUG) { let bonds = find_bonds(storage, source, validator)?; @@ -843,7 +843,7 @@ where ¶ms, validator, amount.change(), - offset_epoch, + current_epoch, offset_opt, )?; } @@ -2778,6 +2778,15 @@ where offset, )?; + insert_validator_into_validator_set( + storage, + ¶ms, + &address, + token::Amount::zero(), + current_epoch, + offset, + )?; + Ok(()) } @@ -4473,9 +4482,15 @@ where &validator, -slash_delta.change(), epoch, - None, + Some(0), + )?; + update_total_deltas( + storage, + ¶ms, + -slash_delta.change(), + epoch, + Some(0), )?; - update_total_deltas(storage, ¶ms, -slash_delta.change(), epoch, None)?; } // TODO: should we clear some storage here as is done in Quint?? @@ -5339,14 +5354,15 @@ pub mod test_utils { validators: impl Iterator, current_epoch: namada_core::types::storage::Epoch, ) -> storage_api::Result<()> - where - S: StorageRead + StorageWrite, + where + S: StorageRead + StorageWrite, { init_genesis(storage, params, current_epoch)?; for GenesisValidator { address, consensus_key, - protocol_key, eth_cold_key, + protocol_key, + eth_cold_key, eth_hot_key, commission_rate, max_commission_rate_change, @@ -5389,16 +5405,16 @@ pub mod test_utils { Ok(()) } - /// Init PoS genesis wrapper helper that also initializes gov params that are - /// used in PoS with default values. + /// Init PoS genesis wrapper helper that also initializes gov params that + /// are used in PoS with default values. pub fn test_init_genesis( storage: &mut S, owned: OwnedPosParams, validators: impl Iterator + Clone, current_epoch: namada_core::types::storage::Epoch, ) -> storage_api::Result - where - S: StorageRead + StorageWrite, + where + S: StorageRead + StorageWrite, { let gov_params = namada_core::ledger::governance::parameters::GovernanceParameters::default(); gov_params.init_storage(storage)?; diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index b75c3f207c..070f93aeaf 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -41,6 +41,7 @@ use test_log::test; use crate::epoched::DEFAULT_NUM_PAST_EPOCHS; use crate::parameters::testing::arb_pos_params; use crate::parameters::{OwnedPosParams, PosParams}; +use crate::test_utils::test_init_genesis; use crate::types::{ into_tm_voting_power, BondDetails, BondId, BondsAndUnbondsDetails, ConsensusValidator, EagerRedelegatedBondsMap, GenesisValidator, Position, @@ -64,11 +65,10 @@ use crate::{ read_consensus_validator_set_addresses_with_stake, read_total_stake, read_validator_deltas_value, read_validator_stake, slash, slash_redelegation, slash_validator, slash_validator_redelegation, - staking_token_address, store_total_consensus_stake, test_utils::test_init_genesis, - total_bonded_handle, total_deltas_handle, total_unbonded_handle, - unbond_handle, unbond_tokens, unjail_validator, update_validator_deltas, - update_validator_set, validator_consensus_key_handle, - validator_incoming_redelegations_handle, + staking_token_address, store_total_consensus_stake, total_bonded_handle, + total_deltas_handle, total_unbonded_handle, unbond_handle, unbond_tokens, + unjail_validator, update_validator_deltas, update_validator_set, + validator_consensus_key_handle, validator_incoming_redelegations_handle, validator_outgoing_redelegations_handle, validator_set_positions_handle, validator_set_update_tendermint, validator_slashes_handle, validator_state_handle, validator_total_redelegated_bonded_handle, @@ -970,7 +970,8 @@ fn test_become_validator_aux( let staking_token = staking_token_address(&s); let amount = token::Amount::from_uint(100_500_000, 0).unwrap(); credit_tokens(&mut s, &staking_token, &new_validator, amount).unwrap(); - bond_tokens(&mut s, None, &new_validator, amount, current_epoch, None).unwrap(); + bond_tokens(&mut s, None, &new_validator, amount, current_epoch, None) + .unwrap(); // Check the bond delta let bond_handle = bond_handle(&new_validator, &new_validator); @@ -1321,15 +1322,8 @@ fn test_validator_sets() { ) .unwrap(); - update_validator_deltas( - s, - ¶ms, - addr, - stake.change(), - epoch, - None, - ) - .unwrap(); + update_validator_deltas(s, ¶ms, addr, stake.change(), epoch, None) + .unwrap(); // Set their consensus key (needed for // `validator_set_update_tendermint` fn) @@ -1754,17 +1748,17 @@ fn test_validator_sets() { let bond = token::Amount::from_uint(500_000, 0).unwrap(); let stake6 = stake6 + bond; println!("val6 {val6} new stake {}", stake6.to_string_native()); - update_validator_set(&mut s, ¶ms, &val6, bond.change(), pipeline_epoch, None) - .unwrap(); - update_validator_deltas( + update_validator_set( &mut s, ¶ms, &val6, bond.change(), - epoch, + pipeline_epoch, None, ) .unwrap(); + update_validator_deltas(&mut s, ¶ms, &val6, bond.change(), epoch, None) + .unwrap(); let val6_bond_epoch = pipeline_epoch; let consensus_vals: Vec<_> = consensus_validator_set_handle() @@ -2006,15 +2000,8 @@ fn test_validator_sets_swap() { ) .unwrap(); - update_validator_deltas( - s, - ¶ms, - addr, - stake.change(), - epoch, - None, - ) - .unwrap(); + update_validator_deltas(s, ¶ms, addr, stake.change(), epoch, None) + .unwrap(); // Set their consensus key (needed for // `validator_set_update_tendermint` fn) @@ -2068,7 +2055,7 @@ fn test_validator_sets_swap() { &val3, bond3.change(), pipeline_epoch, - None, + None, ) .unwrap(); update_validator_deltas( @@ -2130,7 +2117,7 @@ fn test_validator_sets_swap() { &val3, bonds.change(), epoch, -None, + None, ) .unwrap(); diff --git a/sdk/src/error.rs b/sdk/src/error.rs index aa4c2c9842..3512d1a9f2 100644 --- a/sdk/src/error.rs +++ b/sdk/src/error.rs @@ -19,7 +19,7 @@ pub type Result = std::result::Result; /// /// The general mentality should be that this error type should cover all /// possible errors that one may face. -#[derive( Error, Debug)] +#[derive(Error, Debug)] pub enum Error { /// Errors that are caused by trying to retrieve a pinned transaction #[error("Error in retrieving pinned balance: {0}")] diff --git a/sdk/src/wallet/alias.rs b/sdk/src/wallet/alias.rs index 61ba36d74e..48ab4a9fa0 100644 --- a/sdk/src/wallet/alias.rs +++ b/sdk/src/wallet/alias.rs @@ -5,10 +5,10 @@ use std::fmt::Display; use std::hash::Hash; use std::io::Read; use std::str::FromStr; -use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; +use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::types::address::{Address, InternalAddress}; +use serde::{Deserialize, Serialize}; /// Aliases created from raw strings are kept in-memory as given, but their /// `Serialize` and `Display` instance converts them to lowercase. Their @@ -62,8 +62,8 @@ impl BorshDeserialize for Alias { impl Serialize for Alias { fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, + where + S: serde::Serializer, { Serialize::serialize(&self.normalize(), serializer) } @@ -71,8 +71,8 @@ impl Serialize for Alias { impl<'de> Deserialize<'de> for Alias { fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, + where + D: serde::Deserializer<'de>, { let raw: String = Deserialize::deserialize(deserializer)?; Ok(Self::from(raw)) @@ -85,7 +85,6 @@ impl PartialEq for Alias { } } - impl PartialOrd for Alias { fn partial_cmp(&self, other: &Self) -> Option { self.normalize().partial_cmp(&other.normalize()) @@ -139,7 +138,6 @@ impl FromStr for Alias { } } - impl AsRef for &Alias { fn as_ref(&self) -> &str { &self.0 diff --git a/sdk/src/wallet/mod.rs b/sdk/src/wallet/mod.rs index 8715b02b7c..4570c8a283 100644 --- a/sdk/src/wallet/mod.rs +++ b/sdk/src/wallet/mod.rs @@ -346,7 +346,10 @@ impl Wallet { } /// Find the stored address by an alias. - pub fn find_address(&self, alias: impl AsRef) -> Option> { + pub fn find_address( + &self, + alias: impl AsRef, + ) -> Option> { Alias::is_reserved(alias.as_ref()) .map(std::borrow::Cow::Owned) .or_else(|| { diff --git a/sdk/src/wallet/store.rs b/sdk/src/wallet/store.rs index ddc5c5e03e..4b13291e03 100644 --- a/sdk/src/wallet/store.rs +++ b/sdk/src/wallet/store.rs @@ -537,7 +537,7 @@ impl Store { // abort if the alias is reserved if Alias::is_reserved(&alias).is_some() { println!("The alias {} is reserved", alias); - return ; + return; } key.map(|x| self.keys.insert(alias.clone(), x)); pkh.map(|x| self.pkhs.insert(x, alias.clone())); diff --git a/shared/src/ledger/native_vp/ibc/mod.rs b/shared/src/ledger/native_vp/ibc/mod.rs index 429643256e..902bef0a5b 100644 --- a/shared/src/ledger/native_vp/ibc/mod.rs +++ b/shared/src/ledger/native_vp/ibc/mod.rs @@ -420,7 +420,8 @@ mod tests { namada_proof_of_stake::OwnedPosParams::default(), vec![get_dummy_genesis_validator()].into_iter(), Epoch(1), - ).unwrap(); + ) + .unwrap(); // epoch duration let epoch_duration_key = get_epoch_duration_storage_key(); let epoch_duration = EpochDuration { diff --git a/shared/src/ledger/pos/mod.rs b/shared/src/ledger/pos/mod.rs index 3987b9b533..620d0108d3 100644 --- a/shared/src/ledger/pos/mod.rs +++ b/shared/src/ledger/pos/mod.rs @@ -13,9 +13,9 @@ pub use namada_proof_of_stake; pub use namada_proof_of_stake::parameters::{OwnedPosParams, PosParams}; pub use namada_proof_of_stake::pos_queries::*; pub use namada_proof_of_stake::storage::*; -pub use namada_proof_of_stake::{staking_token_address, types}; #[cfg(any(test, feature = "testing"))] pub use namada_proof_of_stake::test_utils; +pub use namada_proof_of_stake::{staking_token_address, types}; pub use vp::PosVP; use crate::types::address::{Address, InternalAddress}; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 2c21ea9d97..f37cc681b7 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -27,6 +27,7 @@ abciplus = [ ] wasm-runtime = ["namada/wasm-runtime"] +integration = ["namada_apps/integration"] [dependencies] namada = {path = "../shared", features = ["testing"]} @@ -39,6 +40,7 @@ async-trait.workspace = true chrono.workspace = true clap.workspace = true concat-idents.workspace = true +copy_dir = "0.1.3" derivative.workspace = true hyper = {version = "0.14.20", features = ["full"]} lazy_static.workspace = true diff --git a/tests/src/e2e/helpers.rs b/tests/src/e2e/helpers.rs index 5e4edce6aa..09ec4ab808 100644 --- a/tests/src/e2e/helpers.rs +++ b/tests/src/e2e/helpers.rs @@ -19,7 +19,7 @@ use namada::types::address::Address; use namada::types::key::*; use namada::types::storage::Epoch; use namada::types::token; -use namada_apps::config::genesis::genesis_config; +use namada_apps::config::genesis::chain; use namada_apps::config::utils::convert_tm_addr_to_socket_addr; use namada_apps::config::{Config, TendermintMode}; use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; @@ -178,17 +178,19 @@ pub fn get_validator_pk(test: &Test, who: &Who) -> Option { Who::NonValidator => return None, Who::Validator(i) => i, }; - let file = format!("{}.toml", test.net.chain_id.as_str()); - let path = test.test_dir.path().join(file); - let config = genesis_config::open_genesis_config(path).unwrap(); - let pk = config - .validator - .get(&format!("validator-{}", index)) - .unwrap() - .account_public_key - .as_ref() - .unwrap(); - Some(pk.to_public_key().unwrap()) + let path = test.test_dir.path().join("genesis"); + let genesis = chain::Finalized::read_toml_files( + &path.join(test.net.chain_id.as_str()), + ) + .unwrap(); + genesis + .transactions + .validator_account? + .iter() + .find(|val_tx| { + val_tx.tx.alias.to_string() == format!("validator-{}", index) + }) + .map(|val_tx| val_tx.tx.account_key.pk.raw.clone()) } /// Find the address of an account by its alias from the wallet @@ -364,9 +366,6 @@ pub fn generate_bin_command(bin_name: &str, manifest_path: &Path) -> Command { let build_cmd = CargoBuild::new() .package(APPS_PACKAGE) .manifest_path(manifest_path) - // Explicitly disable dev, in case it's enabled when a test is - // invoked - .env("NAMADA_DEV", "false") .bin(bin_name); let build_cmd = if run_debug { diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index 9e8e8d4eba..433770bfa5 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -67,8 +67,9 @@ use namada_apps::client::rpc::{ query_pos_parameters, query_storage_value, query_storage_value_bytes, }; use namada_apps::client::utils::id_from_pk; -use namada_apps::config::ethereum_bridge; -use namada_apps::config::genesis::genesis_config::GenesisConfig; +use namada_apps::config::genesis::{chain, templates}; +use namada_apps::config::utils::set_port; +use namada_apps::config::{ethereum_bridge, TendermintMode}; use namada_apps::facade::tendermint::block::Header as TmHeader; use namada_apps::facade::tendermint::merkle::proof::Proof as TmProof; use namada_apps::facade::tendermint_config::net::Address as TendermintAddress; @@ -82,7 +83,9 @@ use super::setup::set_ethereum_bridge_mode; use crate::e2e::helpers::{ find_address, get_actor_rpc, get_validator_pk, wait_for_wasm_pre_compile, }; -use crate::e2e::setup::{self, sleep, Bin, NamadaCmd, Test, Who}; +use crate::e2e::setup::{ + self, sleep, working_dir, Bin, NamadaCmd, Test, TestDir, Who, +}; use crate::{run, run_as}; #[test] @@ -206,25 +209,92 @@ fn run_ledger_ibc() -> Result<()> { Ok(()) } +/// Set up two Namada chains to talk to each other via IBC. fn setup_two_single_node_nets() -> Result<(Test, Test)> { + const ANOTHER_PROXY_APP: u16 = 27659u16; + const ANOTHER_RPC: u16 = 27660u16; + const ANOTHER_P2P: u16 = 26655u16; // Download the shielded pool parameters before starting node let _ = FsShieldedUtils::new(PathBuf::new()); - // epoch per 100 seconds - let update_genesis = |mut genesis: GenesisConfig| { - genesis.parameters.epochs_per_year = 31536; - genesis.parameters.min_num_of_blocks = 1; - genesis - }; - let update_genesis_b = |mut genesis: GenesisConfig| { - genesis.parameters.epochs_per_year = 31536; - genesis.parameters.min_num_of_blocks = 1; - setup::set_validators(1, genesis, |_| setup::ANOTHER_CHAIN_PORT_OFFSET) + let update_genesis = + |mut genesis: templates::All, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = 315_360; + setup::set_validators(1, genesis, base_dir, |_| 0) + }; + let test_a = setup::network(update_genesis, None)?; + let test_b = Test { + working_dir: working_dir(), + test_dir: TestDir::new(), + net: test_a.net.clone(), + async_runtime: Default::default(), }; - Ok(( - setup::network(update_genesis, None)?, - setup::network(update_genesis_b, None)?, - )) + for entry in std::fs::read_dir(test_a.test_dir.path()).unwrap() { + let entry = entry.unwrap(); + if entry.path().is_dir() { + copy_dir::copy_dir( + entry.path(), + test_b.test_dir.path().join(entry.file_name()), + ) + .map_err(|e| { + eyre!( + "Failed copying directory from test_a to test_b with {}", + e + ) + })?; + } else { + std::fs::copy( + entry.path(), + test_b.test_dir.path().join(entry.file_name()), + ) + .map_err(|e| { + eyre!("Failed copying file from test_a to test_b with {}", e) + })?; + } + } + let genesis_b_dir = test_b + .test_dir + .path() + .join(namada_apps::client::utils::NET_ACCOUNTS_DIR) + .join("validator-0"); + let mut genesis_b = chain::Finalized::read_toml_files( + &genesis_b_dir.join(test_a.net.chain_id.as_str()), + ) + .map_err(|_| eyre!("Could not read genesis files from test b"))?; + // chain b's validator needs to listen on a different port than chain a's + // validator + let validator_tx = genesis_b + .transactions + .validator_account + .as_mut() + .unwrap() + .iter_mut() + .find(|val| val.tx.alias.to_string() == *"validator-0") + .unwrap(); + let new_port = + validator_tx.tx.net_address.port() + setup::ANOTHER_CHAIN_PORT_OFFSET; + validator_tx.tx.net_address.set_port(new_port); + genesis_b + .write_toml_files(&genesis_b_dir.join(test_a.net.chain_id.as_str())) + .map_err(|_| eyre!("Could not write genesis toml files for test_b"))?; + // modify chain b to use different ports for cometbft + let mut config = namada_apps::config::Config::load( + &genesis_b_dir, + &test_a.net.chain_id, + Some(TendermintMode::Validator), + ); + let proxy_app = &mut config.ledger.cometbft.proxy_app; + set_port(proxy_app, ANOTHER_PROXY_APP); + let rpc_addr = &mut config.ledger.cometbft.rpc.laddr; + set_port(rpc_addr, ANOTHER_RPC); + let p2p_addr = &mut config.ledger.cometbft.p2p.laddr; + set_port(p2p_addr, ANOTHER_P2P); + config + .write(&genesis_b_dir, &test_a.net.chain_id, true) + .map_err(|e| { + eyre!("Unable to modify chain b's config file due to {}", e) + })?; + Ok((test_a, test_b)) } fn create_client(test_a: &Test, test_b: &Test) -> Result<(ClientId, ClientId)> { diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 2ee44c62ae..224c3d767f 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -23,15 +23,14 @@ use namada::types::address::Address; use namada::types::storage::Epoch; use namada::types::token; use namada_apps::config::ethereum_bridge; -use namada_apps::config::genesis::genesis_config::{ - GenesisConfig, ParametersConfig, PgfParametersConfig, PosParamsConfig, -}; use namada_apps::config::utils::convert_tm_addr_to_socket_addr; use namada_apps::facade::tendermint_config::net::Address as TendermintAddress; use namada_core::ledger::governance::cli::onchain::{ PgfFunding, PgfFundingTarget, StewardsUpdate, }; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada_sdk::masp::fs::FsShieldedUtils; +use namada_sdk::wallet::alias::Alias; use namada_test_utils::TestWasms; use namada_vp_prelude::BTreeSet; use serde_json::json; @@ -47,7 +46,10 @@ use crate::e2e::helpers::{ epoch_sleep, find_address, find_bonded_stake, get_actor_rpc, get_epoch, is_debug_mode, parse_reached_epoch, }; -use crate::e2e::setup::{self, default_port_offset, sleep, Bin, Who}; +use crate::e2e::setup::{ + self, allow_duplicate_ips, default_port_offset, set_validators, sleep, Bin, + Who, +}; use crate::{run, run_as}; fn start_namada_ledger_node( @@ -125,10 +127,15 @@ fn run_ledger() -> Result<()> { fn test_node_connectivity_and_consensus() -> Result<()> { // Setup 2 genesis validator nodes let test = setup::network( - |genesis| setup::set_validators(2, genesis, default_port_offset), + |genesis, base_dir| { + setup::set_validators(2, genesis, base_dir, default_port_offset) + }, None, )?; + allow_duplicate_ips(&test, &test.net.chain_id, &Who::Validator(0)); + allow_duplicate_ips(&test, &test.net.chain_id, &Who::Validator(1)); + set_ethereum_bridge_mode( &test, &test.net.chain_id, @@ -210,7 +217,7 @@ fn test_node_connectivity_and_consensus() -> 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: 1000010.1")?; + client.exp_string("nam: 980010.1")?; client.assert_success(); } @@ -409,8 +416,7 @@ fn stop_ledger_at_height() -> Result<()> { /// 8. Query the raw bytes of a storage key #[test] fn ledger_txs_and_queries() -> Result<()> { - let test = setup::network(|genesis| genesis, None)?; - + let test = setup::single_node_net()?; set_ethereum_bridge_mode( &test, &test.net.chain_id, @@ -525,7 +531,7 @@ fn ledger_txs_and_queries() -> Result<()> { "init-account", "--public-keys", // Value obtained from `namada::types::key::ed25519::tests::gen_keypair` - "001be519a321e29020fa3cbfbfd01bd5e92db134305609270b71dace25b5a21168", + "pktest1qpqfzxu3gt05jx2mvg82f4anf90psqerkwqhjey4zlqv0qfgwuvkzpklrjh", "--threshold", "1", "--code-path", @@ -594,7 +600,7 @@ fn ledger_txs_and_queries() -> Result<()> { ), // Unspecified token expect all tokens from wallet derived from genesis ( - vec!["balance", "--owner", BERTHA, "--node", &validator_one_rpc], + vec!["balance", "--owner", ALBERT, "--node", &validator_one_rpc], // expect all genesis tokens, sorted by alias vec![ r"apfel: \d+(\.\d+)?", @@ -676,155 +682,6 @@ fn ledger_txs_and_queries() -> Result<()> { Ok(()) } -/// We test shielding, shielded to shielded and unshielding transfers: -/// 1. Run the ledger node -/// 2. Send 20 BTC from Albert to PA(A) -/// 3. Send 7 BTC from SK(A) to PA(B) -/// 4. Assert BTC balance at VK(A) is 13 -/// 5. Send 5 BTC from SK(B) to Bertha -/// 6. Assert BTC balance at VK(B) is 2 -#[test] -fn masp_txs_and_queries() -> Result<()> { - // Download the shielded pool parameters before starting node - let _ = FsShieldedUtils::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, - )?; - set_ethereum_bridge_mode( - &test, - &test.net.chain_id, - &Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - 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![ - // 2. Send 20 BTC from Albert to PA(A) - ( - vec![ - "transfer", - "--source", - ALBERT, - "--target", - AA_PAYMENT_ADDRESS, - "--token", - BTC, - "--amount", - "20", - "--node", - &validator_one_rpc, - ], - "Transaction is valid", - ), - // 3. Send 7 BTC from SK(A) to PA(B) - ( - vec![ - "transfer", - "--source", - A_SPENDING_KEY, - "--target", - AB_PAYMENT_ADDRESS, - "--token", - BTC, - "--amount", - "7", - "--gas-payer", - CHRISTEL_KEY, - "--node", - &validator_one_rpc, - ], - "Transaction is valid", - ), - // 4. Assert BTC balance at VK(A) is 13 - ( - vec![ - "balance", - "--owner", - AA_VIEWING_KEY, - "--token", - BTC, - "--node", - &validator_one_rpc, - ], - "btc: 13", - ), - // 5. Send 5 BTC from SK(B) to Bertha - ( - vec![ - "transfer", - "--source", - B_SPENDING_KEY, - "--target", - BERTHA, - "--token", - BTC, - "--amount", - "5", - "--gas-payer", - CHRISTEL_KEY, - "--node", - &validator_one_rpc, - ], - "Transaction is valid", - ), - // 6. Assert BTC balance at VK(B) is 2 - ( - vec![ - "balance", - "--owner", - AB_VIEWING_KEY, - "--token", - BTC, - "--node", - &validator_one_rpc, - ], - "btc: 2", - ), - ]; - - for (tx_args, tx_result) in &txs_args { - for &dry_run in &[true, false] { - let tx_args = if dry_run && tx_args[0] == "transfer" { - vec![tx_args.clone(), vec!["--dry-run"]].concat() - } else { - tx_args.clone() - }; - let mut client = run!(test, Bin::Client, tx_args, Some(720))?; - - if *tx_result == "Transaction is valid" && !dry_run { - client.exp_string("Transaction accepted")?; - client.exp_string("Transaction applied")?; - } - client.exp_string(tx_result)?; - } - } - - Ok(()) -} - /// Test the optional disposable keypair for wrapper signing /// /// 1. Test that a tx requesting a disposable signer with a correct unshielding @@ -839,16 +696,11 @@ fn wrapper_disposable_signer() -> Result<()> { // 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 - } + |mut genesis, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(120); + genesis.parameters.parameters.min_num_of_blocks = 1; + set_validators(1, genesis, base_dir, default_port_offset) }, None, )?; @@ -862,76 +714,69 @@ fn wrapper_disposable_signer() -> Result<()> { 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, - "--force", - ], - // Not enough funds for fee payment, will use PoW - "Error while processing transaction's fees", - ), + let tx_args = vec![ + "transfer", + "--source", + ALBERT, + "--target", + AA_PAYMENT_ADDRESS, + "--token", + NAM, + "--amount", + "50", + "--ledger-address", + &validator_one_rpc, ]; + let mut client = run!(test, Bin::Client, tx_args, Some(720))?; - for (tx_args, tx_result) in &txs_args { - let mut client = run!(test, Bin::Client, tx_args, Some(720))?; + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; + client.exp_string("Transaction is valid")?; - if *tx_result == "Transaction is valid" { - client.exp_string("Transaction accepted")?; - client.exp_string("Transaction applied")?; - } - client.exp_string(tx_result)?; - } + let _ep1 = epoch_sleep(&test, &validator_one_rpc, 720)?; + let tx_args = vec![ + "transfer", + "--source", + ALBERT, + "--target", + BERTHA, + "--token", + NAM, + "--amount", + "1", + "--gas-spending-key", + A_SPENDING_KEY, + "--disposable-gas-payer", + "--ledger-address", + &validator_one_rpc, + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(720))?; + + client.exp_string("Transaction accepted")?; + client.exp_string("Transaction applied")?; + client.exp_string("Transaction is valid")?; + let _ep1 = epoch_sleep(&test, &validator_one_rpc, 720)?; + let tx_args = 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, + "--force", + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(720))?; + client.exp_string("Error while processing transaction's fees")?; Ok(()) } @@ -1064,29 +909,13 @@ fn pos_bonds() -> Result<()> { let pipeline_len = 2; let unbonding_len = 4; let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - min_num_of_blocks: 6, - max_expected_time_per_block: 1, - epochs_per_year: 31_536_000, - ..genesis.parameters - }; - let pos_params = PosParamsConfig { - pipeline_len, - unbonding_len, - ..genesis.pos_params - }; - let genesis = GenesisConfig { - parameters, - pos_params, - ..genesis - }; - let mut genesis = - setup::set_validators(2, genesis, default_port_offset); - // Remove stake from the 2nd validator so chain can run with a - // single node - genesis.validator.get_mut("validator-1").unwrap().tokens = None; - genesis + |mut genesis, base_dir: &_| { + genesis.parameters.pos_params.pipeline_len = pipeline_len; + genesis.parameters.pos_params.unbonding_len = unbonding_len; + genesis.parameters.parameters.min_num_of_blocks = 6; + genesis.parameters.parameters.max_expected_time_per_block = 1; + genesis.parameters.parameters.epochs_per_year = 31_536_000; + setup::set_validators(1, genesis, base_dir, default_port_offset) }, None, )?; @@ -1106,6 +935,27 @@ fn pos_bonds() -> Result<()> { let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); + // put money in the validator account from its balance account so that it + // can self-bond + let tx_args = vec![ + "transfer", + "--source", + "validator-0-balance-key", + "--target", + "validator-0-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &validator_0_rpc, + ]; + let mut client = + run_as!(test, Who::Validator(0), Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction applied with result:")?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + // 2. Submit a self-bond for the first genesis validator let tx_args = vec![ "bond", @@ -1114,7 +964,7 @@ fn pos_bonds() -> Result<()> { "--amount", "10000.0", "--signing-keys", - "validator-0-account-key", + "validator-0-validator-key", "--node", &validator_0_rpc, ]; @@ -1172,7 +1022,7 @@ fn pos_bonds() -> Result<()> { "--amount", "5100.0", "--signing-keys", - "validator-0-account-key", + "validator-0-validator-key", "--node", &validator_0_rpc, ]; @@ -1251,7 +1101,7 @@ fn pos_bonds() -> Result<()> { "--validator", "validator-0", "--signing-keys", - "validator-0-account-key", + "validator-0-validator-key", "--node", &validator_0_rpc, ]; @@ -1302,24 +1152,13 @@ fn pos_bonds() -> Result<()> { #[test] fn pos_rewards() -> Result<()> { let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - min_num_of_blocks: 4, - epochs_per_year: 31_536_000, - max_expected_time_per_block: 1, - ..genesis.parameters - }; - let pos_params = PosParamsConfig { - pipeline_len: 2, - unbonding_len: 4, - ..genesis.pos_params - }; - let genesis = GenesisConfig { - parameters, - pos_params, - ..genesis - }; - setup::set_validators(3, genesis, default_port_offset) + |mut genesis, base_dir| { + genesis.parameters.parameters.max_expected_time_per_block = 4; + genesis.parameters.parameters.epochs_per_year = 31_536_000; + genesis.parameters.parameters.max_expected_time_per_block = 1; + genesis.parameters.pos_params.pipeline_len = 2; + genesis.parameters.pos_params.unbonding_len = 4; + setup::set_validators(3, genesis, base_dir, default_port_offset) }, None, )?; @@ -1347,7 +1186,6 @@ fn pos_rewards() -> Result<()> { let validator_zero_rpc = get_actor_rpc(&test, &Who::Validator(0)); let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(1)); - let validator_two_rpc = get_actor_rpc(&test, &Who::Validator(2)); // Submit a delegation from Bertha to validator-0 let tx_args = vec![ @@ -1386,7 +1224,26 @@ fn pos_rewards() -> Result<()> { let _bg_validator_0 = validator_0.background(); let _bg_validator_1 = validator_1.background(); let _bg_validator_2 = validator_2.background(); - + // put money in the validator account from its balance account so that it + // can self-bond + let tx_args = vec![ + "transfer", + "--source", + "validator-1-balance-key", + "--target", + "validator-1-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &validator_one_rpc, + ]; + let mut client = + run_as!(test, Who::Validator(1), Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction applied with result:")?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); // Let validator-1 self-bond let tx_args = vec![ "bond", @@ -1399,7 +1256,7 @@ fn pos_rewards() -> Result<()> { "--gas-token", NAM, "--signing-keys", - "validator-1-account-key", + "validator-1-validator-key", "--ledger-address", &validator_one_rpc, ]; @@ -1408,6 +1265,26 @@ fn pos_rewards() -> Result<()> { client.exp_string("Transaction applied with result:")?; client.exp_string("Transaction is valid.")?; client.assert_success(); + // put money in the validator account from its balance account so that it + // can self-bond + let tx_args = vec![ + "transfer", + "--source", + "validator-2-balance-key", + "--target", + "validator-2-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &validator_one_rpc, + ]; + let mut client = + run_as!(test, Who::Validator(2), Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction applied with result:")?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); // Let validator-2 self-bond let tx_args = vec![ @@ -1421,9 +1298,9 @@ fn pos_rewards() -> Result<()> { "--gas-token", NAM, "--signing-keys", - "validator-2-account-key", + "validator-2-validator-key", "--ledger-address", - &validator_two_rpc, + &validator_zero_rpc, ]; let mut client = run_as!(test, Who::Validator(2), Bin::Client, tx_args, Some(40))?; @@ -1466,23 +1343,13 @@ fn test_bond_queries() -> Result<()> { let pipeline_len = 2; let unbonding_len = 4; let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - min_num_of_blocks: 2, - max_expected_time_per_block: 1, - epochs_per_year: 31_536_000, - ..genesis.parameters - }; - let pos_params = PosParamsConfig { - pipeline_len, - unbonding_len, - ..genesis.pos_params - }; - GenesisConfig { - parameters, - pos_params, - ..genesis - } + |mut genesis, base_dir: &_| { + genesis.parameters.parameters.min_num_of_blocks = 2; + genesis.parameters.parameters.max_expected_time_per_block = 1; + genesis.parameters.parameters.epochs_per_year = 31_536_000; + genesis.parameters.pos_params.pipeline_len = pipeline_len; + genesis.parameters.pos_params.unbonding_len = unbonding_len; + setup::set_validators(1, genesis, base_dir, default_port_offset) }, None, )?; @@ -1495,6 +1362,26 @@ fn test_bond_queries() -> Result<()> { let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); let validator_alias = "validator-0"; + // put money in the validator account from its balance account so that it + // can pay gas fees + let tx_args = vec![ + "transfer", + "--source", + "validator-0-balance-key", + "--target", + "validator-0-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &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(); + // 2. Submit a delegation to the genesis validator let tx_args = vec![ "bond", @@ -1597,8 +1484,8 @@ fn test_bond_queries() -> Result<()> { let tx_args = vec!["bonds", "--ledger-address", &validator_one_rpc]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; client.exp_string( - "All bonds total active: 200088.000000\r -All bonds total: 200088.000000\r + "All bonds total active: 120188.000000\r +All bonds total: 120188.000000\r All unbonds total active: 412.000000\r All unbonds total: 412.000000\r All unbonds total withdrawable: 412.000000\r", @@ -1620,31 +1507,40 @@ All unbonds total withdrawable: 412.000000\r", #[test] fn pos_init_validator() -> Result<()> { let pipeline_len = 1; - let validator_stake = 200000_u64; + let validator_stake = token::Amount::native_whole(20000_u64); let test = setup::network( - |genesis| { + |mut genesis, base_dir: &_| { + let stake = genesis + .transactions + .bond + .as_ref() + .unwrap() + .iter() + .filter_map(|bond| { + (bond.data.validator.to_string() == *"validator-0").then( + || { + bond.data + .amount + .increase_precision( + NATIVE_MAX_DECIMAL_PLACES.into(), + ) + .unwrap() + .amount + }, + ) + }) + .sum::(); assert_eq!( - genesis.validator.get("validator-0").unwrap().tokens, - Some(validator_stake), + stake, validator_stake, "Assuming this stake, we give the same amount to the new \ validator to have half of voting power", ); - let parameters = ParametersConfig { - min_num_of_blocks: 4, - epochs_per_year: 31_536_000, - max_expected_time_per_block: 1, - ..genesis.parameters - }; - let pos_params = PosParamsConfig { - pipeline_len, - unbonding_len: 2, - ..genesis.pos_params - }; - GenesisConfig { - parameters, - pos_params, - ..genesis - } + genesis.parameters.parameters.min_num_of_blocks = 4; + genesis.parameters.parameters.epochs_per_year = 31_536_000; + genesis.parameters.parameters.max_expected_time_per_block = 1; + genesis.parameters.pos_params.pipeline_len = pipeline_len; + genesis.parameters.pos_params.unbonding_len = 2; + setup::set_validators(1, genesis, base_dir, default_port_offset) }, None, )?; @@ -1727,7 +1623,7 @@ fn pos_init_validator() -> Result<()> { client.assert_success(); // 4. Transfer some NAM to the new validator - let validator_stake_str = &validator_stake.to_string(); + let validator_stake_str = &validator_stake.to_string_native(); let tx_args = vec![ "transfer", "--source", @@ -1814,11 +1710,12 @@ fn pos_init_validator() -> Result<()> { find_bonded_stake(&test, new_validator, &non_validator_rpc)?; assert_eq!( bonded_stake, - token::Amount::native_whole(validator_stake + delegation) + token::Amount::native_whole(delegation) + validator_stake ); Ok(()) } + /// Test that multiple txs submitted in the same block all get the tx result. /// /// In this test we: @@ -1827,7 +1724,9 @@ fn pos_init_validator() -> Result<()> { #[test] fn ledger_many_txs_in_a_block() -> Result<()> { let test = Arc::new(setup::network( - |genesis| genesis, + |genesis, base_dir: &_| { + setup::set_validators(1, genesis, base_dir, |_| 0) + }, // Set 10s consensus timeout to have more time to submit txs Some("10s"), )?); @@ -1911,22 +1810,20 @@ fn ledger_many_txs_in_a_block() -> Result<()> { #[test] fn proposal_submission() -> Result<()> { let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - epochs_per_year: epochs_per_year_from_min_duration(1), - max_proposal_bytes: Default::default(), - min_num_of_blocks: 4, - max_expected_time_per_block: 1, - ..genesis.parameters - }; - - GenesisConfig { - parameters, - ..genesis - } + |mut genesis, base_dir: &_| { + genesis.parameters.gov_params.max_proposal_code_size = 600000; + genesis.parameters.parameters.max_expected_time_per_block = 1; + setup::set_validators(1, genesis, base_dir, |_| 0u16) }, None, )?; + set_ethereum_bridge_mode( + &test, + &test.net.chain_id, + &Who::Validator(0), + ethereum_bridge::ledger::Mode::Off, + None, + ); let namadac_help = vec!["--help"]; @@ -1941,6 +1838,26 @@ fn proposal_submission() -> Result<()> { let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); + // put money in the validator account from its balance account so that it + // can pay gas fees + let tx_args = vec![ + "transfer", + "--source", + "validator-0-balance-key", + "--target", + "validator-0-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &validator_0_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(); + // 1.1 Delegate some token let tx_args = vec![ "bond", @@ -1977,6 +1894,7 @@ fn proposal_submission() -> Result<()> { &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; + client.exp_string("Transaction applied")?; client.exp_string("Transaction is valid.")?; client.assert_success(); @@ -2010,7 +1928,7 @@ fn proposal_submission() -> Result<()> { ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("nam: 999500")?; + client.exp_string("nam: 979500")?; client.assert_success(); // 5. Query token balance governance @@ -2077,7 +1995,7 @@ fn proposal_submission() -> Result<()> { ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("nam: 999500")?; + client.exp_string("nam: 979500")?; client.assert_success(); // 9. Send a yay vote from a validator @@ -2163,7 +2081,7 @@ fn proposal_submission() -> Result<()> { let mut client = run!(test, Bin::Client, query_proposal, Some(15))?; client.exp_string("Proposal Id: 0")?; client.exp_string( - "passed with 200900.000000 yay votes and 0.000000 nay votes (0.%)", + "passed with 120900.000000 yay votes and 0.000000 nay votes (0.%)", )?; client.assert_success(); @@ -2185,7 +2103,7 @@ fn proposal_submission() -> Result<()> { ]; let mut client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("nam: 1000000")?; + client.exp_string("nam: 980000")?; client.assert_success(); // 13. Check if governance funds are 0 @@ -2225,24 +2143,16 @@ fn proposal_submission() -> Result<()> { #[test] fn pgf_governance_proposal() -> Result<()> { let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - epochs_per_year: epochs_per_year_from_min_duration(1), - max_proposal_bytes: Default::default(), - min_num_of_blocks: 4, - max_expected_time_per_block: 1, - ..genesis.parameters - }; - let pgf_params = PgfParametersConfig { - stewards: BTreeSet::from_iter(Address::from_str("atest1v4ehgw36xguyzsejx5m5xvehxqmnyvejgdzygv6rgcurgdzxxsunzs3nxuc5vwfkg3pnxdf4u4p9h9")), - ..genesis.pgf_params - }; - - GenesisConfig { - parameters, - pgf_params, - ..genesis - } + |mut genesis, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(1); + genesis.parameters.parameters.max_proposal_bytes = + Default::default(); + genesis.parameters.parameters.min_num_of_blocks = 4; + genesis.parameters.parameters.max_expected_time_per_block = 1; + genesis.parameters.pgf_params.stewards = + BTreeSet::from_iter(Alias::from_str("albert")); + setup::set_validators(1, genesis, base_dir, |_| 0) }, None, )?; @@ -2268,6 +2178,26 @@ fn pgf_governance_proposal() -> Result<()> { let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); + // put money in the validator account from its balance account so that it + // can pay gas fees + let tx_args = vec![ + "transfer", + "--source", + "validator-0-balance-key", + "--target", + "validator-0-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &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(); + // Delegate some token let tx_args = vec![ "bond", @@ -2334,7 +2264,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("nam: 999500")?; + client.exp_string("nam: 979500")?; client.assert_success(); // Query token balance governance @@ -2439,7 +2369,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("nam: 1000000")?; + client.exp_string("nam: 980000")?; client.assert_success(); // Check if governance funds are 0 @@ -2543,29 +2473,20 @@ fn pgf_governance_proposal() -> Result<()> { fn proposal_offline() -> Result<()> { let working_dir = setup::working_dir(); let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - epochs_per_year: epochs_per_year_from_min_duration(1), - 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 - }; - - GenesisConfig { - parameters, - ..genesis - } + |mut genesis, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(1); + genesis.parameters.parameters.max_proposal_bytes = + Default::default(); + genesis.parameters.parameters.min_num_of_blocks = 4; + genesis.parameters.parameters.max_expected_time_per_block = 1; + genesis.parameters.parameters.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 + genesis.parameters.parameters.tx_whitelist = + Some(get_all_wasms_hashes(&working_dir, Some("tx_"))); + setup::set_validators(1, genesis, base_dir, |_| 0) }, None, )?; @@ -2695,7 +2616,7 @@ fn proposal_offline() -> Result<()> { let mut client = run!(test, Bin::Client, tally_offline, Some(15))?; client.exp_string("Parsed 1 votes")?; - client.exp_string("rejected with 900.000000 yay votes")?; + client.exp_string("rejected with 20900.000000 yay votes")?; client.assert_success(); Ok(()) @@ -2715,407 +2636,6 @@ fn generate_proposal_json_file( serde_json::to_writer(intent_writer, proposal_content).unwrap(); } -/// In this test we: -/// 1. Setup 2 genesis validators -/// 2. Initialize a new network with the 2 validators -/// 3. Setup and start the 2 genesis validator nodes and a non-validator node -/// 4. Submit a valid token transfer tx from one validator to the other -/// 5. Check that all the nodes processed the tx with the same result -#[test] -fn test_genesis_validators() -> Result<()> { - use std::collections::HashMap; - use std::net::SocketAddr; - use std::str::FromStr; - - use namada::types::chain::ChainId; - use namada_apps::config::genesis::genesis_config::{ - self, ValidatorPreGenesisConfig, - }; - use namada_apps::config::Config; - - // This test is not using the `setup::network`, because we're setting up - // custom genesis validators - setup::INIT.call_once(|| { - if let Err(err) = color_eyre::install() { - eprintln!("Failed setting up colorful error reports {}", err); - } - }); - - let working_dir = setup::working_dir(); - let test_dir = setup::TestDir::new(); - let checksums_path = working_dir - .join("wasm/checksums.json") - .to_string_lossy() - .into_owned(); - - // Same as in `genesis/e2e-tests-single-node.toml` for `validator-0` - let net_address_0 = SocketAddr::from_str("127.0.0.1:27656").unwrap(); - let net_address_port_0 = net_address_0.port(); - // Find the first port (ledger P2P) that should be used for a validator at - // the given index - let get_first_port = |ix: u8| net_address_port_0 + 6 * (ix as u16 + 1); - - // 1. Setup 2 genesis validators, one with ed25519 keys (0) and one with - // secp256k1 keys (1) - let validator_0_alias = "validator-0"; - let validator_1_alias = "validator-1"; - - let mut init_genesis_validator_0 = setup::run_cmd( - Bin::Client, - [ - "utils", - "init-genesis-validator", - "--unsafe-dont-encrypt", - "--alias", - validator_0_alias, - "--scheme", - "ed25519", - "--commission-rate", - "0.05", - "--max-commission-rate-change", - "0.01", - "--net-address", - &format!("127.0.0.1:{}", get_first_port(0)), - ], - Some(5), - &working_dir, - &test_dir, - format!("{}:{}", std::file!(), std::line!()), - )?; - init_genesis_validator_0.assert_success(); - let validator_0_pre_genesis_dir = - namada_apps::client::utils::validator_pre_genesis_dir( - test_dir.path(), - validator_0_alias, - ); - let config = std::fs::read_to_string( - namada_apps::client::utils::validator_pre_genesis_file( - &validator_0_pre_genesis_dir, - ), - ) - .unwrap(); - let mut validator_0_config: ValidatorPreGenesisConfig = - toml::from_str(&config).unwrap(); - let validator_0_config = validator_0_config - .validator - .remove(validator_0_alias) - .unwrap(); - - let mut init_genesis_validator_1 = setup::run_cmd( - Bin::Client, - [ - "utils", - "init-genesis-validator", - "--unsafe-dont-encrypt", - "--alias", - validator_1_alias, - "--scheme", - "secp256k1", - "--commission-rate", - "0.05", - "--max-commission-rate-change", - "0.01", - "--net-address", - &format!("127.0.0.1:{}", get_first_port(1)), - ], - Some(5), - &working_dir, - &test_dir, - format!("{}:{}", std::file!(), std::line!()), - )?; - init_genesis_validator_1.assert_success(); - let validator_1_pre_genesis_dir = - namada_apps::client::utils::validator_pre_genesis_dir( - test_dir.path(), - validator_1_alias, - ); - let config = std::fs::read_to_string( - namada_apps::client::utils::validator_pre_genesis_file( - &validator_1_pre_genesis_dir, - ), - ) - .unwrap(); - let mut validator_1_config: ValidatorPreGenesisConfig = - toml::from_str(&config).unwrap(); - let validator_1_config = validator_1_config - .validator - .remove(validator_1_alias) - .unwrap(); - - // 2. Initialize a new network with the 2 validators - let mut genesis = genesis_config::open_genesis_config( - working_dir.join(setup::SINGLE_NODE_NET_GENESIS), - )?; - let update_validator_config = - |ix: u8, mut config: genesis_config::ValidatorConfig| { - // Setup tokens balances and validity predicates - config.tokens = Some(200000); - config.non_staked_balance = Some(1000000000000); - config.validator_vp = Some("vp_user".into()); - // Setup the validator ports same as what - // `setup::set_validators` would do - let mut net_address = net_address_0; - // 6 ports for each validator - let first_port = get_first_port(ix); - net_address.set_port(first_port); - config.net_address = Some(net_address.to_string()); - config - }; - genesis.validator = HashMap::from_iter([ - ( - validator_0_alias.to_owned(), - update_validator_config(0, validator_0_config), - ), - ( - validator_1_alias.to_owned(), - update_validator_config(1, validator_1_config), - ), - ]); - let genesis_file = test_dir.path().join("e2e-test-genesis-src.toml"); - genesis_config::write_genesis_config(&genesis, &genesis_file); - let genesis_path = genesis_file.to_string_lossy(); - - let archive_dir = test_dir.path().to_string_lossy().to_string(); - let args = vec![ - "utils", - "init-network", - "--unsafe-dont-encrypt", - "--genesis-path", - &genesis_path, - "--chain-prefix", - "e2e-test", - "--localhost", - "--allow-duplicate-ip", - "--wasm-checksums-path", - &checksums_path, - "--archive-dir", - &archive_dir, - ]; - let mut init_network = setup::run_cmd( - Bin::Client, - args, - Some(5), - &working_dir, - &test_dir, - format!("{}:{}", std::file!(), std::line!()), - )?; - - // Get the generated chain_id` from result of the last command - let (unread, matched) = - init_network.exp_regex(r"Derived chain ID: .*\n")?; - let chain_id_raw = - matched.trim().split_once("Derived chain ID: ").unwrap().1; - let chain_id = ChainId::from_str(chain_id_raw.trim())?; - println!("'init-network' output: {}", unread); - let net = setup::Network { - chain_id: chain_id.clone(), - }; - let test = setup::Test { - working_dir: working_dir.clone(), - test_dir, - net, - genesis, - async_runtime: Default::default(), - }; - - // Host the network archive to make it available for `join-network` commands - let network_archive_server = file_serve::Server::new(&working_dir); - let network_archive_addr = network_archive_server.addr().to_owned(); - std::thread::spawn(move || { - network_archive_server.serve().unwrap(); - }); - - // 3. Setup and start the 2 genesis validator nodes and a non-validator node - - // Clean-up the chain dir from the existing validator dir that were created - // by `init-network`, because we want to set them up with `join-network` - // instead - let validator_0_base_dir = test.get_base_dir(&Who::Validator(0)); - let validator_1_base_dir = test.get_base_dir(&Who::Validator(1)); - std::fs::remove_dir_all(&validator_0_base_dir).unwrap(); - std::fs::remove_dir_all(&validator_1_base_dir).unwrap(); - - std::env::set_var( - namada_apps::client::utils::ENV_VAR_NETWORK_CONFIGS_SERVER, - format!("http://{network_archive_addr}/{}", archive_dir), - ); - let pre_genesis_path = validator_0_pre_genesis_dir.to_string_lossy(); - let mut join_network_val_0 = run_as!( - test, - Who::Validator(0), - Bin::Client, - [ - "utils", - "join-network", - "--chain-id", - chain_id.as_str(), - "--pre-genesis-path", - pre_genesis_path.as_ref(), - "--dont-prefetch-wasm", - ], - Some(5) - )?; - join_network_val_0.exp_string("Successfully configured for chain")?; - - let pre_genesis_path = validator_1_pre_genesis_dir.to_string_lossy(); - let mut join_network_val_1 = run_as!( - test, - Who::Validator(1), - Bin::Client, - [ - "utils", - "join-network", - "--chain-id", - chain_id.as_str(), - "--pre-genesis-path", - pre_genesis_path.as_ref(), - "--dont-prefetch-wasm", - ], - Some(5) - )?; - join_network_val_1.exp_string("Successfully configured for chain")?; - - // We have to update the ports in the configs again, because the ones from - // `join-network` use the defaults - // - // TODO: use `update_actor_config` from `setup`, instead - let update_config = |ix: u8, mut config: Config| { - let first_port = net_address_port_0 + 6 * (ix as u16 + 1); - let p2p_addr = - convert_tm_addr_to_socket_addr(&config.ledger.cometbft.p2p.laddr) - .ip() - .to_string(); - - config.ledger.cometbft.p2p.laddr = TendermintAddress::from_str( - &format!("{}:{}", p2p_addr, first_port), - ) - .unwrap(); - let rpc_addr = - convert_tm_addr_to_socket_addr(&config.ledger.cometbft.rpc.laddr) - .ip() - .to_string(); - config.ledger.cometbft.rpc.laddr = TendermintAddress::from_str( - &format!("{}:{}", rpc_addr, first_port + 1), - ) - .unwrap(); - let proxy_app_addr = - convert_tm_addr_to_socket_addr(&config.ledger.cometbft.proxy_app) - .ip() - .to_string(); - config.ledger.cometbft.proxy_app = TendermintAddress::from_str( - &format!("{}:{}", proxy_app_addr, first_port + 2), - ) - .unwrap(); - config - }; - - let validator_0_config = update_config( - 0, - Config::load(&validator_0_base_dir, &test.net.chain_id, None), - ); - validator_0_config - .write(&validator_0_base_dir, &chain_id, true) - .unwrap(); - - let validator_1_config = update_config( - 1, - Config::load(&validator_1_base_dir, &test.net.chain_id, None), - ); - validator_1_config - .write(&validator_1_base_dir, &chain_id, true) - .unwrap(); - - // Copy WASMs to each node's chain dir - let chain_dir = test.test_dir.path().join(chain_id.as_str()); - setup::copy_wasm_to_chain_dir( - &working_dir, - &chain_dir, - &chain_id, - test.genesis.validator.keys(), - ); - - let mut validator_0 = - start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))?; - let mut validator_1 = - start_namada_ledger_node_wait_wasm(&test, Some(1), Some(40))?; - let mut non_validator = - start_namada_ledger_node_wait_wasm(&test, None, Some(40))?; - - // Wait for a first block - validator_0.exp_string("Committed block hash")?; - validator_1.exp_string("Committed block hash")?; - non_validator.exp_string("Committed block hash")?; - - let bg_validator_0 = validator_0.background(); - let bg_validator_1 = validator_1.background(); - let _bg_non_validator = non_validator.background(); - - // 4. Submit a valid token transfer tx - let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); - let tx_args = [ - "transfer", - "--source", - validator_0_alias, - "--target", - validator_1_alias, - "--token", - NAM, - "--amount", - "10.1", - "--node", - &validator_one_rpc, - ]; - let mut client = - run_as!(test, Who::Validator(0), Bin::Client, tx_args, Some(40))?; - client.exp_string("Transaction applied with result:")?; - client.exp_string("Transaction is valid.")?; - client.assert_success(); - - // 3. Check that all the nodes processed the tx with the same result - let mut validator_0 = bg_validator_0.foreground(); - let mut validator_1 = bg_validator_1.foreground(); - - let expected_result = "successful inner txs: 1"; - // We cannot check this on non-validator node as it might sync without - // applying the tx itself, but its state should be the same, checked below. - validator_0.exp_string(expected_result)?; - validator_1.exp_string(expected_result)?; - let _bg_validator_0 = validator_0.background(); - let _bg_validator_1 = validator_1.background(); - - let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); - let validator_1_rpc = get_actor_rpc(&test, &Who::Validator(1)); - let non_validator_rpc = get_actor_rpc(&test, &Who::NonValidator); - - // Find the block height on the validator - let after_tx_height = get_height(&test, &validator_0_rpc)?; - - // Wait for the second validator and non-validator to be synced to at least - // the same height - wait_for_block_height(&test, &validator_1_rpc, after_tx_height, 10)?; - wait_for_block_height(&test, &non_validator_rpc, after_tx_height, 10)?; - - let query_balance_args = |ledger_rpc| { - vec![ - "balance", - "--owner", - validator_1_alias, - "--token", - NAM, - "--node", - ledger_rpc, - ] - }; - 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(r"nam: 1000000000010.1")?; - client.assert_success(); - } - - Ok(()) -} - /// In this test we intentionally make a validator node double sign blocks /// to test that slashing evidence is received and processed by the ledger /// correctly: @@ -3141,21 +2661,22 @@ fn double_signing_gets_slashed() -> Result<()> { // Setup 2 genesis validator nodes let test = setup::network( - |genesis| { + |mut genesis, base_dir| { (pipeline_len, unbonding_len, cubic_offset) = ( - genesis.pos_params.pipeline_len, - genesis.pos_params.unbonding_len, - genesis.pos_params.cubic_slashing_window_length, + genesis.parameters.pos_params.pipeline_len, + genesis.parameters.pos_params.unbonding_len, + genesis.parameters.pos_params.cubic_slashing_window_length, ); - let mut genesis = - setup::set_validators(4, genesis, default_port_offset); // Make faster epochs to be more likely to discover boundary issues - genesis.parameters.min_num_of_blocks = 2; - genesis + genesis.parameters.parameters.min_num_of_blocks = 2; + setup::set_validators(4, genesis, base_dir, default_port_offset) }, None, )?; + allow_duplicate_ips(&test, &test.net.chain_id, &Who::Validator(0)); + allow_duplicate_ips(&test, &test.net.chain_id, &Who::Validator(1)); + set_ethereum_bridge_mode( &test, &test.net.chain_id, @@ -3192,6 +2713,28 @@ fn double_signing_gets_slashed() -> Result<()> { validator_3.exp_string("This node is a validator")?; let _bg_validator_3 = validator_3.background(); + let validator_zero_rpc = get_actor_rpc(&test, &Who::Validator(0)); + // put money in the validator account from its balance account so that it + // can pay gas fees + let tx_args = vec![ + "transfer", + "--source", + "validator-0-balance-key", + "--target", + "validator-0-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &validator_zero_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(); + // 2. Copy the first genesis validator base-dir let validator_0_base_dir = test.get_base_dir(&Who::Validator(0)); let validator_0_base_dir_copy = test @@ -3437,7 +2980,7 @@ fn double_signing_gets_slashed() -> Result<()> { /// 2d. Submit same tx again, this time the client shouldn't reveal again. #[test] fn implicit_account_reveal_pk() -> Result<()> { - let test = setup::network(|genesis| genesis, None)?; + let test = setup::single_node_net()?; // 1. Run the ledger node let _bg_ledger = @@ -3568,16 +3111,11 @@ fn implicit_account_reveal_pk() -> Result<()> { fn test_epoch_sleep() -> Result<()> { // Use slightly longer epochs to give us time to sleep let test = setup::network( - |genesis| { - let parameters = ParametersConfig { - epochs_per_year: epochs_per_year_from_min_duration(30), - min_num_of_blocks: 1, - ..genesis.parameters - }; - GenesisConfig { - parameters, - ..genesis - } + |mut genesis, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(30); + genesis.parameters.parameters.min_num_of_blocks = 1; + setup::set_validators(1, genesis, base_dir, |_| 0) }, None, )?; diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index e98a14735c..233f4be1b2 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -16,9 +16,8 @@ use expectrl::process::unix::{PtyStream, UnixProcess}; use expectrl::session::Session; use expectrl::stream::log::LogStream; use expectrl::{ControlCode, Eof, WaitStatus}; -use eyre::{eyre, Context}; +use eyre::eyre; use itertools::{Either, Itertools}; -use namada_sdk::wallet::alias::Alias; use namada::types::chain::ChainId; use namada_apps::client::utils::{ self, validator_pre_genesis_dir, validator_pre_genesis_txs_file, @@ -30,6 +29,7 @@ use namada_apps::{config, wallet}; use namada_core::types::key::{RefTo, SchemeType}; use namada_core::types::string_encoding::StringEncoded; use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; +use namada_sdk::wallet::alias::Alias; use namada_tx_prelude::token; use namada_vp_prelude::HashSet; use once_cell::sync::Lazy; @@ -148,12 +148,11 @@ where let mut wallet = wallet::load(&wallet_path) .expect("Could not locate pre-genesis wallet used for e2e tests."); let alias = format!("validator-{}-balance-key", val); - let (alias, sk) = wallet - .gen_key(SchemeType::Ed25519, Some(alias), true, None, None) + let (alias, sk, _mnemonic) = wallet + .gen_key(SchemeType::Ed25519, Some(alias), true, None, None, None) .unwrap_or_else(|_| { panic!("Could not generate new key for validator-{}", val) - }) - .unwrap(); + }); wallet::save(&wallet).unwrap(); // assign balance to the key genesis diff --git a/tests/src/integration/setup.rs b/tests/src/integration/setup.rs index 6e8d953bcb..3a5739176d 100644 --- a/tests/src/integration/setup.rs +++ b/tests/src/integration/setup.rs @@ -5,9 +5,11 @@ use std::sync::{Arc, Mutex}; use color_eyre::eyre::{eyre, Result}; use namada_apps::cli::args; +use namada_apps::client::utils::PRE_GENESIS_DIR; use namada_apps::config; -use namada_apps::config::genesis::genesis_config; -use namada_apps::config::genesis::genesis_config::GenesisConfig; +use namada_apps::config::genesis::chain::Finalized; +use namada_apps::config::genesis::templates; +use namada_apps::config::genesis::templates::load_and_validate; use namada_apps::config::TendermintMode; use namada_apps::facade::tendermint::Timeout; use namada_apps::facade::tendermint_proto::google::protobuf::Timestamp; @@ -17,13 +19,11 @@ use namada_apps::node::ledger::shell::testing::node::{ }; use namada_apps::node::ledger::shell::testing::utils::TestDir; use namada_apps::node::ledger::shell::Shell; -use namada_core::types::address::nam; -use namada_core::types::chain::{ChainId, ChainIdPrefix}; -use toml::value::Table; +use namada_apps::wallet::pre_genesis; +use namada_core::types::chain::ChainIdPrefix; +use namada_sdk::wallet::alias::Alias; -use crate::e2e::setup::{ - copy_wasm_to_chain_dir, get_all_wasms_hashes, SINGLE_NODE_NET_GENESIS, -}; +use crate::e2e::setup::{copy_wasm_to_chain_dir, SINGLE_NODE_NET_GENESIS}; /// Env. var for keeping temporary files created by the integration tests const ENV_VAR_KEEP_TEMP: &str = "NAMADA_INT_KEEP_TEMP"; @@ -88,23 +88,24 @@ pub fn initialize_genesis() -> Result<(MockNode, MockServicesController)> { }, ); - finalize_wallet(&template_dir, &global_args, genesis); + let eth_bridge_params = genesis.get_eth_bridge_params(); let auto_drive_services = { // NB: for now, the only condition that // dictates whether mock services should // be enabled is if the Ethereum bridge // is enabled at genesis - genesis.ethereum_bridge_params.is_some() + eth_bridge_params.is_some() }; let enable_eth_oracle = { // NB: we only enable the oracle if the // Ethereum bridge is enabled at genesis - genesis.ethereum_bridge_params.is_some() + eth_bridge_params.is_some() }; let services_cfg = MockServicesCfg { auto_drive_services, enable_eth_oracle, }; + finalize_wallet(&template_dir, &global_args, genesis); create_node(test_dir, global_args, keep_temp, services_cfg) } diff --git a/tests/src/vm_host_env/ibc.rs b/tests/src/vm_host_env/ibc.rs index 88f88820a5..052b5b5e77 100644 --- a/tests/src/vm_host_env/ibc.rs +++ b/tests/src/vm_host_env/ibc.rs @@ -219,7 +219,8 @@ pub fn init_storage() -> (Address, Address) { OwnedPosParams::default(), vec![get_dummy_genesis_validator()].into_iter(), Epoch(1), - ).unwrap(); + ) + .unwrap(); // store wasm code let key = Key::wasm_code(&code_hash); env.wl_storage.storage.write(&key, code.clone()).unwrap(); diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 29b37c5055..eda093d527 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1005,6 +1005,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "copy_dir" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3" +dependencies = [ + "walkdir", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -3468,6 +3477,7 @@ dependencies = [ "chrono", "clap", "concat-idents", + "copy_dir", "derivative", "hyper", "lazy_static", diff --git a/wasm/checksums.json b/wasm/checksums.json index 873f8ae6a1..04ee8555d6 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,23 +1,23 @@ { - "tx_bond.wasm": "tx_bond.dbfe330a50d8d3511e125f611d88a3a32952de2077345ee79b5b320e9d11ece6.wasm", - "tx_bridge_pool.wasm": "tx_bridge_pool.ae875a5ddd27035b768678b2dbf70c8ecfa1bce316faee01b26bd0cbffb48b19.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.39b26c8ff0c06220163322c15a0f304232f5e3a6333c147d10d5a65e3d06c061.wasm", - "tx_ibc.wasm": "tx_ibc.d550ec9d20d965d91530ca4df74a75eeb27f88ae4484da3eef4b5599727bd94f.wasm", - "tx_init_account.wasm": "tx_init_account.06c8639a68cecf1160818bd92d81974a9990aceea08cbbcc47e18c51b2ec9449.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.daa176497bb1f4bb97346d1ba2387e0cc84e704eca70ef78a56228e03969fb76.wasm", - "tx_init_validator.wasm": "tx_init_validator.8d393d2cfdf55fd88beb4f26840d22e98da5e0390aac5fbcf1dc314e0eb4a9ca.wasm", - "tx_redelegate.wasm": "tx_redelegate.c586dc50a452948e2dd79718519bfa7b966999126ba52807bc4ba3a6f7a6290d.wasm", - "tx_resign_steward.wasm": "tx_resign_steward.994a9a3e10baacf900087382d2d8bb8045033927a3d040bf882c6d60ecfe0d5b.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.aceb2779c307f96e377e3e189589ea54e7b05d712cac27c6943e1d941334b421.wasm", - "tx_transfer.wasm": "tx_transfer.adab8a00709de083b6b4b534d4dfe002d479085ce1a766718dd3afe920659ac3.wasm", - "tx_unbond.wasm": "tx_unbond.a1dc615400ad625cf9b82ea28735a2fbffd1dd6745761eff6df4d08358162195.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.9debbc06681dd602e8a34ccf20d50dddee9963447409d2597893bf54937e7cee.wasm", - "tx_update_account.wasm": "tx_update_account.3917c5c25d2ffb0d717b9475a74d825840db94d48fcb4a0fe7450f3b32258cbe.wasm", - "tx_update_steward_commission.wasm": "tx_update_steward_commission.1a6f68eaf3b2eff9da777ce8c73f94865c49fa2ef94b5c0e1d65f46960cc9a5e.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.3f818de8c638e24249a26ccd20ec6a1700092c3700dcf6b91aea11058586ebef.wasm", - "tx_withdraw.wasm": "tx_withdraw.3cf7e6b6b77abd0eae58302659c9de31919894f36c0f7dbd85a6c853cd023483.wasm", - "vp_implicit.wasm": "vp_implicit.46d4206719acaaa8d0a422b6f969719b458cc5ca7c25677eb4bf23b52b4b6490.wasm", - "vp_masp.wasm": "vp_masp.c1e8f569ed3ad2bf5152e34f8ea9e255439066e0800ae73620a7be9a976f2ff7.wasm", - "vp_user.wasm": "vp_user.6b5bf2c3a5367d4f379187a8d2cf71457085aa1184a2b98b9e00ab6978847e08.wasm", - "vp_validator.wasm": "vp_validator.a683a9724e335cdf8f33eaee9ac3127a21d9f76e9769a5f3aea63b3bf04f2011.wasm" -} \ No newline at end of file + "tx_bond.wasm": "tx_bond.9fdaf63c3052a44195c8280d749e1682654ee2bad6edd25c6551bdc8197f7512.wasm", + "tx_bridge_pool.wasm": "tx_bridge_pool.605e9b7e8b0ff27863567b56d3f5e757783f5c75c596260d7c0805a47b7a877e.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.f036ef369e754cb6c75453f9b380f95be588d330f993284b08b477231d1ed0eb.wasm", + "tx_ibc.wasm": "tx_ibc.95832c0d92bb83ac2cde7b3c7cde9edae7c21f8dd5b32d93a9368093d58b5f15.wasm", + "tx_init_account.wasm": "tx_init_account.f525992552827cfd7693d568879dfd1f54406af9906b8827bc86e4beb21a0efe.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.9c3cf15da2b14e2c9898f5910cbfcffb80ef14dfcd0082dc9039b170cb3cc208.wasm", + "tx_init_validator.wasm": "tx_init_validator.1b695e5869532bbaebe5d99d48a267ae0891e9c4470abbbad5d0b37118e35ca6.wasm", + "tx_redelegate.wasm": "tx_redelegate.63f6c6b2f1582b357870ca9b779e88139c1e859b18fe7ada71dfdf5842fd83c7.wasm", + "tx_resign_steward.wasm": "tx_resign_steward.c21e812ba4a095d4b5eca562a94a2ef5071522a95f87a641c5f6e2bb2a4c890d.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.e5b1b047b44cde0cc8f9f89ba15f611dbefe8a3c060c4012363fdf07e92c6875.wasm", + "tx_transfer.wasm": "tx_transfer.c34a0a0b15ae448db7bc455bab8c59e6510c5fe9cb2d6386897030c37a189e42.wasm", + "tx_unbond.wasm": "tx_unbond.e846fd512237bdee232edc8d87e637ab3631942e3e1eda635d595e96d52bf489.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.1c8db6285b60d8ce80ce5c1ff7d43755e50cd3749fad5fb286f66592112ba4cd.wasm", + "tx_update_account.wasm": "tx_update_account.9f9493f2fb39d0a5f0fa3327a78828e387ba15690ad86e96d4b8cdf509cf1efc.wasm", + "tx_update_steward_commission.wasm": "tx_update_steward_commission.92bcf758cbb58be93ae5d7fde616a3974016f0d41c09b7333bc6041bc23e8d30.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.825ffa0c7c7c588cb668af4ff6ba3ce5c5d1c37c0728c18a7eb807a02da52158.wasm", + "tx_withdraw.wasm": "tx_withdraw.0225f3c8ac6e4f2d4636568048035156c936d9a8bc12d4c0d619598089af6f47.wasm", + "vp_implicit.wasm": "vp_implicit.e8dd924d43f11854724350c3e082a35898dc03e21f1543c117cff6763d881d2b.wasm", + "vp_masp.wasm": "vp_masp.64553be3434516e8de6b3e0a445582dce7d7975bd9765f4fbd9875bdbf0f9feb.wasm", + "vp_user.wasm": "vp_user.37fee9afa11b5899c824b6b8e1f0a626f5fd05c9813af454081a797772ca3de6.wasm", + "vp_validator.wasm": "vp_validator.e6283c764480d04942c0fa2fb3d6e3d1cd18565b235b09e6a6deb7918c808f31.wasm" +} diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index f738c90527..974079380c 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1005,6 +1005,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "copy_dir" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3" +dependencies = [ + "walkdir", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -3468,6 +3477,7 @@ dependencies = [ "chrono", "clap", "concat-idents", + "copy_dir", "derivative", "hyper", "lazy_static", From 790521dc0dc7942f79916744fc9e04366cd971c3 Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 30 Oct 2023 11:48:20 +0100 Subject: [PATCH 09/47] [fix]: Fixed unit tests --- apps/src/lib/node/ledger/shell/mod.rs | 42 +++++++++++++++++++++++-- apps/src/lib/node/ledger/storage/mod.rs | 36 ++++++++++++++++++--- core/src/ledger/inflation.rs | 6 +++- proof_of_stake/src/lib.rs | 4 +-- proof_of_stake/src/tests.rs | 12 +++---- 5 files changed, 84 insertions(+), 16 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 38b7456e87..1138467c31 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -1515,13 +1515,15 @@ mod test_utils { use namada::types::keccak::KeccakHash; use namada::types::key::*; use namada::types::storage::{BlockHash, Epoch, Header}; - use namada::types::time::DateTimeUtc; + use namada::types::time::{DateTimeUtc, DurationSecs}; use namada::types::transaction::{Fee, TxType, WrapperTx}; use tempfile::tempdir; use tokio::sync::mpsc::{Sender, UnboundedReceiver}; + use namada::ledger::parameters::{EpochDuration, Parameters}; use super::*; use crate::config::ethereum_bridge::ledger::ORACLE_CHANNEL_BUFFER_SIZE; + use crate::facade::tendermint_proto::abci::{ Misbehavior, RequestInitChain, RequestPrepareProposal, RequestProcessProposal, @@ -2035,14 +2037,50 @@ mod test_utils { .block .pred_epochs .new_epoch(BlockHeight(1)); + // initialize parameter storage + let params = Parameters { + epoch_duration: EpochDuration { min_num_of_blocks: 1, min_duration: DurationSecs(3600) }, + max_expected_time_per_block: DurationSecs(3600), + max_proposal_bytes: Default::default(), + max_block_gas: 100, + vp_whitelist: vec![], + tx_whitelist: vec![], + implicit_vp_code_hash: Default::default(), + epochs_per_year: 365, + max_signatures_per_transaction: 10, + pos_gain_p: Default::default(), + pos_gain_d: Default::default(), + staked_ratio: Default::default(), + pos_inflation_amount: Default::default(), + fee_unshielding_gas_limit: 0, + fee_unshielding_descriptions_limit: 0, + minimum_gas_price: Default::default(), + }; + params.init_storage(&mut shell.wl_storage).expect("Test failed"); + // make wl_storage to update conversion for a new epoch + let token_params = token::Parameters { + max_reward_rate: Default::default(), + kd_gain_nom: Default::default(), + kp_gain_nom: Default::default(), + locked_ratio_target: Default::default(), + }; // Insert a map assigning random addresses to each token alias. // Needed for storage but not for this test. for (token, _) in address::tokens() { + let addr = address::gen_deterministic_established_address(token); + token_params.init_storage(&addr, &mut shell.wl_storage); + shell.wl_storage.write(&token::minted_balance_key(&addr), token::Amount::zero()).unwrap(); shell.wl_storage.storage.conversion_state.tokens.insert( token.to_string(), - address::gen_deterministic_established_address(token), + addr, ); } + shell.wl_storage.storage.conversion_state.tokens.insert("nam".to_string(), shell.wl_storage.storage.native_token.clone()); + token_params.init_storage(&shell.wl_storage.storage.native_token.clone(), &mut shell.wl_storage); + // final adjustments so that updating allowed conversions doesn't panic with + // divide by zero + shell.wl_storage.write(&token::minted_balance_key(&shell.wl_storage.storage.native_token.clone()), token::Amount::zero()).unwrap(); + shell.wl_storage.storage.conversion_state.normed_inflation = Some(1); update_allowed_conversions(&mut shell.wl_storage) .expect("update conversions failed"); shell.wl_storage.commit_block().expect("commit failed"); diff --git a/apps/src/lib/node/ledger/storage/mod.rs b/apps/src/lib/node/ledger/storage/mod.rs index 329d0a7bd8..1a6a880063 100644 --- a/apps/src/lib/node/ledger/storage/mod.rs +++ b/apps/src/lib/node/ledger/storage/mod.rs @@ -67,6 +67,8 @@ mod tests { use proptest::prelude::*; use proptest::test_runner::Config; use tempfile::TempDir; + use namada::ledger::parameters::{EpochDuration, Parameters}; + use namada::types::time::DurationSecs; use super::*; @@ -135,15 +137,36 @@ mod tests { let key = Key::parse("key").expect("cannot parse the key string"); let value: u64 = 1; let value_bytes = types::encode(&value); - + let mut wl_storage = WlStorage::new(WriteLog::default(), storage); + // initialize parameter storage + let params = Parameters { + epoch_duration: EpochDuration { min_num_of_blocks: 1, min_duration: DurationSecs(3600) }, + max_expected_time_per_block: DurationSecs(3600), + max_proposal_bytes: Default::default(), + max_block_gas: 100, + vp_whitelist: vec![], + tx_whitelist: vec![], + implicit_vp_code_hash: Default::default(), + epochs_per_year: 365, + max_signatures_per_transaction: 10, + pos_gain_p: Default::default(), + pos_gain_d: Default::default(), + staked_ratio: Default::default(), + pos_inflation_amount: Default::default(), + fee_unshielding_gas_limit: 0, + fee_unshielding_descriptions_limit: 0, + minimum_gas_price: Default::default(), + }; + params.init_storage(&mut wl_storage).expect("Test failed"); // insert and commit - storage + wl_storage + .storage .write(&key, value_bytes.clone()) .expect("write failed"); - storage.block.epoch = storage.block.epoch.next(); - storage.block.pred_epochs.new_epoch(BlockHeight(100)); + wl_storage.storage.block.epoch = wl_storage.storage.block.epoch.next(); + wl_storage.storage.block.pred_epochs.new_epoch(BlockHeight(100)); // make wl_storage to update conversion for a new epoch - let mut wl_storage = WlStorage::new(WriteLog::default(), storage); + let token_params = token::Parameters { max_reward_rate: Default::default(), kd_gain_nom: Default::default(), @@ -161,8 +184,11 @@ mod tests { addr, ); } + wl_storage.storage.conversion_state.tokens.insert("nam".to_string(), wl_storage.storage.native_token.clone()); token_params.init_storage(&wl_storage.storage.native_token.clone(), &mut wl_storage); + wl_storage.write(&token::minted_balance_key(&wl_storage.storage.native_token.clone()), token::Amount::zero()).unwrap(); + wl_storage.storage.conversion_state.normed_inflation = Some(1); update_allowed_conversions(&mut wl_storage) .expect("update conversions failed"); wl_storage.commit_block().expect("commit failed"); diff --git a/core/src/ledger/inflation.rs b/core/src/ledger/inflation.rs index 3e1902a445..477eb7ade9 100644 --- a/core/src/ledger/inflation.rs +++ b/core/src/ledger/inflation.rs @@ -73,7 +73,11 @@ impl RewardsController { .expect("Should not fail to convert Uint to Dec"); let epochs_py: Dec = epochs_per_year.into(); - let locked_ratio = locked / total; + let locked_ratio = if total.is_zero() { + Dec::one() + } else { + locked / total + }; let max_inflation = total_native * max_reward_rate / epochs_py; let p_gain = p_gain_nom * max_inflation; let d_gain = d_gain_nom * max_inflation; diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 72b58150a5..ffe85f0358 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -4467,7 +4467,7 @@ where &validator, -slash_amount.change(), epoch, - None, + Some(0), )?; } } @@ -5310,7 +5310,7 @@ where ¶ms, dest_validator, amount_after_slashing.change(), - pipeline_epoch, + current_epoch, None, )?; } diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 070f93aeaf..7d5f79f099 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -1550,7 +1550,7 @@ fn test_validator_sets() { ¶ms, &val1, -unbond.change(), - pipeline_epoch, + epoch, None, ) .unwrap(); @@ -1753,7 +1753,7 @@ fn test_validator_sets() { ¶ms, &val6, bond.change(), - pipeline_epoch, + epoch, None, ) .unwrap(); @@ -2035,7 +2035,7 @@ fn test_validator_sets_swap() { ¶ms, &val2, bond2.change(), - pipeline_epoch, + epoch, None, ) .unwrap(); @@ -2054,7 +2054,7 @@ fn test_validator_sets_swap() { ¶ms, &val3, bond3.change(), - pipeline_epoch, + epoch, None, ) .unwrap(); @@ -2088,7 +2088,7 @@ fn test_validator_sets_swap() { ¶ms, &val2, bonds.change(), - pipeline_epoch, + epoch, None, ) .unwrap(); @@ -2107,7 +2107,7 @@ fn test_validator_sets_swap() { ¶ms, &val3, bonds.change(), - pipeline_epoch, + epoch, None, ) .unwrap(); From af55362244c502d0e0c475df56e8f2edb7d556bc Mon Sep 17 00:00:00 2001 From: satan Date: Mon, 30 Oct 2023 16:58:55 +0100 Subject: [PATCH 10/47] More fixes --- apps/src/lib/config/genesis/templates.rs | 3 +- apps/src/lib/node/ledger/shell/init_chain.rs | 1 - apps/src/lib/node/ledger/shell/mod.rs | 51 ++++++++++---- apps/src/lib/node/ledger/storage/mod.rs | 48 +++++++++---- core/src/types/token.rs | 18 +++-- proof_of_stake/src/lib.rs | 4 +- proof_of_stake/src/tests.rs | 66 ++++-------------- ...B3D80CA615B4BBA75BDD5AA773E1E33792217D.bin | Bin 0 -> 7448 bytes ...FF8BF07596031CD460FEBFAEA4F75AF65D5402.bin | Bin 9208 -> 0 bytes ...D76149D3088F539CF8372D404609B89B095EF7.bin | Bin 7448 -> 0 bytes ...EB66F886B3A3B6C71D33F456B859D01DA47ADD.bin | Bin 9208 -> 0 bytes ...6EF25580F3F5916126CAC201AD67B3D644691.bin} | Bin 7448 -> 7448 bytes ...FC3DA2C57E4FD8FF0811D9CB129887F0F9F706.bin | Bin 25031 -> 0 bytes ...8B6780B6F18A312AE3909BEA19D16FCFE837DC.bin | Bin 18792 -> 0 bytes ...E72A01F0B169F946835583DC2C71B550315603.bin | Bin 19947 -> 0 bytes ...CFE8EEC08E2D8512695A667D294AE1A4A8D4E6.bin | Bin 9649 -> 0 bytes ...8DC6BBB31619C7E93A1A5A2E64B694DBE1BD6E.bin | Bin 7448 -> 0 bytes ...FA3F379DB351AB4AE081207ABFDFC429C9FA48.bin | Bin 17018 -> 0 bytes ...344FFFAA6CA273027CD480AEA68DDED57D88CA.bin | Bin 7448 -> 0 bytes ...B827EEEDA858AB983D16024AAA415579A68953.bin | Bin 9649 -> 0 bytes ...5A3F8ED350401B95715A6AF9BDD8CD1C367BA.bin} | Bin 7448 -> 7448 bytes tests/src/e2e/ibc_tests.rs | 4 +- tests/src/integration/masp.rs | 30 ++++---- 23 files changed, 111 insertions(+), 114 deletions(-) create mode 100644 test_fixtures/masp_proofs/00E4DF5E2DCAD1EC5923537C64B3D80CA615B4BBA75BDD5AA773E1E33792217D.bin delete mode 100644 test_fixtures/masp_proofs/29AC8DE3B07495BEABEAF50FE8FF8BF07596031CD460FEBFAEA4F75AF65D5402.bin delete mode 100644 test_fixtures/masp_proofs/37332141CB34FC30FF51F4BEE8D76149D3088F539CF8372D404609B89B095EF7.bin delete mode 100644 test_fixtures/masp_proofs/52984E26D4A044A259B441C1DAEB66F886B3A3B6C71D33F456B859D01DA47ADD.bin rename test_fixtures/masp_proofs/{1362F1CF9B836CF8B05D8189EA9CB1712CCA85B0E96A3330A63BE7CD9E5ECD22.bin => 5A70C79871C7B5DF84C5CF3C4D86EF25580F3F5916126CAC201AD67B3D644691.bin} (54%) delete mode 100644 test_fixtures/masp_proofs/9883C2EF7971504BB1CF651BAFFC3DA2C57E4FD8FF0811D9CB129887F0F9F706.bin delete mode 100644 test_fixtures/masp_proofs/99393E3AC8046F86ABA05519568B6780B6F18A312AE3909BEA19D16FCFE837DC.bin delete mode 100644 test_fixtures/masp_proofs/BA4FED83467B6FEE522748C6F7E72A01F0B169F946835583DC2C71B550315603.bin delete mode 100644 test_fixtures/masp_proofs/C7ECE8C02C2E764EFD5B6A0756CFE8EEC08E2D8512695A667D294AE1A4A8D4E6.bin delete mode 100644 test_fixtures/masp_proofs/EE7C912B7E21F07494D58AA6668DC6BBB31619C7E93A1A5A2E64B694DBE1BD6E.bin delete mode 100644 test_fixtures/masp_proofs/EEB91EB873807EC77BBCA95D4CFA3F379DB351AB4AE081207ABFDFC429C9FA48.bin delete mode 100644 test_fixtures/masp_proofs/F068FDF05B8F25DD923E667215344FFFAA6CA273027CD480AEA68DDED57D88CA.bin delete mode 100644 test_fixtures/masp_proofs/F36A8353F15FD6D8158DBC67DDB827EEEDA858AB983D16024AAA415579A68953.bin rename test_fixtures/masp_proofs/{8B29BC2E1A96DF331C7C3A2B227C98D1E5AAAA9988F26B1A47090ACCE693572F.bin => F39ADA174E1FB9F45BFFB0C68625A3F8ED350401B95715A6AF9BDD8CD1C367BA.bin} (52%) diff --git a/apps/src/lib/config/genesis/templates.rs b/apps/src/lib/config/genesis/templates.rs index 15defbd17f..0296907a46 100644 --- a/apps/src/lib/config/genesis/templates.rs +++ b/apps/src/lib/config/genesis/templates.rs @@ -210,10 +210,9 @@ pub struct Tokens { )] pub struct TokenConfig { pub denom: Denomination, - pub parameters: token::Parameters + pub parameters: token::Parameters, } - #[derive( Clone, Debug, diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 2b36a4e7a8..2038ff30fb 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -336,7 +336,6 @@ where total_token_balance, ) .unwrap(); - } } diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 1138467c31..61d909d408 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -1499,6 +1499,7 @@ mod test_utils { use data_encoding::HEXUPPER; use namada::core::ledger::storage::EPOCH_SWITCH_BLOCKS_DELAY; + use namada::ledger::parameters::{EpochDuration, Parameters}; use namada::ledger::storage::mockdb::MockDB; use namada::ledger::storage::{ update_allowed_conversions, LastBlock, Sha256Hasher, @@ -1519,11 +1520,9 @@ mod test_utils { use namada::types::transaction::{Fee, TxType, WrapperTx}; use tempfile::tempdir; use tokio::sync::mpsc::{Sender, UnboundedReceiver}; - use namada::ledger::parameters::{EpochDuration, Parameters}; use super::*; use crate::config::ethereum_bridge::ledger::ORACLE_CHANNEL_BUFFER_SIZE; - use crate::facade::tendermint_proto::abci::{ Misbehavior, RequestInitChain, RequestPrepareProposal, RequestProcessProposal, @@ -2039,7 +2038,10 @@ mod test_utils { .new_epoch(BlockHeight(1)); // initialize parameter storage let params = Parameters { - epoch_duration: EpochDuration { min_num_of_blocks: 1, min_duration: DurationSecs(3600) }, + epoch_duration: EpochDuration { + min_num_of_blocks: 1, + min_duration: DurationSecs(3600), + }, max_expected_time_per_block: DurationSecs(3600), max_proposal_bytes: Default::default(), max_block_gas: 100, @@ -2056,7 +2058,9 @@ mod test_utils { fee_unshielding_descriptions_limit: 0, minimum_gas_price: Default::default(), }; - params.init_storage(&mut shell.wl_storage).expect("Test failed"); + params + .init_storage(&mut shell.wl_storage) + .expect("Test failed"); // make wl_storage to update conversion for a new epoch let token_params = token::Parameters { max_reward_rate: Default::default(), @@ -2069,17 +2073,36 @@ mod test_utils { for (token, _) in address::tokens() { let addr = address::gen_deterministic_established_address(token); token_params.init_storage(&addr, &mut shell.wl_storage); - shell.wl_storage.write(&token::minted_balance_key(&addr), token::Amount::zero()).unwrap(); - shell.wl_storage.storage.conversion_state.tokens.insert( - token.to_string(), - addr, - ); + shell + .wl_storage + .write(&token::minted_balance_key(&addr), token::Amount::zero()) + .unwrap(); + shell + .wl_storage + .storage + .conversion_state + .tokens + .insert(token.to_string(), addr); } - shell.wl_storage.storage.conversion_state.tokens.insert("nam".to_string(), shell.wl_storage.storage.native_token.clone()); - token_params.init_storage(&shell.wl_storage.storage.native_token.clone(), &mut shell.wl_storage); - // final adjustments so that updating allowed conversions doesn't panic with - // divide by zero - shell.wl_storage.write(&token::minted_balance_key(&shell.wl_storage.storage.native_token.clone()), token::Amount::zero()).unwrap(); + shell.wl_storage.storage.conversion_state.tokens.insert( + "nam".to_string(), + shell.wl_storage.storage.native_token.clone(), + ); + token_params.init_storage( + &shell.wl_storage.storage.native_token.clone(), + &mut shell.wl_storage, + ); + // final adjustments so that updating allowed conversions doesn't panic + // with divide by zero + shell + .wl_storage + .write( + &token::minted_balance_key( + &shell.wl_storage.storage.native_token.clone(), + ), + token::Amount::zero(), + ) + .unwrap(); shell.wl_storage.storage.conversion_state.normed_inflation = Some(1); update_allowed_conversions(&mut shell.wl_storage) .expect("update conversions failed"); diff --git a/apps/src/lib/node/ledger/storage/mod.rs b/apps/src/lib/node/ledger/storage/mod.rs index 1a6a880063..7a1f44e6f9 100644 --- a/apps/src/lib/node/ledger/storage/mod.rs +++ b/apps/src/lib/node/ledger/storage/mod.rs @@ -54,6 +54,7 @@ mod tests { use std::collections::HashMap; use itertools::Itertools; + use namada::ledger::parameters::{EpochDuration, Parameters}; use namada::ledger::storage::write_log::WriteLog; use namada::ledger::storage::{ types, update_allowed_conversions, WlStorage, @@ -62,13 +63,12 @@ mod tests { use namada::types::chain::ChainId; use namada::types::hash::Hash; use namada::types::storage::{BlockHash, BlockHeight, Key}; + use namada::types::time::DurationSecs; use namada::types::{address, storage, token}; use proptest::collection::vec; use proptest::prelude::*; use proptest::test_runner::Config; use tempfile::TempDir; - use namada::ledger::parameters::{EpochDuration, Parameters}; - use namada::types::time::DurationSecs; use super::*; @@ -140,7 +140,10 @@ mod tests { let mut wl_storage = WlStorage::new(WriteLog::default(), storage); // initialize parameter storage let params = Parameters { - epoch_duration: EpochDuration { min_num_of_blocks: 1, min_duration: DurationSecs(3600) }, + epoch_duration: EpochDuration { + min_num_of_blocks: 1, + min_duration: DurationSecs(3600), + }, max_expected_time_per_block: DurationSecs(3600), max_proposal_bytes: Default::default(), max_block_gas: 100, @@ -164,7 +167,11 @@ mod tests { .write(&key, value_bytes.clone()) .expect("write failed"); wl_storage.storage.block.epoch = wl_storage.storage.block.epoch.next(); - wl_storage.storage.block.pred_epochs.new_epoch(BlockHeight(100)); + wl_storage + .storage + .block + .pred_epochs + .new_epoch(BlockHeight(100)); // make wl_storage to update conversion for a new epoch let token_params = token::Parameters { @@ -178,16 +185,33 @@ mod tests { for (token, _) in address::tokens() { let addr = address::gen_deterministic_established_address(token); token_params.init_storage(&addr, &mut wl_storage); - wl_storage.write(&token::minted_balance_key(&addr), token::Amount::zero()).unwrap(); - wl_storage.storage.conversion_state.tokens.insert( - token.to_string(), - addr, - ); + wl_storage + .write(&token::minted_balance_key(&addr), token::Amount::zero()) + .unwrap(); + wl_storage + .storage + .conversion_state + .tokens + .insert(token.to_string(), addr); } - wl_storage.storage.conversion_state.tokens.insert("nam".to_string(), wl_storage.storage.native_token.clone()); - token_params.init_storage(&wl_storage.storage.native_token.clone(), &mut wl_storage); + wl_storage + .storage + .conversion_state + .tokens + .insert("nam".to_string(), wl_storage.storage.native_token.clone()); + token_params.init_storage( + &wl_storage.storage.native_token.clone(), + &mut wl_storage, + ); - wl_storage.write(&token::minted_balance_key(&wl_storage.storage.native_token.clone()), token::Amount::zero()).unwrap(); + wl_storage + .write( + &token::minted_balance_key( + &wl_storage.storage.native_token.clone(), + ), + token::Amount::zero(), + ) + .unwrap(); wl_storage.storage.conversion_state.normed_inflation = Some(1); update_allowed_conversions(&mut wl_storage) .expect("update conversions failed"); diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 343fc3e3e5..372cd9a2c0 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1027,17 +1027,15 @@ impl Parameters { locked_ratio_target: locked_target, } = self; wl_storage - .write( - &masp_last_inflation_key(&address), - Amount::zero(), - ) - .expect("last inflation key for the given asset must be initialized"); + .write(&masp_last_inflation_key(address), Amount::zero()) + .expect( + "last inflation key for the given asset must be initialized", + ); wl_storage - .write( - &masp_last_locked_ratio_key(&address), - Dec::zero(), - ) - .expect("last locked ratio key for the given asset must be initialized"); + .write(&masp_last_locked_ratio_key(address), Dec::zero()) + .expect( + "last locked ratio key for the given asset must be initialized", + ); wl_storage .write(&masp_max_reward_rate_key(address), max_rate) .expect("max reward rate for the given asset must be initialized"); diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index ffe85f0358..f977ff4a0c 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -2780,8 +2780,8 @@ where insert_validator_into_validator_set( storage, - ¶ms, - &address, + params, + address, token::Amount::zero(), current_epoch, offset, diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 7d5f79f099..35c1d04fae 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -1545,15 +1545,8 @@ fn test_validator_sets() { // Because `update_validator_set` and `update_validator_deltas` are // effective from pipeline offset, we use pipeline epoch for the rest of the // checks - update_validator_set( - &mut s, - ¶ms, - &val1, - -unbond.change(), - epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val1, -unbond.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -1748,15 +1741,8 @@ fn test_validator_sets() { let bond = token::Amount::from_uint(500_000, 0).unwrap(); let stake6 = stake6 + bond; println!("val6 {val6} new stake {}", stake6.to_string_native()); - update_validator_set( - &mut s, - ¶ms, - &val6, - bond.change(), - epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val6, bond.change(), epoch, None) + .unwrap(); update_validator_deltas(&mut s, ¶ms, &val6, bond.change(), epoch, None) .unwrap(); let val6_bond_epoch = pipeline_epoch; @@ -2030,15 +2016,8 @@ fn test_validator_sets_swap() { assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake2), 0); assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake3), 0); - update_validator_set( - &mut s, - ¶ms, - &val2, - bond2.change(), - epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val2, bond2.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -2049,15 +2028,8 @@ fn test_validator_sets_swap() { ) .unwrap(); - update_validator_set( - &mut s, - ¶ms, - &val3, - bond3.change(), - epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val3, bond3.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -2083,15 +2055,8 @@ fn test_validator_sets_swap() { into_tm_voting_power(params.tm_votes_per_token, stake3) ); - update_validator_set( - &mut s, - ¶ms, - &val2, - bonds.change(), - epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val2, bonds.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -2102,15 +2067,8 @@ fn test_validator_sets_swap() { ) .unwrap(); - update_validator_set( - &mut s, - ¶ms, - &val3, - bonds.change(), - epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val3, bonds.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, diff --git a/test_fixtures/masp_proofs/00E4DF5E2DCAD1EC5923537C64B3D80CA615B4BBA75BDD5AA773E1E33792217D.bin b/test_fixtures/masp_proofs/00E4DF5E2DCAD1EC5923537C64B3D80CA615B4BBA75BDD5AA773E1E33792217D.bin new file mode 100644 index 0000000000000000000000000000000000000000..648bc521605b4d51b77d4d6163cdf12b860706a7 GIT binary patch literal 7448 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 diff --git a/test_fixtures/masp_proofs/37332141CB34FC30FF51F4BEE8D76149D3088F539CF8372D404609B89B095EF7.bin b/test_fixtures/masp_proofs/37332141CB34FC30FF51F4BEE8D76149D3088F539CF8372D404609B89B095EF7.bin deleted file mode 100644 index 18f83d0543674902e112707b9a890c8baf916c7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7448 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 diff --git a/test_fixtures/masp_proofs/52984E26D4A044A259B441C1DAEB66F886B3A3B6C71D33F456B859D01DA47ADD.bin b/test_fixtures/masp_proofs/52984E26D4A044A259B441C1DAEB66F886B3A3B6C71D33F456B859D01DA47ADD.bin deleted file mode 100644 index c51b5ed0d978ab920e1c513daaffdee73adc4aed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9208 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 Od;j+x0s`nCH~$7@I|zCJ diff --git a/test_fixtures/masp_proofs/1362F1CF9B836CF8B05D8189EA9CB1712CCA85B0E96A3330A63BE7CD9E5ECD22.bin b/test_fixtures/masp_proofs/5A70C79871C7B5DF84C5CF3C4D86EF25580F3F5916126CAC201AD67B3D644691.bin similarity index 54% rename from test_fixtures/masp_proofs/1362F1CF9B836CF8B05D8189EA9CB1712CCA85B0E96A3330A63BE7CD9E5ECD22.bin rename to test_fixtures/masp_proofs/5A70C79871C7B5DF84C5CF3C4D86EF25580F3F5916126CAC201AD67B3D644691.bin index 741f4d5106a2c0107b1dbbcb346b1da4d04cc5fa..f1c8aa5c63490eff0eb4bc67a0feabd687ccc7dd 100644 GIT binary patch delta 2142 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 Yx6j}GyTc!+KWFv+m;Lqmb9t421Kf{AI{*Lx diff --git a/test_fixtures/masp_proofs/99393E3AC8046F86ABA05519568B6780B6F18A312AE3909BEA19D16FCFE837DC.bin b/test_fixtures/masp_proofs/99393E3AC8046F86ABA05519568B6780B6F18A312AE3909BEA19D16FCFE837DC.bin deleted file mode 100644 index f456d94d7d508b81413aada12461d083d9b29c0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18792 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 diff --git a/test_fixtures/masp_proofs/BA4FED83467B6FEE522748C6F7E72A01F0B169F946835583DC2C71B550315603.bin b/test_fixtures/masp_proofs/BA4FED83467B6FEE522748C6F7E72A01F0B169F946835583DC2C71B550315603.bin deleted file mode 100644 index 565d189c0ce9f5b3fc605e1d63a57a4b3ff949e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19947 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 NzCZiK~)dy{2)v!?Iv~r|L{bjoXQNN0??% zC)8Vfc>Y$J&x{-d+Kqsv=!>+iXWZ12965yPK5KsQ!Sn5Ox!ZdvZIec2Ga4kYC^i$2 zk&B`9p4}LlZ=fEqnnS*-T*6Y%f}cwcvMC| z@%BTx&l6L4c#XrgR+HY3({7o!Oi3XPv`UtMh8LgM5}2Vbha(y~DQ#F7t;?RjZijwS z+k~fy>ly|$vBMJ!JK*@+bW_>Ue;PZ$j4kb9m! zqo)CaQPv?815&S=46xFM-=Tz_CC70hx7T61=({Un@gP2XW-u%Otcc$XIUnjl4e&qV z2Nn)_m{ou6MID(wS?)Yt&Q*Ov1uC#_>f!t(*uH~tr-t3}72onzEicT20WJXAM9RdJ z8Hji=6=?r31b?+%w zH)6x3thvA>JyU|rydeMzq==QGrc@o-VcY6x{9fjE`FVI=m8CsMj(%z`=xl@n1XS

Mq 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 LYVHrJbZ_wAYcnI= delta 70006 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

m#8;!)>*=8Z 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 VxK3>Bfn?pg9f`*ht=su>{(s+~)bIcR delta 69425 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 URb5LruO`>2?+1^sCbP)@1B2ASnE(I) diff --git a/wasm_for_tests/vp_read_storage_key.wasm b/wasm_for_tests/vp_read_storage_key.wasm index 9655b9f356f7baa4ade8321616cb90144c361c36..ef65d9152e3f2595fa59fd2c44036c11c1c8309b 100755 GIT binary patch delta 111653 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 Date: Thu, 2 Nov 2023 10:48:25 +0100 Subject: [PATCH 23/47] [fix]: Compiling now. Tests need fixing --- apps/src/lib/config/genesis/templates.rs | 3 +- apps/src/lib/node/ledger/shell/init_chain.rs | 1 + apps/src/lib/node/ledger/storage/mod.rs | 74 ++++---------------- core/src/types/token.rs | 18 ++--- proof_of_stake/src/lib.rs | 8 +-- proof_of_stake/src/tests.rs | 66 +++++++++++++---- tests/src/e2e/ledger_tests.rs | 11 +-- wasm/checksums.json | 44 ++++++------ 8 files changed, 107 insertions(+), 118 deletions(-) diff --git a/apps/src/lib/config/genesis/templates.rs b/apps/src/lib/config/genesis/templates.rs index 0296907a46..15defbd17f 100644 --- a/apps/src/lib/config/genesis/templates.rs +++ b/apps/src/lib/config/genesis/templates.rs @@ -210,9 +210,10 @@ pub struct Tokens { )] pub struct TokenConfig { pub denom: Denomination, - pub parameters: token::Parameters, + pub parameters: token::Parameters } + #[derive( Clone, Debug, diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 2038ff30fb..2b36a4e7a8 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -336,6 +336,7 @@ where total_token_balance, ) .unwrap(); + } } diff --git a/apps/src/lib/node/ledger/storage/mod.rs b/apps/src/lib/node/ledger/storage/mod.rs index 7a1f44e6f9..329d0a7bd8 100644 --- a/apps/src/lib/node/ledger/storage/mod.rs +++ b/apps/src/lib/node/ledger/storage/mod.rs @@ -54,7 +54,6 @@ mod tests { use std::collections::HashMap; use itertools::Itertools; - use namada::ledger::parameters::{EpochDuration, Parameters}; use namada::ledger::storage::write_log::WriteLog; use namada::ledger::storage::{ types, update_allowed_conversions, WlStorage, @@ -63,7 +62,6 @@ mod tests { use namada::types::chain::ChainId; use namada::types::hash::Hash; use namada::types::storage::{BlockHash, BlockHeight, Key}; - use namada::types::time::DurationSecs; use namada::types::{address, storage, token}; use proptest::collection::vec; use proptest::prelude::*; @@ -137,43 +135,15 @@ mod tests { let key = Key::parse("key").expect("cannot parse the key string"); let value: u64 = 1; let value_bytes = types::encode(&value); - let mut wl_storage = WlStorage::new(WriteLog::default(), storage); - // initialize parameter storage - let params = Parameters { - epoch_duration: EpochDuration { - min_num_of_blocks: 1, - min_duration: DurationSecs(3600), - }, - max_expected_time_per_block: DurationSecs(3600), - max_proposal_bytes: Default::default(), - max_block_gas: 100, - vp_whitelist: vec![], - tx_whitelist: vec![], - implicit_vp_code_hash: Default::default(), - epochs_per_year: 365, - max_signatures_per_transaction: 10, - pos_gain_p: Default::default(), - pos_gain_d: Default::default(), - staked_ratio: Default::default(), - pos_inflation_amount: Default::default(), - fee_unshielding_gas_limit: 0, - fee_unshielding_descriptions_limit: 0, - minimum_gas_price: Default::default(), - }; - params.init_storage(&mut wl_storage).expect("Test failed"); + // insert and commit - wl_storage - .storage + storage .write(&key, value_bytes.clone()) .expect("write failed"); - wl_storage.storage.block.epoch = wl_storage.storage.block.epoch.next(); - wl_storage - .storage - .block - .pred_epochs - .new_epoch(BlockHeight(100)); + storage.block.epoch = storage.block.epoch.next(); + storage.block.pred_epochs.new_epoch(BlockHeight(100)); // make wl_storage to update conversion for a new epoch - + let mut wl_storage = WlStorage::new(WriteLog::default(), storage); let token_params = token::Parameters { max_reward_rate: Default::default(), kd_gain_nom: Default::default(), @@ -185,34 +155,14 @@ mod tests { for (token, _) in address::tokens() { let addr = address::gen_deterministic_established_address(token); token_params.init_storage(&addr, &mut wl_storage); - wl_storage - .write(&token::minted_balance_key(&addr), token::Amount::zero()) - .unwrap(); - wl_storage - .storage - .conversion_state - .tokens - .insert(token.to_string(), addr); + wl_storage.write(&token::minted_balance_key(&addr), token::Amount::zero()).unwrap(); + wl_storage.storage.conversion_state.tokens.insert( + token.to_string(), + addr, + ); } - wl_storage - .storage - .conversion_state - .tokens - .insert("nam".to_string(), wl_storage.storage.native_token.clone()); - token_params.init_storage( - &wl_storage.storage.native_token.clone(), - &mut wl_storage, - ); - - wl_storage - .write( - &token::minted_balance_key( - &wl_storage.storage.native_token.clone(), - ), - token::Amount::zero(), - ) - .unwrap(); - wl_storage.storage.conversion_state.normed_inflation = Some(1); + token_params.init_storage(&wl_storage.storage.native_token.clone(), &mut wl_storage); + wl_storage.write(&token::minted_balance_key(&wl_storage.storage.native_token.clone()), token::Amount::zero()).unwrap(); update_allowed_conversions(&mut wl_storage) .expect("update conversions failed"); wl_storage.commit_block().expect("commit failed"); diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 372cd9a2c0..343fc3e3e5 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1027,15 +1027,17 @@ impl Parameters { locked_ratio_target: locked_target, } = self; wl_storage - .write(&masp_last_inflation_key(address), Amount::zero()) - .expect( - "last inflation key for the given asset must be initialized", - ); + .write( + &masp_last_inflation_key(&address), + Amount::zero(), + ) + .expect("last inflation key for the given asset must be initialized"); wl_storage - .write(&masp_last_locked_ratio_key(address), Dec::zero()) - .expect( - "last locked ratio key for the given asset must be initialized", - ); + .write( + &masp_last_locked_ratio_key(&address), + Dec::zero(), + ) + .expect("last locked ratio key for the given asset must be initialized"); wl_storage .write(&masp_max_reward_rate_key(address), max_rate) .expect("max reward rate for the given asset must be initialized"); diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index f977ff4a0c..72b58150a5 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -2780,8 +2780,8 @@ where insert_validator_into_validator_set( storage, - params, - address, + ¶ms, + &address, token::Amount::zero(), current_epoch, offset, @@ -4467,7 +4467,7 @@ where &validator, -slash_amount.change(), epoch, - Some(0), + None, )?; } } @@ -5310,7 +5310,7 @@ where ¶ms, dest_validator, amount_after_slashing.change(), - current_epoch, + pipeline_epoch, None, )?; } diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 35c1d04fae..070f93aeaf 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -1545,8 +1545,15 @@ fn test_validator_sets() { // Because `update_validator_set` and `update_validator_deltas` are // effective from pipeline offset, we use pipeline epoch for the rest of the // checks - update_validator_set(&mut s, ¶ms, &val1, -unbond.change(), epoch, None) - .unwrap(); + update_validator_set( + &mut s, + ¶ms, + &val1, + -unbond.change(), + pipeline_epoch, + None, + ) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -1741,8 +1748,15 @@ fn test_validator_sets() { let bond = token::Amount::from_uint(500_000, 0).unwrap(); let stake6 = stake6 + bond; println!("val6 {val6} new stake {}", stake6.to_string_native()); - update_validator_set(&mut s, ¶ms, &val6, bond.change(), epoch, None) - .unwrap(); + update_validator_set( + &mut s, + ¶ms, + &val6, + bond.change(), + pipeline_epoch, + None, + ) + .unwrap(); update_validator_deltas(&mut s, ¶ms, &val6, bond.change(), epoch, None) .unwrap(); let val6_bond_epoch = pipeline_epoch; @@ -2016,8 +2030,15 @@ fn test_validator_sets_swap() { assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake2), 0); assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake3), 0); - update_validator_set(&mut s, ¶ms, &val2, bond2.change(), epoch, None) - .unwrap(); + update_validator_set( + &mut s, + ¶ms, + &val2, + bond2.change(), + pipeline_epoch, + None, + ) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -2028,8 +2049,15 @@ fn test_validator_sets_swap() { ) .unwrap(); - update_validator_set(&mut s, ¶ms, &val3, bond3.change(), epoch, None) - .unwrap(); + update_validator_set( + &mut s, + ¶ms, + &val3, + bond3.change(), + pipeline_epoch, + None, + ) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -2055,8 +2083,15 @@ fn test_validator_sets_swap() { into_tm_voting_power(params.tm_votes_per_token, stake3) ); - update_validator_set(&mut s, ¶ms, &val2, bonds.change(), epoch, None) - .unwrap(); + update_validator_set( + &mut s, + ¶ms, + &val2, + bonds.change(), + pipeline_epoch, + None, + ) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -2067,8 +2102,15 @@ fn test_validator_sets_swap() { ) .unwrap(); - update_validator_set(&mut s, ¶ms, &val3, bonds.change(), epoch, None) - .unwrap(); + update_validator_set( + &mut s, + ¶ms, + &val3, + bonds.change(), + pipeline_epoch, + None, + ) + .unwrap(); update_validator_deltas( &mut s, ¶ms, diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index ac0ab57240..224c3d767f 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -915,18 +915,11 @@ fn pos_bonds() -> Result<()> { genesis.parameters.parameters.min_num_of_blocks = 6; genesis.parameters.parameters.max_expected_time_per_block = 1; genesis.parameters.parameters.epochs_per_year = 31_536_000; - let mut genesis = setup::set_validators(2, genesis, base_dir, default_port_offset); - let bonds = genesis.transactions.bond.unwrap(); - genesis.transactions.bond = Some( - bonds - .into_iter() - .filter(|bond| (&bond.data.validator).as_ref() != "validator-1") - .collect() - ); - genesis + setup::set_validators(1, genesis, base_dir, default_port_offset) }, None, )?; + set_ethereum_bridge_mode( &test, &test.net.chain_id, diff --git a/wasm/checksums.json b/wasm/checksums.json index 873f8ae6a1..04ee8555d6 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,23 +1,23 @@ { - "tx_bond.wasm": "tx_bond.dbfe330a50d8d3511e125f611d88a3a32952de2077345ee79b5b320e9d11ece6.wasm", - "tx_bridge_pool.wasm": "tx_bridge_pool.ae875a5ddd27035b768678b2dbf70c8ecfa1bce316faee01b26bd0cbffb48b19.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.39b26c8ff0c06220163322c15a0f304232f5e3a6333c147d10d5a65e3d06c061.wasm", - "tx_ibc.wasm": "tx_ibc.d550ec9d20d965d91530ca4df74a75eeb27f88ae4484da3eef4b5599727bd94f.wasm", - "tx_init_account.wasm": "tx_init_account.06c8639a68cecf1160818bd92d81974a9990aceea08cbbcc47e18c51b2ec9449.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.daa176497bb1f4bb97346d1ba2387e0cc84e704eca70ef78a56228e03969fb76.wasm", - "tx_init_validator.wasm": "tx_init_validator.8d393d2cfdf55fd88beb4f26840d22e98da5e0390aac5fbcf1dc314e0eb4a9ca.wasm", - "tx_redelegate.wasm": "tx_redelegate.c586dc50a452948e2dd79718519bfa7b966999126ba52807bc4ba3a6f7a6290d.wasm", - "tx_resign_steward.wasm": "tx_resign_steward.994a9a3e10baacf900087382d2d8bb8045033927a3d040bf882c6d60ecfe0d5b.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.aceb2779c307f96e377e3e189589ea54e7b05d712cac27c6943e1d941334b421.wasm", - "tx_transfer.wasm": "tx_transfer.adab8a00709de083b6b4b534d4dfe002d479085ce1a766718dd3afe920659ac3.wasm", - "tx_unbond.wasm": "tx_unbond.a1dc615400ad625cf9b82ea28735a2fbffd1dd6745761eff6df4d08358162195.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.9debbc06681dd602e8a34ccf20d50dddee9963447409d2597893bf54937e7cee.wasm", - "tx_update_account.wasm": "tx_update_account.3917c5c25d2ffb0d717b9475a74d825840db94d48fcb4a0fe7450f3b32258cbe.wasm", - "tx_update_steward_commission.wasm": "tx_update_steward_commission.1a6f68eaf3b2eff9da777ce8c73f94865c49fa2ef94b5c0e1d65f46960cc9a5e.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.3f818de8c638e24249a26ccd20ec6a1700092c3700dcf6b91aea11058586ebef.wasm", - "tx_withdraw.wasm": "tx_withdraw.3cf7e6b6b77abd0eae58302659c9de31919894f36c0f7dbd85a6c853cd023483.wasm", - "vp_implicit.wasm": "vp_implicit.46d4206719acaaa8d0a422b6f969719b458cc5ca7c25677eb4bf23b52b4b6490.wasm", - "vp_masp.wasm": "vp_masp.c1e8f569ed3ad2bf5152e34f8ea9e255439066e0800ae73620a7be9a976f2ff7.wasm", - "vp_user.wasm": "vp_user.6b5bf2c3a5367d4f379187a8d2cf71457085aa1184a2b98b9e00ab6978847e08.wasm", - "vp_validator.wasm": "vp_validator.a683a9724e335cdf8f33eaee9ac3127a21d9f76e9769a5f3aea63b3bf04f2011.wasm" -} \ No newline at end of file + "tx_bond.wasm": "tx_bond.9fdaf63c3052a44195c8280d749e1682654ee2bad6edd25c6551bdc8197f7512.wasm", + "tx_bridge_pool.wasm": "tx_bridge_pool.605e9b7e8b0ff27863567b56d3f5e757783f5c75c596260d7c0805a47b7a877e.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.f036ef369e754cb6c75453f9b380f95be588d330f993284b08b477231d1ed0eb.wasm", + "tx_ibc.wasm": "tx_ibc.95832c0d92bb83ac2cde7b3c7cde9edae7c21f8dd5b32d93a9368093d58b5f15.wasm", + "tx_init_account.wasm": "tx_init_account.f525992552827cfd7693d568879dfd1f54406af9906b8827bc86e4beb21a0efe.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.9c3cf15da2b14e2c9898f5910cbfcffb80ef14dfcd0082dc9039b170cb3cc208.wasm", + "tx_init_validator.wasm": "tx_init_validator.1b695e5869532bbaebe5d99d48a267ae0891e9c4470abbbad5d0b37118e35ca6.wasm", + "tx_redelegate.wasm": "tx_redelegate.63f6c6b2f1582b357870ca9b779e88139c1e859b18fe7ada71dfdf5842fd83c7.wasm", + "tx_resign_steward.wasm": "tx_resign_steward.c21e812ba4a095d4b5eca562a94a2ef5071522a95f87a641c5f6e2bb2a4c890d.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.e5b1b047b44cde0cc8f9f89ba15f611dbefe8a3c060c4012363fdf07e92c6875.wasm", + "tx_transfer.wasm": "tx_transfer.c34a0a0b15ae448db7bc455bab8c59e6510c5fe9cb2d6386897030c37a189e42.wasm", + "tx_unbond.wasm": "tx_unbond.e846fd512237bdee232edc8d87e637ab3631942e3e1eda635d595e96d52bf489.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.1c8db6285b60d8ce80ce5c1ff7d43755e50cd3749fad5fb286f66592112ba4cd.wasm", + "tx_update_account.wasm": "tx_update_account.9f9493f2fb39d0a5f0fa3327a78828e387ba15690ad86e96d4b8cdf509cf1efc.wasm", + "tx_update_steward_commission.wasm": "tx_update_steward_commission.92bcf758cbb58be93ae5d7fde616a3974016f0d41c09b7333bc6041bc23e8d30.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.825ffa0c7c7c588cb668af4ff6ba3ce5c5d1c37c0728c18a7eb807a02da52158.wasm", + "tx_withdraw.wasm": "tx_withdraw.0225f3c8ac6e4f2d4636568048035156c936d9a8bc12d4c0d619598089af6f47.wasm", + "vp_implicit.wasm": "vp_implicit.e8dd924d43f11854724350c3e082a35898dc03e21f1543c117cff6763d881d2b.wasm", + "vp_masp.wasm": "vp_masp.64553be3434516e8de6b3e0a445582dce7d7975bd9765f4fbd9875bdbf0f9feb.wasm", + "vp_user.wasm": "vp_user.37fee9afa11b5899c824b6b8e1f0a626f5fd05c9813af454081a797772ca3de6.wasm", + "vp_validator.wasm": "vp_validator.e6283c764480d04942c0fa2fb3d6e3d1cd18565b235b09e6a6deb7918c808f31.wasm" +} From 46841a25ae5958c81509770f8eb526b3a333711e Mon Sep 17 00:00:00 2001 From: brentstone Date: Mon, 30 Oct 2023 23:23:13 -0400 Subject: [PATCH 24/47] fix `make check-crates` --- ethereum_bridge/Cargo.toml | 1 + shared/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/ethereum_bridge/Cargo.toml b/ethereum_bridge/Cargo.toml index 40d8e6f422..08c5731e28 100644 --- a/ethereum_bridge/Cargo.toml +++ b/ethereum_bridge/Cargo.toml @@ -48,6 +48,7 @@ tracing = "0.1.30" [dev-dependencies] # Added "testing" feature. namada_core = {path = "../core", default-features = false, features = ["ferveo-tpke", "ethers-derive", "testing"]} +namada_proof_of_stake = {path = "../proof_of_stake", default-features = false, features = ["testing"]} assert_matches.workspace = true data-encoding.workspace = true ethabi.workspace = true diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 0795ca3c39..ce39a22ef8 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -167,6 +167,7 @@ wasmtimer = "0.2.0" [dev-dependencies] namada_core = {path = "../core", default-features = false, features = ["testing", "ibc-mocks"]} namada_ethereum_bridge = {path = "../ethereum_bridge", default-features = false, features = ["testing"]} +namada_proof_of_stake = {path = "../proof_of_stake", default-features = false, features = ["testing"]} namada_test_utils = {path = "../test_utils"} assert_matches.workspace = true async-trait.workspace = true From 1639cbbb78b0155bdf36fa8ae6d163312ad77481 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 2 Nov 2023 11:03:37 +0100 Subject: [PATCH 25/47] merging in upstream changes from feature branch --- apps/src/lib/config/genesis/templates.rs | 3 +- apps/src/lib/node/ledger/shell/init_chain.rs | 1 - apps/src/lib/node/ledger/storage/mod.rs | 74 ++++++++++++++++---- core/src/ledger/storage/masp_conversions.rs | 4 +- core/src/types/token.rs | 18 +++-- proof_of_stake/src/lib.rs | 8 +-- proof_of_stake/src/tests.rs | 66 ++++------------- tests/src/e2e/ledger_tests.rs | 18 ++++- 8 files changed, 104 insertions(+), 88 deletions(-) diff --git a/apps/src/lib/config/genesis/templates.rs b/apps/src/lib/config/genesis/templates.rs index 15defbd17f..0296907a46 100644 --- a/apps/src/lib/config/genesis/templates.rs +++ b/apps/src/lib/config/genesis/templates.rs @@ -210,10 +210,9 @@ pub struct Tokens { )] pub struct TokenConfig { pub denom: Denomination, - pub parameters: token::Parameters + pub parameters: token::Parameters, } - #[derive( Clone, Debug, diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 2b36a4e7a8..2038ff30fb 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -336,7 +336,6 @@ where total_token_balance, ) .unwrap(); - } } diff --git a/apps/src/lib/node/ledger/storage/mod.rs b/apps/src/lib/node/ledger/storage/mod.rs index 329d0a7bd8..7a1f44e6f9 100644 --- a/apps/src/lib/node/ledger/storage/mod.rs +++ b/apps/src/lib/node/ledger/storage/mod.rs @@ -54,6 +54,7 @@ mod tests { use std::collections::HashMap; use itertools::Itertools; + use namada::ledger::parameters::{EpochDuration, Parameters}; use namada::ledger::storage::write_log::WriteLog; use namada::ledger::storage::{ types, update_allowed_conversions, WlStorage, @@ -62,6 +63,7 @@ mod tests { use namada::types::chain::ChainId; use namada::types::hash::Hash; use namada::types::storage::{BlockHash, BlockHeight, Key}; + use namada::types::time::DurationSecs; use namada::types::{address, storage, token}; use proptest::collection::vec; use proptest::prelude::*; @@ -135,15 +137,43 @@ mod tests { let key = Key::parse("key").expect("cannot parse the key string"); let value: u64 = 1; let value_bytes = types::encode(&value); - + let mut wl_storage = WlStorage::new(WriteLog::default(), storage); + // initialize parameter storage + let params = Parameters { + epoch_duration: EpochDuration { + min_num_of_blocks: 1, + min_duration: DurationSecs(3600), + }, + max_expected_time_per_block: DurationSecs(3600), + max_proposal_bytes: Default::default(), + max_block_gas: 100, + vp_whitelist: vec![], + tx_whitelist: vec![], + implicit_vp_code_hash: Default::default(), + epochs_per_year: 365, + max_signatures_per_transaction: 10, + pos_gain_p: Default::default(), + pos_gain_d: Default::default(), + staked_ratio: Default::default(), + pos_inflation_amount: Default::default(), + fee_unshielding_gas_limit: 0, + fee_unshielding_descriptions_limit: 0, + minimum_gas_price: Default::default(), + }; + params.init_storage(&mut wl_storage).expect("Test failed"); // insert and commit - storage + wl_storage + .storage .write(&key, value_bytes.clone()) .expect("write failed"); - storage.block.epoch = storage.block.epoch.next(); - storage.block.pred_epochs.new_epoch(BlockHeight(100)); + wl_storage.storage.block.epoch = wl_storage.storage.block.epoch.next(); + wl_storage + .storage + .block + .pred_epochs + .new_epoch(BlockHeight(100)); // make wl_storage to update conversion for a new epoch - let mut wl_storage = WlStorage::new(WriteLog::default(), storage); + let token_params = token::Parameters { max_reward_rate: Default::default(), kd_gain_nom: Default::default(), @@ -155,14 +185,34 @@ mod tests { for (token, _) in address::tokens() { let addr = address::gen_deterministic_established_address(token); token_params.init_storage(&addr, &mut wl_storage); - wl_storage.write(&token::minted_balance_key(&addr), token::Amount::zero()).unwrap(); - wl_storage.storage.conversion_state.tokens.insert( - token.to_string(), - addr, - ); + wl_storage + .write(&token::minted_balance_key(&addr), token::Amount::zero()) + .unwrap(); + wl_storage + .storage + .conversion_state + .tokens + .insert(token.to_string(), addr); } - token_params.init_storage(&wl_storage.storage.native_token.clone(), &mut wl_storage); - wl_storage.write(&token::minted_balance_key(&wl_storage.storage.native_token.clone()), token::Amount::zero()).unwrap(); + wl_storage + .storage + .conversion_state + .tokens + .insert("nam".to_string(), wl_storage.storage.native_token.clone()); + token_params.init_storage( + &wl_storage.storage.native_token.clone(), + &mut wl_storage, + ); + + wl_storage + .write( + &token::minted_balance_key( + &wl_storage.storage.native_token.clone(), + ), + token::Amount::zero(), + ) + .unwrap(); + wl_storage.storage.conversion_state.normed_inflation = Some(1); update_allowed_conversions(&mut wl_storage) .expect("update conversions failed"); wl_storage.commit_block().expect("commit failed"); diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 4b3a2e45e9..5cbbca570a 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -137,9 +137,7 @@ where Uint::from(precision), total_token_in_masp.raw_amount(), ) - .and_then(|x| { - x.0.try_into().ok() - }) + .and_then(|x| x.0.try_into().ok()) .unwrap_or_else(|| { tracing::warn!( "MASP inflation for {} assumed to be 0 because the \ diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 343fc3e3e5..372cd9a2c0 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -1027,17 +1027,15 @@ impl Parameters { locked_ratio_target: locked_target, } = self; wl_storage - .write( - &masp_last_inflation_key(&address), - Amount::zero(), - ) - .expect("last inflation key for the given asset must be initialized"); + .write(&masp_last_inflation_key(address), Amount::zero()) + .expect( + "last inflation key for the given asset must be initialized", + ); wl_storage - .write( - &masp_last_locked_ratio_key(&address), - Dec::zero(), - ) - .expect("last locked ratio key for the given asset must be initialized"); + .write(&masp_last_locked_ratio_key(address), Dec::zero()) + .expect( + "last locked ratio key for the given asset must be initialized", + ); wl_storage .write(&masp_max_reward_rate_key(address), max_rate) .expect("max reward rate for the given asset must be initialized"); diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 72b58150a5..f977ff4a0c 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -2780,8 +2780,8 @@ where insert_validator_into_validator_set( storage, - ¶ms, - &address, + params, + address, token::Amount::zero(), current_epoch, offset, @@ -4467,7 +4467,7 @@ where &validator, -slash_amount.change(), epoch, - None, + Some(0), )?; } } @@ -5310,7 +5310,7 @@ where ¶ms, dest_validator, amount_after_slashing.change(), - pipeline_epoch, + current_epoch, None, )?; } diff --git a/proof_of_stake/src/tests.rs b/proof_of_stake/src/tests.rs index 070f93aeaf..35c1d04fae 100644 --- a/proof_of_stake/src/tests.rs +++ b/proof_of_stake/src/tests.rs @@ -1545,15 +1545,8 @@ fn test_validator_sets() { // Because `update_validator_set` and `update_validator_deltas` are // effective from pipeline offset, we use pipeline epoch for the rest of the // checks - update_validator_set( - &mut s, - ¶ms, - &val1, - -unbond.change(), - pipeline_epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val1, -unbond.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -1748,15 +1741,8 @@ fn test_validator_sets() { let bond = token::Amount::from_uint(500_000, 0).unwrap(); let stake6 = stake6 + bond; println!("val6 {val6} new stake {}", stake6.to_string_native()); - update_validator_set( - &mut s, - ¶ms, - &val6, - bond.change(), - pipeline_epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val6, bond.change(), epoch, None) + .unwrap(); update_validator_deltas(&mut s, ¶ms, &val6, bond.change(), epoch, None) .unwrap(); let val6_bond_epoch = pipeline_epoch; @@ -2030,15 +2016,8 @@ fn test_validator_sets_swap() { assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake2), 0); assert_eq!(into_tm_voting_power(params.tm_votes_per_token, stake3), 0); - update_validator_set( - &mut s, - ¶ms, - &val2, - bond2.change(), - pipeline_epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val2, bond2.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -2049,15 +2028,8 @@ fn test_validator_sets_swap() { ) .unwrap(); - update_validator_set( - &mut s, - ¶ms, - &val3, - bond3.change(), - pipeline_epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val3, bond3.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -2083,15 +2055,8 @@ fn test_validator_sets_swap() { into_tm_voting_power(params.tm_votes_per_token, stake3) ); - update_validator_set( - &mut s, - ¶ms, - &val2, - bonds.change(), - pipeline_epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val2, bonds.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, @@ -2102,15 +2067,8 @@ fn test_validator_sets_swap() { ) .unwrap(); - update_validator_set( - &mut s, - ¶ms, - &val3, - bonds.change(), - pipeline_epoch, - None, - ) - .unwrap(); + update_validator_set(&mut s, ¶ms, &val3, bonds.change(), epoch, None) + .unwrap(); update_validator_deltas( &mut s, ¶ms, diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 224c3d767f..809feb334d 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -915,11 +915,25 @@ fn pos_bonds() -> Result<()> { genesis.parameters.parameters.min_num_of_blocks = 6; genesis.parameters.parameters.max_expected_time_per_block = 1; genesis.parameters.parameters.epochs_per_year = 31_536_000; - setup::set_validators(1, genesis, base_dir, default_port_offset) + let mut genesis = setup::set_validators( + 2, + genesis, + base_dir, + default_port_offset, + ); + let bonds = genesis.transactions.bond.unwrap(); + genesis.transactions.bond = Some( + bonds + .into_iter() + .filter(|bond| { + (&bond.data.validator).as_ref() != "validator-1" + }) + .collect(), + ); + genesis }, None, )?; - set_ethereum_bridge_mode( &test, &test.net.chain_id, From 3a9d9da4c32751285f9e807bc8cd46ee61aa86e1 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 2 Nov 2023 13:26:52 +0100 Subject: [PATCH 26/47] [fix] Regenerated MASP integration test proofs --- ...B3D80CA615B4BBA75BDD5AA773E1E33792217D.bin | Bin 7448 -> 0 bytes ...CEA6651CDA5F544434A2FB9C5743209C0FE00E.bin | Bin 0 -> 15257 bytes ...CE2366CA1ECCFB3B50AADB75C4369AA9B1FE2E.bin | Bin 0 -> 7448 bytes ...9AD1132C9C91D103BB2A0C3F0FD677B1E3B735.bin | Bin 0 -> 20518 bytes ...AB97FD31CF24CDED81325016A0657158104757.bin | Bin 0 -> 7448 bytes ...024A7C32F1FD5A19811F4616E7E03C4B1DB10C.bin | Bin 0 -> 17018 bytes ...F036695988B745CBF51F6D8CB56C6A11C9D498.bin | Bin 0 -> 9941 bytes ...B3E0126D872DE64654C9F7225AE18E42650564.bin | Bin 0 -> 10382 bytes ...036C47707BFCE3CE7672A526ADEFA192F21577.bin | Bin 0 -> 24494 bytes ...4DEE977AB0FC421801231D05A1384ED6185D90.bin | Bin 0 -> 7448 bytes ...8ED96DFBF9A925BDE8E2A32DD3A3A160E74D9.bin} | Bin 7448 -> 7448 bytes ...CA923BE35ED8C131F062F63AC08645A8C82CFC.bin | Bin 0 -> 9649 bytes ...25A3F8ED350401B95715A6AF9BDD8CD1C367BA.bin | Bin 7448 -> 0 bytes ...629B8C97C137CD8F44F86196F2C42FE45201D9.bin | Bin 0 -> 7448 bytes ...04CB0C9BC7E998E8ADA84DB497859600C70826.bin | Bin 0 -> 12448 bytes 15 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test_fixtures/masp_proofs/00E4DF5E2DCAD1EC5923537C64B3D80CA615B4BBA75BDD5AA773E1E33792217D.bin create mode 100644 test_fixtures/masp_proofs/02710BEDC9AAEF8281AD3294C0CEA6651CDA5F544434A2FB9C5743209C0FE00E.bin create mode 100644 test_fixtures/masp_proofs/1701259D7EE079DAD0D27D1CDFCE2366CA1ECCFB3B50AADB75C4369AA9B1FE2E.bin create mode 100644 test_fixtures/masp_proofs/28DCE740F1BD7D7206389056C49AD1132C9C91D103BB2A0C3F0FD677B1E3B735.bin create mode 100644 test_fixtures/masp_proofs/3BE539A4BD341C5D970735AB70AB97FD31CF24CDED81325016A0657158104757.bin create mode 100644 test_fixtures/masp_proofs/3CE0EAA759CE865C5F92F4739E024A7C32F1FD5A19811F4616E7E03C4B1DB10C.bin create mode 100644 test_fixtures/masp_proofs/71630F282F9492A759691BEBC4F036695988B745CBF51F6D8CB56C6A11C9D498.bin create mode 100644 test_fixtures/masp_proofs/823B8C4280FBB0A1CF75A4EE81B3E0126D872DE64654C9F7225AE18E42650564.bin create mode 100644 test_fixtures/masp_proofs/9CCDEA31EDE3973806C1247BD7036C47707BFCE3CE7672A526ADEFA192F21577.bin create mode 100644 test_fixtures/masp_proofs/9D2160E776A448BE5E380DEE784DEE977AB0FC421801231D05A1384ED6185D90.bin rename test_fixtures/masp_proofs/{5A70C79871C7B5DF84C5CF3C4D86EF25580F3F5916126CAC201AD67B3D644691.bin => AAF849D0C3C7352A4241AB8C9918ED96DFBF9A925BDE8E2A32DD3A3A160E74D9.bin} (55%) create mode 100644 test_fixtures/masp_proofs/D58CB9F05F35F3001E8C3026F8CA923BE35ED8C131F062F63AC08645A8C82CFC.bin delete mode 100644 test_fixtures/masp_proofs/F39ADA174E1FB9F45BFFB0C68625A3F8ED350401B95715A6AF9BDD8CD1C367BA.bin create mode 100644 test_fixtures/masp_proofs/F534964AD0DF72DF15E18A0FE7629B8C97C137CD8F44F86196F2C42FE45201D9.bin create mode 100644 test_fixtures/masp_proofs/FBE9AE978398F28195774744E704CB0C9BC7E998E8ADA84DB497859600C70826.bin diff --git a/test_fixtures/masp_proofs/00E4DF5E2DCAD1EC5923537C64B3D80CA615B4BBA75BDD5AA773E1E33792217D.bin b/test_fixtures/masp_proofs/00E4DF5E2DCAD1EC5923537C64B3D80CA615B4BBA75BDD5AA773E1E33792217D.bin deleted file mode 100644 index 648bc521605b4d51b77d4d6163cdf12b860706a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7448 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 literal 0 HcmV?d00001 diff --git a/test_fixtures/masp_proofs/28DCE740F1BD7D7206389056C49AD1132C9C91D103BB2A0C3F0FD677B1E3B735.bin b/test_fixtures/masp_proofs/28DCE740F1BD7D7206389056C49AD1132C9C91D103BB2A0C3F0FD677B1E3B735.bin new file mode 100644 index 0000000000000000000000000000000000000000..f25848165a5ef017f1cc709e8b4bde393d31ac0b GIT binary patch literal 20518 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 Q_k5rJ=LirG$bZ@Y0SDBI5&!@I literal 0 HcmV?d00001 diff --git a/test_fixtures/masp_proofs/3BE539A4BD341C5D970735AB70AB97FD31CF24CDED81325016A0657158104757.bin b/test_fixtures/masp_proofs/3BE539A4BD341C5D970735AB70AB97FD31CF24CDED81325016A0657158104757.bin new file mode 100644 index 0000000000000000000000000000000000000000..8de16fa778aed4b1ec8115f831265d2e163c5ae8 GIT binary patch literal 7448 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 literal 0 HcmV?d00001 diff --git a/test_fixtures/masp_proofs/3CE0EAA759CE865C5F92F4739E024A7C32F1FD5A19811F4616E7E03C4B1DB10C.bin b/test_fixtures/masp_proofs/3CE0EAA759CE865C5F92F4739E024A7C32F1FD5A19811F4616E7E03C4B1DB10C.bin new file mode 100644 index 0000000000000000000000000000000000000000..dd35f134536524b92f0bf5bceed6f3550f3b042a GIT binary patch literal 17018 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�Nl;(2k*%_&Z!2vJ3?hah& z3_BstV@2D~p2xyCUv)LcjnQS#TOVz^S2rEGN>tYFYl8V^4%pO`*zkgNoH6koV?XnB ziS51G4J0UEetva%^5fB=r$bsstPK<=S!?S~vTm6G+}E^!>9{pa zbZWHmNtg}zxtFNcszGem(?iBDqLk?U6+yjxsA1Unc7$H?HClsise$*D7D?o}y>ryj zVd_lRUXmx_CWBA1VEYNxMzELYlhZ`X6GpF;P)< zY0XBU+kRN0EdbxNqujQ~+4iE2+7`iWWFUUEPuUtz{H6%T3PG~8L%h-@#ax?ODnr6t z8k(yXF#AAew|zz5N?}kxI(skr`*nY7%dspT?^t$%5;Tx?1NbzXe9$}F} z=`mNzC|FX;u}9~9B_b|Gzr<3GLv)ypAI8?yvLNCd@7Y$O+wDq7nQk_yH1Sbmk=D@%oq6G{?qmJ^3U!(;)lPc}E6a zzESBiANUvWCGzN~E%dNJ>kK@ZOOJECA5NwxFGR}Z@q&5y?BaVTwA|ITQcCRp=#c=Vm2n6-GqPtT$4cRZx4667&-W>qOlGVL>sJ z%D4SzTIh8YS=E{Izi3Ql--5NSPL6RkVu1`!w_S8Q8p-FZXc`olC>d$?FnY-(Kx6Wa zS>M|I^sK{#Zee#lD($Ur4mGc-?4j1JzvSC7kv$M*mHKX3a^sLmMoS6n1r}Plzc9AZ z1qcc=PSu|F7<%C8OItL!b$-fV!F-F76m(gK=`i@CtA%vZrK!n#RFILwlthV+XUZ+) zvPzH=IoD%IYR&bE9sXFLAI*f1)C)otkKx^~dSHS-Nh-{YGps0YvEDMZqlFtgSL6j=K9_H75hVFNSkyN*p`R>11MBmon-j7NrS zQRl!=darrCgSXjk@MCCX*n|>GP#(dLk1@#Cgc3LTmPvGG(e5jnP3g#^hP9q)!XscHa9v+kB(s(ZR@1@bq@FOF@$NNm{WV zzyyt#2n7sxi*O;c{3N&2ZAxxI+Z5}F^r)Az6)x*|*tfE@H`(O=^^UzYhaq-1ZxHi7f$6zI#o;XJvZVIr-Vuc=TbciI=9n|&2k*0I zDVeA{RwNwXVQo=BLoyLMg-Kyi+sI@N&{R8=c)8ZxRnB1(1jEUlc|vo>6=(iNc@AtF z-2R^8!4xA^<0m@k*D0hRr!;TOn1eK0ZOhO zyG2AxAsBhtq>H+O|BwjFp@e{)jzcds==d1?)^t552?1ACZ z@`_h}7P5gOxhF}Ou70*lq1&kgG^RK0iYoAEGyyW97?V73AuM1IpW9?0PgJ9%bj92Q z=Iaw?4+T&Ms7mpu=Fl*ye_pLR3YS)G13JCtsLCXlV0}!-%8sk3su$8ncGd|TO!9%# zEm%+5S-vsGNEVoV^jII^d5jB;lf(D-PRv0dI)R||+ zhvE(8D~MWoX0+?~3y0%Mv?sIC8Y_0IK-rZf{ISPm!})w?iM6>j_+h{Fh@sswuhBDe zd0w@({`9+lh4mwPyE?WUVlC9LMEZEnGHnI2=j@wCg0z;Y@|FeaNr~z1kDTDGEVK}e z}=#*edAA?{khLa+nBL!!c(@_H%OS7DV91o^GEBzUa$E-@IW+h`<>^ z@;jIn#xh;-XA7T%@IznrDfDN^YtBEnFcRGvemyQM5&o1(&8NHxZ4BnxZ;;4`4fG!N z z+q?+m@0u|GuJ2JoW%GuJ94O@h5PilvgdORL!fkTwvZJ%cDfi! zk81@UmnM6V5^=HS0AbMjt<&7_4BNNTB{O!9A$>4{1u&VD%%7vC`UM7ch}n=o5)O}b zjpuH$BX$DfiC&RyH*WX_zMKzO?iz07<$B#_!^ZhSz%$3nD%94vG(XqhCv3wtp|!gM3gk+V1t%?iA$~y=JF5d`X zqQklL5SUuFk-6cci4%Q~#|Zs>jx6fnO)CtjZFnb8fIQbZ;4WzvRD)6xK27a`HCSl~ zhx(6f=>!CY0eF(hxM4YQPYjNv`#XM!WyUg#%L~r8u`QE4pzR$)>Q7KUz6(>ivZ@&q zAuxa=HvOr>Ss_0b922|EseJe29`b|FJpziWb1qxB9hdDUnJbUio*pEz~;g5&XM zD|q3O0audE)j&llEy3rFQ?7f&M5jGZ0Ujo=P6>t2MRXNtt5T;i71j5Ry6o=EC0QK} z8B2(MLdL8oCv?k4qpz0ts~Niai3l_(eO13l-Fd!Al)(?Uct-zRJp<5izk_QQtc8Ay)^?jQ{vvfseb}>A0 zgc!Ke3Z#-}m5mUo9|=uv5Gj(GYw=eXwnju;UgoaNYCd!Dz z5C`msfY^m-qM9*$Yf-I`hCiVXd^T>azUS$U$~1Oz(PPRDkPf<}vUXMws@NzBcUnNU zAz7W4b(0?)wG^2iAb1+06s=P9e#842Wax$PGyg|^O40oF4#!d8d4RAHU{nSN1>@T??+T_Ej9971L>}i9(;it3I@^ zWx(%TsQNvJ(gtKO$oBLIaXJSjw?sWMPn+g3BsZmL(cOk-ltHtu5x(PCK|bR|%3K)n z^ss|!ON%(S9YbV9=I7Ym)J{eH~mMY97$cz7lOHIil6tN)4TFoQ}v*nR{Z%v8k>v;@lVr z8*Jy}A-Rxbo+AIQbiG;VZ2;tao{#gJB}|WQaFokCuzu|lJ!4V9k48!}R@ZH9^pVPs ztnfm4CBIVN{9N@xENy?@rz45;!z1dT+>GIRc7OTE@CPf`JM)nb;fp#O(Q^%3t(RrP zu@R|Qgot|_kJZ|)y$ncM!r_o4#}om91e(J)G_DtL>T__5_vG~>p`akNcS25T>jSF1 z7z@OOpW4MNW+BGti!lTja-(P|~&-h zEmLk9si_QjlY|3G$oLxbs8(r92x7Yk1C!NC zu?Q|C2hpTZTZ!P-yM4CS96o41K#RB*T>eVd*x0E6fbpO(1HOs4qa!Z+lgWJM9aL5HgpKCo^0Bb+`^h{5=`-DW ztBfCM0EU%x<{O;x5ZdOhLOkaG8llh|ZXw)&=zT@2^z^*gQf=QV5D zCmt)f-yuOeeGS90RNvx|>Mi{$;M7ZZKg)57Qp|qw7W_H;@Qa!z?3jwoP4q_-2=Z@( z2N-*8{LxN~ijmylvJyF{c9@|x640-<<8Is$oVh(Dti5oQdp1J{Z> zhV-#Z-rWhVvkPUPsmiU_ea2d9RQc1~5npu&Ikt`(KGNLte&Siv$9CQa` z`fLY`qgnYZwJ0;-^#ocUqtzrc2{yKU|6qNfS_vFrvq!6|4$x8^djboq4b>gA<+O@; zWO&8@*2+_ff8KmDAr}7)y_^h$?~6_`MwuVPEBd{S@M^ugu{vrrO*;m&L@L;y6~TXR o)A*xz?SJ_1zrLORvt;;x1;k&+;D6;m$Nd?Q|4JZ#j`=hG58M4SGXMYp literal 0 HcmV?d00001 diff --git a/test_fixtures/masp_proofs/71630F282F9492A759691BEBC4F036695988B745CBF51F6D8CB56C6A11C9D498.bin b/test_fixtures/masp_proofs/71630F282F9492A759691BEBC4F036695988B745CBF51F6D8CB56C6A11C9D498.bin new file mode 100644 index 0000000000000000000000000000000000000000..e44a42c0d20e55c28142f5ed5421185cb7a9f9de GIT binary patch literal 9941 zcmeI2Wl$a4vd1?PT!PDnFu@@}(BST}fovdX2u=td+}#Q8?(XgyEI4c+gy0e!f;;c# z)Hzl6etGAZuS5Hm-rt9Ag2Lgcz{;_{8xPNRH-!Mc`evp-HnJVz+ zNapT3fF|Dr-`t{q^mLnc9f3yBtcbOD zkH+_6=UP6n2SG;Dc(!0a)^4=O@e{=cre6X6Cqe#*^y`^Y95Yvohpdh!!{}tnN9uOO z%5)_B>g(kkOiUBI=K_2Pe|i40W$wagSf9oVsVKNN<(^XMk;sJKBVTSC##GTSk$9v{ zwyP81qDMl|$@uCpx^u_kKAL^(sUObKGLyi<=q#kfjn5Zj+r2!1VaartHhL&qT?53E z7a;WDK3Xt$pOUAV$VFDgTAW^HDt2^cLSzG{#&-yULW2-lL=BG+g^54G7-;lHc+x!9 ztk|w~@N~AU>&%tW4z&SURz!5e?u;vSXC<15bSKp%UUhUb?uE%j4{~D^ej55$vwjEj z^+?zE+~hy943#*Tf?Fc~N~`3#P7hZfs_Lb>M1e%vuF6x#AUh4G&XH}c1$;@Tii z6zSO&OkB0i3#+KuEskZ%r-K$B>p|5SSD3HTol46<2M!; za6^2LBt8YhPR6w>{aB;0c|WM*N+J=XHS_YZ+w8b4(0z($T#yduVvgHQ54v8aRN63q__@gqccBc8MC5 z(%v0NZ8O1>gjG9)I84Qw8D~-;Uy58YY#M6Aj@Z#a=c%02{2ls$k%2SlhIOjnomL!uqTB=Gm!Canq@*5)WP@ z#h{_?{Vz`dW+#-ol+Z@Wo^uMrK-vC0HGf8HMMSg;NK$~RAvd)q5ModD{)J`ao4X&{ z*Pjb7vALEDd92hF-QSQJ6mj+DngFsrUyn|Ay7F`2JT_hH*1B1CnT`$UalL3Yu$77{ z-%tTobEX|>Cf_`)JzHKA+s=Q}m(-(eJbk*V%b{5&x*wwlEMg&tv6d4$BSn=~uz^?b zFc?!1Ffok>e=_c~8yy7j10kH6+NJs^*EL%=4zSg!fISx%GF``~T%n@|ALNPPp99T2 zE<;Vymgcq2MrZ`R!q zeUi_F*`(m^t_Dm%Cfc(kRRI-Zwy&Cq=rFx3^&v@dr(zB{5yPx4KGI4b$T$ezxNbFt zsa;pJHB@e9mSVUF-N_efO=W(`$u$P- z;aMth078XS_7CqBKSb2Sl8`u_f67kyQk!tMxcr_io;~SwLk)0=;3SMU!MQO}fA(|} zJ&XyhQ^kw0a)4$Gw!c;5f=>;2f2hvw3XxPKRM$Bid&YNk$f)m6qf4B_DctzbWbSDR zC`M>bCdS2y~?E3YyfDh zU;QeLz6F`$tsH12Jnw2t*CbmS(_MOi5g!|1GNQ=20*V*6j09M4_E@k9mG4Q8y`Jck zT1Jpu z`9*w<`xZ=hgBB0O3pu*21MO&t0E24n&|6iwwqsoGr~_|L;{M6^X-jUi!H zp~3%!s=w_2BMR&Ify0q64BetX%GKztdnThUp+CJsTKAi;^ky19Pp_nTk3y_R5V5k18&d_9XZP;*&zJ6IZM_OUfeXH-;F21SG=YUs7i>8@mi!q0$s7tfg@>ZSGz-z_?g9<7qf4ggVNI5cznw{)>?aq z%klpJamm8A(U+OjgmRy86%JWTG)cecyGHeWnM(U2S8f8PC2gH*t=xtgG<%`bOAt`1 zYg3SD!OfZSJeq-p?(|Ev9()S>YJp++wgY|cF{^-U+J1}T%)_x71>ZfHvvHdVYotag zn7w;Qi*o;Y&_^e3Gd||8L=xblBu1!+Bi}tp4B}u}pH-PZT$!*fMc1M47?0RuBLev}+{kALGDODvnrW$N4$U z1CTC}^Km%WPHqx&rDhhiKMTfRQV0Cs2`Ujxe|rCE{tkoXE06!OM&MtVuaE7rY?lo! zf_eijYrRtqw$J1qjaU`e8pJ^2#ebgrn_9oA^&d&C6hXYE=_;n3`gQlNxO(gj>ms(E zVhm%2^EbJm1w9>Ipv6>%UMT!-Q^V4>rbuAklg>0|YGf4(|8Q{;3>mF>OrW!0p=fYH}p2%hJLl+1eEBdiZ*yYW;UGqcK)7~*O?pY_-w5@Or zhV;gg8# zyCe8`@_{A33B(t3-DY=?1Uayi0^Ti8@a8;AL6CMTV~A*;M3|yg^$2`{b|*+DDs4=YZ3w$TYcl(9pZj;RaI?NiRXSrRPRJx ziFbV&^FrfCg47Bl4a2Z|^7_)nzk>Gn)qmI<_$5)H(G}7bI6UtBO8t+v2i)I%p8s9L z1EGPt_mFuY{&N*l{N-@XRE88B7Is;nL${i~2vG}QV}i}~IQ@xs=tKc8klExjO3=ev zYPDR^5_utM_}u}|4gqEzUdLHvcSWB^G^x-Qo5?08NJ5dlF^e8m0>WETrawf8wCJz4 zyrLCQj@PJmvGaZ{f!R+Vf2C)DC%!>gc8Zgyydhyi-*tw~x?&QwGgWsQgMc7{dam?_ zl8T?T;BY2`g^Hoom2di9w8qKtnCelFB5^*~l{Q96BGO3sDF%EP8y{Eiu2uW#aFH9L z1_cJRduO3-xpiC=kvWU3uYB90W|J8##$eTbIK|))k$VE|TKGCLwO0je`%QGSQVm{OiK1sNX$dzfRz4|nfXNiu7+W7ULL(H?2 zjmd#zeCiC9-GEy7&CmIR{tcd25q(6TSmDPh@R^+hgmBzwJkQ(7@$O(qKD8^17~2Dh$(G=OVg&%b!NMHCh6! z@=UQV^XYRmk+JeBx5i}JyLqj-$Mb2g5|626V%a#1vMH3p%-XD)FG0h~HHJEya@p#> zqc74da$eBD?Y5r|qTG-+?Zy(B5YVmMB4}-fvpp6;I*G}C6f!n|J8r|i#l@q$V}EZM zf9VK+xkyE$&Appu)UuWt_1ZZ;9s;E@)DoF7NN?^8i4fd1Wyc-2%&Dexv-`lGiD>eo zIrgiAJOSZO7VFc7ZK%5@RqJ<;BcX5`5%iF`1JnTu^rWXA>bsE$X`+vm6t;jG4}Cn@ zi?r1p6{}C`tQgDFj%-I8$fF^!sjxsi)rsm`3r*s8nsd7p8Gg2?j{#(|iO=8|W%2Kcx<1%-n%~Gy<_4nG51Mu|Eq}433g%yshCC>nU(Z= zp(L4P)Q4OtcBi#{*=&!^cB$lkyf!A^Y%`A0U`|&}CF79LA!Bi4Nqw(fS@FTnCv(6> zW!(q9maZwT5V_Ki?fpv2@|H*&Toyy`BHj(um)#eXtKGwI9zLvw@k$zhA90QA#RsRy zeo?m*h6_3@)g$8ueAYxK+Whrd+{c~!C95O8ie$igWRKGgD7dX~h2M(E$l>VkEtcHy zFGgyt%&Fl(zD-HMT6wXI_%4IUTCryjncZ!sDh*q!-V#3s2Hdf@$$n!Fms!eAYd^}A zy)+=NfF&!leiMvJOlQ7SpI3M*xHNh?I;GHt=^r5*yd*6xVhKv7dHDI(%Aq(LZ#n%; z+XMR4UEHFBcfv!-*#i@=i4n26S|?{gd#57M-_51e#Q@K!HNkbBxG#AbTM@-l+x88! zgNDDj>gKjD)D9`!Ub_JM>)?vmEaOrR0}PBSMsl7#6#agEvSnyI!3t_qhaLNN6cO4e zGRNya$MlM9jp)6k!%4oPz0&39MSxQj6@iceX#g(4`lJL*)I5+mk-lm_+K8&}74j^Q zZo(TrY%Tmt3g>{v$3pR1EeA%;<cqC?pof*HpMq9s>PmaQh2aCrwi0C>^FxR&53Ykw?@KTq$mM660FqB+4m- z_+@KLkz&zfHac>3^>&0JJYIH*dJ2-@?jZUPU>tDB3`m0YwD!{>XU;$gRerNmJ;)IV zISz$mxkmq4Rp&PQ4ugF>=Pa3HYuHQKPQ^$eGeIFt!*aj!3qr7bZwHQca?oHTiY+hW zkIJ<$4(RRoBEh1?+FZ5aT%lHKea1*xj!T(c=@(k#^|f0q%!Hg=I*O#{lIn6Pvn{V| zI#CiR(&o(r9JS=tN~p3Dr7s4iLdi-6bSAXzPf@*F4}}+B67}#CD6Y}x^nckzd^pX0 zXA+LMV?GAORO)Q^5Nw=n929AE?G>&-W8vZlsj`oQcbg3lcr5r-mB$<-GuBV0-qwta z7HCZ(f(fWSXT=K#&kM@BsBxYZpRFE228gR{ceUW1B#F&iDElvr1k%$~Kr?N95n)YC zXGf30%;l;shI>3vV$ z-Wnw8E~gu&NYSU#GiRT(q)e@ZhkP<*3`D2{$6Xg@{@efQzlxf_n??A$kpkb$dgmi{ zw~QWg*2S1qH6)_;G0u;XHn!gE7GyGN7?-x|K4z~mim@09k!mw;?p~L^=RO$0OAU(I zhp3HsT$MiJd@;j(=L?~@rBSI%(rD;#{=hKMm}YIaae`tDu&@(8mY*_F^8b821}}?@ zv2HNLrqVO#Zor_PensArA~2UsuF218;3u|lifMPm>l;!$O(3R6Y%PGsbCmwZ(G7uc zHBW*{^i>DED@}nbgM_t18!h!+8RI6(F+Xdr_Jdl}Sk!7-q+Q#ua5$!nRgSWo&V79uiU2!ij5#&QAngF}_D@{6^Hx zk)$^yMECE&7D751{u%P>xRr`sRe1JzW;TbpXPSNVd>q=7##fA=?hFa$C)FL@?$<)Q z4>*w87zF4@k>H=Bf=523uYV~I=LPD2KoIG%6`St8bYFOxQXs)Otg{zYs8< z67q~?Q7YV~ck+{{3Pp7OWN{UQ8Zpuwh?y|WxIh=S+=rpwdq5NR0|-uo$;e0akyViL zHh9tJET1woMPv7bgE9*hl#~71FPh0MQUR)=K!0jAgTD4)=ROM^Jkv1mpB#&cZ(ZqHVYtSM`y&IbKF8IEBj88@BeoBNP zDT$Pp&uht$np<&OOEDJ9NZ#tp9ibu&?1I|)86Pv{ag zgDwN|zMXjrW7$|&EdqF@Mc7-m#NqWG>JC%k$&Fd;w~?2;F37XO9>lq_bzwp)V7ga+ zC@(r~A@Tx`Nvu)`udHbani(&^oIJ0Ge%K8Ns$D;MH-g}eMzTO4|?W_uh^u#lA&vxjGc8ubU&I*iIBqgT5B){Gk;B@%$JoiiNOo#Zw`GeK` zJ;9~)Oa$cuLnwKJim6}FVIw8l=wk_tdRe~63Ngd`QrHJHPa7;!8&3@G0&~>El+QgG zuAP(z)IM~t@>AGafQZyCh$NX|zB^Z$9d8P#V?{RJ6(*6aSHtoOmvSyM@GB2KvAv%9 zY04haEMJ-BsHqOi1Jz+_MUFX+_O2@A`a6;=tU|_|7u>$PRrUm1LbEw3Q56v6Objr} zN`t3yzpp~*xt^!E5^#Q_Xj6{B-7x(Lx?=Knjsy^)rZ!Sj?@r%Iw?7E!wCFKROKm4A?6u9-P?$$<*Q8!=L+$z%-r665rb7EJgN6 zk~`7I`-F|UoZ4Ew6mOXGR68(&hDCTp9;UR^G2>cNr7qb~X?$lYuGSHn|BA3@-pD`t zq8b~alNq8thk5E9P@aZgT4Su}XIXs2 zFjBDEj{wVKN7N|kR~>IDsB(UQM~6}#%Ga<2Q%d%fbx-82ar29n4c?B`O=0XcEl}9t z+MATnk&eT=VGtV4czx*ZvY@{;r|LhWDPXgipKge$A8`;EQJ0X+tn`r;Udie%`xYIT c4*J!~`XxpD^ZIMZ;GggRAy@qAgz=~SFE7uvxPU{=YfuvHb22Q8fttxCN18OQsn7rm_~lo?{#4NF~R?2k{>ViLr|#~fMV}C zZYd*{ruecY+?+Tpp3uN+`LgoEh}B*^tib^M+3(Mzq}+)?9AMvVNUM|ZFzIa1+e*@c zx|dm;D3a$~D8KVeDI&W{M&PI{w2zQBU6hWEQcEBsPZ<*|uN#Nj@e-GP7n=WUlC$~q z*xK!)5BMQ#TXn1|28h#7zH?8tZ{j04dGkoU?WE7`lzYZ^uB6Z=R&^UN)LT?;9>~#< z%O3+sN*$3V=y4Ql*kqqHFc+@p7(s7T2-p;h^pd%E_`%D0HrQ>t? zE1*k;eZwJvwA8cNxI%np>t<@Bjw#`!S?!6R%Yqtq?>|LZ;%6mr3nN@?GcaS(Y5Ezgc=mvti_<+CPzWIOL4MKAq?V`e zbEYLc$mu8YLc6aKdM!L)b~i{l_HY>Y8Lf9+8c==JZ%@I>uaS@27Iu*;qi>Y&&iMPSjer)BN(JJ9IV;bQmEVxIOL2wX{W*?7-Sf%hAb?Bj?B zg<7!a+r`hbK(HVfV6J3t!3~bQu@LY4dRiB@wvt8ldWMrNNjbikr-w<@&X6}Ouz?>O zcc#wSiZ)$s$fr=FQ{KC+R@+Q+?-kM&EeTJ(X8JiLxKN2&J^G_If)Bl;fyGZVugA~R z>%2VmMse&-YhiaYDp*|mijYr*5U6Ms;p{8eaw8aEqIHnws7=s^UwmceE z^^!nc+BHw0FtSKI8Qa&cA1hvR@uCO4@>2%ODxXdze^S`)QNk9VFQiPGQd>>06FiXx zYQIWGv~)Ot>*mpD;@cFqdNk@4RjdMM~V0MACZeN;dw9gsp+ zh1#1xO=inmPhOPyidv|L4|eYj-2-!ZV%4QZH7fR;(wc{2V82r)f%By%EEED0fr=j8DS-wAF-{Z5`4;2ST3K$xdn_*qnYdJn*ABBNe#F{ktMwH0vZ}qIT>YZn>Wwr57W%+x5HY&=)S5Gxa}xQ)QTu_$h1&o# z?QjXuGf3Ns!R}l@a16Xlit42L#$U&1#16i) zn}sl$kmm_VH{Lec`8j~q;|xDQq!nzpr$d6TLq+GqVvhMWqIWe??n5!|H13C@~&x*H5&L_<@<>tx>ZkrcL}5Z5kO+hoW^6B6Hwx1kVKJZS!=~yQk9tzE zFxZl(4C3#;cUH}+9NvIuX|_fi06fE zTQ4LqY(B`Ex`YAZAM~1`M!47OFqRqRH-&#w_&0_BjVWw-?Vk``NV3Z@Ofy{5fKw)C zanWO1;d>{4XxoH!)8=yTFZ%w7htYnGHBqBLm+QNqLU@|*U}FTOad*2wQVOe06A>x* zB2sXZ>=aIgRS_JclfpmQ{-^$DQ-zdJZe^JT>uYRh1btQofxuLRWiK0x zQAd^bkIdkQ{HEM*%KfI?Kcd{q4j>d$H7vdiu@;-n(~l7K2xA!Lypq~vha10Byukn; zy-=-sDqucJnq?GbQ*m7lsTn3?g>~%W`~3N z19tUGa;R?g+l|H4wN0dw5=?Z%>Tx{k#wIfr_-{~2s1v$4qO%&#XJ_7sVgP);|qA%m+995!> zncb5ZB2VHRV?udS5VC=GPK@!`OAPN%PM`pOMJ)3FgxlXCr{{yiDC0$V94N9VDjzso z$YqsT#vO*bRCa~}!d?(?BO(6l$$zQOJmm=;&(>UXB4L{bUVGpU{%_0uL2DeAV&)?EyX_yh{$3|T{b$qOe^em@%*B5o_;ex(yg6>5 z&qaNT5;oqaDR0@X@A;U&WTR|bqz_``J7`xhO)HB5MRU64x{J9WlPe_{Od68SWKG-8 z_*$5EMJDpa-!;OkrH9~q{Ub+8%U5PZiACjFwGdF!=MQbnH~aCm`!OOM#Is%OGp)N% zX9dU#VqUBC>w4IsjOxR&QK?w!EX5f(-}>!^II~~8SIn*$Tfk#~s^Gh{F(M~%%I9WDtW5%TvUHynfw zZJn;KJYxMU($uE?l3DHhn)}|lh~2LC_daM|l+$8e#gGn*Y5y?bVLHz}rq;0F2i6jS zteQH{ESBO|WvQ4y@q$~G0uqD2G80jI@>%$-+hdI1fA5PVT1R{}oA70_c|z@xacVt_ z7FXmEV|KbyW4G$!I#?~w?&g@5%lt6UJpRSWMVlfIA3OEq0g+zT>$`o@>53w}uRGWC z&unKo;nnr!4=Fh}p>GPdu`aAXojmCgX8p3TtSf^&`?n!o^cbm$$8H# zMmkZ`@`TDOfg+A+H}B*e!vyX+)-((ddmRdx$VczN6Sl)ZVwOR7)4#cSvkW<-L^7 zJ$QUigjRn^1^2k37>#Iu0vDf|Vg7!;9k0N9A2Oj8j**s0JRGo*ssA457k2YyPr;0U zZv^B|y2INd@5R#BQKvQ6HDT}dGXT)mdfiI(kysM1Sx!&}{f$IbzCi3eGdvs&h+K-U z_O96NcNPihkSyhNC=p7OvRJZyFa%djP+Ud0q<7;7sgYTjAM4w`%#l2L!i%JU=uKl3R@b+s^>0={ZtkU?=cS8J>K&pY)%n%AvuH~1u zfP}GS5Q!)!x#-h11Uz#BmcNj4>!Ed@brhcQ;qhT|gfM3R!Rk80!UdB#uTg=B!HzKp z5G!M&E}@{2md(XgIKld5FY^t&o%CY#<72p*yz9)RTC16EVq<07`lBd=+`{#mq;gwS zA}GhP2O1CEjf&|@+}X^Oppt!1w*Rt{lbAC<9Zf`_>(Qll#VQnstizoZHrFT_o@d7ix-OCddp`yX@2sab6E}?n#qB%WynOSNIX$(heW*%sXG%r?|zJP z08eE`#(U+&FMs&I{HS~2yTG83ot_8Uh*ZLtiQ(Ve>~dBbrPx0{3!=%kzRx<(h>;zA z&Y_GS(j`=3grrQYg?0qCPdP}Oj#q62AIE;+sqU_`7XXhD**qaN^ih>@^)oN$I-pg# zTb^Yy>&k{ZIOvzmU`dLry{kT1_uPYfGl8NWsgDyU8+=XE9jPAB6wh#MQdV})# z2ko!N{iTk1GH)+alxC;kMpTf=5{lgTk4)3yQ>Y%<_ABER%XE@t$x|!z-}w6IN0N)2 zQtBlGBe=TzCi2+Q=`$JXUx?MRX|A*^ZTOOglN8KWx23-E4Zc%6j}NJYpWy0?mvFr5 z8EH?!r#k<97lcRfN+SwBnYIvT%8^t1jlHCwLdVVpqcpa)t2&xE{2y8kR zo>H-fbYhlW#qtJ4XyZudqlY=R3~_Eh@xa{?MAgwLxulo7==3w@sCb4gwN>ptaw%Sx zEk)M#NaF%hVyB!{1ht6PUOyWNp{s7zt)AS??BQtq+Ryt6+tNkeC$U^?fGzyC;*9<{@7)wIi9mlEY$I@p_n^|4PpqQoo!gMotY0C}LQW$&^E{V|Ran-iwhd5H zNOK_z>@qda>)*$B783CB1P(Z(2w=>%O+voZoxi%9sDz+wCJOLis z80C_$mk3wU zhLf1`-`EuX(|-1+)Be@wlV9y23EX+ZzO(32#9dapZ0>85p?m(&7SHiuyWf=}1`ApT z%I+LZdmzW|B$^+D>1dBG58iA_f=8Zo#>5l5l%+Ww|eth+TVmw?oKA~I2 zK23jBl}#W4jO}6c09a@d*<)=?{MDA+i+JV&f)48rekGF^t{nf)#I4RVw6CC5`KqjmI`dQG?pdR*a( zH4Ll)83`qZq}gJi4GySW6`~-!&ZEGL-GfIgWCw+1gSP)B^|GKH78Fccn&`9%f;mM; zUA&v#H1@ePB9YxfTYtV|!srqltK98sL$+S6p2T#4=^i&xEK%L!w_QQ@YEk!Dx{1J2~ zs)i~$A+jkGQcKtJPg3#u!;vpp(NYj0b*cPi-u*ekm7;}V z!Z&n{06h3{H-<&I{)=)Y`NcAJY-l!c^6--1<-xm~)thURtKp z%DgL!(lQc!tQR}*d`c1~m56-pr=MyYras#kLp*OZDrgC9QR|aGi5`EKvev}EVhqc( z&^x9+GdB~WV@%_gH&s#|9B5z|P5%;^&$X!m>7G!%@`n~Zq3lyS-ve0%B8YOIqT!fZ z6uJ+;HL!1TTMyC;zrV%ZIx^6A%7uc-TRd<1D1d7gj6-39%(II{9gtQ=^CD6P=iLeWIJ+|U{nd*rAv$Oo&(pry9w~1(?zobJK ze@K~3C&2(5tM5_rh-V{#Nuug!FUNAT4+caH(kREc3!3`jNA1V72%YsqPyusf*+}4fCzp!0A z4oV&iQ0ruR)sIdZKJjX%7O1nRAl2@C7tXc1YY!uc$XJ_92K8!&dJ4@iaV`5!^SE`n z=un?(;KcPTzb`?UY@DKVSI%*>sb#-r3)u2A0gZe=B4*&Lb31*mJSO5t#)~ZAGDV9U zisBG7hUFu`dA$Zg6?>QSEIC~}O_-Qg`F=E3s|IV4aOFww9Y7$6P5Fz)O!xiujfHbc fli0wA$|;sy^nr>n(2s2Cr~EAC{j=9F{u=lnIbuX= literal 0 HcmV?d00001 diff --git a/test_fixtures/masp_proofs/9CCDEA31EDE3973806C1247BD7036C47707BFCE3CE7672A526ADEFA192F21577.bin b/test_fixtures/masp_proofs/9CCDEA31EDE3973806C1247BD7036C47707BFCE3CE7672A526ADEFA192F21577.bin new file mode 100644 index 0000000000000000000000000000000000000000..521a1e1431d43cf4468aa231669134a63901040c GIT binary patch literal 24494 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 S-{;=G|L6bw{nNKC8vg?)l}z6N literal 0 HcmV?d00001 diff --git a/test_fixtures/masp_proofs/9D2160E776A448BE5E380DEE784DEE977AB0FC421801231D05A1384ED6185D90.bin b/test_fixtures/masp_proofs/9D2160E776A448BE5E380DEE784DEE977AB0FC421801231D05A1384ED6185D90.bin new file mode 100644 index 0000000000000000000000000000000000000000..86bd6b2d1fed70023bc90eda112d4636fed638b2 GIT binary patch literal 7448 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 literal 0 HcmV?d00001 diff --git a/test_fixtures/masp_proofs/5A70C79871C7B5DF84C5CF3C4D86EF25580F3F5916126CAC201AD67B3D644691.bin b/test_fixtures/masp_proofs/AAF849D0C3C7352A4241AB8C9918ED96DFBF9A925BDE8E2A32DD3A3A160E74D9.bin similarity index 55% rename from test_fixtures/masp_proofs/5A70C79871C7B5DF84C5CF3C4D86EF25580F3F5916126CAC201AD67B3D644691.bin rename to test_fixtures/masp_proofs/AAF849D0C3C7352A4241AB8C9918ED96DFBF9A925BDE8E2A32DD3A3A160E74D9.bin index f1c8aa5c63490eff0eb4bc67a0feabd687ccc7dd..1ee637c62dcf660bc633030a9766d9e89de8f18c 100644 GIT binary patch delta 2142 zcmbPXHN&c&k%56h6o^@Xc={SekBZJb^&2PmM}5}klgdjI7M~#|w(a^(zd-+n`2YDcd}`>db@aPr1^) zGJmChaIW6>8>9~e(Dc@WH7{$o5S(`QciKb8NB?{l7^m_2FXA(@IBo8=mcih$rgtnv zD~4vUUiO_zQ{7*RTG%z_rUmYvlO*tLQ}@Y^o7Vro`JA2pAlDI}UIqqjpfo55K$wvC zCu=f#st{5H3ZqHvuA6T&`Y#t5xJ@={qCth9CCo=Ki-rxVO$DOo{P&7Dxc%o+J_B$&l zy0UTZseN{N&Uf|6hSa?pR^5#N^B&_sQMDCJIlxYY&Dm zj_tF#`tccGokxGVy?scvk?140Q@jg|)?a?K`MxmFepc?bH%|J-0TFWw_X)?W*gu(H zEWqJFw97vI59${iFO=KveVu4DCsK7%T|_kt&$DmEjfdl}9D6gb?EJR&nDjbjva)0#;Z&b#ib$QGN9M<{HIY+t(g+*m)s%bH~AxVyjNeN4tJ33V3o| zHPWY0_rh+Y$_Zze*ZddOSYLa)#@8{_GxhqPc8O)+%}jpvEJsd z65H``);(7$85X-0O$=K3cT1lHqZO0=)%*N;8F2+OB?^^n*a*l?Vdrq=VEzlI1e1hTGb1_~M2F9ozWv40& zuPeOdIezK=#m@Nj(2l|fT5}s>MOmg~@4gn2S5<%aq16^k5!p0jjn5KU=1zN#+*xxj zTqt{c$bO*=f9c7K?tkRI=eusocZNXy>Wku6Rz_%CoS-9{Qh2z0{S{mHnKRZj$==Ps zy1a_1+-KWYGZD$eoYVxnljRpxKR*jx#>L5J^nc;hs@&99*PaQPX>3k;kyR=v`?j~| zky8Ixy_UG-df%&&fiDknYX3;RbtLS!-c%0j*SDF3*#dI1X8fdCnxMGh7nucF(Q99OU@?)c0=RCCTjPuB6I0&l3JCJ@dS6eUN|7;Uhm8 z%rzSBp87IrgM8nXs1*6RT}Bf>GACANM~lqPTy#fqkFP~zfcVbzh@CS+o6@BixmNAr zb_o~eP+TWgXyg^Bo48U+bms==vsv403O?xmWalxuxb0@b1yy!XJpuwOli$iHbATEW GAOHaIwdb(_ delta 2142 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 PulX-d|LZgQKNkNBXCd5p literal 0 HcmV?d00001 diff --git a/test_fixtures/masp_proofs/F39ADA174E1FB9F45BFFB0C68625A3F8ED350401B95715A6AF9BDD8CD1C367BA.bin b/test_fixtures/masp_proofs/F39ADA174E1FB9F45BFFB0C68625A3F8ED350401B95715A6AF9BDD8CD1C367BA.bin deleted file mode 100644 index b04b1f1f04a611396d4fa9e38ba6cd92e118182c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7448 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 diff --git a/test_fixtures/masp_proofs/F534964AD0DF72DF15E18A0FE7629B8C97C137CD8F44F86196F2C42FE45201D9.bin b/test_fixtures/masp_proofs/F534964AD0DF72DF15E18A0FE7629B8C97C137CD8F44F86196F2C42FE45201D9.bin new file mode 100644 index 0000000000000000000000000000000000000000..8a1b586b166acf1aeff0572f7db482b75ce35193 GIT binary patch literal 7448 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 Date: Thu, 2 Nov 2023 13:36:04 +0100 Subject: [PATCH 27/47] [fix]: Linting --- apps/src/lib/node/ledger/ethereum_oracle/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs index 371ecf4a92..64a05b64ff 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs @@ -327,6 +327,7 @@ pub(crate) enum ProcessEventAction { impl ProcessEventAction { /// Check whether the action commands a new block to be processed. #[inline] + #[allow(dead_code)] pub fn process_new_block(&self) -> bool { matches!(self, Self::ProceedToNextBlock) } From 95ac1bb28d2cd5a705fd11e411f14d34f4a4cd28 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 2 Nov 2023 16:33:31 +0100 Subject: [PATCH 28/47] [fix]: Fixed signer decoding for ibc --- core/src/types/address.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/core/src/types/address.rs b/core/src/types/address.rs index c9a9396187..1bebe23f27 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -401,7 +401,23 @@ impl TryFrom for Address { type Error = DecodeError; fn try_from(signer: Signer) -> Result { - Address::decode(signer.as_ref()) + // The given address should be an address or payment address. When + // sending a token from a spending key, it has been already + // replaced with the MASP address. + Address::decode(signer.as_ref()).or( + match crate::types::masp::PaymentAddress::from_str(signer.as_ref()) + { + Ok(_) => Ok(masp()), + Err(_) => { + tracing::debug!( + "\n\n\n Invalid address for IBC transfer: {signer}\n\n\n" + ); + Err(DecodeError::InvalidInnerEncodingStr(format!( + "Invalid address for IBC transfer: {signer}" + ))) + }, + }, + ) } } From 96c2547766ccc331e3ad0297a27055f8e56ba37d Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 2 Nov 2023 17:05:29 +0100 Subject: [PATCH 29/47] [fix]: Formatting --- core/src/types/address.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/src/types/address.rs b/core/src/types/address.rs index 1bebe23f27..b3d251b290 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -409,13 +409,10 @@ impl TryFrom for Address { { Ok(_) => Ok(masp()), Err(_) => { - tracing::debug!( - "\n\n\n Invalid address for IBC transfer: {signer}\n\n\n" - ); Err(DecodeError::InvalidInnerEncodingStr(format!( "Invalid address for IBC transfer: {signer}" ))) - }, + } }, ) } From 29443a672337de9e899fd3db2107e47d92d8d1de Mon Sep 17 00:00:00 2001 From: satan Date: Fri, 3 Nov 2023 12:55:12 +0100 Subject: [PATCH 30/47] [fix]: Fixed benchmarks, e2e test ci scripts, masp conversion persistence issue, and formatting --- .github/workflows/build-and-test.yml | 3 +++ apps/src/lib/config/genesis.rs | 3 +-- apps/src/lib/node/ledger/shell/mod.rs | 2 +- benches/Cargo.toml | 2 +- core/src/ledger/storage/mod.rs | 17 ++++++++++++++--- core/src/types/address.rs | 8 +++----- 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6fff3e2bfe..fe80c97694 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -580,6 +580,9 @@ jobs: path: | /tmp/.*/logs/ /tmp/.*/e2e-test.*/setup/validator-*/.namada/logs/*.log + /tmp/.*/logs/* + /tmp/.*/setup/validator-* + /tmp/.*/setup/validator-*/* retention-days: 5 - name: Print sccache stats if: always() diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 659617d99a..ac3d65f887 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -286,10 +286,9 @@ pub fn make_dev_genesis(num_validators: u64) -> Finalized { .expect("Current directory should exist") .canonicalize() .expect("Current directory should exist"); - while current_path.file_name().unwrap() != "apps" { + while current_path.file_name().unwrap() != "namada" { current_path.pop(); } - current_path.pop(); let chain_dir = current_path.join("genesis").join("localnet"); let templates = templates::load_and_validate(&chain_dir) .expect("Missing genesis files"); diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 61d909d408..023ab27fc9 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -450,7 +450,7 @@ where std::fs::create_dir(&base_dir) .expect("Creating directory for Namada should not fail"); } - let native_token = if !cfg!(test) { + let native_token = if !cfg!(test) && !cfg!(feature = "benches") { let chain_dir = base_dir.join(chain_id.as_str()); let genesis = genesis::chain::Finalized::read_toml_files(&chain_dir) diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 0e72e98a4b..0aa1688a79 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -41,7 +41,7 @@ path = "host_env.rs" [dev-dependencies] namada = { path = "../shared", features = ["testing"] } -namada_apps = { path = "../apps", features = ["testing"] } +namada_apps = { path = "../apps", features = ["benches"] } borsh.workspace = true borsh-ext.workspace = true criterion = { version = "0.5", features = ["html_reports"] } diff --git a/core/src/ledger/storage/mod.rs b/core/src/ledger/storage/mod.rs index 4f506a61be..b484246228 100644 --- a/core/src/ledger/storage/mod.rs +++ b/core/src/ledger/storage/mod.rs @@ -484,7 +484,7 @@ where // Rebuild Merkle tree self.block.tree = MerkleTree::new(merkle_tree_stores) .or_else(|_| self.get_merkle_tree(height))?; - if self.last_epoch.0 > 0 { + if self.block.height.0 > 0 { // The derived conversions will be placed in MASP address space let masp_addr = masp(); let key_prefix: Key = masp_addr.to_db_key().into(); @@ -493,13 +493,24 @@ where let state_key = key_prefix .push(&(token::CONVERSION_KEY_PREFIX.to_owned())) .map_err(Error::KeyError)?; - self.conversion_state = types::decode( + let ConversionState { + normed_inflation, + tree, + tokens, + assets, + } = types::decode( self.read(&state_key) .expect("unable to read conversion state") .0 .expect("unable to find conversion state"), ) - .expect("unable to decode conversion state") + .expect("unable to decode conversion state"); + self.conversion_state.tokens = tokens; + if self.last_epoch.0 > 0 { + self.conversion_state.normed_inflation = normed_inflation; + self.conversion_state.tree = tree; + self.conversion_state.assets = assets; + } } #[cfg(feature = "ferveo-tpke")] { diff --git a/core/src/types/address.rs b/core/src/types/address.rs index b3d251b290..608e723b4d 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -408,11 +408,9 @@ impl TryFrom for Address { match crate::types::masp::PaymentAddress::from_str(signer.as_ref()) { Ok(_) => Ok(masp()), - Err(_) => { - Err(DecodeError::InvalidInnerEncodingStr(format!( - "Invalid address for IBC transfer: {signer}" - ))) - } + Err(_) => Err(DecodeError::InvalidInnerEncodingStr(format!( + "Invalid address for IBC transfer: {signer}" + ))), }, ) } From 4a306045282de2034bb8411b3c7f2cdd27e08ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sat, 4 Nov 2023 14:34:31 +0100 Subject: [PATCH 31/47] remove the rest of the "dev" feature usages --- apps/src/lib/node/ledger/shell/mod.rs | 3 - apps/src/lib/node/ledger/tendermint_node.rs | 4 +- apps/src/lib/wasm_loader/mod.rs | 67 --------------------- 3 files changed, 1 insertion(+), 73 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 023ab27fc9..35b529c76a 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -85,9 +85,6 @@ use crate::facade::tower_abci::{request, response}; use crate::node::ledger::shims::abcipp_shim_types::shim; use crate::node::ledger::shims::abcipp_shim_types::shim::response::TxResult; use crate::node::ledger::{storage, tendermint_node}; -#[cfg(feature = "dev")] -use crate::wallet; -#[allow(unused_imports)] use crate::wallet::{ValidatorData, ValidatorKeys}; fn key_to_tendermint( diff --git a/apps/src/lib/node/ledger/tendermint_node.rs b/apps/src/lib/node/ledger/tendermint_node.rs index ace037cd95..25a2b9e114 100644 --- a/apps/src/lib/node/ledger/tendermint_node.rs +++ b/apps/src/lib/node/ledger/tendermint_node.rs @@ -364,9 +364,7 @@ async fn update_tendermint_config( Moniker::from_str(&format!("{}-{}", config.moniker, namada_version())) .expect("Invalid moniker"); - // In "dev", only produce blocks when there are txs or when the AppHash - // changes - config.consensus.create_empty_blocks = true; // !cfg!(feature = "dev"); + config.consensus.create_empty_blocks = true; // We set this to true as we don't want any invalid tx be re-applied. This // also implies that it's not possible for an invalid tx to become valid diff --git a/apps/src/lib/wasm_loader/mod.rs b/apps/src/lib/wasm_loader/mod.rs index 2d1aa95175..fe497b1505 100644 --- a/apps/src/lib/wasm_loader/mod.rs +++ b/apps/src/lib/wasm_loader/mod.rs @@ -111,31 +111,6 @@ fn wasm_url_prefix(wasm_name: &str) -> String { /// Download all the pre-built wasms, or if they're already downloaded, verify /// their checksums. pub async fn pre_fetch_wasm(wasm_directory: impl AsRef) { - #[cfg(feature = "dev")] - { - let checksums_path = wasm_directory - .as_ref() - .join(crate::config::DEFAULT_WASM_CHECKSUMS_FILE); - // If the checksums file doesn't exists ... - if tokio::fs::canonicalize(&checksums_path).await.is_err() { - tokio::fs::create_dir_all(&wasm_directory).await.unwrap(); - // ... try to copy checksums from the Namada WASM root dir - if tokio::fs::copy( - std::env::current_dir() - .unwrap() - .join(crate::config::DEFAULT_WASM_DIR) - .join(crate::config::DEFAULT_WASM_CHECKSUMS_FILE), - &checksums_path, - ) - .await - .is_ok() - { - tracing::info!("WASM checksums copied from WASM root dir."); - return; - } - } - } - // load json with wasm hashes let checksums = Checksums::read_checksums_async(&wasm_directory).await; @@ -166,26 +141,6 @@ pub async fn pre_fetch_wasm(wasm_directory: impl AsRef) { &derived_name, &full_name ); - #[cfg(feature = "dev")] - { - // try to copy built file from the Namada WASM root dir - if tokio::fs::copy( - std::env::current_dir() - .unwrap() - .join(crate::config::DEFAULT_WASM_DIR) - .join(&full_name), - wasm_directory.join(&full_name), - ) - .await - .is_ok() - { - tracing::info!( - "File {} copied from WASM root dir.", - full_name - ); - return; - } - } let url = wasm_url_prefix(&full_name); match download_wasm(url).await { @@ -209,28 +164,6 @@ pub async fn pre_fetch_wasm(wasm_directory: impl AsRef) { // if the doesn't file exist, download it. Err(err) => match err.kind() { std::io::ErrorKind::NotFound => { - #[cfg(feature = "dev")] - { - // try to copy built file from the Namada WASM root - // dir - if tokio::fs::copy( - std::env::current_dir() - .unwrap() - .join(crate::config::DEFAULT_WASM_DIR) - .join(&full_name), - wasm_directory.join(&full_name), - ) - .await - .is_ok() - { - tracing::info!( - "File {} copied from WASM root dir.", - full_name - ); - return; - } - } - let url = wasm_url_prefix(&full_name); match download_wasm(url).await { Ok(bytes) => { From 17af8a32943474d8a522bfba0bf300ff5c86c415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sat, 4 Nov 2023 14:49:47 +0100 Subject: [PATCH 32/47] apps/genesis: change root dir detection --- apps/src/lib/config/genesis.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index ac3d65f887..42467ef282 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -286,7 +286,8 @@ pub fn make_dev_genesis(num_validators: u64) -> Finalized { .expect("Current directory should exist") .canonicalize() .expect("Current directory should exist"); - while current_path.file_name().unwrap() != "namada" { + // Find the project root dir + while !current_path.join("rust-toolchain.toml").exists() { current_path.pop(); } let chain_dir = current_path.join("genesis").join("localnet"); From fb350be95c0e6fe8694607d9dc6f8f82997b0ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sat, 4 Nov 2023 15:08:35 +0100 Subject: [PATCH 33/47] bench: fix voting epochs to correspond to PoS params --- apps/src/lib/bench_utils.rs | 7 ++++--- benches/native_vps.rs | 19 +++++++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/src/lib/bench_utils.rs b/apps/src/lib/bench_utils.rs index ef2791ac7a..b98cb7d30e 100644 --- a/apps/src/lib/bench_utils.rs +++ b/apps/src/lib/bench_utils.rs @@ -209,7 +209,8 @@ impl Default for BenchShell { // Initialize governance proposal let content_section = Section::ExtraData(Code::new(vec![])); - let voting_start_epoch = Epoch(25); + let voting_start_epoch = + Epoch(2 + params.pipeline_len + params.unbonding_len); let signed_tx = generate_tx( TX_INIT_PROPOSAL_WASM, InitProposalData { @@ -218,8 +219,8 @@ impl Default for BenchShell { author: defaults::albert_address(), r#type: ProposalType::Default(None), voting_start_epoch, - voting_end_epoch: 28.into(), - grace_epoch: 34.into(), + voting_end_epoch: voting_start_epoch + 3_u64, + grace_epoch: voting_start_epoch + 9_u64, }, None, Some(vec![content_section]), diff --git a/benches/native_vps.rs b/benches/native_vps.rs index 6a5d8e52ae..eecfbd6a32 100644 --- a/benches/native_vps.rs +++ b/benches/native_vps.rs @@ -25,6 +25,7 @@ use namada::ledger::native_vp::ibc::Ibc; use namada::ledger::native_vp::multitoken::MultitokenVp; use namada::ledger::native_vp::{Ctx, NativeVp}; use namada::ledger::storage_api::StorageRead; +use namada::proof_of_stake; use namada::proto::{Code, Section}; use namada::types::address::InternalAddress; use namada::types::storage::{Epoch, TxIndex}; @@ -88,7 +89,10 @@ fn governance(c: &mut Criterion) { } "minimal_proposal" => { let content_section = Section::ExtraData(Code::new(vec![])); - let voting_start_epoch = Epoch(25); + let params = + proof_of_stake::read_pos_params(&shell.wl_storage).unwrap(); + let voting_start_epoch = + Epoch(2 + params.pipeline_len + params.unbonding_len); // Must start after current epoch debug_assert_eq!( shell.wl_storage.get_block_epoch().unwrap().next(), @@ -102,8 +106,8 @@ fn governance(c: &mut Criterion) { author: defaults::albert_address(), r#type: ProposalType::Default(None), voting_start_epoch, - voting_end_epoch: 28.into(), - grace_epoch: 34.into(), + voting_end_epoch: voting_start_epoch + 3_u64, + grace_epoch: voting_start_epoch + 9_u64, }, None, Some(vec![content_section]), @@ -135,7 +139,10 @@ fn governance(c: &mut Criterion) { let wasm_code_section = Section::ExtraData(Code::new(vec![0; max_code_size as _])); - let voting_start_epoch = Epoch(25); + let params = + proof_of_stake::read_pos_params(&shell.wl_storage).unwrap(); + let voting_start_epoch = + Epoch(2 + params.pipeline_len + params.unbonding_len); // Must start after current epoch debug_assert_eq!( shell.wl_storage.get_block_epoch().unwrap().next(), @@ -151,8 +158,8 @@ fn governance(c: &mut Criterion) { wasm_code_section.get_hash(), )), voting_start_epoch, - voting_end_epoch: 28.into(), - grace_epoch: 34.into(), + voting_end_epoch: voting_start_epoch + 3_u64, + grace_epoch: voting_start_epoch + 9_u64, }, None, Some(vec![content_section, wasm_code_section]), From e2edd62077374c82c59c53c0407b0b4a098623fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sat, 4 Nov 2023 16:05:24 +0100 Subject: [PATCH 34/47] genesis: use the default addresses for tests and benches --- apps/src/lib/config/genesis.rs | 14 ++++++++++++++ apps/src/lib/wallet/defaults.rs | 8 ++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 42467ef282..509ef0bb43 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -317,6 +317,8 @@ pub fn make_dev_genesis(num_validators: u64) -> Finalized { if let Some(vals) = genesis.transactions.validator_account.as_mut() { vals[0].address = defaults::validator_address(); } + + // Use the default address for matching established accounts let default_addresses: HashMap = defaults::addresses().into_iter().collect(); if let Some(accs) = genesis.transactions.established_account.as_mut() { @@ -326,6 +328,18 @@ pub fn make_dev_genesis(num_validators: u64) -> Finalized { } } } + + // Use the default token address for matching tokens + let default_tokens: HashMap = defaults::tokens() + .into_iter() + .map(|(address, alias)| (Alias::from(alias), address)) + .collect(); + for (alias, token) in genesis.tokens.token.iter_mut() { + if let Some(addr) = default_tokens.get(alias) { + token.address = addr.clone(); + } + } + // remove Albert's bond since it messes up existing unit test math if let Some(bonds) = genesis.transactions.bond.as_mut() { bonds.retain(|bond| { diff --git a/apps/src/lib/wallet/defaults.rs b/apps/src/lib/wallet/defaults.rs index cbb027b8f8..57894d002e 100644 --- a/apps/src/lib/wallet/defaults.rs +++ b/apps/src/lib/wallet/defaults.rs @@ -4,8 +4,8 @@ pub use dev::{ addresses, albert_address, albert_keypair, bertha_address, bertha_keypair, christel_address, christel_keypair, daewon_address, daewon_keypair, - ester_address, ester_keypair, keys, validator_address, validator_keypair, - validator_keys, + ester_address, ester_keypair, keys, tokens, validator_address, + validator_keypair, validator_keys, }; #[cfg(any(test, feature = "testing", feature = "benches"))] @@ -67,8 +67,8 @@ mod dev { ] } - /// Deprecated function, soon to be deleted. Generates default tokens - fn tokens() -> HashMap { + /// The default tokens with their aliases. + pub fn tokens() -> HashMap { vec![ (nam(), "NAM"), (btc(), "BTC"), From df47a3a8fe7d00e8c060a630fd11531043a317ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sat, 4 Nov 2023 16:05:49 +0100 Subject: [PATCH 35/47] make: skip pos_state_machine_test in unit tests --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f535c04cff..b7e1f54d3f 100644 --- a/Makefile +++ b/Makefile @@ -176,7 +176,7 @@ test-unit: $(cargo) +$(nightly) test \ $(TEST_FILTER) \ $(jobs) \ - -- --skip e2e --skip integration \ + -- --skip e2e --skip integration --skip pos_state_machine_test \ -Z unstable-options --report-time test-unit-mainnet: @@ -191,7 +191,7 @@ test-unit-debug: $(debug-cargo) +$(nightly) test \ $(jobs) \ $(TEST_FILTER) \ - -- --skip e2e --skip integration \ + -- --skip e2e --skip integration --skip pos_state_machine_test \ --nocapture \ -Z unstable-options --report-time From 718312a1ae42d53b97cc750f9d6048257f9e067d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sat, 4 Nov 2023 18:19:59 +0100 Subject: [PATCH 36/47] bench: fix txs benches by writing a chain genesis config to FS --- apps/src/lib/bench_utils.rs | 16 ++++++++-- apps/src/lib/cli/context.rs | 32 +++----------------- apps/src/lib/config/genesis.rs | 13 +++++++- apps/src/lib/config/genesis/templates.rs | 4 +-- apps/src/lib/config/global.rs | 7 ++--- apps/src/lib/node/ledger/shell/init_chain.rs | 5 ++- apps/src/lib/node/ledger/shell/mod.rs | 3 +- 7 files changed, 40 insertions(+), 40 deletions(-) diff --git a/apps/src/lib/bench_utils.rs b/apps/src/lib/bench_utils.rs index b98cb7d30e..4f12c5b6b1 100644 --- a/apps/src/lib/bench_utils.rs +++ b/apps/src/lib/bench_utils.rs @@ -86,6 +86,7 @@ use tempfile::TempDir; use crate::cli::context::FromContext; use crate::cli::Context; +use crate::config::global::GlobalConfig; use crate::config::TendermintMode; use crate::facade::tendermint_proto::abci::RequestInitChain; use crate::facade::tendermint_proto::google::protobuf::Timestamp; @@ -682,13 +683,24 @@ impl Client for BenchShell { impl Default for BenchShieldedCtx { fn default() -> Self { let mut shell = BenchShell::default(); + let base_dir = shell.tempdir.as_ref().canonicalize().unwrap(); + + // Create a global config and an empty wallet in the chain dir - this is + // needed in `Context::new` + let config = GlobalConfig::new(shell.inner.chain_id.clone()); + config.write(&base_dir).unwrap(); + let wallet = crate::wallet::CliWalletUtils::new( + base_dir.join(shell.inner.chain_id.as_str()), + ); + wallet.save().unwrap(); let ctx = Context::new::(crate::cli::args::Global { - chain_id: None, - base_dir: shell.tempdir.as_ref().canonicalize().unwrap(), + chain_id: Some(shell.inner.chain_id.clone()), + base_dir, wasm_dir: Some(WASM_DIR.into()), }) .unwrap(); + let mut chain_ctx = ctx.take_chain_or_exit(); // Generate spending key for Albert and Bertha diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index 3b8d942040..be0c2e3c4d 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -8,7 +8,6 @@ use std::str::FromStr; use color_eyre::eyre::Result; use namada::ledger::ibc::storage::ibc_token; use namada::types::address::{Address, InternalAddress}; -use namada::types::chain::ChainId; use namada::types::ethereum_events::EthAddress; use namada::types::ibc::is_ibc_denom; use namada::types::io::Io; @@ -22,12 +21,10 @@ use namada_sdk::{Namada, NamadaImpl}; use super::args; use crate::cli::utils; use crate::config::global::GlobalConfig; -use crate::config::{self, genesis, Config}; +use crate::config::{genesis, Config}; use crate::wallet::CliWalletUtils; use crate::{wallet, wasm_loader}; -/// Env. var to set chain ID -const ENV_VAR_CHAIN_ID: &str = "NAMADA_CHAIN_ID"; /// Env. var to set wasm directory pub const ENV_VAR_WASM_DIR: &str = "NAMADA_WASM_DIR"; @@ -251,34 +248,13 @@ impl ChainContext { } /// Load global config from expected path in the `base_dir` or try to generate a -/// new one if it doesn't exist. +/// new one without a chain if it doesn't exist. pub fn read_or_try_new_global_config( global_args: &args::Global, ) -> GlobalConfig { GlobalConfig::read(&global_args.base_dir).unwrap_or_else(|err| { - if let config::global::Error::FileNotFound(_) = err { - let chain_id = global_args.chain_id.clone().or_else(|| { - env::var(ENV_VAR_CHAIN_ID).ok().map(|chain_id| { - ChainId::from_str(&chain_id).unwrap_or_else(|err| { - eprintln!("Invalid chain ID: {}", err); - super::safe_exit(1) - }) - }) - }); - - // If not specified, use the default - let chain_id = chain_id.unwrap_or_default(); - - let config = GlobalConfig::new(chain_id); - config.write(&global_args.base_dir).unwrap_or_else(|err| { - tracing::error!("Error writing global config file: {}", err); - super::safe_exit(1) - }); - config - } else { - eprintln!("Error reading global config: {}", err); - super::safe_exit(1) - } + eprintln!("Error reading global config: {}", err); + super::safe_exit(1) }) } diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 509ef0bb43..afd726673c 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -266,7 +266,10 @@ impl From for HexKeyError { /// This includes adding the Ethereum bridge parameters and /// adding a specified number of validators. #[cfg(all(any(test, feature = "benches"), not(feature = "integration")))] -pub fn make_dev_genesis(num_validators: u64) -> Finalized { +pub fn make_dev_genesis( + num_validators: u64, + target_chain_dir: std::path::PathBuf, +) -> Finalized { use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::time::Duration; @@ -448,6 +451,14 @@ pub fn make_dev_genesis(num_validators: u64) -> Finalized { } } + // Write out the TOML files for benches + #[cfg(feature = "benches")] + genesis + .write_toml_files(&target_chain_dir) + .expect("Must be able to write the finalized genesis"); + #[cfg(not(feature = "benches"))] + let _ = target_chain_dir; // avoid unused warn + genesis } diff --git a/apps/src/lib/config/genesis/templates.rs b/apps/src/lib/config/genesis/templates.rs index 0296907a46..d4012163e8 100644 --- a/apps/src/lib/config/genesis/templates.rs +++ b/apps/src/lib/config/genesis/templates.rs @@ -470,11 +470,11 @@ pub struct EthBridgeParams { /// Minimum number of confirmations needed to trust an Ethereum branch. /// This must be at least one. pub min_confirmations: MinimumConfirmations, + /// List of ERC20 token types whitelisted at genesis time. + pub erc20_whitelist: Vec, /// The addresses of the Ethereum contracts that need to be directly known /// by validators. pub contracts: Contracts, - /// List of ERC20 token types whitelisted at genesis time. - pub erc20_whitelist: Vec, } impl TokenBalances { diff --git a/apps/src/lib/config/global.rs b/apps/src/lib/config/global.rs index 6c9bae0e4a..1dc1380635 100644 --- a/apps/src/lib/config/global.rs +++ b/apps/src/lib/config/global.rs @@ -14,8 +14,6 @@ pub const FILENAME: &str = "global-config.toml"; pub enum Error { #[error("Error while reading config: {0}")] ReadError(config::ConfigError), - #[error("Error while reading config: {0}")] - FileNotFound(String), #[error("Error while deserializing config: {0}")] DeserializationError(config::ConfigError), #[error("Error while writing config: {0}")] @@ -40,7 +38,8 @@ impl GlobalConfig { } } - /// Try to read the global config from a file. + /// Try to read the global config from a file. Returns a config without + /// a `default_chain_id` if none exists. pub fn read(base_dir: impl AsRef) -> Result { let file_path = Self::file_path(base_dir.as_ref()); let file_name = file_path.to_str().expect("Expected UTF-8 file path"); @@ -49,7 +48,7 @@ impl GlobalConfig { config .merge(config::File::with_name(file_name)) .map_err(Error::ReadError)?; - }; + } config.try_into().map_err(Error::DeserializationError) } diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 2038ff30fb..228538108b 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -71,7 +71,10 @@ where any(test, feature = "benches"), not(feature = "integration") ))] - let genesis = genesis::make_dev_genesis(_num_validators); + let genesis = { + let chain_dir = self.base_dir.join(chain_id); + genesis::make_dev_genesis(_num_validators, chain_dir) + }; #[cfg(all( any(test, feature = "benches"), not(feature = "integration") diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 35b529c76a..c5fad75ea1 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -369,8 +369,7 @@ where H: StorageHasher + Sync + 'static, { /// The id of the current chain - #[allow(dead_code)] - chain_id: ChainId, + pub chain_id: ChainId, /// The persistent storage with write log pub wl_storage: WlStorage, /// Byzantine validators given from ABCI++ `prepare_proposal` are stored in From e8a08bd11660aff512a98727d1eda2d877895d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sat, 4 Nov 2023 18:21:29 +0100 Subject: [PATCH 37/47] rename `HexString::to_bytes` to `parse` and remove other unused methods --- apps/src/lib/config/genesis.rs | 20 +------------------- apps/src/lib/node/ledger/shell/init_chain.rs | 5 +---- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index afd726673c..21fe5cc8cd 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -208,28 +208,10 @@ pub struct Parameters { pub struct HexString(pub String); impl HexString { - pub fn to_bytes(&self) -> Result, HexKeyError> { + pub fn parse(&self) -> Result, HexKeyError> { let bytes = HEXLOWER.decode(self.0.as_ref())?; Ok(bytes) } - - pub fn to_sha256_bytes(&self) -> Result<[u8; 32], HexKeyError> { - let bytes = HEXLOWER.decode(self.0.as_ref())?; - let slice = bytes.as_slice(); - let array: [u8; 32] = slice.try_into()?; - Ok(array) - } - - pub fn to_public_key(&self) -> Result { - let key = common::PublicKey::from_str(&self.0) - .map_err(HexKeyError::InvalidPublicKey)?; - Ok(key) - } - - pub fn to_dkg_public_key(&self) -> Result { - let key = DkgPublicKey::from_str(&self.0)?; - Ok(key) - } } #[derive(thiserror::Error, Debug)] diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 228538108b..57ddde6b26 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -391,10 +391,7 @@ where ); for (key, value) in storage { self.wl_storage - .write_bytes( - &sub_key.join(key), - value.to_bytes().unwrap(), - ) + .write_bytes(&sub_key.join(key), value.parse().unwrap()) .unwrap(); } } From 3769849b86dbe56ceeae110ed29e000956e0b2c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sun, 5 Nov 2023 10:54:27 +0100 Subject: [PATCH 38/47] test+bench: use `defaults::validator_keypair` for genesis validator-0 --- apps/src/lib/config/genesis.rs | 16 +++++++++++++++- apps/src/lib/config/genesis/transactions.rs | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 21fe5cc8cd..19da319c8f 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -265,6 +265,7 @@ pub fn make_dev_genesis( use namada_sdk::wallet::alias::Alias; use crate::config::genesis::chain::finalize; + use crate::config::genesis::transactions::UnsignedValidatorAccountTx; use crate::wallet::defaults; let mut current_path = std::env::current_dir() @@ -300,7 +301,20 @@ pub fn make_dev_genesis( }); if let Some(vals) = genesis.transactions.validator_account.as_mut() { - vals[0].address = defaults::validator_address(); + let tx = vals.get_mut(0).unwrap(); + // Use the default validator address + tx.address = defaults::validator_address(); + + // Use the default validator key - have to add it with a sig + tx.address = defaults::validator_address(); + let sk = defaults::validator_keypair(); + let pk = StringEncoded::new(sk.to_public()); + let unsigned_tx = UnsignedValidatorAccountTx::from(&tx.tx); + let sig = transactions::sign_tx(&unsigned_tx, &sk); + tx.tx.account_key = transactions::SignedPk { + pk, + authorization: sig, + }; } // Use the default address for matching established accounts diff --git a/apps/src/lib/config/genesis/transactions.rs b/apps/src/lib/config/genesis/transactions.rs index 8595db5f18..21db15f43a 100644 --- a/apps/src/lib/config/genesis/transactions.rs +++ b/apps/src/lib/config/genesis/transactions.rs @@ -396,7 +396,7 @@ pub fn sign_delegation_bond_tx( unsigned_tx.sign(&source_key) } -fn sign_tx( +pub fn sign_tx( tx_data: &T, keypair: &common::SecretKey, ) -> StringEncoded { From 1a4eb1ca3922d5f9af9acb7971ebc9e31c827432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sun, 5 Nov 2023 11:36:37 +0100 Subject: [PATCH 39/47] genesis: fix import warn --- apps/src/lib/config/genesis.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 19da319c8f..a518686a0d 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -7,7 +7,6 @@ pub mod transactions; use std::array::TryFromSliceError; use std::collections::{BTreeMap, HashMap}; -use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use data_encoding::HEXLOWER; @@ -253,6 +252,7 @@ pub fn make_dev_genesis( target_chain_dir: std::path::PathBuf, ) -> Finalized { use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::str::FromStr; use std::time::Duration; use namada::core::types::string_encoding::StringEncoded; From 8f4e4bcd02f3de6af46c64bf9fcdbd9a4f8ffb96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sun, 5 Nov 2023 12:27:39 +0100 Subject: [PATCH 40/47] allow to join-network with archive in a dir --- apps/src/lib/client/utils.rs | 46 +++++++++++++++++++++++++----------- tests/src/e2e/setup.rs | 13 ++++------ 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index cb17d4bc62..a38b8714ca 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -33,6 +33,7 @@ use crate::wasm_loader; pub const NET_ACCOUNTS_DIR: &str = "setup"; pub const NET_OTHER_ACCOUNTS_DIR: &str = "other"; +pub const ENV_VAR_NETWORK_CONFIGS_DIR: &str = "NAMADA_NETWORK_CONFIGS_DIR"; /// Github URL prefix of released Namada network configs pub const ENV_VAR_NETWORK_CONFIGS_SERVER: &str = "NAMADA_NETWORK_CONFIGS_SERVER"; @@ -125,24 +126,37 @@ pub async fn join_network( }); let release_filename = format!("{}.tar.gz", chain_id); - let release_url = format!( - "{}/{}", - network_configs_url_prefix(&chain_id), - release_filename - ); + let net_config = if let Some(configs_dir) = network_configs_dir() { + fs::read(PathBuf::from(&configs_dir).join(release_filename)) + .await + .unwrap_or_else(|err| { + panic!( + "Network config not found or couldn't be read from dir \ + \"{configs_dir}\" set by an env var \ + {ENV_VAR_NETWORK_CONFIGS_DIR}. Error: {err}." + ) + }) + } else { + let release_url = format!( + "{}/{}", + network_configs_url_prefix(&chain_id), + release_filename + ); - // Read or download the release archive - println!("Downloading config release from {} ...", release_url); - let release = match download_file(release_url).await { - Ok(contents) => contents, - Err(error) => { - eprintln!("Error downloading release: {}", error); - safe_exit(1); - } + // Read or download the release archive + println!("Downloading config release from {} ...", release_url); + let release: Bytes = match download_file(release_url).await { + Ok(contents) => contents, + Err(error) => { + eprintln!("Error downloading release: {}", error); + safe_exit(1); + } + }; + release.to_vec() }; // Decode and unpack the archive - let decoder = GzDecoder::new(&release[..]); + let decoder = GzDecoder::new(&net_config[..]); let mut archive = tar::Archive::new(decoder); // If the base-dir is non-default, unpack the archive into a temp dir inside @@ -706,6 +720,10 @@ fn network_configs_url_prefix(chain_id: &ChainId) -> String { }) } +fn network_configs_dir() -> Option { + std::env::var(ENV_VAR_NETWORK_CONFIGS_DIR).ok() +} + /// Write the node key into tendermint config dir. pub fn write_tendermint_node_key( tm_home_dir: &Path, diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index 233f4be1b2..5e6fb9787f 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -384,16 +384,11 @@ pub fn network( drop(init_network); - // Host the network archive to make it available for `join-network` commands - // TODO: allow to unpack from a file instead of having to host it - let network_archive_server = file_serve::Server::new(&archive_dir); - let network_archive_addr = network_archive_server.addr().to_owned(); - std::thread::spawn(move || { - network_archive_server.serve().unwrap(); - }); + // Set the network archive dir to make it available for `join-network` + // commands std::env::set_var( - namada_apps::client::utils::ENV_VAR_NETWORK_CONFIGS_SERVER, - format!("http://{network_archive_addr}"), + namada_apps::client::utils::ENV_VAR_NETWORK_CONFIGS_DIR, + archive_dir, ); let genesis_new = chain::Finalized::read_toml_files( From f2a557ce42cf9f4f5322b911b55d3445a29267b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sun, 5 Nov 2023 12:39:26 +0100 Subject: [PATCH 41/47] deps: remove now unused file-serve --- Cargo.lock | 36 ------------------------------------ Cargo.toml | 1 - tests/Cargo.toml | 1 - 3 files changed, 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f5ac23b68..baff6188a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,12 +278,6 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" -[[package]] -name = "ascii" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" - [[package]] name = "assert_cmd" version = "1.0.8" @@ -1052,12 +1046,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "chunked_transfer" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a" - [[package]] name = "ciborium" version = "0.2.1" @@ -2459,17 +2447,6 @@ dependencies = [ "subtle 2.4.1", ] -[[package]] -name = "file-serve" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "547ebf393d987692a02b5d2be1c0b398b16a5b185c23a047c1d3fc3050d6d803" -dependencies = [ - "log", - "mime_guess", - "tiny_http", -] - [[package]] name = "filetime" version = "0.2.21" @@ -4314,7 +4291,6 @@ dependencies = [ "escargot", "expectrl", "eyre", - "file-serve", "fs_extra", "hyper", "itertools 0.10.5", @@ -6839,18 +6815,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "tiny_http" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" -dependencies = [ - "ascii", - "chunked_transfer", - "httpdate", - "log", -] - [[package]] name = "tinytemplate" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index 9d1885001e..7e1f114225 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,6 @@ eyre = "0.6.5" fd-lock = "3.0.12" ferveo = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} ferveo-common = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} -file-serve = "0.2.0" flate2 = "1.0.22" fs_extra = "1.2.0" futures = "0.3" diff --git a/tests/Cargo.toml b/tests/Cargo.toml index f37cc681b7..ca4cfabb1f 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -68,7 +68,6 @@ data-encoding.workspace = true escargot = {workspace = true} # , features = ["print"]} expectrl.workspace = true eyre.workspace = true -file-serve.workspace = true fs_extra.workspace = true itertools.workspace = true once_cell.workspace = true From 7f6e268ba44b9f382d39fc51e397213c86974613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sun, 5 Nov 2023 14:04:56 +0100 Subject: [PATCH 42/47] fix integration tests shell initialization --- apps/src/lib/node/ledger/shell/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index c5fad75ea1..4c924400fc 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -446,7 +446,9 @@ where std::fs::create_dir(&base_dir) .expect("Creating directory for Namada should not fail"); } - let native_token = if !cfg!(test) && !cfg!(feature = "benches") { + let native_token = if cfg!(feature = "integration") + || (!cfg!(test) && !cfg!(feature = "benches")) + { let chain_dir = base_dir.join(chain_id.as_str()); let genesis = genesis::chain::Finalized::read_toml_files(&chain_dir) From 55698511d27c4f98d2e64ac0a0028dd10f4bbc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sun, 5 Nov 2023 14:06:31 +0100 Subject: [PATCH 43/47] genesis: rm dbg!s --- apps/src/lib/config/genesis/chain.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/src/lib/config/genesis/chain.rs b/apps/src/lib/config/genesis/chain.rs index 69dd23f658..711266c542 100644 --- a/apps/src/lib/config/genesis/chain.rs +++ b/apps/src/lib/config/genesis/chain.rs @@ -100,9 +100,7 @@ impl Finalized { validator: Option<(Alias, pre_genesis::ValidatorWallet)>, ) -> Wallet { let mut wallet = crate::wallet::load_or_new(base_dir); - dbg!(&wallet); for (alias, config) in &self.tokens.token { - dbg!("add token", alias); wallet.add_address( alias.normalize(), config.address.clone(), From d9bc3266d5e929b04bc04dc90cc5b3f4c123d4af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sun, 5 Nov 2023 16:33:21 +0100 Subject: [PATCH 44/47] make: separate integration tests from units coverage --- Makefile | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index b7e1f54d3f..c1c5b93ae7 100644 --- a/Makefile +++ b/Makefile @@ -126,14 +126,19 @@ audit: test: test-unit test-e2e test-wasm test-benches test-coverage: - # Run integration tests with pre-built MASP proofs - NAMADA_MASP_TEST_SEED=$(NAMADA_MASP_TEST_SEED) \ - NAMADA_MASP_TEST_PROOFS=load \ + # Run integration tests separately because they require `integration` + # feature (and without coverage) and run them with pre-built MASP proofs $(cargo) +$(nightly) llvm-cov --output-dir target \ --features namada/testing \ --html \ - -- --skip e2e --skip pos_state_machine_test \ - -Z unstable-options --report-time + -- --skip e2e --skip pos_state_machine_test --skip integration \ + -Z unstable-options --report-time && \ + NAMADA_MASP_TEST_SEED=$(NAMADA_MASP_TEST_SEED) \ + NAMADA_MASP_TEST_PROOFS=load \ + $(cargo) +$(nightly) test integration:: --output-dir target \ + --features integration \ + --html \ + -- -Z unstable-options --report-time # NOTE: `TEST_FILTER` is prepended with `e2e::`. Since filters in `cargo test` # work with a substring search, TEST_FILTER only works if it contains a string From 6ef52c14d23764c6f46855127732fec0a899d072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sun, 5 Nov 2023 16:35:29 +0100 Subject: [PATCH 45/47] wallet: don't crash when no wallet --- apps/src/lib/wallet/mod.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/src/lib/wallet/mod.rs b/apps/src/lib/wallet/mod.rs index 4cf55df158..140a85f69d 100644 --- a/apps/src/lib/wallet/mod.rs +++ b/apps/src/lib/wallet/mod.rs @@ -243,10 +243,9 @@ pub fn save(wallet: &Wallet) -> std::io::Result<()> { /// Load a wallet from the store file. pub fn load(store_dir: &Path) -> Option> { let mut wallet = CliWalletUtils::new(store_dir.to_path_buf()); - wallet.load().unwrap_or_else(|err| { - eprintln!("Unable to load the wallet: {}", err); - cli::safe_exit(1) - }); + if wallet.load().is_err() { + return None; + } Some(wallet) } From cf00f269b0bbcb07bd17bdb838f0972ee79e5717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sun, 5 Nov 2023 17:00:46 +0100 Subject: [PATCH 46/47] make: fix test-coverage --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c1c5b93ae7..1fd4e64edc 100644 --- a/Makefile +++ b/Makefile @@ -135,9 +135,8 @@ test-coverage: -Z unstable-options --report-time && \ NAMADA_MASP_TEST_SEED=$(NAMADA_MASP_TEST_SEED) \ NAMADA_MASP_TEST_PROOFS=load \ - $(cargo) +$(nightly) test integration:: --output-dir target \ + $(cargo) +$(nightly) test integration:: \ --features integration \ - --html \ -- -Z unstable-options --report-time # NOTE: `TEST_FILTER` is prepended with `e2e::`. Since filters in `cargo test` From 2faf58e1f639c607f104952e74d68e05d35e5821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Sun, 5 Nov 2023 17:39:30 +0100 Subject: [PATCH 47/47] changelog: add #2088 --- .changelog/unreleased/features/2088-new-genesis.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changelog/unreleased/features/2088-new-genesis.md diff --git a/.changelog/unreleased/features/2088-new-genesis.md b/.changelog/unreleased/features/2088-new-genesis.md new file mode 100644 index 0000000000..eacd1bf44a --- /dev/null +++ b/.changelog/unreleased/features/2088-new-genesis.md @@ -0,0 +1,11 @@ +- Added bech32m string encoding for `common::PublicKey` and `DkgPublicKey`. + ([\#2088](https://github.com/anoma/namada/pull/2088)) +- Added `--pre-genesis` argument to the wallet commands to allow to generate + keys, implicit addresses and shielded keys without having a chain setup. If + no chain is setup yet (i.e. there's no base-dir or it's empty), the wallet + defaults to use the pre-genesis wallet even without the `--pre-genesis` + flag. The pre-genesis wallet is located inside base-dir in + `pre-genesis/wallet.toml`. + ([\#2088](https://github.com/anoma/namada/pull/2088)) +- Reworked the genesis templates, setup and related utils commands. + ([\#2088](https://github.com/anoma/namada/pull/2088))

uE-H%Yk6E~GN6&ybD%-m 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 diff --git a/test_fixtures/masp_proofs/EE7C912B7E21F07494D58AA6668DC6BBB31619C7E93A1A5A2E64B694DBE1BD6E.bin b/test_fixtures/masp_proofs/EE7C912B7E21F07494D58AA6668DC6BBB31619C7E93A1A5A2E64B694DBE1BD6E.bin deleted file mode 100644 index fee4361a2b54e7e9b59cdc715893a58d59f9ada9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7448 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 diff --git a/test_fixtures/masp_proofs/F36A8353F15FD6D8158DBC67DDB827EEEDA858AB983D16024AAA415579A68953.bin b/test_fixtures/masp_proofs/F36A8353F15FD6D8158DBC67DDB827EEEDA858AB983D16024AAA415579A68953.bin deleted file mode 100644 index 755c32011e4d8f737561ac85b8cb3357c052506c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9649 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 S<8^*Kf>!4E`4(MeJ1o diff --git a/test_fixtures/masp_proofs/8B29BC2E1A96DF331C7C3A2B227C98D1E5AAAA9988F26B1A47090ACCE693572F.bin b/test_fixtures/masp_proofs/F39ADA174E1FB9F45BFFB0C68625A3F8ED350401B95715A6AF9BDD8CD1C367BA.bin similarity index 52% rename from test_fixtures/masp_proofs/8B29BC2E1A96DF331C7C3A2B227C98D1E5AAAA9988F26B1A47090ACCE693572F.bin rename to test_fixtures/masp_proofs/F39ADA174E1FB9F45BFFB0C68625A3F8ED350401B95715A6AF9BDD8CD1C367BA.bin index afcad7c65449afcb2942ccb2e2db2169ed4ffc23..b04b1f1f04a611396d4fa9e38ba6cd92e118182c 100644 GIT binary patch delta 2133 zcmc(geKZsLAIE2R9+#1^@Jq;S$RZD0o1Zq%RE&Gey&fy%QvDioiDV2RB~0j&8a8bn zW+wJaG!G%dMNjhBRA{-^G`ShM)BWe3d(XLk=luS-pL0Hcec!M1`F`H-?>XNhToH~e z0{{Rtqz#fbZ%MdR5`0AW5~;RE3qJr>YKMBiEi7S4FsI^ z)+M)Ib2dkltjg-Cd{*c9nEmGT-?0F|9_jI>1)IIq`cz{XH~3bfO^@>Aow#E%hk<%q znL_0#x^Z(rQsq_-d{H{dOO;#g%jGC00?Gf&H8-94Qyl+Klz&W;{0~G40P(Lao1H0* zDemt>PnpV>`O#68mNS%A6{0!mWl1&=Z@j;IUV`P+e>_+xy5+i%=kL;X;p?Q-)f!8; z$z($90T4A<*-Gy?(Z~G?rPcekh4?(j)l<)U+lQ5@l%hGmqh8HGyR{W=ld>LA^b`JP_*B(+wra^r4mDxkSPH`c~8Y~y}DuC~c^&NVX z1M+#S+E{v26o`*9(h#gNGK*43&-K;tte|&`FFw`E3Ji+hPiIw#tc=uW$~zhDlNQu0 zsFmJheh1xH9A_8-{>rcW^A1s!$pm{&=hi4(aQU0eeR0v;DX4Tl{ixS$bziM7qAS)5 zBp5$TRe^aD;+tQa+&`E23WRfMxS{V9XtCX}ooTgOj0mtFT6)K;fu z_%#FHmiodmv}<%QwCSG7-R-AQnAG;NP@2Q~fVpIJtt8fbZK{p!#3W^DYSl1!=CWguhmKds*@ z4q<)8Shwf|HHhu8N++Prm;phM&=MQuBb)&f7QWvb8NzF?`Z*54L7DG85)uQtZq|`A zH+c_hM{$lUtFnXUJ;9RMEQ`p|q2YZm2MRaD#F6e5W8&3BlmIDTm+=;6(inA)cb0PU)Gq^ybJ>ckVK=bcfjSIiqIk#~-Jb29zyBmAQ) z+dNE$iz5ZB3^yuM4r3HHWAjSu@`-_=O2-#Q-q>XWiWKwwb3burB zfo`IC;0=V%EU4q}K_uALR|FRlF0<%$UECFS^iEoSlL;J%HEB^669r^IEtow|uh&5iVB zS!S6AV3#>fR~DcMHMos2(md*9$jCU+T({r#!d+g0b_be*k8Av0onlrmFPKE)7i1Wr(|~KVs)j}pWR3Z48>Q6VCrUG1 zL}t}GfO=lrq}!{-db8Rx!_F==BIk2MRl*Oay}$|iYWU8JbU@})Fdx3}F1>5A>`O#hXEnypB|4mwaZ=amfD-4JKz;J0r zO$3bPz`LLo7$6=vdKPUhtBM-DlKDh>Wtz($KvRC^&}rL$t&1FWtP62iEvJE( zSBAAUZIKhtc3VV^$W7f98QE4;A)VcO87ZxVx20u?_VO9&N}K=4W`U>+JNNtsyU5x* delta 2133 zcmbPXHN&c&k%56h6o^@Xc!AEJ>w$YXE~hT5W zF)arwW7ra~HB_^`>PUs-qG+B&W(g*tSAHfv;#}KTGkf;soB4i$QOvRI#j#=s<7GVW z9cp|Qq?dgYqz?qp^wxtl8;Gk<-@U%7uwkYSOHC7VvgzY5f#j=Ipuo?<0s- z49#G@B^HNviR^r-xT9vBpz{Ww=9jzQelUEqx9Xa)=o9bM!bAAA86V3s5DY5CE%mWe2JxtZ+`k>{r{>PA2bsvg0&Igu>7Ah32Qd`Dbq5zo%wv-tm_?l8w)&c-wwB z6S#tL&2o>W=fo5ymkXFE=>F8xiP>g4QU3iW;n+St7MY_ zE)uk+MSkI&>_cmI_Z*)z;nhSnjrv)MmVHS}Ua!m8nEaa|4^xLl|oGrY5aDvdClGiUX0?7uv3CUM&9pm~d2ZR?6%> z*JiQldR%J}=t8FKE@@Si`K$w%}~_|ID=o?OA39u7>}*7#s41A8{#$@09kGdz$z8*6Q;c z?y?$%)amuU(z|?ke|q7&C$+c2HU8P=JLdGxYBgWl%FoCZe}E}w$N%!k8{ZvHevnAh z2-18R!QCNRq9NsbAZ$^dt#C-*wc{O&f0lAA>V5ntlDRQr`GQ8b`rjJY{yMDN&{fKxJ4#+#g3lM5WmK*ap`+(@t32)lUDHV+hicX$~%qq%l#X%ex5r{dMpYM zlEzkIvWOmd|)(zpcS3YD>(k!}OG&uI_jZ;D_HMu)d z@`H|M7aB9ak13nDTV!?O+%<=nAGVoV@-;Xy!#3fboL_)i4q(7C`K^pL0As4% Ag#Z8m diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index 433770bfa5..0b93d0df94 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -1411,7 +1411,7 @@ fn check_balances( ); client.exp_string(&expected)?; // Check the source balance - let expected = ": 900000, owned by albert".to_string(); + let expected = ": 880000, owned by albert".to_string(); client.exp_string(&expected)?; client.assert_success(); @@ -1475,7 +1475,7 @@ fn check_balances_after_back( ); client.exp_string(&expected)?; // Check the source balance - let expected = ": 950000, owned by albert".to_string(); + let expected = ": 930000, owned by albert".to_string(); client.exp_string(&expected)?; client.assert_success(); diff --git a/tests/src/integration/masp.rs b/tests/src/integration/masp.rs index f01b97c01f..d644e5e8db 100644 --- a/tests/src/integration/masp.rs +++ b/tests/src/integration/masp.rs @@ -8,11 +8,7 @@ use namada_sdk::masp::fs::FsShieldedUtils; use test_log::test; use super::setup; -use crate::e2e::setup::constants::{ - AA_PAYMENT_ADDRESS, AA_VIEWING_KEY, AB_PAYMENT_ADDRESS, AB_VIEWING_KEY, - AC_PAYMENT_ADDRESS, AC_VIEWING_KEY, ALBERT, A_SPENDING_KEY, - BB_PAYMENT_ADDRESS, BERTHA, BTC, B_SPENDING_KEY, CHRISTEL, ETH, MASP, NAM, -}; +use crate::e2e::setup::constants::{AA_PAYMENT_ADDRESS, AA_VIEWING_KEY, AB_PAYMENT_ADDRESS, AB_VIEWING_KEY, AC_PAYMENT_ADDRESS, AC_VIEWING_KEY, ALBERT, A_SPENDING_KEY, BB_PAYMENT_ADDRESS, BERTHA, BTC, B_SPENDING_KEY, CHRISTEL, ETH, MASP, NAM, BERTHA_KEY, ALBERT_KEY, CHRISTEL_KEY}; /// In this test we verify that users of the MASP receive the correct rewards /// for leaving their assets in the pool for varying periods of time. @@ -347,7 +343,7 @@ fn masp_incentives() -> Result<()> { "--amount", "10", "--signing-keys", - BERTHA, + BERTHA_KEY, "--node", validator_one_rpc, ], @@ -433,7 +429,7 @@ fn masp_incentives() -> Result<()> { "--amount", "20", "--signing-keys", - ALBERT, + ALBERT_KEY, "--node", validator_one_rpc, ], @@ -578,7 +574,7 @@ fn masp_incentives() -> Result<()> { "--amount", "141.49967", "--signing-keys", - BERTHA, + BERTHA_KEY, "--node", validator_one_rpc, ], @@ -603,7 +599,7 @@ fn masp_incentives() -> Result<()> { "--amount", "1980.356", "--signing-keys", - ALBERT, + ALBERT_KEY, "--node", validator_one_rpc, ], @@ -867,7 +863,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "10", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -886,7 +882,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "15", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -922,7 +918,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "10", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -941,7 +937,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "7", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -960,7 +956,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "7", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -979,7 +975,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "7", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -998,7 +994,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "6", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], @@ -1054,7 +1050,7 @@ fn masp_txs_and_queries() -> Result<()> { "--amount", "20", "--gas-payer", - CHRISTEL, + CHRISTEL_KEY, "--node", validator_one_rpc, ], From e6dd02d03c575ffc6a42ff1c3f1e4d65cbf7d76e Mon Sep 17 00:00:00 2001 From: brentstone Date: Mon, 30 Oct 2023 23:23:13 -0400 Subject: [PATCH 11/47] fix `make check-crates` --- ethereum_bridge/Cargo.toml | 1 + shared/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/ethereum_bridge/Cargo.toml b/ethereum_bridge/Cargo.toml index 40d8e6f422..08c5731e28 100644 --- a/ethereum_bridge/Cargo.toml +++ b/ethereum_bridge/Cargo.toml @@ -48,6 +48,7 @@ tracing = "0.1.30" [dev-dependencies] # Added "testing" feature. namada_core = {path = "../core", default-features = false, features = ["ferveo-tpke", "ethers-derive", "testing"]} +namada_proof_of_stake = {path = "../proof_of_stake", default-features = false, features = ["testing"]} assert_matches.workspace = true data-encoding.workspace = true ethabi.workspace = true diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 0795ca3c39..ce39a22ef8 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -167,6 +167,7 @@ wasmtimer = "0.2.0" [dev-dependencies] namada_core = {path = "../core", default-features = false, features = ["testing", "ibc-mocks"]} namada_ethereum_bridge = {path = "../ethereum_bridge", default-features = false, features = ["testing"]} +namada_proof_of_stake = {path = "../proof_of_stake", default-features = false, features = ["testing"]} namada_test_utils = {path = "../test_utils"} assert_matches.workspace = true async-trait.workspace = true From 0e04a9288aa5d2f6aebec0556d9e577da1d03810 Mon Sep 17 00:00:00 2001 From: brentstone Date: Tue, 31 Oct 2023 10:58:01 -0400 Subject: [PATCH 12/47] fix fmt --- tests/src/integration/masp.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/src/integration/masp.rs b/tests/src/integration/masp.rs index d644e5e8db..8cfd25958f 100644 --- a/tests/src/integration/masp.rs +++ b/tests/src/integration/masp.rs @@ -8,7 +8,12 @@ use namada_sdk::masp::fs::FsShieldedUtils; use test_log::test; use super::setup; -use crate::e2e::setup::constants::{AA_PAYMENT_ADDRESS, AA_VIEWING_KEY, AB_PAYMENT_ADDRESS, AB_VIEWING_KEY, AC_PAYMENT_ADDRESS, AC_VIEWING_KEY, ALBERT, A_SPENDING_KEY, BB_PAYMENT_ADDRESS, BERTHA, BTC, B_SPENDING_KEY, CHRISTEL, ETH, MASP, NAM, BERTHA_KEY, ALBERT_KEY, CHRISTEL_KEY}; +use crate::e2e::setup::constants::{ + AA_PAYMENT_ADDRESS, AA_VIEWING_KEY, AB_PAYMENT_ADDRESS, AB_VIEWING_KEY, + AC_PAYMENT_ADDRESS, AC_VIEWING_KEY, ALBERT, ALBERT_KEY, A_SPENDING_KEY, + BB_PAYMENT_ADDRESS, BERTHA, BERTHA_KEY, BTC, B_SPENDING_KEY, CHRISTEL, + CHRISTEL_KEY, ETH, MASP, NAM, +}; /// In this test we verify that users of the MASP receive the correct rewards /// for leaving their assets in the pool for varying periods of time. From 3838cef679f61dc1aa31f4099a3f304dbb75c720 Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 2 Nov 2023 10:45:02 +0100 Subject: [PATCH 13/47] [fix]: Fix integration tests --- core/src/ledger/storage/masp_conversions.rs | 4 +- genesis/localnet/balances.toml | 2 +- tests/src/e2e/ledger_tests.rs | 11 +- tests/src/integration/masp.rs | 145 +++++++++++--------- 4 files changed, 91 insertions(+), 71 deletions(-) diff --git a/core/src/ledger/storage/masp_conversions.rs b/core/src/ledger/storage/masp_conversions.rs index 5cbbca570a..4b3a2e45e9 100644 --- a/core/src/ledger/storage/masp_conversions.rs +++ b/core/src/ledger/storage/masp_conversions.rs @@ -137,7 +137,9 @@ where Uint::from(precision), total_token_in_masp.raw_amount(), ) - .and_then(|x| x.0.try_into().ok()) + .and_then(|x| { + x.0.try_into().ok() + }) .unwrap_or_else(|| { tracing::warn!( "MASP inflation for {} assumed to be 0 because the \ diff --git a/genesis/localnet/balances.toml b/genesis/localnet/balances.toml index 8b84002267..2c2d78a9f3 100644 --- a/genesis/localnet/balances.toml +++ b/genesis/localnet/balances.toml @@ -19,7 +19,7 @@ pktest1qpyfnrl6qdqvguah9kknvp9t6ajcrec7fge56pcgaa655zkua3ndsl9tn4a = "2000000" # christel pktest1qp6uy52q0fldjxupznuskm69fkuswx3fq3vw9kekzp4enkh5h7pmzy242c2 = "2000000" # daewon -pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "1000000" +pktest1qzz4x4fammhdcfa0g8xw4udkq8s4n6kjhzlxh00ul3da05wuu9wkyuzdckd = "100000000" # ester pktest1qypvqpzu74nafjahlwyq272dj76qq9rz30dulyc94883tmj893mquqsafsprd = "1000000" # validator-0-key diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 224c3d767f..ac0ab57240 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -915,11 +915,18 @@ fn pos_bonds() -> Result<()> { genesis.parameters.parameters.min_num_of_blocks = 6; genesis.parameters.parameters.max_expected_time_per_block = 1; genesis.parameters.parameters.epochs_per_year = 31_536_000; - setup::set_validators(1, genesis, base_dir, default_port_offset) + let mut genesis = setup::set_validators(2, genesis, base_dir, default_port_offset); + let bonds = genesis.transactions.bond.unwrap(); + genesis.transactions.bond = Some( + bonds + .into_iter() + .filter(|bond| (&bond.data.validator).as_ref() != "validator-1") + .collect() + ); + genesis }, None, )?; - set_ethereum_bridge_mode( &test, &test.net.chain_id, diff --git a/tests/src/integration/masp.rs b/tests/src/integration/masp.rs index d644e5e8db..e89d42ee93 100644 --- a/tests/src/integration/masp.rs +++ b/tests/src/integration/masp.rs @@ -24,7 +24,7 @@ fn masp_incentives() -> Result<()> { let (mut node, _services) = setup::setup()?; // Wait till epoch boundary node.next_epoch(); - // Send 20 BTC from Albert to PA + // Send 1 BTC from Albert to PA run( &node, Bin::Client, @@ -37,14 +37,14 @@ fn masp_incentives() -> Result<()> { "--token", BTC, "--amount", - "20", + "1", "--node", validator_one_rpc, ], )?; node.assert_success(); - // Assert BTC balance at VK(A) is 20 + // Assert BTC balance at VK(A) is 1 let captured = CapturedOutput::of(|| { run( &node, @@ -61,7 +61,7 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("btc: 20")); + assert!(captured.contains("btc: 1")); // Assert NAM balance at VK(A) is 0 let captured = CapturedOutput::of(|| { @@ -85,7 +85,7 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary node.next_epoch(); - // Assert BTC balance at VK(A) is 20 + // Assert BTC balance at VK(A) is still 1 let captured = CapturedOutput::of(|| { run( &node, @@ -102,9 +102,9 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("btc: 20")); + assert!(captured.contains("btc: 1")); - // Assert NAM balance at VK(A) is 20*BTC_reward*(epoch_1-epoch_0) + // Assert NAM balance is a non-zero number (rewards have been dispensed) let captured = CapturedOutput::of(|| { run( &node, @@ -122,9 +122,10 @@ fn masp_incentives() -> Result<()> { }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 22.74")); + assert!(captured.contains("nam: 0.022")); - // Assert NAM balance at MASP pool is 20*BTC_reward*(epoch_1-epoch_0) + // Assert NAM balance at MASP pool is exclusively the + // rewards from the shielded BTC let captured = CapturedOutput::of(|| { run( &node, @@ -141,12 +142,12 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 22.74")); + assert!(captured.contains("nam: 0.022")); // Wait till epoch boundary node.next_epoch(); - // Assert BTC balance at VK(A) is 20 + // Assert BTC balance at VK(A) is still 1 let captured = CapturedOutput::of(|| { run( &node, @@ -163,9 +164,10 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("btc: 20")); + assert!(captured.contains("btc: 1")); - // Assert NAM balance at VK(A) is 20*BTC_reward*(epoch_2-epoch_0) + // Assert NAM balance is a number greater than the last epoch's balance + // (more rewards have been dispensed) let captured = CapturedOutput::of(|| { run( &node, @@ -182,9 +184,10 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 90.96")); + assert!(captured.contains("nam: 0.08729")); - // Assert NAM balance at MASP pool is 20*BTC_reward*(epoch_2-epoch_0) + // Assert NAM balance at MASP pool is exclusively the + // rewards from the shielded BTC let captured = CapturedOutput::of(|| { run( &node, @@ -201,12 +204,12 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 90.96")); + assert!(captured.contains("nam: 0.08729")); // Wait till epoch boundary node.next_epoch(); - // Send 10 ETH from Albert to PA(B) + // Send 0.001 ETH from Albert to PA(B) run( &node, Bin::Client, @@ -219,14 +222,14 @@ fn masp_incentives() -> Result<()> { "--token", ETH, "--amount", - "10", + "0.001", "--node", validator_one_rpc, ], )?; node.assert_success(); - // Assert ETH balance at VK(B) is 10 + // Assert ETH balance at VK(B) is 0.001 let captured = CapturedOutput::of(|| { run( &node, @@ -243,7 +246,7 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("eth: 10")); + assert!(captured.contains("eth: 0.001")); // Assert NAM balance at VK(B) is 0 let captured = CapturedOutput::of(|| { @@ -267,7 +270,7 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary node.next_epoch(); - // Assert ETH balance at VK(B) is 10 + // Assert ETH balance at VK(B) is still 0.001 let captured = CapturedOutput::of(|| { run( &node, @@ -284,9 +287,10 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("eth: 10")); + assert!(captured.contains("eth: 0.001")); - // Assert NAM balance at VK(B) is 10*ETH_reward*(epoch_4-epoch_3) + // Assert NAM balance at VK(B) is non-zero (rewards have been + // dispensed) let captured = CapturedOutput::of(|| { run( &node, @@ -303,10 +307,10 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 22.71432")); + assert!(captured.contains("nam: 0.0207")); - // Assert NAM balance at MASP pool is - // 20*BTC_reward*(epoch_4-epoch_0)+10*ETH_reward*(epoch_4-epoch_3) + // Assert NAM balance at MASP pool is an accumulation of + // rewards from both the shielded BTC and shielded ETH let captured = CapturedOutput::of(|| { run( &node, @@ -323,12 +327,12 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 386.46336")); + assert!(captured.contains("nam: 0.3726")); // Wait till epoch boundary node.next_epoch(); - // Send 10 ETH from SK(B) to Christel + // Send 0.001 ETH from SK(B) to Christel run( &node, Bin::Client, @@ -341,7 +345,7 @@ fn masp_incentives() -> Result<()> { "--token", ETH, "--amount", - "10", + "0.001", "--signing-keys", BERTHA_KEY, "--node", @@ -371,7 +375,8 @@ fn masp_incentives() -> Result<()> { node.next_epoch(); - // Assert NAM balance at VK(B) is 10*ETH_reward*(ep-epoch_3) + // Assert VK(B) retains the NAM rewards dispensed in the correct + // amount. let captured = CapturedOutput::of(|| { run( &node, @@ -388,11 +393,12 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 86.60024")); + assert!(captured.contains("nam: 0.085204")); node.next_epoch(); + // Assert NAM balance at MASP pool is - // 20*BTC_reward*(epoch_5-epoch_0)+10*ETH_reward*(epoch_5-epoch_3) + // the accumulation of rewards from the shielded assets (BTC and ETH) let captured = CapturedOutput::of(|| { run( &node, @@ -409,12 +415,12 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 1180.41525")); + assert!(captured.contains("nam: 1.134567")); // Wait till epoch boundary node.next_epoch(); - // Send 20 BTC from SK(A) to Christel + // Send 1 BTC from SK(A) to Christel run( &node, Bin::Client, @@ -427,7 +433,7 @@ fn masp_incentives() -> Result<()> { "--token", BTC, "--amount", - "20", + "1", "--signing-keys", ALBERT_KEY, "--node", @@ -455,7 +461,7 @@ fn masp_incentives() -> Result<()> { assert!(captured.result.is_ok()); assert!(captured.contains("No shielded btc balance found")); - // Assert NAM balance at VK(A) is 20*BTC_reward*(epoch_6-epoch_0) + // Assert VK(A) retained the NAM rewards let captured = CapturedOutput::of(|| { run( &node, @@ -472,10 +478,10 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 1407.16324")); + assert!(captured.contains("nam: 1.355211")); // Assert NAM balance at MASP pool is - // 20*BTC_reward*(epoch_6-epoch_0)+20*ETH_reward*(epoch_5-epoch_3) + // the accumulation of rewards from the shielded assets (BTC and ETH) let captured = CapturedOutput::of(|| { run( &node, @@ -492,12 +498,14 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 1520.37191")); + assert!(captured.contains("nam: 1.459458")); // Wait till epoch boundary node.next_epoch(); - // Assert NAM balance at VK(A) is 20*BTC_reward*(epoch_6-epoch_0) + // Assert NAM balance at VK(A) is the rewards dispensed earlier + // (since VK(A) has no shielded assets, no further rewards should + // be dispensed to that account) let captured = CapturedOutput::of(|| { run( &node, @@ -514,9 +522,11 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 1573.18")); + assert!(captured.contains("nam: 1.520493")); - // Assert NAM balance at VK(B) is 10*ETH_reward*(epoch_5-epoch_3) + // Assert NAM balance at VK(B) is the rewards dispensed earlier + // (since VK(A) has no shielded assets, no further rewards should + // be dispensed to that account) let captured = CapturedOutput::of(|| { run( &node, @@ -533,10 +543,10 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 126.565")); + assert!(captured.contains("nam: 0.125958")); // Assert NAM balance at MASP pool is - // 20*BTC_reward*(epoch_6-epoch_0)+10*ETH_reward*(epoch_5-epoch_3) + // the accumulation of rewards from the shielded assets (BTC and ETH) let captured = CapturedOutput::of(|| { run( &node, @@ -553,13 +563,13 @@ fn masp_incentives() -> Result<()> { ) }); assert!(captured.result.is_ok()); - assert!(captured.contains("nam: 1699.745")); + assert!(captured.contains("nam: 1.637454")); // Wait till epoch boundary to prevent conversion expiry during transaction // construction node.next_epoch(); - // Send 10*ETH_reward*(epoch_5-epoch_3) NAM from SK(B) to Christel + // Send all NAM rewards from SK(B) to Christel run( &node, Bin::Client, @@ -572,7 +582,7 @@ fn masp_incentives() -> Result<()> { "--token", NAM, "--amount", - "141.49967", + "0.137354", "--signing-keys", BERTHA_KEY, "--node", @@ -584,7 +594,7 @@ fn masp_incentives() -> Result<()> { // Wait till epoch boundary node.next_epoch(); - // Send 20*BTC_reward*(epoch_6-epoch_0) NAM from SK(A) to Bertha + // Send all NAM rewards from SK(A) to Bertha run( &node, Bin::Client, @@ -597,7 +607,7 @@ fn masp_incentives() -> Result<()> { "--token", NAM, "--amount", - "1980.356", + "1.916208", "--signing-keys", ALBERT_KEY, "--node", @@ -606,24 +616,25 @@ fn masp_incentives() -> Result<()> { )?; node.assert_success(); - // Assert NAM balance at VK(A) is 0 - let captured = CapturedOutput::of(|| { - run( - &node, - Bin::Client, - vec![ - "balance", - "--owner", - AA_VIEWING_KEY, - "--token", - NAM, - "--node", - validator_one_rpc, - ], - ) - }); - assert!(captured.result.is_ok()); - assert!(captured.contains("No shielded nam balance found")); + // TODO: Fix once we can unsheild less than 10^-3 tokens + // // Assert NAM balance at VK(A) is 0 + // let captured = CapturedOutput::of(|| { + // run( + // &node, + // Bin::Client, + // vec![ + // "balance", + // "--owner", + // AA_VIEWING_KEY, + // "--token", + // NAM, + // "--node", + // validator_one_rpc, + // ], + // ) + // }); + // assert!(captured.result.is_ok()); + // assert!(captured.contains("No shielded nam balance found")); // Assert NAM balance at VK(B) is 0 let captured = CapturedOutput::of(|| { From 5356f55ea66bf6f654f6c688bf7a88bb198795ec Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Mon, 30 Oct 2023 14:02:47 +0100 Subject: [PATCH 14/47] [ci skip] specifiy aws region --- .github/workflows/build-and-test.yml | 1 + .github/workflows/checks.yml | 1 + .github/workflows/docs.yml | 1 + .github/workflows/release.yml | 1 + .github/workflows/triggerable.yml | 1 + 5 files changed, 5 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e25046f4b5..6fff3e2bfe 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -29,6 +29,7 @@ env: SCCACHE_ENDPOINT: ${{ secrets.CACHE_ENDPOINT }} AWS_ACCESS_KEY_ID: ${{ secrets.CACHE_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CACHE_SECRET_KEY }} + AWS_REGION: us-east-1 jobs: build-wasm: diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 44bef7bd9a..4db3342bc9 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -27,6 +27,7 @@ env: SCCACHE_ENDPOINT: ${{ secrets.CACHE_ENDPOINT }} AWS_ACCESS_KEY_ID: ${{ secrets.CACHE_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CACHE_SECRET_KEY }} + AWS_REGION: us-east-1 jobs: clippy-fmt: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index da52d82c73..03e22f6176 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,6 +27,7 @@ env: SCCACHE_ENDPOINT: ${{ secrets.CACHE_ENDPOINT }} AWS_ACCESS_KEY_ID: ${{ secrets.CACHE_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CACHE_SECRET_KEY }} + AWS_REGION: us-east-1 jobs: docs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60d49a1a05..cf38d406b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,7 @@ env: SCCACHE_ENDPOINT: ${{ secrets.CACHE_ENDPOINT }} AWS_ACCESS_KEY_ID: ${{ secrets.CACHE_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CACHE_SECRET_KEY }} + AWS_REGION: us-east-1 jobs: build: diff --git a/.github/workflows/triggerable.yml b/.github/workflows/triggerable.yml index 001a4b7b9a..b13af18ad1 100644 --- a/.github/workflows/triggerable.yml +++ b/.github/workflows/triggerable.yml @@ -23,6 +23,7 @@ env: SCCACHE_ENDPOINT: ${{ secrets.CACHE_ENDPOINT }} AWS_ACCESS_KEY_ID: ${{ secrets.CACHE_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CACHE_SECRET_KEY }} + AWS_REGION: us-east-1 jobs: pos: From b674b8bec38f53fd9801dc0d412c8d79aa0491d3 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Fri, 27 Oct 2023 12:25:45 +0200 Subject: [PATCH 15/47] wasm server download via env --- apps/src/lib/wasm_loader/mod.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/src/lib/wasm_loader/mod.rs b/apps/src/lib/wasm_loader/mod.rs index 9a075fbcf8..2d1aa95175 100644 --- a/apps/src/lib/wasm_loader/mod.rs +++ b/apps/src/lib/wasm_loader/mod.rs @@ -33,7 +33,9 @@ pub enum Error { #[serde(transparent)] pub struct Checksums(pub HashMap); -const S3_URL: &str = "https://namada-wasm-master.s3.eu-west-1.amazonaws.com"; +/// Github URL prefix of released Namada network configs +pub const ENV_VAR_WASM_SERVER: &str = "NAMADA_NETWORK_CONFIGS_SERVER"; +const DEFAULT_WASM_SERVER: &str = "https://artifacts.heliax.click/namada-wasm"; impl Checksums { /// Read WASM checksums from the given path @@ -101,6 +103,11 @@ impl Checksums { } } +fn wasm_url_prefix(wasm_name: &str) -> String { + std::env::var(ENV_VAR_WASM_SERVER) + .unwrap_or_else(|_| format!("{DEFAULT_WASM_SERVER}/{wasm_name}")) +} + /// Download all the pre-built wasms, or if they're already downloaded, verify /// their checksums. pub async fn pre_fetch_wasm(wasm_directory: impl AsRef) { @@ -180,7 +187,7 @@ pub async fn pre_fetch_wasm(wasm_directory: impl AsRef) { } } - let url = format!("{}/{}", S3_URL, full_name); + let url = wasm_url_prefix(&full_name); match download_wasm(url).await { Ok(bytes) => { if let Err(e) = @@ -224,7 +231,7 @@ pub async fn pre_fetch_wasm(wasm_directory: impl AsRef) { } } - let url = format!("{}/{}", S3_URL, full_name); + let url = wasm_url_prefix(&full_name); match download_wasm(url).await { Ok(bytes) => { if let Err(e) = From ac778559ece4a16b02949caa76caf0b1e95ec105 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Fri, 27 Oct 2023 12:28:25 +0200 Subject: [PATCH 16/47] added changelog --- .changelog/unreleased/improvements/2064-wasm-download.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/2064-wasm-download.md diff --git a/.changelog/unreleased/improvements/2064-wasm-download.md b/.changelog/unreleased/improvements/2064-wasm-download.md new file mode 100644 index 0000000000..cac4b13030 --- /dev/null +++ b/.changelog/unreleased/improvements/2064-wasm-download.md @@ -0,0 +1,2 @@ +- Define the wasm download endpoint via environemnt variable. + ([\#2064](https://github.com/anoma/namada/pull/2064)) \ No newline at end of file From 4fe551fbe58a87b96c649996811275a516bc2733 Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Fri, 27 Oct 2023 13:41:59 +0200 Subject: [PATCH 17/47] improve sdk --- sdk/src/args.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ sdk/src/lib.rs | 18 +++++++++++++++- sdk/src/tx.rs | 2 ++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/sdk/src/args.rs b/sdk/src/args.rs index b89c6b4209..f8454648b5 100644 --- a/sdk/src/args.rs +++ b/sdk/src/args.rs @@ -666,6 +666,63 @@ pub struct TxInitAccount { pub threshold: Option, } +impl TxBuilder for TxInitAccount { + fn tx(self, func: F) -> Self + where + F: FnOnce(Tx) -> Tx, + { + TxInitAccount { + tx: func(self.tx), + ..self + } + } +} + +impl TxInitAccount { + /// A vector of public key to associate with the new account + pub fn public_keys(self, public_keys: Vec) -> Self { + Self { + public_keys, + ..self + } + } + + /// A threshold to associate with the new account + pub fn threshold(self, threshold: u8) -> Self { + Self { + threshold: Some(threshold), + ..self + } + } + + /// Path to the VP WASM code file + pub fn vp_code_path(self, vp_code_path: PathBuf) -> Self { + Self { + vp_code_path, + ..self + } + } + + /// Path to the TX WASM code file + pub fn tx_code_path(self, tx_code_path: PathBuf) -> Self { + Self { + tx_code_path, + ..self + } + } +} + +impl TxInitAccount { + /// Build a transaction from this builder + pub async fn build<'a>( + &self, + context: &impl Namada<'a>, + ) -> crate::error::Result<(crate::proto::Tx, SigningTxData, Option)> + { + tx::build_init_account(context, self).await + } +} + /// Transaction to initialize a new account #[derive(Clone, Debug)] pub struct TxInitValidator { diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 77e03c2b4c..e53b7cfb69 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -5,6 +5,7 @@ pub use namada_core::proto; pub use tendermint_rpc; #[cfg(feature = "tendermint-rpc-abcipp")] pub use tendermint_rpc_abcipp as tendermint_rpc; +use tx::{TX_INIT_ACCOUNT_WASM, VP_VALIDATOR_WASM}; pub use { bip39, borsh, masp_primitives, masp_proofs, namada_core as core, namada_proof_of_stake as proof_of_stake, zeroize, @@ -161,6 +162,21 @@ pub trait Namada<'a>: Sized { } } + /// Make a InitAccount builder from the given minimum set of arguments + fn new_init_account( + &self, + public_keys: Vec, + threshold: Option, + ) -> args::TxInitAccount { + args::TxInitAccount { + tx: self.tx_builder(), + vp_code_path: PathBuf::from(VP_USER_WASM), + tx_code_path: PathBuf::from(TX_INIT_ACCOUNT_WASM), + public_keys, + threshold, + } + } + /// Make a RevealPK builder from the given minimum set of arguments fn new_reveal_pk(&self, public_key: common::PublicKey) -> args::RevealPk { args::RevealPk { @@ -297,7 +313,7 @@ pub trait Namada<'a>: Sized { eth_cold_key: None, eth_hot_key: None, protocol_key: None, - validator_vp_code_path: PathBuf::from(VP_USER_WASM), + validator_vp_code_path: PathBuf::from(VP_VALIDATOR_WASM), unsafe_dont_encrypt: false, tx_code_path: PathBuf::from(TX_INIT_VALIDATOR_WASM), tx: self.tx_builder(), diff --git a/sdk/src/tx.rs b/sdk/src/tx.rs index 962c6964a3..2a7919895b 100644 --- a/sdk/src/tx.rs +++ b/sdk/src/tx.rs @@ -88,6 +88,8 @@ pub const TX_TRANSFER_WASM: &str = "tx_transfer.wasm"; pub const TX_IBC_WASM: &str = "tx_ibc.wasm"; /// User validity predicate WASM path pub const VP_USER_WASM: &str = "vp_user.wasm"; +/// Validator validity predicate WASM path +pub const VP_VALIDATOR_WASM: &str = "vp_validator.wasm"; /// Bond WASM path pub const TX_BOND_WASM: &str = "tx_bond.wasm"; /// Unbond WASM path From d501475d7f288c92feda3ee069c30d17a60b7a5a Mon Sep 17 00:00:00 2001 From: Gianmarco Fraccaroli Date: Mon, 30 Oct 2023 09:49:31 +0100 Subject: [PATCH 18/47] remove vp/tx cache at startup --- apps/src/lib/node/ledger/shell/mod.rs | 1 + .../src/vm/wasm/compilation_cache/common.rs | 45 ++++++++----------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 5e3c6ac2a3..61d909d408 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -549,6 +549,7 @@ where // TODO: config event log params event_log: EventLog::default(), }; + shell.update_eth_oracle(); shell } diff --git a/shared/src/vm/wasm/compilation_cache/common.rs b/shared/src/vm/wasm/compilation_cache/common.rs index 702264cba4..f866188970 100644 --- a/shared/src/vm/wasm/compilation_cache/common.rs +++ b/shared/src/vm/wasm/compilation_cache/common.rs @@ -6,19 +6,19 @@ use std::collections::hash_map::RandomState; use std::collections::HashMap; +use std::fs; use std::marker::PhantomData; use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use std::thread::sleep; use std::time::Duration; -use std::{cmp, fs}; use clru::{CLruCache, CLruCacheConfig, WeightScale}; use wasmer::{Module, Store}; use wasmer_cache::{FileSystemCache, Hash as CacheHash}; -use crate::core::types::hash::{Hash, HASH_LENGTH}; +use crate::core::types::hash::Hash; use crate::types::control_flow::time::{ExponentialBackoff, SleepStrategy}; use crate::vm::wasm::run::untrusted_wasm_store; use crate::vm::wasm::{self, memory}; @@ -61,18 +61,8 @@ enum Compilation { struct ModuleCacheScale; impl WeightScale for ModuleCacheScale { - fn weight(&self, key: &Hash, value: &Module) -> usize { - // We only want to limit the max memory size, not the number of - // elements, so we use the size of the module as its scale - // and subtract 1 from it to negate the increment of the cache length. - - let size = loupe::size_of_val(&value) + HASH_LENGTH; - tracing::debug!( - "WASM module hash {}, size including the hash {}", - key.to_string(), - size - ); - cmp::max(1, size) - 1 + fn weight(&self, _key: &Hash, _value: &Module) -> usize { + 1 } } @@ -89,8 +79,10 @@ impl Cache { ); let in_memory = Arc::new(RwLock::new(cache)); let dir = dir.into(); + fs::create_dir_all(&dir) .expect("Couldn't create the wasm cache directory"); + Self { dir, progress: Default::default(), @@ -459,10 +451,6 @@ fn hash_of_code(code: impl AsRef<[u8]>) -> Hash { Hash::sha256(code.as_ref()) } -fn hash_to_store_dir(hash: &Hash) -> PathBuf { - PathBuf::from("vp_wasm_cache").join(hash.to_string().to_lowercase()) -} - fn compile( code: impl AsRef<[u8]>, ) -> Result<(Module, Store), wasm::run::Error> { @@ -500,18 +488,21 @@ fn file_load_module(dir: impl AsRef, hash: &Hash) -> (Module, Store) { } fn fs_cache(dir: impl AsRef, hash: &Hash) -> FileSystemCache { - let path = dir.as_ref().join(hash_to_store_dir(hash)); + let path = dir.as_ref().join(hash.to_string().to_lowercase()); let mut fs_cache = FileSystemCache::new(path).unwrap(); fs_cache.set_cache_extension(Some(file_ext())); fs_cache } fn module_file_exists(dir: impl AsRef, hash: &Hash) -> bool { - let file = dir.as_ref().join(hash_to_store_dir(hash)).join(format!( - "{}.{}", - hash.to_string().to_lowercase(), - file_ext() - )); + let file = + dir.as_ref() + .join(hash.to_string().to_lowercase()) + .join(format!( + "{}.{}", + hash.to_string().to_lowercase(), + file_ext() + )); file.exists() } @@ -1022,7 +1013,7 @@ mod test { fn load_wasm(file: impl AsRef) -> WasmWithMeta { // When `WeightScale` calls `loupe::size_of_val` in the cache, for some // reason it returns 8 bytes more than the same call in here. - let extra_bytes = 8; + let _extra_bytes = 8; let file = file.as_ref(); let code = fs::read(file).unwrap(); @@ -1033,9 +1024,9 @@ mod test { // No in-memory cache needed, but must be non-zero 1, ); - let (module, _store) = + let (_module, _store) = cache.compile_or_fetch(&code).unwrap().unwrap(); - loupe::size_of_val(&module) + HASH_LENGTH + extra_bytes + 1 }; println!( "Compiled module {} size including the hash: {} ({})", From 01c1231bf29d7438aaf7b71e37c91dbfb1d1fe23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 25 Oct 2023 08:21:22 +0200 Subject: [PATCH 19/47] apps: turn off rocksdb jemalloc on windows --- apps/Cargo.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 272aef9679..8a783d4dc5 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -121,7 +121,6 @@ regex.workspace = true reqwest.workspace = true ripemd.workspace = true rlimit.workspace = true -rocksdb.workspace = true rpassword.workspace = true serde_bytes.workspace = true serde_json = {workspace = true, features = ["raw_value"]} @@ -144,10 +143,15 @@ tracing-subscriber = { workspace = true, features = ["std", "json", "ansi", "tra tracing.workspace = true winapi.workspace = true zeroize.workspace = true - warp = "0.3.2" bytes = "1.1.0" +[target.'cfg(not(windows))'.dependencies] +rocksdb = { workspace = true, features = ['jemalloc'] } # jemalloc is not supported on windows + +[target.'cfg(windows)'.dependencies] +rocksdb = { workspace = true } + [dev-dependencies] assert_matches = "1.5.0" namada = {path = "../shared", default-features = false, features = ["testing", "wasm-runtime"]} From d3be7e2ee8bdd0ea6b271b93dcdcc5da4d6a2d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 25 Oct 2023 08:24:44 +0200 Subject: [PATCH 20/47] changelog: add #2047 --- .changelog/unreleased/bug-fixes/2047-win-build.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/bug-fixes/2047-win-build.md diff --git a/.changelog/unreleased/bug-fixes/2047-win-build.md b/.changelog/unreleased/bug-fixes/2047-win-build.md new file mode 100644 index 0000000000..903daf95db --- /dev/null +++ b/.changelog/unreleased/bug-fixes/2047-win-build.md @@ -0,0 +1,2 @@ +- Fix Windows build by disabling RocksDB jemalloc feature. + ([\#2047](https://github.com/anoma/namada/pull/2047)) \ No newline at end of file From 3537ef7ad79a350c30ed4db7b7691c5d6c6b9922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Mon, 23 Oct 2023 11:06:58 +0200 Subject: [PATCH 21/47] test: remove dead multitoken_tests --- tests/src/e2e/multitoken_tests.rs | 372 ------------------------------ 1 file changed, 372 deletions(-) delete mode 100644 tests/src/e2e/multitoken_tests.rs diff --git a/tests/src/e2e/multitoken_tests.rs b/tests/src/e2e/multitoken_tests.rs deleted file mode 100644 index 5d713b2aed..0000000000 --- a/tests/src/e2e/multitoken_tests.rs +++ /dev/null @@ -1,372 +0,0 @@ -//! Tests for multitoken functionality -use color_eyre::eyre::Result; -use namada_core::types::token; - -use super::helpers::get_actor_rpc; -use super::setup::constants::{ALBERT, BERTHA}; -use super::setup::{self, Who}; -use crate::e2e; -use crate::e2e::setup::constants::{ALBERT_KEY, BERTHA_KEY, CHRISTEL_KEY}; - -mod helpers; - -#[test] -fn test_multitoken_transfer_implicit_to_implicit() -> Result<()> { - let (test, _ledger) = e2e::helpers::setup_single_node_test()?; - - let rpc_addr = get_actor_rpc(&test, &Who::Validator(0)); - let multitoken_alias = helpers::init_multitoken_vp(&test, &rpc_addr)?; - - // establish a multitoken VP with the following balances - // - #atest5blah/tokens/red/balance/$albert_established = 100 - // - #atest5blah/tokens/red/balance/$bertha = 0 - - let multitoken_vp_addr = - e2e::helpers::find_address(&test, &multitoken_alias)?; - println!("Fake multitoken VP established at {}", multitoken_vp_addr); - - let albert_addr = e2e::helpers::find_address(&test, ALBERT)?; - let albert_starting_red_balance = token::Amount::native_whole(100_000_000); - helpers::mint_red_tokens( - &test, - &rpc_addr, - &multitoken_vp_addr, - &albert_addr, - &albert_starting_red_balance, - )?; - - let transfer_amount = token::Amount::native_whole(10_000_000); - - // make a transfer from Albert to Bertha, signed by Christel - this should - // be rejected - let mut unauthorized_transfer = helpers::attempt_red_tokens_transfer( - &test, - &rpc_addr, - &multitoken_alias, - ALBERT, - BERTHA, - CHRISTEL_KEY, - &transfer_amount, - )?; - unauthorized_transfer.exp_string("Transaction applied with result")?; - unauthorized_transfer.exp_string("Transaction is invalid")?; - unauthorized_transfer.exp_string(&format!("Rejected: {albert_addr}"))?; - unauthorized_transfer.assert_success(); - - let albert_balance = helpers::fetch_red_token_balance( - &test, - &rpc_addr, - &multitoken_alias, - ALBERT, - )?; - assert_eq!(albert_balance, albert_starting_red_balance); - - // make a transfer from Albert to Bertha, signed by Albert - this should - // be accepted - let mut authorized_transfer = helpers::attempt_red_tokens_transfer( - &test, - &rpc_addr, - &multitoken_alias, - ALBERT, - BERTHA, - ALBERT_KEY, - &token::Amount::native_whole(10_000_000), - )?; - authorized_transfer.exp_string("Transaction applied with result")?; - authorized_transfer.exp_string("Transaction is valid")?; - authorized_transfer.assert_success(); - - let albert_balance = helpers::fetch_red_token_balance( - &test, - &rpc_addr, - &multitoken_alias, - ALBERT, - )?; - assert_eq!( - albert_balance, - albert_starting_red_balance - transfer_amount - ); - Ok(()) -} - -#[test] -fn test_multitoken_transfer_established_to_implicit() -> Result<()> { - let (test, _ledger) = e2e::helpers::setup_single_node_test()?; - - let rpc_addr = get_actor_rpc(&test, &Who::Validator(0)); - let multitoken_alias = helpers::init_multitoken_vp(&test, &rpc_addr)?; - - let multitoken_vp_addr = - e2e::helpers::find_address(&test, &multitoken_alias)?; - println!("Fake multitoken VP established at {}", multitoken_vp_addr); - - // create an established account that Albert controls - let established_alias = "established"; - e2e::helpers::init_established_account( - &test, - &rpc_addr, - ALBERT, - ALBERT_KEY, - established_alias, - )?; - - let established_starting_red_balance = - token::Amount::native_whole(100_000_000); - // mint some red tokens for the established account - let established_addr = - e2e::helpers::find_address(&test, established_alias)?; - helpers::mint_red_tokens( - &test, - &rpc_addr, - &multitoken_vp_addr, - &established_addr, - &established_starting_red_balance, - )?; - - let transfer_amount = token::Amount::native_whole(10_000_000); - // attempt an unauthorized transfer to Albert from the established account - let mut unauthorized_transfer = helpers::attempt_red_tokens_transfer( - &test, - &rpc_addr, - &multitoken_alias, - established_alias, - BERTHA, - CHRISTEL_KEY, - &transfer_amount, - )?; - unauthorized_transfer.exp_string("Transaction applied with result")?; - unauthorized_transfer.exp_string("Transaction is invalid")?; - unauthorized_transfer - .exp_string(&format!("Rejected: {established_addr}"))?; - unauthorized_transfer.assert_success(); - - let established_balance = helpers::fetch_red_token_balance( - &test, - &rpc_addr, - &multitoken_alias, - established_alias, - )?; - assert_eq!(established_balance, established_starting_red_balance); - - // attempt an authorized transfer to Albert from the established account - let mut authorized_transfer = helpers::attempt_red_tokens_transfer( - &test, - &rpc_addr, - &multitoken_alias, - established_alias, - BERTHA, - ALBERT_KEY, - &transfer_amount, - )?; - authorized_transfer.exp_string("Transaction applied with result")?; - authorized_transfer.exp_string("Transaction is valid")?; - authorized_transfer.assert_success(); - - let established_balance = helpers::fetch_red_token_balance( - &test, - &rpc_addr, - &multitoken_alias, - established_alias, - )?; - assert_eq!( - established_balance, - established_starting_red_balance - transfer_amount - ); - - Ok(()) -} - -#[test] -fn test_multitoken_transfer_implicit_to_established() -> Result<()> { - let (test, _ledger) = e2e::helpers::setup_single_node_test()?; - - let rpc_addr = get_actor_rpc(&test, &Who::Validator(0)); - let multitoken_alias = helpers::init_multitoken_vp(&test, &rpc_addr)?; - - let multitoken_vp_addr = - e2e::helpers::find_address(&test, &multitoken_alias)?; - println!("Fake multitoken VP established at {}", multitoken_vp_addr); - - // create an established account controlled by Bertha - let established_alias = "established"; - e2e::helpers::init_established_account( - &test, - &rpc_addr, - BERTHA, - BERTHA_KEY, - established_alias, - )?; - - let albert_addr = e2e::helpers::find_address(&test, ALBERT)?; - let albert_starting_red_balance = token::Amount::native_whole(100_000_000); - helpers::mint_red_tokens( - &test, - &rpc_addr, - &multitoken_vp_addr, - &albert_addr, - &albert_starting_red_balance, - )?; - - let transfer_amount = token::Amount::native_whole(10_000_000); - - // attempt an unauthorized transfer from Albert to the established account - let mut unauthorized_transfer = helpers::attempt_red_tokens_transfer( - &test, - &rpc_addr, - &multitoken_alias, - ALBERT, - established_alias, - CHRISTEL_KEY, - &transfer_amount, - )?; - unauthorized_transfer.exp_string("Transaction applied with result")?; - unauthorized_transfer.exp_string("Transaction is invalid")?; - unauthorized_transfer.exp_string(&format!("Rejected: {albert_addr}"))?; - unauthorized_transfer.assert_success(); - - let albert_balance = helpers::fetch_red_token_balance( - &test, - &rpc_addr, - &multitoken_alias, - ALBERT, - )?; - assert_eq!(albert_balance, albert_starting_red_balance); - - // attempt an authorized transfer to Albert from the established account - let mut authorized_transfer = helpers::attempt_red_tokens_transfer( - &test, - &rpc_addr, - &multitoken_alias, - ALBERT, - established_alias, - ALBERT_KEY, - &transfer_amount, - )?; - authorized_transfer.exp_string("Transaction applied with result")?; - authorized_transfer.exp_string("Transaction is valid")?; - authorized_transfer.assert_success(); - - let albert_balance = helpers::fetch_red_token_balance( - &test, - &rpc_addr, - &multitoken_alias, - ALBERT, - )?; - assert_eq!( - albert_balance, - albert_starting_red_balance - transfer_amount - ); - - Ok(()) -} - -#[test] -fn test_multitoken_transfer_established_to_established() -> Result<()> { - let (test, _ledger) = e2e::helpers::setup_single_node_test()?; - - let rpc_addr = get_actor_rpc(&test, &Who::Validator(0)); - let multitoken_alias = helpers::init_multitoken_vp(&test, &rpc_addr)?; - - let multitoken_vp_addr = - e2e::helpers::find_address(&test, &multitoken_alias)?; - println!("Fake multitoken VP established at {}", multitoken_vp_addr); - - // create an established account that Albert controls - let established_alias = "established"; - e2e::helpers::init_established_account( - &test, - &rpc_addr, - ALBERT, - ALBERT_KEY, - established_alias, - )?; - - let established_starting_red_balance = - token::Amount::native_whole(100_000_000); - // mint some red tokens for the established account - let established_addr = - e2e::helpers::find_address(&test, established_alias)?; - helpers::mint_red_tokens( - &test, - &rpc_addr, - &multitoken_vp_addr, - &established_addr, - &established_starting_red_balance, - )?; - - // create another established account to receive transfers - let receiver_alias = "receiver"; - e2e::helpers::init_established_account( - &test, - &rpc_addr, - BERTHA, - BERTHA_KEY, - receiver_alias, - )?; - - let established_starting_red_balance = - token::Amount::native_whole(100_000_000); - // mint some red tokens for the established account - let established_addr = - e2e::helpers::find_address(&test, established_alias)?; - helpers::mint_red_tokens( - &test, - &rpc_addr, - &multitoken_vp_addr, - &established_addr, - &established_starting_red_balance, - )?; - - let transfer_amount = token::Amount::native_whole(10_000_000); - - // attempt an unauthorized transfer - let mut unauthorized_transfer = helpers::attempt_red_tokens_transfer( - &test, - &rpc_addr, - &multitoken_alias, - established_alias, - receiver_alias, - CHRISTEL_KEY, - &transfer_amount, - )?; - unauthorized_transfer.exp_string("Transaction applied with result")?; - unauthorized_transfer.exp_string("Transaction is invalid")?; - unauthorized_transfer - .exp_string(&format!("Rejected: {established_addr}"))?; - unauthorized_transfer.assert_success(); - - let established_balance = helpers::fetch_red_token_balance( - &test, - &rpc_addr, - &multitoken_alias, - established_alias, - )?; - assert_eq!(established_balance, established_starting_red_balance); - - // attempt an authorized transfer which should succeed - let mut authorized_transfer = helpers::attempt_red_tokens_transfer( - &test, - &rpc_addr, - &multitoken_alias, - established_alias, - receiver_alias, - ALBERT_KEY, - &transfer_amount, - )?; - authorized_transfer.exp_string("Transaction applied with result")?; - authorized_transfer.exp_string("Transaction is valid")?; - authorized_transfer.assert_success(); - - let established_balance = helpers::fetch_red_token_balance( - &test, - &rpc_addr, - &multitoken_alias, - established_alias, - )?; - assert_eq!( - established_balance, - established_starting_red_balance - transfer_amount - ); - - Ok(()) -} From 755586b7ed09e78682ada8003714812428e99fee Mon Sep 17 00:00:00 2001 From: satan Date: Thu, 2 Nov 2023 10:47:21 +0100 Subject: [PATCH 22/47] Namada 0.24.1 --- .../bug-fixes/2047-win-build.md | 0 .../improvements/2064-wasm-download.md | 0 .changelog/v0.24.1/summary.md | 1 + CHANGELOG.md | 14 ++++++ Cargo.lock | 28 ++++++------ Cargo.toml | 2 +- wasm/Cargo.lock | 28 ++++++------ wasm/checksums.json | 42 +++++++++--------- wasm/tx_template/Cargo.toml | 2 +- wasm/vp_template/Cargo.toml | 2 +- wasm/wasm_source/Cargo.toml | 2 +- wasm_for_tests/tx_memory_limit.wasm | Bin 449805 -> 467595 bytes wasm_for_tests/tx_mint_tokens.wasm | Bin 595637 -> 595637 bytes wasm_for_tests/tx_no_op.wasm | Bin 368345 -> 386288 bytes wasm_for_tests/tx_proposal_code.wasm | Bin 528766 -> 549613 bytes wasm_for_tests/tx_read_storage_key.wasm | Bin 452867 -> 468670 bytes wasm_for_tests/tx_write.wasm | Bin 461123 -> 477357 bytes wasm_for_tests/tx_write_storage_key.wasm | Bin 465585 -> 465585 bytes wasm_for_tests/vp_always_false.wasm | Bin 425746 -> 445732 bytes wasm_for_tests/vp_always_true.wasm | Bin 425743 -> 445717 bytes wasm_for_tests/vp_eval.wasm | Bin 498635 -> 516510 bytes wasm_for_tests/vp_memory_limit.wasm | Bin 477026 -> 494900 bytes wasm_for_tests/vp_read_storage_key.wasm | Bin 483839 -> 500134 bytes wasm_for_tests/wasm_source/Cargo.lock | 24 +++++----- wasm_for_tests/wasm_source/Cargo.toml | 2 +- 25 files changed, 81 insertions(+), 66 deletions(-) rename .changelog/{unreleased => v0.24.1}/bug-fixes/2047-win-build.md (100%) rename .changelog/{unreleased => v0.24.1}/improvements/2064-wasm-download.md (100%) create mode 100644 .changelog/v0.24.1/summary.md diff --git a/.changelog/unreleased/bug-fixes/2047-win-build.md b/.changelog/v0.24.1/bug-fixes/2047-win-build.md similarity index 100% rename from .changelog/unreleased/bug-fixes/2047-win-build.md rename to .changelog/v0.24.1/bug-fixes/2047-win-build.md diff --git a/.changelog/unreleased/improvements/2064-wasm-download.md b/.changelog/v0.24.1/improvements/2064-wasm-download.md similarity index 100% rename from .changelog/unreleased/improvements/2064-wasm-download.md rename to .changelog/v0.24.1/improvements/2064-wasm-download.md diff --git a/.changelog/v0.24.1/summary.md b/.changelog/v0.24.1/summary.md new file mode 100644 index 0000000000..67ae3d9d1a --- /dev/null +++ b/.changelog/v0.24.1/summary.md @@ -0,0 +1 @@ +Namada 0.24.1 is patch release addressing ledger startup problems with wasm artifacts and several other minor fixes. diff --git a/CHANGELOG.md b/CHANGELOG.md index 179ae91e99..b4b1425391 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # CHANGELOG +## v0.24.1 + +Namada 0.24.1 is patch release addressing ledger startup problems with wasm artifacts and several other minor fixes. + +### BUG FIXES + +- Fix Windows build by disabling RocksDB jemalloc feature. + ([\#2047](https://github.com/anoma/namada/pull/2047)) + +### IMPROVEMENTS + +- Define the wasm download endpoint via environemnt variable. + ([\#2064](https://github.com/anoma/namada/pull/2064)) + ## v0.24.0 Namada 0.24.0 is a minor release that introduces an SDK crate, PoS redelegation, various updates and fixes for IBC, PoS, governance, ETH bridge and the ledger. diff --git a/Cargo.lock b/Cargo.lock index 9c150d5178..2f5ac23b68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3939,7 +3939,7 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "namada" -version = "0.24.0" +version = "0.24.1" dependencies = [ "assert_matches", "async-trait", @@ -4009,7 +4009,7 @@ dependencies = [ [[package]] name = "namada_apps" -version = "0.24.0" +version = "0.24.1" dependencies = [ "ark-serialize", "ark-std", @@ -4100,7 +4100,7 @@ dependencies = [ [[package]] name = "namada_benchmarks" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "borsh-ext", @@ -4115,7 +4115,7 @@ dependencies = [ [[package]] name = "namada_core" -version = "0.24.0" +version = "0.24.1" dependencies = [ "ark-bls12-381", "ark-ec", @@ -4174,7 +4174,7 @@ dependencies = [ [[package]] name = "namada_encoding_spec" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "itertools 0.10.5", @@ -4185,7 +4185,7 @@ dependencies = [ [[package]] name = "namada_ethereum_bridge" -version = "0.24.0" +version = "0.24.1" dependencies = [ "assert_matches", "borsh 1.0.0-alpha.4", @@ -4210,7 +4210,7 @@ dependencies = [ [[package]] name = "namada_macros" -version = "0.24.0" +version = "0.24.1" dependencies = [ "proc-macro2", "quote", @@ -4219,7 +4219,7 @@ dependencies = [ [[package]] name = "namada_proof_of_stake" -version = "0.24.0" +version = "0.24.1" dependencies = [ "assert_matches", "borsh 1.0.0-alpha.4", @@ -4240,7 +4240,7 @@ dependencies = [ [[package]] name = "namada_sdk" -version = "0.24.0" +version = "0.24.1" dependencies = [ "assert_matches", "async-trait", @@ -4289,7 +4289,7 @@ dependencies = [ [[package]] name = "namada_test_utils" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "namada_core", @@ -4298,7 +4298,7 @@ dependencies = [ [[package]] name = "namada_tests" -version = "0.24.0" +version = "0.24.1" dependencies = [ "assert_cmd", "async-trait", @@ -4348,7 +4348,7 @@ dependencies = [ [[package]] name = "namada_tx_prelude" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "borsh-ext", @@ -4363,7 +4363,7 @@ dependencies = [ [[package]] name = "namada_vm_env" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "masp_primitives", @@ -4372,7 +4372,7 @@ dependencies = [ [[package]] name = "namada_vp_prelude" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "borsh-ext", diff --git a/Cargo.toml b/Cargo.toml index 50ad557a43..9d1885001e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ keywords = ["blockchain", "privacy", "crypto", "protocol", "network"] license = "GPL-3.0" readme = "README.md" repository = "https://github.com/anoma/namada" -version = "0.24.0" +version = "0.24.1" [workspace.dependencies] ark-bls12-381 = {version = "0.3"} diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index fe18edd852..eda093d527 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -3259,7 +3259,7 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "namada" -version = "0.24.0" +version = "0.24.1" dependencies = [ "async-trait", "bimap", @@ -3320,7 +3320,7 @@ dependencies = [ [[package]] name = "namada_core" -version = "0.24.0" +version = "0.24.1" dependencies = [ "ark-bls12-381", "ark-ec", @@ -3374,7 +3374,7 @@ dependencies = [ [[package]] name = "namada_ethereum_bridge" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "borsh-ext", @@ -3395,7 +3395,7 @@ dependencies = [ [[package]] name = "namada_macros" -version = "0.24.0" +version = "0.24.1" dependencies = [ "proc-macro2", "quote", @@ -3404,7 +3404,7 @@ dependencies = [ [[package]] name = "namada_proof_of_stake" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "data-encoding", @@ -3418,7 +3418,7 @@ dependencies = [ [[package]] name = "namada_sdk" -version = "0.24.0" +version = "0.24.1" dependencies = [ "async-trait", "bimap", @@ -3462,7 +3462,7 @@ dependencies = [ [[package]] name = "namada_test_utils" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "namada_core", @@ -3471,7 +3471,7 @@ dependencies = [ [[package]] name = "namada_tests" -version = "0.24.0" +version = "0.24.1" dependencies = [ "async-trait", "chrono", @@ -3503,7 +3503,7 @@ dependencies = [ [[package]] name = "namada_tx_prelude" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "borsh-ext", @@ -3518,7 +3518,7 @@ dependencies = [ [[package]] name = "namada_vm_env" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "masp_primitives", @@ -3527,7 +3527,7 @@ dependencies = [ [[package]] name = "namada_vp_prelude" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "borsh-ext", @@ -3541,7 +3541,7 @@ dependencies = [ [[package]] name = "namada_wasm" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "getrandom 0.2.9", @@ -5816,7 +5816,7 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tx_template" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "getrandom 0.2.9", @@ -5939,7 +5939,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vp_template" -version = "0.24.0" +version = "0.24.1" dependencies = [ "borsh 1.0.0-alpha.4", "getrandom 0.2.9", diff --git a/wasm/checksums.json b/wasm/checksums.json index a46ac8e119..873f8ae6a1 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,23 +1,23 @@ { - "tx_bond.wasm": "tx_bond.9fdaf63c3052a44195c8280d749e1682654ee2bad6edd25c6551bdc8197f7512.wasm", - "tx_bridge_pool.wasm": "tx_bridge_pool.605e9b7e8b0ff27863567b56d3f5e757783f5c75c596260d7c0805a47b7a877e.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.f036ef369e754cb6c75453f9b380f95be588d330f993284b08b477231d1ed0eb.wasm", - "tx_ibc.wasm": "tx_ibc.95832c0d92bb83ac2cde7b3c7cde9edae7c21f8dd5b32d93a9368093d58b5f15.wasm", - "tx_init_account.wasm": "tx_init_account.f525992552827cfd7693d568879dfd1f54406af9906b8827bc86e4beb21a0efe.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.9c3cf15da2b14e2c9898f5910cbfcffb80ef14dfcd0082dc9039b170cb3cc208.wasm", - "tx_init_validator.wasm": "tx_init_validator.1b695e5869532bbaebe5d99d48a267ae0891e9c4470abbbad5d0b37118e35ca6.wasm", - "tx_redelegate.wasm": "tx_redelegate.63f6c6b2f1582b357870ca9b779e88139c1e859b18fe7ada71dfdf5842fd83c7.wasm", - "tx_resign_steward.wasm": "tx_resign_steward.c21e812ba4a095d4b5eca562a94a2ef5071522a95f87a641c5f6e2bb2a4c890d.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.e5b1b047b44cde0cc8f9f89ba15f611dbefe8a3c060c4012363fdf07e92c6875.wasm", - "tx_transfer.wasm": "tx_transfer.c34a0a0b15ae448db7bc455bab8c59e6510c5fe9cb2d6386897030c37a189e42.wasm", - "tx_unbond.wasm": "tx_unbond.e846fd512237bdee232edc8d87e637ab3631942e3e1eda635d595e96d52bf489.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.1c8db6285b60d8ce80ce5c1ff7d43755e50cd3749fad5fb286f66592112ba4cd.wasm", - "tx_update_account.wasm": "tx_update_account.9f9493f2fb39d0a5f0fa3327a78828e387ba15690ad86e96d4b8cdf509cf1efc.wasm", - "tx_update_steward_commission.wasm": "tx_update_steward_commission.92bcf758cbb58be93ae5d7fde616a3974016f0d41c09b7333bc6041bc23e8d30.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.825ffa0c7c7c588cb668af4ff6ba3ce5c5d1c37c0728c18a7eb807a02da52158.wasm", - "tx_withdraw.wasm": "tx_withdraw.0225f3c8ac6e4f2d4636568048035156c936d9a8bc12d4c0d619598089af6f47.wasm", - "vp_implicit.wasm": "vp_implicit.e8dd924d43f11854724350c3e082a35898dc03e21f1543c117cff6763d881d2b.wasm", - "vp_masp.wasm": "vp_masp.64553be3434516e8de6b3e0a445582dce7d7975bd9765f4fbd9875bdbf0f9feb.wasm", - "vp_user.wasm": "vp_user.37fee9afa11b5899c824b6b8e1f0a626f5fd05c9813af454081a797772ca3de6.wasm", - "vp_validator.wasm": "vp_validator.e6283c764480d04942c0fa2fb3d6e3d1cd18565b235b09e6a6deb7918c808f31.wasm" + "tx_bond.wasm": "tx_bond.dbfe330a50d8d3511e125f611d88a3a32952de2077345ee79b5b320e9d11ece6.wasm", + "tx_bridge_pool.wasm": "tx_bridge_pool.ae875a5ddd27035b768678b2dbf70c8ecfa1bce316faee01b26bd0cbffb48b19.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.39b26c8ff0c06220163322c15a0f304232f5e3a6333c147d10d5a65e3d06c061.wasm", + "tx_ibc.wasm": "tx_ibc.d550ec9d20d965d91530ca4df74a75eeb27f88ae4484da3eef4b5599727bd94f.wasm", + "tx_init_account.wasm": "tx_init_account.06c8639a68cecf1160818bd92d81974a9990aceea08cbbcc47e18c51b2ec9449.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.daa176497bb1f4bb97346d1ba2387e0cc84e704eca70ef78a56228e03969fb76.wasm", + "tx_init_validator.wasm": "tx_init_validator.8d393d2cfdf55fd88beb4f26840d22e98da5e0390aac5fbcf1dc314e0eb4a9ca.wasm", + "tx_redelegate.wasm": "tx_redelegate.c586dc50a452948e2dd79718519bfa7b966999126ba52807bc4ba3a6f7a6290d.wasm", + "tx_resign_steward.wasm": "tx_resign_steward.994a9a3e10baacf900087382d2d8bb8045033927a3d040bf882c6d60ecfe0d5b.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.aceb2779c307f96e377e3e189589ea54e7b05d712cac27c6943e1d941334b421.wasm", + "tx_transfer.wasm": "tx_transfer.adab8a00709de083b6b4b534d4dfe002d479085ce1a766718dd3afe920659ac3.wasm", + "tx_unbond.wasm": "tx_unbond.a1dc615400ad625cf9b82ea28735a2fbffd1dd6745761eff6df4d08358162195.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.9debbc06681dd602e8a34ccf20d50dddee9963447409d2597893bf54937e7cee.wasm", + "tx_update_account.wasm": "tx_update_account.3917c5c25d2ffb0d717b9475a74d825840db94d48fcb4a0fe7450f3b32258cbe.wasm", + "tx_update_steward_commission.wasm": "tx_update_steward_commission.1a6f68eaf3b2eff9da777ce8c73f94865c49fa2ef94b5c0e1d65f46960cc9a5e.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.3f818de8c638e24249a26ccd20ec6a1700092c3700dcf6b91aea11058586ebef.wasm", + "tx_withdraw.wasm": "tx_withdraw.3cf7e6b6b77abd0eae58302659c9de31919894f36c0f7dbd85a6c853cd023483.wasm", + "vp_implicit.wasm": "vp_implicit.46d4206719acaaa8d0a422b6f969719b458cc5ca7c25677eb4bf23b52b4b6490.wasm", + "vp_masp.wasm": "vp_masp.c1e8f569ed3ad2bf5152e34f8ea9e255439066e0800ae73620a7be9a976f2ff7.wasm", + "vp_user.wasm": "vp_user.6b5bf2c3a5367d4f379187a8d2cf71457085aa1184a2b98b9e00ab6978847e08.wasm", + "vp_validator.wasm": "vp_validator.a683a9724e335cdf8f33eaee9ac3127a21d9f76e9769a5f3aea63b3bf04f2011.wasm" } \ No newline at end of file diff --git a/wasm/tx_template/Cargo.toml b/wasm/tx_template/Cargo.toml index b65a60ce0a..c7ead2a75f 100644 --- a/wasm/tx_template/Cargo.toml +++ b/wasm/tx_template/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" license = "GPL-3.0" name = "tx_template" resolver = "2" -version = "0.24.0" +version = "0.24.1" [lib] crate-type = ["cdylib"] diff --git a/wasm/vp_template/Cargo.toml b/wasm/vp_template/Cargo.toml index be1215b857..154d2166fd 100644 --- a/wasm/vp_template/Cargo.toml +++ b/wasm/vp_template/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" license = "GPL-3.0" name = "vp_template" resolver = "2" -version = "0.24.0" +version = "0.24.1" [lib] crate-type = ["cdylib"] diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index acb6629852..932a4fbc52 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -4,7 +4,7 @@ edition = "2021" license = "GPL-3.0" name = "namada_wasm" resolver = "2" -version = "0.24.0" +version = "0.24.1" [lib] crate-type = ["cdylib"] diff --git a/wasm_for_tests/tx_memory_limit.wasm b/wasm_for_tests/tx_memory_limit.wasm index 6d73c405902a863fae16b9c74cc77c5d0dc30611..e10747856edf799d1b4407ee50cccaea0b90c66b 100755 GIT binary patch delta 84969 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ℑY0t=OspJI0?AN1I4pCr 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 diff --git a/wasm_for_tests/tx_mint_tokens.wasm b/wasm_for_tests/tx_mint_tokens.wasm index 217ba3975c3e1bf61fde0e4b1c0f9185b7d1b94a..ee848eed089b10d16c247d1d6dd6c726dacf93f9 100755 GIT binary patch delta 95 zcmdnGR%Pp2m4+6^ElmFAocW4u3M`IK7;>jam@`$#vMMqtFgh}L@G^2cGAJ-8u)2e} oMZ7Fh3T)Hgnlq^~vb76aFaa?$5VHU=D-eUk*tZK?a4fn909Owc{Qv*} delta 95 zcmdnGR%Pp2m4+6^ElmFAoVki@3M`IK81kn_m@`$#vMDksFgh}L@G^2cGAJ-8u(^Y| oMZ7Fh3ar!Lnlq^~vbGCbFaa?$5VHU=h|dPZ?AwJcI2PRl09P&*{Qv*} diff --git a/wasm_for_tests/tx_no_op.wasm b/wasm_for_tests/tx_no_op.wasm index c34ecd040a80399f5e27a8be152f49e55b8ba160..01bda735b82b53c55ac742153a99cacd72f7e6d3 100755 GIT binary patch delta 72344 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 diff --git a/wasm_for_tests/tx_proposal_code.wasm b/wasm_for_tests/tx_proposal_code.wasm index adffd2edf1402faa9d1b23430d39d1253eeef66c..dc792449b7d755b360bd54f6aca622a6ac51787f 100755 GIT binary patch delta 127637 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⁢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 ktubcs+O@_wWks@at)VOGR diff --git a/wasm_for_tests/tx_read_storage_key.wasm b/wasm_for_tests/tx_read_storage_key.wasm index 30a43f536730b055c4df33c5e1bb23483f57a874..0ade8a1c281ebbc27a40129b8653670830b39c08 100755 GIT binary patch delta 84236 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 N{;i)_`g|X(AC7D)h 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 KzRk|S@c#puuQS{L delta 68552 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 I=K1FT0lu|>#sB~S diff --git a/wasm_for_tests/tx_write.wasm b/wasm_for_tests/tx_write.wasm index c9144cf73dd5d186170613391f9c9a49ea95a4fb..7e06edb91da89409948241034c10fad9992daa79 100755 GIT binary patch delta 88429 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 H3+Dd=F+qsE delta 71657 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 K?MA(9^}PUGr4wQR delta 59 zcmdnERc7N>nT8g|Ele#voVki@3M`IK81kq0_Apg4vQ202Wzu3~Z8z#=0%B$$W&vW> K?MA(9^}PUGoD*UI diff --git a/wasm_for_tests/vp_always_false.wasm b/wasm_for_tests/vp_always_false.wasm index 8f00805b20aa05b355b694f054fe2515447e1b4e..6d7fb32d2df5c1ec2891f1ceab22331a2f82f623 100755 GIT binary patch delta 89352 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 delta 69794 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 +-ray|W1Y60~1r@E!y-%;1sKdh`A~ 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 diff --git a/wasm_for_tests/vp_always_true.wasm b/wasm_for_tests/vp_always_true.wasm index 1d7337441661d38159a7eb7a0b5223f40f8e0f17..1655683098a887144357d4d3f7da38f4bd028f21 100755 GIT binary patch delta 89183 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