package parlia import ( "crypto/rand" "fmt" mrand "math/rand" "testing" "golang.org/x/crypto/sha3" "github.com/ethereum/go-ethereum/common" cmath "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" ) const ( upperLimitOfVoteBlockNumber = 11 ) func TestImpactOfValidatorOutOfService(t *testing.T) { testCases := []struct { totalValidators int downValidators int turnLength int }{ {3, 1, 1}, {5, 2, 1}, {10, 1, 2}, {10, 4, 2}, {21, 1, 3}, {21, 3, 3}, {21, 5, 4}, {21, 10, 5}, } for _, tc := range testCases { simulateValidatorOutOfService(tc.totalValidators, tc.downValidators, tc.turnLength) } } // refer Snapshot.SignRecently func signRecently(idx int, recents map[uint64]int, turnLength int) bool { recentSignTimes := 0 for _, signIdx := range recents { if signIdx == idx { recentSignTimes += 1 } } return recentSignTimes >= turnLength } // refer Snapshot.minerHistoryCheckLen func minerHistoryCheckLen(totalValidators int, turnLength int) uint64 { return uint64(totalValidators/2+1)*uint64(turnLength) - 1 } // refer Snapshot.inturnValidator func inturnValidator(totalValidators int, turnLength int, height int) int { return height / turnLength % totalValidators } func simulateValidatorOutOfService(totalValidators int, downValidators int, turnLength int) { downBlocks := 10000 recoverBlocks := 10000 recents := make(map[uint64]int) validators := make(map[int]bool, totalValidators) down := make([]int, totalValidators) for i := 0; i < totalValidators; i++ { validators[i] = true down[i] = i } mrand.Shuffle(totalValidators, func(i, j int) { down[i], down[j] = down[j], down[i] }) for i := 0; i < downValidators; i++ { delete(validators, down[i]) } isRecentSign := func(idx int) bool { return signRecently(idx, recents, turnLength) } isInService := func(idx int) bool { return validators[idx] } downDelay := uint64(0) for h := 1; h <= downBlocks; h++ { if limit := minerHistoryCheckLen(totalValidators, turnLength) + 1; uint64(h) >= limit { delete(recents, uint64(h)-limit) } proposer := inturnValidator(totalValidators, turnLength, h) if !isInService(proposer) || isRecentSign(proposer) { candidates := make(map[int]bool, totalValidators/2) for v := range validators { if !isRecentSign(v) { candidates[v] = true } } if len(candidates) == 0 { panic("can not test such case") } idx, delay := producerBlockDelay(candidates, h, totalValidators) downDelay = downDelay + delay recents[uint64(h)] = idx } else { recents[uint64(h)] = proposer } } fmt.Printf("average delay is %v when there is %d validators and %d is down \n", downDelay/uint64(downBlocks), totalValidators, downValidators) for i := 0; i < downValidators; i++ { validators[down[i]] = true } recoverDelay := uint64(0) lastseen := downBlocks for h := downBlocks + 1; h <= downBlocks+recoverBlocks; h++ { if limit := minerHistoryCheckLen(totalValidators, turnLength) + 1; uint64(h) >= limit { delete(recents, uint64(h)-limit) } proposer := inturnValidator(totalValidators, turnLength, h) if !isInService(proposer) || isRecentSign(proposer) { lastseen = h candidates := make(map[int]bool, totalValidators/2) for v := range validators { if !isRecentSign(v) { candidates[v] = true } } if len(candidates) == 0 { panic("can not test such case") } idx, delay := producerBlockDelay(candidates, h, totalValidators) recoverDelay = recoverDelay + delay recents[uint64(h)] = idx } else { recents[uint64(h)] = proposer } } fmt.Printf("total delay is %v after recover when there is %d validators down ever, last seen not proposer at height %d\n", recoverDelay, downValidators, lastseen) } func producerBlockDelay(candidates map[int]bool, height, numOfValidators int) (int, uint64) { s := mrand.NewSource(int64(height)) r := mrand.New(s) n := numOfValidators backOffSteps := make([]int, 0, n) for idx := 0; idx < n; idx++ { backOffSteps = append(backOffSteps, idx) } r.Shuffle(n, func(i, j int) { backOffSteps[i], backOffSteps[j] = backOffSteps[j], backOffSteps[i] }) minDelay := numOfValidators minCandidate := 0 for c := range candidates { if minDelay > backOffSteps[c] { minDelay = backOffSteps[c] minCandidate = c } } delay := initialBackOffTime + uint64(minDelay)*wiggleTime return minCandidate, delay } func randomAddress() common.Address { addrBytes := make([]byte, 20) rand.Read(addrBytes) return common.BytesToAddress(addrBytes) } // ========================================================================= // ======= Simulator P2P network to verify fast finality ============ // ========================================================================= type MockBlock struct { parent *MockBlock blockNumber uint64 blockHash common.Hash coinbase *MockValidator td uint64 // Total difficulty from genesis block to current block attestation uint64 // Vote attestation for parent block, zero means no attestation } var GenesisBlock = &MockBlock{ parent: nil, blockNumber: 0, blockHash: common.Hash{}, coinbase: nil, td: diffInTurn.Uint64(), attestation: 0, } func (b *MockBlock) Hash() (hash common.Hash) { hasher := sha3.NewLegacyKeccak256() rlp.Encode(hasher, []interface{}{ b.parent, b.blockNumber, b.coinbase, b.td, b.attestation, }) hasher.Sum(hash[:0]) return hash } func (b *MockBlock) IsConflicted(a *MockBlock) bool { if a.blockNumber > b.blockNumber { p := a.parent for ; p.blockNumber > b.blockNumber; p = p.parent { } return p.blockHash != b.blockHash } if a.blockNumber < b.blockNumber { p := b.parent for ; p.blockNumber > a.blockNumber; p = p.parent { } return p.blockHash != a.blockHash } return a.blockHash != b.blockHash } // GetJustifiedNumberAndHash returns number and hash of the highest justified block, // keep same func signature with consensus even if `error` will be nil definitely func (b *MockBlock) GetJustifiedNumberAndHash() (uint64, common.Hash, error) { justifiedBlock := GenesisBlock for curBlock := b; curBlock.blockNumber > 1; curBlock = curBlock.parent { // justified if curBlock.attestation != 0 { justifiedBlock = curBlock.parent break } } return justifiedBlock.blockNumber, justifiedBlock.blockHash, nil } func (b *MockBlock) GetJustifiedNumber() uint64 { justifiedBlockNumber, _, _ := b.GetJustifiedNumberAndHash() return justifiedBlockNumber } // GetFinalizedBlock returns highest finalized block, // include current block's attestation. func (b *MockBlock) GetFinalizedBlock() *MockBlock { if b.blockNumber < 3 { return GenesisBlock } if b.attestation != 0 && b.parent.attestation != 0 { return b.parent.parent } return b.parent.GetFinalizedBlock() } type MockValidator struct { index int validatorSet int // validators number head *MockBlock voteRecords map[uint64]*types.VoteData } func NewMockValidator(index int, validatorSet int) *MockValidator { v := &MockValidator{ index: index, validatorSet: validatorSet, head: GenesisBlock, voteRecords: make(map[uint64]*types.VoteData), } return v } func (v *MockValidator) SignRecently() bool { parent := v.head for i := 0; i < v.validatorSet*1/2; i++ { if parent.blockNumber == 0 { return false } if parent.coinbase == v { return true } parent = parent.parent } return false } func (v *MockValidator) Produce(attestation uint64) (*MockBlock, error) { if v.SignRecently() { return nil, fmt.Errorf("v %d sign recently", v.index) } block := &MockBlock{ parent: v.head, blockNumber: v.head.blockNumber + 1, coinbase: v, td: v.head.td + 1, attestation: attestation, } if (block.blockNumber-1)%uint64(v.validatorSet) == uint64(v.index) { block.td = v.head.td + 2 } block.blockHash = block.Hash() return block, nil } func (v *MockValidator) Vote(block *MockBlock) bool { // Rule 3: The block should be the latest block of canonical chain if block != v.head { return false } // Rule 1: No double vote if _, ok := v.voteRecords[block.blockNumber]; ok { return false } // Rule 2: No surround vote justifiedBlockNumber, justifiedBlockHash, _ := block.GetJustifiedNumberAndHash() for targetNumber := justifiedBlockNumber + 1; targetNumber < block.blockNumber; targetNumber++ { if vote, ok := v.voteRecords[targetNumber]; ok { if vote.SourceNumber > justifiedBlockNumber { return false } } } for targetNumber := block.blockNumber; targetNumber <= block.blockNumber+upperLimitOfVoteBlockNumber; targetNumber++ { if vote, ok := v.voteRecords[targetNumber]; ok { if vote.SourceNumber < justifiedBlockNumber { return false } } } v.voteRecords[block.blockNumber] = &types.VoteData{ SourceNumber: justifiedBlockNumber, SourceHash: justifiedBlockHash, TargetNumber: block.blockNumber, TargetHash: block.blockHash, } return true } func (v *MockValidator) InsertBlock(block *MockBlock) { // Reject block too old. if block.blockNumber+13 < v.head.blockNumber { return } // The higher justified block is the longest chain. if block.GetJustifiedNumber() < v.head.GetJustifiedNumber() { return } if block.GetJustifiedNumber() > v.head.GetJustifiedNumber() { v.head = block return } // The same finalized number, the larger difficulty is the longest chain. if block.td > v.head.td { v.head = block } } type BlockSimulator struct { blockNumber uint64 coinbaseIndex int voteMap uint64 insertMap uint64 } type ChainSimulator []*BlockSimulator func (s ChainSimulator) Valid() bool { var pre *BlockSimulator for index, bs := range s { if index == 0 { if bs.blockNumber != 1 { return false } } else { if bs.blockNumber != pre.blockNumber+1 { return false } } pre = bs } return true } type Coordinator struct { validators []*MockValidator attestations map[common.Hash]uint64 } func NewCoordinator(validatorsNumber int) *Coordinator { validators := make([]*MockValidator, validatorsNumber) for i := 0; i < validatorsNumber; i++ { validators[i] = NewMockValidator(i, validatorsNumber) } return &Coordinator{ validators: validators, attestations: make(map[common.Hash]uint64), } } // SimulateP2P simulate a P2P network func (c *Coordinator) SimulateP2P(cs ChainSimulator) error { for _, bs := range cs { parent := c.validators[bs.coinbaseIndex].head if bs.blockNumber != parent.blockNumber+1 { return fmt.Errorf("can't produce discontinuous block, head block: %d, expect produce: %d", parent.blockNumber, bs.blockNumber) } attestation := c.attestations[parent.blockHash] block, err := c.validators[bs.coinbaseIndex].Produce(attestation) if err != nil { return fmt.Errorf("produce block %v error %v", bs, err) } c.PropagateBlock(bs, block) err = c.AggregateVotes(bs, block) if err != nil { return err } } return nil } func (c *Coordinator) AggregateVotes(bs *BlockSimulator, block *MockBlock) error { var attestation uint64 count := 0 for index, voteMap := 0, bs.voteMap; voteMap > 0; index, voteMap = index+1, voteMap>>1 { if voteMap&0x1 == 0 { continue } if !c.validators[index].Vote(block) { return fmt.Errorf("validator(%d) couldn't vote for block %d produced by validator(%d)", index, block.blockNumber, block.coinbase.index) } attestation |= 1 << index count++ } if count >= cmath.CeilDiv(len(c.validators)*2, 3) { c.attestations[block.blockHash] = attestation } return nil } func (c *Coordinator) PropagateBlock(bs *BlockSimulator, block *MockBlock) { for index, insertMap := 0, bs.insertMap; insertMap > 0; index, insertMap = index+1, insertMap>>1 { if insertMap&0x1 == 0 { continue } c.validators[index].InsertBlock(block) } } func (c *Coordinator) CheckChain() bool { // All validators highest finalized block should not be conflicted finalizedBlocks := make([]*MockBlock, len(c.validators)) for index, val := range c.validators { finalizedBlocks[index] = val.head.GetFinalizedBlock() } for i := 0; i < len(finalizedBlocks)-1; i++ { for j := i + 1; j < len(finalizedBlocks); j++ { if finalizedBlocks[i].IsConflicted(finalizedBlocks[j]) { return false } } } return true } type TestSimulatorParam struct { validatorsNumber int cs ChainSimulator } var simulatorTestcases = []*TestSimulatorParam{ { // 3 validators, all active validatorsNumber: 3, cs: []*BlockSimulator{ {1, 0, 0x7, 0x7}, {2, 1, 0x7, 0x7}, {3, 2, 0x7, 0x7}, {4, 0, 0x7, 0x7}, {5, 1, 0x7, 0x7}, }, }, { // 5 validators, 4 active, 1 down validatorsNumber: 5, cs: []*BlockSimulator{ {1, 0, 0x1f, 0x1f}, {2, 1, 0x1f, 0x1f}, {3, 2, 0x1f, 0x1f}, {4, 3, 0x1f, 0x1f}, {5, 0, 0x1f, 0x1f}, {6, 1, 0x1f, 0x1f}, {7, 2, 0x1f, 0x1f}, }, }, { // 21 validators, all active validatorsNumber: 21, cs: []*BlockSimulator{ {1, 0, 0x1fffff, 0x1fffff}, {2, 1, 0x1fffff, 0x1fffff}, {3, 2, 0x1fffff, 0x1fffff}, {4, 3, 0x1fffff, 0x1fffff}, {5, 4, 0x1fffff, 0x1fffff}, {6, 5, 0x1fffff, 0x1fffff}, {7, 6, 0x1fffff, 0x1fffff}, {8, 7, 0x1fffff, 0x1fffff}, {9, 8, 0x1fffff, 0x1fffff}, {10, 9, 0x1fffff, 0x1fffff}, {11, 10, 0x1fffff, 0x1fffff}, {12, 11, 0x1fffff, 0x1fffff}, {13, 12, 0x1fffff, 0x1fffff}, {14, 13, 0x1fffff, 0x1fffff}, {15, 14, 0x1fffff, 0x1fffff}, {16, 0, 0x1fffff, 0x1fffff}, {17, 1, 0x1fffff, 0x1fffff}, {18, 2, 0x1fffff, 0x1fffff}, }, }, { // 21 validators, all active, the finalized fork can keep grow validatorsNumber: 21, cs: []*BlockSimulator{ {1, 1, 0x00fffe, 0x00fffe}, {2, 2, 0x00fffe, 0x00fffe}, {1, 0, 0x1f0001, 0x1fffff}, {2, 16, 0x1f0001, 0x1ffff1}, {3, 17, 0x1f0001, 0x1ffff1}, {4, 18, 0x1f0001, 0x1ffff1}, {5, 19, 0x1f0001, 0x1ffff1}, {3, 3, 0x00fffe, 0x00fffe}, // justify block 2 and finalize block 1 {6, 20, 0x1f0001, 0x1fffff}, {4, 4, 0x00fffe, 0x1fffff}, {5, 5, 0x00fffe, 0x1fffff}, {6, 6, 0x00fffe, 0x1fffff}, {7, 7, 0x1fffff, 0x1fffff}, {8, 8, 0x1fffff, 0x1fffff}, }, }, { // 21 validators, all active, the finalized fork can keep grow validatorsNumber: 21, cs: []*BlockSimulator{ {1, 14, 0x00fffe, 0x00fffe}, {2, 15, 0x00fffe, 0x00fffe}, // The block 3 will never produce {1, 0, 0x1f0001, 0x1fffff}, {2, 16, 0x1f0001, 0x1fffff}, {3, 1, 0x1f0001, 0x1fffff}, // based block produced by 15 {4, 2, 0x1f0001, 0x1fffff}, {5, 3, 0x1f0001, 0x1fffff}, {6, 4, 0x1f0001, 0x1fffff}, {7, 5, 0x1f0001, 0x1fffff}, {8, 6, 0x1f0001, 0x1fffff}, {9, 7, 0x1f0001, 0x1fffff}, {10, 8, 0x1f0001, 0x1fffff}, {11, 9, 0x1f0001, 0x1fffff}, {12, 10, 0x1f0001, 0x1fffff}, {13, 11, 0x1f0001, 0x1fffff}, {14, 12, 0x1f0001, 0x1fffff}, {15, 13, 0x1f0001, 0x1fffff}, {16, 14, 0x1f0001, 0x1fffff}, {17, 15, 0x1fffff, 0x1fffff}, // begin new round vote {18, 16, 0x1fffff, 0x1fffff}, // attestation for block 17 {19, 17, 0x1fffff, 0x1fffff}, // attestation for block 18 }, }, } func TestSimulateP2P(t *testing.T) { for index, testcase := range simulatorTestcases { c := NewCoordinator(testcase.validatorsNumber) err := c.SimulateP2P(testcase.cs) if err != nil { t.Fatalf("[Testcase %d] simulate P2P error: %v", index, err) } for _, val := range c.validators { t.Logf("[Testcase %d] validator(%d) head block: %d", index, val.index, val.head.blockNumber) t.Logf("[Testcase %d] validator(%d) highest justified block: %d", index, val.index, val.head.GetJustifiedNumber()) t.Logf("[Testcase %d] validator(%d) highest finalized block: %d", index, val.index, val.head.GetFinalizedBlock().blockNumber) } if c.CheckChain() == false { t.Fatalf("[Testcase %d] chain not works as expected", index) } } }