Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relay validator set updates #1157

Merged
merged 10 commits into from
Feb 15, 2023
3 changes: 3 additions & 0 deletions apps/src/bin/namada-relayer/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ pub async fn main() -> Result<()> {
cmds::ValidatorSet::ValidatorSetProof(args) => {
validator_set::query_validator_set_update_proof(args).await;
}
cmds::ValidatorSet::ValidatorSetUpdateRelay(args) => {
validator_set::relay_validator_set_update(args).await;
}
},
}
Ok(())
Expand Down
99 changes: 97 additions & 2 deletions apps/src/lib/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1735,12 +1735,15 @@ pub mod cmds {
pub enum ValidatorSet {
/// Query an Ethereum ABI encoding of the active validator
/// set in Namada, at the given epoch, or the latest
/// one, if none is provided..
/// one, if none is provided.
ActiveValidatorSet(args::ActiveValidatorSet),
/// Query an Ethereum ABI encoding of a proof of the active
/// validator set in Namada, at the given epoch, or the next
/// one, if none is provided.
ValidatorSetProof(args::ValidatorSetProof),
/// Relay a validator set update to Namada's Ethereum bridge
/// smart contracts.
ValidatorSetUpdateRelay(args::ValidatorSetUpdateRelay),
}

impl SubCmd for ValidatorSet {
Expand All @@ -1752,7 +1755,9 @@ pub mod cmds {
.map(|args| Self::ActiveValidatorSet(args.0));
let validator_set_proof = ValidatorSetProof::parse(matches)
.map(|args| Self::ValidatorSetProof(args.0));
active_validator_set.or(validator_set_proof)
let relay = ValidatorSetUpdateRelay::parse(matches)
.map(|args| Self::ValidatorSetUpdateRelay(args.0));
active_validator_set.or(validator_set_proof).or(relay)
})
}

Expand All @@ -1766,6 +1771,7 @@ pub mod cmds {
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(ActiveValidatorSet::def().display_order(1))
.subcommand(ValidatorSetProof::def().display_order(1))
.subcommand(ValidatorSetUpdateRelay::def().display_order(1))
}
}

Expand Down Expand Up @@ -1814,6 +1820,28 @@ pub mod cmds {
.add_args::<args::ValidatorSetProof>()
}
}

#[derive(Clone, Debug)]
pub struct ValidatorSetUpdateRelay(args::ValidatorSetUpdateRelay);

impl SubCmd for ValidatorSetUpdateRelay {
const CMD: &'static str = "relay";

fn parse(matches: &ArgMatches) -> Option<Self> {
matches.subcommand_matches(Self::CMD).map(|matches| {
Self(args::ValidatorSetUpdateRelay::parse(matches))
})
}

fn def() -> App {
App::new(Self::CMD)
.about(
"Relay a validator set update to Namada's Ethereum bridge \
smart contracts.",
)
.add_args::<args::ValidatorSetUpdateRelay>()
}
}
}

pub mod args {
Expand Down Expand Up @@ -1881,7 +1909,14 @@ pub mod args {
const DRY_RUN_TX: ArgFlag = flag("dry-run");
const EPOCH: ArgOpt<Epoch> = arg_opt("epoch");
const ERC20: Arg<EthAddress> = arg("erc20");
const ETH_CONFIRMATIONS: Arg<u64> = arg("confirmations");
const ETH_GAS: ArgOpt<u64> = arg_opt("eth-gas");
const ETH_GAS_PRICE: ArgOpt<u64> = arg_opt("eth-gas-price");
const ETH_ADDRESS: Arg<EthAddress> = arg("ethereum-address");
const ETH_RPC_ENDPOINT: ArgDefault<String> = arg_default(
"eth-rpc-endpoint",
DefaultFn(|| "http://localhost:8545".into()),
);
const FEE_AMOUNT: ArgDefault<token::Amount> =
arg_default("fee-amount", DefaultFn(|| token::Amount::from(0)));
const FEE_PAYER: Arg<WalletAddress> = arg("fee-payer");
Expand Down Expand Up @@ -2192,6 +2227,66 @@ pub mod args {
}
}

#[derive(Debug, Clone)]
pub struct ValidatorSetUpdateRelay {
sug0 marked this conversation as resolved.
Show resolved Hide resolved
/// The query parameters.
pub query: Query,
/// The number of block confirmations on Ethereum.
pub confirmations: u64,
/// The Ethereum RPC endpoint.
pub eth_rpc_endpoint: String,
/// The epoch of the validator set to relay.
pub epoch: Option<Epoch>,
/// The Ethereum gas that can be spent during
/// the relay call.
pub gas: Option<u64>,
/// The price of Ethereum gas, during the
/// relay call.
pub gas_price: Option<u64>,
}

impl Args for ValidatorSetUpdateRelay {
fn parse(matches: &ArgMatches) -> Self {
let query = Query::parse(matches);
let epoch = EPOCH.parse(matches);
let gas = ETH_GAS.parse(matches);
let gas_price = ETH_GAS_PRICE.parse(matches);
let eth_rpc_endpoint = ETH_RPC_ENDPOINT.parse(matches);
let confirmations = ETH_CONFIRMATIONS.parse(matches);
Self {
query,
epoch,
gas,
gas_price,
confirmations,
eth_rpc_endpoint,
}
}

fn def(app: App) -> App {
app.add_args::<Query>()
.arg(
EPOCH
.def()
.about("The epoch of the set of validators to relay."),
)
.arg(ETH_GAS.def().about(
"The Ethereum gas that can be spent during the relay call.",
))
.arg(
ETH_GAS_PRICE.def().about(
"The price of Ethereum gas, during the relay call.",
),
)
.arg(ETH_RPC_ENDPOINT.def().about("The Ethereum RPC endpoint."))
.arg(
ETH_CONFIRMATIONS.def().about(
"The number of block confirmations on Ethereum.",
),
)
}
}

/// Custom transaction arguments
#[derive(Clone, Debug)]
pub struct TxCustom {
Expand Down
84 changes: 84 additions & 0 deletions apps/src/lib/client/eth_bridge/validator_set.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
use std::sync::Arc;

use data_encoding::HEXLOWER;
use ethbridge_governance_contract::Governance;
use namada::core::types::storage::Epoch;
use namada::eth_bridge::ethers::abi::{AbiDecode, AbiType, Tokenizable};
use namada::eth_bridge::ethers::providers::{Http, Provider};
use namada::eth_bridge::structs::{Signature, ValidatorSetArgs};
use namada::ledger::queries::RPC;

use crate::cli::args;
Expand Down Expand Up @@ -44,3 +51,80 @@ pub async fn query_validator_set_args(args: args::ActiveValidatorSet) {

println!("0x{}", HEXLOWER.encode(encoded_validator_set_args.as_ref()));
}

/// Relay a validator set update, signed off for a given epoch.
pub async fn relay_validator_set_update(args: args::ValidatorSetUpdateRelay) {
let nam_client = HttpClient::new(args.query.ledger_address).unwrap();

let epoch_to_relay = if let Some(epoch) = args.epoch {
epoch
} else {
RPC.shell().epoch(&nam_client).await.unwrap().next()
};
let shell = RPC.shell().eth_bridge();
let encoded_proof_fut =
shell.read_valset_upd_proof(&nam_client, &epoch_to_relay);

let bridge_current_epoch = Epoch(epoch_to_relay.0.saturating_sub(2));
let shell = RPC.shell().eth_bridge();
let encoded_validator_set_args_fut =
shell.read_active_valset(&nam_client, &bridge_current_epoch);

let shell = RPC.shell().eth_bridge();
let governance_address_fut = shell.read_governance_contract(&nam_client);

let (encoded_proof, encoded_validator_set_args, governance_contract) =
futures::try_join!(
encoded_proof_fut,
encoded_validator_set_args_fut,
governance_address_fut
)
.unwrap();

let (bridge_hash, gov_hash, signatures): (
[u8; 32],
[u8; 32],
Vec<Signature>,
) = abi_decode_struct(encoded_proof);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I seems like we are doing lots of unnecessary rounds of serialization.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the RPC method could return a non-abi encoded data response. right now it borsh serializes the abi encoding of the data. CLI commands requesting a proof are kind of obsolete now, but if we wanted to keep them anyway (so users could potentially implement shell scripts around this or smth?), they could be responsible for returning the ABI encoding of the data queried from our RPC endpoints

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes more sense to borsh serialize the raw struct and down the road, if some script needs abi encoding, it can call abi encoding on the raw struct itself. I don't think that's an unreasonable burden to put on people implementing their own scripts.

I just feel like we have a lot of artifact code that I would like to clear out. This damn repo is complicated enough as it is.

Copy link
Collaborator Author

@sug0 sug0 Feb 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel the same. I'll work on this later though, right now it's out of the scope of this PR

EDIT: opened #1160

let active_set: ValidatorSetArgs =
abi_decode_struct(encoded_validator_set_args);

let eth_client =
Arc::new(Provider::<Http>::try_from(&args.eth_rpc_endpoint).unwrap());
let governance = Governance::new(governance_contract.address, eth_client);

let mut relay_op = governance.update_validators_set(
active_set,
bridge_hash,
gov_hash,
signatures,
epoch_to_relay.0.into(),
);
if let Some(gas) = args.gas {
relay_op.tx.set_gas(gas);
}
if let Some(gas_price) = args.gas_price {
relay_op.tx.set_gas_price(gas_price);
}

let pending_tx = relay_op.send().await.unwrap();
let transf_result = pending_tx
.confirmations(args.confirmations as usize)
.await
.unwrap();

println!("{transf_result:?}");
}

// NOTE: there's a bug (or feature?!) in ethers, where
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a feature 😆

// `EthAbiCodec` derived `AbiDecode` implementations
// have a decode method that expects a tuple, but
// passes invalid param types to `abi::decode()`
fn abi_decode_struct<T, D>(data: T) -> D
where
T: AsRef<[u8]>,
D: Tokenizable + AbiDecode + AbiType,
{
let decoded: (D,) = AbiDecode::decode(data).unwrap();
decoded.0
}
83 changes: 81 additions & 2 deletions shared/src/ledger/queries/shell/eth_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,26 @@ use namada_core::ledger::storage_api::{
self, CustomError, ResultExt, StorageRead,
};
use namada_core::types::address::Address;
use namada_core::types::ethereum_events::{EthereumEvent, TransferToEthereum};
use namada_core::types::ethereum_events::{
EthAddress, EthereumEvent, TransferToEthereum,
};
use namada_core::types::storage::{BlockHeight, DbKeySeg, Key};
use namada_core::types::vote_extensions::validator_set_update::{
ValidatorSetArgs, VotingPowersMap,
};
use namada_core::types::voting_power::FractionalVotingPower;
use namada_ethereum_bridge::parameters::UpgradeableContract;
use namada_ethereum_bridge::storage::eth_bridge_queries::EthBridgeQueries;
use namada_ethereum_bridge::storage::proof::{
tokenize_relay_proof, EthereumProof, RelayProof,
};
use namada_ethereum_bridge::storage::vote_tallies;
use namada_ethereum_bridge::storage::vote_tallies::{
eth_msgs_prefix, BODY_KEY_SEGMENT, VOTING_POWER_KEY_SEGMENT,
};
use namada_ethereum_bridge::storage::{
bridge_contract_key, governance_contract_key, native_erc20_key,
vote_tallies,
};

use crate::ledger::queries::{EncodedResponseQuery, RequestCtx, RequestQuery};
use crate::types::eth_abi::{Encode, EncodeCell};
Expand Down Expand Up @@ -68,6 +74,79 @@ router! {ETH_BRIDGE,
// The request may fail if no validator set exists at that epoch.
( "validator_set" / "active" / [epoch: Epoch] )
-> EncodeCell<ValidatorSetArgs> = read_active_valset,

// Read the address and version of the Ethereum bridge's Governance
// smart contract.
( "contracts" / "governance" )
-> UpgradeableContract = read_governance_contract,

// Read the address and version of the Ethereum bridge's Bridge
// smart contract.
( "contracts" / "bridge" )
-> UpgradeableContract = read_bridge_contract,

// Read the address of the Ethereum bridge's native ERC20
// smart contract.
( "contracts" / "native_erc20" )
-> EthAddress = read_native_erc20_contract,
}

/// Helper function to read a smart contract from storage.
fn read_contract<T, D, H>(
key: &Key,
ctx: RequestCtx<'_, D, H>,
) -> storage_api::Result<T>
where
D: 'static + DB + for<'iter> DBIter<'iter> + Sync,
H: 'static + StorageHasher + Sync,
T: BorshDeserialize,
{
let Some(contract) = StorageRead::read(ctx.storage, key)? else {
return Err(storage_api::Error::SimpleMessage(
"Failed to read contract: The Ethereum bridge \
storage is not initialized",
));
};
Ok(contract)
}

/// Read the address and version of the Ethereum bridge's Governance
/// smart contract.
#[inline]
fn read_governance_contract<D, H>(
ctx: RequestCtx<'_, D, H>,
) -> storage_api::Result<UpgradeableContract>
where
D: 'static + DB + for<'iter> DBIter<'iter> + Sync,
H: 'static + StorageHasher + Sync,
{
read_contract(&governance_contract_key(), ctx)
}

/// Read the address and version of the Ethereum bridge's Bridge
/// smart contract.
#[inline]
fn read_bridge_contract<D, H>(
ctx: RequestCtx<'_, D, H>,
) -> storage_api::Result<UpgradeableContract>
where
D: 'static + DB + for<'iter> DBIter<'iter> + Sync,
H: 'static + StorageHasher + Sync,
{
read_contract(&bridge_contract_key(), ctx)
}

/// Read the address of the Ethereum bridge's native ERC20
/// smart contract.
#[inline]
fn read_native_erc20_contract<D, H>(
ctx: RequestCtx<'_, D, H>,
) -> storage_api::Result<EthAddress>
where
D: 'static + DB + for<'iter> DBIter<'iter> + Sync,
H: 'static + StorageHasher + Sync,
{
read_contract(&native_erc20_key(), ctx)
}

/// Read the current contents of the Ethereum bridge
Expand Down