diff --git a/.changelog/unreleased/features/592-implicit-vp.md b/.changelog/unreleased/features/592-implicit-vp.md new file mode 100644 index 0000000000..ab93e1fc0f --- /dev/null +++ b/.changelog/unreleased/features/592-implicit-vp.md @@ -0,0 +1,6 @@ +- Added a validity predicate for implicit accounts. This is set in + protocol parameters and may be changed via governance. Additionally, + added automatic public key reveal in the client that use an implicit + account that hasn't revealed its PK yet as a source. It's also + possible to manually submit reveal transaction with client command + ([#592](https://github.com/anoma/namada/pull/592)) \ No newline at end of file diff --git a/apps/src/bin/anoma-client/cli.rs b/apps/src/bin/anoma-client/cli.rs index b87cdb5c66..3df3668c38 100644 --- a/apps/src/bin/anoma-client/cli.rs +++ b/apps/src/bin/anoma-client/cli.rs @@ -39,6 +39,9 @@ pub async fn main() -> Result<()> { Sub::TxVoteProposal(TxVoteProposal(args)) => { tx::submit_vote_proposal(ctx, args).await; } + Sub::TxRevealPk(TxRevealPk(args)) => { + tx::submit_reveal_pk(ctx, args).await; + } Sub::Bond(Bond(args)) => { tx::submit_bond(ctx, args).await; } diff --git a/apps/src/bin/anoma/cli.rs b/apps/src/bin/anoma/cli.rs index ccde0c3618..f0bd3ee740 100644 --- a/apps/src/bin/anoma/cli.rs +++ b/apps/src/bin/anoma/cli.rs @@ -46,6 +46,7 @@ fn handle_command(cmd: cli::cmds::Anoma, raw_sub_cmd: String) -> Result<()> { | cli::cmds::Anoma::TxCustom(_) | cli::cmds::Anoma::TxTransfer(_) | cli::cmds::Anoma::TxUpdateVp(_) + | cli::cmds::Anoma::TxRevealPk(_) | cli::cmds::Anoma::TxInitNft(_) | cli::cmds::Anoma::TxMintNft(_) | cli::cmds::Anoma::TxInitProposal(_) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 8e4c7f78c9..6cdb72c7bc 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -51,6 +51,7 @@ pub mod cmds { TxMintNft(TxMintNft), TxInitProposal(TxInitProposal), TxVoteProposal(TxVoteProposal), + TxRevealPk(TxRevealPk), } impl Cmd for Anoma { @@ -66,6 +67,7 @@ pub mod cmds { .subcommand(TxMintNft::def()) .subcommand(TxInitProposal::def()) .subcommand(TxVoteProposal::def()) + .subcommand(TxRevealPk::def()) } fn parse(matches: &ArgMatches) -> Option { @@ -82,6 +84,7 @@ pub mod cmds { SubCmd::parse(matches).map(Self::TxInitProposal); let tx_vote_proposal = SubCmd::parse(matches).map(Self::TxVoteProposal); + let tx_reveal_pk = SubCmd::parse(matches).map(Self::TxRevealPk); node.or(client) .or(wallet) .or(ledger) @@ -92,6 +95,7 @@ pub mod cmds { .or(tx_nft_mint) .or(tx_init_proposal) .or(tx_vote_proposal) + .or(tx_reveal_pk) } } @@ -154,7 +158,7 @@ pub mod cmds { .subcommand(TxTransfer::def().display_order(1)) .subcommand(TxUpdateVp::def().display_order(1)) .subcommand(TxInitAccount::def().display_order(1)) - .subcommand(TxInitValidator::def().display_order(1)) + .subcommand(TxRevealPk::def().display_order(1)) // Nft transactions .subcommand(TxInitNft::def().display_order(1)) .subcommand(TxMintNft::def().display_order(1)) @@ -162,6 +166,7 @@ pub mod cmds { .subcommand(TxInitProposal::def().display_order(1)) .subcommand(TxVoteProposal::def().display_order(1)) // PoS transactions + .subcommand(TxInitValidator::def().display_order(2)) .subcommand(Bond::def().display_order(2)) .subcommand(Unbond::def().display_order(2)) .subcommand(Withdraw::def().display_order(2)) @@ -188,6 +193,7 @@ pub mod cmds { let tx_init_account = Self::parse_with_ctx(matches, TxInitAccount); let tx_init_validator = Self::parse_with_ctx(matches, TxInitValidator); + let tx_reveal_pk = Self::parse_with_ctx(matches, TxRevealPk); let tx_nft_create = Self::parse_with_ctx(matches, TxInitNft); let tx_nft_mint = Self::parse_with_ctx(matches, TxMintNft); let tx_init_proposal = @@ -215,11 +221,12 @@ pub mod cmds { .or(tx_transfer) .or(tx_update_vp) .or(tx_init_account) - .or(tx_init_validator) + .or(tx_reveal_pk) .or(tx_nft_create) .or(tx_nft_mint) .or(tx_init_proposal) .or(tx_vote_proposal) + .or(tx_init_validator) .or(bond) .or(unbond) .or(withdraw) @@ -279,6 +286,7 @@ pub mod cmds { TxMintNft(TxMintNft), TxInitProposal(TxInitProposal), TxVoteProposal(TxVoteProposal), + TxRevealPk(TxRevealPk), Bond(Bond), Unbond(Unbond), Withdraw(Withdraw), @@ -1122,6 +1130,36 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct TxRevealPk(pub args::RevealPk); + + impl SubCmd for TxRevealPk { + const CMD: &'static str = "reveal-pk"; + + fn parse(matches: &ArgMatches) -> Option + where + Self: Sized, + { + matches + .subcommand_matches(Self::CMD) + .map(|matches| TxRevealPk(args::RevealPk::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about( + "Submit a tx to reveal the public key an implicit \ + account. Typically, you don't have to do this manually \ + and the client will detect when a tx to reveal PK is \ + needed and submit it automatically. This will write the \ + PK into the account's storage so that it can be used for \ + signature verification on transactions authorized by \ + this account.", + ) + .add_args::() + } + } + #[derive(Clone, Debug)] pub enum Utils { JoinNetwork(JoinNetwork), @@ -1863,6 +1901,28 @@ pub mod args { } } + #[derive(Clone, Debug)] + pub struct RevealPk { + /// Common tx arguments + pub tx: Tx, + /// A public key to be revealed on-chain + pub public_key: WalletPublicKey, + } + + impl Args for RevealPk { + fn parse(matches: &ArgMatches) -> Self { + let tx = Tx::parse(matches); + let public_key = PUBLIC_KEY.parse(matches); + + Self { tx, public_key } + } + + fn def(app: App) -> App { + app.add_args::() + .arg(PUBLIC_KEY.def().about("A public key to reveal.")) + } + } + #[derive(Clone, Debug)] pub struct QueryProposal { /// Common query args diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index fb87151c82..963b556244 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -85,12 +85,25 @@ pub async fn sign_tx( ) -> (Context, TxBroadcastData) { let (tx, keypair) = if let Some(signing_key) = &args.signing_key { let signing_key = ctx.get_cached(signing_key); + + // Check if the signing key needs to reveal its PK first + let pk: common::PublicKey = signing_key.ref_to(); + super::tx::reveal_pk_if_needed(&mut ctx, &pk, args).await; + (tx.sign(&signing_key), signing_key) } else if let Some(signer) = args.signer.as_ref().or(default) { let signer = ctx.get(signer); let signing_key = find_keypair(&mut ctx.wallet, &signer, args.ledger_address.clone()) .await; + + // Check if the signer is implicit account that needs to reveal its PK + // first + if matches!(signer, Address::Implicit(_)) { + let pk: common::PublicKey = signing_key.ref_to(); + super::tx::reveal_pk_if_needed(&mut ctx, &pk, args).await; + } + (tx.sign(&signing_key), signing_key) } else { panic!( diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index e749a681c6..7d3ded6e37 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -47,6 +47,7 @@ const TX_INIT_ACCOUNT_WASM: &str = "tx_init_account.wasm"; const TX_INIT_VALIDATOR_WASM: &str = "tx_init_validator.wasm"; const TX_INIT_PROPOSAL: &str = "tx_init_proposal.wasm"; const TX_VOTE_PROPOSAL: &str = "tx_vote_proposal.wasm"; +const TX_REVEAL_PK: &str = "tx_reveal_pk.wasm"; const TX_UPDATE_VP_WASM: &str = "tx_update_vp.wasm"; const TX_TRANSFER_WASM: &str = "tx_transfer.wasm"; const TX_INIT_NFT: &str = "tx_init_nft.wasm"; @@ -817,6 +818,114 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { } } +pub async fn submit_reveal_pk(mut ctx: Context, args: args::RevealPk) { + let args::RevealPk { + tx: args, + public_key, + } = args; + let public_key = ctx.get_cached(&public_key); + if !reveal_pk_if_needed(&mut ctx, &public_key, &args).await { + let addr: Address = (&public_key).into(); + println!("PK for {addr} is already revealed, nothing to do."); + } +} + +pub async fn reveal_pk_if_needed( + ctx: &mut Context, + public_key: &common::PublicKey, + args: &args::Tx, +) -> bool { + let addr: Address = public_key.into(); + // Check if PK revealed + if args.force || !has_revealed_pk(&addr, args.ledger_address.clone()).await + { + // If not, submit it + submit_reveal_pk_aux(ctx, public_key, args).await; + true + } else { + false + } +} + +pub async fn has_revealed_pk( + addr: &Address, + ledger_address: TendermintAddress, +) -> bool { + rpc::get_public_key(addr, ledger_address).await.is_some() +} + +pub async fn submit_reveal_pk_aux( + ctx: &mut Context, + public_key: &common::PublicKey, + args: &args::Tx, +) { + 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() + .expect("Encoding a public key shouldn't fail"); + let tx_code = ctx.read_wasm(TX_REVEAL_PK); + let tx = Tx::new(tx_code, Some(tx_data)); + + // submit_tx without signing the inner tx + let keypair = if let Some(signing_key) = &args.signing_key { + ctx.get_cached(signing_key) + } else if let Some(signer) = args.signer.as_ref() { + let signer = ctx.get(signer); + find_keypair(&mut ctx.wallet, &signer, args.ledger_address.clone()) + .await + } else { + find_keypair(&mut ctx.wallet, &addr, args.ledger_address.clone()).await + }; + let epoch = rpc::query_epoch(args::Query { + ledger_address: args.ledger_address.clone(), + }) + .await; + let to_broadcast = if args.dry_run { + TxBroadcastData::DryRun(tx) + } else { + super::signing::sign_wrapper(ctx, args, epoch, tx, &keypair).await + }; + + if args.dry_run { + if let TxBroadcastData::DryRun(tx) = to_broadcast { + rpc::dry_run_tx(&args.ledger_address, tx.to_bytes()).await; + } else { + panic!( + "Expected a dry-run transaction, received a wrapper \ + transaction instead" + ); + } + } else { + // Either broadcast or submit transaction and collect result into + // sum type + let result = if args.broadcast_only { + Left(broadcast_tx(args.ledger_address.clone(), &to_broadcast).await) + } else { + Right(submit_tx(args.ledger_address.clone(), to_broadcast).await) + }; + // Return result based on executed operation, otherwise deal with + // the encountered errors uniformly + match result { + Right(Err(err)) => { + eprintln!( + "Encountered error while broadcasting transaction: {}", + err + ); + safe_exit(1) + } + Left(Err(err)) => { + eprintln!( + "Encountered error while broadcasting transaction: {}", + err + ); + safe_exit(1) + } + _ => {} + } + } +} + /// 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. diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 9425e3b019..90ddd57f06 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -7,14 +7,14 @@ use std::path::Path; use borsh::{BorshDeserialize, BorshSerialize}; use derivative::Derivative; use namada::ledger::governance::parameters::GovParams; -use namada::ledger::parameters::Parameters; +use namada::ledger::parameters::EpochDuration; use namada::ledger::pos::{GenesisValidator, PosParams}; use namada::types::address::Address; #[cfg(not(feature = "dev"))] use namada::types::chain::ChainId; use namada::types::key::dkg_session_keys::DkgPublicKey; use namada::types::key::*; -use namada::types::time::DateTimeUtc; +use namada::types::time::{DateTimeUtc, DurationSecs}; use namada::types::{storage, token}; /// Genesis configuration file format @@ -28,7 +28,7 @@ pub mod genesis_config { use data_encoding::HEXLOWER; use eyre::Context; use namada::ledger::governance::parameters::GovParams; - use namada::ledger::parameters::{EpochDuration, Parameters}; + use namada::ledger::parameters::EpochDuration; use namada::ledger::pos::types::BasisPoints; use namada::ledger::pos::{GenesisValidator, PosParams}; use namada::types::address::Address; @@ -40,7 +40,8 @@ pub mod genesis_config { use thiserror::Error; use super::{ - EstablishedAccount, Genesis, ImplicitAccount, TokenAccount, Validator, + EstablishedAccount, Genesis, ImplicitAccount, Parameters, TokenAccount, + Validator, }; use crate::cli; @@ -233,6 +234,8 @@ pub mod genesis_config { // Hashes of whitelisted txs array. `None` value or an empty array // disables whitelisting. pub tx_whitelist: Option>, + /// Filename of implicit accounts validity predicate WASM code + pub implicit_vp: String, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -512,6 +515,18 @@ pub mod genesis_config { }) .collect(); + let implicit_vp_config = + wasms.get(&config.parameters.implicit_vp).unwrap(); + let implicit_vp_code_path = implicit_vp_config.filename.to_owned(); + let implicit_vp_sha256 = implicit_vp_config + .sha256 + .clone() + .unwrap_or_else(|| { + eprintln!("Unknown implicit VP WASM sha256"); + cli::safe_exit(1); + }) + .to_sha256_bytes() + .unwrap(); let parameters = Parameters { epoch_duration: EpochDuration { min_num_of_blocks: config.parameters.min_num_of_blocks, @@ -527,6 +542,8 @@ pub mod genesis_config { .into(), vp_whitelist: config.parameters.vp_whitelist.unwrap_or_default(), tx_whitelist: config.parameters.tx_whitelist.unwrap_or_default(), + implicit_vp_code_path, + implicit_vp_sha256, }; let gov_params = GovParams { @@ -714,6 +731,36 @@ pub struct ImplicitAccount { pub public_key: common::PublicKey, } +/// Protocol parameters. This is almost the same as +/// `ledger::parameters::Parameters`, but instead of having the `implicit_vp` +/// WASM code bytes, it only has the name and sha as the actual code is loaded +/// on `init_chain` +#[derive( + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BorshSerialize, + BorshDeserialize, +)] +pub struct Parameters { + /// Epoch duration + pub epoch_duration: EpochDuration, + /// Maximum expected time per block + pub max_expected_time_per_block: DurationSecs, + /// Whitelisted validity predicate hashes + pub vp_whitelist: Vec, + /// Whitelisted tx hashes + pub tx_whitelist: Vec, + /// Implicit accounts validity predicate code WASM + pub implicit_vp_code_path: String, + /// Expected SHA-256 hash of the implicit VP + pub implicit_vp_sha256: [u8; 32], +} + #[cfg(not(feature = "dev"))] pub fn genesis(base_dir: impl AsRef, chain_id: &ChainId) -> Genesis { let path = base_dir @@ -723,11 +770,11 @@ pub fn genesis(base_dir: impl AsRef, chain_id: &ChainId) -> Genesis { } #[cfg(feature = "dev")] pub fn genesis() -> Genesis { - use namada::ledger::parameters::EpochDuration; use namada::types::address; use crate::wallet; + let vp_implicit_path = "vp_implicit.wasm"; let vp_token_path = "vp_token.wasm"; let vp_user_path = "vp_user.wasm"; @@ -772,6 +819,8 @@ pub fn genesis() -> Genesis { max_expected_time_per_block: namada::types::time::DurationSecs(30), vp_whitelist: vec![], tx_whitelist: vec![], + implicit_vp_code_path: vp_implicit_path.into(), + implicit_vp_sha256: Default::default(), }; let albert = EstablishedAccount { address: wallet::defaults::albert_address(), diff --git a/apps/src/lib/node/ledger/protocol/mod.rs b/apps/src/lib/node/ledger/protocol/mod.rs index ed776fe21a..13377c5dca 100644 --- a/apps/src/lib/node/ledger/protocol/mod.rs +++ b/apps/src/lib/node/ledger/protocol/mod.rs @@ -217,12 +217,10 @@ where { verifiers .par_iter() - // TODO temporary pending on - .filter(|addr| !matches!(addr, Address::Implicit(_))) .try_fold(VpsResult::default, |mut result, addr| { let mut gas_meter = VpGasMeter::new(initial_gas); let accept = match &addr { - Address::Established(_) => { + Address::Implicit(_) | Address::Established(_) => { let (vp, gas) = storage .validity_predicate(addr) .map_err(Error::StorageError)?; @@ -359,8 +357,6 @@ where accepted } - // TODO temporary pending on - Address::Implicit(_) => unreachable!(), }; // Returning error from here will short-circuit the VP parallel diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 8cb6842c50..83f6308da1 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::hash::Hash; +use namada::ledger::parameters::Parameters; use namada::types::key::*; #[cfg(not(feature = "dev"))] use sha2::{Digest, Sha256}; @@ -57,16 +58,48 @@ where let genesis_time: DateTimeUtc = (Utc.timestamp(ts.seconds, ts.nanos as u32)).into(); - genesis.parameters.init_storage(&mut self.storage); + // Initialize protocol parameters + let genesis::Parameters { + epoch_duration, + max_expected_time_per_block, + vp_whitelist, + tx_whitelist, + implicit_vp_code_path, + implicit_vp_sha256, + } = genesis.parameters; + let implicit_vp = + wasm_loader::read_wasm(&self.wasm_dir, &implicit_vp_code_path) + .map_err(Error::ReadingWasm)?; + // In dev, we don't check the hash + #[cfg(feature = "dev")] + let _ = implicit_vp_sha256; + #[cfg(not(feature = "dev"))] + { + let mut hasher = Sha256::new(); + hasher.update(&implicit_vp); + let vp_code_hash = hasher.finalize(); + assert_eq!( + vp_code_hash.as_slice(), + &implicit_vp_sha256, + "Invalid implicit account's VP sha256 hash for {}", + implicit_vp_code_path + ); + } + let parameters = Parameters { + epoch_duration, + max_expected_time_per_block, + vp_whitelist, + tx_whitelist, + implicit_vp, + }; + parameters.init_storage(&mut self.storage); + + // Initialize governance parameters genesis.gov_params.init_storage(&mut self.storage); // Depends on parameters being initialized self.storage - .init_genesis_epoch( - initial_height, - genesis_time, - &genesis.parameters, - ) + .init_genesis_epoch(initial_height, genesis_time, ¶meters) .expect("Initializing genesis epoch must not fail"); // Loaded VP code cache to avoid loading the same files multiple times diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index 0e3a6d3fc8..54afa8cd4c 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -113,22 +113,21 @@ vp = "vp_user" # 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" # Token VP [wasm.vp_token] filename = "vp_token.wasm" -sha256 = "e428a11f570d21dd3c871f5d35de6fe18098eb8ee0456b3e11a72ccdd8685cd0" # Faucet VP [wasm.vp_testnet_faucet] filename = "vp_testnet_faucet.wasm" -sha256 = "2038d93afd456a77c45123811b671627f488c8d2a72b714d82dd494cbbd552bc" # General protocol parameters. [parameters] @@ -142,6 +141,8 @@ max_expected_time_per_block = 30 vp_whitelist = [] # tx whitelist tx_whitelist = [] +# Implicit VP WASM name +implicit_vp = "vp_implicit" # Proof of stake parameters. [pos_params] diff --git a/shared/src/ledger/parameters/mod.rs b/shared/src/ledger/parameters/mod.rs index fdc2a110d0..52ac12a6f8 100644 --- a/shared/src/ledger/parameters/mod.rs +++ b/shared/src/ledger/parameters/mod.rs @@ -122,6 +122,8 @@ pub struct Parameters { pub vp_whitelist: Vec, /// Whitelisted tx hashes pub tx_whitelist: Vec, + /// Implicit accounts validity predicate WASM code + pub implicit_vp: Vec, } /// Epoch duration. A new epoch begins as soon as both the `min_num_of_blocks` @@ -152,41 +154,55 @@ impl Parameters { DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, H: ledger_storage::StorageHasher, { + let Self { + epoch_duration, + max_expected_time_per_block, + vp_whitelist, + tx_whitelist, + implicit_vp, + } = self; + // write epoch parameters let epoch_key = storage::get_epoch_storage_key(); - let epoch_value = encode(&self.epoch_duration); - storage.write(&epoch_key, epoch_value).expect( - "Epoch parameters must be initialized in the genesis block", - ); + let epoch_value = encode(&epoch_duration); + storage + .write(&epoch_key, epoch_value) + .expect("Epoch parameter must be initialized in the genesis block"); // write vp whitelist parameter let vp_whitelist_key = storage::get_vp_whitelist_storage_key(); - let vp_whitelist_value = encode(&self.vp_whitelist); + let vp_whitelist_value = encode(&vp_whitelist); storage.write(&vp_whitelist_key, vp_whitelist_value).expect( - "Vp whitelist parameters must be initialized in the genesis block", + "Vp whitelist parameter must be initialized in the genesis block", ); // write tx whitelist parameter let tx_whitelist_key = storage::get_tx_whitelist_storage_key(); - let tx_whitelist_value = encode(&self.tx_whitelist); + let tx_whitelist_value = encode(&tx_whitelist); storage.write(&tx_whitelist_key, tx_whitelist_value).expect( - "Tx whitelist parameters must be initialized in the genesis block", + "Tx whitelist parameter must be initialized in the genesis block", ); // write tx whitelist parameter let max_expected_time_per_block_key = storage::get_max_expected_time_per_block_key(); let max_expected_time_per_block_value = - encode(&self.max_expected_time_per_block); + encode(&max_expected_time_per_block); storage .write( &max_expected_time_per_block_key, max_expected_time_per_block_value, ) .expect( - "Max expected time per block parameters must be initialized \ - in the genesis block", + "Max expected time per block parameter must be initialized in \ + the genesis block", ); + + // write implicit vp parameter + let implicit_vp_key = storage::get_implicit_vp_key(); + storage.write(&implicit_vp_key, implicit_vp).expect( + "Implicit VP parameter must be initialized in the genesis block", + ); } } @@ -246,6 +262,24 @@ where update(storage, value, key) } +/// Update the implicit VP parameter in storage. Return the gas cost. +pub fn update_implicit_vp( + storage: &mut Storage, + implicit_vp: &[u8], +) -> std::result::Result +where + DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, + H: ledger_storage::StorageHasher, +{ + let key = storage::get_implicit_vp_key(); + // Not using `fn update` here, because implicit_vp doesn't need to be + // encoded, it's bytes already. + let (gas, _size_diff) = storage + .write(&key, implicit_vp) + .map_err(WriteError::StorageError)?; + Ok(gas) +} + /// Update the parameters in storage. Returns the parameters and gas /// cost. pub fn update( @@ -326,14 +360,21 @@ where decode(value.ok_or(ReadError::ParametersMissing)?) .map_err(ReadError::StorageTypeError)?; + let implicit_vp_key = storage::get_implicit_vp_key(); + let (value, gas_implicit_vp) = storage + .read(&implicit_vp_key) + .map_err(ReadError::StorageError)?; + let implicit_vp = value.ok_or(ReadError::ParametersMissing)?; + Ok(( Parameters { epoch_duration, max_expected_time_per_block, vp_whitelist, tx_whitelist, + implicit_vp, }, - gas_epoch + gas_tx + gas_vp + gas_time, + gas_epoch + gas_tx + gas_vp + gas_time + gas_implicit_vp, )) } diff --git a/shared/src/ledger/parameters/storage.rs b/shared/src/ledger/parameters/storage.rs index 4041b82f2f..b4b2b1013b 100644 --- a/shared/src/ledger/parameters/storage.rs +++ b/shared/src/ledger/parameters/storage.rs @@ -6,6 +6,7 @@ const EPOCH_DURATION_KEY: &str = "epoch_duration"; const VP_WHITELIST_KEY: &str = "vp_whitelist"; const TX_WHITELIST_KEY: &str = "tx_whitelist"; const MAX_EXPECTED_TIME_PER_BLOCK_KEY: &str = "max_expected_time_per_block"; +const IMPLICIT_VP_KEY: &str = "implicit_vp"; /// Returns if the key is a parameter key. pub fn is_parameter_key(key: &Key) -> bool { @@ -52,6 +53,14 @@ pub fn is_vp_whitelist_key(key: &Key) -> bool { ] if addr == &ADDRESS && vp_whitelist == VP_WHITELIST_KEY) } +/// Returns if the key is the implicit VP key. +pub fn is_implicit_vp_key(key: &Key) -> bool { + matches!(&key.segments[..], [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(sub_key), + ] if addr == &ADDRESS && sub_key == IMPLICIT_VP_KEY) +} + /// Storage key used for epoch parameter. pub fn get_epoch_storage_key() -> Key { Key { @@ -91,3 +100,13 @@ pub fn get_max_expected_time_per_block_key() -> Key { ], } } + +/// Storage key used for implicit VP parameter. +pub fn get_implicit_vp_key() -> Key { + Key { + segments: vec![ + DbKeySeg::AddressSeg(ADDRESS), + DbKeySeg::StringSeg(IMPLICIT_VP_KEY.to_string()), + ], + } +} diff --git a/shared/src/ledger/storage/mod.rs b/shared/src/ledger/storage/mod.rs index 16c3ecf180..be90a6c793 100644 --- a/shared/src/ledger/storage/mod.rs +++ b/shared/src/ledger/storage/mod.rs @@ -500,7 +500,11 @@ where &self, addr: &Address, ) -> Result<(Option>, u64)> { - let key = Key::validity_predicate(addr); + let key = if let Address::Implicit(_) = addr { + parameters::storage::get_implicit_vp_key() + } else { + Key::validity_predicate(addr) + }; self.read(&key) } @@ -986,7 +990,8 @@ mod tests { epoch_duration: epoch_duration.clone(), max_expected_time_per_block: Duration::seconds(max_expected_time_per_block).into(), vp_whitelist: vec![], - tx_whitelist: vec![] + tx_whitelist: vec![], + implicit_vp: vec![], }; parameters.init_storage(&mut storage); diff --git a/shared/src/ledger/storage_api/key.rs b/shared/src/ledger/storage_api/key.rs new file mode 100644 index 0000000000..06b3c76bad --- /dev/null +++ b/shared/src/ledger/storage_api/key.rs @@ -0,0 +1,26 @@ +//! Cryptographic signature keys storage API + +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: for<'iter> StorageRead<'iter>, +{ + 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<()> +where + S: StorageWrite, +{ + let addr: Address = pk.into(); + let key = pk_key(&addr); + storage.write(&key, pk) +} diff --git a/shared/src/ledger/storage_api/mod.rs b/shared/src/ledger/storage_api/mod.rs index b806f35801..cae687a1ec 100644 --- a/shared/src/ledger/storage_api/mod.rs +++ b/shared/src/ledger/storage_api/mod.rs @@ -3,6 +3,7 @@ pub mod collections; mod error; +pub mod key; pub mod validation; use borsh::{BorshDeserialize, BorshSerialize}; diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index a998844764..24578690fe 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -9,6 +9,7 @@ //! To keep the temporary files created by a test, use env var //! `ANOMA_E2E_KEEP_TEMP=true`. +use std::path::PathBuf; use std::process::Command; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -16,6 +17,7 @@ use std::time::{Duration, Instant}; use borsh::BorshSerialize; use color_eyre::eyre::Result; use data_encoding::HEXLOWER; +use namada::types::address::Address; use namada::types::token; use namada_apps::config::genesis::genesis_config::{ GenesisConfig, ParametersConfig, PosParamsConfig, @@ -275,7 +277,7 @@ fn ledger_txs_and_queries() -> Result<()> { let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); let txs_args = vec![ - // 2. Submit a token transfer tx + // 2. Submit a token transfer tx (from an established account) vec![ "transfer", "--source", @@ -295,6 +297,26 @@ fn ledger_txs_and_queries() -> Result<()> { "--ledger-address", &validator_one_rpc, ], + // Submit a token transfer tx (from an implicit account) + vec![ + "transfer", + "--source", + DAEWON, + "--target", + ALBERT, + "--token", + XAN, + "--amount", + "10.1", + "--fee-amount", + "0", + "--gas-limit", + "0", + "--fee-token", + XAN, + "--ledger-address", + &validator_one_rpc, + ], // 3. Submit a transaction to update an account's validity // predicate vec![ @@ -1036,6 +1058,7 @@ fn proposal_submission() -> Result<()> { &working_dir, Some("tx_"), )), + ..genesis.parameters }; GenesisConfig { @@ -1084,36 +1107,8 @@ fn proposal_submission() -> Result<()> { client.assert_success(); // 2. Submit valid proposal - let proposal_code = wasm_abs_path(TX_PROPOSAL_CODE); - let albert = find_address(&test, ALBERT)?; - let valid_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": 12_u64, - "voting_end_epoch": 24_u64, - "grace_epoch": 30_u64, - "proposal_code_path": proposal_code.to_str().unwrap() - } - ); - let valid_proposal_json_path = - test.test_dir.path().join("valid_proposal.json"); - generate_proposal_json_file( - valid_proposal_json_path.as_path(), - &valid_proposal_json, - ); - + let valid_proposal_json_path = prepare_proposal_data(&test, albert); let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); let submit_proposal_args = vec![ @@ -2026,3 +2021,162 @@ fn double_signing_gets_slashed() -> Result<()> { Ok(()) } + +/// In this test we: +/// 1. Run the ledger node +/// 2. For some transactions that need signature authorization: +/// 2a. Generate a new key for an implicit account. +/// 2b. Send some funds to the implicit account. +/// 2c. Submit the tx with the implicit account as the source, that +/// requires that the account has revealed its PK. This should be done +/// by the client automatically. +/// 2d. Submit same tx again, this time the client shouldn't reveal again. +#[test] +fn implicit_account_reveal_pk() -> Result<()> { + let test = setup::network(|genesis| genesis, None)?; + + // 1. Run the ledger node + let mut ledger = + run_as!(test, Who::Validator(0), Bin::Node, &["ledger"], Some(40))?; + + ledger.exp_string("Anoma ledger node started")?; + let _bg_ledger = ledger.background(); + + let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); + + // 2. Some transactions that need signature authorization: + let txs_args: Vec Vec>> = vec![ + // A token transfer tx + Box::new(|source| { + [ + "transfer", + "--source", + source, + "--target", + ALBERT, + "--token", + XAN, + "--amount", + "10.1", + "--ledger-address", + &validator_one_rpc, + ] + .into_iter() + .map(|x| x.to_owned()) + .collect() + }), + // A bond + Box::new(|source| { + vec![ + "bond", + "--validator", + "validator-0", + "--source", + source, + "--amount", + "10.1", + "--ledger-address", + &validator_one_rpc, + ] + .into_iter() + .map(|x| x.to_owned()) + .collect() + }), + // Submit proposal + Box::new(|source| { + // Gen data for proposal tx + let source = find_address(&test, source).unwrap(); + let valid_proposal_json_path = prepare_proposal_data(&test, source); + vec![ + "init-proposal", + "--data-path", + valid_proposal_json_path.to_str().unwrap(), + "--ledger-address", + &validator_one_rpc, + ] + .into_iter() + .map(|x| x.to_owned()) + .collect() + }), + ]; + + for (ix, tx_args) in txs_args.into_iter().enumerate() { + let key_alias = format!("key-{ix}"); + + // 2a. Generate a new key for an implicit account. + let mut cmd = run!( + test, + Bin::Wallet, + &["key", "gen", "--alias", &key_alias, "--unsafe-dont-encrypt"], + Some(20), + )?; + cmd.assert_success(); + + // Apply the key_alias once the key is generated to obtain tx args + let tx_args = tx_args(&key_alias); + + // 2b. Send some funds to the implicit account. + let credit_args = [ + "transfer", + "--source", + BERTHA, + "--target", + &key_alias, + "--token", + XAN, + "--amount", + "1000", + "--ledger-address", + &validator_one_rpc, + ]; + let mut client = run!(test, Bin::Client, credit_args, Some(40))?; + client.assert_success(); + + // 2c. Submit the tx with the implicit account as the source. + let expected_reveal = "Submitting a tx to reveal the public key"; + let mut client = run!(test, Bin::Client, &tx_args, Some(40))?; + client.exp_string(expected_reveal)?; + client.assert_success(); + + // 2d. Submit same tx again, this time the client shouldn't reveal + // again. + let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + let unread = client.exp_eof()?; + assert!(!unread.contains(expected_reveal)) + } + + Ok(()) +} + +/// Prepare proposal data in the test's temp dir from the given source address. +/// This can be submitted with "init-proposal" command. +fn prepare_proposal_data(test: &setup::Test, source: Address) -> PathBuf { + let proposal_code = wasm_abs_path(TX_PROPOSAL_CODE); + let valid_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": source, + "voting_start_epoch": 12_u64, + "voting_end_epoch": 24_u64, + "grace_epoch": 30_u64, + "proposal_code_path": proposal_code.to_str().unwrap() + } + ); + let valid_proposal_json_path = + test.test_dir.path().join("valid_proposal.json"); + generate_proposal_json_file( + valid_proposal_json_path.as_path(), + &valid_proposal_json, + ); + valid_proposal_json_path +} diff --git a/tx_prelude/src/key.rs b/tx_prelude/src/key.rs new file mode 100644 index 0000000000..fa83f3d1b4 --- /dev/null +++ b/tx_prelude/src/key.rs @@ -0,0 +1,11 @@ +//! Cryptographic signature keys + +pub use namada::types::key::*; + +use super::*; + +/// 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(ctx: &mut Ctx, pk: &common::PublicKey) -> EnvResult<()> { + storage_api::key::reveal_pk(ctx, pk) +} diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index 730adb3155..a1e1d39df3 100644 --- a/tx_prelude/src/lib.rs +++ b/tx_prelude/src/lib.rs @@ -8,6 +8,7 @@ pub mod governance; pub mod ibc; +pub mod key; pub mod nft; pub mod proof_of_stake; pub mod token; diff --git a/vp_prelude/src/key.rs b/vp_prelude/src/key.rs index 5ef2a5e28c..5cd034852f 100644 --- a/vp_prelude/src/key.rs +++ b/vp_prelude/src/key.rs @@ -5,9 +5,8 @@ pub use namada::types::key::*; use super::*; -/// Get the public key associated with the given address. Panics if not -/// found. +/// Get the public key associated with the given address from the state prior to +/// tx execution. Returns `Ok(None)` if not found. pub fn get(ctx: &Ctx, owner: &Address) -> EnvResult> { - let key = pk_key(owner); - ctx.read_pre(&key) + storage_api::key::get(&ctx.pre(), owner) } diff --git a/wasm/checksums.json b/wasm/checksums.json index 01140354ba..cfca78f0bc 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,18 +1,20 @@ { - "tx_bond.wasm": "tx_bond.38c037a51f9215c2be9c1b01f647251ffdc96a02a0c958c5d3db4ee36ccde43b.wasm", - "tx_ibc.wasm": "tx_ibc.5f86477029d987073ebfec66019dc991b0bb8b80717d4885b860f910916cbcdd.wasm", - "tx_init_account.wasm": "tx_init_account.8d901bce15d1ab63a591def00421183a651d4d5e09ace4291bf0a9044692741d.wasm", - "tx_init_nft.wasm": "tx_init_nft.1991808f44c1c24d4376a3d46b602bed27575f6c0359095c53f37b9225050ffc.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.716cd08d59b26bd75815511f03e141e6ac27bc0b7d7be10a71b04559244722c2.wasm", - "tx_init_validator.wasm": "tx_init_validator.611edff2746f71cdaa7547a84a96676b555821f00af8375a28f8dab7ae9fc9fa.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.3f20f1a86da43cc475ccc127428944bd177d40fbe2d2d1588c6fadd069cbe4b2.wasm", - "tx_transfer.wasm": "tx_transfer.5653340103a32e6685f9668ec24855f65ae17bcc43035c2559a13f5c47bb67af.wasm", - "tx_unbond.wasm": "tx_unbond.71e66ac6f792123a2aaafd60b3892d74a7d0e7a03c3ea34f15fea9089010b810.wasm", + "tx_bond.wasm": "tx_bond.6df70c52076962e190d4bc27a73a99dfc4047a43d85183ee494bedd430f4a162.wasm", + "tx_ibc.wasm": "tx_ibc.611ddc77233ae77b7920893db3fb46c64d393fecf0945679436082ba994613f8.wasm", + "tx_init_account.wasm": "tx_init_account.1534d1343f540acb0a06a691c8bc969ac6af07cb7b8484ae6333563f0ad23c94.wasm", + "tx_init_nft.wasm": "tx_init_nft.c217f2d03b0e95e286e99edd076ff024f34a4b69f6c483611cc3cb5f89440e41.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.3de969e6505e1840a97865cca6f121bd420635dd1d97e7e6e44b825053d96ba0.wasm", + "tx_init_validator.wasm": "tx_init_validator.dd5bd60bba985442455770a990776670c23c4d768e82595d03d87e743102874d.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.1a3e1e0cf55d42dfcbce21effbbc1278c742f57bda3ca662dd6d43a7e035dff9.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.559e3f63dfb3b9f5c4d5d77c56cacdc6611653e3fa783a5ab9f142b61e752d7d.wasm", + "tx_transfer.wasm": "tx_transfer.4322cc5570acdf0dc4a7cba4da414eefe7c85e2f9dff6cdd0b5d77d6ed5e4b48.wasm", + "tx_unbond.wasm": "tx_unbond.3affb0adf515c173b4db6051d435875befe373dab7514083cd0245632ef7af2a.wasm", "tx_update_vp.wasm": "tx_update_vp.6d291dadb43545a809ba33fe26582b7984c67c65f05e363a93dbc62e06a33484.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.ff3def7b4bb0c46635bd6d544ac1745362757ce063feb8142d2ed9ab207f2a12.wasm", - "tx_withdraw.wasm": "tx_withdraw.ba1a743cf8914a353d7706777e0b1a37e20cd271b16e022fd3b50ad28971291f.wasm", - "vp_nft.wasm": "vp_nft.4471284b5c5f3e28c973f0a2ad2dde52ebe4a1dcd5dc15e93b380706fd0e35ea.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.7d7eb09cddc7ae348417da623e21ec4a4f8c78f15ae12de5abe7087eeab1e0db.wasm", - "vp_token.wasm": "vp_token.4a5436f7519de15c80103557add57e8d06e766e1ec1f7a642ffca252be01c5d0.wasm", - "vp_user.wasm": "vp_user.729b18aab60e8ae09b75b5f067658f30459a5ccfcd34f909b88da96523681019.wasm" + "tx_vote_proposal.wasm": "tx_vote_proposal.206bdcfbf1e640c6f5f665122f96ea55fdd6ee00807aa45d19bfe6e79974c160.wasm", + "tx_withdraw.wasm": "tx_withdraw.7bc289a6fd32e5513698d061f6b0fee134c8a0cd38d8dd83197346859ef83a64.wasm", + "vp_implicit.wasm": "vp_implicit.590d8b324271485cc2079d68283cc64832e4b77811c4d426fe0f6d3fe8964d02.wasm", + "vp_nft.wasm": "vp_nft.5eed9af8cb6edda135e950d7869a3068fa54ab40a94a5fd82985c91ed0d41746.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.61e537b46e43bfd89bd7ed92d06ae53e2c2461261b6f53dc6a7bf7ad6e43f2e9.wasm", + "vp_token.wasm": "vp_token.eb7fc5d8d36c92108b122bef4badca05ae57eb259f1c076d677e210d5bd8ec80.wasm", + "vp_user.wasm": "vp_user.c6d2854e59cdb2df2f755bd03dda9c7baca510e156ebf4393c6407b4e639bf21.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index 7f48f2aea6..fd8fd95597 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -20,11 +20,13 @@ tx_init_nft = ["namada_tx_prelude"] tx_init_proposal = ["namada_tx_prelude"] tx_init_validator = ["namada_tx_prelude"] tx_mint_nft = ["namada_tx_prelude"] +tx_reveal_pk = ["namada_tx_prelude"] tx_transfer = ["namada_tx_prelude"] tx_unbond = ["namada_tx_prelude"] tx_update_vp = ["namada_tx_prelude"] tx_vote_proposal = ["namada_tx_prelude"] tx_withdraw = ["namada_tx_prelude"] +vp_implicit = ["namada_vp_prelude", "once_cell", "rust_decimal"] vp_nft = ["namada_vp_prelude"] vp_testnet_faucet = ["namada_vp_prelude", "once_cell"] vp_token = ["namada_vp_prelude"] diff --git a/wasm/wasm_source/Makefile b/wasm/wasm_source/Makefile index 48d4675ea6..460c31767f 100644 --- a/wasm/wasm_source/Makefile +++ b/wasm/wasm_source/Makefile @@ -12,11 +12,13 @@ wasms += tx_init_nft wasms += tx_init_validator wasms += tx_init_proposal wasms += tx_mint_nft +wasms += tx_reveal_pk wasms += tx_vote_proposal wasms += tx_transfer wasms += tx_unbond wasms += tx_update_vp wasms += tx_withdraw +wasms += vp_implicit wasms += vp_nft wasms += vp_testnet_faucet wasms += vp_token diff --git a/wasm/wasm_source/src/lib.rs b/wasm/wasm_source/src/lib.rs index 8929992754..62fa05f3c2 100644 --- a/wasm/wasm_source/src/lib.rs +++ b/wasm/wasm_source/src/lib.rs @@ -12,6 +12,8 @@ pub mod tx_init_proposal; pub mod tx_init_validator; #[cfg(feature = "tx_mint_nft")] pub mod tx_mint_nft; +#[cfg(feature = "tx_reveal_pk")] +pub mod tx_reveal_pk; #[cfg(feature = "tx_transfer")] pub mod tx_transfer; #[cfg(feature = "tx_unbond")] @@ -22,6 +24,9 @@ pub mod tx_update_vp; pub mod tx_vote_proposal; #[cfg(feature = "tx_withdraw")] pub mod tx_withdraw; + +#[cfg(feature = "vp_implicit")] +pub mod vp_implicit; #[cfg(feature = "vp_nft")] pub mod vp_nft; #[cfg(feature = "vp_testnet_faucet")] diff --git a/wasm/wasm_source/src/tx_reveal_pk.rs b/wasm/wasm_source/src/tx_reveal_pk.rs new file mode 100644 index 0000000000..be3bacce6d --- /dev/null +++ b/wasm/wasm_source/src/tx_reveal_pk.rs @@ -0,0 +1,15 @@ +//! A tx to reveal a public key of an implicit account. +//! This tx expects borsh encoded [`common::PublicKey`] in `tx_data` and it's +//! not signed as the authenticity of the public key can be trivially verified +//! against the address into which it's being written. + +use namada_tx_prelude::key::common; +use namada_tx_prelude::*; + +#[transaction] +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { + let pk = common::PublicKey::try_from_slice(&tx_data[..]) + .wrap_err("failed to decode common::PublicKey from tx_data")?; + debug_log!("tx_reveal_pk called with pk: {pk}"); + key::reveal_pk(ctx, &pk) +} diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs new file mode 100644 index 0000000000..d761dc3979 --- /dev/null +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -0,0 +1,744 @@ +//! Implicit account VP. All implicit accounts share this same VP. +//! +//! This VP currently provides a signature verification against a public key for +//! sending tokens (receiving tokens is permissive). +//! +//! It allows to reveal a PK, as long as its address matches with the address +//! that can be derived from the PK. +//! +//! It allows to bond, unbond and withdraw tokens to and from PoS system with a +//! valid signature. +//! +//! Any other storage key changes are allowed only with a valid signature. + +use namada_vp_prelude::storage::KeySeg; +use namada_vp_prelude::*; +use once_cell::unsync::Lazy; + +enum KeyType<'a> { + /// Public key - written once revealed + Pk(&'a Address), + Token(&'a Address), + PoS, + Nft(&'a Address), + GovernanceVote(&'a Address), + Unknown, +} + +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) { + Self::Pk(address) + } else if let Some(address) = token::is_any_token_balance_key(key) { + Self::Token(address) + } else if let Some((_, address)) = + token::is_any_multitoken_balance_key(key) + { + Self::Token(address) + } else if proof_of_stake::is_pos_key(key) { + Self::PoS + } else if let Some(address) = nft::is_nft_key(key) { + Self::Nft(address) + } else if gov_storage::is_vote_key(key) { + let voter_address = gov_storage::get_voter_address(key); + if let Some(address) = voter_address { + Self::GovernanceVote(address) + } else { + Self::Unknown + } + } else { + Self::Unknown + } + } +} + +#[validity_predicate] +fn validate_tx( + ctx: &Ctx, + tx_data: Vec, + addr: Address, + keys_changed: BTreeSet, + verifiers: BTreeSet
, +) -> VpResult { + debug_log!( + "vp_user called with user addr: {}, key_changed: {:?}, verifiers: {:?}", + addr, + keys_changed, + verifiers + ); + + let signed_tx_data = + Lazy::new(|| SignedTxData::try_from_slice(&tx_data[..])); + + let valid_sig = Lazy::new(|| match &*signed_tx_data { + Ok(signed_tx_data) => { + let pk = key::get(ctx, &addr); + match pk { + Ok(Some(pk)) => { + matches!( + ctx.verify_tx_signature(&pk, &signed_tx_data.sig), + Ok(true) + ) + } + _ => false, + } + } + _ => false, + }); + + if !is_valid_tx(ctx, &tx_data)? { + return reject(); + } + + for key in keys_changed.iter() { + let key_type: KeyType = key.into(); + let is_valid = match key_type { + KeyType::Pk(owner) => { + if owner == &addr { + if ctx.has_key_pre(key)? { + // If the PK is already reveal, reject the tx + return reject(); + } + let post: Option = + ctx.read_post(key)?; + match post { + Some(pk) => { + let addr_from_pk: Address = (&pk).into(); + // Check that address matches with the address + // derived from the PK + if addr_from_pk != addr { + return reject(); + } + } + None => { + // Revealed PK cannot be deleted + return reject(); + } + } + } + true + } + KeyType::Token(owner) => { + if owner == &addr { + let pre: token::Amount = + ctx.read_pre(key)?.unwrap_or_default(); + let post: token::Amount = + ctx.read_post(key)?.unwrap_or_default(); + let change = post.change() - pre.change(); + // debit has to signed, credit doesn't + let valid = change >= 0 || *valid_sig; + debug_log!( + "token key: {}, change: {}, valid_sig: {}, valid \ + modification: {}", + key, + change, + *valid_sig, + valid + ); + valid + } else { + debug_log!( + "This address ({}) is not of owner ({}) of token key: \ + {}", + addr, + owner, + key + ); + // If this is not the owner, allow any change + true + } + } + KeyType::PoS => { + // Allow the account to be used in PoS + let bond_id = proof_of_stake::is_bond_key(key) + .or_else(|| proof_of_stake::is_unbond_key(key)); + let valid = match bond_id { + Some(bond_id) => { + // Bonds and unbonds changes for this address + // must be signed + bond_id.source != addr || *valid_sig + } + None => { + // Any other PoS changes are allowed without signature + true + } + }; + debug_log!( + "PoS key {} {}", + key, + if valid { "accepted" } else { "rejected" } + ); + valid + } + KeyType::Nft(owner) => { + if owner == &addr { + *valid_sig + } else { + true + } + } + KeyType::GovernanceVote(voter) => { + if voter == &addr { + *valid_sig + } else { + true + } + } + KeyType::Unknown => { + if key.segments.get(0) == Some(&addr.to_db_key()) { + // Unknown changes to this address space require a valid + // signature + *valid_sig + } else { + // Unknown changes anywhere else are permitted + true + } + } + }; + if !is_valid { + debug_log!("key {} modification failed vp", key); + return reject(); + } + } + + accept() +} + +#[cfg(test)] +mod tests { + // Use this as `#[test]` annotation to enable logging + use namada_tests::log::test; + use namada_tests::tx::{self, tx_host_env, TestTxEnv}; + use namada_tests::vp::vp_host_env::storage::Key; + use namada_tests::vp::*; + use namada_tx_prelude::{StorageWrite, TxEnv}; + use namada_vp_prelude::key::RefTo; + use proptest::prelude::*; + use storage::testing::arb_account_storage_key_no_vp; + + use super::*; + + const VP_ALWAYS_TRUE_WASM: &str = + "../../wasm_for_tests/vp_always_true.wasm"; + + /// Test that no-op transaction (i.e. no storage modifications) accepted. + #[test] + fn test_no_op_transaction() { + let tx_data: Vec = vec![]; + let addr: Address = address::testing::established_address_1(); + let keys_changed: BTreeSet = BTreeSet::default(); + let verifiers: BTreeSet
= BTreeSet::default(); + + // The VP env must be initialized before calling `validate_tx` + vp_host_env::init(); + + assert!( + validate_tx(&CTX, tx_data, addr, keys_changed, verifiers).unwrap() + ); + } + + /// Test that a PK can be revealed when it's not revealed and cannot be + /// revealed anymore once it's already revealed. + #[test] + fn test_can_reveal_pk() { + // The SK to be used for the implicit account + let secret_key = key::testing::keypair_1(); + let public_key = secret_key.ref_to(); + let addr: Address = (&public_key).into(); + + // Initialize a tx environment + let tx_env = TestTxEnv::default(); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(addr.clone(), tx_env, |_address| { + // Apply reveal_pk in a transaction + tx_host_env::key::reveal_pk(tx::ctx(), &public_key).unwrap(); + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + 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, tx_data, addr.clone(), keys_changed, verifiers) + .unwrap(), + "Revealing PK that's not yet revealed and is matching the address \ + must be accepted" + ); + + // Commit the transaction and create another tx_env + let vp_env = vp_host_env::take(); + tx_host_env::set_from_vp_env(vp_env); + tx_host_env::commit_tx_and_block(); + let tx_env = tx_host_env::take(); + + // Try to reveal it again + vp_host_env::init_from_tx(addr.clone(), tx_env, |_address| { + // Apply reveal_pk in a transaction + tx_host_env::key::reveal_pk(tx::ctx(), &public_key).unwrap(); + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + 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, tx_data, addr, keys_changed, verifiers).unwrap(), + "Revealing PK that's already revealed should be rejected" + ); + } + + /// Test that a revealed PK that doesn't correspond to the account's address + /// is rejected. + #[test] + fn test_reveal_wrong_pk_rejected() { + // The SK to be used for the implicit account + let secret_key = key::testing::keypair_1(); + let public_key = secret_key.ref_to(); + let addr: Address = (&public_key).into(); + + // Another SK to be revealed for the address above (not matching it) + let mismatched_sk = key::testing::keypair_2(); + let mismatched_pk = mismatched_sk.ref_to(); + + // Initialize a tx environment + let tx_env = TestTxEnv::default(); + + // 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 vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + 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, tx_data, addr, keys_changed, verifiers).unwrap(), + "Mismatching PK must be rejected" + ); + } + + /// Test that a credit transfer is accepted. + #[test] + fn test_credit_transfer_accepted() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let secret_key = key::testing::keypair_1(); + let public_key = secret_key.ref_to(); + let vp_owner: Address = (&public_key).into(); + let source = address::testing::established_address_2(); + let token = address::xan(); + let amount = token::Amount::from(10_098_123); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner, &source, &token]); + + // Credit the tokens to the source before running the transaction to be + // able to transfer from it + tx_env.credit_tokens(&source, &token, amount); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Apply transfer in a transaction + tx_host_env::token::transfer( + tx::ctx(), + &source, + address, + &token, + None, + amount, + ) + .unwrap(); + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + 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, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + /// Test that a debit transfer without a valid signature is rejected. + #[test] + fn test_unsigned_debit_transfer_rejected() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let secret_key = key::testing::keypair_1(); + let public_key = secret_key.ref_to(); + let vp_owner: Address = (&public_key).into(); + let target = address::testing::established_address_2(); + let token = address::xan(); + let amount = token::Amount::from(10_098_123); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner, &target, &token]); + + // Credit the tokens to the VP owner before running the transaction to + // be able to transfer from it + tx_env.credit_tokens(&vp_owner, &token, amount); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Apply transfer in a transaction + tx_host_env::token::transfer( + tx::ctx(), + address, + &target, + &token, + None, + amount, + ) + .unwrap(); + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + 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, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + /// Test that a debit transfer with a valid signature is accepted. + #[test] + fn test_signed_debit_transfer_accepted() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let secret_key = key::testing::keypair_1(); + let public_key = secret_key.ref_to(); + let vp_owner: Address = (&public_key).into(); + let target = address::testing::established_address_2(); + let token = address::xan(); + let amount = token::Amount::from(10_098_123); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner, &target, &token]); + + // Credit the tokens to the VP owner before running the transaction to + // 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| { + // Apply transfer in a transaction + tx_host_env::token::transfer( + tx::ctx(), + address, + &target, + &token, + None, + amount, + ) + .unwrap(); + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + let signed_tx = tx.sign(&secret_key); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + 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, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + /// Test that a transfer on with accounts other than self is accepted. + #[test] + fn test_transfer_between_other_parties_accepted() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let secret_key = key::testing::keypair_1(); + let public_key = secret_key.ref_to(); + let vp_owner: Address = (&public_key).into(); + let source = address::testing::established_address_2(); + let target = address::testing::established_address_3(); + let token = address::xan(); + let amount = token::Amount::from(10_098_123); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner, &source, &target, &token]); + + // Credit the tokens to the VP owner before running the transaction to + // be able to transfer from it + tx_env.credit_tokens(&source, &token, amount); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + tx::ctx().insert_verifier(address).unwrap(); + // Apply transfer in a transaction + tx_host_env::token::transfer( + tx::ctx(), + &source, + &target, + &token, + None, + amount, + ) + .unwrap(); + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + 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, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + /// Generates a keypair, derive an implicit address from it and generate + /// a storage key inside its storage. + fn arb_account_storage_subspace_key() + -> impl Strategy { + // Generate a keypair + key::testing::arb_common_keypair().prop_flat_map(|sk| { + let pk = sk.ref_to(); + let addr: Address = (&pk).into(); + // Generate a storage key other than its VP key (VP cannot be + // modified directly via `write`, it has to be modified via + // `tx::update_validity_predicate`. + let storage_key = arb_account_storage_key_no_vp(addr.clone()); + (Just(sk), Just(addr), storage_key) + }) + } + + proptest! { + /// 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(), + // 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(); + + // Spawn all the accounts in the storage key to be able to modify + // their storage + let storage_key_addresses = storage_key.find_addresses(); + tx_env.spawn_accounts(storage_key_addresses); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { + // Write or delete some data in the transaction + if let Some(value) = &storage_value { + tx::ctx().write(&storage_key, value).unwrap(); + } else { + tx::ctx().delete(&storage_key).unwrap(); + } + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + 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, 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::>>(), + ) { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + // Spawn all the accounts in the storage key to be able to modify + // their storage + let storage_key_addresses = storage_key.find_addresses(); + tx_env.spawn_accounts(storage_key_addresses); + + let public_key = secret_key.ref_to(); + 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| { + // Write or delete some data in the transaction + if let Some(value) = &storage_value { + tx::ctx().write(&storage_key, value).unwrap(); + } else { + tx::ctx().delete(&storage_key).unwrap(); + } + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + let signed_tx = tx.sign(&secret_key); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + 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, tx_data, vp_owner, keys_changed, verifiers).unwrap()); + } + } + + /// Test that a validity predicate update without a valid signature is + /// rejected. + #[test] + fn test_unsigned_vp_update_rejected() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let secret_key = key::testing::keypair_1(); + let public_key = secret_key.ref_to(); + let vp_owner: Address = (&public_key).into(); + let vp_code = + std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner]); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Update VP in a transaction + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + 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, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + /// Test that a tx is rejected if not whitelisted + #[test] + fn test_tx_not_whitelisted_rejected() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let secret_key = key::testing::keypair_1(); + let public_key = secret_key.ref_to(); + let vp_owner: Address = (&public_key).into(); + let vp_code = + std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); + + let vp_hash = sha256(&vp_code); + tx_env.init_parameters( + None, + Some(vec![vp_hash.to_string()]), + Some(vec!["some_hash".to_string()]), + ); + + // 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); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Update VP in a transaction + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + let signed_tx = tx.sign(&secret_key); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + 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, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + #[test] + fn test_tx_whitelisted_accepted() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let secret_key = key::testing::keypair_1(); + let public_key = secret_key.ref_to(); + let vp_owner: Address = (&public_key).into(); + let vp_code = + std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); + + // hardcoded hash of VP_ALWAYS_TRUE_WASM + tx_env.init_parameters(None, None, Some(vec!["E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855".to_string()])); + + // 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); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Update VP in a transaction + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + let signed_tx = tx.sign(&secret_key); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + 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, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } +}