From 373e65b8b9f71fcae51869c794e1b8d633827772 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 8 Dec 2023 11:12:21 +0100 Subject: [PATCH] ethclient/simulated: split up backend and client Also changes the method signature of Fork, removing the context parameter. To preserve backwards compatibility, a wrapper of Fork that still takes a context is introduced in the old simulated backend. --- accounts/abi/bind/backends/simulated.go | 21 ++++- eth/catalyst/simulated_beacon.go | 3 +- ethclient/simulated/backend.go | 108 ++++++++++++++---------- ethclient/simulated/backend_test.go | 81 +++++++++++------- 4 files changed, 134 insertions(+), 79 deletions(-) diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index b6ac22a37d..867e79e426 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -17,12 +17,23 @@ package backends import ( + "context" + + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/ethclient/simulated" ) +// SimulatedBackend is a simulated blockchain. +// Deprecated: use package github.com/ethereum/go-ethereum/ethclient/simulated instead. type SimulatedBackend struct { - simulated.Backend + *simulated.Backend + simulated.Client +} + +// Fork sets the head to a new block, which is based on the provided parentHash. +func (b *SimulatedBackend) Fork(ctx context.Context, parentHash common.Hash) error { + return b.Backend.Fork(parentHash) } // New creates a new binding backend using a simulated blockchain @@ -32,6 +43,10 @@ type SimulatedBackend struct { // // Deprecated: please use simulated.Backend from package // github.com/ethereum/go-ethereum/ethclient/simulated instead. -func NewSimulatedBackend(alloc core.GenesisAlloc, gasLimit uint64) SimulatedBackend { - return SimulatedBackend{simulated.New(alloc, gasLimit)} +func NewSimulatedBackend(alloc core.GenesisAlloc, gasLimit uint64) *SimulatedBackend { + b := simulated.New(alloc, gasLimit) + return &SimulatedBackend{ + Backend: b, + Client: b.Client(), + } } diff --git a/eth/catalyst/simulated_beacon.go b/eth/catalyst/simulated_beacon.go index 2f5d685a3b..84ebffc80a 100644 --- a/eth/catalyst/simulated_beacon.go +++ b/eth/catalyst/simulated_beacon.go @@ -17,7 +17,6 @@ package catalyst import ( - "context" "crypto/rand" "errors" "math" @@ -293,7 +292,7 @@ func (c *SimulatedBeacon) Rollback() { } // Fork sets the head to the provided hash. -func (c *SimulatedBeacon) Fork(ctx context.Context, parentHash common.Hash) error { +func (c *SimulatedBeacon) Fork(parentHash common.Hash) error { if len(c.eth.TxPool().Pending(false)) != 0 { return errors.New("pending block dirty") } diff --git a/ethclient/simulated/backend.go b/ethclient/simulated/backend.go index 71afddd42d..a415ce46c8 100644 --- a/ethclient/simulated/backend.go +++ b/ethclient/simulated/backend.go @@ -17,7 +17,6 @@ package simulated import ( - "context" "math" "time" @@ -37,32 +36,24 @@ import ( "github.com/ethereum/go-ethereum/rpc" ) -var _ bind.ContractBackend = (Backend)(nil) -var _ bind.ContractBackend = (*simBackend)(nil) +var _ bind.ContractBackend = (Client)(nil) -type SimChainManagement interface { - // Commit seals a block and moves the chain forward to a new empty block. - Commit() common.Hash - - // Rollback un-sends previously added transactions. - Rollback() - - // Fork sets the head to a new block, which is based on the provided parentHash. - Fork(ctx context.Context, parentHash common.Hash) error - - // AdjustTime changes the block timestamp. - AdjustTime(adjustment time.Duration) error - - // Close closes the backend. You need to call this to clean up resources. - Close() error +// 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 } -// Backend all interfaces in the ethereum package, but is based on a -// simulated blockchain. It is intended for testing purposes. -type Backend interface { - SimChainManagement +// simClient wraps ethclient. This exists to prevent extracting ethclient.Client from the +// Client interface returned by Backend. +type simClient struct { + *ethclient.Client +} - // The backend implements all interfaces in the ethereum package. +// Client exposes the methods provided by the Ethereum RPC client. +type Client interface { ethereum.BlockNumberReader ethereum.ChainReader ethereum.ChainStateReader @@ -78,16 +69,10 @@ type Backend interface { ethereum.ChainIDReader } -type simBackend struct { - eth *eth.Ethereum - *catalyst.SimulatedBeacon - *ethclient.Client -} - // 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 { +func New(alloc core.GenesisAlloc, gasLimit uint64) *Backend { // Setup the node object nodeConf := node.DefaultConfig nodeConf.DataDir = "" @@ -118,7 +103,7 @@ func New(alloc core.GenesisAlloc, gasLimit uint64) Backend { // 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) { +func NewWithNode(stack *node.Node, conf *eth.Config, blockPeriod uint64) (*Backend, error) { backend, err := eth.New(stack, conf) if err != nil { return nil, err @@ -143,26 +128,63 @@ func NewWithNode(stack *node.Node, conf *eth.Config, blockPeriod uint64) (Backen } // Reorg our chain back to genesis - if err := beacon.Fork(context.Background(), backend.BlockChain().GetCanonicalHash(0)); err != nil { + if err := beacon.Fork(backend.BlockChain().GetCanonicalHash(0)); err != nil { return nil, err } - return &simBackend{ - eth: backend, - SimulatedBeacon: beacon, - Client: ethclient.NewClient(stack.Attach()), + return &Backend{ + eth: backend, + beacon: beacon, + client: simClient{ethclient.NewClient(stack.Attach())}, }, nil } -func (n *simBackend) Close() error { - if n.Client != nil { - n.Client.Close() - n.Client = nil +func (n *Backend) Close() error { + if n.client.Client != nil { + n.client.Close() + n.client = simClient{} } - if n.SimulatedBeacon != nil { - err := n.SimulatedBeacon.Stop() - n.SimulatedBeacon = nil + 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. +// 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 +} diff --git a/ethclient/simulated/backend_test.go b/ethclient/simulated/backend_test.go index fe315d2229..0f55d4b50f 100644 --- a/ethclient/simulated/backend_test.go +++ b/ethclient/simulated/backend_test.go @@ -36,7 +36,7 @@ var ( testAddr = crypto.PubkeyToAddress(testKey.PublicKey) ) -func simTestBackend(testAddr common.Address) Backend { +func simTestBackend(testAddr common.Address) *Backend { return New( core.GenesisAlloc{ testAddr: {Balance: big.NewInt(10000000000000000)}, @@ -44,13 +44,15 @@ func simTestBackend(testAddr common.Address) Backend { ) } -func newTx(sim Backend, key *ecdsa.PrivateKey) (*types.Transaction, error) { +func newTx(sim *Backend, key *ecdsa.PrivateKey) (*types.Transaction, error) { + client := sim.Client() + // create a signed transaction to send - head, _ := sim.HeaderByNumber(context.Background(), nil) // Should be child's, good enough + 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, _ := sim.ChainID(context.Background()) - nonce, err := sim.PendingNonceAt(context.Background(), addr) + chainid, _ := client.ChainID(context.Background()) + nonce, err := client.PendingNonceAt(context.Background(), addr) if err != nil { return nil, err } @@ -68,7 +70,10 @@ func newTx(sim Backend, key *ecdsa.PrivateKey) (*types.Transaction, error) { func TestNewSim(t *testing.T) { sim := New(core.GenesisAlloc{}, 30_000_000) - num, err := sim.BlockNumber(context.Background()) + defer sim.Close() + + client := sim.Client() + num, err := client.BlockNumber(context.Background()) if err != nil { t.Fatal(err) } @@ -77,7 +82,7 @@ func TestNewSim(t *testing.T) { } // Create a block sim.Commit() - num, err = sim.BlockNumber(context.Background()) + num, err = client.BlockNumber(context.Background()) if err != nil { t.Fatal(err) } @@ -90,12 +95,14 @@ func TestAdjustTime(t *testing.T) { sim := New(core.GenesisAlloc{}, 10_000_000) defer sim.Close() - block1, _ := sim.BlockByNumber(context.Background(), nil) + client := sim.Client() + block1, _ := client.BlockByNumber(context.Background(), nil) + // Create a block if err := sim.AdjustTime(time.Minute); err != nil { t.Fatal(err) } - block2, _ := sim.BlockByNumber(context.Background(), nil) + block2, _ := client.BlockByNumber(context.Background(), nil) prevTime := block1.Time() newTime := block2.Time() if newTime-prevTime != uint64(time.Minute) { @@ -106,19 +113,21 @@ func TestAdjustTime(t *testing.T) { func TestSendTransaction(t *testing.T) { sim := simTestBackend(testAddr) defer sim.Close() - bgCtx := context.Background() + + 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 = sim.SendTransaction(bgCtx, signedTx) + err = client.SendTransaction(ctx, signedTx) if err != nil { t.Errorf("could not add tx to pending block: %v", err) } sim.Commit() - block, err := sim.BlockByNumber(bgCtx, big.NewInt(1)) + block, err := client.BlockByNumber(ctx, big.NewInt(1)) if err != nil { t.Errorf("could not get block at height 1: %v", err) } @@ -144,10 +153,11 @@ func TestFork(t *testing.T) { sim := simTestBackend(testAddr) defer sim.Close() + client := sim.Client() ctx := context.Background() // 1. - parent, _ := sim.HeaderByNumber(ctx, nil) + parent, _ := client.HeaderByNumber(ctx, nil) // 2. n := int(rand.Int31n(21)) @@ -156,13 +166,13 @@ func TestFork(t *testing.T) { } // 3. - b, _ := sim.BlockNumber(ctx) + b, _ := client.BlockNumber(ctx) if b != uint64(n) { t.Error("wrong chain length") } // 4. - sim.Fork(ctx, parent.Hash()) + sim.Fork(parent.Hash()) // 5. for i := 0; i < n+1; i++ { @@ -170,7 +180,7 @@ func TestFork(t *testing.T) { } // 6. - b, _ = sim.BlockNumber(ctx) + b, _ = client.BlockNumber(ctx) if b != uint64(n+1) { t.Error("wrong chain length") } @@ -191,36 +201,38 @@ func TestForkResendTx(t *testing.T) { sim := simTestBackend(testAddr) defer sim.Close() - // 1. + client := sim.Client() ctx := context.Background() - parent, _ := sim.HeaderByNumber(ctx, nil) + + // 1. + parent, _ := client.HeaderByNumber(ctx, nil) // 2. tx, err := newTx(sim, testKey) if err != nil { t.Fatalf("could not create transaction: %v", err) } - sim.SendTransaction(context.Background(), tx) + client.SendTransaction(ctx, tx) sim.Commit() // 3. - receipt, _ := sim.TransactionReceipt(context.Background(), tx.Hash()) + 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(context.Background(), parent.Hash()); err != nil { + if err := sim.Fork(parent.Hash()); err != nil { t.Errorf("forking: %v", err) } // 5. sim.Commit() - if err := sim.SendTransaction(context.Background(), tx); err != nil { + if err := client.SendTransaction(ctx, tx); err != nil { t.Fatalf("sending transaction: %v", err) } sim.Commit() - receipt, _ = sim.TransactionReceipt(context.Background(), tx.Hash()) + receipt, _ = client.TransactionReceipt(ctx, tx.Hash()) if h := receipt.BlockNumber.Uint64(); h != 2 { t.Errorf("TX included in wrong block: %d", h) } @@ -232,26 +244,30 @@ func TestCommitReturnValue(t *testing.T) { sim := simTestBackend(testAddr) defer sim.Close() + client := sim.Client() + ctx := context.Background() + // Test if Commit returns the correct block hash h1 := sim.Commit() - cur, _ := sim.HeaderByNumber(context.Background(), nil) + 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, _ := sim.HeaderByNumber(context.Background(), nil) // Should be child's, good enough + 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) - sim.SendTransaction(context.Background(), tx) + 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(context.Background(), h1); err != nil { + if err := sim.Fork(h1); err != nil { t.Errorf("forking: %v", err) } @@ -260,7 +276,7 @@ func TestCommitReturnValue(t *testing.T) { if h2 == h2fork { t.Error("The block in the fork and the original block are the same block!") } - if header, err := sim.HeaderByHash(context.Background(), h2fork); err != nil || header == nil { + if header, err := client.HeaderByHash(ctx, h2fork); err != nil || header == nil { t.Error("Could not retrieve the just created block (side-chain)") } } @@ -273,15 +289,18 @@ func TestAdjustTimeAfterFork(t *testing.T) { sim := simTestBackend(testAddr) defer sim.Close() + client := sim.Client() + ctx := context.Background() + sim.Commit() // h1 - h1, _ := sim.HeaderByNumber(context.Background(), nil) + h1, _ := client.HeaderByNumber(ctx, nil) sim.Commit() // h2 - sim.Fork(context.Background(), h1.Hash()) + sim.Fork(h1.Hash()) sim.AdjustTime(1 * time.Second) sim.Commit() - head, _ := sim.HeaderByNumber(context.Background(), nil) + head, _ := client.HeaderByNumber(ctx, nil) if head.Number.Uint64() == 2 && head.ParentHash != h1.Hash() { t.Errorf("failed to build block on fork") }