eth,warp: gate EXPORT surfaces on the ⅔-stake Quasar tier, not the Nova accept tip (v1.36)

The two-tier consensus (v1.36) advances VM.Accept at a BARE MAJORITY (Nova), which is reorgable
and can fork under f=1 equivocation. The Ethereum `finalized`/`safe` tags and warp cross-chain
exports were still reading the accept tip — the "semantic collapse" the split exists to prevent.
Re-point every EXPORT surface onto the export-final (Quasar, ⅔-by-stake) frontier consensus
already exposes.

#1 CRITICAL — eth `finalized`/`safe` tags:
- eth.Ethereum gains a DURABLE, monotone lastQuasarHeight (SetLastQuasarHeight/LastQuasarHeight),
  pushed by consensus strictly AFTER accept. The plugin VM's SetLastQuasarFinalized persists it to
  acceptedBlockDB and reloads it on boot (readLastQuasarHeight), so the export surface never
  regresses on restart.
- api_backend HeaderByNumber/BlockByNumber resolve Finalized(-3)/Safe(-4) to GetBlockByNumber(
  LastQuasarHeight) BEFORE the IsAccepted() catch-all — NEVER a block above the Quasar height.
  `latest`/`pending` stay on the accept tip (correct — not finality tags).
- Test: TestFinalizedSafeResolveToQuasarNeverAboveIt proves finalized == the Quasar height,
  monotone, and never above the accepted tip.

#2 HIGH — warp cross-chain export double-spend:
- warp.verifyBlockMessage now requires the block be EXPORT-final (height ≤ BlockClient.
  LastQuasarHeight), so a validator signs a block hash only at Quasar finality, never on a
  reorgable Nova accept (two conflicting Nova forks could otherwise each aggregate a valid export).
- WarpQuorumNumeratorMinimum floored 33 → 67 (⅔): any warp message needs ≥⅔ stake to verify, so at
  most one of two conflicting exports can validate (two ⅔ sets intersect on >⅓ honest stake) —
  restoring the ⅔-accept safety the pre-split accept tier gave for free.

The consensus engine pushes the frontier via NetworkConfig.QuasarObserver; the node bridges it into
the VM (separate node commit). Nova-only chains (no export surface) are unaffected.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-10 04:58:54 -07:00
co-authored by Hanzo Dev
parent 1f8281597a
commit 9c6d66ef6b
9 changed files with 260 additions and 4 deletions
+50
View File
@@ -139,6 +139,10 @@ const (
var (
// Set last accepted key to be longer than the keys used to store accepted block IDs.
lastAcceptedKey = []byte("last_accepted_key")
// quasarHeightKey stores the highest EXPORT-FINAL (Quasar, ⅔-by-stake) block number — the
// block the eth `finalized`/`safe` tags resolve to. Persisted so the export surface never
// regresses on restart (the in-memory consensus frontier resets to 0 until a cert re-forms).
quasarHeightKey = []byte("quasar_export_height_key")
acceptedPrefix = []byte("chain_accepted")
metadataPrefix = []byte("metadata")
warpPrefix = []byte("warp")
@@ -1050,6 +1054,11 @@ func (vm *VM) initializeChain(lastAcceptedHash common.Hash, ethConfig ethconfig.
vm.txPool = vm.eth.TxPool()
vm.blockChain = vm.eth.BlockChain()
debugLog("initializeChain: got txPool and blockChain")
// Restore the durable EXPORT-FINAL (Quasar) height so the `finalized`/`safe` tags are correct
// from the first RPC — before consensus re-forms a ⅔-stake cert this session. Advance-only.
if h := vm.readLastQuasarHeight(); h > 0 {
vm.eth.SetLastQuasarHeight(h)
}
// Upgrade the consensus context on the live chain to the FULL chain Runtime, which adds
// the cross-chain atomic SharedMemory capability (the DEX 0x9999 C<->D shared-memory seam)
// on top of the identity already bound at construction. Also covers the warp message
@@ -2054,6 +2063,47 @@ func (vm *VM) readLastAccepted() (common.Hash, uint64, error) {
}
}
// SetLastQuasarFinalized records that the block at `height` has reached EXPORT finality (Quasar,
// ⅔-by-stake). It is the sink the consensus export-frontier observer pushes into — strictly AFTER
// the block's local (Nova majority) Accept. It advances the in-memory export height the eth
// `finalized`/`safe` tags read (eth.SetLastQuasarHeight is monotone) AND persists it so the export
// surface survives a restart without regressing (the in-memory consensus frontier resets to 0
// until a cert re-forms). No-op before the eth backend exists.
func (vm *VM) SetLastQuasarFinalized(height uint64) {
if vm.eth == nil {
return
}
if height <= vm.eth.LastQuasarHeight() {
return // monotone: the export frontier only advances
}
vm.eth.SetLastQuasarHeight(height)
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], height)
if err := vm.acceptedBlockDB.Put(quasarHeightKey, buf[:]); err != nil {
log.Warn("failed to persist EXPORT-FINAL (quasar) height", "height", height, "err", err)
}
}
// LastQuasarHeight returns the highest EXPORT-FINAL (Quasar) block number the eth `finalized`/
// `safe` tags resolve to (0 before the first export). The node reads it on boot to re-seed the
// consensus export frontier so GetQuasarTip / QuasarHeight do not regress on restart.
func (vm *VM) LastQuasarHeight() uint64 {
if vm.eth == nil {
return 0
}
return vm.eth.LastQuasarHeight()
}
// readLastQuasarHeight loads the durable EXPORT-FINAL (Quasar) height from acceptedBlockDB on
// boot. 0 (→ genesis) when never exported or missing.
func (vm *VM) readLastQuasarHeight() uint64 {
b, err := vm.acceptedBlockDB.Get(quasarHeightKey)
if err != nil || len(b) != 8 {
return 0
}
return binary.BigEndian.Uint64(b)
}
// attachEthService registers the backend RPC services provided by Ethereum
// to the provided handler under their assigned namespaces.
func attachEthService(handler *rpc.Server, apis []rpc.API, names []string) error {
+7
View File
@@ -33,3 +33,10 @@ func (w *warpBlockClient) GetAcceptedBlock(ctx context.Context, blkID ids.ID) (n
return w.vm.newBlock(ethBlock), nil
}
// LastQuasarHeight returns the VM's highest EXPORT-FINAL (Quasar, ⅔-by-stake) block number — the
// export-tier gate for warp block signatures (a cross-chain export must sit at export finality,
// not the reorgable local Accept tip).
func (w *warpBlockClient) LastQuasarHeight() uint64 {
return w.vm.LastQuasarHeight()
}