diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index 53811d43bdf..c3b4fd448cf 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -3,7 +3,7 @@ use color_eyre::eyre::Result; use namada_apps::cli; use namada_apps::cli::cmds::*; -use namada_apps::client::eth_bridge::bridge_pool; +use namada_apps::client::eth_bridge::{bridge_pool, validator_set}; use namada_apps::client::{rpc, tx, utils}; pub async fn main() -> Result<()> { @@ -49,10 +49,15 @@ pub async fn main() -> Result<()> { Sub::Withdraw(Withdraw(args)) => { tx::submit_withdraw(ctx, args).await; } - // Eth bridge pool + // Eth bridge Sub::AddToEthBridgePool(args) => { bridge_pool::add_to_eth_bridge_pool(ctx, args.0).await; } + Sub::SubmitValidatorSetUpdate(SubmitValidatorSetUpdate( + args, + )) => { + validator_set::submit_validator_set_update(ctx, args).await; + } // Ledger queries Sub::QueryEpoch(QueryEpoch(args)) => { rpc::query_and_print_epoch(args).await; diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 8c479d6935a..ef1d28b6d69 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -220,8 +220,9 @@ pub mod cmds { .subcommand(Bond::def().display_order(2)) .subcommand(Unbond::def().display_order(2)) .subcommand(Withdraw::def().display_order(2)) - // Ethereum bridge pool + // Ethereum bridge .subcommand(AddToEthBridgePool::def().display_order(3)) + .subcommand(SubmitValidatorSetUpdate::def().display_order(3)) // Queries .subcommand(QueryEpoch::def().display_order(4)) .subcommand(QueryTransfers::def().display_order(4)) @@ -279,6 +280,8 @@ pub mod cmds { Self::parse_with_ctx(matches, QueryProtocolParameters); let add_to_eth_bridge_pool = Self::parse_with_ctx(matches, AddToEthBridgePool); + let submit_validator_set_update = + Self::parse_with_ctx(matches, SubmitValidatorSetUpdate); let utils = SubCmd::parse(matches).map(Self::WithoutContext); tx_custom .or(tx_transfer) @@ -293,6 +296,7 @@ pub mod cmds { .or(unbond) .or(withdraw) .or(add_to_eth_bridge_pool) + .or(submit_validator_set_update) .or(query_epoch) .or(query_transfers) .or(query_conversions) @@ -357,6 +361,7 @@ pub mod cmds { Unbond(Unbond), Withdraw(Withdraw), AddToEthBridgePool(AddToEthBridgePool), + SubmitValidatorSetUpdate(SubmitValidatorSetUpdate), QueryEpoch(QueryEpoch), QueryTransfers(QueryTransfers), QueryConversions(QueryConversions), @@ -1715,6 +1720,25 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct SubmitValidatorSetUpdate(pub args::SubmitValidatorSetUpdate); + + impl SubCmd for SubmitValidatorSetUpdate { + const CMD: &'static str = "validator-set-update"; + + fn parse(matches: &ArgMatches) -> Option { + matches.subcommand_matches(Self::CMD).map(|matches| { + Self(args::SubmitValidatorSetUpdate::parse(matches)) + }) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Submit a validator set update protocol tx.") + .add_args::() + } + } + #[derive(Clone, Debug)] pub struct ConstructProof(pub args::BridgePoolProof); @@ -2334,6 +2358,31 @@ pub mod args { } } + /// A transfer to be added to the Ethereum bridge pool. + #[derive(Clone, Debug)] + pub struct SubmitValidatorSetUpdate { + /// The query parameters. + pub query: Query, + /// The epoch of the validator set to relay. + pub epoch: Option, + } + + impl Args for SubmitValidatorSetUpdate { + fn parse(matches: &ArgMatches) -> Self { + let epoch = EPOCH.parse(matches); + let query = Query::parse(matches); + Self { epoch, query } + } + + fn def(app: App) -> App { + app.add_args::().arg( + EPOCH + .def() + .about("The epoch of the validator set to relay."), + ) + } + } + #[derive(Debug, Clone)] pub struct RecommendBatch { /// The query parameters. diff --git a/apps/src/lib/client/eth_bridge/bridge_pool.rs b/apps/src/lib/client/eth_bridge/bridge_pool.rs index 26e18ba71a6..430b9126c80 100644 --- a/apps/src/lib/client/eth_bridge/bridge_pool.rs +++ b/apps/src/lib/client/eth_bridge/bridge_pool.rs @@ -396,7 +396,7 @@ mod recommendations { let voting_powers = RPC .shell() .eth_bridge() - .eth_voting_powers(&client, &height) + .voting_powers_at_height(&client, &height) .await .unwrap(); let valset_size = voting_powers.len() as u64; diff --git a/apps/src/lib/client/eth_bridge/validator_set.rs b/apps/src/lib/client/eth_bridge/validator_set.rs index eefa9c9a723..99f42c8d08a 100644 --- a/apps/src/lib/client/eth_bridge/validator_set.rs +++ b/apps/src/lib/client/eth_bridge/validator_set.rs @@ -1,21 +1,109 @@ use std::cmp::Ordering; use std::sync::Arc; +use borsh::BorshSerialize; use data_encoding::HEXLOWER; use ethbridge_governance_contract::Governance; use futures::future::FutureExt; use namada::core::types::storage::Epoch; +use namada::core::types::vote_extensions::validator_set_update; use namada::eth_bridge::ethers::abi::{AbiDecode, AbiType, Tokenizable}; use namada::eth_bridge::ethers::core::types::TransactionReceipt; use namada::eth_bridge::ethers::providers::{Http, Provider}; use namada::eth_bridge::structs::{Signature, ValidatorSetArgs}; use namada::ledger::queries::RPC; +use namada::proto::Tx; +use namada::types::key::RefTo; +use namada::types::transaction::protocol::{ProtocolTx, ProtocolTxType}; +use namada::types::transaction::TxType; use tokio::time::{Duration, Instant}; use super::{block_on_eth_sync, eth_sync_or, eth_sync_or_exit}; -use crate::cli::{args, safe_exit}; +use crate::cli::{args, safe_exit, Context}; use crate::client::eth_bridge::BlockOnEthSync; -use crate::facade::tendermint_rpc::HttpClient; +use crate::facade::tendermint_rpc::{Client, HttpClient}; + +/// Submit a validator set update protocol tx to the network. +pub async fn submit_validator_set_update( + mut ctx: Context, + args: args::SubmitValidatorSetUpdate, +) { + let maybe_validator_data = ctx.wallet.take_validator_data(); + let Some(validator_data) = maybe_validator_data else { + println!("No validator keys found in the Namada directory."); + safe_exit(1); + }; + + let args::SubmitValidatorSetUpdate { + query, + epoch: maybe_epoch, + } = args; + + let client = HttpClient::new(query.ledger_address).unwrap(); + + let epoch = if let Some(epoch) = maybe_epoch { + epoch + } else { + RPC.shell().epoch(&client).await.unwrap().next() + }; + + if epoch.0 == 0 { + println!( + "Validator set update proofs should only be requested from epoch \ + 1 onwards" + ); + safe_exit(1); + } + + let voting_powers = match RPC + .shell() + .eth_bridge() + .voting_powers_at_epoch(&client, &epoch) + .await + { + Ok(voting_powers) => voting_powers, + Err(e) => { + println!("Failed to get voting powers: {e}"); + safe_exit(1); + } + }; + let protocol_tx = ProtocolTxType::ValSetUpdateVext( + validator_set_update::Vext { + voting_powers, + signing_epoch: epoch - 1, + validator_addr: validator_data.address, + } + .sign(&validator_data.keys.eth_bridge_keypair), + ); + let tx = Tx::new( + vec![], + Some( + TxType::Protocol(ProtocolTx { + pk: validator_data.keys.protocol_keypair.ref_to(), + tx: protocol_tx, + }) + .try_to_vec() + .expect("Could not serialize ProtocolTx"), + ), + ) + .sign(&validator_data.keys.protocol_keypair); + + let response = match client.broadcast_tx_sync(tx.to_bytes().into()).await { + Ok(response) => response, + Err(e) => { + println!("Failed to broadcast protocol tx: {e}"); + safe_exit(1); + } + }; + + if response.code == 0.into() { + println!("Transaction added to mempool: {:?}", response); + } else { + let err = serde_json::to_string(&response).unwrap(); + eprintln!("Encountered error while broadcasting transaction: {err}"); + safe_exit(1); + } +} /// Query an ABI encoding of the validator set to be installed /// at the given epoch, and its associated proof. diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 145450770b5..3257c04f87c 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -441,7 +441,7 @@ where .unwrap(), ); wallet - .take_validator_data() + .into_validator_data() .map(|data| ShellMode::Validator { data, broadcast_sender, diff --git a/apps/src/lib/wallet/mod.rs b/apps/src/lib/wallet/mod.rs index c6a806912fb..63675446b72 100644 --- a/apps/src/lib/wallet/mod.rs +++ b/apps/src/lib/wallet/mod.rs @@ -41,6 +41,8 @@ pub enum FindKeyError { KeyNotFound, #[error("{0}")] KeyDecryptionError(keys::DecryptionError), + #[error("Expected a Secp256k1 key, but found another key kind")] + NotSecp256k1Error, } impl Wallet { @@ -168,6 +170,12 @@ impl Wallet { protocol_pk: Option, protocol_key_scheme: SchemeType, ) -> Result { + match ð_bridge_pk { + Some(common::PublicKey::Secp256k1(_)) | None => {} + _ => { + return Err(FindKeyError::NotSecp256k1Error); + } + } let protocol_keypair = self .find_secret_key(protocol_pk, |data| data.keys.protocol_keypair)?; let eth_bridge_keypair = self @@ -215,15 +223,20 @@ impl Wallet { self.store.add_validator_data(address, keys); } - /// Returns the validator data, if it exists. + /// Returns a reference to the validator data, if it exists. pub fn get_validator_data(&self) -> Option<&ValidatorData> { self.store.get_validator_data() } + /// Take the validator data, if it exists. + pub fn take_validator_data(&mut self) -> Option { + self.store.validator_data.take() + } + /// Returns the validator data, if it exists. /// [`Wallet::save`] cannot be called after using this - /// method as it involves a partial move - pub fn take_validator_data(self) -> Option { + /// method as it involves a partial move. + pub fn into_validator_data(self) -> Option { self.store.validator_data() } diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index e024b1a2711..80fbd56d451 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -64,7 +64,8 @@ router! {ETH_BRIDGE, // Iterates over all ethereum events and returns the amount of // voting power backing each `TransferToEthereum` event. ( "pool" / "transfer_to_eth_progress" ) - -> HashMap = transfer_to_ethereum_progress, + -> HashMap + = transfer_to_ethereum_progress, // Request a proof of a validator set signed off for // the given epoch. @@ -95,11 +96,19 @@ router! {ETH_BRIDGE, ( "contracts" / "native_erc20" ) -> EthAddress = read_native_erc20_contract, - // Read the voting powers map for the requested validator set. - ( "eth_voting_powers" / [height: BlockHeight]) -> VotingPowersMap = eth_voting_powers, + // Read the voting powers map for the requested validator set + // at the given block height. + ( "voting_powers" / "height" / [height: BlockHeight] ) + -> VotingPowersMap = voting_powers_at_height, + + // Read the voting powers map for the requested validator set + // at the given block height. + ( "voting_powers" / "epoch" / [epoch: Epoch] ) + -> VotingPowersMap = voting_powers_at_epoch, // Read the total supply of some wrapped ERC20 token in Namada. - ( "erc20" / "supply" / [asset: EthAddress] ) -> Option = read_erc20_supply, + ( "erc20" / "supply" / [asset: EthAddress] ) + -> Option = read_erc20_supply, } /// Read the total supply of some wrapped ERC20 token in Namada. @@ -499,9 +508,9 @@ where } } -/// The validator set in order with corresponding -/// voting powers. -fn eth_voting_powers( +/// Retrieve the consensus validator voting powers at the +/// given [`BlockHeight`]. +fn voting_powers_at_height( ctx: RequestCtx<'_, D, H>, height: BlockHeight, ) -> storage_api::Result @@ -509,11 +518,35 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - let epoch = ctx.wl_storage.pos_queries().get_epoch(height); + let maybe_epoch = ctx.wl_storage.pos_queries().get_epoch(height); + let Some(epoch) = maybe_epoch else { + return Err(storage_api::Error::SimpleMessage( + "The epoch of the requested height does not exist", + )); + }; + voting_powers_at_epoch(ctx, epoch) +} + +/// Retrieve the consensus validator voting powers at the +/// given [`Epoch`]. +fn voting_powers_at_epoch( + ctx: RequestCtx<'_, D, H>, + epoch: Epoch, +) -> storage_api::Result +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + let current_epoch = ctx.wl_storage.storage.get_current_epoch().0; + if epoch > current_epoch + 1u64 { + return Err(storage_api::Error::SimpleMessage( + "The requested epoch cannot be queried", + )); + } let (_, voting_powers) = ctx .wl_storage .ethbridge_queries() - .get_validator_set_args(epoch); + .get_validator_set_args(Some(epoch)); Ok(voting_powers) }