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.
This commit is contained in:
Felix Lange 2023-12-08 11:12:21 +01:00 committed by Marius van der Wijden
parent b5da90d38c
commit 373e65b8b9
4 changed files with 134 additions and 79 deletions

View file

@ -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(),
}
}

View file

@ -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")
}

View file

@ -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{
return &Backend{
eth: backend,
SimulatedBeacon: beacon,
Client: ethclient.NewClient(stack.Attach()),
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
}

View file

@ -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")
}