Files
de-mls/tests/protocol_join_scale.rs
Ekaterina Broslavskaia 2eef52fc13 fix: prevent join-time group forks (list sync + session cap) (#147)
* fix(steward): learn the elected list to prevent the join-time fork

A member welcomed in the epoch a voted election runs missed the vote and
kept the pre-election list, so it rejected the next steward's commit and
forked. Stewards re-broadcast the list after the election, members with a
stale list request it, and the sync carries the settled basis so the list
is verified by recomputation rather than trusted.

* fix(consensus): free resolved consensus sessions

The session cap counted finished sessions and trimmed by HashMap order, so
different nodes evicted different sessions and diverged on SessionNotFound.
Free a session once its outcome is recorded; late votes fall back to the
resolved cache.

* chore(config): raise default max_consensus_sessions to 128
2026-08-21 08:46:28 +03:00

68 lines
3.1 KiB
Rust

//! Growing a group one member at a time past the first *voted* steward
//! election must not fork it. A joiner welcomed in the election epoch misses
//! the vote (no consensus session for it yet), so it must still learn the
//! elected steward list; a joiner left on the pre-election list rejects the
//! next steward's commit as unauthorized — a permanent MLS fork that equal
//! member counts hide and only the epoch authenticator reveals. Regression
//! cover for the #199 fork.
mod common;
use common::harness::{TestHarness, fast_config};
use de_mls::StewardListConfig;
const ALICE: &str = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
const BOB: &str = "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
const CHARLIE: &str = "5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a";
const DAVE: &str = "7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6";
const ERIN: &str = "47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a";
const FRANK: &str = "8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba";
const GRACE: &str = "0628631ed9050dfbe4e811d3bd4fd90396c8314ad458c4801071e97cb44ae809";
const HEIDI: &str = "cef64c066877b4e37a487c2cb70735543104f53bea556e60f6eb0f4956a125a9";
const IVAN: &str = "c46755cb0f6afda5c654974581054fbd82c4a572a6972c6614ce55be1d9c22f6";
const JUDY: &str = "91d8febe864c6b456da36ef0e87fcd23bb4b6a4e0d0135187053b1a4993062e6";
const KEN: &str = "a438374c68d626ae855c1f4fe3997bc144e55d754e405f4c663e9865e3734d93";
/// `sn_max = 2` forces a voted election once the settled set exceeds two, and
/// one-at-a-time adds cross it around a group of five — the regime where a
/// joiner is welcomed in the same epoch the election runs.
#[test]
fn one_at_a_time_growth_past_election_keeps_group_converged() {
const CONV: &str = "scale";
let mut h = TestHarness::<11>::start(
[
ALICE, BOB, CHARLIE, DAVE, ERIN, FRANK, GRACE, HEIDI, IVAN, JUDY, KEN,
],
CONV,
fast_config(),
StewardListConfig::new(1, 2).unwrap(),
);
const STEP: std::time::Duration = std::time::Duration::from_millis(50);
for k in 1..11 {
let kp = h.member_mut(k).mint_key_package();
h.member_mut(0).add_member(&kp);
// Land the add for the members that accept it (member 0 is the creator,
// never the stranded joiner, so it always reaches the new count)...
h.process_until(&format!("member {k} join lands"), |h| {
h.member(k).is_working() && h.member(0).member_count() == k + 1
});
// ...then let any steward election this add triggered resolve and its
// list reach every member before the next add.
for _ in 0..30 {
h.process(STEP);
}
assert!(
h.converged(),
"fork after adding member {k}: epoch_authenticator diverged (n={})",
h.member(0).member_count(),
);
}
assert_eq!(h.member(0).member_count(), 11, "all 11 members present");
assert!(
h.membership_agrees(),
"every member sees the same member set"
);
}