Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"crates/data-chain",
"crates/storage",
"crates/node",
"crates/consensus",
]
resolver = "2"

Expand Down Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions crates/consensus/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
96 changes: 96 additions & 0 deletions crates/consensus/README.md
Original file line number Diff line number Diff line change
@@ -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<Cut> for ConsensusValue`**: Enables direct conversion from DCL cuts to consensus values.
- **`From<ConsensusValueId> 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<ConsensusValueId>`), 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<ValueId>` per Malachite’s API.
- Vote extensions are typed as `Vec<u8>` 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+.
49 changes: 49 additions & 0 deletions crates/consensus/src/config.rs
Original file line number Diff line number Diff line change
@@ -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<String>) -> 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
}
}
140 changes: 140 additions & 0 deletions crates/consensus/src/context.rs
Original file line number Diff line number Diff line change
@@ -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<u8>;

/// 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<ConsensusAddress> {
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<ValueId<Self>>,
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<ValueId<Self>>,
address: Self::Address,
) -> Self::Vote {
ConsensusVote::new(height, round, value_id, VoteType::Precommit, address)
}
}
Loading
Loading