mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
Two-tier finality (Nova = local majority accept, drives VM.Accept; Quasar = ⅔-by-stake export) requires that NO export surface ever names a block above the served accept tip. RED's final pass approved SHIP with three LOW items to close first. LOW-1 — DRY the accept-tip clamp into ONE place. New eth.Ethereum.ClampedQuasarHeight() = min(LastQuasarHeight, LastAccepted.Number) is the single clamp; BOTH export surfaces read it: EthAPIBackend.quasarHeight() (RPC finalized/safe) and warpBlockClient.LastQuasarHeight() (warp export gate, via the BlockClient interface). Removes the second inlined clamp copy that could drift. LastQuasarHeight() is documented as RAW (node boot re-seed only, where reconcile has already run). LOW-2 — exactly ONE admin_importChain. Deleted the eth-package AdminAPI.ImportChain (which neither reconciled the export height nor skipped <=head blocks); the plugin VM's (plugin/evm/admin_api.go) is the sole one — it runs under vmLock and calls reconcileQuasarWithAccept after each SetLastAcceptedBlockDirect. Left a NOTE so it is not re-added and reopens the rewind hole. Dropped the now-unused rlp import. LOW-3 — exclude concurrent export-height writers + prove the real boot wiring. SetLastQuasarFinalized now takes vmLock (the same lock reconcileQuasarWithAccept's admin_importChain caller holds), so the observer callback's durable Put+Store can never interleave with a re-import's Reset(0)+Delete on the same key. Fixed a boot ordering gap: the startup auto-import (importBlocksFromFile) runs AFTER initializeChain's reconcile, so a --import-chain-data rewind could leave a stale export height — added a reconcile right after the auto-import. New eth.NewTestEthereum test-support ctor. Tests (all green): eth -race adds TestClampedQuasarHeight_RealRewind (genuine SetLastAcceptedBlockDirect rewind; belt clamps finalized/safe to the rewound tip while raw is unchanged); plugin/evm adds TestBootReconcile_RealLastAccepted_ClearsStaleQuasarOnRewind (drives the REAL restored LastAcceptedBlock through belt + suspenders) and TestSetLastQuasarFinalized_LockedAgainstReconcile (concurrency: durable key <-> in-mem always consistent). gofmt/vet/ontology clean. Co-authored-by: Hanzo Dev <[email protected]>
164 lines
8.1 KiB
Go
164 lines
8.1 KiB
Go
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package eth
|
|
|
|
import (
|
|
"context"
|
|
"math/big"
|
|
"testing"
|
|
|
|
"github.com/luxfi/crypto"
|
|
"github.com/luxfi/evm/consensus/dummy"
|
|
"github.com/luxfi/evm/core"
|
|
"github.com/luxfi/evm/params"
|
|
"github.com/luxfi/evm/rpc"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/geth/core/types"
|
|
"github.com/luxfi/geth/core/vm"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestFinalizedSafeResolveToQuasarNeverAboveIt is the export-boundary gate for the two-tier
|
|
// consensus: VM.Accept now advances the local NOVA (bare-majority) tip, which is reorgable and
|
|
// MUST NOT be exported. The Ethereum `finalized`(-3)/`safe`(-4) block tags must therefore resolve
|
|
// to the EXPORT-FINAL (Quasar, ⅔-by-stake) block — NEVER a block above LastQuasarHeight() (which
|
|
// trails, and can never exceed, the accepted tip). This proves `eth_getBlockByNumber("finalized")`
|
|
// never returns a block above the Quasar height, that it is monotone (never regresses on a
|
|
// restart-seed race), and that `latest` stays on the accepted tip (a correct non-finality tag).
|
|
// buildAcceptedChain builds a REAL canonical chain (genesis + n accepted blocks) so the
|
|
// `finalized`/`safe` + export-height resolution is exercised against a real chain (LastAccepted /
|
|
// GetBlockByNumber / the clamp), not a mock. Accept tip = n.
|
|
func buildAcceptedChain(t *testing.T, n int) *core.BlockChain {
|
|
t.Helper()
|
|
key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
|
cryptoAddr := crypto.PubkeyToAddress(key.PublicKey)
|
|
addr := common.BytesToAddress(cryptoAddr[:])
|
|
bal, _ := new(big.Int).SetString("100000000000000000000000", 10)
|
|
gspec := &core.Genesis{Config: params.TestChainConfig, Alloc: types.GenesisAlloc{addr: {Balance: bal}}}
|
|
engine := dummy.NewETHFaker()
|
|
// The dummy engine requires the fee-recipient (blackhole) coinbase on generated blocks.
|
|
coinbase := common.HexToAddress("0x0100000000000000000000000000000000000000")
|
|
genDb, blocks, _, err := core.GenerateChainWithGenesis(gspec, engine, n, 10, func(i int, b *core.BlockGen) {
|
|
b.SetCoinbase(coinbase)
|
|
})
|
|
require.NoError(t, err)
|
|
chain, err := core.NewBlockChain(genDb, core.DefaultCacheConfig, gspec, engine, vm.Config{}, common.Hash{}, false, nil)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { chain.Stop() })
|
|
_, err = chain.InsertChain(blocks)
|
|
require.NoError(t, err)
|
|
for _, blk := range blocks {
|
|
require.NoError(t, chain.Accept(blk))
|
|
}
|
|
chain.DrainAcceptorQueue()
|
|
require.EqualValues(t, n, chain.LastAcceptedBlock().NumberU64())
|
|
return chain
|
|
}
|
|
|
|
func TestFinalizedSafeResolveToQuasarNeverAboveIt(t *testing.T) {
|
|
const numBlocks = 5
|
|
chain := buildAcceptedChain(t, numBlocks)
|
|
|
|
// The eth backend's blockchain is the canonical chain (genesis..numBlocks). LastAccepted /
|
|
// `latest` is the Nova tip (numBlocks); the export (Quasar) height is set separately below.
|
|
eth := &Ethereum{blockchain: chain}
|
|
backend := &EthAPIBackend{eth: eth}
|
|
ctx := context.Background()
|
|
tip := chain.CurrentBlock().Number.Uint64()
|
|
require.EqualValues(t, numBlocks, tip)
|
|
|
|
num := func(tag rpc.BlockNumber) uint64 {
|
|
blk, berr := backend.BlockByNumber(ctx, tag)
|
|
require.NoError(t, berr)
|
|
require.NotNil(t, blk, "resolution for tag %d returned nil", tag)
|
|
return blk.NumberU64()
|
|
}
|
|
hdrNum := func(tag rpc.BlockNumber) uint64 {
|
|
h, herr := backend.HeaderByNumber(ctx, tag)
|
|
require.NoError(t, herr)
|
|
require.NotNil(t, h, "header resolution for tag %d returned nil", tag)
|
|
return h.Number.Uint64()
|
|
}
|
|
|
|
// Before any export forms, `finalized`/`safe` are genesis (0) — NEVER the accepted tip.
|
|
require.Zero(t, num(rpc.FinalizedBlockNumber), "finalized must be genesis before the first export, not the accepted tip")
|
|
require.Zero(t, num(rpc.SafeBlockNumber))
|
|
require.Zero(t, hdrNum(rpc.FinalizedBlockNumber))
|
|
|
|
// Export advances to height 3 (strictly below the accepted tip 5).
|
|
eth.SetLastQuasarHeight(3)
|
|
require.EqualValues(t, 3, num(rpc.FinalizedBlockNumber))
|
|
require.EqualValues(t, 3, num(rpc.SafeBlockNumber))
|
|
require.EqualValues(t, 3, hdrNum(rpc.FinalizedBlockNumber))
|
|
require.Less(t, num(rpc.FinalizedBlockNumber), tip, "finalized MUST NOT exceed the Quasar height (which trails the accepted tip)")
|
|
|
|
// `latest`/`pending` stay on the accepted tip — they are NOT finality tags.
|
|
require.EqualValues(t, tip, num(rpc.LatestBlockNumber), "latest must remain the accepted tip, not the export height")
|
|
|
|
// Monotone: a lower export height is ignored (never regress, e.g. a boot-seed racing a cert).
|
|
eth.SetLastQuasarHeight(2)
|
|
require.EqualValues(t, 3, num(rpc.FinalizedBlockNumber), "SetLastQuasarHeight must be monotone (never regress the export frontier)")
|
|
|
|
// Export catches up to the tip.
|
|
eth.SetLastQuasarHeight(numBlocks)
|
|
require.EqualValues(t, numBlocks, num(rpc.FinalizedBlockNumber))
|
|
|
|
// BELT — the case RED flagged as false confidence: seed the export height ABOVE the accept tip
|
|
// (the rewind-stale-key scenario, where a re-import lowered the accept tip below a persisted
|
|
// quasar height) and assert finalized/safe CLAMP to the accept tip. The code now ENFORCES this;
|
|
// previously it was only incidentally true because normal op keeps quasar ≤ accept, so a
|
|
// stale-high key would have named a block above (or off) the served chain.
|
|
eth.SetLastQuasarHeight(numBlocks + 100) // stale-high, far above the accept tip
|
|
require.EqualValues(t, tip, num(rpc.FinalizedBlockNumber), "finalized MUST clamp to the accept tip when the export height is stale-high")
|
|
require.EqualValues(t, tip, num(rpc.SafeBlockNumber), "safe MUST clamp to the accept tip when the export height is stale-high")
|
|
require.EqualValues(t, tip, hdrNum(rpc.FinalizedBlockNumber))
|
|
require.LessOrEqual(t, num(rpc.FinalizedBlockNumber), tip, "finalized must NEVER exceed the accept tip, even with a stale-high export key")
|
|
// Restore for the exhaustive loop below.
|
|
eth.ResetLastQuasarHeight(numBlocks)
|
|
|
|
// THE INVARIANT, exhaustively: for EVERY export height, `finalized` resolves to exactly that
|
|
// height and NEVER above the accepted tip.
|
|
for h := uint64(0); h <= numBlocks; h++ {
|
|
e := &Ethereum{blockchain: chain}
|
|
e.SetLastQuasarHeight(h)
|
|
b := &EthAPIBackend{eth: e}
|
|
blk, berr := b.BlockByNumber(ctx, rpc.FinalizedBlockNumber)
|
|
require.NoError(t, berr)
|
|
require.EqualValues(t, h, blk.NumberU64())
|
|
require.LessOrEqual(t, blk.NumberU64(), tip, "finalized must never exceed the accepted tip")
|
|
}
|
|
}
|
|
|
|
// TestClampedQuasarHeight_RealRewind exercises the BELT against a GENUINE rewind
|
|
// (SetLastAcceptedBlockDirect — the primitive the EVM-rewind→re-import restore runbook uses): with a
|
|
// persisted export height of 8 and the accept tip rewound to 5, ClampedQuasarHeight (the ONE shared
|
|
// clamp both `finalized`/`safe` and the warp gate read) resolves to 5 — never the stale 8 above the
|
|
// served accept tip — WITHOUT relying on the VM's reconcile having run. This is the structural belt
|
|
// that the warp gate now shares (LOW-1), verified under -race at the eth level.
|
|
func TestClampedQuasarHeight_RealRewind(t *testing.T) {
|
|
chain := buildAcceptedChain(t, 10)
|
|
eth := &Ethereum{blockchain: chain}
|
|
backend := &EthAPIBackend{eth: eth}
|
|
ctx := context.Background()
|
|
|
|
eth.SetLastQuasarHeight(8) // valid, ≤ accept tip 10
|
|
require.EqualValues(t, 8, eth.ClampedQuasarHeight())
|
|
|
|
// REWIND to block 5 (SetLastAcceptedBlockDirect): accept tip drops below the export height.
|
|
require.NoError(t, chain.SetLastAcceptedBlockDirect(chain.GetBlockByNumber(5)))
|
|
require.EqualValues(t, 5, chain.LastAcceptedBlock().NumberU64())
|
|
|
|
// The RAW export height is still 8 (clearing it is the VM reconcile's job) — but the BELT clamps.
|
|
require.EqualValues(t, 8, eth.LastQuasarHeight())
|
|
require.EqualValues(t, 5, eth.ClampedQuasarHeight(), "ClampedQuasarHeight MUST clamp to the rewound accept tip")
|
|
|
|
// `finalized`/`safe` therefore resolve to block 5, never the stale block 8.
|
|
fin, err := backend.BlockByNumber(ctx, rpc.FinalizedBlockNumber)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 5, fin.NumberU64(), "finalized MUST clamp to the rewound accept tip, never the stale export height")
|
|
safe, err := backend.BlockByNumber(ctx, rpc.SafeBlockNumber)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 5, safe.NumberU64())
|
|
}
|