mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
constants.BlackholeAddr is a plain [20]byte, so constants no longer imports geth. The fourteen sites here all want a geth common.Address, and each file already imports geth/common, so each takes the free conversion itself. Only one site was a compile error: the .Bytes() method call in rewardmanager.DisableFeeRewards. Everywhere else [20]byte is assignable to common.Address, so the change was invisible to the compiler, and the two require.Equal assertions in rewardmanager would have compiled and then failed at run time, because reflect.DeepEqual compares types before bytes. Those two are wrapped for that reason, not for style. The two failures in core, TestHeadPin_Across4096Boundary_NoLeak and the invalid-chainID-0 panic in TestCreateThenDeletePostByzantium, reproduce identically with these lines reverted and are unrelated. Co-authored-by: Hanzo Dev <[email protected]>
313 lines
9.4 KiB
Go
313 lines
9.4 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
//
|
|
// This file is a derived work, based on the go-ethereum library whose original
|
|
// notices appear below.
|
|
//
|
|
// It is distributed under a license compatible with the licensing terms of the
|
|
// original code from which it is derived.
|
|
//
|
|
// Much love to the original authors for their work.
|
|
// **********
|
|
// Copyright 2023 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package simulated
|
|
|
|
import (
|
|
"errors"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/evm/consensus/dummy"
|
|
"github.com/luxfi/evm/core"
|
|
"github.com/luxfi/evm/eth"
|
|
"github.com/luxfi/evm/eth/ethconfig"
|
|
"github.com/luxfi/evm/ethclient"
|
|
"github.com/luxfi/evm/interfaces"
|
|
"github.com/luxfi/evm/node"
|
|
"github.com/luxfi/evm/params"
|
|
"github.com/luxfi/evm/params/extras"
|
|
"github.com/luxfi/evm/rpc"
|
|
ethereum "github.com/luxfi/geth"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/geth/core/rawdb"
|
|
"github.com/luxfi/geth/core/types"
|
|
"github.com/luxfi/timer/mockable"
|
|
)
|
|
|
|
var _ eth.PushGossiper = (*fakePushGossiper)(nil)
|
|
|
|
type fakePushGossiper struct{}
|
|
|
|
func (*fakePushGossiper) Add(*types.Transaction) {}
|
|
|
|
// Client exposes the methods provided by the Ethereum RPC client.
|
|
type Client interface {
|
|
ethereum.BlockNumberReader
|
|
ethereum.ChainReader
|
|
ethereum.ChainStateReader
|
|
ethereum.ContractCaller
|
|
ethereum.GasEstimator
|
|
ethereum.GasPricer
|
|
ethereum.GasPricer1559
|
|
ethereum.FeeHistoryReader
|
|
ethereum.LogFilterer
|
|
interfaces.AcceptedStateReader
|
|
interfaces.AcceptedContractCaller
|
|
ethereum.TransactionReader
|
|
ethereum.TransactionSender
|
|
ethereum.ChainIDReader
|
|
}
|
|
|
|
// simClient wraps ethclient. This exists to prevent extracting ethclient.Client
|
|
// from the Client interface returned by Backend.
|
|
type simClient struct {
|
|
ethclient.Client
|
|
}
|
|
|
|
// Backend is a simulated blockchain. You can use it to test your contracts or
|
|
// other code that interacts with the Ethereum chain.
|
|
type Backend struct {
|
|
eth *eth.Ethereum
|
|
client simClient
|
|
clock *mockable.Clock
|
|
server *rpc.Server
|
|
}
|
|
|
|
// NewBackend creates a new simulated blockchain that can be used as a backend for
|
|
// contract bindings in unit tests.
|
|
//
|
|
// A simulated backend always uses chainID 1337.
|
|
func NewBackend(alloc types.GenesisAlloc, options ...func(nodeConf *node.Config, ethConf *ethconfig.Config)) *Backend {
|
|
// Use TestEVMChainConfig which doesn't have Shanghai/Cancun enabled.
|
|
// This is needed for backward compatibility with legacy contract bytecode
|
|
// that uses pre-Shanghai opcodes (e.g., SELFDESTRUCT, certain JUMP patterns).
|
|
chainConfig := params.Copy(params.TestEVMChainConfig)
|
|
chainConfig.ChainID = big.NewInt(1337)
|
|
|
|
// Create the default configurations for the outer node shell and the Ethereum
|
|
// service to mutate with the options afterwards
|
|
nodeConf := node.DefaultConfig
|
|
|
|
ethConf := ethconfig.DefaultConfig
|
|
|
|
// Initialize the extras with proper defaults for EVM without Shanghai/Cancun
|
|
extra := &extras.ChainConfig{
|
|
FeeConfig: extras.TestFeeConfig, // Use TestFeeConfig with minimal fees for test compatibility
|
|
NetworkUpgrades: extras.TestEVMChainConfig.NetworkUpgrades,
|
|
}
|
|
params.SetExtra(chainConfig, extra)
|
|
|
|
ethConf.Genesis = &core.Genesis{
|
|
Config: chainConfig,
|
|
GasLimit: extra.FeeConfig.GasLimit.Uint64(),
|
|
Alloc: alloc,
|
|
}
|
|
ethConf.AllowUnfinalizedQueries = true
|
|
ethConf.Miner.Etherbase = common.Address(constants.BlackholeAddr)
|
|
ethConf.Miner.TestOnlyAllowDuplicateBlocks = true
|
|
ethConf.TxPool.NoLocals = true
|
|
// Use hash scheme for simulated backend to avoid pathdb flattening issues
|
|
// when accepting blocks on empty/genesis state
|
|
ethConf.StateScheme = rawdb.HashScheme
|
|
// Disable snapshots for simulated backend to avoid flattening issues
|
|
ethConf.SnapshotCache = 0
|
|
|
|
for _, option := range options {
|
|
option(&nodeConf, ðConf)
|
|
}
|
|
// Assemble the Ethereum stack to run the chain with
|
|
stack, err := node.New(&nodeConf)
|
|
if err != nil {
|
|
panic(err) // this should never happen
|
|
}
|
|
sim, err := newWithNode(stack, ðConf, 0)
|
|
if err != nil {
|
|
panic(err) // this should never happen
|
|
}
|
|
return sim
|
|
}
|
|
|
|
// newWithNode sets up a simulated backend on an existing node. The provided node
|
|
// must not be started and will be started by this method.
|
|
func newWithNode(stack *node.Node, conf *eth.Config, blockPeriod uint64) (*Backend, error) {
|
|
chaindb := rawdb.NewMemoryDatabase()
|
|
clock := &mockable.Clock{}
|
|
clock.Set(time.Unix(0, 0))
|
|
|
|
engine := dummy.NewFakerWithModeAndClock(
|
|
dummy.Mode{ModeSkipCoinbase: true}, clock,
|
|
)
|
|
|
|
backend, err := eth.New(
|
|
stack, conf, &fakePushGossiper{}, chaindb, eth.Settings{}, common.Hash{},
|
|
engine, clock,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
server := rpc.NewServer(0)
|
|
for _, api := range backend.APIs() {
|
|
if err := server.RegisterName(api.Namespace, api.Service); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &Backend{
|
|
eth: backend,
|
|
client: simClient{ethclient.NewClient(rpc.DialInProc(server))},
|
|
clock: clock,
|
|
server: server,
|
|
}, nil
|
|
}
|
|
|
|
// Close shuts down the simBackend.
|
|
// The simulated backend can't be used afterwards.
|
|
func (n *Backend) Close() error {
|
|
if n.client.Client != nil {
|
|
n.client.Close()
|
|
}
|
|
n.server.Stop()
|
|
return nil
|
|
}
|
|
|
|
// Commit seals a block and moves the chain forward to a new empty block.
|
|
func (n *Backend) Commit(accept bool) common.Hash {
|
|
hash, err := n.buildBlock(accept, 10)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return hash
|
|
}
|
|
|
|
func (n *Backend) buildBlock(accept bool, gap uint64) (common.Hash, error) {
|
|
chain := n.eth.BlockChain()
|
|
parent := chain.CurrentBlock()
|
|
|
|
if err := n.eth.TxPool().Sync(); err != nil {
|
|
return common.Hash{}, err
|
|
}
|
|
|
|
n.clock.Set(time.Unix(int64(parent.Time+gap), 0))
|
|
block, err := n.eth.Miner().GenerateBlock(nil)
|
|
if err != nil {
|
|
return common.Hash{}, err
|
|
}
|
|
if err := chain.InsertBlock(block); err != nil {
|
|
return common.Hash{}, err
|
|
}
|
|
|
|
// For simulated backend, we only accept blocks when explicitly requested
|
|
// This avoids snapshot flattening issues during initialization
|
|
if accept && block.NumberU64() > 0 {
|
|
if err := n.acceptAncestors(block); err != nil {
|
|
return common.Hash{}, err
|
|
}
|
|
chain.DrainAcceptorQueue()
|
|
}
|
|
return block.Hash(), nil
|
|
}
|
|
|
|
func (n *Backend) acceptAncestors(block *types.Block) error {
|
|
chain := n.eth.BlockChain()
|
|
lastAccepted := chain.LastConsensusAcceptedBlock()
|
|
|
|
// Accept all ancestors of the block
|
|
toAccept := []*types.Block{block}
|
|
for block.ParentHash() != lastAccepted.Hash() {
|
|
block = chain.GetBlockByHash(block.ParentHash())
|
|
toAccept = append(toAccept, block)
|
|
if block.NumberU64() < lastAccepted.NumberU64() {
|
|
return errors.New("last accepted must be an ancestor of the block to accept")
|
|
}
|
|
}
|
|
|
|
for i := len(toAccept) - 1; i >= 0; i-- {
|
|
if err := chain.Accept(toAccept[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Rollback removes all pending transactions, reverting to the last committed state.
|
|
func (n *Backend) Rollback() {
|
|
// Flush all transactions from the transaction pools
|
|
maxUint256 := new(big.Int).Sub(new(big.Int).Lsh(common.Big1, 256), common.Big1)
|
|
original := n.eth.TxPool().GasTip()
|
|
n.eth.TxPool().SetGasTip(maxUint256)
|
|
n.eth.TxPool().SetGasTip(original)
|
|
}
|
|
|
|
// Fork creates a side-chain that can be used to simulate reorgs.
|
|
//
|
|
// This function should be called with the ancestor block where the new side
|
|
// chain should be started. Transactions (old and new) can then be applied on
|
|
// top and Commit-ed.
|
|
//
|
|
// Note, the side-chain will only become canonical (and trigger the events) when
|
|
// it becomes longer. Until then CallContract will still operate on the current
|
|
// canonical chain.
|
|
//
|
|
// There is a % chance that the side chain becomes canonical at the same length
|
|
// to simulate live network behavior.
|
|
func (n *Backend) Fork(parentHash common.Hash) error {
|
|
chain := n.eth.BlockChain()
|
|
|
|
if chain.CurrentBlock().Hash() == parentHash {
|
|
return nil
|
|
}
|
|
|
|
parent := chain.GetBlockByHash(parentHash)
|
|
if parent == nil {
|
|
return errors.New("parent block not found")
|
|
}
|
|
|
|
ch := make(chan core.NewTxPoolReorgEvent, 1)
|
|
sub := n.eth.TxPool().SubscribeNewReorgEvent(ch)
|
|
defer sub.Unsubscribe()
|
|
|
|
if err := n.eth.BlockChain().SetPreference(parent); err != nil {
|
|
return err
|
|
}
|
|
for {
|
|
select {
|
|
case reorg := <-ch:
|
|
// Wait for tx pool to reorg, then flush the tx pool
|
|
if reorg.Head.Hash() == parent.Hash() {
|
|
n.Rollback()
|
|
return nil
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
return errors.New("fork not accepted")
|
|
}
|
|
}
|
|
}
|
|
|
|
// AdjustTime changes the block timestamp and creates a new block.
|
|
// It can only be called on empty blocks.
|
|
func (n *Backend) AdjustTime(adjustment time.Duration) error {
|
|
// Convert duration from nanoseconds to seconds for the block timestamp
|
|
_, err := n.buildBlock(false, uint64(adjustment.Seconds()))
|
|
return err
|
|
}
|
|
|
|
// Client returns a client that accesses the simulated chain.
|
|
func (n *Backend) Client() Client {
|
|
return n.client
|
|
}
|