ouroboros-consensus
Safe HaskellNone
LanguageHaskell2010

Ouroboros.Consensus.MiniProtocol.LeiosNotify.Client.Announcements.Validation

Description

Validation of a relayed Leios announcement, which is carried as an RB Header.

The announcement can arrive out-of-order with respect to the local chain, so we run only the protocol-level header validation against the immutable tip's ledger state, forecast to the announced slot; the envelope (chain-extension) check is deliberately skipped, except for the bounds a ledger view alone determines.

For a caught-up node the immutable tip is always within the forecast horizon of a fresh announcement (by appeal to Praos Chain Growth); a LeiosNotify client sends no request until then (see awaitImmTipCanForecastNow).

Synopsis

Documentation

data AnnouncementInvalidity blk Source #

Reasons a LeiosNotify client would reject its upstream peer's announcement, regardless of other messages they had sent

Constructors

OutsideHorizon !OutsideForecastRange

The announced slot is beyond the forecast horizon from the immutable tip. This should not occur for a caught-up node (by appeal to Praos Chain Growth), so it is either a bogus far-future slot or we are falling behind; either way disconnecting is acceptable (if we are behind, then either we are unhealthy or the peer is not serving us well). We cannot simply ignore it, because there is an unbounded supply of far-future slots.

SlotBeforeImmutableTip

The announced slot is before the immutable tip (the forecast anchor), so it cannot be forecast/validated at all. This is a separate case from OutsideHorizon because forecastFor does not report a below-anchor slot as OutsideForecastRange (that only bounds the future end)---instead, a below-anchor slot violates forecastFor's precondition. For a caught-up node a slot so stale is as bogus as one in the far-future.

The ErrTooOld bound rejects any announcement older than maxAnnouncementAgeRecv (minutes), which is always far younger than the immutable tip (hours), so this case should never actually fire. It is kept as a distinct error to throw in case that branch is somehow reached, eg on an oddly-configured testnet.

HeaderInvalid !(ValidationErr (BlockProtocol blk))

The header failed the relaxed, out-of-context protocol-level validation (election proof and signature). See validateLeiosAnnouncement for which check is relaxed (the OCIN upper bound) and validateAnnouncementHeader for which are skipped (the chain-extension envelope checks).

RbHeaderEnvelopeInvalid !(OtherHeaderEnvelopeError blk)

The RbHeader failed an envelope check — chiefly, it exceeds the protocol's max header size (see validateAnnouncementHeader).

data AnnouncingHeader blk Source #

A header that carries an announcement.

Instances

Instances details
HasHeader (Header blk) ⇒ Eq (AnnouncingHeader blk) Source # 
Instance details

Defined in Ouroboros.Consensus.MiniProtocol.LeiosNotify.Client.Announcements.Validation

Methods

(==) ∷ AnnouncingHeader blk → AnnouncingHeader blk → Bool #

(/=) ∷ AnnouncingHeader blk → AnnouncingHeader blk → Bool #

Generic (AnnouncingHeader blk) Source # 
Instance details

Defined in Ouroboros.Consensus.MiniProtocol.LeiosNotify.Client.Announcements.Validation

Associated Types

type Rep (AnnouncingHeader blk) 
Instance details

Defined in Ouroboros.Consensus.MiniProtocol.LeiosNotify.Client.Announcements.Validation

type Rep (AnnouncingHeader blk) = D1 ('MetaData "AnnouncingHeader" "Ouroboros.Consensus.MiniProtocol.LeiosNotify.Client.Announcements.Validation" "ouroboros-consensus-5.0.0.0-inplace" 'False) (C1 ('MetaCons "UnsafeMkAnnouncingHeader" 'PrefixI 'True) (S1 ('MetaSel ('Just "ancHeader") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 (Header blk)) :*: (S1 ('MetaSel ('Just "ancElectionId") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 ElectionId) :*: S1 ('MetaSel ('Just "ancAnnouncement") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 LeiosAnnouncement))))

Methods

from ∷ AnnouncingHeader blk → Rep (AnnouncingHeader blk) x #

to ∷ Rep (AnnouncingHeader blk) x → AnnouncingHeader blk #

NoThunks (Header blk) ⇒ NoThunks (AnnouncingHeader blk) Source # 
Instance details

Defined in Ouroboros.Consensus.MiniProtocol.LeiosNotify.Client.Announcements.Validation

type Rep (AnnouncingHeader blk) Source # 
Instance details

Defined in Ouroboros.Consensus.MiniProtocol.LeiosNotify.Client.Announcements.Validation

type Rep (AnnouncingHeader blk) = D1 ('MetaData "AnnouncingHeader" "Ouroboros.Consensus.MiniProtocol.LeiosNotify.Client.Announcements.Validation" "ouroboros-consensus-5.0.0.0-inplace" 'False) (C1 ('MetaCons "UnsafeMkAnnouncingHeader" 'PrefixI 'True) (S1 ('MetaSel ('Just "ancHeader") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 (Header blk)) :*: (S1 ('MetaSel ('Just "ancElectionId") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 ElectionId) :*: S1 ('MetaSel ('Just "ancAnnouncement") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 LeiosAnnouncement))))

announcementValidity Source #

Arguments

∷ ∀ (m ∷ Type → Type) blk. (IOLike m, LedgerSupportsProtocol blk, LeiosProtocol (BlockProtocol blk)) 
⇒ SystemTime m 
→ SomeHeaderInFutureCheck m blk 
→ TopLevelConfig blk 
→ ExtLedgerState blk EmptyMK

The immutable tip's ledger state, for the OCIN revocation check: a revocation only counts once it cannot be rolled back.

→ AnnouncingHeader blk 
→ ExceptT (Error (AnnouncementInvalidity blk)) m OCINStaleness 

Validate a relayed announcement, after the peer's per-election counting checks have passed.

First apply ChainSync's in-future check to the announced slot's wall-clock onset (reusing the node's own SomeHeaderInFutureCheck): a far-future slot raises HeaderArrivalException (disconnecting the peer), a near-future slot blocks until the slot's onset (Ouroboros Chronos) — blocking the per-peer handler is acceptable, as a (near-)future announcement is the peer's fault.

A valid announcement may still be StaleOCIN, which the peer's dedup state counts but the node does not process.

maxAnnouncementAgeRecv ∷ NominalDiffTime Source #

Disconnect an upstream peer that relays an announcement whose slot's wall-clock onset is older than this. See ErrTooOld.

Comfortably greater than how long an honest peer relays an announcement, so that an honest peer is never disconnected on account of transmission time or clock skew.

TODO magic number; should be a config/RunNode option... or even a protocol parameter?

mkAnnouncingHeader ∷ (BlockSupportsProtocol blk, LeiosProtocol (BlockProtocol blk)) ⇒ TopLevelConfig blk → Header blk → Maybe (AnnouncingHeader blk) Source #

Interpret a header as a relayed announcement, or Nothing if it carries none (so it should not have been relayed as one).

validateAnnouncementHeader ∷ (LedgerSupportsProtocol blk, LeiosProtocol (BlockProtocol blk)) ⇒ TopLevelConfig blk → ExtLedgerState blk EmptyMK → AnnouncingHeader blk → Except (AnnouncementInvalidity blk) OCINStaleness Source #

Protocol-level validation of an announced RB Header against the immutable tip's ledger state (forecast to the header's slot). Envelope check skipped; see the module header.

The operational certificate (opcert) issue number (OCIN) governs only relay, not acceptance. We cannot soundly constrain it as strictly as genuine header validation does because we're using merely the immutable ledger state, which may be out of date. Only a non-OCIN failure — a bad election proof or signature, a slot before the immutable tip, an oversized header — rejects and disconnects. But even if an announcement is not considered invalid, it might otherwise be ignored. An announcement whose OCIN is at least our immutable tip's recorded counter is FreshOCIN: processed and relayed as usual. A lower one, or one from a key we have no recorded counter for, is StaleOCIN (see validateLeiosAnnouncement): accepted from the peer — updating that peer's dedup state, never disconnecting — but not processed, published, or relayed.

Skipping OCIN validation effectively treats every one of a pool's OCINs as the pool owner's, which is contrary to the intended purpose of OCINs, so omitting the two checks that strict header validation would make needs justification.

We do not enforce the counter's upper bound: we accept that the true OCIN may have run ahead of our imm tip's view. In fact, this logic treats distinct OCINs as the same identity. The crux is a Catch 22 if we were to distinguish them: we would have to either treat those identities independently (as distinct elections) or prioritize the greater OCIN (which seems intuitive). But an adversary can mint arbitrarily many OCINs for its own pools and abuse either choice. The former lets them multiply the Leios load per election; the latter lets them "partition" the network, one clique certifying a lower OCIN's announcement while the other ignores it. So elections are keyed merely on (slot, pool) alone (see ElectionId).

We do not promptly enforce the counter's lower bound. An OCIN increment revokes the old opcert immediately for Praos (at header validation/chain adoption), but for Leios only once the increment reaches our immutable tip. Until then our recorded counter is still the old one, so an announcement bearing it (or greater) is FreshOCIN and we process and relay it as usual. The downside of not being prompt is that a leaked hot key lets the attacker equivocate the victim's announcements until the victim notices, lands a new opcert, and waits for it to become immutable (~12 hr, <= ~36 hr) — and during that window announcements using the revoked OCIN are fresh, so they can even be voted for/part of equivocation proofs/etc. Not ideal, but tolerable.

Once the increment is immutable for us, an announcement bearing the old counter is StaleOCIN. But even then we must not reject it, only decline to process or relay it. Different honest nodes might have different immutable tips, so honest nodes can briefly disagree on whether the increment is yet immutable. An adversary who bumps a pool's OCIN and then sends a ~on-time announcement using the revoked OCIN, timed to straddle that boundary, would otherwise make "fast" nodes (increment immutable, so StaleOCIN) disconnect the "slow" honest peers (increment not yet immutable, so still FreshOCIN) that legitimately relay it — an OCIN-timed partition.

That results in a diffusion asymmetry — the slow peers relay the old-counter announcement, the fast ones do not — but it is not the certify/ignore partition the upper-bound Catch 22 warned about, because once the increment is immutable even just for us the announcement can never be voted for, hence never certified:

  • Once the increment is immutable for any honest node then, by the Praos Common Prefix property, it is in every healthy honest node's selection now and forever, so every honest node's up-to-date view records the incremented counter.
  • A vote follows selection, and selection is driven by ChainSync MsgRollForward headers. Those are not dangling: they extend the node's current chain and undergo full header validation with the exact chain-dependent OCIN bounds. Only announcements are dangling, which is precisely why only they fall back to the immutable-tip ledger view and this relaxed, report-don't-reject OCIN check.

So the old-counter RB fails MsgRollForward validation on every healthy honest node — even the slow ones still relaying its announcement — never becomes a selected tip, and is never voted for; the asymmetry never yields a certified EB, so it is no Linear Leios availability violation.

Returns the OCIN staleness.