bsc/core/vote/vote_pool_test.go

456 lines
14 KiB
Go

// Copyright 2014 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 vote
import (
"container/heap"
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"testing"
"time"
"github.com/google/uuid"
"github.com/prysmaticlabs/prysm/v5/crypto/bls"
"github.com/prysmaticlabs/prysm/v5/validator/accounts"
"github.com/prysmaticlabs/prysm/v5/validator/accounts/iface"
"github.com/prysmaticlabs/prysm/v5/validator/keymanager"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"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/downloader"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/params"
)
var (
// testKey is a private key to use for funding a tester account.
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
// testAddr is the Ethereum address of the tester account.
testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
password = "secretPassword"
timeThreshold = 30
)
type mockPOSA struct {
consensus.PoSA
}
type mockInvalidPOSA struct {
consensus.PoSA
}
// testBackend is a mock implementation of the live Ethereum message handler.
type testBackend struct {
eventMux *event.TypeMux
}
func newTestBackend() *testBackend {
return &testBackend{eventMux: new(event.TypeMux)}
}
func (b *testBackend) IsMining() bool { return true }
func (b *testBackend) EventMux() *event.TypeMux { return b.eventMux }
func (p *mockPOSA) GetJustifiedNumberAndHash(chain consensus.ChainHeaderReader, headers []*types.Header) (uint64, common.Hash, error) {
parentHeader := chain.GetHeaderByHash(headers[len(headers)-1].ParentHash)
if parentHeader == nil {
return 0, common.Hash{}, errors.New("unexpected error")
}
return parentHeader.Number.Uint64(), parentHeader.Hash(), nil
}
func (p *mockInvalidPOSA) GetJustifiedNumberAndHash(chain consensus.ChainHeaderReader, headers []*types.Header) (uint64, common.Hash, error) {
return 0, common.Hash{}, errors.New("not supported")
}
func (m *mockPOSA) VerifyVote(chain consensus.ChainHeaderReader, vote *types.VoteEnvelope) error {
return nil
}
func (m *mockInvalidPOSA) VerifyVote(chain consensus.ChainHeaderReader, vote *types.VoteEnvelope) error {
return nil
}
func (m *mockPOSA) IsActiveValidatorAt(chain consensus.ChainHeaderReader, header *types.Header, checkVoteKeyFn func(bLSPublicKey *types.BLSPublicKey) bool) bool {
return true
}
func (m *mockInvalidPOSA) IsActiveValidatorAt(chain consensus.ChainHeaderReader, header *types.Header, checkVoteKeyFn func(bLSPublicKey *types.BLSPublicKey) bool) bool {
return true
}
func (pool *VotePool) verifyStructureSizeOfVotePool(receivedVotes, curVotes, futureVotes, curVotesPq, futureVotesPq int) bool {
for i := 0; i < timeThreshold; i++ {
time.Sleep(1 * time.Second)
if pool.receivedVotes.Cardinality() == receivedVotes && len(pool.curVotes) == curVotes && len(pool.futureVotes) == futureVotes && pool.curVotesPq.Len() == curVotesPq && pool.futureVotesPq.Len() == futureVotesPq {
return true
}
}
return false
}
func (journal *VoteJournal) verifyJournal(size, lastLatestVoteNumber int) bool {
for i := 0; i < timeThreshold; i++ {
time.Sleep(1 * time.Second)
lastIndex, _ := journal.walLog.LastIndex()
firstIndex, _ := journal.walLog.FirstIndex()
if int(lastIndex)-int(firstIndex)+1 == size {
return true
}
lastVote, _ := journal.ReadVote(lastIndex)
if lastVote != nil && lastVote.Data.TargetNumber == uint64(lastLatestVoteNumber) {
return true
}
}
return false
}
func TestValidVotePool(t *testing.T) {
testVotePool(t, true)
}
func TestInvalidVotePool(t *testing.T) {
testVotePool(t, false)
}
func testVotePool(t *testing.T, isValidRules bool) {
walletPasswordDir, walletDir := setUpKeyManager(t)
genesis := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{testAddr: {Balance: big.NewInt(1000000)}},
}
mux := new(event.TypeMux)
db := rawdb.NewMemoryDatabase()
chain, _ := core.NewBlockChain(db, nil, genesis, nil, ethash.NewFullFaker(), vm.Config{}, nil, nil)
var mockEngine consensus.PoSA
if isValidRules {
mockEngine = &mockPOSA{}
} else {
mockEngine = &mockInvalidPOSA{}
}
// Create vote pool
votePool := NewVotePool(chain, mockEngine)
// Create vote manager
// Create a temporary file for the votes journal
file, err := os.CreateTemp("", "")
if err != nil {
t.Fatalf("failed to create temporary file path: %v", err)
}
journal := file.Name()
defer os.Remove(journal)
// Clean up the temporary file, we only need the path for now
file.Close()
os.Remove(journal)
voteManager, err := NewVoteManager(newTestBackend(), chain, votePool, journal, walletPasswordDir, walletDir, mockEngine)
if err != nil {
t.Fatalf("failed to create vote managers")
}
voteJournal := voteManager.journal
// Send the done event of downloader
time.Sleep(10 * time.Millisecond)
mux.Post(downloader.DoneEvent{})
bs, _ := core.GenerateChain(params.TestChainConfig, chain.Genesis(), ethash.NewFaker(), db, 1, nil)
if _, err := chain.InsertChain(bs); err != nil {
panic(err)
}
for i := 0; i < 10+blocksNumberSinceMining; i++ {
bs, _ = core.GenerateChain(params.TestChainConfig, bs[len(bs)-1], ethash.NewFaker(), db, 1, nil)
if _, err := chain.InsertChain(bs); err != nil {
panic(err)
}
}
if !isValidRules {
if votePool.verifyStructureSizeOfVotePool(11, 11, 0, 11, 0) {
t.Fatalf("put vote failed")
}
return
}
if !votePool.verifyStructureSizeOfVotePool(11, 11, 0, 11, 0) {
t.Fatalf("put vote failed")
}
// Verify if votesPq is min heap
votesPq := votePool.curVotesPq
pqBuffer := make([]*types.VoteData, 0)
lastVotedBlockNumber := uint64(0)
for votesPq.Len() > 0 {
voteData := heap.Pop(votesPq).(*types.VoteData)
if voteData.TargetNumber < lastVotedBlockNumber {
t.Fatalf("votesPq verification failed")
}
lastVotedBlockNumber = voteData.TargetNumber
pqBuffer = append(pqBuffer, voteData)
}
for _, voteData := range pqBuffer {
heap.Push(votesPq, voteData)
}
// Verify journal
if !voteJournal.verifyJournal(11, 11) {
t.Fatalf("journal failed")
}
bs, _ = core.GenerateChain(params.TestChainConfig, bs[len(bs)-1], ethash.NewFaker(), db, 1, nil)
if _, err := chain.InsertChain(bs); err != nil {
panic(err)
}
if !votePool.verifyStructureSizeOfVotePool(12, 12, 0, 12, 0) {
t.Fatalf("put vote failed")
}
// Verify journal
if !voteJournal.verifyJournal(12, 12) {
t.Fatalf("journal failed")
}
for i := 0; i < 256; i++ {
bs, _ = core.GenerateChain(params.TestChainConfig, bs[len(bs)-1], ethash.NewFaker(), db, 1, nil)
if _, err := chain.InsertChain(bs); err != nil {
panic(err)
}
}
// Verify journal
if !voteJournal.verifyJournal(268, 268) {
t.Fatalf("journal failed")
}
// currently chain size is 268, and votePool should be pruned, so vote pool size should be 256!
if !votePool.verifyStructureSizeOfVotePool(256, 256, 0, 256, 0) {
t.Fatalf("put vote failed")
}
// Test invalid vote whose number larger than latestHeader + 13
invalidVote := &types.VoteEnvelope{
Data: &types.VoteData{
TargetNumber: 1000,
},
}
voteManager.pool.PutVote(invalidVote)
if !votePool.verifyStructureSizeOfVotePool(256, 256, 0, 256, 0) {
t.Fatalf("put vote failed")
}
votes := votePool.GetVotes()
if len(votes) != 256 {
t.Fatalf("get votes failed")
}
// Verify journal
if !voteJournal.verifyJournal(268, 268) {
t.Fatalf("journal failed")
}
// Test future votes scenario: votes number within latestBlockHeader ~ latestBlockHeader + 13
futureVote := &types.VoteEnvelope{
Data: &types.VoteData{
TargetNumber: 279,
},
}
if err := voteManager.signer.SignVote(futureVote); err != nil {
t.Fatalf("sign vote failed")
}
voteManager.pool.PutVote(futureVote)
if !votePool.verifyStructureSizeOfVotePool(257, 256, 1, 256, 1) {
t.Fatalf("put vote failed")
}
// Verify journal
if !voteJournal.verifyJournal(268, 268) {
t.Fatalf("journal failed")
}
// Test duplicate vote case, shouldn'd be put into vote pool
duplicateVote := &types.VoteEnvelope{
Data: &types.VoteData{
TargetNumber: 279,
},
}
if err := voteManager.signer.SignVote(duplicateVote); err != nil {
t.Fatalf("sign vote failed")
}
voteManager.pool.PutVote(duplicateVote)
if !votePool.verifyStructureSizeOfVotePool(257, 256, 1, 256, 1) {
t.Fatalf("put vote failed")
}
// Verify journal
if !voteJournal.verifyJournal(268, 268) {
t.Fatalf("journal failed")
}
// Test future votes larger than latestBlockNumber + 13 should be rejected
futureVote = &types.VoteEnvelope{
Data: &types.VoteData{
TargetNumber: 282,
TargetHash: common.Hash{},
},
}
voteManager.pool.PutVote(futureVote)
if !votePool.verifyStructureSizeOfVotePool(257, 256, 1, 256, 1) {
t.Fatalf("put vote failed")
}
// Test transfer votes from future to cur, latest block header is #288 after the following generation
// For the above BlockNumber 279, it did not have blockHash, should be assigned as well below.
curNumber := 268
var futureBlockHash common.Hash
for i := 0; i < 20; i++ {
bs, _ = core.GenerateChain(params.TestChainConfig, bs[len(bs)-1], ethash.NewFaker(), db, 1, nil)
curNumber += 1
if curNumber == 279 {
futureBlockHash = bs[0].Hash()
futureVotesMap := votePool.futureVotes
voteBox := futureVotesMap[common.Hash{}]
futureVotesMap[futureBlockHash] = voteBox
delete(futureVotesMap, common.Hash{})
futureVotesPq := votePool.futureVotesPq
futureVotesPq.Peek().TargetHash = futureBlockHash
}
if _, err := chain.InsertChain(bs); err != nil {
panic(err)
}
}
for i := 0; i < timeThreshold; i++ {
time.Sleep(1 * time.Second)
_, ok := votePool.curVotes[futureBlockHash]
if ok && len(votePool.curVotes[futureBlockHash].voteMessages) == 2 {
break
}
}
if votePool.curVotes[futureBlockHash] == nil || len(votePool.curVotes[futureBlockHash].voteMessages) != 2 {
t.Fatalf("transfer vote failed")
}
// Pruner will keep the size of votePool as latestBlockHeader-255~latestBlockHeader, then final result should be 256!
if !votePool.verifyStructureSizeOfVotePool(257, 256, 0, 256, 0) {
t.Fatalf("put vote failed")
}
// Verify journal
if !voteJournal.verifyJournal(288, 288) {
t.Fatalf("journal failed")
}
for i := 0; i < 224; i++ {
bs, _ = core.GenerateChain(params.TestChainConfig, bs[len(bs)-1], ethash.NewFaker(), db, 1, nil)
if _, err := chain.InsertChain(bs); err != nil {
panic(err)
}
}
// Verify journal
if !voteJournal.verifyJournal(512, 512) {
t.Fatalf("journal failed")
}
bs, _ = core.GenerateChain(params.TestChainConfig, bs[len(bs)-1], ethash.NewFaker(), db, 1, nil)
if _, err := chain.InsertChain(bs); err != nil {
panic(err)
}
// Verify if journal no longer than 512
if !voteJournal.verifyJournal(512, 513) {
t.Fatalf("journal failed")
}
}
func setUpKeyManager(t *testing.T) (string, string) {
walletDir := filepath.Join(t.TempDir(), "wallet")
opts := []accounts.Option{}
opts = append(opts, accounts.WithWalletDir(walletDir))
opts = append(opts, accounts.WithWalletPassword(password))
opts = append(opts, accounts.WithKeymanagerType(keymanager.Local))
opts = append(opts, accounts.WithSkipMnemonicConfirm(true))
acc, err := accounts.NewCLIManager(opts...)
if err != nil {
t.Fatalf("New Accounts CLI Manager failed: %v.", err)
}
walletPasswordDir := filepath.Join(t.TempDir(), "password")
if err := os.MkdirAll(filepath.Dir(walletPasswordDir), 0700); err != nil {
t.Fatalf("failed to create walletPassword dir: %v", err)
}
if err := os.WriteFile(walletPasswordDir, []byte(password), 0600); err != nil {
t.Fatalf("failed to write wallet password dir: %v", err)
}
w, err := acc.WalletCreate(context.Background())
if err != nil {
t.Fatalf("failed to create wallet: %v", err)
}
km, _ := w.InitializeKeymanager(context.Background(), iface.InitKeymanagerConfig{ListenForChanges: false})
k, _ := km.(keymanager.Importer)
secretKey, _ := bls.RandKey()
encryptor := keystorev4.New()
pubKeyBytes := secretKey.PublicKey().Marshal()
cryptoFields, err := encryptor.Encrypt(secretKey.Marshal(), password)
if err != nil {
t.Fatalf("failed: %v", err)
}
id, _ := uuid.NewRandom()
keystore := &keymanager.Keystore{
Crypto: cryptoFields,
ID: id.String(),
Pubkey: fmt.Sprintf("%x", pubKeyBytes),
Version: encryptor.Version(),
Name: encryptor.Name(),
}
encodedFile, _ := json.MarshalIndent(keystore, "", "\t")
keyStoreDir := filepath.Join(t.TempDir(), "keystore")
keystoreFile, _ := os.Create(fmt.Sprintf("%s/keystore-%s.json", keyStoreDir, "publichh"))
keystoreFile.Write(encodedFile)
accounts.ImportAccounts(context.Background(), &accounts.ImportAccountsConfig{
Importer: k,
Keystores: []*keymanager.Keystore{keystore},
AccountPassword: password,
})
return walletPasswordDir, walletDir
}