2d08c99009
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>
310 lines
8.4 KiB
Go
310 lines
8.4 KiB
Go
// 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")
|
|
}
|
|
}
|