diff --git a/Cargo.toml b/Cargo.toml index 1a55921..345b359 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/data-chain", "crates/storage", "crates/node", + "crates/consensus", ] resolver = "2" @@ -70,6 +71,20 @@ hex = "0.4" toml = "0.8" chrono = "0.4" +# Malachite BFT (gated behind the consensus crate's `malachite` feature) +informalsystems-malachitebft-app = { version = "0.5", default-features = false } +informalsystems-malachitebft-app-channel = { version = "0.5", default-features = false } +informalsystems-malachitebft-core-consensus = { version = "0.5", default-features = false } +informalsystems-malachitebft-core-driver = { version = "0.5", default-features = false } +informalsystems-malachitebft-core-types = { version = "0.5", default-features = false } +informalsystems-malachitebft-engine = { version = "0.5", default-features = false } +informalsystems-malachitebft-network = { version = "0.5", default-features = false } +informalsystems-malachitebft-config = { version = "0.5", default-features = false } +informalsystems-malachitebft-sync = { version = "0.5", default-features = false } +informalsystems-malachitebft-signing-ed25519 = { version = "0.5", default-features = false } +informalsystems-malachitebft-codec = { version = "0.5", default-features = false } +informalsystems-malachitebft-metrics = { version = "0.5", default-features = false } + [profile.release] opt-level = 3 lto = true diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml new file mode 100644 index 0000000..3848020 --- /dev/null +++ b/crates/consensus/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "cipherbft-consensus" +version = "0.1.0" +edition = "2021" +license = "MIT" +publish = false + +[features] +default = [] +# Enable this to pull in Malachite crates and compile the Context/adapter code. +malachite = [ + "informalsystems-malachitebft-app", + "informalsystems-malachitebft-app-channel", + "informalsystems-malachitebft-core-consensus", + "informalsystems-malachitebft-core-driver", + "informalsystems-malachitebft-core-types", + "informalsystems-malachitebft-engine", + "informalsystems-malachitebft-network", + "informalsystems-malachitebft-config", + "informalsystems-malachitebft-sync", + "informalsystems-malachitebft-signing-ed25519", + "informalsystems-malachitebft-codec", + "informalsystems-malachitebft-metrics", +] + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +tracing = { workspace = true } +hex = { workspace = true } + +cipherbft-types = { path = "../types" } +cipherbft-crypto = { path = "../crypto" } +cipherbft-data-chain = { path = "../data-chain" } + +# Malachite BFT (all optional; enable via the `malachite` feature) +informalsystems-malachitebft-app = { workspace = true, optional = true } +informalsystems-malachitebft-app-channel = { workspace = true, optional = true } +informalsystems-malachitebft-core-consensus = { workspace = true, optional = true } +informalsystems-malachitebft-core-driver = { workspace = true, optional = true } +informalsystems-malachitebft-core-types = { workspace = true, optional = true } +informalsystems-malachitebft-engine = { workspace = true, optional = true } +informalsystems-malachitebft-network = { workspace = true, optional = true } +informalsystems-malachitebft-config = { workspace = true, optional = true } +informalsystems-malachitebft-sync = { workspace = true, optional = true } +informalsystems-malachitebft-signing-ed25519 = { workspace = true, optional = true } +informalsystems-malachitebft-codec = { workspace = true, optional = true } +informalsystems-malachitebft-metrics = { workspace = true, optional = true } diff --git a/crates/consensus/README.md b/crates/consensus/README.md new file mode 100644 index 0000000..ffec3b7 --- /dev/null +++ b/crates/consensus/README.md @@ -0,0 +1,96 @@ +# CipherBFT Consensus + +Consensus layer scaffold for integrating Malachite BFT. + +- Default build keeps Malachite crates disabled; enable with `--features malachite`. +- Malachite crates currently require Rust 1.85+ per their `rust-version`. + +## Modules + +- **`config`**: Consensus configuration (chain ID, timeouts). +- **`types`**: Core consensus types with Malachite trait implementations. +- **`context`**: `malachitebft_core_types::Context` implementation (round-robin proposer selection). +- **`proposal`**: Cut-as-proposal wrapper implementing Malachite `Proposal` and `ProposalPart` traits. +- **`validator_set`**: Validator set management with voting power and deterministic ordering. +- **`signing`**: Ed25519 signing scheme implementation for Malachite. +- **`vote`**: Vote types implementing Malachite `Vote` trait with optional extensions. +- **`engine`**: Wiring helper that spawns Malachite consensus/host/network/WAL actors when provided with the external handles. + +## Malachite Trait Implementations + +This crate implements the following Malachite core types traits (all gated behind the `malachite` feature): + +### Core Type Traits (`types.rs` - `malachite_impls` module) + +The `malachite_impls` module in `types.rs` contains: + +- **`Height` for `ConsensusHeight`**: Implements height arithmetic (increment/decrement), zero and initial height constants (`ZERO = 0`, `INITIAL = 1`), and conversion to/from `u64`. +- **`Value` for `ConsensusValue`**: Wraps DCL `Cut` as a consensus value, with `ConsensusValueId` (Cut hash) as the associated `Id` type. The `id()` method returns the hash of the underlying `Cut`. +- **`From for ConsensusValue`**: Enables direct conversion from DCL cuts to consensus values. +- **`From for Hash`**: Conversion from value IDs back to hash types for compatibility with other parts of the codebase. +- **`ConsensusRound` type alias**: Re-exports Malachite's `Round` type when the feature is enabled (falls back to `i64` when disabled). + +### Context and Protocol Traits (`context.rs`, `proposal.rs`, `vote.rs`) + +- **`Context` for `CipherBftContext`** (`context.rs`): Main context implementation providing proposer selection (round-robin based on validator set ordering), proposal/vote creation helpers, and validator set access. Defines all associated types for the consensus protocol. + +- **`Proposal` for `CutProposal`** (`proposal.rs`): Proposal type carrying height, round, value (ConsensusValue), POL round, and proposer address. Implements methods for accessing proposal metadata and extracting the consensus value. + +- **`ProposalPart` for `CutProposalPart`** (`proposal.rs`): Single-part proposal chunks with `is_first()` and `is_last()` flags. Currently supports single-chunk proposals via `CutProposalPart::single()`, but structured to support multi-part streaming in the future. + +- **`Vote` for `ConsensusVote`** (`vote.rs`): Prevote and precommit vote types with height, round, value ID (`NilOrVal`), vote type, validator address, and optional signed extensions. Supports vote extension through the `extend()` method. + +### Validator Traits (`validator_set.rs`) + +- **`Address` for `ConsensusAddress`**: Validator address type implemented as a marker trait, wrapping Ed25519-derived validator ID (`ValidatorId`). + +- **`Validator` for `ConsensusValidator`**: Individual validator entry providing access to address, public key (as Malachite `PublicKey`), and voting power. Each validator is uniquely identified by its `ConsensusAddress`. + +- **`ValidatorSet` for `ConsensusValidatorSet`**: Deterministically ordered validator set that implements voting power bookkeeping. Validators are sorted by power (descending), then by address (ascending) to ensure deterministic ordering required by Malachite. Provides methods for lookup by address or index, total voting power calculation, and validator count. + +### Signing Traits (`signing.rs`) + +- **`SigningScheme` for `Ed25519SigningScheme`**: Ed25519 signature scheme with 64-byte signature encoding/decoding, integrated with `cipherbft-crypto` Ed25519 types. + +## Engine Wiring (`engine.rs`) + +The `engine` module provides a builder pattern for wiring CipherBFT context and types into Malachite's actor-based consensus engine: + +- **`MalachiteEngineBuilder`**: Builder that assembles all required components (context, consensus parameters, signing provider, network/host/WAL/sync actors, metrics, and event channels) and spawns the Malachite consensus and node supervisors. Expects callers to provide pre-instantiated network, host, and WAL actors that already satisfy Malachite's message contracts. + +- **`EngineHandles`**: Bundles all actor references returned after spawning: node, consensus, network, WAL, host, optional sync actor, event channel, and metrics registry. These handles allow external code to interact with the running consensus engine. + +The builder pattern allows optional configuration via: + - `with_sync()`: Attach the sync actor for state synchronization + - `with_metrics()`: Override the default metrics registry + - `with_events()`: Override the default event channel + +The `spawn()` method creates and starts the consensus and node actors, returning handles that can be used to send messages and monitor the consensus process. + +## How it fits together (workflow) + +1) Configuration and types + - `ConsensusConfig` carries chain id and timeouts. + - `ConsensusHeight` implements Malachite `Height`; `ConsensusValue` wraps a DCL `Cut`; `ConsensusValueId` is the Cut hash. + - `ConsensusRound` reuses Malachite `Round` when the feature is on. + +2) Validator set and proposer selection + - `ConsensusValidatorSet` sorts validators by (power desc, address asc) to meet Malachite ordering requirements. + - `CipherBftContext::select_proposer` uses round-robin over that ordered list (nil round maps to index 0). + +3) Proposal path + - `ConsensusValue::id()` returns the Cut hash; Malachite binds votes to this `ValueId`. + - `context.new_proposal` builds a `CutProposal` with height/round/POL round and proposer address. + - Proposal parts are currently single-chunk (`CutProposalPart::single`); streaming hooks are in place if we need to split large payloads later. + +4) Vote path + - `context.new_prevote` / `new_precommit` emit `ConsensusVote` with `NilOrVal` per Malachite’s API. + - Vote extensions are typed as `Vec` for now; they are carried through the trait methods but not populated yet. + +5) Signing scheme + - `Ed25519SigningScheme` wraps existing `cipherbft-crypto` Ed25519 types to satisfy Malachite `SigningScheme`. + - `ConsensusSignature` stores the raw 64-byte Ed25519 signature; encode/decode helpers plug directly into Malachite’s signature handling. + +6) Feature gating + - All Malachite-dependent code sits behind the `malachite` feature; default builds stay unaffected. + - Run `cargo check -p cipherbft-consensus --features malachite` after enabling Rust 1.85+. diff --git a/crates/consensus/src/config.rs b/crates/consensus/src/config.rs new file mode 100644 index 0000000..652c48b --- /dev/null +++ b/crates/consensus/src/config.rs @@ -0,0 +1,49 @@ +use std::time::Duration; + +/// Basic consensus configuration shared with the Malachite context. +#[derive(Clone, Debug)] +pub struct ConsensusConfig { + /// Chain identifier used for domain separation. + pub chain_id: String, + /// Timeout for proposal creation/broadcast. + pub propose_timeout: Duration, + /// Timeout for prevote step. + pub prevote_timeout: Duration, + /// Timeout for precommit step. + pub precommit_timeout: Duration, +} + +impl ConsensusConfig { + /// Create a new config with sensible defaults. + pub fn new(chain_id: impl Into) -> Self { + Self { + chain_id: chain_id.into(), + propose_timeout: Duration::from_secs(1), + prevote_timeout: Duration::from_secs(1), + precommit_timeout: Duration::from_secs(1), + } + } + + /// Set proposal timeout. + pub fn with_propose_timeout(mut self, duration: Duration) -> Self { + self.propose_timeout = duration; + self + } + + /// Set prevote timeout. + pub fn with_prevote_timeout(mut self, duration: Duration) -> Self { + self.prevote_timeout = duration; + self + } + + /// Set precommit timeout. + pub fn with_precommit_timeout(mut self, duration: Duration) -> Self { + self.precommit_timeout = duration; + self + } + + /// Chain ID accessor. + pub fn chain_id(&self) -> &str { + &self.chain_id + } +} diff --git a/crates/consensus/src/context.rs b/crates/consensus/src/context.rs new file mode 100644 index 0000000..141fbd9 --- /dev/null +++ b/crates/consensus/src/context.rs @@ -0,0 +1,140 @@ +use informalsystems_malachitebft_core_types::{ + Context as MalachiteContext, NilOrVal, Round, ValueId, VoteType, +}; + +use crate::config::ConsensusConfig; +use crate::proposal::{CutProposal, CutProposalPart}; +use crate::signing::Ed25519SigningScheme; +use crate::types::{ConsensusHeight, ConsensusValue}; +use crate::validator_set::{ConsensusAddress, ConsensusValidator, ConsensusValidatorSet}; +use crate::vote::ConsensusVote; + +/// Extension payload for votes (empty for now). +pub type CipherBftContextExtension = Vec; + +/// Aliases to make trait impl signatures clearer. +pub type CipherBftContextAddress = ConsensusAddress; +pub type CipherBftContextValidator = ConsensusValidator; +pub type CipherBftContextValidatorSet = ConsensusValidatorSet; +pub type CipherBftContextProposal = CutProposal; +pub type CipherBftContextProposalPart = CutProposalPart; +pub type CipherBftContextValue = ConsensusValue; +pub type CipherBftContextVote = ConsensusVote; +pub type CipherBftContextSigningScheme = Ed25519SigningScheme; + +/// Malachite context implementation scaffold. +#[derive(Clone, Debug)] +pub struct CipherBftContext { + /// Static consensus configuration. + pub config: ConsensusConfig, + /// Deterministic validator set for proposer selection and voting power. + pub validator_set: ConsensusValidatorSet, + /// Height the engine should start from. + pub initial_height: ConsensusHeight, +} + +impl CipherBftContext { + /// Create a new context. + pub fn new( + config: ConsensusConfig, + validator_set: ConsensusValidatorSet, + initial_height: ConsensusHeight, + ) -> Self { + Self { + config, + validator_set, + initial_height, + } + } + + /// Access the initial height. + pub fn initial_height(&self) -> ConsensusHeight { + self.initial_height + } + + /// Access the validator set. + pub fn validator_set(&self) -> &ConsensusValidatorSet { + &self.validator_set + } + + /// Chain ID accessor. + pub fn chain_id(&self) -> &str { + self.config.chain_id() + } + + /// Deterministic round-robin proposer selection. + pub fn proposer_at_round(&self, round: Round) -> Option { + let count = self.validator_set.len(); + if count == 0 { + return None; + } + + // Use round index modulo validator count; nil rounds map to first validator. + let idx = match round.as_i64() { + x if x < 0 => 0, + x => (x as usize) % count, + }; + self.validator_set.as_slice().get(idx).map(|v| v.address) + } +} + +impl MalachiteContext for CipherBftContext { + type Address = ConsensusAddress; + type Height = ConsensusHeight; + type ProposalPart = CutProposalPart; + type Proposal = CutProposal; + type Validator = ConsensusValidator; + type ValidatorSet = ConsensusValidatorSet; + type Value = ConsensusValue; + type Vote = ConsensusVote; + type Extension = CipherBftContextExtension; + type SigningScheme = Ed25519SigningScheme; + + fn select_proposer<'a>( + &self, + validator_set: &'a Self::ValidatorSet, + _height: Self::Height, + round: Round, + ) -> &'a Self::Validator { + let count = validator_set.len(); + let idx = match round.as_i64() { + x if x < 0 => 0, + x => (x as usize) % count.max(1), + }; + validator_set + .as_slice() + .get(idx) + .expect("validator_set must not be empty") + } + + fn new_proposal( + &self, + height: Self::Height, + round: Round, + value: Self::Value, + pol_round: Round, + address: Self::Address, + ) -> Self::Proposal { + CutProposal::new(height, round, value, pol_round, address) + } + + fn new_prevote( + &self, + height: Self::Height, + round: Round, + value_id: NilOrVal>, + address: Self::Address, + ) -> Self::Vote { + ConsensusVote::new(height, round, value_id, VoteType::Prevote, address) + } + + fn new_precommit( + &self, + height: Self::Height, + round: Round, + value_id: NilOrVal>, + address: Self::Address, + ) -> Self::Vote { + ConsensusVote::new(height, round, value_id, VoteType::Precommit, address) + } +} diff --git a/crates/consensus/src/engine.rs b/crates/consensus/src/engine.rs new file mode 100644 index 0000000..613fc82 --- /dev/null +++ b/crates/consensus/src/engine.rs @@ -0,0 +1,221 @@ +//! Malachite engine wiring scaffold. +//! +//! This module wires CipherBFT context/types into the Malachite engine actors. +//! It expects callers to provide network/host/WAL/sync actors that already +//! satisfy Malachite's message contracts; we simply glue them together and +//! spawn the consensus + node supervisors. +//! +//! ## Components for `MalachiteEngineBuilder::new()` +//! +//! 1. `CipherBftContext` - already implemented (`create_context` helper). +//! 2. `ConsensusParams` - helper `default_consensus_params` provided (ProposalOnly payload). +//! 3. `EngineConsensusConfig` - helper `default_engine_config_single_part` provided (ProposalOnly payload). +//! 4. `SigningProvider` - implemented in `signing.rs` as `ConsensusSigningProvider`. +//! 5. `NetworkRef` - still needed: create/bridge a network actor for consensus messages. +//! 6. `HostRef` - still needed: host actor to handle `AppMsg` (DCL Cut fetch/execute, etc.). +//! 7. `WalRef` - still needed: instantiate WAL actor with codec/path. + +use anyhow::Result; +use informalsystems_malachitebft_config::{ + ConsensusConfig as EngineConsensusConfig, ValuePayload as EngineValuePayload, +}; +use informalsystems_malachitebft_core_consensus::Params as ConsensusParams; +use informalsystems_malachitebft_core_types::ValuePayload; +use informalsystems_malachitebft_core_driver::ThresholdParams; +use informalsystems_malachitebft_app::types::core::SigningProvider; +use informalsystems_malachitebft_engine::consensus::{Consensus, ConsensusRef}; +use informalsystems_malachitebft_engine::host::HostRef; +use informalsystems_malachitebft_engine::network::NetworkRef; +use informalsystems_malachitebft_engine::node::{Node, NodeRef}; +use informalsystems_malachitebft_engine::sync::SyncRef; +use informalsystems_malachitebft_engine::util::events::TxEvent; +use informalsystems_malachitebft_engine::wal::WalRef; +use informalsystems_malachitebft_metrics::Metrics; +use tracing::info_span; + +use crate::config::ConsensusConfig; +use crate::context::CipherBftContext; +use crate::types::ConsensusHeight; +use crate::validator_set::{ConsensusAddress, ConsensusValidator, ConsensusValidatorSet}; + +/// Helper functions for creating CipherBftContext and Malachite-ready configs. +/// +/// Use these to assemble the pieces needed by `MalachiteEngineBuilder`: +/// - `create_context`: build a Context from chain_id/validators/height. +/// - `default_consensus_params`: wrap Context pieces into Malachite `Params`. +/// - `default_engine_config_single_part`: engine config tuned for proposal-only (single-part) values. + +/// Create a `CipherBftContext` from configuration and validators. +/// +/// This is a convenience function that constructs a `CipherBftContext` with +/// sensible defaults. The validator set is created from a list of validators +/// with their Ed25519 public keys and voting power. +/// +/// # Arguments +/// * `chain_id` - Chain identifier +/// * `validators` - List of validators with Ed25519 public keys and voting power +/// * `initial_height` - Starting height for consensus (defaults to 1 if None) +/// +/// # Example +/// ```rust,ignore +/// use cipherbft_consensus::{create_context, ConsensusValidator}; +/// use cipherbft_crypto::Ed25519PublicKey; +/// use cipherbft_types::ValidatorId; +/// +/// let validators = vec![ +/// ConsensusValidator::new( +/// validator_id, +/// ed25519_pubkey, +/// 100, // voting power +/// ), +/// ]; +/// let ctx = create_context("my-chain", validators, None); +/// ``` +pub fn create_context( + chain_id: impl Into, + validators: Vec, + initial_height: Option, +) -> CipherBftContext { + let config = ConsensusConfig::new(chain_id); + let validator_set = ConsensusValidatorSet::new(validators); + let initial_height = initial_height.unwrap_or_else(|| ConsensusHeight::from(1)); + + CipherBftContext::new(config, validator_set, initial_height) +} + +/// Create Malachite consensus params using our Context components. +/// +/// - `our_address` should match the validator's ConsensusAddress (derived from our Ed25519 key). +/// - `value_payload` is set to `ProposalOnly` because we currently send single-part cuts. +pub fn default_consensus_params( + ctx: &CipherBftContext, + our_address: ConsensusAddress, +) -> ConsensusParams { + ConsensusParams { + initial_height: ctx.initial_height(), + initial_validator_set: ctx.validator_set().clone(), + address: our_address, + threshold_params: ThresholdParams::default(), + value_payload: ValuePayload::ProposalOnly, + } +} + +/// Engine config tuned for single-part proposals (no proposal-part streaming). +pub fn default_engine_config_single_part() -> EngineConsensusConfig { + let mut cfg = EngineConsensusConfig::default(); + cfg.value_payload = EngineValuePayload::ProposalOnly; + cfg +} + +/// Bundles all actor handles returned after spawning. +pub struct EngineHandles { + pub node: NodeRef, + pub consensus: ConsensusRef, + pub network: NetworkRef, + pub wal: WalRef, + pub host: HostRef, + pub sync: Option>, + pub events: TxEvent, + pub metrics: Metrics, +} + +/// Builder for spinning up Malachite consensus actors. +pub struct MalachiteEngineBuilder { + pub ctx: CipherBftContext, + pub params: ConsensusParams, + pub consensus_config: EngineConsensusConfig, + pub signing_provider: Box>, + pub network: NetworkRef, + pub host: HostRef, + pub wal: WalRef, + pub sync: Option>, + pub metrics: Metrics, + pub events: TxEvent, +} + +impl MalachiteEngineBuilder { + /// Create a new builder with default metrics/event channels. + pub fn new( + ctx: CipherBftContext, + params: ConsensusParams, + consensus_config: EngineConsensusConfig, + signing_provider: Box>, + network: NetworkRef, + host: HostRef, + wal: WalRef, + ) -> Self { + Self { + ctx, + params, + consensus_config, + signing_provider, + network, + host, + wal, + sync: None, + metrics: Metrics::new(), + events: TxEvent::new(), + } + } + + /// Optionally attach the sync actor. + pub fn with_sync(mut self, sync: SyncRef) -> Self { + self.sync = Some(sync); + self + } + + /// Override metrics registry. + pub fn with_metrics(mut self, metrics: Metrics) -> Self { + self.metrics = metrics; + self + } + + /// Override event channel. + pub fn with_events(mut self, events: TxEvent) -> Self { + self.events = events; + self + } + + /// Spawn consensus + node supervisors. + pub async fn spawn(self) -> Result { + let span = info_span!("cipherbft-malachite", chain_id = %self.ctx.chain_id()); + + let consensus = Consensus::spawn( + self.ctx.clone(), + self.params, + self.consensus_config, + self.signing_provider, + self.network.clone(), + self.host.clone(), + self.wal.clone(), + self.sync.clone(), + self.metrics.clone(), + self.events.clone(), + span.clone(), + ) + .await?; + + let node = Node::new( + self.ctx, + self.network.clone(), + consensus.clone(), + self.wal.clone(), + self.sync.clone(), + self.host.clone(), + span, + ); + + let (node_ref, _) = node.spawn().await?; + + Ok(EngineHandles { + node: node_ref, + consensus, + network: self.network, + wal: self.wal, + host: self.host, + sync: self.sync, + events: self.events, + metrics: self.metrics, + }) + } +} diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs new file mode 100644 index 0000000..ab1e99f --- /dev/null +++ b/crates/consensus/src/lib.rs @@ -0,0 +1,40 @@ +//! Consensus Layer scaffolding +//! +//! This crate hosts the Malachite integration surface area. Malachite +//! dependencies are optional and gated behind the `malachite` feature so +//! existing builds remain unaffected while we wire up the consensus layer. + +pub mod config; +pub mod types; + +#[cfg(feature = "malachite")] +pub mod context; +#[cfg(feature = "malachite")] +pub mod proposal; +#[cfg(feature = "malachite")] +pub mod signing; +#[cfg(feature = "malachite")] +pub mod validator_set; +#[cfg(feature = "malachite")] +pub mod vote; +#[cfg(feature = "malachite")] +pub mod engine; + +pub use config::ConsensusConfig; +pub use types::{ConsensusHeight, ConsensusRound, ConsensusValue}; + +#[cfg(feature = "malachite")] +pub use context::CipherBftContext; +#[cfg(feature = "malachite")] +pub use proposal::{CutProposal, CutProposalPart}; +#[cfg(feature = "malachite")] +pub use signing::{ConsensusSigner, ConsensusSigningProvider}; +#[cfg(feature = "malachite")] +pub use validator_set::ConsensusValidatorSet; +#[cfg(feature = "malachite")] +pub use vote::ConsensusVote; +#[cfg(feature = "malachite")] +pub use engine::{ + create_context, default_consensus_params, default_engine_config_single_part, EngineHandles, + MalachiteEngineBuilder, +}; diff --git a/crates/consensus/src/proposal.rs b/crates/consensus/src/proposal.rs new file mode 100644 index 0000000..e1c32e5 --- /dev/null +++ b/crates/consensus/src/proposal.rs @@ -0,0 +1,100 @@ +use cipherbft_data_chain::Cut; +use informalsystems_malachitebft_core_types::{Proposal as MalachiteProposal, ProposalPart as MalachiteProposalPart, Round}; + +use crate::context::CipherBftContext; +use crate::types::{ConsensusHeight, ConsensusValue}; +use crate::validator_set::ConsensusAddress; + +/// Proposal wrapper carrying a cut and metadata. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CutProposal { + pub height: ConsensusHeight, + pub round: Round, + pub value: ConsensusValue, + pub pol_round: Round, + pub proposer: ConsensusAddress, +} + +impl CutProposal { + pub fn new( + height: ConsensusHeight, + round: Round, + value: ConsensusValue, + pol_round: Round, + proposer: ConsensusAddress, + ) -> Self { + Self { + height, + round, + value, + pol_round, + proposer, + } + } + + pub fn into_cut(self) -> Cut { + self.value.into_cut() + } +} + +impl MalachiteProposal for CutProposal { + fn height(&self) -> ConsensusHeight { + self.height + } + + fn round(&self) -> Round { + self.round + } + + fn value(&self) -> &::Value { + &self.value + } + + fn take_value(self) -> ::Value { + self.value + } + + fn pol_round(&self) -> Round { + self.pol_round + } + + fn validator_address(&self) -> &ConsensusAddress { + &self.proposer + } +} + +/// Single-part proposal chunk. +#[derive(Clone, Debug)] +pub struct CutProposalPart { + pub cut: Cut, + pub first: bool, + pub last: bool, +} + +impl CutProposalPart { + pub fn single(cut: Cut) -> Self { + Self { + cut, + first: true, + last: true, + } + } +} + +impl MalachiteProposalPart for CutProposalPart { + fn is_first(&self) -> bool { + self.first + } + + fn is_last(&self) -> bool { + self.last + } +} + +impl PartialEq for CutProposalPart { + fn eq(&self, other: &Self) -> bool { + self.cut.hash() == other.cut.hash() + } +} + +impl Eq for CutProposalPart {} diff --git a/crates/consensus/src/signing.rs b/crates/consensus/src/signing.rs new file mode 100644 index 0000000..4c8725d --- /dev/null +++ b/crates/consensus/src/signing.rs @@ -0,0 +1,276 @@ +use std::fmt::{Debug, Display}; + +use cipherbft_crypto::{ + Ed25519KeyPair, Ed25519PublicKey as CryptoPublicKey, Ed25519SecretKey as CryptoSecretKey, + Ed25519Signature as CryptoSignature, +}; +use informalsystems_malachitebft_core_types::SigningScheme; + +/// Wrapper around Ed25519 public key for Malachite. +#[derive(Clone, PartialEq, Eq)] +pub struct ConsensusPublicKey(pub CryptoPublicKey); + +impl Debug for ConsensusPublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ConsensusPublicKey({:?})", self.0) + } +} + +/// Wrapper around Ed25519 secret key for Malachite. +#[derive(Clone)] +pub struct ConsensusPrivateKey(pub CryptoSecretKey); + +impl Debug for ConsensusPrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ConsensusPrivateKey([REDACTED])") + } +} + +/// Wrapper around Ed25519 signature for Malachite. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ConsensusSignature(pub [u8; 64]); + +impl Debug for ConsensusSignature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ConsensusSignature({})", hex::encode(&self.0[..8])) + } +} + +impl From for ConsensusSignature { + fn from(sig: CryptoSignature) -> Self { + Self(sig.to_bytes()) + } +} + +impl ConsensusSignature { + pub fn to_crypto(&self) -> Result { + CryptoSignature::from_bytes(&self.0) + } +} + +/// Error when decoding a signature. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SignatureDecodingError(&'static str); + +impl Display for SignatureDecodingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.0) + } +} + +/// Ed25519 signing scheme for Malachite integration. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Ed25519SigningScheme; + +impl SigningScheme for Ed25519SigningScheme { + type DecodingError = SignatureDecodingError; + type Signature = ConsensusSignature; + type PublicKey = ConsensusPublicKey; + type PrivateKey = ConsensusPrivateKey; + + fn decode_signature(bytes: &[u8]) -> Result { + if bytes.len() != 64 { + return Err(SignatureDecodingError("invalid signature length")); + } + let mut arr = [0u8; 64]; + arr.copy_from_slice(bytes); + Ok(ConsensusSignature(arr)) + } + + fn encode_signature(signature: &Self::Signature) -> Vec { + signature.0.to_vec() + } +} + +/// Thin wrapper to keep consensus signing concerns localized. +#[derive(Clone, Debug)] +pub struct ConsensusSigner { + keypair: Ed25519KeyPair, +} + +impl ConsensusSigner { + /// Create from an existing keypair. + pub fn new(keypair: Ed25519KeyPair) -> Self { + Self { keypair } + } + + /// Public key accessor. + pub fn public_key(&self) -> ConsensusPublicKey { + ConsensusPublicKey(self.keypair.public_key.clone()) + } + + /// Validator ID derived from the public key. + pub fn validator_id(&self) -> cipherbft_types::ValidatorId { + self.keypair.validator_id() + } + + /// Sign arbitrary bytes. + pub fn sign(&self, msg: &[u8]) -> ConsensusSignature { + ConsensusSignature(self.keypair.sign(msg).to_bytes()) + } +} + +#[cfg(feature = "malachite")] +mod signing_provider { + use super::{ConsensusPublicKey, ConsensusSignature, ConsensusSigner}; + use crate::context::CipherBftContext; + use crate::proposal::{CutProposal, CutProposalPart}; + use crate::vote::ConsensusVote; + use informalsystems_malachitebft_core_types::{Signature, SignedMessage, SigningProvider}; + + /// Deterministic byte encoding for signatures. + fn encode_vote(vote: &ConsensusVote) -> Vec { + let mut out = Vec::with_capacity(64); + out.extend_from_slice(&vote.height.0.to_be_bytes()); + out.extend_from_slice(&vote.round.as_i64().to_be_bytes()); + out.push(match vote.vote_type { + informalsystems_malachitebft_core_types::VoteType::Prevote => 0, + informalsystems_malachitebft_core_types::VoteType::Precommit => 1, + }); + match vote.value.as_ref() { + informalsystems_malachitebft_core_types::NilOrVal::Nil => out.push(0), + informalsystems_malachitebft_core_types::NilOrVal::Val(id) => { + out.push(1); + out.extend_from_slice(id.0.as_bytes()); + } + } + out.extend_from_slice(vote.validator.0.as_bytes()); + if let Some(ext) = &vote.extension { + out.extend_from_slice(&(ext.signature.0.len() as u32).to_be_bytes()); + out.extend_from_slice(&ext.signature.0); + } else { + out.extend_from_slice(&0u32.to_be_bytes()); + } + out + } + + fn encode_proposal(proposal: &CutProposal) -> Vec { + let mut out = Vec::with_capacity(64); + out.extend_from_slice(&proposal.height.0.to_be_bytes()); + out.extend_from_slice(&proposal.round.as_i64().to_be_bytes()); + out.extend_from_slice(&proposal.pol_round.as_i64().to_be_bytes()); + out.extend_from_slice(proposal.value.cut().hash().as_bytes()); + out.extend_from_slice(proposal.proposer.0.as_ref()); + out + } + + fn encode_proposal_part(part: &CutProposalPart) -> Vec { + let mut out = Vec::with_capacity(64); + out.extend_from_slice(part.cut.hash().as_bytes()); + out.push(part.first as u8); + out.push(part.last as u8); + out + } + + #[derive(Clone, Debug)] + pub struct ConsensusSigningProvider { + signer: ConsensusSigner, + } + + impl ConsensusSigningProvider { + pub fn new(signer: ConsensusSigner) -> Self { + Self { signer } + } + + pub fn public_key(&self) -> ConsensusPublicKey { + self.signer.public_key() + } + + pub fn validator_id(&self) -> cipherbft_types::ValidatorId { + self.signer.validator_id() + } + + fn sign_bytes(&self, bytes: &[u8]) -> ConsensusSignature { + self.signer.sign(bytes) + } + + fn verify_bytes( + &self, + pk: &ConsensusPublicKey, + sig: &ConsensusSignature, + bytes: &[u8], + ) -> bool { + if let Ok(crypto_sig) = sig.to_crypto() { + pk.0.verify(bytes, &crypto_sig) + } else { + false + } + } + } + + impl SigningProvider for ConsensusSigningProvider { + fn sign_vote(&self, vote: ConsensusVote) -> SignedMessage { + let bytes = encode_vote(&vote); + let signature = self.sign_bytes(&bytes); + SignedMessage::new(vote, signature) + } + + fn verify_signed_vote( + &self, + vote: &ConsensusVote, + signature: &Signature, + public_key: &informalsystems_malachitebft_core_types::PublicKey, + ) -> bool { + let bytes = encode_vote(vote); + self.verify_bytes(public_key, signature, &bytes) + } + + fn sign_proposal( + &self, + proposal: CutProposal, + ) -> SignedMessage { + let bytes = encode_proposal(&proposal); + let signature = self.sign_bytes(&bytes); + SignedMessage::new(proposal, signature) + } + + fn verify_signed_proposal( + &self, + proposal: &CutProposal, + signature: &Signature, + public_key: &informalsystems_malachitebft_core_types::PublicKey, + ) -> bool { + let bytes = encode_proposal(proposal); + self.verify_bytes(public_key, signature, &bytes) + } + + fn sign_proposal_part( + &self, + proposal_part: CutProposalPart, + ) -> SignedMessage { + let bytes = encode_proposal_part(&proposal_part); + let signature = self.sign_bytes(&bytes); + SignedMessage::new(proposal_part, signature) + } + + fn verify_signed_proposal_part( + &self, + proposal_part: &CutProposalPart, + signature: &Signature, + public_key: &informalsystems_malachitebft_core_types::PublicKey, + ) -> bool { + let bytes = encode_proposal_part(proposal_part); + self.verify_bytes(public_key, signature, &bytes) + } + + fn sign_vote_extension( + &self, + extension: Vec, + ) -> SignedMessage> { + let signature = self.sign_bytes(&extension); + SignedMessage::new(extension, signature) + } + + fn verify_signed_vote_extension( + &self, + extension: &Vec, + signature: &Signature, + public_key: &informalsystems_malachitebft_core_types::PublicKey, + ) -> bool { + self.verify_bytes(public_key, signature, extension) + } + } +} + +#[cfg(feature = "malachite")] +pub use signing_provider::ConsensusSigningProvider; diff --git a/crates/consensus/src/types.rs b/crates/consensus/src/types.rs new file mode 100644 index 0000000..af242c9 --- /dev/null +++ b/crates/consensus/src/types.rs @@ -0,0 +1,135 @@ +use std::cmp::Ordering; +use std::fmt::{Debug, Display}; + +use cipherbft_data_chain::Cut; +use cipherbft_types::Hash; + +/// Consensus height wrapper to keep Malachite types explicit. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct ConsensusHeight(pub u64); + +impl ConsensusHeight { + /// Advance to the next height. + pub fn next(self) -> Self { + Self(self.0 + 1) + } +} + +impl From for ConsensusHeight { + fn from(value: u64) -> Self { + Self(value) + } +} + +impl From for u64 { + fn from(value: ConsensusHeight) -> Self { + value.0 + } +} + +impl Display for ConsensusHeight { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Consensus value ID (hash of a `Cut`). +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct ConsensusValueId(pub Hash); + +impl Display for ConsensusValueId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Consensus value wrapper (DCL `Cut`). +#[derive(Clone, Debug)] +pub struct ConsensusValue(pub Cut); + +impl ConsensusValue { + /// Access the inner cut. + pub fn cut(&self) -> &Cut { + &self.0 + } + + /// Consume into the inner cut. + pub fn into_cut(self) -> Cut { + self.0 + } +} + +impl PartialEq for ConsensusValue { + fn eq(&self, other: &Self) -> bool { + self.0.hash() == other.0.hash() + } +} + +impl Eq for ConsensusValue {} + +impl PartialOrd for ConsensusValue { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ConsensusValue { + fn cmp(&self, other: &Self) -> Ordering { + self.0.hash().cmp(&other.0.hash()) + } +} + +#[cfg(feature = "malachite")] +mod malachite_impls { + use super::{ConsensusHeight, ConsensusValue, ConsensusValueId}; + use cipherbft_data_chain::Cut; + use cipherbft_types::Hash; + use informalsystems_malachitebft_core_types::{Height as MalachiteHeight, Round, Value}; + + /// Use Malachite's `Round` type directly for consensus. + pub type ConsensusRound = Round; + + impl MalachiteHeight for ConsensusHeight { + const ZERO: Self = Self(0); + const INITIAL: Self = Self(1); + + fn increment_by(&self, n: u64) -> Self { + Self(self.0.saturating_add(n)) + } + + fn decrement_by(&self, n: u64) -> Option { + self.0.checked_sub(n).map(Self) + } + + fn as_u64(&self) -> u64 { + self.0 + } + } + + impl Value for ConsensusValue { + type Id = ConsensusValueId; + + fn id(&self) -> Self::Id { + ConsensusValueId(self.0.hash()) + } + } + + impl From for ConsensusValue { + fn from(cut: Cut) -> Self { + Self(cut) + } + } + + impl From for Hash { + fn from(value: ConsensusValueId) -> Self { + value.0 + } + } +} + +#[cfg(feature = "malachite")] +pub use malachite_impls::ConsensusRound; + +#[cfg(not(feature = "malachite"))] +/// Placeholder round type when Malachite is disabled. +pub type ConsensusRound = i64; diff --git a/crates/consensus/src/validator_set.rs b/crates/consensus/src/validator_set.rs new file mode 100644 index 0000000..a197087 --- /dev/null +++ b/crates/consensus/src/validator_set.rs @@ -0,0 +1,125 @@ +use std::fmt::{Debug, Display}; + +use cipherbft_crypto::Ed25519PublicKey; +use cipherbft_types::ValidatorId; + +#[cfg(feature = "malachite")] +use informalsystems_malachitebft_core_types::{Address as MalachiteAddress, Validator as MalachiteValidator, ValidatorSet as MalachiteValidatorSet, VotingPower}; + +use crate::signing::ConsensusPublicKey; + +/// Consensus address wrapper (Ed25519-derived validator ID). +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ConsensusAddress(pub ValidatorId); + +impl Debug for ConsensusAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ConsensusAddress({})", self.0) + } +} + +impl Display for ConsensusAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[cfg(feature = "malachite")] +impl MalachiteAddress for ConsensusAddress {} + +/// Validator entry with voting power and public key. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ConsensusValidator { + pub address: ConsensusAddress, + pub public_key: ConsensusPublicKey, + pub voting_power: u64, +} + +impl ConsensusValidator { + pub fn new(address: ValidatorId, public_key: Ed25519PublicKey, voting_power: u64) -> Self { + Self { + address: ConsensusAddress(address), + public_key: ConsensusPublicKey(public_key), + voting_power, + } + } +} + +/// Deterministic validator set (sorted). +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ConsensusValidatorSet { + validators: Vec, +} + +impl ConsensusValidatorSet { + /// Build from an unsorted validator list. + pub fn new(mut validators: Vec) -> Self { + // Sort descending by power, then ascending by address (per CometBFT rules). + validators.sort_by(|a, b| { + b.voting_power + .cmp(&a.voting_power) + .then_with(|| a.address.cmp(&b.address)) + }); + Self { validators } + } + + /// Append another validator (re-sorts internally). + pub fn push(&mut self, validator: ConsensusValidator) { + self.validators.push(validator); + self.validators.sort_by(|a, b| { + b.voting_power + .cmp(&a.voting_power) + .then_with(|| a.address.cmp(&b.address)) + }); + } + + /// Accessor for underlying list. + pub fn as_slice(&self) -> &[ConsensusValidator] { + &self.validators + } + + /// Number of validators. + pub fn len(&self) -> usize { + self.validators.len() + } +} + +#[cfg(feature = "malachite")] +impl MalachiteValidatorSet for ConsensusValidatorSet { + fn count(&self) -> usize { + self.validators.len() + } + + fn total_voting_power(&self) -> VotingPower { + self.validators.iter().map(|v| v.voting_power).sum() + } + + fn get_by_address( + &self, + address: &crate::context::CipherBftContextAddress, + ) -> Option<&crate::context::CipherBftContextValidator> { + self.validators.iter().find(|v| &v.address == address) + } + + fn get_by_index( + &self, + index: usize, + ) -> Option<&crate::context::CipherBftContextValidator> { + self.validators.get(index) + } +} + +#[cfg(feature = "malachite")] +impl MalachiteValidator for ConsensusValidator { + fn address(&self) -> &crate::context::CipherBftContextAddress { + &self.address + } + + fn public_key(&self) -> &informalsystems_malachitebft_core_types::PublicKey { + &self.public_key + } + + fn voting_power(&self) -> VotingPower { + self.voting_power + } +} diff --git a/crates/consensus/src/vote.rs b/crates/consensus/src/vote.rs new file mode 100644 index 0000000..d2b1d5d --- /dev/null +++ b/crates/consensus/src/vote.rs @@ -0,0 +1,77 @@ +use informalsystems_malachitebft_core_types::{ + NilOrVal, Round, SignedExtension, Vote as MalachiteVote, VoteType, +}; + +use crate::context::CipherBftContext; +use crate::types::{ConsensusHeight, ConsensusValueId}; +use crate::validator_set::ConsensusAddress; + +/// Consensus vote (prevote/precommit). +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ConsensusVote { + pub height: ConsensusHeight, + pub round: Round, + pub value: NilOrVal, + pub vote_type: VoteType, + pub validator: ConsensusAddress, + pub extension: Option>, +} + +impl ConsensusVote { + pub fn new( + height: ConsensusHeight, + round: Round, + value: NilOrVal, + vote_type: VoteType, + validator: ConsensusAddress, + ) -> Self { + Self { + height, + round, + value, + vote_type, + validator, + extension: None, + } + } +} + +impl MalachiteVote for ConsensusVote { + fn height(&self) -> ConsensusHeight { + self.height + } + + fn round(&self) -> Round { + self.round + } + + fn value(&self) -> &NilOrVal { + &self.value + } + + fn take_value(self) -> NilOrVal { + self.value + } + + fn vote_type(&self) -> VoteType { + self.vote_type + } + + fn validator_address(&self) -> &ConsensusAddress { + &self.validator + } + + fn extension(&self) -> Option<&SignedExtension> { + self.extension.as_ref() + } + + fn take_extension(&mut self) -> Option> { + self.extension.take() + } + + fn extend(self, extension: SignedExtension) -> Self { + let mut vote = self; + vote.extension = Some(extension); + vote + } +} diff --git a/crates/types/src/hash.rs b/crates/types/src/hash.rs index be408fa..d79e491 100644 --- a/crates/types/src/hash.rs +++ b/crates/types/src/hash.rs @@ -5,7 +5,7 @@ use sha2::{Digest, Sha256}; use std::fmt; /// SHA-256 hash (32 bytes) -#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] pub struct Hash(#[serde(with = "hex_bytes")] pub [u8; 32]); impl Hash {