go-ethereum/eth/downloader/whitelist/service_test.go
Dimitris Apostolou ce68cb17ac
fix typos
2024-01-29 09:21:47 +02:00

1152 lines
40 KiB
Go

// nolint
package whitelist
import (
"errors"
"fmt"
"math/big"
"reflect"
"sort"
"testing"
"time"
"pgregory.net/rapid"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb"
)
// NewMockService creates a new mock whitelist service
func NewMockService(db ethdb.Database) *Service {
return &Service{
&checkpoint{
finality[*rawdb.Checkpoint]{
doExist: false,
interval: 256,
db: db,
},
},
&milestone{
finality: finality[*rawdb.Milestone]{
doExist: false,
interval: 256,
db: db,
},
LockedMilestoneIDs: make(map[string]struct{}),
FutureMilestoneList: make(map[uint64]common.Hash),
FutureMilestoneOrder: make([]uint64, 0),
MaxCapacity: 10,
},
}
}
// TestWhitelistCheckpoint checks the checkpoint whitelist setter and getter functions.
func TestWhitelistedCheckpoint(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
//Creating the service for the whitelisting the checkpoints
s := NewMockService(db)
cp := s.checkpointService.(*checkpoint)
require.Equal(t, cp.doExist, false, "expected false as no cp exist at this point")
_, _, err := rawdb.ReadFinality[*rawdb.Checkpoint](db)
require.NotNil(t, err, "Error should be nil while reading from the db")
//Adding the checkpoint
s.ProcessCheckpoint(11, common.Hash{})
require.Equal(t, cp.doExist, true, "expected true as cp exist")
//Removing the checkpoint
s.PurgeWhitelistedCheckpoint()
require.Equal(t, cp.doExist, false, "expected false as no cp exist at this point")
//Adding the checkpoint
s.ProcessCheckpoint(12, common.Hash{1})
//Receiving the stored checkpoint
doExist, number, hash := s.GetWhitelistedCheckpoint()
//Validating the values received
require.Equal(t, doExist, true, "expected true ascheckpoint exist at this point")
require.Equal(t, number, uint64(12), "expected number to be 11 but got", number)
require.Equal(t, hash, common.Hash{1}, "expected the 1 hash but got", hash)
require.NotEqual(t, hash, common.Hash{}, "expected the hash to be different from zero hash")
c1 := s.checkpointService.(*checkpoint)
fmt.Println("!!!-0", c1.doExist)
s.PurgeWhitelistedCheckpoint()
fmt.Println("!!!-1", c1.doExist)
doExist, number, hash = s.GetWhitelistedCheckpoint()
fmt.Println("!!!-2", c1.doExist)
//Validating the values received from the db, not memory
require.Equal(t, doExist, true, "expected true ascheckpoint exist at this point")
require.Equal(t, number, uint64(12), "expected number to be 11 but got", number)
require.Equal(t, hash, common.Hash{1}, "expected the 1 hash but got", hash)
require.NotEqual(t, hash, common.Hash{}, "expected the hash to be different from zero hash")
checkpointNumber, checkpointHash, err := rawdb.ReadFinality[*rawdb.Checkpoint](db)
require.Nil(t, err, "Error should be nil while reading from the db")
require.Equal(t, checkpointHash, common.Hash{1}, "expected the 1 hash but got", hash)
require.Equal(t, checkpointNumber, uint64(12), "expected number to be 11 but got", number)
}
// TestMilestone checks the milestone whitelist setter and getter functions
func TestMilestone(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
s := NewMockService(db)
milestone := s.milestoneService.(*milestone)
//Checking for the variables when no milestone is Processed
require.Equal(t, milestone.doExist, false, "expected false as no milestone exist at this point")
require.Equal(t, milestone.Locked, false, "expected false as it was not locked")
require.Equal(t, milestone.LockedMilestoneNumber, uint64(0), "expected 0 as it was not initialized")
_, _, err := rawdb.ReadFinality[*rawdb.Milestone](db)
require.NotNil(t, err, "Error should be nil while reading from the db")
//Acquiring the mutex lock
milestone.LockMutex(11)
require.Equal(t, milestone.Locked, false, "expected false as sprint is not locked till this point")
//Releasing the mutex lock
milestone.UnlockMutex(true, "milestoneID1", uint64(11), common.Hash{})
require.Equal(t, milestone.LockedMilestoneNumber, uint64(11), "expected 11 as it was not initialized")
require.Equal(t, milestone.Locked, true, "expected true as sprint is locked now")
require.Equal(t, len(milestone.LockedMilestoneIDs), 1, "expected 1 as only 1 milestoneID has been entered")
_, ok := milestone.LockedMilestoneIDs["milestoneID1"]
require.True(t, ok, "milestoneID1 should exist in the LockedMilestoneIDs map")
_, ok = milestone.LockedMilestoneIDs["milestoneID2"]
require.False(t, ok, "milestoneID2 shouldn't exist in the LockedMilestoneIDs map")
milestone.LockMutex(11)
milestone.UnlockMutex(true, "milestoneID2", uint64(11), common.Hash{})
require.Equal(t, len(milestone.LockedMilestoneIDs), 1, "expected 1 as only 1 milestoneID has been entered")
_, ok = milestone.LockedMilestoneIDs["milestoneID2"]
require.True(t, ok, "milestoneID2 should exist in the LockedMilestoneIDs map")
milestone.RemoveMilestoneID("milestoneID1")
require.Equal(t, len(milestone.LockedMilestoneIDs), 1, "expected 1 as one out of two has been removed in previous step")
require.Equal(t, milestone.Locked, true, "expected true as sprint is locked now")
milestone.RemoveMilestoneID("milestoneID2")
require.Equal(t, len(milestone.LockedMilestoneIDs), 0, "expected 1 as both the milestonesIDs has been removed in previous step")
require.Equal(t, milestone.Locked, false, "expected false")
milestone.LockMutex(11)
milestone.UnlockMutex(true, "milestoneID3", uint64(11), common.Hash{})
require.True(t, milestone.Locked, "expected true")
require.Equal(t, milestone.LockedMilestoneNumber, uint64(11), "Expected 11")
milestone.LockMutex(15)
require.True(t, milestone.Locked, "expected true")
require.Equal(t, milestone.LockedMilestoneNumber, uint64(11), "Expected 11")
milestone.UnlockMutex(true, "milestoneID4", uint64(15), common.Hash{})
require.True(t, milestone.Locked, "expected true as final confirmation regarding the lock has been made")
require.Equal(t, len(milestone.LockedMilestoneIDs), 1, "expected 1 as previous milestonesIDs has been removed in previous step")
//Adding the milestone
s.ProcessMilestone(11, common.Hash{})
require.True(t, milestone.Locked, "expected true as locked sprint is of number 15")
require.Equal(t, milestone.doExist, true, "expected true as milestone exist")
require.Equal(t, len(milestone.LockedMilestoneIDs), 1, "expected 1 as still last milestone of sprint number 15 exist")
//Reading from the Db
locked, lockedMilestoneNumber, lockedMilestoneHash, lockedMilestoneIDs, err := rawdb.ReadLockField(db)
require.Nil(t, err)
require.True(t, locked, "expected true as locked sprint is of number 15")
require.Equal(t, lockedMilestoneNumber, uint64(15), "Expected 15")
require.Equal(t, lockedMilestoneHash, common.Hash{}, "Expected", common.Hash{})
require.Equal(t, len(lockedMilestoneIDs), 1, "expected 1 as still last milestone of sprint number 15 exist")
_, ok = lockedMilestoneIDs["milestoneID4"]
require.True(t, ok, "expected true as milestoneIDList should contain 'milestoneID4'")
//Asking the lock for sprintNumber less than last whitelisted milestone
require.False(t, milestone.LockMutex(11), "Cant lock the sprintNumber less than equal to latest whitelisted milestone")
milestone.UnlockMutex(false, "", uint64(11), common.Hash{}) //Unlock is required after every lock to release the mutex
//Adding the milestone
s.ProcessMilestone(51, common.Hash{})
require.False(t, milestone.Locked, "expected false as lock from sprint number 15 is removed")
require.Equal(t, milestone.doExist, true, "expected true as milestone exist")
require.Equal(t, len(milestone.LockedMilestoneIDs), 0, "expected 0 as all the milestones have been removed")
//Reading from the Db
locked, _, _, lockedMilestoneIDs, err = rawdb.ReadLockField(db)
require.Nil(t, err)
require.False(t, locked, "expected true as locked sprint is of number 15")
require.Equal(t, len(lockedMilestoneIDs), 0, "expected 0 as milestoneID exist in the map")
//Removing the milestone
s.PurgeWhitelistedMilestone()
require.Equal(t, milestone.doExist, false, "expected false as no milestone exist at this point")
//Removing the milestone
s.ProcessMilestone(11, common.Hash{1})
doExist, number, hash := s.GetWhitelistedMilestone()
//validating the values received
require.Equal(t, doExist, true, "expected true as milestone exist at this point")
require.Equal(t, number, uint64(11), "expected number to be 11 but got", number)
require.Equal(t, hash, common.Hash{1}, "expected the 1 hash but got", hash)
s.PurgeWhitelistedMilestone()
doExist, number, hash = s.GetWhitelistedMilestone()
//Validating the values received from the db, not memory
require.Equal(t, doExist, true, "expected true as milestone exist at this point")
require.Equal(t, number, uint64(11), "expected number to be 11 but got", number)
require.Equal(t, hash, common.Hash{1}, "expected the 1 hash but got", hash)
milestoneNumber, milestoneHash, err := rawdb.ReadFinality[*rawdb.Milestone](db)
require.Nil(t, err, "Error should be nil while reading from the db")
require.Equal(t, milestoneHash, common.Hash{1}, "expected the 1 hash but got", hash)
require.Equal(t, milestoneNumber, uint64(11), "expected number to be 11 but got", number)
_, _, err = rawdb.ReadFutureMilestoneList(db)
require.NotNil(t, err, "Error should be not nil")
s.ProcessFutureMilestone(16, common.Hash{16})
require.Equal(t, len(milestone.FutureMilestoneOrder), 1, "expected length is 1 as we added only 1 future milestone")
require.Equal(t, milestone.FutureMilestoneOrder[0], uint64(16), "expected value is 16 but got", milestone.FutureMilestoneOrder[0])
require.Equal(t, milestone.FutureMilestoneList[16], common.Hash{16}, "expected value is", common.Hash{16}.String()[2:], "but got", milestone.FutureMilestoneList[16])
order, list, err := rawdb.ReadFutureMilestoneList(db)
require.Nil(t, err, "Error should be nil while reading from the db")
require.Equal(t, len(order), 1, "expected the 1 hash but got", len(order))
require.Equal(t, order[0], uint64(16), "expected number to be 16 but got", order[0])
require.Equal(t, list[order[0]], common.Hash{16}, "expected value is", common.Hash{16}.String()[2:], "but got", list[order[0]])
capacity := milestone.MaxCapacity
for i := 16; i <= 16*(capacity+1); i = i + 16 {
s.ProcessFutureMilestone(uint64(i), common.Hash{16})
}
require.Equal(t, len(milestone.FutureMilestoneOrder), capacity, "expected length is", capacity)
require.Equal(t, milestone.FutureMilestoneOrder[capacity-1], uint64(16*capacity), "expected value is", uint64(16*capacity), "but got", milestone.FutureMilestoneOrder[capacity-1])
}
// TestIsValidPeer checks the IsValidPeer function in isolation
// for different cases by providing a mock fetchHeadersByNumber function
func TestIsValidPeer(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
s := NewMockService(db)
// case1: no checkpoint whitelist, should consider the chain as valid
res, err := s.IsValidPeer(nil)
require.NoError(t, err, "expected no error")
require.Equal(t, res, true, "expected chain to be valid")
// add checkpoint entry and mock fetchHeadersByNumber function
s.ProcessCheckpoint(uint64(1), common.Hash{})
// add milestone entry and mock fetchHeadersByNumber function
s.ProcessMilestone(uint64(1), common.Hash{})
checkpoint := s.checkpointService.(*checkpoint)
milestone := s.milestoneService.(*milestone)
//Check whether the milestone and checkpoint exist
require.Equal(t, checkpoint.doExist, true, "expected true as checkpoint exists")
require.Equal(t, milestone.doExist, true, "expected true as milestone exists")
// create a false function, returning absolutely nothing
falseFetchHeadersByNumber := func(number uint64, amount int, skip int, reverse bool) ([]*types.Header, []common.Hash, error) {
return nil, nil, nil
}
// case2: false fetchHeadersByNumber function provided, should consider the chain as invalid
// and throw `ErrNoRemoteCheckoint` error
res, err = s.IsValidPeer(falseFetchHeadersByNumber)
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, ErrNoRemote) {
t.Fatalf("expected error ErrNoRemote, got %v", err)
}
require.Equal(t, res, false, "expected peer chain to be invalid")
// create a mock function, returning the required header
fetchHeadersByNumber := func(number uint64, _ int, _ int, _ bool) ([]*types.Header, []common.Hash, error) {
hash := common.Hash{}
header := types.Header{Number: big.NewInt(0)}
switch number {
case 0:
return []*types.Header{&header}, []common.Hash{hash}, nil
case 1:
header.Number = big.NewInt(1)
return []*types.Header{&header}, []common.Hash{hash}, nil
case 2:
header.Number = big.NewInt(1) // sending wrong header for misamatch
return []*types.Header{&header}, []common.Hash{hash}, nil
default:
return nil, nil, errors.New("invalid number")
}
}
// case3: correct fetchHeadersByNumber function provided, should consider the chain as valid
res, err = s.IsValidPeer(fetchHeadersByNumber)
require.NoError(t, err, "expected no error")
require.Equal(t, res, true, "expected chain to be valid")
// add checkpoint whitelist entry
s.ProcessCheckpoint(uint64(2), common.Hash{})
require.Equal(t, checkpoint.doExist, true, "expected true as checkpoint exists")
// case4: correct fetchHeadersByNumber function provided with wrong header
// for block number 2. Should consider the chain as invalid and throw an error
res, err = s.IsValidPeer(fetchHeadersByNumber)
require.Equal(t, err, ErrMismatch, "expected mismatch error")
require.Equal(t, res, false, "expected chain to be invalid")
// create a mock function, returning the required header
fetchHeadersByNumber = func(number uint64, _ int, _ int, _ bool) ([]*types.Header, []common.Hash, error) {
hash := common.Hash{}
header := types.Header{Number: big.NewInt(0)}
switch number {
case 0:
return []*types.Header{&header}, []common.Hash{hash}, nil
case 1:
header.Number = big.NewInt(1)
return []*types.Header{&header}, []common.Hash{hash}, nil
case 2:
header.Number = big.NewInt(2)
return []*types.Header{&header}, []common.Hash{hash}, nil
case 3:
header.Number = big.NewInt(3)
hash3 := common.Hash{3}
return []*types.Header{&header}, []common.Hash{hash3}, nil
default:
return nil, nil, errors.New("invalid number")
}
}
s.ProcessMilestone(uint64(3), common.Hash{})
//Case5: correct fetchHeadersByNumber function provided with hash mismatch, should consider the chain as invalid
res, err = s.IsValidPeer(fetchHeadersByNumber)
require.Equal(t, err, ErrMismatch, "expected milestone mismatch error")
require.Equal(t, res, false, "expected chain to be invalid")
s.ProcessMilestone(uint64(2), common.Hash{})
// create a mock function, returning the required header
fetchHeadersByNumber = func(number uint64, _ int, _ int, _ bool) ([]*types.Header, []common.Hash, error) {
hash := common.Hash{}
header := types.Header{Number: big.NewInt(0)}
switch number {
case 0:
return []*types.Header{&header}, []common.Hash{hash}, nil
case 1:
header.Number = big.NewInt(1)
return []*types.Header{&header}, []common.Hash{hash}, nil
case 2:
header.Number = big.NewInt(2)
return []*types.Header{&header}, []common.Hash{hash}, nil
default:
return nil, nil, errors.New("invalid number")
}
}
// case6: correct fetchHeadersByNumber function provided, should consider the chain as valid
res, err = s.IsValidPeer(fetchHeadersByNumber)
require.NoError(t, err, "expected no error")
require.Equal(t, res, true, "expected chain to be valid")
// create a mock function, returning the required header
fetchHeadersByNumber = func(number uint64, _ int, _ int, _ bool) ([]*types.Header, []common.Hash, error) {
hash := common.Hash{}
hash3 := common.Hash{3}
header := types.Header{Number: big.NewInt(0)}
switch number {
case 0:
return []*types.Header{&header}, []common.Hash{hash}, nil
case 1:
header.Number = big.NewInt(1)
return []*types.Header{&header}, []common.Hash{hash}, nil
case 2:
header.Number = big.NewInt(2)
return []*types.Header{&header}, []common.Hash{hash}, nil
case 3:
header.Number = big.NewInt(2) // sending wrong header for misamatch
return []*types.Header{&header}, []common.Hash{hash}, nil
case 4:
header.Number = big.NewInt(4) // sending wrong header for misamatch
return []*types.Header{&header}, []common.Hash{hash3}, nil
default:
return nil, nil, errors.New("invalid number")
}
}
//Add one more milestone in the list
s.ProcessMilestone(uint64(3), common.Hash{})
// case7: correct fetchHeadersByNumber function provided with wrong header for block 3, should consider the chain as invalid
res, err = s.IsValidPeer(fetchHeadersByNumber)
require.Equal(t, err, ErrMismatch, "expected milestone mismatch error")
require.Equal(t, res, false, "expected chain to be invalid")
//require.Equal(t, milestone.length(), 3, "expected 3 items in milestoneList")
//Add one more milestone in the list
s.ProcessMilestone(uint64(4), common.Hash{})
// case8: correct fetchHeadersByNumber function provided with wrong hash for block 3, should consider the chain as valid
res, err = s.IsValidPeer(fetchHeadersByNumber)
require.Equal(t, err, ErrMismatch, "expected milestone mismatch error")
require.Equal(t, res, false, "expected chain to be invalid")
}
// TestIsValidChain checks the IsValidChain function in isolation
// for different cases by providing a mock current header and chain
func TestIsValidChain(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
s := NewMockService(db)
chainA := createMockChain(1, 20) // A1->A2...A19->A20
//Case1: no checkpoint whitelist and no milestone and no locking, should consider the chain as valid
res, err := s.IsValidChain(nil, chainA)
require.Nil(t, err)
require.Equal(t, res, true, "Expected chain to be valid")
tempChain := createMockChain(21, 22) // A21->A22
// add mock checkpoint entry
s.ProcessCheckpoint(tempChain[1].Number.Uint64(), tempChain[1].Hash())
//Make the mock chain with zero blocks
zeroChain := make([]*types.Header, 0)
//Case2: As input chain is of zero length,should consider the chain as invalid
res, err = s.IsValidChain(nil, zeroChain)
require.Nil(t, err)
require.Equal(t, res, false, "expected chain to be invalid", len(zeroChain))
//Case3A: As the received chain and current tip of local chain is behind the oldest whitelisted block entry, should consider
// the chain as valid
res, err = s.IsValidChain(chainA[len(chainA)-1], chainA)
require.Nil(t, err)
require.Equal(t, res, true, "expected chain to be valid")
//Case3B: As the received chain is behind the oldest whitelisted block entry,but current tip is at par with whitelisted checkpoint, should consider
// the chain as invalid
res, err = s.IsValidChain(tempChain[1], chainA)
require.Nil(t, err)
require.Equal(t, res, false, "expected chain to be invalid ")
// add mock milestone entry
s.ProcessMilestone(tempChain[1].Number.Uint64(), tempChain[1].Hash())
//Case4A: As the received chain and current tip of local chain is behind the oldest whitelisted block entry, should consider
// the chain as valid
res, err = s.IsValidChain(chainA[len(chainA)-1], chainA)
require.Nil(t, err)
require.Equal(t, res, true, "expected chain to be valid")
//Case4B: As the received chain is behind the oldest whitelisted block entry and but current tip is at par with whitelisted milestine, should consider
// the chain as invalid
res, err = s.IsValidChain(tempChain[1], chainA)
require.Nil(t, err)
require.Equal(t, res, false, "expected chain to be invalid")
//Remove the whitelisted checkpoint
s.PurgeWhitelistedCheckpoint()
//Case5: As the received chain is still invalid after removing the checkpoint as it is
//still behind the whitelisted milestone
res, err = s.IsValidChain(tempChain[1], chainA)
require.Nil(t, err)
require.Equal(t, res, false, "expected chain to be invalid")
//Remove the whitelisted milestone
s.PurgeWhitelistedMilestone()
//At this stage there is no whitelisted milestone and checkpoint
checkpoint := s.checkpointService.(*checkpoint)
milestone := s.milestoneService.(*milestone)
//Locking for sprintNumber 15
milestone.LockMutex(chainA[len(chainA)-5].Number.Uint64())
milestone.UnlockMutex(true, "MilestoneID1", chainA[len(chainA)-5].Number.Uint64(), chainA[len(chainA)-5].Hash())
//Case6: As the received chain is valid as the locked sprintHash matches with the incoming chain.
res, err = s.IsValidChain(chainA[len(chainA)-1], chainA)
require.Nil(t, err)
require.Equal(t, res, true, "expected chain to be valid as incoming chain matches with the locked value ")
hash3 := common.Hash{3}
//Locking for sprintNumber 16 with different hash
milestone.LockMutex(chainA[len(chainA)-4].Number.Uint64())
milestone.UnlockMutex(true, "MilestoneID2", chainA[len(chainA)-4].Number.Uint64(), hash3)
res, err = s.IsValidChain(chainA[len(chainA)-1], chainA)
require.Nil(t, err)
require.Equal(t, res, false, "expected chain to be invalid as incoming chain does match with the locked value hash ")
//Locking for sprintNumber 19
milestone.LockMutex(chainA[len(chainA)-1].Number.Uint64())
milestone.UnlockMutex(true, "MilestoneID1", chainA[len(chainA)-1].Number.Uint64(), chainA[len(chainA)-1].Hash())
//Case7: As the received chain is valid as the locked sprintHash matches with the incoming chain.
res, err = s.IsValidChain(chainA[len(chainA)-1], chainA)
require.Nil(t, err)
require.Equal(t, res, false, "expected chain to be invalid as incoming chain is less than the locked value ")
//Locking for sprintNumber 19
milestone.LockMutex(uint64(21))
milestone.UnlockMutex(true, "MilestoneID1", uint64(21), hash3)
//Case8: As the received chain is invalid as the locked sprintHash matches is ahead of incoming chain.
res, err = s.IsValidChain(chainA[len(chainA)-1], chainA)
require.Nil(t, err)
require.Equal(t, res, false, "expected chain to be invalid as incoming chain is less than the locked value ")
//Unlocking the sprint
milestone.UnlockSprint(uint64(21))
// Clear checkpoint whitelist and add block A15 in whitelist
s.PurgeWhitelistedCheckpoint()
s.ProcessCheckpoint(chainA[15].Number.Uint64(), chainA[15].Hash())
require.Equal(t, checkpoint.doExist, true, "expected true as checkpoint exists.")
// case9: As the received chain is having valid checkpoint,should consider the chain as valid.
res, err = s.IsValidChain(chainA[len(chainA)-1], chainA)
require.Nil(t, err)
require.Equal(t, res, true, "expected chain to be valid")
// add mock milestone entries
s.ProcessMilestone(tempChain[1].Number.Uint64(), tempChain[1].Hash())
// case10: Try importing a past chain having valid checkpoint, should
// consider the chain as invalid as still lastest milestone is ahead of the chain.
res, err = s.IsValidChain(tempChain[1], chainA)
require.Nil(t, err)
require.Equal(t, res, false, "expected chain to be invalid")
// add mock milestone entries
s.ProcessMilestone(chainA[19].Number.Uint64(), chainA[19].Hash())
// case12: Try importing a chain having valid checkpoint and milestone, should
// consider the chain as valid
res, err = s.IsValidChain(tempChain[1], chainA)
require.Nil(t, err)
require.Equal(t, res, true, "expected chain to be invalid")
// add mock milestone entries
s.ProcessMilestone(chainA[19].Number.Uint64(), chainA[19].Hash())
// case13: Try importing a past chain having valid checkpoint and milestone, should
// consider the chain as valid
res, err = s.IsValidChain(tempChain[1], chainA)
require.Nil(t, err)
require.Equal(t, res, true, "expected chain to be valid")
// add mock milestone entries with wrong hash
s.ProcessMilestone(chainA[19].Number.Uint64(), chainA[18].Hash())
// case14: Try importing a past chain having valid checkpoint and milestone with wrong hash, should
// consider the chain as invalid
res, err = s.IsValidChain(chainA[len(chainA)-1], chainA)
require.Nil(t, err)
require.Equal(t, res, false, "expected chain to be invalid as hash mismatches")
// Clear milestone and add blocks A15 in whitelist
s.ProcessMilestone(chainA[15].Number.Uint64(), chainA[15].Hash())
// case16: Try importing a past chain having valid checkpoint, should
// consider the chain as valid
res, err = s.IsValidChain(tempChain[1], chainA)
require.Nil(t, err)
require.Equal(t, res, true, "expected chain to be valid")
// Clear checkpoint whitelist and mock blocks in whitelist
tempChain = createMockChain(20, 20) // A20
s.PurgeWhitelistedCheckpoint()
s.ProcessCheckpoint(tempChain[0].Number.Uint64(), tempChain[0].Hash())
require.Equal(t, checkpoint.doExist, true, "expected true")
// case17: Try importing a past chain having invalid checkpoint,should consider the chain as invalid
res, err = s.IsValidChain(tempChain[0], chainA)
require.Nil(t, err)
require.Equal(t, res, false, "expected chain to be invalid")
// Not checking error here because we return nil in case of checkpoint mismatch
// case18: Try importing a future chain but within interval, should consider the chain as valid
res, err = s.IsValidChain(tempChain[len(tempChain)-1], tempChain)
require.Nil(t, err)
require.Equal(t, res, true, "expected chain to be invalid")
// create a future chain to be imported of length <= `checkpointInterval`
chainB := createMockChain(21, 30) // B21->B22...B29->B30
// case19: Try importing a future chain of acceptable length,should consider the chain as valid
res, err = s.IsValidChain(tempChain[0], chainB)
require.Nil(t, err)
require.Equal(t, res, true, "expected chain to be valid")
s.PurgeWhitelistedCheckpoint()
s.PurgeWhitelistedMilestone()
chainB = createMockChain(21, 29) // C21->C22....C29
s.milestoneService.ProcessFutureMilestone(29, chainB[8].Hash())
// case20: Try importing a future chain which match the future milestone should the chain as valid
res, err = s.IsValidChain(tempChain[0], chainB)
require.Nil(t, err)
require.Equal(t, res, true, "expected chain to be valid")
chainB = createMockChain(21, 27) // C21->C22...C39->C40...C->256
// case21: Try importing a chain whose end point is less than future milestone
res, err = s.IsValidChain(tempChain[0], chainB)
require.Nil(t, err)
require.Equal(t, res, true, "expected chain to be valid")
chainB = createMockChain(30, 39) // C21->C22...C39->C40...C->256
//Processing wrong hash
s.milestoneService.ProcessFutureMilestone(38, chainB[9].Hash())
// case22: Try importing a future chain with mismatch future milestone
res, err = s.IsValidChain(tempChain[0], chainB)
require.Nil(t, err)
require.Equal(t, res, false, "expected chain to be invalid")
chainB = createMockChain(40, 49) // C40->C41...C48->C49
// case23: Try importing a future chain whose starting point is ahead of latest future milestone
res, err = s.IsValidChain(tempChain[0], chainB)
require.Nil(t, err)
require.Equal(t, res, true, "expected chain to be invalid")
}
func TestPropertyBasedTestingMilestone(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
db := rawdb.NewMemoryDatabase()
milestone := milestone{
finality: finality[*rawdb.Milestone]{
doExist: false,
Number: 0,
Hash: common.Hash{},
interval: 256,
db: db,
},
Locked: false,
LockedMilestoneNumber: 0,
LockedMilestoneHash: common.Hash{},
LockedMilestoneIDs: make(map[string]struct{}),
FutureMilestoneList: make(map[uint64]common.Hash),
FutureMilestoneOrder: make([]uint64, 0),
MaxCapacity: 10,
}
var (
milestoneEndNum = rapid.Uint64().Draw(t, "endBlock")
milestoneID = rapid.String().Draw(t, "MilestoneID")
doLock = rapid.Bool().Draw(t, "Voted")
)
val := milestone.LockMutex(milestoneEndNum.(uint64))
if !val {
t.Error("LockMutex need to return true when there is no whitelisted milestone and locked milestone")
}
milestone.UnlockMutex(doLock.(bool), milestoneID.(string), milestoneEndNum.(uint64), common.Hash{})
if doLock.(bool) {
//Milestone should not be whitelisted
if milestone.doExist {
t.Error("Milestone is not expected to be whitelisted")
}
//Local chain should be locked
if !milestone.Locked {
t.Error("Milestone is expected to be locked at", milestoneEndNum.(uint64))
}
if milestone.LockedMilestoneNumber != milestoneEndNum.(uint64) {
t.Error("Locked milestone number is expected to be", milestoneEndNum.(uint64))
}
if len(milestone.LockedMilestoneIDs) != 1 {
t.Error("List should contain 1 milestone")
}
_, ok := milestone.LockedMilestoneIDs[milestoneID.(string)]
if !ok {
t.Error("List doesn't contain correct milestoneID")
}
}
if !doLock.(bool) {
if milestone.doExist {
t.Error("Milestone is not expected to be whitelisted")
}
if milestone.Locked {
t.Error("Milestone is expected not to be locked")
}
if milestone.LockedMilestoneNumber != 0 {
t.Error("Locked milestone number is expected to be", 0)
}
if len(milestone.LockedMilestoneIDs) != 0 {
t.Error("List should not contain milestone")
}
_, ok := milestone.LockedMilestoneIDs[milestoneID.(string)]
if ok {
t.Error("List shouldn't contain any milestoneID")
}
}
fitlerFn := func(i uint64) bool {
if i <= uint64(1000) {
return true
}
return false
}
var (
start = rapid.Uint64Max(milestoneEndNum.(uint64)).Draw(t, "start for mock chain")
end = rapid.Uint64Min(start.(uint64)).Filter(fitlerFn).Draw(t, "end for mock chain")
)
chainTemp := createMockChain(start.(uint64), end.(uint64))
val, err := milestone.IsValidChain(chainTemp[0], chainTemp)
if err != nil {
t.Error("Error", err)
}
if doLock.(bool) && val {
t.Error("When the chain is locked at milestone, it should not pass IsValidChain for incompatible incoming chain")
}
if !doLock.(bool) && !val {
t.Error("When the chain is not locked at milestone, it should pass IsValidChain for incoming chain")
}
var (
milestoneEndNum2 = rapid.Uint64().Draw(t, "endBlockNum 2")
milestoneID2 = rapid.String().Draw(t, "MilestoneID 2")
doLock2 = rapid.Bool().Draw(t, "Voted 2")
)
val = milestone.LockMutex(milestoneEndNum2.(uint64))
if doLock.(bool) && milestoneEndNum.(uint64) > milestoneEndNum2.(uint64) && val {
t.Error("LockMutex need to return false as previous locked milestone is greater")
}
if doLock.(bool) && milestoneEndNum.(uint64) <= milestoneEndNum2.(uint64) && !val {
t.Error("LockMutex need to return true as previous locked milestone is less")
}
milestone.UnlockMutex(doLock2.(bool), milestoneID2.(string), milestoneEndNum2.(uint64), common.Hash{})
if doLock2.(bool) {
if milestone.doExist {
t.Error("Milestone is not expected to be whitelisted")
}
if !milestone.Locked {
t.Error("Milestone is expected to be locked at", milestoneEndNum2.(uint64))
}
if milestone.LockedMilestoneNumber != milestoneEndNum2.(uint64) {
t.Error("Locked milestone number is expected to be", milestoneEndNum.(uint64))
}
if len(milestone.LockedMilestoneIDs) != 1 {
t.Error("List should contain 1 milestone")
}
_, ok := milestone.LockedMilestoneIDs[milestoneID2.(string)]
if !ok {
t.Error("List doesn't contain correct milestoneID")
}
}
if !doLock2.(bool) {
if milestone.doExist {
t.Error("Milestone is not expected to be whitelisted")
}
if !doLock.(bool) && milestone.Locked {
t.Error("Milestone is expected not to be locked")
}
if doLock.(bool) && !milestone.Locked {
t.Error("Milestone is expected to be locked at", milestoneEndNum.(uint64))
}
if !doLock.(bool) && milestone.LockedMilestoneNumber != 0 {
t.Error("Locked milestone number is expected to be", 0)
}
if doLock.(bool) && milestone.LockedMilestoneNumber != milestoneEndNum.(uint64) {
t.Error("Locked milestone number is expected to be", milestoneEndNum.(uint64))
}
if !doLock.(bool) && len(milestone.LockedMilestoneIDs) != 0 {
t.Error("List should not contain milestone")
}
if doLock.(bool) && len(milestone.LockedMilestoneIDs) != 1 {
t.Error("List should not contain milestone")
}
_, ok := milestone.LockedMilestoneIDs[milestoneID.(string)]
if !doLock.(bool) && ok {
t.Error("List shouldn't contain any milestoneID")
}
if doLock.(bool) && !ok {
t.Error("List should contain milestoneID")
}
}
var (
milestoneNum = rapid.Uint64().Draw(t, "milestone Number")
)
lockedValue := milestone.LockedMilestoneNumber
milestone.Process(milestoneNum.(uint64), common.Hash{})
isChainLocked := doLock.(bool) || doLock2.(bool)
if !milestone.doExist {
t.Error("Should have the whitelisted milestone")
}
if milestone.finality.Number != milestoneNum.(uint64) {
t.Error("Should have the whitelisted milestone", milestoneNum.(uint64))
}
if isChainLocked {
if milestoneNum.(uint64) < lockedValue {
if !milestone.Locked {
t.Error("Milestone is expected to be locked")
}
} else {
if milestone.Locked {
t.Error("Milestone is expected not to be locked")
}
}
}
var (
futureMilestoneNum = rapid.Uint64Min(milestoneNum.(uint64)).Draw(t, "future milestone Number")
)
isChainLocked = milestone.Locked
milestone.ProcessFutureMilestone(futureMilestoneNum.(uint64), common.Hash{})
if isChainLocked {
if futureMilestoneNum.(uint64) < lockedValue {
if !milestone.Locked {
t.Error("Milestone is expected to be locked")
}
} else {
if milestone.Locked {
t.Error("Milestone is expected not to be locked")
}
}
}
})
}
func TestSplitChain(t *testing.T) {
t.Parallel()
type Result struct {
pastStart uint64
pastEnd uint64
futureStart uint64
futureEnd uint64
pastLength int
futureLength int
}
// Current chain is at block: X
// Incoming chain is represented as [N, M]
testCases := []struct {
name string
current uint64
chain []*types.Header
result Result
}{
{name: "X = 10, N = 11, M = 20", current: uint64(10), chain: createMockChain(11, 20), result: Result{futureStart: 11, futureEnd: 20, futureLength: 10}},
{name: "X = 10, N = 13, M = 20", current: uint64(10), chain: createMockChain(13, 20), result: Result{futureStart: 13, futureEnd: 20, futureLength: 8}},
{name: "X = 10, N = 2, M = 10", current: uint64(10), chain: createMockChain(2, 10), result: Result{pastStart: 2, pastEnd: 10, pastLength: 9}},
{name: "X = 10, N = 2, M = 9", current: uint64(10), chain: createMockChain(2, 9), result: Result{pastStart: 2, pastEnd: 9, pastLength: 8}},
{name: "X = 10, N = 2, M = 8", current: uint64(10), chain: createMockChain(2, 8), result: Result{pastStart: 2, pastEnd: 8, pastLength: 7}},
{name: "X = 10, N = 5, M = 15", current: uint64(10), chain: createMockChain(5, 15), result: Result{pastStart: 5, pastEnd: 10, pastLength: 6, futureStart: 11, futureEnd: 15, futureLength: 5}},
{name: "X = 10, N = 10, M = 20", current: uint64(10), chain: createMockChain(10, 20), result: Result{pastStart: 10, pastEnd: 10, pastLength: 1, futureStart: 11, futureEnd: 20, futureLength: 10}},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
past, future := splitChain(tc.current, tc.chain)
require.Equal(t, len(past), tc.result.pastLength)
require.Equal(t, len(future), tc.result.futureLength)
if len(past) > 0 {
// Check if we have expected block/s
require.Equal(t, past[0].Number.Uint64(), tc.result.pastStart)
require.Equal(t, past[len(past)-1].Number.Uint64(), tc.result.pastEnd)
}
if len(future) > 0 {
// Check if we have expected block/s
require.Equal(t, future[0].Number.Uint64(), tc.result.futureStart)
require.Equal(t, future[len(future)-1].Number.Uint64(), tc.result.futureEnd)
}
})
}
}
//nolint:gocognit
func TestSplitChainProperties(t *testing.T) {
t.Parallel()
// Current chain is at block: X
// Incoming chain is represented as [N, M]
currentChain := []int{0, 1, 2, 3, 10, 100} // blocks starting from genesis
blockDiffs := []int{0, 1, 2, 3, 4, 5, 9, 10, 11, 12, 90, 100, 101, 102}
caseParams := make(map[int]map[int]map[int]struct{}) // X -> N -> M
for _, current := range currentChain {
// past cases only + past to current
for _, diff := range blockDiffs {
from := current - diff
// use int type for everything to not care about underflow
if from < 0 {
continue
}
for _, diff := range blockDiffs {
to := current - diff
if to >= from {
addTestCaseParams(caseParams, current, from, to)
}
}
}
// future only + current to future
for _, diff := range blockDiffs {
from := current + diff
if from < 0 {
continue
}
for _, diff := range blockDiffs {
to := current + diff
if to >= from {
addTestCaseParams(caseParams, current, from, to)
}
}
}
// past-current-future
for _, diff := range blockDiffs {
from := current - diff
if from < 0 {
continue
}
for _, diff := range blockDiffs {
to := current + diff
if to >= from {
addTestCaseParams(caseParams, current, from, to)
}
}
}
}
type testCase struct {
current int
remoteStart int
remoteEnd int
}
var ts []testCase
// X -> N -> M
for x, nm := range caseParams {
for n, mMap := range nm {
for m := range mMap {
ts = append(ts, testCase{x, n, m})
}
}
}
//nolint:paralleltest
for i, tc := range ts {
tc := tc
name := fmt.Sprintf("test case: index = %d, X = %d, N = %d, M = %d", i, tc.current, tc.remoteStart, tc.remoteEnd)
t.Run(name, func(t *testing.T) {
t.Parallel()
chain := createMockChain(uint64(tc.remoteStart), uint64(tc.remoteEnd))
past, future := splitChain(uint64(tc.current), chain)
// properties
if len(past) > 0 {
// Check if the chain is ordered
isOrdered := sort.SliceIsSorted(past, func(i, j int) bool {
return past[i].Number.Uint64() < past[j].Number.Uint64()
})
require.True(t, isOrdered, "an ordered past chain expected: %v", past)
isSequential := sort.SliceIsSorted(past, func(i, j int) bool {
return past[i].Number.Uint64() == past[j].Number.Uint64()-1
})
require.True(t, isSequential, "a sequential past chain expected: %v", past)
// Check if current block >= past chain's last block
require.Equal(t, past[len(past)-1].Number.Uint64() <= uint64(tc.current), true)
}
if len(future) > 0 {
// Check if the chain is ordered
isOrdered := sort.SliceIsSorted(future, func(i, j int) bool {
return future[i].Number.Uint64() < future[j].Number.Uint64()
})
require.True(t, isOrdered, "an ordered future chain expected: %v", future)
isSequential := sort.SliceIsSorted(future, func(i, j int) bool {
return future[i].Number.Uint64() == future[j].Number.Uint64()-1
})
require.True(t, isSequential, "a sequential future chain expected: %v", future)
// Check if future chain's first block > current block
require.Equal(t, future[len(future)-1].Number.Uint64() > uint64(tc.current), true)
}
// Check if both chains are continuous
if len(past) > 0 && len(future) > 0 {
require.Equal(t, past[len(past)-1].Number.Uint64(), future[0].Number.Uint64()-1)
}
// Check if we get the original chain on appending both
gotChain := append(past, future...)
require.Equal(t, reflect.DeepEqual(gotChain, chain), true)
})
}
}
// createMockChain returns a chain with dummy headers
// starting from `start` to `end` (inclusive)
func createMockChain(start, end uint64) []*types.Header {
var (
i uint64
idx uint64
)
chain := make([]*types.Header, end-start+1)
for i = start; i <= end; i++ {
header := &types.Header{
Number: big.NewInt(int64(i)),
Time: uint64(time.Now().UnixMicro()) + i,
}
chain[idx] = header
idx++
}
return chain
}
// mXNM should be initialized
func addTestCaseParams(mXNM map[int]map[int]map[int]struct{}, x, n, m int) {
//nolint:ineffassign
mNM, ok := mXNM[x]
if !ok {
mNM = make(map[int]map[int]struct{})
mXNM[x] = mNM
}
//nolint:ineffassign
_, ok = mNM[n]
if !ok {
mM := make(map[int]struct{})
mNM[n] = mM
}
mXNM[x][n][m] = struct{}{}
}