go-ethereum/tests/bor/bor_test.go

1148 lines
35 KiB
Go

//go:build integration
// +build integration
package bor
import (
"context"
"crypto/ecdsa"
"encoding/hex"
"fmt"
"io"
"math/big"
"os"
"sync"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/sha3"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/fdlimit"
"github.com/ethereum/go-ethereum/consensus/bor"
"github.com/ethereum/go-ethereum/consensus/bor/clerk"
"github.com/ethereum/go-ethereum/consensus/bor/heimdall/checkpoint"
"github.com/ethereum/go-ethereum/consensus/bor/valset"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/tests/bor/mocks"
)
var (
// addr1 = 0x71562b71999873DB5b286dF957af199Ec94617F7
pkey1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
// addr2 = 0x9fB29AAc15b9A4B7F17c3385939b007540f4d791
pkey2, _ = crypto.HexToECDSA("9b28f36fbd67381120752d6172ecdcf10e06ab2d9a1367aac00cdcd6ac7855d3")
)
func TestValidatorWentOffline(t *testing.T) {
log.Root().SetHandler(log.LvlFilterHandler(log.LvlInfo, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
fdlimit.Raise(2048)
// Generate a batch of accounts to seal and fund with
faucets := make([]*ecdsa.PrivateKey, 128)
for i := 0; i < len(faucets); i++ {
faucets[i], _ = crypto.GenerateKey()
}
// Create an Ethash network based off of the Ropsten config
// Generate a batch of accounts to seal and fund with
genesis := InitGenesis(t, faucets, "./testdata/genesis_2val.json", 8)
var (
stacks []*node.Node
nodes []*eth.Ethereum
enodes []*enode.Node
)
for i := 0; i < 2; i++ {
// Start the node and wait until it's up
stack, ethBackend, err := InitMiner(genesis, keys[i], true)
if err != nil {
panic(err)
}
defer stack.Close()
for stack.Server().NodeInfo().Ports.Listener == 0 {
time.Sleep(250 * time.Millisecond)
}
// Connect the node to all the previous ones
for _, n := range enodes {
stack.Server().AddPeer(n)
}
// Start tracking the node and its enode
stacks = append(stacks, stack)
nodes = append(nodes, ethBackend)
enodes = append(enodes, stack.Server().Self())
}
// Iterate over all the nodes and start mining
time.Sleep(3 * time.Second)
for _, node := range nodes {
if err := node.StartMining(1); err != nil {
panic(err)
}
}
for {
// for block 1 to 8, the primary validator is node0
// for block 9 to 16, the primary validator is node1
// for block 17 to 24, the primary validator is node0
// for block 25 to 32, the primary validator is node1
blockHeaderVal0 := nodes[0].BlockChain().CurrentHeader()
// we remove peer connection between node0 and node1
if blockHeaderVal0.Number.Uint64() == 9 {
stacks[0].Server().RemovePeer(enodes[1])
}
// here, node1 is the primary validator, node0 will sign out-of-turn
// we add peer connection between node1 and node0
if blockHeaderVal0.Number.Uint64() == 14 {
stacks[0].Server().AddPeer(enodes[1])
}
// reorg happens here, node1 has higher difficulty, it will replace blocks by node0
if blockHeaderVal0.Number.Uint64() == 30 {
break
}
}
// check block 10 miner ; expected author is node1 signer
blockHeaderVal0 := nodes[0].BlockChain().GetHeaderByNumber(10)
blockHeaderVal1 := nodes[1].BlockChain().GetHeaderByNumber(10)
authorVal0, err := nodes[0].Engine().Author(blockHeaderVal0)
if err != nil {
log.Error("Error in getting author", "err", err)
}
authorVal1, err := nodes[1].Engine().Author(blockHeaderVal1)
if err != nil {
log.Error("Error in getting author", "err", err)
}
// check both nodes have the same block 10
assert.Equal(t, authorVal0, authorVal1)
// check node0 has block mined by node1
assert.Equal(t, authorVal0, nodes[1].AccountManager().Accounts()[0])
// check node1 has block mined by node1
assert.Equal(t, authorVal1, nodes[1].AccountManager().Accounts()[0])
// check block 11 miner ; expected author is node1 signer
blockHeaderVal0 = nodes[0].BlockChain().GetHeaderByNumber(11)
blockHeaderVal1 = nodes[1].BlockChain().GetHeaderByNumber(11)
authorVal0, err = nodes[0].Engine().Author(blockHeaderVal0)
if err != nil {
log.Error("Error in getting author", "err", err)
}
authorVal1, err = nodes[1].Engine().Author(blockHeaderVal1)
if err != nil {
log.Error("Error in getting author", "err", err)
}
// check both nodes have the same block 11
assert.Equal(t, authorVal0, authorVal1)
// check node0 has block mined by node1
assert.Equal(t, authorVal0, nodes[1].AccountManager().Accounts()[0])
// check node1 has block mined by node1
assert.Equal(t, authorVal1, nodes[1].AccountManager().Accounts()[0])
// check block 12 miner ; expected author is node1 signer
blockHeaderVal0 = nodes[0].BlockChain().GetHeaderByNumber(12)
blockHeaderVal1 = nodes[1].BlockChain().GetHeaderByNumber(12)
authorVal0, err = nodes[0].Engine().Author(blockHeaderVal0)
if err != nil {
log.Error("Error in getting author", "err", err)
}
authorVal1, err = nodes[1].Engine().Author(blockHeaderVal1)
if err != nil {
log.Error("Error in getting author", "err", err)
}
// check both nodes have the same block 12
assert.Equal(t, authorVal0, authorVal1)
// check node0 has block mined by node1
assert.Equal(t, authorVal0, nodes[1].AccountManager().Accounts()[0])
// check node1 has block mined by node1
assert.Equal(t, authorVal1, nodes[1].AccountManager().Accounts()[0])
// check block 17 miner ; expected author is node0 signer
blockHeaderVal0 = nodes[0].BlockChain().GetHeaderByNumber(17)
blockHeaderVal1 = nodes[1].BlockChain().GetHeaderByNumber(17)
authorVal0, err = nodes[0].Engine().Author(blockHeaderVal0)
if err != nil {
log.Error("Error in getting author", "err", err)
}
authorVal1, err = nodes[1].Engine().Author(blockHeaderVal1)
if err != nil {
log.Error("Error in getting author", "err", err)
}
// check both nodes have the same block 17
assert.Equal(t, authorVal0, authorVal1)
// check node0 has block mined by node1
assert.Equal(t, authorVal0, nodes[0].AccountManager().Accounts()[0])
// check node1 has block mined by node1
assert.Equal(t, authorVal1, nodes[0].AccountManager().Accounts()[0])
}
func TestForkWithBlockTime(t *testing.T) {
cases := []struct {
name string
sprint map[string]uint64
blockTime map[string]uint64
change uint64
producerDelay map[string]uint64
forkExpected bool
}{
{
name: "No fork after 2 sprints with producer delay = max block time",
sprint: map[string]uint64{
"0": 128,
},
blockTime: map[string]uint64{
"0": 5,
"128": 2,
"256": 8,
},
change: 2,
producerDelay: map[string]uint64{
"0": 8,
},
forkExpected: false,
},
{
name: "No Fork after 1 sprint producer delay = max block time",
sprint: map[string]uint64{
"0": 64,
},
blockTime: map[string]uint64{
"0": 5,
"64": 2,
},
change: 1,
producerDelay: map[string]uint64{
"0": 5,
},
forkExpected: false,
},
{
name: "Fork after 4 sprints with producer delay < max block time",
sprint: map[string]uint64{
"0": 16,
},
blockTime: map[string]uint64{
"0": 2,
"64": 5,
},
change: 4,
producerDelay: map[string]uint64{
"0": 4,
},
forkExpected: true,
},
}
// Create an Ethash network based off of the Ropsten config
// Generate a batch of accounts to seal and fund with
faucets := make([]*ecdsa.PrivateKey, 128)
for i := 0; i < len(faucets); i++ {
faucets[i], _ = crypto.GenerateKey()
}
genesis := InitGenesis(t, faucets, "./testdata/genesis_2val.json", 8)
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
genesis.Config.Bor.Sprint = test.sprint
genesis.Config.Bor.Period = test.blockTime
genesis.Config.Bor.BackupMultiplier = test.blockTime
genesis.Config.Bor.ProducerDelay = test.producerDelay
stacks, nodes, _ := setupMiner(t, 2, genesis)
defer func() {
for _, stack := range stacks {
stack.Close()
}
}()
// Iterate over all the nodes and start mining
for _, node := range nodes {
if err := node.StartMining(1); err != nil {
t.Fatal("Error occured while starting miner", "node", node, "error", err)
}
}
var wg sync.WaitGroup
blockHeaders := make([]*types.Header, 2)
ticker := time.NewTicker(time.Duration(test.blockTime["0"]) * time.Second)
defer ticker.Stop()
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
for range ticker.C {
blockHeaders[i] = nodes[i].BlockChain().GetHeaderByNumber(test.sprint["0"]*test.change + 10)
if blockHeaders[i] != nil {
break
}
}
}(i)
}
wg.Wait()
// Before the end of sprint
blockHeaderVal0 := nodes[0].BlockChain().GetHeaderByNumber(test.sprint["0"] - 1)
blockHeaderVal1 := nodes[1].BlockChain().GetHeaderByNumber(test.sprint["0"] - 1)
assert.Equal(t, blockHeaderVal0.Hash(), blockHeaderVal1.Hash())
assert.Equal(t, blockHeaderVal0.Time, blockHeaderVal1.Time)
author0, err := nodes[0].Engine().Author(blockHeaderVal0)
if err != nil {
t.Error("Error occured while fetching author", "err", err)
}
author1, err := nodes[1].Engine().Author(blockHeaderVal1)
if err != nil {
t.Error("Error occured while fetching author", "err", err)
}
assert.Equal(t, author0, author1)
// After the end of sprint
author2, err := nodes[0].Engine().Author(blockHeaders[0])
if err != nil {
t.Error("Error occured while fetching author", "err", err)
}
author3, err := nodes[1].Engine().Author(blockHeaders[1])
if err != nil {
t.Error("Error occured while fetching author", "err", err)
}
if test.forkExpected {
assert.NotEqual(t, blockHeaders[0].Hash(), blockHeaders[1].Hash())
assert.NotEqual(t, blockHeaders[0].Time, blockHeaders[1].Time)
assert.NotEqual(t, author2, author3)
} else {
assert.Equal(t, blockHeaders[0].Hash(), blockHeaders[1].Hash())
assert.Equal(t, blockHeaders[0].Time, blockHeaders[1].Time)
assert.Equal(t, author2, author3)
}
})
}
}
func TestInsertingSpanSizeBlocks(t *testing.T) {
init := buildEthereumInstance(t, rawdb.NewMemoryDatabase())
chain := init.ethereum.BlockChain()
engine := init.ethereum.Engine()
_bor := engine.(*bor.Bor)
defer _bor.Close()
_, currentSpan := loadSpanFromFile(t)
h, ctrl := getMockedHeimdallClient(t, currentSpan)
defer ctrl.Finish()
h.EXPECT().Close().AnyTimes()
h.EXPECT().FetchCheckpoint(gomock.Any(), int64(-1)).Return(&checkpoint.Checkpoint{
Proposer: currentSpan.SelectedProducers[0].Address,
StartBlock: big.NewInt(0),
EndBlock: big.NewInt(int64(spanSize)),
}, nil).AnyTimes()
_bor.SetHeimdallClient(h)
block := init.genesis.ToBlock()
// to := int64(block.Header().Time)
currentValidators := []*valset.Validator{valset.NewValidator(addr, 10)}
spanner := getMockedSpanner(t, currentValidators)
_bor.SetSpanner(spanner)
// Insert sprintSize # of blocks so that span is fetched at the start of a new sprint
for i := uint64(1); i <= spanSize; i++ {
block = buildNextBlock(t, _bor, chain, block, nil, init.genesis.Config.Bor, nil, currentValidators)
insertNewBlock(t, chain, block)
}
spanner = getMockedSpanner(t, currentSpan.ValidatorSet.Validators)
_bor.SetSpanner(spanner)
validators, err := _bor.GetCurrentValidators(context.Background(), block.Hash(), spanSize) // check validator set at the first block of new span
if err != nil {
t.Fatalf("%s", err)
}
require.Equal(t, 3, len(validators))
for i, validator := range validators {
require.Equal(t, validator.Address.Bytes(), currentSpan.SelectedProducers[i].Address.Bytes())
require.Equal(t, validator.VotingPower, currentSpan.SelectedProducers[i].VotingPower)
}
}
func TestFetchStateSyncEvents(t *testing.T) {
init := buildEthereumInstance(t, rawdb.NewMemoryDatabase())
chain := init.ethereum.BlockChain()
engine := init.ethereum.Engine()
_bor := engine.(*bor.Bor)
defer _bor.Close()
// A. Insert blocks for 0th sprint
block := init.genesis.ToBlock()
// B.1 Mock /bor/span/1
res, _ := loadSpanFromFile(t)
currentValidators := []*valset.Validator{valset.NewValidator(addr, 10)}
spanner := getMockedSpanner(t, currentValidators)
_bor.SetSpanner(spanner)
// Insert sprintSize # of blocks so that span is fetched at the start of a new sprint
for i := uint64(1); i < sprintSize; i++ {
if IsSpanEnd(i) {
currentValidators = res.Result.ValidatorSet.Validators
}
block = buildNextBlock(t, _bor, chain, block, nil, init.genesis.Config.Bor, nil, currentValidators)
insertNewBlock(t, chain, block)
}
// B. Before inserting 1st block of the next sprint, mock heimdall deps
ctrl := gomock.NewController(t)
defer ctrl.Finish()
h := mocks.NewMockIHeimdallClient(ctrl)
h.EXPECT().Close().AnyTimes()
h.EXPECT().Span(gomock.Any(), uint64(1)).Return(&res.Result, nil).AnyTimes()
// B.2 Mock State Sync events
fromID := uint64(1)
// at # sprintSize, events are fetched for [fromID, (block-sprint).Time)
to := int64(chain.GetHeaderByNumber(0).Time)
eventCount := 50
sample := getSampleEventRecord(t)
sample.Time = time.Unix(to-int64(eventCount+1), 0) // last event.Time will be just < to
eventRecords := generateFakeStateSyncEvents(sample, eventCount)
h.EXPECT().StateSyncEvents(gomock.Any(), fromID, to).Return(eventRecords, nil).AnyTimes()
_bor.SetHeimdallClient(h)
block = buildNextBlock(t, _bor, chain, block, nil, init.genesis.Config.Bor, nil, res.Result.ValidatorSet.Validators)
// Validate the state sync transactions set by consensus
validateStateSyncEvents(t, eventRecords, chain.GetStateSync())
insertNewBlock(t, chain, block)
}
func validateStateSyncEvents(t *testing.T, expected []*clerk.EventRecordWithTime, got []*types.StateSyncData) {
require.Equal(t, len(expected), len(got), "number of state sync events should be equal")
for i := 0; i < len(expected); i++ {
require.Equal(t, expected[i].ID, got[i].ID, fmt.Sprintf("state sync ids should be equal - index: %d, expected: %d, got: %d", i, expected[i].ID, got[i].ID))
}
}
func TestFetchStateSyncEvents_2(t *testing.T) {
init := buildEthereumInstance(t, rawdb.NewMemoryDatabase())
chain := init.ethereum.BlockChain()
engine := init.ethereum.Engine()
_bor := engine.(*bor.Bor)
defer _bor.Close()
// Mock /bor/span/1
res, _ := loadSpanFromFile(t)
// add the block producer
res.Result.ValidatorSet.Validators = append(res.Result.ValidatorSet.Validators, valset.NewValidator(addr, 4500))
ctrl := gomock.NewController(t)
defer ctrl.Finish()
h := mocks.NewMockIHeimdallClient(ctrl)
h.EXPECT().Close().AnyTimes()
h.EXPECT().Span(gomock.Any(), uint64(1)).Return(&res.Result, nil).AnyTimes()
// Mock State Sync events
// at # sprintSize, events are fetched for [fromID, (block-sprint).Time)
fromID := uint64(1)
to := int64(chain.GetHeaderByNumber(0).Time)
sample := getSampleEventRecord(t)
// First query will be from [id=1, (block-sprint).Time]
// Insert 5 events in this time range
eventRecords := []*clerk.EventRecordWithTime{
buildStateEvent(sample, 1, 3), // id = 1, time = 1
buildStateEvent(sample, 2, 1), // id = 2, time = 3
buildStateEvent(sample, 3, 2), // id = 3, time = 2
// event with id 5 is missing
buildStateEvent(sample, 4, 5), // id = 4, time = 5
buildStateEvent(sample, 6, 4), // id = 6, time = 4
}
h.EXPECT().StateSyncEvents(gomock.Any(), fromID, to).Return(eventRecords, nil).AnyTimes()
_bor.SetHeimdallClient(h)
// Insert blocks for 0th sprint
block := init.genesis.ToBlock()
var currentValidators []*valset.Validator
for i := uint64(1); i <= sprintSize; i++ {
if IsSpanEnd(i) {
currentValidators = res.Result.ValidatorSet.Validators
} else {
currentValidators = []*valset.Validator{valset.NewValidator(addr, 10)}
}
spanner := getMockedSpanner(t, currentValidators)
_bor.SetSpanner(spanner)
block = buildNextBlock(t, _bor, chain, block, nil, init.genesis.Config.Bor, nil, currentValidators)
insertNewBlock(t, chain, block)
}
lastStateID, _ := _bor.GenesisContractsClient.LastStateId(nil, sprintSize, block.Hash())
// state 6 was not written
require.Equal(t, uint64(4), lastStateID.Uint64())
//
fromID = uint64(5)
to = int64(chain.GetHeaderByNumber(sprintSize).Time)
eventRecords = []*clerk.EventRecordWithTime{
buildStateEvent(sample, 5, 7),
buildStateEvent(sample, 6, 4),
}
h.EXPECT().StateSyncEvents(gomock.Any(), fromID, to).Return(eventRecords, nil).AnyTimes()
for i := sprintSize + 1; i <= spanSize; i++ {
if IsSpanEnd(i) {
currentValidators = res.Result.ValidatorSet.Validators
} else {
currentValidators = []*valset.Validator{valset.NewValidator(addr, 10)}
}
spanner := getMockedSpanner(t, currentValidators)
_bor.SetSpanner(spanner)
block = buildNextBlock(t, _bor, chain, block, nil, init.genesis.Config.Bor, nil, res.Result.ValidatorSet.Validators)
insertNewBlock(t, chain, block)
}
lastStateID, _ = _bor.GenesisContractsClient.LastStateId(nil, spanSize, block.Hash())
require.Equal(t, uint64(6), lastStateID.Uint64())
}
func TestOutOfTurnSigning(t *testing.T) {
init := buildEthereumInstance(t, rawdb.NewMemoryDatabase())
chain := init.ethereum.BlockChain()
engine := init.ethereum.Engine()
_bor := engine.(*bor.Bor)
defer _bor.Close()
_, heimdallSpan := loadSpanFromFile(t)
proposer := valset.NewValidator(addr, 10)
heimdallSpan.ValidatorSet.Validators = append(heimdallSpan.ValidatorSet.Validators, proposer)
// add the block producer
h, ctrl := getMockedHeimdallClient(t, heimdallSpan)
defer ctrl.Finish()
h.EXPECT().Close().AnyTimes()
spanner := getMockedSpanner(t, heimdallSpan.ValidatorSet.Validators)
_bor.SetSpanner(spanner)
_bor.SetHeimdallClient(h)
db := init.ethereum.ChainDb()
block := init.genesis.ToBlock(db)
setDifficulty := func(header *types.Header) {
if IsSprintStart(header.Number.Uint64()) {
header.Difficulty = big.NewInt(int64(len(heimdallSpan.ValidatorSet.Validators)))
}
}
for i := uint64(1); i < spanSize; i++ {
block = buildNextBlock(t, _bor, chain, block, nil, init.genesis.Config.Bor, nil, heimdallSpan.ValidatorSet.Validators, setDifficulty)
insertNewBlock(t, chain, block)
}
// insert spanSize-th block
// This account is one the out-of-turn validators for 1st (0-indexed) span
signer := "c8deb0bea5c41afe8e37b4d1bd84e31adff11b09c8c96ff4b605003cce067cd9"
signerKey, _ := hex.DecodeString(signer)
newKey, _ := crypto.HexToECDSA(signer)
newAddr := crypto.PubkeyToAddress(newKey.PublicKey)
expectedSuccessionNumber := 2
parentTime := block.Time()
setParentTime := func(header *types.Header) {
header.Time = parentTime + 1
}
const turn = 1
setDifficulty = func(header *types.Header) {
header.Difficulty = big.NewInt(int64(len(heimdallSpan.ValidatorSet.Validators)) - turn)
}
block = buildNextBlock(t, _bor, chain, block, signerKey, init.genesis.Config.Bor, nil, heimdallSpan.ValidatorSet.Validators, setParentTime, setDifficulty)
_, err := chain.InsertChain([]*types.Block{block})
require.Equal(t,
bor.BlockTooSoonError{Number: spanSize, Succession: expectedSuccessionNumber},
*err.(*bor.BlockTooSoonError))
expectedDifficulty := uint64(len(heimdallSpan.ValidatorSet.Validators) - expectedSuccessionNumber - turn) // len(validators) - succession
header := block.Header()
diff := bor.CalcProducerDelay(header.Number.Uint64(), expectedSuccessionNumber, init.genesis.Config.Bor)
header.Time += diff
sign(t, header, signerKey, init.genesis.Config.Bor)
block = types.NewBlockWithHeader(header)
_, err = chain.InsertChain([]*types.Block{block})
require.NotNil(t, err)
require.Equal(t,
bor.WrongDifficultyError{Number: spanSize, Expected: expectedDifficulty, Actual: 3, Signer: newAddr.Bytes()},
*err.(*bor.WrongDifficultyError))
header.Difficulty = new(big.Int).SetUint64(expectedDifficulty)
sign(t, header, signerKey, init.genesis.Config.Bor)
block = types.NewBlockWithHeader(header)
_, err = chain.InsertChain([]*types.Block{block})
require.Nil(t, err)
}
func TestSignerNotFound(t *testing.T) {
init := buildEthereumInstance(t, rawdb.NewMemoryDatabase())
chain := init.ethereum.BlockChain()
engine := init.ethereum.Engine()
_bor := engine.(*bor.Bor)
defer _bor.Close()
_, heimdallSpan := loadSpanFromFile(t)
h, ctrl := getMockedHeimdallClient(t, heimdallSpan)
defer ctrl.Finish()
h.EXPECT().Close().AnyTimes()
_bor.SetHeimdallClient(h)
block := init.genesis.ToBlock()
// random signer account that is not a part of the validator set
const signer = "3714d99058cd64541433d59c6b391555b2fd9b54629c2b717a6c9c00d1127b6b"
signerKey, _ := hex.DecodeString(signer)
newKey, _ := crypto.HexToECDSA(signer)
newAddr := crypto.PubkeyToAddress(newKey.PublicKey)
_bor.Authorize(newAddr, func(account accounts.Account, s string, data []byte) ([]byte, error) {
return crypto.Sign(crypto.Keccak256(data), newKey)
})
block = buildNextBlock(t, _bor, chain, block, signerKey, init.genesis.Config.Bor, nil, heimdallSpan.ValidatorSet.Validators)
_, err := chain.InsertChain([]*types.Block{block})
require.Equal(t,
*err.(*bor.UnauthorizedSignerError),
bor.UnauthorizedSignerError{Number: 0, Signer: newAddr.Bytes()})
}
// TestEIP1559Transition tests the following:
//
// 1. A transaction whose gasFeeCap is greater than the baseFee is valid.
// 2. Gas accounting for access lists on EIP-1559 transactions is correct.
// 3. Only the transaction's tip will be received by the coinbase.
// 4. The transaction sender pays for both the tip and baseFee.
// 5. The coinbase receives only the partially realized tip when
// gasFeeCap - gasTipCap < baseFee.
// 6. Legacy transaction behave as expected (e.g. gasPrice = gasFeeCap = gasTipCap).
func TestEIP1559Transition(t *testing.T) {
var (
aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa")
// Generate a canonical chain to act as the main dataset
db = rawdb.NewMemoryDatabase()
engine = ethash.NewFaker()
// A sender who makes transactions, has some funds
key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
key2, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a")
key3, _ = crypto.HexToECDSA("225171aed3793cba1c029832886d69785b7e77a54a44211226b447aa2d16b058")
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
addr2 = crypto.PubkeyToAddress(key2.PublicKey)
addr3 = crypto.PubkeyToAddress(key3.PublicKey)
funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether))
gspec = &core.Genesis{
Config: params.BorUnittestChainConfig,
Alloc: core.GenesisAlloc{
addr1: {Balance: funds},
addr2: {Balance: funds},
addr3: {Balance: funds},
// The address 0xAAAA sloads 0x00 and 0x01
aa: {
Code: []byte{
byte(vm.PC),
byte(vm.PC),
byte(vm.SLOAD),
byte(vm.SLOAD),
},
Nonce: 0,
Balance: big.NewInt(0),
},
},
}
)
gspec.Config.BerlinBlock = common.Big0
gspec.Config.LondonBlock = common.Big0
genesis := gspec.MustCommit(db)
signer := types.LatestSigner(gspec.Config)
blocks, _ := core.GenerateChain(gspec.Config, genesis, engine, db, 1, func(i int, b *core.BlockGen) {
b.SetCoinbase(common.Address{1})
// One transaction to 0xAAAA
accesses := types.AccessList{types.AccessTuple{
Address: aa,
StorageKeys: []common.Hash{{0}},
}}
txdata := &types.DynamicFeeTx{
ChainID: gspec.Config.ChainID,
Nonce: 0,
To: &aa,
Gas: 30000,
GasFeeCap: newGwei(5),
GasTipCap: big.NewInt(2),
AccessList: accesses,
Data: []byte{},
}
tx := types.NewTx(txdata)
tx, _ = types.SignTx(tx, signer, key1)
b.AddTx(tx)
})
diskdb := rawdb.NewMemoryDatabase()
gspec.MustCommit(diskdb)
chain, err := core.NewBlockChain(diskdb, nil, gspec, nil, engine, vm.Config{}, nil, nil, nil)
if err != nil {
t.Fatalf("failed to create tester chain: %v", err)
}
if n, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
block := chain.GetBlockByNumber(1)
// 1+2: Ensure EIP-1559 access lists are accounted for via gas usage.
expectedGas := params.TxGas + params.TxAccessListAddressGas + params.TxAccessListStorageKeyGas +
vm.GasQuickStep*2 + params.WarmStorageReadCostEIP2929 + params.ColdSloadCostEIP2929
if block.GasUsed() != expectedGas {
t.Fatalf("incorrect amount of gas spent: expected %d, got %d", expectedGas, block.GasUsed())
}
state, _ := chain.State()
// 3: Ensure that miner received only the tx's tip.
actual := state.GetBalance(block.Coinbase())
expected := new(big.Int).Add(
new(big.Int).SetUint64(block.GasUsed()*block.Transactions()[0].GasTipCap().Uint64()),
ethash.ConstantinopleBlockReward,
)
if actual.Cmp(expected) != 0 {
t.Fatalf("miner balance incorrect: expected %d, got %d", expected, actual)
}
// check burnt contract balance
actual = state.GetBalance(common.HexToAddress(params.BorUnittestChainConfig.Bor.CalculateBurntContract(block.NumberU64())))
expected = new(big.Int).Mul(new(big.Int).SetUint64(block.GasUsed()), block.BaseFee())
burntContractBalance := expected
if actual.Cmp(expected) != 0 {
t.Fatalf("burnt contract balance incorrect: expected %d, got %d", expected, actual)
}
// 4: Ensure the tx sender paid for the gasUsed * (tip + block baseFee).
actual = new(big.Int).Sub(funds, state.GetBalance(addr1))
expected = new(big.Int).SetUint64(block.GasUsed() * (block.Transactions()[0].GasTipCap().Uint64() + block.BaseFee().Uint64()))
if actual.Cmp(expected) != 0 {
t.Fatalf("sender balance incorrect: expected %d, got %d", expected, actual)
}
blocks, _ = core.GenerateChain(gspec.Config, block, engine, db, 1, func(i int, b *core.BlockGen) {
b.SetCoinbase(common.Address{2})
txdata := &types.LegacyTx{
Nonce: 0,
To: &aa,
Gas: 30000,
GasPrice: newGwei(5),
}
tx := types.NewTx(txdata)
tx, _ = types.SignTx(tx, signer, key2)
b.AddTx(tx)
})
if n, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
block = chain.GetBlockByNumber(2)
state, _ = chain.State()
effectiveTip := block.Transactions()[0].GasTipCap().Uint64() - block.BaseFee().Uint64()
// 6+5: Ensure that miner received only the tx's effective tip.
actual = state.GetBalance(block.Coinbase())
expected = new(big.Int).Add(
new(big.Int).SetUint64(block.GasUsed()*effectiveTip),
ethash.ConstantinopleBlockReward,
)
if actual.Cmp(expected) != 0 {
t.Fatalf("miner balance incorrect: expected %d, got %d", expected, actual)
}
// check burnt contract balance
actual = state.GetBalance(common.HexToAddress(params.BorUnittestChainConfig.Bor.CalculateBurntContract(block.NumberU64())))
expected = new(big.Int).Add(burntContractBalance, new(big.Int).Mul(new(big.Int).SetUint64(block.GasUsed()), block.BaseFee()))
burntContractBalance = expected
if actual.Cmp(expected) != 0 {
t.Fatalf("burnt contract balance incorrect: expected %d, got %d", expected, actual)
}
// 4: Ensure the tx sender paid for the gasUsed * (effectiveTip + block baseFee).
actual = new(big.Int).Sub(funds, state.GetBalance(addr2))
expected = new(big.Int).SetUint64(block.GasUsed() * (effectiveTip + block.BaseFee().Uint64()))
if actual.Cmp(expected) != 0 {
t.Fatalf("sender balance incorrect: expected %d, got %d", expected, actual)
}
blocks, _ = core.GenerateChain(gspec.Config, block, engine, db, 1, func(i int, b *core.BlockGen) {
b.SetCoinbase(common.Address{3})
txdata := &types.LegacyTx{
Nonce: 0,
To: &aa,
Gas: 30000,
GasPrice: newGwei(5),
}
tx := types.NewTx(txdata)
tx, _ = types.SignTx(tx, signer, key3)
b.AddTx(tx)
accesses := types.AccessList{types.AccessTuple{
Address: aa,
StorageKeys: []common.Hash{{0}},
}}
txdata2 := &types.DynamicFeeTx{
ChainID: gspec.Config.ChainID,
Nonce: 1,
To: &aa,
Gas: 30000,
GasFeeCap: newGwei(5),
GasTipCap: big.NewInt(2),
AccessList: accesses,
Data: []byte{},
}
tx = types.NewTx(txdata2)
tx, _ = types.SignTx(tx, signer, key3)
b.AddTx(tx)
})
if n, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
block = chain.GetBlockByNumber(3)
state, _ = chain.State()
// check burnt contract balance
actual = state.GetBalance(common.HexToAddress(params.BorUnittestChainConfig.Bor.CalculateBurntContract(block.NumberU64())))
burntAmount := new(big.Int).Mul(
block.BaseFee(),
big.NewInt(int64(block.GasUsed())),
)
expected = new(big.Int).Add(burntContractBalance, burntAmount)
if actual.Cmp(expected) != 0 {
t.Fatalf("burnt contract balance incorrect: expected %d, got %d", expected, actual)
}
}
// EIP1559 is not supported without EIP155. An error is expected
func TestEIP1559TransitionWithEIP155(t *testing.T) {
var (
aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa")
// Generate a canonical chain to act as the main dataset
db = rawdb.NewMemoryDatabase()
engine = ethash.NewFaker()
// A sender who makes transactions, has some funds
key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
key2, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a")
key3, _ = crypto.HexToECDSA("225171aed3793cba1c029832886d69785b7e77a54a44211226b447aa2d16b058")
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
addr2 = crypto.PubkeyToAddress(key2.PublicKey)
addr3 = crypto.PubkeyToAddress(key3.PublicKey)
funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether))
gspec = &core.Genesis{
Config: params.BorUnittestChainConfig,
Alloc: core.GenesisAlloc{
addr1: {Balance: funds},
addr2: {Balance: funds},
addr3: {Balance: funds},
// The address 0xAAAA sloads 0x00 and 0x01
aa: {
Code: []byte{
byte(vm.PC),
byte(vm.PC),
byte(vm.SLOAD),
byte(vm.SLOAD),
},
Nonce: 0,
Balance: big.NewInt(0),
},
},
}
)
genesis := gspec.MustCommit(db)
// Use signer without chain ID
signer := types.HomesteadSigner{}
_, _ = core.GenerateChain(gspec.Config, genesis, engine, db, 1, func(i int, b *core.BlockGen) {
b.SetCoinbase(common.Address{1})
// One transaction to 0xAAAA
accesses := types.AccessList{types.AccessTuple{
Address: aa,
StorageKeys: []common.Hash{{0}},
}}
txdata := &types.DynamicFeeTx{
ChainID: gspec.Config.ChainID,
Nonce: 0,
To: &aa,
Gas: 30000,
GasFeeCap: newGwei(5),
GasTipCap: big.NewInt(2),
AccessList: accesses,
Data: []byte{},
}
var err error
tx := types.NewTx(txdata)
tx, err = types.SignTx(tx, signer, key1)
require.ErrorIs(t, err, types.ErrTxTypeNotSupported)
})
}
// it is up to a user to use protected transactions. so if a transaction is unprotected no errors related to chainID are expected.
// transactions are checked in 2 places: transaction pool and blockchain processor.
func TestTransitionWithoutEIP155(t *testing.T) {
var (
aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa")
// Generate a canonical chain to act as the main dataset
db = rawdb.NewMemoryDatabase()
engine = ethash.NewFaker()
// A sender who makes transactions, has some funds
key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
key2, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a")
key3, _ = crypto.HexToECDSA("225171aed3793cba1c029832886d69785b7e77a54a44211226b447aa2d16b058")
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
addr2 = crypto.PubkeyToAddress(key2.PublicKey)
addr3 = crypto.PubkeyToAddress(key3.PublicKey)
funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether))
gspec = &core.Genesis{
Config: params.BorUnittestChainConfig,
Alloc: core.GenesisAlloc{
addr1: {Balance: funds},
addr2: {Balance: funds},
addr3: {Balance: funds},
// The address 0xAAAA sloads 0x00 and 0x01
aa: {
Code: []byte{
byte(vm.PC),
byte(vm.PC),
byte(vm.SLOAD),
byte(vm.SLOAD),
},
Nonce: 0,
Balance: big.NewInt(0),
},
},
}
)
genesis := gspec.MustCommit(db)
// Use signer without chain ID
signer := types.HomesteadSigner{}
//signer := types.FrontierSigner{}
blocks, _ := core.GenerateChain(gspec.Config, genesis, engine, db, 1, func(i int, b *core.BlockGen) {
b.SetCoinbase(common.Address{1})
txdata := &types.LegacyTx{
Nonce: 0,
To: &aa,
Gas: 30000,
GasPrice: newGwei(5),
}
var err error
tx := types.NewTx(txdata)
tx, err = types.SignTx(tx, signer, key1)
require.Nil(t, err)
require.False(t, tx.Protected())
from, err := types.Sender(types.EIP155Signer{}, tx)
require.Equal(t, addr1, from)
require.Nil(t, err)
b.AddTx(tx)
})
diskdb := rawdb.NewMemoryDatabase()
gspec.MustCommit(diskdb)
chain, err := core.NewBlockChain(diskdb, nil, gspec, nil, engine, vm.Config{}, nil, nil, nil)
if err != nil {
t.Fatalf("failed to create tester chain: %v", err)
}
if n, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
block := chain.GetBlockByNumber(1)
require.Len(t, block.Transactions(), 1)
}
func TestJaipurFork(t *testing.T) {
init := buildEthereumInstance(t, rawdb.NewMemoryDatabase())
chain := init.ethereum.BlockChain()
engine := init.ethereum.Engine()
_bor := engine.(*bor.Bor)
defer _bor.Close()
block := init.genesis.ToBlock()
res, _ := loadSpanFromFile(t)
spanner := getMockedSpanner(t, res.Result.ValidatorSet.Validators)
_bor.SetSpanner(spanner)
for i := uint64(1); i < sprintSize; i++ {
block = buildNextBlock(t, _bor, chain, block, nil, init.genesis.Config.Bor, nil, res.Result.ValidatorSet.Validators)
insertNewBlock(t, chain, block)
if block.Number().Uint64() == init.genesis.Config.Bor.JaipurBlock.Uint64()-1 {
require.Equal(t, testSealHash(block.Header(), init.genesis.Config.Bor), bor.SealHash(block.Header(), init.genesis.Config.Bor))
}
if block.Number().Uint64() == init.genesis.Config.Bor.JaipurBlock.Uint64() {
require.Equal(t, testSealHash(block.Header(), init.genesis.Config.Bor), bor.SealHash(block.Header(), init.genesis.Config.Bor))
}
}
}
// SealHash returns the hash of a block prior to it being sealed.
func testSealHash(header *types.Header, c *params.BorConfig) (hash common.Hash) {
hasher := sha3.NewLegacyKeccak256()
testEncodeSigHeader(hasher, header, c)
hasher.Sum(hash[:0])
return hash
}
func testEncodeSigHeader(w io.Writer, header *types.Header, c *params.BorConfig) {
enc := []interface{}{
header.ParentHash,
header.UncleHash,
header.Coinbase,
header.Root,
header.TxHash,
header.ReceiptHash,
header.Bloom,
header.Difficulty,
header.Number,
header.GasLimit,
header.GasUsed,
header.Time,
header.Extra[:len(header.Extra)-65], // Yes, this will panic if extra is too short
header.MixDigest,
header.Nonce,
}
if c.IsJaipur(header.Number) {
if header.BaseFee != nil {
enc = append(enc, header.BaseFee)
}
}
if err := rlp.Encode(w, enc); err != nil {
panic("can't encode: " + err.Error())
}
}