mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
bootstrap: connect catch-up by accepted parent ID
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user