Files
coreth/plugin/evm/wrapped_block.go

455 lines
16 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"bytes"
"context"
"errors"
"fmt"
"time"
"github.com/luxfi/constants"
"github.com/luxfi/coreth/core"
"github.com/luxfi/coreth/params"
"github.com/luxfi/coreth/params/extras"
"github.com/luxfi/coreth/plugin/evm/customtypes"
"github.com/luxfi/coreth/plugin/evm/extension"
"github.com/luxfi/coreth/plugin/evm/header"
"github.com/luxfi/coreth/precompile/precompileconfig"
"github.com/luxfi/coreth/predicate"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/rawdb"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/rlp"
"github.com/luxfi/geth/trie"
log "github.com/luxfi/log"
"github.com/luxfi/consensus/engine/chain/block"
"github.com/luxfi/ids"
)
var (
_ block.Block = (*wrappedBlock)(nil)
_ block.WithVerifyContext = (*wrappedBlock)(nil)
_ extension.ExtendedBlock = (*wrappedBlock)(nil)
)
// wrappedBlock implements the quasarman.wrappedBlock interface
type wrappedBlock struct {
id ids.ID
ethBlock *types.Block
extension extension.BlockExtension
vm *VM
}
// wrapBlock returns a new Block wrapping the ethBlock type and implementing the block.Block interface
func wrapBlock(ethBlock *types.Block, vm *VM) (*wrappedBlock, error) {
b := &wrappedBlock{
id: ids.ID(ethBlock.Hash()),
ethBlock: ethBlock,
vm: vm,
}
if vm.extensionConfig.BlockExtender != nil {
extension, err := vm.extensionConfig.BlockExtender.NewBlockExtension(b)
if err != nil {
return nil, fmt.Errorf("failed to create block extension: %w", err)
}
b.extension = extension
}
return b, nil
}
// ID implements the block.Block interface
func (b *wrappedBlock) ID() ids.ID { return b.id }
// Accept implements the block.Block interface
func (b *wrappedBlock) Accept(context.Context) error {
vm := b.vm
// Although returning an error from Accept is considered fatal, it is good
// practice to cleanup the batch we were modifying in the case of an error.
defer vm.versiondb.Abort()
blkID := b.ID()
log.Debug("accepting block",
"hash", blkID.Hex(),
"id", blkID,
"height", b.Height(),
)
// Call Accept for relevant precompile logs. Note we do this prior to
// calling Accept on the blockChain so any side effects (eg warp signatures)
// take place before the accepted log is emitted to subscribers.
rules := b.vm.rules(b.ethBlock.Number(), b.ethBlock.Time())
if err := b.handlePrecompileAccept(rules); err != nil {
return err
}
if err := vm.blockChain.Accept(b.ethBlock); err != nil {
return fmt.Errorf("chain could not accept %s: %w", blkID, err)
}
if err := vm.PutLastAcceptedID(blkID); err != nil {
return fmt.Errorf("failed to put %s as the last accepted block: %w", blkID, err)
}
// Get pending operations on the vm's versionDB so we can apply them atomically
// with the block extension's changes.
vdbBatch, err := vm.versiondb.CommitBatch()
if err != nil {
return fmt.Errorf("could not create commit batch processing block[%s]: %w", blkID, err)
}
if b.extension != nil {
// Apply any changes atomically with other pending changes to
// the vm's versionDB.
// Accept flushes the changes in the batch to the database.
return b.extension.Accept(vdbBatch)
}
// If there is no extension, we still need to apply the changes to the versionDB
return vdbBatch.Write()
}
// handlePrecompileAccept calls Accept on any logs generated with an active precompile address that implements
// contract.Accepter
func (b *wrappedBlock) handlePrecompileAccept(rules extras.Rules) error {
// Short circuit early if there are no precompile accepters to execute
if len(rules.AccepterPrecompiles) == 0 {
return nil
}
// Read receipts from disk
receipts := rawdb.ReadReceipts(b.vm.chaindb, b.ethBlock.Hash(), b.ethBlock.NumberU64(), b.ethBlock.Time(), b.vm.chainConfig)
// If there are no receipts, ReadReceipts may be nil, so we check the length and confirm the ReceiptHash
// is empty to ensure that missing receipts results in an error on accept.
if len(receipts) == 0 && b.ethBlock.ReceiptHash() != types.EmptyRootHash {
return fmt.Errorf("failed to fetch receipts for accepted block with non-empty root hash (%s) (Block: %s, Height: %d)", b.ethBlock.ReceiptHash(), b.ethBlock.Hash(), b.ethBlock.NumberU64())
}
acceptCtx := &precompileconfig.AcceptContext{
ConsensusCtx: b.vm.runtime,
Warp: b.vm.warpBackend,
}
for _, receipt := range receipts {
for logIdx, log := range receipt.Logs {
accepter, ok := rules.AccepterPrecompiles[log.Address]
if !ok {
continue
}
if err := accepter.Accept(acceptCtx, log.BlockHash, log.BlockNumber, log.TxHash, logIdx, log.Topics, log.Data); err != nil {
return err
}
}
}
return nil
}
// Reject implements the block.Block interface
// If [b] contains an atomic transaction, attempt to re-issue it
func (b *wrappedBlock) Reject(context.Context) error {
blkID := b.ID()
log.Debug("rejecting block",
"hash", blkID.Hex(),
"id", blkID,
"height", b.Height(),
)
if b.extension != nil {
if err := b.extension.Reject(); err != nil {
return err
}
}
return b.vm.blockChain.Reject(b.ethBlock)
}
// Parent implements the block.Block interface (alias for ParentID)
func (b *wrappedBlock) Parent() ids.ID {
return ids.ID(b.ethBlock.ParentHash())
}
// ParentID implements the block.Block interface
func (b *wrappedBlock) ParentID() ids.ID {
return ids.ID(b.ethBlock.ParentHash())
}
// Height implements the block.Block interface
func (b *wrappedBlock) Height() uint64 {
return b.ethBlock.NumberU64()
}
// Status implements the block.Block interface
// Note: The actual status management is handled by the chain.State wrapper
func (b *wrappedBlock) Status() uint8 {
return 2 // StatusProcessing
}
// Timestamp implements the block.Block interface
func (b *wrappedBlock) Timestamp() time.Time {
return time.Unix(int64(b.ethBlock.Time()), 0)
}
// Verify implements the block.Block interface
func (b *wrappedBlock) Verify(context.Context) error {
return b.verify(&precompileconfig.PredicateContext{
ConsensusCtx: b.vm.runtime,
ProposerVMBlockCtx: nil,
}, true)
}
// ShouldVerifyWithContext implements the block.WithVerifyContext interface
func (b *wrappedBlock) ShouldVerifyWithContext(context.Context) (bool, error) {
rules := b.vm.rules(b.ethBlock.Number(), b.ethBlock.Time())
predicates := rules.Predicaters
// Short circuit early if there are no predicates to verify
if len(predicates) == 0 {
return false, nil
}
// Check if any of the transactions in the block specify a precompile that enforces a predicate, which requires
// the ProposerVMBlockCtx.
for _, tx := range b.ethBlock.Transactions() {
for _, accessTuple := range tx.AccessList() {
if _, ok := predicates[accessTuple.Address]; ok {
log.Debug("Block verification requires proposerVM context", "block", b.ID(), "height", b.Height())
return true, nil
}
}
}
log.Debug("Block verification does not require proposerVM context", "block", b.ID(), "height", b.Height())
return false, nil
}
// VerifyWithContext implements the block.WithVerifyContext interface
func (b *wrappedBlock) VerifyWithContext(ctx context.Context, proposerVMBlockCtx *block.Context) error {
return b.verify(&precompileconfig.PredicateContext{
ConsensusCtx: b.vm.runtime,
ProposerVMBlockCtx: proposerVMBlockCtx,
}, true)
}
// Verify the block is valid.
// Enforces that the predicates are valid within [predicateContext].
// Writes the block details to disk and the state to the trie manager iff writes=true.
func (b *wrappedBlock) verify(predicateContext *precompileconfig.PredicateContext, writes bool) error {
if predicateContext.ProposerVMBlockCtx != nil {
log.Debug("Verifying block with context", "block", b.ID(), "height", b.Height())
} else {
log.Debug("Verifying block without context", "block", b.ID(), "height", b.Height())
}
if err := b.syntacticVerify(); err != nil {
return fmt.Errorf("syntactic block verification failed: %w", err)
}
if err := b.semanticVerify(); err != nil {
return fmt.Errorf("failed to verify block: %w", err)
}
// If the VM is not marked as bootstrapped the other chains may also be
// bootstrapping and not have populated the required indices. Since
// bootstrapping only verifies blocks that have been canonically accepted by
// the network, these checks would be guaranteed to pass on a synced node.
if b.vm.bootstrapped.Get() {
// Verify that all the ICM messages are correctly marked as either valid
// or invalid.
if err := b.verifyPredicates(predicateContext); err != nil {
return fmt.Errorf("failed to verify predicates: %w", err)
}
}
// The engine may call VerifyWithContext multiple times on the same block with different contexts.
// Since the engine will only call Accept/Reject once, we should only call InsertBlockManual once.
// Additionally, if a block is already in processing, then it has already passed verification and
// at this point we have checked the predicates are still valid in the different context so we
// can return nil.
if b.vm.State.IsProcessing(b.id) {
return nil
}
err := b.vm.blockChain.InsertBlockManual(b.ethBlock, writes)
// If this was not called with intention to writing to the database or
// got an error while inserting to blockchain, we may need to cleanup the extension.
// so that the extension can be garbage collected.
if doCleanup := err != nil || !writes; b.extension != nil && doCleanup {
b.extension.CleanupVerified()
}
return err
}
// semanticVerify verifies that a *Block is internally consistent.
func (b *wrappedBlock) semanticVerify() error {
// Make sure the block isn't too far in the future
blockTimestamp := b.ethBlock.Time()
if maxBlockTime := uint64(b.vm.clock.Time().Add(maxFutureBlockTime).Unix()); blockTimestamp > maxBlockTime {
return fmt.Errorf("block timestamp is too far in the future: %d > allowed %d", blockTimestamp, maxBlockTime)
}
if b.extension != nil {
if err := b.extension.SemanticVerify(); err != nil {
return err
}
}
return nil
}
// syntacticVerify verifies that a *Block is well-formed.
func (b *wrappedBlock) syntacticVerify() error {
if b == nil || b.ethBlock == nil {
return errInvalidBlock
}
// Skip verification of the genesis block since it should already be marked as accepted.
if b.ethBlock.Hash() == b.vm.genesisHash {
return nil
}
ethHeader := b.ethBlock.Header()
rules := b.vm.chainConfig.Rules(ethHeader.Number, params.IsMergeTODO, ethHeader.Time)
rulesExtra := params.GetRulesExtra(rules)
// Perform block and header sanity checks
if !ethHeader.Number.IsUint64() {
return fmt.Errorf("invalid block number: %v", ethHeader.Number)
}
if !ethHeader.Difficulty.IsUint64() || ethHeader.Difficulty.Cmp(common.Big1) != 0 {
return fmt.Errorf("invalid difficulty: %d", ethHeader.Difficulty)
}
if ethHeader.Nonce.Uint64() != 0 {
return fmt.Errorf(
"expected nonce to be 0 but got %d: %w",
ethHeader.Nonce.Uint64(), errInvalidNonce,
)
}
if ethHeader.MixDigest != (common.Hash{}) {
return fmt.Errorf("invalid mix digest: %v", ethHeader.MixDigest)
}
// Verify the extra data is well-formed.
if err := header.VerifyExtra(rulesExtra.LuxRules, ethHeader.Extra); err != nil {
return err
}
if version := customtypes.GetBlockVersion(b.ethBlock); version != 0 {
return fmt.Errorf("invalid version: %d", version)
}
// Check that the tx hash in the header matches the body
txsHash := types.DeriveSha(b.ethBlock.Transactions(), trie.NewStackTrie(nil))
if txsHash != ethHeader.TxHash {
return fmt.Errorf("invalid txs hash %v does not match calculated txs hash %v", ethHeader.TxHash, txsHash)
}
// Check that the uncle hash in the header matches the body
uncleHash := types.CalcUncleHash(b.ethBlock.Uncles())
if uncleHash != ethHeader.UncleHash {
return fmt.Errorf("invalid uncle hash %v does not match calculated uncle hash %v", ethHeader.UncleHash, uncleHash)
}
// Coinbase must match the BlackholeAddr on C-Chain
if ethHeader.Coinbase != constants.BlackholeAddr {
return fmt.Errorf("invalid coinbase %v does not match required blackhole address %v", ethHeader.Coinbase, constants.BlackholeAddr)
}
// Block must not have any uncles
if len(b.ethBlock.Uncles()) > 0 {
return errUnclesUnsupported
}
// Under activate-all-implicitly dynamic fees are live from genesis. The
// header must carry a base fee.
if ethHeader.BaseFee == nil {
return errNilBaseFee
}
if bfLen := ethHeader.BaseFee.BitLen(); bfLen > 256 {
return fmt.Errorf("too large base fee: bitlen %d", bfLen)
}
headerExtra := customtypes.GetHeaderExtra(ethHeader)
if headerExtra.BlockGasCost != nil && !headerExtra.BlockGasCost.IsUint64() {
return fmt.Errorf("too large blockGasCost: %d", headerExtra.BlockGasCost)
}
// Verify the existence / non-existence of excessBlobGas
cancun := rules.IsCancun
if !cancun && ethHeader.ExcessBlobGas != nil {
return fmt.Errorf("invalid excessBlobGas: have %d, expected nil", *ethHeader.ExcessBlobGas)
}
if !cancun && ethHeader.BlobGasUsed != nil {
return fmt.Errorf("invalid blobGasUsed: have %d, expected nil", *ethHeader.BlobGasUsed)
}
if cancun && ethHeader.ExcessBlobGas == nil {
return errors.New("header is missing excessBlobGas")
}
if cancun && ethHeader.BlobGasUsed == nil {
return errors.New("header is missing blobGasUsed")
}
if !cancun && ethHeader.ParentBeaconRoot != nil {
return fmt.Errorf("invalid parentBeaconRoot: have %x, expected nil", *ethHeader.ParentBeaconRoot)
}
if cancun {
switch {
case ethHeader.ParentBeaconRoot == nil:
return errors.New("header is missing parentBeaconRoot")
case *ethHeader.ParentBeaconRoot != (common.Hash{}):
return fmt.Errorf("invalid parentBeaconRoot: have %x, expected empty hash", ethHeader.ParentBeaconRoot)
}
if ethHeader.BlobGasUsed == nil {
return fmt.Errorf("blob gas used must not be nil in Cancun")
} else if *ethHeader.BlobGasUsed > 0 {
return fmt.Errorf("blobs not enabled on lux networks: used %d blob gas, expected 0", *ethHeader.BlobGasUsed)
}
}
if b.extension != nil {
if err := b.extension.SyntacticVerify(*rulesExtra); err != nil {
return err
}
}
return nil
}
// verifyPredicates verifies the predicates in the block are valid according to predicateContext.
// Under activate-all-implicitly predicates are live from genesis.
func (b *wrappedBlock) verifyPredicates(predicateContext *precompileconfig.PredicateContext) error {
rules := b.vm.chainConfig.Rules(b.ethBlock.Number(), params.IsMergeTODO, b.ethBlock.Time())
predicateResults := predicate.NewResults()
for _, tx := range b.ethBlock.Transactions() {
results, err := core.CheckPredicates(rules, predicateContext, tx)
if err != nil {
return err
}
predicateResults.SetTxResults(tx.Hash(), results)
}
// TODO: document required gas constraints to ensure marshalling predicate results does not error
predicateResultsBytes, err := predicateResults.Bytes()
if err != nil {
return fmt.Errorf("failed to marshal predicate results: %w", err)
}
extraData := b.ethBlock.Extra()
luxRules := params.GetRulesExtra(rules).LuxRules
headerPredicateResultsBytes := header.PredicateBytesFromExtra(luxRules, extraData)
if !bytes.Equal(headerPredicateResultsBytes, predicateResultsBytes) {
return fmt.Errorf("%w (remote: %x local: %x)", errInvalidHeaderPredicateResults, headerPredicateResultsBytes, predicateResultsBytes)
}
return nil
}
// Bytes implements the block.Block interface
func (b *wrappedBlock) Bytes() []byte {
res, err := rlp.EncodeToBytes(b.ethBlock)
if err != nil {
panic(err)
}
return res
}
func (b *wrappedBlock) String() string { return fmt.Sprintf("EVM block, ID = %s", b.ID()) }
func (b *wrappedBlock) GetEthBlock() *types.Block {
return b.ethBlock
}
func (b *wrappedBlock) GetBlockExtension() extension.BlockExtension {
return b.extension
}