diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index 14a07bb6c1..534a316579 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -95,6 +95,7 @@ pub async fn main() -> Result<()> { Sub::QueryProtocolParameters(QueryProtocolParameters(args)) => { rpc::query_protocol_parameters(ctx, args).await; } + Sub::SignTx(SignTx(args)) => rpc::sign_tx(ctx, args).await, } } cli::NamadaClient::WithoutContext(cmd, global_args) => match cmd { diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 9c23cadb46..a1b431d970 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -180,6 +180,8 @@ pub mod cmds { .subcommand(QueryProposal::def().display_order(3)) .subcommand(QueryProposalResult::def().display_order(3)) .subcommand(QueryProtocolParameters::def().display_order(3)) + // Commands + .subcommand(SignTx::def().display_order(4)) // Utils .subcommand(Utils::def().display_order(5)) } @@ -220,6 +222,7 @@ pub mod cmds { Self::parse_with_ctx(matches, QueryProposalResult); let query_protocol_parameters = Self::parse_with_ctx(matches, QueryProtocolParameters); + let sign_tx = Self::parse_with_ctx(matches, SignTx); let utils = SubCmd::parse(matches).map(Self::WithoutContext); tx_custom .or(tx_transfer) @@ -247,6 +250,7 @@ pub mod cmds { .or(query_proposal) .or(query_proposal_result) .or(query_protocol_parameters) + .or(sign_tx) .or(utils) } } @@ -310,6 +314,7 @@ pub mod cmds { QueryProposal(QueryProposal), QueryProposalResult(QueryProposalResult), QueryProtocolParameters(QueryProtocolParameters), + SignTx(SignTx), } #[allow(clippy::large_enum_variant)] @@ -1557,6 +1562,25 @@ pub mod cmds { .add_args::() } } + + #[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| Self(args::SignTx::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Sign and dump a transaction signature.") + .add_args::() + } + } } pub mod args { @@ -1588,6 +1612,7 @@ pub mod args { use crate::facade::tendermint_config::net::Address as TendermintAddress; const ADDRESS: Arg = arg("address"); + const ADDRESS_OPT: ArgOpt = ADDRESS.opt(); const ALIAS_OPT: ArgOpt = ALIAS.opt(); const ALIAS: Arg = arg("alias"); const ALLOW_DUPLICATE_IP: ArgFlag = flag("allow-duplicate-ip"); @@ -1618,8 +1643,9 @@ pub mod args { const DATA_PATH: Arg = arg("data-path"); const DECRYPT: ArgFlag = flag("decrypt"); const DONT_ARCHIVE: ArgFlag = flag("dont-archive"); - const DRY_RUN_TX: ArgFlag = flag("dry-run"); const DUMP_TX: ArgFlag = flag("dump-tx"); + const DRY_RUN_TX: ArgFlag = flag("dry-run"); + const OFFLINE_TX: ArgFlag = flag("offline-tx"); const EPOCH: ArgOpt = arg_opt("epoch"); const FORCE: ArgFlag = flag("force"); const DONT_PREFETCH_WASM: ArgFlag = flag("dont-prefetch-wasm"); @@ -1639,7 +1665,6 @@ pub mod args { let raw = "127.0.0.1:26657"; TendermintAddress::from_str(raw).unwrap() })); - const LEDGER_ADDRESS: Arg = arg("ledger-address"); const LOCALHOST: ArgFlag = flag("localhost"); const MASP_VALUE: Arg = arg("value"); @@ -1661,6 +1686,7 @@ pub mod args { const PROTOCOL_KEY: ArgOpt = arg_opt("protocol-key"); const PRE_GENESIS_PATH: ArgOpt = arg_opt("pre-genesis-path"); const PUBLIC_KEY: Arg = arg("public-key"); + const PUBLIC_KEYS: ArgMulti = arg_multi("public-keys"); const PROPOSAL_ID: Arg = arg("proposal-id"); const PROPOSAL_ID_OPT: ArgOpt = arg_opt("proposal-id"); const PROPOSAL_VOTE: Arg = arg("vote"); @@ -1670,9 +1696,10 @@ pub mod args { const RECEIVER: Arg = arg("receiver"); const SCHEME: ArgDefault = arg_default("scheme", DefaultFn(|| SchemeType::Ed25519)); - const SIGNER: ArgOpt = arg_opt("signer"); - const SIGNING_KEY_OPT: ArgOpt = SIGNING_KEY.opt(); - const SIGNING_KEY: Arg = arg("signing-key"); + const SIGNERS: ArgMulti = arg_multi("signers"); + const SIGNING_TX: ArgOpt = arg_opt("signing-tx"); + const SIGNING_KEYS: ArgMulti = arg_multi("signing-keys"); + const SIGNATURES: ArgMulti = arg_multi("signatures"); const SOURCE: Arg = arg("source"); const SOURCE_OPT: ArgOpt = SOURCE.opt(); const STORAGE_KEY: Arg = arg("storage-key"); @@ -1683,13 +1710,15 @@ pub mod args { const TOKEN: Arg = arg("token"); const TRANSFER_SOURCE: Arg = arg("source"); const TRANSFER_TARGET: Arg = arg("target"); + const THRESHOLD: ArgOpt = arg_opt("threshold"); const TX_HASH: Arg = arg("tx-hash"); + const TX_TIMESTAMP: ArgOpt = arg_opt("timestamp"); const UNSAFE_DONT_ENCRYPT: ArgFlag = flag("unsafe-dont-encrypt"); const UNSAFE_SHOW_SECRET: ArgFlag = flag("unsafe-show-secret"); const VALIDATOR: Arg = arg("validator"); const VALIDATOR_OPT: ArgOpt = VALIDATOR.opt(); - const VALIDATOR_ACCOUNT_KEY: ArgOpt = - arg_opt("account-key"); + const VALIDATOR_ACCOUNT_KEYS: ArgMulti = + arg_multi("account-keys"); const VALIDATOR_CONSENSUS_KEY: ArgOpt = arg_opt("consensus-key"); const VALIDATOR_CODE_PATH: ArgOpt = arg_opt("validator-code-path"); @@ -1830,6 +1859,10 @@ pub mod args { pub code_path: PathBuf, /// Path to the data file pub data_path: Option, + /// Optional timestamp field + pub timestamp: Option, + /// The address + pub address: Option, } impl Args for TxCustom { @@ -1837,10 +1870,14 @@ pub mod args { let tx = Tx::parse(matches); let code_path = CODE_PATH.parse(matches); let data_path = DATA_PATH_OPT.parse(matches); + let timestamp = TX_TIMESTAMP.parse(matches); + let address = ADDRESS_OPT.parse(matches); Self { tx, code_path, data_path, + timestamp, + address, } } @@ -1856,6 +1893,48 @@ pub mod args { will be passed to the transaction code when it's \ executed.", )) + .arg( + TX_TIMESTAMP.def().about( + "The timestamp to set be set on the transaction.", + ), + ) + .arg(ADDRESS_OPT.def().about("The address to lookup.")) + } + } + + #[derive(Clone, Debug)] + pub struct SignTx { + pub tx: Tx, + pub data_path: Option, + pub signing_tx: Option, + } + + impl Args for SignTx { + fn parse(matches: &ArgMatches) -> Self { + let tx = Tx::parse(matches); + let data_path = DATA_PATH_OPT.parse(matches); + let signing_tx = SIGNING_TX.parse(matches); + Self { + tx, + data_path, + signing_tx, + } + } + + fn def(app: App) -> App { + app.add_args::() + .arg( + DATA_PATH_OPT + .def() + .about("Path to hex encoeded signing tx file.") + .conflicts_with(SIGNING_TX.name), + ) + .arg( + SIGNING_TX + .def() + .about("The hex encoded transaction to be signed.") + .conflicts_with(DATA_PATH_OPT.name), + ) } } @@ -2005,41 +2084,59 @@ pub mod args { /// Common tx arguments pub tx: Tx, /// Address of the source account - pub source: WalletAddress, + pub source: Option, /// Path to the VP WASM code file for the new account pub vp_code_path: Option, /// Public key for the new account - pub public_key: WalletPublicKey, + pub public_keys: Vec, + /// The threshold for multsignature account + pub threshold: Option, } impl Args for TxInitAccount { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); - let source = SOURCE.parse(matches); + let source = SOURCE_OPT.parse(matches); let vp_code_path = CODE_PATH_OPT.parse(matches); - let public_key = PUBLIC_KEY.parse(matches); + let public_keys = PUBLIC_KEYS.parse(matches); + let threshold = THRESHOLD.parse(matches); Self { tx, source, vp_code_path, - public_key, + public_keys, + threshold, } } fn def(app: App) -> App { app.add_args::() - .arg(SOURCE.def().about( - "The source account's address that signs the transaction.", - )) + .arg( + SOURCE_OPT + .def() + .about( + "The source account's address that signs the \ + transaction.", + ) + .required(true) + .min_values(1), + ) .arg(CODE_PATH_OPT.def().about( "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().about( - "A public key to be used for the new account in \ - hexadecimal encoding.", - )) + .arg( + PUBLIC_KEYS + .def() + .about( + "A public key to be used for the new account in \ + hexadecimal encoding.", + ) + .required(true) + .min_values(1), + ) + .arg(THRESHOLD.def().about("Multisgnature threshold.")) } } @@ -2049,12 +2146,13 @@ pub mod args { pub tx: Tx, pub source: WalletAddress, pub scheme: SchemeType, - pub account_key: Option, + pub account_keys: Vec, pub consensus_key: Option, pub protocol_key: Option, pub commission_rate: Decimal, pub max_commission_rate_change: Decimal, pub validator_vp_code_path: Option, + pub threshold: Option, pub unsafe_dont_encrypt: bool, } @@ -2063,24 +2161,26 @@ pub mod args { 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 protocol_key = PROTOCOL_KEY.parse(matches); let commission_rate = COMMISSION_RATE.parse(matches); let max_commission_rate_change = MAX_COMMISSION_RATE_CHANGE.parse(matches); let validator_vp_code_path = VALIDATOR_CODE_PATH.parse(matches); + let threshold = THRESHOLD.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); Self { tx, source, scheme, - account_key, + account_keys, consensus_key, protocol_key, commission_rate, max_commission_rate_change, validator_vp_code_path, + threshold, unsafe_dont_encrypt, } } @@ -2094,7 +2194,7 @@ pub mod args { "The key scheme/type used for the validator keys. \ Currently supports ed25519 and secp256k1.", )) - .arg(VALIDATOR_ACCOUNT_KEY.def().about( + .arg(VALIDATOR_ACCOUNT_KEYS.def().about( "A public key for the validator account. A new one will \ be generated if none given.", )) @@ -2122,6 +2222,9 @@ pub mod args { for the validator account. Uses the default validator VP \ if none specified.", )) + .arg(THRESHOLD.def().about( + "Specifiy the treshold if a multisignature account.", + )) .arg(UNSAFE_DONT_ENCRYPT.def().about( "UNSAFE: Do not encrypt the generated keypairs. Do not \ use this for keys used in a live network.", @@ -2297,6 +2400,8 @@ pub mod args { pub offline: bool, /// The proposal file path pub proposal_data: Option, + /// The address that will send the vote + pub address: WalletAddress, } impl Args for VoteProposal { @@ -2306,6 +2411,7 @@ pub mod args { let vote = PROPOSAL_VOTE.parse(matches); let offline = PROPOSAL_OFFLINE.parse(matches); let proposal_data = DATA_PATH_OPT.parse(matches); + let address = ADDRESS.parse(matches); Self { tx, @@ -2313,6 +2419,7 @@ pub mod args { vote, offline, proposal_data, + address, } } @@ -2347,6 +2454,7 @@ pub mod args { ) .conflicts_with(PROPOSAL_ID.name), ) + .arg(ADDRESS.def().about("The address to vote with.")) } } @@ -2863,10 +2971,14 @@ pub mod args { pub fee_token: WalletAddress, /// The max amount of gas used to process tx pub gas_limit: GasLimit, + /// Dump the signing tx to file + pub offline_tx: bool, /// Sign the tx with the key for the given alias from your wallet - pub signing_key: Option, + pub signing_keys: Vec, /// Sign the tx with the keypair of the public key of the given address - pub signer: Option, + pub signers: Vec, + /// The paths to signatures + pub signatures: Vec, } impl Tx { @@ -2883,11 +2995,18 @@ pub mod args { fee_amount: self.fee_amount, fee_token: ctx.get(&self.fee_token), gas_limit: self.gas_limit.clone(), - signing_key: self - .signing_key - .as_ref() - .map(|sk| ctx.get_cached(sk)), - signer: self.signer.as_ref().map(|signer| ctx.get(signer)), + offline_tx: self.offline_tx, + signing_keys: self + .signing_keys + .iter() + .map(|sk| ctx.get_cached(sk)) + .collect(), + signers: self + .signers + .iter() + .map(|signer| ctx.get(signer)) + .collect(), + signatures: self.signatures.clone(), } } } @@ -2923,25 +3042,27 @@ pub mod args { "The maximum amount of gas needed to run transaction", ), ) + .arg(OFFLINE_TX.def().about("Dump tx to file.")) .arg( - SIGNING_KEY_OPT + SIGNING_KEYS .def() .about( "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_all(&[SIGNERS.name]), ) .arg( - SIGNER + SIGNERS .def() .about( "Sign the transaction with the keypair of the public \ key of the given address.", ) - .conflicts_with(SIGNING_KEY_OPT.name), + .conflicts_with_all(&[SIGNING_KEYS.name]), ) + .arg(SIGNATURES.def().about("The paths to signatures.")) } fn parse(matches: &ArgMatches) -> Self { @@ -2954,9 +3075,10 @@ pub mod args { let fee_amount = GAS_AMOUNT.parse(matches); let fee_token = GAS_TOKEN.parse(matches); let gas_limit = GAS_LIMIT.parse(matches).into(); - - let signing_key = SIGNING_KEY_OPT.parse(matches); - let signer = SIGNER.parse(matches); + let offline_tx = OFFLINE_TX.parse(matches); + let signing_keys = SIGNING_KEYS.parse(matches); + let signers = SIGNERS.parse(matches); + let signatures = SIGNATURES.parse(matches); Self { dry_run, dump_tx, @@ -2967,8 +3089,10 @@ pub mod args { fee_amount, fee_token, gas_limit, - signing_key, - signer, + signing_keys, + signers, + offline_tx, + signatures, } } } diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index e61fda9dfc..d8caf8328e 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -12,6 +12,7 @@ use namada::types::key::*; use namada::types::masp::*; use super::args; +use crate::cli::safe_exit; use crate::client::tx::ShieldedContext; use crate::config::genesis::genesis_config; use crate::config::global::GlobalConfig; @@ -185,7 +186,23 @@ impl Context { /// Read the given WASM file from the WASM directory or an absolute path. pub fn read_wasm(&self, file_name: impl AsRef) -> Vec { - wasm_loader::read_wasm_or_exit(self.wasm_dir(), file_name) + match self.try_read_wasm(&file_name) { + Some(bytes) => bytes, + None => { + println!( + "Unable to read {}.", + file_name.as_ref().to_string_lossy() + ); + safe_exit(1) + } + } + } + + pub fn try_read_wasm( + &self, + file_name: impl AsRef, + ) -> Option> { + wasm_loader::read_wasm(self.wasm_dir(), file_name).ok() } } diff --git a/apps/src/lib/cli/utils.rs b/apps/src/lib/cli/utils.rs index 56965d72ef..b956126b50 100644 --- a/apps/src/lib/cli/utils.rs +++ b/apps/src/lib/cli/utils.rs @@ -278,6 +278,21 @@ where } } +impl ArgMulti> { + pub fn def(&self) -> ClapArg { + ClapArg::new(self.name) + .long(self.name) + .takes_value(true) + .multiple(true) + .require_delimiter(true) + } + + pub fn parse(&self, matches: &ArgMatches) -> Vec> { + let raw = matches.values_of(self.name).unwrap_or_default(); + raw.map(|val| FromContext::new(val.to_string())).collect() + } +} + /// Extensions for defining commands and arguments. /// Every function here should have a matcher in [`ArgMatchesExt`]. pub trait AppExt { diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 4dd36ea2eb..b6584ffda8 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -23,6 +23,7 @@ use masp_primitives::transaction::components::Amount; use masp_primitives::zip32::ExtendedFullViewingKey; #[cfg(not(feature = "mainnet"))] use namada::core::ledger::testnet_pow; +use namada::core::types::key; use namada::ledger::events::Event; use namada::ledger::governance::parameters::GovParams; use namada::ledger::governance::storage as gov_storage; @@ -33,7 +34,7 @@ use namada::ledger::pos::{ }; use namada::ledger::queries::{self, RPC}; use namada::ledger::storage::ConversionState; -use namada::proto::{SignedTxData, Tx}; +use namada::proto::{SignedTxData, SigningTx, Tx}; use namada::types::address::{masp, tokens, Address}; use namada::types::governance::{ OfflineProposal, OfflineVote, ProposalResult, ProposalVote, TallyResult, @@ -53,7 +54,8 @@ use namada::types::transaction::{ use namada::types::{address, storage, token}; use tokio::time::{Duration, Instant}; -use crate::cli::{self, args, Context}; +use super::signing::{tx_signers, OfflineSignature}; +use crate::cli::{self, args, safe_exit, Context}; use crate::client::tendermint_rpc_types::TxResponse; use crate::client::tx::{ Conversions, PinnedBalanceError, TransactionDelta, TransferDelta, @@ -1259,6 +1261,7 @@ pub async fn query_proposal_result( let public_key = get_public_key( &proposal.address, + 0, args.query.ledger_address.clone(), ) .await @@ -1380,6 +1383,49 @@ pub async fn query_bond( RPC.vp().pos().bond(client, source, validator, &epoch).await, ) } +pub async fn sign_tx( + mut ctx: Context, + args::SignTx { + tx, + data_path, + signing_tx, + }: args::SignTx, +) { + let data = match (data_path, signing_tx) { + (None, Some(data)) => HEXLOWER + .decode(data.as_bytes()) + .expect("SHould be hex decodable."), + (Some(path), None) => { + let data = fs::read(path.clone()).await.unwrap_or_else(|_| { + panic!("File {} should exist.", path.to_string_lossy()) + }); + HEXLOWER.decode(&data).expect("SHould be hex decodable.") + } + (_, _) => { + println!("--data-path or --signing-tx required."); + safe_exit(1) + } + }; + + let signing_tx = + SigningTx::try_from_slice(&data).expect("Tx should be deserialiable."); + let keypairs = + tx_signers(&mut ctx, &tx, vec![super::signing::TxSigningKey::None]) + .await; + let keypair = keypairs.get(0).expect("One signer should be provided."); + + let signature = signing_tx.compute_signature(keypair); + let public_key = keypair.ref_to(); + + let offline_signature = OfflineSignature { + sig: signature, + public_key, + }; + + let offline_signature_out = File::create("offline_signature.tx").unwrap(); + serde_json::to_writer_pretty(offline_signature_out, &offline_signature) + .expect("Signature should be deserializable.") +} pub async fn query_unbond_with_slashing( client: &HttpClient, @@ -1745,10 +1791,11 @@ pub async fn dry_run_tx(ledger_address: &TendermintAddress, tx_bytes: Vec) { /// Get account's public key stored in its storage sub-space pub async fn get_public_key( address: &Address, + index: u64, ledger_address: TendermintAddress, ) -> Option { let client = HttpClient::new(ledger_address).unwrap(); - let key = pk_key(address); + let key = pk_key(address, index); query_storage_value(&client, &key).await } @@ -1778,6 +1825,24 @@ pub async fn is_delegator_at( ) } +pub async fn get_address_pks_map( + client: &HttpClient, + address: &Address, +) -> HashMap { + let key = key::pk_prefix_key(address); + let pks_iter = + query_storage_prefix::(client, &key).await; + if let Some(pks) = pks_iter { + HashMap::from_iter( + pks.map(|(_key, pk)| pk) + .zip(0u64..) + .collect::>(), + ) + } else { + HashMap::new() + } +} + /// Check if the address exists on chain. Established address exists if it has a /// stored validity predicate. Implicit and internal addresses always return /// true. @@ -2278,7 +2343,7 @@ pub async fn get_proposal_offline_votes( 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 key = pk_key(&proposal_vote.address, 0); let public_key = query_storage_value(client, &key) .await .expect("Public key should exist."); diff --git a/apps/src/lib/client/signing.rs b/apps/src/lib/client/signing.rs index 9b1a00b987..54548c029c 100644 --- a/apps/src/lib/client/signing.rs +++ b/apps/src/lib/client/signing.rs @@ -1,16 +1,19 @@ //! Helpers for making digital signatures using cryptographic keys from the //! wallet. +use std::collections::HashMap; +use std::fs; + use borsh::BorshSerialize; use namada::ledger::parameters::storage as parameter_storage; use namada::proto::Tx; use namada::types::address::{Address, ImplicitAddress}; -use namada::types::hash::Hash; use namada::types::key::*; use namada::types::storage::Epoch; use namada::types::token; use namada::types::token::Amount; use namada::types::transaction::{hash_tx, Fee, WrapperTx, MIN_FEE}; +use serde::{Deserialize, Serialize}; use super::rpc; use crate::cli::context::{WalletAddress, WalletKeypair}; @@ -20,6 +23,12 @@ use crate::facade::tendermint_config::net::Address as TendermintAddress; use crate::facade::tendermint_rpc::HttpClient; use crate::wallet::Wallet; +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct OfflineSignature { + pub sig: common::Signature, + pub public_key: common::PublicKey, +} + /// Find the public key for the given address and try to load the keypair /// for it from the wallet. Panics if the key cannot be found or loaded. pub async fn find_keypair( @@ -33,7 +42,7 @@ pub async fn find_keypair( "Looking-up public key of {} from the ledger...", addr.encode() ); - let public_key = rpc::get_public_key(addr, ledger_address) + let public_key = rpc::get_public_key(addr, 0, ledger_address) .await .unwrap_or_else(|| { eprintln!( @@ -96,9 +105,9 @@ pub async fn tx_signer( mut default: TxSigningKey, ) -> common::SecretKey { // Override the default signing key source if possible - if let Some(signing_key) = &args.signing_key { + if let Some(signing_key) = args.signing_keys.get(0) { default = TxSigningKey::WalletKeypair(signing_key.clone()); - } else if let Some(signer) = &args.signer { + } else if let Some(signer) = args.signers.get(0) { default = TxSigningKey::WalletAddress(signer.clone()); } // Now actually fetch the signing key and apply it @@ -137,6 +146,67 @@ pub async fn tx_signer( } } +pub async fn tx_signers( + ctx: &mut Context, + args: &args::Tx, + mut default: Vec, +) -> Vec { + if !args.signing_keys.is_empty() { + default = args + .signing_keys + .iter() + .map(|signing_key| TxSigningKey::WalletKeypair(signing_key.clone())) + .collect(); + } else if !args.signers.is_empty() { + default = args + .signers + .iter() + .map(|signing_key| TxSigningKey::WalletAddress(signing_key.clone())) + .collect(); + } + + let mut keys = Vec::new(); + + for key in default { + match key { + TxSigningKey::WalletKeypair(signing_key) => { + keys.push(ctx.get_cached(&signing_key)); + } + TxSigningKey::WalletAddress(signer) => { + 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(ctx, &pk, args).await; + } + keys.push(signing_key); + } + TxSigningKey::SecretKey(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(ctx, &pk, args).await; + keys.push(signing_key); + } + TxSigningKey::None => { + panic!( + "All transactions must be signed; please either specify \ + the key or the address from which to look up the signing \ + key." + ); + } + } + } + + keys +} + /// Sign a transaction with a given signing key or public key of a given signer. /// If no explicit signer given, use the `default`. If no `default` is given, /// panics. @@ -145,22 +215,33 @@ 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 async fn sign_tx_multisignature( mut ctx: Context, tx: Tx, args: &args::Tx, - default: TxSigningKey, + pks_index_map: HashMap, + default: Vec, #[cfg(not(feature = "mainnet"))] requires_pow: bool, ) -> (Context, TxBroadcastData) { - if args.dump_tx { - dump_tx_helper(&ctx, &tx, "unsigned", None); - } - - let keypair = tx_signer(&mut ctx, args, default).await; - let tx = tx.sign(&keypair); - if args.dump_tx { - dump_tx_helper(&ctx, &tx, "signed", None); - } + let keypairs = tx_signers(&mut ctx, args, default).await; + let tx = match args.signatures.is_empty() { + true => tx.sign_multisignature(&keypairs, pks_index_map), + false => { + let signatures = args + .signatures + .iter() + .map(|signature_path| { + let content = fs::read(signature_path) + .expect("Signature file should exist."); + let offline_signature: OfflineSignature = + serde_json::from_slice(&content) + .expect("Signature should be deserializable."); + (offline_signature.public_key, offline_signature.sig) + }) + .collect::>(); + tx.add_signatures(signatures, pks_index_map) + } + }; let epoch = rpc::query_and_print_epoch(args::Query { ledger_address: args.ledger_address.clone(), @@ -174,56 +255,15 @@ pub async fn sign_tx( args, epoch, tx, - &keypair, + &keypairs[0], #[cfg(not(feature = "mainnet"))] requires_pow, ) .await }; - - if args.dump_tx && !args.dry_run { - let (wrapper_tx, wrapper_hash) = match broadcast_data { - TxBroadcastData::DryRun(_) => panic!( - "somehow created a dry run transaction without --dry-run" - ), - TxBroadcastData::Wrapper { - ref tx, - ref wrapper_hash, - decrypted_hash: _, - } => (tx, wrapper_hash), - }; - - dump_tx_helper(&ctx, wrapper_tx, "wrapper", Some(wrapper_hash)); - } - (ctx, broadcast_data) } -pub fn dump_tx_helper( - ctx: &Context, - tx: &Tx, - extension: &str, - precomputed_hash: Option<&String>, -) { - let chain_dir = ctx.config.ledger.chain_dir(); - let hash = match precomputed_hash { - Some(hash) => hash.to_owned(), - None => { - let hash: Hash = tx - .hash() - .as_ref() - .try_into() - .expect("expected hash of dumped tx to be a hash"); - format!("{}", hash) - } - }; - let filename = chain_dir.join(hash).with_extension(extension); - let tx_bytes = tx.to_bytes(); - - std::fs::write(filename, tx_bytes) - .expect("expected to be able to write tx dump file"); -} - /// 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. diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 0014833ca8..8de7a8d275 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1,17 +1,19 @@ use std::borrow::Cow; use std::collections::hash_map::Entry; use std::collections::{BTreeMap, HashMap, HashSet}; -use std::env; use std::fmt::Debug; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; use std::ops::Deref; use std::path::PathBuf; +use std::{env, fs}; use async_std::io::prelude::WriteExt; use async_std::io::{self}; use borsh::{BorshDeserialize, BorshSerialize}; +use data_encoding::HEXLOWER; use itertools::Either::*; +use itertools::Itertools; use masp_primitives::asset_type::AssetType; use masp_primitives::consensus::{BranchId, TestNetwork}; use masp_primitives::convert::AllowedConversion; @@ -65,11 +67,12 @@ use sha2::Digest; use tokio::time::{Duration, Instant}; use super::rpc; +use super::signing::sign_tx_multisignature; use super::types::ShieldedTransferContext; use crate::cli::context::WalletAddress; use crate::cli::{args, safe_exit, Context}; use crate::client::rpc::{query_conversion, query_storage_value}; -use crate::client::signing::{find_keypair, sign_tx, tx_signer, TxSigningKey}; +use crate::client::signing::{find_keypair, tx_signer, TxSigningKey}; use crate::client::tendermint_rpc_types::{TxBroadcastData, TxResponse}; use crate::client::types::ParsedTxTransferArgs; use crate::facade::tendermint_config::net::Address as TendermintAddress; @@ -102,24 +105,50 @@ const ENV_VAR_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: &str = const DEFAULT_NAMADA_EVENTS_MAX_WAIT_TIME_SECONDS: u64 = 60; pub async fn submit_custom(ctx: Context, args: args::TxCustom) { - let tx_code = ctx.read_wasm(args.code_path); - let data = args.data_path.map(|data_path| { - std::fs::read(data_path).expect("Expected a file at given data path") - }); - let tx = Tx::new(tx_code, data); - let (ctx, initialized_accounts) = process_tx( + let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); + + let tx_code = match ctx.try_read_wasm(&args.code_path) { + Some(bytes) => bytes, + None => { + fs::read(args.clone().code_path).expect("Code path file not found.") + } + }; + let tx_data = std::fs::read(args.clone().data_path.unwrap()) + .expect("Expected a file at given data path"); + let timestamp = args.timestamp.unwrap_or_default(); + + let address = match args.address { + Some(address) => ctx.get(&address), + None => { + if args.tx.signers.len() == 1 { + ctx.get(args.tx.signers.first().unwrap()) + } else { + eprintln!("Must specify and address."); + safe_exit(1); + } + } + }; + + // let address = ctx.get(&args.address); + let pks_index_map = rpc::get_address_pks_map(&client, &address).await; + + let tx = Tx::new_with_timestamp(tx_code, Some(tx_data), timestamp); + + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, - TxSigningKey::None, + pks_index_map, + vec![TxSigningKey::None], #[cfg(not(feature = "mainnet"))] false, ) .await; - save_initialized_accounts(ctx, &args.tx, initialized_accounts).await; } pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { + let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); + let addr = ctx.get(&args.addr); // Check that the address is established and exists on chain @@ -166,15 +195,22 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { let tx_code = ctx.read_wasm(TX_UPDATE_VP_WASM); - let data = UpdateVp { addr, vp_code }; + let data = UpdateVp { + addr: addr.clone(), + vp_code, + }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); - process_tx( + + let pks_map = rpc::get_address_pks_map(&client, &addr).await; + + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(args.addr), + pks_map, + vec![TxSigningKey::WalletAddress(args.addr)], #[cfg(not(feature = "mainnet"))] false, ) @@ -182,7 +218,34 @@ pub async fn submit_update_vp(ctx: Context, args: args::TxUpdateVp) { } pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { - let public_key = ctx.get_cached(&args.public_key); + let public_keys: Vec = args + .public_keys + .iter() + .map(|pk| ctx.get_cached(pk)) + .sorted() + .collect(); + + let threshold = match args.threshold { + Some(threshold) => { + if threshold > public_keys.len() as u64 { + eprintln!( + "Threshold must be less or equal to the number of pks." + ); + safe_exit(1); + } else { + threshold + } + } + None => { + if public_keys.len() as u64 == 1 { + 1u64 + } else { + eprintln!("Missing threshold for multisignature account."); + safe_exit(1); + } + } + }; + let vp_code = args .vp_code_path .map(|path| ctx.read_wasm(path)) @@ -197,17 +260,28 @@ pub async fn submit_init_account(mut ctx: Context, args: args::TxInitAccount) { let tx_code = ctx.read_wasm(TX_INIT_ACCOUNT_WASM); let data = InitAccount { - public_key, + public_keys: public_keys.clone(), vp_code, + threshold, }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); + let mut pks_map = HashMap::new(); + for (index, pk) in public_keys.iter().enumerate() { + pks_map.insert(pk.clone(), index as u64); + } + + // TODO: refactor args.source and require either --signer or --signing-keys + // or --signatures + let default_signer = args.source.unwrap(); + let tx = Tx::new(tx_code, Some(data)); let (ctx, initialized_accounts) = process_tx( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(args.source), + pks_map, + vec![TxSigningKey::WalletAddress(default_signer)], #[cfg(not(feature = "mainnet"))] false, ) @@ -221,12 +295,13 @@ pub async fn submit_init_validator( tx: tx_args, source, scheme, - account_key, + account_keys, consensus_key, protocol_key, commission_rate, max_commission_rate_change, validator_vp_code_path, + threshold, unsafe_dont_encrypt, }: args::TxInitValidator, ) { @@ -238,17 +313,47 @@ pub async fn submit_init_validator( let validator_key_alias = format!("{}-key", alias); let consensus_key_alias = format!("{}-consensus-key", alias); - let account_key = ctx.get_opt_cached(&account_key).unwrap_or_else(|| { + + let account_keys = if !account_keys.is_empty() { + account_keys + .iter() + .map(|account_key| ctx.get_cached(account_key)) + .collect() + } else { println!("Generating validator account key..."); - ctx.wallet + let public_key = ctx + .wallet .gen_key( scheme, Some(validator_key_alias.clone()), unsafe_dont_encrypt, ) .1 - .ref_to() - }); + .ref_to(); + vec![public_key] + }; + + let threshold = match threshold { + Some(threshold) => { + if threshold > account_keys.len() as u64 { + eprintln!( + "Threshold must be less or equal to the number of public \ + keys." + ); + safe_exit(1); + } else { + threshold + } + } + None => { + if account_keys.len() as u64 == 1 { + 1u64 + } else { + eprintln!("Missing threshold for multisignature account."); + safe_exit(1); + } + } + }; let consensus_key = ctx .get_opt_cached(&consensus_key) @@ -326,21 +431,24 @@ pub async fn submit_init_validator( let tx_code = ctx.read_wasm(TX_INIT_VALIDATOR_WASM); let data = InitValidator { - account_key, + account_keys, consensus_key: consensus_key.ref_to(), protocol_key, dkg_key, commission_rate, max_commission_rate_change, + threshold, validator_vp_code, }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); + let (mut ctx, initialized_accounts) = process_tx( ctx, &tx_args, tx, - TxSigningKey::WalletAddress(source), + HashMap::new(), + vec![TxSigningKey::WalletAddress(source)], #[cfg(not(feature = "mainnet"))] false, ) @@ -1584,27 +1692,28 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { // 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 (default_signer, amount, token) = - if source == masp_addr && target == masp_addr { - // TODO Refactor me, we shouldn't rely on any specific token here. - ( - TxSigningKey::SecretKey(masp_tx_key()), - 0.into(), - ctx.native_token.clone(), - ) - } else if source == masp_addr { - ( - TxSigningKey::SecretKey(masp_tx_key()), - args.amount, - parsed_args.token.clone(), - ) - } else { - ( - TxSigningKey::WalletAddress(args.source.to_address()), - args.amount, - parsed_args.token.clone(), - ) - }; + let (default_signer, amount, token) = if args.tx.offline_tx { + (TxSigningKey::None, args.amount, parsed_args.token.clone()) + } else if source == masp_addr && target == masp_addr { + // TODO Refactor me, we shouldn't rely on any specific token here. + ( + TxSigningKey::SecretKey(masp_tx_key()), + 0.into(), + ctx.native_token.clone(), + ) + } else if source == masp_addr { + ( + TxSigningKey::SecretKey(masp_tx_key()), + args.amount, + parsed_args.token.clone(), + ) + } else { + ( + TxSigningKey::WalletAddress(args.source.to_address()), + args.amount, + parsed_args.token.clone(), + ) + }; // If our chosen signer is the MASP sentinel key, then our shielded inputs // will need to cover the gas fees. let chosen_signer = tx_signer(&mut ctx, &args.tx, default_signer.clone()) @@ -1622,7 +1731,7 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { rpc::is_faucet_account(&source, args.tx.ledger_address.clone()).await; let transfer = token::Transfer { - source, + source: source.clone(), target, token, sub_prefix, @@ -1672,6 +1781,7 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { } }, }; + tracing::debug!("Transfer data {:?}", transfer); let data = transfer .try_to_vec() @@ -1680,11 +1790,14 @@ pub async fn submit_transfer(mut ctx: Context, args: args::TxTransfer) { let tx = Tx::new(tx_code, Some(data)); let signing_address = TxSigningKey::WalletAddress(args.source.to_address()); - process_tx( + let pks_map = rpc::get_address_pks_map(&client, &source).await; + + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, - signing_address, + pks_map, + vec![signing_address], #[cfg(not(feature = "mainnet"))] is_source_faucet, ) @@ -1798,11 +1911,15 @@ pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) { .expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); + + let pks_map = rpc::get_address_pks_map(&client, &source).await; + process_tx( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(args.source), + pks_map, + vec![TxSigningKey::WalletAddress(args.source)], #[cfg(not(feature = "mainnet"))] false, ) @@ -1944,11 +2061,14 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { let tx_code = ctx.read_wasm(TX_INIT_PROPOSAL); let tx = Tx::new(tx_code, Some(data)); + let pks_map = rpc::get_address_pks_map(&client, &proposal.author).await; + process_tx( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(signer), + pks_map, + vec![TxSigningKey::WalletAddress(signer)], #[cfg(not(feature = "mainnet"))] false, ) @@ -1957,15 +2077,8 @@ pub async fn submit_init_proposal(mut ctx: Context, args: args::InitProposal) { } pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { - let signer = if let Some(addr) = &args.tx.signer { - addr - } else { - eprintln!("Missing mandatory argument --signer."); - safe_exit(1) - }; - if args.offline { - let signer = ctx.get(signer); + let signer = ctx.get(&args.address); let proposal_file_path = args.proposal_data.expect("Proposal file should exist."); let file = File::open(&proposal_file_path).expect("File must exist."); @@ -1974,6 +2087,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { serde_json::from_reader(file).expect("JSON was not well-formatted"); let public_key = rpc::get_public_key( &proposal.address, + 0, args.tx.ledger_address.clone(), ) .await @@ -2020,7 +2134,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { }) .await; - let voter_address = ctx.get(signer); + let voter_address = ctx.get(&args.address); let proposal_id = args.proposal_id.unwrap(); let proposal_start_epoch_key = gov_storage::get_voting_start_epoch_key(proposal_id); @@ -2074,7 +2188,7 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { let tx_data = VoteProposalData { id: proposal_id, vote: args.vote, - voter: voter_address, + voter: voter_address.clone(), delegations: delegations.into_iter().collect(), }; @@ -2084,11 +2198,15 @@ pub async fn submit_vote_proposal(mut ctx: Context, args: args::VoteProposal) { let tx_code = ctx.read_wasm(TX_VOTE_PROPOSAL); let tx = Tx::new(tx_code, Some(data)); + let pks_map = + rpc::get_address_pks_map(&client, &voter_address).await; + process_tx( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(signer.clone()), + pks_map, + vec![TxSigningKey::WalletAddress(args.address)], #[cfg(not(feature = "mainnet"))] false, ) @@ -2140,7 +2258,7 @@ pub async fn has_revealed_pk( addr: &Address, ledger_address: TendermintAddress, ) -> bool { - rpc::get_public_key(addr, ledger_address).await.is_some() + rpc::get_public_key(addr, 0, ledger_address).await.is_some() } pub async fn submit_reveal_pk_aux( @@ -2157,9 +2275,9 @@ pub async fn submit_reveal_pk_aux( 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 { + let keypair = if let Some(signing_key) = args.signing_keys.get(0) { ctx.get_cached(signing_key) - } else if let Some(signer) = args.signer.as_ref() { + } else if let Some(signer) = args.signers.get(0) { let signer = ctx.get(signer); find_keypair(&mut ctx.wallet, &signer, args.ledger_address.clone()) .await @@ -2325,6 +2443,7 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { // balance let bond_source = source.as_ref().unwrap_or(&validator); let balance_key = token::balance_key(&ctx.native_token, bond_source); + let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); match rpc::query_storage_value::(&client, &balance_key).await { Some(balance) => { @@ -2351,19 +2470,23 @@ pub async fn submit_bond(ctx: Context, args: args::Bond) { let tx_code = ctx.read_wasm(TX_BOND_WASM); println!("Wasm tx bond code bytes length = {}\n", tx_code.len()); let bond = pos::Bond { - validator, + validator: validator.clone(), amount: args.amount, - source, + source: source.clone(), }; let data = bond.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - process_tx( + + let pks_map = rpc::get_address_pks_map(&client, bond_source).await; + + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(default_signer), + pks_map, + vec![TxSigningKey::WalletAddress(default_signer)], #[cfg(not(feature = "mainnet"))] false, ) @@ -2414,11 +2537,15 @@ pub async fn submit_unbond(ctx: Context, args: args::Unbond) { let tx_code = ctx.read_wasm(TX_UNBOND_WASM); let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - let (_ctx, _) = process_tx( + + let pks_map = rpc::get_address_pks_map(&client, &bond_source).await; + + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(default_signer), + pks_map, + vec![TxSigningKey::WalletAddress(default_signer)], #[cfg(not(feature = "mainnet"))] false, ) @@ -2473,17 +2600,24 @@ pub async fn submit_withdraw(ctx: Context, args: args::Withdraw) { println!("Submitting transaction to withdraw them..."); } - let data = pos::Withdraw { validator, source }; + let data = pos::Withdraw { + validator: validator.clone(), + source: source.clone(), + }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx_code = ctx.read_wasm(TX_WITHDRAW_WASM); let tx = Tx::new(tx_code, Some(data)); let default_signer = args.source.unwrap_or(args.validator); - process_tx( + + let pks_map = rpc::get_address_pks_map(&client, &bond_source).await; + + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(default_signer), + pks_map, + vec![TxSigningKey::WalletAddress(default_signer)], #[cfg(not(feature = "mainnet"))] false, ) @@ -2565,11 +2699,15 @@ pub async fn submit_validator_commission_change( let tx = Tx::new(tx_code, Some(data)); let default_signer = args.validator; - process_tx( + + let pks_map = rpc::get_address_pks_map(&client, &validator).await; + + let (_ctx, _initialized_accounts) = process_tx( ctx, &args.tx, tx, - TxSigningKey::WalletAddress(default_signer), + pks_map, + vec![TxSigningKey::WalletAddress(default_signer)], #[cfg(not(feature = "mainnet"))] false, ) @@ -2582,27 +2720,53 @@ async fn process_tx( ctx: Context, args: &args::Tx, tx: Tx, - default_signer: TxSigningKey, + pks_index_map: HashMap, + default_signers: Vec, #[cfg(not(feature = "mainnet"))] requires_pow: bool, ) -> (Context, Vec
) { - let (ctx, to_broadcast) = sign_tx( + if args.offline_tx { + // TODO: use async version of fs + tokio::fs::write("code.tx", tx.clone().code) + .await + .expect("Should be able to write a file to disk."); + if let Some(ref data) = tx.data { + tokio::fs::write("data.tx", data) + .await + .expect("Should be able to write a file to disk."); + } + + let signing_tx_blob = tx + .clone() + .signing_tx() + .try_to_vec() + .expect("Tx should be serializable."); + println!( + "Transaction code, data and timestmamp have been written to files \ + code.tx and data.tx." + ); + println!( + "You can share the following blob with the other signers:\n{}\n", + HEXLOWER.encode(&signing_tx_blob) + ); + println!( + "You can later submit the tx with:\nnamada client tx --code-path \ + code.tx --data-path data.tx --timestamp {}", + tx.timestamp.to_rfc3339() + ); + + return (ctx, vec![]); + } + + let (ctx, to_broadcast) = sign_tx_multisignature( ctx, tx, args, - default_signer, + pks_index_map, + default_signers, #[cfg(not(feature = "mainnet"))] requires_pow, ) .await; - // NOTE: use this to print the request JSON body: - - // let request = - // tendermint_rpc::endpoint::broadcast::tx_commit::Request::new( - // tx_bytes.clone().into(), - // ); - // use tendermint_rpc::Request; - // let request_body = request.into_json(); - // println!("HTTP request body: {}", request_body); if args.dry_run { if let TxBroadcastData::DryRun(tx) = to_broadcast { diff --git a/apps/src/lib/client/types.rs b/apps/src/lib/client/types.rs index d75d5a596c..6cd1a143c1 100644 --- a/apps/src/lib/client/types.rs +++ b/apps/src/lib/client/types.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use async_trait::async_trait; use masp_primitives::merkle_tree::MerklePath; use masp_primitives::primitives::{Diversifier, Note, ViewingKey}; @@ -35,10 +37,14 @@ pub struct ParsedTxArgs { pub fee_token: Address, /// The max amount of gas used to process tx pub gas_limit: GasLimit, + /// Dump the signing tx to file + pub offline_tx: bool, /// Sign the tx with the key for the given alias from your wallet - pub signing_key: Option, + pub signing_keys: Vec, /// Sign the tx with the keypair of the public key of the given address - pub signer: Option
, + pub signers: Vec
, + /// The path to signatures + pub signatures: Vec, } #[derive(Clone, Debug)] diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index fec7864306..b912d96dee 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -195,7 +195,7 @@ where .unwrap(); if let Some(pk) = public_key { - let pk_storage_key = pk_key(&address); + let pk_storage_key = pk_key(&address, 0); self.wl_storage .write_bytes(&pk_storage_key, pk.try_to_vec().unwrap()) .unwrap(); @@ -228,7 +228,7 @@ where for genesis::ImplicitAccount { public_key } in genesis.implicit_accounts { let address: address::Address = (&public_key).into(); - let pk_storage_key = pk_key(&address); + let pk_storage_key = pk_key(&address, 0); self.wl_storage.write(&pk_storage_key, public_key).unwrap(); } @@ -304,7 +304,7 @@ where .write_bytes(&Key::validity_predicate(addr), vp_code) .expect("Unable to write user VP"); // Validator account key - let pk_key = pk_key(addr); + let pk_key = pk_key(addr, 0); self.wl_storage .write(&pk_key, &validator.account_key) .expect("Unable to set genesis user public key"); diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 6b4b05b5ad..a8491816a7 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -684,7 +684,7 @@ where self.mode.get_validator_address().map(|addr| { let sk: common::SecretKey = self .wl_storage - .read(&pk_key(addr)) + .read(&pk_key(addr, 0)) .expect( "A validator should have a public key associated with \ it's established account", diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index 11deec9e13..225c648b20 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -302,7 +302,7 @@ mod test_process_proposal { .expect("Test failed"); let new_tx = if let Some(Ok(SignedTxData { data: Some(data), - sig, + sigs, })) = wrapper .data .take() @@ -326,8 +326,8 @@ mod test_process_proposal { code: vec![], data: Some( SignedTxData { - sig, data: Some(new_data), + sigs, } .try_to_vec() .expect("Test failed"), diff --git a/apps/src/lib/wasm_loader/mod.rs b/apps/src/lib/wasm_loader/mod.rs index 9a075fbcf8..d8d092a263 100644 --- a/apps/src/lib/wasm_loader/mod.rs +++ b/apps/src/lib/wasm_loader/mod.rs @@ -292,19 +292,6 @@ pub fn read_wasm( )) } -pub fn read_wasm_or_exit( - wasm_directory: impl AsRef, - file_path: impl AsRef, -) -> Vec { - match read_wasm(wasm_directory, file_path) { - Ok(wasm) => wasm, - Err(err) => { - eprintln!("Error reading wasm: {}", err); - safe_exit(1); - } - } -} - async fn download_wasm(url: String) -> Result, Error> { tracing::info!("Downloading WASM {}...", url); let response = reqwest::get(&url).await; diff --git a/core/src/ledger/storage_api/key.rs b/core/src/ledger/storage_api/key.rs index 6e3eba64aa..ee51f0df4d 100644 --- a/core/src/ledger/storage_api/key.rs +++ b/core/src/ledger/storage_api/key.rs @@ -6,11 +6,25 @@ 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> +pub fn get( + storage: &S, + owner: &Address, + index: u64, +) -> Result> where S: StorageRead, { - let key = pk_key(owner); + let key = pk_key(owner, index); + storage.read(&key) +} + +/// Get the public key associated with the given address. Returns `Ok(None)` if +/// not found. +pub fn threshold(storage: &S, owner: &Address) -> Result> +where + S: StorageRead, +{ + let key = threshold_key(owner); storage.read(&key) } @@ -21,6 +35,6 @@ where S: StorageWrite, { let addr: Address = pk.into(); - let key = pk_key(&addr); + let key = pk_key(&addr, 0); storage.write(&key, pk) } diff --git a/core/src/proto/mod.rs b/core/src/proto/mod.rs index 3271037595..81258eb66a 100644 --- a/core/src/proto/mod.rs +++ b/core/src/proto/mod.rs @@ -3,7 +3,9 @@ pub mod generated; mod types; -pub use types::{Dkg, Error, Signed, SignedTxData, Tx}; +pub use types::{ + Dkg, Error, SignatureIndex, Signed, SignedTxData, SigningTx, Tx, +}; #[cfg(test)] mod tests { diff --git a/core/src/proto/types.rs b/core/src/proto/types.rs index 40e343d1bf..e1997d5517 100644 --- a/core/src/proto/types.rs +++ b/core/src/proto/types.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::hash::{Hash, Hasher}; @@ -37,6 +38,22 @@ pub enum Error { pub type Result = std::result::Result; +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema)] +pub struct SignatureIndex { + pub sig: common::Signature, + pub index: u64, +} + +impl SignatureIndex { + pub fn from_single_signature(sig: common::Signature) -> Self { + Self { sig, index: 0 } + } + + pub fn to_vec(&self) -> Vec { + vec![self.clone()] + } +} + /// This can be used to sign an arbitrary tx. The signature is produced and /// verified on the tx data concatenated with the tx code, however the tx code /// itself is not part of this structure. @@ -50,7 +67,35 @@ pub struct SignedTxData { pub data: Option>, /// The signature is produced on the tx data concatenated with the tx code /// and the timestamp. - pub sig: common::Signature, + pub sigs: Vec, +} + +impl SignedTxData { + pub fn get_signature_by_index( + &self, + index: u64, + ) -> Option { + for signature in &self.sigs { + if signature.index == index { + return Some(signature.sig.clone()); + } + } + None + } + + pub fn total_signatures(&self) -> u64 { + self.sigs.len() as u64 + } + + pub fn from_single_signature( + data: Option>, + signature: common::Signature, + ) -> Self { + Self { + data, + sigs: vec![SignatureIndex::from_single_signature(signature)], + } + } } /// A generic signed data wrapper for Borsh encode-able data. @@ -154,11 +199,10 @@ impl SigningTx { /// Sign a transaction using [`SignedTxData`]. pub fn sign(self, keypair: &common::SecretKey) -> Self { - let to_sign = self.hash(); - let sig = common::SigScheme::sign(keypair, to_sign); + let sig = self.compute_signature(keypair); let signed = SignedTxData { data: self.data, - sig, + sigs: SignatureIndex::from_single_signature(sig).to_vec(), } .try_to_vec() .expect("Encoding transaction data shouldn't fail"); @@ -169,6 +213,86 @@ impl SigningTx { } } + pub fn compute_signature( + &self, + keypair: &common::SecretKey, + ) -> common::Signature { + let to_sign = self.hash(); + common::SigScheme::sign(keypair, to_sign) + } + + pub fn sign_multisignature( + self, + keypairs: &[common::SecretKey], + pks_index_map: HashMap, + ) -> Self { + let signatures = self.compute_signatures(keypairs); + let signature_indexes = + self.compute_signature_indexes(signatures, pks_index_map); + + let signed = SignedTxData { + data: self.data, + sigs: signature_indexes, + }; + + SigningTx { + code_hash: self.code_hash, + data: signed.try_to_vec().ok(), + timestamp: self.timestamp, + } + } + + pub fn add_signatures( + self, + signatures: Vec<(common::PublicKey, common::Signature)>, + pks_index_map: HashMap, + ) -> Self { + let signature_indexes = + self.compute_signature_indexes(signatures, pks_index_map); + let signed = SignedTxData { + data: self.data, + sigs: signature_indexes, + }; + + SigningTx { + code_hash: self.code_hash, + data: signed.try_to_vec().ok(), + timestamp: self.timestamp, + } + } + + fn compute_signature_indexes( + &self, + signatures: Vec<(common::PublicKey, common::Signature)>, + pks_index_map: HashMap, + ) -> Vec { + signatures + .iter() + .filter_map(|(public_key, signature)| { + let pk_index = pks_index_map.get(public_key); + pk_index.map(|index| SignatureIndex { + sig: signature.clone(), + index: *index, + }) + }) + .collect::>() + } + + fn compute_signatures( + &self, + keypairs: &[common::SecretKey], + ) -> Vec<(common::PublicKey, common::Signature)> { + let to_sign = self.hash(); + keypairs + .iter() + .map(|key| { + let signature = common::SigScheme::sign(key, to_sign); + let public_key = key.ref_to(); + (public_key, signature) + }) + .collect() + } + /// Verify that the transaction has been signed by the secret key /// counterpart of the given public key. pub fn verify_sig( @@ -350,6 +474,18 @@ impl Tx { } } + pub fn new_with_timestamp( + code: Vec, + data: Option>, + timestamp: DateTimeUtc, + ) -> Self { + Tx { + code, + data, + timestamp, + } + } + pub fn to_bytes(&self) -> Vec { let mut bytes = vec![]; let tx: types::Tx = self.clone().into(); @@ -375,6 +511,34 @@ impl Tx { .expect("code hashes to unexpected value") } + pub fn signing_tx(self) -> SigningTx { + SigningTx::from(self) + } + + pub fn add_signatures( + self, + signatures: Vec<(common::PublicKey, common::Signature)>, + pks_index_map: HashMap, + ) -> Self { + let code = self.code.clone(); + SigningTx::from(self) + .add_signatures(signatures, pks_index_map) + .expand(code) + .expect("code hashes to unexpected value") + } + + pub fn sign_multisignature( + self, + keypairs: &[common::SecretKey], + pks_index_map: HashMap, + ) -> Self { + let code = self.code.clone(); + SigningTx::from(self) + .sign_multisignature(keypairs, pks_index_map) + .expand(code) + .expect("code hashes to unexpected value") + } + /// Verify that the transaction has been signed by the secret key /// counterpart of the given public key. pub fn verify_sig( diff --git a/core/src/types/key/mod.rs b/core/src/types/key/mod.rs index 157b0f4f5b..1ffd515e12 100644 --- a/core/src/types/key/mod.rs +++ b/core/src/types/key/mod.rs @@ -22,21 +22,50 @@ use super::address::Address; use super::storage::{self, DbKeySeg, Key, KeySeg}; use crate::types::address; -const PK_STORAGE_KEY: &str = "public_key"; +const PK_STORAGE_KEY: &str = "public_keys"; const PROTOCOL_PK_STORAGE_KEY: &str = "protocol_public_key"; +const PK_STORAGE_THRESHOLD_KEY: &str = "threshold"; /// Obtain a storage key for user's public key. -pub fn pk_key(owner: &Address) -> storage::Key { +pub fn pk_key(owner: &Address, index: u64) -> storage::Key { Key::from(owner.to_db_key()) .push(&PK_STORAGE_KEY.to_owned()) .expect("Cannot obtain a storage key") + .push(&index) // this should be fine if the architecture is 64bit + .expect("Cannot obtain a storage key") +} + +/// Obtain a storage key for user's public key. +pub fn pk_prefix_key(owner: &Address) -> storage::Key { + Key::from(owner.to_db_key()) + .push(&PK_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Obtain a storage key for user's threshold. +pub fn threshold_key(owner: &Address) -> storage::Key { + Key::from(owner.to_db_key()) + .push(&PK_STORAGE_THRESHOLD_KEY.to_owned()) + .expect("Cannot obtain a storage key") } /// 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> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(owner), + DbKeySeg::StringSeg(key), + DbKeySeg::StringSeg(_index), + ] if key == PK_STORAGE_KEY => Some(owner), + _ => None, + } +} + +/// Check if the given storage key is a public key. If it is, returns the owner. +pub fn is_threshold_key(key: &Key) -> Option<&Address> { match &key.segments[..] { [DbKeySeg::AddressSeg(owner), DbKeySeg::StringSeg(key)] - if key == PK_STORAGE_KEY => + if key == PK_STORAGE_THRESHOLD_KEY => { Some(owner) } diff --git a/core/src/types/time.rs b/core/src/types/time.rs index 7288d88bab..e5769f1868 100644 --- a/core/src/types/time.rs +++ b/core/src/types/time.rs @@ -111,6 +111,12 @@ impl DateTimeUtc { } } +impl Default for DateTimeUtc { + fn default() -> Self { + DateTimeUtc::now() + } +} + impl FromStr for DateTimeUtc { type Err = ParseError; @@ -241,6 +247,12 @@ impl From for Rfc3339String { } } +impl Display for DateTimeUtc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + #[cfg(any(feature = "tendermint", feature = "tendermint-abcipp"))] impl TryFrom for crate::tendermint::time::Time { type Error = crate::tendermint::Error; diff --git a/core/src/types/transaction/mod.rs b/core/src/types/transaction/mod.rs index 0e0a5e980e..bbdc8c9eb0 100644 --- a/core/src/types/transaction/mod.rs +++ b/core/src/types/transaction/mod.rs @@ -162,9 +162,11 @@ 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, + pub public_keys: Vec, /// The VP code pub vp_code: Vec, + /// The multisignature threshold + pub threshold: u64, } /// A tx data type to initialize a new validator account. @@ -182,7 +184,7 @@ 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, + pub account_keys: Vec, /// A key to be used for signing blocks and votes on blocks. pub consensus_key: common::PublicKey, /// Public key used to sign protocol transactions @@ -194,6 +196,8 @@ pub struct InitValidator { /// The maximum change allowed per epoch to the commission rate. This is /// immutable once set here. pub max_commission_rate_change: Decimal, + /// The multisignature threshold + pub threshold: u64, /// The VP code for validator account pub validator_vp_code: Vec, } @@ -284,35 +288,40 @@ pub mod tx_types { /// indicating it is a wrapper. Otherwise, an error is /// returned indicating the signature was not valid pub fn process_tx(tx: Tx) -> Result { - if let Some(Ok(SignedTxData { - data: Some(data), - ref sig, - })) = tx + if let Some(Ok(tx_data)) = tx .data .as_ref() .map(|data| SignedTxData::try_from_slice(&data[..])) { let signed_hash = Tx { code: tx.code, - data: Some(data.clone()), + data: tx_data.data.clone(), timestamp: tx.timestamp, } .hash(); match TxType::try_from(Tx { code: vec![], - data: Some(data), + data: tx_data.data.clone(), timestamp: tx.timestamp, }) .map_err(|err| TxError::Deserialization(err.to_string()))? { // verify signature and extract signed data TxType::Wrapper(wrapper) => { - wrapper.validate_sig(signed_hash, sig)?; + let sig = + tx_data.get_signature_by_index(0).ok_or_else(|| { + TxError::SigError("Unsigned wrappr".to_string()) + })?; + wrapper.validate_sig(signed_hash, &sig)?; Ok(TxType::Wrapper(wrapper)) } // verify signature and extract signed data TxType::Protocol(protocol) => { - protocol.validate_sig(signed_hash, sig)?; + let sig = + tx_data.get_signature_by_index(0).ok_or_else(|| { + TxError::SigError("Unsigned protocol".to_string()) + })?; + protocol.validate_sig(signed_hash, &sig)?; Ok(TxType::Protocol(protocol)) } // we extract the signed data, but don't check the signature @@ -524,16 +533,13 @@ pub mod tx_types { has_valid_pow: false, }; // Invalid signed data + let data = TxType::Decrypted(decrypted) + .try_to_vec() + .expect("Test failed"); let ed_sig = ed25519::Signature::try_from_slice([0u8; 64].as_ref()).unwrap(); - let signed = SignedTxData { - data: Some( - TxType::Decrypted(decrypted) - .try_to_vec() - .expect("Test failed"), - ), - sig: common::Signature::try_from_sig(&ed_sig).unwrap(), - }; + let signature = common::Signature::try_from_sig(&ed_sig).unwrap(); + let signed = SignedTxData::from_single_signature(Some(data), signature); // create the tx with signed decrypted data let tx = Tx::new(vec![], Some(signed.try_to_vec().expect("Test failed"))); diff --git a/core/src/types/transaction/wrapper.rs b/core/src/types/transaction/wrapper.rs index 70ef2827bc..184926505f 100644 --- a/core/src/types/transaction/wrapper.rs +++ b/core/src/types/transaction/wrapper.rs @@ -488,8 +488,11 @@ pub mod wrapper_tx { tx.data = Some(signed_tx_data.try_to_vec().expect("Test failed")); // check that the signature is not valid - tx.verify_sig(&keypair.ref_to(), &signed_tx_data.sig) - .expect_err("Test failed"); + tx.verify_sig( + &keypair.ref_to(), + &signed_tx_data.sigs.get(0).unwrap().sig, + ) + .expect_err("Test failed"); // check that the try from method also fails let err = crate::types::transaction::process_tx(tx) .expect_err("Test failed"); diff --git a/tests/src/e2e/eth_bridge_tests.rs b/tests/src/e2e/eth_bridge_tests.rs index fe97731b22..450d8a911c 100644 --- a/tests/src/e2e/eth_bridge_tests.rs +++ b/tests/src/e2e/eth_bridge_tests.rs @@ -56,7 +56,7 @@ fn everything() { let ledger_addr = get_actor_rpc(&test, &SOLE_VALIDATOR); let tx_args = vec![ "tx", - "--signer", + "--signers", ALBERT, "--code-path", &tx_code_path, diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index 8151cdaaca..eb045715d5 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -1057,7 +1057,7 @@ fn submit_ibc_tx( &code_path, "--data-path", &data_path, - "--signer", + "--signers", signer, "--gas-amount", "0", @@ -1097,7 +1097,7 @@ fn transfer( sender.as_ref(), "--receiver", &receiver, - "--signer", + "--signers", sender.as_ref(), "--token", token.as_ref(), diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 9437103546..bc6f6d340f 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -358,7 +358,7 @@ fn ledger_txs_and_queries() -> Result<()> { // 4. Submit a custom tx vec![ "tx", - "--signer", + "--signers", BERTHA, "--code-path", &tx_no_op, @@ -378,7 +378,7 @@ fn ledger_txs_and_queries() -> Result<()> { "init-account", "--source", BERTHA, - "--public-key", + "--public-keys", // Value obtained from `namada::types::key::ed25519::tests::gen_keypair` "001be519a321e29020fa3cbfbfd01bd5e92db134305609270b71dace25b5a21168", "--code-path", @@ -407,7 +407,7 @@ fn ledger_txs_and_queries() -> Result<()> { "--amount", "10.1", // Faucet withdrawal requires an explicit signer - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc, @@ -596,7 +596,7 @@ fn masp_txs_and_queries() -> Result<()> { ETH, "--amount", "10", - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc, @@ -615,7 +615,7 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc, @@ -634,7 +634,7 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc, @@ -653,7 +653,7 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "7", - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc, @@ -672,7 +672,7 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "6", - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc, @@ -728,7 +728,7 @@ fn masp_txs_and_queries() -> Result<()> { BTC, "--amount", "20", - "--signer", + "--signers", BERTHA, "--ledger-address", &validator_one_rpc, @@ -1296,7 +1296,7 @@ fn masp_incentives() -> Result<()> { ETH, "--amount", "30", - "--signer", + "--signers", BERTHA, "--ledger-address", &validator_one_rpc @@ -1388,7 +1388,7 @@ fn masp_incentives() -> Result<()> { BTC, "--amount", "20", - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc @@ -1546,7 +1546,7 @@ fn masp_incentives() -> Result<()> { NAM, "--amount", &((amt30 * masp_rewards[ð()]).0 * (ep5.0 - ep3.0)).to_string(), - "--signer", + "--signers", BERTHA, "--ledger-address", &validator_one_rpc @@ -1573,7 +1573,7 @@ fn masp_incentives() -> Result<()> { NAM, "--amount", &((amt20 * masp_rewards[&btc()]).0 * (ep6.0 - ep0.0)).to_string(), - "--signer", + "--signers", ALBERT, "--ledger-address", &validator_one_rpc @@ -1688,7 +1688,9 @@ fn invalid_transactions() -> Result<()> { &tx_wasm_path, "--data-path", &tx_data_path, - "--signing-key", + "--signing-keys", + &daewon_lower, + "--address", &daewon_lower, "--gas-amount", "0", @@ -1737,7 +1739,7 @@ fn invalid_transactions() -> Result<()> { "transfer", "--source", DAEWON, - "--signing-key", + "--signing-keys", &daewon_lower, "--target", ALBERT, @@ -2500,7 +2502,7 @@ fn proposal_submission() -> Result<()> { "0", "--vote", "yay", - "--signer", + "--address", "validator-0", "--ledger-address", &validator_one_rpc, @@ -2524,7 +2526,7 @@ fn proposal_submission() -> Result<()> { "0", "--vote", "nay", - "--signer", + "--address", BERTHA, "--ledger-address", &validator_one_rpc, @@ -2544,7 +2546,7 @@ fn proposal_submission() -> Result<()> { "0", "--vote", "yay", - "--signer", + "--address", ALBERT, "--ledger-address", &validator_one_rpc, @@ -2732,7 +2734,7 @@ fn proposal_offline() -> Result<()> { proposal_path.to_str().unwrap(), "--vote", "yay", - "--signer", + "--address", ALBERT, "--offline", "--ledger-address", diff --git a/tests/src/vm_host_env/mod.rs b/tests/src/vm_host_env/mod.rs index 04a545e8b1..a0f7d70edf 100644 --- a/tests/src/vm_host_env/mod.rs +++ b/tests/src/vm_host_env/mod.rs @@ -434,7 +434,7 @@ mod tests { let addr = address::testing::established_address_1(); // Write the public key to storage - let pk_key = key::pk_key(&addr); + let pk_key = key::pk_key(&addr, 0); let keypair = key::testing::keypair_1(); let pk = keypair.ref_to(); env.wl_storage @@ -461,7 +461,10 @@ mod tests { assert_eq!(&signed_tx_data.data, data); assert!( vp::CTX - .verify_tx_signature(&pk, &signed_tx_data.sig) + .verify_tx_signature( + &pk, + &signed_tx_data.sigs.get(0).unwrap().sig + ) .unwrap() ); @@ -470,7 +473,7 @@ mod tests { !vp::CTX .verify_tx_signature( &other_keypair.ref_to(), - &signed_tx_data.sig + &signed_tx_data.sigs.get(0).unwrap().sig ) .unwrap() ); diff --git a/tests/src/vm_host_env/tx.rs b/tests/src/vm_host_env/tx.rs index 6c3ccd55ae..5f7c57a303 100644 --- a/tests/src/vm_host_env/tx.rs +++ b/tests/src/vm_host_env/tx.rs @@ -186,14 +186,28 @@ impl TestTxEnv { &mut self, address: &Address, public_key: &key::common::PublicKey, + index: u64, ) { - let storage_key = key::pk_key(address); + let storage_key = key::pk_key(address, index); self.wl_storage .storage .write(&storage_key, public_key.try_to_vec().unwrap()) .unwrap(); } + /// Set public key for the address. + pub fn write_account_threshold( + &mut self, + address: &Address, + threshold: u64, + ) { + let storage_key = key::threshold_key(address); + self.wl_storage + .storage + .write(&storage_key, threshold.try_to_vec().unwrap()) + .unwrap(); + } + /// Apply the tx changes to the write log. pub fn execute_tx(&mut self) -> Result<(), Error> { let empty_data = vec![]; diff --git a/tx_prelude/src/account.rs b/tx_prelude/src/account.rs new file mode 100644 index 0000000000..4bf08339f8 --- /dev/null +++ b/tx_prelude/src/account.rs @@ -0,0 +1,18 @@ +use namada_core::types::key::pk_key; +use namada_core::types::transaction::InitAccount; + +use super::*; + +pub fn init_account(ctx: &mut Ctx, data: InitAccount) -> EnvResult
{ + let address = ctx.init_account(&data.vp_code)?; + + let pk_threshold = key::threshold_key(&address); + ctx.write(&pk_threshold, data.threshold)?; + + for (pk, index) in data.public_keys.iter().zip(0u64..) { + let pk_key = pk_key(&address, index); + ctx.write(&pk_key, pk)?; + } + + Ok(address) +} diff --git a/tx_prelude/src/lib.rs b/tx_prelude/src/lib.rs index d7d6b84c96..df6ca73cc9 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; diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 6e4c4cf136..aab4d5d0bf 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -1,6 +1,6 @@ //! Proof of Stake system integration with functions for transactions -use namada_core::types::transaction::InitValidator; +use namada_core::types::transaction::{InitAccount, InitValidator}; use namada_core::types::{key, token}; pub use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::{ @@ -66,20 +66,23 @@ impl Ctx { pub fn init_validator( &mut self, InitValidator { - account_key, + account_keys, consensus_key, protocol_key, dkg_key, commission_rate, max_commission_rate_change, + threshold, validator_vp_code, }: InitValidator, ) -> EnvResult
{ let current_epoch = self.get_block_epoch()?; - // Init validator account - let validator_address = self.init_account(&validator_vp_code)?; - let pk_key = key::pk_key(&validator_address); - self.write(&pk_key, &account_key)?; + let account_data = InitAccount { + public_keys: account_keys, + threshold, + vp_code: validator_vp_code, + }; + let validator_address = account::init_account(self, account_data)?; 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/vm_env/src/common.rs b/vm_env/src/common.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vp_prelude/src/key.rs b/vp_prelude/src/key.rs index 95946ff268..e37ff7a92f 100644 --- a/vp_prelude/src/key.rs +++ b/vp_prelude/src/key.rs @@ -7,6 +7,16 @@ use super::*; /// 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> { - storage_api::key::get(&ctx.pre(), owner) +pub fn get( + ctx: &Ctx, + owner: &Address, + index: u64, +) -> EnvResult> { + storage_api::key::get(&ctx.pre(), owner, index) +} + +/// Get the threshold associated with the given address from the state prior to +/// tx execution. Returns `Ok(None)` if not found. +pub fn threshold(ctx: &Ctx, owner: &Address) -> EnvResult> { + storage_api::key::threshold(&ctx.pre(), owner) } diff --git a/vp_prelude/src/lib.rs b/vp_prelude/src/lib.rs index 0d0680a2e6..1d5dddc139 100644 --- a/vp_prelude/src/lib.rs +++ b/vp_prelude/src/lib.rs @@ -40,6 +40,36 @@ use namada_vm_env::vp::*; use namada_vm_env::{read_from_buffer, read_key_val_bytes_from_buffer}; pub use sha2::{Digest, Sha256, Sha384, Sha512}; +pub fn verify_signatures( + ctx: &Ctx, + signed_tx_data: &SignedTxData, + addr: &Address, +) -> bool { + let threshold = match key::threshold(ctx, addr) { + Ok(Some(threshold)) => threshold, + _ => 1, + }; + if signed_tx_data.total_signatures() < threshold { + return false; + } + let mut valid_signatures = 0; + for sig_data in &signed_tx_data.sigs { + let pk = key::get(ctx, addr, sig_data.index); + if let Ok(Some(public_key)) = pk { + let signature_result = ctx + .verify_tx_signature(&public_key, &sig_data.sig) + .unwrap_or(false); + if signature_result { + valid_signatures += 1; + } + if valid_signatures >= threshold { + return true; + } + } + } + valid_signatures >= threshold +} + pub fn sha256(bytes: &[u8]) -> Hash { let digest = Sha256::digest(bytes); Hash(*digest.as_ref()) diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 3d3d7d8280..bc956d61dd 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -2657,6 +2657,7 @@ dependencies = [ "namada_vp_prelude", "once_cell", "proptest", + "rand 0.8.5", "rust_decimal", "tracing", "tracing-subscriber", diff --git a/wasm/checksums.json b/wasm/checksums.json index 7c2b824921..a57216d31a 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,20 +1,20 @@ { - "tx_bond.wasm": "tx_bond.f0094b887c57565472bede01d98fb77f6faac2f72597e2efb2ebfe9b1bf7c234.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.02dca468021b1ec811d0f35cc4b55a24f7c3f7b5e51f16399709257421f4a1f4.wasm", - "tx_ibc.wasm": "tx_ibc.a1735e3221f1ae055c74bb52327765dd37e8676e15fab496f9ab0ed4d0628f51.wasm", - "tx_init_account.wasm": "tx_init_account.7b6eafeceb81b679c382279a5d9c40dfd81fcf37e5a1940340355c9f55af1543.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.f2ed71fe70fc564e1d67e4e7d2ea25466327b62ba2eee18ece0021abff9e2c82.wasm", - "tx_init_validator.wasm": "tx_init_validator.fedcfaecaf37e3e7d050c76a4512baa399fc528710a27038573df53596613a2c.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.3e5417561e8108d4045775bf6d095cbaad22c73ff17a5ba2ad11a1821665a58a.wasm", - "tx_transfer.wasm": "tx_transfer.833a3849ca2c417f4e907c95c6eb15e6b52827458cf603e1c4f5511ab3e4fe76.wasm", - "tx_unbond.wasm": "tx_unbond.d4fd6c94abb947533a2728940b43fb021a008ad593c7df7a3886c4260cac39b5.wasm", - "tx_update_vp.wasm": "tx_update_vp.6d1eabab15dc6d04eec5b25ad687f026a4d6c3f118a1d7aca9232394496bd845.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.54b594f686a72869b0d7f15492591854f26e287a1cf3b6e543b0246d5ac61003.wasm", - "tx_withdraw.wasm": "tx_withdraw.342c222d0707eb5b5a44b89fc1245f527be3fdf841af64152a9ab35a4766e1b5.wasm", - "vp_implicit.wasm": "vp_implicit.73678ac01aa009ac4e0d4a49eecaa19b49cdd3d95f6862a9558c9b175ae68260.wasm", - "vp_masp.wasm": "vp_masp.85446251f8e1defed81549dab37edfe8e640339c7230e678b65340cf71ce1369.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.573b882a806266d6cdfa635fe803e46d6ce89c99321196c231c61d05193a086d.wasm", - "vp_token.wasm": "vp_token.8c6e5a86f047e7b1f1004f0d8a4e91fad1b1c0226a6e42d7fe350f98dc84359b.wasm", - "vp_user.wasm": "vp_user.75c68f018f163d18d398cb4082b261323d115aae43ec021c868d1128e4b0ee29.wasm", - "vp_validator.wasm": "vp_validator.2dc9f1c8f106deeef5ee988955733955444d16b400ebb16a25e7d71e4b1be874.wasm" + "tx_bond.wasm": "tx_bond.e7ce9a96968175c9834f946abd58619b03d50466ccb01c6777144479f314a359.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.627360703b218fe83912d9e4f32b294df09e47e89d04a341ef5a40787ab84195.wasm", + "tx_ibc.wasm": "tx_ibc.81665084a4d1f3f6d0d0bf996e0f5eee88b513da445e606d281b68b35ab88cdd.wasm", + "tx_init_account.wasm": "tx_init_account.69eac03e2aab6bc263c0b9cee71fc40fd17e708cb9211bd32b66f463ae97532a.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.69c91a7edcece48053a345f0ded721dc48857a91d9bfa32c8ba5498a2bcf3f49.wasm", + "tx_init_validator.wasm": "tx_init_validator.422ab5d40d1a268f28d879f8300cd7d3f92bed5da198c3db5bd2449902fdaf78.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.98e7e0084363c1aca40d2a2f2b87f66bd36145b64f249508c64292cadf75ff63.wasm", + "tx_transfer.wasm": "tx_transfer.4f656bc6f4459d065e0dd4ce9ae4aa5278f53cd29adf69d6a2060180e432e164.wasm", + "tx_unbond.wasm": "tx_unbond.c065510900650e995d850c2887a230134d0547494ea4670afcb87c8e701688b5.wasm", + "tx_update_vp.wasm": "tx_update_vp.67ba11b57e8c144886b4355a4d62910b3ec848358637723e9072e47223929c65.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.c5258a6047bc599f78b95a9a4e0147f7ec07b1efdeb403d4b814afa16953013a.wasm", + "tx_withdraw.wasm": "tx_withdraw.ca7549691c43f2ea68b7ad1c4fe208e37995860457a604fbd20f1579d8b54eae.wasm", + "vp_implicit.wasm": "vp_implicit.7c5a53331188da2ff33e1fb5809148115d381ea81c28aaaaf3f5405c530ae099.wasm", + "vp_masp.wasm": "vp_masp.74de81daf2d1ebd219e2969b58ae52d6233794194d7da3d8d07c506dd1fa5767.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.6e2cfc3281956cd39100cdd353b77afd517c61ffc6fe64efa91bdcc13df6a65d.wasm", + "vp_token.wasm": "vp_token.74c4efcc7e9a9258965cc50e4e42ec9c9920ea58ff3fa358573051144b2de6f1.wasm", + "vp_user.wasm": "vp_user.c2a7e556b61c46f5fd6763d970088469d9bf60eff11cadabb57f40911c05aac8.wasm", + "vp_validator.wasm": "vp_validator.5251edeb5e18d2da681eca0caf3766312f6239c238b1da055bb9a3baec51552a.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index a2cd2ba674..c4d2772de7 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -53,3 +53,4 @@ proptest = {git = "https://github.com/heliaxdev/proptest", branch = "tomas/sm"} tracing = "0.1.30" tracing-subscriber = {version = "0.3.7", default-features = false, features = ["env-filter", "fmt"]} rust_decimal = "1.26.1" +rand = "0.8.5" diff --git a/wasm/wasm_source/src/tx_init_account.rs b/wasm/wasm_source/src/tx_init_account.rs index e0fe700d63..4104087ad3 100644 --- a/wasm/wasm_source/src/tx_init_account.rs +++ b/wasm/wasm_source/src/tx_init_account.rs @@ -12,8 +12,14 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { .wrap_err("failed to decode InitAccount")?; debug_log!("apply_tx called to init a new established account"); - let address = ctx.init_account(&tx_data.vp_code)?; - let pk_key = key::pk_key(&address); - ctx.write(&pk_key, &tx_data.public_key)?; + match account::init_account(ctx, tx_data) { + Ok(account_address) => { + debug_log!("Created account {}", account_address.encode()) + } + Err(err) => { + debug_log!("Account creation failed with: {}", err); + panic!() + } + } Ok(()) } diff --git a/wasm/wasm_source/src/vp_implicit.rs b/wasm/wasm_source/src/vp_implicit.rs index 31a920a540..b43f8edcd4 100644 --- a/wasm/wasm_source/src/vp_implicit.rs +++ b/wasm/wasm_source/src/vp_implicit.rs @@ -15,6 +15,7 @@ use namada_vp_prelude::storage::KeySeg; use namada_vp_prelude::*; use once_cell::unsync::Lazy; +#[derive(Debug)] enum KeyType<'a> { /// Public key - written once revealed Pk(&'a Address), @@ -68,18 +69,7 @@ fn validate_tx( 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, - } - } + Ok(signed_tx_data) => verify_signatures(ctx, signed_tx_data, &addr), _ => false, }); @@ -287,7 +277,8 @@ mod tests { assert!( !validate_tx(&CTX, tx_data, addr, keys_changed, verifiers).unwrap(), - "Revealing PK that's already revealed should be rejected" + "Revealing PK that's already + revealed should be rejected" ); } @@ -310,7 +301,7 @@ 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); + let key = namada_tx_prelude::key::pk_key(&addr, 0); tx_host_env::ctx().write(&key, &mismatched_pk).unwrap(); }); @@ -480,7 +471,7 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -576,7 +567,7 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -730,7 +721,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); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -813,7 +804,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -855,7 +846,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { diff --git a/wasm/wasm_source/src/vp_testnet_faucet.rs b/wasm/wasm_source/src/vp_testnet_faucet.rs index 1b8802df6e..211b1d538f 100644 --- a/wasm/wasm_source/src/vp_testnet_faucet.rs +++ b/wasm/wasm_source/src/vp_testnet_faucet.rs @@ -29,18 +29,7 @@ fn validate_tx( 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, - } - } + Ok(signed_tx_data) => verify_signatures(ctx, signed_tx_data, &addr), _ => false, }); @@ -239,7 +228,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.write_public_key(&vp_owner, public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -353,10 +342,7 @@ mod tests { // The signature itself doesn't matter and is not being checked in this // test, it's just used to construct `SignedTxData` let sig = key::common::SigScheme::sign(&target_key, &solution_bytes); - let signed_solution = SignedTxData { - data: Some(solution_bytes), - sig, - }; + let signed_solution = SignedTxData::from_single_signature(Some(solution_bytes), sig); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -403,7 +389,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.write_public_key(&vp_owner, public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { diff --git a/wasm/wasm_source/src/vp_user.rs b/wasm/wasm_source/src/vp_user.rs index b8cbc20982..056f35e731 100644 --- a/wasm/wasm_source/src/vp_user.rs +++ b/wasm/wasm_source/src/vp_user.rs @@ -68,18 +68,7 @@ fn validate_tx( 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, - } - } + Ok(signed_tx_data) => verify_signatures(ctx, signed_tx_data, &addr), _ => false, }); @@ -190,6 +179,8 @@ fn validate_tx( #[cfg(test)] mod tests { + use std::collections::HashMap; + use address::testing::arb_non_internal_address; use namada::ledger::pos::{GenesisValidator, PosParams}; use namada::types::storage::Epoch; @@ -200,8 +191,9 @@ 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::key::RefTo; + use namada_vp_prelude::key::{common, ed25519, RefTo, SecretKey}; use proptest::prelude::*; + use rand::seq::SliceRandom; use storage::testing::arb_account_storage_key_no_vp; use super::*; @@ -337,7 +329,7 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -475,7 +467,7 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -624,7 +616,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -700,7 +692,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -741,7 +733,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -784,7 +776,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -831,7 +823,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -873,7 +865,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -897,4 +889,126 @@ mod tests { .unwrap() ); } + + proptest! { + /// Test that a signed tx that performs arbitrary storage writes or + /// deletes to the account is accepted. + #[test] + fn test_multisignature_accept( + (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::>>(), + signers_total in (1u64..40) + ) { + let mut random = rand::thread_rng(); + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let keypairs: Vec = (0..=signers_total).map(|_| { + common::SecretKey::try_from_sk(&key::testing::gen_keypair::()).unwrap() + }).collect(); + + let public_keys: Vec = keypairs.iter().map(|keypair| { + keypair.ref_to() + }).collect(); + + // 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 mut pks_index_map = HashMap::new(); + for (pk, index) in public_keys.iter().zip(0u64..) { + tx_env.write_public_key(&vp_owner, pk, index); + pks_index_map.insert(pk.to_owned(), index); + } + + let threshold: u64 = random.gen_range(1..=signers_total); + tx_env.write_account_threshold(&vp_owner, threshold); + + // 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_multisignature(&keypairs.choose_multiple(&mut random, threshold as usize).cloned().collect::>(), pks_index_map); + 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()); + } + } + + proptest! { + /// Test that a signed tx that performs arbitrary storage writes or + /// deletes to the account is accepted. + #[test] + fn test_multisignature_reject( + (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::>>(), + signers_total in (0u64..15) + ) { + let mut random = rand::thread_rng(); + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let keypairs: Vec = (0..=signers_total).map(|_| { + common::SecretKey::try_from_sk(&key::testing::gen_keypair::()).unwrap() + }).collect(); + + let public_keys: Vec = keypairs.iter().map(|keypair| { + keypair.ref_to() + }).collect(); + + // 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 mut pks_index_map = HashMap::new(); + for (pk, index) in public_keys.iter().zip(0u64..) { + tx_env.write_public_key(&vp_owner, pk, index); + pks_index_map.insert(pk.to_owned(), index); + } + + let threshold: u64 = if signers_total == 0 { + 1 + } else { + random.gen_range(1..=signers_total) + }; + tx_env.write_account_threshold(&vp_owner, threshold); + + // 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_multisignature(&keypairs.choose_multiple(&mut random, (threshold - 1) as usize).cloned().collect::>(), pks_index_map); + 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()); + } + } } diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs index c9e4700d8e..ac286f9b29 100644 --- a/wasm/wasm_source/src/vp_validator.rs +++ b/wasm/wasm_source/src/vp_validator.rs @@ -68,18 +68,7 @@ fn validate_tx( 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, - } - } + Ok(signed_tx_data) => verify_signatures(ctx, signed_tx_data, &addr), _ => false, }); @@ -346,7 +335,7 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -490,7 +479,7 @@ mod tests { // be able to transfer from it tx_env.credit_tokens(&vp_owner, &token, None, amount); - tx_env.write_public_key(&vp_owner, &public_key); + tx_env.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -645,7 +634,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { @@ -721,7 +710,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -762,7 +751,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -805,7 +794,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -852,7 +841,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { @@ -894,7 +883,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.write_public_key(&vp_owner, &public_key, 0); // Initialize VP environment from a transaction vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { diff --git a/wasm_for_tests/tx_memory_limit.wasm b/wasm_for_tests/tx_memory_limit.wasm index f8db36d004..4c31ef06dd 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 5713105bbe..25ae84c413 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 105a68cd1b..3e0b1ef997 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 f05e0e95bd..9a241a8b5e 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 ff5d78b8e1..b7b78af428 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 761eded647..ed80d2321e 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 f0693e332d..f7dc23ea65 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 27fe81fde5..39e34bc6ed 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 192b3a9367..ca12412f9b 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 717ceeba4d..ea17ea9f9e 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 3790bd302d..5add498ee5 100755 Binary files a/wasm_for_tests/vp_read_storage_key.wasm and b/wasm_for_tests/vp_read_storage_key.wasm differ