From 7eaa08dc283a3196f8ad8ec3a1b5531357acdcf1 Mon Sep 17 00:00:00 2001 From: AkashJana18 Date: Thu, 24 Sep 2026 10:01:18 +0530 Subject: [PATCH] fix(alpenglow): fault picker whale overshoot & Alpenglow dead-slot hero tx loss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seed 9684: fault picker, not consensus. Pareto draw gave node 0 94.6% of stake. Picker took all 11 small nodes (5.4%), fell short, then "overshoot with one more node" grabbed the only one left: the whale. 100% offline, zero blocks, hero never submitted. Same code on main, so pre-existing. Fix: overshoot only when it lands closer to target than stopping short. New unit tests stake_pct_never_overshoots_with_a_whale, stake_pct_lands_near_target_for_pareto_stakes. Seed 19 (n=12): dead-slot inclusion. Leader kept producing slices for slots already Skip-certified. Hero landed in dead slot 2 after its Skip cert, so skip handler never fired, RPC saw block, stopped retrying. Fix: leader stops producing once Pool has Skip cert for slot; receivers ignore hero in skipped slot. Evidence - Sweep 164 seeds × 19.1% offline: 0 failures (was 5/104 before fixes). - cargo test --workspace green, 18 unit + 4 property tests. - Builtin scenario timings unchanged, wasm rebuilt, 4 Playwright specs pass. --- CLAUDE.md | 3 +- crates/sim-core/src/protocol/alpenglow/mod.rs | 8 ++- crates/sim-core/src/scenario.rs | 46 +++++++++++++++++- web/e2e/screenshots/compare-leader-down.png | Bin 166294 -> 166292 bytes 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 62c069e..d58b233 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -133,7 +133,8 @@ emits ~60k `msg_sent` events, so never render events as DOM. - Alpenglow `standstill_ms` (default 2 s, paper value much longer): with no new finalization for that long a node re-broadcasts its votes and certificates. Without it a healed partition deadlocks, because votes are sent once and both halves have already voted. - A validator's stake counts once towards a Skip certificate even if it casts Skip and SkipFallback (`pool.rs` tracks `skip_voters`). - Rotor relays are sampled per shred with replacement, proportional to stake; a permutation made every node a relay in small clusters and offline low-stake nodes starved slices. -- `Target::StakePct` picks random nodes that fit under the target and, if far short, the *smallest* remaining node; the old "any node" overshoot could take a 45% whale offline for a 5% target. +- `Target::StakePct` picks random nodes that fit under the target and, if far short, the *smallest* remaining node only when that lands closer to the target; the old "any node" overshoot took a 95% whale offline for a 19% target (liveness proptest seed 9684). Realized offline stake can therefore be *below* the target. +- An Alpenglow leader stops producing slices for a slot once its Pool holds a Skip certificate for it, and receivers do not count a block in a skipped slot as "landed" for the hero tx; otherwise a tx could be buried in a dead block with no retry. ## Tests diff --git a/crates/sim-core/src/protocol/alpenglow/mod.rs b/crates/sim-core/src/protocol/alpenglow/mod.rs index 2d621f3..6acd450 100644 --- a/crates/sim-core/src/protocol/alpenglow/mod.rs +++ b/crates/sim-core/src/protocol/alpenglow/mod.rs @@ -549,7 +549,8 @@ impl Node { return; } ctx.emit(TraceEvent::BlockReceived { node: self.id, slot: meta.slot, hash: meta.hash }); - if meta.txs.contains(&HERO_TX) { + // A block in an already-skipped slot can never be notarized; do not treat its txs as landed. + if meta.txs.contains(&HERO_TX) && !self.pool.is_skipped(meta.slot) { self.hero_block = Some(meta.hash); self.leader.included.insert(HERO_TX); } @@ -588,6 +589,11 @@ impl Node { } fn produce_slice(&mut self, slot: u64, slice: u32, ctx: &mut Ctx) { + // A slot the cluster has already closed with a Skip certificate is dead: producing more + // slices for it would only bury transactions in a block nobody will notarize. + if self.pool.is_skipped(slot) { + return; + } let ap = ctx.params.alpenglow.clone(); let me = self.id; let ws = ctx.schedule.window_start(slot); diff --git a/crates/sim-core/src/scenario.rs b/crates/sim-core/src/scenario.rs index c8e9191..f32afe5 100644 --- a/crates/sim-core/src/scenario.rs +++ b/crates/sim-core/src/scenario.rs @@ -280,10 +280,14 @@ impl Target { } } // If we fell far short (very coarse stakes), allow one overshoot with the - // smallest remaining node, so a 5% target can never take a 45% whale offline. + // smallest remaining node, but only if that lands closer to the goal than + // stopping short: a 19% target must never take a 95% whale offline. if (acc as f64) < 0.9 * goal as f64 { if let Some(id) = ids.iter().filter(|id| !out.contains(id)).min_by_key(|id| vs.stake(**id)) { - out.push(*id); + let with = acc + vs.stake(*id); + if with.abs_diff(goal) < acc.abs_diff(goal) { + out.push(*id); + } } } out.sort_unstable(); @@ -546,3 +550,41 @@ impl Default for TowerParams { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn resolve(stakes: Vec, pct: f64, seed: u64) -> (Vec, u64, u64) { + let vs = ValidatorSet::new(stakes); + let sched = LeaderSchedule::generate(&vs, &mut Rng::seed(seed), 40, 4); + let nodes = Target::StakePct { stake_pct: pct }.resolve(&vs, &sched, &mut Rng::seed(seed)); + let sum = vs.sum(nodes.iter().copied()); + (nodes, sum, vs.total) + } + + /// Seed 9684 of the liveness proptest: one validator holds 94.6% of the stake. A 19.1% + /// offline target must fall short (5.4%) rather than take the whale (100%) offline. + #[test] + fn stake_pct_never_overshoots_with_a_whale() { + let stakes = vec![946159, 13846, 8897, 8408, 4323, 3266, 3077, 3048, 2540, 2364, 2047, 2027]; + for seed in 1..20 { + let (nodes, sum, total) = resolve(stakes.clone(), 0.191, seed); + assert!(!nodes.contains(&0), "whale taken offline (seed {seed})"); + assert!(sum <= (0.191 * total as f64) as u64, "overshoot: {sum} of {total} (seed {seed})"); + } + } + + /// With a mainnet-like distribution the realized offline stake stays close to the target. + #[test] + fn stake_pct_lands_near_target_for_pareto_stakes() { + let spec = ValidatorSpec::default(); + for seed in 1..40 { + let vs = spec.build(&mut Rng::seed(seed)); + let (_, sum, total) = resolve(vs.stakes.clone(), 0.25, seed); + let frac = sum as f64 / total as f64; + assert!(frac <= 0.25 + 1e-9, "overshoot {frac} (seed {seed})"); + assert!(frac >= 0.18, "fell too short: {frac} (seed {seed})"); + } + } +} diff --git a/web/e2e/screenshots/compare-leader-down.png b/web/e2e/screenshots/compare-leader-down.png index 81b25445580de82a374bdb4e4f9305494bdae2bc..850071e2de3b398671e5bab55adc725894b0b5db 100644 GIT binary patch delta 6997 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 delta 6998 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 I&iwQL16F$5_5c6?