LinearLeiosTuner.html: prettier Mempool chart
Home /
Input Output /
ouroboros-leios
May 14, 8-9 AM (0)
May 14, 9-10 AM (0)
May 14, 10-11 AM (0)
May 14, 11-12 PM (0)
May 14, 12-1 PM (0)
May 14, 1-2 PM (0)
May 14, 2-3 PM (1)
May 14, 3-4 PM (0)
May 14, 4-5 PM (0)
May 14, 5-6 PM (0)
May 14, 6-7 PM (0)
May 14, 7-8 PM (1)
May 14, 8-9 PM (1)
May 14, 9-10 PM (1)
May 14, 10-11 PM (0)
May 14, 11-12 AM (0)
May 15, 12-1 AM (0)
May 15, 1-2 AM (0)
May 15, 2-3 AM (0)
May 15, 3-4 AM (0)
May 15, 4-5 AM (0)
May 15, 5-6 AM (0)
May 15, 6-7 AM (0)
May 15, 7-8 AM (0)
May 15, 8-9 AM (0)
May 15, 9-10 AM (0)
May 15, 10-11 AM (0)
May 15, 11-12 PM (0)
May 15, 12-1 PM (1)
May 15, 1-2 PM (0)
May 15, 2-3 PM (1)
May 15, 3-4 PM (0)
May 15, 4-5 PM (1)
May 15, 5-6 PM (0)
May 15, 6-7 PM (0)
May 15, 7-8 PM (0)
May 15, 8-9 PM (0)
May 15, 9-10 PM (0)
May 15, 10-11 PM (0)
May 15, 11-12 AM (0)
May 16, 12-1 AM (0)
May 16, 1-2 AM (0)
May 16, 2-3 AM (0)
May 16, 3-4 AM (0)
May 16, 4-5 AM (0)
May 16, 5-6 AM (0)
May 16, 6-7 AM (0)
May 16, 7-8 AM (0)
May 16, 8-9 AM (0)
May 16, 9-10 AM (0)
May 16, 10-11 AM (0)
May 16, 11-12 PM (0)
May 16, 12-1 PM (0)
May 16, 1-2 PM (0)
May 16, 2-3 PM (0)
May 16, 3-4 PM (0)
May 16, 4-5 PM (0)
May 16, 5-6 PM (0)
May 16, 6-7 PM (0)
May 16, 7-8 PM (0)
May 16, 8-9 PM (0)
May 16, 9-10 PM (0)
May 16, 10-11 PM (0)
May 16, 11-12 AM (0)
May 17, 12-1 AM (0)
May 17, 1-2 AM (0)
May 17, 2-3 AM (0)
May 17, 3-4 AM (0)
May 17, 4-5 AM (0)
May 17, 5-6 AM (0)
May 17, 6-7 AM (0)
May 17, 7-8 AM (0)
May 17, 8-9 AM (0)
May 17, 9-10 AM (0)
May 17, 10-11 AM (0)
May 17, 11-12 PM (0)
May 17, 12-1 PM (0)
May 17, 1-2 PM (0)
May 17, 2-3 PM (0)
May 17, 3-4 PM (0)
May 17, 4-5 PM (0)
May 17, 5-6 PM (0)
May 17, 6-7 PM (0)
May 17, 7-8 PM (0)
May 17, 8-9 PM (0)
May 17, 9-10 PM (0)
May 17, 10-11 PM (0)
May 17, 11-12 AM (0)
May 18, 12-1 AM (0)
May 18, 1-2 AM (0)
May 18, 2-3 AM (0)
May 18, 3-4 AM (0)
May 18, 4-5 AM (0)
May 18, 5-6 AM (0)
May 18, 6-7 AM (0)
May 18, 7-8 AM (0)
May 18, 8-9 AM (1)
May 18, 9-10 AM (0)
May 18, 10-11 AM (1)
May 18, 11-12 PM (0)
May 18, 12-1 PM (0)
May 18, 1-2 PM (0)
May 18, 2-3 PM (0)
May 18, 3-4 PM (0)
May 18, 4-5 PM (0)
May 18, 5-6 PM (0)
May 18, 6-7 PM (0)
May 18, 7-8 PM (0)
May 18, 8-9 PM (0)
May 18, 9-10 PM (0)
May 18, 10-11 PM (0)
May 18, 11-12 AM (0)
May 19, 12-1 AM (0)
May 19, 1-2 AM (0)
May 19, 2-3 AM (0)
May 19, 3-4 AM (0)
May 19, 4-5 AM (0)
May 19, 5-6 AM (0)
May 19, 6-7 AM (0)
May 19, 7-8 AM (0)
May 19, 8-9 AM (2)
May 19, 9-10 AM (0)
May 19, 10-11 AM (0)
May 19, 11-12 PM (1)
May 19, 12-1 PM (0)
May 19, 1-2 PM (0)
May 19, 2-3 PM (1)
May 19, 3-4 PM (3)
May 19, 4-5 PM (0)
May 19, 5-6 PM (3)
May 19, 6-7 PM (0)
May 19, 7-8 PM (0)
May 19, 8-9 PM (0)
May 19, 9-10 PM (0)
May 19, 10-11 PM (0)
May 19, 11-12 AM (0)
May 20, 12-1 AM (0)
May 20, 1-2 AM (0)
May 20, 2-3 AM (0)
May 20, 3-4 AM (0)
May 20, 4-5 AM (0)
May 20, 5-6 AM (0)
May 20, 6-7 AM (0)
May 20, 7-8 AM (0)
May 20, 8-9 AM (0)
May 20, 9-10 AM (3)
May 20, 10-11 AM (2)
May 20, 11-12 PM (0)
May 20, 12-1 PM (0)
May 20, 1-2 PM (4)
May 20, 2-3 PM (0)
May 20, 3-4 PM (88)
May 20, 4-5 PM (6)
May 20, 5-6 PM (0)
May 20, 6-7 PM (0)
May 20, 7-8 PM (0)
May 20, 8-9 PM (0)
May 20, 9-10 PM (1)
May 20, 10-11 PM (0)
May 20, 11-12 AM (0)
May 21, 12-1 AM (0)
May 21, 1-2 AM (0)
May 21, 2-3 AM (0)
May 21, 3-4 AM (0)
May 21, 4-5 AM (0)
May 21, 5-6 AM (0)
May 21, 6-7 AM (0)
May 21, 7-8 AM (0)
May 21, 8-9 AM (0)
123 commits this week
May 14, 2026
-
May 21, 2026
sim-core: expose every tcp-envelope knob as a YAML override
RawTcpEnvelope now exposes the full LinkEnvelopeCfg surface as optional fields (mss-bytes, initial-cwnd-segments, idle-reset- threshold-ms, rto-ms, loss-bw-depth, cold-bw-depth, cold-release-ms, cold-release-shape, loss-release-ms, loss-release-shape). Any field left unset falls through to the physics-derived default from LinkEnvelopeCfg::defaults_for. Unknown fields are rejected. Adds kebab-case serde rename on tcp_model::Curve so the YAML enum values are spelt "step" / "linear" / "geometric" rather than the PascalCase Rust variants. Four new parsing tests cover empty blocks, the full override schema, the deny-unknown-fields guard, and the layered-defaults semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-core: wire tcp-model envelope into Connection (opt-in per link)
Each directed Connection optionally carries a tcp_model::LinkState plus
the (from, to) link identity and the stateless Rng oracle used to seed
per-message loss draws. Behaviour with no envelope configured is
byte-identical to today; the regression suite is unchanged.
When an envelope is attached:
- send() calls LinkState::on_send with a loss outcome drawn from the
oracle under context ("tcp_loss", from, to, send_time, message_id).
- update_bandwidth_queues() integrates `bps * bw_mult(t)` to find the
total bytes deliverable over [last_event, now], then fair-shares them
among miniprotocols exactly as before. Per-message arrival timestamps
are computed by inverting bytes_deliverable so the slow-start ramp is
reflected in arrivals (not averaged out over the update window).
- All arrivals are clamped to LinkState::delivery_floor, modelling
cross-protocol HoL blocking during a loss-induced RTO stall.
Topology YAML grows an optional per-link `tcp-envelope` block; for now
only `loss-prob-per-segment` is exposed, with all other params derived
from the link's latency and bandwidth via LinkEnvelopeCfg::defaults_for.
Three new connection.rs tests cover: cold-start delaying a 1 MB transfer,
loss-induced delivery floor, and envelope-disabled byte-identical
matching against the no-envelope path.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-core: thread tcp-envelope through the sequential/turbo engine
sequential.rs builds Connections directly (it doesn't route through NetworkCoordinator), so the envelope wiring added earlier was only reaching the actor engine. Fix: construct an EnvelopeWiring at link- build time whenever `LinkConfiguration.tcp_envelope` is `Some`, using the same `Rng::new(config.seed)` stateless oracle already used by other sequential-engine machinery. Caught by a NA,0.200 / top-stake-fraction / 750n sanity run with loss-prob-per-segment 0.01: the turbo-engine output was byte-identical to baseline because the envelope cfg was being discarded at the Connection::new call site. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
tcp-model: caller-supplied loss draw; add inversion helper
Drops the internal RNG: on_send now takes a pre-drawn `loss_drawn` boolean, and LinkEnvelopeCfg gains `msg_loss_prob(bytes)` for the caller to sample from any deterministic oracle. This keeps tcp-model purely deterministic state with no rand dependency. Adds `invert_bytes_deliverable(bps, target, t0, upper)` — binary search for when a cumulative byte count crosses a threshold, used by consumers to compute envelope-aware per-message arrival times. Adds `has_active_envelopes()` so consumers can fast-path the unperturbed case, and stops retaining envelopes that fire immediately expired (e.g. under `LinkEnvelopeCfg::disabled`), guaranteeing byte-identical behaviour when envelopes are turned off. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
tcp-model: scaffold crate in shared-rs workspace
Workspace skeleton for the shared TCP-behaviour envelope model crate that will be consumed by sim-rs and net-rs. No implementation yet; modules and API follow. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: pass header issuer to PraosState::on_tip_advanced
shared-consensus's `on_tip_advanced` grew an `&[u8]` `header_issuer` argument in 7920317e4 (RB-header equivocation detection on tip announce); the sim-rs adapter was still calling the old 8-arg signature, which CI surfaced after the prc/con-rs branch was rebased onto a main that includes the equivocation-detection commit. Sim's notion of an issuer is the producing `NodeId`; feed it in as its little-endian byte representation. Honest-only sims never produce two distinct headers at the same `(slot, issuer)` pair so the new equivocation predicate stays dormant, but the wiring is now ready for behaviour-driven adversarial runs. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
shared-consensus: fix 11 rustdoc intra-doc-link warnings
Mix of stale renames, unqualified type references inside nested modules, and one private-item link. cargo doc -p shared-consensus --no-deps is now warning-free. - praos.rs: parent_announced_eb → parent_announced_eb_for_cert (renamed at some point); qualify equivocating_rb_slots and note_header_first_seen with PraosState::; drop the link to the private LeiosState::decide_vote and point at the public ChainTipContext::equivocating_slots that actually carries the data. - leios.rs: [`EmitVote`] → [`LeiosEffect::EmitVote`]. - mempool.rs: [`get_body_by_id`] → [`MempoolState::get_body_by_id`]. - chain_tree.rs: [`select_chain_once`](super::consensus::praos) was wrong on both ends — fix to the real path crate::praos::PraosState::select_chain_once. - behaviour/mod.rs: inside `mod behaviours`, [`Behaviour`] resolves to nothing — use [`super::Behaviour`]; same for the [`decide_body_path`] cross-method link (use [`Self::decide_body_path`]); drop the redundant explicit link target on [`registry::build`]. - behaviour/behaviours/lazy_voter.rs: [`LeiosEffect::NoVote`] → [`crate::leios::LeiosEffect::NoVote`]. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
shared-consensus, sim-rs: Declined NoVote reason for policy abstention
`LazyVoter` previously defaulted to reporting `WrongEB`, which made its abstentions indistinguishable from real chain-tip-race predicate failures in the JSONL telemetry — on the honest 30-node + 2-lazy sample-cluster-lazy-voter sweep, 96% of NoVote events were tagged WrongEB and only the LazyVoter share was actually policy-driven. Without a way to tell them apart, the sweep can't validate whether WrongEB is real chain-tip dynamics or just the configured lazy voters declining. Add a dedicated `NoVoteReason::Declined` variant to both shared-consensus's enum and sim-rs's `model::NoVoteReason`, plumbed through the sim adapter mapping at `sim-rs/sim-core/src/sim/shared_consensus.rs`. `LazyVoter::default` and `default_lazy_reason` for the `BehaviourSpec::LazyVoter` YAML node both flip from `WrongEB` to `Declined`; explicit `reason: X` overrides on either side still work if a sweep wants to mimic an undetectable lazy voter by picking one of the honest predicate failures. Verified on sample-cluster-lazy-voter.toml (30 nodes mainnet-shaped, 2 LazyVoter pools holding ~22% of voting stake, 5-min run): Before: NoVote = MissingTX 5 + WrongEB 130 After: NoVote = MissingTX 5 + Declined 130 + WrongEB 0 Confirms the dominant signal is the policy abstention, not a chain-tip race — and that real WrongEB at this throughput is zero, i.e. tx and EB diffusion are timely. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
net-rs, shared-consensus: wire RbProductionStrategy into block production
Adds the wrapper-side hook the behaviour trait was missing. After `BlockProducer::try_produce_block` returns a winning RB, the main loop asks the behaviour what to do via the new `Consensus::rb_production_strategy`: - `Normal` — register and inject as today (honest path). - `Suppress` — log "suppressing produced RB" and drop the block. Lottery win is wasted; no telemetry, no chain insert, no broadcast. - `Equivocate` — register + inject the primary, then call `BlockProducer::produce_equivocation_extra` to build a duplicate RB sharing the primary's `(slot, issuer)` but carrying an empty body. Different body_hash → different header_hash, so CIP-0164 `note_header_for_equivocation` flags the slot when the duplicate reaches a peer. shared-consensus - `LeiosState::ask_rb_production_strategy(&PraosState, slot)` runs the standard take/restore around the strategy hook. Lives on Leios because the behaviour itself does (chosen during scaffolding for the broadest hook surface); both states are reachable from one method. net-node - `BlockProducer` now caches the issuer_vkey for the current slot rather than regenerating per `make_fake_block` call. Honest paths refresh at the top of `try_produce_block`; equivocation extras reuse the cached value. This is also a pre-existing-bug fix: honest blocks now have a stable per-call issuer that `note_header_for_equivocation` can correlate against. - `BlockProducer::produce_equivocation_extra(&primary, prev_hash, …)` builds the duplicate. - `LeiosConsensus::state_mut` + `PraosConsensus::state` expose the inner states so the wrapper can run the take/restore hook. Smoke (10-node cluster, node-3 = RbEquivocator, rb_generation_probability=1.0): node-3 logs `emitting equivocation extra RB primary=… extra=…` on every slot it wins. The producer's own `equivocating_rb_slots` records the slot via the existing `note_header_for_equivocation` path. Known limitation (documented in the plan file): peer-side detection doesn't fire yet because the cluster's chain-advertise path publishes a single tip at a time. Closing that gap is a wire-layer change in `net_core::multi_peer::coordinator` and is queued as a follow-up. Tests: 263 shared-consensus + 119 net-node + 43 net-cluster + 316 net-core all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: own bitmap helpers and missing-eb-tx logic
Moves the sparse BTreeMap<u16, u64> bitmap helpers (from_indices / select_all / contains / iter_indices) into con_rs::bitmap. The encoded form already crosses the con-rs trait boundary (EbTxsFetchPolicy::pick, on_eb_txs_offered), so its encoders belong on the same side. net-core's leios_fetch::bitmap stays as a thin re-export to preserve callers. Adds LeiosState::missing_eb_tx_bitmap(eb_hash, &MempoolState) → the wire-encoded sparse bitmap of manifest indices whose bodies aren't yet locally available. Empty bitmap when the manifest is unknown or every referenced tx is held — both deliberately suppress further fetches (a "select all" reply against a peer that doesn't have the bodies either triggers the retry storm we hit on the broadcast-n=3 experiment). Net-rs's bitmap_for_missing_txs now delegates to con-rs. LeiosConsensus is the only consumer with both the LeiosState and mempool handles, so the wrapper exposes a borrow via Mempool::as_inner. Dead Mempool::current_tx_ids method removed. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: NoFetch policy + per-class fetch-policy YAML knob
Adds a NoFetch policy implementing BlockFetchPolicy, EbFetchPolicy,
EbTxsFetchPolicy, and VoteFetchPolicy. Every pick returns empty so
the matching offer never fans out into a fetch effect.
Surfaces it as a config in both consumers:
net-rs: FetchPolicyKind::NoFetch added alongside LowestRtt /
Broadcast, routed through all four into_*_policy helpers.
sim-rs: new top-level `fetch-policy` YAML stanza mirroring net-rs's
FetchPolicyConfig shape (block / eb / eb-txs / votes), each
class independently set to lowest-rtt | broadcast | no-fetch.
Default `lowest-rtt` everywhere preserves LeiosState::new
behaviour.
Wired into the sim adapter via set_eb_policy / set_eb_txs_policy /
set_vote_policy on LeiosState and set_fetch_policy on PraosState.
Empirical note: at NA,0.350 / wfa-ls / 750n / -s 1500, switching the
eb-txs class to NoFetch produces results bit-identical to LowestRtt.
The sim adapter sends EBs via `Message::EB(eb)` with the full
LinearEndorserBlock.txs Arcs in-band; the receiver never enters a
manifest-without-bodies state, so on_eb_txs_offered is never called.
Modelling separate EB-tx fetch in the simulator needs an adapter
change to split EB transmission into manifest + body fetch.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
shared-rs, sim-rs: fix four pre-existing clippy lints
- shared-consensus/praos.rs: drop unused `let now` in equivocation test - sim-core/clock/coordinator.rs: `while let Poll::Pending = poll!(...)` -> `while poll!(...).is_pending()` - sim-core/sim/shared_consensus.rs: drop redundant `as u64` cast on `rb_win_threshold` (already returns u64) - sim-core/sim/shared_consensus.rs: collapse `contains_key + insert` into `entry(id).or_insert(...)` in `receive_announce_eb` Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: parent-tied cert selection in con-rs adapter
CIP-0164 reads a Praos+Leios cert as endorsing the EB announced by
*the* preceding block ("a preceding block" in the spec language,
but in practice the immediate parent in the chain). The con-rs
adapter's previous selection — "iterate all cert-eligible EBs in
`votes_by_eb`, pick the first whose hash has quorum" — could pick
any cert-eligible EB at the certified slot, including ones the
chain has long since moved past.
Switch to the linear-equivalent selection: look up the parent's
`eb_announcement` from the parent RB header, check that the
parent's EB has cleared `3·Δhdr + L_vote + L_diff` slots and has
quorum, and build the cert. Mirrors `linear_leios.rs`'s
`ebs_by_rb.get(&parent_rb_id)` lookup.
This is the load-bearing change that closes the +21% EB inflation
gap at NA,0.350 that has tracked con-rs vs linear since the W2.6
sweep. With this + the `preserve quorum-reached elections` fix
(47680d34c) + the placeholder-elections fix (1c414cdfe), seed 1 at
NA,0.350 lands at 74 EBs / 28 endorsed vs linear's 71 / 29 —
within seed-noise.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: con-rs adapter — fan out AnnounceRB after body validation
The adapter only sent AnnounceRBHeader (in `finish_validating_rb_header`) and relied on the header response's `has_body` flag to propagate body fetches. When a relay node first announced a header to its consumers, its own state was Pending / Requested (body still in flight), so the consumers saw `has_body=false` and didn't request the body. When the relay later transitioned to Received, it never told the consumers, so bodies only propagated one hop — out to the producer's direct consumers — and stalled there. Mirror linear_leios's `publish_rb` fan-out: send AnnounceRB to every consumer when `finish_validating_rb` completes. Consumers in Pending state pick it up via `receive_announce_rb` and request the body. NA,0.200 / -s 1500 / seed 0 now produces 60 EBs / 10 uncertified / 26 endorsements / 28 755 votes — matching linear's 61 / 10 / 26 / 29 569. Peak RSS stays ~5 GB (was ~38 GB), since the mempool drain on cert now fires at every node instead of just direct consumers of the producer. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
net-cluster: replace behaviour selection fields with BehaviourSelection enum
The previous shape spread three loosely-coupled fields across the
cluster config — `behaviour_nodes`, `behaviour_fraction`,
`behaviour_source` — with semantics scattered between them and a
magic-string `source` that the type system could not enforce. Replace
with a single tagged enum, `BehaviourSelection`, holding five variants:
- `All` — every node.
- `Nodes { indices }` — verbatim list.
- `StakeOrdered { count }` — top N stake-bearing nodes by stake.
- `StakeRandom { count }` — deterministic-random N stake-bearing nodes,
seeded from the cluster seed.
- `StakeFraction { fraction }` — smallest prefix of stake-bearing
nodes (stake-desc) whose cumulative stake covers `fraction` of the
total. Same shape as CIP-0164 top-stake committee selection and
the right knob for abstention-pressure experiments — `0.2` makes
20% of the *voting weight* run the behaviour regardless of how
many nodes that turns out to be.
TOML:
```toml
[behaviour_selection]
kind = "stake-fraction"
fraction = 0.2
```
All variants are deterministic for a given `seed` so re-runs land on
the same nodes. Stake-aware variants filter to `stake > 0` so relays
under `mainnet-shaped` are never picked. Resolver lives next to the
stake-distribution code in `topology.rs` with unit coverage for each
variant.
Also adds `sample-cluster-lazy-voter.toml`: 30 nodes, mainnet-shaped
distribution, `tx_rate = 1.0`, `LazyVoter` behaviour installed on the
top 20% of stake via `stake-fraction`. Pairs with the
`sample-cluster-leios-baseline.toml` baseline for delta studies (see
the caveat in that commit's message about the underlying MissingTX
ceiling — the threshold experiment is currently dominated by net-rs's
EB-tx fetch starvation, not the lazy stake).
`sample-cluster-equivocator.toml` migrates from `behaviour_nodes = [3]`
to the new `[behaviour_selection] kind = "nodes" indices = [3]` form.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: enforce CIP-0164 PV/NPV pool partition
CIP-0164 partitions pools by stake-ordering into persistent (indices
[1, n_1]) and non-persistent candidates ([i*, |P|]) — disjoint by
construction. A pool emits exactly one vote per election: PV xor
NPV, never both. Spec text in post-cip/weighted-fait-accompli.md
(§ Persistent seats / § Non-persistent candidates).
The current implementation violated the partition on both sides:
- `decide_vote` (leios.rs) ran the NPV trial unconditionally; a
persistent-seated voter could emit both a PV and an NPV signature
for the same EB.
- `weight_for` (elections.rs) accepted both and credited them
independently in `voter_weights`, since `record_vote` dedups by
`(voter_id, tag)` — so dual-emitters were double-counted toward
quorum.
Now:
- `decide_vote` returns NPV signature only when `persistent_seats == 0`.
- `weight_for(tag=1, ..)` returns 0 if the voter is in the
persistent committee. Defence-in-depth: a malformed dual-vote
arriving over the wire is silently dropped instead of inflating
weight.
Empirical at NA,0.350 / wfa-ls / 750n / -s 500 (con-rs):
bundles: 2093 PV-only + 207 NPV-only + 1593 dual (pre)
= 3686 PV-only + 207 NPV-only + 0 dual (post)
total weight: 11748 → 9648 (−18%, the prior over-count was the
dual NPV weight from PV-seated voters)
uncertified: 6/26 → 9/26 (cert rate degrades because the previous
quorum-passing weights were inflated by
the spec violation; the new number is
the honest baseline)
The mean per-EB weight drops 452 → 371, crossing below the configured
`vote-threshold: 450` on three more EBs. Recovering cert rate from
this regime is a committee-size / threshold tuning question, not a
correctness fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: con-rs adapter — drain endorsed-EB txs from mempool
Mirror linear_leios's two-path drain so the local mempool doesn't keep re-including txs that are already on chain via an endorsement: - Production path (`try_produce_rb`): build the endorsement first, then drain its EB's txs from `mempool.txs` and the adapter's tx-tracking maps before `BodyPath::decide` walks the mempool to shape the new EB body. - Validation path (`finish_validating_rb`): on receipt of an RB that carries an endorsement, drain the endorsed EB's txs locally if its body has already validated; otherwise stash the EB id in `incomplete_onchain_ebs` and drain on the next `finish_validating_eb`. At NA,0.200 / -s 1500 / seed 0 this shrinks producer EB tx counts mid-run (slot 175 13312 → 2399 txs, slot 185 16648 → 3070) so the network isn't propagating ever-growing EB bodies. Voting cert rate barely moves on its own — the remaining gap (87 EBs / 72 uncertified vs linear's 61 / 10) needs separate work on RB-body propagation and the WrongEB predicate input. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: cap EB body at eb_body_max_bytes on overflow
`BodyPath::decide` now takes an `eb_body_max_bytes` cap. When the mempool overflows the RB body cap, the EB manifest is truncated at this byte limit (FIFO-ordered, at-least-one guarantee preserved) and the remainder stays in the mempool for the next RB. `MempoolState::produce_eb` takes a matching `count` so the drain commits exactly the manifest's prefix instead of every free tx. Sim-rs's adapter passes `eb_referenced_txs_max_size_bytes`; net-rs gains an `eb_body_max_bytes` config field (default 16 MB). At NA,0.350 / wfa-ls / 750n / -s 1500: lifts con-rs cert rate from 11% to 83%, beats `linear-with-tx-references` (53%) on the same config. Pre-cap EBs grew monotonically with mempool ingest because overflow spilled the whole pool into one EB; capped EBs now sit at ~5k-10k txs in the regime where the cap binds. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
net-rs: clear pre-existing clippy lints
Four lints were firing under cargo clippy --all-targets -- -D warnings: - net-core/src/store/leios_store.rs: manual_is_multiple_of → version.is_multiple_of(stats_log_interval) - net-node/src/consensus/leios/mod.rs: cloned_ref_to_slice_refs → std::slice::from_ref(&body1) - net-node/src/consensus/leios/mod.rs: dead_code on election_phase / election_count test helpers, orphaned by the duplicate-test cull in 4d6588ee7 - net-node/src/mempool.rs: dead_code on Mempool::peek_up_to (only ever called from its own self-tests) and the unused make_tx test helper The first two are mechanical clippy fixes; the latter two are genuine dead code, deleted along with their self-tests. Whole- workspace cargo clippy --all-targets -- -D warnings is now clean, so future regressions surface immediately instead of getting shrugged off as "pre-existing." 493 net-rs tests pass (down 2 from removed peek_up_to self-tests). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: emit TxRejected{EbClosurePruned} when EB closures age out
Sim-rs distinguishes tx losses by reason — `EBExpired` covers txs that were pinned under an EB whose manifest aged past the retention window. Con-rs's mempool already pruned these silently; surface them as a `MempoolEffect::TxRejected` so adapters can record the loss. - New `TxRejectReason::EbClosurePruned` variant - `prune_eb_slot_window` returns `Vec<MempoolEffect>`, one per body released from `eb_pinned` because no surviving manifest referenced it - `produce_eb` returns `(manifest, effects)`; `record_eb_manifest` returns `effects`; both propagate evictions up through `bump_eb_slot` - `MempoolEffect` now derives `PartialEq, Eq` for test assertions - Net-rs's `Mempool::produce_eb` becomes `-> ()` (the manifest is already known from `BodyPath::Eb`); evictions dropped on the floor for now (no net-rs telemetry plumbing yet) Tests: con-rs 206 → 207 (+1 for produce_eb evict path; existing slot_retention test extended to assert the effect emission). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: pluggable multi-peer fetch policies + candidate tracker
Replaces net-core's LeiosTracker (peer-selection algorithm + offer
maps + dedup, ~670 lines) and the inline fragment-scan-then-RTT-min
routing in coordinator.rs with four independently swappable policy
traits hosted in con-rs/src/fetch.rs:
BlockFetchPolicy — Praos block-range fetch
EbFetchPolicy — Leios EB body fetch
EbTxsFetchPolicy — Leios EB tx-list fetch (with bitmap)
VoteFetchPolicy — Leios vote-batch grouping across peers
Two stock implementations cover the trivial wiring case by
implementing all four traits: LowestRttFirst (single peer, current
behaviour) and BroadcastN (fan-out by RTT, with N=1 / N=∞ degenerating
to RequestFromFirst / RequestFromAll respectively). Sims and
production wrappers can swap each class independently as research
evolves.
CandidateTracker holds the offer maps + pending-fetch dedup +
EB-txs-attempts retry-skip that LeiosTracker used to manage. Lives on
LeiosState as a field; net-core's coordinator no longer dedups offers
or picks fetch peers — every offer flows through to consensus with
peer_id attached, and consensus emits fetch effects with concrete
peer lists already chosen.
PraosState similarly holds a BlockFetchPolicy + PeerRtt; its
issue_fetch_internal walks peer_chains for candidates (the
fragment-scan equivalent) before delegating to the policy. The
peer_id hint that triggered selection still gets included in the
candidate pool as a fallback.
Effect / event / command schema changes:
PraosEffect::FetchBlockRange { peer_id: Option<PeerId> }
→ { peers: Vec<PeerId> }
LeiosEffect::FetchLeiosBlock { ..peers: Vec<PeerId> }
FetchLeiosBlockTxs { ..peers: Vec<PeerId> }
FetchLeiosVotes { per_peer: BTreeMap<PeerId, ..> }
NetworkEvent::LeiosBlockOffered + peer_id: PeerId
LeiosBlockTxsOffered + peer_id: PeerId
LeiosVotesOffered + peer_id: PeerId
NetworkCommand::FetchLeiosBlock + peer_id: PeerId (mandatory)
FetchLeiosBlockTxs + peer_id: PeerId
FetchLeiosVotes + peer_id: PeerId
Net-rs dispatchers loop over the peers vec / per_peer map, emitting
one NetworkCommand per peer (empty list ⇒ silent drop). Coordinator
is now a thin pass-through: dispatches to the named peer without
running its own selection.
Verification: 155 con-rs tests (13 new fetch.rs tests) + 512 net-rs
tests pass. cargo clippy --all-targets -- -D warnings clean in
con-rs. net-rs's lone clippy hit is a pre-existing
manual_is_multiple_of in leios_store.rs unrelated to this work.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: clear EB-tx pending guard on response, respect it on retry
Two-line bookkeeping bug in LeiosState's CandidateTracker integration caused a runaway retry loop on partial EB-tx fetch responses: 1. on_eb_txs_received cleared the per-slot in_flight gate but never called candidates.finish_eb_txs_fetch — the per-Point pending guard stayed permanently set after the first fetch. 2. retry_eb_tx_fetch called candidates.start_eb_txs_fetch but ignored the returned bool, firing a retry whether the guard was already set or not. Combined with main.rs's ordering — match_eb_tx_response runs BEFORE handle_event in the event loop — every partial response from a single peer triggered another fetch to a different peer, and so on through the candidate pool, with each retry sitting in the per-protocol mpsc channel until the consumer caught up. In a 25-node cluster smoke this manifested as ~360 IngressOverflow events per node on protocol 19 (LeiosFetch) over 60 seconds, tearing down peer mux connections and starving the rest of the Leios pipeline. Fixes: - match_eb_tx_response now calls candidates.finish_eb_txs_fetch. By definition the in-flight fetch is done the moment its response is matched; clearing here covers both the wrapper-direct flow (test fixtures call match_eb_tx_response without going through on_eb_txs_received) and the production main.rs flow (where match_eb_tx_response runs before handle_event). - on_eb_txs_received also calls candidates.finish_eb_txs_fetch as a defence-in-depth idempotent clear — covers wrappers that drive the receive path without separately calling match_eb_tx_response. - retry_eb_tx_fetch respects start_eb_txs_fetch's return value, skipping the fetch when a previous one is still in flight. The next response will trigger the next retry attempt naturally. Smoke verification in the same 25-node cluster: IngressOverflow events on protocol 19 drop from 360 to 9 in node-0, partial-retry warnings from hundreds-per-EB to ~3. 182 con-rs + 478 net-rs tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: drive Leios slot tick + RB lottery in con-rs adapter
`handle_new_slot` now advances the local Leios state via `LeiosState::on_slot` and runs the Praos RB lottery using `con_rs::lottery::rb_win_threshold` against sim's existing `Rng::draw_range` VRF — the formula is shared with net-rs but each consumer keeps its own draw form, per the W2.0 design note. A new `apply_leios_effects` helper drains the effect vec. Variants without a target yet (fetch / vote-emit / validate / record-manifest) fall through to no-ops; the next slices wire them when the RB / EB / vote message families come up. `NoVote` and the QuorumReached / ElectionExpired telemetry events are matched explicitly so we don't silently drop them later. On a lottery win the adapter records the win via `track_praos_block_lottery_won` and stops — building the RB body, scheduling `RBBlockGenerated`, and announcing the header lands in the production slice once `BodyPath::decide` and the RB / Praos message handlers are in place. `tx_known` passed to `LeiosState::on_slot` is `|_| true` until the EB-manifest path lands; without EBs in flight the predicate never fires. Build green; 55 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: W2.3 — YAML → con-rs config translation
Three new helpers replace the previous placeholder values in `ConRs::new`: - `derive_pipeline` builds `PipelineConfig` from `leios-header-diffusion-time-ms`, `linear-vote-stage-length-slots`, `linear-diffuse-stage-length-slots`, and the residual portion of `linear-tx-max-age-slots` as the dedup window. - `derive_committee_selection` maps sim's three committee algorithms to con-rs's `CommitteeSelection` variants. For `WfaLs` it collapses sim's single combined `vote_probability` into `persistent_voters` (entire expected committee × node count) with `non_persistent_voters` forced to 0, because `SimConfiguration` doesn't preserve the per-class breakdown. - `derive_quorum_fraction` turns sim's absolute `vote-threshold` count into the fractional form con-rs expects (`vote_threshold / expected committee size`), with the CIP-0164 default 0.75 as fallback. Vote-bundle byte budgets now come from `Sizes::vote_bundle(1)` on both the PV and NPV legs — sim collapses them into a single curve, so we pass the same value to both fields. The module docstring gains an at-a-glance table listing every YAML knob the adapter reads and which con-rs destination it lands in, plus a sibling table of the values that are still hardcoded and why (`WfaLs.non_persistent_voters`, `StakeCentile.top_centile_of_stake`, PraosState `k`, fetch policies). Build green; 55 sim-core tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>