A lightweight Rust library for making binary decisions in peer-to-peer or gossipsub networks. Perfect for group governance, voting systems, or any scenario where you need distributed agreement.
- Fast - Reaches consensus in O(log n) rounds
- Byzantine fault tolerant - Correct even if up to 1/3 of peers are malicious
- Pluggable storage - In-memory by default; implement
ConsensusStoragefor persistence - Network-agnostic - Works with both Gossipsub (fixed 2-round) and P2P (dynamic rounds) topologies
- Event-driven - Subscribe to consensus outcomes via a broadcast event bus
- Cryptographic integrity - Votes are signed with secp256k1 and chained in a hashgraph structure
Based on the Hashgraph-like Consensus Protocol RFC.
Add to your Cargo.toml:
[dependencies]
hashgraph-like-consensus = { git = "https://github.com/vacp2p/hashgraph-like-consensus" }use hashgraph_like_consensus::{
scope::ScopeID,
service::DefaultConsensusService,
types::CreateProposalRequest,
};
use alloy::signers::local::PrivateKeySigner;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let service = DefaultConsensusService::default();
let scope = ScopeID::from("example-scope");
let signer = PrivateKeySigner::random();
// Create a proposal
let proposal = service
.create_proposal(
&scope,
CreateProposalRequest::new(
"Upgrade contract".into(), // name
b"Switch to v2".to_vec(), // payload (bytes)
signer.address().as_slice().to_vec(), // owner
3, // expected voters
60, // expiration (seconds from now)
true, // tie-breaker: YES wins on equality
)?,
)
.await?;
// Cast a vote
let vote = service
.cast_vote(&scope, proposal.proposal_id, true, signer)
.await?;
println!("Recorded vote {}", vote.vote_id);
Ok(())
}A scope groups related proposals together and carries default configuration (network type, threshold, timeout). Proposals inherit scope defaults unless overridden individually.
Scope (group / channel)
├── ScopeConfig (defaults for all proposals)
└── Proposals
├── Proposal 1 → Session (inherits scope config)
├── Proposal 2 → Session (inherits scope config)
└── Proposal 3 → Session (overrides scope config)
| Type | Rounds | Behavior |
|---|---|---|
| Gossipsub (default) | Fixed 2 rounds | Round 1 = proposal broadcast, Round 2 = all votes |
| P2P | Dynamic ceil(2n/3) |
Each vote advances the round by one |
use hashgraph_like_consensus::service::DefaultConsensusService;
// Default: in-memory storage, 10 max sessions per scope
let service = DefaultConsensusService::default();
// Custom session limit
let service = DefaultConsensusService::new_with_max_sessions(20);
// Fully custom: plug in your own storage and event bus
let service = ConsensusService::new_with_components(my_storage, my_event_bus, 10);use hashgraph_like_consensus::{
scope::ScopeID,
scope_config::NetworkType,
service::DefaultConsensusService,
};
use std::time::Duration;
let service = DefaultConsensusService::default();
let scope = ScopeID::from("team_votes");
// Initialize with the builder
service
.scope(&scope)
.await?
.with_network_type(NetworkType::P2P)
.with_threshold(0.75)
.with_timeout(Duration::from_secs(120))
.with_liveness_criteria(false)
.initialize()
.await?;
// Update later (single field)
service
.scope(&scope)
.await?
.with_threshold(0.8)
.update()
.await?;Built-in presets are also available:
// High confidence (threshold = 0.9)
service.scope(&scope).await?.strict_consensus().initialize().await?;
// Low latency (threshold = 0.6, timeout = 30 s)
service.scope(&scope).await?.fast_consensus().initialize().await?;// Create a proposal
let proposal = service
.create_proposal(&scope, CreateProposalRequest::new(
"Upgrade contract".into(),
b"Switch to v2".to_vec(),
owner_address,
3, // expected voters
60, // expiration (seconds from now)
true, // tie-breaker: YES wins on equality
)?)
.await?;
// Process a proposal received from the network
service.process_incoming_proposal(&scope, proposal).await?;
// List active proposals
let active: Option<Vec<Proposal>> = service.get_active_proposals(&scope).await?;
// List finalized proposals (proposal_id -> result)
let finalized: Option<HashMap<u32, bool>> = service.get_reached_proposals(&scope).await?;// Cast your vote (yes = true, no = false)
let vote = service.cast_vote(&scope, proposal_id, true, signer).await?;
// Cast a vote and get the updated proposal (useful for gossiping)
let proposal = service
.cast_vote_and_get_proposal(&scope, proposal_id, true, signer)
.await?;
// Process a vote received from the network
service.process_incoming_vote(&scope, vote).await?;// Get the final consensus result (Ok(true) = YES, Ok(false) = NO)
let result: bool = service.get_consensus_result(&scope, proposal_id).await?;
// Check if enough votes have been collected
let enough: bool = service
.has_sufficient_votes_for_proposal(&scope, proposal_id)
.await?;use hashgraph_like_consensus::types::ConsensusEvent;
let mut rx = service.subscribe_to_events();
tokio::spawn(async move {
while let Ok((scope, event)) = rx.recv().await {
match event {
ConsensusEvent::ConsensusReached { proposal_id, result, timestamp } => {
println!("Proposal {} → {}", proposal_id, if result { "YES" } else { "NO" });
}
ConsensusEvent::ConsensusFailed { proposal_id, timestamp } => {
println!("Proposal {} failed to reach consensus", proposal_id);
}
}
}
});let stats = service.get_scope_stats(&scope).await;
println!(
"Active: {}, Reached: {}, Failed: {}",
stats.active_sessions, stats.consensus_reached, stats.failed_sessions
);Implement the ConsensusStorage trait to persist proposals to a database:
use hashgraph_like_consensus::storage::ConsensusStorage;
pub trait ConsensusStorage<Scope> {
async fn save_session(&self, scope: &Scope, session: ConsensusSession) -> Result<()>;
async fn get_session(&self, scope: &Scope, proposal_id: u32) -> Result<Option<ConsensusSession>>;
// ... see storage.rs for the full trait
}Implement ConsensusEventBus for alternative event delivery:
use hashgraph_like_consensus::events::ConsensusEventBus;
pub trait ConsensusEventBus<Scope> {
type Receiver;
fn subscribe(&self) -> Self::Receiver;
fn publish(&self, scope: Scope, event: ConsensusEvent);
}The utils module provides low-level helpers:
| Function | Description |
|---|---|
validate_proposal() |
Validate a proposal and its votes |
validate_vote() |
Verify a vote's signature and structure |
validate_vote_chain() |
Ensure parent/received hash chains are correct |
has_sufficient_votes() |
Quick threshold check (count-based) |
calculate_consensus_result() |
Determine result from collected votes using threshold and liveness rules |
# Build
cargo build
# Run tests
cargo test
# Generate docs
cargo doc --openNote: Requires a working
protoc(Protocol Buffers compiler) since the library generates code from.protofiles at build time.