// 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{}{} }