Files
consensus/engine/chain/ledger.go
T
zeekayandHanzo Dev 83b495aced engine/chain: canonicalize the intermediate-ancestor walk (close v1.36 load-livelock)
pathFromTip resolved every intermediate ancestor by its OUTER proposervm-wrapper id
with no canonical fallback — the one finality path the mainnet-644 canonicalization
missed (vote placement, cert's-own-block, and the equivocation key were all
canonicalized; this walk was not). Under sustained saturation the fleet tracks
distinct wrappers of the same inner block (aliases, not forks); a verified cert
threading a peer's wrapper could not be applied by a node holding the identical
inner execution under its own wrapper, so finality livelocked on an unservable
outer id (~1174 finality-guard refusals/min, tip frozen ~2h, safety held — no fork).

Carry the signature-covered ParentCanonical through the fold Cert (it was dropped at
the engine.go seam); on an outer-id miss, collapse to a locally-held sibling wrapper
of the same inner commitment (WrapperByCanonical) before the fail-closed defer,
rebasing the child's parent link so sibling-pruning keys on the held envelope. A
genuinely unheld inner block still returns ErrAncestorNotTracked (behind-node defer
preserved). Bare chains and the normal tracked-outer path are byte-for-byte
unchanged (empty ParentCanonical ⇒ outer-id-only walk; the miss branch is additive).

Alias-collapse is sound because canonicalRep = CanonicalID() is a collision-resistant
hash of the inner execution (binds inner parent + payload + state root) that no
unverified block can spoof — every tracked block is Verify()'d before AddBlock, and
the matched parent commitment is signature-covered in the cert. It can therefore
only ever stand in a byte-identical execution, never a divergent state: no fork risk
HEAD lacked, livelock closed.

RED-verified SHIP: 0 critical / 0 high / 0 medium (2 optional-cleanup LOWs), 12
adversarial tests + the 107-test finality suite green, go build + vet clean.
Regression guards: intermediate_sibling_livelock_test.go (repro FAIL->PASS),
intermediate_sibling_adversarial_test.go, wrapper_bycanonical_adversarial_test.go.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-10 15:28:01 -07:00

523 lines
25 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// ledger.go — the FUNCTIONAL CORE of finality, decomplected from the engine.
//
// FinalityLedger is the committed, append-only prefix of finalized history — an
// immutable VALUE. Finalize is a PURE FOLD of a cert into that value:
//
// Finalize : (Ledger, Cert, DAG) -> (Ledger', Plan, error)
//
// It has no receiver, no lock, no VM, and reads the preference DAG only through the
// read-only Ancestry interface. Because the ledger is replaced as a whole value
// (never mutated in place) and there is no field-poking method, "accept =
// markFinalized(...)" is structurally unwriteable: finality advances only by folding a
// cert and replacing the ledger (applyCertLocked) — the import reconcile (SyncState) is
// the one other whole-value replacement.
//
// This mirrors the upstream linear-chain consensus's topological layer, with finality's
// trigger swapped from β-consecutive-polls to a quorum CERT: the preference tree +
// poll stays mutable and sibling-tolerant (topological.go here), while the
// committed-prefix advance — β-driven upstream — is this pure, cert-driven fold.
package chain
import (
"errors"
"fmt"
"github.com/luxfi/ids"
)
// The finality safety invariants — the vocabulary of the fold. Two are genuine
// α-of-K-cert properties the pure upstream engine does not need (its certs are β-witnessed, not
// external/attacker-chosen-Round): a SECOND valid cert for a DIFFERENT block at an
// already-decided height is equivocation evidence, never a silent second VM.Accept;
// and a cert for a block that does NOT descend from the finalized frontier conflicts
// with finalized history and is refused. The single-non-branching-chain property is
// achieved the avalanchego way — not by refusing siblings at admission, but by REORG
// (prune the losing sibling subtree when the cert selects the winner).
var (
// ErrHeightAlreadyFinalized: a DIFFERENT block is already finalized at the target
// height — two valid α-certs at one height across different rounds. The first
// finalizes; the second is refused and IS equivocation evidence. A genuine α-of-K
// safety property the pure upstream engine does not need.
ErrHeightAlreadyFinalized = errors.New("chain: a different block is already finalized at this height (equivocation: two finalized blocks at one height)")
// ErrNonMonotonicFinalizedHeight: a finalize at or below the frontier with a
// different block, or a cert-selected branch not CONTIGUOUS with the frontier (a
// height gap / malformed linkage). Finality only ever moves forward, one height at
// a time, along a tracked ancestry chain.
ErrNonMonotonicFinalizedHeight = errors.New("chain: finalized height must strictly increase by contiguous steps (cannot re-finalize an old/equal height, nor jump a height gap)")
// ErrConflictsWithFinalizedBranch: a cert-selected block does NOT descend from the
// finalized tip — its ancestry reaches a block at/below the finalized height that is
// NOT the finalized tip (a losing/pruned sibling branch). Under <⅓ Byzantine this
// can only happen for a branch the network did not finalize; finalizing it would
// branch finalized history, so it is refused.
ErrConflictsWithFinalizedBranch = errors.New("chain: cert-selected block does not extend the finalized frontier (it descends from a losing/pruned sibling branch)")
// ErrAncestorNotTracked: the path from the finalized tip up to the cert-selected
// block cannot be proven because an ANCESTOR on it is not in the live DAG. This is a
// DEFER, not a conflict: the node is behind and must fetch the missing ancestors,
// then re-apply. It must NEVER finalize on this error.
ErrAncestorNotTracked = errors.New("chain: cannot finalize — an ancestor between the finalized tip and the cert-selected block is not tracked (behind; fetch and retry)")
)
// AncestorNotTracked is the TYPED form of ErrAncestorNotTracked. It carries the SPECIFIC
// untracked ancestor the finalize path needs fetched, so a catch-up handler can target the
// fetch at exactly that block — the plain sentinel named the id only inside a formatted string,
// which is why the receive-side cert handler could log the defer but never issue the fetch (the
// mainnet behind-node self-heal gap). errors.Is(err, ErrAncestorNotTracked) still holds via
// Unwrap, so every existing classifier is unchanged; self-heal handlers use errors.As to recover
// Missing and issue exactly one RequestAncestors for it.
type AncestorNotTracked struct {
Missing ids.ID // the untracked ancestor whose absence blocks finalize — the fetch target
Target ids.ID // the certified block whose path to the finalized tip is blocked
}
func (e *AncestorNotTracked) Error() string {
return fmt.Sprintf("%s: ancestor %s of %s missing", ErrAncestorNotTracked.Error(), e.Missing, e.Target)
}
// Unwrap makes errors.Is(err, ErrAncestorNotTracked) hold, so the sentinel-based classifiers
// (topology.go, tryAccept, tests) are byte-for-byte unaffected by the typed carrier.
func (e *AncestorNotTracked) Unwrap() error { return ErrAncestorNotTracked }
// finalizedEntry is the CERTIFIED record at one finalized height: the canonical
// execution commitment (the authoritative finality identity) and the outer
// envelope id (transport, retained for serving/diagnostics). Equivocation and
// idempotency are decided on `canonical` ONLY — the envelope is non-authoritative.
type finalizedEntry struct {
canonical ids.ID // inner execution commitment — THE finalized identity at this height
envelope ids.ID // outer/proposervm id — transport cache key (non-authoritative)
}
// FinalityLedger is the committed, append-only prefix of finalized history — an
// immutable VALUE. It is never mutated in place; Finalize returns a NEW one.
//
// THE CERTIFIED FRONTIER vs THE RECOVERY HINT (the incident-1082814 durable rule).
// The ledger separates two notions that the pre-fix code fatally conflated:
//
// - The CERTIFIED frontier (tip/canonical/height/set + byHeight) advances ONLY
// by folding a verified quorum cert (or the bootstrap frontier-trust path).
// byHeight indexes the CANONICAL commitment finalized at each height; a second
// cert for a DIFFERENT canonical id at an already-certified height is the ONLY
// thing that is equivocation. Two outer envelopes wrapping the SAME canonical
// block are duplicates, never a fork.
// - The recovery HINT (hint/hintHeight/hasHint) is seeded from vm.LastAccepted on
// boot/import. It is NON-AUTHORITATIVE: it never registers as finalized height,
// never enters byHeight, and can NEVER trigger equivocation. It is a build
// anchor only — "where to build next" until a real cert arrives. A cert at the
// hint's height with a different canonical id simply seeds certified history;
// the wrong local guess is silently superseded by network truth.
//
// All fields are unexported and read-only after construction; the projections
// (CertifiedTip/Height/At/BuildAnchor) are the only outside view.
type FinalityLedger struct {
// Certified frontier — advanced ONLY by a verified QC fold (or bootstrap
// frontier-trust). `tip` is the OUTER envelope id of the certified head (the
// join point the ancestry walk seeks in the transport DAG); `canonical` is its
// canonical commitment.
tip ids.ID
canonical ids.ID
height uint64
set bool // false until the first CERT (or bootstrap) finalizes
byHeight map[uint64]finalizedEntry
// Recovery hint — from vm.LastAccepted. Non-authoritative build anchor only.
hint ids.ID // outer id to build on until a cert finalizes
hintHeight uint64
hasHint bool
}
// Tip is the OUTER envelope id of the certified head (ids.Empty before the first
// cert). This is the true finalized tip — backed by a QC. The recovery hint is NOT
// returned here (use BuildAnchor for the build view).
func (l FinalityLedger) Tip() ids.ID { return l.tip }
// CanonicalTip is the canonical execution commitment of the certified head
// (ids.Empty before the first cert).
func (l FinalityLedger) CanonicalTip() ids.ID { return l.canonical }
// Height returns the CERTIFIED finalized height and whether any cert has
// finalized yet. The recovery hint does NOT count: a hint-only ledger returns
// (0,false), so the finality height gate and equivocation index see no finalized
// height until a real cert exists.
func (l FinalityLedger) Height() (uint64, bool) { return l.height, l.set }
// At returns the CANONICAL commitment finalized at height, if a CERTIFIED entry
// exists there (equivocation evidence lookup). Hints are never returned.
func (l FinalityLedger) At(height uint64) (ids.ID, bool) {
e, ok := l.byHeight[height]
if !ok {
return ids.Empty, false
}
return e.canonical, true
}
// EnvelopeAt returns the outer transport id finalized at height (for serving /
// diagnostics), if a certified entry exists there.
func (l FinalityLedger) EnvelopeAt(height uint64) (ids.ID, bool) {
e, ok := l.byHeight[height]
if !ok {
return ids.Empty, false
}
return e.envelope, true
}
// BuildAnchor returns the outer id the VM should build/prefer on, and whether any
// anchor exists. It is the HIGHER of {certified tip, recovery hint}: the certified
// tip normally, but a forward recovery hint (vm.LastAccepted above the certified
// height — e.g. a state-sync import) wins so the VM builds where the node actually
// has state. This is a BUILD concern (transport), strictly decoupled from the
// finality Height() — advancing the build anchor past the certified tip touches no
// finality decision (a hint can never finalize), so it affects only liveness.
func (l FinalityLedger) BuildAnchor() (ids.ID, bool) {
switch {
case l.set && l.hasHint:
if l.hintHeight > l.height {
return l.hint, true
}
return l.tip, true
case l.set:
return l.tip, true
case l.hasHint:
return l.hint, true
default:
return ids.Empty, false
}
}
// Ancestry is the READ-ONLY view of the preference DAG the fold needs. The
// preference layer (topological.go) implements it over the live block tree.
// Finalize NEVER mutates the DAG — it reads ancestry to prove the certified path and
// to collect the losing-sibling subtrees to prune.
type Ancestry interface {
// Parent returns id's OUTER parent, id's OWN height, id's CANONICAL execution
// commitment, and the CANONICAL commitment of id's PARENT; ok is false if id is
// untracked. The canonical id is what the per-height equivocation index records for
// an intermediate catch-up-path block. parentCanonical is carried so the ancestry
// walk can collapse the NEXT sibling wrapper by inner identity (see WrapperByCanonical).
Parent(id ids.ID) (parent ids.ID, height uint64, canonical, parentCanonical ids.ID, ok bool)
// Children returns the ids of every tracked block whose parent is id.
Children(id ids.ID) []ids.ID
// WrapperByCanonical resolves an inner execution commitment at a height to ANY
// locally-tracked OUTER wrapper of it (any alias). The ancestry walk uses it to stand
// a local wrapper in for the exact outer envelope a cert named — collapsing sibling
// wrappers (aliases, not forks) the way every other finality path does. It NEVER
// invents an execution the node does not hold: a miss (ok=false) preserves the
// fail-closed behind-node defer (ErrAncestorNotTracked, fetch and retry).
WrapperByCanonical(canonical ids.ID, height uint64) (id ids.ID, ok bool)
}
// Cert is the minimal finality subject — the block a quorum certificate selects,
// decoupled from the wire VerifiedQuorumCert. Finality is "fold a Cert into the
// ledger".
//
// Block/Parent are the OUTER transport ids (used to walk the transport DAG and as
// the VM accept target); Canonical is the inner execution commitment — the
// AUTHORITATIVE finality identity the fold keys equivocation/idempotency on. For a
// non-wrapped block Canonical == Block. Parent is ids.Empty only for the genesis /
// first finalize.
//
// ParentCanonical is the inner commitment of Parent (from the signed
// VotePosition.ParentCanonicalID). It lets the ancestry walk collapse a
// canonical-equivalent SIBLING WRAPPER in place of the exact outer envelope Parent
// names — the fix for the intermediate-ancestor livelock (a cert whose ancestry
// threads a wrapper the node holds under a DIFFERENT envelope must still finalize).
// Empty on a bare (non-wrapped) chain or the genesis finalize; the walk then stays
// outer-id only, byte-for-byte as before.
type Cert struct {
Block ids.ID
Parent ids.ID
ParentCanonical ids.ID
Height uint64
Canonical ids.ID
}
// Plan is what Finalize decides and the engine applies to the VM and DAG. It mirrors
// avalanchego topological.go's accept/reject split:
//
// - Accept: the path from the OLD finalized tip up to the certified block, in
// ASCENDING height order — acceptPreferredChild along a path (usually one block,
// more on a catch-up jump).
// - Reject: every block on a LOSING sibling subtree — a sibling of a path block plus
// all its descendants — rejectTransitively.
type Plan struct {
Accept []ids.ID
Reject []ids.ID
}
// Finalize is THE finality function: a pure fold of a Cert into the ledger. No
// receiver, no lock, no mutation, no VM. On ANY error the INPUT ledger is returned
// unchanged (the caller assigns nothing). On success it returns the advanced ledger
// (a fresh value) and the plan the engine applies.
//
// It enforces:
//
// (a) ONE CANONICAL commitment finalized per height. Keyed on the inner execution
// commitment (cert.Canonical), NOT the outer envelope: the SAME canonical id
// already finalized here is an idempotent no-op regardless of which envelope the
// cert names (a duplicate alias — the incident-1082814 case); a DIFFERENT
// canonical id at an already-finalized height is equivocation →
// ErrHeightAlreadyFinalized.
// (b) the certified block must DESCEND from the finalized tip via a tracked,
// contiguous ancestry: a non-tip ancestor at/below the finalized height →
// ErrConflictsWithFinalizedBranch; an untracked ancestor → ErrAncestorNotTracked
// (DEFER, behind); a height gap / malformed linkage → ErrNonMonotonicFinalizedHeight.
func Finalize(led FinalityLedger, cert Cert, dag Ancestry) (FinalityLedger, Plan, error) {
// The AUTHORITATIVE finality identity is the canonical commitment, never the
// outer envelope. A cert that omits it (canonical == Empty) is degenerate; fall
// back to the outer id so a non-wrapped chain (outer == canonical) is unchanged.
canonical := cert.Canonical
if canonical == ids.Empty {
canonical = cert.Block
}
// (a) idempotent / equivocation at the target height — keyed on the CANONICAL
// commitment. byHeight holds CERTIFIED entries only (hints never enter it), so a
// hit here is always a prior QC-backed finalization.
if existing, ok := led.byHeight[cert.Height]; ok {
if existing.canonical == canonical {
// SAME inner block already certified here — a no-op regardless of which
// outer envelope this cert names (duplicate alias, NOT a fork).
return led, Plan{}, nil
}
// A DIFFERENT canonical commitment is already CERTIFIED at this height: two
// valid certs select different execution blocks at one height — the real fork.
return led, Plan{}, fmt.Errorf("%w: height %d already finalized canonical %s (envelope %s), refused canonical %s (envelope %s)",
ErrHeightAlreadyFinalized, cert.Height, existing.canonical, existing.envelope, canonical, cert.Block)
}
// First CERT finalize seeds certified history — no prior certified tip to
// extend or reorg. The recovery hint (if any) is non-authoritative and is simply
// superseded here: a cert at the hint's height, even for a different canonical id
// than the hint guessed, finalizes cleanly (the hint never blocks a cert).
if !led.set {
return seedLedger(cert.Block, canonical, cert.Height), Plan{Accept: []ids.ID{cert.Block}}, nil
}
// Below/at the frontier with no record for this height → stale or non-monotonic.
if cert.Height <= led.height {
return led, Plan{}, fmt.Errorf("%w: refused height %d at finalized height %d (block %s)",
ErrNonMonotonicFinalizedHeight, cert.Height, led.height, cert.Block)
}
// Walk the certified branch finalizedTip → target, proving contiguous tracked
// ancestry. A cert may certify a descendant several heights above the tip (a
// catch-up jump), so the path is 1..k blocks.
path, err := pathFromTip(led, cert, dag)
if err != nil {
return led, Plan{}, err
}
// Build the NEXT ledger by COPY (never mutate the input map) and the plan: Accept
// the path ascending, Reject every losing-sibling subtree along it. byHeight
// records the CANONICAL commitment of each step (the authoritative finality id).
next := led.clone()
var plan Plan
for _, s := range path {
plan.Reject = append(plan.Reject, losingSubtrees(s.id, s.parentID, dag)...)
next.byHeight[s.height] = finalizedEntry{canonical: s.canonical, envelope: s.id}
next.tip = s.id
next.canonical = s.canonical
next.height = s.height
plan.Accept = append(plan.Accept, s.id)
}
next.pruneBelowWindow() // keep byHeight (and the next clone) O(window), not O(chain height)
return next, plan, nil
}
// seedLedger constructs the first CERTIFIED ledger value from the seed (outer
// envelope id, canonical commitment, height). Clears any recovery hint — certified
// history now dominates the build anchor.
func seedLedger(envelope, canonical ids.ID, height uint64) FinalityLedger {
return FinalityLedger{
tip: envelope,
canonical: canonical,
height: height,
set: true,
byHeight: map[uint64]finalizedEntry{height: {canonical: canonical, envelope: envelope}},
}
}
// withHint returns a COPY of the ledger with the recovery-hint fields set to
// (envelope, height), PRESERVING any certified frontier. The hint is
// NON-AUTHORITATIVE: it sets no certified state (Height stays (0,false) until a
// cert; byHeight is untouched), so equivocation can never fire from it. It is a
// build anchor only. A hint must never wipe a QC-backed frontier — hence the copy
// rather than a fresh value.
func (l FinalityLedger) withHint(envelope ids.ID, height uint64) FinalityLedger {
next := l.clone()
next.hint = envelope
next.hintHeight = height
next.hasHint = true
return next
}
// clone returns a deep copy of the ledger (a fresh byHeight map) so the fold never
// mutates the receiver value's map. One copy per Finalize. byHeight is bounded to
// equivocationWindow entries (pruneBelowWindow), so this copy is O(window) — constant
// cost at any chain height — never O(chain height).
func (l FinalityLedger) clone() FinalityLedger {
bh := make(map[uint64]finalizedEntry, len(l.byHeight)+1)
for h, e := range l.byHeight {
bh[h] = e
}
return FinalityLedger{
tip: l.tip, canonical: l.canonical, height: l.height, set: l.set, byHeight: bh,
hint: l.hint, hintHeight: l.hintHeight, hasHint: l.hasHint,
}
}
// equivocationWindow bounds how many heights below the finalized tip the per-height
// index is retained. Equivocation is only actionable near the tip — a fork is attempted
// at or above the last finalized height; an older height is refused outright by the
// monotonic guard (cert.Height <= led.height) without consulting the index. Bounding
// byHeight to this window keeps the ledger — and therefore clone() — O(window) rather
// than O(chain height), and bounds its memory. Same "evidence is only useful near the
// tip" rationale as engine.go's slashingRetentionHeights.
const equivocationWindow = 1024
// pruneBelowWindow drops index entries older than equivocationWindow below the tip, so
// byHeight stays O(window) as the chain grows without bound. Pure: it mutates only the
// receiver's own already-cloned map (never the caller's input ledger).
func (l *FinalityLedger) pruneBelowWindow() {
if l.height < equivocationWindow {
return
}
cutoff := l.height - equivocationWindow
for h := range l.byHeight {
if h < cutoff {
delete(l.byHeight, h)
}
}
}
// step is one block on the certified path from the finalized tip to the cert target.
type step struct {
id ids.ID
height uint64
parentID ids.ID
canonical ids.ID // canonical commitment of this step (recorded in byHeight)
}
// pathFromTip returns the contiguous ancestry finalizedTip → target in ASCENDING
// height order, by walking target's parent links through the DAG. Errors distinguish
// the three non-extending cases (conflict / behind / gap). Caller guarantees
// cert.Height > led.height and led.set. The top step's canonical comes from the
// cert; intermediate steps' canonical come from the tracked DAG.
func pathFromTip(led FinalityLedger, cert Cert, dag Ancestry) ([]step, error) {
topCanonical := cert.Canonical
if topCanonical == ids.Empty {
topCanonical = cert.Block
}
steps := []step{{id: cert.Block, height: cert.Height, parentID: cert.Parent, canonical: topCanonical}}
cur := cert.Parent
childHeight := cert.Height
// parentCanonHint is the INNER commitment the NEXT ancestor must equal, carried down
// the walk so a canonical-equivalent SIBLING WRAPPER can stand in for the exact outer
// envelope the cert named (the intermediate-ancestor livelock fix; mainnet-644 sibling
// class). Seeded from the cert's ParentCanonicalID, else from the certified block's own
// recorded parent-canonical. Empty (a bare, non-wrapped chain) ⇒ the walk stays
// outer-id only, byte-for-byte as before.
parentCanonHint := cert.ParentCanonical
if parentCanonHint == ids.Empty {
if _, _, _, pc, ok := dag.Parent(cert.Block); ok {
parentCanonHint = pc
}
}
for cur != led.tip {
parent, curHeight, curCanonical, curParentCanon, ok := dag.Parent(cur)
if !ok {
// The exact outer envelope is untracked. Before the fail-closed behind-node
// DEFER, try to stand a LOCAL wrapper of the SAME inner block (a
// canonical-equivalent alias) in its place — collapsing sibling wrappers the
// way convergedWinnerAtHeightLocked and finalizeLocalAliasFromVerifiedCert
// already do everywhere else. A genuinely-unheld inner block (no wrapper at
// all) still falls through to the DEFER, so safety is unchanged.
if parentCanonHint != ids.Empty {
if localID, found := dag.WrapperByCanonical(parentCanonHint, childHeight-1); found {
// Rebase the child's parent link onto the LOCAL wrapper so BOTH the
// accept path and the reject-subtree walk (losingSubtrees) key on the
// wrapper the VM actually holds, never the unheld outer alias.
steps[len(steps)-1].parentID = localID
cur = localID
parent, curHeight, curCanonical, curParentCanon, ok = dag.Parent(cur)
}
}
if !ok {
return nil, &AncestorNotTracked{Missing: cur, Target: cert.Block}
}
}
// Heights must strictly decrease toward the tip; a parent at/above its child's
// height is malformed linkage.
if curHeight >= childHeight {
return nil, fmt.Errorf("%w: ancestor %s height %d not below child height %d",
ErrNonMonotonicFinalizedHeight, cur, curHeight, childHeight)
}
// Reaching the finalized height (or below) at a block that is not the tip →
// target descends from a branch the network did not finalize.
if curHeight <= led.height {
return nil, fmt.Errorf("%w: %s ancestry reaches %s (height %d) not finalized tip %s",
ErrConflictsWithFinalizedBranch, cert.Block, cur, curHeight, led.tip)
}
stepCanonical := curCanonical
if stepCanonical == ids.Empty {
stepCanonical = cur
}
steps = append(steps, step{id: cur, height: curHeight, parentID: parent, canonical: stepCanonical})
cur = parent
childHeight = curHeight
parentCanonHint = curParentCanon
}
// Reverse to ascending height and assert contiguity with the frontier: the lowest
// step must be exactly finalizedHeight+1 and each step exactly +1. A gap means a
// height was skipped (an honest block's height is its parent's +1, so an honest
// path always passes; a malformed cert/linkage fails).
for i, j := 0, len(steps)-1; i < j; i, j = i+1, j-1 {
steps[i], steps[j] = steps[j], steps[i]
}
for i := range steps {
want := led.height + 1 + uint64(i)
if steps[i].height != want {
return nil, fmt.Errorf("%w: path height %d at position %d, want %d (gap)",
ErrNonMonotonicFinalizedHeight, steps[i].height, i, want)
}
}
return steps, nil
}
// losingSubtrees returns every tracked block on a LOSING sibling subtree of keepID:
// the other children of parentID (siblings of keepID) plus all their descendants.
// This is avalanchego rejectTransitively's reachable set.
func losingSubtrees(keepID, parentID ids.ID, dag Ancestry) []ids.ID {
var queue []ids.ID
for _, id := range dag.Children(parentID) {
if id != keepID {
queue = append(queue, id)
}
}
if len(queue) == 0 {
return nil
}
out := make([]ids.ID, 0, len(queue))
seen := make(map[ids.ID]bool, len(queue))
for len(queue) > 0 {
id := queue[0]
queue = queue[1:]
if seen[id] {
continue
}
seen[id] = true
out = append(out, id)
for _, cid := range dag.Children(id) {
if !seen[cid] {
queue = append(queue, cid)
}
}
}
return out
}