May 13, 4-5 PM (33)
May 13, 5-6 PM (30)
May 13, 6-7 PM (51)
May 13, 7-8 PM (33)
May 13, 8-9 PM (9)
May 13, 9-10 PM (24)
May 13, 10-11 PM (30)
May 13, 11-12 AM (11)
May 14, 12-1 AM (18)
May 14, 1-2 AM (3)
May 14, 2-3 AM (4)
May 14, 3-4 AM (21)
May 14, 4-5 AM (11)
May 14, 5-6 AM (18)
May 14, 6-7 AM (18)
May 14, 7-8 AM (47)
May 14, 8-9 AM (53)
May 14, 9-10 AM (35)
May 14, 10-11 AM (20)
May 14, 11-12 PM (114)
May 14, 12-1 PM (54)
May 14, 1-2 PM (151)
May 14, 2-3 PM (32)
May 14, 3-4 PM (17)
May 14, 4-5 PM (14)
May 14, 5-6 PM (38)
May 14, 6-7 PM (12)
May 14, 7-8 PM (22)
May 14, 8-9 PM (37)
May 14, 9-10 PM (35)
May 14, 10-11 PM (27)
May 14, 11-12 AM (14)
May 15, 12-1 AM (18)
May 15, 1-2 AM (15)
May 15, 2-3 AM (5)
May 15, 3-4 AM (3)
May 15, 4-5 AM (13)
May 15, 5-6 AM (14)
May 15, 6-7 AM (10)
May 15, 7-8 AM (31)
May 15, 8-9 AM (23)
May 15, 9-10 AM (52)
May 15, 10-11 AM (71)
May 15, 11-12 PM (70)
May 15, 12-1 PM (73)
May 15, 1-2 PM (73)
May 15, 2-3 PM (66)
May 15, 3-4 PM (26)
May 15, 4-5 PM (13)
May 15, 5-6 PM (30)
May 15, 6-7 PM (29)
May 15, 7-8 PM (25)
May 15, 8-9 PM (8)
May 15, 9-10 PM (34)
May 15, 10-11 PM (34)
May 15, 11-12 AM (25)
May 16, 12-1 AM (2)
May 16, 1-2 AM (2)
May 16, 2-3 AM (3)
May 16, 3-4 AM (3)
May 16, 4-5 AM (0)
May 16, 5-6 AM (6)
May 16, 6-7 AM (2)
May 16, 7-8 AM (10)
May 16, 8-9 AM (1)
May 16, 9-10 AM (2)
May 16, 10-11 AM (1)
May 16, 11-12 PM (13)
May 16, 12-1 PM (11)
May 16, 1-2 PM (8)
May 16, 2-3 PM (15)
May 16, 3-4 PM (10)
May 16, 4-5 PM (2)
May 16, 5-6 PM (2)
May 16, 6-7 PM (2)
May 16, 7-8 PM (10)
May 16, 8-9 PM (6)
May 16, 9-10 PM (9)
May 16, 10-11 PM (29)
May 16, 11-12 AM (42)
May 17, 12-1 AM (9)
May 17, 1-2 AM (1)
May 17, 2-3 AM (0)
May 17, 3-4 AM (1)
May 17, 4-5 AM (0)
May 17, 5-6 AM (3)
May 17, 6-7 AM (2)
May 17, 7-8 AM (1)
May 17, 8-9 AM (1)
May 17, 9-10 AM (1)
May 17, 10-11 AM (6)
May 17, 11-12 PM (6)
May 17, 12-1 PM (4)
May 17, 1-2 PM (5)
May 17, 2-3 PM (9)
May 17, 3-4 PM (4)
May 17, 4-5 PM (8)
May 17, 5-6 PM (14)
May 17, 6-7 PM (10)
May 17, 7-8 PM (2)
May 17, 8-9 PM (4)
May 17, 9-10 PM (2)
May 17, 10-11 PM (20)
May 17, 11-12 AM (13)
May 18, 12-1 AM (10)
May 18, 1-2 AM (4)
May 18, 2-3 AM (5)
May 18, 3-4 AM (9)
May 18, 4-5 AM (14)
May 18, 5-6 AM (2)
May 18, 6-7 AM (37)
May 18, 7-8 AM (28)
May 18, 8-9 AM (35)
May 18, 9-10 AM (41)
May 18, 10-11 AM (43)
May 18, 11-12 PM (29)
May 18, 12-1 PM (136)
May 18, 1-2 PM (34)
May 18, 2-3 PM (89)
May 18, 3-4 PM (33)
May 18, 4-5 PM (45)
May 18, 5-6 PM (21)
May 18, 6-7 PM (16)
May 18, 7-8 PM (13)
May 18, 8-9 PM (23)
May 18, 9-10 PM (4)
May 18, 10-11 PM (25)
May 18, 11-12 AM (12)
May 19, 12-1 AM (7)
May 19, 1-2 AM (2)
May 19, 2-3 AM (9)
May 19, 3-4 AM (5)
May 19, 4-5 AM (10)
May 19, 5-6 AM (3)
May 19, 6-7 AM (53)
May 19, 7-8 AM (23)
May 19, 8-9 AM (46)
May 19, 9-10 AM (66)
May 19, 10-11 AM (30)
May 19, 11-12 PM (48)
May 19, 12-1 PM (79)
May 19, 1-2 PM (70)
May 19, 2-3 PM (41)
May 19, 3-4 PM (51)
May 19, 4-5 PM (15)
May 19, 5-6 PM (20)
May 19, 6-7 PM (18)
May 19, 7-8 PM (9)
May 19, 8-9 PM (21)
May 19, 9-10 PM (10)
May 19, 10-11 PM (28)
May 19, 11-12 AM (13)
May 20, 12-1 AM (21)
May 20, 1-2 AM (9)
May 20, 2-3 AM (4)
May 20, 3-4 AM (5)
May 20, 4-5 AM (9)
May 20, 5-6 AM (37)
May 20, 6-7 AM (47)
May 20, 7-8 AM (53)
May 20, 8-9 AM (50)
May 20, 9-10 AM (16)
May 20, 10-11 AM (40)
May 20, 11-12 PM (28)
May 20, 12-1 PM (50)
May 20, 1-2 PM (91)
May 20, 2-3 PM (17)
May 20, 3-4 PM (165)
May 20, 4-5 PM (3)
4,114 commits this week
May 13, 2026
-
May 20, 2026
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]>
fix: handle duplicate redeemer pointers in script context
Signed-off-by: Eric Torreborre <[email protected]>
chore(openapi): remove all cardano db v1 routes and types
test(gateway): capture uncertified stability height fallback
Subsume `RuleListEra` into `EraTest`:
There is no need to have a separate type class for a common concept that we aready have for all eras that is provided by `EraTest`. This commit also hides unnecessary for public view implementation detail: `EraRuleProof` and `UnliftRules`. Thanks to `EraRulesWithFailures` type family and relocation of predicate failure CBOR roundtrip tests into `ledgerTestMain` we are guaranteed that these tests will run for all eras. Moreover, this changes is in line with overall tests hierarchy that we are converging onto.
net-rs: wire shared-consensus behaviour hooks into net-node + net-cluster
Static config + dynamic toggle for the per-node `Behaviour` introduced in the shared-consensus commit. net-node - `NodeConfig.behaviour: Option<BehaviourSpec>` (TOML field). Applied in `Consensus::new` to all three state machines (Praos, Leios, Mempool) before the consensus task starts. - `DynamicConfigUpdate.behaviour: Option<BehaviourSpec>` for runtime swap via the existing stdin JSON channel. `Consensus::set_behaviour` applies it across the live state machines. - Per-layer `set_behaviour` setters on `PraosConsensus`, `LeiosConsensus`, and `Mempool` materialise the spec via `shared_consensus::behaviour::build`. net-cluster - `ClusterConfig.behaviour: Option<BehaviourSpec>` plus `behaviour_nodes: Vec<usize>` for per-node selection. Empty `behaviour_nodes` with a non-`None` spec attaches to every node. - `NodeTopology` carries the resolved per-node spec; `render_overlay` serialises it through the `toml` crate so the [behaviour] table stays in sync with serde's view of `BehaviourSpec`. - Sample config `sample-cluster-equivocator.toml` runs a 10-node cluster with node-3 as the `RbEquivocator`. Smoke-verified: the generated `node-3.toml` carries `[behaviour]\nkind = "rb-equivocator"`, peer overlays carry none, and node-3's startup log emits `installing per-node behaviour spec=RbEquivocator`. Tests: 119 net-node + 43 net-cluster + 316 net-core + 263 shared-consensus pass; sim-rs still builds. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: forbid cert + inline-body in BodyPath::decide
CIP-0164 specifies that an RB body contains either a Leios certificate OR a list of transactions, not both: > "RB' contains either a certificate for the EB announced in RB, > or a list of transactions forming a valid extension of RB." > "When a certificate is included, no further transactions are > allowed in the RB for the same reason." Rationale (per the CIP): a validator carrying a cert must build ledger state from the endorsed EB's closure before it could validate fresh inline txs in the same block, and the slot timing budget can't accommodate both. Announcing a new EB in the header is independent of this rule and remains available alongside a cert. Previously `BodyPath::decide` made the inline-vs-Eb choice purely on mempool-size grounds, ignoring whether the caller was about to attach a cert. At low throughput (mempool routinely <= rb body cap) the cert+small-mempool path would return Inline, producing an RB body with both a cert and inline txs — a CIP violation. At high throughput the bug was suppressed because the mempool overflow threshold pushed every cert-carrying slot into the Eb branch, but the violation was still latent. This thread `endorsement_present: bool` through `BodyPath::decide`. When the cert is about to attach and the mempool is below the overflow threshold, the body returns Empty rather than Inline. All four legal CIP combinations remain reachable: cert + empty body (small mempool + cert) cert + empty body + announced EB (large mempool + cert) inline body (small mempool, no cert) empty body + announced EB (large mempool, no cert) Threaded through both consumers — sim-rs's `con_rs.rs` adapter passes `endorsement.is_some()`, net-rs's `try_produce_block` passes its `certified_eb` flag. Existing tests in con-rs updated to the new signature (10 call sites); three new tests cover the cert-present cases. Verification: con-rs 231 tests pass (3 new), sim-core 55 pass, net-rs workspace 119 pass. At NA,0.350 the bug was suppressed by mempool size; effect on the 5-seed sweep should be invisible or seed-noise. At low-throughput configs the change is observable: cert-carrying RBs with small mempools now emit empty bodies instead of cert+inline. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: con-rs emits CIP-0164 per-vote messages, not bundles
The sim's `VoteBundle` aggregation is a pre-CIP-0164 simplification —
in the real protocol every PV / NPV vote is one BLS signature on the
wire, diffused independently, aggregated only at the certifier. The
con-rs adapter now mirrors that shape:
model.rs:
Vote, VoteId<Node>, VoteKind (no `weight` field — weight is
re-derived at verification time from the persistent-committee
registry / NPV-VRF check, matching CIP wire encoding)
linear_wire.rs:
Message::{AnnounceVote, RequestVote, Vote(Arc<Vote>)}
CpuTask::{VoteGenerated, VoteValidated}
Vote class costs the matching `persistent_vote_bytes` or
`non_persistent_vote_bytes` from VotingConfig.
con_rs.rs:
`votes: BTreeMap<VoteId, VoteState>` (was `vote_bundles`)
`emit_vote` produces ONE Vote per (voter, EB) honouring Part A's
PV-xor-NPV partition. Receivers call
`Elections::weight_for(voter_id, tag, sig)` to re-derive weight —
same code path net-rs uses.
events.rs / sim-cli:
Event::{VoteGenerated, VoteSent, VoteReceived} alongside the
existing bundle variants. `weight` is carried on
`VoteGenerated` for telemetry aggregation (NOT on the wire).
sim-cli stats aggregator handles the new events.
`linear_leios.rs` is untouched in behaviour — bundle Message /
CpuTask variants stay live for it; the per-vote variants are
`unreachable!` from its dispatch. Conversely the bundle variants
are unreachable in con-rs. Strict adapter ownership keeps the
union-typed `Message` enum honest.
Empirical at NA,0.350 / wfa-ls / 750n / -s 200:
1085 PV-only signatures + 57 NPV-only signatures = 1142 votes
0 dual emissions (Part A partition holding)
0 bundles emitted by con-rs (`"There were 0 bundle(s) of votes"`)
weighted vote total 2825 matches PV-multi-seat + NPV-unit-seat
aggregation.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
net-rs: default smoke clusters to top-stake-fraction committee
The wFA-LS default (480 PV + 120 NPV) assumes mainnet-scale pool counts. At the smoke cluster's 9 pool nodes only PV is allocatable (CIP-0164 partitions PV/NPV pool sets disjointly, and every pool wins at least one PV seat at this scale), so the achievable max is ~480 vs the threshold of 0.75 × 600 = 450 — only ~30 seats of margin above the threshold. Losing a single pool's PV seats drops the cluster below quorum, masking the abstention-budget behaviour the lazy-voter sample is meant to exercise. Switch the shared base (`mainnet.toml`) to top-stake-fraction selection: every pool covered by the top 99% of stake casts a weight-1 vote. The committee size now matches the actual voter count (9), so the threshold (`floor(0.75 × 9) = 6`) is proportional to who can actually vote. Lazy-voter sweep on the 30/9-pool sample, 5 min each, identical seed/topology (push-on-admit fanout already in tree): fraction=0.0 → 0 / 9 lazy → 9 honest → 92% cert fraction=0.1 → 1 / 9 lazy → 8 honest → 92% cert fraction=0.2 → 2 / 9 lazy → 7 honest → 92% cert fraction=0.25 → 3 / 9 lazy → 6 honest → 92% cert (at threshold) fraction=0.34 → 4 / 9 lazy → 5 honest → 0% cert (below threshold) The 13/13 EB-generation count is deterministic across all five runs. The 12/13 ceiling (and the 5 residual MissingTX seen in every run) is producer-rotation residue — same artefact across the sweep. Also trims the sample-cluster-lazy-voter docstring to match the TSF math. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
shared-consensus, net-node: detect RB-header equivocation on tip announcement
Move `note_header_for_equivocation` onto the ChainSync announce path
(`PraosState::on_tip_advanced`) in addition to the existing
block-arrival path (`on_block_received` and `register_self_produced`).
Why: an adversary that advertises divergent RB headers to disjoint
peer subsets (CIP-0164 RB-header equivocation) can keep honest peers
unaware of the divergence indefinitely. Each peer fetches one
variant, sees no `(slot, issuer)` collision, and never bothers
fetching the sibling because Praos chain selection picks the first /
lowest-hash header at that slot and treats the alternative as a
discarded fork. Without a block fetch, `on_block_received` never
fires, so the existing tracker never trips even when the peer has
already received the divergent header.
Detection from the header alone is enough — the equivocation predicate
keys on `(slot, issuer)` against a set of distinct header hashes, all
of which are derivable from the ChainSync `MsgRollForward` payload.
`on_tip_advanced` gains a `header_issuer: &[u8]` parameter; the
net-node wrapper passes `info.issuer` from the parsed header.
Also: an `info!` line on the first detection per slot ("RB-header
equivocation detected") so operators can see the tracker fire from a
log, and a small `was_new` guard so the log fires only when the slot
transitions from non-equivocating to equivocating.
Verified end-to-end on a 10-node cluster with one node running
`RbHeaderEquivocator { ways = 2 }`: every slot the equivocator wins
its bucket-1 peer detects the divergence and inserts the slot into
`equivocating_rb_slots`. Two test fixtures in `praos::tests` were
updated for the new signature.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: gate con-rs EB validation on local tx availability
Mirrors `LinearWithTxReferences`'s CIP-0164 receiver gate: an EB can only be validated once every referenced tx body is in the local mempool. Two side maps thread the dependency: - `eb_pending_txs: EBId → BTreeSet<TxId>` — per-EB still-missing set - `missing_tx_index: TxId → BTreeSet<EBId>` — reverse index for the tx-admit release path `finish_validating_eb_header` now computes `missing` against `mempool.has_tx` (union of free pool + EB-pinned bodies) and either schedules `EBBlockValidated` immediately or stashes the EB pending in both maps. `acknowledge_tx_for_pending_ebs` walks the reverse index after every successful `admit_validated` and releases EBs whose set reaches empty. Chain-prune sweeps both maps in lockstep with the existing `self.ebs` retention. Empirical note: NA,0.350 / wfa-ls / 750n / -s 1500 produces bit-identical results with and without the gate (88/15/33/42084 either way), because tx diffusion is fast enough in this sim that every receiver already has the bodies in mempool before the EB arrives. The gate matters in regimes where Message::Tx lags Message::EB (cluster-scale net-rs, withheld-tx attacks, or once the upcoming sim message split surfaces a real manifest-only intermediate state). It's the protocol-correct behaviour either way — without it, an EB could in principle validate and open voting before any voter has the bodies, an outcome the CIP-0164 MissingTX predicate is supposed to prevent at validation time, not just at decide-vote time. 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]>
sim-rs: plumb NPV through con-rs WfaLs adapter
Renames the two committee-size config fields to match con-rs's
internal naming and the CIP-0164 terminology:
persistent-vote-generation-probability → persistent-voters
non-persistent-vote-generation-probability → non-persistent-voters
Old YAML keys keep working via `#[serde(alias = ...)]` so the 70+
existing experiment configs round-trip unchanged. The "probability"
framing was always misleading — these have always been expected
committee sizes (defaults 400 / 100, not 0.4 / 0.1).
SimConfiguration grows `persistent_voters` / `non_persistent_voters`
alongside the existing `vote_probability` (= their sum). Linear's
VRF lottery keeps consuming the sum unchanged. The con-rs adapter
now wires both into `CommitteeSelection::WfaLs { persistent_voters,
non_persistent_voters }` — previously NPV was hard-zeroed because
the sim collapsed both into one number.
Empirical at NA,0.350 / wfa-ls / 750n / -s 500:
bundles: 2093 PV-only + 207 NPV-only + 1593 both
total: 3893 bundles, 11748 votes, mean 3.018/bundle
uncertified: 6/26, endorsements: 9
NPV-only bundles previously were 0 across every sim run; the 207
new ones are voters without a PV seat who won the per-EB Bernoulli
trial against their stake share — exactly the CIP-0164 contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
shared-consensus: behaviour hook system
Replaces ad-hoc adversarial flags (sim-rs's `ib_equivocation`,
`withhold_txs`) with a pluggable `Behaviour` trait under
`shared-rs/consensus/src/behaviour/`. Each state machine
(LeiosState, PraosState, MempoolState) owns a `Box<dyn Behaviour>`
and consults it at every effect-emitting handler via a small
take/restore helper.
Hook surface:
- Reactive `on_*` hooks return `BehaviourOutcome<E>` —
`Continue` (honest unchanged), `Replace(Vec<E>)`
(substitute the effect list) or `Append(Vec<E>)` (run honest
AND add extras — the equivocation pattern).
- Decision hook `decide_vote` returns `DecisionOutcome<T>` —
`Continue` (use honest predicate) or `Override(T)`.
- Strategy hook `rb_production_strategy` returns
`RbProductionStrategy { Normal, Suppress, Equivocate }` for the
wrapper to act on (RB construction stays consumer-side).
Wired entry points: `on_slot`, `on_eb_offered`, `on_eb_txs_offered`,
`on_votes_offered`, `on_eb_received`, `on_votes_received`,
`decide_vote` (Leios); `on_tip_advanced`, `on_block_received`,
`on_peer_disconnected` (Praos); `on_tx_received`, `on_tx_validated`
(Mempool).
Two concrete behaviours land alongside the trait, each in its own
file under `behaviour/behaviours/`:
- `RbEquivocator` — returns `RbProductionStrategy::Equivocate`.
- `LazyVoter` — overrides `decide_vote` to always abstain.
`BehaviourSpec` serde enum + `build()` factory in `registry.rs`
deserialises a behaviour from per-node config; `CompositeBehaviour`
composes children with first-non-`Continue` wins. `DelayQueue<E>`
helper for slot-granular delay behaviours that hold and re-emit
effects on later slot ticks (no scheduler in shared-consensus).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
net-rs: wire EB-safety gate into apply-time
Carry CIP-0164's `certified_eb` flag through `ParsedHeaderInfo` / `CachedBlock`, plus a new `PraosState::parent_announced_eb_for_cert` that resolves a freshly-applied cert RB back to (parent EB slot, parent EB hash) using only block-cache state. `Consensus::on_validation_outcome` calls `LeiosState::on_chain_endorsement` for the resolved EB on every successful `Applied`. The gate fires when a peer's cert RB lands before the EB body diffuses locally; the next own-production attempt sees `has_endorsed_unvalidated_eb()` and emits an empty body. Self-produced cert RBs flow through the same hook for symmetry (and are no-ops in practice, since cert formation requires local validation). Sim adapter's `parsed_header_from_rb` now populates `certified_eb` from `rb.endorsement.is_some()`. Its existing direct calls to `leios.on_chain_endorsement` from `try_produce_rb` / `finish_applying_rb` remain — same end-state, different path that matches the sim's explicit endorsement reference. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: gate con-rs adapter's log_memory_stats behind --memory-stats
The per-component dump is synchronous on the slot tick (reads /proc/self/status, walks every BTreeMap on node 0) and shows up as a regular CPU heartbeat in htop. Off by default; pass --memory-stats on sim-cli to opt in. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
shared-consensus: clear 6 clippy errors in behaviour subsystem
Five derivable_impls: OutboundDecision, BehaviourSpec, BehaviourOutcome<E>, DecisionOutcome<T>, RbProductionStrategy all had manual Default impls picking a unit variant — replaced with #[derive(Default)] + #[default]. One type_complexity: Behaviour::decide_vote took and returned `Result<(bool, Option<Vec<u8>>), NoVoteReason>`, repeated four times across the trait, CompositeBehaviour, LazyVoter, and a test stub. Promoted to a public `VoteDecision` type alias next to NoVoteReason in leios.rs and updated every callsite. `cargo clippy -p shared-consensus --all-targets -- -D warnings` is now clean; 270 unit tests pass; net-rs still builds. 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]>
sim-rs: extract shared Linear-Leios wire types into linear_wire
The `Message`, `CpuTask`, and `TimedEvent` enums sat inside `linear_leios.rs` but were consumed by both adapters that implement Linear Leios in the sim — `linear_leios.rs` itself and the newer `con_rs.rs`. Splitting them into `sim/linear_wire.rs` makes the ownership match reality: the wire shape is the union both adapters exchange, neither adapter owns it. Pure refactor: enum variants unchanged, `SimMessage`/`SimCpuTask` impls move with their enums, all 55 sim-core tests pass. The adapter-specific dispatch (linear's bundle path; con-rs to follow with per-vote variants in the next commit) stays in each adapter's `handle_message` / `handle_cpu_task`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: CIP-0164 RB-header equivocation detection
CIP-0164 requires voters to abstain from voting for any EB associated with a slot at which RB-header equivocation has been detected: > "It has not detected any equivocating RB header for the same > slot." > "By waiting 3 L_hdr slots before voting begins, the protocol > ensures that if any equivocation occurred soon enough to > matter, all honest nodes will have detected it and will refuse > to vote for any EB from an RB slot where equivocation was > detected." Previously the 3·Δhdr `EquivocationCheck` pipeline phase was just a waiting state with no detection logic — both linear and con-rs had the timing window but neither implemented the actual abstention rule. Item 15 of the diff plan was misclassified as locus-only. This adds: - `ParsedHeaderInfo.issuer: Vec<u8>` — producer identity (Shelley+ issuer_vkey in net-rs; NodeId bytes in sim-rs). Empty bytes disable detection for that header (preserves legacy behavior for callers that don't populate it yet). - `PraosState.header_hashes_by_slot_issuer` — per (slot, issuer) pair, the set of distinct RB header hashes observed. Fed from `on_block_received` and `register_self_produced` on every parsed-header arrival. - `PraosState.equivocating_rb_slots` — slots flagged because the same issuer signed multiple distinct headers there. Exposed via `is_equivocating_slot(slot)` and the public `equivocating_rb_slots` set for wrapper consumption. - `ChainTipContext.equivocating_slots` — wrapper-fed BTreeSet populated on every chain-tip refresh by cloning the PraosState set. Cheap clone — bounded by the pipeline-window size. - `NoVoteReason::EquivocatingRB` in con-rs and `EquivocatingRB` in sim-rs's `model::NoVoteReason`. - New voting predicate in `LeiosState::decide_vote`, run after the existing WrongEB / LateRBHeader checks: if `eb_slot` is in `chain_tip_ctx.equivocating_slots`, return Err(EquivocatingRB). Consumer wiring: - sim adapter: `parsed_header_from_rb` populates `issuer` from `h.id.producer` (NodeId → little-endian u64 bytes). `update_chain_tip_ctx` clones `praos.equivocating_rb_slots` into the refreshed ChainTipContext. - net-rs: `parse_header` in `consensus/praos/mod.rs` populates `issuer` from `i.issuer_vkey.to_vec()`. `refresh_chain_tip_ctx` in `consensus/mod.rs` clones the equivocation set from praos. Added `PraosConsensus::equivocating_rb_slots()` accessor. Two valid RBs from different producers winning the same slot via VRF is a legitimate Praos fork — different (slot, issuer) pairs — not an equivocation. Praos's existing fork-resolution handles that case; this detection is specifically for same-producer signing of multiple headers at one slot. Tests: - con-rs: 5 new equivocation-tracking tests in praos.rs (same-issuer flags, different-issuer doesn't, empty-issuer disabled, duplicate-hash no-op, self-produced equivocation), 1 new voting test in leios.rs (`no_vote_equivocating_rb_slot`). - Existing tests in both crates updated for the new `issuer` / `equivocating_slots` fields via `..Default::default()` literals. Verification: con-rs 237 tests pass (+6), sim-core 55, net-rs workspace 119. Behaviorally invisible in benign sweeps — no node equivocates in current scenarios — so the in-flight NA,0.350 sanity check (pinned to the pre-equivocation commit anyway) is unaffected. Material under adversarial / threat-model studies. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
net-cluster: add tx_rate override and honest Leios baseline sample
The cluster config gains an optional top-level `tx_rate` that, when set, overrides `[transactions] tx_rate` in every node's overlay. Lets cluster scenarios drive EB production without authoring a dedicated base config — e.g. abstention-pressure experiments that need the mempool busy enough to overflow into EBs. Plumbed through the same `control_fields` / `read_node_config_defaults` path as `rb_generation_probability`, so future UI work can read or change it dynamically. Also adds `sample-cluster-leios-baseline.toml`: 30-node `mainnet-shaped` cluster, `rb_generation_probability = 0.05` (the mainnet-realistic minimum at which Leios still has a chance), no adversarial behaviour. Useful as the apples-to-apples comparison point for any subsequent behaviour-bearing scenarios. Empirical note (not encoded in any test, just for context): smoking this baseline at `tx_rate = 1.0` doesn't currently reach quorum on its own — 100% of NoVotes are `MissingTX`, confirming the net-rs EB-tx fetch hub-spoke ceiling is the dominant failure mode at this tx rate. That's an upstream net-core issue tracked separately (`project_netrs_vs_sim_NA200`); the cluster knob just exposes it. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: retry_vote_in_window knob covers predicate + lottery retries
Adds `VotingConfig.retry_vote_in_window` (default `true`). The flag gates both in-window retry paths uniformly: - predicate failure (`WrongEB` / `LateRBHeader` / `MissingTX`): a later slot in the window may see chain-tip / mempool state that flips the predicate to success; - lottery loss (no PV seat and no NPV win): the NPV trial re-runs with a fresh per-slot VRF input, so a non-seated voter has up to `vote_window` independent chances to win. `LateEB` stays permanent regardless — `eb_seen_slot` is fixed. `true` matches the CIP-0164 reading that the voting window is the licence to vote at any in-window moment. `false` gives one decision per (voter, EB) and one NPV trial — useful for adversarial sims and for like-for-like comparison against linear_leios.rs. Surfaced as `retry-vote-in-window` in sim-rs's RawParameters. net-rs hardcodes `true` (production stays on CIP semantics). Empirical: at NA,0.350 / wfa-ls / 750n / -s 1500 the flag doesn't move the aggregate (this workload's WfaLs committee is pure PV; the NPV path is dormant). The real divergence vs linear_leios turned out to be that linear's WfaLs is a per-EB VRF lottery rather than the CIP-0164 stake-weighted Hare-quota seat allocation con-rs uses — covered separately by item 1 in the diff plan. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: preserve quorum-reached elections through pipeline expiry
`Elections::on_slot` dropped every election whose `phase_for_elapsed`
returned `None` — i.e. once `elapsed >= 3*delta_hdr + L_vote +
L_diff + dedup_window`. Quorum-reached entries went out with the
rest, which made a parent-tied cert-selection adapter unable to
assemble a cert for any parent EB older than `dedup_window` slots
past CertEligible start — even though the cert was provably valid
from accumulated votes.
A quorum-reached election is an assemblable cert; pruning it is a
correctness loss disguised as bookkeeping. Linear's pruning
("drop only when a strictly newer EB has been endorsed") keeps such
entries available until something supersedes them. Match that
semantic by keeping `quorum_reached` entries past pipeline expiry;
non-quorum entries still drop on schedule.
Diagnostic confirmation: sim-rs adapter at NA,0.350 seed 1 with
parent-tied cert selection went from 14 endorsements (the buggy
state where ~27/99 attempts hit `no_quorum` with sums of 467-477
against threshold 450 because the election had been pruned) to 28
endorsements (within seed-noise of linear's 29), closing the +21%
EB inflation gap that has tracked con-rs vs linear since the W2.6
sweep.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
shared-consensus: refresh README and CLAUDE; document behaviour plugins
README adds module entries for lottery / bitmap / fetch / mempool / production / behaviour, four flow-specific sequence diagrams (transactions, RBs, EBs, votes), an events-and-effects graph for MempoolState, and summary sections for the Behaviour and Fetch subsystems. CLAUDE picks up the new modules in the map and adds a Behaviours section pointing contributors at the subsystem README. src/behaviour/README.md is new: hook taxonomy (reactive / decision / strategy / notification), outcome enums, BehaviourHandle ownership, composition rules, registry shape (BehaviourSpec, build, swap_handle), outbound transforms, DelayQueue, shipped behaviours, and the recipe for adding a new one — all under the same determinism contract. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>