// 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 . 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 }