go-ethereum/eth/downloader/whitelist/service_test.go
Arpit Temani b48b89f852
all: implement milestones (#961)
* Milestone Implementation

* Merge branch 'POS-347' into reciept-e2e-test

* Changes for testing, will be removed after testing

* Debugged the error

* Just for testing purpose

* refactor debug api methods, rename whitelist -> checkpoint

* remove first iteration based vars

* fix linters

* Rewind Changes

* Error changes

* RewindBack function in bor_checkpoint_verifier

* Testcases

* Added the fetch test for milestone and checkpoint

* Debugged the lint changes

* Debugged the lint changes

* Debugged the lint changes

* Debugged the lint changes

* Improved the error in Miner test file

* Improved the error of pointing to the wrong function

* Locking the sprint after the vote has been made on it.

* Adding more logs for testing

* Adding more logs for testing

* Adding more logs for testing

* Implemented the NoAckMilestone fetching mechanism

* Testcases for milestone implementation

* Testing code  for fetchNoAckMilestone and fetchLastNoAckMilestone

* Testing changes

* refactor else-if

* Corrected the number of params in bor_ext.go

* Dummy API for testing

* Defined the GetVoteOnRootHash in interface

* Defined the GetVoteOnRootHash in interface

* Made changes in the web3ext file

* Added the GetVoteOnRootHash in PublicBlockChain API

* Added the GetVoteOnRootHash in filterBackend

* Added the log of Root and RooHash

* Removed the 0x from rootHash

* Just for testing purpose

* "GetVoteOnRootHash" mock implementation

* bor_test.go

* Added the test for milestone implementation

* Added service for fetching milestone by ID

* Improved the comments

* Removed the duplicate code

* use setter for borVerifier

* use setter for borVerifier

* refactor handleNoAckMilestone

* remove code repetition with retry function

* Converged the repetitive code

* after CR

* persistence

* persistence implementation

* feature flag

* Persistence Changes

* cr

* initial

* fix

* fix

* Whitelist Flag

* 1 Add:Included the milestone flag  2.Add:Hardlimit the rewind to maximum of 255 blocks

* Chg:Updated go.mod file

* Remove:Dubai Hardfork code

* Add:checked errors for call functions to the Db, Rmv: Remote Header variable from the IsValidPeer() function

* Fix:Linting issues'

* Add:MilestoneGRPC functions

* Fix:Lint issues

* Fix:Lint issues

* Fix: TestFetchMilestoneFromMockHeimdall

* Fix:Integrations tests

* Add:Test for sprint length and milestone changes

* Add:Functionality to fetch the finalized block

* Chg:Changed default val of TriesInmemory to 1024

* fix:Some functions of heimdallGRPC client

* Restored the GRPC functionality, was commented out for  developing purpose

* Fix:Bor_checkpoint_Verfier function

* Test:Added the chain Rewinding test

* Test:Added the Sprint Length + Milestone merge test

* Add:Implemented the future milestone

* Add:Future milestone changes

* Add:Future milestone changes

* Chg: Voting on endBlockHash rather than rootHash

* Chg: Changed the logic of future milestone from rootHash checking to endBlockHash checking

* Fix:Using endBockHash while verifying the incoming milestone

* Chg:Variable names for better readiblity

* Fix:Testing changes

* Add:metrics for milestone implementation

* Add:Metrics for milestone implementatian

* Fix:Order of statements in a function for better optimization

* Chg:Removed unrequired file

* Fix:new variable intialization

* Add:Comment to increase readiblity

* Fix:Logs

* Chg:Name of GetVoteOnRootHash to GetVoteOnHash

* Fix:Linting issues

* Fixed linting issues

* Rmv: Unnecessary logs and Add:Skip test for long tests

* Fix:Checking current chain with whitelisted milestone or checkpoint in Finalized block function

* Fix:Test

* Fix:Whitelisting of Milestone and Checkpoint process

* Fix: Milestone JSON structure

* Chg:Testcases changes

* Fix:Change from VoteOnRootHash to VoteOnHash

* Fix:Variable name fix

* Fix:Finalized API

* internal/jsre/deps: update web3.js bundle

* Fix:milestone verifier

* Chg:Handling the long future chain import issue

* Fix:Lint issues

* Fix:TestLowDiffLongChain and TestPrunedImportSide tests, used hardcoded value 128 instead of DefaultTriesInMemory value

* Chg:Testcode for producing metrics

* Chg:Milestong polling value to 32 secs

* Add:Testcases

* Add:Implemented the check to fetch the milestoneId from heimdall before locking the fork

* Added GRPC method for FetchMilestoneID

* Fix:lint issue

* Fix:lint issue

* Skiped out the tests which were mainly used to produce the supporting data

* remove vcs build when running snyk

* Add:Improved the logs and comments

* fix linters

* Skipped some test as they are panic due to timeout issue in github

* Chg:Variable name LockerSprintNumber to LockedMilestoneNumber for better readablity and clarity

* Chg:Conflicting variable names in milestone test file

* Chg:Conflicting function names in milestone test file

* fix : minor fix in TestInsertingSpanSizeBlocks

* Fix:Mocking issue in TestInsertingSpanSizeBlocks

* Fix:GRPC Polyproto Version

* eth/downloader: skip peer drop due to whitelisting err

* eth, tests/bor: bug fixes and minor refactor

* Add:Implemented the milestone related functions in the HeimdallApp

* Fix:Lint Errors & Remove:Redundant Code

* Fix:Testing Errors

* Fix:Bor integeration tests

* Fix:Test errors

* update heimdall client mock files

* remove unused arguments

* remove redundant code

* Chg:Changed the milestone polling intervals

* Add: added block finality from whitelisted checkpoint

* skip future chain validation

* Add:confirmation check of 16 blocks over the end block while voting for the milestone in GetVoteHash() function

* Chg:Included endBlockNum in UnlockMutex function

* Add:Property based test for milestone

* Fix:Opening the lock while processing future milestone

* Add:Property based test for futureMilestone

* Defined the value of TempTriesInMemory

* Fixed the finalized api

* Fixed lint issues

* eth: add logs while fetching and rewinding

* fix linters: use default returns instead of recursive calls

* Fix:Milestone intergration test

* Add:GetVoteHash fn in mock backend

* tests/bor: fix mock span

* tests/bor: remove t.Parallel()

* use bor namespace in ethclient, fix mock function

---------

Co-authored-by: Vaibhav Jindal <vaibhavjindal29@gmail.com>
Co-authored-by: VaibhavJindal <74560896+VAIBHAVJINDAL3012@users.noreply.github.com>
Co-authored-by: Manav Darji <manavdarji.india@gmail.com>
Co-authored-by: Evgeny Danienko <6655321@bk.ru>
Co-authored-by: Shivam Sharma <shivam691999@gmail.com>
Co-authored-by: Anshal Shukla <shukla.anshal85@gmail.com>
2023-08-28 18:42:21 +05:30

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]])
capicity := milestone.MaxCapacity
for i := 16; i <= 16*(capicity+1); i = i + 16 {
s.ProcessFutureMilestone(uint64(i), common.Hash{16})
}
require.Equal(t, len(milestone.FutureMilestoneOrder), capicity, "expected length is", capicity)
require.Equal(t, milestone.FutureMilestoneOrder[capicity-1], uint64(16*capicity), "expected value is", uint64(16*capicity), "but got", milestone.FutureMilestoneOrder[capicity-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{}{}
}