mirror of
https://github.com/luxfi/vm.git
synced 2026-10-30 15:13:40 +00:00
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]>
118 lines
3.8 KiB
Go
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)
|
|
}
|