diff --git a/.changelog/unreleased/features/1765-multisignature-draft-rebase-0.20.0.md b/.changelog/unreleased/features/1765-multisignature-draft-rebase-0.20.0.md new file mode 100644 index 00000000000..25697effb8c --- /dev/null +++ b/.changelog/unreleased/features/1765-multisignature-draft-rebase-0.20.0.md @@ -0,0 +1,4 @@ +- Introduce multisignature accounts and transaction format. It is now possible + to supply multiple public keys when creating a new account/validator and + specify the minimum number of signatures required to authorize a transaction. + ([\#1765](https://github.com/anoma/namada/pull/1765)) \ No newline at end of file diff --git a/.changelog/unreleased/features/1803-refactor-governance.md b/.changelog/unreleased/features/1803-refactor-governance.md new file mode 100644 index 00000000000..5ef71605056 --- /dev/null +++ b/.changelog/unreleased/features/1803-refactor-governance.md @@ -0,0 +1,2 @@ +- Introduce a simplified version of Public Good Fundings. + ([\#1803](https://github.com/anoma/namada/pull/1803)) \ No newline at end of file diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index bf91fc15882..025bd2b96f3 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -10,10 +10,10 @@ "e2e::ledger_tests::masp_pinned_txs": 75, "e2e::ledger_tests::masp_txs_and_queries": 282, "e2e::ledger_tests::pos_bonds": 77, + "e2e::ledger_tests::implicit_account_reveal_pk": 30, "e2e::ledger_tests::pos_init_validator": 40, "e2e::ledger_tests::proposal_offline": 21, "e2e::ledger_tests::pgf_governance_proposal": 100, - "e2e::ledger_tests::eth_governance_proposal": 100, "e2e::ledger_tests::proposal_submission": 200, "e2e::ledger_tests::run_ledger": 5, "e2e::ledger_tests::run_ledger_load_state_and_reset": 23, diff --git a/Cargo.lock b/Cargo.lock index 9517b5cf7b2..6d57d9b2089 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4561,6 +4561,7 @@ dependencies = [ "prost", "rand 0.8.5", "regex", + "serde 1.0.163", "serde_json", "sha2 0.9.9", "tempfile", diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index d6a3cbdaf91..7a03a2b49b7 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -3,6 +3,7 @@ use color_eyre::eyre::{eyre, Report, Result}; use namada::ledger::eth_bridge::bridge_pool; use namada::ledger::rpc::wait_until_node_is_synched; +use namada::ledger::tx::dump_tx; use namada::ledger::{signing, tx as sdk_tx}; use namada::types::control_flow::ProceedOrElse; use namada_apps::cli; @@ -67,7 +68,7 @@ pub async fn main() -> Result<()> { tx::submit_ibc_transfer::(&client, ctx, args) .await?; } - Sub::TxUpdateVp(TxUpdateVp(mut args)) => { + Sub::TxUpdateAccount(TxUpdateAccount(mut args)) => { let client = HttpClient::new(utils::take_config_address( &mut args.tx.ledger_address, )) @@ -76,8 +77,10 @@ pub async fn main() -> Result<()> { .await .proceed_or_else(error)?; let args = args.to_sdk(&mut ctx); - tx::submit_update_vp::(&client, &mut ctx, args) - .await?; + tx::submit_update_account::( + &client, &mut ctx, args, + ) + .await?; } Sub::TxInitAccount(TxInitAccount(mut args)) => { let client = HttpClient::new(utils::take_config_address( @@ -213,26 +216,51 @@ pub async fn main() -> Result<()> { .proceed_or_else(error)?; let args = args.to_sdk(&mut ctx); let tx_args = args.tx.clone(); - let (mut tx, addr, pk) = bridge_pool::build_bridge_pool_tx( + + let default_signer = + signing::signer_from_address(Some(args.sender.clone())); + let signing_data = signing::aux_signing_data( &client, &mut ctx.wallet, - args, + &args.tx, + &args.sender, + default_signer, ) - .await - .unwrap(); + .await?; + tx::submit_reveal_aux( &client, &mut ctx, - &tx_args, - addr, - pk.clone(), - &mut tx, + tx_args.clone(), + &args.sender, ) .await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &tx_args, &pk) - .await?; - sdk_tx::process_tx(&client, &mut ctx.wallet, &tx_args, tx) + + let tx_builder = bridge_pool::build_bridge_pool_tx( + &client, + args.clone(), + signing_data.gas_payer.clone(), + ) + .await?; + + if args.tx.dump_tx { + dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &tx_args, + tx_builder, + signing_data, + )?; + + sdk_tx::process_tx( + &client, + &mut ctx.wallet, + &tx_args, + tx_builder, + ) .await?; + } } Sub::TxUnjailValidator(TxUnjailValidator(mut args)) => { let client = HttpClient::new(utils::take_config_address( @@ -463,6 +491,16 @@ pub async fn main() -> Result<()> { let args = args.to_sdk(&mut ctx); rpc::query_protocol_parameters(&client, args).await; } + Sub::QueryAccount(QueryAccount(args)) => { + let client = + HttpClient::new(args.query.ledger_address.clone()) + .unwrap(); + wait_until_node_is_synched(&client) + .await + .proceed_or_else(error)?; + let args = args.to_sdk(&mut ctx); + rpc::query_account(&client, args).await; + } } } cli::NamadaClient::WithoutContext(cmd, global_args) => match cmd { diff --git a/apps/src/bin/namada/cli.rs b/apps/src/bin/namada/cli.rs index 8c7a1e0b494..0259ba525aa 100644 --- a/apps/src/bin/namada/cli.rs +++ b/apps/src/bin/namada/cli.rs @@ -47,7 +47,7 @@ fn handle_command(cmd: cli::cmds::Namada, raw_sub_cmd: String) -> Result<()> { | cli::cmds::Namada::TxCustom(_) | cli::cmds::Namada::TxTransfer(_) | cli::cmds::Namada::TxIbcTransfer(_) - | cli::cmds::Namada::TxUpdateVp(_) + | cli::cmds::Namada::TxUpdateAccount(_) | cli::cmds::Namada::TxRevealPk(_) | cli::cmds::Namada::TxInitProposal(_) | cli::cmds::Namada::TxVoteProposal(_) => { diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 1c0bdba287d..4f12f697242 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -57,7 +57,7 @@ pub mod cmds { TxCustom(TxCustom), TxTransfer(TxTransfer), TxIbcTransfer(TxIbcTransfer), - TxUpdateVp(TxUpdateVp), + TxUpdateAccount(TxUpdateAccount), TxInitProposal(TxInitProposal), TxVoteProposal(TxVoteProposal), TxRevealPk(TxRevealPk), @@ -74,7 +74,7 @@ pub mod cmds { .subcommand(TxCustom::def()) .subcommand(TxTransfer::def()) .subcommand(TxIbcTransfer::def()) - .subcommand(TxUpdateVp::def()) + .subcommand(TxUpdateAccount::def()) .subcommand(TxInitProposal::def()) .subcommand(TxVoteProposal::def()) .subcommand(TxRevealPk::def()) @@ -92,7 +92,8 @@ pub mod cmds { let tx_transfer = SubCmd::parse(matches).map(Self::TxTransfer); let tx_ibc_transfer = SubCmd::parse(matches).map(Self::TxIbcTransfer); - let tx_update_vp = SubCmd::parse(matches).map(Self::TxUpdateVp); + let tx_update_account = + SubCmd::parse(matches).map(Self::TxUpdateAccount); let tx_init_proposal = SubCmd::parse(matches).map(Self::TxInitProposal); let tx_vote_proposal = @@ -106,7 +107,7 @@ pub mod cmds { .or(tx_custom) .or(tx_transfer) .or(tx_ibc_transfer) - .or(tx_update_vp) + .or(tx_update_account) .or(tx_init_proposal) .or(tx_vote_proposal) .or(tx_reveal_pk) @@ -213,7 +214,7 @@ pub mod cmds { .subcommand(TxCustom::def().display_order(1)) .subcommand(TxTransfer::def().display_order(1)) .subcommand(TxIbcTransfer::def().display_order(1)) - .subcommand(TxUpdateVp::def().display_order(1)) + .subcommand(TxUpdateAccount::def().display_order(1)) .subcommand(TxInitAccount::def().display_order(1)) .subcommand(TxRevealPk::def().display_order(1)) // Proposal transactions @@ -230,6 +231,7 @@ pub mod cmds { .subcommand(AddToEthBridgePool::def().display_order(3)) // Queries .subcommand(QueryEpoch::def().display_order(4)) + .subcommand(QueryAccount::def().display_order(4)) .subcommand(QueryTransfers::def().display_order(4)) .subcommand(QueryConversions::def().display_order(4)) .subcommand(QueryBlock::def().display_order(4)) @@ -244,9 +246,12 @@ pub mod cmds { .subcommand(QueryProposal::def().display_order(4)) .subcommand(QueryProposalResult::def().display_order(4)) .subcommand(QueryProtocolParameters::def().display_order(4)) + .subcommand(QueryPgf::def().display_order(4)) .subcommand(QueryValidatorState::def().display_order(4)) + // Actions + .subcommand(SignTx::def().display_order(5)) // Utils - .subcommand(Utils::def().display_order(5)) + .subcommand(Utils::def().display_order(6)) } fn parse(matches: &ArgMatches) -> Option { @@ -254,7 +259,8 @@ pub mod cmds { let tx_custom = Self::parse_with_ctx(matches, TxCustom); let tx_transfer = Self::parse_with_ctx(matches, TxTransfer); let tx_ibc_transfer = Self::parse_with_ctx(matches, TxIbcTransfer); - let tx_update_vp = Self::parse_with_ctx(matches, TxUpdateVp); + let tx_update_account = + Self::parse_with_ctx(matches, TxUpdateAccount); let tx_init_account = Self::parse_with_ctx(matches, TxInitAccount); let tx_init_validator = Self::parse_with_ctx(matches, TxInitValidator); @@ -271,6 +277,7 @@ pub mod cmds { let unbond = Self::parse_with_ctx(matches, Unbond); let withdraw = Self::parse_with_ctx(matches, Withdraw); let query_epoch = Self::parse_with_ctx(matches, QueryEpoch); + let query_account = Self::parse_with_ctx(matches, QueryAccount); let query_transfers = Self::parse_with_ctx(matches, QueryTransfers); let query_conversions = Self::parse_with_ctx(matches, QueryConversions); @@ -291,15 +298,17 @@ pub mod cmds { Self::parse_with_ctx(matches, QueryProposalResult); let query_protocol_parameters = Self::parse_with_ctx(matches, QueryProtocolParameters); + let query_pgf = Self::parse_with_ctx(matches, QueryPgf); let query_validator_state = Self::parse_with_ctx(matches, QueryValidatorState); let add_to_eth_bridge_pool = Self::parse_with_ctx(matches, AddToEthBridgePool); + let sign_tx = Self::parse_with_ctx(matches, SignTx); let utils = SubCmd::parse(matches).map(Self::WithoutContext); tx_custom .or(tx_transfer) .or(tx_ibc_transfer) - .or(tx_update_vp) + .or(tx_update_account) .or(tx_init_account) .or(tx_reveal_pk) .or(tx_init_proposal) @@ -326,7 +335,10 @@ pub mod cmds { .or(query_proposal) .or(query_proposal_result) .or(query_protocol_parameters) + .or(query_pgf) .or(query_validator_state) + .or(query_account) + .or(sign_tx) .or(utils) } } @@ -368,7 +380,7 @@ pub mod cmds { TxTransfer(TxTransfer), TxIbcTransfer(TxIbcTransfer), QueryResult(QueryResult), - TxUpdateVp(TxUpdateVp), + TxUpdateAccount(TxUpdateAccount), TxInitAccount(TxInitAccount), TxInitValidator(TxInitValidator), TxCommissionRateChange(TxCommissionRateChange), @@ -381,6 +393,7 @@ pub mod cmds { Withdraw(Withdraw), AddToEthBridgePool(AddToEthBridgePool), QueryEpoch(QueryEpoch), + QueryAccount(QueryAccount), QueryTransfers(QueryTransfers), QueryConversions(QueryConversions), QueryBlock(QueryBlock), @@ -395,7 +408,9 @@ pub mod cmds { QueryProposal(QueryProposal), QueryProposalResult(QueryProposalResult), QueryProtocolParameters(QueryProtocolParameters), + QueryPgf(QueryPgf), QueryValidatorState(QueryValidatorState), + SignTx(SignTx), } #[allow(clippy::large_enum_variant)] @@ -1174,6 +1189,28 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct QueryPgf(pub args::QueryPgf); + + impl SubCmd for QueryPgf { + const CMD: &'static str = "query-pgf"; + + fn parse(matches: &ArgMatches) -> Option + where + Self: Sized, + { + matches + .subcommand_matches(Self::CMD) + .map(|matches| QueryPgf(args::QueryPgf::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Query pgf stewards and continous funding.") + .add_args::>() + } + } + #[derive(Clone, Debug)] pub struct TxCustom(pub args::TxCustom); @@ -1232,15 +1269,15 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct TxUpdateVp(pub args::TxUpdateVp); + pub struct TxUpdateAccount(pub args::TxUpdateAccount); - impl SubCmd for TxUpdateVp { - const CMD: &'static str = "update"; + impl SubCmd for TxUpdateAccount { + const CMD: &'static str = "update-account"; fn parse(matches: &ArgMatches) -> Option { - matches - .subcommand_matches(Self::CMD) - .map(|matches| TxUpdateVp(args::TxUpdateVp::parse(matches))) + matches.subcommand_matches(Self::CMD).map(|matches| { + TxUpdateAccount(args::TxUpdateAccount::parse(matches)) + }) } fn def() -> App { @@ -1249,7 +1286,7 @@ pub mod cmds { "Send a signed transaction to update account's validity \ predicate.", ) - .add_args::>() + .add_args::>() } } @@ -1394,6 +1431,28 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct QueryAccount(pub args::QueryAccount); + + impl SubCmd for QueryAccount { + const CMD: &'static str = "query-account"; + + fn parse(matches: &ArgMatches) -> Option { + matches + .subcommand_matches(Self::CMD) + .map(|matches| QueryAccount(args::QueryAccount::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about( + "Query the substorage space of a specific enstablished \ + address.", + ) + .add_args::>() + } + } + #[derive(Clone, Debug)] pub struct QueryConversions(pub args::QueryConversions); @@ -1489,6 +1548,25 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct SignTx(pub args::SignTx); + + impl SubCmd for SignTx { + const CMD: &'static str = "sign-tx"; + + fn parse(matches: &ArgMatches) -> Option { + matches + .subcommand_matches(Self::CMD) + .map(|matches| SignTx(args::SignTx::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Query PoS bonded stake.") + .add_args::>() + } + } + #[derive(Clone, Debug)] pub struct QueryValidatorState( pub args::QueryValidatorState, @@ -2316,6 +2394,7 @@ pub mod args { pub const TX_INIT_PROPOSAL: &str = "tx_init_proposal.wasm"; pub const TX_INIT_VALIDATOR_WASM: &str = "tx_init_validator.wasm"; pub const TX_REVEAL_PK: &str = "tx_reveal_pk.wasm"; + pub const TX_UPDATE_ACCOUNT_WASM: &str = "tx_update_account.wasm"; pub const TX_TRANSFER_WASM: &str = "tx_transfer.wasm"; pub const TX_UNBOND_WASM: &str = "tx_unbond.wasm"; pub const TX_UNJAIL_VALIDATOR_WASM: &str = "tx_unjail_validator.wasm"; @@ -2378,15 +2457,8 @@ pub mod args { ); pub const ETH_SYNC: ArgFlag = flag("sync"); pub const EXPIRATION_OPT: ArgOpt = arg_opt("expiration"); - pub const FEE_AMOUNT: ArgDefault = arg_default( - "fee-amount", - DefaultFn(|| token::DenominatedAmount { - amount: token::Amount::default(), - denom: NATIVE_MAX_DECIMAL_PLACES.into(), - }), - ); - pub const FEE_PAYER: Arg = arg("fee-payer"); pub const FORCE: ArgFlag = flag("force"); + pub const GAS_PAYER: ArgOpt = arg("gas-payer").opt(); pub const GAS_AMOUNT: ArgDefault = arg_default( "gas-amount", DefaultFn(|| token::DenominatedAmount { @@ -2403,6 +2475,14 @@ pub mod args { ); pub const GAS_TOKEN: ArgDefaultFromCtx = arg_default_from_ctx("gas-token", DefaultFn(|| "NAM".parse().unwrap())); + pub const FEE_PAYER: Arg = arg("fee-payer"); + pub const FEE_AMOUNT: ArgDefault = arg_default( + "fee-amount", + DefaultFn(|| token::DenominatedAmount { + amount: token::Amount::default(), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }), + ); pub const GENESIS_PATH: Arg = arg("genesis-path"); pub const GENESIS_VALIDATOR: ArgOpt = arg("genesis-validator").opt(); @@ -2433,6 +2513,8 @@ pub mod args { pub const NAMADA_START_TIME: ArgOpt = arg_opt("time"); pub const NO_CONVERSIONS: ArgFlag = flag("no-conversions"); pub const OUT_FILE_PATH_OPT: ArgOpt = arg_opt("out-file-path"); + 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 PIN: ArgFlag = flag("pin"); @@ -2440,10 +2522,14 @@ pub mod args { "port-id", DefaultFn(|| PortId::from_str("transfer").unwrap()), ); + 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"); pub const PROPOSAL_OFFLINE: ArgFlag = flag("offline"); pub const PROTOCOL_KEY: ArgOpt = arg_opt("protocol-key"); pub const PRE_GENESIS_PATH: ArgOpt = arg_opt("pre-genesis-path"); pub const PUBLIC_KEY: Arg = arg("public-key"); + pub const PUBLIC_KEYS: ArgMulti = arg_multi("public-keys"); pub const PROPOSAL_ID: Arg = arg("proposal-id"); pub const PROPOSAL_ID_OPT: ArgOpt = arg_opt("proposal-id"); pub const PROPOSAL_VOTE_PGF_OPT: ArgOpt = arg_opt("pgf"); @@ -2459,9 +2545,8 @@ pub mod args { pub const SAFE_MODE: ArgFlag = flag("safe-mode"); pub const SCHEME: ArgDefault = arg_default("scheme", DefaultFn(|| SchemeType::Ed25519)); - 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"); pub const SOURCE_OPT: ArgOpt = SOURCE.opt(); pub const STORAGE_KEY: Arg = arg("storage-key"); @@ -2474,16 +2559,19 @@ pub mod args { pub const TRANSFER_SOURCE: Arg = arg("source"); pub const TRANSFER_TARGET: Arg = arg("target"); pub const TX_HASH: Arg = arg("tx-hash"); + pub const THRESOLD: ArgOpt = arg_opt("threshold"); pub const UNSAFE_DONT_ENCRYPT: ArgFlag = flag("unsafe-dont-encrypt"); pub const UNSAFE_SHOW_SECRET: ArgFlag = flag("unsafe-show-secret"); pub const VALIDATOR: Arg = arg("validator"); pub const VALIDATOR_OPT: ArgOpt = VALIDATOR.opt(); pub const VALIDATOR_ACCOUNT_KEY: ArgOpt = arg_opt("account-key"); - pub const VALIDATOR_CODE_PATH: ArgOpt = - arg_opt("validator-code-path"); + pub const VALIDATOR_ACCOUNT_KEYS: ArgMulti = + arg_multi("account-keys"); pub const VALIDATOR_CONSENSUS_KEY: ArgOpt = arg_opt("consensus-key"); + pub const VALIDATOR_CODE_PATH: ArgOpt = + arg_opt("validator-code-path"); pub const VALIDATOR_ETH_COLD_KEY: ArgOpt = arg_opt("eth-cold-key"); pub const VALIDATOR_ETH_HOT_KEY: ArgOpt = @@ -2495,6 +2583,8 @@ pub mod args { pub const WALLET_ALIAS_FORCE: ArgFlag = flag("wallet-alias-force"); pub const WASM_CHECKSUMS_PATH: Arg = arg("wasm-checksums-path"); pub const WASM_DIR: ArgOpt = arg_opt("wasm-dir"); + pub const TX_PATH: Arg = arg("tx-path"); + pub const TX_PATH_OPT: ArgOpt = TX_PATH.opt(); /// Global command arguments #[derive(Clone, Debug)] @@ -2701,9 +2791,9 @@ pub mod args { recipient: self.recipient, sender: ctx.get(&self.sender), amount: self.amount, - gas_amount: self.gas_amount, - gas_payer: ctx.get(&self.gas_payer), - code_path: ctx.read_wasm(self.code_path), + fee_amount: self.fee_amount, + fee_payer: ctx.get(&self.fee_payer), + code_path: self.code_path, } } } @@ -2715,8 +2805,8 @@ pub mod args { let recipient = ETH_ADDRESS.parse(matches); let sender = ADDRESS.parse(matches); let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); - let gas_amount = FEE_AMOUNT.parse(matches).amount; - let gas_payer = FEE_PAYER.parse(matches); + let fee_amount = FEE_AMOUNT.parse(matches).amount; + let fee_payer = FEE_PAYER.parse(matches); let code_path = PathBuf::from(TX_BRIDGE_POOL_WASM); Self { tx, @@ -2724,8 +2814,8 @@ pub mod args { recipient, sender, amount, - gas_amount, - gas_payer, + fee_amount, + fee_payer, code_path, } } @@ -3109,11 +3199,15 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> TxCustom { TxCustom:: { tx: self.tx.to_sdk(ctx), - code_path: ctx.read_wasm(self.code_path), + code_path: self.code_path, data_path: self.data_path.map(|data_path| { std::fs::read(data_path) - .expect("Expected a file at given data path") + .expect("Expected a file at given path") + }), + serialized_tx: self.serialized_tx.map(|path| { + std::fs::read(path).expect("Expected a file at given path") }), + owner: ctx.get(&self.owner), } } } @@ -3121,26 +3215,50 @@ pub mod args { impl Args for TxCustom { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); - let code_path = CODE_PATH.parse(matches); + let code_path = CODE_PATH_OPT.parse(matches); let data_path = DATA_PATH_OPT.parse(matches); + let serialized_tx = TX_PATH_OPT.parse(matches); + let owner = OWNER.parse(matches); Self { tx, code_path, data_path, + serialized_tx, + owner, } } fn def(app: App) -> App { app.add_args::>() .arg( - CODE_PATH + CODE_PATH_OPT + .def() + .help("The path to the transaction's WASM code.") + .conflicts_with(TX_PATH_OPT.name), + ) + .arg( + DATA_PATH_OPT .def() - .help("The path to the transaction's WASM code."), + .help( + "The data file at this path containing arbitrary \ + bytes will be passed to the transaction code \ + when it's executed.", + ) + .requires(CODE_PATH_OPT.name) + .conflicts_with(TX_PATH_OPT.name), ) - .arg(DATA_PATH_OPT.def().help( - "The data file at this path containing arbitrary bytes \ - will be passed to the transaction code when it's \ - executed.", + .arg( + TX_PATH_OPT + .def() + .help("The path to a serialized transaction.") + .conflicts_with_all([ + CODE_PATH_OPT.name, + DATA_PATH_OPT.name, + ]), + ) + .arg(OWNER.def().help( + "The address corresponding to the signatures or signing \ + keys.", )) } } @@ -3262,10 +3380,14 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> TxInitAccount { TxInitAccount:: { tx: self.tx.to_sdk(ctx), - source: ctx.get(&self.source), - vp_code_path: self.vp_code_path.to_path_buf(), - tx_code_path: self.tx_code_path.to_path_buf(), - public_key: ctx.get_cached(&self.public_key), + 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)) + .collect(), + threshold: self.threshold, } } } @@ -3273,34 +3395,36 @@ pub mod args { impl Args for TxInitAccount { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); - let source = SOURCE.parse(matches); let vp_code_path = CODE_PATH_OPT .parse(matches) .unwrap_or_else(|| PathBuf::from(VP_USER_WASM)); let tx_code_path = PathBuf::from(TX_INIT_ACCOUNT_WASM); - let public_key = PUBLIC_KEY.parse(matches); + let public_keys = PUBLIC_KEYS.parse(matches); + let threshold = THRESOLD.parse(matches); Self { tx, - source, vp_code_path, - public_key, + public_keys, + threshold, tx_code_path, } } fn def(app: App) -> App { app.add_args::>() - .arg(SOURCE.def().help( - "The source account's address that signs the transaction.", - )) .arg(CODE_PATH_OPT.def().help( "The path to the validity predicate WASM code to be used \ for the new account. Uses the default user VP if none \ specified.", )) - .arg(PUBLIC_KEY.def().help( - "A public key to be used for the new account in \ - hexadecimal encoding.", + .arg(PUBLIC_KEYS.def().help( + "A list public keys to be associated with the new account \ + in hexadecimal encoding.", + )) + .arg(THRESOLD.def().help( + "The minimum number of signature to be provided for \ + authorization. Must be less then the maximum number of \ + public keys provided.", )) } } @@ -3309,9 +3433,13 @@ pub mod args { fn to_sdk(self, ctx: &mut Context) -> TxInitValidator { TxInitValidator:: { tx: self.tx.to_sdk(ctx), - source: ctx.get(&self.source), scheme: self.scheme, - account_key: self.account_key.map(|x| ctx.get_cached(&x)), + account_keys: self + .account_keys + .iter() + .map(|x| 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)), @@ -3330,9 +3458,8 @@ pub mod args { impl Args for TxInitValidator { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); - let source = SOURCE.parse(matches); let scheme = SCHEME.parse(matches); - let account_key = VALIDATOR_ACCOUNT_KEY.parse(matches); + let account_keys = VALIDATOR_ACCOUNT_KEYS.parse(matches); let consensus_key = VALIDATOR_CONSENSUS_KEY.parse(matches); let eth_cold_key = VALIDATOR_ETH_COLD_KEY.parse(matches); let eth_hot_key = VALIDATOR_ETH_HOT_KEY.parse(matches); @@ -3345,11 +3472,12 @@ pub mod args { .unwrap_or_else(|| PathBuf::from(VP_USER_WASM)); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); let tx_code_path = PathBuf::from(TX_INIT_VALIDATOR_WASM); + let threshold = THRESOLD.parse(matches); Self { tx, - source, scheme, - account_key, + account_keys, + threshold, consensus_key, eth_cold_key, eth_hot_key, @@ -3364,16 +3492,14 @@ pub mod args { fn def(app: App) -> App { app.add_args::>() - .arg(SOURCE.def().help( - "The source account's address that signs the transaction.", - )) .arg(SCHEME.def().help( "The key scheme/type used for the validator keys. \ Currently supports ed25519 and secp256k1.", )) - .arg(VALIDATOR_ACCOUNT_KEY.def().help( - "A public key for the validator account. A new one will \ - be generated if none given.", + .arg(VALIDATOR_ACCOUNT_KEYS.def().help( + "A list public keys to be associated with the new account \ + in hexadecimal encoding. A new one will be generated if \ + none given.", )) .arg(VALIDATOR_CONSENSUS_KEY.def().help( "A consensus key for the validator account. A new one \ @@ -3414,38 +3540,53 @@ pub mod args { "UNSAFE: Do not encrypt the generated keypairs. Do not \ use this for keys used in a live network.", )) + .arg(THRESOLD.def().help( + "The minimum number of signature to be provided for \ + authorization. Must be less then the maximum number of \ + public keys provided.", + )) } } - impl CliToSdk> for TxUpdateVp { - fn to_sdk(self, ctx: &mut Context) -> TxUpdateVp { - TxUpdateVp:: { + impl CliToSdk> for TxUpdateAccount { + fn to_sdk(self, ctx: &mut Context) -> TxUpdateAccount { + TxUpdateAccount:: { tx: self.tx.to_sdk(ctx), vp_code_path: self.vp_code_path, tx_code_path: self.tx_code_path, addr: ctx.get(&self.addr), + public_keys: self + .public_keys + .iter() + .map(|pk| ctx.get_cached(pk)) + .collect(), + threshold: self.threshold, } } } - impl Args for TxUpdateVp { + impl Args for TxUpdateAccount { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); - let vp_code_path = CODE_PATH.parse(matches); + let vp_code_path = CODE_PATH_OPT.parse(matches); let addr = ADDRESS.parse(matches); - let tx_code_path = PathBuf::from(TX_UPDATE_VP_WASM); + let tx_code_path = PathBuf::from(TX_UPDATE_ACCOUNT_WASM); + let public_keys = PUBLIC_KEYS.parse(matches); + let threshold = THRESOLD.parse(matches); Self { tx, vp_code_path, addr, tx_code_path, + public_keys, + threshold, } } fn def(app: App) -> App { app.add_args::>() .arg( - CODE_PATH.def().help( + CODE_PATH_OPT.def().help( "The path to the new validity predicate WASM code.", ), ) @@ -3453,6 +3594,15 @@ pub mod args { "The account's address. It's key is used to produce the \ signature.", )) + .arg(PUBLIC_KEYS.def().help( + "A list public keys to be associated with the new account \ + in hexadecimal encoding.", + )) + .arg(THRESOLD.def().help( + "The minimum number of signature to be provided for \ + authorization. Must be less then the maximum number of \ + public keys provided.", + )) } } @@ -3556,26 +3706,15 @@ pub mod args { )) } } - #[derive(Clone, Debug)] - pub struct InitProposal { - /// Common tx arguments - pub tx: Tx, - /// The proposal file path - pub proposal_data: PathBuf, - /// Flag if proposal should be run offline - pub offline: bool, - /// Native token address - pub native_token: C::NativeAddress, - /// Path to the TX WASM code file - pub tx_code_path: PathBuf, - } impl CliToSdk> for InitProposal { fn to_sdk(self, ctx: &mut Context) -> InitProposal { InitProposal:: { tx: self.tx.to_sdk(ctx), - proposal_data: self.proposal_data, - offline: self.offline, + proposal_data: std::fs::read(self.proposal_data).expect(""), + 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(), tx_code_path: self.tx_code_path, } @@ -3586,15 +3725,19 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let proposal_data = DATA_PATH.parse(matches); - let offline = PROPOSAL_OFFLINE.parse(matches); + let is_offline = PROPOSAL_OFFLINE.parse(matches); + let is_pgf_stewards = PROPOSAL_PGF_STEWARD.parse(matches); + let is_pgf_funding = PROPOSAL_PGF_FUNDING.parse(matches); let tx_code_path = PathBuf::from(TX_INIT_PROPOSAL); Self { tx, proposal_data, - offline, native_token: (), tx_code_path, + is_offline, + is_pgf_stewards, + is_pgf_funding, } } @@ -3606,42 +3749,66 @@ pub mod args { .arg( PROPOSAL_OFFLINE .def() - .help("Flag if the proposal vote should run offline."), + .help( + "Flag if the proposal should be serialized \ + offline (only for default types).", + ) + .conflicts_with_all([ + PROPOSAL_PGF_FUNDING.name, + PROPOSAL_PGF_STEWARD.name, + PROPOSAL_ETH.name, + ]), + ) + .arg( + PROPOSAL_ETH + .def() + .help("Flag if the proposal is of type eth.") + .conflicts_with_all([ + PROPOSAL_PGF_FUNDING.name, + PROPOSAL_PGF_STEWARD.name, + ]), + ) + .arg( + PROPOSAL_PGF_STEWARD + .def() + .help( + "Flag if the proposal is of type pgf-stewards. \ + Used to elect/remove stewards.", + ) + .conflicts_with_all([ + PROPOSAL_ETH.name, + PROPOSAL_PGF_FUNDING.name, + ]), + ) + .arg( + PROPOSAL_PGF_FUNDING + .def() + .help( + "Flag if the proposal is of type pgf-funding. \ + Used to control continous/retro pgf fundings.", + ) + .conflicts_with_all([ + PROPOSAL_ETH.name, + PROPOSAL_PGF_STEWARD.name, + ]), ) } } - #[derive(Clone, Debug)] - pub struct VoteProposal { - /// Common tx arguments - pub tx: Tx, - /// Proposal id - pub proposal_id: Option, - /// The vote - pub vote: String, - /// PGF proposal - pub proposal_pgf: Option, - /// ETH proposal - pub proposal_eth: Option, - /// Flag if proposal vote should be run offline - pub offline: bool, - /// The proposal file path - pub proposal_data: Option, - /// Path to the TX WASM code file - pub tx_code_path: PathBuf, - } - impl CliToSdk> for VoteProposal { fn to_sdk(self, ctx: &mut Context) -> VoteProposal { VoteProposal:: { tx: self.tx.to_sdk(ctx), proposal_id: self.proposal_id, vote: self.vote, - offline: self.offline, - proposal_data: self.proposal_data, + voter: ctx.get(&self.voter), + is_offline: self.is_offline, + proposal_data: self.proposal_data.map(|path| { + println!("Not able to read {}.", path.to_string_lossy()); + std::fs::read(path) + .expect("Should be able to read the file.") + }), tx_code_path: self.tx_code_path.to_path_buf(), - proposal_pgf: self.proposal_pgf, - proposal_eth: self.proposal_eth, } } } @@ -3650,10 +3817,9 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let proposal_id = PROPOSAL_ID_OPT.parse(matches); - let proposal_pgf = PROPOSAL_VOTE_PGF_OPT.parse(matches); - let proposal_eth = PROPOSAL_VOTE_ETH_OPT.parse(matches); let vote = PROPOSAL_VOTE.parse(matches); - let offline = PROPOSAL_OFFLINE.parse(matches); + let voter = ADDRESS.parse(matches); + let is_offline = PROPOSAL_OFFLINE.parse(matches); let proposal_data = DATA_PATH_OPT.parse(matches); let tx_code_path = PathBuf::from(TX_VOTE_PROPOSAL); @@ -3661,9 +3827,8 @@ pub mod args { tx, proposal_id, vote, - proposal_pgf, - proposal_eth, - offline, + is_offline, + voter, proposal_data, tx_code_path, } @@ -3683,29 +3848,7 @@ pub mod args { .arg( PROPOSAL_VOTE .def() - .help("The vote for the proposal. Either yay or nay"), - ) - .arg( - PROPOSAL_VOTE_PGF_OPT - .def() - .help( - "The list of proposed councils and spending \ - caps:\n$council1 $cap1 $council2 $cap2 ... \ - (council is bech32m encoded address, cap is \ - expressed in microNAM", - ) - .requires(PROPOSAL_ID.name) - .conflicts_with(PROPOSAL_VOTE_ETH_OPT.name), - ) - .arg( - PROPOSAL_VOTE_ETH_OPT - .def() - .help( - "The signing key and message bytes (hex encoded) \ - to be signed: $signing_key $message", - ) - .requires(PROPOSAL_ID.name) - .conflicts_with(PROPOSAL_VOTE_PGF_OPT.name), + .help("The vote for the proposal. Either yay or nay."), ) .arg( PROPOSAL_OFFLINE @@ -3720,8 +3863,10 @@ pub mod args { "The data path file (json) that describes the \ proposal.", ) + .requires(PROPOSAL_OFFLINE.name) .conflicts_with(PROPOSAL_ID.name), ) + .arg(ADDRESS.def().help("The address of the voter.")) } } @@ -3811,7 +3956,15 @@ pub mod args { fn def(app: App) -> App { app.add_args::>() - .arg(PROPOSAL_ID_OPT.def().help("The proposal identifier.")) + .arg( + PROPOSAL_ID_OPT + .def() + .help("The proposal identifier.") + .conflicts_with_all([ + PROPOSAL_OFFLINE.name, + DATA_PATH_OPT.name, + ]), + ) .arg( PROPOSAL_OFFLINE .def() @@ -3819,16 +3972,18 @@ pub mod args { "Flag if the proposal result should run on \ offline data.", ) - .conflicts_with(PROPOSAL_ID.name), + .conflicts_with(PROPOSAL_ID.name) + .requires(DATA_PATH_OPT.name), ) .arg( DATA_PATH_OPT .def() .help( "The path to the folder containing the proposal \ - json and votes", + and votes files in json format.", ) - .conflicts_with(PROPOSAL_ID.name), + .conflicts_with(PROPOSAL_ID.name) + .requires(PROPOSAL_OFFLINE.name), ) } } @@ -3858,6 +4013,26 @@ pub mod args { } } + impl CliToSdk> for QueryPgf { + fn to_sdk(self, ctx: &mut Context) -> QueryPgf { + QueryPgf:: { + query: self.query.to_sdk(ctx), + } + } + } + + impl Args for QueryPgf { + fn parse(matches: &ArgMatches) -> Self { + let query = Query::parse(matches); + + Self { query } + } + + fn def(app: App) -> App { + app.add_args::>() + } + } + impl CliToSdk> for Withdraw { fn to_sdk(self, ctx: &mut Context) -> Withdraw { Withdraw:: { @@ -3931,6 +4106,32 @@ pub mod args { } } + impl CliToSdk> for QueryAccount { + fn to_sdk(self, ctx: &mut Context) -> QueryAccount { + QueryAccount:: { + query: self.query.to_sdk(ctx), + owner: ctx.get(&self.owner), + } + } + } + + impl Args for QueryAccount { + fn parse(matches: &ArgMatches) -> Self { + let query = Query::parse(matches); + let owner = OWNER.parse(matches); + Self { query, owner } + } + + fn def(app: App) -> App { + app.add_args::>().arg( + OWNER + .def() + .help("The substorage space address to query.") + .required(true), + ) + } + } + impl CliToSdk> for QueryBalance { fn to_sdk(self, ctx: &mut Context) -> QueryBalance { QueryBalance:: { @@ -4187,6 +4388,39 @@ pub mod args { } } + impl CliToSdk> for SignTx { + fn to_sdk(self, ctx: &mut Context) -> SignTx { + SignTx:: { + tx: self.tx.to_sdk(ctx), + tx_data: std::fs::read(self.tx_data).expect(""), + owner: ctx.get(&self.owner), + } + } + } + + impl Args for SignTx { + fn parse(matches: &ArgMatches) -> Self { + let tx = Tx::parse(matches); + let tx_path = TX_PATH.parse(matches); + let owner = OWNER.parse(matches); + Self { + tx, + tx_data: tx_path, + owner, + } + } + + fn def(app: App) -> App { + app.add_args::>() + .arg( + TX_PATH.def().help( + "The path to the tx file with the serialized tx.", + ), + ) + .arg(OWNER.def().help("The address of the account owner")) + } + } + impl CliToSdk> for QueryCommissionRate { fn to_sdk(self, ctx: &mut Context) -> QueryCommissionRate { QueryCommissionRate:: { @@ -4341,19 +4575,29 @@ pub mod args { Tx:: { dry_run: self.dry_run, dump_tx: self.dump_tx, + output_folder: self.output_folder, force: self.force, broadcast_only: self.broadcast_only, ledger_address: (), initialized_account_alias: self.initialized_account_alias, wallet_alias_force: self.wallet_alias_force, - fee_amount: self.fee_amount, - fee_token: ctx.get(&self.fee_token), + gas_payer: ctx.get_opt_cached(&self.gas_payer), + gas_amount: self.gas_amount, + gas_token: ctx.get(&self.gas_token), gas_limit: self.gas_limit, - signing_key: self.signing_key.map(|x| ctx.get_cached(&x)), + signing_keys: self + .signing_keys + .iter() + .map(|key| ctx.get_cached(key)) + .collect(), + signatures: self + .signatures + .iter() + .map(|path| std::fs::read(path).unwrap()) + .collect(), verification_key: self .verification_key - .map(|x| ctx.get_cached(&x)), - signer: self.signer.map(|x| ctx.get(&x)), + .map(|public_key| ctx.get_cached(&public_key)), tx_reveal_code_path: self.tx_reveal_code_path, password: self.password, expiration: self.expiration, @@ -4395,6 +4639,10 @@ pub mod args { .arg(WALLET_ALIAS_FORCE.def().help( "Override the alias without confirmation if it already exists.", )) + .arg(GAS_PAYER.def().help( + "The implicit address of the gas payer. It defaults to the \ + address associated to the first key passed to --signing-keys.", + )) .arg(GAS_AMOUNT.def().help( "The amount being paid for the inclusion of this transaction", )) @@ -4411,26 +4659,35 @@ pub mod args { 12:12:12Z\n2012- 12-12T12: 12:12Z", )) .arg( - SIGNING_KEY_OPT + SIGNING_KEYS .def() .help( "Sign the transaction with the key for the given \ public key, public key hash or alias from your \ wallet.", ) - .conflicts_with(SIGNER.name) - .conflicts_with(VERIFICATION_KEY.name), + .conflicts_with_all([ + SIGNATURES.name, + VERIFICATION_KEY.name, + ]), ) .arg( - SIGNER + SIGNATURES .def() .help( - "Sign the transaction with the keypair of the public \ - key of the given address.", + "List of file paths containing a serialized signature \ + to be attached to a transaction. Requires to provide \ + a gas payer.", ) - .conflicts_with(SIGNING_KEY_OPT.name) - .conflicts_with(VERIFICATION_KEY.name), + .conflicts_with_all([ + SIGNING_KEYS.name, + VERIFICATION_KEY.name, + ]) + .requires(GAS_PAYER.name), ) + .arg(OUTPUT_FOLDER_PATH.def().help( + "The output folder path where the artifact will be stored.", + )) .arg( VERIFICATION_KEY .def() @@ -4439,8 +4696,7 @@ pub mod args { public key, public key hash or alias from your \ wallet.", ) - .conflicts_with(SIGNER.name) - .conflicts_with(SIGNING_KEY_OPT.name), + .conflicts_with_all([SIGNING_KEYS.name, SIGNATURES.name]), ) .arg(CHAIN_ID_OPT.def().help("The chain ID.")) } @@ -4453,17 +4709,19 @@ pub mod args { let ledger_address = LEDGER_ADDRESS_DEFAULT.parse(matches); let initialized_account_alias = ALIAS_OPT.parse(matches); let wallet_alias_force = WALLET_ALIAS_FORCE.parse(matches); - let fee_amount = + let gas_payer = GAS_PAYER.parse(matches); + let gas_amount = InputAmount::Unvalidated(GAS_AMOUNT.parse(matches)); - let fee_token = GAS_TOKEN.parse(matches); + let gas_token = GAS_TOKEN.parse(matches); let gas_limit = GAS_LIMIT.parse(matches).amount.into(); let expiration = EXPIRATION_OPT.parse(matches); - let signing_key = SIGNING_KEY_OPT.parse(matches); + let signing_keys = SIGNING_KEYS.parse(matches); + let signatures = SIGNATURES.parse(matches); let verification_key = VERIFICATION_KEY.parse(matches); - let signer = SIGNER.parse(matches); let tx_reveal_code_path = PathBuf::from(TX_REVEAL_PK); let chain_id = CHAIN_ID_OPT.parse(matches); let password = None; + let output_folder = OUTPUT_FOLDER_PATH.parse(matches); Self { dry_run, dump_tx, @@ -4472,16 +4730,18 @@ pub mod args { ledger_address, initialized_account_alias, wallet_alias_force, - fee_amount, - fee_token, + gas_payer, + gas_amount, + gas_token, gas_limit, expiration, - signing_key, + signing_keys, + signatures, verification_key, - signer, tx_reveal_code_path, password, chain_id, + output_folder, } } } diff --git a/apps/src/lib/cli/client.rs b/apps/src/lib/cli/client.rs index 6b683168501..2f52d09dff3 100644 --- a/apps/src/lib/cli/client.rs +++ b/apps/src/lib/cli/client.rs @@ -1,5 +1,6 @@ use color_eyre::eyre::{eyre, Report, Result}; use namada::ledger::eth_bridge::bridge_pool; +use namada::ledger::tx::dump_tx; use namada::ledger::{signing, tx as sdk_tx}; use namada::types::control_flow::ProceedOrElse; @@ -76,7 +77,7 @@ impl CliApi { let args = args.to_sdk(&mut ctx); tx::submit_ibc_transfer(&client, ctx, args).await?; } - Sub::TxUpdateVp(TxUpdateVp(mut args)) => { + Sub::TxUpdateAccount(TxUpdateAccount(mut args)) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.tx.ledger_address, @@ -87,7 +88,8 @@ impl CliApi { .await .proceed_or_else(error)?; let args = args.to_sdk(&mut ctx); - tx::submit_update_vp(&client, &mut ctx, args).await?; + tx::submit_update_account(&client, &mut ctx, args) + .await?; } Sub::TxInitAccount(TxInitAccount(mut args)) => { let client = client.unwrap_or_else(|| { @@ -236,37 +238,52 @@ impl CliApi { .proceed_or_else(error)?; let args = args.to_sdk(&mut ctx); let tx_args = args.tx.clone(); - let (mut tx, addr, pk) = - bridge_pool::build_bridge_pool_tx( - &client, - &mut ctx.wallet, - args, - ) - .await - .unwrap(); - tx::submit_reveal_aux( + + let default_signer = signing::signer_from_address( + Some(args.sender.clone()), + ); + let signing_data = signing::aux_signing_data( &client, - &mut ctx, - &tx_args, - addr, - pk.clone(), - &mut tx, - ) - .await?; - signing::sign_tx( &mut ctx.wallet, - &mut tx, - &tx_args, - &pk, + &args.tx, + &args.sender, + default_signer, ) .await?; - sdk_tx::process_tx( + + let tx_builder = bridge_pool::build_bridge_pool_tx( &client, - &mut ctx.wallet, - &tx_args, - tx, + args.clone(), + signing_data.gas_payer.clone(), ) .await?; + + if args.tx.dump_tx { + dump_tx(&args.tx, tx_builder); + } else { + tx::submit_reveal_aux( + &client, + &mut ctx, + tx_args.clone(), + &args.sender, + ) + .await?; + + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &tx_args, + tx_builder, + signing_data, + )?; + + sdk_tx::process_tx( + &client, + &mut ctx.wallet, + &tx_args, + tx_builder.build(), + ) + .await?; + } } Sub::TxUnjailValidator(TxUnjailValidator(mut args)) => { let client = client.unwrap_or_else(|| { @@ -527,6 +544,45 @@ impl CliApi { let args = args.to_sdk(&mut ctx); rpc::query_protocol_parameters(&client, args).await; } + Sub::QueryPgf(QueryPgf(mut args)) => { + let client = client.unwrap_or_else(|| { + C::from_tendermint_address( + &mut args.query.ledger_address, + ) + }); + client + .wait_until_node_is_synced() + .await + .proceed_or_else(error)?; + let args = args.to_sdk(&mut ctx); + rpc::query_pgf(&client, args).await; + } + Sub::QueryAccount(QueryAccount(mut args)) => { + let client = client.unwrap_or_else(|| { + C::from_tendermint_address( + &mut args.query.ledger_address, + ) + }); + client + .wait_until_node_is_synced() + .await + .proceed_or_else(error)?; + let args = args.to_sdk(&mut ctx); + rpc::query_account(&client, args).await; + } + Sub::SignTx(SignTx(mut args)) => { + let client = client.unwrap_or_else(|| { + C::from_tendermint_address( + &mut args.tx.ledger_address, + ) + }); + client + .wait_until_node_is_synced() + .await + .proceed_or_else(error)?; + let args = args.to_sdk(&mut ctx); + tx::sign_tx(&client, &mut ctx, args).await?; + } } } cli::NamadaClient::WithoutContext(cmd, global_args) => match cmd { diff --git a/apps/src/lib/cli/utils.rs b/apps/src/lib/cli/utils.rs index 8236c16eab3..aed45507d90 100644 --- a/apps/src/lib/cli/utils.rs +++ b/apps/src/lib/cli/utils.rs @@ -224,6 +224,27 @@ where } } +impl ArgMulti> +where + T: FromStr, + ::Err: Debug, +{ + pub fn def(&self) -> ClapArg { + ClapArg::new(self.name) + .long(self.name) + .num_args(1..) + .value_delimiter(',') + } + + pub fn parse(&self, matches: &ArgMatches) -> Vec> { + matches + .get_many(self.name) + .unwrap_or_default() + .map(|raw: &String| FromContext::new(raw.to_string())) + .collect() + } +} + impl ArgDefaultFromCtx> where T: FromStr, diff --git a/apps/src/lib/client/mod.rs b/apps/src/lib/client/mod.rs index 57f3c5a043d..8862c5a5643 100644 --- a/apps/src/lib/client/mod.rs +++ b/apps/src/lib/client/mod.rs @@ -1,4 +1,3 @@ pub mod rpc; -pub mod signing; pub mod tx; pub mod utils; diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 6a1c79edf5f..23ea89115d0 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -2,10 +2,9 @@ use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use std::fs::File; +use std::fs::{self, read_dir}; use std::io::{self, Write}; use std::iter::Iterator; -use std::path::PathBuf; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; @@ -15,22 +14,28 @@ use masp_primitives::asset_type::AssetType; use masp_primitives::merkle_tree::MerklePath; use masp_primitives::sapling::{Node, ViewingKey}; use masp_primitives::zip32::ExtendedFullViewingKey; -use namada::core::types::transaction::governance::ProposalType; +use namada::core::ledger::governance::cli::offline::{ + find_offline_proposal, find_offline_votes, read_offline_files, + OfflineSignedProposal, OfflineVote, +}; +use namada::core::ledger::governance::parameters::GovernanceParameters; +use namada::core::ledger::governance::storage::keys as governance_storage; +use namada::core::ledger::governance::storage::proposal::{ + PGFTarget, StorageProposal, +}; +use namada::core::ledger::governance::utils::{ + compute_proposal_result, ProposalVotes, TallyType, TallyVote, VotePower, +}; use namada::ledger::events::Event; -use namada::ledger::governance::parameters::GovParams; -use namada::ledger::governance::storage as gov_storage; use namada::ledger::masp::{ Conversions, MaspAmount, MaspChange, PinnedBalanceError, ShieldedContext, ShieldedUtils, }; -use namada::ledger::native_vp::governance::utils::{self, Votes}; use namada::ledger::parameters::{storage as param_storage, EpochDuration}; -use namada::ledger::pos::{ - self, BondId, BondsAndUnbondsDetail, CommissionPair, PosParams, Slash, -}; +use namada::ledger::pos::{CommissionPair, PosParams, Slash}; use namada::ledger::queries::RPC; use namada::ledger::rpc::{ - enriched_bonds_and_unbonds, format_denominated_amount, query_epoch, + self, enriched_bonds_and_unbonds, format_denominated_amount, query_epoch, TxResponse, }; use namada::ledger::storage::ConversionState; @@ -38,9 +43,6 @@ use namada::ledger::wallet::{AddressVpType, Wallet}; use namada::proof_of_stake::types::{ValidatorState, WeightedValidator}; use namada::types::address::{masp, Address}; use namada::types::control_flow::ProceedOrElse; -use namada::types::governance::{ - OfflineProposal, OfflineVote, ProposalVote, VotePower, VoteType, -}; use namada::types::hash::Hash; use namada::types::key::*; use namada::types::masp::{BalanceOwner, ExtendedViewingKey, PaymentAddress}; @@ -572,133 +574,48 @@ pub async fn query_proposal( client: &C, args: args::QueryProposal, ) { - async fn print_proposal( - client: &C, - id: u64, - current_epoch: Epoch, - details: bool, - ) -> Option<()> { - let author_key = gov_storage::get_author_key(id); - let start_epoch_key = gov_storage::get_voting_start_epoch_key(id); - let end_epoch_key = gov_storage::get_voting_end_epoch_key(id); - let proposal_type_key = gov_storage::get_proposal_type_key(id); - - let author = - query_storage_value::(client, &author_key).await?; - let start_epoch = - query_storage_value::(client, &start_epoch_key).await?; - let end_epoch = - query_storage_value::(client, &end_epoch_key).await?; - let proposal_type = - query_storage_value::(client, &proposal_type_key) - .await?; - - if details { - let content_key = gov_storage::get_content_key(id); - let grace_epoch_key = gov_storage::get_grace_epoch_key(id); - let content = query_storage_value::>( - client, - &content_key, - ) - .await?; - let grace_epoch = - query_storage_value::(client, &grace_epoch_key) - .await?; - - println!("Proposal: {}", id); - println!("{:4}Type: {}", "", proposal_type); - println!("{:4}Author: {}", "", author); - println!("{:4}Content:", ""); - for (key, value) in &content { - println!("{:8}{}: {}", "", key, value); - } - println!("{:4}Start Epoch: {}", "", start_epoch); - println!("{:4}End Epoch: {}", "", end_epoch); - println!("{:4}Grace Epoch: {}", "", grace_epoch); - let votes = get_proposal_votes(client, start_epoch, id).await; - let total_stake = get_total_staked_tokens(client, start_epoch) + let current_epoch = query_and_print_epoch(client).await; + + if let Some(id) = args.proposal_id { + let proposal = query_proposal_by_id(client, id).await; + if let Some(proposal) = proposal { + println!("{}", proposal.to_string_with_status(current_epoch)); + } else { + eprintln!("No proposal found with id: {}", id); + } + } else { + println!("asd2"); + let last_proposal_id_key = governance_storage::get_counter_key(); + let last_proposal_id = + query_storage_value::(client, &last_proposal_id_key) .await - .try_into() .unwrap(); - if start_epoch > current_epoch { - println!("{:4}Status: pending", ""); - } else if start_epoch <= current_epoch && current_epoch <= end_epoch - { - match utils::compute_tally(votes, total_stake, &proposal_type) { - Ok(partial_proposal_result) => { - println!( - "{:4}Yay votes: {}", - "", partial_proposal_result.total_yay_power - ); - println!( - "{:4}Nay votes: {}", - "", partial_proposal_result.total_nay_power - ); - println!("{:4}Status: on-going", ""); - } - Err(msg) => { - eprintln!("Error in tally computation: {}", msg) - } - } - } else { - match utils::compute_tally(votes, total_stake, &proposal_type) { - Ok(proposal_result) => { - println!("{:4}Status: done", ""); - println!("{:4}Result: {}", "", proposal_result); - } - Err(msg) => { - eprintln!("Error in tally computation: {}", msg) - } - } - } + + let from_id = if last_proposal_id > 10 { + last_proposal_id - 10 } else { - println!("Proposal: {}", id); - println!("{:4}Type: {}", "", proposal_type); - println!("{:4}Author: {}", "", author); - println!("{:4}Start Epoch: {}", "", start_epoch); - println!("{:4}End Epoch: {}", "", end_epoch); - if start_epoch > current_epoch { - println!("{:4}Status: pending", ""); - } else if start_epoch <= current_epoch && current_epoch <= end_epoch - { - println!("{:4}Status: on-going", ""); - } else { - println!("{:4}Status: done", ""); - } - } + 0 + }; - Some(()) - } + println!("id: {}", last_proposal_id); - let current_epoch = query_and_print_epoch(client).await; - match args.proposal_id { - Some(id) => { - if print_proposal::(client, id, current_epoch, true) + for id in from_id..last_proposal_id { + let proposal = query_proposal_by_id(client, id) .await - .is_none() - { - eprintln!("No valid proposal was found with id {}", id) - } - } - None => { - let last_proposal_id_key = gov_storage::get_counter_key(); - let last_proposal_id = - query_storage_value::(client, &last_proposal_id_key) - .await - .unwrap(); - - for id in 0..last_proposal_id { - if print_proposal::(client, id, current_epoch, false) - .await - .is_none() - { - eprintln!("No valid proposal was found with id {}", id) - }; - } + .expect("Proposal should be written to storage."); + println!("{}", proposal); } } } +/// Query proposal by Id +pub async fn query_proposal_by_id( + client: &C, + proposal_id: u64, +) -> Option { + namada::ledger::rpc::query_proposal_by_id(client, proposal_id).await +} + /// Query token shielded balance(s) pub async fn query_shielded_balance< C: namada::ledger::queries::Client + Sync, @@ -977,169 +894,145 @@ pub async fn query_proposal_result< client: &C, args: args::QueryProposalResult, ) { - let current_epoch = query_epoch(client).await; + if args.proposal_id.is_some() { + let proposal_id = + args.proposal_id.expect("Proposal id should be defined."); + let proposal = if let Some(proposal) = + query_proposal_by_id(client, proposal_id).await + { + proposal + } else { + eprintln!("Proposal {} not found.", proposal_id); + return; + }; - match args.proposal_id { - Some(id) => { - let end_epoch_key = gov_storage::get_voting_end_epoch_key(id); - let end_epoch = - query_storage_value::(client, &end_epoch_key).await; - - match end_epoch { - Some(end_epoch) => { - if current_epoch > end_epoch { - let votes = - get_proposal_votes(client, end_epoch, id).await; - let proposal_type_key = - gov_storage::get_proposal_type_key(id); - let proposal_type = query_storage_value::< - C, - ProposalType, - >( - client, &proposal_type_key - ) - .await - .expect("Could not read proposal type from storage"); - let total_stake = - get_total_staked_tokens(client, end_epoch) - .await - .try_into() - .unwrap(); - println!("Proposal: {}", id); - match utils::compute_tally( - votes, - total_stake, - &proposal_type, - ) { - Ok(proposal_result) => { - println!("{:4}Result: {}", "", proposal_result) - } - Err(msg) => { - eprintln!("Error in tally computation: {}", msg) - } - } - } else { - eprintln!("Proposal is still in progress."); - cli::safe_exit(1) - } - } - None => { - eprintln!("Error while retriving proposal."); - cli::safe_exit(1) - } - } - } - None => { - if args.offline { - match args.proposal_folder { - Some(path) => { - let mut dir = tokio::fs::read_dir(&path) - .await - .expect("Should be able to read the directory."); - let mut files = HashSet::new(); - let mut is_proposal_present = false; - - while let Some(entry) = - dir.next_entry().await.transpose() - { - match entry { - Ok(entry) => match entry.file_type().await { - Ok(entry_stat) => { - if entry_stat.is_file() { - if entry.file_name().eq(&"proposal") - { - is_proposal_present = true - } else if entry - .file_name() - .to_string_lossy() - .starts_with("proposal-vote-") - { - // Folder may contain other - // files than just the proposal - // and the votes - files.insert(entry.path()); - } - } - } - Err(e) => { - eprintln!( - "Can't read entry type: {}.", - e - ); - cli::safe_exit(1) - } - }, - Err(e) => { - eprintln!("Can't read entry: {}.", e); - cli::safe_exit(1) - } - } - } + let tally_type = proposal.get_tally_type(); + let total_voting_power = + get_total_staked_tokens(client, proposal.voting_end_epoch).await; - if !is_proposal_present { - eprintln!( - "The folder must contain the offline proposal \ - in a file named \"proposal\"" - ); - cli::safe_exit(1) - } + let votes = compute_proposal_votes( + client, + proposal_id, + proposal.voting_end_epoch, + ) + .await; - let file = File::open(path.join("proposal")) - .expect("Proposal file must exist."); - let proposal: OfflineProposal = - serde_json::from_reader(file).expect( - "JSON was not well-formatted for proposal.", - ); - - let public_key = - get_public_key(client, &proposal.address) - .await - .expect("Public key should exist."); - - if !proposal.check_signature(&public_key) { - eprintln!("Bad proposal signature."); - cli::safe_exit(1) - } + let proposal_result = + compute_proposal_result(votes, total_voting_power, tally_type); - let votes = get_proposal_offline_votes( - client, - proposal.clone(), - files, - ) - .await; - let total_stake = get_total_staked_tokens( - client, - proposal.tally_epoch, - ) - .await - .try_into() - .unwrap(); - match utils::compute_tally( - votes, - total_stake, - &ProposalType::Default(None), - ) { - Ok(proposal_result) => { - println!("{:4}Result: {}", "", proposal_result) - } - Err(msg) => { - eprintln!("Error in tally computation: {}", msg) - } - } - } - None => { - eprintln!( - "Offline flag must be followed by data-path." - ); - cli::safe_exit(1) - } - }; + println!("Proposal Id: {} ", proposal_id); + println!("{:4}{}", "", proposal_result); + } else { + let proposal_folder = args.proposal_folder.expect( + "The argument --proposal-folder is required with --offline.", + ); + let data_directory = read_dir(&proposal_folder).unwrap_or_else(|_| { + panic!( + "Should be able to read {} directory.", + proposal_folder.to_string_lossy() + ) + }); + let files = read_offline_files(data_directory); + let proposal_path = find_offline_proposal(&files); + + let proposal = if let Some(path) = proposal_path { + let proposal_file = + fs::File::open(path).expect("file should open read only"); + let proposal: OfflineSignedProposal = + serde_json::from_reader(proposal_file) + .expect("file should be proper JSON"); + + let author_account = + rpc::get_account_info(client, &proposal.proposal.author) + .await + .expect("Account should exist."); + + let proposal = proposal.validate( + &author_account.public_keys_map, + author_account.threshold, + ); + + if proposal.is_ok() { + proposal.unwrap() } else { - eprintln!( - "Either --proposal-id or --data-path should be provided \ - as arguments." - ); - cli::safe_exit(1) + eprintln!("The offline proposal is not valid."); + return; + } + } else { + eprintln!("Couldn't find a file name offline_proposal_*.json."); + return; + }; + + let votes = find_offline_votes(&files) + .iter() + .map(|path| { + let vote_file = fs::File::open(path).expect(""); + let vote: OfflineVote = + serde_json::from_reader(vote_file).expect(""); + vote + }) + .collect::>(); + + let proposal_votes = + compute_offline_proposal_votes(client, &proposal, votes.clone()) + .await; + let total_voting_power = + get_total_staked_tokens(client, proposal.proposal.tally_epoch) + .await; + + let proposal_result = compute_proposal_result( + proposal_votes, + total_voting_power, + TallyType::TwoThird, + ); + + println!("Proposal offline: {}", proposal.proposal.hash()); + println!("Parsed {} votes.", votes.len()); + println!("{:4}{}", "", proposal_result); + } +} + +pub async fn query_account( + client: &C, + args: args::QueryAccount, +) { + let account = rpc::get_account_info(client, &args.owner).await; + if let Some(account) = account { + println!("Address: {}", account.address); + println!("Threshold: {}", account.threshold); + println!("Public keys:"); + for (public_key, _) in account.public_keys_map.pk_to_idx { + println!("- {}", public_key); + } + } else { + println!("No account exists for {}", args.owner); + } +} + +pub async fn query_pgf( + client: &C, + _args: args::QueryPgf, +) { + let stewards = query_pgf_stewards(client).await; + let fundings = query_pgf_fundings(client).await; + + match stewards.len() { + 0 => println!("Pgf stewards: no stewards are currectly set."), + _ => { + println!("Pgf stewards:"); + for steward in stewards { + println!("{:4}- {}", "", steward); + } + } + } + + match fundings.len() { + 0 => println!("Pgf fundings: no fundings are currently set."), + _ => { + println!("Pgf fundings:"); + for payment in fundings { + println!("{:4}- {}", "", payment.target); + println!("{:6}{}", "", payment.amount.to_string_native()); } } } @@ -1151,8 +1044,8 @@ pub async fn query_protocol_parameters< client: &C, _args: args::QueryProtocolParameters, ) { - let gov_parameters = get_governance_parameters(client).await; - println!("Governance Parameters\n {:4}", gov_parameters); + let governance_parameters = query_governance_parameters(client).await; + println!("Governance Parameters\n {:4}", governance_parameters); println!("Protocol parameters"); let key = param_storage::get_epoch_duration_storage_key(); @@ -1187,10 +1080,7 @@ pub async fn query_protocol_parameters< println!("{:4}Transactions whitelist: {:?}", "", tx_whitelist); println!("PoS parameters"); - let key = pos::params_key(); - let pos_params = query_storage_value::(client, &key) - .await - .expect("Parameter should be defined."); + let pos_params = query_pos_parameters(client).await; println!( "{:4}Block proposer reward: {}", "", pos_params.block_proposer_reward @@ -1242,6 +1132,30 @@ pub async fn query_unbond_with_slashing< ) } +pub async fn query_pos_parameters( + client: &C, +) -> PosParams { + unwrap_client_response::( + RPC.vp().pos().pos_params(client).await, + ) +} + +pub async fn query_pgf_stewards( + client: &C, +) -> BTreeSet
{ + unwrap_client_response::>( + RPC.vp().pgf().stewards(client).await, + ) +} + +pub async fn query_pgf_fundings( + client: &C, +) -> BTreeSet { + unwrap_client_response::>( + RPC.vp().pgf().funding(client).await, + ) +} + pub async fn query_and_print_unbonds< C: namada::ledger::queries::Client + Sync, >( @@ -1780,8 +1694,9 @@ where pub async fn get_public_key( client: &C, address: &Address, + index: u8, ) -> Option { - namada::ledger::rpc::get_public_key(client, address).await + namada::ledger::rpc::get_public_key_at(client, address, index).await } /// Check if the given address is a known validator. @@ -2031,199 +1946,6 @@ pub async fn epoch_sleep( } } -pub async fn get_proposal_votes( - client: &C, - epoch: Epoch, - proposal_id: u64, -) -> Votes { - namada::ledger::rpc::get_proposal_votes(client, epoch, proposal_id).await -} - -pub async fn get_proposal_offline_votes< - C: namada::ledger::queries::Client + Sync, ->( - client: &C, - proposal: OfflineProposal, - files: HashSet, -) -> Votes { - // let validators = get_all_validators(client, proposal.tally_epoch).await; - - let proposal_hash = proposal.compute_hash(); - - let mut yay_validators: HashMap = - HashMap::new(); - let mut delegators: HashMap< - Address, - HashMap, - > = HashMap::new(); - - for path in files { - let file = File::open(&path).expect("Proposal file must exist."); - let proposal_vote: OfflineVote = serde_json::from_reader(file) - .expect("JSON was not well-formatted for offline vote."); - - let key = pk_key(&proposal_vote.address); - let public_key = query_storage_value(client, &key) - .await - .expect("Public key should exist."); - - if !proposal_vote.proposal_hash.eq(&proposal_hash) - || !proposal_vote.check_signature(&public_key) - { - continue; - } - - if proposal_vote.vote.is_yay() - // && validators.contains(&proposal_vote.address) - && unwrap_client_response::( - RPC.vp().pos().is_validator(client, &proposal_vote.address).await, - ) - { - let amount: VotePower = get_validator_stake( - client, - proposal.tally_epoch, - &proposal_vote.address, - ) - .await - .unwrap_or_default() - .try_into() - .expect("Amount out of bounds"); - yay_validators.insert( - proposal_vote.address, - (amount, ProposalVote::Yay(VoteType::Default)), - ); - } else if is_delegator_at( - client, - &proposal_vote.address, - proposal.tally_epoch, - ) - .await - { - // TODO: decide whether to do this with `bond_with_slashing` RPC - // endpoint or with `bonds_and_unbonds` - let bonds_and_unbonds: pos::types::BondsAndUnbondsDetails = - unwrap_client_response::( - RPC.vp() - .pos() - .bonds_and_unbonds( - client, - &Some(proposal_vote.address.clone()), - &None, - ) - .await, - ); - for ( - BondId { - source: _, - validator, - }, - BondsAndUnbondsDetail { - bonds, - unbonds: _, - slashes: _, - }, - ) in bonds_and_unbonds - { - let mut delegated_amount = token::Amount::zero(); - for delta in bonds { - if delta.start <= proposal.tally_epoch { - delegated_amount += delta.amount - - delta.slashed_amount.unwrap_or_default(); - } - } - - let entry = delegators - .entry(proposal_vote.address.clone()) - .or_default(); - entry.insert( - validator, - ( - VotePower::try_from(delegated_amount).unwrap(), - proposal_vote.vote.clone(), - ), - ); - } - - // let key = pos::bonds_for_source_prefix(&proposal_vote.address); - // let bonds_iter = - // query_storage_prefix::(client, &key).await; - // if let Some(bonds) = bonds_iter { - // for (key, epoched_bonds) in bonds { - // // Look-up slashes for the validator in this key and - // // apply them if any - // let validator = - // pos::get_validator_address_from_bond(&key) - // .expect( - // "Delegation key should contain validator - // address.", ); - // let slashes_key = pos::validator_slashes_key(&validator); - // let slashes = query_storage_value::( - // client, - // &slashes_key, - // ) - // .await - // .unwrap_or_default(); - // let mut delegated_amount: token::Amount = 0.into(); - // let bond = epoched_bonds - // .get(proposal.tally_epoch) - // .expect("Delegation bond should be defined."); - // let mut to_deduct = bond.neg_deltas; - // for (start_epoch, &(mut delta)) in - // bond.pos_deltas.iter().sorted() - // { - // // deduct bond's neg_deltas - // if to_deduct > delta { - // to_deduct -= delta; - // // If the whole bond was deducted, continue to - // // the next one - // continue; - // } else { - // delta -= to_deduct; - // to_deduct = token::Amount::zero(); - // } - - // delta = apply_slashes( - // &slashes, - // delta, - // *start_epoch, - // None, - // None, - // ); - // delegated_amount += delta; - // } - - // let validator_address = - // pos::get_validator_address_from_bond(&key).expect( - // "Delegation key should contain validator - // address.", ); - // if proposal_vote.vote.is_yay() { - // let entry = yay_delegators - // .entry(proposal_vote.address.clone()) - // .or_default(); - // entry.insert( - // validator_address, - // VotePower::from(delegated_amount), - // ); - // } else { - // let entry = nay_delegators - // .entry(proposal_vote.address.clone()) - // .or_default(); - // entry.insert( - // validator_address, - // VotePower::from(delegated_amount), - // ); - // } - // } - // } - } - } - - Votes { - yay_validators, - delegators, - } -} - pub async fn get_bond_amount_at( client: &C, delegator: &Address, @@ -2282,12 +2004,23 @@ pub async fn get_delegators_delegation< namada::ledger::rpc::get_delegators_delegation(client, address).await } -pub async fn get_governance_parameters< +pub async fn get_delegators_delegation_at< C: namada::ledger::queries::Client + Sync, >( client: &C, -) -> GovParams { - namada::ledger::rpc::get_governance_parameters(client).await + address: &Address, + epoch: Epoch, +) -> HashMap { + namada::ledger::rpc::get_delegators_delegation_at(client, address, epoch) + .await +} + +pub async fn query_governance_parameters< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, +) -> GovernanceParameters { + namada::ledger::rpc::query_governance_parameters(client).await } /// Try to find an alias for a given address from the wallet. If not found, @@ -2308,3 +2041,121 @@ fn unwrap_client_response( cli::safe_exit(1) }) } + +pub async fn compute_offline_proposal_votes< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, + proposal: &OfflineSignedProposal, + votes: Vec, +) -> ProposalVotes { + let mut validators_vote: HashMap = HashMap::default(); + let mut validator_voting_power: HashMap = + HashMap::default(); + let mut delegators_vote: HashMap = HashMap::default(); + let mut delegator_voting_power: HashMap< + Address, + HashMap, + > = HashMap::default(); + for vote in votes { + let is_validator = is_validator(client, &vote.address).await; + let is_delegator = is_delegator(client, &vote.address).await; + if is_validator { + let validator_stake = get_validator_stake( + client, + proposal.proposal.tally_epoch, + &vote.address, + ) + .await + .unwrap_or_default(); + validators_vote.insert(vote.address.clone(), vote.clone().into()); + validator_voting_power + .insert(vote.address.clone(), validator_stake); + } else if is_delegator { + let validators = get_delegators_delegation_at( + client, + &vote.address.clone(), + proposal.proposal.tally_epoch, + ) + .await; + + for validator in vote.delegations.clone() { + let delegator_stake = + validators.get(&validator).cloned().unwrap_or_default(); + + delegators_vote + .insert(vote.address.clone(), vote.clone().into()); + delegator_voting_power + .entry(vote.address.clone()) + .or_default() + .insert(validator, delegator_stake); + } + } else { + println!( + "Skipping vote, not a validator/delegator at epoch {}.", + proposal.proposal.tally_epoch + ); + } + } + + ProposalVotes { + validators_vote, + validator_voting_power, + delegators_vote, + delegator_voting_power, + } +} + +pub async fn compute_proposal_votes< + C: namada::ledger::queries::Client + Sync, +>( + client: &C, + proposal_id: u64, + epoch: Epoch, +) -> ProposalVotes { + let votes = + namada::ledger::rpc::query_proposal_votes(client, proposal_id).await; + + let mut validators_vote: HashMap = HashMap::default(); + let mut validator_voting_power: HashMap = + HashMap::default(); + let mut delegators_vote: HashMap = HashMap::default(); + let mut delegator_voting_power: HashMap< + Address, + HashMap, + > = HashMap::default(); + + for vote in votes { + if vote.is_validator() { + let validator_stake = + get_validator_stake(client, epoch, &vote.validator.clone()) + .await + .unwrap_or_default(); + + validators_vote.insert(vote.validator.clone(), vote.data.into()); + validator_voting_power.insert(vote.validator, validator_stake); + } else { + let delegator_stake = get_bond_amount_at( + client, + &vote.delegator, + &vote.validator, + epoch, + ) + .await + .unwrap_or_default(); + + delegators_vote.insert(vote.delegator.clone(), vote.data.into()); + delegator_voting_power + .entry(vote.delegator.clone()) + .or_default() + .insert(vote.validator, delegator_stake); + } + } + + ProposalVotes { + validators_vote, + validator_voting_power, + delegators_vote, + delegator_voting_power, + } +} diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 3c2a5d67fe1..93ebe5f1146 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -31,7 +31,7 @@ where /// signer. Return the given signing key or public key of the given signer if /// possible. If no explicit signer given, use the `default`. If no `default` /// is given, panics. -pub async fn tx_signer( +pub async fn tx_signers( client: &C, wallet: &mut Wallet, args: &args::Tx, @@ -42,7 +42,7 @@ where C::Error: std::fmt::Display, U: WalletUtils, { - namada::ledger::signing::tx_signer::(client, wallet, args, default) + namada::ledger::signing::tx_signers::(client, wallet, args, default) .await } diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 974e940db41..d40adbe19f8 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1,4 +1,3 @@ -use std::collections::HashSet; use std::env; use std::fmt::Debug; use std::fs::{File, OpenOptions}; @@ -7,32 +6,28 @@ use std::path::PathBuf; use async_trait::async_trait; use borsh::{BorshDeserialize, BorshSerialize}; -use data_encoding::HEXLOWER_PERMISSIVE; use masp_proofs::prover::LocalTxProver; -use namada::ledger::governance::storage as gov_storage; +use namada::core::ledger::governance::cli::offline::{ + OfflineSignedProposal, OfflineVote, +}; +use namada::core::ledger::governance::cli::onchain::{ + DefaultProposal, PgfFundingProposal, PgfStewardProposal, ProposalVote, +}; use namada::ledger::queries::Client; use namada::ledger::rpc::{TxBroadcastData, TxResponse}; -use namada::ledger::signing::TxSigningKey; use namada::ledger::wallet::{Wallet, WalletUtils}; use namada::ledger::{masp, pos, signing, tx}; use namada::proof_of_stake::parameters::PosParams; -use namada::proto::{Code, Data, Section, Tx}; -use namada::types::address::Address; +use namada::proto::Tx; +use namada::types::address::{Address, ImplicitAddress}; use namada::types::dec::Dec; -use namada::types::governance::{ - OfflineProposal, OfflineVote, Proposal, ProposalVote, VoteType, -}; use namada::types::key::{self, *}; -use namada::types::storage::{Epoch, Key}; -use namada::types::token; -use namada::types::transaction::governance::{ProposalType, VoteProposalData}; -use namada::types::transaction::{InitValidator, TxType}; +use namada::types::transaction::pos::InitValidator; +use namada::types::tx::TxBuilder; use super::rpc; -use crate::cli::context::WalletAddress; use crate::cli::{args, safe_exit, Context}; use crate::client::rpc::query_wasm_code_hash; -use crate::client::signing::find_pk; use crate::client::tx::tx::ProcessTxResponse; use crate::config::TendermintMode; use crate::facade::tendermint_rpc::endpoint::broadcast::tx_sync::Response; @@ -43,31 +38,45 @@ use crate::wallet::{gen_validator_keys, read_and_confirm_encryption_password}; pub async fn submit_reveal_aux( client: &C, ctx: &mut Context, - args: &args::Tx, - addr: Option
, - pk: common::PublicKey, - tx: &mut Tx, + args: args::Tx, + address: &Address, ) -> Result<(), tx::Error> { - if let Some(Address::Implicit(_)) = addr { - let reveal_pk = tx::build_reveal_pk( - client, - &mut ctx.wallet, - args::RevealPk { - tx: args.clone(), - public_key: pk.clone(), - }, - ) - .await?; - if let Some((mut rtx, _, pk)) = reveal_pk { - // Sign the reveal public key transaction with the fee payer - signing::sign_tx(&mut ctx.wallet, &mut rtx, args, &pk).await?; - // Submit the reveal public key transaction first - tx::process_tx(client, &mut ctx.wallet, args, rtx).await?; - // Update the stateful PoW challenge of the outer transaction - #[cfg(not(feature = "mainnet"))] - signing::update_pow_challenge(client, args, tx, &pk, false).await; + if args.dump_tx { + return Ok(()); + } + + if let Address::Implicit(ImplicitAddress(pkh)) = address { + let key = ctx + .wallet + .find_key_by_pkh(pkh, args.clone().password) + .map_err(|e| tx::Error::Other(e.to_string()))?; + let public_key = key.ref_to(); + + if tx::is_reveal_pk_needed::(client, address, args.force).await? { + let gas_payer = if let Some(gas_payer) = + args.clone().gas_payer.or(args.signing_keys.get(0).cloned()) + { + gas_payer + } else { + return Err(tx::Error::InvalidFeePayer); + }; + + let tx_builder = tx::build_reveal_pk::( + client, + &args, + address, + &public_key, + &gas_payer.ref_to(), + ) + .await?; + + let tx_builder = tx_builder.add_gas_payer(gas_payer); + + tx::process_tx(client, &mut ctx.wallet, &args, tx_builder.build()) + .await?; } } + Ok(()) } @@ -80,28 +89,73 @@ where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let (mut tx, addr, pk) = - tx::build_custom(client, &mut ctx.wallet, args.clone()).await?; - submit_reveal_aux(client, ctx, &args.tx, addr, pk.clone(), &mut tx).await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + let default_signer = signing::signer_from_address(Some(args.owner.clone())); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &args.owner, + default_signer, + ) + .await?; + + submit_reveal_aux(client, ctx, args.tx.clone(), &args.owner).await?; + + let tx_builder = + tx::build_custom(client, args.clone(), &signing_data.gas_payer).await?; + + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &args.tx, + tx_builder, + signing_data, + )?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder.build()) + .await?; + } + Ok(()) } -pub async fn submit_update_vp( +pub async fn submit_update_account( client: &C, ctx: &mut Context, - args: args::TxUpdateVp, + args: args::TxUpdateAccount, ) -> Result<(), tx::Error> where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let (mut tx, addr, pk) = - tx::build_update_vp(client, &mut ctx.wallet, args.clone()).await?; - submit_reveal_aux(client, ctx, &args.tx, addr, pk.clone(), &mut tx).await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + let default_signer = signing::signer_from_address(Some(args.addr.clone())); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &args.addr, + default_signer, + ) + .await?; + + let tx_builder = + tx::build_update_account(client, args.clone(), &signing_data.gas_payer) + .await?; + + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &args.tx, + tx_builder, + signing_data, + )?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder.build()) + .await?; + } + Ok(()) } @@ -114,11 +168,29 @@ where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let (mut tx, addr, pk) = - tx::build_init_account(client, &mut ctx.wallet, args.clone()).await?; - submit_reveal_aux(client, ctx, &args.tx, addr, pk.clone(), &mut tx).await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + let gas_payer = if let Some(gas_payer) = args.tx.gas_payer.clone().or(args + .tx + .signing_keys + .get(0) + .cloned()) + { + gas_payer + } else { + return Err(tx::Error::InvalidFeePayer); + }; + + let tx_builder = + tx::build_init_account(client, args.clone(), &gas_payer.ref_to()) + .await?; + + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = tx_builder.add_gas_payer(gas_payer); + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder.build()) + .await?; + } + Ok(()) } @@ -127,9 +199,9 @@ pub async fn submit_init_validator( mut ctx: Context, args::TxInitValidator { tx: tx_args, - source, scheme, - account_key, + account_keys, + threshold, consensus_key, eth_cold_key, eth_hot_key, @@ -160,25 +232,19 @@ where let validator_key_alias = format!("{}-key", alias); let consensus_key_alias = format!("{}-consensus-key", alias); + + let threshold = match threshold { + Some(threshold) => threshold, + None => { + if account_keys.len() == 1 { + 1u8 + } else { + safe_exit(1) + } + } + }; let eth_hot_key_alias = format!("{}-eth-hot-key", alias); let eth_cold_key_alias = format!("{}-eth-cold-key", alias); - let account_key = account_key.unwrap_or_else(|| { - println!("Generating validator account key..."); - let password = - read_and_confirm_encryption_password(unsafe_dont_encrypt); - ctx.wallet - .gen_key( - scheme, - Some(validator_key_alias.clone()), - tx_args.wallet_alias_force, - password, - None, - ) - .expect("Key generation should not fail.") - .expect("No existing alias expected.") - .1 - .ref_to() - }); let consensus_key = consensus_key .map(|key| match key { @@ -311,12 +377,15 @@ where .await .unwrap(); - let mut tx = Tx::new(TxType::Raw); - let extra = tx.add_section(Section::ExtraData(Code::from_hash( - validator_vp_code_hash, - ))); + let chain_id = tx_args.chain_id.clone().unwrap(); + let tx_builder = TxBuilder::new(chain_id, tx_args.expiration); + + let (tx_builder, extra_section_hash) = + tx_builder.add_extra_section_from_hash(validator_vp_code_hash); + let data = InitValidator { - account_key, + account_keys, + threshold, consensus_key: consensus_key.ref_to(), eth_cold_key: key::secp256k1::PublicKey::try_from_pk(ð_cold_pk) .unwrap(), @@ -326,91 +395,113 @@ where dkg_key, commission_rate, max_commission_rate_change, - validator_vp_code_hash: extra.get_hash(), + validator_vp_code_hash: extra_section_hash, + }; + + let tx_builder = tx_builder.add_code_from_hash(tx_code_hash).add_data(data); + + let gas_payer = if let Some(gas_payer) = tx_args + .gas_payer + .clone() + .or(tx_args.signing_keys.get(0).cloned()) + { + gas_payer + } else { + return Err(tx::Error::InvalidFeePayer); }; - let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); - tx.header.chain_id = tx_args.chain_id.clone().unwrap(); - tx.header.expiration = tx_args.expiration; - tx.set_data(Data::new(data)); - tx.set_code(Code::from_hash(tx_code_hash)); - let (mut tx, addr, pk) = tx::prepare_tx( + let tx_builder = tx::prepare_tx( client, - &mut ctx.wallet, &tx_args, - tx, - TxSigningKey::WalletAddress(source), + tx_builder, + gas_payer.ref_to(), #[cfg(not(feature = "mainnet"))] false, ) .await?; - submit_reveal_aux(client, &mut ctx, &tx_args, addr, pk.clone(), &mut tx) - .await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &tx_args, &pk).await?; - let result = tx::process_tx(client, &mut ctx.wallet, &tx_args, tx) + + if tx_args.dump_tx { + tx::dump_tx(&tx_args, tx_builder); + } else { + let tx_builder = tx_builder.add_gas_payer(gas_payer); + + let result = tx::process_tx( + client, + &mut ctx.wallet, + &tx_args, + tx_builder.build(), + ) .await? .initialized_accounts(); - if !tx_args.dry_run { - let (validator_address_alias, validator_address) = match &result[..] { - // There should be 1 account for the validator itself - [validator_address] => { - if let Some(alias) = ctx.wallet.find_alias(validator_address) { - (alias.clone(), validator_address.clone()) - } else { + if !tx_args.dry_run { + let (validator_address_alias, validator_address) = match &result[..] + { + // There should be 1 account for the validator itself + [validator_address] => { + if let Some(alias) = + ctx.wallet.find_alias(validator_address) + { + (alias.clone(), validator_address.clone()) + } else { + eprintln!("Expected one account to be created"); + safe_exit(1) + } + } + _ => { eprintln!("Expected one account to be created"); safe_exit(1) } - } - _ => { - eprintln!("Expected one account to be created"); - safe_exit(1) - } - }; - // add validator address and keys to the wallet - ctx.wallet - .add_validator_data(validator_address, validator_keys); - crate::wallet::save(&ctx.wallet) - .unwrap_or_else(|err| eprintln!("{}", err)); - - let tendermint_home = ctx.config.ledger.cometbft_dir(); - tendermint_node::write_validator_key(&tendermint_home, &consensus_key); - tendermint_node::write_validator_state(tendermint_home); - - // Write Namada config stuff or figure out how to do the above - // tendermint_node things two epochs in the future!!! - ctx.config.ledger.shell.tendermint_mode = TendermintMode::Validator; - ctx.config - .write( - &ctx.config.ledger.shell.base_dir, - &ctx.config.ledger.chain_id, - true, - ) - .unwrap(); + }; + // add validator address and keys to the wallet + ctx.wallet + .add_validator_data(validator_address, validator_keys); + crate::wallet::save(&ctx.wallet) + .unwrap_or_else(|err| eprintln!("{}", err)); + + let tendermint_home = ctx.config.ledger.cometbft_dir(); + tendermint_node::write_validator_key( + &tendermint_home, + &consensus_key, + ); + tendermint_node::write_validator_state(tendermint_home); + + // Write Namada config stuff or figure out how to do the above + // tendermint_node things two epochs in the future!!! + ctx.config.ledger.shell.tendermint_mode = TendermintMode::Validator; + ctx.config + .write( + &ctx.config.ledger.shell.base_dir, + &ctx.config.ledger.chain_id, + true, + ) + .unwrap(); - let key = pos::params_key(); - let pos_params = rpc::query_storage_value::(client, &key) - .await - .expect("Pos parameter should be defined."); + let key = pos::params_key(); + let pos_params = + rpc::query_storage_value::(client, &key) + .await + .expect("Pos parameter should be defined."); - println!(); - println!( - "The validator's addresses and keys were stored in the wallet:" - ); - println!(" Validator address \"{}\"", validator_address_alias); - println!(" Validator account key \"{}\"", validator_key_alias); - println!(" Consensus key \"{}\"", consensus_key_alias); - println!( - "The ledger node has been setup to use this validator's address \ - and consensus key." - ); - println!( - "Your validator will be active in {} epochs. Be sure to restart \ - your node for the changes to take effect!", - pos_params.pipeline_len - ); - } else { - println!("Transaction dry run. No addresses have been saved."); + println!(); + println!( + "The validator's addresses and keys were stored in the wallet:" + ); + println!(" Validator address \"{}\"", validator_address_alias); + println!(" Validator account key \"{}\"", validator_key_alias); + println!(" Consensus key \"{}\"", consensus_key_alias); + println!( + "The ledger node has been setup to use this validator's \ + address and consensus key." + ); + println!( + "Your validator will be active in {} epochs. Be sure to \ + restart your node for the changes to take effect!", + pos_params.pipeline_len + ); + } else { + println!("Transaction dry run. No addresses have been saved."); + } } Ok(()) } @@ -529,46 +620,76 @@ pub async fn submit_transfer( args: args::TxTransfer, ) -> Result<(), tx::Error> { for _ in 0..2 { - let arg = args.clone(); - let (mut tx, addr, pk, tx_epoch, _isf) = - tx::build_transfer(client, &mut ctx.wallet, &mut ctx.shielded, arg) - .await?; + let default_signer = + signing::signer_from_address(Some(args.source.effective_address())); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &args.source.effective_address(), + default_signer, + ) + .await?; + submit_reveal_aux( client, &mut ctx, - &args.tx, - addr, - pk.clone(), - &mut tx, + args.tx.clone(), + &args.source.effective_address(), ) .await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; - let result = - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; - // Query the epoch in which the transaction was probably submitted - let submission_epoch = rpc::query_and_print_epoch(client).await; - - match result { - ProcessTxResponse::Applied(resp) if - // If a transaction is shielded - tx_epoch.is_some() && - // And it is rejected by a VP - resp.code == 1.to_string() && - // And the its submission epoch doesn't match construction epoch - tx_epoch.unwrap() != submission_epoch => - { - // Then we probably straddled an epoch boundary. Let's retry... - eprintln!( - "MASP transaction rejected and this may be due to the \ - epoch changing. Attempting to resubmit transaction.", - ); - continue; - }, - // Otherwise either the transaction was successful or it will not - // benefit from resubmission - _ => break, + + let arg = args.clone(); + let (tx_builder, tx_epoch) = tx::build_transfer( + client, + &mut ctx.shielded, + arg, + &signing_data.gas_payer, + ) + .await?; + + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &args.tx, + tx_builder, + signing_data, + )?; + let result = tx::process_tx( + client, + &mut ctx.wallet, + &args.tx, + tx_builder.build(), + ) + .await?; + + let submission_epoch = rpc::query_and_print_epoch(client).await; + + match result { + ProcessTxResponse::Applied(resp) if + // If a transaction is shielded + tx_epoch.is_some() && + // And it is rejected by a VP + resp.code == 1.to_string() && + // And its submission epoch doesn't match construction epoch + tx_epoch.unwrap() != submission_epoch => + { + // Then we probably straddled an epoch boundary. Let's retry... + eprintln!( + "MASP transaction rejected and this may be due to the \ + epoch changing. Attempting to resubmit transaction.", + ); + continue; + }, + // Otherwise either the transaction was successful or it will not + // benefit from resubmission + _ => break, + } } } + Ok(()) } @@ -581,12 +702,36 @@ where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let (mut tx, addr, pk) = - tx::build_ibc_transfer(client, &mut ctx.wallet, args.clone()).await?; - submit_reveal_aux(client, &mut ctx, &args.tx, addr, pk.clone(), &mut tx) - .await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + let default_signer = + signing::signer_from_address(Some(args.source.clone())); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &args.source, + default_signer, + ) + .await?; + + submit_reveal_aux(client, &mut ctx, args.tx.clone(), &args.source).await?; + + let tx_builder = + tx::build_ibc_transfer(client, args.clone(), &signing_data.gas_payer) + .await?; + + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &args.tx, + tx_builder, + signing_data, + )?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder.build()) + .await?; + } + Ok(()) } @@ -599,180 +744,181 @@ where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let file = File::open(&args.proposal_data).expect("File must exist."); - let proposal: Proposal = - serde_json::from_reader(file).expect("JSON was not well-formatted"); - - let signer = WalletAddress::new(proposal.clone().author.to_string()); let current_epoch = rpc::query_and_print_epoch(client).await; + let governance_parameters = rpc::query_governance_parameters(client).await; - let governance_parameters = rpc::get_governance_parameters(client).await; - if proposal.voting_start_epoch <= current_epoch - || proposal.voting_start_epoch.0 - % governance_parameters.min_proposal_period - != 0 - { - println!("{}", proposal.voting_start_epoch <= current_epoch); - println!( - "{}", - proposal.voting_start_epoch.0 - % governance_parameters.min_proposal_period - == 0 - ); - eprintln!( - "Invalid proposal start epoch: {} must be greater than current \ - epoch {} and a multiple of {}", - proposal.voting_start_epoch, - current_epoch, - governance_parameters.min_proposal_period - ); - if !args.tx.force { - safe_exit(1) - } - } else if proposal.voting_end_epoch <= proposal.voting_start_epoch - || proposal.voting_end_epoch.0 - proposal.voting_start_epoch.0 - < governance_parameters.min_proposal_period - || proposal.voting_end_epoch.0 - proposal.voting_start_epoch.0 - > governance_parameters.max_proposal_period - || proposal.voting_end_epoch.0 % 3 != 0 - { - eprintln!( - "Invalid proposal end epoch: difference between proposal start \ - and end epoch must be at least {} and at max {} and end epoch \ - must be a multiple of {}", - governance_parameters.min_proposal_period, - governance_parameters.max_proposal_period, - governance_parameters.min_proposal_period - ); - if !args.tx.force { - safe_exit(1) - } - } else if proposal.grace_epoch <= proposal.voting_end_epoch - || proposal.grace_epoch.0 - proposal.voting_end_epoch.0 - < governance_parameters.min_proposal_grace_epochs - { - eprintln!( - "Invalid proposal grace epoch: difference between proposal grace \ - and end epoch must be at least {}", - governance_parameters.min_proposal_grace_epochs - ); - if !args.tx.force { - safe_exit(1) - } - } - - if args.offline { - let signer = ctx.get(&signer); - let key = find_pk(client, &mut ctx.wallet, &signer).await?; - let signing_key = - signing::find_key_by_pk(&mut ctx.wallet, &args.tx, &key)?; - let offline_proposal = - OfflineProposal::new(proposal, signer, &signing_key); - let proposal_filename = args - .proposal_data - .parent() - .expect("No parent found") - .join("proposal"); - let out = File::create(&proposal_filename).unwrap(); - match serde_json::to_writer_pretty(out, &offline_proposal) { - Ok(_) => { - println!( - "Proposal created: {}.", - proposal_filename.to_string_lossy() - ); - } - Err(e) => { - eprintln!("Error while creating proposal file: {}.", e); - safe_exit(1) - } - } - Ok(()) - } else { - let signer = ctx.get(&signer); - let tx_data = proposal.clone().try_into(); - let (mut init_proposal_data, init_proposal_content, init_proposal_code) = - if let Ok(data) = tx_data { - data - } else { - eprintln!("Invalid data for init proposal transaction."); - safe_exit(1) - }; + let (tx_builder, signing_data) = if args.is_offline { + let proposal = namada::core::ledger::governance::cli::offline::OfflineProposal::try_from(args.proposal_data.as_ref()).map_err(|e| tx::Error::FailedGovernaneProposalDeserialize(e.to_string()))?.validate(current_epoch) + .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; - let balance = - rpc::get_token_balance(client, &ctx.native_token, &proposal.author) - .await; - if balance - < token::Amount::from_uint( - governance_parameters.min_proposal_fund, - 0, - ) - .unwrap() - { - eprintln!( - "Address {} doesn't have enough funds.", - &proposal.author - ); - safe_exit(1); - } + let default_signer = + signing::signer_from_address(Some(proposal.author.clone())); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &proposal.author, + default_signer, + ) + .await?; - if init_proposal_content.len() - > governance_parameters.max_proposal_content_size as usize - { - eprintln!("Proposal content size too big.",); - safe_exit(1); - } + let signed_offline_proposal = proposal + .sign(args.tx.signing_keys, &signing_data.account_public_keys_map); + let output_file_path = signed_offline_proposal + .serialize(args.tx.output_folder) + .map_err(|e| { + tx::Error::FailedGovernaneProposalDeserialize(e.to_string()) + })?; + + println!("Proposal serialized to: {}", output_file_path); + return Ok(()); + } else if args.is_pgf_funding { + let proposal = + PgfFundingProposal::try_from(args.proposal_data.as_ref()) + .map_err(|e| { + tx::Error::FailedGovernaneProposalDeserialize(e.to_string()) + })? + .validate(&governance_parameters, current_epoch) + .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; + + let default_signer = signing::signer_from_address(Some( + proposal.proposal.author.clone(), + )); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &proposal.proposal.author, + default_signer, + ) + .await?; - let mut tx = Tx::new(TxType::Raw); - let tx_code_hash = query_wasm_code_hash(client, args::TX_INIT_PROPOSAL) - .await - .unwrap(); - tx.header.chain_id = ctx.config.ledger.chain_id.clone(); - tx.header.expiration = args.tx.expiration; - // Put the content of this proposal into an extra section - { - let content_sec = tx.add_section(Section::ExtraData(Code::new( - init_proposal_content, - ))); - let content_sec_hash = content_sec.get_hash(); - init_proposal_data.content = content_sec_hash; - } - // Put any proposal code into an extra section - if let Some(init_proposal_code) = init_proposal_code { - let code_sec = tx - .add_section(Section::ExtraData(Code::new(init_proposal_code))); - let code_sec_hash = code_sec.get_hash(); - init_proposal_data.r#type = - ProposalType::Default(Some(code_sec_hash)); - } - let data = init_proposal_data - .try_to_vec() - .expect("Encoding proposal data shouldn't fail"); - tx.set_data(Data::new(data)); - tx.set_code(Code::from_hash(tx_code_hash)); + submit_reveal_aux( + client, + &mut ctx, + args.tx.clone(), + &proposal.proposal.author, + ) + .await?; - let (mut tx, addr, pk) = tx::prepare_tx( + ( + tx::build_pgf_funding_proposal( + client, + args.clone(), + proposal, + &signing_data.gas_payer, + ) + .await?, + signing_data, + ) + } else if args.is_pgf_stewards { + let proposal = PgfStewardProposal::try_from( + args.proposal_data.as_ref(), + ) + .map_err(|e| { + tx::Error::FailedGovernaneProposalDeserialize(e.to_string()) + })?; + let author_balane = rpc::get_token_balance( + client, + &ctx.native_token, + &proposal.proposal.author, + ) + .await; + let proposal = proposal + .validate(&governance_parameters, current_epoch, author_balane) + .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; + + let default_signer = signing::signer_from_address(Some( + proposal.proposal.author.clone(), + )); + let signing_data = signing::aux_signing_data( client, &mut ctx.wallet, &args.tx, - tx, - TxSigningKey::WalletAddress(signer), - #[cfg(not(feature = "mainnet"))] - false, + &proposal.proposal.author, + default_signer, ) .await?; + submit_reveal_aux( client, &mut ctx, + args.tx.clone(), + &proposal.proposal.author, + ) + .await?; + + ( + tx::build_pgf_stewards_proposal( + client, + args.clone(), + proposal, + &signing_data.gas_payer, + ) + .await?, + signing_data, + ) + } else { + let proposal = DefaultProposal::try_from(args.proposal_data.as_ref()) + .map_err(|e| { + tx::Error::FailedGovernaneProposalDeserialize(e.to_string()) + })?; + let author_balane = rpc::get_token_balance( + client, + &ctx.native_token, + &proposal.proposal.author, + ) + .await; + let proposal = proposal + .validate(&governance_parameters, current_epoch, author_balane) + .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; + + let default_signer = signing::signer_from_address(Some( + proposal.proposal.author.clone(), + )); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, &args.tx, - addr, - pk.clone(), - &mut tx, + &proposal.proposal.author, + default_signer, ) .await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; - Ok(()) + + submit_reveal_aux( + client, + &mut ctx, + args.tx.clone(), + &proposal.proposal.author, + ) + .await?; + + ( + tx::build_default_proposal( + client, + args.clone(), + proposal, + &signing_data.gas_payer, + ) + .await?, + signing_data, + ) + }; + + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &args.tx, + tx_builder, + signing_data, + )?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder.build()) + .await?; } + + Ok(()) } pub async fn submit_vote_proposal( @@ -784,350 +930,176 @@ where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let signer = if let Some(addr) = &args.tx.signer { - addr - } else { - eprintln!("Missing mandatory argument --signer."); - safe_exit(1) - }; - - // Construct vote - let proposal_vote = match args.vote.to_ascii_lowercase().as_str() { - "yay" => { - if let Some(pgf) = args.proposal_pgf { - let splits = pgf.trim().split_ascii_whitespace(); - let address_iter = splits.clone().step_by(2); - let cap_iter = splits.into_iter().skip(1).step_by(2); - let mut set = HashSet::new(); - for (address, cap) in - address_iter.zip(cap_iter).map(|(addr, cap)| { - ( - addr.parse() - .expect("Failed to parse pgf council address"), - cap.parse::() - .expect("Failed to parse pgf spending cap"), - ) - }) - { - set.insert(( - address, - token::Amount::from_uint(cap, 0).unwrap(), - )); - } - - ProposalVote::Yay(VoteType::PGFCouncil(set)) - } else if let Some(eth) = args.proposal_eth { - let mut splits = eth.trim().split_ascii_whitespace(); - // Sign the message - let sigkey = splits - .next() - .expect("Expected signing key") - .parse::() - .expect("Signing key parsing failed."); - - let msg = splits.next().expect("Missing message to sign"); - if splits.next().is_some() { - eprintln!("Unexpected argument after message"); - safe_exit(1); - } + let current_epoch = rpc::query_and_print_epoch(client).await; - ProposalVote::Yay(VoteType::ETHBridge(common::SigScheme::sign( - &sigkey, - HEXLOWER_PERMISSIVE - .decode(msg.as_bytes()) - .expect("Error while decoding message"), - ))) - } else { - ProposalVote::Yay(VoteType::Default) - } - } - "nay" => ProposalVote::Nay, - _ => { - eprintln!("Vote must be either yay or nay"); - safe_exit(1); - } - }; + let default_signer = signing::signer_from_address(Some(args.voter.clone())); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &args.voter, + default_signer.clone(), + ) + .await?; - if args.offline { - if !proposal_vote.is_default_vote() { - eprintln!( - "Wrong vote type for offline proposal. Just vote yay or nay!" - ); - safe_exit(1); - } - let proposal_file_path = - args.proposal_data.expect("Proposal file should exist."); - let file = File::open(&proposal_file_path).expect("File must exist."); + let tx_builder = if args.is_offline { + let proposal_vote = ProposalVote::try_from(args.vote) + .map_err(|_| tx::Error::InvalidProposalVote)?; - let proposal: OfflineProposal = - serde_json::from_reader(file).expect("JSON was not well-formatted"); - let public_key = rpc::get_public_key(client, &proposal.address) - .await - .expect("Public key should exist."); - if !proposal.check_signature(&public_key) { - eprintln!("Proposal signature mismatch!"); - safe_exit(1) - } + let proposal = OfflineSignedProposal::try_from( + args.proposal_data.unwrap().as_ref(), + ) + .map_err(|e| tx::Error::InvalidProposal(e.to_string()))? + .validate( + &signing_data.account_public_keys_map, + signing_data.threshold, + ) + .map_err(|e| tx::Error::InvalidProposal(e.to_string()))?; + let delegations = rpc::get_delegators_delegation_at( + client, + &args.voter, + proposal.proposal.tally_epoch, + ) + .await + .keys() + .cloned() + .collect::>(); - let key = find_pk(client, &mut ctx.wallet, signer).await?; - let signing_key = - signing::find_key_by_pk(&mut ctx.wallet, &args.tx, &key)?; let offline_vote = OfflineVote::new( &proposal, proposal_vote, - signer.clone(), - &signing_key, + args.voter.clone(), + delegations, ); - let proposal_vote_filename = proposal_file_path - .parent() - .expect("No parent found") - .join(format!("proposal-vote-{}", &signer.to_string())); - let out = File::create(&proposal_vote_filename).unwrap(); - match serde_json::to_writer_pretty(out, &offline_vote) { - Ok(_) => { - println!( - "Proposal vote created: {}.", - proposal_vote_filename.to_string_lossy() - ); - Ok(()) - } - Err(e) => { - eprintln!("Error while creating proposal vote file: {}.", e); - safe_exit(1) - } - } - } else { - let current_epoch = rpc::query_and_print_epoch(client).await; + let offline_signed_vote = offline_vote + .sign(args.tx.signing_keys, &signing_data.account_public_keys_map); + let output_file_path = offline_signed_vote + .serialize(args.tx.output_folder) + .expect("Should be able to serialize the offline proposal"); - let voter_address = signer.clone(); - let proposal_id = args.proposal_id.unwrap(); - let proposal_start_epoch_key = - gov_storage::get_voting_start_epoch_key(proposal_id); - let proposal_start_epoch = rpc::query_storage_value::( + println!("Proposal vote serialized to: {}", output_file_path); + return Ok(()); + } else { + tx::build_vote_proposal( client, - &proposal_start_epoch_key, + args.clone(), + current_epoch, + &signing_data.gas_payer, ) - .await; - - // Check vote type and memo - let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); - let proposal_type: ProposalType = rpc::query_storage_value::< - C, - ProposalType, - >(client, &proposal_type_key) - .await - .unwrap_or_else(|| { - panic!("Didn't find type of proposal id {} in storage", proposal_id) - }); - - if let ProposalVote::Yay(ref vote_type) = proposal_vote { - if &proposal_type != vote_type { - eprintln!( - "Expected vote of type {}, found {}", - proposal_type, args.vote - ); - safe_exit(1); - } else if let VoteType::PGFCouncil(set) = vote_type { - // Check that addresses proposed as council are established and - // are present in storage - for (address, _) in set { - match address { - Address::Established(_) => { - let vp_key = Key::validity_predicate(address); - if !rpc::query_has_storage_key::(client, &vp_key) - .await - { - eprintln!( - "Proposed PGF council {} cannot be found \ - in storage", - address - ); - safe_exit(1); - } - } - _ => { - eprintln!( - "PGF council vote contains a non-established \ - address: {}", - address - ); - safe_exit(1); - } - } - } - } - } - - match proposal_start_epoch { - Some(epoch) => { - if current_epoch < epoch { - eprintln!( - "Current epoch {} is not greater than proposal start \ - epoch {}", - current_epoch, epoch - ); - - if !args.tx.force { - safe_exit(1) - } - } - let mut delegations = - rpc::get_delegators_delegation(client, &voter_address) - .await; - - // Optimize by quering if a vote from a validator - // is equal to ours. If so, we can avoid voting, but ONLY if we - // are voting in the last third of the voting - // window, otherwise there's the risk of the - // validator changing his vote and, effectively, invalidating - // the delgator's vote - if !args.tx.force - && is_safe_voting_window(client, proposal_id, epoch).await? - { - delegations = filter_delegations( - client, - delegations, - proposal_id, - &proposal_vote, - ) - .await; - } + .await? + }; - let tx_data = VoteProposalData { - id: proposal_id, - vote: proposal_vote, - voter: voter_address, - delegations: delegations.into_iter().collect(), - }; - - let chain_id = args.tx.chain_id.clone().unwrap(); - let expiration = args.tx.expiration; - let data = tx_data - .try_to_vec() - .expect("Encoding proposal data shouldn't fail"); - - let tx_code_hash = query_wasm_code_hash( - client, - args.tx_code_path.to_str().unwrap(), - ) - .await - .unwrap(); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = chain_id; - tx.header.expiration = expiration; - tx.set_data(Data::new(data)); - tx.set_code(Code::from_hash(tx_code_hash)); - - let (mut tx, addr, pk) = tx::prepare_tx( - client, - &mut ctx.wallet, - &args.tx, - tx, - TxSigningKey::WalletAddress(signer.clone()), - #[cfg(not(feature = "mainnet"))] - false, - ) - .await?; - submit_reveal_aux( - client, - &mut ctx, - &args.tx, - addr, - pk.clone(), - &mut tx, - ) - .await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk) - .await?; - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; - Ok(()) - } - None => { - eprintln!( - "Proposal start epoch for proposal id {} is not definied.", - proposal_id - ); - if !args.tx.force { safe_exit(1) } else { Ok(()) } - } - } + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &args.tx, + tx_builder, + signing_data, + )?; + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder.build()) + .await?; } + + Ok(()) } -pub async fn submit_reveal_pk( +pub async fn sign_tx( client: &C, ctx: &mut Context, - args: args::RevealPk, + args::SignTx { + tx: tx_args, + tx_data, + owner, + }: args::SignTx, ) -> Result<(), tx::Error> where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let reveal_tx = - tx::build_reveal_pk(client, &mut ctx.wallet, args.clone()).await?; - if let Some((mut tx, _, pk)) = reveal_tx { - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + let tx = if let Ok(transaction) = Tx::deserialize(tx_data.as_ref()) { + transaction + } else { + eprintln!("Couldn't decode the transaction."); + safe_exit(1) + }; + + let default_signer = signing::signer_from_address(Some(owner.clone())); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &tx_args, + &owner, + default_signer, + ) + .await?; + + let secret_keys = &signing_data + .public_keys + .iter() + .filter_map(|public_key| { + if let Ok(secret_key) = + signing::find_key_by_pk(&mut ctx.wallet, &tx_args, public_key) + { + Some(secret_key) + } else { + eprintln!( + "Couldn't find the secret key for {}. Skipping signature \ + generation.", + public_key + ); + None + } + }) + .collect::>(); + + let signatures = tx.compute_section_signature( + secret_keys, + &signing_data.account_public_keys_map, + ); + + for signature in &signatures { + let filename = format!( + "offline_signature_{}_{}.tx", + tx.header_hash(), + signature.index + ); + let output_path = match &tx_args.output_folder { + Some(path) => path.join(filename), + None => filename.into(), + }; + + let signature_path = File::create(&output_path) + .expect("Should be able to create signature file."); + + serde_json::to_writer_pretty(signature_path, &signature.serialize()) + .expect("Signature should be deserializable."); + println!( + "Signature for {} serialized at {}", + &signing_data + .account_public_keys_map + .get_public_key_from_index(signature.index) + .unwrap(), + output_path.display() + ); } Ok(()) } -/// Check if current epoch is in the last third of the voting period of the -/// proposal. This ensures that it is safe to optimize the vote writing to -/// storage. -async fn is_safe_voting_window( - client: &C, - proposal_id: u64, - proposal_start_epoch: Epoch, -) -> Result -where - C: namada::ledger::queries::Client + Sync, - C::Error: std::fmt::Display, -{ - tx::is_safe_voting_window(client, proposal_id, proposal_start_epoch).await -} - -/// Removes validators whose vote corresponds to that of the delegator (needless -/// vote) -async fn filter_delegations( +pub async fn submit_reveal_pk( client: &C, - delegations: HashSet
, - proposal_id: u64, - delegator_vote: &ProposalVote, -) -> HashSet
+ ctx: &mut Context, + args: args::RevealPk, +) -> Result<(), tx::Error> where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - // Filter delegations by their validator's vote concurrently - let delegations = futures::future::join_all( - delegations - .into_iter() - // we cannot use `filter/filter_map` directly because we want to - // return a future - .map(|validator_address| async { - let vote_key = gov_storage::get_vote_proposal_key( - proposal_id, - validator_address.to_owned(), - validator_address.to_owned(), - ); + submit_reveal_aux(client, ctx, args.tx, &(&args.public_key).into()).await?; - if let Some(validator_vote) = - rpc::query_storage_value::( - client, &vote_key, - ) - .await - { - if &validator_vote == delegator_vote { - return None; - } - } - Some(validator_address) - }), - ) - .await; - // Take out the `None`s - delegations.into_iter().flatten().collect() + Ok(()) } pub async fn submit_bond( @@ -1139,11 +1111,38 @@ where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let (mut tx, addr, pk) = - tx::build_bond::(client, &mut ctx.wallet, args.clone()).await?; - submit_reveal_aux(client, ctx, &args.tx, addr, pk.clone(), &mut tx).await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + let default_address = args.source.clone().unwrap_or(args.validator.clone()); + let default_signer = + signing::signer_from_address(Some(default_address.clone())); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &default_address, + default_signer, + ) + .await?; + + submit_reveal_aux(client, ctx, args.tx.clone(), &default_address).await?; + + let tx_builder = + tx::build_bond::(client, args.clone(), &signing_data.gas_payer) + .await?; + + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &args.tx, + tx_builder, + signing_data, + )?; + + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder.build()) + .await?; + } + Ok(()) } @@ -1156,12 +1155,42 @@ where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let (mut tx, addr, pk, latest_withdrawal_pre) = - tx::build_unbond(client, &mut ctx.wallet, args.clone()).await?; - submit_reveal_aux(client, ctx, &args.tx, addr, pk.clone(), &mut tx).await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; - tx::query_unbonds(client, args.clone(), latest_withdrawal_pre).await?; + let default_address = args.source.clone().unwrap_or(args.validator.clone()); + let default_signer = + signing::signer_from_address(Some(default_address.clone())); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &default_address, + default_signer, + ) + .await?; + + let (tx_builder, latest_withdrawal_pre) = tx::build_unbond( + client, + &mut ctx.wallet, + args.clone(), + &signing_data.gas_payer, + ) + .await?; + + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &args.tx, + tx_builder, + signing_data, + )?; + + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder.build()) + .await?; + + tx::query_unbonds(client, args.clone(), latest_withdrawal_pre).await?; + } + Ok(()) } @@ -1174,12 +1203,36 @@ where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let (mut tx, addr, pk) = - tx::build_withdraw(client, &mut ctx.wallet, args.clone()).await?; - submit_reveal_aux(client, &mut ctx, &args.tx, addr, pk.clone(), &mut tx) - .await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + let default_address = args.source.clone().unwrap_or(args.validator.clone()); + let default_signer = + signing::signer_from_address(Some(default_address.clone())); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &default_address, + default_signer, + ) + .await?; + + let tx_builder = + tx::build_withdraw(client, args.clone(), &signing_data.gas_payer) + .await?; + + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &args.tx, + tx_builder, + signing_data, + )?; + + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder.build()) + .await?; + } + Ok(()) } @@ -1192,14 +1245,38 @@ where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let arg = args.clone(); - let (mut tx, addr, pk) = - tx::build_validator_commission_change(client, &mut ctx.wallet, arg) + let default_signer = + signing::signer_from_address(Some(args.validator.clone())); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &args.validator, + default_signer, + ) + .await?; + + let tx_builder = tx::build_validator_commission_change( + client, + args.clone(), + &signing_data.gas_payer, + ) + .await?; + + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &args.tx, + tx_builder, + signing_data, + )?; + + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder.build()) .await?; - submit_reveal_aux(client, &mut ctx, &args.tx, addr, pk.clone(), &mut tx) - .await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + Ok(()) } @@ -1214,13 +1291,38 @@ where C: namada::ledger::queries::Client + Sync, C::Error: std::fmt::Display, { - let (mut tx, addr, pk) = - tx::build_unjail_validator(client, &mut ctx.wallet, args.clone()) + let default_signer = + signing::signer_from_address(Some(args.validator.clone())); + let signing_data = signing::aux_signing_data( + client, + &mut ctx.wallet, + &args.tx, + &args.validator, + default_signer, + ) + .await?; + + let tx_builder = tx::build_unjail_validator( + client, + args.clone(), + &signing_data.gas_payer, + ) + .await?; + + if args.tx.dump_tx { + tx::dump_tx(&args.tx, tx_builder); + } else { + let tx_builder = signing::sign_tx( + &mut ctx.wallet, + &args.tx, + tx_builder, + signing_data, + )?; + + tx::process_tx(client, &mut ctx.wallet, &args.tx, tx_builder.build()) .await?; - submit_reveal_aux(client, &mut ctx, &args.tx, addr, pk.clone(), &mut tx) - .await?; - signing::sign_tx(&mut ctx.wallet, &mut tx, &args.tx, &pk).await?; - tx::process_tx(client, &mut ctx.wallet, &args.tx, tx).await?; + } + Ok(()) } @@ -1270,12 +1372,11 @@ where #[cfg(test)] mod test_tx { use masp_primitives::transaction::components::Amount; + use namada::core::types::storage::Epoch; use namada::ledger::masp::{make_asset_type, MaspAmount}; use namada::types::address::testing::gen_established_address; use namada::types::token::MaspDenom; - use super::*; - #[test] fn test_masp_add_amount() { let address_1 = gen_established_address(); diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 7e9500a3b22..59019004f72 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -4,12 +4,12 @@ use std::collections::HashMap; use borsh::{BorshDeserialize, BorshSerialize}; use derivative::Derivative; +use namada::core::ledger::governance::parameters::GovernanceParameters; #[cfg(not(feature = "mainnet"))] use namada::core::ledger::testnet_pow; use namada::ledger::eth_bridge::EthereumBridgeConfig; #[cfg(feature = "dev")] use namada::ledger::eth_bridge::{Contracts, UpgradeableContract}; -use namada::ledger::governance::parameters::GovParams; use namada::ledger::parameters::EpochDuration; use namada::ledger::pos::{Dec, GenesisValidator, PosParams}; #[cfg(feature = "dev")] @@ -35,9 +35,9 @@ pub mod genesis_config { use data_encoding::HEXLOWER; use eyre::Context; + use namada::core::ledger::governance::parameters::GovernanceParameters; #[cfg(not(feature = "mainnet"))] use namada::core::ledger::testnet_pow; - use namada::ledger::governance::parameters::GovParams; use namada::ledger::parameters::EpochDuration; use namada::ledger::pos::{Dec, GenesisValidator, PosParams}; use namada::types::address::Address; @@ -154,7 +154,7 @@ pub mod genesis_config { // Maximum size of proposal in kibibytes (KiB) pub max_proposal_code_size: u64, // Minimum proposal period length in epochs - pub min_proposal_period: u64, + 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 @@ -267,6 +267,8 @@ pub mod genesis_config { 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 @@ -607,25 +609,28 @@ pub mod genesis_config { 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(), + #[cfg(not(feature = "mainnet"))] wrapper_tx_fees: parameters.wrapper_tx_fees, }; let GovernanceParamsConfig { min_proposal_fund, max_proposal_code_size, - min_proposal_period, + min_proposal_voting_period, max_proposal_content_size, min_proposal_grace_epochs, max_proposal_period, } = gov_params; - let gov_params = GovParams { - min_proposal_fund, + let gov_params = GovernanceParameters { + min_proposal_fund: token::Amount::native_whole(min_proposal_fund), max_proposal_code_size, - min_proposal_period, + min_proposal_voting_period, max_proposal_content_size, min_proposal_grace_epochs, max_proposal_period, @@ -726,7 +731,7 @@ pub struct Genesis { pub implicit_accounts: Vec, pub parameters: Parameters, pub pos_params: PosParams, - pub gov_params: GovParams, + pub gov_params: GovernanceParameters, // Ethereum bridge config pub ethereum_bridge_params: Option, } @@ -854,6 +859,8 @@ pub struct Parameters { pub implicit_vp_sha256: [u8; 32], /// Expected number of epochs per year (read only) pub epochs_per_year: u64, + /// Maximum amount of signatures per transaction + pub max_signatures_per_transaction: u8, /// PoS gain p (read only) pub pos_gain_p: Dec, /// PoS gain d (read only) @@ -973,12 +980,14 @@ pub fn genesis(num_validators: u64) -> Genesis { tx_whitelist: vec![], implicit_vp_code_path: vp_implicit_path.into(), implicit_vp_sha256: Default::default(), + max_signatures_per_transaction: 15, epochs_per_year: 525_600, /* 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(), + #[cfg(not(feature = "mainnet"))] wrapper_tx_fees: Some(token::Amount::native_whole(0)), }; let albert = EstablishedAccount { @@ -1078,7 +1087,7 @@ pub fn genesis(num_validators: u64) -> Genesis { token_accounts, parameters, pos_params: PosParams::default(), - gov_params: GovParams::default(), + gov_params: GovernanceParameters::default(), ethereum_bridge_params: Some(EthereumBridgeConfig { eth_start_height: Default::default(), min_confirmations: Default::default(), diff --git a/apps/src/lib/config/mod.rs b/apps/src/lib/config/mod.rs index 0bcb1b5f615..f6307c6bd14 100644 --- a/apps/src/lib/config/mod.rs +++ b/apps/src/lib/config/mod.rs @@ -283,7 +283,7 @@ impl Config { .and_then(|c| c.merge(config::File::with_name(file_name))) .and_then(|c| { c.merge( - config::Environment::with_prefix("namada").separator("__"), + config::Environment::with_prefix("NAMADA").separator("__"), ) }) .map_err(Error::ReadError)?; diff --git a/apps/src/lib/node/ledger/mod.rs b/apps/src/lib/node/ledger/mod.rs index 37b447b4daa..68dda59aee9 100644 --- a/apps/src/lib/node/ledger/mod.rs +++ b/apps/src/lib/node/ledger/mod.rs @@ -14,8 +14,8 @@ use std::thread; use byte_unit::Byte; use futures::future::TryFutureExt; +use namada::core::ledger::governance::storage::keys as governance_storage; use namada::eth_bridge::ethers::providers::{Http, Provider}; -use namada::ledger::governance::storage as gov_storage; use namada::types::storage::Key; use once_cell::unsync::Lazy; use sysinfo::{RefreshKind, System, SystemExt}; @@ -64,7 +64,7 @@ const ENV_VAR_RAYON_THREADS: &str = "NAMADA_RAYON_THREADS"; //``` impl Shell { fn load_proposals(&mut self) { - let proposals_key = gov_storage::get_commiting_proposals_prefix( + let proposals_key = governance_storage::get_commiting_proposals_prefix( self.wl_storage.storage.last_epoch.0, ); @@ -73,7 +73,7 @@ impl Shell { for (key, _, _) in proposal_iter { let key = Key::from_str(key.as_str()).expect("Key should be parsable"); - if gov_storage::get_commit_proposal_epoch(&key).unwrap() + if governance_storage::get_commit_proposal_epoch(&key).unwrap() != self.wl_storage.storage.last_epoch.0 { // NOTE: `iter_prefix` iterate over the matching prefix. In this @@ -85,7 +85,7 @@ impl Shell { continue; } - let proposal_id = gov_storage::get_commit_proposal_id(&key); + let proposal_id = governance_storage::get_commit_proposal_id(&key); if let Some(id) = proposal_id { self.proposal_data.insert(id); } diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 2f178eb47e7..6063c5fdb52 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -3,6 +3,8 @@ use std::collections::HashMap; use data_encoding::HEXUPPER; +use namada::core::ledger::pgf::storage::keys as pgf_storage; +use namada::core::ledger::pgf::ADDRESS as pgf_address; use namada::ledger::parameters::storage as params_storage; use namada::ledger::pos::{namada_proof_of_stake, staking_token_address}; use namada::ledger::storage::EPOCH_SWITCH_BLOCKS_DELAY; @@ -92,8 +94,7 @@ where &mut self.wl_storage, )?; - let _proposals_result = - execute_governance_proposals(self, &mut response)?; + execute_governance_proposals(self, &mut response)?; // Copy the new_epoch + pipeline_len - 1 validator set into // new_epoch + pipeline_len @@ -247,15 +248,15 @@ where self.invalidate_pow_solution_if_valid(wrapper); // Charge fee - let fee_payer = + let gas_payer = if wrapper.pk != address::masp_tx_key().ref_to() { - wrapper.fee_payer() + wrapper.gas_payer() } else { address::masp() }; let balance_key = - token::balance_key(&wrapper.fee.token, &fee_payer); + token::balance_key(&wrapper.fee.token, &gas_payer); let balance: token::Amount = self .wl_storage .read(&balance_key) @@ -817,6 +818,68 @@ where .remove(&mut self.wl_storage, &address)?; } + // Pgf inflation + let pgf_inflation_rate_key = pgf_storage::get_pgf_inflation_rate_key(); + let pgf_inflation_rate = self + .read_storage_key::(&pgf_inflation_rate_key) + .unwrap_or_default(); + + let pgf_pd_rate = pgf_inflation_rate / Dec::from(epochs_per_year); + let pgf_inflation = Dec::from(total_tokens) * pgf_pd_rate; + let pgf_inflation_amount = token::Amount::from(pgf_inflation); + + credit_tokens( + &mut self.wl_storage, + &staking_token, + &pgf_address, + pgf_inflation_amount, + )?; + + tracing::info!( + "Minting {} tokens for PGF rewards distribution into the PGF \ + account.", + pgf_inflation_amount.to_string_native() + ); + + // Pgf steward inflation + let pgf_stewards_inflation_rate_key = + pgf_storage::get_steward_inflation_rate_key(); + let pgf_stewards_inflation_rate = self + .read_storage_key::(&pgf_stewards_inflation_rate_key) + .unwrap_or_default(); + + let pgf_stewards_pd_rate = + pgf_stewards_inflation_rate / Dec::from(epochs_per_year); + let pgf_steward_inflation = + Dec::from(total_tokens) * pgf_stewards_pd_rate; + + let pgf_stewards_key = pgf_storage::get_stewards_key(); + let pgf_stewards: BTreeSet
= + self.read_storage_key(&pgf_stewards_key).unwrap_or_default(); + + let pgf_steward_reward = match pgf_stewards.len() { + 0 => Dec::zero(), + _ => pgf_steward_inflation + .trunc_div(&Dec::from(pgf_stewards.len())) + .unwrap_or_default(), + }; + let pgf_steward_reward = token::Amount::from(pgf_steward_reward); + + for steward in pgf_stewards { + credit_tokens( + &mut self.wl_storage, + &staking_token, + &steward, + pgf_steward_reward, + )?; + tracing::info!( + "Minting {} tokens for PGF Steward rewards distribution into \ + the steward address {}.", + pgf_steward_reward.to_string_native(), + steward, + ); + } + Ok(()) } @@ -941,6 +1004,11 @@ mod test_finalize_block { use data_encoding::HEXUPPER; use namada::core::ledger::eth_bridge::storage::wrapped_erc20s; + use namada::core::ledger::governance::storage::keys::get_proposal_execution_key; + use namada::core::ledger::governance::storage::proposal::ProposalType; + use namada::core::ledger::governance::storage::vote::{ + StorageProposalVote, VoteType, + }; use namada::eth_bridge::storage::bridge_pool::{ self, get_key_from_hash, get_nonce_key, get_signed_root_key, }; @@ -973,7 +1041,6 @@ mod test_finalize_block { use namada::types::ethereum_events::{ EthAddress, TransferToEthereum, Uint as ethUint, }; - use namada::types::governance::ProposalVote; use namada::types::hash::Hash; use namada::types::keccak::KeccakHash; use namada::types::key::tm_consensus_key_raw_hash; @@ -981,10 +1048,11 @@ mod test_finalize_block { use namada::types::time::{DateTimeUtc, DurationSecs}; use namada::types::token::{Amount, NATIVE_MAX_DECIMAL_PLACES}; use namada::types::transaction::governance::{ - InitProposalData, ProposalType, VoteProposalData, + InitProposalData, VoteProposalData, }; use namada::types::transaction::protocol::EthereumTxData; use namada::types::transaction::{Fee, WrapperTx, MIN_FEE_AMOUNT}; + use namada::types::tx::TxBuilder; use namada::types::uint::Uint; use namada::types::vote_extensions::ethereum_events; use namada_test_utils::TestWasms; @@ -1754,11 +1822,8 @@ mod test_finalize_block { }; // Add a proposal to be accepted and one to be rejected. - add_proposal( - 0, - ProposalVote::Yay(namada::types::governance::VoteType::Default), - ); - add_proposal(1, ProposalVote::Nay); + add_proposal(0, StorageProposalVote::Yay(VoteType::Default)); + add_proposal(1, StorageProposalVote::Nay); // Commit the genesis state shell.wl_storage.commit_block().unwrap(); @@ -3505,16 +3570,18 @@ mod test_finalize_block { /// Test that updating the ethereum bridge params via governance works. #[tokio::test] async fn test_eth_bridge_param_updates() { - use namada::ledger::governance::storage as gov_storage; let (mut shell, _broadcaster, _, mut control_receiver) = setup_at_height(3u64); - let proposal_execution_key = gov_storage::get_proposal_execution_key(0); + let proposal_execution_key = get_proposal_execution_key(0); shell .wl_storage .write(&proposal_execution_key, 0u64) .expect("Test failed."); - let mut tx = Tx::new(TxType::Raw); - tx.set_data(Data::new(0u64.try_to_vec().expect("Test failed"))); + let tx_builder = TxBuilder::new(shell.chain_id.clone(), None); + let tx = tx_builder + .add_code_from_hash(Hash::default()) + .add_data(0u64) + .build(); let new_min_confirmations = MinimumConfirmations::from(unsafe { NonZeroU64::new_unchecked(42) }); diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index e488cbf2b80..ead08267586 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -1,22 +1,30 @@ -use namada::core::ledger::slash_fund::ADDRESS as slash_fund_address; -use namada::core::types::transaction::governance::ProposalType; -use namada::ledger::events::EventType; -use namada::ledger::governance::{ - storage as gov_storage, ADDRESS as gov_address, +use std::collections::{BTreeSet, HashMap}; + +use namada::core::ledger::governance::storage::keys as gov_storage; +use namada::core::ledger::governance::storage::proposal::{ + AddRemove, PGFAction, PGFTarget, ProposalType, }; -use namada::ledger::native_vp::governance::utils::{ - compute_tally, get_proposal_votes, ProposalEvent, +use namada::core::ledger::governance::utils::{ + compute_proposal_result, ProposalVotes, TallyResult, TallyType, TallyVote, + VotePower, }; +use namada::core::ledger::governance::ADDRESS as gov_address; +use namada::core::ledger::pgf::storage::keys as pgf_storage; +use namada::core::ledger::pgf::ADDRESS; +use namada::core::ledger::storage_api::governance as gov_api; +use namada::ledger::governance::utils::ProposalEvent; +use namada::ledger::pos::BondId; use namada::ledger::protocol; use namada::ledger::storage::types::encode; use namada::ledger::storage::{DBIter, StorageHasher, DB}; use namada::ledger::storage_api::{token, StorageWrite}; -use namada::proof_of_stake::read_total_stake; +use namada::proof_of_stake::parameters::PosParams; +use namada::proof_of_stake::{bond_amount, read_total_stake}; use namada::proto::{Code, Data}; use namada::types::address::Address; -use namada::types::governance::{Council, Tally, TallyResult, VotePower}; use namada::types::storage::Epoch; +use super::utils::force_read; use super::*; #[derive(Default)] @@ -40,218 +48,333 @@ where let proposal_end_epoch_key = gov_storage::get_voting_end_epoch_key(id); let proposal_type_key = gov_storage::get_proposal_type_key(id); - let funds = shell - .read_storage_key::(&proposal_funds_key) - .ok_or_else(|| { - Error::BadProposal(id, "Invalid proposal funds.".to_string()) - })?; - let proposal_end_epoch = shell - .read_storage_key::(&proposal_end_epoch_key) - .ok_or_else(|| { - Error::BadProposal( - id, - "Invalid proposal end_epoch.".to_string(), - ) - })?; - - let proposal_type = shell - .read_storage_key::(&proposal_type_key) - .ok_or_else(|| { - Error::BadProposal(id, "Invalid proposal type".to_string()) - })?; - - let votes = - get_proposal_votes(&shell.wl_storage, proposal_end_epoch, id) - .map_err(|msg| Error::BadProposal(id, msg.to_string()))?; - let params = read_pos_params(&shell.wl_storage) - .map_err(|msg| Error::BadProposal(id, msg.to_string()))?; - let total_stake = - read_total_stake(&shell.wl_storage, ¶ms, proposal_end_epoch) - .map_err(|msg| Error::BadProposal(id, msg.to_string()))?; - let total_stake = VotePower::try_from(total_stake) - .expect("Voting power exceeds NAM supply"); - let tally_result = compute_tally(votes, total_stake, &proposal_type) - .map_err(|msg| Error::BadProposal(id, msg.to_string()))? - .result; - - // Execute proposal if succesful - let transfer_address = match tally_result { - TallyResult::Passed(tally) => { - let (successful_execution, proposal_event) = match tally { - Tally::Default => execute_default_proposal(shell, id), - Tally::PGFCouncil(council) => { - execute_pgf_proposal(id, council) + let funds: token::Amount = + force_read(&shell.wl_storage, &proposal_funds_key)?; + let proposal_end_epoch: Epoch = + force_read(&shell.wl_storage, &proposal_end_epoch_key)?; + let proposal_type: ProposalType = + force_read(&shell.wl_storage, &proposal_type_key)?; + + let params = read_pos_params(&shell.wl_storage)?; + let total_voting_power = + read_total_stake(&shell.wl_storage, ¶ms, proposal_end_epoch)?; + + let tally_type = TallyType::from(proposal_type.clone()); + let votes = compute_proposal_votes( + &shell.wl_storage, + ¶ms, + id, + proposal_end_epoch, + )?; + let proposal_result = + compute_proposal_result(votes, total_voting_power, tally_type); + + let transfer_address = match proposal_result.result { + TallyResult::Passed => { + let proposal_event = match proposal_type { + ProposalType::Default(_) => { + let proposal_code_key = + gov_storage::get_proposal_code_key(id); + let proposal_code = + shell.wl_storage.read_bytes(&proposal_code_key)?; + let result = execute_default_proposal( + shell, + id, + proposal_code.clone(), + )?; + tracing::info!( + "Governance proposal (default) {} has been \ + executed ({}) and passed.", + id, + result + ); + + ProposalEvent::default_proposal_event( + id, + proposal_code.is_some(), + result, + ) + .into() } - Tally::ETHBridge => execute_eth_proposal(id), - }; + ProposalType::PGFSteward(stewards) => { + let result = execute_pgf_steward_proposal( + &mut shell.wl_storage, + stewards, + )?; + tracing::info!( + "Governance proposal (pgf stewards){} has been \ + executed and passed.", + id + ); + + ProposalEvent::pgf_steward_proposal_event(id, result) + .into() + } + ProposalType::PGFPayment(payments) => { + let native_token = + &shell.wl_storage.get_native_token()?; + let result = execute_pgf_payment_proposal( + &mut shell.wl_storage, + native_token, + payments, + )?; + tracing::info!( + "Governance proposal (pgs payments) {} has been \ + executed and passed.", + id + ); + ProposalEvent::pgf_payments_proposal_event(id, result) + .into() + } + }; response.events.push(proposal_event); - if successful_execution { - proposals_result.passed.push(id); - shell - .read_storage_key::
( - &gov_storage::get_author_key(id), - ) - .ok_or_else(|| { - Error::BadProposal( - id, - "Invalid proposal author.".to_string(), - ) - })? - } else { - proposals_result.rejected.push(id); - slash_fund_address - } + proposals_result.passed.push(id); + + let proposal_author_key = gov_storage::get_author_key(id); + shell.wl_storage.read::
(&proposal_author_key)? } TallyResult::Rejected => { - let proposal_event: Event = ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Rejected, - id, - false, - false, - ) - .into(); + if let ProposalType::PGFPayment(_) = proposal_type { + let two_third_nay = proposal_result.two_third_nay(); + if two_third_nay { + let pgf_stewards_key = pgf_storage::get_stewards_key(); + let proposal_author = gov_storage::get_author_key(id); + + let mut pgf_stewards = shell + .wl_storage + .read::>(&pgf_stewards_key)? + .unwrap_or_default(); + let proposal_author: Address = + force_read(&shell.wl_storage, &proposal_author)?; + + pgf_stewards.remove(&proposal_author); + shell + .wl_storage + .write(&pgf_stewards_key, pgf_stewards)?; + + tracing::info!( + "Governance proposal {} was rejected with 2/3 of \ + nay votes. Removing {} from stewards set.", + id, + proposal_author + ); + } + } + let proposal_event = + ProposalEvent::rejected_proposal_event(id).into(); response.events.push(proposal_event); proposals_result.rejected.push(id); - slash_fund_address + tracing::info!( + "Governance proposal {} has been executed and rejected.", + id + ); + + None } }; let native_token = shell.wl_storage.storage.native_token.clone(); - // transfer proposal locked funds - token::transfer( - &mut shell.wl_storage, - &native_token, - &gov_address, - &transfer_address, - funds, - ) - .expect( - "Must be able to transfer governance locked funds after proposal \ - has been tallied", - ); + if let Some(address) = transfer_address { + token::transfer( + &mut shell.wl_storage, + &native_token, + &gov_address, + &address, + funds, + )?; + } else { + token::burn( + &mut shell.wl_storage, + &native_token, + &gov_address, + funds, + )?; + } } Ok(proposals_result) } +fn compute_proposal_votes( + storage: &S, + params: &PosParams, + proposal_id: u64, + epoch: Epoch, +) -> storage_api::Result +where + S: StorageRead, +{ + let votes = gov_api::get_proposal_votes(storage, proposal_id)?; + + let mut validators_vote: HashMap = HashMap::default(); + let mut validator_voting_power: HashMap = + HashMap::default(); + let mut delegators_vote: HashMap = HashMap::default(); + let mut delegator_voting_power: HashMap< + Address, + HashMap, + > = HashMap::default(); + + for vote in votes { + if vote.is_validator() { + let validator = vote.validator.clone(); + let vote_data = vote.data.clone(); + + let validator_stake = + read_total_stake(storage, params, epoch).unwrap_or_default(); + + validators_vote.insert(validator.clone(), vote_data.into()); + validator_voting_power.insert(validator, validator_stake); + } else { + let validator = vote.validator.clone(); + let delegator = vote.delegator.clone(); + let vote_data = vote.data.clone(); + + let bond_id = BondId { + source: delegator.clone(), + validator: validator.clone(), + }; + let (_, delegator_stake) = + bond_amount(storage, &bond_id, epoch).unwrap_or_default(); + + delegators_vote.insert(delegator.clone(), vote_data.into()); + delegator_voting_power + .entry(delegator) + .or_default() + .insert(validator, delegator_stake); + } + } + + Ok(ProposalVotes { + validators_vote, + validator_voting_power, + delegators_vote, + delegator_voting_power, + }) +} + fn execute_default_proposal( shell: &mut Shell, id: u64, -) -> (bool, Event) + proposal_code: Option>, +) -> storage_api::Result where D: DB + for<'iter> DBIter<'iter> + Sync + 'static, H: StorageHasher + Sync + 'static, { - let proposal_code_key = gov_storage::get_proposal_code_key(id); - let proposal_code = shell.read_storage_key_bytes(&proposal_code_key); - match proposal_code { - Some(proposal_code) => { - let mut tx = Tx::new(TxType::Decrypted(DecryptedTx::Decrypted { - #[cfg(not(feature = "mainnet"))] - has_valid_pow: false, - })); - tx.header.chain_id = shell.chain_id.clone(); - tx.set_data(Data::new(encode(&id))); - tx.set_code(Code::new(proposal_code)); - let pending_execution_key = - gov_storage::get_proposal_execution_key(id); - shell - .wl_storage - .write(&pending_execution_key, ()) - .expect("Should be able to write to storage."); - let tx_result = protocol::dispatch_tx( - tx, - 0, /* this is used to compute the fee - * based on the code size. We dont - * need it here. */ - TxIndex::default(), - &mut BlockGasMeter::default(), - &mut shell.wl_storage, - &mut shell.vp_wasm_cache, - &mut shell.tx_wasm_cache, - ); - shell - .wl_storage - .storage - .delete(&pending_execution_key) - .expect("Should be able to delete the storage."); - match tx_result { - Ok(tx_result) if tx_result.is_accepted() => { + if let Some(code) = proposal_code { + let pending_execution_key = gov_storage::get_proposal_execution_key(id); + shell.wl_storage.write(&pending_execution_key, ())?; + + let mut tx = Tx::new(TxType::Decrypted(DecryptedTx::Decrypted { + #[cfg(not(feature = "mainnet"))] + has_valid_pow: false, + })); + tx.header.chain_id = shell.chain_id.clone(); + tx.set_data(Data::new(encode(&id))); + tx.set_code(Code::new(code)); + + // 0 parameter is used to compute the fee + // based on the code size. We dont + // need it here. + let tx_result = protocol::dispatch_tx( + tx, + 0, /* this is used to compute the fee + * based on the code size. We dont + * need it here. */ + TxIndex::default(), + &mut BlockGasMeter::default(), + &mut shell.wl_storage, + &mut shell.vp_wasm_cache, + &mut shell.tx_wasm_cache, + ); + shell + .wl_storage + .storage + .delete(&pending_execution_key) + .expect("Should be able to delete the storage."); + match tx_result { + Ok(tx_result) => { + if tx_result.is_accepted() { shell.wl_storage.commit_tx(); - ( - tx_result.is_accepted(), - ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed(Tally::Default), - id, - true, - tx_result.is_accepted(), - ) - .into(), - ) - } - _ => { - shell.wl_storage.drop_tx(); - ( - false, - ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed(Tally::Default), - id, - true, - false, - ) - .into(), - ) + Ok(true) + } else { + Ok(false) } } + Err(_) => { + shell.wl_storage.drop_tx(); + Ok(false) + } } - None => ( - true, - ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed(Tally::Default), - id, - false, - false, - ) - .into(), - ), + } else { + tracing::info!( + "Governance proposal {} doesn't have any associated proposal code.", + id + ); + Ok(true) } } -fn execute_pgf_proposal(id: u64, council: Council) -> (bool, Event) { - // TODO: implement when PGF is in place, update the PGF - // council in storage - ( - true, - ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed(Tally::PGFCouncil(council)), - id, - false, - false, - ) - .into(), - ) +fn execute_pgf_steward_proposal( + storage: &mut S, + stewards: HashSet>, +) -> Result +where + S: StorageRead + StorageWrite, +{ + let stewards_key = pgf_storage::get_stewards_key(); + let mut storage_stewards: BTreeSet
= + storage.read(&stewards_key)?.unwrap_or_default(); + + for action in stewards { + match action { + AddRemove::Add(steward) => storage_stewards.insert(steward), + AddRemove::Remove(steward) => storage_stewards.remove(&steward), + }; + } + + let write_result = storage.write(&stewards_key, storage_stewards); + Ok(write_result.is_ok()) } -fn execute_eth_proposal(id: u64) -> (bool, Event) { - // TODO: implement when ETH Bridge. Apply the - // modification requested by the proposal - // - ( - true, - ProposalEvent::new( - EventType::Proposal.to_string(), - TallyResult::Passed(Tally::ETHBridge), - id, - false, - false, - ) - .into(), - ) +fn execute_pgf_payment_proposal( + storage: &mut S, + token: &Address, + payments: Vec, +) -> Result +where + S: StorageRead + StorageWrite, +{ + let continous_payments_key = pgf_storage::get_payments_key(); + let mut continous_payments: BTreeSet = + storage.read(&continous_payments_key)?.unwrap_or_default(); + + for payment in payments { + match payment { + PGFAction::Continuous(action) => match action { + AddRemove::Add(target) => { + continous_payments.insert(target); + } + AddRemove::Remove(target) => { + continous_payments.remove(&target); + } + }, + PGFAction::Retro(target) => { + token::transfer( + storage, + token, + &ADDRESS, + &target.target, + target.amount, + )?; + } + } + } + + let write_result = + storage.write(&continous_payments_key, continous_payments); + Ok(write_result.is_ok()) } diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index ef08373789d..41bc88c03b6 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -90,6 +90,7 @@ where implicit_vp_code_path, implicit_vp_sha256, epochs_per_year, + max_signatures_per_transaction, pos_gain_p, pos_gain_d, staked_ratio, @@ -182,6 +183,7 @@ where tx_whitelist, implicit_vp_code_hash, epochs_per_year, + max_signatures_per_transaction, pos_gain_p, pos_gain_d, staked_ratio, @@ -289,10 +291,12 @@ where .unwrap(); if let Some(pk) = public_key { - let pk_storage_key = pk_key(&address); - self.wl_storage - .write_bytes(&pk_storage_key, pk.try_to_vec().unwrap()) - .unwrap(); + storage_api::account::set_public_key_at( + &mut self.wl_storage, + &address, + &pk, + 0, + )?; } for (key, value) in storage { @@ -328,9 +332,14 @@ where ) { // Initialize genesis implicit for genesis::ImplicitAccount { public_key } in accounts { - let address: Address = (&public_key).into(); - let pk_storage_key = pk_key(&address); - self.wl_storage.write(&pk_storage_key, public_key).unwrap(); + let address: address::Address = (&public_key).into(); + storage_api::account::set_public_key_at( + &mut self.wl_storage, + &address, + &public_key, + 0, + ) + .unwrap(); } } @@ -390,10 +399,13 @@ where .write_bytes(&Key::validity_predicate(addr), vp_code_hash) .expect("Unable to write user VP"); // Validator account key - let pk_key = pk_key(addr); - self.wl_storage - .write(&pk_key, &validator.account_key) - .expect("Unable to set genesis user public 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) diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 2b7a0b7ac71..fe3440b5e76 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -16,6 +16,7 @@ mod stats; #[cfg(any(test, feature = "testing"))] #[allow(dead_code)] pub mod testing; +pub mod utils; mod vote_extensions; use std::collections::{BTreeSet, HashSet}; @@ -1242,13 +1243,13 @@ where } // Check balance for fee - let fee_payer = if wrapper.pk != masp_tx_key().ref_to() { - wrapper.fee_payer() + let gas_payer = if wrapper.pk != masp_tx_key().ref_to() { + wrapper.gas_payer() } else { masp() }; // check that the fee payer has sufficient balance - let balance = self.get_balance(&wrapper.fee.token, &fee_payer); + let balance = self.get_balance(&wrapper.fee.token, &gas_payer); // In testnets with a faucet, tx is allowed to skip fees if // it includes a valid PoW @@ -1287,38 +1288,6 @@ where response } - /// Lookup a validator's keypair for their established account from their - /// wallet. If the node is not validator, this function returns None - #[allow(dead_code)] - fn get_account_keypair(&self) -> Option { - let wallet_path = &self.base_dir.join(self.chain_id.as_str()); - let genesis_path = &self - .base_dir - .join(format!("{}.toml", self.chain_id.as_str())); - let mut wallet = crate::wallet::load_or_new_from_genesis( - wallet_path, - genesis::genesis_config::open_genesis_config(genesis_path).unwrap(), - ); - self.mode.get_validator_address().map(|addr| { - let sk: common::SecretKey = self - .wl_storage - .read(&pk_key(addr)) - .expect( - "A validator should have a public key associated with \ - it's established account", - ) - .expect( - "A validator should have a public key associated with \ - it's established account", - ); - let pk = sk.ref_to(); - wallet.find_key_by_pk(&pk, None).expect( - "A validator's established keypair should be stored in its \ - wallet", - ) - }) - } - #[cfg(not(feature = "mainnet"))] /// Check if the tx has a valid PoW solution. Unlike /// `apply_pow_solution_if_valid`, this won't invalidate the solution. @@ -2142,6 +2111,7 @@ mod test_mempool_validate { use namada::proof_of_stake::Epoch; use namada::proto::{Code, Data, Section, Signature, Tx}; use namada::types::transaction::{Fee, WrapperTx}; + use namada::types::tx::TxBuilder; use super::*; @@ -2234,10 +2204,10 @@ mod test_mempool_validate { fn test_wrong_tx_type() { let (shell, _recv, _, _) = test_utils::setup(); - // Test Raw TxType - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = shell.chain_id.clone(); - tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); + let tx_builder = TxBuilder::new(shell.chain_id.clone(), None); + let tx = tx_builder + .add_code("wasm_code".as_bytes().to_owned()) + .build(); let result = shell.mempool_validate( tx.to_bytes().as_ref(), @@ -2366,18 +2336,12 @@ mod test_mempool_validate { let keypair = super::test_utils::gen_keypair(); let wrong_chain_id = ChainId("Wrong chain id".to_string()); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = wrong_chain_id.clone(); - tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); - tx.set_data(Data::new("transaction data".as_bytes().to_owned())); - tx.add_section(Section::Signature(Signature::new( - vec![ - tx.header_hash(), - tx.sections[0].get_hash(), - tx.sections[1].get_hash(), - ], - &keypair, - ))); + let tx_builder = TxBuilder::new(wrong_chain_id.clone(), None); + let tx = tx_builder + .add_code("wasm_code".as_bytes().to_owned()) + .add_data("transaction data".as_bytes().to_owned()) + .add_gas_payer(keypair) + .build(); let result = shell.mempool_validate( tx.to_bytes().as_ref(), @@ -2401,19 +2365,15 @@ mod test_mempool_validate { let keypair = super::test_utils::gen_keypair(); - let mut tx = Tx::new(TxType::Raw); - tx.header.expiration = Some(DateTimeUtc::default()); - tx.header.chain_id = shell.chain_id.clone(); - tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); - tx.set_data(Data::new("transaction data".as_bytes().to_owned())); - tx.add_section(Section::Signature(Signature::new( - vec![ - tx.header_hash(), - tx.sections[0].get_hash(), - tx.sections[1].get_hash(), - ], - &keypair, - ))); + let tx_builder = TxBuilder::new( + shell.chain_id.clone(), + Some(DateTimeUtc::default()), + ); + let tx = tx_builder + .add_code("wasm_code".as_bytes().to_owned()) + .add_data("transaction data".as_bytes().to_owned()) + .add_gas_payer(keypair) + .build(); let result = shell.mempool_validate( tx.to_bytes().as_ref(), diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index ce4f71a6f5a..af1fd8cb78d 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -841,14 +841,14 @@ where // transaction key, then the fee payer is effectively // the MASP, otherwise derive // the payer from public key. - let fee_payer = if wrapper.pk != masp_tx_key().ref_to() { - wrapper.fee_payer() + let gas_payer = if wrapper.pk != masp_tx_key().ref_to() { + wrapper.gas_payer() } else { masp() }; // check that the fee payer has sufficient balance let balance = - self.get_balance(&wrapper.fee.token, &fee_payer); + self.get_balance(&wrapper.fee.token, &gas_payer); // In testnets, tx is allowed to skip fees if it // includes a valid PoW @@ -964,6 +964,7 @@ mod test_process_proposal { use namada::types::token::Amount; use namada::types::transaction::protocol::EthereumTxData; use namada::types::transaction::{Fee, WrapperTx, MIN_FEE}; + use namada::types::tx::TxBuilder; #[cfg(feature = "abcipp")] use namada::types::vote_extensions::bridge_pool_roots::MultiSignedVext; #[cfg(feature = "abcipp")] @@ -1537,12 +1538,13 @@ mod test_process_proposal { fn test_unsigned_wrapper_rejected() { let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); let keypair = gen_keypair(); + let public_key = keypair.ref_to(); let mut outer_tx = Tx::new(TxType::Wrapper(Box::new(WrapperTx::new( Fee { amount: Default::default(), token: shell.wl_storage.storage.native_token.clone(), }, - keypair.ref_to(), + public_key, Epoch(0), Default::default(), #[cfg(not(feature = "mainnet"))] @@ -1551,6 +1553,7 @@ mod test_process_proposal { outer_tx.header.chain_id = shell.chain_id.clone(); outer_tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); outer_tx.set_data(Data::new("transaction data".as_bytes().to_owned())); + let tx = outer_tx.to_bytes(); let response = { @@ -1568,12 +1571,14 @@ mod test_process_proposal { } }; + println!("{}", response.result.info); + assert_eq!(response.result.code, u32::from(ErrorCodes::InvalidSig)); assert_eq!( response.result.info, String::from( - "WrapperTx signature verification failed: Transaction doesn't \ - have any data with a signature." + "WrapperTx signature verification failed: The wrapper \ + signature is invalid." ) ); } @@ -1622,8 +1627,8 @@ mod test_process_proposal { panic!("Test failed") }; let expected_error = "WrapperTx signature verification \ - failed: Transaction doesn't have any \ - data with a signature."; + failed: The wrapper signature is \ + invalid."; assert_eq!( response.result.code, u32::from(ErrorCodes::InvalidSig) @@ -2023,10 +2028,14 @@ mod test_process_proposal { fn test_raw_tx_rejected() { let (mut shell, _recv, _, _) = test_utils::setup_at_height(3u64); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = shell.chain_id.clone(); - tx.set_code(Code::new("wasm_code".as_bytes().to_owned())); - tx.set_data(Data::new("transaction data".as_bytes().to_owned())); + let keypair = crate::wallet::defaults::daewon_keypair(); + + let tx_builder = TxBuilder::new(shell.chain_id.clone(), None); + let tx = tx_builder + .add_code("wasm_code".as_bytes().to_owned()) + .add_data("transaction data".as_bytes().to_owned()) + .add_gas_payer(keypair) + .build(); let response = { let request = ProcessProposal { diff --git a/apps/src/lib/node/ledger/shell/utils.rs b/apps/src/lib/node/ledger/shell/utils.rs new file mode 100644 index 00000000000..e55f54e8f82 --- /dev/null +++ b/apps/src/lib/node/ledger/shell/utils.rs @@ -0,0 +1,14 @@ +use borsh::BorshDeserialize; +use namada::ledger::storage_api::{self, StorageRead}; +use namada::types::storage::Key; + +pub(super) fn force_read(storage: &S, key: &Key) -> storage_api::Result +where + S: StorageRead, + T: BorshDeserialize, +{ + storage + .read::(key) + .transpose() + .expect("Storage key must be present.") +} diff --git a/apps/src/lib/wallet/defaults.rs b/apps/src/lib/wallet/defaults.rs index 81ca184f161..8ab728c96f7 100644 --- a/apps/src/lib/wallet/defaults.rs +++ b/apps/src/lib/wallet/defaults.rs @@ -8,7 +8,7 @@ pub use dev::{ validator_keys, }; use namada::ledger::wallet::alias::Alias; -use namada::ledger::{eth_bridge, governance, pos}; +use namada::ledger::{eth_bridge, governance, pgf, pos}; use namada::types::address::Address; use namada::types::key::*; @@ -22,6 +22,7 @@ pub fn addresses_from_genesis(genesis: GenesisConfig) -> Vec<(Alias, Address)> { ("pos_slash_pool".into(), pos::SLASH_POOL_ADDRESS), ("governance".into(), governance::ADDRESS), ("eth_bridge".into(), eth_bridge::ADDRESS), + ("pgf".into(), pgf::ADDRESS), ]; // Genesis validators let validator_addresses = @@ -75,7 +76,7 @@ mod dev { use borsh::BorshDeserialize; use namada::ledger::wallet::alias::Alias; - use namada::ledger::{governance, pos}; + use namada::ledger::{governance, pgf, pos}; use namada::types::address::{ apfel, btc, dot, eth, kartoffel, nam, schnitzel, Address, }; @@ -146,6 +147,7 @@ mod dev { ("pos".into(), pos::ADDRESS), ("pos_slash_pool".into(), pos::SLASH_POOL_ADDRESS), ("governance".into(), governance::ADDRESS), + ("governance".into(), pgf::ADDRESS), ("validator".into(), validator_address()), ("albert".into(), albert_address()), ("bertha".into(), bertha_address()), diff --git a/core/src/ledger/governance/cli/mod.rs b/core/src/ledger/governance/cli/mod.rs new file mode 100644 index 00000000000..45b839d1f44 --- /dev/null +++ b/core/src/ledger/governance/cli/mod.rs @@ -0,0 +1,6 @@ +/// CLi governance offline structures +pub mod offline; +/// CLi governance on chain structures +pub mod onchain; +/// CLi governance validation +mod validation; diff --git a/core/src/ledger/governance/cli/offline.rs b/core/src/ledger/governance/cli/offline.rs new file mode 100644 index 00000000000..e63ac2ff498 --- /dev/null +++ b/core/src/ledger/governance/cli/offline.rs @@ -0,0 +1,389 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs::{File, ReadDir}; +use std::path::PathBuf; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +use super::onchain::ProposalVote; +use super::validation::{is_valid_tally_epoch, ProposalValidation}; +use crate::proto::SignatureIndex; +use crate::types::account::AccountPublicKeysMap; +use crate::types::address::Address; +use crate::types::hash::Hash; +use crate::types::key::{common, RefTo, SigScheme}; +use crate::types::storage::Epoch; + +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +/// The offline proposal structure +pub struct OfflineProposal { + /// The proposal content + pub content: BTreeMap, + /// The proposal author address + pub author: Address, + /// The epoch from which this changes are executed + pub tally_epoch: Epoch, +} + +impl OfflineProposal { + /// Validate the offline proposal + pub fn validate( + self, + current_epoch: Epoch, + ) -> Result { + is_valid_tally_epoch(self.tally_epoch, current_epoch)?; + + Ok(self) + } + + /// Hash an offline proposal + pub fn hash(&self) -> Hash { + let content_serialized = serde_json::to_vec(&self.content) + .expect("Conversion to bytes shouldn't fail."); + let author_serialized = serde_json::to_vec(&self.author) + .expect("Conversion to bytes shouldn't fail."); + let tally_epoch_serialized = serde_json::to_vec(&self.tally_epoch) + .expect("Conversion to bytes shouldn't fail."); + let proposal_serialized = &[ + content_serialized, + author_serialized, + tally_epoch_serialized, + ] + .concat(); + Hash::sha256(proposal_serialized) + } + + /// Sign an offline proposal + pub fn sign( + self, + signing_keys: Vec, + account_public_keys_map: &AccountPublicKeysMap, + ) -> OfflineSignedProposal { + let proposal_hash = self.hash(); + + let signatures_index = compute_signatures_index( + &signing_keys, + account_public_keys_map, + &proposal_hash, + ); + + OfflineSignedProposal { + proposal: self, + signatures: signatures_index, + } + } +} + +impl TryFrom<&[u8]> for OfflineProposal { + type Error = serde_json::Error; + + fn try_from(value: &[u8]) -> Result { + serde_json::from_slice(value) + } +} + +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +/// The signed offline proposal structure +pub struct OfflineSignedProposal { + /// The proposal content + pub proposal: OfflineProposal, + /// The signatures over proposal data + pub signatures: BTreeSet, +} + +impl TryFrom<&[u8]> for OfflineSignedProposal { + type Error = serde_json::Error; + + fn try_from(value: &[u8]) -> Result { + serde_json::from_slice(value) + } +} + +impl OfflineSignedProposal { + /// Serialize the proposal to file. Returns the filename if successful. + pub fn serialize( + &self, + output_folder: Option, + ) -> Result { + let proposal_filename = + format!("offline_proposal_{}.json", self.proposal.hash()); + + let filepath = match output_folder { + Some(base_path) => base_path + .join(proposal_filename) + .to_str() + .unwrap() + .to_owned(), + None => proposal_filename, + }; + + let out = + File::create(&filepath).expect("Should be able to create a file."); + serde_json::to_writer_pretty(out, self)?; + + Ok(filepath) + } + + /// Check whether the signature is valid or not + fn check_signature( + &self, + account_public_keys_map: &AccountPublicKeysMap, + threshold: u8, + ) -> bool { + let proposal_hash = self.proposal.hash(); + if self.signatures.len() < threshold as usize { + return false; + } + + let valid_signatures = compute_total_valid_signatures( + &self.signatures, + account_public_keys_map, + &proposal_hash, + ); + + valid_signatures >= threshold + } + + /// Validate an offline proposal + pub fn validate( + self, + account_public_keys_map: &AccountPublicKeysMap, + threshold: u8, + ) -> Result { + let valid_signature = + self.check_signature(account_public_keys_map, threshold); + if !valid_signature { + Err(ProposalValidation::OkNoSignature) + } else { + Ok(self) + } + } +} + +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +/// The offline proposal structure +pub struct OfflineVote { + /// The proposal data hash + pub proposal_hash: Hash, + /// The proposal vote + pub vote: ProposalVote, + /// The signature over proposal data + pub signatures: BTreeSet, + /// The address corresponding to the signature pk + pub address: Address, + /// The validators address to which this address delegated to + pub delegations: Vec
, +} + +impl OfflineVote { + /// Create an offline vote for a proposal + pub fn new( + proposal: &OfflineSignedProposal, + vote: ProposalVote, + address: Address, + delegations: Vec
, + ) -> Self { + let proposal_hash = proposal.proposal.hash(); + + Self { + proposal_hash, + vote, + delegations, + signatures: BTreeSet::default(), + address, + } + } + + /// Sign the offline vote + pub fn sign( + self, + keypairs: Vec, + account_public_keys_map: &AccountPublicKeysMap, + ) -> Self { + let proposal_vote_data = self + .vote + .try_to_vec() + .expect("Conversion to bytes shouldn't fail."); + let delegations_hash = self + .delegations + .try_to_vec() + .expect("Conversion to bytes shouldn't fail."); + + let vote_hash = Hash::sha256( + [ + self.proposal_hash.to_vec(), + proposal_vote_data, + delegations_hash, + ] + .concat(), + ); + + let signatures = compute_signatures_index( + &keypairs, + account_public_keys_map, + &vote_hash, + ); + + Self { signatures, ..self } + } + + /// Check if the vote is yay + pub fn is_yay(&self) -> bool { + self.vote.is_yay() + } + + /// compute the hash of a proposal + pub fn compute_hash(&self) -> Hash { + let proposal_hash_data = self + .proposal_hash + .try_to_vec() + .expect("Conversion to bytes shouldn't fail."); + let proposal_vote_data = self + .vote + .try_to_vec() + .expect("Conversion to bytes shouldn't fail."); + let delegations_hash = self + .delegations + .try_to_vec() + .expect("Conversion to bytes shouldn't fail."); + let vote_serialized = + &[proposal_hash_data, proposal_vote_data, delegations_hash] + .concat(); + + Hash::sha256(vote_serialized) + } + + /// Check whether the signature is valid or not + pub fn check_signature( + &self, + account_public_keys_map: &AccountPublicKeysMap, + threshold: u8, + ) -> bool { + if self.signatures.len() < threshold as usize { + return false; + } + let vote_data_hash = self.compute_hash(); + + let valid_signatures = compute_total_valid_signatures( + &self.signatures, + account_public_keys_map, + &vote_data_hash, + ); + + valid_signatures >= threshold + } + + /// Serialize the proposal to file. Returns the filename if successful. + pub fn serialize( + &self, + output_folder: Option, + ) -> Result { + let vote_filename = format!( + "offline_vote_{}_{}.json", + self.proposal_hash, self.address + ); + let filepath = match output_folder { + Some(base_path) => { + base_path.join(vote_filename).to_str().unwrap().to_owned() + } + None => vote_filename, + }; + let out = File::create(&filepath).unwrap(); + serde_json::to_writer_pretty(out, self)?; + + Ok(filepath) + } +} + +/// Compute the signatures index +fn compute_signatures_index( + keys: &[common::SecretKey], + account_public_keys_map: &AccountPublicKeysMap, + hashed_data: &Hash, +) -> BTreeSet { + keys.iter() + .filter_map(|signing_key| { + let public_key = signing_key.ref_to(); + let public_key_index = + account_public_keys_map.get_index_from_public_key(&public_key); + if public_key_index.is_some() { + let signature = + common::SigScheme::sign(signing_key, hashed_data); + Some(SignatureIndex::from_single_signature(signature)) + } else { + None + } + }) + .collect::>() +} + +/// Compute the total amount of signatures +fn compute_total_valid_signatures( + signatures: &BTreeSet, + account_public_keys_map: &AccountPublicKeysMap, + hashed_data: &Hash, +) -> u8 { + signatures.iter().fold(0_u8, |acc, signature_index| { + let public_key = account_public_keys_map + .get_public_key_from_index(signature_index.index); + if let Some(pk) = public_key { + let sig_check = common::SigScheme::verify_signature( + &pk, + hashed_data, + &signature_index.signature, + ); + if sig_check.is_ok() { acc + 1 } else { acc } + } else { + acc + } + }) +} + +/// Read all offline files from a folder +pub fn read_offline_files(path: ReadDir) -> Vec { + path.filter_map(|path| { + if let Ok(path) = path { + let file_type = path.file_type(); + if let Ok(file_type) = file_type { + if file_type.is_file() + && path.file_name().to_string_lossy().contains("offline_") + { + Some(path.path()) + } else { + None + } + } else { + None + } + } else { + None + } + }) + .collect::>() +} + +/// Find offline votes from a folder +pub fn find_offline_proposal(files: &[PathBuf]) -> Option { + files + .iter() + .filter(|path| path.to_string_lossy().contains("offline_proposal_")) + .cloned() + .collect::>() + .first() + .cloned() +} + +/// Find offline votes from a folder +pub fn find_offline_votes(files: &[PathBuf]) -> Vec { + files + .iter() + .filter(|path| path.to_string_lossy().contains("offline_vote_")) + .cloned() + .collect::>() +} diff --git a/core/src/ledger/governance/cli/onchain.rs b/core/src/ledger/governance/cli/onchain.rs new file mode 100644 index 00000000000..33cabc61566 --- /dev/null +++ b/core/src/ledger/governance/cli/onchain.rs @@ -0,0 +1,326 @@ +use std::collections::BTreeMap; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +use super::validation::{ + is_valid_author_balance, is_valid_content, is_valid_default_proposal_data, + is_valid_end_epoch, is_valid_grace_epoch, is_valid_pgf_funding_data, + is_valid_pgf_stewards_data, is_valid_proposal_period, is_valid_start_epoch, + ProposalValidation, +}; +use crate::ledger::governance::parameters::GovernanceParameters; +use crate::ledger::storage_api::token; +use crate::types::address::Address; +use crate::types::storage::Epoch; + +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +/// The proposal structure +pub struct OnChainProposal { + /// The proposal id + pub id: Option, + /// The proposal content + pub content: BTreeMap, + /// The proposal author address + pub author: Address, + /// The epoch from which voting is allowed + pub voting_start_epoch: Epoch, + /// The epoch from which voting is stopped + pub voting_end_epoch: Epoch, + /// The epoch from which this changes are executed + pub grace_epoch: Epoch, +} + +/// Pgf default proposal +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct DefaultProposal { + /// The proposal data + pub proposal: OnChainProposal, + /// The default proposal extra data + pub data: Option>, +} + +impl DefaultProposal { + /// Validate a default funding proposal + pub fn validate( + self, + governance_parameters: &GovernanceParameters, + current_epoch: Epoch, + balance: token::Amount, + ) -> Result { + is_valid_start_epoch( + self.proposal.voting_start_epoch, + current_epoch, + governance_parameters.min_proposal_voting_period, + )?; + is_valid_end_epoch( + self.proposal.voting_start_epoch, + self.proposal.voting_end_epoch, + current_epoch, + governance_parameters.min_proposal_voting_period, + governance_parameters.min_proposal_voting_period, + governance_parameters.max_proposal_period, + )?; + is_valid_grace_epoch( + self.proposal.grace_epoch, + self.proposal.voting_end_epoch, + governance_parameters.min_proposal_grace_epochs, + )?; + is_valid_proposal_period( + self.proposal.voting_start_epoch, + self.proposal.grace_epoch, + governance_parameters.max_proposal_period, + )?; + is_valid_author_balance( + balance, + governance_parameters.min_proposal_fund, + )?; + is_valid_content( + &self.proposal.content, + governance_parameters.max_proposal_content_size, + )?; + is_valid_default_proposal_data( + &self.data, + governance_parameters.max_proposal_code_size, + )?; + + Ok(self) + } +} + +impl TryFrom<&[u8]> for DefaultProposal { + type Error = serde_json::Error; + + fn try_from(value: &[u8]) -> Result { + serde_json::from_slice(value) + } +} + +/// Pgf stewards proposal +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfStewardProposal { + /// The proposal data + pub proposal: OnChainProposal, + /// The Pgf steward proposal extra data + pub data: Vec, +} + +impl PgfStewardProposal { + /// Validate a Pgf stewards proposal + pub fn validate( + self, + governance_parameters: &GovernanceParameters, + current_epoch: Epoch, + balance: token::Amount, + ) -> Result { + is_valid_start_epoch( + self.proposal.voting_start_epoch, + current_epoch, + governance_parameters.min_proposal_voting_period, + )?; + is_valid_end_epoch( + self.proposal.voting_start_epoch, + self.proposal.voting_end_epoch, + current_epoch, + governance_parameters.min_proposal_voting_period, + governance_parameters.min_proposal_voting_period, + governance_parameters.max_proposal_period, + )?; + is_valid_grace_epoch( + self.proposal.grace_epoch, + self.proposal.voting_end_epoch, + governance_parameters.min_proposal_grace_epochs, + )?; + is_valid_proposal_period( + self.proposal.voting_start_epoch, + self.proposal.grace_epoch, + governance_parameters.max_proposal_period, + )?; + is_valid_author_balance( + balance, + governance_parameters.min_proposal_fund, + )?; + is_valid_content( + &self.proposal.content, + governance_parameters.max_proposal_content_size, + )?; + is_valid_pgf_stewards_data(&self.data)?; + + Ok(self) + } +} + +impl TryFrom<&[u8]> for PgfStewardProposal { + type Error = serde_json::Error; + + fn try_from(value: &[u8]) -> Result { + serde_json::from_slice(value) + } +} + +/// Pgf funding proposal +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfFundingProposal { + /// The proposal data + pub proposal: OnChainProposal, + /// The Pgf funding proposal extra data + pub data: PgfFunding, +} + +impl PgfFundingProposal { + /// Validate a Pgf funding proposal + pub fn validate( + self, + governance_parameters: &GovernanceParameters, + current_epoch: Epoch, + ) -> Result { + is_valid_start_epoch( + self.proposal.voting_start_epoch, + current_epoch, + governance_parameters.min_proposal_voting_period, + )?; + is_valid_end_epoch( + self.proposal.voting_start_epoch, + self.proposal.voting_end_epoch, + current_epoch, + governance_parameters.min_proposal_voting_period, + governance_parameters.min_proposal_voting_period, + governance_parameters.max_proposal_period, + )?; + is_valid_grace_epoch( + self.proposal.grace_epoch, + self.proposal.voting_end_epoch, + governance_parameters.min_proposal_grace_epochs, + )?; + is_valid_proposal_period( + self.proposal.voting_start_epoch, + self.proposal.grace_epoch, + governance_parameters.max_proposal_period, + )?; + is_valid_content( + &self.proposal.content, + governance_parameters.max_proposal_content_size, + )?; + is_valid_pgf_funding_data(&self.data)?; + + Ok(self) + } +} + +impl TryFrom<&[u8]> for PgfFundingProposal { + type Error = serde_json::Error; + + fn try_from(value: &[u8]) -> Result { + serde_json::from_slice(value) + } +} + +/// Pgf stewards +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfSteward { + /// Pgf action + pub action: PgfAction, + /// steward address + pub address: Address, +} + +/// Pgf action +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub enum PgfAction { + /// Add action + Add, + /// Remove action + Remove, +} + +/// Pgf fundings +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfFunding { + /// Pgf continous funding + pub continous: Vec, + /// pgf retro fundings + pub retro: Vec, +} + +/// Pgf continous funding +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfContinous { + /// Pgf target + pub target: PgfFundingTarget, + /// Pgf action + pub action: PgfAction, +} + +/// Pgf retro funding +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfRetro { + /// Pgf retro target + pub target: PgfFundingTarget, +} + +/// Pgf Target +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +pub struct PgfFundingTarget { + /// Target amount + pub amount: token::Amount, + /// Target address + pub address: Address, +} + +/// Rappresent an proposal vote +#[derive( + Debug, + Clone, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, + PartialEq, +)] +pub enum ProposalVote { + /// Rappresent an yay proposal vote + Yay, + /// Rappresent an nay proposal vote + Nay, + /// Rappresent an invalid proposal vote + Invalid, +} + +impl TryFrom for ProposalVote { + type Error = String; + + fn try_from(value: String) -> Result { + match value.trim().to_lowercase().as_str() { + "yay" => Ok(ProposalVote::Yay), + "nay" => Ok(ProposalVote::Nay), + _ => Err("invalid vote".to_string()), + } + } +} + +impl ProposalVote { + /// Check if the proposal type is yay + pub fn is_yay(&self) -> bool { + matches!(self, ProposalVote::Yay) + } +} diff --git a/core/src/ledger/governance/cli/utils.rs b/core/src/ledger/governance/cli/utils.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/src/ledger/governance/cli/validation.rs b/core/src/ledger/governance/cli/validation.rs new file mode 100644 index 00000000000..585a3362ad3 --- /dev/null +++ b/core/src/ledger/governance/cli/validation.rs @@ -0,0 +1,241 @@ +use std::collections::BTreeMap; + +use thiserror::Error; + +use super::onchain::{PgfFunding, PgfSteward}; +use crate::types::storage::Epoch; +use crate::types::token; + +/// This enum raprresent a proposal data +#[derive(Clone, Debug, PartialEq, Error)] +pub enum ProposalValidation { + /// The proposal field are correct but there is no signature + #[error("The proposal is not signed. Can't vote on it")] + OkNoSignature, + /// The proposal start epoch is invalid + #[error( + "Invalid proposal start epoch: {0} must be greater than current epoch \ + {1} and a multiple of {2}" + )] + InvalidStartEpoch(Epoch, Epoch, u64), + /// The proposal difference between start and end epoch is invalid + #[error( + "Invalid proposal end epoch: difference between proposal start and \ + end epoch must be at least {0}, at max {1} and the end epoch must be \ + a multiple of {0}" + )] + InvalidStartEndDifference(u64, u64), + /// The proposal difference between end and grace epoch is invalid + #[error( + "Invalid proposal grace epoch: difference between proposal grace and \ + end epoch must be at least {0}, but found {1}" + )] + InvalidEndGraceDifference(u64, u64), + /// The proposal difference between end and grace epoch is invalid + #[error( + "Invalid proposal period: difference between proposal start and grace \ + epoch must be at most {1}, but found {0}" + )] + InvalidProposalPeriod(u64, u64), + /// The proposal author does not have enought balance to pay for proposal + /// fees + #[error( + "Invalid proposal minimum funds: the author address has {0} but \ + minimum is {1}" + )] + InvalidBalance(String, String), + /// The proposal content is too large + #[error( + "Invalid proposal content length: the proposal content length is {0} \ + but maximum is {1}" + )] + InvalidContentLength(u64, u64), + /// Invalid offline proposal tally epoch + #[error( + "Invalid proposal tally epoch: tally epoch ({0}) must be less than \ + current epoch ({1})" + )] + InvalidTallyEPoch(Epoch, Epoch), + /// The proposal wasm code is not valid + #[error( + "Invalid proposal extra data: file doesn't exist or content size \ + ({0}) is to big (max {1})" + )] + InvalidDefaultProposalExtraData(u64, u64), + /// The pgf stewards data is not valid + #[error("Invalid proposal extra data: cannot be empty.")] + InvalidPgfStewardsExtraData, + /// The pgf funding data is not valid + #[error("invalid proposal extra data: cannot be empty.")] + InvalidPgfFundingExtraData, +} + +pub fn is_valid_author_balance( + author_balance: token::Amount, + min_proposal_fund: token::Amount, +) -> Result<(), ProposalValidation> { + if author_balance.can_spend(&min_proposal_fund) { + Ok(()) + } else { + Err(ProposalValidation::InvalidBalance( + author_balance.to_string_native(), + min_proposal_fund.to_string_native(), + )) + } +} + +pub fn is_valid_start_epoch( + proposal_start_epoch: Epoch, + current_epoch: Epoch, + proposal_epoch_multiplier: u64, +) -> Result<(), ProposalValidation> { + let start_epoch_greater_than_current = proposal_start_epoch > current_epoch; + let start_epoch_is_multipler = + proposal_start_epoch.0 % proposal_epoch_multiplier == 0; + + if start_epoch_greater_than_current && start_epoch_is_multipler { + Ok(()) + } else { + Err(ProposalValidation::InvalidStartEpoch( + proposal_start_epoch, + current_epoch, + proposal_epoch_multiplier, + )) + } +} + +pub fn is_valid_end_epoch( + proposal_start_epoch: Epoch, + proposal_end_epoch: Epoch, + _current_epoch: Epoch, + proposal_epoch_multiplier: u64, + min_proposal_voting_period: u64, + max_proposal_period: u64, +) -> Result<(), ProposalValidation> { + let voting_period = proposal_end_epoch.0 - proposal_start_epoch.0; + let end_epoch_is_multipler = + proposal_end_epoch % proposal_epoch_multiplier == 0; + let is_valid_voting_period = voting_period > 0 + && voting_period >= min_proposal_voting_period + && min_proposal_voting_period <= max_proposal_period; + + if end_epoch_is_multipler && is_valid_voting_period { + Ok(()) + } else { + Err(ProposalValidation::InvalidStartEndDifference( + min_proposal_voting_period, + max_proposal_period, + )) + } +} + +pub fn is_valid_grace_epoch( + proposal_grace_epoch: Epoch, + proposal_end_epoch: Epoch, + min_proposal_grace_epoch: u64, +) -> Result<(), ProposalValidation> { + let grace_period = proposal_grace_epoch.0 - proposal_end_epoch.0; + + if grace_period > 0 && grace_period >= min_proposal_grace_epoch { + Ok(()) + } else { + Err(ProposalValidation::InvalidEndGraceDifference( + grace_period, + min_proposal_grace_epoch, + )) + } +} + +pub fn is_valid_proposal_period( + proposal_start_epoch: Epoch, + proposal_grace_epoch: Epoch, + max_proposal_period: u64, +) -> Result<(), ProposalValidation> { + let proposal_period = proposal_grace_epoch.0 - proposal_start_epoch.0; + + if proposal_period > 0 && proposal_period <= max_proposal_period { + Ok(()) + } else { + Err(ProposalValidation::InvalidProposalPeriod( + proposal_period, + max_proposal_period, + )) + } +} + +pub fn is_valid_content( + proposal_content: &BTreeMap, + max_content_length: u64, +) -> Result<(), ProposalValidation> { + let proposal_content_keys_length: u64 = + proposal_content.keys().map(|key| key.len() as u64).sum(); + let proposal_content_values_length: u64 = proposal_content + .values() + .map(|value| value.len() as u64) + .sum(); + let proposal_content_length = + proposal_content_values_length + proposal_content_keys_length; + + if proposal_content_length <= max_content_length { + Ok(()) + } else { + Err(ProposalValidation::InvalidContentLength( + proposal_content_length, + max_content_length, + )) + } +} + +pub fn is_valid_tally_epoch( + tally_epoch: Epoch, + current_epoch: Epoch, +) -> Result<(), ProposalValidation> { + if tally_epoch <= current_epoch { + Ok(()) + } else { + Err(ProposalValidation::InvalidTallyEPoch( + tally_epoch, + current_epoch, + )) + } +} + +pub fn is_valid_default_proposal_data( + data: &Option>, + max_extra_data_size: u64, +) -> Result<(), ProposalValidation> { + match data { + Some(content) => { + let extra_data_length = content.len() as u64; + if extra_data_length <= max_extra_data_size { + Ok(()) + } else { + Err(ProposalValidation::InvalidDefaultProposalExtraData( + extra_data_length, + max_extra_data_size, + )) + } + } + None => Ok(()), + } +} + +pub fn is_valid_pgf_stewards_data( + data: &Vec, +) -> Result<(), ProposalValidation> { + if !data.is_empty() { + Ok(()) + } else { + Err(ProposalValidation::InvalidPgfStewardsExtraData) + } +} + +pub fn is_valid_pgf_funding_data( + data: &PgfFunding, +) -> Result<(), ProposalValidation> { + if !data.continous.is_empty() || !data.retro.is_empty() { + Ok(()) + } else { + Err(ProposalValidation::InvalidPgfFundingExtraData) + } +} diff --git a/core/src/ledger/governance/mod.rs b/core/src/ledger/governance/mod.rs index ae488383bfc..00fcb3a990d 100644 --- a/core/src/ledger/governance/mod.rs +++ b/core/src/ledger/governance/mod.rs @@ -2,10 +2,14 @@ use crate::types::address::{self, Address}; +/// governance CLI structures +pub mod cli; /// governance parameters pub mod parameters; /// governance storage pub mod storage; +/// Governance utility functions/structs +pub mod utils; /// The governance internal address pub const ADDRESS: Address = address::GOV; diff --git a/core/src/ledger/governance/parameters.rs b/core/src/ledger/governance/parameters.rs index 2d247bc24f2..ebb28372af6 100644 --- a/core/src/ledger/governance/parameters.rs +++ b/core/src/ledger/governance/parameters.rs @@ -2,9 +2,9 @@ use std::fmt::Display; use borsh::{BorshDeserialize, BorshSerialize}; -use super::storage as gov_storage; +use super::storage::keys as goverance_storage; use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; -use crate::types::token::Amount; +use crate::types::token; #[derive( Clone, @@ -18,13 +18,13 @@ use crate::types::token::Amount; BorshDeserialize, )] /// Governance parameter structure -pub struct GovParams { +pub struct GovernanceParameters { /// Minimum amount of locked funds - pub min_proposal_fund: u64, + pub min_proposal_fund: token::Amount, /// Maximum kibibyte length for proposal code pub max_proposal_code_size: u64, /// Minimum proposal voting period in epochs - pub min_proposal_period: u64, + pub min_proposal_voting_period: u64, /// Maximum proposal voting period in epochs pub max_proposal_period: u64, /// Maximum number of characters for proposal content @@ -33,16 +33,16 @@ pub struct GovParams { pub min_proposal_grace_epochs: u64, } -impl Display for GovParams { +impl Display for GovernanceParameters { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Min. proposal fund: {}\nMax. proposal code size: {}\nMin. \ proposal period: {}\nMax. proposal period: {}\nMax. proposal \ content size: {}\nMin. proposal grace epochs: {}", - self.min_proposal_fund, + self.min_proposal_fund.to_string_native(), self.max_proposal_code_size, - self.min_proposal_period, + self.min_proposal_voting_period, self.max_proposal_period, self.max_proposal_content_size, self.min_proposal_grace_epochs @@ -50,12 +50,12 @@ impl Display for GovParams { } } -impl Default for GovParams { +impl Default for GovernanceParameters { fn default() -> Self { Self { - min_proposal_fund: 500, + min_proposal_fund: token::Amount::native_whole(500), max_proposal_code_size: 300_000, - min_proposal_period: 3, + min_proposal_voting_period: 3, max_proposal_period: 27, max_proposal_content_size: 10_000, min_proposal_grace_epochs: 6, @@ -63,7 +63,7 @@ impl Default for GovParams { } } -impl GovParams { +impl GovernanceParameters { /// Initialize governance parameters into storage pub fn init_storage(&self, storage: &mut S) -> storage_api::Result<()> where @@ -72,39 +72,42 @@ impl GovParams { let Self { min_proposal_fund, max_proposal_code_size, - min_proposal_period, + min_proposal_voting_period, max_proposal_period, max_proposal_content_size, min_proposal_grace_epochs, } = self; - let min_proposal_fund_key = gov_storage::get_min_proposal_fund_key(); - let amount = Amount::native_whole(*min_proposal_fund); - storage.write(&min_proposal_fund_key, amount)?; + let min_proposal_fund_key = + goverance_storage::get_min_proposal_fund_key(); + storage.write(&min_proposal_fund_key, min_proposal_fund)?; let max_proposal_code_size_key = - gov_storage::get_max_proposal_code_size_key(); + goverance_storage::get_max_proposal_code_size_key(); storage.write(&max_proposal_code_size_key, max_proposal_code_size)?; - let min_proposal_period_key = - gov_storage::get_min_proposal_period_key(); - storage.write(&min_proposal_period_key, min_proposal_period)?; + let min_proposal_voting_period_key = + goverance_storage::get_min_proposal_voting_period_key(); + storage.write( + &min_proposal_voting_period_key, + min_proposal_voting_period, + )?; let max_proposal_period_key = - gov_storage::get_max_proposal_period_key(); + goverance_storage::get_max_proposal_period_key(); storage.write(&max_proposal_period_key, max_proposal_period)?; let max_proposal_content_size_key = - gov_storage::get_max_proposal_content_key(); + goverance_storage::get_max_proposal_content_key(); storage .write(&max_proposal_content_size_key, max_proposal_content_size)?; let min_proposal_grace_epoch_key = - gov_storage::get_min_proposal_grace_epoch_key(); + goverance_storage::get_min_proposal_grace_epoch_key(); storage .write(&min_proposal_grace_epoch_key, min_proposal_grace_epochs)?; - let counter_key = gov_storage::get_counter_key(); + let counter_key = goverance_storage::get_counter_key(); storage.write(&counter_key, u64::MIN) } } diff --git a/core/src/ledger/governance/storage.rs b/core/src/ledger/governance/storage/keys.rs similarity index 78% rename from core/src/ledger/governance/storage.rs rename to core/src/ledger/governance/storage/keys.rs index e00c4be6787..a975b6541fd 100644 --- a/core/src/ledger/governance/storage.rs +++ b/core/src/ledger/governance/storage/keys.rs @@ -1,27 +1,32 @@ +use namada_macros::StorageKeys; + use crate::ledger::governance::ADDRESS; use crate::types::address::Address; use crate::types::storage::{DbKeySeg, Key, KeySeg}; -const PROPOSAL_PREFIX: &str = "proposal"; -const PROPOSAL_VOTE: &str = "vote"; -const PROPOSAL_AUTHOR: &str = "author"; -const PROPOSAL_TYPE: &str = "type"; -const PROPOSAL_CONTENT: &str = "content"; -const PROPOSAL_START_EPOCH: &str = "start_epoch"; -const PROPOSAL_END_EPOCH: &str = "end_epoch"; -const PROPOSAL_GRACE_EPOCH: &str = "grace_epoch"; -const PROPOSAL_FUNDS: &str = "funds"; -const PROPOSAL_CODE: &str = "proposal_code"; -const PROPOSAL_COMMITTING_EPOCH: &str = "epoch"; - -const MIN_PROPOSAL_FUND_KEY: &str = "min_fund"; -const MAX_PROPOSAL_CODE_SIZE_KEY: &str = "max_code_size"; -const MIN_PROPOSAL_PERIOD_KEY: &str = "min_period"; -const MAX_PROPOSAL_PERIOD_KEY: &str = "max_period"; -const MAX_PROPOSAL_CONTENT_SIZE_KEY: &str = "max_content"; -const MIN_GRACE_EPOCH_KEY: &str = "min_grace_epoch"; -const COUNTER_KEY: &str = "counter"; -const PENDING_PROPOSAL: &str = "pending"; +/// Storage keys for governance internal address. +#[derive(StorageKeys)] +struct Keys { + proposal: &'static str, + vote: &'static str, + author: &'static str, + proposal_type: &'static str, + content: &'static str, + start_epoch: &'static str, + end_epoch: &'static str, + grace_epoch: &'static str, + funds: &'static str, + proposal_code: &'static str, + committing_epoch: &'static str, + min_fund: &'static str, + max_code_size: &'static str, + min_period: &'static str, + max_period: &'static str, + max_content: &'static str, + min_grace_epoch: &'static str, + counter: &'static str, + pending: &'static str, +} /// Check if key is inside governance address space pub fn is_governance_key(key: &Key) -> bool { @@ -39,8 +44,8 @@ pub fn is_vote_key(key: &Key) -> bool { DbKeySeg::AddressSeg(_validator_address), DbKeySeg::AddressSeg(_address), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && vote == PROPOSAL_VOTE => + && prefix == Keys::VALUES.proposal + && vote == Keys::VALUES.vote => { id.parse::().is_ok() } @@ -57,8 +62,8 @@ pub fn is_author_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(author), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && author == PROPOSAL_AUTHOR => + && prefix == Keys::VALUES.proposal + && author == Keys::VALUES.author => { id.parse::().is_ok() } @@ -75,8 +80,8 @@ pub fn is_proposal_code_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(proposal_code), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && proposal_code == PROPOSAL_CODE => + && prefix == Keys::VALUES.proposal + && proposal_code == Keys::VALUES.proposal_code => { id.parse::().is_ok() } @@ -93,8 +98,8 @@ pub fn is_grace_epoch_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(grace_epoch), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && grace_epoch == PROPOSAL_GRACE_EPOCH => + && prefix == Keys::VALUES.proposal + && grace_epoch == Keys::VALUES.grace_epoch => { id.parse::().is_ok() } @@ -111,8 +116,8 @@ pub fn is_content_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(content), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && content == PROPOSAL_CONTENT => + && prefix == Keys::VALUES.proposal + && content == Keys::VALUES.content => { id.parse::().is_ok() } @@ -129,8 +134,8 @@ pub fn is_balance_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(funds), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && funds == PROPOSAL_FUNDS => + && prefix == Keys::VALUES.proposal + && funds == Keys::VALUES.funds => { id.parse::().is_ok() } @@ -147,8 +152,8 @@ pub fn is_start_epoch_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(start_epoch), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && start_epoch == PROPOSAL_START_EPOCH => + && prefix == Keys::VALUES.proposal + && start_epoch == Keys::VALUES.start_epoch => { id.parse::().is_ok() } @@ -165,8 +170,8 @@ pub fn is_end_epoch_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(end_epoch), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && end_epoch == PROPOSAL_END_EPOCH => + && prefix == Keys::VALUES.proposal + && end_epoch == Keys::VALUES.end_epoch => { id.parse::().is_ok() } @@ -183,8 +188,8 @@ pub fn is_proposal_type_key(key: &Key) -> bool { DbKeySeg::StringSeg(id), DbKeySeg::StringSeg(proposal_type), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && proposal_type == PROPOSAL_TYPE => + && prefix == Keys::VALUES.proposal + && proposal_type == Keys::VALUES.proposal_type => { id.parse::().is_ok() } @@ -194,7 +199,7 @@ pub fn is_proposal_type_key(key: &Key) -> bool { /// Check if key is counter key pub fn is_counter_key(key: &Key) -> bool { - matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(counter)] if addr == &ADDRESS && counter == COUNTER_KEY) + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(counter)] if addr == &ADDRESS && counter == Keys::VALUES.counter) } /// Check if key is a proposal fund parameter key @@ -202,7 +207,7 @@ pub fn is_min_proposal_fund_key(key: &Key) -> bool { matches!(&key.segments[..], [ DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(min_funds_param), - ] if addr == &ADDRESS && min_funds_param == MIN_PROPOSAL_FUND_KEY) + ] if addr == &ADDRESS && min_funds_param == Keys::VALUES.min_fund) } /// Check if key is a proposal max content parameter key @@ -211,7 +216,7 @@ pub fn is_max_content_size_key(key: &Key) -> bool { DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(max_content_size_param), ] if addr == &ADDRESS - && max_content_size_param == MAX_PROPOSAL_CONTENT_SIZE_KEY) + && max_content_size_param == Keys::VALUES.max_content) } /// Check if key is a max proposal size key @@ -220,16 +225,16 @@ pub fn is_max_proposal_code_size_key(key: &Key) -> bool { DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(max_content_size_param), ] if addr == &ADDRESS - && max_content_size_param == MAX_PROPOSAL_CONTENT_SIZE_KEY) + && max_content_size_param == Keys::VALUES.max_code_size) } /// Check if key is a min proposal period param key -pub fn is_min_proposal_period_key(key: &Key) -> bool { +pub fn is_min_proposal_voting_period_key(key: &Key) -> bool { matches!(&key.segments[..], [ DbKeySeg::AddressSeg(addr), - DbKeySeg::StringSeg(min_proposal_period_param), + DbKeySeg::StringSeg(min_proposal_voting_period_param), ] if addr == &ADDRESS - && min_proposal_period_param == MIN_PROPOSAL_PERIOD_KEY) + && min_proposal_voting_period_param == Keys::VALUES.min_period) } /// Check if key is a max proposal period param key @@ -238,7 +243,7 @@ pub fn is_max_proposal_period_key(key: &Key) -> bool { DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(max_proposal_period_param), ] if addr == &ADDRESS - && max_proposal_period_param == MAX_PROPOSAL_PERIOD_KEY) + && max_proposal_period_param == Keys::VALUES.max_period) } /// Check if key is a min grace epoch key @@ -250,8 +255,8 @@ pub fn is_commit_proposal_key(key: &Key) -> bool { DbKeySeg::StringSeg(_epoch), DbKeySeg::StringSeg(_id), ] if addr == &ADDRESS - && prefix == PROPOSAL_PREFIX - && epoch_prefix == PROPOSAL_COMMITTING_EPOCH + && prefix == Keys::VALUES.proposal + && epoch_prefix == Keys::VALUES.committing_epoch ) } @@ -261,7 +266,7 @@ pub fn is_min_grace_epoch_key(key: &Key) -> bool { DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(min_grace_epoch_param), ] if addr == &ADDRESS - && min_grace_epoch_param == MIN_GRACE_EPOCH_KEY) + && min_grace_epoch_param == Keys::VALUES.min_grace_epoch) } /// Check if key is parameter key @@ -269,7 +274,7 @@ pub fn is_parameter_key(key: &Key) -> bool { is_min_proposal_fund_key(key) || is_max_content_size_key(key) || is_max_proposal_code_size_key(key) - || is_min_proposal_period_key(key) + || is_min_proposal_voting_period_key(key) || is_max_proposal_period_key(key) || is_min_grace_epoch_key(key) } @@ -282,56 +287,56 @@ pub fn is_start_or_end_epoch_key(key: &Key) -> bool { /// Get governance prefix key pub fn proposal_prefix() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&PROPOSAL_PREFIX.to_owned()) + .push(&Keys::VALUES.proposal.to_owned()) .expect("Cannot obtain a storage key") } /// Get key for the minimum proposal fund pub fn get_min_proposal_fund_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&MIN_PROPOSAL_FUND_KEY.to_owned()) + .push(&Keys::VALUES.min_fund.to_owned()) .expect("Cannot obtain a storage key") } /// Get maximum proposal code size key pub fn get_max_proposal_code_size_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&MAX_PROPOSAL_CODE_SIZE_KEY.to_owned()) + .push(&Keys::VALUES.max_code_size.to_owned()) .expect("Cannot obtain a storage key") } /// Get minimum proposal period key -pub fn get_min_proposal_period_key() -> Key { +pub fn get_min_proposal_voting_period_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&MIN_PROPOSAL_PERIOD_KEY.to_owned()) + .push(&Keys::VALUES.min_period.to_owned()) .expect("Cannot obtain a storage key") } /// Get maximum proposal period key pub fn get_max_proposal_period_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&MAX_PROPOSAL_PERIOD_KEY.to_owned()) + .push(&Keys::VALUES.max_period.to_owned()) .expect("Cannot obtain a storage key") } /// Get maximum proposal content key pub fn get_max_proposal_content_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&MAX_PROPOSAL_CONTENT_SIZE_KEY.to_owned()) + .push(&Keys::VALUES.max_content.to_owned()) .expect("Cannot obtain a storage key") } /// Get min grace epoch proposal key pub fn get_min_proposal_grace_epoch_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&MIN_GRACE_EPOCH_KEY.to_owned()) + .push(&Keys::VALUES.min_grace_epoch.to_owned()) .expect("Cannot obtain a storage key") } /// Get key of proposal ids counter pub fn get_counter_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&COUNTER_KEY.to_owned()) + .push(&Keys::VALUES.counter.to_owned()) .expect("Cannot obtain a storage key") } @@ -340,7 +345,7 @@ pub fn get_content_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_CONTENT.to_owned()) + .push(&Keys::VALUES.content.to_owned()) .expect("Cannot obtain a storage key") } @@ -349,7 +354,7 @@ pub fn get_author_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_AUTHOR.to_owned()) + .push(&Keys::VALUES.author.to_owned()) .expect("Cannot obtain a storage key") } @@ -358,7 +363,7 @@ pub fn get_proposal_type_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_TYPE.to_owned()) + .push(&Keys::VALUES.proposal_type.to_owned()) .expect("Cannot obtain a storage key") } @@ -367,7 +372,7 @@ pub fn get_voting_start_epoch_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_START_EPOCH.to_owned()) + .push(&Keys::VALUES.start_epoch.to_owned()) .expect("Cannot obtain a storage key") } @@ -376,7 +381,7 @@ pub fn get_voting_end_epoch_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_END_EPOCH.to_owned()) + .push(&Keys::VALUES.end_epoch.to_owned()) .expect("Cannot obtain a storage key") } @@ -385,7 +390,7 @@ pub fn get_funds_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_FUNDS.to_owned()) + .push(&Keys::VALUES.funds.to_owned()) .expect("Cannot obtain a storage key") } @@ -394,14 +399,14 @@ pub fn get_grace_epoch_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_GRACE_EPOCH.to_owned()) + .push(&Keys::VALUES.grace_epoch.to_owned()) .expect("Cannot obtain a storage key") } /// Get the proposal committing key prefix pub fn get_commiting_proposals_prefix(epoch: u64) -> Key { proposal_prefix() - .push(&PROPOSAL_COMMITTING_EPOCH.to_owned()) + .push(&Keys::VALUES.committing_epoch.to_owned()) .expect("Cannot obtain a storage key") .push(&epoch.to_string()) .expect("Cannot obtain a storage key") @@ -412,7 +417,7 @@ pub fn get_proposal_code_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_CODE.to_owned()) + .push(&Keys::VALUES.proposal_code.to_owned()) .expect("Cannot obtain a storage key") } @@ -428,7 +433,7 @@ pub fn get_proposal_vote_prefix_key(id: u64) -> Key { proposal_prefix() .push(&id.to_string()) .expect("Cannot obtain a storage key") - .push(&PROPOSAL_VOTE.to_owned()) + .push(&Keys::VALUES.vote.to_owned()) .expect("Cannot obtain a storage key") } @@ -448,7 +453,7 @@ pub fn get_vote_proposal_key( /// Get the proposal execution key pub fn get_proposal_execution_key(id: u64) -> Key { Key::from(ADDRESS.to_db_key()) - .push(&PENDING_PROPOSAL.to_owned()) + .push(&Keys::VALUES.pending.to_owned()) .expect("Cannot obtain a storage key") .push(&id.to_string()) .expect("Cannot obtain a storage key") diff --git a/core/src/ledger/governance/storage/mod.rs b/core/src/ledger/governance/storage/mod.rs new file mode 100644 index 00000000000..e2de8e6ab89 --- /dev/null +++ b/core/src/ledger/governance/storage/mod.rs @@ -0,0 +1,6 @@ +/// Governance proposal keys +pub mod keys; +/// Proposal structures +pub mod proposal; +/// Vote structures +pub mod vote; diff --git a/core/src/ledger/governance/storage/proposal.rs b/core/src/ledger/governance/storage/proposal.rs new file mode 100644 index 00000000000..b5ac1284c47 --- /dev/null +++ b/core/src/ledger/governance/storage/proposal.rs @@ -0,0 +1,270 @@ +use std::collections::{BTreeMap, HashSet}; +use std::fmt::Display; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::ledger::governance::cli::onchain::{ + PgfAction, PgfContinous, PgfRetro, PgfSteward, +}; +use crate::ledger::governance::utils::{ProposalStatus, TallyType}; +use crate::ledger::storage_api::token::Amount; +use crate::types::address::Address; +use crate::types::hash::Hash; +use crate::types::storage::Epoch; + +#[allow(missing_docs)] +#[derive(Debug, Error)] +pub enum ProposalTypeError { + #[error("Invalid proposal type.")] + InvalidProposalType, +} + +/// An add or remove action for PGF +#[derive( + Debug, + Clone, + Hash, + PartialEq, + Eq, + PartialOrd, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, +)] +pub enum AddRemove { + /// Add + Add(T), + /// Remove + Remove(T), +} + +/// The target of a PGF payment +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, + Ord, + Eq, + PartialOrd, +)] +pub struct PGFTarget { + /// The target address + pub target: Address, + /// The amount of token to fund the target address + pub amount: Amount, +} + +/// The actions that a PGF Steward can propose to execute +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, +)] +pub enum PGFAction { + /// A continuous payment + Continuous(AddRemove), + /// A retro payment + Retro(PGFTarget), +} + +/// The type of a Proposal +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, +)] +pub enum ProposalType { + /// Default governance proposal with the optional wasm code + Default(Option), + /// PGF stewards proposal + PGFSteward(HashSet>), + /// PGF funding proposal + PGFPayment(Vec), +} + +impl ProposalType { + /// Check if the proposal type is default + pub fn is_default(&self) -> bool { + matches!(self, ProposalType::Default(_)) + } +} + +impl Display for ProposalType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProposalType::Default(_) => write!(f, "Default"), + ProposalType::PGFSteward(_) => write!(f, "Pgf steward"), + ProposalType::PGFPayment(_) => write!(f, "Pgf funding"), + } + } +} + +impl TryFrom for AddRemove
{ + type Error = ProposalTypeError; + + fn try_from(value: PgfSteward) -> Result { + match value.action { + PgfAction::Add => Ok(AddRemove::Add(value.address)), + PgfAction::Remove => Ok(AddRemove::Remove(value.address)), + } + } +} + +impl TryFrom for PGFAction { + type Error = ProposalTypeError; + + fn try_from(value: PgfContinous) -> Result { + match value.action { + PgfAction::Add => { + Ok(PGFAction::Continuous(AddRemove::Add(PGFTarget { + target: value.target.address, + amount: value.target.amount, + }))) + } + PgfAction::Remove => { + Ok(PGFAction::Continuous(AddRemove::Remove(PGFTarget { + target: value.target.address, + amount: value.target.amount, + }))) + } + } + } +} + +impl TryFrom for PGFAction { + type Error = ProposalTypeError; + + fn try_from(value: PgfRetro) -> Result { + Ok(PGFAction::Retro(PGFTarget { + target: value.target.address, + amount: value.target.amount, + })) + } +} + +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] +/// Proposal rappresentation when fetched from the storage +pub struct StorageProposal { + /// The proposal id + pub id: u64, + /// The proposal content + pub content: BTreeMap, + /// The proposal author address + pub author: Address, + /// The proposal type + pub r#type: ProposalType, + /// The epoch from which voting is allowed + pub voting_start_epoch: Epoch, + /// The epoch from which voting is stopped + pub voting_end_epoch: Epoch, + /// The epoch from which this changes are executed + pub grace_epoch: Epoch, +} + +impl StorageProposal { + /// Check if the proposal can be voted + pub fn can_be_voted( + &self, + current_epoch: Epoch, + is_validator: bool, + ) -> bool { + if is_validator { + self.voting_start_epoch < self.voting_end_epoch + && current_epoch * 3 + <= self.voting_start_epoch + self.voting_end_epoch * 2 + } else { + let valid_start_epoch = current_epoch >= self.voting_start_epoch; + let valid_end_epoch = current_epoch <= self.voting_end_epoch; + valid_start_epoch && valid_end_epoch + } + } + + /// Return the type of tally for the proposal + pub fn get_tally_type(&self) -> TallyType { + TallyType::from(self.r#type.clone()) + } + + /// Return the status of a proposal + pub fn get_status(&self, current_epoch: Epoch) -> ProposalStatus { + if self.voting_start_epoch > self.voting_end_epoch { + ProposalStatus::Pending + } else if self.voting_start_epoch <= current_epoch + && current_epoch <= self.voting_end_epoch + { + ProposalStatus::OnGoing + } else { + ProposalStatus::Ended + } + } + + /// Serialize a proposal to string + pub fn to_string_with_status(&self, current_epoch: Epoch) -> String { + format!( + "Proposal Id: {} + {:2}Type: {} + {:2}Author: {} + {:2}Content: {:?} + {:2}Start Epoch: {} + {:2}End Epoch: {} + {:2}Grace Epoch: {} + {:2}Status: {} + ", + self.id, + "", + self.r#type, + "", + self.author, + "", + self.content, + "", + self.voting_start_epoch, + "", + self.voting_end_epoch, + "", + self.grace_epoch, + "", + self.get_status(current_epoch) + ) + } +} + +impl Display for StorageProposal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Proposal Id: {} + {:2}Type: {} + {:2}Author: {} + {:2}Start Epoch: {} + {:2}End Epoch: {} + {:2}Grace Epoch: {} + ", + self.id, + "", + self.r#type, + "", + self.author, + "", + self.voting_start_epoch, + "", + self.voting_end_epoch, + "", + self.grace_epoch + ) + } +} diff --git a/core/src/ledger/governance/storage/vote.rs b/core/src/ledger/governance/storage/vote.rs new file mode 100644 index 00000000000..3ba8ec2ae25 --- /dev/null +++ b/core/src/ledger/governance/storage/vote.rs @@ -0,0 +1,127 @@ +use std::fmt::Display; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +use super::super::cli::onchain::ProposalVote; +use super::proposal::ProposalType; + +/// The type of a governance vote with the optional associated Memo +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Eq, + Serialize, + Deserialize, +)] +pub enum VoteType { + /// A default vote without Memo + Default, + /// A vote for the PGF stewards + PGFSteward, + /// A vote for a PGF payment proposal + PGFPayment, +} + +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Eq, + Serialize, + Deserialize, +)] +/// The vote for a proposal +pub enum StorageProposalVote { + /// Yes + Yay(VoteType), + /// No + Nay, +} + +impl StorageProposalVote { + /// Check if a vote is yay + pub fn is_yay(&self) -> bool { + matches!(self, StorageProposalVote::Yay(_)) + } + + /// Check if vote is of type default + pub fn is_default_vote(&self) -> bool { + matches!( + self, + StorageProposalVote::Yay(VoteType::Default) + | StorageProposalVote::Nay + ) + } + + /// Check if a vote is compatible with a proposal + pub fn is_compatible(&self, proposal_type: &ProposalType) -> bool { + match self { + StorageProposalVote::Yay(vote_type) => proposal_type.eq(vote_type), + StorageProposalVote::Nay => true, + } + } + + /// Create a new vote + pub fn build( + proposal_vote: &ProposalVote, + proposal_type: &ProposalType, + ) -> Option { + match (proposal_vote, proposal_type) { + (ProposalVote::Yay, ProposalType::Default(_)) => { + Some(StorageProposalVote::Yay(VoteType::Default)) + } + (ProposalVote::Yay, ProposalType::PGFSteward(_)) => { + Some(StorageProposalVote::Yay(VoteType::PGFSteward)) + } + (ProposalVote::Yay, ProposalType::PGFPayment(_)) => { + Some(StorageProposalVote::Yay(VoteType::PGFPayment)) + } + (ProposalVote::Nay, ProposalType::Default(_)) => { + Some(StorageProposalVote::Nay) + } + (ProposalVote::Nay, ProposalType::PGFSteward(_)) => { + Some(StorageProposalVote::Nay) + } + (ProposalVote::Nay, ProposalType::PGFPayment(_)) => { + Some(StorageProposalVote::Nay) + } + _ => None, + } + } +} + +impl Display for StorageProposalVote { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StorageProposalVote::Yay(vote_type) => match vote_type { + VoteType::Default + | VoteType::PGFSteward + | VoteType::PGFPayment => write!(f, "yay"), + }, + + StorageProposalVote::Nay => write!(f, "nay"), + } + } +} + +impl PartialEq for ProposalType { + fn eq(&self, other: &VoteType) -> bool { + match self { + Self::Default(_) => { + matches!(other, VoteType::Default) + } + Self::PGFSteward(_) => { + matches!(other, VoteType::PGFSteward) + } + Self::PGFPayment(_) => { + matches!(other, VoteType::PGFPayment) + } + } + } +} diff --git a/core/src/ledger/governance/utils.rs b/core/src/ledger/governance/utils.rs new file mode 100644 index 00000000000..30df25e3fb3 --- /dev/null +++ b/core/src/ledger/governance/utils.rs @@ -0,0 +1,291 @@ +use std::collections::HashMap; +use std::fmt::Display; + +use borsh::{BorshDeserialize, BorshSerialize}; + +use super::cli::offline::OfflineVote; +use super::storage::proposal::ProposalType; +use super::storage::vote::StorageProposalVote; +use crate::types::address::Address; +use crate::types::storage::Epoch; +use crate::types::token; + +/// Proposal status +pub enum ProposalStatus { + /// Pending proposal status + Pending, + /// Ongoing proposal status + OnGoing, + /// Ended proposal status + Ended, +} + +impl Display for ProposalStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProposalStatus::Pending => write!(f, "pending"), + ProposalStatus::OnGoing => write!(f, "on-going"), + ProposalStatus::Ended => write!(f, "ended"), + } + } +} + +/// Alias to comulate voting power +pub type VotePower = token::Amount; + +/// Structure rappresenting a proposal vote +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] +pub struct Vote { + /// Field holding the address of the validator + pub validator: Address, + /// Field holding the address of the delegator + pub delegator: Address, + /// Field holding vote data + pub data: StorageProposalVote, +} + +impl Vote { + /// Check if a vote is from a validator + pub fn is_validator(&self) -> bool { + self.validator.eq(&self.delegator) + } +} + +/// Rappresent a tally type +pub enum TallyType { + /// Rappresent a tally type for proposal requiring 2/3 of the votes + TwoThird, + /// Rappresent a tally type for proposal requiring 1/3 of the votes + OneThird, + /// Rappresent a tally type for proposal requiring less than 1/3 of the + /// votes to be nay + LessOneThirdNay, +} + +impl From for TallyType { + fn from(proposal_type: ProposalType) -> Self { + match proposal_type { + ProposalType::Default(_) => TallyType::TwoThird, + ProposalType::PGFSteward(_) => TallyType::TwoThird, + ProposalType::PGFPayment(_) => TallyType::LessOneThirdNay, + } + } +} + +/// The result of a proposal +pub enum TallyResult { + /// Proposal was accepted with the associated value + Passed, + /// Proposal was rejected + Rejected, +} + +impl Display for TallyResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TallyResult::Passed => write!(f, "passed"), + TallyResult::Rejected => write!(f, "rejected"), + } + } +} + +impl TallyResult { + /// Create a new tally result + pub fn new( + tally_type: &TallyType, + yay_voting_power: VotePower, + nay_voting_power: VotePower, + total_voting_power: VotePower, + ) -> Self { + let passed = match tally_type { + TallyType::TwoThird => { + let at_least_two_third_voted = yay_voting_power + + nay_voting_power + >= total_voting_power / 3 * 2; + let at_last_half_voted_yay = + yay_voting_power > nay_voting_power; + at_least_two_third_voted && at_last_half_voted_yay + } + TallyType::OneThird => { + let at_least_two_third_voted = yay_voting_power + + nay_voting_power + >= total_voting_power / 3; + let at_last_half_voted_yay = + yay_voting_power > nay_voting_power; + at_least_two_third_voted && at_last_half_voted_yay + } + TallyType::LessOneThirdNay => { + nay_voting_power <= total_voting_power / 3 + } + }; + + if passed { Self::Passed } else { Self::Rejected } + } +} + +/// The result with votes of a proposal +pub struct ProposalResult { + /// The result of a proposal + pub result: TallyResult, + /// The total voting power during the proposal tally + pub total_voting_power: VotePower, + /// The total voting power from yay votes + pub total_yay_power: VotePower, + /// The total voting power from nay votes (unused at the moment) + pub total_nay_power: VotePower, +} + +impl Display for ProposalResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let percentage = self + .total_yay_power + .checked_div(self.total_voting_power) + .unwrap_or_default(); + + write!( + f, + "{} with {} yay votes and {} nay votes ({:.2}%)", + self.result, + self.total_yay_power.to_string_native(), + self.total_nay_power.to_string_native(), + percentage + .checked_mul(token::Amount::from_u64(100)) + .unwrap_or_default() + .to_string_native() + ) + } +} + +impl ProposalResult { + /// Return true if two third of total voting power voted nay + pub fn two_third_nay(&self) -> bool { + self.total_nay_power >= (self.total_voting_power / 3) * 2 + } +} + +/// /// General rappresentation of a vote +pub enum TallyVote { + /// Rappresent a vote for a proposal onchain + OnChain(StorageProposalVote), + /// Rappresent a vote for a proposal offline + Offline(OfflineVote), +} + +impl From for TallyVote { + fn from(vote: StorageProposalVote) -> Self { + Self::OnChain(vote) + } +} + +impl From for TallyVote { + fn from(vote: OfflineVote) -> Self { + Self::Offline(vote) + } +} + +impl TallyVote { + /// Check if a vote is yay + pub fn is_yay(&self) -> bool { + match self { + TallyVote::OnChain(vote) => vote.is_yay(), + TallyVote::Offline(vote) => vote.is_yay(), + } + } + + /// Check if two votes are equal + pub fn is_same_side(&self, other: &TallyVote) -> bool { + let both_yay = self.is_yay() && other.is_yay(); + let both_nay = !self.is_yay() && !other.is_yay(); + + both_yay || !both_nay + } +} + +/// Proposal structure holding votes information necessary to compute the +/// outcome +pub struct ProposalVotes { + /// Map from validator address to vote + pub validators_vote: HashMap, + /// Map from validator to their voting power + pub validator_voting_power: HashMap, + /// Map from delegation address to their vote + pub delegators_vote: HashMap, + /// Map from delegator address to the corresponding validator voting power + pub delegator_voting_power: HashMap>, +} + +/// Compute the result of a proposal +pub fn compute_proposal_result( + votes: ProposalVotes, + total_voting_power: VotePower, + tally_at: TallyType, +) -> ProposalResult { + let mut yay_voting_power = VotePower::default(); + let mut nay_voting_power = VotePower::default(); + + for (address, vote_power) in votes.validator_voting_power { + let vote_type = votes.validators_vote.get(&address); + if let Some(vote) = vote_type { + if vote.is_yay() { + yay_voting_power += vote_power; + } else { + nay_voting_power += vote_power; + } + } + } + + for (delegator, degalations) in votes.delegator_voting_power { + let delegator_vote = match votes.delegators_vote.get(&delegator) { + Some(vote) => vote, + None => continue, + }; + for (validator, voting_power) in degalations { + let validator_vote = votes.validators_vote.get(&validator); + if let Some(validator_vote) = validator_vote { + if !validator_vote.is_same_side(delegator_vote) { + if delegator_vote.is_yay() { + yay_voting_power += voting_power; + nay_voting_power -= voting_power; + } else { + nay_voting_power += voting_power; + yay_voting_power -= voting_power; + } + } + } else if delegator_vote.is_yay() { + yay_voting_power += voting_power; + } else { + nay_voting_power += voting_power; + } + } + } + + let tally_result = TallyResult::new( + &tally_at, + yay_voting_power, + nay_voting_power, + total_voting_power, + ); + + ProposalResult { + result: tally_result, + total_voting_power, + total_yay_power: yay_voting_power, + total_nay_power: nay_voting_power, + } +} + +/// Calculate the valid voting window for validator given a proposal epoch +/// details +pub fn is_valid_validator_voting_period( + current_epoch: Epoch, + voting_start_epoch: Epoch, + voting_end_epoch: Epoch, +) -> bool { + if voting_start_epoch >= voting_end_epoch { + false + } else { + let duration = voting_end_epoch - voting_start_epoch; + let two_third_duration = (duration / 3) * 2; + current_epoch <= voting_start_epoch + two_third_duration + } +} diff --git a/core/src/ledger/mod.rs b/core/src/ledger/mod.rs index 89b8105551c..9a84fbc1264 100644 --- a/core/src/ledger/mod.rs +++ b/core/src/ledger/mod.rs @@ -6,8 +6,8 @@ pub mod governance; #[cfg(any(feature = "abciplus", feature = "abcipp"))] pub mod ibc; pub mod parameters; +pub mod pgf; pub mod replay_protection; -pub mod slash_fund; pub mod storage; pub mod storage_api; pub mod testnet_pow; diff --git a/core/src/ledger/parameters/mod.rs b/core/src/ledger/parameters/mod.rs index c507e55d493..03d27fa2da7 100644 --- a/core/src/ledger/parameters/mod.rs +++ b/core/src/ledger/parameters/mod.rs @@ -46,6 +46,8 @@ pub struct Parameters { pub implicit_vp_code_hash: Hash, /// Expected number of epochs per year (read only) pub epochs_per_year: u64, + /// Maximum number of signature per transaction + pub max_signatures_per_transaction: u8, /// PoS gain p (read only) pub pos_gain_p: Dec, /// PoS gain d (read only) @@ -117,6 +119,7 @@ impl Parameters { tx_whitelist, implicit_vp_code_hash, epochs_per_year, + max_signatures_per_transaction, pos_gain_p, pos_gain_d, staked_ratio, @@ -168,6 +171,13 @@ impl Parameters { let epochs_per_year_key = storage::get_epochs_per_year_key(); storage.write(&epochs_per_year_key, epochs_per_year)?; + let max_signatures_per_transaction_key = + storage::get_max_signatures_per_transaction_key(); + storage.write( + &max_signatures_per_transaction_key, + max_signatures_per_transaction, + )?; + let pos_gain_p_key = storage::get_pos_gain_p_key(); storage.write(&pos_gain_p_key, pos_gain_p)?; @@ -197,6 +207,17 @@ impl Parameters { } } +/// Get the max signatures per transactio parameter +pub fn max_signatures_per_transaction( + storage: &S, +) -> storage_api::Result> +where + S: StorageRead, +{ + let key = storage::get_max_signatures_per_transaction_key(); + storage.read(&key) +} + /// Update the max_expected_time_per_block parameter in storage. Returns the /// parameters and gas cost. pub fn update_max_expected_time_per_block_parameter( @@ -340,6 +361,20 @@ where storage.write_bytes(&key, implicit_vp) } +/// Update the max signatures per transaction storage parameter +pub fn update_max_signature_per_tx( + storage: &mut S, + value: u8, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = storage::get_max_signatures_per_transaction_key(); + // Using `fn write_bytes` here, because implicit_vp doesn't need to be + // encoded, it's bytes already. + storage.write(&key, value) +} + /// Read the the epoch duration parameter from store pub fn read_epoch_duration_parameter( storage: &S, @@ -434,6 +469,15 @@ where .ok_or(ReadError::ParametersMissing) .into_storage_result()?; + // read the maximum signatures per transaction + let max_signatures_per_transaction_key = + storage::get_max_signatures_per_transaction_key(); + let value: Option = + storage.read(&max_signatures_per_transaction_key)?; + let max_signatures_per_transaction: u8 = value + .ok_or(ReadError::ParametersMissing) + .into_storage_result()?; + // read PoS gain P let pos_gain_p_key = storage::get_pos_gain_p_key(); let value = storage.read(&pos_gain_p_key)?; @@ -478,6 +522,7 @@ where tx_whitelist, implicit_vp_code_hash, epochs_per_year, + max_signatures_per_transaction, pos_gain_p, pos_gain_d, staked_ratio, diff --git a/core/src/ledger/parameters/storage.rs b/core/src/ledger/parameters/storage.rs index 94498e3578f..d32b1d221f9 100644 --- a/core/src/ledger/parameters/storage.rs +++ b/core/src/ledger/parameters/storage.rs @@ -44,6 +44,7 @@ struct Keys { max_proposal_bytes: &'static str, faucet_account: &'static str, wrapper_tx_fees: &'static str, + max_signatures_per_transaction: &'static str, } /// Returns if the key is a parameter key. @@ -183,3 +184,8 @@ pub fn get_faucet_account_key() -> Key { pub fn get_wrapper_tx_fees_key() -> Key { get_wrapper_tx_fees_key_at_addr(ADDRESS) } + +/// Storage key used for the max signatures per transaction key +pub fn get_max_signatures_per_transaction_key() -> Key { + get_max_signatures_per_transaction_key_at_addr(ADDRESS) +} diff --git a/core/src/ledger/slash_fund/mod.rs b/core/src/ledger/pgf/mod.rs similarity index 55% rename from core/src/ledger/slash_fund/mod.rs rename to core/src/ledger/pgf/mod.rs index 7a7d53963b0..a36621e49e1 100644 --- a/core/src/ledger/slash_fund/mod.rs +++ b/core/src/ledger/pgf/mod.rs @@ -1,8 +1,11 @@ -//! SlashFund library code +//! Pgf library code use crate::types::address::{Address, InternalAddress}; -/// Internal SlashFund address -pub const ADDRESS: Address = Address::Internal(InternalAddress::SlashFund); - +/// Pgf parameters +pub mod parameters; +/// Pgf storage pub mod storage; + +/// The Pgf internal address +pub const ADDRESS: Address = Address::Internal(InternalAddress::Pgf); diff --git a/core/src/ledger/pgf/parameters.rs b/core/src/ledger/pgf/parameters.rs new file mode 100644 index 00000000000..335ec566079 --- /dev/null +++ b/core/src/ledger/pgf/parameters.rs @@ -0,0 +1,70 @@ +use std::collections::BTreeSet; + +use borsh::{BorshDeserialize, BorshSerialize}; + +use super::storage::keys as pgf_storage; +use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::types::address::Address; +use crate::types::dec::Dec; + +#[derive( + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BorshSerialize, + BorshDeserialize, +)] +/// Pgf parameter structure +pub struct PgfParams { + /// The set of stewards + pub stewards: BTreeSet
, + /// The set of continous payments + pub payments: BTreeSet, + /// The pgf funding inflation rate + pub pgf_inflation_rate: Dec, + /// The pgf stewards inflation rate + pub stewards_inflation_rate: Dec, +} + +impl Default for PgfParams { + fn default() -> Self { + Self { + stewards: BTreeSet::default(), + payments: BTreeSet::default(), + pgf_inflation_rate: Dec::new(5, 2).unwrap(), + stewards_inflation_rate: Dec::new(1, 2).unwrap(), + } + } +} + +impl PgfParams { + /// Initialize governance parameters into storage + pub fn init_storage(&self, storage: &mut S) -> storage_api::Result<()> + where + S: StorageRead + StorageWrite, + { + let Self { + stewards, + payments, + pgf_inflation_rate, + stewards_inflation_rate, + } = self; + + let stewards_key = pgf_storage::get_stewards_key(); + storage.write(&stewards_key, stewards)?; + + let payments_key = pgf_storage::get_payments_key(); + storage.write(&payments_key, payments)?; + + let pgf_inflation_rate_key = pgf_storage::get_pgf_inflation_rate_key(); + storage.write(&pgf_inflation_rate_key, pgf_inflation_rate)?; + + let steward_inflation_rate_key = + pgf_storage::get_steward_inflation_rate_key(); + storage.write(&steward_inflation_rate_key, stewards_inflation_rate) + } +} diff --git a/core/src/ledger/pgf/storage/keys.rs b/core/src/ledger/pgf/storage/keys.rs new file mode 100644 index 00000000000..fa4875bb779 --- /dev/null +++ b/core/src/ledger/pgf/storage/keys.rs @@ -0,0 +1,67 @@ +use namada_macros::StorageKeys; + +use crate::ledger::pgf::ADDRESS; +use crate::types::address::Address; +use crate::types::storage::{DbKeySeg, Key, KeySeg}; + +/// Storage keys for pgf internal address. +#[derive(StorageKeys)] +struct Keys { + stewards: &'static str, + payments: &'static str, + pgf_inflation_rate: &'static str, + steward_inflation_rate: &'static str, +} + +/// Check if key is inside governance address space +pub fn is_pgf_key(key: &Key) -> bool { + matches!(&key.segments[0], DbKeySeg::AddressSeg(addr) if addr == &ADDRESS) +} + +/// Check if key is a steward key +pub fn is_stewards_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix)] if addr == &ADDRESS && prefix == Keys::VALUES.stewards) +} + +/// Check if key is a payments key +pub fn is_payments_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix)] if addr == &ADDRESS && prefix == Keys::VALUES.payments) +} + +/// Check if key is a pgf inflation rate key +pub fn is_pgf_inflation_rate_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix)] if addr == &ADDRESS && prefix == Keys::VALUES.pgf_inflation_rate) +} + +/// Check if key is a steward inflation rate key +pub fn is_steward_inflation_rate_key(key: &Key) -> bool { + matches!(&key.segments[..], [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(prefix)] if addr == &ADDRESS && prefix == Keys::VALUES.steward_inflation_rate) +} + +/// Get key for stewards key +pub fn get_stewards_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&Keys::VALUES.stewards.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Get key for payments key +pub fn get_payments_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&Keys::VALUES.payments.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Get key for inflation rate key +pub fn get_pgf_inflation_rate_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&Keys::VALUES.pgf_inflation_rate.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Get key for inflation rate key +pub fn get_steward_inflation_rate_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&Keys::VALUES.steward_inflation_rate.to_owned()) + .expect("Cannot obtain a storage key") +} diff --git a/core/src/ledger/pgf/storage/mod.rs b/core/src/ledger/pgf/storage/mod.rs new file mode 100644 index 00000000000..fb9d6f8896c --- /dev/null +++ b/core/src/ledger/pgf/storage/mod.rs @@ -0,0 +1,2 @@ +/// Pgf storage keys +pub mod keys; diff --git a/core/src/ledger/slash_fund/storage.rs b/core/src/ledger/slash_fund/storage.rs deleted file mode 100644 index 9c437da591c..00000000000 --- a/core/src/ledger/slash_fund/storage.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Slash fund storage - -use crate::types::storage::{DbKeySeg, Key}; - -/// Check if a key is a slash fund key -pub fn is_slash_fund_key(key: &Key) -> bool { - matches!(&key.segments[0], DbKeySeg::AddressSeg(addr) if addr == &super::ADDRESS) -} diff --git a/core/src/ledger/storage/mod.rs b/core/src/ledger/storage/mod.rs index 6509c02d342..94669a34f4d 100644 --- a/core/src/ledger/storage/mod.rs +++ b/core/src/ledger/storage/mod.rs @@ -1225,6 +1225,7 @@ mod tests { tx_whitelist: vec![], implicit_vp_code_hash: Hash::zero(), epochs_per_year: 100, + max_signatures_per_transaction: 15, pos_gain_p: Dec::new(1,1).expect("Cannot fail"), pos_gain_d: Dec::new(1,1).expect("Cannot fail"), staked_ratio: Dec::new(1,1).expect("Cannot fail"), diff --git a/core/src/ledger/storage_api/account.rs b/core/src/ledger/storage_api/account.rs new file mode 100644 index 00000000000..4896de635fe --- /dev/null +++ b/core/src/ledger/storage_api/account.rs @@ -0,0 +1,106 @@ +//! Cryptographic signature keys storage API + +use super::*; +use crate::types::account::AccountPublicKeysMap; +use crate::types::address::Address; +use crate::types::key::*; +use crate::types::storage::Key; + +/// Init the subspace of a new account +pub fn init_account_storage( + storage: &mut S, + owner: &Address, + public_keys: &[common::PublicKey], + threshold: u8, +) -> Result<()> +where + S: StorageWrite + StorageRead, +{ + for (index, public_key) in public_keys.iter().enumerate() { + let index = index as u8; + pks_handle(owner).insert(storage, index, public_key.clone())?; + } + let threshold_key = threshold_key(owner); + storage.write(&threshold_key, threshold) +} + +/// Get the threshold associated with an account +pub fn threshold(storage: &S, owner: &Address) -> Result> +where + S: StorageRead, +{ + let threshold_key = threshold_key(owner); + storage.read(&threshold_key) +} + +/// Get the public keys associated with an account +pub fn public_keys( + storage: &S, + owner: &Address, +) -> Result> +where + S: StorageRead, +{ + let public_keys = pks_handle(owner) + .iter(storage)? + .filter_map(|data| match data { + Ok((_index, public_key)) => Some(public_key), + Err(_) => None, + }) + .collect::>(); + + Ok(public_keys) +} + +/// Get the public key index map associated with an account +pub fn public_keys_index_map( + storage: &S, + owner: &Address, +) -> Result +where + S: StorageRead, +{ + let public_keys = public_keys(storage, owner)?; + + Ok(AccountPublicKeysMap::from_iter(public_keys)) +} + +/// Check if an account exists in storage +pub fn exists(storage: &S, owner: &Address) -> Result +where + S: StorageRead, +{ + match owner { + Address::Established(_) => { + let vp_key = Key::validity_predicate(owner); + storage.has_key(&vp_key) + } + Address::Implicit(_) | Address::Internal(_) => Ok(true), + } +} + +/// Set public key at specific index +pub fn set_public_key_at( + storage: &mut S, + owner: &Address, + public_key: &common::PublicKey, + index: u8, +) -> Result<()> +where + S: StorageWrite + StorageRead, +{ + pks_handle(owner).insert(storage, index, public_key.clone())?; + Ok(()) +} + +/// Clear the public keys account subtorage space +pub fn clear_public_keys(storage: &mut S, owner: &Address) -> Result<()> +where + S: StorageWrite + StorageRead, +{ + let total_pks = pks_handle(owner).len(storage)?; + for index in 0..total_pks as u8 { + pks_handle(owner).remove(storage, &index)?; + } + Ok(()) +} diff --git a/core/src/ledger/storage_api/governance.rs b/core/src/ledger/storage_api/governance.rs index c2316bffa74..1d4d3f767ef 100644 --- a/core/src/ledger/storage_api/governance.rs +++ b/core/src/ledger/storage_api/governance.rs @@ -1,12 +1,25 @@ //! Governance +use std::collections::BTreeMap; + +use borsh::BorshDeserialize; + use super::token; -use crate::ledger::governance::{storage, ADDRESS as governance_address}; +use crate::ledger::governance::storage::keys as governance_keys; +use crate::ledger::governance::storage::proposal::{ + ProposalType, StorageProposal, +}; +use crate::ledger::governance::storage::vote::StorageProposalVote; +use crate::ledger::governance::utils::Vote; +use crate::ledger::governance::ADDRESS as governance_address; use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::types::address::Address; +use crate::types::storage::Epoch; use crate::types::transaction::governance::{ - InitProposalData, ProposalType, VoteProposalData, + InitProposalData, VoteProposalData, }; +/// A proposal creation transaction. /// A proposal creation transaction. pub fn init_proposal( storage: &mut S, @@ -17,25 +30,26 @@ pub fn init_proposal( where S: StorageRead + StorageWrite, { - let counter_key = storage::get_counter_key(); + let counter_key = governance_keys::get_counter_key(); let proposal_id = if let Some(id) = data.id { id } else { storage.read(&counter_key)?.unwrap() }; - let content_key = storage::get_content_key(proposal_id); + let content_key = governance_keys::get_content_key(proposal_id); storage.write_bytes(&content_key, content)?; - let author_key = storage::get_author_key(proposal_id); + let author_key = governance_keys::get_author_key(proposal_id); storage.write(&author_key, data.author.clone())?; - let proposal_type_key = storage::get_proposal_type_key(proposal_id); + let proposal_type_key = governance_keys::get_proposal_type_key(proposal_id); match data.r#type { ProposalType::Default(Some(_)) => { // Remove wasm code and write it under a different subkey storage.write(&proposal_type_key, ProposalType::Default(None))?; - let proposal_code_key = storage::get_proposal_code_key(proposal_id); + let proposal_code_key = + governance_keys::get_proposal_code_key(proposal_id); let proposal_code = code.clone().ok_or( storage_api::Error::new_const("Missing proposal code"), )?; @@ -45,17 +59,19 @@ where } let voting_start_epoch_key = - storage::get_voting_start_epoch_key(proposal_id); + governance_keys::get_voting_start_epoch_key(proposal_id); storage.write(&voting_start_epoch_key, data.voting_start_epoch)?; - let voting_end_epoch_key = storage::get_voting_end_epoch_key(proposal_id); + let voting_end_epoch_key = + governance_keys::get_voting_end_epoch_key(proposal_id); storage.write(&voting_end_epoch_key, data.voting_end_epoch)?; - let grace_epoch_key = storage::get_grace_epoch_key(proposal_id); + let grace_epoch_key = governance_keys::get_grace_epoch_key(proposal_id); storage.write(&grace_epoch_key, data.grace_epoch)?; if let ProposalType::Default(Some(_)) = data.r#type { - let proposal_code_key = storage::get_proposal_code_key(proposal_id); + let proposal_code_key = + governance_keys::get_proposal_code_key(proposal_id); let proposal_code = code.ok_or(storage_api::Error::new_const("Missing proposal code"))?; storage.write_bytes(&proposal_code_key, proposal_code)?; @@ -63,16 +79,19 @@ where storage.write(&counter_key, proposal_id + 1)?; - let min_proposal_funds_key = storage::get_min_proposal_fund_key(); + let min_proposal_funds_key = governance_keys::get_min_proposal_fund_key(); let min_proposal_funds: token::Amount = storage.read(&min_proposal_funds_key)?.unwrap(); - let funds_key = storage::get_funds_key(proposal_id); + let funds_key = governance_keys::get_funds_key(proposal_id); storage.write(&funds_key, min_proposal_funds)?; // this key must always be written for each proposal let committing_proposals_key = - storage::get_committing_proposals_key(proposal_id, data.grace_epoch.0); + governance_keys::get_committing_proposals_key( + proposal_id, + data.grace_epoch.0, + ); storage.write(&committing_proposals_key, ())?; token::transfer( @@ -93,7 +112,7 @@ where S: StorageRead + StorageWrite, { for delegation in data.delegations { - let vote_key = storage::get_vote_proposal_key( + let vote_key = governance_keys::get_vote_proposal_key( data.id, data.voter.clone(), delegation, @@ -102,3 +121,99 @@ where } Ok(()) } + +/// Read a proposal by id from storage +pub fn get_proposal_by_id( + storage: &S, + id: u64, +) -> storage_api::Result> +where + S: StorageRead, +{ + let author_key = governance_keys::get_author_key(id); + let content = governance_keys::get_content_key(id); + let start_epoch_key = governance_keys::get_voting_start_epoch_key(id); + let end_epoch_key = governance_keys::get_voting_end_epoch_key(id); + let grace_epoch_key = governance_keys::get_grace_epoch_key(id); + let proposal_type_key = governance_keys::get_proposal_type_key(id); + + let author: Option
= storage.read(&author_key)?; + let content: Option> = storage.read(&content)?; + let voting_start_epoch: Option = storage.read(&start_epoch_key)?; + let voting_end_epoch: Option = storage.read(&end_epoch_key)?; + let grace_epoch: Option = storage.read(&grace_epoch_key)?; + let proposal_type: Option = + storage.read(&proposal_type_key)?; + + let proposal = proposal_type.map(|proposal_type| StorageProposal { + id, + content: content.unwrap(), + author: author.unwrap(), + r#type: proposal_type, + voting_start_epoch: voting_start_epoch.unwrap(), + voting_end_epoch: voting_end_epoch.unwrap(), + grace_epoch: grace_epoch.unwrap(), + }); + + Ok(proposal) +} + +/// Query all the votes for a proposal_id +pub fn get_proposal_votes( + storage: &S, + proposal_id: u64, +) -> storage_api::Result> +where + S: storage_api::StorageRead, +{ + let vote_prefix_key = + governance_keys::get_proposal_vote_prefix_key(proposal_id); + let vote_iter = storage_api::iter_prefix::( + storage, + &vote_prefix_key, + )?; + + let votes = vote_iter + .filter_map(|vote_result| { + if let Ok((vote_key, vote)) = vote_result { + let voter_address = + governance_keys::get_voter_address(&vote_key); + let delegator_address = + governance_keys::get_vote_delegation_address(&vote_key); + match (voter_address, delegator_address) { + (Some(delegator_address), Some(validator_address)) => { + Some(Vote { + validator: validator_address.to_owned(), + delegator: delegator_address.to_owned(), + data: vote, + }) + } + _ => None, + } + } else { + None + } + }) + .collect::>(); + + Ok(votes) +} + +/// Check if an accepted proposal is being executed +pub fn is_proposal_accepted( + storage: &S, + tx_data: &[u8], +) -> storage_api::Result +where + S: storage_api::StorageRead, +{ + let proposal_id = u64::try_from_slice(tx_data).ok(); + match proposal_id { + Some(id) => { + let proposal_execution_key = + governance_keys::get_proposal_execution_key(id); + storage.has_key(&proposal_execution_key) + } + None => Ok(false), + } +} diff --git a/core/src/ledger/storage_api/key.rs b/core/src/ledger/storage_api/key.rs index 6e3eba64aa8..69bdd507203 100644 --- a/core/src/ledger/storage_api/key.rs +++ b/core/src/ledger/storage_api/key.rs @@ -4,23 +4,17 @@ use super::*; use crate::types::address::Address; use crate::types::key::*; -/// Get the public key associated with the given address. Returns `Ok(None)` if -/// not found. -pub fn get(storage: &S, owner: &Address) -> Result> -where - S: StorageRead, -{ - let key = pk_key(owner); - storage.read(&key) -} - /// Reveal a PK of an implicit account - the PK is written into the storage /// of the address derived from the PK. -pub fn reveal_pk(storage: &mut S, pk: &common::PublicKey) -> Result<()> +pub fn reveal_pk( + storage: &mut S, + public_key: &common::PublicKey, +) -> Result<()> where - S: StorageWrite, + S: StorageWrite + StorageRead, { - let addr: Address = pk.into(); - let key = pk_key(&addr); - storage.write(&key, pk) + let owner: Address = public_key.into(); + pks_handle(&owner).insert(storage, 0, public_key.clone())?; + + Ok(()) } diff --git a/core/src/ledger/storage_api/mod.rs b/core/src/ledger/storage_api/mod.rs index 451996d0e58..1108c44e3d0 100644 --- a/core/src/ledger/storage_api/mod.rs +++ b/core/src/ledger/storage_api/mod.rs @@ -1,10 +1,12 @@ //! The common storage read trait is implemented in the storage, client RPC, tx //! and VPs (both native and WASM). +pub mod account; pub mod collections; mod error; pub mod governance; pub mod key; +pub mod pgf; pub mod token; pub mod validation; diff --git a/core/src/ledger/storage_api/pgf.rs b/core/src/ledger/storage_api/pgf.rs new file mode 100644 index 00000000000..8dfafee0fb7 --- /dev/null +++ b/core/src/ledger/storage_api/pgf.rs @@ -0,0 +1,30 @@ +//! Pgf + +use std::collections::BTreeSet; + +use crate::ledger::governance::storage::proposal::PGFTarget; +use crate::ledger::pgf::storage::keys as pgf_keys; +use crate::ledger::storage_api::{self}; +use crate::types::address::Address; + +/// Query the current pgf steward set +pub fn get_stewards(storage: &S) -> storage_api::Result> +where + S: storage_api::StorageRead, +{ + let stewards_key = pgf_keys::get_stewards_key(); + let stewards: Option> = storage.read(&stewards_key)?; + + Ok(stewards.unwrap_or_default()) +} + +/// Query the current pgf continous payments +pub fn get_payments(storage: &S) -> storage_api::Result> +where + S: storage_api::StorageRead, +{ + let payment_key = pgf_keys::get_payments_key(); + let payments: Option> = storage.read(&payment_key)?; + + Ok(payments.unwrap_or_default()) +} diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs index 1985d8325c7..95987d6a83f 100644 --- a/core/src/ledger/storage_api/token.rs +++ b/core/src/ledger/storage_api/token.rs @@ -134,3 +134,35 @@ where storage.write(&balance_key, new_balance)?; storage.write(&total_supply_key, new_supply) } + +/// Burn an amount of token for a specific address. +pub fn burn( + storage: &mut S, + token: &Address, + source: &Address, + amount: token::Amount, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let key = token::balance_key(token, source); + let balance = read_balance(storage, token, source)?; + + let amount_to_burn = match balance.checked_sub(amount) { + Some(new_balance) => { + storage.write(&key, new_balance)?; + amount + } + None => { + storage.write(&key, token::Amount::default())?; + balance + } + }; + + let total_supply = read_total_supply(&*storage, source)?; + let new_total_supply = + total_supply.checked_sub(amount_to_burn).unwrap_or_default(); + + let total_supply_key = token::minted_balance_key(token); + storage.write(&total_supply_key, new_total_supply) +} diff --git a/core/src/proto/mod.rs b/core/src/proto/mod.rs index 27ae37ff695..e8411a41d2d 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, Data, Dkg, Error, Header, MaspBuilder, Section, Signable, - SignableEthMessage, Signature, Signed, Tx, TxError, + Code, Commitment, Data, Dkg, Error, Header, MaspBuilder, MultiSignature, + Section, Signable, SignableEthMessage, Signature, SignatureIndex, Signed, + Tx, TxError, }; #[cfg(test)] diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index 610d4536544..ec1e9db5f46 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; -use std::collections::{HashMap, HashSet}; +use std::cmp::Ordering; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::convert::TryFrom; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; @@ -10,6 +11,7 @@ use ark_ec::AffineCurve; use ark_ec::PairingEngine; use borsh::schema::{Declaration, Definition}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use data_encoding::HEXUPPER; use masp_primitives::transaction::builder::Builder; use masp_primitives::transaction::components::sapling::builder::SaplingMetadata; use masp_primitives::transaction::Transaction; @@ -24,6 +26,7 @@ use super::generated::types; use crate::ledger::storage::{KeccakHasher, Sha256Hasher, StorageHasher}; #[cfg(any(feature = "tendermint", feature = "tendermint-abcipp"))] use crate::tendermint_proto::abci::ResponseDeliverTx; +use crate::types::account::AccountPublicKeysMap; use crate::types::address::Address; use crate::types::chain::ChainId; use crate::types::keccak::{keccak_hash, KeccakHash}; @@ -49,6 +52,8 @@ pub enum Error { TxDecodingError(prost::DecodeError), #[error("Error deserializing transaction field bytes: {0}")] TxDeserializingError(std::io::Error), + #[error("Error deserializing transaction")] + OfflineTxDeserializationError, #[error("Error decoding an DkgGossipMessage from bytes: {0}")] DkgDecodingError(prost::DecodeError), #[error("Dkg is empty")] @@ -57,6 +62,12 @@ pub enum Error { NoTimestampError, #[error("Timestamp is invalid: {0}")] InvalidTimestamp(prost_types::TimestampError), + #[error("The section signature is invalid: {0}")] + InvalidSectionSignature(String), + #[error("Couldn't serialize transaction from JSON at {0}")] + InvalidJSONDeserialization(String), + #[error("The wrapper signature is invalid.")] + InvalidWrapperSignature, } pub type Result = std::result::Result; @@ -360,6 +371,164 @@ impl Code { } } +#[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, + Eq, + PartialEq, +)] +pub struct SignatureIndex { + pub signature: common::Signature, + pub index: u8, +} + +impl SignatureIndex { + pub fn from_single_signature(signature: common::Signature) -> Self { + Self { + signature, + index: 0, + } + } + + pub fn to_vec(&self) -> Vec { + vec![self.clone()] + } + + pub fn verify( + &self, + public_key_index_map: &AccountPublicKeysMap, + data: &impl SignableBytes, + ) -> std::result::Result<(), VerifySigError> { + let public_key = + public_key_index_map.get_public_key_from_index(self.index); + if let Some(public_key) = public_key { + common::SigScheme::verify_signature( + &public_key, + data, + &self.signature, + ) + } else { + Err(VerifySigError::MissingData) + } + } + + pub fn serialize(&self) -> String { + let signature_bytes = + self.try_to_vec().expect("Signature should be serializable"); + HEXUPPER.encode(&signature_bytes) + } + + pub fn deserialize(data: &[u8]) -> Result { + if let Ok(hex) = serde_json::from_slice::(data) { + match HEXUPPER.decode(hex.as_bytes()) { + Ok(bytes) => Self::try_from_slice(&bytes) + .map_err(Error::TxDeserializingError), + Err(_) => Err(Error::OfflineTxDeserializationError), + } + } else { + Err(Error::OfflineTxDeserializationError) + } + } +} + +impl Ord for SignatureIndex { + fn cmp(&self, other: &Self) -> Ordering { + self.index.cmp(&other.index) + } +} + +impl PartialOrd for SignatureIndex { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// A section representing a multisig over another section +#[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, +)] +pub struct MultiSignature { + /// The hash of the section being signed + pub targets: Vec, + /// The signature over the above hash + pub signatures: BTreeSet, +} + +impl MultiSignature { + /// Sign the given section hash with the given key and return a section + pub fn new( + targets: Vec, + secret_keys: &[common::SecretKey], + public_keys_index_map: &AccountPublicKeysMap, + ) -> Self { + let target = Self { + targets: targets.clone(), + signatures: BTreeSet::new(), + } + .get_hash(); + + let signatures_public_keys_map = + secret_keys.iter().map(|secret_key: &common::SecretKey| { + let signature = common::SigScheme::sign(secret_key, target); + let public_key = secret_key.ref_to(); + (public_key, signature) + }); + + let signatures = signatures_public_keys_map + .filter_map(|(public_key, signature)| { + let public_key_index = public_keys_index_map + .get_index_from_public_key(&public_key); + public_key_index + .map(|index| SignatureIndex { signature, index }) + }) + .collect::>(); + + Self { + targets, + signatures, + } + } + + pub fn total_signatures(&self) -> u8 { + self.signatures.len() as u8 + } + + /// Hash this signature section + pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { + hasher.update( + self.try_to_vec() + .expect("unable to serialize multisignature section"), + ); + hasher + } + + /// Get the hash of this section + pub fn get_hash(&self) -> crate::types::hash::Hash { + crate::types::hash::Hash( + self.hash(&mut Sha256::new()).finalize_reset().into(), + ) + } + + pub fn get_raw_hash(&self) -> crate::types::hash::Hash { + Self { + signatures: BTreeSet::new(), + ..self.clone() + } + .get_hash() + } +} + /// A section representing the signature over another section #[derive( Clone, @@ -371,26 +540,19 @@ impl Code { Deserialize, )] pub struct Signature { - /// Additional random data - salt: [u8; 8], /// The hash of the section being signed targets: Vec, - /// The public key to verify the below signature - pub_key: common::PublicKey, /// The signature over the above hashes pub signature: Option, } impl Signature { - /// Sign the given section hash with the given key and return a section pub fn new( targets: Vec, sec_key: &common::SecretKey, ) -> Self { let mut sec = Self { - salt: DateTimeUtc::now().0.timestamp_millis().to_le_bytes(), targets, - pub_key: sec_key.ref_to(), signature: None, }; sec.signature = Some(common::SigScheme::sign(sec_key, sec.get_hash())); @@ -414,11 +576,14 @@ impl Signature { } /// Verify that the signature contained in this section is valid - pub fn verify_signature(&self) -> std::result::Result<(), VerifySigError> { + pub fn verify_signature( + &self, + public_key: &common::PublicKey, + ) -> std::result::Result<(), VerifySigError> { let signature = self.signature.as_ref().ok_or(VerifySigError::MissingData)?; common::SigScheme::verify_signature( - &self.pub_key, + public_key, &Self { signature: None, ..self.clone() @@ -730,6 +895,8 @@ pub enum Section { /// Transaction code. Sending to hardware wallets optional Code(Code), /// A transaction signature. Often produced by hardware wallets + SectionSignature(MultiSignature), + /// A transaction header/protocol signature Signature(Signature), /// Ciphertext obtained by encrypting arbitrary transaction sections Ciphertext(Ciphertext), @@ -759,7 +926,8 @@ impl Section { Self::Data(data) => data.hash(hasher), Self::ExtraData(extra) => extra.hash(hasher), Self::Code(code) => code.hash(hasher), - Self::Signature(sig) => sig.hash(hasher), + Self::Signature(signature) => signature.hash(hasher), + Self::SectionSignature(signatures) => signatures.hash(hasher), Self::Ciphertext(ct) => ct.hash(hasher), Self::MaspBuilder(mb) => mb.hash(hasher), Self::MaspTx(tx) => { @@ -831,6 +999,15 @@ impl Section { } } + /// Extract the section signature from this section if possible + pub fn section_signature(&self) -> Option { + if let Self::SectionSignature(data) = self { + Some(data.clone()) + } else { + None + } + } + /// Extract the ciphertext from this section if possible pub fn ciphertext(&self) -> Option { if let Self::Ciphertext(data) = self { @@ -987,6 +1164,27 @@ impl Tx { } } + /// Serialize tx to hex string + pub fn serialize(&self) -> String { + let tx_bytes = self + .try_to_vec() + .expect("Transation should be serializable"); + HEXUPPER.encode(&tx_bytes) + } + + // Deserialize from hex encoding + pub fn deserialize(data: &[u8]) -> Result { + if let Ok(hex) = serde_json::from_slice::(data) { + match HEXUPPER.decode(hex.as_bytes()) { + Ok(bytes) => Tx::try_from_slice(&bytes) + .map_err(Error::TxDeserializingError), + Err(_) => Err(Error::OfflineTxDeserializationError), + } + } else { + Err(Error::OfflineTxDeserializationError) + } + } + /// Get the transaction header pub fn header(&self) -> Header { self.header.clone() @@ -1105,36 +1303,113 @@ impl Tx { bytes } + /// Get the inner section hashes + pub fn inner_section_targets(&self) -> Vec { + self.sections + .iter() + .filter_map(|section| match section { + Section::Data(_) | Section::Code(_) => Some(section.get_hash()), + _ => None, + }) + .collect() + } + + /// Verify that the section with the given hash has been signed by the given + /// public key + pub fn verify_section_signatures( + &self, + hashes: &[crate::types::hash::Hash], + public_keys_index_map: AccountPublicKeysMap, + threshold: u8, + max_signatures: Option, + ) -> std::result::Result<(), Error> { + let max_signatures = max_signatures.unwrap_or(u8::MAX); + let mut valid_signatures = 0; + + for section in &self.sections { + if let Section::SectionSignature(signatures) = section { + if !hashes.iter().all(|x| { + signatures.targets.contains(x) || section.get_hash() == *x + }) { + return Err(Error::InvalidSectionSignature( + "missing target hash.".to_string(), + )); + } + + for target in &signatures.targets { + if self.get_section(target).is_none() { + return Err(Error::InvalidSectionSignature( + "Missing target section.".to_string(), + )); + } + } + + if signatures.total_signatures() > max_signatures { + return Err(Error::InvalidSectionSignature( + "too many signatures.".to_string(), + )); + } + + if signatures.total_signatures() < threshold { + return Err(Error::InvalidSectionSignature( + "too few signatures.".to_string(), + )); + } + + for signature_index in &signatures.signatures { + let is_valid_signature = signature_index + .verify( + &public_keys_index_map, + &signatures.get_raw_hash(), + ) + .is_ok(); + if is_valid_signature { + valid_signatures += 1; + } + if valid_signatures >= threshold { + return Ok(()); + } + } + } + } + Err(Error::InvalidSectionSignature( + "invalid signatures.".to_string(), + )) + } + /// Verify that the sections with the given hashes have been signed together /// by the given public key. I.e. this function looks for one signature that /// covers over the given slice of hashes. pub fn verify_signature( &self, - pk: &common::PublicKey, + public_key: &common::PublicKey, hashes: &[crate::types::hash::Hash], - ) -> std::result::Result<&Signature, VerifySigError> { + ) -> Result<&Signature> { for section in &self.sections { - if let Section::Signature(sig_sec) = section { - // Check that the signer is matched and that the hashes being + if let Section::Signature(signature) = section { + // Check that the hashes being // checked are a subset of those in this section - if sig_sec.pub_key == *pk - && hashes.iter().all(|x| { - sig_sec.targets.contains(x) || section.get_hash() == *x - }) - { + if hashes.iter().all(|x| { + signature.targets.contains(x) || section.get_hash() == *x + }) { // Ensure that all the sections the signature signs over are // present - for target in &sig_sec.targets { + for target in &signature.targets { if self.get_section(target).is_none() { - return Err(VerifySigError::MissingData); + return Err(Error::InvalidSectionSignature( + "Target section is missing.".to_string(), + )); } } // Finally verify that the signature itself is valid - return sig_sec.verify_signature().map(|_| sig_sec); + return signature + .verify_signature(public_key) + .map(|_| signature) + .map_err(|_| Error::InvalidWrapperSignature); } } } - Err(VerifySigError::MissingData) + Err(Error::InvalidWrapperSignature) } /// Validate any and all ciphertexts stored in this transaction @@ -1153,6 +1428,16 @@ impl Tx { valid } + pub fn compute_section_signature( + &self, + secret_keys: &[common::SecretKey], + public_keys_index_map: &AccountPublicKeysMap, + ) -> BTreeSet { + let targets = [*self.data_sechash(), *self.code_sechash()].to_vec(); + MultiSignature::new(targets, secret_keys, public_keys_index_map) + .signatures + } + /// Decrypt any and all ciphertexts stored in this transaction use the /// given decryption key #[cfg(feature = "ferveo-tpke")] diff --git a/core/src/types/account.rs b/core/src/types/account.rs new file mode 100644 index 00000000000..d66876ba37b --- /dev/null +++ b/core/src/types/account.rs @@ -0,0 +1,92 @@ +//! Helper structures to manage accounts + +use std::collections::HashMap; + +use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +use super::address::Address; +use super::key::common; + +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, +)] +/// Account data +pub struct Account { + /// The map between indexes and public keys for an account + pub public_keys_map: AccountPublicKeysMap, + /// The account signature threshold + pub threshold: u8, + /// The address corresponding to the account owner + pub address: Address, +} + +impl Account { + /// Retrive a public key from the index + pub fn get_public_key_from_index( + &self, + index: u8, + ) -> Option { + self.public_keys_map.get_public_key_from_index(index) + } + + /// Retrive the index of a public key + pub fn get_index_from_public_key( + &self, + public_key: &common::PublicKey, + ) -> Option { + self.public_keys_map.get_index_from_public_key(public_key) + } +} + +#[derive( + Debug, + Clone, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, + Default, +)] +/// Holds the public key map data as a bimap for efficient quering +pub struct AccountPublicKeysMap { + /// Hashmap from public key to index + pub pk_to_idx: HashMap, + /// Hashmap from index key to public key + pub idx_to_pk: HashMap, +} + +impl FromIterator for AccountPublicKeysMap { + fn from_iter>(iter: T) -> Self { + let mut pk_to_idx = HashMap::new(); + let mut idx_to_pk = HashMap::new(); + + for (index, public_key) in iter.into_iter().enumerate() { + pk_to_idx.insert(public_key.to_owned(), index as u8); + idx_to_pk.insert(index as u8, public_key.to_owned()); + } + + Self { + pk_to_idx, + idx_to_pk, + } + } +} + +impl AccountPublicKeysMap { + /// Retrive a public key from the index + pub fn get_public_key_from_index( + &self, + index: u8, + ) -> Option { + self.idx_to_pk.get(&index).cloned() + } + + /// Retrive the index of a public key + pub fn get_index_from_public_key( + &self, + public_key: &common::PublicKey, + ) -> Option { + self.pk_to_idx.get(public_key).cloned() + } +} diff --git a/core/src/types/address.rs b/core/src/types/address.rs index 1a6611a2f5d..0576511cec4 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -75,8 +75,6 @@ mod internal { "ano::Protocol Parameters "; pub const GOVERNANCE: &str = "ano::Governance "; - pub const SLASH_FUND: &str = - "ano::Slash Fund "; pub const IBC: &str = "ibc::Inter-Blockchain Communication "; pub const ETH_BRIDGE: &str = @@ -87,6 +85,8 @@ mod internal { "ano::Replay Protection "; pub const MULTITOKEN: &str = "ano::Multitoken "; + pub const PGF: &str = + "ano::Pgf "; } /// Fixed-length address strings prefix for established addresses. @@ -219,9 +219,6 @@ impl Address { InternalAddress::Governance => { internal::GOVERNANCE.to_string() } - InternalAddress::SlashFund => { - internal::SLASH_FUND.to_string() - } InternalAddress::Ibc => internal::IBC.to_string(), InternalAddress::IbcToken(hash) => { format!("{}::{}", PREFIX_IBC, hash) @@ -243,6 +240,7 @@ impl Address { InternalAddress::Multitoken => { internal::MULTITOKEN.to_string() } + InternalAddress::Pgf => internal::PGF.to_string(), }; debug_assert_eq!(string.len(), FIXED_LEN_STRING_BYTES); string @@ -304,9 +302,6 @@ impl Address { internal::GOVERNANCE => { Ok(Address::Internal(InternalAddress::Governance)) } - internal::SLASH_FUND => { - Ok(Address::Internal(InternalAddress::SlashFund)) - } internal::ETH_BRIDGE => { Ok(Address::Internal(InternalAddress::EthBridge)) } @@ -319,6 +314,7 @@ impl Address { internal::MULTITOKEN => { Ok(Address::Internal(InternalAddress::Multitoken)) } + internal::PGF => Ok(Address::Internal(InternalAddress::Pgf)), _ => Err(Error::new( ErrorKind::InvalidData, "Invalid internal address", @@ -541,8 +537,6 @@ pub enum InternalAddress { IbcToken(String), /// Governance address Governance, - /// SlashFund address for governance - SlashFund, /// Bridge to Ethereum EthBridge, /// The pool of transactions to be relayed to Ethereum @@ -553,6 +547,8 @@ pub enum InternalAddress { ReplayProtection, /// Multitoken Multitoken, + /// Pgf + Pgf, } impl Display for InternalAddress { @@ -565,7 +561,6 @@ impl Display for InternalAddress { Self::PosSlashPool => "PosSlashPool".to_string(), Self::Parameters => "Parameters".to_string(), Self::Governance => "Governance".to_string(), - Self::SlashFund => "SlashFund".to_string(), Self::Ibc => "IBC".to_string(), Self::IbcToken(hash) => format!("IbcToken: {}", hash), Self::EthBridge => "EthBridge".to_string(), @@ -573,6 +568,7 @@ impl Display for InternalAddress { Self::Erc20(eth_addr) => format!("Erc20: {}", eth_addr), Self::ReplayProtection => "ReplayProtection".to_string(), Self::Multitoken => "Multitoken".to_string(), + Self::Pgf => "PublicGoodFundings".to_string(), } ) } @@ -859,7 +855,6 @@ pub mod testing { InternalAddress::PoS => {} InternalAddress::PosSlashPool => {} InternalAddress::Governance => {} - InternalAddress::SlashFund => {} InternalAddress::Parameters => {} InternalAddress::Ibc => {} InternalAddress::IbcToken(_) => {} @@ -867,6 +862,7 @@ pub mod testing { InternalAddress::EthBridgePool => {} InternalAddress::Erc20(_) => {} InternalAddress::ReplayProtection => {} + InternalAddress::Pgf => {} InternalAddress::Multitoken => {} /* Add new addresses in the * `prop_oneof` below. */ }; @@ -877,12 +873,12 @@ pub mod testing { Just(InternalAddress::Parameters), arb_ibc_token(), Just(InternalAddress::Governance), - Just(InternalAddress::SlashFund), Just(InternalAddress::EthBridge), Just(InternalAddress::EthBridgePool), Just(arb_erc20()), Just(InternalAddress::ReplayProtection), Just(InternalAddress::Multitoken), + Just(InternalAddress::Pgf), ] } diff --git a/core/src/types/governance.rs b/core/src/types/governance.rs deleted file mode 100644 index d3450dccd1c..00000000000 --- a/core/src/types/governance.rs +++ /dev/null @@ -1,384 +0,0 @@ -//! Files defyining the types used in governance. - -use std::collections::{BTreeMap, HashSet}; -use std::fmt::{self, Display}; - -use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -use crate::types::address::Address; -use crate::types::hash::Hash; -use crate::types::key::common::{self, Signature}; -use crate::types::key::SigScheme; -use crate::types::storage::Epoch; -use crate::types::token::{ - Amount, DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES, -}; -use crate::types::uint::Uint; - -/// Type alias for vote power -pub type VotePower = Uint; - -/// A PGF cocuncil composed of the address and spending cap -pub type Council = (Address, Amount); - -/// The type of a governance vote with the optional associated Memo -#[derive( - Debug, - Clone, - PartialEq, - BorshSerialize, - BorshDeserialize, - Serialize, - Deserialize, - Eq, -)] -pub enum VoteType { - /// A default vote without Memo - Default, - /// A vote for the PGF council - PGFCouncil(HashSet), - /// A vote for ETH bridge carrying the signature over the proposed message - ETHBridge(Signature), -} - -#[derive( - Debug, - Clone, - PartialEq, - BorshSerialize, - BorshDeserialize, - Serialize, - Deserialize, - Eq, -)] -/// The vote for a proposal -pub enum ProposalVote { - /// Yes - Yay(VoteType), - /// No - Nay, -} - -impl ProposalVote { - /// Check if a vote is yay - pub fn is_yay(&self) -> bool { - matches!(self, ProposalVote::Yay(_)) - } - - /// Check if vote is of type default - pub fn is_default_vote(&self) -> bool { - matches!( - self, - ProposalVote::Yay(VoteType::Default) | ProposalVote::Nay - ) - } -} - -impl Display for ProposalVote { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ProposalVote::Yay(vote_type) => match vote_type { - VoteType::Default => write!(f, "yay"), - VoteType::PGFCouncil(councils) => { - writeln!(f, "yay with councils:")?; - for (address, spending_cap) in councils { - writeln!( - f, - "Council: {}, spending cap: {}", - address, - spending_cap.to_string_native() - )? - } - - Ok(()) - } - VoteType::ETHBridge(sig) => { - write!(f, "yay with signature: {:#?}", sig) - } - }, - - ProposalVote::Nay => write!(f, "nay"), - } - } -} - -#[allow(missing_docs)] -#[derive(Debug, Error)] -pub enum ProposalVoteParseError { - #[error("Invalid vote. Vote shall be yay or nay.")] - InvalidVote, -} - -/// The type of the tally -#[derive(Clone, Debug)] -pub enum Tally { - /// Default proposal - Default, - /// PGF proposal - PGFCouncil(Council), - /// ETH Bridge proposal - ETHBridge, -} - -/// The result of a proposal -#[derive(Clone, Debug)] -pub enum TallyResult { - /// Proposal was accepted with the associated value - Passed(Tally), - /// Proposal was rejected - Rejected, -} - -/// The result with votes of a proposal -pub struct ProposalResult { - /// The result of a proposal - pub result: TallyResult, - /// The total voting power during the proposal tally - pub total_voting_power: VotePower, - /// The total voting power from yay votes - pub total_yay_power: VotePower, - /// The total voting power from nay votes (unused at the moment) - pub total_nay_power: VotePower, -} - -impl Display for ProposalResult { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let percentage = DenominatedAmount { - amount: Amount::from_uint( - self.total_yay_power - .fixed_precision_div(&self.total_voting_power, 4) - .unwrap_or_default(), - 0, - ) - .unwrap(), - denom: 2.into(), - }; - - write!( - f, - "{} with {} yay votes over {} ({}%)", - self.result, - DenominatedAmount { - amount: Amount::from_uint(self.total_yay_power, 0).unwrap(), - denom: NATIVE_MAX_DECIMAL_PLACES.into() - }, - DenominatedAmount { - amount: Amount::from_uint(self.total_voting_power, 0).unwrap(), - denom: NATIVE_MAX_DECIMAL_PLACES.into() - }, - percentage - ) - } -} - -impl Display for TallyResult { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - TallyResult::Passed(vote) => match vote { - Tally::Default | Tally::ETHBridge => write!(f, "passed"), - Tally::PGFCouncil((council, cap)) => write!( - f, - "passed with PGF council address: {}, spending cap: {}", - council, - cap.to_string_native() - ), - }, - TallyResult::Rejected => write!(f, "rejected"), - } - } -} - -/// The type of a governance proposal -#[derive( - Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, -)] -pub enum ProposalType { - /// A default proposal with the optional path to wasm code - Default(Option), - /// A PGF council proposal - PGFCouncil, - /// An ETH bridge proposal - ETHBridge, -} - -#[derive( - Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, -)] -/// The proposal structure -pub struct Proposal { - /// The proposal id - pub id: Option, - /// The proposal content - pub content: BTreeMap, - /// The proposal author address - pub author: Address, - /// The proposal type - pub r#type: ProposalType, - /// The epoch from which voting is allowed - pub voting_start_epoch: Epoch, - /// The epoch from which voting is stopped - pub voting_end_epoch: Epoch, - /// The epoch from which this changes are executed - pub grace_epoch: Epoch, -} - -impl Display for Proposal { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "id: {:?}, author: {:?}", self.id, self.author) - } -} - -#[allow(missing_docs)] -#[derive(Debug, Error)] -pub enum ProposalError { - #[error("Invalid proposal data.")] - InvalidProposalData, -} - -#[derive( - Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, -)] -/// The offline proposal structure -pub struct OfflineProposal { - /// The proposal content - pub content: BTreeMap, - /// The proposal author address - pub author: Address, - /// The epoch from which this changes are executed - pub tally_epoch: Epoch, - /// The signature over proposal data - pub signature: Signature, - /// The address corresponding to the signature pk - pub address: Address, -} - -impl OfflineProposal { - /// Create an offline proposal with a signature - pub fn new( - proposal: Proposal, - address: Address, - signing_key: &common::SecretKey, - ) -> Self { - let content_serialized = serde_json::to_vec(&proposal.content) - .expect("Conversion to bytes shouldn't fail."); - let author_serialized = serde_json::to_vec(&proposal.author) - .expect("Conversion to bytes shouldn't fail."); - let tally_epoch_serialized = serde_json::to_vec(&proposal.grace_epoch) - .expect("Conversion to bytes shouldn't fail."); - let proposal_serialized = &[ - content_serialized, - author_serialized, - tally_epoch_serialized, - ] - .concat(); - let proposal_data_hash = Hash::sha256(proposal_serialized); - let signature = - common::SigScheme::sign(signing_key, proposal_data_hash); - Self { - content: proposal.content, - author: proposal.author, - tally_epoch: proposal.grace_epoch, - signature, - address, - } - } - - /// Check whether the signature is valid or not - pub fn check_signature(&self, public_key: &common::PublicKey) -> bool { - let proposal_data_hash = self.compute_hash(); - common::SigScheme::verify_signature( - public_key, - &proposal_data_hash, - &self.signature, - ) - .is_ok() - } - - /// Compute the hash of the proposal - pub fn compute_hash(&self) -> Hash { - let content_serialized = serde_json::to_vec(&self.content) - .expect("Conversion to bytes shouldn't fail."); - let author_serialized = serde_json::to_vec(&self.author) - .expect("Conversion to bytes shouldn't fail."); - let tally_epoch_serialized = serde_json::to_vec(&self.tally_epoch) - .expect("Conversion to bytes shouldn't fail."); - let proposal_serialized = &[ - content_serialized, - author_serialized, - tally_epoch_serialized, - ] - .concat(); - Hash::sha256(proposal_serialized) - } -} - -#[derive( - Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, -)] -/// The offline proposal structure -pub struct OfflineVote { - /// The proposal data hash - pub proposal_hash: Hash, - /// The proposal vote - pub vote: ProposalVote, - /// The signature over proposal data - pub signature: Signature, - /// The address corresponding to the signature pk - pub address: Address, -} - -impl OfflineVote { - /// Create an offline vote for a proposal - pub fn new( - proposal: &OfflineProposal, - vote: ProposalVote, - address: Address, - signing_key: &common::SecretKey, - ) -> Self { - let proposal_hash = proposal.compute_hash(); - let proposal_hash_data = proposal_hash - .try_to_vec() - .expect("Conversion to bytes shouldn't fail."); - let proposal_vote_data = vote - .try_to_vec() - .expect("Conversion to bytes shouldn't fail."); - let vote_serialized = - &[proposal_hash_data, proposal_vote_data].concat(); - let signature = common::SigScheme::sign(signing_key, vote_serialized); - Self { - proposal_hash, - vote, - signature, - address, - } - } - - /// compute the hash of a proposal - pub fn compute_hash(&self) -> Hash { - let proposal_hash_data = self - .proposal_hash - .try_to_vec() - .expect("Conversion to bytes shouldn't fail."); - let proposal_vote_data = self - .vote - .try_to_vec() - .expect("Conversion to bytes shouldn't fail."); - let vote_serialized = - &[proposal_hash_data, proposal_vote_data].concat(); - - Hash::sha256(vote_serialized) - } - - /// Check whether the signature is valid or not - pub fn check_signature(&self, public_key: &common::PublicKey) -> bool { - let vote_data_hash = self.compute_hash(); - common::SigScheme::verify_signature( - public_key, - &vote_data_hash, - &self.signature, - ) - .is_ok() - } -} diff --git a/core/src/types/key/mod.rs b/core/src/types/key/mod.rs index 8c59262f72e..685e3464f7d 100644 --- a/core/src/types/key/mod.rs +++ b/core/src/types/key/mod.rs @@ -12,6 +12,8 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use data_encoding::HEXUPPER; +use lazy_map::LazyMap; +use namada_macros::StorageKeys; #[cfg(feature = "rand")] use rand::{CryptoRng, RngCore}; use serde::Serialize; @@ -19,25 +21,45 @@ use sha2::{Digest, Sha256}; use thiserror::Error; use super::address::Address; -use super::storage::{self, DbKeySeg, Key, KeySeg}; +use super::storage::{self, DbKeySeg, Key}; use crate::ledger::storage::{Sha256Hasher, StorageHasher}; +use crate::ledger::storage_api::collections::{lazy_map, LazyCollection}; use crate::types::address; -const PK_STORAGE_KEY: &str = "public_key"; -const PROTOCOL_PK_STORAGE_KEY: &str = "protocol_public_key"; +/// Storage keys for account. +#[derive(StorageKeys)] +struct Keys { + public_keys: &'static str, + threshold: &'static str, + protocol_public_keys: &'static str, +} /// Obtain a storage key for user's public key. -pub fn pk_key(owner: &Address) -> storage::Key { - Key::from(owner.to_db_key()) - .push(&PK_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") +pub fn pks_key_prefix(owner: &Address) -> storage::Key { + Key { + segments: vec![ + DbKeySeg::AddressSeg(owner.to_owned()), + DbKeySeg::StringSeg(Keys::VALUES.public_keys.to_string()), + ], + } +} + +/// Object that LazyMap handler for the user's public key subspace +pub fn pks_handle(owner: &Address) -> LazyMap { + LazyMap::open(pks_key_prefix(owner)) } /// Check if the given storage key is a public key. If it is, returns the owner. -pub fn is_pk_key(key: &Key) -> Option<&Address> { +pub fn is_pks_key(key: &Key) -> Option<&Address> { match &key.segments[..] { - [DbKeySeg::AddressSeg(owner), DbKeySeg::StringSeg(key)] - if key == PK_STORAGE_KEY => + [ + DbKeySeg::AddressSeg(owner), + DbKeySeg::StringSeg(prefix), + DbKeySeg::StringSeg(data), + DbKeySeg::StringSeg(index), + ] if prefix.as_str() == Keys::VALUES.public_keys + && data.as_str() == lazy_map::DATA_SUBKEY + && index.parse::().is_ok() => { Some(owner) } @@ -45,18 +67,43 @@ pub fn is_pk_key(key: &Key) -> Option<&Address> { } } +/// Check if the given storage key is a threshol key. +pub fn is_threshold_key(key: &Key) -> Option<&Address> { + match &key.segments[..] { + [DbKeySeg::AddressSeg(owner), DbKeySeg::StringSeg(prefix)] + if prefix.as_str() == Keys::VALUES.threshold => + { + Some(owner) + } + _ => None, + } +} + +/// Obtain the storage key for a user threshold +pub fn threshold_key(owner: &Address) -> storage::Key { + Key { + segments: vec![ + DbKeySeg::AddressSeg(owner.to_owned()), + DbKeySeg::StringSeg(Keys::VALUES.threshold.to_string()), + ], + } +} + /// Obtain a storage key for user's protocol public key. pub fn protocol_pk_key(owner: &Address) -> storage::Key { - Key::from(owner.to_db_key()) - .push(&PROTOCOL_PK_STORAGE_KEY.to_owned()) - .expect("Cannot obtain a storage key") + Key { + segments: vec![ + DbKeySeg::AddressSeg(owner.to_owned()), + DbKeySeg::StringSeg(Keys::VALUES.protocol_public_keys.to_string()), + ], + } } /// Check if the given storage key is a public key. If it is, returns the owner. pub fn is_protocol_pk_key(key: &Key) -> Option<&Address> { match &key.segments[..] { [DbKeySeg::AddressSeg(owner), DbKeySeg::StringSeg(key)] - if key == PROTOCOL_PK_STORAGE_KEY => + if key.as_str() == Keys::VALUES.protocol_public_keys => { Some(owner) } diff --git a/core/src/types/mod.rs b/core/src/types/mod.rs index c35b3142969..8aee038d9b1 100644 --- a/core/src/types/mod.rs +++ b/core/src/types/mod.rs @@ -1,5 +1,6 @@ //! Types definitions. +pub mod account; pub mod address; pub mod chain; pub mod dec; @@ -7,7 +8,6 @@ pub mod eth_abi; pub mod eth_bridge_pool; pub mod ethereum_events; pub mod ethereum_structs; -pub mod governance; pub mod hash; pub mod ibc; pub mod internal; diff --git a/core/src/types/token.rs b/core/src/types/token.rs index bf4e23cbe3c..6056495cd1c 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -76,6 +76,11 @@ impl Amount { self.raw = self.raw.checked_sub(amount.raw).unwrap(); } + /// Check if there are enough funds. + pub fn can_spend(&self, amount: &Amount) -> bool { + self.raw >= amount.raw + } + /// Receive a given amount. /// Panics on overflow and when [`uint::MAX_SIGNED_VALUE`] is exceeded. pub fn receive(&mut self, amount: &Amount) { @@ -154,6 +159,20 @@ impl Amount { Self { raw: change.abs() } } + /// Checked division. Returns `None` on underflow. + pub fn checked_div(&self, amount: Amount) -> Option { + self.raw + .checked_div(amount.raw) + .map(|result| Self { raw: result }) + } + + /// Checked division. Returns `None` on overflow. + pub fn checked_mul(&self, amount: Amount) -> Option { + self.raw + .checked_mul(amount.raw) + .map(|result| Self { raw: result }) + } + /// Given a string and a denomination, parse an amount from string. pub fn from_str( string: impl AsRef, diff --git a/core/src/types/transaction/account.rs b/core/src/types/transaction/account.rs new file mode 100644 index 00000000000..f2eaafe7ef9 --- /dev/null +++ b/core/src/types/transaction/account.rs @@ -0,0 +1,52 @@ +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +use crate::types::address::Address; +use crate::types::hash::Hash; +use crate::types::key::common; + +/// A tx data type to initialize a new established account +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, +)] +pub struct InitAccount { + /// Public keys to be written into the account's storage. This can be used + /// for signature verification of transactions for the newly created + /// account. + pub public_keys: Vec, + /// The VP code hash + pub vp_code_hash: Hash, + /// The account signature threshold + pub threshold: u8, +} + +/// A tx data type to update an account's validity predicate +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, +)] +pub struct UpdateAccount { + /// An address of the account + pub addr: Address, + /// The new VP code hash + pub vp_code_hash: Option, + /// Public keys to be written into the account's storage. This can be used + /// for signature verification of transactions for the newly created + /// account. + pub public_keys: Vec, + /// The account signature threshold + pub threshold: Option, +} diff --git a/core/src/types/transaction/governance.rs b/core/src/types/transaction/governance.rs index bfc17eeaefd..0d653c44cf0 100644 --- a/core/src/types/transaction/governance.rs +++ b/core/src/types/transaction/governance.rs @@ -1,86 +1,25 @@ -use std::fmt::Display; +use std::collections::HashSet; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; +use thiserror::Error; -use crate::types::address::Address; -use crate::types::governance::{ - self, Proposal, ProposalError, ProposalVote, VoteType, +use crate::ledger::governance::cli::onchain::{ + DefaultProposal, PgfFundingProposal, PgfStewardProposal, +}; +use crate::ledger::governance::storage::proposal::{ + AddRemove, PGFAction, ProposalType, }; +use crate::ledger::governance::storage::vote::StorageProposalVote; +use crate::types::address::Address; use crate::types::hash::Hash; use crate::types::storage::Epoch; -/// The type of a Proposal -#[derive( - Debug, - Clone, - PartialEq, - BorshSerialize, - BorshDeserialize, - Serialize, - Deserialize, -)] -pub enum ProposalType { - /// Default governance proposal with the optional wasm code - Default(Option), - /// PGF council proposal - PGFCouncil, - /// ETH proposal - ETHBridge, -} - -impl Display for ProposalType { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - ProposalType::Default(_) => write!(f, "Default"), - ProposalType::PGFCouncil => write!(f, "PGF Council"), - ProposalType::ETHBridge => write!(f, "ETH Bridge"), - } - } -} - -impl PartialEq for ProposalType { - fn eq(&self, other: &VoteType) -> bool { - match self { - Self::Default(_) => { - matches!(other, VoteType::Default) - } - Self::PGFCouncil => { - matches!(other, VoteType::PGFCouncil(..)) - } - Self::ETHBridge => { - matches!(other, VoteType::ETHBridge(_)) - } - } - } -} - -impl TryFrom for (ProposalType, Option>) { - type Error = ProposalError; - - fn try_from(value: governance::ProposalType) -> Result { - match value { - governance::ProposalType::Default(path) => { - if let Some(p) = path { - match std::fs::read(p) { - Ok(code) => Ok(( - ProposalType::Default(Some(Hash::default())), - Some(code), - )), - Err(_) => Err(Self::Error::InvalidProposalData), - } - } else { - Ok((ProposalType::Default(None), None)) - } - } - governance::ProposalType::PGFCouncil => { - Ok((ProposalType::PGFCouncil, None)) - } - governance::ProposalType::ETHBridge => { - Ok((ProposalType::ETHBridge, None)) - } - } - } +#[allow(missing_docs)] +#[derive(Debug, Error)] +pub enum ProposalError { + #[error("Invalid proposal data.")] + InvalidProposalData, } /// A tx data type to hold proposal data @@ -110,6 +49,16 @@ pub struct InitProposalData { pub grace_epoch: Epoch, } +impl InitProposalData { + /// Get the hash of the corresponding extra data section + pub fn get_section_code_hash(&self) -> Option { + match self.r#type { + ProposalType::Default(hash) => hash, + _ => None, + } + } +} + /// A tx data type to hold vote proposal data #[derive( Debug, @@ -124,30 +73,82 @@ pub struct VoteProposalData { /// The proposal id pub id: u64, /// The proposal vote - pub vote: ProposalVote, + pub vote: StorageProposalVote, /// The proposal author address pub voter: Address, /// Delegator addreses pub delegations: Vec
, } -impl TryFrom for (InitProposalData, Vec, Option>) { +impl TryFrom for InitProposalData { + type Error = ProposalError; + + fn try_from(value: DefaultProposal) -> Result { + Ok(InitProposalData { + id: value.proposal.id, + content: Hash::default(), + author: value.proposal.author, + r#type: ProposalType::Default(None), + voting_start_epoch: value.proposal.voting_start_epoch, + voting_end_epoch: value.proposal.voting_end_epoch, + grace_epoch: value.proposal.grace_epoch, + }) + } +} + +impl TryFrom for InitProposalData { type Error = ProposalError; - fn try_from(proposal: Proposal) -> Result { - let (r#type, code) = proposal.r#type.try_into()?; - Ok(( - InitProposalData { - id: proposal.id, - content: Hash::default(), - author: proposal.author, - r#type, - voting_start_epoch: proposal.voting_start_epoch, - voting_end_epoch: proposal.voting_end_epoch, - grace_epoch: proposal.grace_epoch, - }, - proposal.content.try_to_vec().unwrap(), - code, - )) + fn try_from(value: PgfStewardProposal) -> Result { + let extra_data = value + .data + .iter() + .cloned() + .map(|steward| AddRemove::
::try_from(steward).unwrap()) + .collect::>>(); + + Ok(InitProposalData { + id: value.proposal.id, + content: Hash::default(), + author: value.proposal.author, + r#type: ProposalType::PGFSteward(extra_data), + voting_start_epoch: value.proposal.voting_start_epoch, + voting_end_epoch: value.proposal.voting_end_epoch, + grace_epoch: value.proposal.grace_epoch, + }) + } +} + +impl TryFrom for InitProposalData { + type Error = ProposalError; + + fn try_from(value: PgfFundingProposal) -> Result { + let continous_fundings = value + .data + .continous + .iter() + .cloned() + .map(|funding| PGFAction::try_from(funding).unwrap()) + .collect::>(); + + let retro_fundings = value + .data + .retro + .iter() + .cloned() + .map(|funding| PGFAction::try_from(funding).unwrap()) + .collect::>(); + + let extra_data = [continous_fundings, retro_fundings].concat(); + + Ok(InitProposalData { + id: value.proposal.id, + content: Hash::default(), + author: value.proposal.author, + r#type: ProposalType::PGFPayment(extra_data), + voting_start_epoch: value.proposal.voting_start_epoch, + voting_end_epoch: value.proposal.voting_end_epoch, + grace_epoch: value.proposal.grace_epoch, + }) } } diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index c6cc23dffa6..88ded1970df 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -1,5 +1,7 @@ //! Types that are used in transactions. +/// txs to manage accounts +pub mod account; /// txs that contain decrypted payloads or assertions of /// non-decryptability pub mod decrypted; @@ -7,6 +9,7 @@ pub mod decrypted; pub mod encrypted; /// txs to manage governance pub mod governance; +/// txs to manage pos pub mod pos; /// transaction protocols made by validators pub mod protocol; @@ -27,10 +30,8 @@ pub use wrapper::*; use crate::ledger::gas::VpsGas; use crate::types::address::Address; -use crate::types::dec::Dec; use crate::types::hash::Hash; use crate::types::ibc::IbcEvent; -use crate::types::key::*; use crate::types::storage; #[cfg(feature = "ferveo-tpke")] use crate::types::transaction::protocol::ProtocolTx; @@ -131,80 +132,6 @@ fn iterable_to_string( } } -/// A tx data type to update an account's validity predicate -#[derive( - Debug, - Clone, - PartialEq, - BorshSerialize, - BorshDeserialize, - BorshSchema, - Serialize, - Deserialize, -)] -pub struct UpdateVp { - /// An address of the account - pub addr: Address, - /// The new VP code hash - pub vp_code_hash: Hash, -} - -/// A tx data type to initialize a new established account -#[derive( - Debug, - Clone, - PartialEq, - BorshSerialize, - BorshDeserialize, - BorshSchema, - Serialize, - Deserialize, -)] -pub struct InitAccount { - /// Public key to be written into the account's storage. This can be used - /// for signature verification of transactions for the newly created - /// account. - pub public_key: common::PublicKey, - /// The VP code hash - pub vp_code_hash: Hash, -} - -/// A tx data type to initialize a new validator account. -#[derive( - Debug, - Clone, - PartialEq, - BorshSerialize, - BorshDeserialize, - BorshSchema, - Serialize, - Deserialize, -)] -pub struct InitValidator { - /// Public key to be written into the account's storage. This can be used - /// for signature verification of transactions for the newly created - /// account. - pub account_key: common::PublicKey, - /// A key to be used for signing blocks and votes on blocks. - pub consensus_key: common::PublicKey, - /// An Eth bridge governance public key - pub eth_cold_key: secp256k1::PublicKey, - /// An Eth bridge hot signing public key used for validator set updates and - /// cross-chain transactions - pub eth_hot_key: secp256k1::PublicKey, - /// Public key used to sign protocol transactions - pub protocol_key: common::PublicKey, - /// Serialization of the public session key used in the DKG - pub dkg_key: crate::types::key::dkg_session_keys::DkgPublicKey, - /// The initial commission rate charged for delegation rewards - pub commission_rate: Dec, - /// The maximum change allowed per epoch to the commission rate. This is - /// immutable once set here. - pub max_commission_rate_change: Dec, - /// The VP code for validator account - pub validator_vp_code_hash: Hash, -} - /// Struct that classifies that kind of Tx /// based on the contents of its data. #[derive( @@ -241,6 +168,7 @@ mod test_process_tx { use super::*; use crate::proto::{Code, Data, Section, Signature, Tx, TxError}; use crate::types::address::nam; + use crate::types::key::*; use crate::types::storage::Epoch; use crate::types::token::Amount; @@ -411,6 +339,8 @@ fn test_process_tx_decrypted_unsigned() { #[test] fn test_process_tx_decrypted_signed() { use crate::proto::{Code, Data, Section, Signature, Tx}; + use crate::types::key::*; + fn gen_keypair() -> common::SecretKey { use rand::prelude::ThreadRng; use rand::thread_rng; diff --git a/core/src/types/transaction/pos.rs b/core/src/types/transaction/pos.rs index d334047ffe5..fa0e3d08915 100644 --- a/core/src/types/transaction/pos.rs +++ b/core/src/types/transaction/pos.rs @@ -5,8 +5,48 @@ use serde::{Deserialize, Serialize}; use crate::types::address::Address; use crate::types::dec::Dec; +use crate::types::hash::Hash; +use crate::types::key::{common, secp256k1}; use crate::types::token; +/// A tx data type to initialize a new validator account. +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, +)] +pub struct InitValidator { + /// Public key to be written into the account's storage. This can be used + /// for signature verification of transactions for the newly created + /// account. + pub account_keys: Vec, + /// The minimum number of signatures needed + pub threshold: u8, + /// A key to be used for signing blocks and votes on blocks. + pub consensus_key: common::PublicKey, + /// An Eth bridge governance public key + pub eth_cold_key: secp256k1::PublicKey, + /// An Eth bridge hot signing public key used for validator set updates and + /// cross-chain transactions + pub eth_hot_key: secp256k1::PublicKey, + /// Public key used to sign protocol transactions + pub protocol_key: common::PublicKey, + /// Serialization of the public session key used in the DKG + pub dkg_key: crate::types::key::dkg_session_keys::DkgPublicKey, + /// The initial commission rate charged for delegation rewards + pub commission_rate: Dec, + /// The maximum change allowed per epoch to the commission rate. This is + /// immutable once set here. + pub max_commission_rate_change: Dec, + /// The VP code for validator account + pub validator_vp_code_hash: Hash, +} + /// A bond is a validator's self-bond or a delegation from non-validator to a /// validator. #[derive( diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index 403641a0b63..7b72c225eb0 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -13,6 +13,7 @@ pub mod wrapper_tx { use sha2::{Digest, Sha256}; use thiserror::Error; + use crate::ledger::testnet_pow; use crate::types::address::Address; use crate::types::key::*; use crate::types::storage::Epoch; @@ -240,7 +241,7 @@ pub mod wrapper_tx { epoch: Epoch, gas_limit: GasLimit, #[cfg(not(feature = "mainnet"))] pow_solution: Option< - crate::ledger::testnet_pow::Solution, + testnet_pow::Solution, >, ) -> WrapperTx { Self { @@ -255,7 +256,7 @@ pub mod wrapper_tx { /// Get the address of the implicit account associated /// with the public key - pub fn fee_payer(&self) -> Address { + pub fn gas_payer(&self) -> Address { Address::from(&self.pk) } diff --git a/documentation/dev/src/explore/design/ledger/governance.md b/documentation/dev/src/explore/design/ledger/governance.md index d9e578eed5f..a7215c79b20 100644 --- a/documentation/dev/src/explore/design/ledger/governance.md +++ b/documentation/dev/src/explore/design/ledger/governance.md @@ -13,7 +13,7 @@ Also, it introduces some protocol parameters: - `min_proposal_fund` - `max_proposal_code_size` -- `min_proposal_period` +- `min_proposal_voting_period` - `max_proposal_period` - `max_proposal_content_size` - `min_proposal_grace_epochs` @@ -26,7 +26,7 @@ On-chain proposals are created under the `governance_address` storage space and, /$GovernanceAddress/counter: u64 /$GovernanceAddress/min_proposal_fund: u64 /$GovernanceAddress/max_proposal_code_size: u64 -/$GovernanceAddress/min_proposal_period: u64 +/$GovernanceAddress/min_proposal_voting_period: u64 /$GovernanceAddress/max_proposal_period: u64 /$GovernanceAddress/max_proposal_content_size: u64 /$GovernanceAddress/min_proposal_grace_epochs: u64 @@ -50,11 +50,11 @@ and follow these rules: - `$id` must be equal to `counter + 1`. - `startEpoch` must: - be greater than `currentEpoch`, where current epoch is the epoch in which the transaction is executed and included in a block - - be a multiple of `min_proposal_period`. + - be a multiple of `min_proposal_voting_period`. - `endEpoch` must: - - be at least `min_proposal_period` epochs greater than `startEpoch` + - be at least `min_proposal_voting_period` epochs greater than `startEpoch` - be at most `max_proposal_period` epochs greater than `startEpoch` - - be a multiple of `min_proposal_period` + - be a multiple of `min_proposal_voting_period` - `graceEpoch` must: - be at least `min_grace_epoch` epochs greater than `endEpoch` - `proposalCode` can be empty and must be a valid transaction with size less than `max_proposal_code_size` kibibytes. diff --git a/documentation/dev/src/specs/ledger/default-transactions.md b/documentation/dev/src/specs/ledger/default-transactions.md index fb254f0cd3b..d1e36b923aa 100644 --- a/documentation/dev/src/specs/ledger/default-transactions.md +++ b/documentation/dev/src/specs/ledger/default-transactions.md @@ -28,7 +28,7 @@ Transparently transfer `amount` of fungible `token` from the `source` to the `ta Attach [Transfer](../encoding.md#transfer) to the `data`. -### tx_update_vp +### tx_update_account Update a validity predicate of an established account. diff --git a/encoding_spec/src/main.rs b/encoding_spec/src/main.rs index b9e2b034ef4..5889b03b8dc 100644 --- a/encoding_spec/src/main.rs +++ b/encoding_spec/src/main.rs @@ -70,10 +70,13 @@ fn main() -> Result<(), Box> { let public_key_schema = PublicKey::schema_container(); // TODO update after let signature_schema = Signature::schema_container(); - let init_account_schema = transaction::InitAccount::schema_container(); - let init_validator_schema = transaction::InitValidator::schema_container(); + let init_account_schema = + transaction::account::InitAccount::schema_container(); + let init_validator_schema = + transaction::pos::InitValidator::schema_container(); let token_transfer_schema = token::Transfer::schema_container(); - let update_vp_schema = transaction::UpdateVp::schema_container(); + let update_account = + transaction::account::UpdateAccount::schema_container(); let pos_bond_schema = pos::Bond::schema_container(); let pos_withdraw_schema = pos::Withdraw::schema_container(); let wrapper_tx_schema = transaction::WrapperTx::schema_container(); @@ -98,7 +101,7 @@ fn main() -> Result<(), Box> { definitions.extend(init_account_schema.definitions); definitions.extend(init_validator_schema.definitions); definitions.extend(token_transfer_schema.definitions); - definitions.extend(update_vp_schema.definitions); + definitions.extend(update_account.definitions); definitions.extend(pos_bond_schema.definitions); definitions.extend(pos_withdraw_schema.definitions); definitions.extend(wrapper_tx_schema.definitions); @@ -179,11 +182,11 @@ fn main() -> Result<(), Box> { ).with_rust_doc_link("https://dev.namada.net/master/rustdoc/namada/types/token/struct.Transfer.html"); tables.push(token_transfer_table); - let update_vp_definition = - definitions.remove(&update_vp_schema.declaration).unwrap(); - let update_vp_table = - definition_to_table(update_vp_schema.declaration, update_vp_definition).with_rust_doc_link("https://dev.namada.net/master/rustdoc/namada/types/transaction/struct.UpdateVp.html"); - tables.push(update_vp_table); + let update_account_definition = + definitions.remove(&update_account.declaration).unwrap(); + let update_accoun_table = + definition_to_table(update_account.declaration, update_account_definition).with_rust_doc_link("https://dev.namada.net/master/rustdoc/namada/types/transaction/struct.UpdateVp.html"); + tables.push(update_accoun_table); let pos_bond_definition = definitions.remove(&pos_bond_schema.declaration).unwrap(); diff --git a/genesis/dev.toml b/genesis/dev.toml index 19985a3b9ee..a2320a1cf06 100644 --- a/genesis/dev.toml +++ b/genesis/dev.toml @@ -1,148 +1,203 @@ -# Example genesis with dev settings. -genesis_time = "2021-09-30:10:00.00Z" +# Developer network +genesis_time = "2021-12-20T15:00:00.00Z" native_token = "NAM" - -# A genesis validator with alias "validator". -[validator.validator] -# Validator's public key for consensus. -consensus_public_key = "5e704c4e46265e1ccc87505149f79b9d2e414d01a4e3806dfc65f0a73901c1d0" -# Public key of the validator's Namada account. -account_public_key = "5e704c4e46265e1ccc87505149f79b9d2e414d01a4e3806dfc65f0a73901c1d0" -# Address of the validator. -address = "a1qq5qqqqqgfqnsd6pxse5zdj9g5crzsf5x4zyzv6yxerr2d2rxpryzwp5g5m5zvfjxv6ygsekjmraj0" -# Validator's token balance at genesis. -tokens = 200000 -# Amount of the validator's genesis token balance which is not staked. -non_staked_balance = 100000 -# 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:port address -net_address = "127.0.0.1:26656" +faucet_pow_difficulty = 0 +faucet_withdrawal_limit = "1000" # Some tokens present at genesis. [token.NAM] address = "atest1v4ehgw36x3prswzxggunzv6pxqmnvdj9xvcyzvpsggeyvs3cg9qnywf589qnwvfsg5erg3fkl09rg5" -denom = 8 +denom = 6 +vp = "vp_token" [token.NAM.balances] -# In token balances, we can use: -# 1. An address any account -a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6mtz = 1000000 -atest1v4ehgw36x3qng3jzggu5yvpsxgcngv2xgguy2dpkgvu5x33kx3pr2w2zgep5xwfkxscrxs2pj8075p = 1000000 -atest1v4ehgw36xvcyyvejgvenxs34g3zygv3jxqunjd6rxyeyys3sxy6rwvfkx4qnj33hg9qnvse4lsfctw = 1000000 -# 2. An alias of any account +Albert = "1000000" +"Albert.public_key" = "100" Bertha = "1000000" -# 3. A public key of a validator or an established account from which the -# address of the implicit account is derived) -"bertha.public_key" = 100 -"validator.public_key" = 100 +"Bertha.public_key" = "2000" +Christel = "1000000" +"Christel.public_key" = "100" +Daewon = "1000000" +Ester = "1000000" +faucet = "922337203685400000000" +"faucet.public_key" = "100" +[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] -atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 -atest1v4ehgw36x3qng3jzggu5yvpsxgcngv2xgguy2dpkgvu5x33kx3pr2w2zgep5xwfkxscrxs2pj8075p = 1000000 -atest1v4ehgw36xvcyyvejgvenxs34g3zygv3jxqunjd6rxyeyys3sxy6rwvfkx4qnj33hg9qnvse4lsfctw = 1000000 -a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6mtz = 1000000 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +Ester = "1000000" +faucet = "9223372036854" +[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] -atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 -atest1v4ehgw36x3qng3jzggu5yvpsxgcngv2xgguy2dpkgvu5x33kx3pr2w2zgep5xwfkxscrxs2pj8075p = 1000000 -atest1v4ehgw36xvcyyvejgvenxs34g3zygv3jxqunjd6rxyeyys3sxy6rwvfkx4qnj33hg9qnvse4lsfctw = 1000000 -a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6mtz = 1000000 +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +Ester = "1000000" +faucet = "9223372036854" +[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] -atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 -atest1v4ehgw36x3qng3jzggu5yvpsxgcngv2xgguy2dpkgvu5x33kx3pr2w2zgep5xwfkxscrxs2pj8075p = 1000000 -atest1v4ehgw36xvcyyvejgvenxs34g3zygv3jxqunjd6rxyeyys3sxy6rwvfkx4qnj33hg9qnvse4lsfctw = 1000000 -a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6mtz = 1000000 - -[token.schnitzel] +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +Ester = "1000000" +faucet = "9223372036854" +[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 -[token.schnitzel.balances] -atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 -atest1v4ehgw36x3qng3jzggu5yvpsxgcngv2xgguy2dpkgvu5x33kx3pr2w2zgep5xwfkxscrxs2pj8075p = 1000000 -atest1v4ehgw36xvcyyvejgvenxs34g3zygv3jxqunjd6rxyeyys3sxy6rwvfkx4qnj33hg9qnvse4lsfctw = 1000000 -a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6mtz = 1000000 - -[token.apfel] +vp = "vp_token" +[token.Schnitzel.balances] +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +Ester = "1000000" +faucet = "9223372036854" +[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 -[token.apfel.balances] -atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 -atest1v4ehgw36x3qng3jzggu5yvpsxgcngv2xgguy2dpkgvu5x33kx3pr2w2zgep5xwfkxscrxs2pj8075p = 1000000 -atest1v4ehgw36xvcyyvejgvenxs34g3zygv3jxqunjd6rxyeyys3sxy6rwvfkx4qnj33hg9qnvse4lsfctw = 1000000 -a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6mtz = 1000000 - -[token.kartoffel] +vp = "vp_token" +[token.Apfel.balances] +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +Ester = "1000000" +faucet = "9223372036854" +[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" -denom = 6 public_key = "" -[token.kartoffel.balances] -atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4 = 1000000 -atest1v4ehgw36x3qng3jzggu5yvpsxgcngv2xgguy2dpkgvu5x33kx3pr2w2zgep5xwfkxscrxs2pj8075p = 1000000 -atest1v4ehgw36xvcyyvejgvenxs34g3zygv3jxqunjd6rxyeyys3sxy6rwvfkx4qnj33hg9qnvse4lsfctw = 1000000 -a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6mtz = 1000000 +denom = 6 +vp = "vp_token" +[token.Kartoffel.balances] +Albert = "1000000" +Bertha = "1000000" +Christel = "1000000" +Daewon = "1000000" +Ester = "1000000" +faucet = "9223372036854" +[token.Kartoffel.parameters] +max_reward_rate = "0.1" +kd_gain_nom = "0.1" +kp_gain_nom = "0.1" +locked_ratio_target_key = "0.6667" # Some established accounts present at genesis. +[established.faucet] +vp = "vp_testnet_faucet" -[established.albert] -address = "atest1v4ehgw368ycryv2z8qcnxv3cxgmrgvjpxs6yg333gym5vv2zxepnj334g4rryvj9xucrgve4x3xvr4" -public_key = "a57281e1dd9fd39ec3e8a162a1643ca7c836c0f2dae3bef1412a3a61a2fde1a7" +[established.Albert] vp = "vp_user" -[established.bertha] -address = "atest1v4ehgw36xvcyyvejgvenxs34g3zygv3jxqunjd6rxyeyys3sxy6rwvfkx4qnj33hg9qnvse4lsfctw" -public_key = "572512a95b190d615b1987f7072572a64951ad50f4f97ef9dbb83545c46ae600" +[established.Bertha] vp = "vp_user" -[established.christel] -address = "atest1v4ehgw36x3qng3jzggu5yvpsxgcngv2xgguy2dpkgvu5x33kx3pr2w2zgep5xwfkxscrxs2pj8075p" -public_key = "d06f8d4f897f329a50fd23ba5d2503bbe22fab2f14d5f625e07a65f617eb2778" +[established.Christel] vp = "vp_user" -# An implicit account present at genesis. +[established.masp] +address = "atest1v4ehgw36xaryysfsx5unvve4g5my2vjz89p52sjxxgenzd348yuyyv3hg3pnjs35g5unvde4ca36y5" +vp = "vp_masp" + +[implicit.Daewon] + +[implicit.Ester] -# Daewon (a1qyqzsqqqqqcyvvf5xcu5vd6rg4z5233hg9pn23pjgdryzdjy8pz52wzxxscnvvjxx3rryvzz8y5p6mtz) -[implicit.daewon] -public_key = "2b5b8fda66fb6fc4ef0d86f84b21f250034077effc459fc2403a77a35aa95e3f" +# Wasm VP definitions # Wasm VP definitions -# Default user VP +# Implicit VP +[wasm.vp_implicit] +filename = "vp_implicit.wasm" + +# Default user VP in established accounts [wasm.vp_user] -# filename (relative to wasm path used by the node) filename = "vp_user.wasm" -# SHA-256 hash of the wasm file -sha256 = "dc7b97f0448f2369bd2401c3c1d8898f53cac8c464a8c1b1f7f81415a658625d" # Default validator VP [wasm.vp_validator] # filename (relative to wasm path used by the node) filename = "vp_validator.wasm" +# Faucet VP +[wasm.vp_testnet_faucet] +filename = "vp_testnet_faucet.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 = 10 +min_num_of_blocks = 4 # Maximum expected time per block (in seconds). max_expected_time_per_block = 30 -# Expected epochs per year (also sets the minimum duration of an epoch in seconds) -epochs_per_year = 525_600 # 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 = 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 # Proof of stake parameters. [pos_params] @@ -153,9 +208,9 @@ max_validator_slots = 128 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 = 21 +unbonding_len = 3 # Votes per fundamental staking token (namnam) -tm_votes_per_token = "1" +tm_votes_per_token = "0.1" # Reward for proposing a block. block_proposer_reward = "0.125" # Reward for voting on a block. @@ -173,18 +228,21 @@ 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 +# 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_period = 3 +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 = 5000 +max_proposal_content_size = 10000 # minimum epochs between end and grace epoch -min_proposal_grace_epochs = 6 +min_proposal_grace_epochs = 6 \ No newline at end of file diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index 83fbf74301c..500761f19d0 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -4,7 +4,7 @@ genesis_time = "2021-09-30T10:00:00Z" native_token = "NAM" -faucet_pow_difficulty = 1 +faucet_pow_difficulty = 0 faucet_withdrawal_limit = "1000" [validator.validator-0] @@ -172,6 +172,8 @@ epochs_per_year = 31_536_000 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 # Proof of stake parameters. [pos_params] @@ -213,7 +215,7 @@ min_proposal_fund = 500 # proposal code size in bytes max_proposal_code_size = 1000000 # min proposal period length in epochs -min_proposal_period = 3 +min_proposal_voting_period = 3 # max proposal period length in epochs max_proposal_period = 27 # maximum number of characters in the proposal content diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 4c087dbbd63..58573166f12 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -869,7 +869,7 @@ pub fn is_validator( address: &Address, ) -> storage_api::Result where - S: StorageRead + StorageWrite, + S: StorageRead, { let rate = read_validator_max_commission_rate_change(storage, address)?; Ok(rate.is_some()) diff --git a/scripts/generator.sh b/scripts/generator.sh index 701f8fbabe9..3fe1792a494 100755 --- a/scripts/generator.sh +++ b/scripts/generator.sh @@ -212,79 +212,79 @@ elif [ "$1" = "client" ]; then # proposal_submission - cargo run --bin namadac --features std -- --mode full bond --validator validator-0 --source Bertha --amount 900 --gas-amount 0 --gas-limit 0 --gas-token NAM --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- bond --validator validator-0 --source Bertha --amount 900 --gas-amount 0 --gas-limit 0 --gas-token NAM --node 127.0.0.1:27657 - cargo run --bin namadac --features std -- --mode full change-commission-rate --validator Bertha --commission-rate 0.02 --gas-amount 0 --gas-limit 0 --gas-token NAM --force --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- change-commission-rate --validator Bertha --commission-rate 0.02 --gas-amount 0 --gas-limit 0 --gas-token NAM --force --node 127.0.0.1:27657 - PROPOSAL_ID_0=$(cargo run --bin namadac --features std -- --mode full init-proposal --force --data-path proposal_submission_valid_proposal.json --node 127.0.0.1:27657 | grep -o -P '(?<=/proposal/).*(?=/author)') + PROPOSAL_ID_0=$(cargo run --bin namadac --features std -- init-proposal --force --data-path proposal_submission_valid_proposal.json --node 127.0.0.1:27657 | grep -o -P '(?<=/proposal/).*(?=/author)') - cargo run --bin namadac --features std -- --base-dir $NAMADA_BASE_DIR/setup/validator-0/.namada --mode validator vote-proposal --force --proposal-id $PROPOSAL_ID_0 --vote yay --signer validator-0 --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- --base-dir $NAMADA_BASE_DIR/setup/validator-0/.namada vote-proposal --force --proposal-id $PROPOSAL_ID_0 --vote yay --address validator-0 --node 127.0.0.1:27657 - cargo run --bin namadac --features std -- --mode full vote-proposal --force --proposal-id $PROPOSAL_ID_0 --vote nay --signer Bertha --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- vote-proposal --force --proposal-id $PROPOSAL_ID_0 --vote nay --address Bertha --node 127.0.0.1:27657 - cargo run --bin namadac --features std -- --mode full vote-proposal --force --proposal-id $PROPOSAL_ID_0 --vote yay --signer Albert --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- vote-proposal --force --proposal-id $PROPOSAL_ID_0 --vote yay --address Albert --node 127.0.0.1:27657 # proposal_offline - cargo run --bin namadac --features std -- --mode full bond --validator validator-0 --source Albert --amount 900 --gas-amount 0 --gas-limit 0 --gas-token NAM --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- bond --validator validator-0 --source Albert --amount 900 --gas-amount 0 --gas-limit 0 --gas-token NAM --node 127.0.0.1:27657 - cargo run --bin namadac --features std -- --mode full change-commission-rate --validator Albert --commission-rate 0.05 --gas-amount 0 --gas-limit 0 --gas-token NAM --force --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- change-commission-rate --validator Albert --commission-rate 0.05 --gas-amount 0 --gas-limit 0 --gas-token NAM --force --node 127.0.0.1:27657 - cargo run --bin namadac --features std -- --mode full init-proposal --force --data-path proposal_offline_valid_proposal.json --offline --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- init-proposal --force --data-path proposal_offline_valid_proposal.json --offline --node 127.0.0.1:27657 - cargo run --bin namadac --features std -- --mode full vote-proposal --data-path proposal_offline_proposal --vote yay --signer Albert --offline --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- vote-proposal --data-path proposal_offline_proposal --vote yay --address Albert --offline --node 127.0.0.1:27657 # eth_governance_proposal - cargo run --bin namadac --features std -- --mode full bond --validator validator-0 --source Bertha --amount 900 --gas-amount 0 --gas-limit 0 --gas-token NAM --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- bond --validator validator-0 --source Bertha --amount 900 --gas-amount 0 --gas-limit 0 --gas-token NAM --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- --mode full change-commission-rate --validator Bertha --commission-rate 0.07 --gas-amount 0 --gas-limit 0 --gas-token NAM --force --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- change-commission-rate --validator Bertha --commission-rate 0.07 --gas-amount 0 --gas-limit 0 --gas-token NAM --force --node 127.0.0.1:27657 - PROPOSAL_ID_0=$(cargo run --bin namadac --features std -- --mode full init-proposal --force --data-path eth_governance_proposal_valid_proposal.json --ledger-address 127.0.0.1:27657 | grep -o -P '(?<=/proposal/).*(?=/author)') + PROPOSAL_ID_0=$(cargo run --bin namadac --features std -- init-proposal --force --data-path eth_governance_proposal_valid_proposal.json --ledger-address 127.0.0.1:27657 | grep -o -P '(?<=/proposal/).*(?=/author)') - cargo run --bin namadac --features std -- --mode full vote-proposal --force --proposal-id 0 --vote yay --eth '011586062748ba53bc53155e817ec1ea708de75878dcb9a5713bf6986d87fe14e7 fd34672ab5' --signer Bertha --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- vote-proposal --force --proposal-id 0 --vote yay --eth '011586062748ba53bc53155e817ec1ea708de75878dcb9a5713bf6986d87fe14e7 fd34672ab5' --address Bertha --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- --base-dir $NAMADA_BASE_DIR/setup/validator-0/.namada --mode validator vote-proposal --force --proposal-id $PROPOSAL_ID_0 --vote yay --eth '011586062748ba53bc53155e817ec1ea708de75878dcb9a5713bf6986d87fe14e7 fd34672ab5' --signer validator-0 --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- --base-dir $NAMADA_BASE_DIR/setup/validator-0/.namada vote-proposal --force --proposal-id $PROPOSAL_ID_0 --vote yay --eth '011586062748ba53bc53155e817ec1ea708de75878dcb9a5713bf6986d87fe14e7 fd34672ab5' --address validator-0 --ledger-address 127.0.0.1:27657 # pgf_governance_proposal - cargo run --bin namadac --features std -- --mode full bond --validator validator-0 --source Bertha --amount 900 --gas-amount 0 --gas-limit 0 --gas-token NAM --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- bond --validator validator-0 --source Bertha --amount 900 --gas-amount 0 --gas-limit 0 --gas-token NAM --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- --mode full change-commission-rate --validator Bertha --commission-rate 0.09 --gas-amount 0 --gas-limit 0 --gas-token NAM --force --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- change-commission-rate --validator Bertha --commission-rate 0.09 --gas-amount 0 --gas-limit 0 --gas-token NAM --force --node 127.0.0.1:27657 - PROPOSAL_ID_0=$(cargo run --bin namadac --features std -- --mode full init-proposal --force --data-path pgf_governance_proposal_valid_proposal.json --ledger-address 127.0.0.1:27657 | grep -o -P '(?<=/proposal/).*(?=/author)') + PROPOSAL_ID_0=$(cargo run --bin namadac --features std -- init-proposal --force --data-path pgf_governance_proposal_valid_proposal.json --ledger-address 127.0.0.1:27657 | grep -o -P '(?<=/proposal/).*(?=/author)') - PROPOSAL_ID_1=$(cargo run --bin namadac --features std -- --mode full init-proposal --force --data-path pgf_governance_proposal_valid_proposal.json --ledger-address 127.0.0.1:27657 | grep -o -P '(?<=/proposal/).*(?=/author)') + PROPOSAL_ID_1=$(cargo run --bin namadac --features std -- init-proposal --force --data-path pgf_governance_proposal_valid_proposal.json --ledger-address 127.0.0.1:27657 | grep -o -P '(?<=/proposal/).*(?=/author)') - cargo run --bin namadac --features std -- --base-dir $NAMADA_BASE_DIR/setup/validator-0/.namada --mode validator vote-proposal --force --proposal-id $PROPOSAL_ID_0 --vote yay --pgf "$ALBERT_ADDRESS 1000" --signer validator-0 --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- --base-dir $NAMADA_BASE_DIR/setup/validator-0/.namada vote-proposal --force --proposal-id $PROPOSAL_ID_0 --vote yay --pgf "$ALBERT_ADDRESS 1000" --address validator-0 --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- --mode full vote-proposal --force --proposal-id $PROPOSAL_ID_0 --vote yay --pgf "$ALBERT_ADDRESS 900" --signer Bertha --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- vote-proposal --force --proposal-id $PROPOSAL_ID_0 --vote yay --pgf "$ALBERT_ADDRESS 900" --address Bertha --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- --mode full vote-proposal --force --proposal-id $PROPOSAL_ID_1 --vote yay --pgf "$ALBERT_ADDRESS 900" --signer Bertha --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- vote-proposal --force --proposal-id $PROPOSAL_ID_1 --vote yay --pgf "$ALBERT_ADDRESS 900" --address Bertha --ledger-address 127.0.0.1:27657 # non-proposal tests - cargo run --bin namadac --features std -- transfer --source bertha --target christel --token btc --amount 23 --force --signing-key bertha-key --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- transfer --source bertha --target christel --token btc --amount 23 --force --signing-keys bertha-key --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- bond --validator bertha --amount 25 --signing-key bertha-key --force --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- bond --validator bertha --amount 25 --signing-keys bertha-key --force --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- --mode full change-commission-rate --validator Bertha --commission-rate 0.11 --gas-amount 0 --gas-limit 0 --gas-token NAM --force --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- change-commission-rate --validator Bertha --commission-rate 0.11 --gas-amount 0 --gas-limit 0 --gas-token NAM --force --node 127.0.0.1:27657 - cargo run --bin namadac --features std -- reveal-pk --public-key albert-key --force --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- reveal-pk --public-key albert-key --gas-payer albert-key --force --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- update --code-path vp_user.wasm --address bertha --signing-key bertha-key --force --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- update-account --code-path vp_user.wasm --address bertha --signing-keys bertha-key --force --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- init-validator --alias bertha-validator --source bertha --commission-rate 0.05 --max-commission-rate-change 0.01 --signing-key bertha-key --unsafe-dont-encrypt --force --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- init-validator --alias bertha-validator --account-keys bertha --commission-rate 0.05 --max-commission-rate-change 0.01 --signing-keys bertha-key --unsafe-dont-encrypt --force --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- unbond --validator christel --amount 5 --signing-key christel-key --force --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- unbond --validator christel --amount 5 --signing-keys christel-key --force --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- withdraw --validator albert --signing-key albert-key --force --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- withdraw --validator albert --signing-keys albert-key --force --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- init-account --alias albert-account --source albert --public-key albert-key --signing-key albert-key --force --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- init-account --alias albert-account --public-keys albert-key --signing-keys albert-key --force --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- tx --code-path $NAMADA_DIR/wasm_for_tests/tx_no_op.wasm --data-path README.md --signing-key albert-key --force --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- tx --code-path $NAMADA_DIR/wasm_for_tests/tx_no_op.wasm --data-path README.md --signing-keys albert-key --owner albert --force --ledger-address 127.0.0.1:27657 - cargo run --bin namadac --features std -- ibc-transfer --source bertha --receiver christel --token btc --amount 24 --channel-id channel-141 --signing-key bertha-key --force --ledger-address 127.0.0.1:27657 + cargo run --bin namadac --features std -- ibc-transfer --source bertha --receiver christel --token btc --amount 24 --channel-id channel-141 --signing-keys bertha-key --force --ledger-address 127.0.0.1:27657 cargo run --bin namadaw -- masp add --alias a_spending_key --value xsktest1qqqqqqqqqqqqqq9v0sls5r5de7njx8ehu49pqgmqr9ygelg87l5x8y4s9r0pjlvu69au6gn3su5ewneas486hdccyayx32hxvt64p3d0hfuprpgcgv2q9gdx3jvxrn02f0nnp3jtdd6f5vwscfuyum083cvfv4jun75ak5sdgrm2pthzj3sflxc0jx0edrakx3vdcngrfjmru8ywkguru8mxss2uuqxdlglaz6undx5h8w7g70t2es850g48xzdkqay5qs0yw06rtxcvedhsv --unsafe-dont-encrypt diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 029c77ff0e7..309ef97b945 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -99,9 +99,13 @@ pub struct TxCustom { /// Common tx arguments pub tx: Tx, /// Path to the tx WASM code file - pub code_path: C::Data, + pub code_path: Option, /// Path to the data file pub data_path: Option, + /// Path to the serialized transaction + pub serialized_tx: Option, + /// The address that correspond to the signatures/signing-keys + pub owner: C::Address, } /// Transfer transaction arguments @@ -159,19 +163,57 @@ pub struct TxIbcTransfer { pub tx_code_path: PathBuf, } +/// Transaction to initialize create a new proposal +#[derive(Clone, Debug)] +pub struct InitProposal { + /// Common tx arguments + pub tx: Tx, + /// The proposal data + pub proposal_data: C::Data, + /// Native token address + pub native_token: C::NativeAddress, + /// Flag if proposal should be run offline + pub is_offline: bool, + /// Flag if proposal is of type Pgf stewards + pub is_pgf_stewards: bool, + /// Flag if proposal is of type Pgf funding + pub is_pgf_funding: bool, + /// Path to the tx WASM file + pub tx_code_path: PathBuf, +} + +/// Transaction to vote on a proposal +#[derive(Clone, Debug)] +pub struct VoteProposal { + /// Common tx arguments + pub tx: Tx, + /// Proposal id + pub proposal_id: Option, + /// The vote + pub vote: String, + /// The address of the voter + pub voter: C::Address, + /// Flag if proposal vote should be run offline + pub is_offline: bool, + /// The proposal file path + pub proposal_data: Option, + /// Path to the TX WASM code file + pub tx_code_path: PathBuf, +} + /// Transaction to initialize a new account #[derive(Clone, Debug)] pub struct TxInitAccount { /// Common tx arguments pub tx: Tx, - /// Address of the source account - pub source: C::Address, /// Path to the VP WASM code file for the new account pub vp_code_path: PathBuf, /// Path to the TX WASM code file pub tx_code_path: PathBuf, /// Public key for the new account - pub public_key: C::PublicKey, + pub public_keys: Vec, + /// The account multisignature threshold + pub threshold: Option, } /// Transaction to initialize a new account @@ -179,12 +221,12 @@ pub struct TxInitAccount { pub struct TxInitValidator { /// Common tx arguments pub tx: Tx, - /// Source - pub source: C::Address, /// Signature scheme pub scheme: SchemeType, - /// Account key - pub account_key: Option, + /// Account keys + pub account_keys: Vec, + /// The account multisignature threshold + pub threshold: Option, /// Consensus key pub consensus_key: Option, /// Ethereum cold key @@ -207,15 +249,19 @@ pub struct TxInitValidator { /// Transaction to update a VP arguments #[derive(Clone, Debug)] -pub struct TxUpdateVp { +pub struct TxUpdateAccount { /// Common tx arguments pub tx: Tx, /// Path to the VP WASM code file - pub vp_code_path: PathBuf, + pub vp_code_path: Option, /// Path to the TX WASM code file pub tx_code_path: PathBuf, /// Address of the account whose VP is to be updated pub addr: C::Address, + /// Public keys + pub public_keys: Vec, + /// The account threshold + pub threshold: Option, } /// Bond arguments @@ -277,6 +323,13 @@ pub struct QueryProtocolParameters { pub query: Query, } +/// Query pgf data +#[derive(Clone, Debug)] +pub struct QueryPgf { + /// Common query args + pub query: Query, +} + /// Withdraw arguments #[derive(Clone, Debug)] pub struct Withdraw { @@ -302,6 +355,15 @@ pub struct QueryConversions { pub epoch: Option, } +/// Query token balance(s) +#[derive(Clone, Debug)] +pub struct QueryAccount { + /// Common query args + pub query: Query, + /// Address of an owner + pub owner: C::Address, +} + /// Query token balance(s) #[derive(Clone, Debug)] pub struct QueryBalance { @@ -383,6 +445,17 @@ pub struct TxUnjailValidator { pub tx_code_path: PathBuf, } +#[derive(Clone, Debug)] +/// Sign a transaction offline +pub struct SignTx { + /// Common tx arguments + pub tx: Tx, + /// Transaction data + pub tx_data: C::Data, + /// The account address + pub owner: C::Address, +} + /// Query PoS commission rate #[derive(Clone, Debug)] pub struct QueryCommissionRate { @@ -435,8 +508,10 @@ pub struct QueryRawBytes { pub struct Tx { /// Simulate applying the transaction pub dry_run: bool, - /// Dump the transaction bytes + /// Dump the transaction bytes to file pub dump_tx: bool, + /// The output directory path to where serialize the data + pub output_folder: Option, /// Submit the transaction even if it doesn't pass client checks pub force: bool, /// Do not wait for the transaction to be added to the blockchain @@ -449,10 +524,12 @@ pub struct Tx { /// Whether to force overwrite the above alias, if it is provided, in the /// wallet. pub wallet_alias_force: bool, + /// The fee payer signing key + pub gas_payer: Option, /// The amount being payed to include the transaction - pub fee_amount: InputAmount, + pub gas_amount: InputAmount, /// The token in which the fee is being paid - pub fee_token: C::Address, + pub gas_token: C::Address, /// The max amount of gas used to process tx pub gas_limit: GasLimit, /// The optional expiration of the transaction @@ -460,9 +537,9 @@ pub struct Tx { /// The chain id for which the transaction is intended pub chain_id: Option, /// Sign the tx with the key for the given alias from your wallet - pub signing_key: Option, - /// Sign the tx with the keypair of the public key of the given address - pub signer: Option, + pub signing_keys: Vec, + /// List of signatures to attach to the transaction + pub signatures: Vec, /// Path to the TX WASM code file to reveal PK pub tx_reveal_code_path: PathBuf, /// Sign the tx with the public key for the given alias from your wallet @@ -633,11 +710,11 @@ pub struct EthereumBridgePool { /// The amount to be transferred pub amount: InputAmount, /// The amount of fees (in NAM) - pub gas_amount: token::Amount, + pub fee_amount: token::Amount, /// The account of fee payer. - pub gas_payer: C::Address, + pub fee_payer: C::Address, /// Path to the tx WASM code file - pub code_path: C::Data, + pub code_path: PathBuf, } /// Bridge pool proof arguments. diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 30b1f4e1714..867881c1fba 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -18,11 +18,8 @@ use crate::eth_bridge::ethers::abi::AbiDecode; use crate::eth_bridge::structs::RelayProof; use crate::ledger::args; use crate::ledger::queries::{Client, RPC}; -use crate::ledger::rpc::validate_amount; -use crate::ledger::signing::TxSigningKey; +use crate::ledger::rpc::{query_wasm_code_hash, validate_amount}; use crate::ledger::tx::{prepare_tx, Error}; -use crate::ledger::wallet::{Wallet, WalletUtils}; -use crate::proto::{Code, Data, Tx}; use crate::types::address::Address; use crate::types::control_flow::time::{Duration, Instant}; use crate::types::control_flow::{ @@ -34,62 +31,59 @@ use crate::types::eth_bridge_pool::{ }; use crate::types::keccak::KeccakHash; use crate::types::token::{Amount, DenominatedAmount}; -use crate::types::transaction::TxType; +use crate::types::tx::TxBuilder; use crate::types::voting_power::FractionalVotingPower; /// Craft a transaction that adds a transfer to the Ethereum bridge pool. -pub async fn build_bridge_pool_tx< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( +pub async fn build_bridge_pool_tx( client: &C, - wallet: &mut Wallet, - args: args::EthereumBridgePool, -) -> Result<(Tx, Option
, common::PublicKey), Error> { - let args::EthereumBridgePool { - ref tx, + args::EthereumBridgePool { + tx, asset, recipient, sender, amount, - gas_amount, - gas_payer, - code_path: wasm_code, - } = args; + fee_amount, + fee_payer, + code_path, + }: args::EthereumBridgePool, + gas_payer: common::PublicKey, +) -> Result { let DenominatedAmount { amount, .. } = validate_amount(client, amount, &BRIDGE_ADDRESS, tx.force) .await .expect("Failed to validate amount"); + let transfer = PendingTransfer { transfer: TransferToEthereum { asset, recipient, - sender, + sender: sender.clone(), amount, }, gas_fee: GasFee { - amount: gas_amount, - payer: gas_payer, + amount: fee_amount, + payer: fee_payer, }, }; - let mut transfer_tx = Tx::new(TxType::Raw); - transfer_tx.header.chain_id = tx.chain_id.clone().unwrap(); - transfer_tx.header.expiration = tx.expiration; - transfer_tx.set_data(Data::new( - transfer - .try_to_vec() - .expect("Serializing tx should not fail"), - )); - // TODO: change the wasm code to a hash - transfer_tx.set_code(Code::new(wasm_code)); - - prepare_tx::( + let tx_code_hash = + query_wasm_code_hash(client, code_path.to_str().unwrap()) + .await + .unwrap(); + + let chain_id = tx.chain_id.clone().unwrap(); + let tx_builder = TxBuilder::new(chain_id, tx.expiration); + + let tx_builder = tx_builder + .add_code_from_hash(tx_code_hash) + .add_data(transfer); + + prepare_tx::( client, - wallet, - tx, - transfer_tx, - TxSigningKey::None, + &tx, + tx_builder, + gas_payer.clone(), #[cfg(not(feature = "mainnet"))] false, ) diff --git a/shared/src/ledger/events.rs b/shared/src/ledger/events.rs index 03e68417e28..0b5aa0253bc 100644 --- a/shared/src/ledger/events.rs +++ b/shared/src/ledger/events.rs @@ -10,7 +10,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use thiserror::Error; -use crate::ledger::native_vp::governance::utils::ProposalEvent; +use crate::ledger::governance::utils::ProposalEvent; use crate::tendermint_proto::abci::EventAttribute; use crate::types::ibc::IbcEvent; #[cfg(feature = "ferveo-tpke")] @@ -50,6 +50,8 @@ pub enum EventType { Ibc(String), /// The proposal that has been executed Proposal, + /// The pgf payment + PgfPayment, } impl Display for EventType { @@ -59,6 +61,7 @@ impl Display for EventType { EventType::Applied => write!(f, "applied"), EventType::Ibc(t) => write!(f, "{}", t), EventType::Proposal => write!(f, "proposal"), + EventType::PgfPayment => write!(f, "pgf_payment"), }?; Ok(()) } @@ -72,6 +75,7 @@ impl FromStr for EventType { "accepted" => Ok(EventType::Accepted), "applied" => Ok(EventType::Applied), "proposal" => Ok(EventType::Proposal), + "pgf_payments" => Ok(EventType::PgfPayment), // IBC "update_client" => Ok(EventType::Ibc("update_client".to_string())), "send_packet" => Ok(EventType::Ibc("send_packet".to_string())), diff --git a/shared/src/ledger/native_vp/governance/mod.rs b/shared/src/ledger/governance/mod.rs similarity index 50% rename from shared/src/ledger/native_vp/governance/mod.rs rename to shared/src/ledger/governance/mod.rs index 7d9323f256c..0ec7af6c435 100644 --- a/shared/src/ledger/native_vp/governance/mod.rs +++ b/shared/src/ledger/governance/mod.rs @@ -4,18 +4,22 @@ pub mod utils; use std::collections::BTreeSet; -use namada_core::ledger::governance::storage as gov_storage; -use namada_core::ledger::storage; +use borsh::BorshDeserialize; +use namada_core::ledger::governance::storage::keys as gov_storage; +use namada_core::ledger::governance::storage::proposal::ProposalType; +use namada_core::ledger::governance::storage::vote::StorageProposalVote; +use namada_core::ledger::governance::utils::is_valid_validator_voting_period; +use namada_core::ledger::storage_api::governance::is_proposal_accepted; use namada_core::ledger::vp_env::VpEnv; -use namada_core::types::governance::{ProposalVote, VoteType}; -use namada_core::types::transaction::governance::ProposalType; +use namada_core::ledger::{pgf, storage}; +use namada_core::proto::Tx; +use namada_proof_of_stake::is_validator; use thiserror::Error; -use utils::is_valid_validator_voting_period; +use self::utils::ReadType; use crate::ledger::native_vp::{Ctx, NativeVp}; use crate::ledger::storage_api::StorageRead; use crate::ledger::{native_vp, pos}; -use crate::proto::Tx; use crate::types::address::{Address, InternalAddress}; use crate::types::storage::{Epoch, Key}; use crate::types::token; @@ -24,11 +28,23 @@ use crate::vm::WasmCacheAccess; /// for handling Governance NativeVP errors pub type Result = std::result::Result; +/// The governance internal address +pub const ADDRESS: Address = Address::Internal(InternalAddress::Governance); + +/// The maximum number of item in a pgf proposal +pub const MAX_PGF_ACTIONS: usize = 20; + #[allow(missing_docs)] #[derive(Error, Debug)] pub enum Error { #[error("Native VP error: {0}")] NativeVpError(#[from] native_vp::Error), + #[error("Proposal field should not be empty: {0}")] + EmptyProposalField(String), + #[error("Vote key is not valid: {0}")] + InvalidVoteKey(String), + #[error("Vote type is not compatible with proposal type.")] + InvalidVoteType, } /// Governance VP @@ -101,13 +117,7 @@ where (KeyType::PROPOSAL_COMMIT, _) => { self.is_valid_proposal_commit() } - (KeyType::PARAMETER, _) => self.is_valid_parameter( - if let Some(data) = &tx_data.data() { - data - } else { - return false; - }, - ), + (KeyType::PARAMETER, _) => self.is_valid_parameter(tx_data), (KeyType::BALANCE, _) => self.is_valid_balance(&native_token), (KeyType::UNKNOWN_GOVERNANCE, _) => Ok(false), (KeyType::UNKNOWN, _) => Ok(true), @@ -128,10 +138,9 @@ where { fn is_valid_key_set(&self, keys: &BTreeSet) -> Result<(bool, u64)> { let counter_key = gov_storage::get_counter_key(); - let pre_counter: u64 = - self.ctx.pre().read(&counter_key)?.unwrap_or_default(); + let pre_counter: u64 = self.force_read(&counter_key, ReadType::Pre)?; let post_counter: u64 = - self.ctx.post().read(&counter_key)?.unwrap_or_default(); + self.force_read(&counter_key, ReadType::Post)?; if post_counter < pre_counter { return Ok((false, 0)); @@ -152,7 +161,7 @@ where gov_storage::get_grace_epoch_key(counter), ]); - // Check that expected set is a subset the actual one + // Check that expected set is a subset of the actual one if !keys.is_superset(&mandatory_keys) { return Ok((false, 0)); } @@ -172,122 +181,80 @@ where gov_storage::get_voting_start_epoch_key(proposal_id); let voting_end_epoch_key = gov_storage::get_voting_end_epoch_key(proposal_id); + let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); - let current_epoch = self.ctx.get_block_epoch().ok(); + let current_epoch = self.ctx.get_block_epoch()?; - let pre_counter: Option = self.ctx.pre().read(&counter_key)?; - let pre_voting_start_epoch: Option = - self.ctx.pre().read(&voting_start_epoch_key)?; - let pre_voting_end_epoch: Option = - self.ctx.pre().read(&voting_end_epoch_key)?; + let pre_counter: u64 = self.force_read(&counter_key, ReadType::Pre)?; + let pre_voting_start_epoch: Epoch = + self.force_read(&voting_start_epoch_key, ReadType::Pre)?; + let pre_voting_end_epoch: Epoch = + self.force_read(&voting_end_epoch_key, ReadType::Pre)?; + let proposal_type: ProposalType = + self.force_read(&proposal_type_key, ReadType::Pre)?; let voter = gov_storage::get_voter_address(key); let delegation_address = gov_storage::get_vote_delegation_address(key); - let vote: Option = self.ctx.read_post(key)?; - let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); - let proposal_type: Option = - self.ctx.read_pre(&proposal_type_key)?; - - match ( - pre_counter, - proposal_type, - vote, - voter, - delegation_address, + let vote: StorageProposalVote = self.force_read(key, ReadType::Post)?; + + let (voter_address, delegation_address) = + match (voter, delegation_address) { + (Some(voter_address), Some(delegator_address)) => { + (voter_address, delegator_address) + } + _ => return Err(Error::InvalidVoteKey(key.to_string())), + }; + + // Invalid proposal id + if pre_counter <= proposal_id { + return Ok(false); + } + + // Voted outside of voting window. We dont check for validator because + // if the proposal type is validator, we need to let + // them vote for the entire voting window. + if !self.is_valid_voting_window( current_epoch, pre_voting_start_epoch, pre_voting_end_epoch, + false, ) { - ( - Some(pre_counter), - Some(proposal_type), - Some(vote), - Some(voter_address), - Some(delegation_address), - Some(current_epoch), - Some(pre_voting_start_epoch), - Some(pre_voting_end_epoch), - ) => { - if pre_counter <= proposal_id { - // Invalid proposal id - return Ok(false); - } - if current_epoch < pre_voting_start_epoch - || current_epoch > pre_voting_end_epoch - { - // Voted outside of voting window - return Ok(false); - } + return Ok(false); + } - if let ProposalVote::Yay(vote_type) = vote { - if proposal_type != vote_type { - return Ok(false); - } - - // Vote type specific checks - if let VoteType::PGFCouncil(set) = vote_type { - // Check that all the addresses are established - for (address, _) in set { - match address { - Address::Established(_) => { - // Check that established address exists in - // storage - let vp_key = - Key::validity_predicate(&address); - if !self.ctx.has_key_pre(&vp_key)? { - return Ok(false); - } - } - _ => return Ok(false), - } - } - } else if let VoteType::ETHBridge(_sig) = vote_type { - // TODO: Check the validity of the signature with the - // governance ETH key in storage for the given validator - // - } - } + if !vote.is_compatible(&proposal_type) { + return Err(Error::InvalidVoteType); + } - match proposal_type { - ProposalType::Default(_) | ProposalType::PGFCouncil => { - if self - .is_validator( - pre_voting_start_epoch, - verifiers, - voter_address, - delegation_address, - ) - .unwrap_or(false) - { - Ok(is_valid_validator_voting_period( - current_epoch, - pre_voting_start_epoch, - pre_voting_end_epoch, - )) - } else { - Ok(self - .is_delegator( - pre_voting_start_epoch, - verifiers, - voter_address, - delegation_address, - ) - .unwrap_or(false)) - } - } - ProposalType::ETHBridge => Ok(self - .is_validator( - pre_voting_start_epoch, - verifiers, - voter_address, - delegation_address, - ) - .unwrap_or(false)), - } - } - _ => Ok(false), + // first check if validator, then check if delegator + let is_validator = self + .is_validator( + pre_voting_start_epoch, + verifiers, + voter_address, + delegation_address, + ) + .unwrap_or(false); + + if is_validator { + let valid_voting_period = is_valid_validator_voting_period( + current_epoch, + pre_voting_start_epoch, + pre_voting_end_epoch, + ); + return Ok(valid_voting_period); } + + let is_delegator = self + .is_delegator( + pre_voting_start_epoch, + verifiers, + voter_address, + delegation_address, + ) + .unwrap_or(false); + Ok(is_delegator) } /// Validate a content key @@ -301,39 +268,53 @@ where return Ok(false); } - let max_content_length: Option = - self.ctx.pre().read(&max_content_length_parameter_key)?; - let post_content: Option> = - self.ctx.read_bytes_post(&content_key)?; + let max_content_length: usize = + self.force_read(&max_content_length_parameter_key, ReadType::Pre)?; + let post_content = + self.ctx.read_bytes_post(&content_key)?.unwrap_or_default(); - match (post_content, max_content_length) { - (Some(post_content), Some(max_content_length)) => { - Ok(post_content.len() < max_content_length) - } - _ => Ok(false), - } + Ok(post_content.len() < max_content_length) } /// Validate the proposal type pub fn is_valid_proposal_type(&self, proposal_id: u64) -> Result { let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); - Ok(self - .ctx - .read_post::(&proposal_type_key)? - .is_some()) + let proposal_type: ProposalType = + self.force_read(&proposal_type_key, ReadType::Post)?; + + match proposal_type { + ProposalType::PGFSteward(stewards) => { + Ok(stewards.len() < MAX_PGF_ACTIONS) + } + ProposalType::PGFPayment(payments) => { + if payments.len() < MAX_PGF_ACTIONS { + return Ok(true); + } + let stewards_key = pgf::storage::keys::get_stewards_key(); + let author_key = gov_storage::get_author_key(proposal_id); + + let author: Option
= + self.ctx.pre().read(&author_key)?; + let stewards: BTreeSet
= + self.force_read(&stewards_key, ReadType::Pre)?; + + match author { + Some(address) => Ok(stewards.contains(&address)), + None => Ok(false), + } + } + _ => Ok(true), // default proposal + } } /// Validate a proposal code pub fn is_valid_proposal_code(&self, proposal_id: u64) -> Result { - let proposal_type_key: Key = - gov_storage::get_proposal_type_key(proposal_id); - let proposal_type: Option = - self.ctx.read_post(&proposal_type_key)?; + let proposal_type_key = gov_storage::get_proposal_type_key(proposal_id); + let proposal_type: ProposalType = + self.force_read(&proposal_type_key, ReadType::Post)?; - // Check that the proposal type admits wasm code - match proposal_type { - Some(ProposalType::Default(_)) => (), - _ => return Ok(false), + if !proposal_type.is_default() { + return Ok(false); } let code_key = gov_storage::get_proposal_code_key(proposal_id); @@ -345,22 +326,21 @@ where return Ok(false); } - let max_proposal_length: Option = - self.ctx.pre().read(&max_code_size_parameter_key)?; - let post_code: Option> = self.ctx.read_bytes_post(&code_key)?; + let max_proposal_length: usize = + self.force_read(&max_code_size_parameter_key, ReadType::Pre)?; + let post_code: Vec = + self.ctx.read_bytes_post(&code_key)?.unwrap_or_default(); - match (post_code, max_proposal_length) { - (Some(post_code), Some(max_content_length)) => { - Ok(post_code.len() < max_content_length) - } - _ => Ok(false), - } + Ok(post_code.len() <= max_proposal_length) } /// Validate a grace_epoch key pub fn is_valid_grace_epoch(&self, proposal_id: u64) -> Result { + let start_epoch_key = + gov_storage::get_voting_start_epoch_key(proposal_id); let end_epoch_key = gov_storage::get_voting_end_epoch_key(proposal_id); let grace_epoch_key = gov_storage::get_grace_epoch_key(proposal_id); + let max_proposal_period = gov_storage::get_max_proposal_period_key(); let min_grace_epoch_key = gov_storage::get_min_proposal_grace_epoch_key(); @@ -369,27 +349,32 @@ where return Ok(false); } - let end_epoch: Option = self.ctx.post().read(&end_epoch_key)?; - let grace_epoch: Option = - self.ctx.post().read(&grace_epoch_key)?; - let min_grace_epoch: Option = - self.ctx.pre().read(&min_grace_epoch_key)?; - match (min_grace_epoch, grace_epoch, end_epoch) { - (Some(min_grace_epoch), Some(grace_epoch), Some(end_epoch)) => { - let committing_epoch_key = - gov_storage::get_committing_proposals_key( - proposal_id, - grace_epoch, - ); - let has_post_committing_epoch = - self.ctx.has_key_post(&committing_epoch_key)?; - - Ok(has_post_committing_epoch - && end_epoch < grace_epoch - && grace_epoch - end_epoch >= min_grace_epoch) - } - _ => Ok(false), - } + let start_epoch: Epoch = + self.force_read(&start_epoch_key, ReadType::Post)?; + let end_epoch: Epoch = + self.force_read(&end_epoch_key, ReadType::Post)?; + let grace_epoch: Epoch = + self.force_read(&grace_epoch_key, ReadType::Post)?; + let min_grace_epoch: u64 = + self.force_read(&min_grace_epoch_key, ReadType::Pre)?; + let max_proposal_period: u64 = + self.force_read(&max_proposal_period, ReadType::Pre)?; + + let committing_epoch_key = gov_storage::get_committing_proposals_key( + proposal_id, + grace_epoch.into(), + ); + let has_post_committing_epoch = + self.ctx.has_key_post(&committing_epoch_key)?; + + let is_valid_grace_epoch = end_epoch < grace_epoch + && (grace_epoch - end_epoch).0 >= min_grace_epoch; + let is_valid_max_proposal_perido = start_epoch < grace_epoch + && grace_epoch.0 - start_epoch.0 <= max_proposal_period; + + Ok(has_post_committing_epoch + && is_valid_grace_epoch + && is_valid_max_proposal_perido) } /// Validate a start_epoch key @@ -398,9 +383,9 @@ where gov_storage::get_voting_start_epoch_key(proposal_id); let end_epoch_key = gov_storage::get_voting_end_epoch_key(proposal_id); let min_period_parameter_key = - gov_storage::get_min_proposal_period_key(); + gov_storage::get_min_proposal_voting_period_key(); - let current_epoch = self.ctx.get_block_epoch().ok(); + let current_epoch = self.ctx.get_block_epoch()?; let has_pre_start_epoch = self.ctx.has_key_pre(&start_epoch_key)?; let has_pre_end_epoch = self.ctx.has_key_pre(&end_epoch_key)?; @@ -409,27 +394,19 @@ where return Ok(false); } - let start_epoch: Option = - self.ctx.post().read(&start_epoch_key)?; - let end_epoch: Option = self.ctx.post().read(&end_epoch_key)?; - let min_period: Option = - self.ctx.pre().read(&min_period_parameter_key)?; - - match (min_period, start_epoch, end_epoch, current_epoch) { - ( - Some(min_period), - Some(start_epoch), - Some(end_epoch), - Some(current_epoch), - ) => { - if end_epoch <= start_epoch || start_epoch <= current_epoch { - return Ok(false); - } - Ok((end_epoch - start_epoch) % min_period == 0 - && (end_epoch - start_epoch).0 >= min_period) - } - _ => Ok(false), + let start_epoch: Epoch = + self.force_read(&start_epoch_key, ReadType::Post)?; + let end_epoch: Epoch = + self.force_read(&end_epoch_key, ReadType::Post)?; + let min_period: u64 = + self.force_read(&min_period_parameter_key, ReadType::Pre)?; + + if end_epoch <= start_epoch || start_epoch <= current_epoch { + return Ok(false); } + + Ok((end_epoch - start_epoch) % min_period == 0 + && (end_epoch - start_epoch).0 >= min_period) } /// Validate a end_epoch key @@ -438,11 +415,11 @@ where gov_storage::get_voting_start_epoch_key(proposal_id); let end_epoch_key = gov_storage::get_voting_end_epoch_key(proposal_id); let min_period_parameter_key = - gov_storage::get_min_proposal_period_key(); + gov_storage::get_min_proposal_voting_period_key(); let max_period_parameter_key = gov_storage::get_max_proposal_period_key(); - let current_epoch = self.ctx.get_block_epoch().ok(); + let current_epoch = self.ctx.get_block_epoch()?; let has_pre_start_epoch = self.ctx.has_key_pre(&start_epoch_key)?; let has_pre_end_epoch = self.ctx.has_key_pre(&end_epoch_key)?; @@ -451,36 +428,21 @@ where return Ok(false); } - let start_epoch: Option = - self.ctx.post().read(&start_epoch_key)?; - let end_epoch: Option = self.ctx.post().read(&end_epoch_key)?; - let min_period: Option = - self.ctx.pre().read(&min_period_parameter_key)?; - let max_period: Option = - self.ctx.pre().read(&max_period_parameter_key)?; - match ( - min_period, - max_period, - start_epoch, - end_epoch, - current_epoch, - ) { - ( - Some(min_period), - Some(max_period), - Some(start_epoch), - Some(end_epoch), - Some(current_epoch), - ) => { - if end_epoch <= start_epoch || start_epoch <= current_epoch { - return Ok(false); - } - Ok((end_epoch - start_epoch) % min_period == 0 - && (end_epoch - start_epoch).0 >= min_period - && (end_epoch - start_epoch).0 <= max_period) - } - _ => Ok(false), + let start_epoch: Epoch = + self.force_read(&start_epoch_key, ReadType::Post)?; + let end_epoch: Epoch = + self.force_read(&end_epoch_key, ReadType::Post)?; + let min_period: u64 = + self.force_read(&min_period_parameter_key, ReadType::Pre)?; + let max_period: u64 = + self.force_read(&max_period_parameter_key, ReadType::Pre)?; + + if end_epoch <= start_epoch || start_epoch <= current_epoch { + return Ok(false); } + Ok((end_epoch - start_epoch) % min_period == 0 + && (end_epoch - start_epoch).0 >= min_period + && (end_epoch - start_epoch).0 <= max_period) } /// Validate a funds key @@ -494,33 +456,20 @@ where token::balance_key(native_token_address, self.ctx.address); let min_funds_parameter_key = gov_storage::get_min_proposal_fund_key(); - let min_funds_parameter: Option = - self.ctx.pre().read(&min_funds_parameter_key)?; + let min_funds_parameter: token::Amount = + self.force_read(&min_funds_parameter_key, ReadType::Pre)?; let pre_balance: Option = self.ctx.pre().read(&balance_key)?; - let post_balance: Option = - self.ctx.post().read(&balance_key)?; - let post_funds: Option = - self.ctx.post().read(&funds_key)?; - - match (min_funds_parameter, pre_balance, post_balance, post_funds) { - ( - Some(min_funds_parameter), - Some(pre_balance), - Some(post_balance), - Some(post_funds), - ) => Ok(post_funds >= min_funds_parameter - && post_balance - pre_balance == post_funds), - ( - Some(min_funds_parameter), - None, - Some(post_balance), - Some(post_funds), - ) => { - Ok(post_funds >= min_funds_parameter - && post_balance == post_funds) - } - _ => Ok(false), + let post_balance: token::Amount = + self.force_read(&balance_key, ReadType::Post)?; + let post_funds: token::Amount = + self.force_read(&funds_key, ReadType::Post)?; + + if let Some(pre_balance) = pre_balance { + Ok(post_funds >= min_funds_parameter + && post_balance - pre_balance == post_funds) + } else { + Ok(post_funds >= min_funds_parameter && post_balance == post_funds) } } @@ -530,24 +479,19 @@ where token::balance_key(native_token_address, self.ctx.address); let min_funds_parameter_key = gov_storage::get_min_proposal_fund_key(); - let min_funds_parameter: Option = - self.ctx.pre().read(&min_funds_parameter_key)?; let pre_balance: Option = self.ctx.pre().read(&balance_key)?; - let post_balance: Option = - self.ctx.post().read(&balance_key)?; - - match (min_funds_parameter, pre_balance, post_balance) { - ( - Some(min_funds_parameter), - Some(pre_balance), - Some(post_balance), - ) => Ok(post_balance > pre_balance - && post_balance - pre_balance >= min_funds_parameter), - (Some(min_funds_parameter), None, Some(post_balance)) => { - Ok(post_balance >= min_funds_parameter) - } - _ => Ok(false), + + let min_funds_parameter: token::Amount = + self.force_read(&min_funds_parameter_key, ReadType::Pre)?; + let post_balance: token::Amount = + self.force_read(&balance_key, ReadType::Post)?; + + if let Some(pre_balance) = pre_balance { + Ok(post_balance > pre_balance + && post_balance - pre_balance >= min_funds_parameter) + } else { + Ok(post_balance >= min_funds_parameter) } } @@ -560,70 +504,61 @@ where let author_key = gov_storage::get_author_key(proposal_id); let has_pre_author = self.ctx.has_key_pre(&author_key)?; - if has_pre_author { return Ok(false); } - let author = self.ctx.post().read(&author_key)?; + let author = self.force_read(&author_key, ReadType::Post)?; match author { - Some(author) => match author { - Address::Established(_) => { - let address_exist_key = Key::validity_predicate(&author); - let address_exist = - self.ctx.has_key_post(&address_exist_key)?; + Address::Established(_) => { + let address_exist_key = Key::validity_predicate(&author); + let address_exist = + self.ctx.has_key_post(&address_exist_key)?; - Ok(address_exist && verifiers.contains(&author)) - } - Address::Implicit(_) => Ok(verifiers.contains(&author)), - Address::Internal(_) => Ok(false), - }, - _ => Ok(false), + Ok(address_exist && verifiers.contains(&author)) + } + Address::Implicit(_) => Ok(verifiers.contains(&author)), + Address::Internal(_) => Ok(false), } } /// Validate a counter key pub fn is_valid_counter(&self, set_count: u64) -> Result { let counter_key = gov_storage::get_counter_key(); - let pre_counter: Option = self.ctx.pre().read(&counter_key)?; - let post_counter: Option = self.ctx.post().read(&counter_key)?; + let pre_counter: u64 = self.force_read(&counter_key, ReadType::Pre)?; + let post_counter: u64 = + self.force_read(&counter_key, ReadType::Post)?; - match (pre_counter, post_counter) { - (Some(pre_counter), Some(post_counter)) => { - Ok(pre_counter + set_count == post_counter) - } - _ => Ok(false), - } + Ok(pre_counter + set_count == post_counter) } /// Validate a commit key pub fn is_valid_proposal_commit(&self) -> Result { let counter_key = gov_storage::get_counter_key(); - let pre_counter: Option = self.ctx.pre().read(&counter_key)?; - let post_counter: Option = self.ctx.post().read(&counter_key)?; - - match (pre_counter, post_counter) { - (Some(pre_counter), Some(post_counter)) => { - // NOTE: can't do pre_counter + set_count == post_counter here - // because someone may update an empty proposal that just - // register a committing key causing a bug - Ok(pre_counter < post_counter) - } - _ => Ok(false), - } + let pre_counter: u64 = self.force_read(&counter_key, ReadType::Pre)?; + let post_counter: u64 = + self.force_read(&counter_key, ReadType::Post)?; + + // NOTE: can't do pre_counter + set_count == post_counter here + // because someone may update an empty proposal that just + // register a committing key causing a bug + Ok(pre_counter < post_counter) } /// Validate a governance parameter - pub fn is_valid_parameter(&self, tx_data: &[u8]) -> Result { - utils::is_proposal_accepted(&self.ctx.pre(), tx_data) - .map_err(Error::NativeVpError) + pub fn is_valid_parameter(&self, tx: &Tx) -> Result { + match tx.data() { + Some(data) => is_proposal_accepted(&self.ctx.pre(), data.as_ref()) + .map_err(Error::NativeVpError), + None => Ok(true), + } } /// Check if a vote is from a validator pub fn is_validator( &self, - epoch: Epoch, + _epoch: Epoch, verifiers: &BTreeSet
, address: &Address, delegation_address: &Address, @@ -633,23 +568,47 @@ where H: 'static + storage::StorageHasher, CA: 'static + WasmCacheAccess, { - let all_validators = - pos::namada_proof_of_stake::read_all_validator_addresses( - &self.ctx.pre(), - epoch, - )?; - if !all_validators.is_empty() { - let is_voter_validator = all_validators - .into_iter() - .any(|validator| validator.eq(address)); - let is_signer_validator = verifiers.contains(address); - let is_delegation_address = delegation_address.eq(address); - - Ok(is_voter_validator - && is_signer_validator - && is_delegation_address) + if !address.eq(delegation_address) { + return Ok(false); + } + + let is_validator = is_validator(&self.ctx.pre(), address)?; + + Ok(is_validator && verifiers.contains(address)) + } + + /// Private method to read from storage data that are 100% in storage. + fn force_read(&self, key: &Key, read_type: ReadType) -> Result + where + T: BorshDeserialize, + { + let res = match read_type { + ReadType::Pre => self.ctx.pre().read::(key), + ReadType::Post => self.ctx.post().read::(key), + }?; + + if let Some(data) = res { + Ok(data) } else { - Ok(false) + Err(Error::EmptyProposalField(key.to_string())) + } + } + + fn is_valid_voting_window( + &self, + current_epoch: Epoch, + start_epoch: Epoch, + end_epoch: Epoch, + is_validator: bool, + ) -> bool { + if is_validator { + is_valid_validator_voting_period( + current_epoch, + start_epoch, + end_epoch, + ) + } else { + current_epoch >= start_epoch && current_epoch <= end_epoch } } @@ -661,10 +620,6 @@ where address: &Address, delegation_address: &Address, ) -> Result { - // let bond_key = pos::bond_key(&BondId { - // source: address.clone(), - // validator: delegation_address.clone(), - // }); let bond_handle = pos::namada_proof_of_stake::bond_handle( address, delegation_address, @@ -672,7 +627,6 @@ where let params = pos::namada_proof_of_stake::read_pos_params(&self.ctx.pre())?; let bond = bond_handle.get_sum(&self.ctx.pre(), epoch, ¶ms)?; - // let bonds: Option = self.ctx.pre().read(&bond_key)?; if bond.is_some() && verifiers.contains(address) { Ok(true) @@ -683,7 +637,7 @@ where } #[allow(clippy::upper_case_acronyms)] -#[derive(Clone, Debug)] +#[derive(Debug)] enum KeyType { #[allow(non_camel_case_types)] COUNTER, diff --git a/shared/src/ledger/governance/utils.rs b/shared/src/ledger/governance/utils.rs new file mode 100644 index 00000000000..d4b4d1316c4 --- /dev/null +++ b/shared/src/ledger/governance/utils.rs @@ -0,0 +1,122 @@ +//! Governance utility functions + +use std::collections::HashMap; + +use namada_core::ledger::governance::utils::TallyResult; +use thiserror::Error; + +use crate::ledger::events::EventType; + +pub(super) enum ReadType { + Pre, + Post, +} + +/// Proposal errors +#[derive(Error, Debug)] +pub enum Error { + /// Invalid validator set deserialization + #[error("Invalid validator set")] + InvalidValidatorSet, + /// Invalid proposal field deserialization + #[error("Invalid proposal {0}")] + InvalidProposal(u64), + /// Error during tally + #[error("Error while tallying proposal: {0}")] + Tally(String), +} + +/// Proposal event definition +pub struct ProposalEvent { + /// Proposal event type + pub event_type: String, + /// Proposal event attributes + pub attributes: HashMap, +} + +impl ProposalEvent { + /// Create a proposal event + pub fn new( + event_type: String, + tally: TallyResult, + id: u64, + has_proposal_code: bool, + proposal_code_exit_status: bool, + ) -> Self { + let attributes = HashMap::from([ + ("tally_result".to_string(), tally.to_string()), + ("proposal_id".to_string(), id.to_string()), + ( + "has_proposal_code".to_string(), + (!has_proposal_code as u64).to_string(), + ), + ( + "proposal_code_exit_status".to_string(), + (!proposal_code_exit_status as u64).to_string(), + ), + ]); + Self { + event_type, + attributes, + } + } + + /// Create a new proposal event for rejected proposal + pub fn rejected_proposal_event(proposal_id: u64) -> Self { + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Rejected, + proposal_id, + false, + false, + ) + } + + /// Create a new proposal event for default proposal + pub fn default_proposal_event( + proposal_id: u64, + has_code: bool, + execution_status: bool, + ) -> Self { + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed, + proposal_id, + has_code, + execution_status, + ) + } + + /// Create a new proposal event for pgf stewards proposal + pub fn pgf_steward_proposal_event(proposal_id: u64, result: bool) -> Self { + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed, + proposal_id, + false, + result, + ) + } + + /// Create a new proposal event for pgf payments proposal + pub fn pgf_payments_proposal_event(proposal_id: u64, result: bool) -> Self { + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed, + proposal_id, + false, + result, + ) + } + + /// Create a new proposal event for eth proposal + pub fn eth_proposal_event(proposal_id: u64, result: bool) -> Self { + ProposalEvent::new( + EventType::Proposal.to_string(), + TallyResult::Passed, + proposal_id, + false, + result, + ) + } +} diff --git a/shared/src/ledger/ibc/vp/mod.rs b/shared/src/ledger/ibc/vp/mod.rs index 04c980a21bc..22c01624429 100644 --- a/shared/src/ledger/ibc/vp/mod.rs +++ b/shared/src/ledger/ibc/vp/mod.rs @@ -395,6 +395,7 @@ mod tests { use crate::types::time::DurationSecs; use crate::types::token::{balance_key, Amount}; use crate::types::transaction::TxType; + use crate::types::tx::TxBuilder; use crate::vm::wasm; const ADDRESS: Address = Address::Internal(InternalAddress::Ibc); @@ -829,14 +830,13 @@ mod tests { let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = wl_storage.storage.chain_id.clone(); - tx.set_code(Code::new(tx_code)); - tx.set_data(Data::new(tx_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &keypair_1(), - ))); + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(keypair_1()) + .build(); let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = @@ -967,14 +967,13 @@ mod tests { let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = wl_storage.storage.chain_id.clone(); - tx.set_code(Code::new(tx_code)); - tx.set_data(Data::new(tx_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &keypair_1(), - ))); + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(keypair_1()) + .build(); let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = @@ -1171,14 +1170,13 @@ mod tests { let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = wl_storage.storage.chain_id.clone(); - tx.set_code(Code::new(tx_code)); - tx.set_data(Data::new(tx_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &keypair_1(), - ))); + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(keypair_1()) + .build(); let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = @@ -1299,14 +1297,15 @@ mod tests { let tx_code = vec![]; let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = wl_storage.storage.chain_id.clone(); - tx.set_code(Code::new(tx_code)); - tx.set_data(Data::new(tx_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &keypair_1(), - ))); + + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(keypair_1()) + .build(); + let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -1945,14 +1944,15 @@ mod tests { let tx_code = vec![]; let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = wl_storage.storage.chain_id.clone(); - tx.set_code(Code::new(tx_code)); - tx.set_data(Data::new(tx_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &keypair_1(), - ))); + + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(keypair_1()) + .build(); + let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2082,14 +2082,15 @@ mod tests { let tx_code = vec![]; let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = wl_storage.storage.chain_id.clone(); - tx.set_code(Code::new(tx_code)); - tx.set_data(Data::new(tx_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &keypair_1(), - ))); + + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(keypair_1()) + .build(); + let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2257,14 +2258,15 @@ mod tests { let tx_code = vec![]; let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = wl_storage.storage.chain_id.clone(); - tx.set_code(Code::new(tx_code)); - tx.set_data(Data::new(tx_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &keypair_1(), - ))); + + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(keypair_1()) + .build(); + let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2401,14 +2403,15 @@ mod tests { let tx_code = vec![]; let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = wl_storage.storage.chain_id.clone(); - tx.set_code(Code::new(tx_code)); - tx.set_data(Data::new(tx_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &keypair_1(), - ))); + + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(keypair_1()) + .build(); + let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2550,14 +2553,15 @@ mod tests { let tx_code = vec![]; let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = wl_storage.storage.chain_id.clone(); - tx.set_code(Code::new(tx_code)); - tx.set_data(Data::new(tx_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &keypair_1(), - ))); + + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(keypair_1()) + .build(); + let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); @@ -2699,14 +2703,15 @@ mod tests { let tx_code = vec![]; let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = wl_storage.storage.chain_id.clone(); - tx.set_code(Code::new(tx_code)); - tx.set_data(Data::new(tx_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &keypair_1(), - ))); + + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(keypair_1()) + .build(); + let gas_meter = VpGasMeter::new(0); let (vp_wasm_cache, _vp_cache_dir) = wasm::compilation_cache::common::testing::cache(); diff --git a/shared/src/ledger/masp.rs b/shared/src/ledger/masp.rs index a843e050cfb..6f5525a20d8 100644 --- a/shared/src/ledger/masp.rs +++ b/shared/src/ledger/masp.rs @@ -1403,13 +1403,13 @@ impl ShieldedContext { let tx_fee = // If there are shielded inputs if let Some(sk) = spending_key { - let InputAmount::Validated(fee) = args.tx.fee_amount else { + let InputAmount::Validated(fee) = args.tx.gas_amount else { unreachable!("The function `gen_shielded_transfer` is only called by `submit_tx` which validates amounts.") }; // Transaction fees need to match the amount in the wrapper Transfer // when MASP source is used let (_, shielded_fee) = - convert_amount(epoch, &args.tx.fee_token, fee.amount); + convert_amount(epoch, &args.tx.gas_token, fee.amount); let required_amt = if shielded_gas { amount + shielded_fee.clone() } else { diff --git a/shared/src/ledger/mod.rs b/shared/src/ledger/mod.rs index 399f75f800b..a5b2a06819e 100644 --- a/shared/src/ledger/mod.rs +++ b/shared/src/ledger/mod.rs @@ -3,10 +3,12 @@ pub mod args; pub mod eth_bridge; pub mod events; +pub mod governance; pub mod ibc; pub mod inflation; pub mod masp; pub mod native_vp; +pub mod pgf; pub mod pos; #[cfg(all(feature = "wasm-runtime", feature = "ferveo-tpke"))] pub mod protocol; @@ -20,5 +22,5 @@ pub mod vp_host_fns; pub mod wallet; pub use namada_core::ledger::{ - gas, governance, parameters, replay_protection, storage_api, tx_env, vp_env, + gas, parameters, replay_protection, storage_api, tx_env, vp_env, }; 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 e3cb7e24e81..f5bd77e1526 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 @@ -357,13 +357,13 @@ mod test_bridge_pool_vp { use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{Storage, WlStorage}; use crate::ledger::storage_api::StorageWrite; - use crate::proto::Data; use crate::types::address::{nam, wnam}; use crate::types::chain::ChainId; use crate::types::eth_bridge_pool::{GasFee, TransferToEthereum}; use crate::types::hash::Hash; use crate::types::storage::TxIndex; use crate::types::transaction::TxType; + use crate::types::tx::TxBuilder; use crate::vm::wasm::VpCache; use crate::vm::WasmCacheRwAccess; @@ -624,8 +624,9 @@ mod test_bridge_pool_vp { ), }; - let mut tx = Tx::new(TxType::Raw); - tx.set_data(Data::new(transfer.try_to_vec().expect("Test failed"))); + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder.add_data(transfer).build(); let res = vp.validate_tx(&tx, &keys_changed, &verifiers); match expect { @@ -962,8 +963,9 @@ mod test_bridge_pool_vp { ), }; - let mut tx = Tx::new(TxType::Raw); - tx.set_data(Data::new(transfer.try_to_vec().expect("Test failed"))); + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder.add_data(transfer).build(); let res = vp.validate_tx(&tx, &keys_changed, &verifiers); assert!(!res.expect("Test failed")); @@ -1024,8 +1026,9 @@ mod test_bridge_pool_vp { ), }; - let mut tx = Tx::new(TxType::Raw); - tx.set_data(Data::new(transfer.try_to_vec().expect("Test failed"))); + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder.add_data(transfer).build(); let res = vp .validate_tx(&tx, &keys_changed, &verifiers) @@ -1110,8 +1113,9 @@ mod test_bridge_pool_vp { ), }; - let mut tx = Tx::new(TxType::Raw); - tx.set_data(Data::new(transfer.try_to_vec().expect("Test failed"))); + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder.add_data(transfer).build(); let res = vp .validate_tx(&tx, &keys_changed, &verifiers) @@ -1197,8 +1201,9 @@ mod test_bridge_pool_vp { ), }; - let mut tx = Tx::new(TxType::Raw); - tx.set_data(Data::new(transfer.try_to_vec().expect("Test failed"))); + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder.add_data(transfer).build(); let res = vp .validate_tx(&tx, &keys_changed, &verifiers) @@ -1311,8 +1316,9 @@ mod test_bridge_pool_vp { ), }; - let mut tx = Tx::new(TxType::Raw); - tx.set_data(Data::new(transfer.try_to_vec().expect("Test failed"))); + let tx_builder = + TxBuilder::new(wl_storage.storage.chain_id.clone(), None); + let tx = tx_builder.add_data(transfer).build(); let res = vp .validate_tx(&tx, &keys_changed, &verifiers) diff --git a/shared/src/ledger/native_vp/governance/utils.rs b/shared/src/ledger/native_vp/governance/utils.rs deleted file mode 100644 index c00f57b5d9a..00000000000 --- a/shared/src/ledger/native_vp/governance/utils.rs +++ /dev/null @@ -1,484 +0,0 @@ -//! Governance utility functions - -use std::collections::HashMap; - -use borsh::BorshDeserialize; -use namada_core::types::governance::ProposalResult; -use namada_core::types::transaction::governance::ProposalType; -use namada_proof_of_stake::{ - bond_amount, read_all_validator_addresses, read_pos_params, - read_validator_stake, -}; -use thiserror::Error; - -use crate::ledger::governance::storage as gov_storage; -use crate::ledger::pos::BondId; -use crate::ledger::storage_api; -use crate::types::address::Address; -use crate::types::governance::{ - ProposalVote, Tally, TallyResult, VotePower, VoteType, -}; -use crate::types::storage::Epoch; - -/// Proposal structure holding votes information necessary to compute the -/// outcome -pub struct Votes { - /// Map from validators who votes yay to their total stake amount - pub yay_validators: HashMap, - /// Map from delegation votes to their bond amount - pub delegators: - HashMap>, -} - -/// Proposal errors -#[derive(Error, Debug)] -pub enum Error { - /// Invalid validator set deserialization - #[error("Invalid validator set")] - InvalidValidatorSet, - /// Invalid proposal field deserialization - #[error("Invalid proposal {0}")] - InvalidProposal(u64), - /// Error during tally - #[error("Error while tallying proposal: {0}")] - Tally(String), -} - -/// Proposal event definition -pub struct ProposalEvent { - /// Proposal event type - pub event_type: String, - /// Proposal event attributes - pub attributes: HashMap, -} - -impl ProposalEvent { - /// Create a proposal event - pub fn new( - event_type: String, - tally: TallyResult, - id: u64, - has_proposal_code: bool, - proposal_code_exit_status: bool, - ) -> Self { - let attributes = HashMap::from([ - ("tally_result".to_string(), tally.to_string()), - ("proposal_id".to_string(), id.to_string()), - ( - "has_proposal_code".to_string(), - (!has_proposal_code as u64).to_string(), - ), - ( - "proposal_code_exit_status".to_string(), - (!proposal_code_exit_status as u64).to_string(), - ), - ]); - Self { - event_type, - attributes, - } - } -} - -/// Return a proposal result -pub fn compute_tally( - votes: Votes, - total_stake: VotePower, - proposal_type: &ProposalType, -) -> Result { - let Votes { - yay_validators, - delegators, - } = votes; - - match proposal_type { - ProposalType::Default(_) | ProposalType::ETHBridge => { - let mut total_yay_staked_tokens = VotePower::default(); - - for (_, (amount, validator_vote)) in yay_validators.iter() { - if let ProposalVote::Yay(vote_type) = validator_vote { - if proposal_type == vote_type { - total_yay_staked_tokens += *amount; - } else { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: {}, Found: {}", - proposal_type, - validator_vote - ); - continue; - } - } else { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: {}, Found: {}", - proposal_type, - validator_vote - ); - continue; - } - } - - // This loop is taken only for Default proposals - for (_, vote_map) in delegators.iter() { - for (validator_address, (vote_power, delegator_vote)) in - vote_map.iter() - { - match delegator_vote { - ProposalVote::Yay(VoteType::Default) => { - if !yay_validators.contains_key(validator_address) { - // YAY: Add delegator amount whose validator - // didn't vote / voted nay - total_yay_staked_tokens += *vote_power; - } - } - ProposalVote::Nay => { - // NAY: Remove delegator amount whose validator - // validator vote yay - - if yay_validators.contains_key(validator_address) { - total_yay_staked_tokens -= *vote_power; - } - } - - _ => { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: {}, Found: {}", - proposal_type, - delegator_vote - ); - continue; - } - } - } - } - - // Proposal passes if 2/3 of total voting power voted Yay - if total_yay_staked_tokens >= (total_stake / 3) * 2 { - let tally_result = match proposal_type { - ProposalType::Default(_) => { - TallyResult::Passed(Tally::Default) - } - ProposalType::ETHBridge => { - TallyResult::Passed(Tally::ETHBridge) - } - _ => { - return Err(Error::Tally(format!( - "Unexpected proposal type: {}", - proposal_type - ))); - } - }; - - Ok(ProposalResult { - result: tally_result, - total_voting_power: total_stake, - total_yay_power: total_yay_staked_tokens, - total_nay_power: 0.into(), - }) - } else { - Ok(ProposalResult { - result: TallyResult::Rejected, - total_voting_power: total_stake, - total_yay_power: total_yay_staked_tokens, - total_nay_power: 0.into(), - }) - } - } - ProposalType::PGFCouncil => { - let mut total_yay_staked_tokens = HashMap::new(); - for (_, (amount, validator_vote)) in yay_validators.iter() { - if let ProposalVote::Yay(VoteType::PGFCouncil(votes)) = - validator_vote - { - for v in votes { - *total_yay_staked_tokens - .entry(v) - .or_insert(VotePower::zero()) += *amount; - } - } else { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: PGFCouncil, Found: {}", - validator_vote - ); - continue; - } - } - - // YAY: Add delegator amount whose validator didn't vote / voted nay - // or adjust voting power if delegator voted yay with a - // different memo - for (_, vote_map) in delegators.iter() { - for (validator_address, (vote_power, delegator_vote)) in - vote_map.iter() - { - match delegator_vote { - ProposalVote::Yay(VoteType::PGFCouncil( - delegator_votes, - )) => { - match yay_validators.get(validator_address) { - Some((_, validator_vote)) => { - if let ProposalVote::Yay( - VoteType::PGFCouncil(validator_votes), - ) = validator_vote - { - for vote in validator_votes - .symmetric_difference( - delegator_votes, - ) - { - if validator_votes.contains(vote) { - // Delegator didn't vote for - // this, reduce voting power - if let Some(power) = - total_yay_staked_tokens - .get_mut(vote) - { - *power -= *vote_power; - } else { - return Err(Error::Tally( - format!( - "Expected PGF \ - vote {:?} was \ - not in tally", - vote - ), - )); - } - } else { - // Validator didn't vote for - // this, add voting power - *total_yay_staked_tokens - .entry(vote) - .or_insert( - VotePower::zero(), - ) += *vote_power; - } - } - } else { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: \ - PGFCouncil, Found: {}", - validator_vote - ); - continue; - } - } - None => { - // Validator didn't vote or voted nay, add - // delegator vote - - for vote in delegator_votes { - *total_yay_staked_tokens - .entry(vote) - .or_insert(VotePower::zero()) += - *vote_power; - } - } - } - } - ProposalVote::Nay => { - for ( - validator_address, - (vote_power, _delegator_vote), - ) in vote_map.iter() - { - if let Some((_, validator_vote)) = - yay_validators.get(validator_address) - { - if let ProposalVote::Yay( - VoteType::PGFCouncil(votes), - ) = validator_vote - { - for vote in votes { - if let Some(power) = - total_yay_staked_tokens - .get_mut(vote) - { - *power -= *vote_power; - } else { - return Err(Error::Tally( - format!( - "Expected PGF vote \ - {:?} was not in tally", - vote - ), - )); - } - } - } else { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: \ - PGFCouncil, Found: {}", - validator_vote - ); - continue; - } - } - } - } - _ => { - // Log the error and continue - tracing::error!( - "Unexpected vote type. Expected: PGFCouncil, \ - Found: {}", - delegator_vote - ); - continue; - } - } - } - } - - // At least 1/3 of the total voting power must vote Yay - let total_yay_voted_power = total_yay_staked_tokens - .iter() - .fold(VotePower::zero(), |acc, (_, vote_power)| { - acc + *vote_power - }); - - match total_yay_voted_power.checked_mul(3.into()) { - Some(v) if v < total_stake => Ok(ProposalResult { - result: TallyResult::Rejected, - total_voting_power: total_stake, - total_yay_power: total_yay_voted_power, - total_nay_power: VotePower::zero(), - }), - _ => { - // Select the winner council based on approval voting - // (majority) - let council = total_yay_staked_tokens - .into_iter() - .max_by(|a, b| a.1.cmp(&b.1)) - .map(|(vote, _)| vote.to_owned()) - .ok_or_else(|| { - Error::Tally( - "Missing expected elected council".to_string(), - ) - })?; - - Ok(ProposalResult { - result: TallyResult::Passed(Tally::PGFCouncil(council)), - total_voting_power: total_stake, - total_yay_power: total_yay_voted_power, - total_nay_power: VotePower::zero(), - }) - } - } - } - } -} - -/// Prepare Votes structure to compute proposal tally -pub fn get_proposal_votes( - storage: &S, - epoch: Epoch, - proposal_id: u64, -) -> storage_api::Result -where - S: storage_api::StorageRead, -{ - let params = read_pos_params(storage)?; - let validators = read_all_validator_addresses(storage, epoch)?; - - let vote_prefix_key = - gov_storage::get_proposal_vote_prefix_key(proposal_id); - let vote_iter = - storage_api::iter_prefix::(storage, &vote_prefix_key)?; - - let mut yay_validators = HashMap::new(); - let mut delegators: HashMap< - Address, - HashMap, - > = HashMap::new(); - - for next_vote in vote_iter { - let (vote_key, vote) = next_vote?; - let voter_address = gov_storage::get_voter_address(&vote_key); - match voter_address { - Some(voter_address) => { - if vote.is_yay() && validators.contains(voter_address) { - let amount: VotePower = read_validator_stake( - storage, - ¶ms, - voter_address, - epoch, - )? - .unwrap_or_default() - .try_into() - .expect("Amount out of bounds"); - - yay_validators - .insert(voter_address.clone(), (amount, vote)); - } else if !validators.contains(voter_address) { - let validator_address = - gov_storage::get_vote_delegation_address(&vote_key); - match validator_address { - Some(validator) => { - let bond_id = BondId { - source: voter_address.clone(), - validator: validator.clone(), - }; - let amount = - bond_amount(storage, &bond_id, epoch)?.1; - - if !amount.is_zero() { - let entry = delegators - .entry(voter_address.to_owned()) - .or_default(); - entry.insert( - validator.to_owned(), - ( - VotePower::try_from(amount).unwrap(), - vote, - ), - ); - } - } - None => continue, - } - } - } - None => continue, - } - } - - Ok(Votes { - yay_validators, - delegators, - }) -} - -/// Calculate the valid voting window for validator given a proposal epoch -/// details -pub fn is_valid_validator_voting_period( - current_epoch: Epoch, - voting_start_epoch: Epoch, - voting_end_epoch: Epoch, -) -> bool { - voting_start_epoch < voting_end_epoch - && current_epoch * 3 <= voting_start_epoch + voting_end_epoch * 2 -} - -/// Check if an accepted proposal is being executed -pub fn is_proposal_accepted( - storage: &S, - tx_data: &[u8], -) -> storage_api::Result -where - S: storage_api::StorageRead, -{ - let proposal_id = u64::try_from_slice(tx_data).ok(); - match proposal_id { - Some(id) => { - let proposal_execution_key = - gov_storage::get_proposal_execution_key(id); - storage.has_key(&proposal_execution_key) - } - None => Ok(false), - } -} diff --git a/shared/src/ledger/native_vp/mod.rs b/shared/src/ledger/native_vp/mod.rs index c7d58f0563e..a8a3eb01f52 100644 --- a/shared/src/ledger/native_vp/mod.rs +++ b/shared/src/ledger/native_vp/mod.rs @@ -2,11 +2,9 @@ //! as the PoS and IBC modules. pub mod ethereum_bridge; -pub mod governance; pub mod multitoken; pub mod parameters; pub mod replay_protection; -pub mod slash_fund; use std::cell::RefCell; use std::collections::BTreeSet; diff --git a/shared/src/ledger/native_vp/parameters.rs b/shared/src/ledger/native_vp/parameters.rs index d367c166987..a66b0738c4a 100644 --- a/shared/src/ledger/native_vp/parameters.rs +++ b/shared/src/ledger/native_vp/parameters.rs @@ -8,7 +8,7 @@ use namada_core::types::address::{Address, InternalAddress}; use namada_core::types::storage::Key; use thiserror::Error; -use super::governance; +use crate::core::ledger::storage_api::governance; use crate::ledger::native_vp::{self, Ctx, NativeVp}; use crate::vm::WasmCacheAccess; @@ -57,11 +57,10 @@ where return false; }; match key_type { - KeyType::PARAMETER => governance::utils::is_proposal_accepted( - &self.ctx.pre(), - &data, - ) - .unwrap_or(false), + KeyType::PARAMETER => { + governance::is_proposal_accepted(&self.ctx.pre(), &data) + .unwrap_or(false) + } KeyType::UNKNOWN_PARAMETER => false, KeyType::UNKNOWN => true, } diff --git a/shared/src/ledger/native_vp/slash_fund.rs b/shared/src/ledger/native_vp/slash_fund.rs deleted file mode 100644 index bed71d3bd93..00000000000 --- a/shared/src/ledger/native_vp/slash_fund.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! SlashFund VP - -use std::collections::BTreeSet; - -use namada_core::ledger::slash_fund; -/// SlashFund storage -pub use namada_core::ledger::slash_fund::storage; -use thiserror::Error; - -use crate::ledger::native_vp::{self, governance, Ctx, NativeVp}; -use crate::ledger::storage::{self as ledger_storage, StorageHasher}; -use crate::ledger::storage_api::StorageRead; -use crate::proto::Tx; -use crate::types::address::{Address, InternalAddress}; -use crate::types::storage::Key; -use crate::types::token; -use crate::vm::WasmCacheAccess; - -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum Error { - #[error("Native VP error: {0}")] - NativeVpError(#[from] native_vp::Error), -} - -/// SlashFund functions result -pub type Result = std::result::Result; - -/// SlashFund VP -pub struct SlashFundVp<'a, DB, H, CA> -where - DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, - H: StorageHasher, - CA: WasmCacheAccess, -{ - /// Context to interact with the host structures. - pub ctx: Ctx<'a, DB, H, CA>, -} - -impl<'a, DB, H, CA> NativeVp for SlashFundVp<'a, DB, H, CA> -where - DB: 'static + ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, - H: 'static + StorageHasher, - CA: 'static + WasmCacheAccess, -{ - type Error = Error; - - const ADDR: InternalAddress = InternalAddress::SlashFund; - - fn validate_tx( - &self, - tx_data: &Tx, - keys_changed: &BTreeSet, - _verifiers: &BTreeSet
, - ) -> Result { - let native_token = self.ctx.pre().get_native_token()?; - let result = keys_changed.iter().all(|key| { - let key_type: KeyType = get_key_type(key, &native_token); - match key_type { - KeyType::BALANCE(addr) => { - if addr.ne(&slash_fund::ADDRESS) { - return true; - } - let data = if let Some(data) = tx_data.data() { - data - } else { - return false; - }; - governance::utils::is_proposal_accepted( - &self.ctx.pre(), - &data, - ) - .unwrap_or(false) - } - KeyType::UNKNOWN_SLASH_FUND => false, - KeyType::UNKNOWN => true, - } - }); - Ok(result) - } -} - -#[allow(clippy::upper_case_acronyms)] -enum KeyType { - #[allow(clippy::upper_case_acronyms)] - BALANCE(Address), - #[allow(clippy::upper_case_acronyms)] - #[allow(non_camel_case_types)] - UNKNOWN_SLASH_FUND, - #[allow(clippy::upper_case_acronyms)] - UNKNOWN, -} - -fn get_key_type(value: &Key, native_token: &Address) -> KeyType { - if storage::is_slash_fund_key(value) { - KeyType::UNKNOWN_SLASH_FUND - } else if token::is_any_token_balance_key(value).is_some() { - match token::is_balance_key(native_token, value) { - Some(addr) => KeyType::BALANCE(addr.clone()), - None => KeyType::UNKNOWN, - } - } else { - KeyType::UNKNOWN - } -} diff --git a/shared/src/ledger/pgf/mod.rs b/shared/src/ledger/pgf/mod.rs new file mode 100644 index 00000000000..458083e6f95 --- /dev/null +++ b/shared/src/ledger/pgf/mod.rs @@ -0,0 +1,128 @@ +//! Pgf VP + +/// Pgf utility functions and structures +pub mod utils; + +use std::collections::BTreeSet; + +use namada_core::ledger::pgf::storage::keys as pgf_storage; +use namada_core::ledger::storage; +use namada_core::ledger::storage_api::governance::is_proposal_accepted; +use namada_core::proto::Tx; +use thiserror::Error; + +use crate::ledger::native_vp; +use crate::ledger::native_vp::{Ctx, NativeVp}; +use crate::types::address::{Address, InternalAddress}; +use crate::types::storage::Key; +use crate::vm::WasmCacheAccess; + +/// for handling Pgf NativeVP errors +pub type Result = std::result::Result; + +/// The PGF internal address +pub const ADDRESS: Address = Address::Internal(InternalAddress::Pgf); + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum Error { + #[error("Native VP error: {0}")] + NativeVpError(#[from] native_vp::Error), +} + +/// Pgf VP +pub struct PgfVp<'a, DB, H, CA> +where + DB: storage::DB + for<'iter> storage::DBIter<'iter>, + H: storage::StorageHasher, + CA: WasmCacheAccess, +{ + /// Context to interact with the host structures. + pub ctx: Ctx<'a, DB, H, CA>, +} + +impl<'a, DB, H, CA> NativeVp for PgfVp<'a, DB, H, CA> +where + DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, + H: 'static + storage::StorageHasher, + CA: 'static + WasmCacheAccess, +{ + type Error = Error; + + const ADDR: InternalAddress = InternalAddress::Pgf; + + fn validate_tx( + &self, + tx_data: &Tx, + keys_changed: &BTreeSet, + _verifiers: &BTreeSet
, + ) -> Result { + let result = keys_changed.iter().all(|key| { + let key_type = KeyType::from(key); + + let result = match key_type { + KeyType::STEWARDS => Ok(false), + KeyType::PAYMENTS => Ok(false), + KeyType::PGF_INFLATION_RATE + | KeyType::STEWARD_INFLATION_RATE => { + self.is_valid_parameter_change(tx_data) + } + KeyType::UNKNOWN_PGF => Ok(false), + KeyType::UNKNOWN => Ok(true), + }; + result.unwrap_or(false) + }); + Ok(result) + } +} + +impl<'a, DB, H, CA> PgfVp<'a, DB, H, CA> +where + DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, + H: 'static + storage::StorageHasher, + CA: 'static + WasmCacheAccess, +{ + /// Validate a governance parameter + pub fn is_valid_parameter_change(&self, tx: &Tx) -> Result { + match tx.data() { + Some(data) => is_proposal_accepted(&self.ctx.pre(), data.as_ref()) + .map_err(Error::NativeVpError), + None => Ok(true), + } + } +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug)] +enum KeyType { + #[allow(non_camel_case_types)] + STEWARDS, + #[allow(non_camel_case_types)] + PAYMENTS, + #[allow(non_camel_case_types)] + PGF_INFLATION_RATE, + #[allow(non_camel_case_types)] + STEWARD_INFLATION_RATE, + #[allow(non_camel_case_types)] + UNKNOWN_PGF, + #[allow(non_camel_case_types)] + UNKNOWN, +} + +impl From<&Key> for KeyType { + fn from(key: &Key) -> Self { + if pgf_storage::is_stewards_key(key) { + Self::STEWARDS + } else if pgf_storage::is_payments_key(key) { + KeyType::PAYMENTS + } else if pgf_storage::is_pgf_inflation_rate_key(key) { + Self::PGF_INFLATION_RATE + } else if pgf_storage::is_steward_inflation_rate_key(key) { + Self::STEWARD_INFLATION_RATE + } else if pgf_storage::is_pgf_key(key) { + KeyType::UNKNOWN_PGF + } else { + KeyType::UNKNOWN + } + } +} diff --git a/shared/src/ledger/pgf/utils.rs b/shared/src/ledger/pgf/utils.rs new file mode 100644 index 00000000000..e1bec701baa --- /dev/null +++ b/shared/src/ledger/pgf/utils.rs @@ -0,0 +1,66 @@ +use std::collections::HashMap; + +use namada_core::types::address::Address; +use namada_core::types::token; + +use crate::ledger::events::EventType; + +/// Proposal event definition +pub struct ProposalEvent { + /// Proposal event type + pub event_type: String, + /// Proposal event attributes + pub attributes: HashMap, +} + +impl ProposalEvent { + /// Create a proposal event + pub fn new( + event_type: String, + target: Address, + amount: token::Amount, + is_steward: bool, + success: bool, + ) -> Self { + let attributes = HashMap::from([ + ("target".to_string(), target.to_string()), + ("amount".to_string(), amount.to_string_native()), + ("is_steward".to_string(), is_steward.to_string()), + ("successed".to_string(), success.to_string()), + ]); + Self { + event_type, + attributes, + } + } + + /// Create a new proposal event for pgf continous funding + pub fn pgf_funding_payment( + target: Address, + amount: token::Amount, + success: bool, + ) -> Self { + ProposalEvent::new( + EventType::PgfPayment.to_string(), + target, + amount, + false, + success, + ) + } + + /// Create a new proposal event for steward payments + pub fn pgf_steward_payment( + target: Address, + amount: token::Amount, + success: bool, + ) -> Self { + ProposalEvent::new( + EventType::PgfPayment.to_string(), + target, + amount, + true, + success, + ) + } +} diff --git a/shared/src/ledger/pos/vp.rs b/shared/src/ledger/pos/vp.rs index dda34970273..1e571d0bb49 100644 --- a/shared/src/ledger/pos/vp.rs +++ b/shared/src/ledger/pos/vp.rs @@ -3,6 +3,7 @@ use std::collections::BTreeSet; use std::panic::{RefUnwindSafe, UnwindSafe}; +use namada_core::ledger::storage_api::governance; // use borsh::BorshDeserialize; pub use namada_proof_of_stake; pub use namada_proof_of_stake::parameters::PosParams; @@ -12,7 +13,7 @@ pub use namada_proof_of_stake::types; use thiserror::Error; use super::is_params_key; -use crate::ledger::native_vp::{self, governance, Ctx, NativeVp}; +use crate::ledger::native_vp::{self, Ctx, NativeVp}; // use crate::ledger::pos::{ // is_validator_address_raw_hash_key, // is_validator_max_commission_rate_change_key, @@ -108,18 +109,14 @@ where tracing::debug!("\nValidating PoS Tx\n"); for key in keys_changed { - // println!("KEY: {}\n", key); if is_params_key(key) { let data = if let Some(data) = tx_data.data() { data } else { return Ok(false); }; - if !governance::utils::is_proposal_accepted( - &self.ctx.pre(), - &data, - ) - .map_err(Error::NativeVpError)? + if !governance::is_proposal_accepted(&self.ctx.pre(), &data) + .map_err(Error::NativeVpError)? { return Ok(false); } diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index 6cab156d7a8..7a0244932a0 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -8,15 +8,15 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use thiserror::Error; use crate::ledger::gas::{self, BlockGasMeter, VpGasMeter}; +use crate::ledger::governance::GovernanceVp; use crate::ledger::ibc::vp::Ibc; use crate::ledger::native_vp::ethereum_bridge::bridge_pool_vp::BridgePoolVp; use crate::ledger::native_vp::ethereum_bridge::vp::EthBridge; -use crate::ledger::native_vp::governance::GovernanceVp; use crate::ledger::native_vp::multitoken::MultitokenVp; use crate::ledger::native_vp::parameters::{self, ParametersVp}; use crate::ledger::native_vp::replay_protection::ReplayProtectionVp; -use crate::ledger::native_vp::slash_fund::SlashFundVp; use crate::ledger::native_vp::{self, NativeVp}; +use crate::ledger::pgf::PgfVp; use crate::ledger::pos::{self, PosVP}; use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{DBIter, Storage, StorageHasher, WlStorage, DB}; @@ -61,9 +61,9 @@ pub enum Error { #[error("IBC Token native VP: {0}")] MultitokenNativeVpError(crate::ledger::native_vp::multitoken::Error), #[error("Governance native VP error: {0}")] - GovernanceNativeVpError(crate::ledger::native_vp::governance::Error), - #[error("SlashFund native VP error: {0}")] - SlashFundNativeVpError(crate::ledger::native_vp::slash_fund::Error), + GovernanceNativeVpError(crate::ledger::governance::Error), + #[error("Pgf native VP error: {0}")] + PgfNativeVpError(crate::ledger::pgf::Error), #[error("Ethereum bridge native VP error: {0}")] EthBridgeNativeVpError(native_vp::ethereum_bridge::vp::Error), #[error("Ethereum bridge pool native VP error: {0}")] @@ -543,14 +543,6 @@ where gas_meter = governance.ctx.gas_meter.into_inner(); result } - InternalAddress::SlashFund => { - let slash_fund = SlashFundVp { ctx }; - let result = slash_fund - .validate_tx(tx, &keys_changed, &verifiers) - .map_err(Error::SlashFundNativeVpError); - gas_meter = slash_fund.ctx.gas_meter.into_inner(); - result - } InternalAddress::Multitoken => { let multitoken = MultitokenVp { ctx }; let result = multitoken @@ -585,6 +577,14 @@ where replay_protection_vp.ctx.gas_meter.into_inner(); result } + InternalAddress::Pgf => { + let pgf_vp = PgfVp { ctx }; + let result = pgf_vp + .validate_tx(tx, &keys_changed, &verifiers) + .map_err(Error::PgfNativeVpError); + gas_meter = pgf_vp.ctx.gas_meter.into_inner(); + result + } InternalAddress::IbcToken(_) | InternalAddress::Erc20(_) => { // The address should be a part of a multitoken key diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 3eb95c8e8c9..38fb7ca61a5 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -5,6 +5,7 @@ use masp_primitives::asset_type::AssetType; use masp_primitives::merkle_tree::MerklePath; use masp_primitives::sapling::Node; use namada_core::ledger::storage::LastBlock; +use namada_core::types::account::{Account, AccountPublicKeysMap}; use namada_core::types::address::Address; use namada_core::types::hash::Hash; use namada_core::types::storage::{BlockHeight, BlockResults, KeySeg}; @@ -75,6 +76,12 @@ router! {SHELL, // was the transaction applied? ( "applied" / [tx_hash: Hash] ) -> Option = applied, + // Query account subspace + ( "account" / [owner: Address] ) -> Option = account, + + // Query public key revealad + ( "revealed" / [owner: Address] ) -> bool = revealed, + // IBC UpdateClient event ( "ibc_client_update" / [client_id: ClientId] / [consensus_height: BlockHeight] ) -> Option = ibc_client_update, @@ -438,6 +445,46 @@ where .cloned()) } +fn account( + ctx: RequestCtx<'_, D, H>, + owner: Address, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + let account_exists = storage_api::account::exists(ctx.wl_storage, &owner)?; + + if account_exists { + let public_keys = + storage_api::account::public_keys(ctx.wl_storage, &owner)?; + let threshold = + storage_api::account::threshold(ctx.wl_storage, &owner)?; + + Ok(Some(Account { + public_keys_map: AccountPublicKeysMap::from_iter(public_keys), + address: owner, + threshold: threshold.unwrap_or(1), + })) + } else { + Ok(None) + } +} + +fn revealed( + ctx: RequestCtx<'_, D, H>, + owner: Address, +) -> storage_api::Result +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + let public_keys = + storage_api::account::public_keys(ctx.wl_storage, &owner)?; + + Ok(!public_keys.is_empty()) +} + #[cfg(test)] mod test { diff --git a/shared/src/ledger/queries/vp/governance.rs b/shared/src/ledger/queries/vp/governance.rs new file mode 100644 index 00000000000..230612cbb86 --- /dev/null +++ b/shared/src/ledger/queries/vp/governance.rs @@ -0,0 +1,38 @@ +// cd shared && cargo expand ledger::queries::vp::governance + +use namada_core::ledger::governance::storage::proposal::StorageProposal; +use namada_core::ledger::governance::utils::Vote; + +use crate::ledger::queries::types::RequestCtx; +use crate::ledger::storage::{DBIter, StorageHasher, DB}; +use crate::ledger::storage_api; + +// Governance queries +router! {GOV, + ( "proposal" / [id: u64 ] ) -> Option = proposal_id, + ( "proposal" / [id: u64 ] / "votes" ) -> Vec = proposal_id_votes, +} + +/// Find if the given address belongs to a validator account. +fn proposal_id( + ctx: RequestCtx<'_, D, H>, + id: u64, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + storage_api::governance::get_proposal_by_id(ctx.wl_storage, id) +} + +/// Find if the given address belongs to a validator account. +fn proposal_id_votes( + ctx: RequestCtx<'_, D, H>, + id: u64, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + storage_api::governance::get_proposal_votes(ctx.wl_storage, id) +} diff --git a/shared/src/ledger/queries/vp/mod.rs b/shared/src/ledger/queries/vp/mod.rs index ad05a2b88b1..c53386b2f8f 100644 --- a/shared/src/ledger/queries/vp/mod.rs +++ b/shared/src/ledger/queries/vp/mod.rs @@ -1,10 +1,16 @@ //! Queries router and handlers for validity predicates // Re-export to show in rustdoc! +pub use governance::Gov; +use governance::GOV; pub use pos::Pos; use pos::POS; pub use token::Token; use token::TOKEN; +mod governance; +pub use pgf::Pgf; +use pgf::PGF; +mod pgf; pub mod pos; mod token; @@ -13,6 +19,8 @@ mod token; router! {VP, ( "pos" ) = (sub POS), ( "token" ) = (sub TOKEN), + ( "governance" ) = (sub GOV), + ( "pgf" ) = (sub PGF), } /// Client-only methods for the router type are composed from router functions. diff --git a/shared/src/ledger/queries/vp/pgf.rs b/shared/src/ledger/queries/vp/pgf.rs new file mode 100644 index 00000000000..932d898068d --- /dev/null +++ b/shared/src/ledger/queries/vp/pgf.rs @@ -0,0 +1,36 @@ +use std::collections::BTreeSet; + +use namada_core::ledger::governance::storage::proposal::PGFTarget; +use namada_core::types::address::Address; + +use crate::ledger::queries::types::RequestCtx; +use crate::ledger::storage::{DBIter, StorageHasher, DB}; +use crate::ledger::storage_api; + +// PoS validity predicate queries +router! {PGF, + ( "stewards" ) -> BTreeSet
= stewards, + ( "fundings" ) -> BTreeSet = funding, +} + +/// Query the currect pgf steward set +fn stewards( + ctx: RequestCtx<'_, D, H>, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + storage_api::pgf::get_stewards(ctx.wl_storage) +} + +/// Query the continous pgf fundings +fn funding( + ctx: RequestCtx<'_, D, H>, +) -> storage_api::Result> +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + storage_api::pgf::get_payments(ctx.wl_storage) +} diff --git a/shared/src/ledger/queries/vp/pos.rs b/shared/src/ledger/queries/vp/pos.rs index 075b936e252..aff5181a19b 100644 --- a/shared/src/ledger/queries/vp/pos.rs +++ b/shared/src/ledger/queries/vp/pos.rs @@ -69,6 +69,9 @@ router! {POS, ( "delegations" / [owner: Address] ) -> HashSet
= delegation_validators, + ( "delegations_at" / [owner: Address] / [epoch: opt Epoch] ) + -> HashMap = delegations, + ( "bond_deltas" / [source: Address] / [validator: Address] ) -> HashMap = bond_deltas, @@ -463,7 +466,6 @@ where /// Find all the validator addresses to whom the given `owner` address has /// some delegation in any epoch -#[allow(dead_code)] fn delegations( ctx: RequestCtx<'_, D, H>, owner: Address, diff --git a/shared/src/ledger/rpc.rs b/shared/src/ledger/rpc.rs index 7cd7565f664..d0fad687be5 100644 --- a/shared/src/ledger/rpc.rs +++ b/shared/src/ledger/rpc.rs @@ -8,8 +8,13 @@ use borsh::BorshDeserialize; use masp_primitives::asset_type::AssetType; use masp_primitives::merkle_tree::MerklePath; use masp_primitives::sapling::Node; +use namada_core::ledger::governance::parameters::GovernanceParameters; +use namada_core::ledger::governance::storage::proposal::StorageProposal; +use namada_core::ledger::governance::utils::Vote; use namada_core::ledger::storage::LastBlock; +#[cfg(not(feature = "mainnet"))] use namada_core::ledger::testnet_pow; +use namada_core::types::account::Account; use namada_core::types::address::Address; use namada_core::types::storage::Key; use namada_core::types::token::{ @@ -21,11 +26,9 @@ use namada_proof_of_stake::types::{ }; use serde::Serialize; +use crate::core::ledger::governance::storage::keys as gov_storage; use crate::ledger::args::InputAmount; use crate::ledger::events::Event; -use crate::ledger::governance::parameters::GovParams; -use crate::ledger::governance::storage as gov_storage; -use crate::ledger::native_vp::governance::utils::Votes; use crate::ledger::queries::vp::pos::EnrichedBondsAndUnbondsDetails; use crate::ledger::queries::RPC; use crate::proto::Tx; @@ -35,9 +38,8 @@ use crate::tendermint_rpc::error::Error as TError; use crate::tendermint_rpc::query::Query; use crate::tendermint_rpc::Order; use crate::types::control_flow::{time, Halt, TryHalt}; -use crate::types::governance::{ProposalVote, VotePower}; use crate::types::hash::Hash; -use crate::types::key::*; +use crate::types::key::common; use crate::types::storage::{BlockHeight, BlockResults, Epoch, PrefixValue}; use crate::types::{storage, token}; @@ -122,8 +124,8 @@ pub async fn query_block( fn unwrap_client_response( response: Result, ) -> T { - response.unwrap_or_else(|_err| { - panic!("Error in the query"); + response.unwrap_or_else(|err| { + panic!("Error in the query: {:?}", err.to_string()); }) } @@ -145,15 +147,6 @@ pub async fn get_token_balance( ) } -/// Get account's public key stored in its storage sub-space -pub async fn get_public_key( - client: &C, - address: &Address, -) -> Option { - let key = pk_key(address); - query_storage_value(client, &key).await -} - /// Check if the given address is a known validator. pub async fn is_validator( client: &C, @@ -635,69 +628,6 @@ pub async fn query_tx_response( Ok(result) } -/// Get the votes for a given proposal id -pub async fn get_proposal_votes( - client: &C, - epoch: Epoch, - proposal_id: u64, -) -> Votes { - let validators = get_all_validators(client, epoch).await; - - let vote_prefix_key = - gov_storage::get_proposal_vote_prefix_key(proposal_id); - let vote_iter = - query_storage_prefix::(client, &vote_prefix_key).await; - - let mut yay_validators: HashMap = - HashMap::new(); - let mut delegators: HashMap< - Address, - HashMap, - > = HashMap::new(); - - if let Some(vote_iter) = vote_iter { - for (key, vote) in vote_iter { - let voter_address = gov_storage::get_voter_address(&key) - .expect("Vote key should contain the voting address.") - .clone(); - if vote.is_yay() && validators.contains(&voter_address) { - let amount: VotePower = - get_validator_stake(client, epoch, &voter_address) - .await - .try_into() - .expect("Amount of bonds"); - yay_validators.insert(voter_address, (amount, vote)); - } else if !validators.contains(&voter_address) { - let validator_address = - gov_storage::get_vote_delegation_address(&key) - .expect( - "Vote key should contain the delegation address.", - ) - .clone(); - let delegator_token_amount = get_bond_amount_at( - client, - &voter_address, - &validator_address, - epoch, - ) - .await; - if let Some(amount) = delegator_token_amount { - let entry = delegators.entry(voter_address).or_default(); - entry.insert( - validator_address, - (VotePower::from(amount), vote), - ); - } - } - } - } - - Votes { - yay_validators, - delegators, - } -} - /// Get the PoS parameters pub async fn get_pos_params( client: &C, @@ -771,6 +701,34 @@ pub async fn get_delegators_delegation< ) } +/// Get the delegator's delegation at some epoh +pub async fn get_delegators_delegation_at< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, + address: &Address, + epoch: Epoch, +) -> HashMap { + unwrap_client_response::( + RPC.vp() + .pos() + .delegations(client, address, &Some(epoch)) + .await, + ) +} + +/// Query proposal by Id +pub async fn query_proposal_by_id( + client: &C, + proposal_id: u64, +) -> Option { + // let a = RPC.vp().gov().proposal_id(client, &proposal_id).await; + // println!("{:?}", a.err().unwrap()); + unwrap_client_response::( + RPC.vp().gov().proposal_id(client, &proposal_id).await, + ) +} + /// Query and return validator's commission rate and max commission rate change /// per epoch pub async fn query_commission_rate( @@ -798,6 +756,42 @@ pub async fn query_bond( ) } +/// Query the accunt substorage space of an address +pub async fn get_account_info( + client: &C, + owner: &Address, +) -> Option { + unwrap_client_response::>( + RPC.shell().account(client, owner).await, + ) +} + +/// Query if the public_key is revealed +pub async fn is_public_key_revealed< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, + owner: &Address, +) -> bool { + unwrap_client_response::(RPC.shell().revealed(client, owner).await) +} + +/// Query an account substorage at a specific index +pub async fn get_public_key_at( + client: &C, + owner: &Address, + index: u8, +) -> Option { + let account = unwrap_client_response::>( + RPC.shell().account(client, owner).await, + ); + if let Some(account) = account { + account.get_public_key_from_index(index) + } else { + None + } +} + /// Query a validator's unbonds for a given epoch pub async fn query_and_print_unbonds< C: crate::ledger::queries::Client + Sync, @@ -871,11 +865,11 @@ pub async fn query_unbond_with_slashing< } /// Get the givernance parameters -pub async fn get_governance_parameters< +pub async fn query_governance_parameters< C: crate::ledger::queries::Client + Sync, >( client: &C, -) -> GovParams { +) -> GovernanceParameters { let key = gov_storage::get_max_proposal_code_size_key(); let max_proposal_code_size = query_storage_value::(client, &key) .await @@ -896,27 +890,37 @@ pub async fn get_governance_parameters< .await .expect("Parameter should be definied."); - let key = gov_storage::get_min_proposal_period_key(); - let min_proposal_period = query_storage_value::(client, &key) - .await - .expect("Parameter should be definied."); + let key = gov_storage::get_min_proposal_voting_period_key(); + let min_proposal_voting_period = + query_storage_value::(client, &key) + .await + .expect("Parameter should be definied."); let key = gov_storage::get_max_proposal_period_key(); let max_proposal_period = query_storage_value::(client, &key) .await .expect("Parameter should be definied."); - GovParams { - min_proposal_fund: u128::try_from(min_proposal_fund) - .expect("Amount out of bounds") as u64, + GovernanceParameters { + min_proposal_fund, max_proposal_code_size, - min_proposal_period, + min_proposal_voting_period, max_proposal_period, max_proposal_content_size, min_proposal_grace_epochs, } } +/// Get the givernance parameters +pub async fn query_proposal_votes( + client: &C, + proposal_id: u64, +) -> Vec { + unwrap_client_response::>( + RPC.vp().gov().proposal_id_votes(client, &proposal_id).await, + ) +} + /// Get the bond amount at the given epoch pub async fn get_bond_amount_at( client: &C, @@ -947,7 +951,6 @@ pub async fn bonds_and_unbonds( .await, ) } - /// Get bonds and unbonds with all details (slashes and rewards, if any) /// grouped by their bond IDs, enriched with extra information calculated from /// the data. diff --git a/shared/src/ledger/signing.rs b/shared/src/ledger/signing.rs index 4fdd426172e..bfa8214e8d8 100644 --- a/shared/src/ledger/signing.rs +++ b/shared/src/ledger/signing.rs @@ -1,12 +1,6 @@ //! Functions to sign transactions use std::collections::HashMap; -#[cfg(feature = "std")] -use std::env; -#[cfg(feature = "std")] -use std::fs::File; use std::io::ErrorKind; -#[cfg(feature = "std")] -use std::io::Write; use borsh::{BorshDeserialize, BorshSerialize}; use data_encoding::HEXLOWER; @@ -15,11 +9,14 @@ use masp_primitives::asset_type::AssetType; use masp_primitives::transaction::components::sapling::fees::{ InputView, OutputView, }; +use namada_core::proto::SignatureIndex; +use namada_core::types::account::AccountPublicKeysMap; use namada_core::types::address::{ masp, masp_tx_key, Address, ImplicitAddress, }; +// use namada_core::types::storage::Key; use namada_core::types::token::{self, Amount, DenominatedAmount, MaspDenom}; -use namada_core::types::transaction::{pos, MIN_FEE}; +use namada_core::types::transaction::pos; use prost::Message; use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; @@ -30,29 +27,28 @@ use crate::ibc::applications::transfer::msgs::transfer::{ use crate::ibc_proto::google::protobuf::Any; use crate::ledger::masp::make_asset_type; use crate::ledger::parameters::storage as parameter_storage; -use crate::ledger::rpc::{ - format_denominated_amount, query_wasm_code_hash, TxBroadcastData, -}; +use crate::ledger::rpc::{format_denominated_amount, query_wasm_code_hash}; use crate::ledger::tx::{ Error, TX_BOND_WASM, TX_CHANGE_COMMISSION_WASM, TX_IBC_WASM, TX_INIT_ACCOUNT_WASM, TX_INIT_PROPOSAL, TX_INIT_VALIDATOR_WASM, - TX_REVEAL_PK, TX_TRANSFER_WASM, TX_UNBOND_WASM, TX_UPDATE_VP_WASM, + TX_REVEAL_PK, TX_TRANSFER_WASM, TX_UNBOND_WASM, TX_UPDATE_ACCOUNT_WASM, TX_VOTE_PROPOSAL, TX_WITHDRAW_WASM, VP_USER_WASM, }; pub use crate::ledger::wallet::store::AddressVpType; use crate::ledger::wallet::{Wallet, WalletUtils}; use crate::ledger::{args, rpc}; -use crate::proto::{MaspBuilder, Section, Signature, Tx}; +use crate::proto::{MaspBuilder, Section, Tx}; use crate::types::key::*; use crate::types::masp::{ExtendedViewingKey, PaymentAddress}; use crate::types::storage::Epoch; use crate::types::token::Transfer; +use crate::types::transaction::account::{InitAccount, UpdateAccount}; use crate::types::transaction::governance::{ InitProposalData, VoteProposalData, }; -use crate::types::transaction::{ - Fee, InitAccount, InitValidator, TxType, UpdateVp, WrapperTx, -}; +use crate::types::transaction::pos::InitValidator; +use crate::types::transaction::{Fee, TxType}; +use crate::types::tx::TxBuilder; #[cfg(feature = "std")] /// Env. var specifying where to store signing test vectors @@ -61,6 +57,28 @@ const ENV_VAR_LEDGER_LOG_PATH: &str = "NAMADA_LEDGER_LOG_PATH"; /// Env. var specifying where to store transaction debug outputs const ENV_VAR_TX_LOG_PATH: &str = "NAMADA_TX_LOG_PATH"; +/// A struture holding the signing data to craft a transaction +#[derive(Clone)] +pub struct SigningTxData { + /// The public keys associated to an account + pub public_keys: Vec, + /// The threshold associated to an account + pub threshold: u8, + /// The public keys to index map associated to an account + pub account_public_keys_map: AccountPublicKeysMap, + /// The public keys of the fee payer + pub gas_payer: common::PublicKey, +} + +/// Generate a signing key from an address. Default to None if address is empty. +pub fn signer_from_address(address: Option
) -> TxSigningKey { + if let Some(address) = address { + TxSigningKey::WalletAddress(address) + } else { + TxSigningKey::None + } +} + /// Find the public key for the given address and try to load the keypair /// for it from the wallet. If the keypair is encrypted but a password is not /// supplied, then it is interactively prompted. Errors if the key cannot be @@ -80,12 +98,12 @@ pub async fn find_pk< "Looking-up public key of {} from the ledger...", addr.encode() ); - rpc::get_public_key(client, addr).await.ok_or(Error::Other( - format!( + rpc::get_public_key_at(client, addr, 0) + .await + .ok_or(Error::Other(format!( "No public key found for the address {}", addr.encode() - ), - )) + ))) } Address::Implicit(ImplicitAddress(pkh)) => Ok(wallet .find_key_by_pkh(pkh, password) @@ -111,28 +129,20 @@ pub async fn find_pk< pub fn find_key_by_pk( wallet: &mut Wallet, args: &args::Tx, - keypair: &common::PublicKey, + public_key: &common::PublicKey, ) -> Result { - if *keypair == masp_tx_key().ref_to() { + if *public_key == masp_tx_key().ref_to() { // We already know the secret key corresponding to the MASP sentinal key Ok(masp_tx_key()) - } else if args - .signing_key - .as_ref() - .map(|x| x.ref_to() == *keypair) - .unwrap_or(false) - { - // We can lookup the secret key from the CLI arguments in this case - Ok(args.signing_key.clone().unwrap()) } else { // Otherwise we need to search the wallet for the secret key wallet - .find_key_by_pk(keypair, args.password.clone()) + .find_key_by_pk(public_key, args.password.clone()) .map_err(|err| { Error::Other(format!( "Unable to load the keypair from the wallet for public \ key {}. Failed with: {}", - keypair, err + public_key, err )) }) } @@ -152,7 +162,7 @@ pub enum TxSigningKey { /// signer. Return the given signing key or public key of the given signer if /// possible. If no explicit signer given, use the `default`. If no `default` /// is given, an `Error` is returned. -pub async fn tx_signer< +pub async fn tx_signers< C: crate::ledger::queries::Client + Sync, U: WalletUtils, >( @@ -160,32 +170,27 @@ pub async fn tx_signer< wallet: &mut Wallet, args: &args::Tx, default: TxSigningKey, -) -> Result<(Option
, common::PublicKey), Error> { - let signer = if args.dry_run { - // We cannot override the signer if we're doing a dry run - default - } else if let Some(signing_key) = &args.signing_key { - // Otherwise use the signing key override provided by user - return Ok((None, signing_key.ref_to())); +) -> Result, Error> { + let signer = if !&args.signing_keys.is_empty() { + let public_keys = + args.signing_keys.iter().map(|key| key.ref_to()).collect(); + return Ok(public_keys); } else if let Some(verification_key) = &args.verification_key { - return Ok((None, verification_key.clone())); - } else if let Some(signer) = &args.signer { - // Otherwise use the signer address provided by user - TxSigningKey::WalletAddress(signer.clone()) + return Ok(vec![verification_key.clone()]); } else { // Otherwise use the signer determined by the caller default }; + // Now actually fetch the signing key and apply it match signer { TxSigningKey::WalletAddress(signer) if signer == masp() => { - Ok((None, masp_tx_key().ref_to())) + Ok(vec![masp_tx_key().ref_to()]) } - TxSigningKey::WalletAddress(signer) => Ok(( - Some(signer.clone()), + TxSigningKey::WalletAddress(signer) => Ok(vec![ find_pk::(client, wallet, &signer, args.password.clone()) .await?, - )), + ]), TxSigningKey::None => other_err( "All transactions must be signed; please either specify the key \ or the address from which to look up the signing key." @@ -202,26 +207,94 @@ pub async fn tx_signer< /// hashes needed for monitoring the tx on chain. /// /// If it is a dry run, it is not put in a wrapper, but returned as is. -pub async fn sign_tx( +pub fn sign_tx( wallet: &mut Wallet, - tx: &mut Tx, args: &args::Tx, - keypair: &common::PublicKey, -) -> Result<(), Error> { - let keypair = find_key_by_pk(wallet, args, keypair)?; - // Sign over the transacttion data - tx.add_section(Section::Signature(Signature::new( - vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, - ))); - // Remove all the sensitive sections - tx.protocol_filter(); - // Then sign over the bound wrapper - tx.add_section(Section::Signature(Signature::new( - tx.sechashes(), - &keypair, - ))); - Ok(()) + tx_builder: TxBuilder, + signing_data: SigningTxData, +) -> Result { + let fee_payer_keypair = + find_key_by_pk(wallet, args, &signing_data.gas_payer).expect(""); + let tx_builder = tx_builder.add_gas_payer(fee_payer_keypair); + + let tx_builder = if args.signatures.is_empty() { + let signing_tx_keypairs = signing_data + .public_keys + .iter() + .filter_map(|public_key| { + match find_key_by_pk(wallet, args, public_key) { + Ok(secret_key) => Some(secret_key), + Err(_) => None, + } + }) + .collect::>(); + + tx_builder.add_signing_keys( + signing_tx_keypairs, + signing_data.account_public_keys_map, + ) + } else { + let signatures = args + .signatures + .iter() + .map(|bytes| SignatureIndex::deserialize(bytes).unwrap()) + .collect(); + tx_builder.add_signatures(signatures) + }; + + Ok(tx_builder) +} + +/// Return the necessary data regarding an account to be able to generate a +/// multisignature section +pub async fn aux_signing_data< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + args: &args::Tx, + owner: &Address, + default_signer: TxSigningKey, +) -> Result { + let public_keys = + tx_signers::(client, wallet, args, default_signer.clone()) + .await?; + + let (account_public_keys_map, threshold) = match owner { + Address::Established(_) => { + let account = rpc::get_account_info::(client, owner).await; + if let Some(account) = account { + (account.public_keys_map, account.threshold) + } else { + return Err(Error::InvalidAccount(owner.encode())); + } + } + Address::Implicit(_) => { + (AccountPublicKeysMap::from_iter(public_keys.clone()), 1u8) + } + Address::Internal(_) => { + return Err(Error::InvalidAccount(owner.encode())); + } + }; + + let gas_payer = match &args.gas_payer { + Some(keypair) => keypair.ref_to(), + None => { + if let Some(public_key) = public_keys.get(0) { + public_key.clone() + } else { + return Err(Error::InvalidFeePayer); + } + } + }; + + Ok(SigningTxData { + public_keys, + threshold, + account_public_keys_map, + gas_payer, + }) } #[cfg(not(feature = "mainnet"))] @@ -234,26 +307,26 @@ pub async fn solve_pow_challenge( requires_pow: bool, ) -> (Option, Fee) { let wrapper_tx_fees_key = parameter_storage::get_wrapper_tx_fees_key(); - let fee_amount = rpc::query_storage_value::( + let gas_amount = rpc::query_storage_value::( client, &wrapper_tx_fees_key, ) .await .unwrap_or_default(); - let fee_token = &args.fee_token; + let gas_token = &args.gas_token; let source = Address::from(keypair); - let balance_key = token::balance_key(fee_token, &source); + let balance_key = token::balance_key(gas_token, &source); let balance = rpc::query_storage_value::(client, &balance_key) .await .unwrap_or_default(); - let is_bal_sufficient = fee_amount <= balance; + let is_bal_sufficient = gas_amount <= balance; if !is_bal_sufficient { - let token_addr = args.fee_token.clone(); + let token_addr = args.gas_token.clone(); let err_msg = format!( "The wrapper transaction source doesn't have enough balance to \ pay fee {}, got {}.", - format_denominated_amount(client, &token_addr, fee_amount).await, + format_denominated_amount(client, &token_addr, gas_amount).await, format_denominated_amount(client, &token_addr, balance).await, ); if !args.force && cfg!(feature = "mainnet") { @@ -261,13 +334,13 @@ pub async fn solve_pow_challenge( } } let fee = Fee { - amount: fee_amount, - token: fee_token.clone(), + amount: gas_amount, + token: gas_token.clone(), }; // A PoW solution can be used to allow zero-fee testnet transactions // If the address derived from the keypair doesn't have enough balance // to pay for the fee, allow to find a PoW solution instead. - if requires_pow || !is_bal_sufficient { + if (requires_pow || !is_bal_sufficient) && !args.dump_tx { println!("The transaction requires the completion of a PoW challenge."); // Obtain a PoW challenge for faucet withdrawal let challenge = rpc::get_testnet_pow_challenge(client, source).await; @@ -300,210 +373,26 @@ pub async fn update_pow_challenge( /// Create a wrapper tx from a normal tx. Get the hash of the /// wrapper and its payload which is needed for monitoring its /// progress on chain. -pub async fn wrap_tx< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( +pub async fn wrap_tx( client: &C, - #[allow(unused_variables)] wallet: &mut Wallet, + tx_builder: TxBuilder, args: &args::Tx, epoch: Epoch, - mut tx: Tx, - keypair: &common::PublicKey, + gas_payer: common::PublicKey, #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> Tx { +) -> TxBuilder { #[cfg(not(feature = "mainnet"))] let (pow_solution, fee) = - solve_pow_challenge(client, args, keypair, requires_pow).await; - // This object governs how the payload will be processed - tx.update_header(TxType::Wrapper(Box::new(WrapperTx::new( - fee, - keypair.clone(), - epoch, - args.gas_limit.clone(), - #[cfg(not(feature = "mainnet"))] - pow_solution, - )))); - tx.header.chain_id = args.chain_id.clone().unwrap(); - tx.header.expiration = args.expiration; - - #[cfg(feature = "std")] - // Attempt to decode the construction - if let Ok(path) = env::var(ENV_VAR_LEDGER_LOG_PATH) { - let mut tx = tx.clone(); - // Contract the large data blobs in the transaction - tx.wallet_filter(); - // Convert the transaction to Ledger format - let decoding = to_ledger_vector(client, wallet, &tx) - .await - .expect("unable to decode transaction"); - let output = serde_json::to_string(&decoding) - .expect("failed to serialize decoding"); - // Record the transaction at the identified path - let mut f = File::options() - .append(true) - .create(true) - .open(path) - .expect("failed to open test vector file"); - writeln!(f, "{},", output) - .expect("unable to write test vector to file"); - } - #[cfg(feature = "std")] - // Attempt to decode the construction - if let Ok(path) = env::var(ENV_VAR_TX_LOG_PATH) { - let mut tx = tx.clone(); - // Contract the large data blobs in the transaction - tx.wallet_filter(); - // Record the transaction at the identified path - let mut f = File::options() - .append(true) - .create(true) - .open(path) - .expect("failed to open test vector file"); - writeln!(f, "{:x?},", tx).expect("unable to write test vector to file"); - } - - tx -} - -/// Create a wrapper tx from a normal tx. Get the hash of the -/// wrapper and its payload which is needed for monitoring its -/// progress on chain. -pub async fn sign_wrapper< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( - client: &C, - #[allow(unused_variables)] wallet: &mut Wallet, - args: &args::Tx, - epoch: Epoch, - mut tx: Tx, - keypair: &common::SecretKey, - #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> TxBroadcastData { - let fee_amount = if cfg!(feature = "mainnet") { - Amount::native_whole(MIN_FEE) - } else { - let wrapper_tx_fees_key = parameter_storage::get_wrapper_tx_fees_key(); - rpc::query_storage_value::( - client, - &wrapper_tx_fees_key, - ) - .await - .unwrap_or_default() - }; - let fee_token = &args.fee_token; - let source = Address::from(&keypair.ref_to()); - let balance_key = token::balance_key(fee_token, &source); - let balance = - rpc::query_storage_value::(client, &balance_key) - .await - .unwrap_or_default(); - let is_bal_sufficient = fee_amount <= balance; - if !is_bal_sufficient { - let token_addr = args.fee_token.clone(); - let err_msg = format!( - "The wrapper transaction source doesn't have enough balance to \ - pay fee {}, got {}.", - format_denominated_amount(client, &token_addr, fee_amount).await, - format_denominated_amount(client, &token_addr, balance).await, - ); - eprintln!("{}", err_msg); - if !args.force && cfg!(feature = "mainnet") { - panic!("{}", err_msg); - } - } + solve_pow_challenge(client, args, &gas_payer, requires_pow).await; - #[cfg(not(feature = "mainnet"))] - // A PoW solution can be used to allow zero-fee testnet transactions - let pow_solution: Option = { - // If the address derived from the keypair doesn't have enough balance - // to pay for the fee, allow to find a PoW solution instead. - if requires_pow || !is_bal_sufficient { - println!( - "The transaction requires the completion of a PoW challenge." - ); - // Obtain a PoW challenge for faucet withdrawal - let challenge = - rpc::get_testnet_pow_challenge(client, source).await; - - // Solve the solution, this blocks until a solution is found - let solution = challenge.solve(); - Some(solution) - } else { - None - } - }; - - // This object governs how the payload will be processed - tx.update_header(TxType::Wrapper(Box::new(WrapperTx::new( - Fee { - amount: fee_amount, - token: fee_token.clone(), - }, - keypair.ref_to(), + tx_builder.add_wrapper( + fee, + gas_payer, epoch, args.gas_limit.clone(), #[cfg(not(feature = "mainnet"))] pow_solution, - )))); - tx.header.chain_id = args.chain_id.clone().unwrap(); - tx.header.expiration = args.expiration; - - #[cfg(feature = "std")] - // Attempt to decode the construction - if let Ok(path) = env::var(ENV_VAR_LEDGER_LOG_PATH) { - let mut tx = tx.clone(); - // Contract the large data blobs in the transaction - tx.wallet_filter(); - // Convert the transaction to Ledger format - let decoding = to_ledger_vector(client, wallet, &tx) - .await - .expect("unable to decode transaction"); - let output = serde_json::to_string(&decoding) - .expect("failed to serialize decoding"); - // Record the transaction at the identified path - let mut f = File::options() - .append(true) - .create(true) - .open(path) - .expect("failed to open test vector file"); - writeln!(f, "{},", output) - .expect("unable to write test vector to file"); - } - #[cfg(feature = "std")] - // Attempt to decode the construction - if let Ok(path) = env::var(ENV_VAR_TX_LOG_PATH) { - let mut tx = tx.clone(); - // Contract the large data blobs in the transaction - tx.wallet_filter(); - // Record the transaction at the identified path - let mut f = File::options() - .append(true) - .create(true) - .open(path) - .expect("failed to open test vector file"); - writeln!(f, "{:x?},", tx).expect("unable to write test vector to file"); - } - - // Remove all the sensitive sections - tx.protocol_filter(); - // Then sign over the bound wrapper committing to all other sections - tx.add_section(Section::Signature(Signature::new(tx.sechashes(), keypair))); - // We use this to determine when the wrapper tx makes it on-chain - let wrapper_hash = tx.header_hash().to_string(); - // We use this to determine when the decrypted inner tx makes it - // on-chain - let decrypted_hash = tx - .clone() - .update_header(TxType::Raw) - .header_hash() - .to_string(); - TxBroadcastData::Wrapper { - tx, - wrapper_hash, - decrypted_hash, - } + ) } #[allow(clippy::result_large_err)] @@ -713,6 +602,55 @@ pub async fn make_ledger_masp_endpoints< } } +/// Internal method used to generate transaction test vectors +#[cfg(feature = "std")] +pub async fn generate_test_vector< + C: crate::ledger::queries::Client + Sync, + U: WalletUtils, +>( + client: &C, + wallet: &mut Wallet, + tx: &Tx, +) { + use std::env; + use std::fs::File; + use std::io::Write; + + if let Ok(path) = env::var(ENV_VAR_LEDGER_LOG_PATH) { + let mut tx = tx.clone(); + // Contract the large data blobs in the transaction + tx.wallet_filter(); + // Convert the transaction to Ledger format + let decoding = to_ledger_vector(client, wallet, &tx) + .await + .expect("unable to decode transaction"); + let output = serde_json::to_string(&decoding) + .expect("failed to serialize decoding"); + // Record the transaction at the identified path + let mut f = File::options() + .append(true) + .create(true) + .open(path) + .expect("failed to open test vector file"); + writeln!(f, "{},", output) + .expect("unable to write test vector to file"); + } + + // Attempt to decode the construction + if let Ok(path) = env::var(ENV_VAR_TX_LOG_PATH) { + let mut tx = tx.clone(); + // Contract the large data blobs in the transaction + tx.wallet_filter(); + // Record the transaction at the identified path + let mut f = File::options() + .append(true) + .create(true) + .open(path) + .expect("failed to open test vector file"); + writeln!(f, "{:x?},", tx).expect("unable to write test vector to file"); + } +} + /// Converts the given transaction to the form that is displayed on the Ledger /// device pub async fn to_ledger_vector< @@ -738,9 +676,10 @@ pub async fn to_ledger_vector< .unwrap(); let reveal_pk_hash = query_wasm_code_hash(client, TX_REVEAL_PK).await.unwrap(); - let update_vp_hash = query_wasm_code_hash(client, TX_UPDATE_VP_WASM) - .await - .unwrap(); + let update_account_hash = + query_wasm_code_hash(client, TX_UPDATE_ACCOUNT_WASM) + .await + .unwrap(); let transfer_hash = query_wasm_code_hash(client, TX_TRANSFER_WASM) .await .unwrap(); @@ -811,12 +750,12 @@ pub async fn to_ledger_vector< tv.output.extend(vec![ format!("Type : Init Account"), - format!("Public key : {}", init_account.public_key), + format!("Public key : {:?}", init_account.public_keys), format!("VP type : {}", vp_code), ]); tv.output_expert.extend(vec![ - format!("Public key : {}", init_account.public_key), + format!("Public key : {:?}", init_account.public_keys), format!("VP type : {}", HEXLOWER.encode(&extra.0)), ]); } else if code_hash == init_validator_hash { @@ -841,7 +780,7 @@ pub async fn to_ledger_vector< tv.output.extend(vec![ format!("Type : Init Validator"), - format!("Account key : {}", init_validator.account_key), + format!("Account key : {:?}", init_validator.account_keys), format!("Consensus key : {}", init_validator.consensus_key), format!("Protocol key : {}", init_validator.protocol_key), format!("DKG key : {}", init_validator.dkg_key), @@ -854,7 +793,7 @@ pub async fn to_ledger_vector< ]); tv.output_expert.extend(vec![ - format!("Account key : {}", init_validator.account_key), + format!("Account key : {:?}", init_validator.account_keys), format!("Consensus key : {}", init_validator.consensus_key), format!("Protocol key : {}", init_validator.protocol_key), format!("DKG key : {}", init_validator.dkg_key), @@ -952,36 +891,40 @@ pub async fn to_ledger_vector< tv.output_expert .extend(vec![format!("Public key : {}", public_key)]); - } else if code_hash == update_vp_hash { + } else if code_hash == update_account_hash { let transfer = - UpdateVp::try_from_slice(&tx.data().ok_or_else(|| { + UpdateAccount::try_from_slice(&tx.data().ok_or_else(|| { std::io::Error::from(ErrorKind::InvalidData) })?)?; tv.name = "Update VP 0".to_string(); - let extra = tx - .get_section(&transfer.vp_code_hash) - .and_then(|x| Section::extra_data_sec(x.as_ref())) - .expect("unable to load vp code") - .code - .hash(); - let vp_code = if extra == user_hash { - "User".to_string() - } else { - HEXLOWER.encode(&extra.0) - }; - - tv.output.extend(vec![ - format!("Type : Update VP"), - format!("Address : {}", transfer.addr), - format!("VP type : {}", vp_code), - ]); + match &transfer.vp_code_hash { + Some(hash) => { + let extra = tx + .get_section(hash) + .and_then(|x| Section::extra_data_sec(x.as_ref())) + .expect("unable to load vp code") + .code + .hash(); + let vp_code = if extra == user_hash { + "User".to_string() + } else { + HEXLOWER.encode(&extra.0) + }; + tv.output.extend(vec![ + format!("Type : Update VP"), + format!("Address : {}", transfer.addr), + format!("VP type : {}", vp_code), + ]); - tv.output_expert.extend(vec![ - format!("Address : {}", transfer.addr), - format!("VP type : {}", HEXLOWER.encode(&extra.0)), - ]); + tv.output_expert.extend(vec![ + format!("Address : {}", transfer.addr), + format!("VP type : {}", HEXLOWER.encode(&extra.0)), + ]); + } + None => (), + }; } else if code_hash == transfer_hash { let transfer = Transfer::try_from_slice(&tx.data().ok_or_else(|| { diff --git a/shared/src/ledger/tx.rs b/shared/src/ledger/tx.rs index 08243a22e50..41690c63f56 100644 --- a/shared/src/ledger/tx.rs +++ b/shared/src/ledger/tx.rs @@ -1,6 +1,7 @@ //! SDK functions to construct different types of transactions use std::borrow::Cow; use std::collections::{BTreeMap, HashMap, HashSet}; +use std::fs::File; use std::str::FromStr; use std::time::Duration; @@ -15,15 +16,24 @@ use masp_primitives::transaction::components::transparent::fees::{ InputView as TransparentInputView, OutputView as TransparentOutputView, }; use masp_primitives::transaction::components::Amount; -use namada_core::types::address::{masp, masp_tx_key, Address}; +use namada_core::ledger::governance::cli::onchain::{ + DefaultProposal, PgfFundingProposal, PgfStewardProposal, ProposalVote, +}; +use namada_core::ledger::governance::storage::proposal::ProposalType; +use namada_core::ledger::governance::storage::vote::StorageProposalVote; +use namada_core::types::address::{masp, Address}; use namada_core::types::dec::Dec; use namada_core::types::token::MaspDenom; +use namada_core::types::transaction::governance::{ + InitProposalData, VoteProposalData, +}; use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::types::{CommissionPair, ValidatorState}; use prost::EncodeError; use thiserror::Error; use super::rpc::query_wasm_code_hash; +use super::signing; use crate::ibc::applications::transfer::msgs::transfer::MsgTransfer; use crate::ibc::core::ics04_channel::timeout::TimeoutHeight; use crate::ibc::signer::Signer; @@ -32,15 +42,13 @@ use crate::ibc::tx_msg::Msg; use crate::ibc::Height as IbcHeight; use crate::ibc_proto::cosmos::base::v1beta1::Coin; use crate::ledger::args::{self, InputAmount}; -use crate::ledger::governance::storage as gov_storage; use crate::ledger::masp::{ShieldedContext, ShieldedUtils}; use crate::ledger::rpc::{ self, format_denominated_amount, validate_amount, TxBroadcastData, TxResponse, }; -use crate::ledger::signing::{tx_signer, wrap_tx, TxSigningKey}; use crate::ledger::wallet::{Wallet, WalletUtils}; -use crate::proto::{Code, Data, MaspBuilder, Section, Tx}; +use crate::proto::{MaspBuilder, Tx}; use crate::tendermint_rpc::endpoint::broadcast::tx_sync::Response; use crate::tendermint_rpc::error::Error as RpcError; use crate::types::control_flow::{time, ProceedOrElse}; @@ -48,7 +56,9 @@ use crate::types::key::*; use crate::types::masp::TransferTarget; use crate::types::storage::Epoch; use crate::types::time::DateTimeUtc; -use crate::types::transaction::{pos, InitAccount, TxType, UpdateVp}; +use crate::types::transaction::account::{InitAccount, UpdateAccount}; +use crate::types::transaction::{pos, TxType}; +use crate::types::tx::TxBuilder; use crate::types::{storage, token}; use crate::vm; use crate::vm::WasmValidationError; @@ -64,7 +74,7 @@ pub const TX_VOTE_PROPOSAL: &str = "tx_vote_proposal.wasm"; /// Reveal public key transaction WASM path pub const TX_REVEAL_PK: &str = "tx_reveal_pk.wasm"; /// Update validity predicate WASM path -pub const TX_UPDATE_VP_WASM: &str = "tx_update_vp.wasm"; +pub const TX_UPDATE_ACCOUNT_WASM: &str = "tx_update_account.wasm"; /// Transfer transaction WASM path pub const TX_TRANSFER_WASM: &str = "tx_transfer.wasm"; /// IBC transaction WASM path @@ -197,6 +207,21 @@ pub enum Error { /// Like EncodeTxFailure but for the encode error type #[error("Encoding tx data, {0}, shouldn't fail")] EncodeFailure(EncodeError), + /// Failed to deserialize the proposal data from json + #[error("Failed to deserialize the proposal data: {0}")] + FailedGovernaneProposalDeserialize(String), + /// The proposal data are invalid + #[error("Proposal data are invalid: {0}")] + InvalidProposal(String), + /// The proposal vote is not valid + #[error("Proposal vote is invalid")] + InvalidProposalVote, + /// The proposal can't be voted + #[error("Proposal {0} can't be voted")] + InvalidProposalVotingPeriod(u64), + /// The proposal can't be found + #[error("Proposal {0} can't be found")] + ProposalDoesNotExist(u64), /// Encoding public key failure #[error("Encoding a public key, {0}, shouldn't fail")] EncodeKeyFailure(std::io::Error), @@ -220,6 +245,18 @@ pub enum Error { /// Epoch not in storage #[error("Proposal end epoch is not in the storage.")] EpochNotInStorage, + /// Couldn't understand who the fee payer is + #[error("Either --signing-keys or --gas-payer must be available.")] + InvalidFeePayer, + /// Account threshold is not set + #[error("Account threshold must be set.")] + MissingAccountThreshold, + /// Not enough signature + #[error("Account threshold is {0} but the valid signatures are {1}.")] + MissingSigningKeys(u8, u8), + /// Invalid owner account + #[error("The source account {0} is not valid or doesn't exist.")] + InvalidAccount(String), /// Other Errors that may show up when using the interface #[error("{0}")] Other(String), @@ -233,6 +270,8 @@ pub enum ProcessTxResponse { Broadcast(Response), /// Result of dry running transaction DryRun, + /// Dump transaction to disk + Dump, } impl ProcessTxResponse { @@ -245,41 +284,47 @@ impl ProcessTxResponse { } } +/// Build and dump a transaction either to file or to screen +pub fn dump_tx(args: &args::Tx, tx_builder: TxBuilder) { + let tx = tx_builder.build(); + let tx_id = tx.header_hash(); + let serialized_tx = tx.serialize(); + match args.output_folder.to_owned() { + Some(path) => { + let tx_filename = format!("{}.tx", tx_id); + let out = File::create(path.join(tx_filename)).unwrap(); + serde_json::to_writer_pretty(out, &serialized_tx) + .expect("Should be able to write to file.") + } + None => println!("{}", serialized_tx), + } +} + /// Prepare a transaction for signing and submission by adding a wrapper header /// to it. -pub async fn prepare_tx< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( +pub async fn prepare_tx( client: &C, - wallet: &mut Wallet, args: &args::Tx, - tx: Tx, - default_signer: TxSigningKey, + tx_builder: TxBuilder, + gas_payer: common::PublicKey, #[cfg(not(feature = "mainnet"))] requires_pow: bool, -) -> Result<(Tx, Option
, common::PublicKey), Error> { - let (signer_addr, signer_pk) = - tx_signer::(client, wallet, args, default_signer.clone()).await?; - if args.dry_run { - Ok((tx, signer_addr, signer_pk)) +) -> Result { + let tx_builder = if args.dry_run { + tx_builder } else { let epoch = rpc::query_epoch(client).await; - Ok(( - wrap_tx( - client, - wallet, - args, - epoch, - tx.clone(), - &signer_pk, - #[cfg(not(feature = "mainnet"))] - requires_pow, - ) - .await, - signer_addr, - signer_pk, - )) - } + signing::wrap_tx( + client, + tx_builder, + args, + epoch, + gas_payer, + #[cfg(not(feature = "mainnet"))] + requires_pow, + ) + .await + }; + Ok(tx_builder) } /// Submit transaction and wait for result. Returns a list of addresses @@ -291,10 +336,8 @@ pub async fn process_tx< client: &C, wallet: &mut Wallet, args: &args::Tx, - mut tx: Tx, + tx: Tx, ) -> Result { - // Remove all the sensitive sections - tx.protocol_filter(); // NOTE: use this to print the request JSON body: // let request = @@ -305,6 +348,11 @@ pub async fn process_tx< // let request_body = request.into_json(); // println!("HTTP request body: {}", request_body); + #[cfg(feature = "std")] + { + super::signing::generate_test_vector(client, wallet, &tx).await; + } + if args.dry_run { expect_dry_broadcast(TxBroadcastData::DryRun(tx), client).await } else { @@ -345,72 +393,38 @@ pub async fn process_tx< } } -/// Submit transaction to reveal public key -pub async fn build_reveal_pk< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( - client: &C, - wallet: &mut Wallet, - args: args::RevealPk, -) -> Result, common::PublicKey)>, Error> { - let args::RevealPk { - tx: args, - public_key, - } = args; - let public_key = public_key; - if !is_reveal_pk_needed::(client, &public_key, &args).await? { - let addr: Address = (&public_key).into(); - println!("PK for {addr} is already revealed, nothing to do."); - Ok(None) - } else { - // If not, submit it - Ok(Some( - build_reveal_pk_aux::(client, wallet, &public_key, &args) - .await?, - )) - } -} - -/// Submit transaction to rveeal public key if needed -pub async fn is_reveal_pk_needed< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( +/// Check if a reveal public key transaction is needed +pub async fn is_reveal_pk_needed( client: &C, - public_key: &common::PublicKey, - args: &args::Tx, + address: &Address, + force: bool, ) -> Result where C: crate::ledger::queries::Client + Sync, - U: WalletUtils, { - let addr: Address = public_key.into(); // Check if PK revealed - Ok(args.force || !has_revealed_pk(client, &addr).await) + Ok(force || !has_revealed_pk(client, address).await) } /// Check if the public key for the given address has been revealed pub async fn has_revealed_pk( client: &C, - addr: &Address, + address: &Address, ) -> bool { - rpc::get_public_key(client, addr).await.is_some() + rpc::is_public_key_revealed(client, address).await } /// Submit transaction to reveal the given public key -pub async fn build_reveal_pk_aux< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( +pub async fn build_reveal_pk( client: &C, - wallet: &mut Wallet, - public_key: &common::PublicKey, args: &args::Tx, -) -> Result<(Tx, Option
, common::PublicKey), Error> { - let addr: Address = public_key.into(); - println!("Submitting a tx to reveal the public key for address {addr}..."); - let tx_data = public_key.try_to_vec().map_err(Error::EncodeKeyFailure)?; + address: &Address, + public_key: &common::PublicKey, + gas_payer: &common::PublicKey, +) -> Result { + println!( + "Submitting a tx to reveal the public key for address {address}..." + ); let tx_code_hash = query_wasm_code_hash( client, @@ -419,18 +433,18 @@ pub async fn build_reveal_pk_aux< .await .unwrap(); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.chain_id.clone().expect("value should be there"); - tx.header.expiration = args.expiration; - tx.set_data(Data::new(tx_data)); - tx.set_code(Code::from_hash(tx_code_hash)); + let chain_id = args.chain_id.clone().unwrap(); + let tx_builder = TxBuilder::new(chain_id, args.expiration); + + let tx_builder = tx_builder + .add_code_from_hash(tx_code_hash) + .add_data(public_key); - prepare_tx::( + prepare_tx::( client, - wallet, args, - tx, - TxSigningKey::WalletAddress(addr), + tx_builder, + gas_payer.clone(), #[cfg(not(feature = "mainnet"))] false, ) @@ -632,26 +646,30 @@ pub async fn save_initialized_accounts( /// Submit validator comission rate change pub async fn build_validator_commission_change< C: crate::ledger::queries::Client + Sync, - U: WalletUtils, >( client: &C, - wallet: &mut Wallet, - args: args::CommissionRateChange, -) -> Result<(Tx, Option
, common::PublicKey), Error> { + args::CommissionRateChange { + tx, + validator, + rate, + tx_code_path, + }: args::CommissionRateChange, + gas_payer: &common::PublicKey, +) -> Result { let epoch = rpc::query_epoch(client).await; let tx_code_hash = - query_wasm_code_hash(client, args.tx_code_path.to_str().unwrap()) + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) .await .unwrap(); let params: PosParams = rpc::get_pos_params(client).await; - let validator = args.validator.clone(); + let validator = validator.clone(); if rpc::is_validator(client, &validator).await { - if args.rate < Dec::zero() || args.rate > Dec::one() { - eprintln!("Invalid new commission rate, received {}", args.rate); - return Err(Error::InvalidCommissionRate(args.rate)); + if rate < Dec::zero() || rate > Dec::one() { + eprintln!("Invalid new commission rate, received {}", rate); + return Err(Error::InvalidCommissionRate(rate)); } let pipeline_epoch_minus_one = epoch + params.pipeline_len - 1; @@ -667,7 +685,7 @@ pub async fn build_validator_commission_change< commission_rate, max_commission_change_per_epoch, }) => { - if args.rate.abs_diff(&commission_rate) + if rate.abs_diff(&commission_rate) > max_commission_change_per_epoch { eprintln!( @@ -675,44 +693,40 @@ pub async fn build_validator_commission_change< the predecessor epoch in which the rate will take \ effect." ); - if !args.tx.force { - return Err(Error::InvalidCommissionRate(args.rate)); + if !tx.force { + return Err(Error::InvalidCommissionRate(rate)); } } } None => { eprintln!("Error retrieving from storage"); - if !args.tx.force { + if !tx.force { return Err(Error::Retrieval); } } } } else { eprintln!("The given address {validator} is not a validator."); - if !args.tx.force { + if !tx.force { return Err(Error::InvalidValidatorAddress(validator)); } } let data = pos::CommissionChange { - validator: args.validator.clone(), - new_rate: args.rate, + validator: validator.clone(), + new_rate: rate, }; - let data = data.try_to_vec().map_err(Error::EncodeTxFailure)?; - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.tx.chain_id.clone().unwrap(); - tx.header.expiration = args.tx.expiration; - tx.set_data(Data::new(data)); - tx.set_code(Code::from_hash(tx_code_hash)); + let chain_id = tx.chain_id.clone().unwrap(); + let tx_builder = TxBuilder::new(chain_id, tx.expiration); + + let tx_builder = tx_builder.add_code_from_hash(tx_code_hash).add_data(data); - let default_signer = args.validator.clone(); - prepare_tx::( + prepare_tx::( client, - wallet, - &args.tx, - tx, - TxSigningKey::WalletAddress(default_signer), + &tx, + tx_builder, + gas_payer.clone(), #[cfg(not(feature = "mainnet"))] false, ) @@ -722,62 +736,19 @@ pub async fn build_validator_commission_change< /// Submit transaction to unjail a jailed validator pub async fn build_unjail_validator< C: crate::ledger::queries::Client + Sync, - U: WalletUtils, >( client: &C, - wallet: &mut Wallet, - args: args::TxUnjailValidator, -) -> Result<(Tx, Option
, common::PublicKey), Error> { - if !rpc::is_validator(client, &args.validator).await { - eprintln!("The given address {} is not a validator.", &args.validator); - if !args.tx.force { - return Err(Error::InvalidValidatorAddress(args.validator.clone())); - } - } - - let tx_code_hash = - query_wasm_code_hash(client, args.tx_code_path.to_str().unwrap()) - .await - .unwrap(); - - let data = args - .validator - .clone() - .try_to_vec() - .map_err(Error::EncodeTxFailure)?; - - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.tx.chain_id.clone().unwrap(); - tx.header.expiration = args.tx.expiration; - tx.set_data(Data::new(data)); - tx.set_code(Code::from_hash(tx_code_hash)); - - let default_signer = args.validator; - prepare_tx( - client, - wallet, - &args.tx, + args::TxUnjailValidator { tx, - TxSigningKey::WalletAddress(default_signer), - #[cfg(not(feature = "mainnet"))] - false, - ) - .await -} - -/// Submit transaction to unjail a jailed validator -pub async fn submit_unjail_validator< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( - client: &C, - wallet: &mut Wallet, - args: args::TxUnjailValidator, -) -> Result<(), Error> { - if !rpc::is_validator(client, &args.validator).await { - eprintln!("The given address {} is not a validator.", &args.validator); - if !args.tx.force { - return Err(Error::InvalidValidatorAddress(args.validator.clone())); + validator, + tx_code_path, + }: args::TxUnjailValidator, + gas_payer: &common::PublicKey, +) -> Result { + if !rpc::is_validator(client, &validator).await { + eprintln!("The given address {} is not a validator.", &validator); + if !tx.force { + return Err(Error::InvalidValidatorAddress(validator.clone())); } } @@ -786,24 +757,22 @@ pub async fn submit_unjail_validator< let pipeline_epoch = current_epoch + params.pipeline_len; let validator_state_at_pipeline = - rpc::get_validator_state(client, &args.validator, Some(pipeline_epoch)) + rpc::get_validator_state(client, &validator, Some(pipeline_epoch)) .await .expect("Validator state should be defined."); if validator_state_at_pipeline != ValidatorState::Jailed { eprintln!( "The given validator address {} is not jailed at the pipeline \ epoch when it would be restored to one of the validator sets.", - &args.validator + &validator ); - if !args.tx.force { - return Err(Error::ValidatorNotCurrentlyJailed( - args.validator.clone(), - )); + if !tx.force { + return Err(Error::ValidatorNotCurrentlyJailed(validator.clone())); } } let last_slash_epoch_key = - crate::ledger::pos::validator_last_slash_key(&args.validator); + crate::ledger::pos::validator_last_slash_key(&validator); let last_slash_epoch = rpc::query_storage_value::(client, &last_slash_epoch_key) .await; @@ -814,66 +783,64 @@ pub async fn submit_unjail_validator< eprintln!( "The given validator address {} is currently frozen and not \ yet eligible to be unjailed.", - &args.validator + &validator ); - if !args.tx.force { + if !tx.force { return Err(Error::ValidatorNotCurrentlyJailed( - args.validator.clone(), + validator.clone(), )); } } } let tx_code_hash = - query_wasm_code_hash(client, args.tx_code_path.to_str().unwrap()) + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) .await .unwrap(); - let data = args - .validator + let _data = validator .clone() .try_to_vec() .map_err(Error::EncodeTxFailure)?; - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.tx.chain_id.clone().unwrap(); - tx.header.expiration = args.tx.expiration; - tx.set_data(Data::new(data)); - tx.set_code(Code::from_hash(tx_code_hash)); + let chain_id = tx.chain_id.clone().unwrap(); + let tx_builder = TxBuilder::new(chain_id, tx.expiration); + + let tx_builder = tx_builder + .add_code_from_hash(tx_code_hash) + .add_data(validator.clone()); - let default_signer = args.validator; prepare_tx( client, - wallet, - &args.tx, - tx, - TxSigningKey::WalletAddress(default_signer), + &tx, + tx_builder, + gas_payer.clone(), #[cfg(not(feature = "mainnet"))] false, ) - .await?; - Ok(()) + .await } /// Submit transaction to withdraw an unbond -pub async fn build_withdraw< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( +pub async fn build_withdraw( client: &C, - wallet: &mut Wallet, - args: args::Withdraw, -) -> Result<(Tx, Option
, common::PublicKey), Error> { + args::Withdraw { + tx, + validator, + source, + tx_code_path, + }: args::Withdraw, + gas_payer: &common::PublicKey, +) -> Result { let epoch = rpc::query_epoch(client).await; let validator = - known_validator_or_err(args.validator.clone(), args.tx.force, client) - .await?; + known_validator_or_err(validator.clone(), tx.force, client).await?; - let source = args.source.clone(); + let source = source.clone(); let tx_code_hash = - query_wasm_code_hash(client, args.tx_code_path.to_str().unwrap()) + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) .await .unwrap(); @@ -886,6 +853,7 @@ pub async fn build_withdraw< Some(epoch), ) .await; + if tokens.is_zero() { eprintln!( "There are no unbonded bonds ready to withdraw in the current \ @@ -893,7 +861,7 @@ pub async fn build_withdraw< epoch ); rpc::query_and_print_unbonds(client, &bond_source, &validator).await; - if !args.tx.force { + if !tx.force { return Err(Error::NoUnbondReady(epoch)); } } else { @@ -905,21 +873,17 @@ pub async fn build_withdraw< } let data = pos::Withdraw { validator, source }; - let data = data.try_to_vec().map_err(Error::EncodeTxFailure)?; - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.tx.chain_id.clone().unwrap(); - tx.header.expiration = args.tx.expiration; - tx.set_data(Data::new(data)); - tx.set_code(Code::from_hash(tx_code_hash)); + let chain_id = tx.chain_id.clone().unwrap(); + let tx_builder = TxBuilder::new(chain_id, tx.expiration); - let default_signer = args.source.unwrap_or(args.validator); - prepare_tx::( + let tx_builder = tx_builder.add_code_from_hash(tx_code_hash).add_data(data); + + prepare_tx::( client, - wallet, - &args.tx, - tx, - TxSigningKey::WalletAddress(default_signer), + &tx, + tx_builder, + gas_payer.clone(), #[cfg(not(feature = "mainnet"))] false, ) @@ -932,50 +896,48 @@ pub async fn build_unbond< U: WalletUtils, >( client: &C, - wallet: &mut Wallet, - args: args::Unbond, -) -> Result< - ( - Tx, - Option
, - common::PublicKey, - Option<(Epoch, token::Amount)>, - ), - Error, -> { - let source = args.source.clone(); + _wallet: &mut Wallet, + args::Unbond { + tx, + validator, + amount, + source, + tx_code_path, + }: args::Unbond, + gas_payer: &common::PublicKey, +) -> Result<(TxBuilder, Option<(Epoch, token::Amount)>), Error> { + let source = source.clone(); // Check the source's current bond amount - let bond_source = source.clone().unwrap_or_else(|| args.validator.clone()); + let bond_source = source.clone().unwrap_or_else(|| validator.clone()); let tx_code_hash = - query_wasm_code_hash(client, args.tx_code_path.to_str().unwrap()) + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) .await .unwrap(); - if !args.tx.force { - known_validator_or_err(args.validator.clone(), args.tx.force, client) - .await?; + if !tx.force { + known_validator_or_err(validator.clone(), tx.force, client).await?; let bond_amount = - rpc::query_bond(client, &bond_source, &args.validator, None).await; + rpc::query_bond(client, &bond_source, &validator, None).await; println!( "Bond amount available for unbonding: {} NAM", bond_amount.to_string_native() ); - if args.amount > bond_amount { + if amount > bond_amount { eprintln!( "The total bonds of the source {} is lower than the amount to \ be unbonded. Amount to unbond is {} and the total bonds is \ {}.", bond_source, - args.amount.to_string_native(), + amount.to_string_native(), bond_amount.to_string_native() ); - if !args.tx.force { + if !tx.force { return Err(Error::LowerBondThanUnbond( bond_source, - args.amount.to_string_native(), + amount.to_string_native(), bond_amount.to_string_native(), )); } @@ -984,8 +946,7 @@ pub async fn build_unbond< // Query the unbonds before submitting the tx let unbonds = - rpc::query_unbond_with_slashing(client, &bond_source, &args.validator) - .await; + rpc::query_unbond_with_slashing(client, &bond_source, &validator).await; let mut withdrawable = BTreeMap::::new(); for ((_start_epoch, withdraw_epoch), amount) in unbonds.into_iter() { let to_withdraw = withdrawable.entry(withdraw_epoch).or_default(); @@ -994,31 +955,27 @@ pub async fn build_unbond< let latest_withdrawal_pre = withdrawable.into_iter().last(); let data = pos::Unbond { - validator: args.validator.clone(), - amount: args.amount, - source, + validator: validator.clone(), + amount, + source: source.clone(), }; - let data = data.try_to_vec().map_err(Error::EncodeTxFailure)?; - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.tx.chain_id.clone().unwrap(); - tx.header.expiration = args.tx.expiration; - tx.set_data(Data::new(data)); - tx.set_code(Code::from_hash(tx_code_hash)); + let chain_id = tx.chain_id.clone().unwrap(); + let tx_builder = TxBuilder::new(chain_id, tx.expiration); - let default_signer = args.source.unwrap_or_else(|| args.validator.clone()); - let (tx, signer_addr, default_signer) = prepare_tx::( + let tx_builder = tx_builder.add_code_from_hash(tx_code_hash).add_data(data); + + let tx_builder = prepare_tx::( client, - wallet, - &args.tx, - tx, - TxSigningKey::WalletAddress(default_signer), + &tx, + tx_builder, + gas_payer.clone(), #[cfg(not(feature = "mainnet"))] false, ) .await?; - Ok((tx, signer_addr, default_signer, latest_withdrawal_pre)) + Ok((tx_builder, latest_withdrawal_pre)) } /// Query the unbonds post-tx @@ -1084,22 +1041,25 @@ pub async fn query_unbonds( } /// Submit a transaction to bond -pub async fn build_bond< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( +pub async fn build_bond( client: &C, - wallet: &mut Wallet, - args: args::Bond, -) -> Result<(Tx, Option
, common::PublicKey), Error> { + args::Bond { + tx, + validator, + amount, + source, + native_token, + tx_code_path, + }: args::Bond, + gas_payer: &common::PublicKey, +) -> Result { let validator = - known_validator_or_err(args.validator.clone(), args.tx.force, client) - .await?; + known_validator_or_err(validator.clone(), tx.force, client).await?; // Check that the source address exists on chain - let source = args.source.clone(); - let source = match args.source.clone() { - Some(source) => source_exists_or_err(source, args.tx.force, client) + let source = source.clone(); + let source = match source.clone() { + Some(source) => source_exists_or_err(source, tx.force, client) .await .map(Some), None => Ok(source), @@ -1107,117 +1067,318 @@ pub async fn build_bond< // Check bond's source (source for delegation or validator for self-bonds) // balance let bond_source = source.as_ref().unwrap_or(&validator); - let balance_key = token::balance_key(&args.native_token, bond_source); + let balance_key = token::balance_key(&native_token, bond_source); // TODO Should we state the same error message for the native token? check_balance_too_low_err( - &args.native_token, + &native_token, bond_source, - args.amount, + amount, balance_key, - args.tx.force, + tx.force, client, ) .await?; let tx_code_hash = - query_wasm_code_hash(client, args.tx_code_path.to_str().unwrap()) + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) .await .unwrap(); - let bond = pos::Bond { + let data = pos::Bond { validator, - amount: args.amount, + amount, source, }; - let data = bond.try_to_vec().map_err(Error::EncodeTxFailure)?; - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.tx.chain_id.clone().unwrap(); - tx.header.expiration = args.tx.expiration; - tx.set_data(Data::new(data)); - tx.set_code(Code::from_hash(tx_code_hash)); + let chain_id = tx.chain_id.clone().unwrap(); + let tx_builder = TxBuilder::new(chain_id, tx.expiration); - let default_signer = args.source.unwrap_or(args.validator); - prepare_tx::( + let tx_builder = tx_builder.add_code_from_hash(tx_code_hash).add_data(data); + + prepare_tx::( client, - wallet, - &args.tx, + &tx, + tx_builder, + gas_payer.clone(), + #[cfg(not(feature = "mainnet"))] + false, + ) + .await +} + +/// Build a default proposal governance +pub async fn build_default_proposal< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, + args::InitProposal { tx, - TxSigningKey::WalletAddress(default_signer), + proposal_data: _, + native_token: _, + is_offline: _, + is_pgf_stewards: _, + is_pgf_funding: _, + tx_code_path, + }: args::InitProposal, + proposal: DefaultProposal, + gas_payer: &common::PublicKey, +) -> Result { + let mut init_proposal_data = + InitProposalData::try_from(proposal.clone()) + .map_err(|e| Error::InvalidProposal(e.to_string()))?; + + let tx_code_hash = + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) + .await + .unwrap(); + + let chain_id = tx.chain_id.clone().unwrap(); + + let tx_builder = TxBuilder::new(chain_id, tx.expiration); + + let (tx_builder, extra_section_hash) = tx_builder + .add_extra_section(proposal.proposal.content.try_to_vec().unwrap()); + init_proposal_data.content = extra_section_hash; + + let tx_builder = if let Some(init_proposal_code) = proposal.data { + let (tx_builder, extra_section_hash) = + tx_builder.add_extra_section(init_proposal_code); + init_proposal_data.r#type = + ProposalType::Default(Some(extra_section_hash)); + tx_builder + } else { + tx_builder + }; + + let tx_builder = tx_builder + .add_code_from_hash(tx_code_hash) + .add_data(init_proposal_data); + + prepare_tx::( + client, + &tx, + tx_builder, + gas_payer.clone(), #[cfg(not(feature = "mainnet"))] false, ) .await } -/// Check if current epoch is in the last third of the voting period of the -/// proposal. This ensures that it is safe to optimize the vote writing to -/// storage. -pub async fn is_safe_voting_window( +/// Build a proposal vote +pub async fn build_vote_proposal( client: &C, - proposal_id: u64, - proposal_start_epoch: Epoch, -) -> Result { - let current_epoch = rpc::query_epoch(client).await; + args::VoteProposal { + tx, + proposal_id, + vote, + voter, + is_offline: _, + proposal_data: _, + tx_code_path, + }: args::VoteProposal, + epoch: Epoch, + gas_payer: &common::PublicKey, +) -> Result { + let proposal_vote = + ProposalVote::try_from(vote).map_err(|_| Error::InvalidProposalVote)?; + + let proposal_id = proposal_id.expect("Proposal id must be defined."); + let proposal = if let Some(proposal) = + rpc::query_proposal_by_id(client, proposal_id).await + { + proposal + } else { + return Err(Error::ProposalDoesNotExist(proposal_id)); + }; - let proposal_end_epoch_key = - gov_storage::get_voting_end_epoch_key(proposal_id); - let proposal_end_epoch = - rpc::query_storage_value::(client, &proposal_end_epoch_key) - .await; + let storage_vote = + StorageProposalVote::build(&proposal_vote, &proposal.r#type) + .expect("Should be able to build the proposal vote"); - match proposal_end_epoch { - Some(proposal_end_epoch) => { - Ok(!crate::ledger::native_vp::governance::utils::is_valid_validator_voting_period( - current_epoch, - proposal_start_epoch, - proposal_end_epoch, - )) - } - None => { - Err(Error::EpochNotInStorage) - } + let is_validator = rpc::is_validator(client, &voter).await; + + if !proposal.can_be_voted(epoch, is_validator) { + return Err(Error::InvalidProposalVotingPeriod(proposal_id)); } + + let delegations = rpc::get_delegators_delegation_at( + client, + &voter, + proposal.voting_start_epoch, + ) + .await + .keys() + .cloned() + .collect::>(); + + let data = VoteProposalData { + id: proposal_id, + vote: storage_vote, + voter: voter.clone(), + delegations, + }; + + let tx_code_hash = + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) + .await + .unwrap(); + + let chain_id = tx.chain_id.clone().unwrap(); + + let tx_builder = TxBuilder::new(chain_id, tx.expiration); + let tx_builder = tx_builder.add_code_from_hash(tx_code_hash).add_data(data); + + prepare_tx::( + client, + &tx, + tx_builder, + gas_payer.clone(), + #[cfg(not(feature = "mainnet"))] + false, + ) + .await } -/// Submit an IBC transfer -pub async fn build_ibc_transfer< +/// Build a pgf funding proposal governance +pub async fn build_pgf_funding_proposal< C: crate::ledger::queries::Client + Sync, - U: WalletUtils, >( client: &C, - wallet: &mut Wallet, - args: args::TxIbcTransfer, -) -> Result<(Tx, Option
, common::PublicKey), Error> { + args::InitProposal { + tx, + proposal_data: _, + native_token: _, + is_offline: _, + is_pgf_stewards: _, + is_pgf_funding: _, + tx_code_path, + }: args::InitProposal, + proposal: PgfFundingProposal, + gas_payer: &common::PublicKey, +) -> Result { + let mut init_proposal_data = + InitProposalData::try_from(proposal.clone()) + .map_err(|e| Error::InvalidProposal(e.to_string()))?; + + let tx_code_hash = + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) + .await + .unwrap(); + + let chain_id = tx.chain_id.clone().unwrap(); + + let tx_builder = TxBuilder::new(chain_id, tx.expiration); + + let (tx_builder, extra_section_hash) = tx_builder + .add_extra_section(proposal.proposal.content.try_to_vec().unwrap()); + init_proposal_data.content = extra_section_hash; + + let tx_builder = tx_builder + .add_code_from_hash(tx_code_hash) + .add_data(init_proposal_data); + + prepare_tx::( + client, + &tx, + tx_builder, + gas_payer.clone(), + #[cfg(not(feature = "mainnet"))] + false, + ) + .await +} + +/// Build a pgf funding proposal governance +pub async fn build_pgf_stewards_proposal< + C: crate::ledger::queries::Client + Sync, +>( + client: &C, + args::InitProposal { + tx, + proposal_data: _, + native_token: _, + is_offline: _, + is_pgf_stewards: _, + is_pgf_funding: _, + tx_code_path, + }: args::InitProposal, + proposal: PgfStewardProposal, + gas_payer: &common::PublicKey, +) -> Result { + let mut init_proposal_data = + InitProposalData::try_from(proposal.clone()) + .map_err(|e| Error::InvalidProposal(e.to_string()))?; + + let tx_code_hash = + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) + .await + .unwrap(); + + let chain_id = tx.chain_id.clone().unwrap(); + + let tx_builder = TxBuilder::new(chain_id, tx.expiration); + + let (tx_builder, extra_section_hash) = tx_builder + .add_extra_section(proposal.proposal.content.try_to_vec().unwrap()); + init_proposal_data.content = extra_section_hash; + + let tx_builder = tx_builder + .add_code_from_hash(tx_code_hash) + .add_data(init_proposal_data); + + prepare_tx::( + client, + &tx, + tx_builder, + gas_payer.clone(), + #[cfg(not(feature = "mainnet"))] + false, + ) + .await +} + +/// Submit an IBC transfer +pub async fn build_ibc_transfer( + client: &C, + args::TxIbcTransfer { + tx, + source, + receiver, + token, + amount, + port_id, + channel_id, + timeout_height, + timeout_sec_offset, + tx_code_path, + }: args::TxIbcTransfer, + gas_payer: &common::PublicKey, +) -> Result { // Check that the source address exists on chain - let source = - source_exists_or_err(args.source.clone(), args.tx.force, client) - .await?; + let source = source_exists_or_err(source.clone(), tx.force, client).await?; // We cannot check the receiver - let token = args.token; - // Check source balance let balance_key = token::balance_key(&token, &source); check_balance_too_low_err( &token, &source, - args.amount, + amount, balance_key, - args.tx.force, + tx.force, client, ) .await?; let tx_code_hash = - query_wasm_code_hash(client, args.tx_code_path.to_str().unwrap()) + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) .await .unwrap(); - let amount = args - .amount + let amount = amount .to_string_native() .split('.') .next() @@ -1229,7 +1390,7 @@ pub async fn build_ibc_transfer< }; // this height should be that of the destination chain, not this chain - let timeout_height = match args.timeout_height { + let timeout_height = match timeout_height { Some(h) => { TimeoutHeight::At(IbcHeight::new(0, h).expect("invalid height")) } @@ -1238,7 +1399,7 @@ pub async fn build_ibc_transfer< let now: crate::tendermint::Time = DateTimeUtc::now().try_into().unwrap(); let now: IbcTimestamp = now.into(); - let timeout_timestamp = if let Some(offset) = args.timeout_sec_offset { + let timeout_timestamp = if let Some(offset) = timeout_sec_offset { (now + Duration::new(offset, 0)).unwrap() } else if timeout_height == TimeoutHeight::Never { // we cannot set 0 to both the height and the timestamp @@ -1248,32 +1409,32 @@ pub async fn build_ibc_transfer< }; let msg = MsgTransfer { - port_id_on_a: args.port_id, - chan_id_on_a: args.channel_id, + port_id_on_a: port_id, + chan_id_on_a: channel_id, token, sender: Signer::from_str(&source.to_string()).expect("invalid signer"), - receiver: Signer::from_str(&args.receiver).expect("invalid signer"), + receiver: Signer::from_str(&receiver).expect("invalid signer"), timeout_height_on_b: timeout_height, timeout_timestamp_on_b: timeout_timestamp, }; - tracing::debug!("IBC transfer message {:?}", msg); + let any_msg = msg.to_any(); let mut data = vec![]; prost::Message::encode(&any_msg, &mut data) .map_err(Error::EncodeFailure)?; - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.tx.chain_id.clone().unwrap(); - tx.header.expiration = args.tx.expiration; - tx.set_data(Data::new(data)); - tx.set_code(Code::from_hash(tx_code_hash)); + let chain_id = tx.chain_id.clone().unwrap(); - prepare_tx::( + let tx_builder = TxBuilder::new(chain_id, tx.expiration); + let tx_builder = tx_builder + .add_code_from_hash(tx_code_hash) + .add_serialized_data(data); + + prepare_tx::( client, - wallet, - &args.tx, - tx, - TxSigningKey::WalletAddress(args.source), + &tx, + tx_builder, + gas_payer.clone(), #[cfg(not(feature = "mainnet"))] false, ) @@ -1356,15 +1517,13 @@ async fn used_asset_types< /// Submit an ordinary transfer pub async fn build_transfer< C: crate::ledger::queries::Client + Sync, - V: WalletUtils, U: ShieldedUtils, >( client: &C, - wallet: &mut Wallet, shielded: &mut ShieldedContext, mut args: args::TxTransfer, -) -> Result<(Tx, Option
, common::PublicKey, Option, bool), Error> -{ + gas_payer: &common::PublicKey, +) -> Result<(TxBuilder, Option), Error> { let source = args.source.effective_address(); let target = args.target.effective_address(); let token = args.token.clone(); @@ -1383,15 +1542,15 @@ pub async fn build_transfer< .expect("expected to validate amount"); let validate_fee = validate_amount( client, - args.tx.fee_amount, - &args.tx.fee_token, + args.tx.gas_amount, + &args.tx.gas_token, args.tx.force, ) .await .expect("expected to be able to validate fee"); args.amount = InputAmount::Validated(validated_amount); - args.tx.fee_amount = InputAmount::Validated(validate_fee); + args.tx.gas_amount = InputAmount::Validated(validate_fee); check_balance_too_low_err::( &token, &source, @@ -1407,21 +1566,14 @@ pub async fn build_transfer< // signer. Also, if the transaction is shielded, redact the amount and token // types by setting the transparent value to 0 and token type to a constant. // This has no side-effect because transaction is to self. - let (_amount, token) = if source == masp_addr && target == masp_addr { - // TODO Refactor me, we shouldn't rely on any specific token here. - (token::Amount::default(), args.native_token.clone()) - } else { - (validated_amount.amount, token) - }; - let default_signer = - TxSigningKey::WalletAddress(args.source.effective_address()); - // If our chosen signer is the MASP sentinel key, then our shielded inputs - // will need to cover the gas fees. - let chosen_signer = - tx_signer::(client, wallet, &args.tx, default_signer.clone()) - .await? - .1; - let shielded_gas = masp_tx_key().ref_to() == chosen_signer; + let (_amount, token, shielded_gas) = + if source == masp_addr && target == masp_addr { + // TODO Refactor me, we shouldn't rely on any specific token here. + (token::Amount::default(), args.native_token.clone(), true) + } else { + (validated_amount.amount, token, false) + }; + // Determine whether to pin this transaction to a storage key let key = match &args.target { TransferTarget::PaymentAddress(pa) if pa.is_pinned() => Some(pa.hash()), @@ -1430,6 +1582,8 @@ pub async fn build_transfer< #[cfg(not(feature = "mainnet"))] let is_source_faucet = rpc::is_faucet_account(client, &source).await; + #[cfg(feature = "mainnet")] + let is_source_faucet = false; let tx_code_hash = query_wasm_code_hash(client, args.tx_code_path.to_str().unwrap()) @@ -1449,43 +1603,44 @@ pub async fn build_transfer< validated_amount.amount.to_string_native(), Box::new(token.clone()), validate_fee.amount.to_string_native(), - Box::new(args.tx.fee_token.clone()), + Box::new(args.tx.gas_token.clone()), )) } Err(err) => Err(Error::MaspError(err)), }?; - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.tx.chain_id.clone().unwrap(); - tx.header.expiration = args.tx.expiration; + let chain_id = args.tx.chain_id.clone().unwrap(); + let tx_builder = TxBuilder::new(chain_id, args.tx.expiration); + // Add the MASP Transaction and its Builder to facilitate validation - let (masp_hash, shielded_tx_epoch) = if let Some(shielded_parts) = - shielded_parts - { - // Add a MASP Transaction section to the Tx - let masp_tx = tx.add_section(Section::MaspTx(shielded_parts.1)); - // Get the hash of the MASP Transaction section - let masp_hash = masp_tx.get_hash(); - // Get the decoded asset types used in the transaction to give - // offline wallet users more information - let asset_types = used_asset_types(shielded, client, &shielded_parts.0) - .await - .unwrap_or_default(); - // Add the MASP Transaction's Builder to the Tx - tx.add_section(Section::MaspBuilder(MaspBuilder { - asset_types, - // Store how the Info objects map to Descriptors/Outputs - metadata: shielded_parts.2, - // Store the data that was used to construct the Transaction - builder: shielded_parts.0, - // Link the Builder to the Transaction by hash code - target: masp_hash, - })); - // The MASP Transaction section hash will be used in Transfer - (Some(masp_hash), Some(shielded_parts.3)) - } else { - (None, None) - }; + let (tx_builder, masp_hash, shielded_tx_epoch) = + if let Some(shielded_parts) = shielded_parts { + // Add a MASP Transaction section to the Tx and get the tx hash + let (tx_builder, masp_tx_hash) = + tx_builder.add_masp_tx_section(shielded_parts.1); + + // Get the decoded asset types used in the transaction to give + // offline wallet users more information + let asset_types = + used_asset_types(shielded, client, &shielded_parts.0) + .await + .unwrap_or_default(); + + let tx_builder = tx_builder.add_masp_builder(MaspBuilder { + asset_types, + // Store how the Info objects map to Descriptors/Outputs + metadata: shielded_parts.2, + // Store the data that was used to construct the Transaction + builder: shielded_parts.0, + // Link the Builder to the Transaction by hash code + target: masp_tx_hash, + }); + + // The MASP Transaction section hash will be used in Transfer + (tx_builder, Some(masp_tx_hash), Some(shielded_parts.3)) + } else { + (tx_builder, None, None) + }; // Construct the corresponding transparent Transfer object let transfer = token::Transfer { source: source.clone(), @@ -1496,75 +1651,79 @@ pub async fn build_transfer< // Link the Transfer to the MASP Transaction by hash code shielded: masp_hash, }; + tracing::debug!("Transfer data {:?}", transfer); - // Encode the Transfer and store it beside the MASP transaction - let data = transfer - .try_to_vec() - .expect("Encoding tx data shouldn't fail"); - tx.set_data(Data::new(data)); - // Finally store the Traansfer WASM code in the Tx - tx.set_code(Code::from_hash(tx_code_hash)); + + let tx_builder = tx_builder + .add_code_from_hash(tx_code_hash) + .add_data(transfer); // Dry-run/broadcast/submit the transaction - let (tx, signer_addr, def_key) = prepare_tx::( + let tx_builder = prepare_tx::( client, - wallet, &args.tx, - tx, - default_signer.clone(), + tx_builder, + gas_payer.clone(), #[cfg(not(feature = "mainnet"))] is_source_faucet, ) .await?; - Ok(( - tx, - signer_addr, - def_key, - shielded_tx_epoch, - is_source_faucet, - )) + + Ok((tx_builder, shielded_tx_epoch)) } /// Submit a transaction to initialize an account -pub async fn build_init_account< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( +pub async fn build_init_account( client: &C, - wallet: &mut Wallet, - args: args::TxInitAccount, -) -> Result<(Tx, Option
, common::PublicKey), Error> { - let public_key = args.public_key; - + args::TxInitAccount { + tx, + vp_code_path, + tx_code_path, + public_keys, + threshold, + }: args::TxInitAccount, + gas_payer: &common::PublicKey, +) -> Result { let vp_code_hash = - query_wasm_code_hash(client, args.vp_code_path.to_str().unwrap()) + query_wasm_code_hash(client, vp_code_path.to_str().unwrap()) .await .unwrap(); let tx_code_hash = - query_wasm_code_hash(client, args.tx_code_path.to_str().unwrap()) + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) .await .unwrap(); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.tx.chain_id.clone().unwrap(); - tx.header.expiration = args.tx.expiration; - let extra = - tx.add_section(Section::ExtraData(Code::from_hash(vp_code_hash))); + let threshold = match threshold { + Some(threshold) => threshold, + None => { + if public_keys.len() == 1 { + 1u8 + } else { + return Err(Error::MissingAccountThreshold); + } + } + }; + + let chain_id = tx.chain_id.clone().unwrap(); + let tx_builder = TxBuilder::new(chain_id, tx.expiration); + + let (tx_builder, extra_section_hash) = + tx_builder.add_extra_section_from_hash(vp_code_hash); + let data = InitAccount { - public_key, - vp_code_hash: extra.get_hash(), + public_keys, + vp_code_hash: extra_section_hash, + threshold, }; - let data = data.try_to_vec().map_err(Error::EncodeTxFailure)?; - tx.set_data(Data::new(data)); - tx.set_code(Code::from_hash(tx_code_hash)); - prepare_tx::( + let tx_builder = tx_builder.add_code_from_hash(tx_code_hash).add_data(data); + + prepare_tx::( client, - wallet, - &args.tx, - tx, - TxSigningKey::WalletAddress(args.source), + &tx, + tx_builder, + gas_payer.clone(), #[cfg(not(feature = "mainnet"))] false, ) @@ -1572,85 +1731,69 @@ pub async fn build_init_account< } /// Submit a transaction to update a VP -pub async fn build_update_vp< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( +pub async fn build_update_account( client: &C, - wallet: &mut Wallet, - args: args::TxUpdateVp, -) -> Result<(Tx, Option
, common::PublicKey), Error> { - let addr = args.addr.clone(); - - // Check that the address is established and exists on chain - match &addr { - Address::Established(_) => { - let exists = rpc::known_address::(client, &addr).await; - if !exists { - if args.tx.force { - eprintln!("The address {} doesn't exist on chain.", addr); - Ok(()) - } else { - Err(Error::LocationDoesNotExist(addr.clone())) - } - } else { - Ok(()) - } - } - Address::Implicit(_) => { - if args.tx.force { - eprintln!( - "A validity predicate of an implicit address cannot be \ - directly updated. You can use an established address for \ - this purpose." - ); - Ok(()) - } else { - Err(Error::ImplicitUpdate) - } - } - Address::Internal(_) => { - if args.tx.force { - eprintln!( - "A validity predicate of an internal address cannot be \ - directly updated." - ); - Ok(()) - } else { - Err(Error::ImplicitInternalError) - } - } - }?; + args::TxUpdateAccount { + tx, + vp_code_path, + tx_code_path, + addr, + public_keys, + threshold, + }: args::TxUpdateAccount, + gas_payer: &common::PublicKey, +) -> Result { + let addr = if let Some(account) = rpc::get_account_info(client, &addr).await + { + account.address + } else if tx.force { + addr + } else { + return Err(Error::LocationDoesNotExist(addr)); + }; - let vp_code_hash = - query_wasm_code_hash(client, args.vp_code_path.to_str().unwrap()) - .await - .unwrap(); + let vp_code_hash = match vp_code_path { + Some(code_path) => { + let vp_hash = + query_wasm_code_hash(client, code_path.to_str().unwrap()) + .await + .unwrap(); + Some(vp_hash) + } + None => None, + }; let tx_code_hash = - query_wasm_code_hash(client, args.tx_code_path.to_str().unwrap()) + query_wasm_code_hash(client, tx_code_path.to_str().unwrap()) .await .unwrap(); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.tx.chain_id.clone().unwrap(); - tx.header.expiration = args.tx.expiration; - let extra = - tx.add_section(Section::ExtraData(Code::from_hash(vp_code_hash))); - let data = UpdateVp { + let chain_id = tx.chain_id.clone().unwrap(); + let tx_builder = TxBuilder::new(chain_id, tx.expiration); + + let (tx_builder, extra_section_hash) = match vp_code_hash { + Some(vp_code_hash) => { + let (tx_builder, hash) = + tx_builder.add_extra_section_from_hash(vp_code_hash); + (tx_builder, Some(hash)) + } + None => (tx_builder, None), + }; + + let data = UpdateAccount { addr, - vp_code_hash: extra.get_hash(), + vp_code_hash: extra_section_hash, + public_keys, + threshold, }; - let data = data.try_to_vec().map_err(Error::EncodeTxFailure)?; - tx.set_data(Data::new(data)); - tx.set_code(Code::from_hash(tx_code_hash)); - prepare_tx::( + let tx_builder = tx_builder.add_code_from_hash(tx_code_hash).add_data(data); + + prepare_tx::( client, - wallet, - &args.tx, - tx, - TxSigningKey::WalletAddress(args.addr), + &tx, + tx_builder, + gas_payer.clone(), #[cfg(not(feature = "mainnet"))] false, ) @@ -1658,26 +1801,45 @@ pub async fn build_update_vp< } /// Submit a custom transaction -pub async fn build_custom< - C: crate::ledger::queries::Client + Sync, - U: WalletUtils, ->( +pub async fn build_custom( client: &C, - wallet: &mut Wallet, - args: args::TxCustom, -) -> Result<(Tx, Option
, common::PublicKey), Error> { - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = args.tx.chain_id.clone().unwrap(); - tx.header.expiration = args.tx.expiration; - args.data_path.map(|data| tx.set_data(Data::new(data))); - tx.set_code(Code::new(args.code_path)); - - prepare_tx::( - client, - wallet, - &args.tx, + args::TxCustom { tx, - TxSigningKey::None, + code_path, + data_path, + serialized_tx, + owner: _, + }: args::TxCustom, + gas_payer: &common::PublicKey, +) -> Result { + let tx_builder = if let Some(serialized_tx) = serialized_tx { + let tx = Tx::deserialize(serialized_tx.as_ref()).map_err(|_| { + Error::Other("Invalid tx deserialization.".to_string()) + })?; + TxBuilder::from(tx) + } else { + let tx_code_hash = + query_wasm_code_hash(client, code_path.unwrap().to_str().unwrap()) + .await + .unwrap(); + + let chain_id = tx.chain_id.clone().unwrap(); + let mut tx_builder = TxBuilder::new(chain_id, tx.expiration); + + tx_builder = tx_builder.add_code_from_hash(tx_code_hash); + + if let Some(data) = data_path { + tx_builder = tx_builder.add_serialized_data(data); + } + + tx_builder + }; + + prepare_tx::( + client, + &tx, + tx_builder, + gas_payer.clone(), #[cfg(not(feature = "mainnet"))] false, ) diff --git a/shared/src/types/mod.rs b/shared/src/types/mod.rs index 4f1a795d409..5f278514d35 100644 --- a/shared/src/types/mod.rs +++ b/shared/src/types/mod.rs @@ -3,9 +3,10 @@ pub mod control_flow; pub mod ibc; pub mod key; +pub mod tx; pub use namada_core::types::{ - address, chain, dec, eth_abi, eth_bridge_pool, ethereum_events, governance, - hash, internal, keccak, masp, storage, time, token, transaction, uint, + address, chain, dec, eth_abi, eth_bridge_pool, ethereum_events, hash, + internal, keccak, masp, storage, time, token, transaction, uint, validity_predicate, vote_extensions, voting_power, }; diff --git a/shared/src/types/tx.rs b/shared/src/types/tx.rs new file mode 100644 index 00000000000..7cd61db57db --- /dev/null +++ b/shared/src/types/tx.rs @@ -0,0 +1,218 @@ +//! Helper structures to build transactions + +use std::collections::BTreeSet; + +use borsh::BorshSerialize; +use masp_primitives::transaction::Transaction; +use namada_core::ledger::testnet_pow; +use namada_core::proto::{ + Code, Data, MaspBuilder, MultiSignature, Section, Signature, + SignatureIndex, Tx, +}; +use namada_core::types::account::AccountPublicKeysMap; +use namada_core::types::chain::ChainId; +use namada_core::types::hash::Hash; +use namada_core::types::key::common; +use namada_core::types::storage::Epoch; +use namada_core::types::transaction::{Fee, GasLimit, TxType, WrapperTx}; + +use crate::types::time::DateTimeUtc; + +/// A helper structure to build transations +#[derive(Default)] +pub struct TxBuilder { + chain_id: ChainId, + expiration: Option, + sections: Vec
, + wrapper: Option, + gas_payer: Option, + signing_keys: Vec, + account_public_keys_map: Option, + signatures: BTreeSet, +} + +impl TxBuilder { + /// Initialize a new transaction builder + pub fn new(chain_id: ChainId, expiration: Option) -> Self { + Self { + chain_id, + expiration, + sections: vec![], + wrapper: None, + gas_payer: None, + signing_keys: vec![], + account_public_keys_map: None, + signatures: BTreeSet::default(), + } + } + + /// Add an extra section to the tx builder by hash + pub fn add_extra_section_from_hash(mut self, hash: Hash) -> (Self, Hash) { + let sec = self._add_section(Section::ExtraData(Code::from_hash(hash))); + (self, sec.get_hash()) + } + + /// Add an extra section to the tx builder by code + pub fn add_extra_section(mut self, code: Vec) -> (Self, Hash) { + let sec = self._add_section(Section::ExtraData(Code::new(code))); + (self, sec.get_hash()) + } + + /// Add a masp tx section to the tx builder + pub fn add_masp_tx_section(mut self, tx: Transaction) -> (Self, Hash) { + let sec = self._add_section(Section::MaspTx(tx)); + (self, sec.get_hash()) + } + + /// Add a masp builder section to the tx builder + pub fn add_masp_builder(mut self, builder: MaspBuilder) -> Self { + let _sec = self._add_section(Section::MaspBuilder(builder)); + self + } + + /// Add wasm code to the tx builder from hash + pub fn add_code_from_hash(mut self, code_hash: Hash) -> Self { + self._add_section(Section::Code(Code::from_hash(code_hash))); + self + } + + /// Add wasm code to the tx builder + pub fn add_code(mut self, code: Vec) -> Self { + self._add_section(Section::Code(Code::new(code))); + self + } + + /// Add wasm data to the tx builder + pub fn add_data(mut self, data: impl BorshSerialize) -> Self { + let bytes = data.try_to_vec().expect("Encoding tx data shouldn't fail"); + self._add_section(Section::Data(Data::new(bytes))); + self + } + + /// Add wasm data already serialized to the tx builder + pub fn add_serialized_data(mut self, bytes: Vec) -> Self { + self._add_section(Section::Data(Data::new(bytes))); + self + } + + /// Add wrapper tx to the tx builder + pub fn add_wrapper( + mut self, + fee: Fee, + gas_payer: common::PublicKey, + epoch: Epoch, + gas_limit: GasLimit, + #[cfg(not(feature = "mainnet"))] requires_pow: Option< + testnet_pow::Solution, + >, + ) -> Self { + self.wrapper = Some(WrapperTx::new( + fee, + gas_payer, + epoch, + gas_limit, + #[cfg(not(feature = "mainnet"))] + requires_pow, + )); + self + } + + /// Add fee payer keypair to the tx builder + pub fn add_gas_payer(mut self, keypair: common::SecretKey) -> Self { + self.gas_payer = Some(keypair); + self + } + + /// Add signing keys to the tx builder + pub fn add_signing_keys( + mut self, + keypairs: Vec, + account_public_keys_map: AccountPublicKeysMap, + ) -> Self { + self.signing_keys = keypairs; + self.account_public_keys_map = Some(account_public_keys_map); + self + } + + /// Add signature + pub fn add_signatures( + mut self, + signatures: BTreeSet, + ) -> Self { + self.signatures = signatures; + self + } + + /// Generate the corresponding tx + pub fn build(self) -> Tx { + let mut tx = Tx::new(TxType::Raw); + tx.header.chain_id = self.chain_id; + tx.header.expiration = self.expiration; + + for section in self.sections.clone() { + tx.add_section(section); + } + + tx.protocol_filter(); + + for section in self.sections { + match section { + Section::Data(_) => tx.set_data_sechash(section.get_hash()), + Section::Code(_) => tx.set_code_sechash(section.get_hash()), + _ => continue, + } + } + if let Some(wrapper) = self.wrapper { + tx.update_header(TxType::Wrapper(Box::new(wrapper))); + } + + let hashes = tx.inner_section_targets(); + + if !self.signatures.is_empty() { + tx.add_section(Section::SectionSignature(MultiSignature { + targets: hashes, + signatures: self.signatures, + })); + } else if let Some(account_public_keys_map) = + self.account_public_keys_map + { + tx.add_section(Section::SectionSignature(MultiSignature::new( + hashes, + &self.signing_keys, + &account_public_keys_map, + ))); + } + + if let Some(keypair) = self.gas_payer { + let mut sections_hashes = tx + .sections + .iter() + .map(|section| section.get_hash()) + .collect::>(); + sections_hashes.push(tx.header_hash()); + tx.add_section(Section::Signature(Signature::new( + sections_hashes, + &keypair, + ))); + } + + tx + } + + /// Internal method to add a section to the builder + fn _add_section(&mut self, section: Section) -> Section { + self.sections.push(section); + self.sections.last().unwrap().clone() + } +} + +impl From for TxBuilder { + fn from(value: Tx) -> Self { + let mut tx_builder = + TxBuilder::new(value.header.chain_id, value.header.expiration); + for section in value.sections { + tx_builder._add_section(section); + } + tx_builder + } +} diff --git a/shared/src/vm/wasm/run.rs b/shared/src/vm/wasm/run.rs index 9ac4852eede..861b525d8b4 100644 --- a/shared/src/vm/wasm/run.rs +++ b/shared/src/vm/wasm/run.rs @@ -482,6 +482,7 @@ mod tests { use crate::proto::{Code, Data}; use crate::types::hash::Hash; use crate::types::transaction::TxType; + use crate::types::tx::TxBuilder; use crate::types::validity_predicate::EvalVp; use crate::vm::wasm; @@ -617,18 +618,21 @@ mod tests { // Allocating `2^23` (8 MiB) should be below the memory limit and // shouldn't fail let input = 2_usize.pow(23).try_to_vec().unwrap(); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(input)); + + let tx_builder = TxBuilder::new(storage.chain_id.clone(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(input) + .build(); + let eval_vp = EvalVp { vp_code_hash: limit_code_hash, input: tx, }; - let tx_data = eval_vp.try_to_vec().unwrap(); - let mut outer_tx = Tx::new(TxType::Raw); - outer_tx.header.chain_id = storage.chain_id.clone(); - outer_tx.set_code(Code::new(vec![])); - outer_tx.set_data(Data::new(tx_data)); + + let tx_builder = TxBuilder::new(storage.chain_id.clone(), None); + let outer_tx = tx_builder.add_code(vec![]).add_data(eval_vp).build(); + let (vp_cache, _) = wasm::compilation_cache::common::testing::cache(); // When the `eval`ed VP doesn't run out of memory, it should return // `true` @@ -652,18 +656,17 @@ mod tests { // Allocating `2^24` (16 MiB) should be above the memory limit and // should fail let input = 2_usize.pow(24).try_to_vec().unwrap(); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(input)); + let tx_builder = TxBuilder::new(storage.chain_id.clone(), None); + let tx = tx_builder.add_code(vec![]).add_data(input).build(); + let eval_vp = EvalVp { vp_code_hash: limit_code_hash, input: tx, }; - let tx_data = eval_vp.try_to_vec().unwrap(); - let mut outer_tx = Tx::new(TxType::Raw); - outer_tx.header.chain_id = storage.chain_id.clone(); - outer_tx.set_data(Data::new(tx_data)); - outer_tx.set_code(Code::new(vec![])); + + let tx_builder = TxBuilder::new(storage.chain_id.clone(), None); + let outer_tx = tx_builder.add_code(vec![]).add_data(eval_vp).build(); + // When the `eval`ed VP runs out of memory, its result should be // `false`, hence we should also get back `false` from the VP that // called `eval`. @@ -1022,18 +1025,21 @@ mod tests { // Borsh. storage.write(&key, value.try_to_vec().unwrap()).unwrap(); let input = 2_usize.pow(23).try_to_vec().unwrap(); - let mut tx = Tx::new(TxType::Raw); - tx.set_data(Data::new(input)); - tx.set_code(Code::new(vec![])); + + let tx_builder = TxBuilder::new(storage.chain_id.clone(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(input) + .build(); + let eval_vp = EvalVp { vp_code_hash: read_code_hash, input: tx, }; - let tx_data = eval_vp.try_to_vec().unwrap(); - let mut outer_tx = Tx::new(TxType::Raw); - outer_tx.header.chain_id = storage.chain_id.clone(); - outer_tx.set_data(Data::new(tx_data)); - outer_tx.set_code(Code::new(vec![])); + + let tx_builder = TxBuilder::new(storage.chain_id.clone(), None); + let outer_tx = tx_builder.add_code(vec![]).add_data(eval_vp).build(); + let (vp_cache, _) = wasm::compilation_cache::common::testing::cache(); let passed = vp( &code_hash, diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index 6b0352bcc7d..a12fcbff17a 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -18,7 +18,6 @@ pub const WASM_FOR_TESTS_DIR: &str = "wasm_for_tests"; #[derive(Debug, Clone, Copy, EnumIter)] pub enum TestWasms { TxMemoryLimit, - TxMintTokens, TxNoOp, TxProposalCode, TxReadStorageKey, @@ -36,7 +35,6 @@ impl TestWasms { pub fn path(&self) -> PathBuf { let filename = match self { TestWasms::TxMemoryLimit => "tx_memory_limit.wasm", - TestWasms::TxMintTokens => "tx_mint_tokens.wasm", TestWasms::TxNoOp => "tx_no_op.wasm", TestWasms::TxProposalCode => "tx_proposal_code.wasm", TestWasms::TxReadStorageKey => "tx_read_storage_key.wasm", diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 65c0d306f35..b8c4d10c073 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -47,6 +47,7 @@ num-traits.workspace = true prost.workspace = true regex.workspace = true serde_json.workspace = true +serde.workspace = true sha2.workspace = true tempfile.workspace = true test-log.workspace = true diff --git a/tests/src/e2e/eth_bridge_tests.rs b/tests/src/e2e/eth_bridge_tests.rs index 1d15c5323aa..d85b344f45f 100644 --- a/tests/src/e2e/eth_bridge_tests.rs +++ b/tests/src/e2e/eth_bridge_tests.rs @@ -144,17 +144,17 @@ async fn test_roundtrip_eth_transfer() -> Result<()> { "add-erc20-transfer", "--address", BERTHA, - "--signer", - BERTHA, + "--signing-keys", + BERTHA_KEY, "--amount", &amount, "--erc20", &dai_addr, "--ethereum-address", RECEIVER, - "--fee-amount", + "--gas-amount", "10", - "--fee-payer", + "--gas-payer", BERTHA, "--gas-amount", "0", @@ -335,17 +335,17 @@ async fn test_bridge_pool_e2e() { "add-erc20-transfer", "--address", BERTHA, - "--signer", - BERTHA, + "--signing-keys", + BERTHA_KEY, "--amount", "100", "--erc20", &wnam_address, "--ethereum-address", RECEIVER, - "--fee-amount", + "--gas-amount", "10", - "--fee-payer", + "--gas-payer", BERTHA, "--gas-amount", "0", diff --git a/tests/src/e2e/helpers.rs b/tests/src/e2e/helpers.rs index 1fde997ef69..e2d887b5a21 100644 --- a/tests/src/e2e/helpers.rs +++ b/tests/src/e2e/helpers.rs @@ -458,3 +458,9 @@ pub fn wait_for_wasm_pre_compile(ledger: &mut NamadaCmd) -> Result<()> { ledger.exp_string("Finished compiling all")?; Ok(()) } + +/// Convert epoch `min_duration` in seconds to `epochs_per_year` genesis +/// parameter. +pub fn epochs_per_year_from_min_duration(min_duration: u64) -> u64 { + 60 * 60 * 24 * 365 / min_duration +} diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index a7f89d9a9f1..52cb6ca9c04 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -88,6 +88,7 @@ use namada_apps::facade::tendermint_rpc::{Client, HttpClient, Url}; use prost::Message; use setup::constants::*; +use super::helpers::wait_for_wasm_pre_compile; use super::setup::set_ethereum_bridge_mode; use crate::e2e::helpers::{find_address, get_actor_rpc, get_validator_pk}; use crate::e2e::setup::{self, sleep, Bin, NamadaCmd, Test, Who}; @@ -132,6 +133,9 @@ fn run_ledger_ibc() -> Result<()> { ledger_a.exp_string("This node is a validator")?; ledger_b.exp_string("This node is a validator")?; + wait_for_wasm_pre_compile(&mut ledger_a)?; + wait_for_wasm_pre_compile(&mut ledger_b)?; + // Wait for a first block ledger_a.exp_string("Committed block hash")?; ledger_b.exp_string("Committed block hash")?; @@ -216,7 +220,7 @@ fn create_client(test_a: &Test, test_b: &Test) -> Result<(ClientId, ClientId)> { consensus_state: make_consensus_state(test_b, height)?.into(), signer: Signer::from_str("test_a").expect("invalid signer"), }; - let height_a = submit_ibc_tx(test_a, message, ALBERT)?; + let height_a = submit_ibc_tx(test_a, message, ALBERT, ALBERT_KEY, false)?; let height = query_height(test_a)?; let client_state = make_client_state(test_a, height); @@ -226,7 +230,7 @@ fn create_client(test_a: &Test, test_b: &Test) -> Result<(ClientId, ClientId)> { consensus_state: make_consensus_state(test_a, height)?.into(), signer: Signer::from_str("test_b").expect("invalid signer"), }; - let height_b = submit_ibc_tx(test_b, message, ALBERT)?; + let height_b = submit_ibc_tx(test_b, message, ALBERT, ALBERT_KEY, false)?; // convert the client IDs from `ibc_relayer_type` to `ibc` let client_id_a = match get_event(test_a, height_a)? { @@ -356,7 +360,7 @@ fn update_client( client_id: client_id.clone(), signer: Signer::from_str("test").expect("invalid signer"), }; - submit_ibc_tx(target_test, message, ALBERT)?; + submit_ibc_tx(target_test, message, ALBERT, ALBERT_KEY, false)?; } let message = MsgUpdateClient { @@ -364,7 +368,7 @@ fn update_client( client_id: client_id.clone(), signer: Signer::from_str("test").expect("invalid signer"), }; - submit_ibc_tx(target_test, message, ALBERT)?; + submit_ibc_tx(target_test, message, ALBERT, ALBERT_KEY, false)?; check_ibc_update_query( target_test, @@ -428,7 +432,7 @@ fn connection_handshake( signer: Signer::from_str("test_a").expect("invalid signer"), }; // OpenInitConnection on Chain A - let height = submit_ibc_tx(test_a, msg, ALBERT)?; + let height = submit_ibc_tx(test_a, msg, ALBERT, ALBERT_KEY, false)?; let conn_id_a = match get_event(test_a, height)? { Some(IbcEvent::OpenInitConnection(event)) => event .connection_id() @@ -459,7 +463,7 @@ fn connection_handshake( // Update the client state of Chain A on Chain B update_client_with_height(test_a, test_b, client_id_b, height_a)?; // OpenTryConnection on Chain B - let height = submit_ibc_tx(test_b, msg, ALBERT)?; + let height = submit_ibc_tx(test_b, msg, ALBERT, ALBERT_KEY, false)?; let conn_id_b = match get_event(test_b, height)? { Some(IbcEvent::OpenTryConnection(event)) => event .connection_id() @@ -483,7 +487,7 @@ fn connection_handshake( // Update the client state of Chain B on Chain A update_client_with_height(test_b, test_a, client_id_a, height_b)?; // OpenAckConnection on Chain A - submit_ibc_tx(test_a, msg, ALBERT)?; + submit_ibc_tx(test_a, msg, ALBERT, ALBERT_KEY, false)?; // get the proofs on Chain A let height_a = query_height(test_a)?; @@ -497,7 +501,7 @@ fn connection_handshake( // Update the client state of Chain A on Chain B update_client_with_height(test_a, test_b, client_id_b, height_a)?; // OpenConfirmConnection on Chain B - submit_ibc_tx(test_b, msg, ALBERT)?; + submit_ibc_tx(test_b, msg, ALBERT, ALBERT_KEY, false)?; Ok((conn_id_a, conn_id_b)) } @@ -553,7 +557,7 @@ fn channel_handshake( channel, signer: Signer::from_str("test_a").expect("invalid signer"), }; - let height = submit_ibc_tx(test_a, msg, ALBERT)?; + let height = submit_ibc_tx(test_a, msg, ALBERT, ALBERT_KEY, false)?; let channel_id_a = match get_event(test_a, height)? { Some(IbcEvent::OpenInitChannel(event)) => event @@ -589,7 +593,7 @@ fn channel_handshake( // Update the client state of Chain A on Chain B update_client_with_height(test_a, test_b, client_id_b, height_a)?; // OpenTryChannel on Chain B - let height = submit_ibc_tx(test_b, msg, ALBERT)?; + let height = submit_ibc_tx(test_b, msg, ALBERT, ALBERT_KEY, false)?; let channel_id_b = match get_event(test_b, height)? { Some(IbcEvent::OpenTryChannel(event)) => event .channel_id() @@ -615,7 +619,7 @@ fn channel_handshake( // Update the client state of Chain B on Chain A update_client_with_height(test_b, test_a, client_id_a, height_b)?; // OpenAckChannel on Chain A - submit_ibc_tx(test_a, msg, ALBERT)?; + submit_ibc_tx(test_a, msg, ALBERT, ALBERT_KEY, false)?; // get the proofs on Chain A let height_a = query_height(test_a)?; @@ -630,7 +634,7 @@ fn channel_handshake( // Update the client state of Chain A on Chain B update_client_with_height(test_a, test_b, client_id_b, height_a)?; // OpenConfirmChannel on Chain B - submit_ibc_tx(test_b, msg, ALBERT)?; + submit_ibc_tx(test_b, msg, ALBERT, ALBERT_KEY, false)?; Ok((port_channel_id_a, port_channel_id_b)) } @@ -716,8 +720,10 @@ fn transfer_token( &receiver, NAM, &Amount::native_whole(100000), + ALBERT_KEY, port_channel_id_a, None, + false, )?; let packet = match get_event(test_a, height)? { Some(IbcEvent::SendPacket(event)) => event.packet, @@ -735,7 +741,7 @@ fn transfer_token( // Update the client state of Chain A on Chain B update_client_with_height(test_a, test_b, client_id_b, height_a)?; // Receive the token on Chain B - let height = submit_ibc_tx(test_b, msg, ALBERT)?; + let height = submit_ibc_tx(test_b, msg, ALBERT, ALBERT_KEY, false)?; let (acknowledgement, packet) = match get_event(test_b, height)? { Some(IbcEvent::WriteAcknowledgement(event)) => { (event.ack, event.packet) @@ -760,7 +766,7 @@ fn transfer_token( // Update the client state of Chain B on Chain A update_client_with_height(test_b, test_a, client_id_a, height_b)?; // Acknowledge on Chain A - submit_ibc_tx(test_a, msg, ALBERT)?; + submit_ibc_tx(test_a, msg, ALBERT, ALBERT_KEY, false)?; Ok(()) } @@ -829,8 +835,10 @@ fn transfer_back( &receiver, ibc_token, &Amount::native_whole(50000), + BERTHA_KEY, port_channel_id_b, None, + false, )?; let packet = match get_event(test_b, height)? { Some(IbcEvent::SendPacket(event)) => event.packet, @@ -847,7 +855,7 @@ fn transfer_back( // Update the client state of Chain B on Chain A update_client_with_height(test_b, test_a, client_id_a, height_b)?; // Receive the token on Chain A - let height = submit_ibc_tx(test_a, msg, ALBERT)?; + let height = submit_ibc_tx(test_a, msg, ALBERT, ALBERT_KEY, false)?; let (acknowledgement, packet) = match get_event(test_a, height)? { Some(IbcEvent::WriteAcknowledgement(event)) => { (event.ack, event.packet) @@ -867,7 +875,7 @@ fn transfer_back( // Update the client state of Chain A on Chain B update_client_with_height(test_a, test_b, client_id_b, height_a)?; // Acknowledge on Chain B - submit_ibc_tx(test_b, msg, ALBERT)?; + submit_ibc_tx(test_b, msg, ALBERT, ALBERT_KEY, false)?; Ok(()) } @@ -887,8 +895,10 @@ fn transfer_timeout( &receiver, NAM, &Amount::native_whole(100000), + ALBERT_KEY, port_channel_id_a, Some(Duration::new(5, 0)), + false, )?; let packet = match get_event(test_a, height)? { Some(IbcEvent::SendPacket(event)) => event.packet, @@ -909,7 +919,7 @@ fn transfer_timeout( // Update the client state of Chain B on Chain A update_client_with_height(test_b, test_a, client_id_a, height_b)?; // Timeout on Chain A - submit_ibc_tx(test_a, msg, ALBERT)?; + submit_ibc_tx(test_a, msg, ALBERT, ALBERT_KEY, false)?; Ok(()) } @@ -979,7 +989,9 @@ fn commitment_prefix() -> CommitmentPrefix { fn submit_ibc_tx( test: &Test, message: impl Msg + std::fmt::Debug, + owner: &str, signer: &str, + wait_reveal_pk: bool, ) -> Result { let data_path = test.test_dir.path().join("tx.data"); let data = make_ibc_data(message); @@ -996,7 +1008,9 @@ fn submit_ibc_tx( TX_IBC_WASM, "--data-path", &data_path, - "--signer", + "--owner", + owner, + "--signing-keys", signer, "--gas-amount", "0", @@ -1010,6 +1024,9 @@ fn submit_ibc_tx( Some(40) )?; client.exp_string("Transaction applied")?; + if wait_reveal_pk { + client.exp_string("Transaction applied")?; + } check_tx_height(test, &mut client) } @@ -1020,8 +1037,10 @@ fn transfer( receiver: &Address, token: impl AsRef, amount: &Amount, + signer: impl AsRef, port_channel_id: &PortChannelId, timeout_sec: Option, + wait_reveal_pk: bool, ) -> Result { let rpc = get_actor_rpc(test, &Who::Validator(0)); @@ -1035,8 +1054,8 @@ fn transfer( sender.as_ref(), "--receiver", &receiver, - "--signer", - sender.as_ref(), + "--signing-keys", + signer.as_ref(), "--token", token.as_ref(), "--amount", @@ -1057,6 +1076,9 @@ fn transfer( let mut client = run!(test, Bin::Client, tx_args, Some(40))?; client.exp_string("Transaction applied")?; + if wait_reveal_pk { + client.exp_string("Transaction applied")?; + } check_tx_height(test, &mut client) } diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 629d263926f..5bb3f86898c 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -20,7 +20,6 @@ use borsh::BorshSerialize; use color_eyre::eyre::Result; use data_encoding::HEXLOWER; use namada::types::address::Address; -use namada::types::governance::ProposalType; use namada::types::storage::Epoch; use namada::types::token; use namada_apps::config::ethereum_bridge; @@ -29,18 +28,21 @@ use namada_apps::config::genesis::genesis_config::{ }; 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::{PgfAction, PgfSteward}; use namada_test_utils::TestWasms; +use namada_vp_prelude::testnet_pow; use serde_json::json; use setup::constants::*; use setup::Test; use super::helpers::{ - get_height, wait_for_block_height, wait_for_wasm_pre_compile, + epochs_per_year_from_min_duration, get_height, wait_for_block_height, + wait_for_wasm_pre_compile, }; use super::setup::{get_all_wasms_hashes, set_ethereum_bridge_mode, NamadaCmd}; use crate::e2e::helpers::{ epoch_sleep, find_address, find_bonded_stake, get_actor_rpc, get_epoch, - parse_reached_epoch, + is_debug_mode, parse_reached_epoch, }; use crate::e2e::setup::{self, default_port_offset, sleep, Bin, Who}; use crate::{run, run_as}; @@ -170,6 +172,8 @@ fn test_node_connectivity_and_consensus() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--node", &validator_one_rpc, ]; @@ -406,7 +410,22 @@ 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::network( + |genesis| { + #[cfg(not(feature = "mainnet"))] + { + GenesisConfig { + faucet_pow_difficulty: testnet_pow::Difficulty::try_new(1), + ..genesis + } + } + #[cfg(feature = "mainnet")] + { + genesis + } + }, + None, + )?; set_ethereum_bridge_mode( &test, @@ -441,6 +460,9 @@ fn ledger_txs_and_queries() -> Result<()> { let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); + let multisig_account = + format!("{},{},{}", BERTHA_KEY, ALBERT_KEY, CHRISTEL_KEY); + let txs_args = vec![ // 2. Submit a token transfer tx (from an established account) vec![ @@ -459,6 +481,8 @@ fn ledger_txs_and_queries() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--node", &validator_one_rpc, ], @@ -479,6 +503,8 @@ fn ledger_txs_and_queries() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + DAEWON, "--node", &validator_one_rpc, ], @@ -505,46 +531,50 @@ fn ledger_txs_and_queries() -> Result<()> { // 3. Submit a transaction to update an account's validity // predicate vec![ - "update", - "--address", - BERTHA, - "--code-path", - VP_USER_WASM, - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, + "update-account", + "--address", + BERTHA, + "--code-path", + VP_USER_WASM, + "--gas-amount", + "0", + "--gas-limit", + "0", + "--gas-token", + NAM, + "--signing-keys", + BERTHA_KEY, "--node", &validator_one_rpc, ], // 4. Submit a custom tx vec![ "tx", - "--signer", - BERTHA, "--code-path", TX_TRANSFER_WASM, "--data-path", &tx_data_path, + "--owner", + BERTHA, "--gas-amount", "0", "--gas-limit", "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--node", &validator_one_rpc ], // 5. Submit a tx to initialize a new account vec![ "init-account", - "--source", - BERTHA, - "--public-key", + "--public-keys", // Value obtained from `namada::types::key::ed25519::tests::gen_keypair` "001be519a321e29020fa3cbfbfd01bd5e92db134305609270b71dace25b5a21168", + "--threshold", + "1", "--code-path", VP_USER_WASM, "--alias", @@ -555,11 +585,35 @@ fn ledger_txs_and_queries() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--node", &validator_one_rpc, ], - // 6. Submit a tx to withdraw from faucet account (requires PoW challenge - // solution) + // 5. Submit a tx to initialize a new multisig account + vec![ + "init-account", + "--public-keys", + &multisig_account, + "--threshold", + "2", + "--code-path", + VP_USER_WASM, + "--alias", + "Test-Account-2", + "--gas-amount", + "0", + "--gas-limit", + "0", + "--gas-token", + NAM, + "--signing-keys", + BERTHA_KEY, + "--node", + &validator_one_rpc, + ], + // 6. Submit a tx to withdraw from faucet account (requires PoW challenge + // solution) vec![ "transfer", "--source", @@ -571,8 +625,8 @@ fn ledger_txs_and_queries() -> Result<()> { "--amount", "10.1", // Faucet withdrawal requires an explicit signer - "--signer", - ALBERT, + "--signing-keys", + ALBERT_KEY, "--node", &validator_one_rpc, ], @@ -613,6 +667,16 @@ fn ledger_txs_and_queries() -> Result<()> { // expect a decimal r"nam: \d+(\.\d+)?", ), + ( + vec![ + "query-account", + "--owner", + "Test-Account-2", + "--node", + &validator_one_rpc, + ], + "Threshold: 2", + ), ]; for (query_args, expected) in &query_args_and_expected_response { let mut client = run!(test, Bin::Client, query_args, Some(40))?; @@ -679,9 +743,7 @@ fn invalid_transactions() -> Result<()> { let tx_args = vec![ "transfer", "--source", - DAEWON, - "--signing-key", - ALBERT_KEY, + BERTHA, "--target", ALBERT, "--token", @@ -694,8 +756,11 @@ fn invalid_transactions() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + ALBERT_KEY, "--node", &validator_one_rpc, + "--force", ]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; @@ -728,13 +793,16 @@ fn invalid_transactions() -> Result<()> { ledger.exp_string("Committed block hash")?; let _bg_ledger = ledger.background(); + // we need to wait for the rpc endpoint to start + sleep(10); + // 5. Submit an invalid transactions (invalid token address) let daewon_lower = DAEWON.to_lowercase(); let tx_args = vec![ "transfer", "--source", DAEWON, - "--signing-key", + "--signing-keys", &daewon_lower, "--target", ALBERT, @@ -831,6 +899,8 @@ fn pos_bonds() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + "validator-0-account-key", "--node", &validator_one_rpc, ]; @@ -855,6 +925,8 @@ fn pos_bonds() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--node", &validator_one_rpc, ]; @@ -876,6 +948,8 @@ fn pos_bonds() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + "validator-0-account-key", "--node", &validator_one_rpc, ]; @@ -900,6 +974,8 @@ fn pos_bonds() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--node", &validator_one_rpc, ]; @@ -919,7 +995,7 @@ fn pos_bonds() -> Result<()> { epoch, delegation_withdrawable_epoch ); let start = Instant::now(); - let loop_timeout = Duration::new(60, 0); + let loop_timeout = Duration::new(120, 0); loop { if Instant::now().duration_since(start) > loop_timeout { panic!( @@ -944,6 +1020,8 @@ fn pos_bonds() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + "validator-0-account-key", "--node", &validator_one_rpc, ]; @@ -966,6 +1044,8 @@ fn pos_bonds() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--node", &validator_one_rpc, ]; @@ -1042,6 +1122,8 @@ fn pos_rewards() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--ledger-address", &validator_zero_rpc, ]; @@ -1078,6 +1160,8 @@ fn pos_rewards() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + "validator-1-account-key", "--ledger-address", &validator_one_rpc, ]; @@ -1100,6 +1184,8 @@ fn pos_rewards() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + "validator-2-account-key", "--ledger-address", &validator_two_rpc, ]; @@ -1188,6 +1274,8 @@ fn test_bond_queries() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--ledger-address", &validator_one_rpc, ]; @@ -1224,6 +1312,8 @@ fn test_bond_queries() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--ledger-address", &validator_one_rpc, ]; @@ -1247,6 +1337,8 @@ fn test_bond_queries() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--ledger-address", &validator_one_rpc, ]; @@ -1340,14 +1432,13 @@ fn pos_init_validator() -> Result<()> { // 2. Initialize a new validator account with the non-validator node let new_validator = "new-validator"; - let new_validator_key = format!("{}-key", new_validator); + let _new_validator_key = format!("{}-key", new_validator); let tx_args = vec![ "init-validator", "--alias", new_validator, - "--source", - BERTHA, - "--unsafe-dont-encrypt", + "--account-keys", + "bertha-key", "--gas-amount", "0", "--gas-limit", @@ -1358,8 +1449,11 @@ fn pos_init_validator() -> Result<()> { "0.05", "--max-commission-rate-change", "0.01", + "--signing-keys", + "bertha-key", "--node", &non_validator_rpc, + "--unsafe-dont-encrypt", ]; let mut client = run!(test, Bin::Client, tx_args, Some(40))?; client.exp_string("Transaction is valid.")?; @@ -1372,7 +1466,7 @@ fn pos_init_validator() -> Result<()> { "--source", BERTHA, "--target", - &new_validator_key, + new_validator, "--token", NAM, "--amount", @@ -1383,6 +1477,8 @@ fn pos_init_validator() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--node", &non_validator_rpc, ]; @@ -1406,6 +1502,8 @@ fn pos_init_validator() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--node", &non_validator_rpc, ]; @@ -1431,6 +1529,8 @@ fn pos_init_validator() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--node", &non_validator_rpc, ]; @@ -1463,6 +1563,13 @@ fn pos_init_validator() -> Result<()> { non_validator.interrupt()?; non_validator.exp_eof()?; + // it takes a bit before the node is shutdown. We dont want flasky test. + if is_debug_mode() { + sleep(10); + } else { + sleep(5); + } + let loc = format!("{}:{}", std::file!(), std::line!()); let validator_1_base_dir = test.get_base_dir(&Who::NonValidator); let mut validator_1 = setup::run_cmd( @@ -1554,6 +1661,8 @@ fn ledger_many_txs_in_a_block() -> Result<()> { "0", "--gas-token", NAM, + "--signing-keys", + BERTHA_KEY, "--node", ]); @@ -1674,12 +1783,8 @@ fn proposal_submission() -> Result<()> { let valid_proposal_json_path = prepare_proposal_data( &test, albert, - ProposalType::Default(Some( - TestWasms::TxProposalCode - .path() - .to_string_lossy() - .to_string(), - )), + TestWasms::TxProposalCode.read_bytes(), + 12, ); let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); @@ -1709,7 +1814,7 @@ fn proposal_submission() -> Result<()> { ]; let mut client = run!(test, Bin::Client, proposal_query_args, Some(40))?; - client.exp_string("Proposal: 0")?; + client.exp_string("Proposal Id: 0")?; client.assert_success(); // 4. Query token balance proposal author (submitted funds) @@ -1745,66 +1850,28 @@ fn proposal_submission() -> Result<()> { // 6. Submit an invalid proposal // proposal is invalid due to voting_end_epoch - voting_start_epoch < 3 let albert = find_address(&test, ALBERT)?; - let invalid_proposal_json = json!( - { - "content": { - "title": "TheTitle", - "authors": "test@test.com", - "discussions-to": "www.github.com/anoma/aip/1", - "created": "2022-03-10T08:54:37Z", - "license": "MIT", - "abstract": "Ut convallis eleifend orci vel venenatis. Duis - vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit - ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum - fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra - varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida - eros. Nullam sed ex justo. Ut at placerat ipsum, sit amet rhoncus libero. - Sed blandit non purus non suscipit. Phasellus sed quam nec augue bibendum - bibendum ut vitae urna. Sed odio diam, ornare nec sapien eget, congue - viverra enim.", - "motivation": "Ut convallis eleifend orci vel venenatis. Duis - vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit - ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum - fermentum. Morbi aliquet purus at sollicitudin ultrices.", - "details": "Ut convallis eleifend orci vel venenatis. Duis - vulputate metus in lacus sollicitudin vestibulum. Suspendisse vel velit - ac est consectetur feugiat nec ac urna. Ut faucibus ex nec dictum - fermentum. Morbi aliquet purus at sollicitudin ultrices. Quisque viverra - varius cursus. Praesent sed mauris gravida, pharetra turpis non, gravida - eros.", "requires": "2" - }, - "author": albert, - "voting_start_epoch": 9999_u64, - "voting_end_epoch": 10000_u64, - "grace_epoch": 10009_u64, - "type": { - "Default":null - } - } - ); - let invalid_proposal_json_path = - test.test_dir.path().join("invalid_proposal.json"); - generate_proposal_json_file( - invalid_proposal_json_path.as_path(), - &invalid_proposal_json, + let invalid_proposal_json = prepare_proposal_data( + &test, + albert, + TestWasms::TxProposalCode.read_bytes(), + 1, ); let submit_proposal_args = vec![ "init-proposal", "--data-path", - invalid_proposal_json_path.to_str().unwrap(), + invalid_proposal_json.to_str().unwrap(), "--node", &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; - client.exp_string( - "Invalid proposal end epoch: difference between proposal start and \ - end epoch must be at least 3 and at max 27 and end epoch must be a \ - multiple of 3", + client.exp_regex( + "Proposal data are invalid: Invalid proposal start epoch: 1 must be \ + greater than current epoch .* and a multiple of 3", )?; client.assert_failure(); - // 7. Check invalid proposal was not accepted + // 7. Check invalid proposal was not submitted let proposal_query_args = vec![ "query-proposal", "--proposal-id", @@ -1814,7 +1881,7 @@ fn proposal_submission() -> Result<()> { ]; let mut client = run!(test, Bin::Client, proposal_query_args, Some(40))?; - client.exp_string("No valid proposal was found with id 1")?; + client.exp_string("No proposal found with id: 1")?; client.assert_success(); // 8. Query token balance (funds shall not be submitted) @@ -1845,7 +1912,7 @@ fn proposal_submission() -> Result<()> { "0", "--vote", "yay", - "--signer", + "--address", "validator-0", "--node", &validator_one_rpc, @@ -1867,7 +1934,7 @@ fn proposal_submission() -> Result<()> { "0", "--vote", "nay", - "--signer", + "--address", BERTHA, "--node", &validator_one_rpc, @@ -1885,7 +1952,7 @@ fn proposal_submission() -> Result<()> { "0", "--vote", "yay", - "--signer", + "--address", ALBERT, "--node", &validator_one_rpc, @@ -1913,7 +1980,10 @@ fn proposal_submission() -> Result<()> { ]; let mut client = run!(test, Bin::Client, query_proposal, Some(15))?; - client.exp_string("Result: passed")?; + client.exp_string("Proposal Id: 0")?; + client.exp_string( + "passed with 200900.000000 yay votes and 0.000000 nay votes (0.%)", + )?; client.assert_success(); // 12. Wait proposal grace and check proposal author funds @@ -1964,228 +2034,6 @@ fn proposal_submission() -> Result<()> { Ok(()) } -/// Test submission and vote of an ETH proposal. -/// -/// 1 - Submit proposal -/// 2 - Vote with delegator and check failure -/// 3 - Vote with validator and check success -/// 4 - Check that proposal passed and funds -#[test] -fn eth_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: 1, - max_expected_time_per_block: 1, - ..genesis.parameters - }; - - GenesisConfig { - parameters, - ..genesis - } - }, - None, - )?; - - let namadac_help = vec!["--help"]; - - let mut client = run!(test, Bin::Client, namadac_help, Some(40))?; - client.exp_string("Namada client command line interface.")?; - client.assert_success(); - - // 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)); - - // Delegate some token - let tx_args = vec![ - "bond", - "--validator", - "validator-0", - "--source", - BERTHA, - "--amount", - "900", - "--gas-amount", - "0", - "--gas-limit", - "0", - "--gas-token", - NAM, - "--ledger-address", - &validator_one_rpc, - ]; - client = run!(test, Bin::Client, tx_args, Some(40))?; - client.exp_string("Transaction is valid.")?; - client.assert_success(); - - // 1 - Submit proposal - let albert = find_address(&test, ALBERT)?; - let valid_proposal_json_path = - prepare_proposal_data(&test, albert, ProposalType::ETHBridge); - let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); - - let submit_proposal_args = vec![ - "init-proposal", - "--data-path", - valid_proposal_json_path.to_str().unwrap(), - "--ledger-address", - &validator_one_rpc, - ]; - client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; - client.exp_string("Transaction is valid.")?; - client.assert_success(); - - // Query the proposal - let proposal_query_args = vec![ - "query-proposal", - "--proposal-id", - "0", - "--ledger-address", - &validator_one_rpc, - ]; - - client = run!(test, Bin::Client, proposal_query_args, Some(40))?; - client.exp_string("Proposal: 0")?; - client.assert_success(); - - // Query token balance proposal author (submitted funds) - let query_balance_args = vec![ - "balance", - "--owner", - ALBERT, - "--token", - NAM, - "--ledger-address", - &validator_one_rpc, - ]; - - client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("nam: 999500")?; - client.assert_success(); - - // Query token balance governance - let query_balance_args = vec![ - "balance", - "--owner", - GOVERNANCE_ADDRESS, - "--token", - NAM, - "--ledger-address", - &validator_one_rpc, - ]; - - client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("nam: 500")?; - client.assert_success(); - - // 2 - Vote with delegator and check failure - let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); - while epoch.0 <= 13 { - sleep(1); - epoch = get_epoch(&test, &validator_one_rpc).unwrap(); - } - - use namada::types::key::{self, secp256k1, SigScheme}; - use rand::prelude::ThreadRng; - use rand::thread_rng; - - // Generate a signing key to sign the eth message to sign the eth message to - // sign the eth message - let mut rng: ThreadRng = thread_rng(); - let node_sk = secp256k1::SigScheme::generate(&mut rng); - let signing_key = key::common::SecretKey::Secp256k1(node_sk); - let msg = "fd34672ab5"; - let vote_arg = format!("{} {}", signing_key, msg); - let submit_proposal_vote_delagator = vec![ - "vote-proposal", - "--proposal-id", - "0", - "--vote", - "yay", - "--eth", - &vote_arg, - "--signer", - BERTHA, - "--ledger-address", - &validator_one_rpc, - ]; - - client = run!(test, Bin::Client, submit_proposal_vote_delagator, Some(40))?; - client.exp_string("Transaction is invalid.")?; - client.assert_success(); - - // 3 - Send a yay vote from a validator - let vote_arg = format!("{} {}", signing_key, msg); - - let submit_proposal_vote = vec![ - "vote-proposal", - "--proposal-id", - "0", - "--vote", - "yay", - "--eth", - &vote_arg, - "--signer", - "validator-0", - "--ledger-address", - &validator_one_rpc, - ]; - - client = run_as!( - test, - Who::Validator(0), - Bin::Client, - submit_proposal_vote, - Some(15) - )?; - client.exp_string("Transaction is valid.")?; - client.assert_success(); - - // 4 - Wait proposals grace and check proposal author funds - while epoch.0 < 31 { - sleep(1); - epoch = get_epoch(&test, &validator_one_rpc).unwrap(); - } - - let query_balance_args = vec![ - "balance", - "--owner", - ALBERT, - "--token", - NAM, - "--ledger-address", - &validator_one_rpc, - ]; - - client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("nam: 1000000")?; - client.assert_success(); - - // Check if governance funds are 0 - let query_balance_args = vec![ - "balance", - "--owner", - GOVERNANCE_ADDRESS, - "--token", - NAM, - "--ledger-address", - &validator_one_rpc, - ]; - - client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("nam: 0")?; - client.assert_success(); - - Ok(()) -} - /// Test submission and vote of a PGF proposal /// /// 1 - Sumbit two proposals @@ -2259,12 +2107,18 @@ fn pgf_governance_proposal() -> Result<()> { // 1 - Submit proposal let albert = find_address(&test, ALBERT)?; + let pgf_stewards = PgfSteward { + action: PgfAction::Add, + address: albert.clone(), + }; + let valid_proposal_json_path = - prepare_proposal_data(&test, albert.clone(), ProposalType::PGFCouncil); + prepare_proposal_data(&test, albert, vec![pgf_stewards], 12); let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); let submit_proposal_args = vec![ "init-proposal", + "--pgf-stewards", "--data-path", valid_proposal_json_path.to_str().unwrap(), "--ledger-address", @@ -2275,22 +2129,6 @@ fn pgf_governance_proposal() -> Result<()> { client.exp_string("Transaction is valid.")?; client.assert_success(); - // Sumbit another proposal - let valid_proposal_json_path = - prepare_proposal_data(&test, albert, ProposalType::PGFCouncil); - let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); - - let submit_proposal_args = vec![ - "init-proposal", - "--data-path", - valid_proposal_json_path.to_str().unwrap(), - "--ledger-address", - &validator_one_rpc, - ]; - client = run!(test, Bin::Client, submit_proposal_args, Some(40))?; - client.exp_string("Transaction is valid.")?; - client.assert_success(); - // 2 - Query the proposal let proposal_query_args = vec![ "query-proposal", @@ -2301,19 +2139,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, proposal_query_args, Some(40))?; - client.exp_string("Proposal: 0")?; - client.assert_success(); - - let proposal_query_args = vec![ - "query-proposal", - "--proposal-id", - "1", - "--ledger-address", - &validator_one_rpc, - ]; - - client = run!(test, Bin::Client, proposal_query_args, Some(40))?; - client.exp_string("Proposal: 1")?; + client.exp_string("Proposal Id: 0")?; client.assert_success(); // Query token balance proposal author (submitted funds) @@ -2328,7 +2154,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("nam: 999000")?; + client.exp_string("nam: 999500")?; client.assert_success(); // Query token balance governance @@ -2343,7 +2169,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, query_balance_args, Some(40))?; - client.exp_string("nam: 1000")?; + client.exp_string("nam: 500")?; client.assert_success(); // 3 - Send a yay vote from a validator @@ -2354,17 +2180,13 @@ fn pgf_governance_proposal() -> Result<()> { } let albert_address = find_address(&test, ALBERT)?; - let arg_vote = format!("{} 1000", albert_address); - let submit_proposal_vote = vec![ "vote-proposal", "--proposal-id", "0", "--vote", "yay", - "--pgf", - &arg_vote, - "--signer", + "--address", "validator-0", "--ledger-address", &validator_one_rpc, @@ -2382,16 +2204,13 @@ fn pgf_governance_proposal() -> Result<()> { client.assert_success(); // Send different yay vote from delegator to check majority on 1/3 - let different_vote = format!("{} 900", albert_address); let submit_proposal_vote_delagator = vec![ "vote-proposal", "--proposal-id", "0", "--vote", "yay", - "--pgf", - &different_vote, - "--signer", + "--address", BERTHA, "--ledger-address", &validator_one_rpc, @@ -2403,29 +2222,6 @@ fn pgf_governance_proposal() -> Result<()> { client.exp_string("Transaction is valid.")?; client.assert_success(); - // Send vote to the second proposal from delegator - let submit_proposal_vote_delagator = vec![ - "vote-proposal", - "--proposal-id", - "1", - "--vote", - "yay", - "--pgf", - &different_vote, - "--signer", - BERTHA, - "--ledger-address", - &validator_one_rpc, - ]; - - // this is valid because the client filter ALBERT delegation and there are - // none - let mut client = - run!(test, Bin::Client, submit_proposal_vote_delagator, Some(15))?; - client.exp_string("Transaction applied with result:")?; - client.exp_string("Transaction is valid.")?; - client.assert_success(); - // 4 - Query the proposal and check the result is the one voted by the // validator (majority) epoch = get_epoch(&test, &validator_one_rpc).unwrap(); @@ -2443,23 +2239,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, query_proposal, Some(15))?; - client.exp_string(&format!( - "Result: passed with PGF council address: {}, spending cap: 0.001", - albert_address - ))?; - client.assert_success(); - - // Query the second proposal and check the it didn't pass - let query_proposal = vec![ - "query-proposal-result", - "--proposal-id", - "1", - "--ledger-address", - &validator_one_rpc, - ]; - - client = run!(test, Bin::Client, query_proposal, Some(15))?; - client.exp_string("Result: rejected")?; + client.exp_string("passed")?; client.assert_success(); // 12. Wait proposals grace and check proposal author funds @@ -2479,7 +2259,7 @@ fn pgf_governance_proposal() -> Result<()> { ]; client = run!(test, Bin::Client, query_balance_args, Some(30))?; - client.exp_string("nam: 999500")?; + client.exp_string("nam: 1000000")?; client.assert_success(); // Check if governance funds are 0 @@ -2497,6 +2277,15 @@ fn pgf_governance_proposal() -> Result<()> { client.exp_string("nam: 0")?; client.assert_success(); + // 14. Query pgf stewards + let query_pgf = vec!["query-pgf", "--node", &validator_one_rpc]; + + let mut client = run!(test, Bin::Client, query_pgf, Some(30))?; + client.exp_string("Pgf stewards:")?; + client.exp_string(&format!("- {}", albert_address))?; + client.exp_string("Pgf fundings: no fundings are currently set.")?; + client.assert_success(); + Ok(()) } @@ -2574,7 +2363,7 @@ fn proposal_offline() -> Result<()> { client.exp_string("Transaction is valid.")?; client.assert_success(); - // 2. Create an offline + // 2. Create an offline proposal let albert = find_address(&test, ALBERT)?; let valid_proposal_json = json!( { @@ -2590,12 +2379,7 @@ fn proposal_offline() -> Result<()> { "requires": "2" }, "author": albert, - "voting_start_epoch": 3_u64, - "voting_end_epoch": 9_u64, - "grace_epoch": 18_u64, - "type": { - "Default": null - } + "tally_epoch": 3_u64, } ); let valid_proposal_json_path = @@ -2605,6 +2389,12 @@ fn proposal_offline() -> Result<()> { &valid_proposal_json, ); + let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + while epoch.0 <= 3 { + sleep(1); + epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + } + let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); let offline_proposal_args = vec![ @@ -2612,44 +2402,48 @@ fn proposal_offline() -> Result<()> { "--data-path", valid_proposal_json_path.to_str().unwrap(), "--offline", + "--signing-keys", + ALBERT_KEY, + "--output-folder-path", + test.test_dir.path().to_str().unwrap(), "--node", &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, offline_proposal_args, Some(15))?; - client.exp_string("Proposal created: ")?; + let (_, matched) = client.exp_regex("Proposal serialized to: .*")?; client.assert_success(); - // 3. Generate an offline yay vote - let mut epoch = get_epoch(&test, &validator_one_rpc).unwrap(); - while epoch.0 <= 2 { - sleep(1); - epoch = get_epoch(&test, &validator_one_rpc).unwrap(); - } - - let proposal_path = test.test_dir.path().join("proposal"); + let proposal_path = matched + .split(':') + .collect::>() + .get(1) + .unwrap() + .trim() + .to_string(); + // 3. Generate an offline yay vote let submit_proposal_vote = vec![ "vote-proposal", "--data-path", - proposal_path.to_str().unwrap(), + &proposal_path, "--vote", "yay", - "--signer", + "--address", ALBERT, "--offline", + "--signing-keys", + ALBERT_KEY, + "--output-folder-path", + test.test_dir.path().to_str().unwrap(), "--node", &validator_one_rpc, ]; let mut client = run!(test, Bin::Client, submit_proposal_vote, Some(15))?; - client.exp_string("Proposal vote created: ")?; + client.exp_string("Proposal vote serialized to: ")?; client.assert_success(); - let expected_file_name = format!("proposal-vote-{}", albert); - let expected_path_vote = test.test_dir.path().join(expected_file_name); - assert!(expected_path_vote.exists()); - // 4. Compute offline tally let tally_offline = vec![ "query-proposal-result", @@ -2661,7 +2455,8 @@ fn proposal_offline() -> Result<()> { ]; let mut client = run!(test, Bin::Client, tally_offline, Some(15))?; - client.exp_string("Result: rejected")?; + client.exp_string("Parsed 1 votes")?; + client.exp_string("rejected with 900.000000 yay votes")?; client.assert_success(); Ok(()) @@ -3444,6 +3239,8 @@ fn implicit_account_reveal_pk() -> Result<()> { NAM, "--amount", "10.1", + "--signing-keys", + source, "--node", &validator_0_rpc, ] @@ -3461,6 +3258,8 @@ fn implicit_account_reveal_pk() -> Result<()> { source, "--amount", "10.1", + "--signing-keys", + source, "--node", &validator_0_rpc, ] @@ -3471,16 +3270,19 @@ fn implicit_account_reveal_pk() -> Result<()> { // Submit proposal Box::new(|source| { // Gen data for proposal tx - let source = find_address(&test, source).unwrap(); + let author = find_address(&test, source).unwrap(); let valid_proposal_json_path = prepare_proposal_data( &test, - source, - ProposalType::Default(None), + author, + TestWasms::TxProposalCode.read_bytes(), + 12, ); vec![ "init-proposal", "--data-path", valid_proposal_json_path.to_str().unwrap(), + "--signing-keys", + source, "--node", &validator_0_rpc, ] @@ -3516,6 +3318,8 @@ fn implicit_account_reveal_pk() -> Result<()> { NAM, "--amount", "1000", + "--signing-keys", + BERTHA_KEY, "--node", &validator_0_rpc, ]; @@ -3589,10 +3393,11 @@ fn test_epoch_sleep() -> Result<()> { fn prepare_proposal_data( test: &setup::Test, source: Address, - proposal_type: ProposalType, + data: impl serde::Serialize, + start_epoch: u64, ) -> PathBuf { - let valid_proposal_json = json!( - { + let valid_proposal_json = json!({ + "proposal": { "content": { "title": "TheTitle", "authors": "test@test.com", @@ -3605,12 +3410,13 @@ fn prepare_proposal_data( "requires": "2" }, "author": source, - "voting_start_epoch": 12_u64, + "voting_start_epoch": start_epoch, "voting_end_epoch": 24_u64, "grace_epoch": 30_u64, - "type": proposal_type - } - ); + }, + "data": data + }); + let valid_proposal_json_path = test.test_dir.path().join("valid_proposal.json"); generate_proposal_json_file( @@ -3619,9 +3425,3 @@ fn prepare_proposal_data( ); valid_proposal_json_path } - -/// Convert epoch `min_duration` in seconds to `epochs_per_year` genesis -/// parameter. -fn epochs_per_year_from_min_duration(min_duration: u64) -> u64 { - 60 * 60 * 24 * 365 / min_duration -} diff --git a/tests/src/e2e/multitoken_tests.rs b/tests/src/e2e/multitoken_tests.rs new file mode 100644 index 00000000000..5d713b2aedd --- /dev/null +++ b/tests/src/e2e/multitoken_tests.rs @@ -0,0 +1,372 @@ +//! 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(()) +} diff --git a/tests/src/e2e/multitoken_tests/helpers.rs b/tests/src/e2e/multitoken_tests/helpers.rs new file mode 100644 index 00000000000..176692c508e --- /dev/null +++ b/tests/src/e2e/multitoken_tests/helpers.rs @@ -0,0 +1,190 @@ +//! Helpers for use in multitoken tests. +use std::path::PathBuf; + +use borsh::BorshSerialize; +use color_eyre::eyre::Result; +use eyre::Context; +use namada_core::types::address::Address; +use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; +use namada_core::types::{storage, token}; +use namada_test_utils::tx_data::TxWriteData; +use namada_test_utils::TestWasms; +use namada_tx_prelude::storage::KeySeg; +use rand::Rng; +use regex::Regex; + +use super::setup::constants::NAM; +use super::setup::{Bin, NamadaCmd, Test}; +use crate::e2e::setup::constants::{ALBERT, ALBERT_KEY}; +use crate::run; + +const MULTITOKEN_KEY_SEGMENT: &str = "tokens"; +const BALANCE_KEY_SEGMENT: &str = "balance"; +const RED_TOKEN_KEY_SEGMENT: &str = "red"; +const MULTITOKEN_RED_TOKEN_SUB_PREFIX: &str = "tokens/red"; + +const ARBITRARY_SIGNER: &str = ALBERT; +const ARBITRARY_SIGNER_KEY: &str = ALBERT_KEY; + +/// Initializes a VP to represent a multitoken account. +pub fn init_multitoken_vp(test: &Test, rpc_addr: &str) -> Result { + // we use a VP that always returns true for the multitoken VP here, as we + // are testing out the VPs of the sender and receiver of multitoken + // transactions here - not any multitoken VP itself + let multitoken_vp_wasm_path = + TestWasms::VpAlwaysTrue.path().to_string_lossy().to_string(); + let multitoken_alias = "multitoken"; + + let init_account_args = vec![ + "init-account", + "--source", + ARBITRARY_SIGNER, + "--public-key", + // Value obtained from + // `namada::types::key::ed25519::tests::gen_keypair` + "001be519a321e29020fa3cbfbfd01bd5e92db134305609270b71dace25b5a21168", + "--code-path", + &multitoken_vp_wasm_path, + "--alias", + multitoken_alias, + "--gas-amount", + "0", + "--gas-limit", + "0", + "--gas-token", + NAM, + "--ledger-address", + rpc_addr, + ]; + let mut client_init_account = + run!(test, Bin::Client, init_account_args, Some(40))?; + client_init_account.exp_string("Transaction is valid.")?; + client_init_account.exp_string("Transaction applied")?; + client_init_account.assert_success(); + Ok(multitoken_alias.to_string()) +} + +/// Generates a random path within the `test` directory. +fn generate_random_test_dir_path(test: &Test) -> PathBuf { + let rng = rand::thread_rng(); + let random_string: String = rng + .sample_iter(&rand::distributions::Alphanumeric) + .take(24) + .map(char::from) + .collect(); + test.test_dir.path().join(random_string) +} + +/// Writes `contents` to a random path within the `test` directory, and return +/// the path. +pub fn write_test_file( + test: &Test, + contents: impl AsRef<[u8]>, +) -> Result { + let path = generate_random_test_dir_path(test); + std::fs::write(&path, contents)?; + Ok(path) +} + +/// Mint red tokens to the given address. +pub fn mint_red_tokens( + test: &Test, + rpc_addr: &str, + multitoken: &Address, + owner: &Address, + amount: &token::Amount, +) -> Result<()> { + let red_balance_key = storage::Key::from(multitoken.to_db_key()) + .push(&MULTITOKEN_KEY_SEGMENT.to_owned())? + .push(&RED_TOKEN_KEY_SEGMENT.to_owned())? + .push(&BALANCE_KEY_SEGMENT.to_owned())? + .push(owner)?; + + let tx_code_path = TestWasms::TxWriteStorageKey.path(); + let tx_data_path = write_test_file( + test, + TxWriteData { + key: red_balance_key, + value: amount.try_to_vec()?, + } + .try_to_vec()?, + )?; + + let tx_data_path = tx_data_path.to_string_lossy().to_string(); + let tx_code_path = tx_code_path.to_string_lossy().to_string(); + let tx_args = vec![ + "tx", + "--signing-keys", + ARBITRARY_SIGNER_KEY, + "--code-path", + &tx_code_path, + "--data-path", + &tx_data_path, + "--ledger-address", + rpc_addr, + ]; + let mut client_tx = run!(test, Bin::Client, tx_args, Some(40))?; + client_tx.exp_string("Transaction is valid.")?; + client_tx.exp_string("Transaction applied")?; + client_tx.assert_success(); + Ok(()) +} + +pub fn attempt_red_tokens_transfer( + test: &Test, + rpc_addr: &str, + multitoken: &str, + from: &str, + to: &str, + signing_keys: &str, + amount: &token::Amount, +) -> Result { + let amount = amount.to_string_native(); + let transfer_args = vec![ + "transfer", + "--token", + multitoken, + "--sub-prefix", + MULTITOKEN_RED_TOKEN_SUB_PREFIX, + "--source", + from, + "--target", + to, + "--signing-keys", + signing_keys, + "--amount", + &amount, + "--ledger-address", + rpc_addr, + ]; + run!(test, Bin::Client, transfer_args, Some(40)) +} + +pub fn fetch_red_token_balance( + test: &Test, + rpc_addr: &str, + multitoken_alias: &str, + owner_alias: &str, +) -> Result { + let balance_args = vec![ + "balance", + "--owner", + owner_alias, + "--token", + multitoken_alias, + "--sub-prefix", + MULTITOKEN_RED_TOKEN_SUB_PREFIX, + "--ledger-address", + rpc_addr, + ]; + let mut client_balance = run!(test, Bin::Client, balance_args, Some(40))?; + let (_, matched) = client_balance.exp_regex(&format!( + r"{MULTITOKEN_RED_TOKEN_SUB_PREFIX}: (\d*\.?\d+)" + ))?; + let decimal_regex = Regex::new(r"(\d*\.?\d+)").unwrap(); + println!("Got balance for {}: {}", owner_alias, matched); + let decimal = decimal_regex.find(&matched).unwrap().as_str(); + client_balance.assert_success(); + token::Amount::from_str(decimal, NATIVE_MAX_DECIMAL_PLACES) + .wrap_err(format!("Failed to parse {}", matched)) +} diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index 5f148b4fc2c..eecc60588f1 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -839,6 +839,7 @@ pub mod constants { pub const CHRISTEL: &str = "Christel"; pub const CHRISTEL_KEY: &str = "Christel-key"; pub const DAEWON: &str = "Daewon"; + pub const DAEWON_KEY: &str = "Daewon-key"; pub const ESTER: &str = "Ester"; pub const MATCHMAKER_KEY: &str = "matchmaker-key"; pub const MASP: &str = "atest1v4ehgw36xaryysfsx5unvve4g5my2vjz89p52sjxxgenzd348yuyyv3hg3pnjs35g5unvde4ca36y5"; diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index 46478acf480..2e4f6281472 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -8,7 +8,7 @@ mod test_bridge_pool_vp { wrapped_erc20s, Contracts, EthereumBridgeConfig, UpgradeableContract, }; use namada::ledger::native_vp::ethereum_bridge::bridge_pool_vp::BridgePoolVp; - use namada::proto::{Code, Data, Section, Signature, Tx}; + use namada::proto::Tx; use namada::types::address::{nam, wnam}; use namada::types::chain::ChainId; use namada::types::eth_bridge_pool::{ @@ -17,7 +17,7 @@ mod test_bridge_pool_vp { use namada::types::ethereum_events::EthAddress; use namada::types::key::{common, ed25519, SecretKey}; use namada::types::token::Amount; - use namada::types::transaction::TxType; + use namada::types::tx::TxBuilder; use namada_apps::wallet::defaults::{albert_address, bertha_address}; use namada_apps::wasm_loader; @@ -108,15 +108,13 @@ mod test_bridge_pool_vp { let data = transfer.try_to_vec().expect("Test failed"); let wasm_code = wasm_loader::read_wasm_or_exit(wasm_dir(), ADD_TRANSFER_WASM); - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = ChainId::default(); - tx.set_data(Data::new(data)); - tx.set_code(Code::new(wasm_code)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.data_sechash(), *tx.code_sechash()], - keypair, - ))); - tx + + let tx_builder = TxBuilder::new(ChainId::default(), None); + tx_builder + .add_code(wasm_code) + .add_serialized_data(data) + .add_gas_payer(keypair.clone()) + .build() } #[test] diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index b76b4dd041b..077385dc61b 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -118,6 +118,11 @@ pub fn init_pos( tx_env.spawn_accounts([&native_token]); for validator in genesis_validators { tx_env.spawn_accounts([&validator.address]); + tx_env.init_account_storage( + &validator.address, + vec![validator.consensus_key.clone()], + 1, + ) } tx_env.wl_storage.storage.block.epoch = start_epoch; // Initialize PoS storage diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index a8c72cf9b7e..433f8f37d42 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -27,19 +27,22 @@ mod tests { get_dummy_header as tm_dummy_header, Error as IbcError, }; use namada::ledger::tx_env::TxEnv; - use namada::proto::{Code, Data, Section, Signature, Tx}; - use namada::types::address::{Address, InternalAddress}; use namada::types::hash::Hash; use namada::types::key::*; use namada::types::storage::{self, BlockHash, BlockHeight, Key, KeySeg}; use namada::types::time::DateTimeUtc; use namada::types::token::{self, Amount}; - use namada::types::transaction::TxType; + use namada::types::tx::TxBuilder; use namada::types::{address, key}; use namada_core::ledger::ibc::context::transfer_mod::testing::DummyTransferModule; use namada_core::ledger::ibc::Error as IbcActionError; use namada_test_utils::TestWasms; - use namada_tx_prelude::{BorshSerialize, StorageRead, StorageWrite}; + use namada_tx_prelude::address::InternalAddress; + use namada_tx_prelude::chain::ChainId; + use namada_tx_prelude::{ + Address, BorshSerialize, StorageRead, StorageWrite, + }; + use namada_vp_prelude::account::AccountPublicKeysMap; use namada_vp_prelude::VpEnv; use prost::Message; use test_log::test; @@ -437,12 +440,11 @@ mod tests { let addr = address::testing::established_address_1(); // Write the public key to storage - let pk_key = key::pk_key(&addr); let keypair = key::testing::keypair_1(); let pk = keypair.ref_to(); - env.wl_storage - .write(&pk_key, pk.try_to_vec().unwrap()) - .unwrap(); + + let _ = pks_handle(&addr).insert(&mut env.wl_storage, 0_u8, pk.clone()); + // Initialize the environment vp_host_env::set(env); @@ -455,28 +457,32 @@ mod tests { // Tx without any data vec![], ] { + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter(vec![pk.clone()]); let signed_tx_data = vp_host_env::with(|env| { - let mut tx = Tx::new(TxType::Raw); - tx.header.chain_id = env.wl_storage.storage.chain_id.clone(); - tx.header.expiration = expiration; - tx.set_code(Code::new(code.clone())); - tx.set_data(Data::new(data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &keypair, - ))); + let chain_id = env.wl_storage.storage.chain_id.clone(); + let tx_builder = TxBuilder::new(chain_id, expiration); + let tx = tx_builder + .add_code(code.clone()) + .add_serialized_data(data.to_vec()) + .add_gas_payer(keypair.clone()) + .add_signing_keys(keypairs.clone(), pks_map.clone()) + .build(); + env.tx = tx; env.tx.clone() }); assert_eq!(signed_tx_data.data().as_ref(), Some(data)); assert!( signed_tx_data - .verify_signature( - &pk, + .verify_section_signatures( &[ *signed_tx_data.data_sechash(), *signed_tx_data.code_sechash(), ], + pks_map, + 1, + None ) .is_ok() ); @@ -484,12 +490,16 @@ mod tests { let other_keypair = key::testing::keypair_2(); assert!( signed_tx_data - .verify_signature( - &other_keypair.ref_to(), + .verify_section_signatures( &[ *signed_tx_data.data_sechash(), *signed_tx_data.code_sechash(), ], + AccountPublicKeysMap::from_iter([ + other_keypair.ref_to() + ]), + 1, + None ) .is_err() ); @@ -543,13 +553,19 @@ mod tests { // evaluating without any code should fail let empty_code = Hash::zero(); let input_data = vec![]; - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(input_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(input_data.clone()) + .add_gas_payer(keypair.clone()) + .add_signing_keys(keypairs.clone(), pks_map.clone()) + .build(); let result = vp::CTX.eval(empty_code, tx).unwrap(); assert!(!result); @@ -561,14 +577,13 @@ mod tests { let key = Key::wasm_code(&code_hash); env.wl_storage.storage.write(&key, code.clone()).unwrap(); }); - let input_data = vec![]; - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(input_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code_from_hash(code_hash) + .add_serialized_data(input_data.clone()) + .add_gas_payer(keypair.clone()) + .add_signing_keys(keypairs.clone(), pks_map.clone()) + .build(); let result = vp::CTX.eval(code_hash, tx).unwrap(); assert!(result); @@ -581,14 +596,13 @@ mod tests { let key = Key::wasm_code(&code_hash); env.wl_storage.storage.write(&key, code.clone()).unwrap(); }); - let input_data = vec![]; - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(input_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code_from_hash(code_hash) + .add_serialized_data(input_data) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); let result = vp::CTX.eval(code_hash, tx).unwrap(); assert!(!result); } @@ -599,18 +613,23 @@ mod tests { tx_host_env::init(); ibc::init_storage(); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); // Start a transaction to create a new client let msg = ibc::msg_create_client(); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair.clone()) + .add_signing_keys(keypairs.clone(), pks_map.clone()) + .build(); // create a client with the message tx_host_env::ibc::ibc_actions(tx::ctx()) @@ -640,13 +659,13 @@ mod tests { let msg = ibc::msg_update_client(client_id); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // update the client with the message tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -663,6 +682,12 @@ mod tests { // The environment must be initialized first tx_host_env::init(); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + // Set the initial state before starting transactions ibc::init_storage(); let (client_id, client_state, writes) = ibc::prepare_client(); @@ -679,13 +704,13 @@ mod tests { let msg = ibc::msg_connection_open_init(client_id); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair.clone()) + .add_signing_keys(keypairs.clone(), pks_map.clone()) + .build(); // init a connection with the message tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -714,13 +739,13 @@ mod tests { let msg = ibc::msg_connection_open_ack(conn_id, client_state); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // open the connection with the message tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -740,6 +765,12 @@ mod tests { // Set the initial state before starting transactions ibc::init_storage(); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + let (client_id, client_state, writes) = ibc::prepare_client(); writes.into_iter().for_each(|(key, val)| { tx_host_env::with(|env| { @@ -754,13 +785,13 @@ mod tests { let msg = ibc::msg_connection_open_try(client_id, client_state); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair.clone()) + .add_signing_keys(keypairs.clone(), pks_map.clone()) + .build(); // open try a connection with the message tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -789,13 +820,13 @@ mod tests { let msg = ibc::msg_connection_open_confirm(conn_id); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // open the connection with the mssage tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -812,6 +843,12 @@ mod tests { // The environment must be initialized first tx_host_env::init(); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + // Set the initial state before starting transactions ibc::init_storage(); let (client_id, _client_state, mut writes) = ibc::prepare_client(); @@ -831,13 +868,13 @@ mod tests { let msg = ibc::msg_channel_open_init(port_id.clone(), conn_id); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair.clone()) + .add_signing_keys(keypairs.clone(), pks_map.clone()) + .build(); // init a channel with the message tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -866,13 +903,13 @@ mod tests { let msg = ibc::msg_channel_open_ack(port_id, channel_id); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // open the channle with the message tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -903,18 +940,24 @@ mod tests { }); }); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + // Start a transaction for ChannelOpenTry let port_id = ibc::PortId::transfer(); let msg = ibc::msg_channel_open_try(port_id.clone(), conn_id); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair.clone()) + .add_signing_keys(keypairs.clone(), pks_map.clone()) + .build(); // try open a channel with the message tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -944,13 +987,13 @@ mod tests { let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // open a channel with the message tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -984,18 +1027,24 @@ mod tests { }); }); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + // Start a transaction to close the channel let msg = ibc::msg_channel_close_init(port_id, channel_id); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // close the channel with the message let mut actions = tx_host_env::ibc::ibc_actions(tx::ctx()); // the dummy module closes the channel @@ -1037,18 +1086,24 @@ mod tests { }); }); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + // Start a transaction to close the channel let msg = ibc::msg_channel_close_confirm(port_id, channel_id); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // close the channel with the message tx_host_env::ibc::ibc_actions(tx::ctx()) @@ -1083,6 +1138,12 @@ mod tests { }); }); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + // Start a transaction to send a packet let msg = ibc::msg_transfer(port_id, channel_id, token.to_string(), &sender); @@ -1092,13 +1153,13 @@ mod tests { .encode(&mut tx_data) .expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair.clone()) + .add_signing_keys(keypairs.clone(), pks_map.clone()) + .build(); // send the token and a packet with the data tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -1141,13 +1202,13 @@ mod tests { let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // ack the packet with the message tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -1179,6 +1240,12 @@ mod tests { // The environment must be initialized first tx_host_env::init(); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + // Set the initial state before starting transactions let (token, sender) = ibc::init_storage(); let (client_id, _client_state, mut writes) = ibc::prepare_client(); @@ -1222,13 +1289,13 @@ mod tests { let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // send the token and a packet with the data tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -1259,6 +1326,12 @@ mod tests { // The environment must be initialized first tx_host_env::init(); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + // Set the initial state before starting transactions let (token, receiver) = ibc::init_storage(); let (client_id, _client_state, mut writes) = ibc::prepare_client(); @@ -1291,13 +1364,13 @@ mod tests { let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // receive a packet with the message tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -1332,6 +1405,12 @@ mod tests { // The environment must be initialized first tx_host_env::init(); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + // Set the initial state before starting transactions let (token, receiver) = ibc::init_storage(); let (client_id, _client_state, mut writes) = ibc::prepare_client(); @@ -1382,13 +1461,13 @@ mod tests { let msg = ibc::msg_packet_recv(packet); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // receive a packet with the message tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -1420,6 +1499,12 @@ mod tests { // The environment must be initialized first tx_host_env::init(); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + // Set the initial state before starting transactions let (token, receiver) = ibc::init_storage(); let (client_id, _client_state, mut writes) = ibc::prepare_client(); @@ -1477,13 +1562,13 @@ mod tests { let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // receive a packet with the message tx_host_env::ibc::ibc_actions(tx::ctx()) .execute(&tx_data) @@ -1518,6 +1603,12 @@ mod tests { // The environment must be initialized first tx_host_env::init(); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + // Set the initial state before starting transactions let (token, sender) = ibc::init_storage(); let (client_id, _client_state, mut writes) = ibc::prepare_client(); @@ -1573,13 +1664,13 @@ mod tests { let msg = ibc::msg_timeout(packet, ibc::Sequence::from(1)); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // timeout the packet tx_host_env::ibc::ibc_actions(tx::ctx()) @@ -1604,6 +1695,12 @@ mod tests { // The environment must be initialized first tx_host_env::init(); + let keypair = key::testing::keypair_1(); + let keypairs = vec![keypair.clone()]; + let pks_map = AccountPublicKeysMap::from_iter([ + key::testing::keypair_1().ref_to(), + ]); + // Set the initial state before starting transactions let (token, sender) = ibc::init_storage(); let (client_id, _client_state, mut writes) = ibc::prepare_client(); @@ -1658,13 +1755,13 @@ mod tests { let msg = ibc::msg_timeout_on_close(packet, ibc::Sequence::from(1)); let mut tx_data = vec![]; msg.to_any().encode(&mut tx_data).expect("encoding failed"); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(vec![])); - tx.set_data(Data::new(tx_data.clone())); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.code_sechash(), *tx.data_sechash()], - &key::testing::keypair_1(), - ))); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(vec![]) + .add_serialized_data(tx_data.clone()) + .add_gas_payer(keypair) + .add_signing_keys(keypairs, pks_map) + .build(); // timeout the packet tx_host_env::ibc::ibc_actions(tx::ctx()) diff --git a/tests/src/vm_host_env/tx.rs b/tests/src/vm_host_env/tx.rs index d3da806ca5b..aad6510c89b 100644 --- a/tests/src/vm_host_env/tx.rs +++ b/tests/src/vm_host_env/tx.rs @@ -18,7 +18,8 @@ use namada::vm::prefix_iter::PrefixIterators; use namada::vm::wasm::run::Error; use namada::vm::wasm::{self, TxCache, VpCache}; use namada::vm::{self, WasmCacheRwAccess}; -use namada_tx_prelude::{BorshSerialize, Ctx}; +use namada_tx_prelude::{storage_api, BorshSerialize, Ctx}; +use namada_vp_prelude::key::common; use tempfile::TempDir; use crate::vp::TestVpEnv; @@ -102,6 +103,7 @@ impl TestTxEnv { epoch_duration: Option, vp_whitelist: Option>, tx_whitelist: Option>, + max_signatures_per_transaction: Option, ) { parameters::update_epoch_parameter( &mut self.wl_storage, @@ -121,6 +123,11 @@ impl TestTxEnv { vp_whitelist.unwrap_or_default(), ) .unwrap(); + parameters::update_max_signature_per_tx( + &mut self.wl_storage, + max_signatures_per_transaction.unwrap_or(15), + ) + .unwrap(); } pub fn store_wasm_code(&mut self, code: Vec) { @@ -155,6 +162,34 @@ impl TestTxEnv { } } + pub fn init_account_storage( + &mut self, + owner: &Address, + public_keys: Vec, + threshold: u8, + ) { + storage_api::account::init_account_storage( + &mut self.wl_storage, + owner, + &public_keys, + threshold, + ) + .expect("Unable to write Account substorage."); + } + + /// Set public key for the address. + pub fn write_account_threshold( + &mut self, + address: &Address, + threshold: u8, + ) { + let storage_key = key::threshold_key(address); + self.wl_storage + .storage + .write(&storage_key, threshold.try_to_vec().unwrap()) + .unwrap(); + } + /// Commit the genesis state. Typically, you'll want to call this after /// setting up the initial state, before running a transaction. pub fn commit_genesis(&mut self) { @@ -186,19 +221,6 @@ impl TestTxEnv { .unwrap(); } - /// Set public key for the address. - pub fn write_public_key( - &mut self, - address: &Address, - public_key: &key::common::PublicKey, - ) { - let storage_key = key::pk_key(address); - self.wl_storage - .storage - .write(&storage_key, public_key.try_to_vec().unwrap()) - .unwrap(); - } - /// Apply the tx changes to the write log. pub fn execute_tx(&mut self) -> Result<(), Error> { wasm::run::tx( diff --git a/tx_prelude/src/account.rs b/tx_prelude/src/account.rs new file mode 100644 index 00000000000..da6c213601e --- /dev/null +++ b/tx_prelude/src/account.rs @@ -0,0 +1,18 @@ +use namada_core::types::transaction::account::InitAccount; + +use super::*; + +pub fn init_account( + ctx: &mut Ctx, + owner: &Address, + data: InitAccount, +) -> EnvResult
{ + storage_api::account::init_account_storage( + ctx, + owner, + &data.public_keys, + data.threshold, + )?; + + Ok(owner.to_owned()) +} diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index 6e5a0192c87..cd2f5d5f92d 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -6,6 +6,7 @@ #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] +pub mod account; pub mod ibc; pub mod key; pub mod proof_of_stake; @@ -18,7 +19,6 @@ pub use borsh::{BorshDeserialize, BorshSerialize}; pub use namada_core::ledger::eth_bridge; pub use namada_core::ledger::governance::storage as gov_storage; pub use namada_core::ledger::parameters::storage as parameters_storage; -pub use namada_core::ledger::slash_fund::storage as slash_fund_storage; pub use namada_core::ledger::storage::types::encode; pub use namada_core::ledger::storage_api::{ self, governance, iter_prefix, iter_prefix_bytes, Error, OptionExt, diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 48b0cb665f4..cc8bcb7b630 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -2,7 +2,7 @@ use namada_core::types::dec::Dec; use namada_core::types::hash::Hash; -use namada_core::types::transaction::InitValidator; +use namada_core::types::transaction::pos::InitValidator; use namada_core::types::{key, token}; pub use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::{ @@ -74,7 +74,8 @@ impl Ctx { pub fn init_validator( &mut self, InitValidator { - account_key, + account_keys, + threshold, consensus_key, eth_cold_key, eth_hot_key, @@ -89,8 +90,12 @@ impl Ctx { let current_epoch = self.get_block_epoch()?; // Init validator account let validator_address = self.init_account(validator_vp_code_hash)?; - let pk_key = key::pk_key(&validator_address); - self.write(&pk_key, &account_key)?; + storage_api::account::init_account_storage( + self, + &validator_address, + &account_keys, + threshold, + )?; let protocol_pk_key = key::protocol_pk_key(&validator_address); self.write(&protocol_pk_key, &protocol_key)?; let dkg_pk_key = key::dkg_session_keys::dkg_pk_key(&validator_address); diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index 8ec55ef7cd1..c8e555a87bc 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -6,8 +6,6 @@ #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] -pub mod key; - // used in the VP input use core::convert::AsRef; use core::slice; @@ -74,11 +72,32 @@ pub fn log_string>(msg: T) { /// Checks if a proposal id is being executed pub fn is_proposal_accepted(ctx: &Ctx, proposal_id: u64) -> VpResult { let proposal_execution_key = - gov_storage::get_proposal_execution_key(proposal_id); + gov_storage::keys::get_proposal_execution_key(proposal_id); ctx.has_key_pre(&proposal_execution_key) } +/// Verify section signatures +pub fn verify_signatures(ctx: &Ctx, tx: &Tx, owner: &Address) -> VpResult { + let max_signatures_per_transaction = + parameters::max_signatures_per_transaction(&ctx.pre())?; + + let public_keys_index_map = + storage_api::account::public_keys_index_map(&ctx.pre(), owner)?; + let threshold = + storage_api::account::threshold(&ctx.pre(), owner)?.unwrap_or(1); + + let targets = &[*tx.data_sechash(), *tx.code_sechash()]; + tx.verify_section_signatures( + targets, + public_keys_index_map, + threshold, + max_signatures_per_transaction, + ) + .map_err(|_e| Error::SimpleMessage("Invalid signatures")) + .map(|_| true) +} + /// Checks whether a transaction is valid, which happens in two cases: /// - tx is whitelisted, or /// - tx is executed by an approved governance proposal (no need to be diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index f6d14e2c2bb..0c73e22a4ac 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -3890,6 +3890,7 @@ dependencies = [ "num-traits", "prost", "regex", + "serde", "serde_json", "sha2 0.9.9", "tempfile", diff --git a/wasm/checksums.json b/wasm/checksums.json index 71bcf7c9ef8..1dcc94d2479 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,21 +1,21 @@ { - "tx_bond.wasm": "tx_bond.e9201ea2342459a86343e5e6db1400b23d0843fe4407f6f759c65dc63395b5cd.wasm", - "tx_bridge_pool.wasm": "tx_bridge_pool.c1b7457cc85bfb4ca141a3f1157700be2d0f672aec21a2a7883a6264e8775cc1.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.c91a746933ecb67daf41d77d6cb7810277dfb661a72e6f4726cee7300f750cde.wasm", - "tx_ibc.wasm": "tx_ibc.917a7c8ad4138679eb467e8a18b0234d6c3ba56802a728186d7fcd047e0c0c4b.wasm", - "tx_init_account.wasm": "tx_init_account.6fc0c6ebbf933befd27f8515f0d439814ad8ea2732ec62271fe5ec4da177340e.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.0dd098e0962ba8b3fac93eefae0524959125ce334edf05e05edbdf4d38ec9069.wasm", - "tx_init_validator.wasm": "tx_init_validator.88f4d2b5cc358e8e9a53512314450161b8ab123a54ccbbbc44c6272fc3496ee2.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.4d4243a995611cb5c53bb7ad9657dd6112f8f3e309b1497c4af2763dcff5f0e7.wasm", - "tx_transfer.wasm": "tx_transfer.c6d4ac6e8311a75f199a0ca009f2b80c1ef107eeadfe7bbab94ff6453f075954.wasm", - "tx_unbond.wasm": "tx_unbond.00df2be4c4eaa94bec27f4bb89ccb96f7725cfdc4af2c965473ec1e9679f0989.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.96081388ae0cb289df7fb48b37a5f640c867205f19c9c44a2af4f2f9598c4a27.wasm", - "tx_update_vp.wasm": "tx_update_vp.a1b0a203c05769a90c63711ef3ff922c5ba4b82f74511ade2a446ead47a2913b.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.9b4103f524a21e45d39361afb349e60a5010d94db5bf5ed5a8f678ec9c0df1b8.wasm", - "tx_withdraw.wasm": "tx_withdraw.37ffd46d73cc8cb67c14df42389b18fff03cd0c77861e2d9fc64ac486a13f65c.wasm", - "vp_implicit.wasm": "vp_implicit.e16621fd592ef6ad906b820762959dec1224e5a2e6917e8d8f0258601ed8dadd.wasm", - "vp_masp.wasm": "vp_masp.6aee2c99eba0b680198c985aabb1c6be877538ae338e36e9164c60685eec9334.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.1a78846b5aca8ce6a34c258dec573fec7d247ae07c09dd7b92196b2bd0a8d1dd.wasm", - "vp_user.wasm": "vp_user.e0200e6274839583464001363bfce2137c8a0a9191b242a9c34c32e31b525a04.wasm", - "vp_validator.wasm": "vp_validator.3a23a0a5629de85f2029543d3fc3d9d3fd58473280859b4984a9a25723c1ca88.wasm" + "tx_bond.wasm": "tx_bond.f737f9d4c536b627b4dd6c7a7f54de1d93ea7fa4648065f8861a74c5ab779d8c.wasm", + "tx_bridge_pool.wasm": "tx_bridge_pool.d3dcafbe987b7daf28b582f8f6c5ec784f2d7b9b2c5e25ad4e2c371a1e089156.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.ca16e544548224a07274a34d9fa4de0be2ca0317a5c0c75ade474c4f1a52921d.wasm", + "tx_ibc.wasm": "tx_ibc.37526a8aa8c8192afaf1d69190d26fb6f842ce8b812ed19fd4abe0c7b5f398b0.wasm", + "tx_init_account.wasm": "tx_init_account.f660b2d9be134bd33968e75a875b598dc0cd6d2c1b7a455df9cde66b77b0598a.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.bac6e829287e6af56268753db4db4488e9d1d6090e36af5e826337c0493de0d8.wasm", + "tx_init_validator.wasm": "tx_init_validator.2b88e1b4ecf0baee9b8fb61862a1dae91d12c13f65abea03179062485dc0965f.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.6e0e21d30b26f9736dc7df75dda806b311887d60d0767cf916a6adc22f3a970a.wasm", + "tx_transfer.wasm": "tx_transfer.c16423b1c59b9c02418895cf3e612ca9cda2260846f93188666919450f64fa6b.wasm", + "tx_unbond.wasm": "tx_unbond.b106bdcb2eab70b69a792eb64e2fe4c6fb146065569f2d234a27e0f6821aac08.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.201c90d5ffbd371fe852b8ed65d5dd59049ee97adafb4da6963b4b150a6762a0.wasm", + "tx_update_account.wasm": "tx_update_account.9ee92e949b654aec3e6501534252b124ff9840c8060d0359bf8f0e3f226474be.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.aa98455871f48deffa0e52809cd37fa13bb2a32ff15efe314fc903265ba1337e.wasm", + "tx_withdraw.wasm": "tx_withdraw.c81ec78d17156d4c9905a3336da3189c03a8777ea8ee06854bf9cb7411dbce73.wasm", + "vp_implicit.wasm": "vp_implicit.a759a4677ae16f8ac48e9cced46723ae593fe9c981074078e70ec7fbe2be23f7.wasm", + "vp_masp.wasm": "vp_masp.a03d2f286f985301130eecff738f4c3c92958836f2030add8bfac4f0cc552b23.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.4eae2710b3a8f12e0a7ec5402e42965ac1c77803beb4e2f501faacac69330930.wasm", + "vp_user.wasm": "vp_user.93aa25048fe883e581f27f5b2fa782e17d0c0e422faa2f139b300ef31e435229.wasm", + "vp_validator.wasm": "vp_validator.72c78f3a8ef989f0d6939c59c1bc91d4ea9db45420f0242b3114cda3842a8bbd.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index a60c6ce8754..8fd5f1ba829 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -24,7 +24,7 @@ tx_reveal_pk = ["namada_tx_prelude"] tx_transfer = ["namada_tx_prelude"] tx_unbond = ["namada_tx_prelude"] tx_unjail_validator = ["namada_tx_prelude"] -tx_update_vp = ["namada_tx_prelude"] +tx_update_account = ["namada_tx_prelude"] tx_vote_proposal = ["namada_tx_prelude"] tx_withdraw = ["namada_tx_prelude"] vp_implicit = ["namada_vp_prelude", "once_cell"] diff --git a/wasm/wasm_source/Makefile b/wasm/wasm_source/Makefile index f394179dc70..a88065355fc 100644 --- a/wasm/wasm_source/Makefile +++ b/wasm/wasm_source/Makefile @@ -16,7 +16,7 @@ wasms += tx_reveal_pk wasms += tx_transfer wasms += tx_unbond wasms += tx_unjail_validator -wasms += tx_update_vp +wasms += tx_update_account wasms += tx_vote_proposal wasms += tx_withdraw wasms += vp_implicit diff --git a/wasm/wasm_source/src/lib.rs b/wasm/wasm_source/src/lib.rs index 2fc69f65c91..ce5b1b15618 100644 --- a/wasm/wasm_source/src/lib.rs +++ b/wasm/wasm_source/src/lib.rs @@ -20,8 +20,8 @@ pub mod tx_transfer; pub mod tx_unbond; #[cfg(feature = "tx_unjail_validator")] pub mod tx_unjail_validator; -#[cfg(feature = "tx_update_vp")] -pub mod tx_update_vp; +#[cfg(feature = "tx_update_account")] +pub mod tx_update_account; #[cfg(feature = "tx_vote_proposal")] pub mod tx_vote_proposal; #[cfg(feature = "tx_withdraw")] diff --git a/wasm/wasm_source/src/tx_bond.rs b/wasm/wasm_source/src/tx_bond.rs index 33d004591b2..3ebd6aa8a40 100644 --- a/wasm/wasm_source/src/tx_bond.rs +++ b/wasm/wasm_source/src/tx_bond.rs @@ -22,10 +22,9 @@ mod tests { bond_handle, read_consensus_validator_set_addresses_with_stake, read_total_stake, read_validator_stake, }; - use namada::proto::{Code, Data, Signature, Tx}; use namada::types::dec::Dec; use namada::types::storage::Epoch; - use namada::types::transaction::TxType; + use namada::types::tx::TxBuilder; use namada_tests::log::test; use namada_tests::native_vp::pos::init_pos; use namada_tests::native_vp::TestNativeVpEnv; @@ -34,6 +33,7 @@ mod tests { arb_established_address, arb_non_internal_address, }; use namada_tx_prelude::address::InternalAddress; + use namada_tx_prelude::chain::ChainId; use namada_tx_prelude::key::testing::arb_common_keypair; use namada_tx_prelude::key::RefTo; use namada_tx_prelude::proof_of_stake::parameters::testing::arb_pos_params; @@ -108,14 +108,14 @@ mod tests { let tx_code = vec![]; let tx_data = bond.try_to_vec().unwrap(); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(tx_code)); - tx.set_data(Data::new(tx_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.data_sechash(), *tx.code_sechash()], - &key, - ))); - let signed_tx = tx.clone(); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(key) + .build(); + + let signed_tx = tx; // Ensure that the initial stake of the sole validator is equal to the // PoS account balance diff --git a/wasm/wasm_source/src/tx_change_validator_commission.rs b/wasm/wasm_source/src/tx_change_validator_commission.rs index edbad6efaa8..db5486e1f44 100644 --- a/wasm/wasm_source/src/tx_change_validator_commission.rs +++ b/wasm/wasm_source/src/tx_change_validator_commission.rs @@ -21,15 +21,15 @@ mod tests { use namada::ledger::pos::{PosParams, PosVP}; use namada::proof_of_stake::validator_commission_rate_handle; - use namada::proto::{Code, Data, Signature, Tx}; use namada::types::dec::{Dec, POS_DECIMAL_PRECISION}; use namada::types::storage::Epoch; - use namada::types::transaction::TxType; + use namada::types::tx::TxBuilder; use namada_tests::log::test; use namada_tests::native_vp::pos::init_pos; use namada_tests::native_vp::TestNativeVpEnv; use namada_tests::tx::*; use namada_tx_prelude::address::testing::arb_established_address; + use namada_tx_prelude::chain::ChainId; use namada_tx_prelude::key::testing::arb_common_keypair; use namada_tx_prelude::key::RefTo; use namada_tx_prelude::proof_of_stake::parameters::testing::arb_pos_params; @@ -87,14 +87,14 @@ mod tests { let tx_code = vec![]; let tx_data = commission_change.try_to_vec().unwrap(); - let mut tx = Tx::new(TxType::Raw); - tx.set_data(Data::new(tx_data)); - tx.set_code(Code::new(tx_code)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.data_sechash(), *tx.code_sechash()], - &key, - ))); - let signed_tx = tx.clone(); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(key) + .build(); + + let signed_tx = tx; // Read the data before the tx is executed let commission_rate_handle = diff --git a/wasm/wasm_source/src/tx_init_account.rs b/wasm/wasm_source/src/tx_init_account.rs index 2e85b70ae91..346afb2bec7 100644 --- a/wasm/wasm_source/src/tx_init_account.rs +++ b/wasm/wasm_source/src/tx_init_account.rs @@ -7,7 +7,7 @@ use namada_tx_prelude::*; fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { let signed = tx_data; let data = signed.data().ok_or_err_msg("Missing data")?; - let tx_data = transaction::InitAccount::try_from_slice(&data[..]) + let tx_data = transaction::account::InitAccount::try_from_slice(&data[..]) .wrap_err("failed to decode InitAccount")?; debug_log!("apply_tx called to init a new established account"); @@ -18,8 +18,17 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { .ok_or_err_msg("vp code section must be tagged as extra")? .code .hash(); + let address = ctx.init_account(vp_code)?; - let pk_key = key::pk_key(&address); - ctx.write(&pk_key, &tx_data.public_key)?; + + match account::init_account(ctx, &address, tx_data) { + Ok(address) => { + debug_log!("Created account {}", address.encode(),) + } + Err(err) => { + debug_log!("Account creation failed with: {}", err); + panic!() + } + } Ok(()) } diff --git a/wasm/wasm_source/src/tx_init_proposal.rs b/wasm/wasm_source/src/tx_init_proposal.rs index c0da1d9316b..e11ebb7a61f 100644 --- a/wasm/wasm_source/src/tx_init_proposal.rs +++ b/wasm/wasm_source/src/tx_init_proposal.rs @@ -8,22 +8,26 @@ fn apply_tx(ctx: &mut Ctx, tx: Tx) -> TxResult { let tx_data = transaction::governance::InitProposalData::try_from_slice(&data[..]) .wrap_err("failed to decode InitProposalData")?; + // Get the content from the referred to section let content = tx .get_section(&tx_data.content) .ok_or_err_msg("Missing proposal content")? .extra_data() .ok_or_err_msg("Missing full proposal content")?; + // Get the code from the referred to section - let code = match tx_data.r#type { - transaction::governance::ProposalType::Default(Some(hash)) => Some( + let code_hash = tx_data.get_section_code_hash(); + let code = match code_hash { + Some(hash) => Some( tx.get_section(&hash) .ok_or_err_msg("Missing proposal code")? .extra_data() .ok_or_err_msg("Missing full proposal code")?, ), - _ => None, + None => None, }; + log_string("apply_tx called to create a new governance proposal"); governance::init_proposal(ctx, tx_data, content, code) diff --git a/wasm/wasm_source/src/tx_init_validator.rs b/wasm/wasm_source/src/tx_init_validator.rs index 0cd37da1111..eb80ec444ec 100644 --- a/wasm/wasm_source/src/tx_init_validator.rs +++ b/wasm/wasm_source/src/tx_init_validator.rs @@ -1,7 +1,7 @@ //! A tx to initialize a new validator account with a given public keys and a //! validity predicates. -use namada_tx_prelude::transaction::InitValidator; +use namada_tx_prelude::transaction::pos::InitValidator; use namada_tx_prelude::*; #[transaction] diff --git a/wasm/wasm_source/src/tx_unbond.rs b/wasm/wasm_source/src/tx_unbond.rs index e453d48b149..6dd7d76a501 100644 --- a/wasm/wasm_source/src/tx_unbond.rs +++ b/wasm/wasm_source/src/tx_unbond.rs @@ -23,15 +23,15 @@ mod tests { bond_handle, read_consensus_validator_set_addresses_with_stake, read_total_stake, read_validator_stake, unbond_handle, }; - use namada::proto::{Code, Data, Signature, Tx}; use namada::types::dec::Dec; use namada::types::storage::Epoch; - use namada::types::transaction::TxType; + use namada::types::tx::TxBuilder; use namada_tests::log::test; use namada_tests::native_vp::pos::init_pos; use namada_tests::native_vp::TestNativeVpEnv; use namada_tests::tx::*; use namada_tx_prelude::address::InternalAddress; + use namada_tx_prelude::chain::ChainId; use namada_tx_prelude::key::testing::arb_common_keypair; use namada_tx_prelude::key::RefTo; use namada_tx_prelude::proof_of_stake::parameters::testing::arb_pos_params; @@ -127,14 +127,13 @@ mod tests { let tx_code = vec![]; let tx_data = unbond.try_to_vec().unwrap(); - let mut tx = Tx::new(TxType::Raw); - tx.set_data(Data::new(tx_data)); - tx.set_code(Code::new(tx_code)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.data_sechash(), *tx.code_sechash()], - &key, - ))); - let signed_tx = tx.clone(); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(key) + .build(); + let signed_tx = tx; let unbond_src = unbond .source diff --git a/wasm/wasm_source/src/tx_update_account.rs b/wasm/wasm_source/src/tx_update_account.rs new file mode 100644 index 00000000000..c2553759e5d --- /dev/null +++ b/wasm/wasm_source/src/tx_update_account.rs @@ -0,0 +1,45 @@ +//! A tx for updating an account's validity predicate. +//! This tx wraps the validity predicate inside `SignedTxData` as +//! its input as declared in `shared` crate. + +use namada_tx_prelude::key::pks_handle; +use namada_tx_prelude::*; + +#[transaction] +fn apply_tx(ctx: &mut Ctx, tx: Tx) -> TxResult { + let signed = tx; + let data = signed.data().ok_or_err_msg("Missing data")?; + let tx_data = + transaction::account::UpdateAccount::try_from_slice(&data[..]) + .wrap_err("failed to decode UpdateAccount")?; + + let owner = &tx_data.addr; + debug_log!("update VP for: {:#?}", tx_data.addr); + + if let Some(hash) = tx_data.vp_code_hash { + let vp_code_hash = signed + .get_section(&hash) + .ok_or_err_msg("vp code section not found")? + .extra_data_sec() + .ok_or_err_msg("vp code section must be tagged as extra")? + .code + .hash(); + + ctx.update_validity_predicate(owner, vp_code_hash)?; + } + + if let Some(threshold) = tx_data.threshold { + let threshold_key = key::threshold_key(owner); + ctx.write(&threshold_key, threshold)?; + } + + if !tx_data.public_keys.is_empty() { + storage_api::account::clear_public_keys(ctx, owner)?; + for (index, public_key) in tx_data.public_keys.iter().enumerate() { + let index = index as u8; + pks_handle(owner).insert(ctx, index, public_key.clone())?; + } + } + + Ok(()) +} diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index 20b202bdb62..b0046c2cc33 100644 --- a/wasm/wasm_source/src/tx_withdraw.rs +++ b/wasm/wasm_source/src/tx_withdraw.rs @@ -22,10 +22,9 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { mod tests { use namada::ledger::pos::{GenesisValidator, PosParams, PosVP}; use namada::proof_of_stake::unbond_handle; - use namada::proto::{Code, Data, Signature, Tx}; use namada::types::dec::Dec; use namada::types::storage::Epoch; - use namada::types::transaction::TxType; + use namada::types::tx::TxBuilder; use namada_tests::log::test; use namada_tests::native_vp::pos::init_pos; use namada_tests::native_vp::TestNativeVpEnv; @@ -34,6 +33,7 @@ mod tests { arb_established_address, arb_non_internal_address, }; use namada_tx_prelude::address::InternalAddress; + use namada_tx_prelude::chain::ChainId; use namada_tx_prelude::key::testing::arb_common_keypair; use namada_tx_prelude::key::RefTo; use namada_tx_prelude::proof_of_stake::parameters::testing::arb_pos_params; @@ -171,14 +171,13 @@ mod tests { let tx_code = vec![]; let tx_data = withdraw.try_to_vec().unwrap(); - let mut tx = Tx::new(TxType::Raw); - tx.set_code(Code::new(tx_code)); - tx.set_data(Data::new(tx_data)); - tx.add_section(Section::Signature(Signature::new( - vec![*tx.data_sechash(), *tx.code_sechash()], - &key, - ))); - let signed_tx = tx.clone(); + let tx_builder = TxBuilder::new(ChainId::default(), None); + let tx = tx_builder + .add_code(tx_code) + .add_serialized_data(tx_data) + .add_gas_payer(key) + .build(); + let signed_tx = tx; // Read data before we apply tx: let pos_balance_key = token::balance_key( diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index f82c573bc1f..500e53ad031 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -28,14 +28,14 @@ enum KeyType<'a> { impl<'a> From<&'a storage::Key> for KeyType<'a> { fn from(key: &'a storage::Key) -> KeyType<'a> { - if let Some(address) = key::is_pk_key(key) { + if let Some(address) = key::is_pks_key(key) { Self::Pk(address) } else if let Some([_, owner]) = token::is_any_token_balance_key(key) { Self::Token { owner } } else if proof_of_stake::is_pos_key(key) { Self::PoS - } else if gov_storage::is_vote_key(key) { - let voter_address = gov_storage::get_voter_address(key); + } else if gov_storage::keys::is_vote_key(key) { + let voter_address = gov_storage::keys::get_voter_address(key); if let Some(address) = voter_address { Self::GovernanceVote(address) } else { @@ -62,18 +62,8 @@ fn validate_tx( verifiers ); - let valid_sig = Lazy::new(|| { - let pk = key::get(ctx, &addr); - match pk { - Ok(Some(pk)) => tx_data - .verify_signature( - &pk, - &[*tx_data.data_sechash(), *tx_data.code_sechash()], - ) - .is_ok(), - _ => false, - } - }); + let valid_sig = + Lazy::new(|| verify_signatures(ctx, &tx_data, &addr).is_ok()); if !is_valid_tx(ctx, &tx_data)? { return reject(); @@ -196,7 +186,7 @@ fn validate_tx( mod tests { // Use this as `#[test]` annotation to enable logging use namada::ledger::pos::{GenesisValidator, PosParams}; - use namada::proto::{Code, Data, Signature}; + use namada::proto::{Code, Data, MultiSignature}; use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada::types::transaction::TxType; @@ -207,6 +197,7 @@ mod tests { use namada_tests::vp::vp_host_env::storage::Key; use namada_tests::vp::*; use namada_tx_prelude::{storage_api, StorageWrite, TxEnv}; + use namada_vp_prelude::account::AccountPublicKeysMap; use namada_vp_prelude::key::RefTo; use proptest::prelude::*; use storage::testing::arb_account_storage_key_no_vp; @@ -240,7 +231,8 @@ mod tests { let addr: Address = (&public_key).into(); // Initialize a tx environment - let tx_env = TestTxEnv::default(); + let mut tx_env = TestTxEnv::default(); + tx_env.init_parameters(None, None, None, None); // Initialize VP environment from a transaction vp_host_env::init_from_tx(addr.clone(), tx_env, |_address| { @@ -308,8 +300,12 @@ mod tests { // Initialize VP environment from a transaction vp_host_env::init_from_tx(addr.clone(), tx_env, |_address| { // Do the same as reveal_pk, but with the wrong key - let key = namada_tx_prelude::key::pk_key(&addr); - tx_host_env::ctx().write(&key, &mismatched_pk).unwrap(); + let _ = storage_api::account::set_public_key_at( + tx_host_env::ctx(), + &addr, + &mismatched_pk, + 0, + ); }); let vp_env = vp_host_env::take(); @@ -414,6 +410,8 @@ mod tests { // Initialize a tx environment let mut tx_env = tx_host_env::take(); + tx_env.init_parameters(None, Some(vec![]), Some(vec![]), None); + let secret_key = key::testing::keypair_1(); let public_key = secret_key.ref_to(); let vp_owner: Address = (&public_key).into(); @@ -425,6 +423,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); + tx_env.init_account_storage(&vp_owner, vec![public_key], 1); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -461,8 +460,8 @@ mod tests { ); } - /// Test that a PoS action that must be authorized is accepted with a valid - /// signature. + /// Test that a PoS action that must be authorized is accepted with a + /// valid signature. #[test] fn test_signed_pos_action_accepted() { // Init PoS genesis @@ -505,6 +504,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -517,8 +517,6 @@ mod tests { ) .unwrap(); - tx_env.write_public_key(&vp_owner, &public_key); - // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { // Bond the tokens, then unbond some of them @@ -530,14 +528,18 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &secret_key, + &[secret_key], + &pks_map, ))); + let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); let keys_changed: BTreeSet = @@ -565,6 +567,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); + tx_env.init_account_storage(&vp_owner, vec![public_key], 1); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -624,8 +627,11 @@ mod tests { let token = address::nam(); let amount = token::Amount::from_uint(10_098_123, 0).unwrap(); + tx_env.init_parameters(None, None, None, None); + // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -638,8 +644,6 @@ mod tests { ) .unwrap(); - tx_env.write_public_key(&vp_owner, &public_key); - let amount = token::DenominatedAmount { amount, denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), @@ -660,20 +664,25 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &secret_key, + &[secret_key], + &pks_map, ))); + let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); let keys_changed: BTreeSet = vp_env.all_touched_storage_keys(); let verifiers: BTreeSet
= BTreeSet::default(); vp_host_env::set(vp_env); + assert!( validate_tx(&CTX, signed_tx, vp_owner, keys_changed, verifiers) .unwrap() @@ -696,6 +705,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &source, &target, &token]); + tx_env.init_account_storage(&vp_owner, vec![public_key], 1); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -760,8 +770,8 @@ mod tests { } proptest! { - /// Test that an unsigned tx that performs arbitrary storage writes or - /// deletes to the account is rejected. + /// Test that an unsigned tx that performs arbitrary storage writes + /// or deletes to the account is rejected. #[test] fn test_unsigned_arb_storage_write_rejected( (_sk, vp_owner, storage_key) in arb_account_storage_subspace_key(), @@ -795,17 +805,12 @@ mod tests { vp_host_env::set(vp_env); assert!(!validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers).unwrap()); } - } - proptest! { - /// Test that a signed tx that performs arbitrary storage writes or - /// deletes to the account is accepted. - #[test] - fn test_signed_arb_storage_write( - (secret_key, vp_owner, storage_key) in arb_account_storage_subspace_key(), - // Generate bytes to write. If `None`, delete from the key instead - storage_value in any::>>(), - ) { + fn test_signed_arb_storage_write( + (secret_key, vp_owner, storage_key) in arb_account_storage_subspace_key(), + // Generate bytes to write. If `None`, delete from the key instead + storage_value in any::>>(), + ) { // Initialize a tx environment let mut tx_env = TestTxEnv::default(); @@ -815,7 +820,7 @@ mod tests { tx_env.spawn_accounts(storage_key_addresses); let public_key = secret_key.ref_to(); - tx_env.write_public_key(&vp_owner, &public_key); + let _ = storage_api::account::set_public_key_at(tx_host_env::ctx(), &vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -827,11 +832,17 @@ mod tests { } }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new(vec![*tx.data_sechash(), *tx.code_sechash()], &secret_key))); + tx.add_section(Section::SectionSignature(MultiSignature::new( + vec![*tx.data_sechash(), *tx.code_sechash()], + &[secret_key], + &pks_map, + ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); let keys_changed: BTreeSet = @@ -899,12 +910,12 @@ mod tests { None, Some(vec![vp_hash.to_string()]), Some(vec!["some_hash".to_string()]), + None, ); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -914,13 +925,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &secret_key, + &[secret_key], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -947,13 +961,16 @@ mod tests { // for the update tx_env.store_wasm_code(vp_code); - let empty_sha256 = sha256(&[]).to_string(); - tx_env.init_parameters(None, None, Some(vec![empty_sha256])); + tx_env.init_parameters( + None, + Some(vec![vp_hash.to_string()]), + None, + None, + ); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -963,13 +980,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_code(Code::new(vec![])); tx.set_data(Data::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &secret_key, + &[secret_key], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index 5905c3cc4cf..8436bf9707e 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -25,18 +25,8 @@ fn validate_tx( verifiers ); - let valid_sig = Lazy::new(|| { - let pk = key::get(ctx, &addr); - match pk { - Ok(Some(pk)) => tx_data - .verify_signature( - &pk, - &[*tx_data.data_sechash(), *tx_data.code_sechash()], - ) - .is_ok(), - _ => false, - } - }); + let valid_sig = + Lazy::new(|| verify_signatures(ctx, &tx_data, &addr).is_ok()); if !is_valid_tx(ctx, &tx_data)? { return reject(); @@ -117,7 +107,7 @@ fn validate_tx( #[cfg(test)] mod tests { use address::testing::arb_non_internal_address; - use namada::proto::{Code, Data, Signature}; + use namada::proto::{Code, Data, MultiSignature, Signature}; use namada::types::transaction::TxType; use namada_test_utils::TestWasms; // Use this as `#[test]` annotation to enable logging @@ -126,6 +116,7 @@ mod tests { use namada_tests::vp::vp_host_env::storage::Key; use namada_tests::vp::*; use namada_tx_prelude::{StorageWrite, TxEnv}; + use namada_vp_prelude::account::AccountPublicKeysMap; use namada_vp_prelude::key::RefTo; use proptest::prelude::*; use storage::testing::arb_account_storage_key_no_vp; @@ -258,8 +249,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -269,13 +259,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key.clone()]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -365,6 +358,7 @@ mod tests { let target = address::testing::established_address_2(); let target_key = key::testing::keypair_1(); + let _public_key = target_key.ref_to(); let token = address::nam(); let amount = token::Amount::from_uint(amount, 0).unwrap(); @@ -436,7 +430,7 @@ mod tests { let storage_key_addresses = storage_key.find_addresses(); tx_env.spawn_accounts(storage_key_addresses); - tx_env.write_public_key(&vp_owner, public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -448,11 +442,17 @@ mod tests { } }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key.clone()]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new(vec![*tx.data_sechash(), *tx.code_sechash()], &keypair))); + tx.add_section(Section::SectionSignature(MultiSignature::new( + vec![*tx.data_sechash(), *tx.code_sechash()], + &[keypair], + &pks_map, + ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); let keys_changed: BTreeSet = diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index 6d6977997e4..a608bbb9fec 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -28,8 +28,8 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { Self::Token { owner } } else if proof_of_stake::is_pos_key(key) { Self::PoS - } else if gov_storage::is_vote_key(key) { - let voter_address = gov_storage::get_voter_address(key); + } else if gov_storage::keys::is_vote_key(key) { + let voter_address = gov_storage::keys::get_voter_address(key); if let Some(address) = voter_address { Self::GovernanceVote(address) } else { @@ -60,18 +60,8 @@ fn validate_tx( verifiers ); - let valid_sig = Lazy::new(|| { - let pk = key::get(ctx, &addr); - match pk { - Ok(Some(pk)) => tx_data - .verify_signature( - &pk, - &[*tx_data.data_sechash(), *tx_data.code_sechash()], - ) - .is_ok(), - _ => false, - } - }); + let valid_sig = + Lazy::new(|| verify_signatures(ctx, &tx_data, &addr).is_ok()); if !is_valid_tx(ctx, &tx_data)? { return reject(); @@ -184,7 +174,7 @@ fn validate_tx( mod tests { use address::testing::arb_non_internal_address; use namada::ledger::pos::{GenesisValidator, PosParams}; - use namada::proto::{Code, Data, Signature}; + use namada::proto::{Code, Data, MultiSignature}; use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada::types::transaction::TxType; @@ -196,6 +186,7 @@ mod tests { use namada_tests::vp::vp_host_env::storage::Key; use namada_tests::vp::*; use namada_tx_prelude::{StorageWrite, TxEnv}; + use namada_vp_prelude::account::AccountPublicKeysMap; use namada_vp_prelude::key::RefTo; use proptest::prelude::*; use storage::testing::arb_account_storage_key_no_vp; @@ -350,6 +341,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -362,8 +354,6 @@ mod tests { ) .unwrap(); - tx_env.write_public_key(&vp_owner, &public_key); - let amount = token::DenominatedAmount { amount, denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), @@ -385,13 +375,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -435,7 +428,7 @@ mod tests { let mut tx_env = tx_host_env::take(); let secret_key = key::testing::keypair_1(); - let _public_key = secret_key.ref_to(); + let public_key = secret_key.ref_to(); let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); @@ -445,6 +438,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); + tx_env.init_account_storage(&vp_owner, vec![public_key], 1); // write the denomination of NAM into storage storage_api::token::write_denom( &mut tx_env.wl_storage, @@ -525,6 +519,8 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); + // write the denomination of NAM into storage storage_api::token::write_denom( &mut tx_env.wl_storage, @@ -537,8 +533,6 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, amount); - tx_env.write_public_key(&vp_owner, &public_key); - // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { // Bond the tokens, then unbond some of them @@ -550,13 +544,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &secret_key, + &[secret_key], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -697,8 +694,7 @@ mod tests { // their storage let storage_key_addresses = storage_key.find_addresses(); tx_env.spawn_accounts(storage_key_addresses); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -710,11 +706,13 @@ mod tests { } }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_code(Code::new(vec![])); tx.set_data(Data::new(vec![])); - tx.add_section(Section::Signature(Signature::new(vec![*tx.data_sechash(), *tx.code_sechash()], &keypair))); + tx.add_section(Section::SectionSignature(MultiSignature::new(vec![*tx.data_sechash(), *tx.code_sechash()], &[keypair], &pks_map))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); let keys_changed: BTreeSet = @@ -769,7 +767,7 @@ mod tests { fn test_signed_vp_update_accepted() { // Initialize a tx environment let mut tx_env = TestTxEnv::default(); - tx_env.init_parameters(None, None, None); + tx_env.init_parameters(None, None, None, None); let vp_owner = address::testing::established_address_1(); let keypair = key::testing::keypair_1(); @@ -781,8 +779,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -792,13 +789,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -817,7 +817,12 @@ mod tests { fn test_signed_vp_update_not_whitelisted_rejected() { // Initialize a tx environment let mut tx_env = TestTxEnv::default(); - tx_env.init_parameters(None, Some(vec!["some_hash".to_string()]), None); + tx_env.init_parameters( + None, + Some(vec!["some_hash".to_string()]), + None, + None, + ); let vp_owner = address::testing::established_address_1(); let keypair = key::testing::keypair_1(); @@ -829,8 +834,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -840,13 +844,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -874,12 +881,16 @@ mod tests { // for the update tx_env.store_wasm_code(vp_code); - tx_env.init_parameters(None, Some(vec![vp_hash.to_string()]), None); + tx_env.init_parameters( + None, + Some(vec![vp_hash.to_string()]), + None, + None, + ); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -889,13 +900,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -927,12 +941,12 @@ mod tests { None, Some(vec![vp_hash.to_string()]), Some(vec!["some_hash".to_string()]), + None, ); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -942,13 +956,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -975,13 +992,17 @@ mod tests { // for the update tx_env.store_wasm_code(vp_code); - let empty_sha256 = sha256(&[]).to_string(); - tx_env.init_parameters(None, None, Some(vec![empty_sha256])); + // hardcoded hash of VP_ALWAYS_TRUE_WASM + tx_env.init_parameters( + None, + Some(vec![vp_hash.to_string()]), + None, + None, + ); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -991,13 +1012,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_code(Code::new(vec![])); tx.set_data(Data::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index eb80929626e..5d30b00be78 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -30,8 +30,8 @@ impl<'a> From<&'a storage::Key> for KeyType<'a> { Self::Token { owner } } else if proof_of_stake::is_pos_key(key) { Self::PoS - } else if gov_storage::is_vote_key(key) { - let voter_address = gov_storage::get_voter_address(key); + } else if gov_storage::keys::is_vote_key(key) { + let voter_address = gov_storage::keys::get_voter_address(key); if let Some(address) = voter_address { Self::GovernanceVote(address) } else { @@ -60,18 +60,8 @@ fn validate_tx( verifiers ); - let valid_sig = Lazy::new(|| { - let pk = key::get(ctx, &addr); - match pk { - Ok(Some(pk)) => tx_data - .verify_signature( - &pk, - &[*tx_data.data_sechash(), *tx_data.code_sechash()], - ) - .is_ok(), - _ => false, - } - }); + let valid_sig = + Lazy::new(|| verify_signatures(ctx, &tx_data, &addr).is_ok()); if !is_valid_tx(ctx, &tx_data)? { return reject(); @@ -191,7 +181,7 @@ fn validate_tx( mod tests { use address::testing::arb_non_internal_address; use namada::ledger::pos::{GenesisValidator, PosParams}; - use namada::proto::{Code, Data, Signature}; + use namada::proto::{Code, Data, MultiSignature}; use namada::types::dec::Dec; use namada::types::storage::Epoch; use namada::types::transaction::TxType; @@ -203,6 +193,7 @@ mod tests { use namada_tests::vp::vp_host_env::storage::Key; use namada_tests::vp::*; use namada_tx_prelude::{StorageWrite, TxEnv}; + use namada_vp_prelude::account::AccountPublicKeysMap; use namada_vp_prelude::key::RefTo; use proptest::prelude::*; use storage::testing::arb_account_storage_key_no_vp; @@ -357,6 +348,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner, &target, &token]); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -369,7 +361,6 @@ mod tests { ) .unwrap(); - tx_env.write_public_key(&vp_owner, &public_key); let amount = token::DenominatedAmount { amount, denom: token::NATIVE_MAX_DECIMAL_PLACES.into(), @@ -391,13 +382,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -441,7 +435,7 @@ mod tests { let mut tx_env = tx_host_env::take(); let secret_key = key::testing::keypair_1(); - let _public_key = secret_key.ref_to(); + let public_key = secret_key.ref_to(); let vp_owner: Address = address::testing::established_address_2(); let target = address::testing::established_address_3(); let token = address::nam(); @@ -451,6 +445,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&target, &token]); + tx_env.init_account_storage(&vp_owner, vec![public_key], 1); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -536,7 +531,8 @@ mod tests { let unbond_amount = token::Amount::from_uint(3_098_123, 0).unwrap(); // Spawn the accounts to be able to modify their storage - tx_env.spawn_accounts([&target, &token]); + tx_env.spawn_accounts([&vp_owner, &target, &token]); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Credit the tokens to the VP owner before running the transaction to // be able to transfer from it @@ -549,8 +545,6 @@ mod tests { ) .unwrap(); - tx_env.write_public_key(&vp_owner, &public_key); - // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { // Bond the tokens, then unbond some of them @@ -568,13 +562,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &secret_key, + &[secret_key], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -715,7 +712,7 @@ mod tests { let storage_key_addresses = storage_key.find_addresses(); tx_env.spawn_accounts(storage_key_addresses); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -727,11 +724,13 @@ mod tests { } }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new(vec![*tx.data_sechash(), *tx.code_sechash()], &keypair))); + tx.add_section(Section::SectionSignature(MultiSignature::new(vec![*tx.data_sechash(), *tx.code_sechash()], &[keypair], &pks_map))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); let keys_changed: BTreeSet = @@ -785,7 +784,7 @@ mod tests { fn test_signed_vp_update_accepted() { // Initialize a tx environment let mut tx_env = TestTxEnv::default(); - tx_env.init_parameters(None, None, None); + tx_env.init_parameters(None, None, None, None); let vp_owner = address::testing::established_address_1(); let keypair = key::testing::keypair_1(); @@ -797,8 +796,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -808,13 +806,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -833,7 +834,12 @@ mod tests { fn test_signed_vp_update_not_whitelisted_rejected() { // Initialize a tx environment let mut tx_env = TestTxEnv::default(); - tx_env.init_parameters(None, Some(vec!["some_hash".to_string()]), None); + tx_env.init_parameters( + None, + Some(vec!["some_hash".to_string()]), + None, + None, + ); let vp_owner = address::testing::established_address_1(); let keypair = key::testing::keypair_1(); @@ -845,8 +851,7 @@ mod tests { // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -856,13 +861,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -890,12 +898,16 @@ mod tests { // for the update tx_env.store_wasm_code(vp_code); - tx_env.init_parameters(None, Some(vec![vp_hash.to_string()]), None); + tx_env.init_parameters( + None, + Some(vec![vp_hash.to_string()]), + None, + None, + ); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -905,13 +917,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -943,12 +958,12 @@ mod tests { None, Some(vec![vp_hash.to_string()]), Some(vec!["some_hash".to_string()]), + None, ); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -958,13 +973,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_data(Data::new(vec![])); tx.set_code(Code::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); @@ -991,13 +1009,17 @@ mod tests { // for the update tx_env.store_wasm_code(vp_code); - let empty_sha256 = sha256(&[]).to_string(); - tx_env.init_parameters(None, None, Some(vec![empty_sha256])); + // hardcoded hash of VP_ALWAYS_TRUE_WASM + tx_env.init_parameters( + None, + Some(vec![vp_hash.to_string()]), + None, + None, + ); // Spawn the accounts to be able to modify their storage tx_env.spawn_accounts([&vp_owner]); - - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.init_account_storage(&vp_owner, vec![public_key.clone()], 1); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -1007,13 +1029,16 @@ mod tests { .unwrap(); }); + let pks_map = AccountPublicKeysMap::from_iter(vec![public_key]); + let mut vp_env = vp_host_env::take(); let mut tx = vp_env.tx.clone(); tx.set_code(Code::new(vec![])); tx.set_data(Data::new(vec![])); - tx.add_section(Section::Signature(Signature::new( + tx.add_section(Section::SectionSignature(MultiSignature::new( vec![*tx.data_sechash(), *tx.code_sechash()], - &keypair, + &[keypair], + &pks_map, ))); let signed_tx = tx.clone(); vp_env.tx = signed_tx.clone(); diff --git a/wasm_for_tests/tx_memory_limit.wasm b/wasm_for_tests/tx_memory_limit.wasm index 676b16e3cea..f5075dd00b6 100755 Binary files a/wasm_for_tests/tx_memory_limit.wasm and b/wasm_for_tests/tx_memory_limit.wasm differ diff --git a/wasm_for_tests/tx_mint_tokens.wasm b/wasm_for_tests/tx_mint_tokens.wasm index f0c81b5862a..9b2329f215b 100755 Binary files a/wasm_for_tests/tx_mint_tokens.wasm and b/wasm_for_tests/tx_mint_tokens.wasm differ diff --git a/wasm_for_tests/tx_no_op.wasm b/wasm_for_tests/tx_no_op.wasm index 291de29eb23..60045a3028e 100755 Binary files a/wasm_for_tests/tx_no_op.wasm and b/wasm_for_tests/tx_no_op.wasm differ diff --git a/wasm_for_tests/tx_proposal_code.wasm b/wasm_for_tests/tx_proposal_code.wasm index 15221d0434c..d1ec35d376a 100755 Binary files a/wasm_for_tests/tx_proposal_code.wasm and b/wasm_for_tests/tx_proposal_code.wasm differ diff --git a/wasm_for_tests/tx_read_storage_key.wasm b/wasm_for_tests/tx_read_storage_key.wasm index 9189354e11b..11b54dd3c66 100755 Binary files a/wasm_for_tests/tx_read_storage_key.wasm and b/wasm_for_tests/tx_read_storage_key.wasm differ diff --git a/wasm_for_tests/tx_write.wasm b/wasm_for_tests/tx_write.wasm index 1a86b771ea4..3df84833d83 100755 Binary files a/wasm_for_tests/tx_write.wasm and b/wasm_for_tests/tx_write.wasm differ diff --git a/wasm_for_tests/vp_always_false.wasm b/wasm_for_tests/vp_always_false.wasm index 215f5ada5a1..401caf97d5f 100755 Binary files a/wasm_for_tests/vp_always_false.wasm and b/wasm_for_tests/vp_always_false.wasm differ diff --git a/wasm_for_tests/vp_always_true.wasm b/wasm_for_tests/vp_always_true.wasm index 9c908a8af3e..6705db01e81 100755 Binary files a/wasm_for_tests/vp_always_true.wasm and b/wasm_for_tests/vp_always_true.wasm differ diff --git a/wasm_for_tests/vp_eval.wasm b/wasm_for_tests/vp_eval.wasm index ab3184377d9..4108513f48c 100755 Binary files a/wasm_for_tests/vp_eval.wasm and b/wasm_for_tests/vp_eval.wasm differ diff --git a/wasm_for_tests/vp_memory_limit.wasm b/wasm_for_tests/vp_memory_limit.wasm index e8ed1c9d5cc..ee7e08c1ab7 100755 Binary files a/wasm_for_tests/vp_memory_limit.wasm and b/wasm_for_tests/vp_memory_limit.wasm differ diff --git a/wasm_for_tests/vp_read_storage_key.wasm b/wasm_for_tests/vp_read_storage_key.wasm index a24d8e841dc..c2a868a706e 100755 Binary files a/wasm_for_tests/vp_read_storage_key.wasm and b/wasm_for_tests/vp_read_storage_key.wasm differ diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index c145c9062fa..0780cfede0d 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -3890,6 +3890,7 @@ dependencies = [ "num-traits", "prost", "regex", + "serde", "serde_json", "sha2 0.9.9", "tempfile", diff --git a/wasm_for_tests/wasm_source/Cargo.toml b/wasm_for_tests/wasm_source/Cargo.toml index 0d23efd8d0e..ac6fe1f981d 100644 --- a/wasm_for_tests/wasm_source/Cargo.toml +++ b/wasm_for_tests/wasm_source/Cargo.toml @@ -13,7 +13,6 @@ crate-type = ["cdylib"] # Newly added wasms should also be added into the Makefile `$(wasms)` list. [features] tx_memory_limit = [] -tx_mint_tokens = [] tx_no_op = [] tx_read_storage_key = [] tx_write = [] diff --git a/wasm_for_tests/wasm_source/Makefile b/wasm_for_tests/wasm_source/Makefile index ca46c0b569a..f56ab0afb8b 100644 --- a/wasm_for_tests/wasm_source/Makefile +++ b/wasm_for_tests/wasm_source/Makefile @@ -6,7 +6,6 @@ nightly := $(shell cat ../../rust-nightly-version) # All the wasms that can be built from this source, switched via Cargo features # Wasms can be added via the Cargo.toml `[features]` list. wasms := tx_memory_limit -wasms += tx_mint_tokens wasms += tx_no_op wasms += tx_read_storage_key wasms += tx_write diff --git a/wasm_for_tests/wasm_source/src/lib.rs b/wasm_for_tests/wasm_source/src/lib.rs index 561d43fc2ec..790ad1a72c8 100644 --- a/wasm_for_tests/wasm_source/src/lib.rs +++ b/wasm_for_tests/wasm_source/src/lib.rs @@ -34,7 +34,7 @@ pub mod main { #[transaction] fn apply_tx(ctx: &mut Ctx, _tx_data: Tx) -> TxResult { // governance - let target_key = gov_storage::get_min_proposal_grace_epoch_key(); + let target_key = gov_storage::keys::get_min_proposal_grace_epoch_key(); ctx.write(&target_key, 9_u64)?; // parameters @@ -127,30 +127,6 @@ pub mod main { } } -/// A tx that attempts to mint tokens in the transfer's target without debiting -/// the tokens from the source. This tx is expected to be rejected by the -/// token's VP. -#[cfg(feature = "tx_mint_tokens")] -pub mod main { - use namada_test_utils::tx_data::TxMintData; - use namada_tx_prelude::*; - - #[transaction] - fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { - let signed = tx_data; - let mint_data = - TxMintData::try_from_slice(&signed.data().unwrap()[..]).unwrap(); - log_string(format!("apply_tx called to mint tokens: {:#?}", mint_data)); - let TxMintData { - minter, - target, - token, - amount, - } = mint_data; - token::mint(ctx, &minter, &target, &token, amount) - } -} - /// A VP that always returns `true`. #[cfg(feature = "vp_always_true")] pub mod main {