mirror of
https://github.com/luxfi/coreth.git
synced 2026-10-30 17:00:51 +00:00
455 lines
16 KiB
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
|
|
}
|