hashgraph-like-consensus

Crates.iohashgraph-like-consensus
lib.rshashgraph-like-consensus
version0.1.0
created_at2025-12-18 10:08:49.62054+00
updated_at2025-12-18 10:08:49.62054+00
descriptionA lightweight Rust library for making binary decisions in networks using hashgraph-style consensus
homepage
repositoryhttps://github.com/vacp2p/hashgraph-like-consensus
max_upload_size
id1992104
size302,680
Ekaterina Broslavskaya (seemenkina)

documentation

README

Hashgraph-like Consensus

License: MIT

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.

What is this?

This library helps groups of peers vote on proposals and reach consensus, even when some peers are offline or trying to cause trouble. It's based on the Hashgraph-like Consensus Protocol RFC, which means:

  • Fast: Reaches consensus in O(log n) rounds, so it scales well
  • Secure: Works correctly even if up to 1/3 of peers are malicious (Byzantine fault tolerant)
  • Simple: Easy to embed in your application with a clean API

How it works

  1. Someone creates a proposal (like "Should we upgrade to version 2?")
  2. Peers vote yes or no, with each vote cryptographically signed
  3. Votes link together in a hashgraph structure (like a blockchain, but more efficient)
  4. Once enough votes are collected, consensus is reached and everyone knows the result

The library handles all the tricky parts: validating signatures, checking vote chains, managing timeouts, and determining when consensus is reached.

Quick start

use hashgraph_like_consensus::{
    scope::ScopeID,
    service::DefaultConsensusService,
    types::CreateProposalRequest,
};
use alloy::signers::local::PrivateKeySigner;
use std::time::Duration;

#[tokio::main]
async fn main() {
    let service = DefaultConsensusService::default();
    let scope = ScopeID::from("example-scope");
    let owner = PrivateKeySigner::random();

    let proposal = service.create_proposal(
        &scope,
        CreateProposalRequest::new(
            "Upgrade contract".into(),
            "Switch to v2".into(),
            owner.address().as_slice().to_vec(),
            3,                            // expected voters
            Duration::from_secs(60),      // expiration in seconds
            true,                         // tie-breaker favors YES on equality
        ).unwrap(),
    ).await.unwrap();

    let vote = service.cast_vote(&scope, proposal.proposal_id, true, owner).await.unwrap();
    println!("Recorded vote {:?}", vote.vote_id);
}

API Overview

Configuring a Scope

Scope vs Proposal Relationship

Scope (Group/Channel)
  ├── ScopeConfig (defaults for all proposals)
  └── Proposals
       ├── Proposal 1 → Session 1 (inherits scope config)
       ├── Proposal 2 → Session 2 (inherits scope config)
       └── Proposal 3 → Session 3 (overrides scope config)

Scopes carry defaults (network type, thresholds, timeouts) so you don't have to pass config on every proposal. Use the builder to initialize or update a scope:

use hashgraph_like_consensus::{
    scope::ScopeID,
    scope_config::NetworkType,
    service::DefaultConsensusService,
};

async fn example() -> Result<(), Box<dyn std::error::Error>> {
let service = DefaultConsensusService::default();
let scope = ScopeID::from("team_votes");

service
    .scope(&scope)
    .await?
    .with_network_type(NetworkType::P2P)
    .with_threshold(0.75)
    .with_timeout(120)
    .with_liveness_criteria(false)
    .initialize()
    .await?;
Ok(())
}

Creating a Service

// Simple: use defaults (in-memory storage, 10 max sessions per scope)
let service = DefaultConsensusService::default();

// Custom: set your own session limit
let service = DefaultConsensusService::new_with_max_sessions(20);

// Advanced: plug in your own storage and event bus
let service = ConsensusService::new_with_components(
    my_storage,
    my_event_bus,
    10
);

Working with Proposals

Create a proposal - Start a new voting session:

let proposal = service.create_proposal(
    &scope,
    CreateProposalRequest::new(
        "Upgrade contract".into(),
        "Switch to v2".into(),
        owner.address().as_slice().to_vec(),
        3,    // expected voters
        60,   // expiration in seconds
        true, // tie-breaker: YES wins on equality
    )?
).await?;

Process incoming proposals - When you receive a proposal from the network:

service.process_incoming_proposal(&scope, proposal).await?;

List proposals - See what's active or finalized:

let active = service.get_active_proposals(&scope).await;
let finalized = service.get_reached_proposals(&scope).await;

Casting Votes

Cast your vote - Vote yes or no on a proposal:

let vote = service.cast_vote(&scope, proposal_id, true, signer).await?;

Vote and fetch proposal - Useful for the creator who wants to gossip the updated proposal:

let proposal = service.cast_vote_and_get_proposal(&scope, proposal_id, true, signer).await?;

Picking a config for your transport

  • Gossipsub (default): ConsensusConfig::gossipsub() enforces the RFC’s two-round flow (round 1 = proposal, round 2 = everyone’s votes).
  • P2P: use ConsensusConfig::p2p() to derive the round cap dynamically (ceil(2n/3) of expected voters) and advance one round per vote.

Pass configs via create_proposal_with_config (or helper methods) when you need explicit control. Scope defaults are usually easier: set them once with service.scope(&scope).await?.with_network_type(...)...initialize().await?, then override per proposal only when needed.

Process incoming votes - When you receive votes from other peers:

service.process_incoming_vote(&scope, vote).await?;

Checking Results

Get consensus result - See if a proposal has reached consensus:

if let Some(result) = service.get_consensus_result(&scope, proposal_id).await {
    println!("Consensus reached: {}", result);
}

Check vote count - See if enough votes have been collected:

let enough_votes = service
    .has_sufficient_votes_for_proposal(&scope, proposal_id)
    .await?;

Events

Subscribe to events - Get notified when consensus is reached or fails:

use hashgraph_like_consensus::types::ConsensusEvent;

let mut receiver = service.subscribe_to_events();
tokio::spawn(async move {
    while let Ok((scope, event)) = receiver.recv().await {
        match event {
            ConsensusEvent::ConsensusReached { proposal_id, result } => {
                println!("Proposal {} reached consensus: {}", proposal_id, result);
            }
            ConsensusEvent::ConsensusFailed { proposal_id, reason } => {
                println!("Proposal {} failed: {}", proposal_id, reason);
            }
        }
    }
});

Statistics

Get scope statistics - See how many proposals are active, finalized, etc:

let stats = service.get_scope_stats(&scope).await;
println!("Active: {}, Finalized: {}", stats.active_sessions, stats.consensus_reached);

Advanced Usage

Custom storage - Want to persist proposals to a database? Implement the ConsensusStorage trait:

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>>;
    // ... more methods
}

Custom events - Need different event handling? Implement ConsensusEventBus:

pub trait ConsensusEventBus<Scope> {
    type Receiver;
    fn subscribe(&self) -> Self::Receiver;
    fn publish(&self, scope: Scope, event: ConsensusEvent);
}

Utility functions - The utils module provides helpers for validation and ID generation:

  • validate_proposal() - Check if a proposal and its votes are valid
  • validate_vote() - Verify a single vote's signature and structure
  • validate_vote_chain() - Ensure vote parent/received hash chains are correct
  • has_sufficient_votes() - Quick threshold check (count-based only)
  • calculate_consensus_result(votes, expected_voters, consensus_threshold, liveness_criteria_yes) - Determine the result from collected votes (returns Some(result) when consensus is reached, otherwise None), using the configured threshold and liveness rules

Learn More

This library implements the Hashgraph-like Consensus Protocol RFC. For details on how the protocol works, security guarantees, and edge cases, check out the RFC.

Commit count: 0

cargo fmt