diff --git a/apps/src/lib/node/ledger/protocol/mod.rs b/apps/src/lib/node/ledger/protocol/mod.rs index 73e9b5b710..0732799c45 100644 --- a/apps/src/lib/node/ledger/protocol/mod.rs +++ b/apps/src/lib/node/ledger/protocol/mod.rs @@ -14,10 +14,10 @@ use namada::ledger::storage::{DBIter, Storage, StorageHasher, DB}; use namada::ledger::treasury::TreasuryVp; use namada::proto::{self, Tx}; use namada::types::address::{Address, InternalAddress}; -use namada::types::ethereum_events::vote_extensions::VoteExtensionDigest; use namada::types::storage; use namada::types::transaction::protocol::{ProtocolTx, ProtocolTxType}; use namada::types::transaction::{DecryptedTx, TxResult, TxType, VpsResult}; +use namada::types::vote_extensions::ethereum_events; use namada::vm::wasm::{TxCache, VpCache}; use namada::vm::{self, wasm, WasmCacheAccess}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; @@ -159,8 +159,9 @@ where } TxType::Protocol(ProtocolTx { tx: - ProtocolTxType::EthereumEvents(VoteExtensionDigest { - events, .. + ProtocolTxType::EthereumEvents(ethereum_events::VextDigest { + events, + .. }), .. }) if !events.is_empty() => { diff --git a/apps/src/lib/node/ledger/shell/prepare_proposal.rs b/apps/src/lib/node/ledger/shell/prepare_proposal.rs index 1075897672..217e19fac9 100644 --- a/apps/src/lib/node/ledger/shell/prepare_proposal.rs +++ b/apps/src/lib/node/ledger/shell/prepare_proposal.rs @@ -4,10 +4,11 @@ mod prepare_block { use std::collections::{BTreeMap, HashMap, HashSet}; - use namada::types::ethereum_events::vote_extensions::{ - FractionalVotingPower, MultiSignedEthEvent, VoteExtensionDigest, - }; use namada::types::transaction::protocol::ProtocolTxType; + use namada::types::vote_extensions::ethereum_events::{ + self, MultiSignedEthEvent, + }; + use namada::types::voting_power::FractionalVotingPower; use tendermint_proto::abci::{ ExtendedCommitInfo, ExtendedVoteInfo, TxRecord, }; @@ -67,6 +68,8 @@ mod prepare_block { /// Builds a batch of vote extension transactions, comprised of Ethereum /// events + // TODO: add `and, optionally, a validator set update` to the docstring, + // after validator set updates are implemented fn build_vote_extensions_txs( &mut self, local_last_commit: Option, @@ -79,7 +82,7 @@ mod prepare_block { let vote_extension_digest = local_last_commit.and_then(|local_last_commit| { let votes = local_last_commit.votes; - self.compress_vote_extensions(votes) + self.compress_ethereum_events(votes) }); let vote_extension_digest = match (vote_extension_digest, self.storage.last_height) { @@ -88,22 +91,22 @@ mod prepare_block { (Some(_), BlockHeight(0)) => { unreachable!( "We already handle this scenario in \ - validate_vote_extension." + validate_eth_events_vext." ) } // handle block heights > 0 (Some(digest), _) => digest, _ => unreachable!( - "Honest Namada validators will always sign a \ - VoteExtension, even if no Ethereum events were \ - observed at a given block height. In fact, a quorum \ - of signed empty VoteExtension commits the fact no \ - events were observed by a majority of validators. \ - Likewise, a Tendermint quorum should never decide on \ - a block including vote extensions reflecting less \ - than or equal to 2/3 of the total stake. These \ - scenarios are virtually impossible, so we will panic \ - here." + "Honest Namada validators will always sign \ + ethereum_events::Vext instances, even if no Ethereum \ + events were observed at a given block height. In \ + fact, a quorum of signed empty ethereum_events::Vext \ + instances commits the fact no events were observed \ + by a majority of validators. Likewise, a Tendermint \ + quorum should never decide on a block including vote \ + extensions reflecting less than or equal to 2/3 of \ + the total stake. These scenarios are virtually \ + impossible, so we will panic here." ), }; @@ -112,6 +115,8 @@ mod prepare_block { .to_bytes(); let tx_record = record::add(tx); + // TODO: include here a validator set update tx, + // if we are at the end of an epoch vec![tx_record] } @@ -160,13 +165,16 @@ mod prepare_block { .collect() } - /// Compresses a set of vote extensions into a single - /// [`VoteExtensionDigest`], whilst filtering invalid - /// [`SignedExt`] instances in the process - fn compress_vote_extensions( + /// Compresses a set of signed Ethereum events into a single + /// [`ethereum_events::VextDigest`], whilst filtering invalid + /// [`Signed`] instances in the process + // TODO: rename this as `compress_vote_extensions`, and return + // a `VoteExtensionDigest`, which will contain both digests of + // ethereum events and validator set update vote extensions + fn compress_ethereum_events( &self, vote_extensions: Vec, - ) -> Option { + ) -> Option { let events_epoch = self .storage .block @@ -219,15 +227,15 @@ mod prepare_block { ?sig, ?validator_addr, "Overwrote old signature from validator while \ - constructing VoteExtensionDigest" + constructing ethereum_events::VextDigest" ); } } if voting_power <= FractionalVotingPower::TWO_THIRDS { tracing::error!( - "Tendermint has decided on a block including vote \ - extensions reflecting <= 2/3 of the total stake" + "Tendermint has decided on a block including Ethereum \ + events reflecting <= 2/3 of the total stake" ); return None; } @@ -237,7 +245,7 @@ mod prepare_block { .map(|(event, signers)| MultiSignedEthEvent { event, signers }) .collect(); - Some(VoteExtensionDigest { events, signatures }) + Some(ethereum_events::VextDigest { events, signatures }) } } @@ -284,20 +292,19 @@ mod prepare_block { use namada::ledger::pos::namada_proof_of_stake::types::{ VotingPower, WeightedValidator, }; - use namada::proto::SignedTxData; + use namada::proto::{Signed, SignedTxData}; use namada::types::address::xan; - use namada::types::ethereum_events::vote_extensions::VoteExtension; use namada::types::ethereum_events::EthereumEvent; use namada::types::key::common; use namada::types::storage::{BlockHeight, Epoch}; use namada::types::transaction::protocol::ProtocolTxType; use namada::types::transaction::{Fee, TxType}; + use namada::types::vote_extensions::ethereum_events; use tendermint_proto::abci::tx_record::TxAction; use tendermint_proto::abci::{ ExtendedCommitInfo, ExtendedVoteInfo, TxRecord, }; - use super::super::super::vote_extensions::SignedExt; use super::*; use crate::node::ledger::shell::test_utils::{ self, gen_keypair, TestShell, @@ -326,8 +333,11 @@ mod prepare_block { ); } - /// Serialize a [`SignedExt`] to an [`ExtendedVoteInfo`] - fn vote_extension_serialize(vext: SignedExt) -> ExtendedVoteInfo { + /// Serialize a [`Signed`] to an + /// [`ExtendedVoteInfo`] + fn vote_extension_serialize( + vext: Signed, + ) -> ExtendedVoteInfo { ExtendedVoteInfo { vote_extension: vext.try_to_vec().unwrap(), ..Default::default() @@ -335,9 +345,9 @@ mod prepare_block { } /// Check if we are filtering out an invalid vote extension `vext` - fn check_vote_extension_filtering( + fn check_eth_events_filtering( shell: &mut TestShell, - vext: SignedExt, + vext: Signed, ) { let votes = deserialize_vote_extensions(vec![vote_extension_serialize( @@ -349,7 +359,7 @@ mod prepare_block { assert_eq!(filtered_votes, vec![]); } - /// Test if we are filtering out vote extensinos with bad + /// Test if we are filtering out Ethereum events with bad /// signatures in a prepare proposal. #[test] fn test_prepare_proposal_filter_out_bad_vext_signatures() { @@ -365,7 +375,7 @@ mod prepare_block { let validator_addr = wallet::defaults::validator_address(); // generate a valid signature - let mut ext = VoteExtension { + let mut ext = ethereum_events::Vext { validator_addr, block_height: LAST_HEIGHT, ethereum_events: vec![], @@ -378,10 +388,10 @@ mod prepare_block { ext }; - check_vote_extension_filtering(&mut shell, signed_vote_extension); + check_eth_events_filtering(&mut shell, signed_vote_extension); } - /// Test if we are filtering out vote extensinos for + /// Test if we are filtering out Ethereum events seen at /// block heights different than the last height. #[test] fn test_prepare_proposal_filter_out_bad_vext_bheights() { @@ -398,7 +408,7 @@ mod prepare_block { let validator_addr = wallet::defaults::validator_address(); let signed_vote_extension = { - let ext = VoteExtension { + let ext = ethereum_events::Vext { validator_addr, block_height: PRED_LAST_HEIGHT, ethereum_events: vec![], @@ -408,10 +418,10 @@ mod prepare_block { ext }; - check_vote_extension_filtering(&mut shell, signed_vote_extension); + check_eth_events_filtering(&mut shell, signed_vote_extension); } - /// Test if we are filtering out vote extensinos for + /// Test if we are filtering out Ethereum events seen by /// non-validator nodes. #[test] fn test_prepare_proposal_filter_out_bad_vext_validators() { @@ -429,7 +439,7 @@ mod prepare_block { }; let signed_vote_extension = { - let ext = VoteExtension { + let ext = ethereum_events::Vext { validator_addr, block_height: LAST_HEIGHT, ethereum_events: vec![], @@ -439,7 +449,7 @@ mod prepare_block { ext }; - check_vote_extension_filtering(&mut shell, signed_vote_extension); + check_eth_events_filtering(&mut shell, signed_vote_extension); } /// Test if we are filtering out duped Ethereum events in @@ -462,7 +472,7 @@ mod prepare_block { }; let signed_vote_extension = { let ev = ethereum_event; - let ext = VoteExtension { + let ext = ethereum_events::Vext { validator_addr, block_height: LAST_HEIGHT, ethereum_events: vec![ev.clone(), ev.clone(), ev], @@ -475,7 +485,7 @@ mod prepare_block { let maybe_digest = { let votes = vec![vote_extension_serialize(signed_vote_extension)]; - shell.compress_vote_extensions(votes) + shell.compress_ethereum_events(votes) }; // we should be filtering out the vote extension with @@ -485,13 +495,13 @@ mod prepare_block { assert!(maybe_digest.is_none()); } - /// Creates a vote extension digest manually, and encodes it as a + /// Creates an Ethereum events digest manually, and encodes it as a /// [`TxRecord`]. fn manually_assemble_digest( _protocol_key: &common::SecretKey, - ext: SignedExt, + ext: Signed, last_height: BlockHeight, - ) -> VoteExtensionDigest { + ) -> ethereum_events::VextDigest { let events = vec![MultiSignedEthEvent { event: ext.data.ethereum_events[0].clone(), signers: { @@ -507,7 +517,7 @@ mod prepare_block { }; let vote_extension_digest = - VoteExtensionDigest { events, signatures }; + ethereum_events::VextDigest { events, signatures }; assert_eq!( vec![ext], @@ -522,7 +532,7 @@ mod prepare_block { // super::record::add(tx) } - /// Test if vote extension validation and inclusion in a block + /// Test if Ethereum events validation and inclusion in a block /// behaves as expected, considering honest validators. #[test] fn test_prepare_proposal_vext_normal_op() { @@ -541,7 +551,7 @@ mod prepare_block { transfers: vec![], }; let signed_vote_extension = { - let ext = VoteExtension { + let ext = ethereum_events::Vext { validator_addr, block_height: LAST_HEIGHT, ethereum_events: vec![ethereum_event], @@ -599,7 +609,7 @@ mod prepare_block { // assert_eq!(rsp.tx_records, vec![digest]); } - /// Test if vote extension validation and inclusion in a block + /// Test if Ethereum events validation and inclusion in a block /// behaves as expected, considering <= 2/3 voting power. #[test] #[should_panic(expected = "Honest Namada validators")] @@ -656,7 +666,7 @@ mod prepare_block { transfers: vec![], }; let signed_vote_extension = { - let ext = VoteExtension { + let ext = ethereum_events::Vext { validator_addr, block_height: LAST_HEIGHT, ethereum_events: vec![ethereum_event], diff --git a/apps/src/lib/node/ledger/shell/process_proposal.rs b/apps/src/lib/node/ledger/shell/process_proposal.rs index 9f971b831e..9f6af6fd88 100644 --- a/apps/src/lib/node/ledger/shell/process_proposal.rs +++ b/apps/src/lib/node/ledger/shell/process_proposal.rs @@ -1,7 +1,7 @@ //! Implementation of the ['VerifyHeader`], [`ProcessProposal`], //! and [`RevertProposal`] ABCI++ methods for the Shell -use namada::types::ethereum_events::vote_extensions::FractionalVotingPower; use namada::types::transaction::protocol::ProtocolTxType; +use namada::types::voting_power::FractionalVotingPower; #[cfg(not(feature = "ABCI"))] use tendermint_proto::abci::response_process_proposal::ProposalStatus; #[cfg(not(feature = "ABCI"))] @@ -48,7 +48,7 @@ where }) .collect(); - // We should not have more than one `VoteExtensionDigest` in + // We should not have more than one `ethereum_events::VextDigest` in // a proposal from some round's leader. let too_many_vext_digests = vote_ext_digest_num > 1; @@ -374,9 +374,6 @@ mod test_process_proposal { use borsh::BorshDeserialize; use namada::proto::SignedTxData; use namada::types::address::xan; - use namada::types::ethereum_events::vote_extensions::{ - MultiSignedEthEvent, VoteExtension, VoteExtensionDigest, - }; use namada::types::ethereum_events::EthereumEvent; use namada::types::hash::Hash; use namada::types::key::*; @@ -384,6 +381,9 @@ mod test_process_proposal { use namada::types::token::Amount; use namada::types::transaction::encrypted::EncryptedTx; use namada::types::transaction::{EncryptionKey, Fee}; + use namada::types::vote_extensions::ethereum_events::{ + self, MultiSignedEthEvent, + }; #[cfg(not(feature = "ABCI"))] use tendermint_proto::abci::RequestInitChain; #[cfg(not(feature = "ABCI"))] @@ -401,8 +401,8 @@ mod test_process_proposal { }; use crate::wallet; - /// Test that if a proposal contains more than one `VoteExtensionDigest`, - /// we reject it. + /// Test that if a proposal contains more than one + /// `ethereum_events::VextDigest`, we reject it. #[test] fn test_more_than_one_vext_digest_rejected() { const LAST_HEIGHT: BlockHeight = BlockHeight(2); @@ -412,14 +412,16 @@ mod test_process_proposal { let vote_extension_digest = { let validator_addr = wallet::defaults::validator_address(); let signed_vote_extension = { - let ext = - VoteExtension::empty(LAST_HEIGHT, validator_addr.clone()) - .sign(&protocol_key); + let ext = ethereum_events::Vext::empty( + LAST_HEIGHT, + validator_addr.clone(), + ) + .sign(&protocol_key); assert!(ext.verify(&protocol_key.ref_to()).is_ok()); ext }; - // vote extension digest with no observed events - VoteExtensionDigest { + // Ethereum events digest with no observed events + ethereum_events::VextDigest { signatures: { let mut s = HashMap::new(); s.insert(validator_addr, signed_vote_extension.sig); @@ -443,7 +445,7 @@ mod test_process_proposal { fn check_rejected_digest( shell: &mut TestShell, - vote_extension_digest: VoteExtensionDigest, + vote_extension_digest: ethereum_events::VextDigest, protocol_key: common::SecretKey, ) { let tx = ProtocolTxType::EthereumEvents(vote_extension_digest) @@ -467,7 +469,7 @@ mod test_process_proposal { ); } - /// Test that if a proposal contains vote extensions with + /// Test that if a proposal contains Ethereum events with /// invalid validator signatures, we reject it. #[test] fn test_drop_vext_digest_with_invalid_sigs() { @@ -483,7 +485,7 @@ mod test_process_proposal { }; let ext = { // generate a valid signature - let mut ext = VoteExtension { + let mut ext = ethereum_events::Vext { validator_addr: addr.clone(), block_height: LAST_HEIGHT, ethereum_events: vec![event.clone()], @@ -495,7 +497,7 @@ mod test_process_proposal { ext.sig = test_utils::invalidate_signature(ext.sig); ext }; - VoteExtensionDigest { + ethereum_events::VextDigest { signatures: { let mut s = HashMap::new(); s.insert(addr.clone(), ext.sig); @@ -514,7 +516,7 @@ mod test_process_proposal { check_rejected_digest(&mut shell, vote_extension_digest, protocol_key); } - /// Test that if a proposal contains vote extensions with + /// Test that if a proposal contains Ethereum events with /// invalid block heights, we reject it. #[test] fn test_drop_vext_digest_with_invalid_bheights() { @@ -530,7 +532,7 @@ mod test_process_proposal { transfers: vec![], }; let ext = { - let ext = VoteExtension { + let ext = ethereum_events::Vext { validator_addr: addr.clone(), block_height: PRED_LAST_HEIGHT, ethereum_events: vec![event.clone()], @@ -539,7 +541,7 @@ mod test_process_proposal { assert!(ext.verify(&protocol_key.ref_to()).is_ok()); ext }; - VoteExtensionDigest { + ethereum_events::VextDigest { signatures: { let mut s = HashMap::new(); s.insert(addr.clone(), ext.sig); @@ -558,7 +560,7 @@ mod test_process_proposal { check_rejected_digest(&mut shell, vote_extension_digest, protocol_key); } - /// Test that if a proposal contains vote extensions with + /// Test that if a proposal contains Ethereum events with /// invalid validators, we reject it. #[test] fn test_drop_vext_digest_with_invalid_validators() { @@ -576,7 +578,7 @@ mod test_process_proposal { transfers: vec![], }; let ext = { - let ext = VoteExtension { + let ext = ethereum_events::Vext { validator_addr: addr.clone(), block_height: LAST_HEIGHT, ethereum_events: vec![event.clone()], @@ -585,7 +587,7 @@ mod test_process_proposal { assert!(ext.verify(&protocol_key.ref_to()).is_ok()); ext }; - VoteExtensionDigest { + ethereum_events::VextDigest { signatures: { let mut s = HashMap::new(); s.insert(addr.clone(), ext.sig); diff --git a/apps/src/lib/node/ledger/shell/vote_extensions.rs b/apps/src/lib/node/ledger/shell/vote_extensions.rs index 4256e12633..8cd86c1b78 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions.rs @@ -3,16 +3,13 @@ mod extend_votes { use borsh::BorshDeserialize; use namada::ledger::pos::namada_proof_of_stake::types::VotingPower; use namada::proto::Signed; - use namada::types::ethereum_events::vote_extensions::VoteExtension; + use namada::types::vote_extensions::ethereum_events; use tendermint_proto::abci::ExtendedVoteInfo; use super::super::queries::QueriesExt; use super::super::*; - /// A [`VoteExtension`] signed by a Namada validator. - pub type SignedExt = Signed; - - /// The error yielded from [`Shell::validate_vote_ext_and_get_it_back`]. + /// The error yielded from validating faulty vote extensions in the shell #[derive(Error, Debug)] pub enum VoteExtensionError { #[error("The vote extension was issued at block height 0.")] @@ -48,7 +45,7 @@ mod extend_votes { .get_validator_address() .expect("only validators should receive this method call") .to_owned(); - let ext = VoteExtension { + let ext = ethereum_events::Vext { block_height: self.storage.last_height + 1, ethereum_events: self.new_ethereum_events(), validator_addr, @@ -71,11 +68,15 @@ mod extend_votes { &self, req: request::VerifyVoteExtension, ) -> response::VerifyVoteExtension { - if let Ok(signed) = - SignedExt::try_from_slice(&req.vote_extension[..]) - { + // TODO: this should deserialize to + // `namada::types::vote_extensions::VoteExtension`, + // which contains an optional validator set update and + // a set of ethereum events seen at the previous block height + if let Ok(signed) = Signed::::try_from_slice( + &req.vote_extension[..], + ) { response::VerifyVoteExtension { - status: if self.validate_vote_extension( + status: if self.validate_eth_events_vext( signed, self.storage.last_height + 1, ) { @@ -103,31 +104,37 @@ mod extend_votes { } } - /// Validates a vote extension issued at the provided block height - /// Checks that at epoch of the provided height - /// * The tendermint address corresponds to an active validator + /// Validates an Ethereum events vote extension issued at the provided + /// block height + /// + /// Checks that at epoch of the provided height: + /// * The Tendermint address corresponds to an active validator /// * The validator correctly signed the extension /// * The validator signed over the correct height inside of the /// extension + /// * There are no duplicate Ethereum events in this vote extension, + /// and the events are sorted in ascending order #[inline] - pub fn validate_vote_extension( + pub fn validate_eth_events_vext( &self, - ext: SignedExt, + ext: Signed, last_height: BlockHeight, ) -> bool { - self.validate_vote_ext_and_get_it_back(ext, last_height) + self.validate_eth_events_vext_and_get_it_back(ext, last_height) .is_ok() } - /// This method behaves exactly like [`Self::validate_vote_extension`], + /// This method behaves exactly like [`Self::validate_eth_events_vext`], /// with the added bonus of returning the vote extension back, if it /// is valid. - pub fn validate_vote_ext_and_get_it_back( + pub fn validate_eth_events_vext_and_get_it_back( &self, - ext: SignedExt, + ext: Signed, last_height: BlockHeight, - ) -> std::result::Result<(VotingPower, SignedExt), VoteExtensionError> - { + ) -> std::result::Result< + (VotingPower, Signed), + VoteExtensionError, + > { if ext.data.block_height != last_height { let ext_height = ext.data.block_height; tracing::error!( @@ -184,7 +191,7 @@ mod extend_votes { } /// Checks the channel from the Ethereum oracle monitoring - /// the fullnode and retrieves all VoteExtension messages sent. + /// the fullnode and retrieves all seen Ethereum events. pub fn new_ethereum_events(&mut self) -> Vec { match &mut self.mode { ShellMode::Validator { @@ -198,22 +205,25 @@ mod extend_votes { } } - /// Takes an iterator over signed vote extensions, + /// Takes an iterator over vote extension instances, /// and returns another iterator. The latter yields /// valid vote extensions, or the reason why these /// are invalid, in the form of a [`VoteExtensionError`]. + // TODO: the `vote_extensions` iterator should be over `VoteExtension` + // instances, I guess? to be determined in the next PR #[inline] pub fn validate_vote_extension_list( &self, - vote_extensions: impl IntoIterator + 'static, + vote_extensions: impl IntoIterator> + + 'static, ) -> impl Iterator< Item = std::result::Result< - (VotingPower, SignedExt), + (VotingPower, Signed), VoteExtensionError, >, > + '_ { vote_extensions.into_iter().map(|vote_extension| { - self.validate_vote_ext_and_get_it_back( + self.validate_eth_events_vext_and_get_it_back( vote_extension, self.storage.last_height, ) @@ -222,30 +232,43 @@ mod extend_votes { /// Takes a list of signed vote extensions, /// and filters out invalid instances. + // TODO: the `vote_extensions` iterator should be over `VoteExtension` + // instances, I guess? to be determined in the next PR #[inline] pub fn filter_invalid_vote_extensions( &self, - vote_extensions: impl IntoIterator + 'static, - ) -> impl Iterator + '_ { + vote_extensions: impl IntoIterator> + + 'static, + ) -> impl Iterator)> + '_ + { self.validate_vote_extension_list(vote_extensions) .filter_map(|ext| ext.ok()) } } /// Given a `Vec` of [`ExtendedVoteInfo`], return an iterator over the - /// ones we could deserialize to [`SignedExt`] instances. + /// ones we could deserialize to [`Signed`] + /// instances. + // TODO: we need to return an iterator over instances of `VoteExtension`, + // which contain both the ethereum events vote extensions and validator + // set update vote extensions pub fn deserialize_vote_extensions( vote_extensions: Vec, - ) -> impl Iterator + 'static { + ) -> impl Iterator> + 'static { vote_extensions.into_iter().filter_map(|vote| { - SignedExt::try_from_slice(&vote.vote_extension[..]) - .map_err(|err| { - tracing::error!( - ?err, - "Failed to deserialize signed vote extension", - ); - }) - .ok() + Signed::::try_from_slice( + &vote.vote_extension[..], + ) + .map_err(|err| { + tracing::error!( + ?err, + // TODO: change this error message, probably, such that + // it mentions Ethereum events rather than vote + // extensions + "Failed to deserialize signed vote extension", + ); + }) + .ok() }) } @@ -256,16 +279,16 @@ mod extend_votes { use borsh::{BorshDeserialize, BorshSerialize}; use namada::ledger::pos; use namada::ledger::pos::namada_proof_of_stake::PosBase; - use namada::types::ethereum_events::vote_extensions::VoteExtension; + use namada::proto::Signed; use namada::types::ethereum_events::{ EthAddress, EthereumEvent, TransferToEthereum, }; use namada::types::key::*; use namada::types::storage::{BlockHeight, Epoch}; + use namada::types::vote_extensions::ethereum_events; use tendermint_proto::abci::response_verify_vote_extension::VerifyStatus; use tower_abci::request; - use super::SignedExt; use crate::node::ledger::shell::queries::QueriesExt; use crate::node::ledger::shell::test_utils::*; use crate::node::ledger::shims::abcipp_shim_types::shim::request::FinalizeBlock; @@ -343,7 +366,7 @@ mod extend_votes { oracle.send(event_1.clone()).expect("Test failed"); oracle.send(event_2.clone()).expect("Test failed"); let vote_extension = - ::try_from_slice( + as BorshDeserialize>::try_from_slice( &shell.extend_vote(Default::default()).vote_extension[..], ) .expect("Test failed"); @@ -374,7 +397,7 @@ mod extend_votes { assert_eq!(res.status, i32::from(VerifyStatus::Accept)); } - /// Test that Ethereum headers signed by a non-validator is rejected + /// Test that Ethereum events signed by a non-validator are rejected #[test] fn test_eth_events_must_be_signed_by_validator() { let (shell, _, _) = setup(); @@ -384,7 +407,7 @@ mod extend_votes { .get_validator_address() .expect("Test failed") .clone(); - let vote_ext = VoteExtension { + let vote_ext = ethereum_events::Vext { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 1.into(), transfers: vec![TransferToEthereum { @@ -415,7 +438,7 @@ mod extend_votes { ); } - /// Test that validation of vote extensions cast during the + /// Test that validation of Ethereum events cast during the /// previous block are accepted for the current block. This /// should pass even if the epoch changed resulting in a /// change to the validator set. @@ -430,7 +453,7 @@ mod extend_votes { .expect("Test failed") .clone(); let signed_height = shell.storage.last_height + 1; - let vote_ext = VoteExtension { + let vote_ext = ethereum_events::Vext { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 1.into(), transfers: vec![TransferToEthereum { @@ -481,16 +504,16 @@ mod extend_votes { .is_ok() ); - assert!(shell.validate_vote_extension(vote_ext, signed_height)); + assert!(shell.validate_eth_events_vext(vote_ext, signed_height)); } - /// Test that that an event that incorrectly labels what block it was - /// included in a vote extension on is rejected + /// Test that an [`ethereum_events::Vext`] that incorrectly labels what + /// block it was included on in a vote extension is rejected #[test] fn reject_incorrect_block_number() { let (shell, _, _) = setup(); let address = shell.mode.get_validator_address().unwrap().clone(); - let vote_ext = VoteExtension { + let vote_ext = ethereum_events::Vext { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 1.into(), transfers: vec![TransferToEthereum { diff --git a/shared/src/types/ethereum_events.rs b/shared/src/types/ethereum_events.rs index 1b5fcf7657..ce12e6704b 100644 --- a/shared/src/types/ethereum_events.rs +++ b/shared/src/types/ethereum_events.rs @@ -1,7 +1,5 @@ //! Types representing data intended for Anoma via Ethereum events -pub mod vote_extensions; - use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; @@ -174,7 +172,7 @@ pub enum EthereumEvent { impl EthereumEvent { /// SHA256 of the Borsh serialization of the [`EthereumEvent`]. #[allow(dead_code)] - fn hash(&self) -> Result { + pub(crate) fn hash(&self) -> Result { let bytes = self.try_to_vec()?; Ok(Hash::sha256(&bytes)) } diff --git a/shared/src/types/mod.rs b/shared/src/types/mod.rs index 748475a423..ddef48f8eb 100644 --- a/shared/src/types/mod.rs +++ b/shared/src/types/mod.rs @@ -17,3 +17,5 @@ pub mod time; pub mod token; pub mod transaction; pub mod validity_predicate; +pub mod vote_extensions; +pub mod voting_power; diff --git a/shared/src/types/transaction/protocol.rs b/shared/src/types/transaction/protocol.rs index a956935a52..9c70b5b078 100644 --- a/shared/src/types/transaction/protocol.rs +++ b/shared/src/types/transaction/protocol.rs @@ -33,9 +33,9 @@ mod protocol_txs { use super::*; use crate::proto::Tx; - use crate::types::ethereum_events::vote_extensions::VoteExtensionDigest; use crate::types::key::*; use crate::types::transaction::{EllipticCurve, TxError, TxType}; + use crate::types::vote_extensions::ethereum_events; const TX_NEW_DKG_KP_WASM: &str = "tx_update_dkg_session_keypair.wasm"; @@ -78,7 +78,7 @@ mod protocol_txs { /// Tx requesting a new DKG session keypair NewDkgKeypair(Tx), /// Ethereum events contained in vote extensions - EthereumEvents(VoteExtensionDigest), + EthereumEvents(ethereum_events::VextDigest), } impl ProtocolTxType { diff --git a/shared/src/types/vote_extensions.rs b/shared/src/types/vote_extensions.rs new file mode 100644 index 0000000000..7737017aee --- /dev/null +++ b/shared/src/types/vote_extensions.rs @@ -0,0 +1,28 @@ +//! This module contains types necessary for processing vote extensions. + +pub mod ethereum_events; + +// TODO: add a `VoteExtension` type +// +// ```ignore +// pub struct VoteExtension { +// pub ethereum_events: Signed, +// pub validator_set_update: Option, +// } +// ``` + +// 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` diff --git a/shared/src/types/ethereum_events/vote_extensions.rs b/shared/src/types/vote_extensions/ethereum_events.rs similarity index 50% rename from shared/src/types/ethereum_events/vote_extensions.rs rename to shared/src/types/vote_extensions/ethereum_events.rs index f8cc801f14..b0edb0ec28 100644 --- a/shared/src/types/ethereum_events/vote_extensions.rs +++ b/shared/src/types/vote_extensions/ethereum_events.rs @@ -2,23 +2,24 @@ //! in vote extensions. use std::collections::{HashMap, HashSet}; -use std::ops::{Add, AddAssign}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use eyre::{eyre, Result}; -use num_rational::Ratio; -use super::EthereumEvent; use crate::proto::Signed; use crate::types::address::Address; +use crate::types::ethereum_events::EthereumEvent; use crate::types::key::common::{self, Signature}; use crate::types::storage::BlockHeight; +/// Represents a set of [`EthereumEvent`] instances +/// seen by some validator. +/// /// This struct will be created and signed over by each -/// validator as their vote extension. +/// active validator, to be included as a vote extension at the end of a +/// Tendermint PreCommit phase. #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -pub struct VoteExtension { - /// The block height for which this [`VoteExtension`] was made. +pub struct Vext { + /// The block height for which this [`Vext`] was made. pub block_height: BlockHeight, /// TODO: the validator's address is temporarily being included /// until we're able to map a Tendermint address to a validator @@ -29,8 +30,8 @@ pub struct VoteExtension { pub ethereum_events: Vec, } -impl VoteExtension { - /// Creates a [`VoteExtension`] without any Ethereum events. +impl Vext { + /// Creates a [`Vext`] without any Ethereum events. pub fn empty(block_height: BlockHeight, validator_addr: Address) -> Self { Self { block_height, @@ -39,102 +40,13 @@ impl VoteExtension { } } - /// Sign a vote extension and return the data with signature + /// Sign a [`Vext`] with a validator's `signing_key`, + /// and return the signed data. pub fn sign(self, signing_key: &common::SecretKey) -> Signed { Signed::new(signing_key, self) } } -/// A fraction of the total voting power. This should always be a reduced -/// fraction that is between zero and one inclusive. -#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Debug)] -pub struct FractionalVotingPower(Ratio); - -impl FractionalVotingPower { - /// Two thirds of the voting power. - pub const TWO_THIRDS: FractionalVotingPower = - FractionalVotingPower(Ratio::new_raw(2, 3)); - - /// Create a new FractionalVotingPower. It must be between zero and one - /// inclusive. - pub fn new(numer: u64, denom: u64) -> Result { - if denom == 0 { - return Err(eyre!("denominator can't be zero")); - } - let ratio: Ratio = (numer, denom).into(); - if ratio > 1.into() { - return Err(eyre!( - "fractional voting power cannot be greater than one" - )); - } - Ok(Self(ratio)) - } -} - -impl Default for FractionalVotingPower { - fn default() -> Self { - Self::new(0, 1).unwrap() - } -} - -impl From<&FractionalVotingPower> for (u64, u64) { - fn from(ratio: &FractionalVotingPower) -> Self { - (ratio.0.numer().to_owned(), ratio.0.denom().to_owned()) - } -} - -impl Add for FractionalVotingPower { - type Output = Self; - - fn add(self, rhs: FractionalVotingPower) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl AddAssign for FractionalVotingPower { - fn add_assign(&mut self, rhs: FractionalVotingPower) { - *self = Self(self.0 + rhs.0) - } -} - -impl BorshSerialize for FractionalVotingPower { - fn serialize( - &self, - writer: &mut W, - ) -> std::io::Result<()> { - let (numer, denom): (u64, u64) = self.into(); - (numer, denom).serialize(writer) - } -} - -impl BorshDeserialize for FractionalVotingPower { - fn deserialize(buf: &mut &[u8]) -> std::io::Result { - let (numer, denom): (u64, u64) = BorshDeserialize::deserialize(buf)?; - Ok(FractionalVotingPower(Ratio::::new(numer, denom))) - } -} - -impl BorshSchema for FractionalVotingPower { - fn add_definitions_recursively( - definitions: &mut std::collections::HashMap< - borsh::schema::Declaration, - borsh::schema::Definition, - >, - ) { - let fields = - borsh::schema::Fields::UnnamedFields(borsh::maybestd::vec![ - u64::declaration(), - u64::declaration() - ]); - let definition = borsh::schema::Definition::Struct { fields }; - Self::add_definition(Self::declaration(), definition, definitions); - } - - fn declaration() -> borsh::schema::Declaration { - "FractionalVotingPower".into() - } -} - /// Aggregates an Ethereum event with the corresponding // validators who saw this event. #[derive( @@ -147,30 +59,27 @@ pub struct MultiSignedEthEvent { pub signers: HashSet
, } -/// Compresses a set of signed `VoteExtension` instances, to save +/// Compresses a set of signed [`Vext`] instances, to save /// space on a block. #[derive( Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, BorshSchema, )] -pub struct VoteExtensionDigest { - /// The signatures and signing address of each VoteExtension +pub struct VextDigest { + /// The signatures and signing address of each [`Vext`] pub signatures: HashMap, /// The events that were reported pub events: Vec, } -impl VoteExtensionDigest { - /// Decompresses a set of signed `VoteExtension` instances. - pub fn decompress( - self, - last_height: BlockHeight, - ) -> Vec> { - let VoteExtensionDigest { signatures, events } = self; +impl VextDigest { + /// Decompresses a set of signed [`Vext`] instances. + pub fn decompress(self, last_height: BlockHeight) -> Vec> { + let VextDigest { signatures, events } = self; let mut extensions = vec![]; for (addr, sig) in signatures.into_iter() { - let mut ext = VoteExtension::empty(last_height, addr.clone()); + let mut ext = Vext::empty(last_height, addr.clone()); for event in events.iter() { if event.signers.contains(&addr) { @@ -195,11 +104,10 @@ impl VoteExtensionDigest { mod tests { use std::collections::HashSet; - use super::super::EthereumEvent; use super::*; use crate::proto::Signed; use crate::types::address::{self, Address}; - use crate::types::ethereum_events::Uint; + use crate::types::ethereum_events::{EthereumEvent, Uint}; use crate::types::hash::Hash; use crate::types::key; use crate::types::key::RefTo; @@ -225,41 +133,10 @@ mod tests { ); } - /// This test is ultimately just exercising the underlying - /// library we use for fractions, we want to make sure - /// operators work as expected with our FractionalVotingPower - /// type itself - #[test] - fn test_fractional_voting_power_ord_eq() { - assert!( - FractionalVotingPower::TWO_THIRDS - > FractionalVotingPower::new(1, 4).unwrap() - ); - assert!( - FractionalVotingPower::new(1, 3).unwrap() - > FractionalVotingPower::new(1, 4).unwrap() - ); - assert!( - FractionalVotingPower::new(1, 3).unwrap() - == FractionalVotingPower::new(2, 6).unwrap() - ); - } - - /// Test error handling on the FractionalVotingPower type - #[test] - fn test_fractional_voting_power_valid_fractions() { - assert!(FractionalVotingPower::new(0, 0).is_err()); - assert!(FractionalVotingPower::new(1, 0).is_err()); - assert!(FractionalVotingPower::new(0, 1).is_ok()); - assert!(FractionalVotingPower::new(1, 1).is_ok()); - assert!(FractionalVotingPower::new(1, 2).is_ok()); - assert!(FractionalVotingPower::new(3, 2).is_err()); - } - /// Test decompression of a set of Ethereum events #[test] fn test_decompress_ethereum_events() { - // we need to construct a `Vec>` + // we need to construct a `Vec>` let sk_1 = key::testing::keypair_1(); let sk_2 = key::testing::keypair_2(); @@ -277,8 +154,8 @@ mod tests { let validator_1 = address::testing::established_address_1(); let validator_2 = address::testing::established_address_2(); - let ext = |validator: Address| -> VoteExtension { - let mut ext = VoteExtension::empty(last_block_height, validator); + let ext = |validator: Address| -> Vext { + let mut ext = Vext::empty(last_block_height, validator); ext.ethereum_events.push(ev_1.clone()); ext.ethereum_events.push(ev_2.clone()); @@ -294,8 +171,8 @@ mod tests { let ext = vec![ext_1, ext_2]; - // we have the `Signed` instances we need, - // let us now compress them into a single `VoteExtensionDigest` + // we have the `Signed` instances we need, + // let us now compress them into a single `VextDigest` let signatures: HashMap<_, _> = [ (validator_1.clone(), ext[0].sig.clone()), (validator_2.clone(), ext[1].sig.clone()), @@ -319,16 +196,16 @@ mod tests { }, ]; - let digest = VoteExtensionDigest { events, signatures }; + let digest = VextDigest { events, signatures }; - // finally, decompress the `VoteExtensionDigest` back into a - // `Vec>` + // finally, decompress the `VextDigest` back into a + // `Vec>` let mut decompressed = digest .decompress(last_block_height) .into_iter() - .collect::>>(); + .collect::>>(); - // decompressing yields an arbitrary ordering of `VoteExtension` + // decompressing yields an arbitrary ordering of `Vext` // instances, which is fine if decompressed[0].data.validator_addr != ext[0].data.validator_addr { decompressed.swap(0, 1); diff --git a/shared/src/types/voting_power.rs b/shared/src/types/voting_power.rs new file mode 100644 index 0000000000..e5c35c20bb --- /dev/null +++ b/shared/src/types/voting_power.rs @@ -0,0 +1,133 @@ +//! This module contains types related with validator voting power calculations. + +use std::ops::{Add, AddAssign}; + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use eyre::{eyre, Result}; +use num_rational::Ratio; + +/// A fraction of the total voting power. This should always be a reduced +/// fraction that is between zero and one inclusive. +#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Debug)] +pub struct FractionalVotingPower(Ratio); + +impl FractionalVotingPower { + /// Two thirds of the voting power. + pub const TWO_THIRDS: FractionalVotingPower = + FractionalVotingPower(Ratio::new_raw(2, 3)); + + /// Create a new FractionalVotingPower. It must be between zero and one + /// inclusive. + pub fn new(numer: u64, denom: u64) -> Result { + if denom == 0 { + return Err(eyre!("denominator can't be zero")); + } + let ratio: Ratio = (numer, denom).into(); + if ratio > 1.into() { + return Err(eyre!( + "fractional voting power cannot be greater than one" + )); + } + Ok(Self(ratio)) + } +} + +impl Default for FractionalVotingPower { + fn default() -> Self { + Self::new(0, 1).unwrap() + } +} + +impl From<&FractionalVotingPower> for (u64, u64) { + fn from(ratio: &FractionalVotingPower) -> Self { + (ratio.0.numer().to_owned(), ratio.0.denom().to_owned()) + } +} + +impl Add for FractionalVotingPower { + type Output = Self; + + fn add(self, rhs: FractionalVotingPower) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl AddAssign for FractionalVotingPower { + fn add_assign(&mut self, rhs: FractionalVotingPower) { + *self = Self(self.0 + rhs.0) + } +} + +impl BorshSerialize for FractionalVotingPower { + fn serialize( + &self, + writer: &mut W, + ) -> std::io::Result<()> { + let (numer, denom): (u64, u64) = self.into(); + (numer, denom).serialize(writer) + } +} + +impl BorshDeserialize for FractionalVotingPower { + fn deserialize(buf: &mut &[u8]) -> std::io::Result { + let (numer, denom): (u64, u64) = BorshDeserialize::deserialize(buf)?; + Ok(FractionalVotingPower(Ratio::::new(numer, denom))) + } +} + +impl BorshSchema for FractionalVotingPower { + fn add_definitions_recursively( + definitions: &mut std::collections::HashMap< + borsh::schema::Declaration, + borsh::schema::Definition, + >, + ) { + let fields = + borsh::schema::Fields::UnnamedFields(borsh::maybestd::vec![ + u64::declaration(), + u64::declaration() + ]); + let definition = borsh::schema::Definition::Struct { fields }; + Self::add_definition(Self::declaration(), definition, definitions); + } + + fn declaration() -> borsh::schema::Declaration { + "FractionalVotingPower".into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// This test is ultimately just exercising the underlying + /// library we use for fractions, we want to make sure + /// operators work as expected with our FractionalVotingPower + /// type itself + #[test] + fn test_fractional_voting_power_ord_eq() { + assert!( + FractionalVotingPower::TWO_THIRDS + > FractionalVotingPower::new(1, 4).unwrap() + ); + assert!( + FractionalVotingPower::new(1, 3).unwrap() + > FractionalVotingPower::new(1, 4).unwrap() + ); + assert!( + FractionalVotingPower::new(1, 3).unwrap() + == FractionalVotingPower::new(2, 6).unwrap() + ); + } + + /// Test error handling on the FractionalVotingPower type + #[test] + fn test_fractional_voting_power_valid_fractions() { + assert!(FractionalVotingPower::new(0, 0).is_err()); + assert!(FractionalVotingPower::new(1, 0).is_err()); + assert!(FractionalVotingPower::new(0, 1).is_ok()); + assert!(FractionalVotingPower::new(1, 1).is_ok()); + assert!(FractionalVotingPower::new(1, 2).is_ok()); + assert!(FractionalVotingPower::new(3, 2).is_err()); + } +}