From c1e8ad504586da4a1ad4cc77f04aa5005b68e3d4 Mon Sep 17 00:00:00 2001 From: LEE JUNSEO Date: Sat, 27 Dec 2025 20:56:29 +0900 Subject: [PATCH 1/2] malachite-integrate initial commit --- Cargo.toml | 15 ++ crates/consensus/Cargo.toml | 48 +++++ crates/consensus/README.md | 96 +++++++++ crates/consensus/src/config.rs | 49 +++++ crates/consensus/src/context.rs | 140 +++++++++++++ crates/consensus/src/engine.rs | 221 +++++++++++++++++++++ crates/consensus/src/lib.rs | 40 ++++ crates/consensus/src/proposal.rs | 100 ++++++++++ crates/consensus/src/signing.rs | 276 ++++++++++++++++++++++++++ crates/consensus/src/types.rs | 135 +++++++++++++ crates/consensus/src/validator_set.rs | 125 ++++++++++++ crates/consensus/src/vote.rs | 77 +++++++ crates/types/src/hash.rs | 2 +- docs/malachite-integration-notes.md | 165 +++++++++++++++ 14 files changed, 1488 insertions(+), 1 deletion(-) create mode 100644 crates/consensus/Cargo.toml create mode 100644 crates/consensus/README.md create mode 100644 crates/consensus/src/config.rs create mode 100644 crates/consensus/src/context.rs create mode 100644 crates/consensus/src/engine.rs create mode 100644 crates/consensus/src/lib.rs create mode 100644 crates/consensus/src/proposal.rs create mode 100644 crates/consensus/src/signing.rs create mode 100644 crates/consensus/src/types.rs create mode 100644 crates/consensus/src/validator_set.rs create mode 100644 crates/consensus/src/vote.rs create mode 100644 docs/malachite-integration-notes.md 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 { diff --git a/docs/malachite-integration-notes.md b/docs/malachite-integration-notes.md new file mode 100644 index 0000000..bef8032 --- /dev/null +++ b/docs/malachite-integration-notes.md @@ -0,0 +1,165 @@ +# Malachite Integration Notes (with malaketh-layered reference) + +This note captures how Malachite’s actor/channel API is meant to be driven, and how the `malaketh-layered` project wires it to an execution client. Use this as a blueprint for CipherBFT’s Malachite integration. + +## Malachite channel/actor surface (v0.5.x) + +- The Malachite engine exposes a channels-based app API (via `AppMsg`/`ConsensusMsg`/`NetworkMsg` in `malachitebft-app-channel`). +- Core messages the host must handle: + - `ConsensusReady { reply }`: Malachite is initialized; reply with `StartHeight(height, validator_set)`. + - `StartedRound { height, round, proposer }`: informational hook for internal bookkeeping/metrics. + - `GetValue { height, round, timeout, reply }`: build and return a proposal value; also stream proposal parts to peers. + - `ReceivedProposalPart { from, part, reply }`: ingest streamed proposal parts; reply with `ProposedValue` once complete (or `None` if incomplete/invalid). + - `Decided { certificate, extensions, reply }`: commit the decided value (execute/persist), then reply `Next::Start(next_height, validator_set)` or `Next::Restart(height, validator_set)`. + - `GetValidatorSet { height, reply }`: provide validator set for verification at arbitrary heights. + - Sync helpers: `ProcessSyncedValue`, `GetDecidedValue`, `GetHistoryMinHeight`, `RestreamProposal` (for catch-up and restreaming). + - Vote extensions: `ExtendVote` / `VerifyVoteExtension` (can be `None`/`Ok(())` if unused). + - Network topology: `PeerJoined` / `PeerLeft` (optional tracking). +- Malachite engine wiring requires: + - `Context` implementation (done in `cipherbft-consensus`). + - `SigningProvider` (Ed25519) to sign consensus messages. + - Network actor implementing publish/subscribe for proposals, proposal parts, and votes. + - WAL actor for persistence/replay. + - Optional sync actor for catch-up. + +## What malaketh-layered does (Reth + Engine API) + +Source: `app/src/app.rs` and `app/src/state.rs` in the `malaketh-layered` repo. + +### Event handling loop +- `ConsensusReady`: + - Checks execution client capabilities (`engine.check_capabilities()`). + - Fetches latest block from EL (`eth_getBlockByNumber`) and stores it. + - Replies `StartHeight(current_height, validator_set)`. +- `StartedRound`: + - Updates local tracking of height/round/proposer. +- `GetValue` (proposer path): + - Calls `forkchoiceUpdated` with `PayloadAttributes` to ask EL to build a block, then `getPayload` to retrieve it. + - Stores block bytes; builds `LocallyProposedValue` and replies. + - Streams proposal parts over network (`PublishProposalPart`). +- `ReceivedProposalPart` (non-proposer path): + - Stores incoming parts; reassembles when complete, verifies signature, persists undecided proposal + data; replies with `ProposedValue` when ready. +- `GetValidatorSet`: + - Returns genesis validator set (static in the PoC). +- `Decided`: + - Fetches stored block bytes for the decided height/round. + - Decodes execution payload; notifies EL (`newPayload`/`forkchoiceUpdated` equivalent via `notify_new_block` + `set_latest_forkchoice_state`). + - Updates latest block metadata, commits certificate/value to store. + - Replies `StartHeight(next_height, validator_set)`. +- Sync helpers: + - `ProcessSyncedValue`: decode bytes and reply with `ProposedValue`. + - `GetDecidedValue`: return stored decided value bytes + certificate. + - `GetHistoryMinHeight`: return earliest stored height. +- Extensions/restream: + - `RestreamProposal` unimplemented; `ExtendVote`/`VerifyVoteExtension` are no-ops (`None`/`Ok(())`). + +### State management highlights (`state.rs`) +- Stores undecided/decided proposals and block data; prunes history. +- Proposal streaming: + - Splits payload into chunks (`CHUNK_SIZE = 128 KiB`), wraps in `ProposalPart::Data`, framed by `Init` and `Fin` (Fin carries a signature over height, round, and data hash). + - `StreamMessage` carries `sequence` and `StreamId`; final message is `StreamContent::Fin`. +- Proposal assembly/verification: + - Reassembles by concatenating `Data` parts; recomputes hash; verifies `Fin` signature against proposer public key from validator set. +- Commit flow: + - On `Decided`, moves proposal and block data from “undecided” to “decided”, updates height/round, prunes older heights (keeps last N). +- Validator set: + - Static set loaded from genesis; `get_validator_set` used for verification and gossip. + +### Deeper malaketh-layered specifics (for replication) +- Context/proposer selection: `select_proposer = (height-1 + round) % validator_count`, round must be non-nil; validator set order matters. +- Proposal part structure: + - `Init(height, round, proposer)` -> multiple `Data(Bytes)` chunks (128 KiB each) -> `Fin(signature)`. + - Hash for `Fin` signature: Keccak(height || round || all data chunks). `Fin` is signed by Ed25519 proposer key. + - `StreamId = height || round || stream_nonce`; `sequence` increments per part; final message is `StreamContent::Fin`. +- Signing provider (`types/src/signing.rs`): + - Implements Malachite `SigningProvider` for votes/proposals/proposal parts; verifies commit signatures by reconstructing the precommit vote (height/round/value_id, address). + - Exposes helpers to sign arbitrary hashes for the `Fin` chunk. +- Engine RPC wrapper (`engine/src/engine.rs`): + - `check_capabilities` ensures `forkchoice_updated_v3/get_payload_v3/new_payload_v3`. + - `generate_block`: `forkchoice_updated(head, PayloadAttributes)` → `get_payload(payload_id)` to fetch block. + - `notify_new_block`: calls `new_payload` with block + versioned hashes. + - `set_latest_forkchoice_state`: `forkchoice_updated` with decided head to advance/finalize. +- State/store behavior (`app/src/state.rs`): + - Persists undecided proposals and block bytes; verifies incoming proposals via `Fin` signature; prunes decided history beyond last 5 heights. + - On `Decided`: fetch undecided proposal + stored block data, persist as decided, prune, increment height/round. + - Sync helpers: `ProcessSyncedValue` decodes via `ProtobufCodec`; `GetDecidedValue` returns cert + protobuf-encoded value bytes; `GetHistoryMinHeight` from store floor. +- Genesis/validator set: + - Loaded once, static across heights in the PoC; validator pubkeys used for proposal verification and commit sig checking. + +## What is already implemented in this repo (CipherBFT) + +Paths below are workspace-relative. + +- Crate scaffold and feature gating + - `crates/consensus/Cargo.toml`: Malachite deps behind `malachite` feature; all pinned to `0.5.x` (core-types/consensus/engine/codec/metrics/etc.). + - Default build unaffected; enable with `--features malachite`. + +- Core types and context + - `crates/consensus/src/types.rs`: + - `ConsensusHeight` implements Malachite `Height` (ZERO=0, INITIAL=1, increment/decrement/as_u64). + - `ConsensusValue` wraps DCL `Cut`; `ConsensusValue::id()` uses `Cut::hash()`; `ConsensusValueId` wraps `Hash`. + - `ConsensusRound` aliases Malachite `Round` when feature is on (falls back to `i64` otherwise). + - `crates/consensus/src/context.rs`: + - `CipherBftContext` implements Malachite `Context` with round-robin proposer (`round % validators.len()`; nil → index 0). + - Provides constructors for proposals and votes; vote extensions are `Vec` (no-op). + - Aliases exported for address/validator/set/proposal/part/vote/signing scheme to keep trait signatures concise. + - `crates/types/src/hash.rs`: `Hash` derives `Ord`/`PartialOrd` for Malachite’s trait bounds. + +- Proposal / vote / validator set + - `crates/consensus/src/proposal.rs`: + - `CutProposal` (height/round/value/POL round/proposer) implements Malachite `Proposal`. + - `CutProposalPart` implements `ProposalPart` with `is_first`/`is_last`; equality via Cut hash; currently single-part helper `CutProposalPart::single`. + - `crates/consensus/src/vote.rs`: `ConsensusVote` (height/round/`NilOrVal`/vote type/address/optional extension) implements Malachite `Vote`; derives `Ord` to satisfy trait bounds. + - `crates/consensus/src/validator_set.rs`: `ConsensusAddress` (ValidatorId), `ConsensusValidator` (address/pubkey/power), `ConsensusValidatorSet` (sorted by power desc, address asc) implementing Malachite `Validator`/`ValidatorSet`. + +- Signing scheme + - `crates/consensus/src/signing.rs`: Ed25519 signing scheme wrapper implementing Malachite `SigningScheme` (encode/decode 64-byte sigs, PK/SK wrappers); `ConsensusSigner` convenience around `cipherbft-crypto` Ed25519 keypair. `ConsensusSigningProvider` implements Malachite `SigningProvider` with deterministic byte encoding for votes/proposals/parts/extensions and verify helpers. + +- Engine wiring + - `crates/consensus/src/engine.rs`: + - `MalachiteEngineBuilder` glue that spawns Malachite engine actors (`Consensus`, supervising `Node`) when given network/host/WAL/sync actor refs, consensus params/config, metrics/events, and a `SigningProvider`. + - Returns `EngineHandles` (actor refs + metrics/events). This assumes external actors exist; none are implemented here yet. + - Helpers: `create_context`, `default_consensus_params` (ProposalOnly payload, default thresholds), `default_engine_config_single_part` (ProposalOnly engine value payload). + +- Docs + - `crates/consensus/README.md`: module overview and workflow summary. + - This note: high-level plan + malaketh-layered patterns. + +What is **not** done yet +- Host actor that maps `AppMsg` events to DCL/EL/storage (the malaketh-layered-equivalent loop). +- SigningProvider adapter (tying `cipherbft-crypto` keys into Malachite’s SigningProvider trait). +- Network/WAL actor instantiation and codecs for proposals/votes (currently only the builder expects them). +- Sync/restream logic and any chunked proposal streaming (currently single-part Cut). +- Node binary integration to swap Primary runner for Malachite engine. + +## How to apply this to CipherBFT + +1) **Host actor (CipherBFT) mirroring `app.rs`:** + - Map `ConsensusReady` → load latest executed state/Cut height from storage; reply `StartHeight(height, validator_set)`. + - `GetValue` → ask DCL Primary for highest attested `Cut` (or block until ready); persist Cut bytes and stream parts; reply `LocallyProposedValue`. + - `ReceivedProposalPart` → store incoming Cut parts (single-part today), validate hash/signature if present; reply `ProposedValue` when complete. + - `Decided` → execute Cut in EL, persist commit cert + state root, update height, reply `StartHeight(next_height, validator_set)` or `RestartHeight`. + - Implement `GetValidatorSet`, `ProcessSyncedValue`, `GetDecidedValue`, `GetHistoryMinHeight` analogs using CipherBFT storage. + - Keep `ExtendVote`/`VerifyVoteExtension` as no-ops until you define extensions. +2) **Streaming strategy:** + - Short term: single-part proposals (already supported by `CutProposalPart`). + - Long term: follow malaketh-layered pattern—`Init`/`Data` chunks/`Fin` with signature over height/round/data hash; chunk Cuts if they become large. +3) **Signing provider:** + - Implement Malachite `SigningProvider` using `cipherbft-crypto` Ed25519 keys (similar to `malaketh-layered`’s `Ed25519Provider`). +4) **Network/WAL:** + - Use Malachite’s built-in network actor initially; adapt to CipherBFT P2P later if needed. + - Point WAL to per-height log directory; ensure `StartHeight`/`Reset` calls align with height transitions. +5) **Execution bridge:** + - Replace Engine API calls with EL interface: execute Cut → compute state root → persist, then signal `forkchoice` equivalent inside EL/storage. +6) **Sync/re-stream:** + - Implement `ProcessSyncedValue`/`GetDecidedValue`/`RestreamProposal` using stored Cuts and block data for catch-up. +7) **Version alignment:** + - Keep all Malachite crates on `0.5.x` to match the engine, avoiding mixed `0.6.0-pre` dependencies. + +## Suggested reading (already inspected) + +- `malaketh-layered/README.md`: high-level mapping of Malachite events to Engine API calls. +- `app/src/app.rs`: full host loop handling all Malachite `AppMsg` variants and driving Reth via Engine API. +- `app/src/state.rs`: proposal chunking/assembly, signature verification, storage of undecided/decided values, pruning strategy. + +Use this doc as the implementation checklist before wiring CipherBFT’s host/network/storage to Malachite. Once host and signing provider are in place, plug them into `MalachiteEngineBuilder` and start exercising consensus with single-part Cuts, then iterate toward chunked proposals and full sync support. From ab440beb7989f72076a545311f1cf43ff5868cd1 Mon Sep 17 00:00:00 2001 From: LEE JUNSEO Date: Sat, 27 Dec 2025 20:58:11 +0900 Subject: [PATCH 2/2] delete unuse docs --- docs/malachite-integration-notes.md | 165 ---------------------------- 1 file changed, 165 deletions(-) delete mode 100644 docs/malachite-integration-notes.md diff --git a/docs/malachite-integration-notes.md b/docs/malachite-integration-notes.md deleted file mode 100644 index bef8032..0000000 --- a/docs/malachite-integration-notes.md +++ /dev/null @@ -1,165 +0,0 @@ -# Malachite Integration Notes (with malaketh-layered reference) - -This note captures how Malachite’s actor/channel API is meant to be driven, and how the `malaketh-layered` project wires it to an execution client. Use this as a blueprint for CipherBFT’s Malachite integration. - -## Malachite channel/actor surface (v0.5.x) - -- The Malachite engine exposes a channels-based app API (via `AppMsg`/`ConsensusMsg`/`NetworkMsg` in `malachitebft-app-channel`). -- Core messages the host must handle: - - `ConsensusReady { reply }`: Malachite is initialized; reply with `StartHeight(height, validator_set)`. - - `StartedRound { height, round, proposer }`: informational hook for internal bookkeeping/metrics. - - `GetValue { height, round, timeout, reply }`: build and return a proposal value; also stream proposal parts to peers. - - `ReceivedProposalPart { from, part, reply }`: ingest streamed proposal parts; reply with `ProposedValue` once complete (or `None` if incomplete/invalid). - - `Decided { certificate, extensions, reply }`: commit the decided value (execute/persist), then reply `Next::Start(next_height, validator_set)` or `Next::Restart(height, validator_set)`. - - `GetValidatorSet { height, reply }`: provide validator set for verification at arbitrary heights. - - Sync helpers: `ProcessSyncedValue`, `GetDecidedValue`, `GetHistoryMinHeight`, `RestreamProposal` (for catch-up and restreaming). - - Vote extensions: `ExtendVote` / `VerifyVoteExtension` (can be `None`/`Ok(())` if unused). - - Network topology: `PeerJoined` / `PeerLeft` (optional tracking). -- Malachite engine wiring requires: - - `Context` implementation (done in `cipherbft-consensus`). - - `SigningProvider` (Ed25519) to sign consensus messages. - - Network actor implementing publish/subscribe for proposals, proposal parts, and votes. - - WAL actor for persistence/replay. - - Optional sync actor for catch-up. - -## What malaketh-layered does (Reth + Engine API) - -Source: `app/src/app.rs` and `app/src/state.rs` in the `malaketh-layered` repo. - -### Event handling loop -- `ConsensusReady`: - - Checks execution client capabilities (`engine.check_capabilities()`). - - Fetches latest block from EL (`eth_getBlockByNumber`) and stores it. - - Replies `StartHeight(current_height, validator_set)`. -- `StartedRound`: - - Updates local tracking of height/round/proposer. -- `GetValue` (proposer path): - - Calls `forkchoiceUpdated` with `PayloadAttributes` to ask EL to build a block, then `getPayload` to retrieve it. - - Stores block bytes; builds `LocallyProposedValue` and replies. - - Streams proposal parts over network (`PublishProposalPart`). -- `ReceivedProposalPart` (non-proposer path): - - Stores incoming parts; reassembles when complete, verifies signature, persists undecided proposal + data; replies with `ProposedValue` when ready. -- `GetValidatorSet`: - - Returns genesis validator set (static in the PoC). -- `Decided`: - - Fetches stored block bytes for the decided height/round. - - Decodes execution payload; notifies EL (`newPayload`/`forkchoiceUpdated` equivalent via `notify_new_block` + `set_latest_forkchoice_state`). - - Updates latest block metadata, commits certificate/value to store. - - Replies `StartHeight(next_height, validator_set)`. -- Sync helpers: - - `ProcessSyncedValue`: decode bytes and reply with `ProposedValue`. - - `GetDecidedValue`: return stored decided value bytes + certificate. - - `GetHistoryMinHeight`: return earliest stored height. -- Extensions/restream: - - `RestreamProposal` unimplemented; `ExtendVote`/`VerifyVoteExtension` are no-ops (`None`/`Ok(())`). - -### State management highlights (`state.rs`) -- Stores undecided/decided proposals and block data; prunes history. -- Proposal streaming: - - Splits payload into chunks (`CHUNK_SIZE = 128 KiB`), wraps in `ProposalPart::Data`, framed by `Init` and `Fin` (Fin carries a signature over height, round, and data hash). - - `StreamMessage` carries `sequence` and `StreamId`; final message is `StreamContent::Fin`. -- Proposal assembly/verification: - - Reassembles by concatenating `Data` parts; recomputes hash; verifies `Fin` signature against proposer public key from validator set. -- Commit flow: - - On `Decided`, moves proposal and block data from “undecided” to “decided”, updates height/round, prunes older heights (keeps last N). -- Validator set: - - Static set loaded from genesis; `get_validator_set` used for verification and gossip. - -### Deeper malaketh-layered specifics (for replication) -- Context/proposer selection: `select_proposer = (height-1 + round) % validator_count`, round must be non-nil; validator set order matters. -- Proposal part structure: - - `Init(height, round, proposer)` -> multiple `Data(Bytes)` chunks (128 KiB each) -> `Fin(signature)`. - - Hash for `Fin` signature: Keccak(height || round || all data chunks). `Fin` is signed by Ed25519 proposer key. - - `StreamId = height || round || stream_nonce`; `sequence` increments per part; final message is `StreamContent::Fin`. -- Signing provider (`types/src/signing.rs`): - - Implements Malachite `SigningProvider` for votes/proposals/proposal parts; verifies commit signatures by reconstructing the precommit vote (height/round/value_id, address). - - Exposes helpers to sign arbitrary hashes for the `Fin` chunk. -- Engine RPC wrapper (`engine/src/engine.rs`): - - `check_capabilities` ensures `forkchoice_updated_v3/get_payload_v3/new_payload_v3`. - - `generate_block`: `forkchoice_updated(head, PayloadAttributes)` → `get_payload(payload_id)` to fetch block. - - `notify_new_block`: calls `new_payload` with block + versioned hashes. - - `set_latest_forkchoice_state`: `forkchoice_updated` with decided head to advance/finalize. -- State/store behavior (`app/src/state.rs`): - - Persists undecided proposals and block bytes; verifies incoming proposals via `Fin` signature; prunes decided history beyond last 5 heights. - - On `Decided`: fetch undecided proposal + stored block data, persist as decided, prune, increment height/round. - - Sync helpers: `ProcessSyncedValue` decodes via `ProtobufCodec`; `GetDecidedValue` returns cert + protobuf-encoded value bytes; `GetHistoryMinHeight` from store floor. -- Genesis/validator set: - - Loaded once, static across heights in the PoC; validator pubkeys used for proposal verification and commit sig checking. - -## What is already implemented in this repo (CipherBFT) - -Paths below are workspace-relative. - -- Crate scaffold and feature gating - - `crates/consensus/Cargo.toml`: Malachite deps behind `malachite` feature; all pinned to `0.5.x` (core-types/consensus/engine/codec/metrics/etc.). - - Default build unaffected; enable with `--features malachite`. - -- Core types and context - - `crates/consensus/src/types.rs`: - - `ConsensusHeight` implements Malachite `Height` (ZERO=0, INITIAL=1, increment/decrement/as_u64). - - `ConsensusValue` wraps DCL `Cut`; `ConsensusValue::id()` uses `Cut::hash()`; `ConsensusValueId` wraps `Hash`. - - `ConsensusRound` aliases Malachite `Round` when feature is on (falls back to `i64` otherwise). - - `crates/consensus/src/context.rs`: - - `CipherBftContext` implements Malachite `Context` with round-robin proposer (`round % validators.len()`; nil → index 0). - - Provides constructors for proposals and votes; vote extensions are `Vec` (no-op). - - Aliases exported for address/validator/set/proposal/part/vote/signing scheme to keep trait signatures concise. - - `crates/types/src/hash.rs`: `Hash` derives `Ord`/`PartialOrd` for Malachite’s trait bounds. - -- Proposal / vote / validator set - - `crates/consensus/src/proposal.rs`: - - `CutProposal` (height/round/value/POL round/proposer) implements Malachite `Proposal`. - - `CutProposalPart` implements `ProposalPart` with `is_first`/`is_last`; equality via Cut hash; currently single-part helper `CutProposalPart::single`. - - `crates/consensus/src/vote.rs`: `ConsensusVote` (height/round/`NilOrVal`/vote type/address/optional extension) implements Malachite `Vote`; derives `Ord` to satisfy trait bounds. - - `crates/consensus/src/validator_set.rs`: `ConsensusAddress` (ValidatorId), `ConsensusValidator` (address/pubkey/power), `ConsensusValidatorSet` (sorted by power desc, address asc) implementing Malachite `Validator`/`ValidatorSet`. - -- Signing scheme - - `crates/consensus/src/signing.rs`: Ed25519 signing scheme wrapper implementing Malachite `SigningScheme` (encode/decode 64-byte sigs, PK/SK wrappers); `ConsensusSigner` convenience around `cipherbft-crypto` Ed25519 keypair. `ConsensusSigningProvider` implements Malachite `SigningProvider` with deterministic byte encoding for votes/proposals/parts/extensions and verify helpers. - -- Engine wiring - - `crates/consensus/src/engine.rs`: - - `MalachiteEngineBuilder` glue that spawns Malachite engine actors (`Consensus`, supervising `Node`) when given network/host/WAL/sync actor refs, consensus params/config, metrics/events, and a `SigningProvider`. - - Returns `EngineHandles` (actor refs + metrics/events). This assumes external actors exist; none are implemented here yet. - - Helpers: `create_context`, `default_consensus_params` (ProposalOnly payload, default thresholds), `default_engine_config_single_part` (ProposalOnly engine value payload). - -- Docs - - `crates/consensus/README.md`: module overview and workflow summary. - - This note: high-level plan + malaketh-layered patterns. - -What is **not** done yet -- Host actor that maps `AppMsg` events to DCL/EL/storage (the malaketh-layered-equivalent loop). -- SigningProvider adapter (tying `cipherbft-crypto` keys into Malachite’s SigningProvider trait). -- Network/WAL actor instantiation and codecs for proposals/votes (currently only the builder expects them). -- Sync/restream logic and any chunked proposal streaming (currently single-part Cut). -- Node binary integration to swap Primary runner for Malachite engine. - -## How to apply this to CipherBFT - -1) **Host actor (CipherBFT) mirroring `app.rs`:** - - Map `ConsensusReady` → load latest executed state/Cut height from storage; reply `StartHeight(height, validator_set)`. - - `GetValue` → ask DCL Primary for highest attested `Cut` (or block until ready); persist Cut bytes and stream parts; reply `LocallyProposedValue`. - - `ReceivedProposalPart` → store incoming Cut parts (single-part today), validate hash/signature if present; reply `ProposedValue` when complete. - - `Decided` → execute Cut in EL, persist commit cert + state root, update height, reply `StartHeight(next_height, validator_set)` or `RestartHeight`. - - Implement `GetValidatorSet`, `ProcessSyncedValue`, `GetDecidedValue`, `GetHistoryMinHeight` analogs using CipherBFT storage. - - Keep `ExtendVote`/`VerifyVoteExtension` as no-ops until you define extensions. -2) **Streaming strategy:** - - Short term: single-part proposals (already supported by `CutProposalPart`). - - Long term: follow malaketh-layered pattern—`Init`/`Data` chunks/`Fin` with signature over height/round/data hash; chunk Cuts if they become large. -3) **Signing provider:** - - Implement Malachite `SigningProvider` using `cipherbft-crypto` Ed25519 keys (similar to `malaketh-layered`’s `Ed25519Provider`). -4) **Network/WAL:** - - Use Malachite’s built-in network actor initially; adapt to CipherBFT P2P later if needed. - - Point WAL to per-height log directory; ensure `StartHeight`/`Reset` calls align with height transitions. -5) **Execution bridge:** - - Replace Engine API calls with EL interface: execute Cut → compute state root → persist, then signal `forkchoice` equivalent inside EL/storage. -6) **Sync/re-stream:** - - Implement `ProcessSyncedValue`/`GetDecidedValue`/`RestreamProposal` using stored Cuts and block data for catch-up. -7) **Version alignment:** - - Keep all Malachite crates on `0.5.x` to match the engine, avoiding mixed `0.6.0-pre` dependencies. - -## Suggested reading (already inspected) - -- `malaketh-layered/README.md`: high-level mapping of Malachite events to Engine API calls. -- `app/src/app.rs`: full host loop handling all Malachite `AppMsg` variants and driving Reth via Engine API. -- `app/src/state.rs`: proposal chunking/assembly, signature verification, storage of undecided/decided values, pruning strategy. - -Use this doc as the implementation checklist before wiring CipherBFT’s host/network/storage to Malachite. Once host and signing provider are in place, plug them into `MalachiteEngineBuilder` and start exercising consensus with single-part Cuts, then iterate toward chunked proposals and full sync support.