bootstrap: connect catch-up by accepted parent ID

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-21 17:45:38 -07:00
co-authored by Hanzo Dev
parent 39aabaee8c
commit b3c247e262
2 changed files with 166 additions and 8 deletions
+25 -8
View File
@@ -513,6 +513,7 @@ func (b *Bootstrapper) syncOnce(ctx context.Context) (advanced bool, outcome pas
// beacon + α-weighted-stake quorum) closes the OTHER half — a forged FRONTIER can
// never be named.
buffer := make(map[uint64][]byte)
bufferIDs := make(map[uint64]ids.ID)
var bufferBytes int
want := tipID // the next block id we require, descending from the agreed frontier
// sawCheckpoint is pre-satisfied unless an operator pinned a weak-subjectivity
@@ -522,6 +523,7 @@ func (b *Bootstrapper) syncOnce(ctx context.Context) (advanced bool, outcome pas
b.cfg.WeakSubjectivityHeight > lastH
sawCheckpoint := !checkpointActive
connected := false
var executeFrom uint64
for round := 0; round < maxDescentRounds; round++ {
if ctx.Err() != nil {
return advanced, passWorking, ctx.Err()
@@ -554,7 +556,6 @@ func (b *Bootstrapper) syncOnce(ctx context.Context) (advanced bool, outcome pas
// masquerade as an ancestor of the α-agreed frontier because its id is not on
// tipID's parent chain.
haveLowest := false
var lowestHeight uint64
var lowestParent ids.ID
cur := want
for {
@@ -564,16 +565,26 @@ func (b *Bootstrapper) syncOnce(ctx context.Context) (advanced bool, outcome pas
}
if _, dup := buffer[e.height]; !dup {
buffer[e.height] = e.raw
bufferIDs[e.height] = cur
bufferBytes += len(e.raw)
}
if cur == b.cfg.WeakSubjectivityID && e.height == b.cfg.WeakSubjectivityHeight {
sawCheckpoint = true
}
haveLowest = true
lowestHeight = e.height
lowestParent = e.parent
if e.height <= lastH+1 {
break // reached our tip's successor — connected
// Height alone does not connect two proposer-wrapper histories. A peer may
// carry the same already-accepted inner block under a different outer
// envelope, so its H+1 block names an H parent this node does not hold.
// Stopping merely because H+1 was reached makes Verify fail on that missing
// parent forever. Descend until the fetched path names an envelope that is
// actually on this node's accepted chain; overlap blocks above that anchor
// are executed below so the alternate envelopes are stored before their
// children are verified.
if e.parent == lastID || b.cfg.Chain.Accepted(ctx, e.parent) {
connected = true
executeFrom = e.height
break
}
cur = e.parent
}
@@ -583,8 +594,7 @@ func (b *Bootstrapper) syncOnce(ctx context.Context) (advanced bool, outcome pas
return advanced, passWorking, nil
}
if lowestHeight <= lastH+1 {
connected = true
if connected {
break
}
if len(buffer) >= b.cfg.MaxBuffer || bufferBytes >= b.cfg.MaxBufferBytes {
@@ -613,17 +623,24 @@ func (b *Bootstrapper) syncOnce(ctx context.Context) (advanced bool, outcome pas
// re-executes (Verify) against the already-accepted parent and finalizes on
// frontier-trust. A reject (invalid/out-of-order) STOPS the run at that block — the
// sync does not advance past an unverifiable block; the next pass re-fetches it.
for h := lastH + 1; ; h++ {
for h := executeFrom; ; h++ {
raw, ok := buffer[h]
if !ok {
break // reached the top of this contiguous run
}
wasAccepted := b.cfg.Chain.Accepted(ctx, bufferIDs[h])
if aerr := b.cfg.Chain.AcceptBootstrapBlock(ctx, raw); aerr != nil {
b.cfg.Log.Warn("bootstrap: block rejected during execute — sync paused at this height",
log.Uint64("height", h), log.Err(aerr))
break
}
advanced = true
// Replaying an overlapping proposer envelope may be necessary to store a
// parent, but it is not finalized-head progress. Count only a block that
// became accepted during this pass; otherwise a permanently rejected child
// could keep Run alive forever by re-serving already-stored overlap.
if !wasAccepted && b.cfg.Chain.Accepted(ctx, bufferIDs[h]) {
advanced = true
}
}
return advanced, passWorking, nil
}
+141
View File
@@ -209,6 +209,80 @@ func (n *testNode) AcceptBootstrapBlock(_ context.Context, b []byte) error {
return nil
}
// wrapperAliasNode models the recovery boundary where this node accepted inner
// execution block H under local proposer envelope A, while the certified network
// path reaches H+1 through a different envelope B around that same inner H. B is
// not accepted as this node's outer tip, but it must be verified/stored before its
// child can resolve B as a parent.
type wrapperAliasNode struct {
reg map[string]*tBlock
held map[ids.ID]bool
accepted map[ids.ID]bool
tipID ids.ID
height uint64
aliases int
accepts int
}
func newWrapperAliasNode(reg map[string]*tBlock, local []*tBlock) *wrapperAliasNode {
n := &wrapperAliasNode{
reg: reg,
held: make(map[ids.ID]bool, len(local)),
accepted: make(map[ids.ID]bool, len(local)),
}
for _, blk := range local {
n.held[blk.id] = true
n.accepted[blk.id] = true
n.tipID = blk.id
n.height = blk.height
}
return n
}
func (n *wrapperAliasNode) ParseBlock(_ context.Context, b []byte) (block.Block, error) {
if blk, ok := n.reg[string(b)]; ok {
return blk, nil
}
return nil, errUnknownBytes
}
func (n *wrapperAliasNode) LastAccepted(context.Context) (ids.ID, uint64, error) {
return n.tipID, n.height, nil
}
func (n *wrapperAliasNode) Accepted(_ context.Context, id ids.ID) bool {
return n.accepted[id]
}
func (n *wrapperAliasNode) AcceptBootstrapBlock(_ context.Context, b []byte) error {
blk, ok := n.reg[string(b)]
if !ok {
return errUnknownBytes
}
if n.held[blk.id] {
return nil
}
if !blk.valid || !n.held[blk.parent] {
return errOutOfOrder
}
if blk.height <= n.height {
// The inner execution at this height is already accepted. Store the
// alternate outer envelope, but do not replace the local accepted tip.
n.held[blk.id] = true
n.aliases++
return nil
}
if blk.height != n.height+1 {
return errOutOfOrder
}
n.held[blk.id] = true
n.accepted[blk.id] = true
n.tipID = blk.id
n.height = blk.height
n.accepts++
return nil
}
type bootErr string
func (e bootErr) Error() string { return string(e) }
@@ -282,6 +356,73 @@ func TestLoop_PartialNodeConverges(t *testing.T) {
}
}
// TestLoop_DescendsThroughAlternateAcceptedHeightEnvelope reproduces the live
// 16,383 -> 16,384 recovery wedge. The local node accepted execution height H
// under outer envelope A, while the certified network child H+1 names alternate
// envelope B at height H. A height-only connection stops at H+1, never fetches B,
// and retries the child's missing parent forever. The descent must connect by an
// accepted parent ID, replay B as overlap, then verify and accept H+1.
func TestLoop_DescendsThroughAlternateAcceptedHeightEnvelope(t *testing.T) {
const H = 32
shared, reg := chainOf(H, 0)
local := shared
alias := &tBlock{
id: ids.GenerateTestID(),
parent: shared[H-1].id,
height: H,
bytes: []byte("alternate-wrapper@H"),
valid: true,
}
child := &tBlock{
id: ids.GenerateTestID(),
parent: alias.id,
height: H + 1,
bytes: []byte("certified-child@H+1"),
valid: true,
}
reg[string(alias.bytes)] = alias
reg[string(child.bytes)] = child
peerChain := append(append([]*tBlock{}, shared[:H]...), alias, child)
peer := newTestPeer(peerChain)
node := newWrapperAliasNode(reg, local)
if err := runBootstrap(t, peer, node); err != nil {
t.Fatalf("alternate-wrapper recovery must converge: %v", err)
}
if !node.held[alias.id] || node.aliases != 1 {
t.Fatalf("alternate height-H envelope must be stored exactly once before its child: held=%v aliases=%d", node.held[alias.id], node.aliases)
}
if node.tipID != child.id || node.height != H+1 || node.accepts != 1 {
t.Fatalf("node did not cross the wrapper boundary: tip=%s height=%d accepts=%d", node.tipID, node.height, node.accepts)
}
}
func TestLoop_OverlapEnvelopeDoesNotMaskRejectedChildAsProgress(t *testing.T) {
const H = 32
shared, reg := chainOf(H, 0)
alias := &tBlock{
id: ids.GenerateTestID(), parent: shared[H-1].id, height: H,
bytes: []byte("alternate-wrapper-before-invalid-child"), valid: true,
}
child := &tBlock{
id: ids.GenerateTestID(), parent: alias.id, height: H + 1,
bytes: []byte("invalid-certified-child"), valid: false,
}
reg[string(alias.bytes)] = alias
reg[string(child.bytes)] = child
peerChain := append(append([]*tBlock{}, shared[:H]...), alias, child)
node := newWrapperAliasNode(reg, shared)
if err := runBootstrap(t, newTestPeer(peerChain), node); err != ErrStalled {
t.Fatalf("a rejected child after overlap replay must stall, got %v", err)
}
if node.height != H || node.accepts != 0 || node.aliases != 1 {
t.Fatalf("overlap must be stored once without masking zero head progress: height=%d accepts=%d aliases=%d", node.height, node.accepts, node.aliases)
}
}
func TestLoop_InvalidBlockHaltsSync(t *testing.T) {
const N = 50
const bad = uint64(30) // the peer serves a corrupt block at height 30