Skip to content

Leios: omnibus for production-grade EB diffusion - #2282

Merged
ch1bo merged 61 commits into
leios-prototypefrom
nfrisby/leios-issue-1071-validate-cert-asap
Oct 9, 2026
Merged

ch1bo merged 61 commits into
leios-prototypefrom
nfrisby/leios-issue-1071-validate-cert-asap

Conversation

@nfrisby

@nfrisby nfrisby commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Closes all of input-output-hk/ouroboros-leios#1074 subissues.

Also introduces committee-via-forecasting infrastructure that I suspect Issue input-output-hk/ouroboros-leios#1073 should use for validating votes.

This development lead to these two upstream PRs:


When this PR merges, we'll need to add a few small commits to downstream repos:

  • cardano-api needs d9bf166c7 - (HEAD -> nfrisby/integrate-PraosWithLeios, origin/nfrisby/integrate-PraosWithLeios) Consensus: change Dijkstra to PraosWithLeios
  • cardano-node needs addf5beb1 - (HEAD -> nfrisby/leios-issue-1074-omnibus, origin/nfrisby/leios-issue-1074-omnibus) Add ChainDB tracers for forgetting CertRBs at start-up (and its handful of predecessors)
  • ouroboros-leios needs 99179913e - (HEAD -> nfrisby/integrate-LeiosMinOfferLead, origin/nfrisby/integrate-LeiosMinOfferLead) Add appropriate LeiosMinOfferLead to devnets' config files

Another consequence of merging this PR is that the subsequent prototype will have a different codec for pre-Dijkstra eras than prior prototypes: this PR changes the prototype to (correctly) use the unchanged Praos header codecs for pre-Dijkstra and only introduce the Leios fields as of Dijkstra. Our testnet does not start in Dijkstra. So shipping this PR requires respinning the testnet's chain, and requires all nodes to update their header codecs in the same way.


Overview of major changes, roughly in commit order:

  • Issue Validate certificate in a CertRB _before_ ChainSel begins selecting it input-output-hk/ouroboros-leios#1071:

    • Replace data Praos with data BasePraos and type Praos = BasePraos PextNone to make room for type PraosWithLeios = BasePraos PextLeios. Integrate/dispatch to the new upstream HeaderBody types accordingly.
    • We add the predecessor's slotno and its LedgerView to BlockToAdd, so we can know which LeiosPoint the block certifies and which Leios committee to use when validating a CertRB's cert in ChainSel's first pass (before we have its predecessor's ledger state).
    • That slotno is always available: easy in the forge, but because of the ChainSync->BlockFetch flow we needed to add the MatchedBlock annotation. In order to convey that info from ChainSync to BlockFetch, we added hwtPredecessorSlot and hwtLedgerViewOfPredecessor (which makes HeaderWithTime a misnomer, but I left that as a TODO to fix, for now).
    • ChainSel now only calls setTentativeHeader (ie "block diffusion pipelining") if the block it's trying to select just arrived (ie arrived "in order" and after the EB it certifies, if any) (see TentativeHeaderPermission)
    • And that's all enough to let ChainSel validate a CertRB's on arrival, before it's attempting to select that CertRB. (see precheckLeiosCert). Because the corresponding LedgerView is immediately available (see above), that certificate can always be validated.
  • Issue Bound bookkeeping of EB offers that arrive via ChainSync input-output-hk/ouroboros-leios#1069

    • Fetching an EB is justified by elections and certifications, never by a peer's offer. Offers only say which peers can serve what.
    • Nothing lists an EB on the strength of a claim we have not yet verified; the candidates we pursue are bounded by elections rather than by peers (thanks to Issue 1071).
  • Issue Enable the recovery path in LeiosFetch logic input-output-hk/ouroboros-leios#1070

    • ValidClaims records which EB each claim announced, so isCertifiedEb can answer whether an announcement is certified, and
    • focusCertifiedEb moves a verified certificate's election onto the EB it names — which is what lists that second body to fetch, if we had been mislead by an equivocation. We also disconnect from peers that claim committee equivocation; those claims arrive over ChainSync rather than LeiosNotify.
    • This work introduced the node-vs-environment Leios test harness, which invokes the actual initNodeKernel. Some of the subsequent enrich it in order to also test their cases while increasing the node-under-test's fidelity.
    • For Issue 1071, we needed to refine its behavior during node startup.
      • See the valid_claims_startup.md.
      • At startup a CertRB already in the VolatileDB never passes through chainSelAddBlock again, so nothing hands its certificate a ledger view and its claim stays unverified for the run — which closes the Recovery Path for the EB it certifies. We now forget every CertRB not on the selection after initial chain selection: unknown but still stored, so re-adding it writes nothing and re-admits it, and the ordinary path then supplies the view.
  • Issue Require announcements precede offers and tighten LeiosFetch logic input-output-hk/ouroboros-leios#1082

    • Offers become PeerOffer, with the body and the closure offered independently. A peer may offer the parts of only an EB it announced to us, at most once per part, and never for an EB older than our immutable tip; each violation costs it the connection.
    • This node holds itself to the same rules — it offers only what it actually sent announcements for and LeiosMinOfferLead sets how much younger than our immutable tip an EB must be to be relayed. (The Recovery Path catches, after the fact, any offers that these rules prevent from happening over LeiosNotify.)
  • Issue BUG: the prototype relays EBs that lied about their tx's sizes input-output-hk/ouroboros-leios#1115

    • If an announcement gives the wrong size for an EB, then its closure is no longer called complete. Nobody is disconnected — the issuer isn't a peer and the relayer may be honest.
    • Because the node never concludes it has the EB's closure, it never relays the closure and it never votes for the evil announcement.
  • Issue Disconnect if a peer requests an EB body or EB closure that we don't have input-output-hk/ouroboros-leios#1083 and Issue Abort a LeiosFetch request processing if it turns out to be too many bytes input-output-hk/ouroboros-leios#1124

    • The LeiosFetch server disconnects on requests it cannot answer instead of replying with whatever it found.
    • batchRetrieveTxs takes a byte budget, so the LeiosFetch server aborts if it realizes a request is for more bytes than an honest client would put in a single request.
    • The honest client gained a pipelining depth of 150.
    • maxLeiosEbBytesSize/maxLeiosTxsRequestBytesSize were corrected to be the design ceilings,
      dropping maxTxsPerEb from 71,428 to 14,979.
  • Issue Integrate LeiosFetch with Genesis's ChainSync Jumping (CSJ) input-output-hk/ouroboros-leios#1072

    • Record offers implied by a CSJ Jumper accepting a jump via the new CSJ.leiosJumpAcceptedCallback config field.
    • Added maxAcceptedJumpSlot field to Leios.PeerVars to limit this processing to just the headers that weren't also part of previous jumps.

@nfrisby nfrisby self-assigned this Sep 9, 2026
@nfrisby nfrisby added the Leios label Sep 9, 2026
@nfrisby

nfrisby commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

It's in draft because of unacceptable commit names and no tests.

@nfrisby nfrisby moved this to 🏗 In progress in Consensus Team Backlog Sep 9, 2026

@nfrisby nfrisby left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review pass

-- 'Nothing' if the protocol parameter does not yet exist on the current era.
getCurrentThreshold :: LedgerState blk EmptyMK -> Maybe Weight

-- | The committee and quorum threshold according to a /forecast/ ledger view

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the two methods above this one could be retired, since we can compose this new one with protocolLedgerView?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, retired; see the thread below.

Comment thread ouroboros-consensus/src/ouroboros-consensus/LeiosDemoTypes.hs Outdated
, KES.Signable (KES c) (PraosCodec.HeaderBody c)
, VRF.Signable (VRF c) InputVRF
) =>
PraosCrypto c

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't parameterize this over pext because:

  • I know it occurs in very many places.
  • We can use KnownPraosExtension pext as a small workaround to recover the Signable (KES c) HeaderBody dict for some pext in the few places we need it.

reject cert why =
pure $ Left $ LeiosCertificateForecastRejected cert predSlot why

-- | Record the invalid block in the given map and change its fingerprint

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was merely floated out of its where clause so it could be called from a second location.

LedgerState blk mk ->
AF.AnchoredFragment (Header blk) ->
AF.AnchoredFragment (HeaderWithTime blk)
mkHeadersWithTime cfg lst frag =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO is this impl too allocation-heavy?

@nfrisby
nfrisby force-pushed the nfrisby/leios-issue-1071-validate-cert-asap branch from 88a21b6 to 7e7cb83 Compare September 10, 2026 00:59
@nfrisby nfrisby moved this from 🏗 In progress to 👀 In review in Consensus Team Backlog Sep 10, 2026
@nfrisby
nfrisby force-pushed the nfrisby/leios-issue-1071-validate-cert-asap branch from 7e7cb83 to eecca00 Compare September 10, 2026 01:31
@nfrisby

nfrisby commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

My latest pushes are just trying to fix all the CI failures. I'm only compiling with 9.12 -Werror locally.

@nfrisby
nfrisby force-pushed the nfrisby/leios-issue-1071-validate-cert-asap branch from ed2c7a8 to 215a564 Compare September 18, 2026 22:17
@nfrisby
nfrisby force-pushed the nfrisby/leios-issue-1071-validate-cert-asap branch from 215a564 to 7e82b51 Compare September 29, 2026 18:46
@nfrisby nfrisby changed the title Leios: validate CertRB's cert upon arrival if claim isn't already known Leios: omnibus for production-grade EB diffusion Sep 29, 2026
@nfrisby
nfrisby force-pushed the nfrisby/leios-issue-1071-validate-cert-asap branch 2 times, most recently from 1191375 to 4365b9f Compare September 29, 2026 23:57
@nfrisby
nfrisby marked this pull request as ready for review September 29, 2026 23:57
@nfrisby

nfrisby commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

There are a couple testing modules where I haven't yet combed through the comments Claude wrote, but otherwise, I've reviewed every line (and trimmed down a lot of comment, pushed back on extra complexity, etc).

@nfrisby

nfrisby commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

For what it's worth, Claude breaks down the diff's line counts as in the table below.

My highlights:

  • 38% of the added lines are are the new node-vs-environment test, which I'm delighted to have now.
  • LeiosDB is only touched because that's currently where the detection logic is for when we have acquired an EB's entire closure---if that moved into the LeiosFetch decision logic (... which I think @ch1bo's branch might be doing?), then this PR wouldn't alter the LeiosDB at all. (edit: lookupTrustedEbClosure is just a rename to emphasize that it is called by code that trusts this EB and its closure (and their size!) was already validated before this function is called.)
  ┌──────────────────────────────────────────────────┬──────┬─────┬───────┬───────┐
  │                    component                     │  +   │  −  │ churn │ files │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ Praos / BasePraos protocol                       │ 1016 │ 711 │  1727 │    10 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ ChainSel + ChainDB + LedgerDB                    │ 1047 │ 345 │  1392 │    24 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ node-vs-env tests (RecoveryPath)                 │ 1288 │   0 │  1288 │     1 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ LeiosTestBlock (test lib)                        │ 1233 │   0 │  1233 │     1 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ node-vs-env harness (Environment, NodeUnderTest) │ 1043 │   0 │  1043 │     2 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ Recovery Path / ValidClaims                      │  979 │   0 │   979 │     4 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ LeiosFetch decision logic (LeiosDemoLogic)       │  620 │ 148 │   768 │     5 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ Shelley/Cardano ledger integration               │  453 │ 275 │   728 │    29 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ Leios types (LeiosDemoTypes)                     │  588 │  54 │   642 │     1 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ LeiosDb                                          │  431 │ 131 │   562 │     5 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ Node wiring (NodeKernel/NodeToNode/Node)         │  404 │  68 │   472 │     4 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ docs + changelog + cabal + nix                   │  238 │  18 │   256 │     9 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ other                                            │  106 │  22 │   128 │     7 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ other tests                                      │   94 │  26 │   120 │     9 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ LeiosFetch protocol (client/server)              │   61 │   9 │    70 │     1 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ ChainSync + CSJ                                  │   49 │  10 │    59 │     6 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ LeiosNotify protocol (client/server)             │   18 │   1 │    19 │     1 │
  ├──────────────────────────────────────────────────┼──────┼─────┼───────┼───────┤
  │ Leios voting                                     │   13 │   3 │    16 │     2 │
  └──────────────────────────────────────────────────┴──────┴─────┴───────┴───────┘

  The headline: tests and test infrastructure are 3,684 added lines — RecoveryPath.hs, LeiosTestBlock.hs, the harness, plus 120 in other
  tests. That's 38% of the additions and essentially all of it new files, which is why the number looks alarming without being alarming.

  The next largest, Praos/BasePraos at 1,727, is the most deletion-heavy bucket and is mostly not new code: Praos/Header.hs (−338) and
  Praos/VRF.hs (−139) are deleted outright, having moved upstream, so 477 of the 1,821 deletions are that one migration.

  ChainSel + ChainDB + LedgerDB at 1,392 over 24 files is the diffuse one — the Predecessor plumbing reaching through ChainSel, Forker,
  HeaderValidation, the ChainDB API and the LedgerDB.

@nfrisby

nfrisby commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Just now added the commit Config: allow default leiosMinOfferLead to be overriden
0a211e1.

I anticipated the leiosMinOfferLead value needing to be tweaked on devnets while implementing the behavior, but then I was still mystified why my proto-devnet nodes weren't sending any offers. Thankfully, Claude connected the dots between their absence and the comments quickly. New insight = the restriction bites even harder than anticipated at the Origin of a new chain. The fix was to add the ouroboros-leios commit that overrides the config on the proto-devnet with something tolerable: 30 seconds. (See downstream commit mentioned in PR description.)

@nfrisby

nfrisby commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

I added this warning to the PR description:

Another consequence of merging this PR is that the subsequent prototype will have a different codec for pre-Dijkstra eras than prior prototypes: this PR changes the prototype to (correctly) use the unchanged Praos header codecs for pre-Dijkstra and only introduce the Leios fields as of Dijkstra. Our testnet does not start in Dijkstra. So shipping this PR requires respinning the testnet's chain, and requires all nodes to update their header codecs in the same way.

(edit: I kludged the local build of cardano-ledger so that I could run my local build of this PR against the testnet. It's currently syncing.)

nfrisby and others added 23 commits October 9, 2026 11:03
The LeiosFetch state was handling this case well, but its interactions with the
LeiosDB/ChainSel state are not yet.
This fixes the bug where cdbAcquiredEbs wasn't necessary storing the youngest
slot for some EB.

It also ensures AcquiredEb and AcquiredEbTxs are emitted for a LeiosPoint even
if its EB was already in the LeiosDb when this LeiosPoint was first announced.
(This commit message is written as if comparing/contrasting with origin/main
instead of the preceding commits on this feature branch.)

This incurs some duplication, but it's worth it.

- The duplication is not overly burdensome to compensate for by factoring the
  Praos impl somewhat so its types and functions can be reused as much as
  possible. For Leios, at least, that's easy since Leios only adds a few
  independent fields to the header semantics.

- The risk of the two accidentally drifting apart is low: Praos has been running
  on mainnet for years, so any incompatible changes to it need a great deal of
  scrutiny. (And any changes to it at all need to be worth the trouble.)

- Duplicating instances between Praos and PraosWithLeios instead of having them
  share some instances (ala a `BasePraos leiosFlag` protocol type) has a couple
  benefits. First, code is either monomorphic or reuses the existing
  parameterizations over `proto`, which is already ubiquitous. Second, it avoids
  _implicit_ reuse of today's Praos's rules for Leios, which makes _accidental_
  reuse less likely---compare to type class method defaults.

We do _not_ duplicate more than we need to, though. In particular, many of
Praos's data types and some of its classes gain a `proto` parameter, which is
used so that the single data type definition can be reused for Praos with and
without Leios. The benefit is that there is just one constructor/field per Praos
concept, regardless of whether Leios is enabled.

This is not a fully modular design: subsequent additional extensions will also
need to add to these same definitions (eg adding fields for Ouroboros
Phalanx). That is intentional. This code does not need to be classically
extensible, since there is, unfortunately, no such thing as an extensible
security proof. Our protocol changes are well studied before implemented, and
have never happened concurrently.

In other words: it's a very important benefit that there is _one definition_ to
look at in order to see everything all of the Praos extensions _cumulatively_
do. The type-level DSL used to isolate extension components is simple and
legible; see the `EitherLeiosF` data family.
SQLite rejects a body written for a point that writeEbPoint never inserted.
InMemory instead registered the point silently, so InMemory-based tests could
not catch a missing registration.

Today processLeiosBlock always registers the point first, so nothing hits this
and the tests still pass. The guard matters once this TODO lands:

    -- TODO remove the 'writeEbPoint' call below once no important node's
    -- VolatileDB still holds an announcing RB that it fetched without this
    -- patch. [...]
    pointWritten <- writeEbPoint writer point ebBytesSize'

After that, any body reaching processLeiosBlock without a centrally-processed
announcement would fail on SQLite. With this change InMemory fails the same way,
so the tests surface it instead of passing:

```
$> cabal test ouroboros-consensus:consensus-test --test-options='--pattern "/LeiosDemoLogic.Invariants/"'
Test suite consensus-test: RUNNING...
ouroboros-consensus
  Leios
    LeiosDemoLogic.Invariants
      curated sequences
        forge purges a body it already holds (offered first):                                      FAIL
          ouroboros-consensus/test/consensus-test/Test/LeiosDemoLogic/Invariants.hs:136:
          expected: Right []
           but got: Left "exception on Forge [0,1] 12: LeiosDbWriteException {writeJob = \"WriteEbBody (12, 47f6c6404a56ea658d0b40e31c47eba73beaad3d3d71a5e61fc0e90ea48d6d99)\", submittedFrom = \"CallStack (from HasCallStack):\\n  resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\\n  writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common\\n  a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic\", writeFailure = LeiosDbException {errorMessage = \"writeEbBody: point not registered (programmer error)\", callStack = \"CallStack (from HasCallStack):\\n  throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\"}}"
          Use -p '/LeiosDemoLogic.Invariants/&&/forge purges a body it already holds (offered first)/' to rerun this test only.
        an offer of a self-forged EB is not re-fetched (forged first):                             FAIL
          ouroboros-consensus/test/consensus-test/Test/LeiosDemoLogic/Invariants.hs:138:
          expected: Right []
           but got: Left "exception on Forge [0,1] 12: LeiosDbWriteException {writeJob = \"WriteEbBody (12, 47f6c6404a56ea658d0b40e31c47eba73beaad3d3d71a5e61fc0e90ea48d6d99)\", submittedFrom = \"CallStack (from HasCallStack):\\n  resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\\n  writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common\\n  a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic\", writeFailure = LeiosDbException {errorMessage = \"writeEbBody: point not registered (programmer error)\", callStack = \"CallStack (from HasCallStack):\\n  throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\"}}"
          Use -p '/LeiosDemoLogic.Invariants/&&/an offer of a self-forged EB is not re-fetched (forged first)/' to rerun this test only.
      a body is claimed acquired only by a settled write:                                          OK
      acquired EB kept until its greatest slot is below the immutable tip:                         OK
      an announcement raises a forged EB's max slot (so it isn't pruned early):                    OK
      an announcement's onset is recorded (earliest kept); an offer never clobbers it:             OK
      start-up seeding marks each completed EB held, with an empty pool:                           OK
      start-up seeding: a peer's offer of a seeded EB is not re-fetched:                           OK
      a big-ledger peer has a larger, but still finite, closure budget:                            OK
      job assignment draws within the least-requested bucket, at random, respecting exclusions:    OK
      only the announcement that takes an election's focus is tracked:                             OK
      EB-hash collision (ported from #2309)
        forged, then the same EB announced at a new slot: both points registered and notified:     FAIL
          Exception: WriteEbBody (5, 47f6c6404a56ea658d0b40e31c47eba73beaad3d3d71a5e61fc0e90ea48d6d99) failed: writeEbBody: point not registered (programmer error)
          CallStack (from HasCallStack):
            throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory

          submitted from:
          CallStack (from HasCallStack):
            resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory
            writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common
            a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic

          HasCallStack backtrace:
            collectBacktraces, called at libraries/ghc-internal/src/GHC/Internal/Exception.hs:169:13 in ghc-internal:GHC.Internal.Exception
            toExceptionWithBacktrace, called at libraries/ghc-internal/src/GHC/Internal/IO.hs:260:11 in ghc-internal:GHC.Internal.IO
            throwIO, called at ./Control/Concurrent/Async.hs:78:13 in tsty-1.5.4-843ddea7:Control.Concurrent.Async

          Use -p '/LeiosDemoLogic.Invariants/&&/forged, then the same EB announced at a new slot: both points registered and notified/' to rerun this test only.
        body held, then the same EB announced at a new slot: both points registered:               OK
        both announced before the fetch: both points registered:                                   OK
        forged, then the same EB merely offered at a new slot: the offer registers nothing:        FAIL
          Exception: WriteEbBody (5, 47f6c6404a56ea658d0b40e31c47eba73beaad3d3d71a5e61fc0e90ea48d6d99) failed: writeEbBody: point not registered (programmer error)
          CallStack (from HasCallStack):
            throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory

          submitted from:
          CallStack (from HasCallStack):
            resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory
            writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common
            a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic

          HasCallStack backtrace:
            collectBacktraces, called at libraries/ghc-internal/src/GHC/Internal/Exception.hs:169:13 in ghc-internal:GHC.Internal.Exception
            toExceptionWithBacktrace, called at libraries/ghc-internal/src/GHC/Internal/IO.hs:260:11 in ghc-internal:GHC.Internal.IO
            throwIO, called at ./Control/Concurrent/Async.hs:78:13 in tsty-1.5.4-843ddea7:Control.Concurrent.Async

          Use -p '/LeiosDemoLogic.Invariants/&&/forged, then the same EB merely offered at a new slot: the offer registers nothing/' to rerun this test only.
      a certificate tracks an endorser block no announcement did:                                  OK
      an endorser block that references too many tx bytes is dropped, not fetched:                 OK
      an endorser block at exactly its bound is fetched:                                           FAIL
        ouroboros-consensus/test/consensus-test/Test/LeiosDemoLogic/Invariants.hs:476:
        expected BodyAcquired, got Just (MkEbState (SlotNo 5) SNothing NoBody)
        Use -p '/LeiosDemoLogic.Invariants/&&/an endorser block at exactly its bound is fetched/' to rerun this test only.
      a certificate leaves a body we already hold held:                                            OK
      outstanding-state invariants hold across arbitrary sequences:                                FAIL
        *** Failed! Falsified (after 4 tests and 1 shrink):
        [ArriveBody [2,3,4] 11,Announce [1,2] 11]
        Left "exception on Announce [1,2] 11: ExceptionInLinkedThread \"ThreadId [1]\" (LeiosDbWriteException {writeJob = \"WriteEbBody (11, ff3e90aa6fbe36b630f58be98da978061e94c0a998da2376f0b4abf615082391)\", submittedFrom = \"CallStack (from HasCallStack):\\n  resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\\n  writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common\\n  a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic\", writeFailure = LeiosDbException {errorMessage = \"writeEbBody: point not registered (programmer error)\", callStack = \"CallStack (from HasCallStack):\\n  throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\"}})" /= Right ()
        Use --quickcheck-replay="(SMGen 14245007439037612623 12544114665117986533,3)" to reproduce.
        Use -p '/LeiosDemoLogic.Invariants/&&/outstanding-state invariants hold across arbitrary sequences/' to rerun this test only.
      the fetch logic never requests an already-held EB body:                                      FAIL
        *** Failed! Falsified (after 4 tests and 1 shrink):
        [ArriveBody [2,3,4] 10,Disconnect]
        exception on Disconnect: ExceptionInLinkedThread "ThreadId [1]" (LeiosDbWriteException {writeJob = "WriteEbBody (10, ff3e90aa6fbe36b630f58be98da978061e94c0a998da2376f0b4abf615082391)", submittedFrom = "CallStack (from HasCallStack):\n  resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\n  writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common\n  a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic", writeFailure = LeiosDbException {errorMessage = "writeEbBody: point not registered (programmer error)", callStack = "CallStack (from HasCallStack):\n  throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory"}})
        Use --quickcheck-replay="(SMGen 10024954925442282638 16722750794533007117,3)" to reproduce.
        Use -p '/LeiosDemoLogic.Invariants/&&/the fetch logic never requests an already-held EB body/' to rerun this test only.
      a concurrent offer, announcement and body arrival keep the reverse index in sync (IOSimPOR): FAIL
        *** Failed! Falsified (after 1 test):
        Schedule control: ControlDefault
        No thread delayed
        Failure: FailureException (ExceptionInLinkedThread "RacyThreadId [2,2,1]" (LeiosDbWriteException {writeJob = "WriteEbBody (12, 47f6c6404a56ea658d0b40e31c47eba73beaad3d3d71a5e61fc0e90ea48d6d99)", submittedFrom = "CallStack (from HasCallStack):\n  resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\n  writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common\n  a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic", writeFailure = LeiosDbException {errorMessage = "writeEbBody: point not registered (programmer error)", callStack = "CallStack (from HasCallStack):\n  throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory"}}))
        Use --quickcheck-replay="(SMGen 7877884416199475454 17480834384350021,0)" to reproduce.
        Use -p '/LeiosDemoLogic.Invariants/&&/a concurrent offer, announcement and body arrival keep the reverse index in sync (IOSimPOR)/' to rerun this test only.

8 out of 22 tests failed (0.01s)
```
SQLite rejects a body written for a point that `writeEbPoint` never
inserted. InMemory instead registered the point silently, so
InMemory-based tests could not catch a missing registration.

Today `processLeiosBlock` always registers the point first, so nothing
hits this and the tests still pass. The guard matters once this TODO
lands:

```haskell
    -- TODO remove the 'writeEbPoint' call below once no important node's
    -- VolatileDB still holds an announcing RB that it fetched without this
    -- patch. [...]
    pointWritten <- writeEbPoint writer point ebBytesSize'
```

After that, any body reaching processLeiosBlock without a
centrally-processed announcement would fail on SQLite. With this change
InMemory fails the same way, so the tests surface it instead of passing:

```bash
$> cabal test ouroboros-consensus:consensus-test --test-options='--pattern "/LeiosDemoLogic.Invariants/"'
Test suite consensus-test: RUNNING...
ouroboros-consensus
  Leios
    LeiosDemoLogic.Invariants
      curated sequences
        forge purges a body it already holds (offered first):                                      FAIL
          ouroboros-consensus/test/consensus-test/Test/LeiosDemoLogic/Invariants.hs:136:
          expected: Right []
           but got: Left "exception on Forge [0,1] 12: LeiosDbWriteException {writeJob = \"WriteEbBody (12, 47f6c6404a56ea658d0b40e31c47eba73beaad3d3d71a5e61fc0e90ea48d6d99)\", submittedFrom = \"CallStack (from HasCallStack):\\n  resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\\n  writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common\\n  a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic\", writeFailure = LeiosDbException {errorMessage = \"writeEbBody: point not registered (programmer error)\", callStack = \"CallStack (from HasCallStack):\\n  throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\"}}"
          Use -p '/LeiosDemoLogic.Invariants/&&/forge purges a body it already holds (offered first)/' to rerun this test only.
        an offer of a self-forged EB is not re-fetched (forged first):                             FAIL
          ouroboros-consensus/test/consensus-test/Test/LeiosDemoLogic/Invariants.hs:138:
          expected: Right []
           but got: Left "exception on Forge [0,1] 12: LeiosDbWriteException {writeJob = \"WriteEbBody (12, 47f6c6404a56ea658d0b40e31c47eba73beaad3d3d71a5e61fc0e90ea48d6d99)\", submittedFrom = \"CallStack (from HasCallStack):\\n  resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\\n  writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common\\n  a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic\", writeFailure = LeiosDbException {errorMessage = \"writeEbBody: point not registered (programmer error)\", callStack = \"CallStack (from HasCallStack):\\n  throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\"}}"
          Use -p '/LeiosDemoLogic.Invariants/&&/an offer of a self-forged EB is not re-fetched (forged first)/' to rerun this test only.
      a body is claimed acquired only by a settled write:                                          OK
      acquired EB kept until its greatest slot is below the immutable tip:                         OK
      an announcement raises a forged EB's max slot (so it isn't pruned early):                    OK
      an announcement's onset is recorded (earliest kept); an offer never clobbers it:             OK
      start-up seeding marks each completed EB held, with an empty pool:                           OK
      start-up seeding: a peer's offer of a seeded EB is not re-fetched:                           OK
      a big-ledger peer has a larger, but still finite, closure budget:                            OK
      job assignment draws within the least-requested bucket, at random, respecting exclusions:    OK
      only the announcement that takes an election's focus is tracked:                             OK
      EB-hash collision (ported from #2309)
        forged, then the same EB announced at a new slot: both points registered and notified:     FAIL
          Exception: WriteEbBody (5, 47f6c6404a56ea658d0b40e31c47eba73beaad3d3d71a5e61fc0e90ea48d6d99) failed: writeEbBody: point not registered (programmer error)
          CallStack (from HasCallStack):
            throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory

          submitted from:
          CallStack (from HasCallStack):
            resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory
            writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common
            a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic

          HasCallStack backtrace:
            collectBacktraces, called at libraries/ghc-internal/src/GHC/Internal/Exception.hs:169:13 in ghc-internal:GHC.Internal.Exception
            toExceptionWithBacktrace, called at libraries/ghc-internal/src/GHC/Internal/IO.hs:260:11 in ghc-internal:GHC.Internal.IO
            throwIO, called at ./Control/Concurrent/Async.hs:78:13 in tsty-1.5.4-843ddea7:Control.Concurrent.Async

          Use -p '/LeiosDemoLogic.Invariants/&&/forged, then the same EB announced at a new slot: both points registered and notified/' to rerun this test only.
        body held, then the same EB announced at a new slot: both points registered:               OK
        both announced before the fetch: both points registered:                                   OK
        forged, then the same EB merely offered at a new slot: the offer registers nothing:        FAIL
          Exception: WriteEbBody (5, 47f6c6404a56ea658d0b40e31c47eba73beaad3d3d71a5e61fc0e90ea48d6d99) failed: writeEbBody: point not registered (programmer error)
          CallStack (from HasCallStack):
            throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory

          submitted from:
          CallStack (from HasCallStack):
            resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory
            writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common
            a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic

          HasCallStack backtrace:
            collectBacktraces, called at libraries/ghc-internal/src/GHC/Internal/Exception.hs:169:13 in ghc-internal:GHC.Internal.Exception
            toExceptionWithBacktrace, called at libraries/ghc-internal/src/GHC/Internal/IO.hs:260:11 in ghc-internal:GHC.Internal.IO
            throwIO, called at ./Control/Concurrent/Async.hs:78:13 in tsty-1.5.4-843ddea7:Control.Concurrent.Async

          Use -p '/LeiosDemoLogic.Invariants/&&/forged, then the same EB merely offered at a new slot: the offer registers nothing/' to rerun this test only.
      a certificate tracks an endorser block no announcement did:                                  OK
      an endorser block that references too many tx bytes is dropped, not fetched:                 OK
      an endorser block at exactly its bound is fetched:                                           FAIL
        ouroboros-consensus/test/consensus-test/Test/LeiosDemoLogic/Invariants.hs:476:
        expected BodyAcquired, got Just (MkEbState (SlotNo 5) SNothing NoBody)
        Use -p '/LeiosDemoLogic.Invariants/&&/an endorser block at exactly its bound is fetched/' to rerun this test only.
      a certificate leaves a body we already hold held:                                            OK
      outstanding-state invariants hold across arbitrary sequences:                                FAIL
        *** Failed! Falsified (after 4 tests and 1 shrink):
        [ArriveBody [2,3,4] 11,Announce [1,2] 11]
        Left "exception on Announce [1,2] 11: ExceptionInLinkedThread \"ThreadId [1]\" (LeiosDbWriteException {writeJob = \"WriteEbBody (11, ff3e90aa6fbe36b630f58be98da978061e94c0a998da2376f0b4abf615082391)\", submittedFrom = \"CallStack (from HasCallStack):\\n  resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\\n  writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common\\n  a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic\", writeFailure = LeiosDbException {errorMessage = \"writeEbBody: point not registered (programmer error)\", callStack = \"CallStack (from HasCallStack):\\n  throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\"}})" /= Right ()
        Use --quickcheck-replay="(SMGen 14245007439037612623 12544114665117986533,3)" to reproduce.
        Use -p '/LeiosDemoLogic.Invariants/&&/outstanding-state invariants hold across arbitrary sequences/' to rerun this test only.
      the fetch logic never requests an already-held EB body:                                      FAIL
        *** Failed! Falsified (after 4 tests and 1 shrink):
        [ArriveBody [2,3,4] 10,Disconnect]
        exception on Disconnect: ExceptionInLinkedThread "ThreadId [1]" (LeiosDbWriteException {writeJob = "WriteEbBody (10, ff3e90aa6fbe36b630f58be98da978061e94c0a998da2376f0b4abf615082391)", submittedFrom = "CallStack (from HasCallStack):\n  resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\n  writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common\n  a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic", writeFailure = LeiosDbException {errorMessage = "writeEbBody: point not registered (programmer error)", callStack = "CallStack (from HasCallStack):\n  throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory"}})
        Use --quickcheck-replay="(SMGen 10024954925442282638 16722750794533007117,3)" to reproduce.
        Use -p '/LeiosDemoLogic.Invariants/&&/the fetch logic never requests an already-held EB body/' to rerun this test only.
      a concurrent offer, announcement and body arrival keep the reverse index in sync (IOSimPOR): FAIL
        *** Failed! Falsified (after 1 test):
        Schedule control: ControlDefault
        No thread delayed
        Failure: FailureException (ExceptionInLinkedThread "RacyThreadId [2,2,1]" (LeiosDbWriteException {writeJob = "WriteEbBody (12, 47f6c6404a56ea658d0b40e31c47eba73beaad3d3d71a5e61fc0e90ea48d6d99)", submittedFrom = "CallStack (from HasCallStack):\n  resolved, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:197:11 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory\n  writeEbBody, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Common.hs:135:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.Common\n  a use of `writeEbBody', called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoLogic.hs:1112:22 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoLogic", writeFailure = LeiosDbException {errorMessage = "writeEbBody: point not registered (programmer error)", callStack = "CallStack (from HasCallStack):\n  throwLeiosDbException, called at ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/InMemory.hs:297:5 in ouroboros-consensus-3.0.1.0-inplace:LeiosDemoDb.InMemory"}}))
        Use --quickcheck-replay="(SMGen 7877884416199475454 17480834384350021,0)" to reproduce.
        Use -p '/LeiosDemoLogic.Invariants/&&/a concurrent offer, announcement and body arrival keep the reverse index in sync (IOSimPOR)/' to rerun this test only.

8 out of 22 tests failed (0.01s)
```
-Wno-x-partial only exists from GHC 9.8 and foldl' is only in the Prelude
from GHC 9.10.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
New declarations were inserted between existing haddocks and what they
documented, so the comments attached to the wrong thing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
HasLeiosVoting keeps only getLeiosCommitteeFromView for the committee;
leiosCommitteeOfTip reads a ledger state through ledgerViewOfTip, so the
state- and view-derived committees are one definition. LeiosMissingThreshold
becomes unreachable and goes.

Also from review: minCertificationSlot takes the ledger view instead of three
bare durations, the deployed testnet's exception to the announcement size
limit is dropped ahead of the respin, and focusElection gets its haddock back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A body arriving twice before its write settles is written twice, and each
write notifies AcquiredEb, so the server offered the body twice and the
honest client disconnected with ExnLeiosRepeatedOffer.

This is the Leios ThreadNet "late join" failure reported by Pascal Grange:
#2282 (comment)

The server now remembers per announced point what it offered.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Translating the Conway ledger view invented the Leios fields, including a
maximum EB size of 0, so a node whose selection or immutable tip was still in
Conway rejected every announcing Dijkstra header. Translate the ledger state
instead, as the hard fork combinator does when ticking across the boundary,
and forecast with Dijkstra's rules.

The new ThreadNet test forks into Dijkstra at epoch 1, which no Leios test did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rt-up

The keep-set held the selection's blocks but not its anchor, which the
VolatileDB still holds. A CertRB there was forgotten, so its successor's copy
to the ImmutableDB could not find the EB it announced to promote.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ch1bo
ch1bo force-pushed the nfrisby/leios-issue-1071-validate-cert-asap branch from 3132c9d to 36adfc6 Compare October 9, 2026 10:24
@ch1bo

ch1bo commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Rebased and had a 🧞 fix several things:

  • Repeated offers (pgrange's late-join failure): a body arriving twice before its write settles gets written twice, so the server offered it twice and the honest peer disconnected. The notify server now offers each part of an EB at most once per peer.
  • Crossing the hard fork into Dijkstra: a view forecast from a Conway state got made-up Leios values, including a max EB size of 0. Your testnet exception had been hiding this. Without it, a node whose selection or immutable tip is still in Conway rejects every announcing Dijkstra header, and the LeiosNotify path disconnects honest peers. I fixed the Conway→Dijkstra forecast to use Dijkstra's real parameters. The new ThreadNet test forks at epoch 1; it fails without the fix and passes with it.
  • Startup forgetting of certifying blocks: it also dropped the certificate of the block at the immutable tip, which breaks moving its EB into immutable storage after a restart. Fixed (one line, no dedicated test).

@ch1bo ch1bo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased onto leios-prototype and addressed my remaining comments. I have six commits on top, ready to push to this branch:

  • consensus-test: build with GHC 9.6 — this is the 9.6.7 CI failure (-Wno-x-partial), plus a foldl' import 9.6 would have hit next
  • Put orphaned haddocks back on their declarations
  • Leios: derive the committee from the ledger view only
  • LeiosNotify server: offer each part of an EB at most once per peer — fixes the "late join" failure @pgrange reported
  • Forecast Dijkstra views from Conway with Dijkstra's Leios parameters
  • ChainDB: keep the immutable tip's cert when forgetting CertRBs at start-up

Must: the cross-era forecast. Forecasting Dijkstra from a Conway state made up the Leios fields, including a max EB size of 0. The testnet kludge masked it. Without the kludge, a node whose selection or immutable tip is still in Conway rejects every announcing Dijkstra header and drops honest peers via LeiosNotify. No Leios ThreadNet crossed that boundary (they all fork at epoch 0), so there is now one that forks at epoch 1. It fails without the fix. This matters for the musashi respin and other hard-forks.

Could you also update the PR description? It still says batchRetrieveTxs takes a byte budget, but we dropped that.

TODO Downstream: LeiosMissingThreshold is gone, so the cardano-node commit needs to drop its two clauses in Tracers/ChainDB.hs.

-- ledger's value stalls the sync there. Exception granted here and
-- here only: every other use of the limit, and the genesis file
-- itself, are untouched.
maximum' = max 200000 maxEbBodySize

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped. It was also hiding a real bug: the Dijkstra view forecast from Conway had a max EB size of 0. That's fixed in "Forecast Dijkstra views from Conway with Dijkstra's Leios parameters" (see the review summary).

--
-- The ChainDB instantiates all the various type parameters of these databases
-- to conform to the unified interface we provide here.
-- | What the caller knows about the block's predecessor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, and three more of the same kind (praosSharedBlockForging, test_multipleSlotsSameHash and the LedgerSupportsProtocol class).

Comment on lines +2819 to +2829
minCertificationSlot ::
SlotLength ->
-- | Announcement period length
Milliseconds32 ->
-- | Vote period length
Milliseconds32 ->
-- | Diffusion period length
Milliseconds32 ->
-- | Slot of the announcing block
SlotNo ->
SlotNo

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. It now lives next to its only caller in Protocol.Leios and takes the PraosWithLeios ledger view. certificationGapOfPeriods stays the shared core with minCertificationGap.

Comment thread ouroboros-consensus/src/ouroboros-consensus/LeiosDemoTypes.hs
-- 'Nothing' if the protocol parameter does not yet exist on the current era.
getCurrentThreshold :: LedgerState blk EmptyMK -> Maybe Weight

-- | The committee and quorum threshold according to a /forecast/ ledger view

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, retired; see the thread below.

(shouldPersist, bodyClass, mempoolIngest, missedBoth, fills) <- MVar.modifyMVar outstandingVar $ \outstanding -> do
let tooOld = point.pointSlotNo < Leios.acquiredEbBodiesPrunedSlot outstanding
let tooOld = point.pointSlotNo < Leios.outstandingPrunedSlot outstanding
novel = not $ maybe False Leios.ebStateHasBody (Map.lookup ebHash (Leios.ebState outstanding))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should: mark the body write as in flight, so a second arrival is not novel.

novel only turns false once the async settle runs acquireEbBody. A second delivery of the same body in that window gets written again, and each writeEbBody notifies AcquiredEb. That is what made the server repeat its offer in the "late join" failure. I guarded the server side (offer each part once per peer), but the double write is still there. On SQLite it also resets missingTxCount and can re-notify AcquiredEbTxs for a complete closure (sql_init_missing_tx_count has no IS NULL guard).

The comment above ("keyed off the lock, two peers delivering the same body cannot both write it") and the one on acquireEbBody ("a concurrent offer may redundantly re-fetch") contradict each other. Which one do we want?

…wire limit

The fixture set maxEndorserBlockReferencesSize to 1 MiB, above the
512 KiB maxLeiosEbBytesSize, so guardLeiosWireLimit refused to start the
mempool and the replay test failed. 100,000 is what the testnet and
proto-devnet genesis files use, and leaves ample room for the 80 KiB of
transactions the fixture's endorser blocks hold.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ch1bo

ch1bo commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Merging despite the "late join" threadnet tests timing out. I saw various seeds succeed and they might have regressed in their overall lifetime. But also don't want to hold up integration into the prototype any longer - we need to soak it before we can cut a release again.

@ch1bo
ch1bo merged commit 301f075 into leios-prototype Oct 9, 2026
5 of 63 checks passed
@ch1bo
ch1bo deleted the nfrisby/leios-issue-1071-validate-cert-asap branch October 9, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: ✅ Done

3 participants