github.com/onflow/flow-go@v0.33.17/consensus/hotstuff/committees/leader/leader_selection_test.go (about) 1 package leader 2 3 import ( 4 "fmt" 5 "math/rand" 6 "sort" 7 "testing" 8 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 12 "github.com/onflow/flow-go/crypto/random" 13 "github.com/onflow/flow-go/model/flow" 14 "github.com/onflow/flow-go/model/flow/filter" 15 "github.com/onflow/flow-go/utils/unittest" 16 ) 17 18 var someSeed = []uint8{0x6A, 0x23, 0x41, 0xB7, 0x80, 0xE1, 0x64, 0x59, 19 0x6A, 0x53, 0x40, 0xB7, 0x80, 0xE4, 0x64, 0x5C, 20 0x66, 0x53, 0x41, 0xB7, 0x80, 0xE1, 0x64, 0x51, 21 0xAA, 0x53, 0x40, 0xB7, 0x80, 0xE4, 0x64, 0x50} 22 23 // We test that leader selection works for a committee of size one 24 func TestSingleConsensusNode(t *testing.T) { 25 26 identity := unittest.IdentityFixture(unittest.WithWeight(8)) 27 rng := getPRG(t, someSeed) 28 selection, err := ComputeLeaderSelection(0, rng, 10, []*flow.Identity{identity}) 29 require.NoError(t, err) 30 for i := uint64(0); i < 10; i++ { 31 leaderID, err := selection.LeaderForView(i) 32 require.NoError(t, err) 33 require.Equal(t, identity.NodeID, leaderID) 34 } 35 } 36 37 // compare this binary search method with sort.Search() 38 func TestBsearchVSsortSearch(t *testing.T) { 39 weights := []uint64{1, 2, 3, 4, 5, 6, 7, 9, 12, 21, 32} 40 weights2 := []int{1, 2, 3, 4, 5, 6, 7, 9, 12, 21, 32} 41 var sum uint64 42 var sum2 int 43 sums := make([]uint64, 0) 44 sums2 := make([]int, 0) 45 for i := 0; i < len(weights); i++ { 46 sum += weights[i] 47 sum2 += weights2[i] 48 sums = append(sums, sum) 49 sums2 = append(sums2, sum2) 50 } 51 sel := make([]int, 0, 10) 52 for i := 0; i < 10; i++ { 53 index := binarySearchStrictlyBigger(uint64(i), sums) 54 sel = append(sel, index) 55 } 56 57 sel2 := make([]int, 0, 10) 58 for i2 := 1; i2 < 11; i2++ { 59 index := sort.SearchInts(sums2, i2) 60 sel2 = append(sel2, index) 61 } 62 63 require.Equal(t, sel, sel2) 64 } 65 66 // Test binary search implementation 67 func TestBsearch(t *testing.T) { 68 weights := []uint64{1, 2, 3, 4} 69 var sum uint64 70 sums := make([]uint64, 0) 71 for i := 0; i < len(weights); i++ { 72 sum += weights[i] 73 sums = append(sums, sum) 74 } 75 sel := make([]int, 0, 10) 76 for i := 0; i < 10; i++ { 77 index := binarySearchStrictlyBigger(uint64(i), sums) 78 sel = append(sel, index) 79 } 80 require.Equal(t, []int{0, 1, 1, 2, 2, 2, 3, 3, 3, 3}, sel) 81 } 82 83 // compare the result of binary search with the brute force search, 84 // should be the same. 85 func TestBsearchWithNormalSearch(t *testing.T) { 86 count := 100 87 sums := make([]uint64, 0, count) 88 sum := 0 89 for i := 0; i < count; i++ { 90 sum += i 91 sums = append(sums, uint64(sum)) 92 } 93 94 var value uint64 95 total := sums[len(sums)-1] 96 for value = 0; value < total; value++ { 97 expected, err := bruteSearch(value, sums) 98 require.NoError(t, err) 99 100 actual := binarySearchStrictlyBigger(value, sums) 101 require.NoError(t, err) 102 103 require.Equal(t, expected, actual) 104 } 105 } 106 107 func bruteSearch(value uint64, arr []uint64) (int, error) { 108 // value ranges from [arr[0], arr[len(arr) -1 ]) exclusive 109 for i, a := range arr { 110 if a > value { 111 return i, nil 112 } 113 } 114 return 0, fmt.Errorf("not found") 115 } 116 117 func getPRG(t testing.TB, seed []byte) random.Rand { 118 rng, err := random.NewChacha20PRG(seed, []byte("random")) 119 require.NoError(t, err) 120 return rng 121 } 122 123 // Test given the same seed, the leader selection will produce the same selection 124 func TestDeterministic(t *testing.T) { 125 126 const N_VIEWS = 100 127 const N_NODES = 4 128 129 identities := unittest.IdentityListFixture(N_NODES) 130 for i, identity := range identities { 131 identity.Weight = uint64(i + 1) 132 } 133 rng := getPRG(t, someSeed) 134 135 leaders1, err := ComputeLeaderSelection(0, rng, N_VIEWS, identities) 136 require.NoError(t, err) 137 138 rng = getPRG(t, someSeed) 139 140 leaders2, err := ComputeLeaderSelection(0, rng, N_VIEWS, identities) 141 require.NoError(t, err) 142 143 for i := 0; i < N_VIEWS; i++ { 144 l1, err := leaders1.LeaderForView(uint64(i)) 145 require.NoError(t, err) 146 147 l2, err := leaders2.LeaderForView(uint64(i)) 148 require.NoError(t, err) 149 150 require.Equal(t, l1, l2) 151 } 152 } 153 154 func TestInputValidation(t *testing.T) { 155 156 rng := getPRG(t, someSeed) 157 158 // should return an error if we request to compute leader selection for <1 views 159 t.Run("epoch containing no views", func(t *testing.T) { 160 count := 0 161 _, err := ComputeLeaderSelection(0, rng, count, unittest.IdentityListFixture(4)) 162 assert.Error(t, err) 163 count = -1 164 _, err = ComputeLeaderSelection(0, rng, count, unittest.IdentityListFixture(4)) 165 assert.Error(t, err) 166 }) 167 168 // epoch with no possible leaders should return an error 169 t.Run("epoch without participants", func(t *testing.T) { 170 identities := unittest.IdentityListFixture(0) 171 _, err := ComputeLeaderSelection(0, rng, 100, identities) 172 assert.Error(t, err) 173 }) 174 } 175 176 // test that requesting a view outside the given range returns an error 177 func TestViewOutOfRange(t *testing.T) { 178 179 rng := getPRG(t, someSeed) 180 181 firstView := uint64(100) 182 finalView := uint64(200) 183 184 identities := unittest.IdentityListFixture(4) 185 leaders, err := ComputeLeaderSelection(firstView, rng, int(finalView-firstView+1), identities) 186 require.Nil(t, err) 187 188 // confirm the selection has first/final view we expect 189 assert.Equal(t, firstView, leaders.FirstView()) 190 assert.Equal(t, finalView, leaders.FinalView()) 191 192 // boundary views should not return error 193 t.Run("boundary views", func(t *testing.T) { 194 _, err = leaders.LeaderForView(firstView) 195 assert.Nil(t, err) 196 _, err = leaders.LeaderForView(finalView) 197 assert.Nil(t, err) 198 }) 199 200 // views before first view should return error 201 t.Run("before first view", func(t *testing.T) { 202 before := firstView - 1 // 1 before first view 203 _, err = leaders.LeaderForView(before) 204 assert.Error(t, err) 205 206 before = uint64(rand.Intn(int(firstView))) // random view before first view 207 _, err = leaders.LeaderForView(before) 208 assert.Error(t, err) 209 }) 210 211 // views after final view should return error 212 t.Run("after final view", func(t *testing.T) { 213 after := finalView + 1 // 1 after final view 214 _, err = leaders.LeaderForView(after) 215 assert.Error(t, err) 216 217 after = finalView + uint64(rand.Uint32()) + 1 // random view after final view 218 _, err = leaders.LeaderForView(after) 219 assert.Error(t, err) 220 }) 221 } 222 223 func TestDifferentSeedWillProduceDifferentSelection(t *testing.T) { 224 225 const N_VIEWS = 100 226 const N_NODES = 4 227 228 identities := unittest.IdentityListFixture(N_NODES) 229 for i, identity := range identities { 230 identity.Weight = uint64(i) 231 } 232 233 rng1 := getPRG(t, someSeed) 234 235 seed2 := make([]byte, 32) 236 seed2[0] = 8 237 rng2 := getPRG(t, seed2) 238 239 leaders1, err := ComputeLeaderSelection(0, rng1, N_VIEWS, identities) 240 require.NoError(t, err) 241 242 leaders2, err := ComputeLeaderSelection(0, rng2, N_VIEWS, identities) 243 require.NoError(t, err) 244 245 diff := 0 246 for view := 0; view < N_VIEWS; view++ { 247 l1, err := leaders1.LeaderForView(uint64(view)) 248 require.NoError(t, err) 249 250 l2, err := leaders2.LeaderForView(uint64(view)) 251 require.NoError(t, err) 252 253 if l1 != l2 { 254 diff++ 255 } 256 } 257 258 require.True(t, diff > 0) 259 } 260 261 // given a random seed and certain weights, measure the chance each identity selected as leader. 262 // The number of time being selected as leader might not exactly match their weight, but also 263 // won't go too far from that. 264 func TestLeaderSelectionAreWeighted(t *testing.T) { 265 rng := getPRG(t, someSeed) 266 267 const N_VIEWS = 100000 268 const N_NODES = 4 269 270 identities := unittest.IdentityListFixture(N_NODES) 271 for i, identity := range identities { 272 identity.Weight = uint64(i + 1) 273 } 274 275 leaders, err := ComputeLeaderSelection(0, rng, N_VIEWS, identities) 276 require.NoError(t, err) 277 278 selected := make(map[flow.Identifier]uint64) 279 for view := 0; view < N_VIEWS; view++ { 280 nodeID, err := leaders.LeaderForView(uint64(view)) 281 require.NoError(t, err) 282 283 selected[nodeID]++ 284 } 285 286 fmt.Printf("selected for weights [1,2,3,4]: %v\n", selected) 287 for nodeID, selectedCount := range selected { 288 identity, ok := identities.ByNodeID(nodeID) 289 require.True(t, ok) 290 target := uint64(N_VIEWS) * identity.Weight / 10 291 292 var diff uint64 293 if selectedCount > target { 294 diff = selectedCount - target 295 } else { 296 diff = target - selectedCount 297 } 298 299 // difference should be less than 2% 300 stdDiff := N_VIEWS / 10 * 2 / 100 301 require.Less(t, diff, uint64(stdDiff)) 302 } 303 } 304 305 func BenchmarkLeaderSelection(b *testing.B) { 306 307 const N_VIEWS = 15000000 308 const N_NODES = 20 309 310 identities := make([]*flow.Identity, 0, N_NODES) 311 for i := 0; i < N_NODES; i++ { 312 identities = append(identities, unittest.IdentityFixture(unittest.WithWeight(uint64(i)))) 313 } 314 rng := getPRG(b, someSeed) 315 316 for n := 0; n < b.N; n++ { 317 _, err := ComputeLeaderSelection(0, rng, N_VIEWS, identities) 318 319 require.NoError(b, err) 320 } 321 } 322 323 func TestInvalidTotalWeight(t *testing.T) { 324 rng := getPRG(t, someSeed) 325 identities := unittest.IdentityListFixture(4, unittest.WithWeight(0)) 326 _, err := ComputeLeaderSelection(0, rng, 10, identities) 327 require.Error(t, err) 328 } 329 330 func TestZeroWeightNodeWillNotBeSelected(t *testing.T) { 331 332 // create 2 RNGs from the same seed 333 rng := getPRG(t, someSeed) 334 rng_copy := getPRG(t, someSeed) 335 336 // check that if there is some node with 0 weight, the selections for each view should be the same as 337 // with no zero-weight nodes. 338 t.Run("small dataset", func(t *testing.T) { 339 const N_VIEWS = 100 340 341 weightless := unittest.IdentityListFixture(5, unittest.WithWeight(0)) 342 weightful := unittest.IdentityListFixture(5) 343 for i, identity := range weightful { 344 identity.Weight = uint64(i + 1) 345 } 346 347 identities := append(weightless, weightful...) 348 349 selectionFromAll, err := ComputeLeaderSelection(0, rng, N_VIEWS, identities) 350 require.NoError(t, err) 351 352 selectionFromWeightful, err := ComputeLeaderSelection(0, rng_copy, N_VIEWS, weightful) 353 require.NoError(t, err) 354 355 for i := 0; i < N_VIEWS; i++ { 356 nodeIDFromAll, err := selectionFromAll.LeaderForView(uint64(i)) 357 require.NoError(t, err) 358 359 nodeIDFromWeightful, err := selectionFromWeightful.LeaderForView(uint64(i)) 360 require.NoError(t, err) 361 362 // the selection should be the same 363 require.Equal(t, nodeIDFromAll, nodeIDFromWeightful) 364 } 365 }) 366 367 t.Run("fuzzy set", func(t *testing.T) { 368 toolRng := getPRG(t, someSeed) 369 370 // create 1002 nodes with all 0 weight 371 identities := unittest.IdentityListFixture(1002, unittest.WithWeight(0)) 372 373 // create 2 nodes with 1 weight, and place them in between 374 // index 233-777 375 n := toolRng.UintN(777-233) + 233 376 m := toolRng.UintN(777-233) + 233 377 identities[n].Weight = 1 378 identities[m].Weight = 1 379 380 // the following code check the zero weight node should not be selected 381 weightful := identities.Filter(filter.HasWeight(true)) 382 383 count := 1000 384 selectionFromAll, err := ComputeLeaderSelection(0, rng, count, identities) 385 require.NoError(t, err) 386 387 selectionFromWeightful, err := ComputeLeaderSelection(0, rng_copy, count, weightful) 388 require.NoError(t, err) 389 390 for i := 0; i < count; i++ { 391 nodeIDFromAll, err := selectionFromAll.LeaderForView(uint64(i)) 392 require.NoError(t, err) 393 394 nodeIDFromWeightful, err := selectionFromWeightful.LeaderForView(uint64(i)) 395 require.NoError(t, err) 396 397 // the selection should be the same 398 require.Equal(t, nodeIDFromWeightful, nodeIDFromAll) 399 } 400 401 t.Run("if there is only 1 node has weight, then it will be always be the leader and the only leader", func(t *testing.T) { 402 toolRng := getPRG(t, someSeed) 403 404 identities := unittest.IdentityListFixture(1000, unittest.WithWeight(0)) 405 406 n := rng.UintN(1000) 407 weight := n + 1 408 identities[n].Weight = weight 409 onlyNodeWithWeight := identities[n] 410 411 selections, err := ComputeLeaderSelection(0, toolRng, 1000, identities) 412 require.NoError(t, err) 413 414 for i := 0; i < 1000; i++ { 415 nodeID, err := selections.LeaderForView(uint64(i)) 416 require.NoError(t, err) 417 require.Equal(t, onlyNodeWithWeight.NodeID, nodeID) 418 } 419 }) 420 }) 421 }