Files
zeekayandHanzo Dev 52913d973f fix(chain): P0 — accept the inner block BEFORE advancing wrapper state
BlockWrapper.Accept mutated the wrapper's caches and lastAcceptedBlock and
only then called bw.Block.Accept(ctx). If the inner VM refused or failed to
commit, the wrapper had already recorded the block as decided and moved
lastAcceptedBlock to it — a wrapper-vs-inner split that survives the process
and reads on the next boot as:

  proposervm finality index (height N) is BEHIND the inner VM tip (height M)
  ... This cannot be repaired locally

which is precisely the wedge that killed the C-Chain on mainnet luxd-0 and
luxd-2. Reject had the same ordering.

Present in BOTH wrappers: chain/block.go and components/chain/block.go.
Both now accept/reject the underlying block first and mutate only on success,
so a failed inner Accept leaves the block merely verified and the engine free
to retry or re-decide.

Tests (chain/block_accept_ordering_test.go), fault-injecting an inner Accept
failure and asserting lastAcceptedBlock, verifiedBlocks and decidedBlocks are
ALL untouched, plus a positive control that a clean Accept advances all three,
plus the Reject mirror. Proven in both directions: the test FAILS against the
original ordering and PASSES with the fix.

Reported by the owner from a review of the public mirrors; confirmed present
in this tree.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-03 21:17:30 -07:00

118 lines
3.8 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package chain
import (
"context"
"errors"
"github.com/luxfi/runtime"
)
var (
_ Block = (*BlockWrapper)(nil)
_ WithVerifyRuntime = (*BlockWrapper)(nil)
errExpectedBlockWithVerifyRuntime = errors.New("expected block.WithVerifyRuntime")
)
// BlockWrapper wraps a linear Block while adding a smart caching layer to improve
// VM performance.
type BlockWrapper struct {
Block
state *State
}
// Verify verifies the underlying block, evicts from the unverified block cache
// and if the block passes verification, adds it to [cache.verifiedBlocks].
// Note: it is guaranteed that if a block passes verification it will be added to
// consensus and eventually be decided ie. either Accept/Reject will be called
// on [bw] removing it from [verifiedBlocks].
func (bw *BlockWrapper) Verify(ctx context.Context) error {
if err := bw.Block.Verify(ctx); err != nil {
// Note: we cannot cache blocks failing verification in case
// the error is temporary and the block could become valid in
// the future.
return err
}
blkID := bw.ID()
bw.state.unverifiedBlocks.Evict(blkID)
bw.state.verifiedBlocks[blkID] = bw
return nil
}
// VerifyWithRuntime verifies the underlying block with runtime
func (bw *BlockWrapper) VerifyWithRuntime(ctx context.Context, rt *runtime.Runtime) error {
// If the embedded block supports context verification, use it
if withCtx, ok := bw.Block.(WithVerifyRuntime); ok {
shouldVerify, err := withCtx.ShouldVerifyWithRuntime(ctx)
if err != nil {
return err
}
if shouldVerify {
return withCtx.VerifyWithRuntime(ctx, rt)
}
}
// Otherwise fall back to regular Verify
return bw.Verify(ctx)
}
// ShouldVerifyWithRuntime checks if the underlying block should be verified
// with a block context. If the underlying block does not implement the
// block.WithVerifyRuntime interface, returns false without an error. Does not
// touch any block cache.
func (bw *BlockWrapper) ShouldVerifyWithRuntime(ctx context.Context) (bool, error) {
blkWithCtx, ok := bw.Block.(WithVerifyRuntime)
if !ok {
return false, nil
}
return blkWithCtx.ShouldVerifyWithRuntime(ctx)
}
// Accept accepts the underlying block, removes it from verifiedBlocks, caches it as a decided
// block, and updates the last accepted block.
func (bw *BlockWrapper) Accept(ctx context.Context) error {
// Accept the underlying block FIRST. Mutating wrapper state before the inner VM has
// durably accepted leaves lastAcceptedBlock/decidedBlocks pointing at a block the VM
// rejected or failed to commit — a wrapper-vs-inner split that later surfaces as
// "proposervm finality index is BEHIND the inner VM tip" and refuses to init the chain.
// On error the caches MUST be untouched so the block stays merely verified and the
// engine can retry or re-decide.
if err := bw.Block.Accept(ctx); err != nil {
return err
}
blkID := bw.ID()
delete(bw.state.verifiedBlocks, blkID)
bw.state.decidedBlocks.Put(blkID, bw)
bw.state.lastAcceptedBlock = bw
return nil
}
// Reject rejects the underlying block, removes it from processing blocks, and caches it as a
// decided block.
func (bw *BlockWrapper) Reject(ctx context.Context) error {
// Same ordering rule as Accept: do not cache the block as decided until the inner VM
// has actually rejected it.
if err := bw.Block.Reject(ctx); err != nil {
return err
}
blkID := bw.ID()
delete(bw.state.verifiedBlocks, blkID)
bw.state.decidedBlocks.Put(blkID, bw)
return nil
}
// OracleBlock is a block that can have multiple valid children, and one needs
// to be chosen by an oracle.
type OracleBlock interface {
Block
// Options returns the block options that may be chosen by the oracle.
Options(context.Context) ([2]Block, error)
}