mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-07 20:43:48 +00:00
Add a new abstraction called span store in bor consensus. This makes the header verification stateless as instead of fetching validator set (of a span) from genesis contract, we rely on span from heimdall itself for validator set. This doesn't change any assumptions for consensus as the data in genesis contracts is written by fetching span details from heimdall in the first place. This helps us with multiple things such as header verification for snap sync, validating producer set against header's extra data, and using heimdall as a source of truth instead of making a contract call to genesis contract which generally leads to issues when importing side chains (as we need to use the side chain state and not canonical). Span store is a minimal cache which maintains last 10 spans in memory. It provide simple read methods via span id and block number so that we can use it in consensus.
558 lines
15 KiB
Go
558 lines
15 KiB
Go
//go:build integration
|
|
|
|
package bor
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang/mock/gomock"
|
|
|
|
"github.com/ethereum/go-ethereum/accounts"
|
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
|
"github.com/ethereum/go-ethereum/cmd/utils"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/consensus"
|
|
"github.com/ethereum/go-ethereum/consensus/bor"
|
|
"github.com/ethereum/go-ethereum/consensus/bor/clerk"
|
|
"github.com/ethereum/go-ethereum/consensus/bor/heimdall" //nolint:typecheck
|
|
"github.com/ethereum/go-ethereum/consensus/bor/heimdall/checkpoint"
|
|
"github.com/ethereum/go-ethereum/consensus/bor/heimdall/milestone"
|
|
"github.com/ethereum/go-ethereum/consensus/bor/heimdall/span"
|
|
"github.com/ethereum/go-ethereum/consensus/bor/valset"
|
|
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
|
|
"github.com/ethereum/go-ethereum/core"
|
|
"github.com/ethereum/go-ethereum/core/state"
|
|
"github.com/ethereum/go-ethereum/core/txpool/legacypool"
|
|
"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/crypto/secp256k1"
|
|
"github.com/ethereum/go-ethereum/eth"
|
|
"github.com/ethereum/go-ethereum/eth/downloader"
|
|
"github.com/ethereum/go-ethereum/eth/ethconfig"
|
|
"github.com/ethereum/go-ethereum/ethdb"
|
|
"github.com/ethereum/go-ethereum/miner"
|
|
"github.com/ethereum/go-ethereum/node"
|
|
"github.com/ethereum/go-ethereum/p2p"
|
|
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
"github.com/ethereum/go-ethereum/params"
|
|
"github.com/ethereum/go-ethereum/tests/bor/mocks"
|
|
"github.com/ethereum/go-ethereum/triedb"
|
|
)
|
|
|
|
var (
|
|
|
|
// Only this account is a validator for the 0th span
|
|
key, _ = crypto.HexToECDSA(privKey)
|
|
addr = crypto.PubkeyToAddress(key.PublicKey) // 0x71562b71999873DB5b286dF957af199Ec94617F7
|
|
|
|
// This account is one the validators for 1st span (0-indexed)
|
|
key2, _ = crypto.HexToECDSA(privKey2)
|
|
addr2 = crypto.PubkeyToAddress(key2.PublicKey) // 0x9fB29AAc15b9A4B7F17c3385939b007540f4d791
|
|
|
|
keys = []*ecdsa.PrivateKey{key, key2}
|
|
)
|
|
|
|
const (
|
|
privKey = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"
|
|
privKey2 = "9b28f36fbd67381120752d6172ecdcf10e06ab2d9a1367aac00cdcd6ac7855d3"
|
|
|
|
// The genesis for tests was generated with following parameters
|
|
extraSeal = 65 // Fixed number of extra-data suffix bytes reserved for signer seal
|
|
|
|
sprintSize uint64 = 4
|
|
spanSize uint64 = 8
|
|
|
|
validatorHeaderBytesLength = common.AddressLength + 20 // address + power
|
|
)
|
|
|
|
type initializeData struct {
|
|
genesis *core.Genesis
|
|
ethereum *eth.Ethereum
|
|
}
|
|
|
|
func setupMiner(t *testing.T, n int, genesis *core.Genesis) ([]*node.Node, []*eth.Ethereum, []*enode.Node) {
|
|
t.Helper()
|
|
|
|
// Create an Ethash network based off of the Ropsten config
|
|
var (
|
|
stacks []*node.Node
|
|
nodes []*eth.Ethereum
|
|
enodes []*enode.Node
|
|
)
|
|
|
|
for i := 0; i < n; i++ {
|
|
// Start the node and wait until it's up
|
|
stack, ethBackend, err := InitMiner(genesis, keys[i], true)
|
|
if err != nil {
|
|
t.Fatal("Error occurred while initialising miner", "error", err)
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
return stacks, nodes, enodes
|
|
}
|
|
|
|
func buildEthereumInstance(t *testing.T, db ethdb.Database) *initializeData {
|
|
genesisData, err := ioutil.ReadFile("./testdata/genesis.json")
|
|
if err != nil {
|
|
t.Fatalf("%s", err)
|
|
}
|
|
|
|
gen := &core.Genesis{}
|
|
|
|
if err := json.Unmarshal(genesisData, gen); err != nil {
|
|
t.Fatalf("%s", err)
|
|
}
|
|
|
|
ethConf := ð.Config{
|
|
Genesis: gen,
|
|
BorLogs: true,
|
|
StateScheme: "hash",
|
|
}
|
|
|
|
ethConf.Genesis.MustCommit(db, triedb.NewDatabase(db, triedb.HashDefaults))
|
|
|
|
ethereum := utils.CreateBorEthereum(ethConf)
|
|
if err != nil {
|
|
t.Fatalf("failed to register Ethereum protocol: %v", err)
|
|
}
|
|
|
|
ethConf.Genesis.MustCommit(ethereum.ChainDb(), triedb.NewDatabase(ethereum.ChainDb(), triedb.HashDefaults))
|
|
|
|
ethereum.Engine().(*bor.Bor).Authorize(addr, func(account accounts.Account, s string, data []byte) ([]byte, error) {
|
|
return crypto.Sign(crypto.Keccak256(data), key)
|
|
})
|
|
|
|
return &initializeData{
|
|
genesis: gen,
|
|
ethereum: ethereum,
|
|
}
|
|
}
|
|
|
|
func insertNewBlock(t *testing.T, chain *core.BlockChain, block *types.Block) {
|
|
t.Helper()
|
|
|
|
if _, err := chain.InsertChain([]*types.Block{block}); err != nil {
|
|
t.Fatalf("%s", err)
|
|
}
|
|
}
|
|
|
|
type Option func(header *types.Header)
|
|
|
|
func buildNextBlock(t *testing.T, _bor consensus.Engine, chain *core.BlockChain, parentBlock *types.Block, signer []byte, borConfig *params.BorConfig, txs []*types.Transaction, currentValidators []*valset.Validator, opts ...Option) *types.Block {
|
|
t.Helper()
|
|
|
|
header := &types.Header{
|
|
Number: big.NewInt(int64(parentBlock.Number().Uint64() + 1)),
|
|
Difficulty: big.NewInt(int64(parentBlock.Difficulty().Uint64())),
|
|
GasLimit: parentBlock.GasLimit(),
|
|
ParentHash: parentBlock.Hash(),
|
|
}
|
|
number := header.Number.Uint64()
|
|
|
|
if signer == nil {
|
|
signer = getSignerKey(header.Number.Uint64())
|
|
}
|
|
|
|
header.Time = parentBlock.Time() + bor.CalcProducerDelay(header.Number.Uint64(), 0, borConfig)
|
|
header.Extra = make([]byte, 32+65) // vanity + extraSeal
|
|
|
|
isSpanStart := IsSpanStart(number)
|
|
isSprintEnd := IsSprintEnd(number)
|
|
|
|
if isSpanStart {
|
|
header.Difficulty = new(big.Int).SetInt64(int64(len(currentValidators)))
|
|
}
|
|
|
|
if isSprintEnd {
|
|
sort.Sort(valset.ValidatorsByAddress(currentValidators))
|
|
|
|
validatorBytes := make([]byte, len(currentValidators)*validatorHeaderBytesLength)
|
|
header.Extra = make([]byte, 32+len(validatorBytes)+65) // vanity + validatorBytes + extraSeal
|
|
|
|
for i, val := range currentValidators {
|
|
copy(validatorBytes[i*validatorHeaderBytesLength:], val.HeaderBytes())
|
|
}
|
|
|
|
copy(header.Extra[32:], validatorBytes)
|
|
}
|
|
|
|
if chain.Config().IsLondon(header.Number) {
|
|
header.BaseFee = eip1559.CalcBaseFee(chain.Config(), parentBlock.Header())
|
|
|
|
if !chain.Config().IsLondon(parentBlock.Number()) {
|
|
parentGasLimit := parentBlock.GasLimit() * params.ElasticityMultiplier
|
|
header.GasLimit = core.CalcGasLimit(parentGasLimit, parentGasLimit)
|
|
}
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(header)
|
|
}
|
|
|
|
state, err := chain.State()
|
|
if err != nil {
|
|
t.Fatalf("%s", err)
|
|
}
|
|
|
|
b := &blockGen{header: header}
|
|
for _, tx := range txs {
|
|
b.addTxWithChain(chain, state, tx, addr)
|
|
}
|
|
|
|
// Finalize and seal the block
|
|
block, err := _bor.FinalizeAndAssemble(chain, b.header, state, &types.Body{
|
|
Transactions: b.txs,
|
|
}, b.receipts)
|
|
|
|
if err != nil {
|
|
panic(fmt.Sprintf("error finalizing block: %v", err))
|
|
}
|
|
|
|
// Write state changes to db
|
|
root, err := state.Commit(block.NumberU64(), chain.Config().IsEIP158(b.header.Number))
|
|
if err != nil {
|
|
panic(fmt.Sprintf("state write error: %v", err))
|
|
}
|
|
|
|
if err := state.Database().TrieDB().Commit(root, false); err != nil {
|
|
panic(fmt.Sprintf("trie write error: %v", err))
|
|
}
|
|
|
|
res := make(chan *types.Block, 1)
|
|
|
|
err = _bor.Seal(chain, block, res, nil)
|
|
if err != nil {
|
|
// an error case - sign manually
|
|
sign(t, header, signer, borConfig)
|
|
return types.NewBlockWithHeader(header)
|
|
}
|
|
|
|
return <-res
|
|
}
|
|
|
|
type blockGen struct {
|
|
txs []*types.Transaction
|
|
receipts []*types.Receipt
|
|
gasPool *core.GasPool
|
|
header *types.Header
|
|
}
|
|
|
|
func (b *blockGen) addTxWithChain(bc *core.BlockChain, statedb *state.StateDB, tx *types.Transaction, coinbase common.Address) {
|
|
if b.gasPool == nil {
|
|
b.setCoinbase(coinbase)
|
|
}
|
|
|
|
statedb.SetTxContext(tx.Hash(), len(b.txs))
|
|
|
|
receipt, err := core.ApplyTransaction(bc.Config(), bc, &b.header.Coinbase, b.gasPool, statedb, b.header, tx, &b.header.GasUsed, vm.Config{}, nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
b.txs = append(b.txs, tx)
|
|
b.receipts = append(b.receipts, receipt)
|
|
}
|
|
|
|
func (b *blockGen) setCoinbase(addr common.Address) {
|
|
if b.gasPool != nil {
|
|
if len(b.txs) > 0 {
|
|
panic("coinbase must be set before adding transactions")
|
|
}
|
|
|
|
panic("coinbase can only be set once")
|
|
}
|
|
|
|
b.header.Coinbase = addr
|
|
b.gasPool = new(core.GasPool).AddGas(b.header.GasLimit)
|
|
}
|
|
|
|
func sign(t *testing.T, header *types.Header, signer []byte, c *params.BorConfig) {
|
|
t.Helper()
|
|
|
|
sig, err := secp256k1.Sign(crypto.Keccak256(bor.BorRLP(header, c)), signer)
|
|
if err != nil {
|
|
t.Fatalf("%s", err)
|
|
}
|
|
|
|
copy(header.Extra[len(header.Extra)-extraSeal:], sig)
|
|
}
|
|
|
|
//nolint:unused,deadcode
|
|
func stateSyncEventsPayload(t *testing.T) *heimdall.StateSyncEventsResponse {
|
|
t.Helper()
|
|
|
|
stateData, err := ioutil.ReadFile("./testdata/states.json")
|
|
if err != nil {
|
|
t.Fatalf("%s", err)
|
|
}
|
|
|
|
res := &heimdall.StateSyncEventsResponse{}
|
|
if err := json.Unmarshal(stateData, res); err != nil {
|
|
t.Fatalf("%s", err)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
//nolint:unused,deadcode
|
|
func loadSpanFromFile(t *testing.T) (*heimdall.SpanResponse, *span.HeimdallSpan) {
|
|
t.Helper()
|
|
|
|
spanData, err := ioutil.ReadFile("./testdata/span.json")
|
|
if err != nil {
|
|
t.Fatalf("%s", err)
|
|
}
|
|
|
|
res := &heimdall.SpanResponse{}
|
|
|
|
if err := json.Unmarshal(spanData, res); err != nil {
|
|
t.Fatalf("%s", err)
|
|
}
|
|
|
|
return res, &res.Result
|
|
}
|
|
|
|
func getSignerKey(number uint64) []byte {
|
|
signerKey := privKey
|
|
|
|
if IsSpanStart(number) {
|
|
// validator set in the new span has changed
|
|
signerKey = privKey2
|
|
}
|
|
|
|
newKey, _ := hex.DecodeString(signerKey)
|
|
|
|
return newKey
|
|
}
|
|
|
|
func getMockedHeimdallClient(t *testing.T, heimdallSpan *span.HeimdallSpan) (*mocks.MockIHeimdallClient, *gomock.Controller) {
|
|
t.Helper()
|
|
|
|
ctrl := gomock.NewController(t)
|
|
h := mocks.NewMockIHeimdallClient(ctrl)
|
|
|
|
h.EXPECT().Span(gomock.Any(), uint64(1)).Return(heimdallSpan, nil).AnyTimes()
|
|
|
|
h.EXPECT().StateSyncEvents(gomock.Any(), gomock.Any(), gomock.Any()).
|
|
Return([]*clerk.EventRecordWithTime{getSampleEventRecord(t)}, nil).AnyTimes()
|
|
|
|
return h, ctrl
|
|
}
|
|
|
|
func createMockSpan(address common.Address, chainId string) span.HeimdallSpan {
|
|
// Mock span 0 for heimdall calls
|
|
validator := valset.Validator{
|
|
ID: 0,
|
|
Address: address,
|
|
VotingPower: 10,
|
|
ProposerPriority: 0,
|
|
}
|
|
validatorSet := valset.ValidatorSet{
|
|
Validators: []*valset.Validator{&validator},
|
|
Proposer: &validator,
|
|
}
|
|
span0 := span.HeimdallSpan{
|
|
Span: span.Span{
|
|
ID: 0,
|
|
StartBlock: 0,
|
|
EndBlock: 255,
|
|
},
|
|
ValidatorSet: validatorSet,
|
|
SelectedProducers: []valset.Validator{validator},
|
|
ChainID: chainId,
|
|
}
|
|
|
|
return span0
|
|
}
|
|
|
|
func createMockHeimdall(ctrl *gomock.Controller, span0, span1 *span.HeimdallSpan) *mocks.MockIHeimdallClient {
|
|
h := mocks.NewMockIHeimdallClient(ctrl)
|
|
|
|
h.EXPECT().Close().AnyTimes()
|
|
h.EXPECT().Span(gomock.Any(), uint64(0)).Return(span0, nil).AnyTimes()
|
|
h.EXPECT().Span(gomock.Any(), uint64(1)).Return(span1, nil).AnyTimes()
|
|
h.EXPECT().FetchCheckpoint(gomock.Any(), int64(-1)).Return(&checkpoint.Checkpoint{}, nil).AnyTimes()
|
|
h.EXPECT().FetchMilestone(gomock.Any()).Return(&milestone.Milestone{}, nil).AnyTimes()
|
|
h.EXPECT().FetchLastNoAckMilestone(gomock.Any()).Return("", nil).AnyTimes()
|
|
h.EXPECT().FetchNoAckMilestone(gomock.Any(), string("test")).Return(nil).AnyTimes()
|
|
|
|
return h
|
|
}
|
|
|
|
func getMockedSpanner(t *testing.T, validators []*valset.Validator) *bor.MockSpanner {
|
|
t.Helper()
|
|
|
|
spanner := bor.NewMockSpanner(gomock.NewController(t))
|
|
spanner.EXPECT().GetCurrentValidatorsByHash(gomock.Any(), gomock.Any(), gomock.Any()).Return(validators, nil).AnyTimes()
|
|
spanner.EXPECT().GetCurrentValidatorsByBlockNrOrHash(gomock.Any(), gomock.Any(), gomock.Any()).Return(validators, nil).AnyTimes()
|
|
spanner.EXPECT().GetCurrentSpan(gomock.Any(), gomock.Any()).Return(&span.Span{0, 0, 0}, nil).AnyTimes()
|
|
spanner.EXPECT().CommitSpan(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
|
return spanner
|
|
}
|
|
|
|
func generateFakeStateSyncEvents(sample *clerk.EventRecordWithTime, count int) []*clerk.EventRecordWithTime {
|
|
events := make([]*clerk.EventRecordWithTime, count)
|
|
event := *sample
|
|
event.ID = 1
|
|
events[0] = &clerk.EventRecordWithTime{}
|
|
*events[0] = event
|
|
|
|
for i := 1; i < count; i++ {
|
|
event.ID = uint64(i + 1)
|
|
event.Time = event.Time.Add(1 * time.Second)
|
|
events[i] = &clerk.EventRecordWithTime{}
|
|
*events[i] = event
|
|
}
|
|
|
|
return events
|
|
}
|
|
|
|
func buildStateEvent(sample *clerk.EventRecordWithTime, id uint64, timeStamp int64) *clerk.EventRecordWithTime {
|
|
event := *sample
|
|
event.ID = id
|
|
event.Time = time.Unix(timeStamp, 0)
|
|
|
|
return &event
|
|
}
|
|
|
|
func getSampleEventRecord(t *testing.T) *clerk.EventRecordWithTime {
|
|
t.Helper()
|
|
|
|
eventRecords := stateSyncEventsPayload(t)
|
|
eventRecords.Result[0].Time = time.Unix(1, 0)
|
|
|
|
return eventRecords.Result[0]
|
|
}
|
|
|
|
func newGwei(n int64) *big.Int {
|
|
return new(big.Int).Mul(big.NewInt(n), big.NewInt(params.GWei))
|
|
}
|
|
|
|
func IsSpanEnd(number uint64) bool {
|
|
return (number+1)%spanSize == 0
|
|
}
|
|
|
|
func IsSpanStart(number uint64) bool {
|
|
return number%spanSize == 0
|
|
}
|
|
|
|
func IsSprintStart(number uint64) bool {
|
|
return number%sprintSize == 0
|
|
}
|
|
|
|
func IsSprintEnd(number uint64) bool {
|
|
return (number+1)%sprintSize == 0
|
|
}
|
|
|
|
func InitGenesis(t *testing.T, faucets []*ecdsa.PrivateKey, fileLocation string, sprintSize uint64) *core.Genesis {
|
|
t.Helper()
|
|
|
|
// sprint size = 8 in genesis
|
|
genesisData, err := ioutil.ReadFile(fileLocation)
|
|
if err != nil {
|
|
t.Fatalf("%s", err)
|
|
}
|
|
|
|
genesis := &core.Genesis{}
|
|
|
|
if err := json.Unmarshal(genesisData, genesis); err != nil {
|
|
t.Fatalf("%s", err)
|
|
}
|
|
|
|
genesis.Config.ChainID = big.NewInt(15001)
|
|
genesis.Config.EIP150Block = big.NewInt(0)
|
|
|
|
genesis.Config.Bor.Sprint["0"] = sprintSize
|
|
|
|
return genesis
|
|
}
|
|
|
|
func InitMiner(genesis *core.Genesis, privKey *ecdsa.PrivateKey, withoutHeimdall bool) (*node.Node, *eth.Ethereum, error) {
|
|
// Define the basic configurations for the Ethereum node
|
|
datadir, _ := ioutil.TempDir("", "")
|
|
|
|
config := &node.Config{
|
|
Name: "geth",
|
|
Version: params.Version,
|
|
DataDir: datadir,
|
|
P2P: p2p.Config{
|
|
ListenAddr: "0.0.0.0:0",
|
|
NoDiscovery: true,
|
|
MaxPeers: 25,
|
|
},
|
|
UseLightweightKDF: true,
|
|
}
|
|
// Create the node and configure a full Ethereum node on it
|
|
stack, err := node.New(config)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
ethBackend, err := eth.New(stack, ðconfig.Config{
|
|
Genesis: genesis,
|
|
NetworkId: genesis.Config.ChainID.Uint64(),
|
|
SyncMode: downloader.FullSync,
|
|
DatabaseCache: 256,
|
|
DatabaseHandles: 256,
|
|
TxPool: legacypool.DefaultConfig,
|
|
GPO: ethconfig.Defaults.GPO,
|
|
Miner: miner.Config{
|
|
Etherbase: crypto.PubkeyToAddress(privKey.PublicKey),
|
|
GasCeil: genesis.GasLimit * 11 / 10,
|
|
GasPrice: big.NewInt(1),
|
|
Recommit: time.Second,
|
|
},
|
|
WithoutHeimdall: withoutHeimdall,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// register backend to account manager with keystore for signing
|
|
keydir := stack.KeyStoreDir()
|
|
|
|
n, p := keystore.StandardScryptN, keystore.StandardScryptP
|
|
kStore := keystore.NewKeyStore(keydir, n, p)
|
|
|
|
_, err = kStore.ImportECDSA(privKey, "")
|
|
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
acc := kStore.Accounts()[0]
|
|
err = kStore.Unlock(acc, "")
|
|
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// proceed to authorize the local account manager in any case
|
|
ethBackend.AccountManager().AddBackend(kStore)
|
|
|
|
err = stack.Start()
|
|
|
|
return stack, ethBackend, err
|
|
}
|