mirror of https://github.com/ethereum/go-ethereum
ethclient/simulated: implement new sim backend (#28202)
This is a rewrite of the 'simulated backend', an implementation of the ethclient interfaces which is backed by a simulated blockchain. It was getting annoying to maintain the old version of the simulated backend feature because there was a lot of code duplication with the main client. The new version is built using parts that we already have: an in-memory geth node instance running in developer mode provides the chain, while the Go API is provided by ethclient. A backwards-compatibility wrapper is provided, but the simulated backend has also moved to a more sensible import path: github.com/ethereum/go-ethereum/ethclient/simulated --------- Co-authored-by: Felix Lange <fjl@twurst.com> Co-authored-by: Gary Rong <garyrong0905@gmail.com>pull/28787/head
parent
9e018ce3a5
commit
2d08c99009
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,190 @@ |
|||||||
|
// 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 ( |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum" |
||||||
|
"github.com/ethereum/go-ethereum/common" |
||||||
|
"github.com/ethereum/go-ethereum/core" |
||||||
|
"github.com/ethereum/go-ethereum/eth" |
||||||
|
"github.com/ethereum/go-ethereum/eth/catalyst" |
||||||
|
"github.com/ethereum/go-ethereum/eth/downloader" |
||||||
|
"github.com/ethereum/go-ethereum/eth/ethconfig" |
||||||
|
"github.com/ethereum/go-ethereum/eth/filters" |
||||||
|
"github.com/ethereum/go-ethereum/ethclient" |
||||||
|
"github.com/ethereum/go-ethereum/node" |
||||||
|
"github.com/ethereum/go-ethereum/p2p" |
||||||
|
"github.com/ethereum/go-ethereum/params" |
||||||
|
"github.com/ethereum/go-ethereum/rpc" |
||||||
|
) |
||||||
|
|
||||||
|
// 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 |
||||||
|
beacon *catalyst.SimulatedBeacon |
||||||
|
client simClient |
||||||
|
} |
||||||
|
|
||||||
|
// simClient wraps ethclient. This exists to prevent extracting ethclient.Client
|
||||||
|
// from the Client interface returned by Backend.
|
||||||
|
type simClient struct { |
||||||
|
*ethclient.Client |
||||||
|
} |
||||||
|
|
||||||
|
// 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 |
||||||
|
ethereum.PendingStateReader |
||||||
|
ethereum.PendingContractCaller |
||||||
|
ethereum.TransactionReader |
||||||
|
ethereum.TransactionSender |
||||||
|
ethereum.ChainIDReader |
||||||
|
} |
||||||
|
|
||||||
|
// New creates a new binding backend using a simulated blockchain
|
||||||
|
// for testing purposes.
|
||||||
|
// A simulated backend always uses chainID 1337.
|
||||||
|
func New(alloc core.GenesisAlloc, gasLimit uint64) *Backend { |
||||||
|
// Setup the node object
|
||||||
|
nodeConf := node.DefaultConfig |
||||||
|
nodeConf.DataDir = "" |
||||||
|
nodeConf.P2P = p2p.Config{NoDiscovery: true} |
||||||
|
stack, err := node.New(&nodeConf) |
||||||
|
if err != nil { |
||||||
|
// This should never happen, if it does, please open an issue
|
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
// Setup ethereum
|
||||||
|
genesis := core.Genesis{ |
||||||
|
Config: params.AllDevChainProtocolChanges, |
||||||
|
GasLimit: gasLimit, |
||||||
|
Alloc: alloc, |
||||||
|
} |
||||||
|
conf := ethconfig.Defaults |
||||||
|
conf.Genesis = &genesis |
||||||
|
conf.SyncMode = downloader.FullSync |
||||||
|
conf.TxPool.NoLocals = true |
||||||
|
sim, err := newWithNode(stack, &conf, 0) |
||||||
|
if err != nil { |
||||||
|
// This should never happen, if it does, please open an issue
|
||||||
|
panic(err) |
||||||
|
} |
||||||
|
return sim |
||||||
|
} |
||||||
|
|
||||||
|
// newWithNode sets up a simulated backend on an existing node
|
||||||
|
// this allows users to do persistent simulations.
|
||||||
|
// The provided node must not be started and will be started by newWithNode
|
||||||
|
func newWithNode(stack *node.Node, conf *eth.Config, blockPeriod uint64) (*Backend, error) { |
||||||
|
backend, err := eth.New(stack, conf) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Register the filter system
|
||||||
|
filterSystem := filters.NewFilterSystem(backend.APIBackend, filters.Config{}) |
||||||
|
stack.RegisterAPIs([]rpc.API{{ |
||||||
|
Namespace: "eth", |
||||||
|
Service: filters.NewFilterAPI(filterSystem, false), |
||||||
|
}}) |
||||||
|
|
||||||
|
// Start the node
|
||||||
|
if err := stack.Start(); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Set up the simulated beacon
|
||||||
|
beacon, err := catalyst.NewSimulatedBeacon(blockPeriod, backend) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Reorg our chain back to genesis
|
||||||
|
if err := beacon.Fork(backend.BlockChain().GetCanonicalHash(0)); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &Backend{ |
||||||
|
eth: backend, |
||||||
|
beacon: beacon, |
||||||
|
client: simClient{ethclient.NewClient(stack.Attach())}, |
||||||
|
}, 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.client = simClient{} |
||||||
|
} |
||||||
|
if n.beacon != nil { |
||||||
|
err := n.beacon.Stop() |
||||||
|
n.beacon = nil |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Commit seals a block and moves the chain forward to a new empty block.
|
||||||
|
func (n *Backend) Commit() common.Hash { |
||||||
|
return n.beacon.Commit() |
||||||
|
} |
||||||
|
|
||||||
|
// Rollback removes all pending transactions, reverting to the last committed state.
|
||||||
|
func (n *Backend) Rollback() { |
||||||
|
n.beacon.Rollback() |
||||||
|
} |
||||||
|
|
||||||
|
// 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 { |
||||||
|
return n.beacon.Fork(parentHash) |
||||||
|
} |
||||||
|
|
||||||
|
// 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 { |
||||||
|
return n.beacon.AdjustTime(adjustment) |
||||||
|
} |
||||||
|
|
||||||
|
// Client returns a client that accesses the simulated chain.
|
||||||
|
func (n *Backend) Client() Client { |
||||||
|
return n.client |
||||||
|
} |
@ -0,0 +1,309 @@ |
|||||||
|
// Copyright 2019 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 ( |
||||||
|
"context" |
||||||
|
"crypto/ecdsa" |
||||||
|
"math/big" |
||||||
|
"math/rand" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/abi/bind" |
||||||
|
"github.com/ethereum/go-ethereum/common" |
||||||
|
"github.com/ethereum/go-ethereum/core" |
||||||
|
"github.com/ethereum/go-ethereum/core/types" |
||||||
|
"github.com/ethereum/go-ethereum/crypto" |
||||||
|
"github.com/ethereum/go-ethereum/params" |
||||||
|
) |
||||||
|
|
||||||
|
var _ bind.ContractBackend = (Client)(nil) |
||||||
|
|
||||||
|
var ( |
||||||
|
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") |
||||||
|
testAddr = crypto.PubkeyToAddress(testKey.PublicKey) |
||||||
|
) |
||||||
|
|
||||||
|
func simTestBackend(testAddr common.Address) *Backend { |
||||||
|
return New( |
||||||
|
core.GenesisAlloc{ |
||||||
|
testAddr: {Balance: big.NewInt(10000000000000000)}, |
||||||
|
}, 10000000, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
func newTx(sim *Backend, key *ecdsa.PrivateKey) (*types.Transaction, error) { |
||||||
|
client := sim.Client() |
||||||
|
|
||||||
|
// create a signed transaction to send
|
||||||
|
head, _ := client.HeaderByNumber(context.Background(), nil) // Should be child's, good enough
|
||||||
|
gasPrice := new(big.Int).Add(head.BaseFee, big.NewInt(1)) |
||||||
|
addr := crypto.PubkeyToAddress(key.PublicKey) |
||||||
|
chainid, _ := client.ChainID(context.Background()) |
||||||
|
nonce, err := client.PendingNonceAt(context.Background(), addr) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
tx := types.NewTx(&types.DynamicFeeTx{ |
||||||
|
ChainID: chainid, |
||||||
|
Nonce: nonce, |
||||||
|
GasTipCap: big.NewInt(1), |
||||||
|
GasFeeCap: gasPrice, |
||||||
|
Gas: 21000, |
||||||
|
To: &addr, |
||||||
|
}) |
||||||
|
return types.SignTx(tx, types.LatestSignerForChainID(chainid), key) |
||||||
|
} |
||||||
|
|
||||||
|
func TestNewSim(t *testing.T) { |
||||||
|
sim := New(core.GenesisAlloc{}, 30_000_000) |
||||||
|
defer sim.Close() |
||||||
|
|
||||||
|
client := sim.Client() |
||||||
|
num, err := client.BlockNumber(context.Background()) |
||||||
|
if err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
if num != 0 { |
||||||
|
t.Fatalf("expected 0 got %v", num) |
||||||
|
} |
||||||
|
// Create a block
|
||||||
|
sim.Commit() |
||||||
|
num, err = client.BlockNumber(context.Background()) |
||||||
|
if err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
if num != 1 { |
||||||
|
t.Fatalf("expected 1 got %v", num) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestAdjustTime(t *testing.T) { |
||||||
|
sim := New(core.GenesisAlloc{}, 10_000_000) |
||||||
|
defer sim.Close() |
||||||
|
|
||||||
|
client := sim.Client() |
||||||
|
block1, _ := client.BlockByNumber(context.Background(), nil) |
||||||
|
|
||||||
|
// Create a block
|
||||||
|
if err := sim.AdjustTime(time.Minute); err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
block2, _ := client.BlockByNumber(context.Background(), nil) |
||||||
|
prevTime := block1.Time() |
||||||
|
newTime := block2.Time() |
||||||
|
if newTime-prevTime != uint64(time.Minute) { |
||||||
|
t.Errorf("adjusted time not equal to 60 seconds. prev: %v, new: %v", prevTime, newTime) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestSendTransaction(t *testing.T) { |
||||||
|
sim := simTestBackend(testAddr) |
||||||
|
defer sim.Close() |
||||||
|
|
||||||
|
client := sim.Client() |
||||||
|
ctx := context.Background() |
||||||
|
|
||||||
|
signedTx, err := newTx(sim, testKey) |
||||||
|
if err != nil { |
||||||
|
t.Errorf("could not create transaction: %v", err) |
||||||
|
} |
||||||
|
// send tx to simulated backend
|
||||||
|
err = client.SendTransaction(ctx, signedTx) |
||||||
|
if err != nil { |
||||||
|
t.Errorf("could not add tx to pending block: %v", err) |
||||||
|
} |
||||||
|
sim.Commit() |
||||||
|
block, err := client.BlockByNumber(ctx, big.NewInt(1)) |
||||||
|
if err != nil { |
||||||
|
t.Errorf("could not get block at height 1: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
if signedTx.Hash() != block.Transactions()[0].Hash() { |
||||||
|
t.Errorf("did not commit sent transaction. expected hash %v got hash %v", block.Transactions()[0].Hash(), signedTx.Hash()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TestFork check that the chain length after a reorg is correct.
|
||||||
|
// Steps:
|
||||||
|
// 1. Save the current block which will serve as parent for the fork.
|
||||||
|
// 2. Mine n blocks with n ∈ [0, 20].
|
||||||
|
// 3. Assert that the chain length is n.
|
||||||
|
// 4. Fork by using the parent block as ancestor.
|
||||||
|
// 5. Mine n+1 blocks which should trigger a reorg.
|
||||||
|
// 6. Assert that the chain length is n+1.
|
||||||
|
// Since Commit() was called 2n+1 times in total,
|
||||||
|
// having a chain length of just n+1 means that a reorg occurred.
|
||||||
|
func TestFork(t *testing.T) { |
||||||
|
t.Parallel() |
||||||
|
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) |
||||||
|
sim := simTestBackend(testAddr) |
||||||
|
defer sim.Close() |
||||||
|
|
||||||
|
client := sim.Client() |
||||||
|
ctx := context.Background() |
||||||
|
|
||||||
|
// 1.
|
||||||
|
parent, _ := client.HeaderByNumber(ctx, nil) |
||||||
|
|
||||||
|
// 2.
|
||||||
|
n := int(rand.Int31n(21)) |
||||||
|
for i := 0; i < n; i++ { |
||||||
|
sim.Commit() |
||||||
|
} |
||||||
|
|
||||||
|
// 3.
|
||||||
|
b, _ := client.BlockNumber(ctx) |
||||||
|
if b != uint64(n) { |
||||||
|
t.Error("wrong chain length") |
||||||
|
} |
||||||
|
|
||||||
|
// 4.
|
||||||
|
sim.Fork(parent.Hash()) |
||||||
|
|
||||||
|
// 5.
|
||||||
|
for i := 0; i < n+1; i++ { |
||||||
|
sim.Commit() |
||||||
|
} |
||||||
|
|
||||||
|
// 6.
|
||||||
|
b, _ = client.BlockNumber(ctx) |
||||||
|
if b != uint64(n+1) { |
||||||
|
t.Error("wrong chain length") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TestForkResendTx checks that re-sending a TX after a fork
|
||||||
|
// is possible and does not cause a "nonce mismatch" panic.
|
||||||
|
// Steps:
|
||||||
|
// 1. Save the current block which will serve as parent for the fork.
|
||||||
|
// 2. Send a transaction.
|
||||||
|
// 3. Check that the TX is included in block 1.
|
||||||
|
// 4. Fork by using the parent block as ancestor.
|
||||||
|
// 5. Mine a block, Re-send the transaction and mine another one.
|
||||||
|
// 6. Check that the TX is now included in block 2.
|
||||||
|
func TestForkResendTx(t *testing.T) { |
||||||
|
t.Parallel() |
||||||
|
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) |
||||||
|
sim := simTestBackend(testAddr) |
||||||
|
defer sim.Close() |
||||||
|
|
||||||
|
client := sim.Client() |
||||||
|
ctx := context.Background() |
||||||
|
|
||||||
|
// 1.
|
||||||
|
parent, _ := client.HeaderByNumber(ctx, nil) |
||||||
|
|
||||||
|
// 2.
|
||||||
|
tx, err := newTx(sim, testKey) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("could not create transaction: %v", err) |
||||||
|
} |
||||||
|
client.SendTransaction(ctx, tx) |
||||||
|
sim.Commit() |
||||||
|
|
||||||
|
// 3.
|
||||||
|
receipt, _ := client.TransactionReceipt(ctx, tx.Hash()) |
||||||
|
if h := receipt.BlockNumber.Uint64(); h != 1 { |
||||||
|
t.Errorf("TX included in wrong block: %d", h) |
||||||
|
} |
||||||
|
|
||||||
|
// 4.
|
||||||
|
if err := sim.Fork(parent.Hash()); err != nil { |
||||||
|
t.Errorf("forking: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// 5.
|
||||||
|
sim.Commit() |
||||||
|
if err := client.SendTransaction(ctx, tx); err != nil { |
||||||
|
t.Fatalf("sending transaction: %v", err) |
||||||
|
} |
||||||
|
sim.Commit() |
||||||
|
receipt, _ = client.TransactionReceipt(ctx, tx.Hash()) |
||||||
|
if h := receipt.BlockNumber.Uint64(); h != 2 { |
||||||
|
t.Errorf("TX included in wrong block: %d", h) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestCommitReturnValue(t *testing.T) { |
||||||
|
t.Parallel() |
||||||
|
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) |
||||||
|
sim := simTestBackend(testAddr) |
||||||
|
defer sim.Close() |
||||||
|
|
||||||
|
client := sim.Client() |
||||||
|
ctx := context.Background() |
||||||
|
|
||||||
|
// Test if Commit returns the correct block hash
|
||||||
|
h1 := sim.Commit() |
||||||
|
cur, _ := client.HeaderByNumber(ctx, nil) |
||||||
|
if h1 != cur.Hash() { |
||||||
|
t.Error("Commit did not return the hash of the last block.") |
||||||
|
} |
||||||
|
|
||||||
|
// Create a block in the original chain (containing a transaction to force different block hashes)
|
||||||
|
head, _ := client.HeaderByNumber(ctx, nil) // Should be child's, good enough
|
||||||
|
gasPrice := new(big.Int).Add(head.BaseFee, big.NewInt(1)) |
||||||
|
_tx := types.NewTransaction(0, testAddr, big.NewInt(1000), params.TxGas, gasPrice, nil) |
||||||
|
tx, _ := types.SignTx(_tx, types.HomesteadSigner{}, testKey) |
||||||
|
client.SendTransaction(ctx, tx) |
||||||
|
|
||||||
|
h2 := sim.Commit() |
||||||
|
|
||||||
|
// Create another block in the original chain
|
||||||
|
sim.Commit() |
||||||
|
|
||||||
|
// Fork at the first bock
|
||||||
|
if err := sim.Fork(h1); err != nil { |
||||||
|
t.Errorf("forking: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Test if Commit returns the correct block hash after the reorg
|
||||||
|
h2fork := sim.Commit() |
||||||
|
if h2 == h2fork { |
||||||
|
t.Error("The block in the fork and the original block are the same block!") |
||||||
|
} |
||||||
|
if header, err := client.HeaderByHash(ctx, h2fork); err != nil || header == nil { |
||||||
|
t.Error("Could not retrieve the just created block (side-chain)") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TestAdjustTimeAfterFork ensures that after a fork, AdjustTime uses the pending fork
|
||||||
|
// block's parent rather than the canonical head's parent.
|
||||||
|
func TestAdjustTimeAfterFork(t *testing.T) { |
||||||
|
t.Parallel() |
||||||
|
testAddr := crypto.PubkeyToAddress(testKey.PublicKey) |
||||||
|
sim := simTestBackend(testAddr) |
||||||
|
defer sim.Close() |
||||||
|
|
||||||
|
client := sim.Client() |
||||||
|
ctx := context.Background() |
||||||
|
|
||||||
|
sim.Commit() // h1
|
||||||
|
h1, _ := client.HeaderByNumber(ctx, nil) |
||||||
|
|
||||||
|
sim.Commit() // h2
|
||||||
|
sim.Fork(h1.Hash()) |
||||||
|
sim.AdjustTime(1 * time.Second) |
||||||
|
sim.Commit() |
||||||
|
|
||||||
|
head, _ := client.HeaderByNumber(ctx, nil) |
||||||
|
if head.Number.Uint64() == 2 && head.ParentHash != h1.Hash() { |
||||||
|
t.Errorf("failed to build block on fork") |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue