mirror of
https://github.com/luxfi/evmgpu.git
synced 2026-10-30 16:57:11 +00:00
GetRulesExtra derived a Rules' Lux rules from a process-wide last context: the chain config and timestamp of whichever Rules any goroutine evaluated most recently. Rules are evaluated concurrently, by block processing, the miner, the tx pool and every eth_call, so a Rules could read another evaluation's upgrades: its IsDurango, its predicaters, its enabled precompiles, and the intrinsic gas that depends on them. The precompile overrider read the same global when the EVM asked it for a precompile, so which Lux precompiles a call reached could follow another goroutine's block. The context now travels with the Rules value. core's rules hook already put the chain config and timestamp in Rules.Payload for its precompile overrider; the overrider answers them, and GetRulesExtra reads them from the Rules it is given. The overrider decides its own enabled set from them too. RulesAt attaches them where no hook did. The global and SetRulesContext are gone. Every call site evaluates its Rules for the block it reads them for, so a caller that no other goroutine races sees the same rules as before. The blob pool's state checks passed a zero Rules and read the head's context the check before them had left; they now pass the head's Rules. TestRulesExtraIsTheRulesOwn evaluates two rule sets and reads the first; TestPrecompileOverrideIsTheRulesOwn asks each one's overrider for the tx allow list after the other was evaluated, in both orders; TestRulesExtraUnderConcurrentEvaluation reads four rule sets on four goroutines at once, across two times and two chains, including the intrinsic gas of a creation and the precompile a call reaches. TestActivePrecompiles, skipped for this bug, runs again. Co-authored-by: Hanzo Dev <[email protected]>
304 lines
10 KiB
Go
304 lines
10 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/geth/rlp"
|
|
log "github.com/luxfi/log"
|
|
|
|
"github.com/luxfi/evmgpu/core"
|
|
"github.com/luxfi/evmgpu/params"
|
|
"github.com/luxfi/evmgpu/params/extras"
|
|
"github.com/luxfi/evmgpu/plugin/evm/header"
|
|
"github.com/luxfi/evmgpu/precompile/precompileconfig"
|
|
"github.com/luxfi/evmgpu/predicate"
|
|
"github.com/luxfi/geth/core/rawdb"
|
|
"github.com/luxfi/geth/core/types"
|
|
|
|
"github.com/luxfi/consensus/core/choices"
|
|
"github.com/luxfi/ids"
|
|
block "github.com/luxfi/vm/chain"
|
|
)
|
|
|
|
var (
|
|
// Block implements the chain.Block interface
|
|
_ block.Block = (*Block)(nil)
|
|
)
|
|
|
|
// Block implements the chain.Block interface
|
|
type Block struct {
|
|
id ids.ID
|
|
ethBlock *types.Block
|
|
vm *VM
|
|
}
|
|
|
|
// newBlock returns a new Block wrapping the ethBlock type and implementing the chain.Block interface
|
|
func (vm *VM) newBlock(ethBlock *types.Block) *Block {
|
|
return &Block{
|
|
id: ids.ID(ethBlock.Hash()),
|
|
ethBlock: ethBlock,
|
|
vm: vm,
|
|
}
|
|
}
|
|
|
|
// ID implements the chain.Block interface
|
|
func (b *Block) ID() ids.ID { return b.id }
|
|
|
|
// Accept implements the chain.Block interface
|
|
func (b *Block) 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.acceptedBlockDB.Put(lastAcceptedKey, blkID[:]); err != nil {
|
|
return fmt.Errorf("failed to put %s as the last accepted block: %w", blkID, err)
|
|
}
|
|
|
|
return b.vm.versiondb.Commit()
|
|
}
|
|
|
|
// handlePrecompileAccept calls Accept on any logs generated with an active precompile address that implements
|
|
// contract.Accepter
|
|
func (b *Block) 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: context.Background(),
|
|
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 chain.Block interface
|
|
func (b *Block) Reject(context.Context) error {
|
|
blkID := b.ID()
|
|
log.Debug("rejecting block",
|
|
"hash", blkID.Hex(),
|
|
"id", blkID,
|
|
"height", b.Height(),
|
|
)
|
|
return b.vm.blockChain.Reject(b.ethBlock)
|
|
}
|
|
|
|
// Parent implements the chain.Block interface
|
|
func (b *Block) Parent() ids.ID {
|
|
return ids.ID(b.ethBlock.ParentHash())
|
|
}
|
|
|
|
// ParentID implements the chain.Block interface (same as Parent)
|
|
func (b *Block) ParentID() ids.ID {
|
|
return ids.ID(b.ethBlock.ParentHash())
|
|
}
|
|
|
|
// Height implements the chain.Block interface
|
|
func (b *Block) Height() uint64 {
|
|
return b.ethBlock.NumberU64()
|
|
}
|
|
|
|
// Timestamp implements the chain.Block interface
|
|
func (b *Block) Timestamp() time.Time {
|
|
return time.Unix(int64(b.ethBlock.Time()), 0)
|
|
}
|
|
|
|
// syntacticVerify verifies that a *Block is well-formed.
|
|
func (b *Block) syntacticVerify() error {
|
|
if b == nil || b.ethBlock == nil {
|
|
return errInvalidBlock
|
|
}
|
|
|
|
header := b.ethBlock.Header()
|
|
rules := b.vm.chainConfig.Rules(header.Number, params.IsMergeTODO, header.Time)
|
|
return b.vm.syntacticBlockValidator.SyntacticVerify(b, rules)
|
|
}
|
|
|
|
// Verify implements the chain.Block interface
|
|
func (b *Block) Verify(context.Context) error {
|
|
return b.verify(&precompileconfig.PredicateContext{
|
|
ConsensusCtx: context.Background(),
|
|
ProposerVMBlockCtx: nil,
|
|
}, true)
|
|
}
|
|
|
|
// ShouldVerifyWithContext implements the block.WithVerifyContext interface
|
|
func (b *Block) 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 *Block) VerifyWithContext(ctx context.Context, proposerVMBlockCtx *block.Context) error {
|
|
// Convert from node's block.Context to consensus's block.Context
|
|
var consensusBlockCtx *block.Context
|
|
if proposerVMBlockCtx != nil {
|
|
consensusBlockCtx = &block.Context{
|
|
PChainHeight: proposerVMBlockCtx.PChainHeight,
|
|
}
|
|
}
|
|
|
|
return b.verify(&precompileconfig.PredicateContext{
|
|
ConsensusCtx: b.vm.ctx,
|
|
ProposerVMBlockCtx: consensusBlockCtx,
|
|
}, 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 *Block) 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)
|
|
}
|
|
|
|
// Only enforce predicates if the chain has already bootstrapped.
|
|
// If the chain is still bootstrapping, we can assume that all blocks we are verifying have
|
|
// been accepted by the network (so the predicate was validated by the network when the
|
|
// block was originally verified).
|
|
if b.vm.bootstrapped.Get() {
|
|
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.IsProcessing(b.id) {
|
|
return nil
|
|
}
|
|
|
|
return b.vm.blockChain.InsertBlockManual(b.ethBlock, writes)
|
|
}
|
|
|
|
// verifyPredicates verifies the predicates in the block are valid according to predicateContext.
|
|
func (b *Block) verifyPredicates(predicateContext *precompileconfig.PredicateContext) error {
|
|
// The block's rules: GetRulesExtra reads the config and time they carry.
|
|
rules := params.RulesAt(b.vm.chainConfig, b.ethBlock.Number(), params.IsMergeTODO, b.ethBlock.Time())
|
|
rulesExtra := params.GetRulesExtra(rules)
|
|
|
|
switch {
|
|
case !rulesExtra.IsDurango && rulesExtra.PredicatersExist:
|
|
return errors.New("cannot enable predicates before Durango activation")
|
|
case !rulesExtra.IsDurango:
|
|
return nil
|
|
}
|
|
|
|
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()
|
|
headerPredicateResultsBytes := header.PredicateBytesFromExtra(extraData)
|
|
if !bytes.Equal(headerPredicateResultsBytes, predicateResultsBytes) {
|
|
return fmt.Errorf("%w (remote: %x local: %x)", errInvalidHeaderPredicateResults, headerPredicateResultsBytes, predicateResultsBytes)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Bytes implements the chain.Block interface
|
|
func (b *Block) Bytes() []byte {
|
|
res, err := rlp.EncodeToBytes(b.ethBlock)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (b *Block) String() string { return fmt.Sprintf("EVM block, ID = %s", b.ID()) }
|
|
|
|
// Status implements the chain.Block interface
|
|
func (b *Block) Status() uint8 {
|
|
// Return a simple status based on if the block is in the blockchain
|
|
// 0 = unknown, 1 = processing, 2 = accepted, 3 = rejected
|
|
// For simplicity, we'll return 2 (accepted) for now since this is mainly used in consensus
|
|
return 2
|
|
}
|
|
|
|
// SetStatus implements the chain.Block interface
|
|
// This is required for chain.Block but not used in our implementation
|
|
func (b *Block) SetStatus(status choices.Status) {
|
|
// No-op: EVM blocks manage their status internally through the blockchain
|
|
}
|