Files
evm/warp/verifier_backend.go
zeekayandHanzo Dev 9c6d66ef6b 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]>
2026-07-10 04:58:54 -07:00

121 lines
4.1 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package warp
import (
"context"
"errors"
"fmt"
"github.com/luxfi/evm/warp/messages"
"github.com/luxfi/crypto"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/warp"
"github.com/luxfi/warp/payload"
)
const (
ParseErrCode = iota + 1
VerifyErrCode
)
// Verify verifies the signature of the message
// It implements the warp.Verifier interface
func (b *backend) Verify(ctx context.Context, msg *warp.Message, _ []byte) error {
coreID := msg.ID()
messageID := ids.ID(crypto.Keccak256Hash(coreID[:]))
// Known on-chain messages should be signed
if _, err := b.GetMessage(messageID); err == nil {
return nil
} else if err != database.ErrNotFound {
return fmt.Errorf("failed to get message %s: %w", messageID, err)
}
parsed, err := payload.ParsePayload(msg.Payload)
if err != nil {
b.stats.IncMessageParseFail()
return fmt.Errorf("failed to parse payload: %w", err)
}
switch p := parsed.(type) {
case *payload.AddressedCall:
return b.verifyOffchainAddressedCall(p)
case *payload.Hash:
return b.verifyBlockMessage(ctx, p)
default:
b.stats.IncMessageParseFail()
return fmt.Errorf("unknown payload type: %T", p)
}
}
// verifyBlockMessage returns nil if blockHashPayload contains the ID of an EXPORT-FINAL block
// indicating it should be signed by the VM. Under the two-tier consensus a block hash warp
// signature is a CROSS-CHAIN EXPORT, so the block must have reached ⅔-by-stake QUASAR finality —
// NOT merely local (Nova, bare-majority) Accept. A Nova-accepted block is reorgable and can fork
// under f=1 equivocation; signing it would let two conflicting forks each aggregate a valid warp
// export (a double-spend). Requiring Quasar finality (height ≤ LastQuasarHeight) closes that: at
// most one block per height ever reaches Quasar (>⅓ stake would have to double-sign otherwise).
func (b *backend) verifyBlockMessage(ctx context.Context, blockHashPayload *payload.Hash) error {
// Convert []byte to ids.ID
var blockID ids.ID
copy(blockID[:], blockHashPayload.Hash)
blk, err := b.blockClient.GetAcceptedBlock(ctx, blockID)
if err != nil {
b.stats.IncBlockValidationFail()
return fmt.Errorf("failed to get block %s: %w", blockID, err)
}
// EXPORT GATE: the block must be at or below the export-final (Quasar) height.
if quasar := b.blockClient.LastQuasarHeight(); blk.Height() > quasar {
b.stats.IncBlockValidationFail()
return fmt.Errorf("refusing to warp-sign block %s at height %d: not yet EXPORT-final "+
"(quasar height %d) — a warp export requires ⅔-stake finality, not local accept", blockID, blk.Height(), quasar)
}
return nil
}
// verifyOffchainAddressedCall verifies the addressed call message
func (b *backend) verifyOffchainAddressedCall(addressedCall *payload.AddressedCall) error {
// Further, parse the payload to see if it is a known type.
parsed, err := messages.Parse(addressedCall.Payload)
if err != nil {
b.stats.IncMessageParseFail()
return fmt.Errorf("failed to parse addressed call message: %w", err)
}
if len(addressedCall.SourceAddress) != 0 {
return errors.New("source address should be empty for offchain addressed messages")
}
switch p := parsed.(type) {
case *messages.ValidatorUptime:
if err := b.verifyUptimeMessage(p); err != nil {
b.stats.IncUptimeValidationFail()
return err
}
default:
b.stats.IncMessageParseFail()
return fmt.Errorf("unknown message type: %T", p)
}
return nil
}
func (b *backend) verifyUptimeMessage(uptimeMsg *messages.ValidatorUptime) error {
vdr, currentUptime, _, err := b.validatorReader.GetValidatorAndUptime(uptimeMsg.ValidationID)
if err != nil {
return fmt.Errorf("failed to get uptime for validationID %s: %w", uptimeMsg.ValidationID, err)
}
currentUptimeSeconds := uint64(currentUptime.Seconds())
// verify the current uptime against the total uptime in the message
if currentUptimeSeconds < uptimeMsg.TotalUptime {
return fmt.Errorf("current uptime %d is less than queried uptime %d for nodeID %s", currentUptimeSeconds, uptimeMsg.TotalUptime, vdr.NodeID)
}
return nil
}