mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
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]>
121 lines
4.1 KiB
Go
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
|
|
}
|