From a6e23649aa77094373113e16ff9fae324978a2e9 Mon Sep 17 00:00:00 2001 From: dolepee <113950858+dolepee@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:14:12 +0000 Subject: [PATCH 1/5] test(core-consensus): add minimal deterministic harness + in-memory WAL capture/replay --- code/crates/core-consensus/tests/it/basic.rs | 127 +++++++++++++++++++ code/crates/core-consensus/tests/it/main.rs | 3 + code/crates/core-consensus/tests/it/utils.rs | 26 ++++ 3 files changed, 156 insertions(+) create mode 100644 code/crates/core-consensus/tests/it/basic.rs create mode 100644 code/crates/core-consensus/tests/it/main.rs create mode 100644 code/crates/core-consensus/tests/it/utils.rs diff --git a/code/crates/core-consensus/tests/it/basic.rs b/code/crates/core-consensus/tests/it/basic.rs new file mode 100644 index 000000000..7843d244d --- /dev/null +++ b/code/crates/core-consensus/tests/it/basic.rs @@ -0,0 +1,127 @@ +#![allow(clippy::needless_update)] + +use informalsystems_malachitebft_core_consensus::{Effect, Params, Resumable, Resume, State, WalEntry}; +use malachitebft_core_types::{Round, ThresholdParams, ValuePayload}; +use malachitebft_metrics::Metrics; +use malachitebft_test::utils::validators::make_validators; +use malachitebft_test::{Height, TestContext, ValidatorSet}; + +use super::utils::{propose_timeout, wal_entry_to_input}; + +/// Minimal harness for driving core-consensus deterministically. +/// +/// This is intentionally small and does **not** attempt to simulate full networking or timing. +/// It records WAL entries emitted via `Effect::WalAppend` and can replay them after a simulated crash. +struct Harness { + state: State, + // In-memory WAL: (height, entry) + wal: Vec<(Height, WalEntry)>, +} + +impl Harness { + fn new(height: Height, vs: ValidatorSet) -> Self { + let ctx = TestContext::new(); + let params = Params { + address: vs + .get_by_index(0) + .expect("validator set must be non-empty") + .address, + threshold_params: ThresholdParams::default(), + value_payload: ValuePayload::ProposalOnly, + enabled: true, + }; + + Self { + state: State::new(ctx, height, vs, params, 128), + wal: Vec::new(), + } + } + + fn run(&mut self, input: informalsystems_malachitebft_core_consensus::Input) { + let metrics = Metrics::new(); + + // Split borrows so the effect handler doesn't need to borrow `self` while `state` is mutably borrowed. + let state = &mut self.state; + let wal = &mut self.wal; + + // Metrics expects step_start/step_end pairing; initialize it to the current driver step. + metrics.step_start(state.driver.step()); + + let _res: Result<(), informalsystems_malachitebft_core_consensus::Error> = + informalsystems_malachitebft_core_consensus::process!( + input: input, + state: state, + metrics: &metrics, + with: effect => { + let res: Result, informalsystems_malachitebft_core_consensus::Error> = + match effect { + Effect::WalAppend(height, entry, r) => { + wal.push((height, entry)); + Ok(r.resume_with(())) + } + // For this PR we keep the effect handler conservative: always continue. + // Follow-up PRs will add specific effect simulation (signing, publishing, etc.). + other => { + let _ = other; + Ok(Resume::Continue) + } + }; + res + } + ); + + let _ = _res; + } + + fn drain_wal_entries(&self, height: Height) -> Vec> { + self.wal + .iter() + .filter_map(|(h, e)| (*h == height).then(|| e.clone())) + .collect() + } +} + +#[test] +fn wal_entries_can_be_captured_and_replayed_in_memory() { + let [(v1, _sk1), (v2, _sk2), (v3, _sk3), (v4, _sk4)] = make_validators([1, 1, 1, 1]); + let vs = ValidatorSet::new(vec![v1, v2, v3, v4]); + + // First run: start height and trigger a persisted timeout. + let mut h1 = Harness::new(Height::new(1), vs.clone()); + + h1.run(informalsystems_malachitebft_core_consensus::Input::StartHeight( + Height::new(1), + vs.clone(), + false, + )); + + h1.run(informalsystems_malachitebft_core_consensus::Input::TimeoutElapsed( + propose_timeout(0), + )); + + let wal_entries = h1.drain_wal_entries(Height::new(1)); + assert!( + wal_entries.iter().any(|e| matches!(e, WalEntry::Timeout(t) if t.round == Round::new(0))), + "expected a timeout WAL entry for round 0" + ); + + // Simulated crash/restart: new harness, replay WAL entries as inputs. + let vs2 = vs; + let mut h2 = Harness::new(Height::new(1), vs2.clone()); + h2.run(informalsystems_malachitebft_core_consensus::Input::StartHeight( + Height::new(1), + vs2, + true, + )); + + for entry in wal_entries { + h2.run(wal_entry_to_input(entry)); + } + + // After replay, we should have persisted the same timeout again deterministically. + let wal_entries_2 = h2.drain_wal_entries(Height::new(1)); + assert!( + wal_entries_2.iter().any(|e| matches!(e, WalEntry::Timeout(t) if t.round == Round::new(0))), + "expected timeout WAL entry after replay" + ); +} diff --git a/code/crates/core-consensus/tests/it/main.rs b/code/crates/core-consensus/tests/it/main.rs new file mode 100644 index 000000000..8d00bd26d --- /dev/null +++ b/code/crates/core-consensus/tests/it/main.rs @@ -0,0 +1,3 @@ +pub mod basic; + +mod utils; diff --git a/code/crates/core-consensus/tests/it/utils.rs b/code/crates/core-consensus/tests/it/utils.rs new file mode 100644 index 000000000..370426e6c --- /dev/null +++ b/code/crates/core-consensus/tests/it/utils.rs @@ -0,0 +1,26 @@ +use informalsystems_malachitebft_core_consensus::{Input, WalEntry}; +use malachitebft_core_types::{Context, Timeout}; + +/// Convert a WAL entry back into a core-consensus input for deterministic replay. +pub fn wal_entry_to_input(entry: WalEntry) -> Input { + match entry { + WalEntry::ConsensusMsg(msg) => match msg { + informalsystems_malachitebft_core_consensus::SignedConsensusMsg::Vote(v) => { + Input::Vote(v) + } + informalsystems_malachitebft_core_consensus::SignedConsensusMsg::Proposal(p) => { + Input::Proposal(p) + } + }, + WalEntry::Timeout(timeout) => Input::TimeoutElapsed(timeout), + WalEntry::ProposedValue(v) => { + // For now, treat replay as if the value arrived from consensus gossip. + // (Scenarios that require ValueOrigin::Sync will be added in follow-up PRs.) + Input::ProposedValue(v, malachitebft_core_types::ValueOrigin::Consensus) + } + } +} + +pub fn propose_timeout(round: u32) -> Timeout { + Timeout::propose(malachitebft_core_types::Round::new(round)) +} From adcf72b144a69ce0070358e0a95cfc153f88ddff Mon Sep 17 00:00:00 2001 From: dolepee <113950858+dolepee@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:14:12 +0000 Subject: [PATCH 2/5] refactor(test): import process! macro + Error type --- code/crates/core-consensus/tests/it/basic.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/code/crates/core-consensus/tests/it/basic.rs b/code/crates/core-consensus/tests/it/basic.rs index 7843d244d..0ad046b7e 100644 --- a/code/crates/core-consensus/tests/it/basic.rs +++ b/code/crates/core-consensus/tests/it/basic.rs @@ -1,6 +1,6 @@ #![allow(clippy::needless_update)] -use informalsystems_malachitebft_core_consensus::{Effect, Params, Resumable, Resume, State, WalEntry}; +use informalsystems_malachitebft_core_consensus::{process, Effect, Error, Params, Resumable, Resume, State, WalEntry}; use malachitebft_core_types::{Round, ThresholdParams, ValuePayload}; use malachitebft_metrics::Metrics; use malachitebft_test::utils::validators::make_validators; @@ -48,13 +48,12 @@ impl Harness { metrics.step_start(state.driver.step()); let _res: Result<(), informalsystems_malachitebft_core_consensus::Error> = - informalsystems_malachitebft_core_consensus::process!( + process!( input: input, state: state, metrics: &metrics, with: effect => { - let res: Result, informalsystems_malachitebft_core_consensus::Error> = - match effect { + let res: Result, Error> = match effect { Effect::WalAppend(height, entry, r) => { wal.push((height, entry)); Ok(r.resume_with(())) From b6c4ada136a047579ce157227fe2d4957b97b02c Mon Sep 17 00:00:00 2001 From: dolepee <113950858+dolepee@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:14:12 +0000 Subject: [PATCH 3/5] style(test): shorten core-consensus test paths --- code/crates/core-consensus/tests/it/basic.rs | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/code/crates/core-consensus/tests/it/basic.rs b/code/crates/core-consensus/tests/it/basic.rs index 0ad046b7e..525c9c74f 100644 --- a/code/crates/core-consensus/tests/it/basic.rs +++ b/code/crates/core-consensus/tests/it/basic.rs @@ -1,6 +1,6 @@ #![allow(clippy::needless_update)] -use informalsystems_malachitebft_core_consensus::{process, Effect, Error, Params, Resumable, Resume, State, WalEntry}; +use informalsystems_malachitebft_core_consensus::{process, Effect, Error, Input, Params, Resumable, Resume, State, WalEntry}; use malachitebft_core_types::{Round, ThresholdParams, ValuePayload}; use malachitebft_metrics::Metrics; use malachitebft_test::utils::validators::make_validators; @@ -37,7 +37,7 @@ impl Harness { } } - fn run(&mut self, input: informalsystems_malachitebft_core_consensus::Input) { + fn run(&mut self, input: Input) { let metrics = Metrics::new(); // Split borrows so the effect handler doesn't need to borrow `self` while `state` is mutably borrowed. @@ -88,15 +88,9 @@ fn wal_entries_can_be_captured_and_replayed_in_memory() { // First run: start height and trigger a persisted timeout. let mut h1 = Harness::new(Height::new(1), vs.clone()); - h1.run(informalsystems_malachitebft_core_consensus::Input::StartHeight( - Height::new(1), - vs.clone(), - false, - )); + h1.run(Input::StartHeight(Height::new(1), vs.clone(), false)); - h1.run(informalsystems_malachitebft_core_consensus::Input::TimeoutElapsed( - propose_timeout(0), - )); + h1.run(Input::TimeoutElapsed(propose_timeout(0))); let wal_entries = h1.drain_wal_entries(Height::new(1)); assert!( @@ -107,11 +101,7 @@ fn wal_entries_can_be_captured_and_replayed_in_memory() { // Simulated crash/restart: new harness, replay WAL entries as inputs. let vs2 = vs; let mut h2 = Harness::new(Height::new(1), vs2.clone()); - h2.run(informalsystems_malachitebft_core_consensus::Input::StartHeight( - Height::new(1), - vs2, - true, - )); + h2.run(Input::StartHeight(Height::new(1), vs2, true)); for entry in wal_entries { h2.run(wal_entry_to_input(entry)); From 925bd63bce9af804eb28839bba4df5c0e225ed40 Mon Sep 17 00:00:00 2001 From: dolepee <113950858+dolepee@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:14:12 +0000 Subject: [PATCH 4/5] fmt(test): core-consensus it harness --- code/crates/core-consensus/tests/it/basic.rs | 53 +++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/code/crates/core-consensus/tests/it/basic.rs b/code/crates/core-consensus/tests/it/basic.rs index 525c9c74f..3cef7c376 100644 --- a/code/crates/core-consensus/tests/it/basic.rs +++ b/code/crates/core-consensus/tests/it/basic.rs @@ -1,6 +1,8 @@ #![allow(clippy::needless_update)] -use informalsystems_malachitebft_core_consensus::{process, Effect, Error, Input, Params, Resumable, Resume, State, WalEntry}; +use informalsystems_malachitebft_core_consensus::{ + process, Effect, Error, Input, Params, Resumable, Resume, State, WalEntry, +}; use malachitebft_core_types::{Round, ThresholdParams, ValuePayload}; use malachitebft_metrics::Metrics; use malachitebft_test::utils::validators::make_validators; @@ -47,27 +49,26 @@ impl Harness { // Metrics expects step_start/step_end pairing; initialize it to the current driver step. metrics.step_start(state.driver.step()); - let _res: Result<(), informalsystems_malachitebft_core_consensus::Error> = - process!( - input: input, - state: state, - metrics: &metrics, - with: effect => { - let res: Result, Error> = match effect { - Effect::WalAppend(height, entry, r) => { - wal.push((height, entry)); - Ok(r.resume_with(())) - } - // For this PR we keep the effect handler conservative: always continue. - // Follow-up PRs will add specific effect simulation (signing, publishing, etc.). - other => { - let _ = other; - Ok(Resume::Continue) - } - }; - res - } - ); + let _res: Result<(), informalsystems_malachitebft_core_consensus::Error> = process!( + input: input, + state: state, + metrics: &metrics, + with: effect => { + let res: Result, Error> = match effect { + Effect::WalAppend(height, entry, r) => { + wal.push((height, entry)); + Ok(r.resume_with(())) + } + // For this PR we keep the effect handler conservative: always continue. + // Follow-up PRs will add specific effect simulation (signing, publishing, etc.). + other => { + let _ = other; + Ok(Resume::Continue) + } + }; + res + } + ); let _ = _res; } @@ -94,7 +95,9 @@ fn wal_entries_can_be_captured_and_replayed_in_memory() { let wal_entries = h1.drain_wal_entries(Height::new(1)); assert!( - wal_entries.iter().any(|e| matches!(e, WalEntry::Timeout(t) if t.round == Round::new(0))), + wal_entries + .iter() + .any(|e| matches!(e, WalEntry::Timeout(t) if t.round == Round::new(0))), "expected a timeout WAL entry for round 0" ); @@ -110,7 +113,9 @@ fn wal_entries_can_be_captured_and_replayed_in_memory() { // After replay, we should have persisted the same timeout again deterministically. let wal_entries_2 = h2.drain_wal_entries(Height::new(1)); assert!( - wal_entries_2.iter().any(|e| matches!(e, WalEntry::Timeout(t) if t.round == Round::new(0))), + wal_entries_2 + .iter() + .any(|e| matches!(e, WalEntry::Timeout(t) if t.round == Round::new(0))), "expected timeout WAL entry after replay" ); } From 529239e29cb3ddd3874485c55f34fad6ccee0461 Mon Sep 17 00:00:00 2001 From: dolepee Date: Tue, 17 Feb 2026 13:29:07 +0000 Subject: [PATCH 5/5] test(core-consensus): update StartHeight calls for Option --- code/crates/core-consensus/tests/it/basic.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/crates/core-consensus/tests/it/basic.rs b/code/crates/core-consensus/tests/it/basic.rs index 3cef7c376..7726b550b 100644 --- a/code/crates/core-consensus/tests/it/basic.rs +++ b/code/crates/core-consensus/tests/it/basic.rs @@ -89,7 +89,7 @@ fn wal_entries_can_be_captured_and_replayed_in_memory() { // First run: start height and trigger a persisted timeout. let mut h1 = Harness::new(Height::new(1), vs.clone()); - h1.run(Input::StartHeight(Height::new(1), vs.clone(), false)); + h1.run(Input::StartHeight(Height::new(1), vs.clone(), false, None)); h1.run(Input::TimeoutElapsed(propose_timeout(0))); @@ -104,7 +104,7 @@ fn wal_entries_can_be_captured_and_replayed_in_memory() { // Simulated crash/restart: new harness, replay WAL entries as inputs. let vs2 = vs; let mut h2 = Harness::new(Height::new(1), vs2.clone()); - h2.run(Input::StartHeight(Height::new(1), vs2, true)); + h2.run(Input::StartHeight(Height::new(1), vs2, true, None)); for entry in wal_entries { h2.run(wal_entry_to_input(entry));