diff --git a/Cargo.lock b/Cargo.lock index d822a7abd8..6a9b581657 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4243,6 +4243,7 @@ dependencies = [ "tendermint-proto 0.23.5 (git+https://github.com/heliaxdev/tendermint-rs?rev=95c52476bc37927218374f94ac8e2a19bd35bec9)", "test-log", "thiserror", + "tiny-keccak", "tonic-build", "tracing 0.1.35", "tracing-subscriber 0.3.11", diff --git a/shared/Cargo.toml b/shared/Cargo.toml index b851d92413..9cba8d064f 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -111,6 +111,7 @@ tendermint-proto = {git = "https://github.com/heliaxdev/tendermint-rs", rev = "9 tendermint-proto-abci = {package = "tendermint-proto", git = "https://github.com/heliaxdev/tendermint-rs", branch = "murisi/jsfix", optional = true} tendermint-stable = {package = "tendermint", git = "https://github.com/heliaxdev/tendermint-rs", branch = "murisi/jsfix", optional = true} thiserror = "1.0.30" +tiny-keccak = "2.0.2" tracing = "0.1.30" wasmer = {version = "=2.2.0", optional = true} wasmer-cache = {version = "=2.2.0", optional = true} diff --git a/shared/src/proto/mod.rs b/shared/src/proto/mod.rs index aa971f0b96..7c092fd5e8 100644 --- a/shared/src/proto/mod.rs +++ b/shared/src/proto/mod.rs @@ -4,7 +4,8 @@ pub mod generated; mod types; pub use types::{ - Dkg, Error, Intent, IntentGossipMessage, IntentId, Signed, SignedTxData, Tx, + Dkg, Error, Intent, IntentGossipMessage, IntentId, Signed, SignedSerialize, + SignedTxData, Tx, }; #[cfg(test)] diff --git a/shared/src/proto/types.rs b/shared/src/proto/types.rs index ace163e99f..1e9a3ca861 100644 --- a/shared/src/proto/types.rs +++ b/shared/src/proto/types.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::fmt::Display; use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; use borsh::schema::{Declaration, Definition}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; @@ -51,54 +52,69 @@ pub struct SignedTxData { pub sig: common::Signature, } -/// A generic signed data wrapper for Borsh encode-able data. +/// A serialization method to provide to [`Signed`], such +/// that we may sign serialized data. +pub trait SignedSerialize { + /// A byte vector containing the serialized data. + type Output: AsRef<[u8]>; + + /// Encodes `data` as a byte vector, + /// with some arbitrary serialization method. + fn serialize(data: &T) -> Self::Output; +} + +/// Tag type that indicates we should use [`BorshSerialize`] +/// to sign data in a [`Signed`] wrapper. +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] +pub struct SerializeWithBorsh; + +impl SignedSerialize for SerializeWithBorsh { + type Output = Vec; + + fn serialize(data: &T) -> Vec { + data.try_to_vec() + .expect("Encoding data for signing shouldn't fail") + } +} + +/// A generic signed data wrapper for serialize-able types. +/// +/// The default serialization method is [`BorshSerialize`]. #[derive( Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize, )] -pub struct Signed { +pub struct Signed { /// Arbitrary data to be signed pub data: T, /// The signature of the data pub sig: common::Signature, + /// The method to serialize the data with, + /// before it being signed + _serialization: PhantomData, } -impl PartialEq for Signed -where - T: BorshSerialize + BorshDeserialize + PartialEq, -{ +impl Eq for Signed {} + +impl PartialEq for Signed { fn eq(&self, other: &Self) -> bool { self.data == other.data && self.sig == other.sig } } -impl Eq for Signed where - T: BorshSerialize + BorshDeserialize + Eq + PartialEq -{ -} - -impl Hash for Signed -where - T: BorshSerialize + BorshDeserialize + Hash, -{ +impl Hash for Signed { fn hash(&self, state: &mut H) { self.data.hash(state); self.sig.hash(state); } } -impl PartialOrd for Signed -where - T: BorshSerialize + BorshDeserialize + PartialOrd, -{ +impl PartialOrd for Signed { fn partial_cmp(&self, other: &Self) -> Option { self.data.partial_cmp(&other.data) } } -impl BorshSchema for Signed -where - T: BorshSerialize + BorshDeserialize + BorshSchema, -{ +impl BorshSchema for Signed { fn add_definitions_recursively( definitions: &mut HashMap, ) { @@ -117,17 +133,24 @@ where } } -impl Signed -where - T: BorshSerialize + BorshDeserialize, -{ - /// Initialize a new signed data. +impl Signed { + /// Initialize a new [`Signed`] instance from an existing signature. + #[inline] + pub fn new_from(data: T, sig: common::Signature) -> Self { + Self { + data, + sig, + _serialization: PhantomData, + } + } +} + +impl> Signed { + /// Initialize a new [`Signed`] instance. pub fn new(keypair: &common::SecretKey, data: T) -> Self { - let to_sign = data - .try_to_vec() - .expect("Encoding data for signing shouldn't fail"); - let sig = common::SigScheme::sign(keypair, &to_sign); - Self { data, sig } + let to_sign = S::serialize(&data); + let sig = common::SigScheme::sign(keypair, to_sign.as_ref()); + Self::new_from(data, sig) } /// Verify that the data has been signed by the secret key @@ -136,11 +159,8 @@ where &self, pk: &common::PublicKey, ) -> std::result::Result<(), VerifySigError> { - let bytes = self - .data - .try_to_vec() - .expect("Encoding data for verifying signature shouldn't fail"); - common::SigScheme::verify_signature_raw(pk, &bytes, &self.sig) + let bytes = S::serialize(&self.data); + common::SigScheme::verify_signature_raw(pk, bytes.as_ref(), &self.sig) } } diff --git a/shared/src/types/ethereum_events.rs b/shared/src/types/ethereum_events.rs index ce12e6704b..df4df44e8c 100644 --- a/shared/src/types/ethereum_events.rs +++ b/shared/src/types/ethereum_events.rs @@ -51,6 +51,7 @@ impl From for Uint { Eq, PartialOrd, Ord, + Hash, BorshSerialize, BorshDeserialize, BorshSchema, diff --git a/shared/src/types/transaction/protocol.rs b/shared/src/types/transaction/protocol.rs index 9c70b5b078..a0b8a39bc8 100644 --- a/shared/src/types/transaction/protocol.rs +++ b/shared/src/types/transaction/protocol.rs @@ -35,7 +35,9 @@ mod protocol_txs { use crate::proto::Tx; use crate::types::key::*; use crate::types::transaction::{EllipticCurve, TxError, TxType}; - use crate::types::vote_extensions::ethereum_events; + use crate::types::vote_extensions::{ + ethereum_events, validator_set_update, + }; const TX_NEW_DKG_KP_WASM: &str = "tx_update_dkg_session_keypair.wasm"; @@ -79,6 +81,8 @@ mod protocol_txs { NewDkgKeypair(Tx), /// Ethereum events contained in vote extensions EthereumEvents(ethereum_events::VextDigest), + /// Validator set updates contained in vote extensions + ValidatorSetUpdate(validator_set_update::VextDigest), } impl ProtocolTxType { diff --git a/shared/src/types/vote_extensions.rs b/shared/src/types/vote_extensions.rs index 7737017aee..e47d74ecb1 100644 --- a/shared/src/types/vote_extensions.rs +++ b/shared/src/types/vote_extensions.rs @@ -1,28 +1,29 @@ //! This module contains types necessary for processing vote extensions. pub mod ethereum_events; +pub mod validator_set_update; -// TODO: add a `VoteExtension` type -// -// ```ignore -// pub struct VoteExtension { -// pub ethereum_events: Signed, -// pub validator_set_update: Option, -// } -// ``` +use crate::proto::Signed; -// TODO: add a `VoteExtensionDigest` type; this will contain -// the values to be proposed, for a quorum of Ethereum events -// vote extensions, and a separate quorum of validator set update -// vote extensions -// -// ```ignore -// pub struct VoteExtensionDigest { -// pub ethereum_events: ethereum_events::VextDigest, -// pub validator_set_update: Option, -// } -// ``` -// -// from a `VoteExtensionDigest` we yield two signed `ProtocolTxType` values, -// one of `ProtocolTxType::EthereumEvents` and the other of -// `ProtocolTxType::ValidatorSetUpdate` +/// This type represents the data we pass to the extension of +/// a vote at the PreCommit phase of Tendermint. +pub struct VoteExtension { + /// Vote extension data related with Ethereum events. + pub ethereum_events: Signed, + /// Vote extension data related with validator set updates. + pub validator_set_update: Option, +} + +/// The digest of the signatures from different validators +/// in [`VoteExtension`] instances. +/// +/// From a [`VoteExtensionDigest`] we yield two signed +/// [`crate::types::transaction::protocol::ProtocolTxType`] transactions: +/// - A `ProtocolTxType::EthereumEvents` tx, and +/// - A `ProtocolTxType::ValidatorSetUpdate` tx +pub struct VoteExtensionDigest { + /// The digest of Ethereum events vote extension signatures. + pub ethereum_events: ethereum_events::VextDigest, + /// The digest of validator set updates vote extension signatures. + pub validator_set_update: Option, +} diff --git a/shared/src/types/vote_extensions/ethereum_events.rs b/shared/src/types/vote_extensions/ethereum_events.rs index b0edb0ec28..524d03aa52 100644 --- a/shared/src/types/vote_extensions/ethereum_events.rs +++ b/shared/src/types/vote_extensions/ethereum_events.rs @@ -93,7 +93,7 @@ impl VextDigest { // of crate versions changing and such ext.ethereum_events.sort(); - let signed = Signed { data: ext, sig }; + let signed = Signed::new_from(ext, sig); extensions.push(signed); } extensions diff --git a/shared/src/types/vote_extensions/validator_set_update.rs b/shared/src/types/vote_extensions/validator_set_update.rs new file mode 100644 index 0000000000..2e26d6369b --- /dev/null +++ b/shared/src/types/vote_extensions/validator_set_update.rs @@ -0,0 +1,249 @@ +//! Contains types necessary for processing validator set updates +//! in vote extensions. + +pub mod encoding; + +use std::collections::HashMap; + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use encoding::{AbiEncode, Encode, Token}; +use ethabi::ethereum_types as ethereum; +use num_rational::Ratio; + +use crate::ledger::pos::types::{Epoch, VotingPower}; +use crate::proto::Signed; +use crate::types::address::Address; +use crate::types::ethereum_events::{EthAddress, KeccakHash}; +use crate::types::key::common::{self, Signature}; + +// the namespace strings plugged into validator set hashes +const BRIDGE_CONTRACT_NAMESPACE: &str = "bridge"; +const GOVERNANCE_CONTRACT_NAMESPACE: &str = "governance"; + +/// Contains the digest of all signatures from a quorum of +/// validators for a [`Vext`]. +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema)] +pub struct VextDigest { + /// A mapping from a validator address to a [`Signature`]. + pub signatures: HashMap, + /// The addresses of the validators in the new [`Epoch`], + /// and their respective voting power. + pub voting_powers: VotingPowersMap, +} + +impl VextDigest { + /// Decompresses a set of signed [`Vext`] instances. + pub fn decompress(self, epoch: Epoch) -> Vec { + let VextDigest { + signatures, + voting_powers, + } = self; + + let mut extensions = vec![]; + + for (validator_addr, signature) in signatures.into_iter() { + let voting_powers = voting_powers.clone(); + let data = Vext { + validator_addr, + voting_powers, + epoch, + }; + extensions.push(SignedVext::new_from(data, signature)); + } + extensions + } + + /// Returns an Ethereum ABI encoded string with the + /// params to feed to the Ethereum bridge smart contracts. + pub fn abi_params(&self) -> String { + todo!() + } +} + +/// Represents a [`Vext`] signed by some validator, with +/// an Ethereum key. +pub type SignedVext = Signed; + +/// Represents a validator set update, for some new [`Epoch`]. +#[derive( + Eq, PartialEq, Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, +)] +pub struct Vext { + /// The addresses of the validators in the new [`Epoch`], + /// and their respective voting power. + /// + /// When signing a [`Vext`], this [`VotingPowersMap`] is converted + /// into two arrays: one for its keys, and another for its + /// values. The arrays are sorted in descending order based + /// on the voting power of each validator. + pub voting_powers: VotingPowersMap, + /// TODO: the validator's address is temporarily being included + /// until we're able to map a Tendermint address to a validator + /// address (see https://github.com/anoma/namada/issues/200) + pub validator_addr: Address, + /// The new [`Epoch`]. + /// + /// Since this is a monotonically growing sequence number, + /// it is signed together with the rest of the data to + /// prevent replay attacks on validator set updates. + /// + /// Additionally, we can use this [`Epoch`] value to query the appropriate + /// validator set to verify signatures with. + pub epoch: Epoch, +} + +impl Vext { + /// Creates a new signed [`Vext`]. + /// + /// For more information, read the docs of [`SignedVext::new`]. + #[inline] + pub fn sign(&self, sk: &common::SecretKey) -> SignedVext { + SignedVext::new(sk, self.clone()) + } +} + +/// Provides a mapping between [`EthAddress`] and [`VotingPower`] instances. +pub type VotingPowersMap = HashMap; + +/// This trait contains additional methods for a [`HashMap`], related +/// with validator set update vote extensions logic. +pub trait VotingPowersMapExt { + /// Returns the keccak hash of this [`VotingPowersMap`] + /// to be signed by an Ethereum validator key. + fn get_bridge_hash(&self, epoch: Epoch) -> KeccakHash; + + /// Returns the keccak hash of this [`VotingPowersMap`] + /// to be signed by an Ethereum governance key. + fn get_governance_hash(&self, epoch: Epoch) -> KeccakHash; + + /// Returns the list of Ethereum validator addresses and their respective + /// voting power (in this order), with an Ethereum ABI compatible encoding. + fn get_abi_encoded(&self) -> (Vec, Vec); +} + +impl VotingPowersMapExt for VotingPowersMap { + #[inline] + fn get_bridge_hash(&self, epoch: Epoch) -> KeccakHash { + let (validators, voting_powers) = self.get_abi_encoded(); + + compute_hash( + epoch, + BRIDGE_CONTRACT_NAMESPACE, + validators, + voting_powers, + ) + } + + #[inline] + fn get_governance_hash(&self, epoch: Epoch) -> KeccakHash { + compute_hash( + epoch, + GOVERNANCE_CONTRACT_NAMESPACE, + // TODO: get governance validators + vec![], + // TODO: get governance voting powers + vec![], + ) + } + + fn get_abi_encoded(&self) -> (Vec, Vec) { + // get addresses and voting powers all into one vec + let mut unsorted: Vec<_> = self.iter().collect(); + + // sort it by voting power, in descending order + unsorted.sort_by(|&(_, ref power_1), &(_, ref power_2)| { + power_2.cmp(power_1) + }); + + let sorted = unsorted; + let total_voting_power: u64 = sorted + .iter() + .map(|&(_, &voting_power)| u64::from(voting_power)) + .sum(); + + // split the vec into two + sorted + .into_iter() + .map(|(&EthAddress(addr), &voting_power)| { + let voting_power: u64 = voting_power.into(); + + // normalize the voting power + // https://github.com/anoma/ethereum-bridge/blob/main/test/utils/utilities.js#L29 + const NORMALIZED_VOTING_POWER: u64 = 1 << 32; + + let voting_power = Ratio::new(voting_power, total_voting_power) + * NORMALIZED_VOTING_POWER; + let voting_power = voting_power.round().to_integer(); + let voting_power: ethereum::U256 = voting_power.into(); + + ( + Token::Address(ethereum::H160(addr)), + Token::Uint(voting_power), + ) + }) + .unzip() + } +} + +/// Convert an [`Epoch`] to a [`Token`]. +#[inline] +fn epoch_to_token(epoch: Epoch) -> Token { + Token::Uint(u64::from(epoch).into()) +} + +/// Compute the keccak hash of a validator set update. +/// +/// For more information, check the Ethereum bridge smart contracts: +// - +// - +#[inline] +fn compute_hash( + epoch: Epoch, + namespace: &str, + validators: Vec, + voting_powers: Vec, +) -> KeccakHash { + AbiEncode::keccak256(&[ + Token::String(namespace.into()), + Token::Array(validators), + Token::Array(voting_powers), + epoch_to_token(epoch), + ]) +} + +// this is only here so we don't pollute the +// outer namespace with serde traits +mod tag { + use serde::{Deserialize, Serialize}; + + use super::encoding::{AbiEncode, Encode, Token}; + use super::{epoch_to_token, Vext, VotingPowersMapExt}; + use crate::proto::SignedSerialize; + use crate::types::ethereum_events::KeccakHash; + + /// Tag type that indicates we should use [`AbiEncode`] + /// to sign data in a [`crate::proto::Signed`] wrapper. + #[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] + pub struct SerializeWithAbiEncode; + + impl SignedSerialize for SerializeWithAbiEncode { + type Output = [u8; 32]; + + fn serialize(ext: &Vext) -> Self::Output { + let KeccakHash(output) = AbiEncode::signed_keccak256(&[ + Token::String("updateValidatorsSet".into()), + Token::FixedBytes( + ext.voting_powers.get_bridge_hash(ext.epoch).0.to_vec(), + ), + Token::FixedBytes( + ext.voting_powers.get_governance_hash(ext.epoch).0.to_vec(), + ), + epoch_to_token(ext.epoch), + ]); + output + } + } +} + +#[doc(inline)] +pub use tag::SerializeWithAbiEncode; diff --git a/shared/src/types/vote_extensions/validator_set_update/encoding.rs b/shared/src/types/vote_extensions/validator_set_update/encoding.rs new file mode 100644 index 0000000000..7f9e9ff0aa --- /dev/null +++ b/shared/src/types/vote_extensions/validator_set_update/encoding.rs @@ -0,0 +1,88 @@ +//! This module defines encoding methods compatible with Ethereum +//! smart contracts. +// TODO: probably move this module elsewhere + +#[doc(inline)] +pub use ethabi::token::Token; +use tiny_keccak::{Hasher, Keccak}; + +use crate::types::ethereum_events::KeccakHash; + +/// Contains a method to encode data to a format compatible with Ethereum. +pub trait Encode { + /// The data type to be encoded to. Must deref to a hex string with + /// a `0x` prefix. + type HexString: AsRef; + + /// Returns the encoded [`Token`] instances. + fn encode(tokens: &[Token]) -> Self::HexString; + + /// Encodes a slice of [`Token`] instances, and returns the + /// keccak hash of the encoded string. + fn keccak256(tokens: &[Token]) -> KeccakHash { + let mut output = [0; 32]; + + let mut state = Keccak::v256(); + state.update(Self::encode(tokens).as_ref().as_ref()); + state.finalize(&mut output); + + KeccakHash(output) + } + + /// Encodes a slice of [`Token`] instances, and returns the + /// keccak hash of the encoded string appended to an Ethereum + /// signature header. + fn signed_keccak256(tokens: &[Token]) -> KeccakHash { + let mut output = [0; 32]; + + let eth_message = { + let encoded = Self::encode(tokens); + let message: &[u8] = encoded.as_ref().as_ref(); + + let mut eth_message = + format!("\x19Ethereum Signed Message:\n{}", message.len()) + .into_bytes(); + eth_message.extend_from_slice(message); + eth_message + }; + + let mut state = Keccak::v256(); + state.update(ð_message); + state.finalize(&mut output); + + KeccakHash(output) + } +} + +/// Represents an Ethereum encoding method equivalent +/// to `abi.encode`. +pub struct AbiEncode; + +impl Encode for AbiEncode { + type HexString = String; + + fn encode(tokens: &[Token]) -> Self::HexString { + let encoded_data = hex::encode(ethabi::encode(tokens)); + format!("0x{encoded_data}") + } +} + +// TODO: test signatures here once we merge secp keys +#[cfg(test)] +mod tests { + use ethabi::ethereum_types::U256; + + use super::*; + + /// Checks if we get the same result as `abi.encode`, for some given + /// input data. + #[test] + fn test_abi_encode() { + let expected = "0x000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000047465737400000000000000000000000000000000000000000000000000000000"; + let got = AbiEncode::encode(&[ + Token::Uint(U256::from(42u64)), + Token::String("test".into()), + ]); + assert_eq!(expected, got); + } +} diff --git a/wasm/tx_template/Cargo.lock b/wasm/tx_template/Cargo.lock index c4b8e4b65c..2f0a7e676d 100644 --- a/wasm/tx_template/Cargo.lock +++ b/wasm/tx_template/Cargo.lock @@ -1512,6 +1512,7 @@ dependencies = [ "tendermint", "tendermint-proto", "thiserror", + "tiny-keccak", "tonic-build", "tracing", "wasmer", diff --git a/wasm/vp_template/Cargo.lock b/wasm/vp_template/Cargo.lock index 2bbac52319..c866d7ae02 100644 --- a/wasm/vp_template/Cargo.lock +++ b/wasm/vp_template/Cargo.lock @@ -1512,6 +1512,7 @@ dependencies = [ "tendermint", "tendermint-proto", "thiserror", + "tiny-keccak", "tonic-build", "tracing", "wasmer", diff --git a/wasm/wasm_source/Cargo.lock b/wasm/wasm_source/Cargo.lock index d6e7e77bf7..eff69502d9 100644 --- a/wasm/wasm_source/Cargo.lock +++ b/wasm/wasm_source/Cargo.lock @@ -1512,6 +1512,7 @@ dependencies = [ "tendermint", "tendermint-proto", "thiserror", + "tiny-keccak", "tonic-build", "tracing", "wasmer", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 2b641df776..243993548a 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1523,6 +1523,7 @@ dependencies = [ "tendermint", "tendermint-proto", "thiserror", + "tiny-keccak", "tonic-build", "tracing", "wasmer",