From b62250865614a91ceae5076eb1314c4b7804d9ca Mon Sep 17 00:00:00 2001 From: kshprenger Date: Wed, 21 Jan 2026 15:11:27 +0100 Subject: [PATCH] Hotstuff --- Cargo.lock | 69 +++++++----- README.md | 9 +- systems/hotstuff/Cargo.toml | 9 ++ systems/hotstuff/src/bin/hotstuff.rs | 33 ++++++ systems/hotstuff/src/lib.rs | 159 +++++++++++++++++++++++++++ 5 files changed, 246 insertions(+), 33 deletions(-) create mode 100644 systems/hotstuff/Cargo.toml create mode 100644 systems/hotstuff/src/bin/hotstuff.rs create mode 100644 systems/hotstuff/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4fdee7d..98d19f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,15 +69,15 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -169,9 +169,9 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "env_filter" -version = "0.1.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", "regex", @@ -179,9 +179,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ "anstream", "anstyle", @@ -217,11 +217,20 @@ dependencies = [ "wasip2", ] +[[package]] +name = "hotstuff" +version = "0.1.0" +dependencies = [ + "dscale", + "log", + "rand", +] + [[package]] name = "indicatif" -version = "0.18.3" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ "console", "portable-atomic", @@ -238,9 +247,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "jiff" -version = "0.2.19" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" +checksum = "b3e3d65f018c6ae946ab16e80944b97096ed73c35b221d1c478a6c81d8f57940" dependencies = [ "jiff-static", "log", @@ -251,9 +260,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.19" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" +checksum = "a17c2b211d863c7fde02cbea8a3c1a439b98e109286554f2860bdded7ff83818" dependencies = [ "proc-macro2", "quote", @@ -262,9 +271,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "f4eacb0641a310445a4c513f2a5e23e19952e269c6a38887254d5f837a305506" dependencies = [ "once_cell", "wasm-bindgen", @@ -281,9 +290,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.181" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libm" @@ -514,9 +523,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -525,9 +534,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" @@ -558,9 +567,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "05d7d0fce354c88b7982aec4400b3e7fcf723c32737cef571bd165f7613557ee" dependencies = [ "cfg-if", "once_cell", @@ -571,9 +580,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "55839b71ba921e4f75b674cb16f843f4b1f3b26ddfcb3454de1cf65cc021ec0f" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -581,9 +590,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "caf2e969c2d60ff52e7e98b7392ff1588bffdd1ccd4769eba27222fd3d621571" dependencies = [ "bumpalo", "proc-macro2", @@ -594,9 +603,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "0861f0dcdf46ea819407495634953cdcc8a8c7215ab799a7a7ce366be71c7b30" dependencies = [ "unicode-ident", ] diff --git a/README.md b/README.md index 9be1ad0..99a2deb 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ This project provides a fast & deterministic simulation framework for testing an ## Usage -To use the DScale, you need to implement the `ProcessHandle` trait for your distributed system and the `Message` trait for the data exchanged between processes. - ### 1. Install +In your project + ```shell cargo add dscale ``` @@ -30,6 +30,9 @@ impl Message for MyMessage { 1000 } } + +// Or (if there is no need in bandwidth) +impl Message for MyMessage {} ``` ### 3. Implement Process Logic @@ -59,7 +62,7 @@ impl ProcessHandle for MyProcess { fn on_timer(&mut self, _id: TimerId) { // Handle timeouts - broadcasst(MyMessage { data: 42 }); + broadcast(MyMessage { data: 42 }); } } ``` diff --git a/systems/hotstuff/Cargo.toml b/systems/hotstuff/Cargo.toml new file mode 100644 index 0000000..698affc --- /dev/null +++ b/systems/hotstuff/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "hotstuff" +version = "0.1.0" +edition = "2024" + +[dependencies] +log = "0.4.29" +dscale = {path = "../../dscale"} +rand = "0.9.2" diff --git a/systems/hotstuff/src/bin/hotstuff.rs b/systems/hotstuff/src/bin/hotstuff.rs new file mode 100644 index 0000000..581a5a8 --- /dev/null +++ b/systems/hotstuff/src/bin/hotstuff.rs @@ -0,0 +1,33 @@ +use std::rc::Rc; + +use dscale::{Distributions, Jiffies, LatencyDescription, SimulationBuilder, global::anykv}; +use hotstuff::{B0, ChainedHotstuff, HOTSTUFF_POOL, Node}; + +fn main() { + let genesis = Rc::new(Node { + id: 0, + parent: None, + height: 0, + creator: 0, + creation_time: Jiffies(0), + }); + + anykv::set::>(B0, genesis); + anykv::set::<(f64, usize)>("avg_latency", (0.0, 0)); + + let mut sim = SimulationBuilder::default() + .add_pool::(HOTSTUFF_POOL, 53) + .latency_topology(&[LatencyDescription::WithinPool( + HOTSTUFF_POOL, + Distributions::Normal(Jiffies(50), Jiffies(10)), + )]) + .seed(123) + .time_budget(Jiffies(3600_000)) + .build(); + + sim.run(); + + let ordered = anykv::get::<(f64, usize)>("avg_latency").1; + let avg_latency = anykv::get::<(f64, usize)>("avg_latency").0; + println!("ordered: {ordered}, avg_latency: {avg_latency}") +} diff --git a/systems/hotstuff/src/lib.rs b/systems/hotstuff/src/lib.rs new file mode 100644 index 0000000..ddc6fbe --- /dev/null +++ b/systems/hotstuff/src/lib.rs @@ -0,0 +1,159 @@ +// https://arxiv.org/pdf/1803.05069 + +use std::{collections::HashMap, rc::Rc}; + +use dscale::{ + global::{anykv, configuration::process_number}, + helpers::Combiner, + *, +}; + +pub const B0: &str = "b_0"; +pub const HOTSTUFF_POOL: &str = "HotstuffCluster"; + +type NodeId = usize; + +pub struct Node { + pub id: NodeId, + pub parent: Option>, + pub height: usize, + pub creator: ProcessId, + pub creation_time: Jiffies, +} + +pub enum HSMessage { + Propose(Rc), + Vote(Rc), +} + +impl Message for HSMessage {} + +pub struct ChainedHotstuff { + pending_quorums: HashMap>, + vheight: usize, + b_lock: Rc, + b_exec: Rc, + b_leaf: Rc, +} + +impl ProcessHandle for ChainedHotstuff { + fn start(&mut self) { + if rank() == 1 { + broadcast(HSMessage::Propose(self.create_leaf())); + } + } + fn on_message(&mut self, from: ProcessId, message: MessagePtr) { + debug_process!("Got message from {from}"); + match message.as_type::().as_ref() { + HSMessage::Propose(b_new) => { + if b_new.height > self.vheight + && (self.extends(b_new) || b_new.height > self.b_lock.height) + { + self.vheight = b_new.height; + send_to(self.get_next_leader(), HSMessage::Vote(b_new.clone())); + self.update(b_new.clone()); + } else { + debug_process!("Prososal rejected") + } + } + HSMessage::Vote(node) => { + let combiner_size = self.quorum_size(); + let combiner = self + .pending_quorums + .entry(node.id) + .or_insert(Combiner::<()>::new(combiner_size)); + + if let Some(_) = combiner.combine(()) { + self.b_leaf = node.clone(); + debug_process!("Quorum gathered!"); + broadcast(HSMessage::Propose(self.create_leaf())); + } + } + } + } + fn on_timer(&mut self, _id: dscale::TimerId) { + unreachable!() + } +} + +impl Default for ChainedHotstuff { + fn default() -> Self { + let genesis_node = anykv::get::>(B0); + Self { + pending_quorums: HashMap::new(), + vheight: 0, + b_lock: genesis_node.clone(), + b_exec: genesis_node.clone(), + b_leaf: genesis_node.clone(), + } + } +} + +// Internals +impl ChainedHotstuff { + fn update(&mut self, b_star: Rc) { + let b__ = b_star.parent.clone(); + let b_ = b__.map(|b__| b__.parent.clone()).flatten(); + let b = b_.clone().map(|b_| b_.parent.clone()).flatten(); + + if b_star.height > self.b_leaf.height { + self.b_leaf = b_star + } + if let Some(ref b_) = b_ { + if b_.height > self.b_lock.height { + self.b_lock = b_.clone(); + } + } + + if let Some(b) = b { + self.on_commit(b.clone()); + self.b_exec = b; + } + } + + fn on_commit(&mut self, b: Rc) { + if self.b_exec.height < b.height { + self.on_commit(b.parent.clone().unwrap()); + if rank() == b.creator { + anykv::modify::<(f64, usize)>( + "avg_latency", + |(prev_avg_latency, prev_total_ordered)| { + let vertex_latency = now() - b.creation_time; + *prev_avg_latency = (vertex_latency.0 as f64 + + (*prev_avg_latency * *prev_total_ordered as f64)) + as f64 + / (*prev_total_ordered + 1) as f64; + + *prev_total_ordered += 1; + }, + ); + } + } + } +} + +// Utils +impl ChainedHotstuff { + fn create_leaf(&self) -> Rc { + let parent = self.b_leaf.clone(); + Rc::new(Node { + id: global_unique_id(), + parent: Some(parent.clone()), + height: parent.height + 1, + creator: rank(), + creation_time: now(), + }) + } + + fn get_next_leader(&self) -> ProcessId { + (self.vheight % process_number()) + 1 + } + + fn quorum_size(&self) -> usize { + (process_number() * 2) / 3 + 1 + } + + fn extends(&self, child: &Rc) -> bool { + Rc::ptr_eq(child.parent.as_ref().unwrap(), &self.b_lock) + } +}