Skip to content

Commit 1416f9b

Browse files
emlautarom1Copilot
andauthored
feat: add qbft module (#18)
* Add Nix support * Add `typos` * Apply copilot suggestions * Update Cargo.lock * Add Dockerfile and .dockerignore * Add placeholder executable * Include cargo tools in Nix * Remove the need for nightly * Fix checks on placeholder app * Adjust target name * Build only the CLI * Rely on system libraries - Cannot set up nightly without breaking stuff * Remove duplicated inputs * Use `rust:1.89` and `debian:trixie-slim` as images - Bigger image (80 mb vs 14 mb) * Use bookworm instead of trixie * Formatting * Revert changes to `rustfmt.toml` * Revert changes to `pre-push` * Expose port 3030 * Initial qbft impl - Missing public `Run` method * Accept references to `I` * Define errors as structs * Initial qbft implementation * Rename SomeMsg/Msg * Reduce cloning when ref is enough * Complete `Run` method * Use crossbeam for channels * Fix `q_commit` - Wrap in Option - Make classify return None instead of empty vec * Initialize remaining state elements * Renaming * Use `select!` macro * Apply clippy suggestions * Match reference project structure * C * Add qbft module docs * Run `d.log_*` methods * Match visibility modifiers * Match module docs * Regen `Cargo.lock` * Move dependency to workspace * Fix pre-push checks - Code sorting * Revert docs change * Fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix equality comparison with `Default` Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add internal tests * Add `as_any` to `SomeMsg` trait to allow downcasting * Make structs thread safe * Complete `test_qbft` function * Add `happy_0` test * Include `rand` * Disable test due to deadlock * Sort deps * Run formatter * Apply clippy suggestions * Fix wrong check - Ensure `ppj_cache` is None only * Use single `is_leader` function * Simplify imports * Log when all resuts are received. * Propagate cancellation Use `cancellation` crate * Fix deadlock - Main test thread was not exiting due to wrong scope * Match original log messages * Add working tests * Port remaining test cases * Add `duplicate_pre_prepare_rules` test * Formatting * Mutate `input_value` Fixes multiple test cases * Avoid f64 for time * Add `fake_clock` * Sort deps * Sort module declaration * Apply Copilot fixes/suggestions * Use `fake_clock` Speeds up tests but does not fix concurrency issues * Add `cargo nextest` * Update CI to use `nextest` * Test for clock cancellation * Update comments * Use workspace `rand` * Improve retries for flaky tests * Apply copilot comment suggestions * Use explicit error enum * Remove `anyhow` * Small suggestions * Add `Drop` impl for `FakeClock` * Fire and forget in `bcast` * Ignore all tests - Flaky tests * Remove nextest --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent e37d67b commit 1416f9b

9 files changed

Lines changed: 2324 additions & 27 deletions

File tree

Cargo.lock

Lines changed: 48 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,22 @@ publish = false
2424

2525
[workspace.dependencies]
2626
axum = "0.8.6"
27+
blst = "0.3.13"
28+
cancellation = "0.1.0"
2729
chrono = { version = "0.4", features = ["serde"] }
30+
crossbeam = "0.8.4"
2831
hex = { version = "^0.4.3" }
29-
serde = { version = "1.0", features = ["derive"] }
30-
serde_json = { version = "^1.0" }
31-
tokio = { version = "1", features = ["full"] }
32-
libp2p = { version = "0.56", features = ["full", "secp256k1"] }
3332
prost = "0.14"
34-
prost-types = "0.14"
3533
prost-build = "0.14"
36-
blst = "0.3.13"
34+
prost-types = "0.14"
35+
rand = { version = "0.8", features = ["std_rng"] }
3736
rand_core = "0.6"
37+
serde = { version = "1.0", features = ["derive"] }
38+
serde_json = { version = "^1.0" }
3839
thiserror = "2.0.12"
39-
rand = {version = "0.8", features = ["std_rng"]}
40-
uuid = {version = "1.16", features = ["serde", "v4"] }
40+
tokio = { version = "1", features = ["full"] }
41+
libp2p = { version = "0.56", features = ["full", "secp256k1"] }
42+
uuid = { version = "1.16", features = ["serde", "v4"] }
4143
serde_with = { version = "3", features = ["hex", "base64"] }
4244
base64 = { version = "0.22.1" }
4345
sha3 = { version = "0.10.8" }

crates/charon-core/Cargo.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,19 @@ license.workspace = true
77
publish.workspace = true
88

99
[dependencies]
10+
cancellation.workspace = true
11+
chrono.workspace = true
12+
crossbeam.workspace = true
13+
hex.workspace = true
1014
serde.workspace = true
1115
serde_json.workspace = true
16+
thiserror.workspace = true
17+
libp2p.workspace = true
18+
prost.workspace = true
19+
prost-types.workspace = true
20+
21+
[dev-dependencies]
22+
rand.workspace = true
1223
libp2p.workspace = true
1324
prost.workspace = true
1425
prost-types.workspace = true
@@ -19,4 +30,4 @@ chrono.workspace = true
1930
charon-build-proto.workspace = true
2031

2132
[lints]
22-
workspace = true
33+
workspace = true

crates/charon-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! This crate provides the fundamental building blocks, data structures, and
55
//! core algorithms used throughout the Charon system.
66
7+
pub mod qbft;
78
/// Types for the Charon core.
89
pub mod types;
910

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
use crossbeam::channel as mpmc;
2+
use std::{
3+
collections::HashMap,
4+
sync::{Arc, Mutex},
5+
thread,
6+
time::{Duration, Instant},
7+
};
8+
9+
#[derive(Clone)]
10+
pub struct FakeClock {
11+
inner: Arc<Mutex<FakeClockInner>>,
12+
}
13+
14+
struct FakeClockInner {
15+
start: Instant,
16+
now: Instant,
17+
last_id: usize,
18+
clients: HashMap<usize, (mpmc::Sender<Instant>, Instant)>,
19+
}
20+
21+
impl FakeClock {
22+
pub fn new(now: Instant) -> Self {
23+
Self {
24+
inner: Arc::new(Mutex::new(FakeClockInner {
25+
start: now,
26+
now,
27+
last_id: 1,
28+
clients: Default::default(),
29+
})),
30+
}
31+
}
32+
33+
pub fn new_timer(
34+
&self,
35+
duration: Duration,
36+
) -> (
37+
mpmc::Receiver<Instant>,
38+
Box<dyn Fn() + Send + Sync + 'static>,
39+
) {
40+
let (tx, rx) = mpmc::bounded::<Instant>(1);
41+
42+
let client_id = {
43+
let mut inner = self.inner.lock().unwrap();
44+
let id = inner.last_id;
45+
let deadline = inner.now + duration;
46+
47+
inner.last_id += 1;
48+
inner.clients.insert(id, (tx, deadline));
49+
50+
id
51+
};
52+
53+
let inner = Arc::clone(&self.inner);
54+
let cancel = Box::new(move || {
55+
let mut inner = inner.lock().unwrap();
56+
inner.clients.remove(&client_id);
57+
});
58+
59+
(rx, cancel)
60+
}
61+
62+
pub fn advance(&self, duration: Duration) {
63+
// Advance time and collect expired senders under lock, but perform sends
64+
// without holding lock.
65+
let mut expired = vec![];
66+
67+
let now = {
68+
let mut inner = self.inner.lock().unwrap();
69+
inner.now += duration;
70+
let now = inner.now;
71+
72+
for (&id, (ch, deadline)) in inner.clients.iter() {
73+
if *deadline <= now {
74+
expired.push((id, ch.clone()));
75+
}
76+
}
77+
78+
for (id, _) in expired.iter() {
79+
inner.clients.remove(id);
80+
}
81+
82+
now
83+
};
84+
85+
for (_, ch) in expired {
86+
let _ = ch.send(now);
87+
}
88+
}
89+
90+
pub fn elapsed(&self) -> Duration {
91+
let inner = self.inner.lock().unwrap();
92+
inner.now - inner.start
93+
}
94+
95+
pub fn cancel(&self) {
96+
let mut inner = self.inner.lock().unwrap();
97+
inner.clients.clear();
98+
}
99+
}
100+
101+
impl Drop for FakeClock {
102+
fn drop(&mut self) {
103+
self.cancel();
104+
}
105+
}
106+
107+
#[test]
108+
fn multiple_threads_timers() {
109+
let clock = FakeClock::new(Instant::now());
110+
111+
let start = Instant::now();
112+
thread::scope(|s| {
113+
let c1 = clock.clone();
114+
let (ch_1, _) = c1.new_timer(Duration::from_secs(5));
115+
s.spawn(move || {
116+
let _ = ch_1.recv();
117+
});
118+
119+
let c2 = clock.clone();
120+
let (ch_2, _) = c2.new_timer(Duration::from_secs(5));
121+
s.spawn(move || {
122+
let _ = ch_2.recv();
123+
});
124+
125+
clock.advance(Duration::from_secs(6));
126+
});
127+
128+
println!("start={:?}, clock={:?}", start.elapsed(), clock.elapsed());
129+
}
130+
131+
#[test]
132+
fn multiple_threads_cancellation() {
133+
let clock = FakeClock::new(Instant::now());
134+
135+
let start = Instant::now();
136+
thread::scope(|s| {
137+
let c1 = clock.clone();
138+
let (ch_1, _) = c1.new_timer(Duration::from_secs(5));
139+
s.spawn(move || {
140+
let _ = ch_1.recv();
141+
});
142+
143+
let c2 = clock.clone();
144+
let (ch_2, _) = c2.new_timer(Duration::from_secs(5));
145+
s.spawn(move || {
146+
let _ = ch_2.recv();
147+
});
148+
149+
clock.cancel();
150+
});
151+
152+
println!("start={:?}, clock={:?}", start.elapsed(), clock.elapsed());
153+
}

0 commit comments

Comments
 (0)