Skip to content

Commit

Permalink
vote tally initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
cryptoAtwill committed Sep 20, 2024
1 parent 77da09c commit 7ffc600
Show file tree
Hide file tree
Showing 9 changed files with 730 additions and 9 deletions.
35 changes: 35 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ libp2p-mplex = { version = "0.41" }
# libp2p-bitswap = "0.25.1"
libp2p-bitswap = { git = "https://github.com/consensus-shipyard/libp2p-bitswap.git", branch = "chore-upgrade-libipld" } # Updated to libipld 0.16
libsecp256k1 = "0.7"
secp256k1 = {version = "0.27.0" }

literally = "0.1.3"
log = "0.4"
lru_time_cache = "0.11"
Expand Down
2 changes: 1 addition & 1 deletion fendermint/vm/genesis/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ multihash = { workspace = true, optional = true }
fvm_shared = { workspace = true }
ipc-api = { workspace = true }
fendermint_actor_eam = { workspace = true }
hex = { workspace = true }

fendermint_crypto = { path = "../../crypto" }
fendermint_testing = { path = "../../testing", optional = true }
Expand All @@ -32,7 +33,6 @@ fendermint_vm_encoding = { path = "../encoding" }
[dev-dependencies]
quickcheck = { workspace = true }
quickcheck_macros = { workspace = true }
hex = { workspace = true }
serde_json = { workspace = true }

# Enable arb on self for tests.
Expand Down
21 changes: 21 additions & 0 deletions fendermint/vm/genesis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! A Genesis data structure similar to [genesis.Template](https://github.com/filecoin-project/lotus/blob/v1.20.4/genesis/types.go)
//! in Lotus, which is used to [initialize](https://github.com/filecoin-project/lotus/blob/v1.20.4/chain/gen/genesis/genesis.go) the state tree.

use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
use anyhow::anyhow;
use fvm_shared::bigint::{BigInt, Integer};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -150,6 +152,12 @@ impl ValidatorKey {
Self(normalize_public_key(key))
}

pub fn from_compressed_pubkey(compress: &[u8; 33]) -> anyhow::Result<Self> {
Ok(Self(
PublicKey::parse_compressed(compress)?
))
}

pub fn public_key(&self) -> &PublicKey {
&self.0
}
Expand Down Expand Up @@ -246,6 +254,19 @@ pub mod ipc {
}
}

impl Display for ValidatorKey {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "Validator({})", hex::encode(self.0.serialize()))
}
}

impl Hash for ValidatorKey {
fn hash<H: Hasher>(&self, h: &mut H) {
let bytes = self.0.serialize();
Hash::hash(&bytes, h);
}
}

#[cfg(test)]
mod tests {
use fvm_shared::{bigint::BigInt, econ::TokenAmount};
Expand Down
3 changes: 2 additions & 1 deletion fendermint/vm/topdown/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@ prometheus = { workspace = true }
fendermint_vm_genesis = { path = "../genesis" }
fendermint_vm_event = { path = "../event" }
fendermint_tracing = { path = "../../tracing" }
fendermint_crypto = { path = "../../crypto" }

ipc-observability = { workspace = true }
secp256k1 = { workspace = true, features = ["recovery", "bitcoin_hashes"] }

[dev-dependencies]
arbitrary = { workspace = true }
clap = { workspace = true }
rand = { workspace = true }
tracing-subscriber = { workspace = true }

fendermint_crypto = { path = "../../crypto" }
fendermint_testing = { path = "../../testing", features = ["smt"] }
41 changes: 34 additions & 7 deletions fendermint/vm/topdown/src/vote/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
// SPDX-License-Identifier: Apache-2.0, MIT

mod operation;
mod payload;
mod store;
mod tally;

use crate::sync::TopDownSyncEvent;
use crate::vote::operation::{OperationMetrics, OperationStateMachine};
use crate::vote::payload::Vote;
use crate::BlockHeight;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tokio::sync::{broadcast, mpsc};

#[derive(Clone)]
pub struct VoteRecord {}
pub type Weight = u64;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Config {
Expand All @@ -31,8 +34,8 @@ pub struct VoteReactorClient {

pub fn start_vote_reactor(
config: Config,
gossip_rx: broadcast::Receiver<VoteRecord>,
gossip_tx: mpsc::Sender<VoteRecord>,
gossip_rx: broadcast::Receiver<Vote>,
gossip_tx: mpsc::Sender<Vote>,
internal_event_listener: broadcast::Receiver<TopDownSyncEvent>,
) -> VoteReactorClient {
let (tx, rx) = mpsc::channel(config.req_channel_buffer_size);
Expand All @@ -57,6 +60,27 @@ pub fn start_vote_reactor(
VoteReactorClient { tx }
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("the last finalized block has not been set")]
Uninitialized,

#[error("failed to extend chain; height going backwards, current height {0}, got {1}")]
UnexpectedBlock(BlockHeight, BlockHeight),

#[error("validator unknown or has no power")]
UnpoweredValidator,

#[error("equivocation by validator")]
Equivocation,

#[error("validator vote is invalidated")]
VoteCannotBeValidated,

#[error("validator cannot sign vote")]
CannotSignVote,
}

enum VoteReactorRequest {
QueryOperationMode,
QueryVotes(BlockHeight),
Expand All @@ -67,8 +91,8 @@ struct VotingHandler {
/// vote tally status and etc.
req_rx: mpsc::Receiver<VoteReactorRequest>,
/// Receiver from gossip pub/sub, mostly listening to incoming votes
gossip_rx: broadcast::Receiver<VoteRecord>,
gossip_tx: mpsc::Sender<VoteRecord>,
gossip_rx: broadcast::Receiver<Vote>,
gossip_tx: mpsc::Sender<Vote>,
/// Listens to internal events and handles the events accordingly
internal_event_listener: broadcast::Receiver<TopDownSyncEvent>,
config: Config,
Expand All @@ -77,10 +101,11 @@ struct VotingHandler {
impl VotingHandler {
fn handle_request(&self, _req: VoteReactorRequest) {}

fn record_vote(&self, _vote: VoteRecord) {}
fn record_vote(&self, _vote: Vote) {}

fn handle_event(&self, _event: TopDownSyncEvent) {}

/// Process external request, such as RPC queries for debugging and status tracking.
fn process_external_request(&mut self, _metrics: &OperationMetrics) -> usize {
let mut n = 0;
while n < self.config.req_batch_processing_size {
Expand All @@ -99,6 +124,7 @@ impl VotingHandler {
n
}

/// Handles vote tally gossip pab/sub incoming votes from other peers
fn process_gossip_subscription_votes(&mut self) -> usize {
let mut n = 0;
while n < self.config.gossip_req_processing_size {
Expand All @@ -117,6 +143,7 @@ impl VotingHandler {
n
}

/// Poll internal topdown syncer event broadcasted.
fn poll_internal_event(&mut self) -> Option<TopDownSyncEvent> {
match self.internal_event_listener.try_recv() {
Ok(event) => Some(event),
Expand Down
143 changes: 143 additions & 0 deletions fendermint/vm/topdown/src/vote/payload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright 2022-2024 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT

use crate::vote::Weight;
use crate::{BlockHash, BlockHeight, Bytes};
use anyhow::anyhow;
use fendermint_vm_genesis::ValidatorKey;
use secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};

pub type Signature = Bytes;
pub type RecoverableECDSASignature = (i32, Vec<u8>);

pub type PowerTable = HashMap<ValidatorKey, Weight>;

/// The different versions of vote casted in topdown gossip pub-sub channel
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
pub enum Vote {
V1 {
validator: ValidatorKey,
payload: CertifiedObservation,
},
}

/// The actual content that validators should agree upon, or put in another way the content
/// that a quorum should be formed upon
#[derive(Serialize, Deserialize, Hash, Debug, Clone, Eq, PartialEq)]
pub struct Ballot {
parent_height: u64,
/// The hash of the chain unit at that height. Usually a block hash, but could
/// be another entity (e.g. tipset CID), depending on the parent chain
/// and our interface to it. For example, if the parent is a Filecoin network,
/// this would be a tipset CID coerced into a block hash if queried through
/// the Eth API, or the tipset CID as-is if accessed through the Filecoin API.
parent_hash: Bytes,
/// A rolling/cumulative commitment to topdown effects since the beginning of
/// time, including the ones in this block.
cumulative_effects_comm: Bytes,
}

/// The content that validators gossip among each other
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
pub struct Observation {
/// The hash of the subnet's last committed block when this observation was made.
/// Used to discard stale observations that are, e.g. replayed by an attacker
/// at a later time. Also used to detect nodes that might be wrongly gossiping
/// whilst being out of sync.
local_hash: BlockHash,
ballot: Ballot,
}

/// A self-certified observation made by a validator.
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
pub struct CertifiedObservation {
observed: Observation,
/// A "recoverable" ECDSA signature with the validator's secp256k1 private key over the
/// CID of the DAG-CBOR encoded observation using a BLAKE2b-256 multihash.
signature: RecoverableECDSASignature,
}

impl Vote {
pub fn voter(&self) -> ValidatorKey {
match self {
Self::V1 { validator, .. } => validator.clone(),
}
}

pub fn ballot(&self) -> &Ballot {
match self {
Self::V1 { payload, .. } => &payload.observed.ballot,
}
}
}

impl TryFrom<&[u8]> for Vote {
type Error = anyhow::Error;

fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let version = bytes[0];

if version == 0 {
let obs = CertifiedObservation::try_from(&bytes[1..])?;
let to_sign = fvm_ipld_encoding::to_vec(&obs.observed)?;
let (validator, _) = recover_ecdsa_sig(&to_sign, obs.signature.0, &obs.signature.1)?;
return Ok(Self::V1 {
validator,
payload: obs,
});
}

Err(anyhow!("invalid vote version"))
}
}

impl TryFrom<&[u8]> for CertifiedObservation {
type Error = anyhow::Error;

fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
Ok(fvm_ipld_encoding::from_slice(bytes)?)
}
}

fn recover_ecdsa_sig(
payload: &[u8],
rec_id: i32,
sig: &[u8],
) -> anyhow::Result<(ValidatorKey, Signature)> {
let secp = secp256k1::Secp256k1::new();

let message = secp256k1::Message::from_hashed_data::<secp256k1::hashes::sha256::Hash>(payload);
let pubkey = secp.recover_ecdsa(
&message,
&RecoverableSignature::from_compact(sig, RecoveryId::from_i32(rec_id)?)?,
)?;
let signature = secp256k1::ecdsa::Signature::from_compact(sig)?
.serialize_der()
.to_vec();

Ok((
ValidatorKey::from_compressed_pubkey(&pubkey.serialize())?,
signature,
))
}

impl Display for Ballot {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Ballot(parent_height={}, parent_hash={}, commitment={})",
self.parent_height,
hex::encode(&self.parent_hash),
hex::encode(&self.cumulative_effects_comm),
)
}
}

impl Ballot {
pub fn parent_height(&self) -> BlockHeight {
self.parent_height
}
}
Loading

0 comments on commit 7ffc600

Please sign in to comment.