Files

229 lines
7.2 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package vm
import (
"errors"
"fmt"
"github.com/luxfi/coreth/plugin/evm/atomic"
"github.com/luxfi/database"
safemath "github.com/luxfi/math"
luxatomic "github.com/luxfi/vm/chains/atomic"
"github.com/luxfi/coreth/params/extras"
"github.com/luxfi/coreth/plugin/evm/customtypes"
"github.com/luxfi/coreth/plugin/evm/extension"
"github.com/luxfi/coreth/utils"
"github.com/luxfi/geth/common"
log "github.com/luxfi/log"
)
var (
_ extension.BlockExtension = (*blockExtension)(nil)
_ extension.BlockExtender = (*blockExtender)(nil)
_ atomic.AtomicBlockContext = (*blockExtension)(nil)
)
var (
errNilEthBlock = errors.New("nil ethBlock")
ErrMissingUTXOs = errors.New("missing UTXOs")
ErrEmptyBlock = errors.New("empty block")
)
type blockExtender struct {
extDataHashes map[common.Hash]common.Hash
vm *VM
}
type blockExtension struct {
atomicTxs []*atomic.Tx
blockExtender *blockExtender
block extension.ExtendedBlock
}
// newBlockExtender returns a new block extender.
func newBlockExtender(
extDataHashes map[common.Hash]common.Hash,
vm *VM,
) *blockExtender {
return &blockExtender{
extDataHashes: extDataHashes,
// Note: we need VM here to access the atomic backend that
// could be initialized later in the VM.
vm: vm,
}
}
// NewBlockExtension returns a new block extension. Under
// activate-all-implicitly the ext data is always a batch-encoded atomic-tx
func (be *blockExtender) NewBlockExtension(b extension.ExtendedBlock) (extension.BlockExtension, error) {
ethBlock := b.GetEthBlock()
if ethBlock == nil {
return nil, errNilEthBlock
}
atomicTxs, err := atomic.ExtractAtomicTxs(customtypes.BlockExtData(ethBlock), true, atomic.Codec)
if err != nil {
return nil, err
}
return &blockExtension{
atomicTxs: atomicTxs,
blockExtender: be,
block: b,
}, nil
}
// SyntacticVerify checks the syntactic validity of the block. This is called by the wrapper
// block manager's SyntacticVerify method.
func (be *blockExtension) SyntacticVerify(_ extras.Rules) error {
b := be.block
ethBlock := b.GetEthBlock()
// should not happen
if ethBlock == nil {
return errNilEthBlock
}
ethHeader := ethBlock.Header()
headerExtra := customtypes.GetHeaderExtra(ethHeader)
// Under activate-all-implicitly every block carries an ExtDataHash that
// must match the canonical hash of its ext data.
extraData := customtypes.BlockExtData(ethBlock)
if hash := customtypes.CalcExtDataHash(extraData); headerExtra.ExtDataHash != hash {
return fmt.Errorf("extra data hash mismatch: have %x, want %x", headerExtra.ExtDataHash, hash)
}
// Block must not be empty
txs := ethBlock.Transactions()
atomicTxs := be.atomicTxs
if len(txs) == 0 && len(atomicTxs) == 0 {
return ErrEmptyBlock
}
// Under activate-all-implicitly the extDataGasUsed field is validated by
// [header.VerifyGasUsed]; here we cross-check it equals the sum of
// per-atomic-tx fixed-fee gas charges.
var totalGasUsed uint64
for _, atomicTx := range atomicTxs {
gasUsed, err := atomicTx.GasUsed(true)
if err != nil {
return err
}
totalGasUsed, err = safemath.Add(totalGasUsed, gasUsed)
if err != nil {
return err
}
}
if !utils.BigEqualUint64(headerExtra.ExtDataGasUsed, totalGasUsed) {
return fmt.Errorf("invalid extDataGasUsed: have %d, want %d", headerExtra.ExtDataGasUsed, totalGasUsed)
}
return nil
}
// SemanticVerify checks the semantic validity of the block. This is called by the wrapper
// block manager's SemanticVerify method.
func (be *blockExtension) SemanticVerify() error {
vm := be.blockExtender.vm
if vm.bootstrapped.Get() {
// Verify that the UTXOs named in import txs are present in shared
// memory.
//
// This does not fully verify that this block can spend these UTXOs.
// However, it guarantees that any block that fails the later checks was
// built by an incorrect block proposer. This ensures that we only mark
// blocks as BAD BLOCKs if they were incorrectly generated.
if err := be.verifyUTXOsPresent(be.atomicTxs); err != nil {
return err
}
}
return nil
}
// Accept is called when the block is accepted. This is called by the wrapper
// block manager's Accept method. The acceptedBatch contains the changes that
// were made to the database as a result of accepting the block, and it's flushed
// to the database in this method.
func (be *blockExtension) Accept(acceptedBatch database.Batch) error {
vm := be.blockExtender.vm
for _, tx := range be.atomicTxs {
// Remove the accepted transaction from the mempool
vm.AtomicMempool.RemoveTx(tx)
}
// Update VM state for atomic txs in this block. This includes updating the
// atomic tx repo, atomic trie, and shared memory.
atomicState, err := vm.AtomicBackend.GetVerifiedAtomicState(common.Hash(be.block.ID()))
if err != nil {
// should never occur since [b] must be verified before calling Accept
return err
}
// Apply any shared memory changes atomically with other pending batched changes
return atomicState.Accept(acceptedBatch)
}
// Reject is called when the block is rejected. This is called by the wrapper
// block manager's Reject method.
func (be *blockExtension) Reject() error {
vm := be.blockExtender.vm
for _, tx := range be.atomicTxs {
// Re-issue the transaction in the mempool, continue even if it fails
vm.AtomicMempool.RemoveTx(tx)
if err := vm.AtomicMempool.AddRemoteTx(tx); err != nil {
log.Debug("Failed to re-issue transaction in rejected block", "txID", tx.ID(), "err", err)
}
}
atomicState, err := vm.AtomicBackend.GetVerifiedAtomicState(common.Hash(be.block.ID()))
if err != nil {
// should never occur since [b] must be verified before calling Reject
return err
}
return atomicState.Reject()
}
// CleanupVerified is called when the block is cleaned up after a failed insertion.
func (be *blockExtension) CleanupVerified() {
vm := be.blockExtender.vm
if atomicState, err := vm.AtomicBackend.GetVerifiedAtomicState(be.block.GetEthBlock().Hash()); err == nil {
atomicState.Reject()
}
}
// AtomicTxs returns the atomic transactions in this block.
func (be *blockExtension) AtomicTxs() []*atomic.Tx {
return be.atomicTxs
}
// verifyUTXOsPresent verifies all atomic UTXOs consumed by the block are
// present in shared memory.
func (be *blockExtension) verifyUTXOsPresent(atomicTxs []*atomic.Tx) error {
b := be.block
blockHash := common.Hash(b.ID())
vm := be.blockExtender.vm
if vm.AtomicBackend.IsBonus(b.Height(), blockHash) {
log.Info("skipping atomic tx verification on bonus block", "block", blockHash)
return nil
}
// verify UTXOs named in import txs are present in shared memory.
sharedMemory, ok := vm.Runtime.SharedMemory.(luxatomic.SharedMemory)
if !ok {
// SharedMemory unavailable (ZAP transport); skip atomic UTXO verification
log.Warn("skipping atomic UTXO verification: SharedMemory unavailable (ZAP transport)")
return nil
}
for _, atomicTx := range atomicTxs {
utx := atomicTx.UnsignedAtomicTx
chainID, requests, err := utx.AtomicOps()
if err != nil {
return err
}
if _, err := sharedMemory.Get(chainID, requests.RemoveRequests); err != nil {
return fmt.Errorf("%w: %s", ErrMissingUTXOs, err)
}
}
return nil
}