github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/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/onflow/crypto/random" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 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.WithInitialWeight(8)) 27 rng := getPRG(t, someSeed) 28 selection, err := ComputeLeaderSelection(0, rng, 10, flow.IdentitySkeletonList{&identity.IdentitySkeleton}) 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).ToSkeleton() 130 for i, identity := range identities { 131 identity.InitialWeight = 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).ToSkeleton()) 162 assert.Error(t, err) 163 count = -1 164 _, err = ComputeLeaderSelection(0, rng, count, unittest.IdentityListFixture(4).ToSkeleton()) 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).ToSkeleton() 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).ToSkeleton() 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.InitialWeight = 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.ToSkeleton()) 240 require.NoError(t, err) 241 242 leaders2, err := ComputeLeaderSelection(0, rng2, N_VIEWS, identities.ToSkeleton()) 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).ToSkeleton() 271 for i, identity := range identities { 272 identity.InitialWeight = 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.InitialWeight / 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.IdentityList, 0, N_NODES) 311 for i := 0; i < N_NODES; i++ { 312 identities = append(identities, unittest.IdentityFixture(unittest.WithInitialWeight(uint64(i)))) 313 } 314 skeletonIdentities := identities.ToSkeleton() 315 rng := getPRG(b, someSeed) 316 317 for n := 0; n < b.N; n++ { 318 _, err := ComputeLeaderSelection(0, rng, N_VIEWS, skeletonIdentities) 319 320 require.NoError(b, err) 321 } 322 } 323 324 func TestInvalidTotalWeight(t *testing.T) { 325 rng := getPRG(t, someSeed) 326 identities := unittest.IdentityListFixture(4, unittest.WithInitialWeight(0)) 327 _, err := ComputeLeaderSelection(0, rng, 10, identities.ToSkeleton()) 328 require.Error(t, err) 329 } 330 331 func TestZeroWeightNodeWillNotBeSelected(t *testing.T) { 332 333 // create 2 RNGs from the same seed 334 rng := getPRG(t, someSeed) 335 rng_copy := getPRG(t, someSeed) 336 337 // check that if there is some node with 0 weight, the selections for each view should be the same as 338 // with no zero-weight nodes. 339 t.Run("small dataset", func(t *testing.T) { 340 const N_VIEWS = 100 341 342 weightless := unittest.IdentityListFixture(5, unittest.WithInitialWeight(0)).ToSkeleton() 343 weightful := unittest.IdentityListFixture(5).ToSkeleton() 344 for i, identity := range weightful { 345 identity.InitialWeight = uint64(i + 1) 346 } 347 348 identities := append(weightless, weightful...) 349 350 selectionFromAll, err := ComputeLeaderSelection(0, rng, N_VIEWS, identities) 351 require.NoError(t, err) 352 353 selectionFromWeightful, err := ComputeLeaderSelection(0, rng_copy, N_VIEWS, weightful) 354 require.NoError(t, err) 355 356 for i := 0; i < N_VIEWS; i++ { 357 nodeIDFromAll, err := selectionFromAll.LeaderForView(uint64(i)) 358 require.NoError(t, err) 359 360 nodeIDFromWeightful, err := selectionFromWeightful.LeaderForView(uint64(i)) 361 require.NoError(t, err) 362 363 // the selection should be the same 364 require.Equal(t, nodeIDFromAll, nodeIDFromWeightful) 365 } 366 }) 367 368 t.Run("fuzzy set", func(t *testing.T) { 369 toolRng := getPRG(t, someSeed) 370 371 // create 1002 nodes with all 0 weight 372 fullIdentities := unittest.IdentityListFixture(1002, unittest.WithInitialWeight(0)) 373 374 // create 2 nodes with 1 weight, and place them in between 375 // index 233-777 376 n := toolRng.UintN(777-233) + 233 377 m := toolRng.UintN(777-233) + 233 378 fullIdentities[n].InitialWeight = 1 379 fullIdentities[m].InitialWeight = 1 380 381 // the following code checks that zero-weight nodes are not selected (selection probability is proportional to weight) 382 votingConsensusNodes := fullIdentities.Filter(filter.HasInitialWeight[flow.Identity](true)).ToSkeleton() 383 allEpochConsensusNodes := fullIdentities.ToSkeleton() // including zero-weight nodes 384 385 count := 1000 386 selectionFromAll, err := ComputeLeaderSelection(0, rng, count, allEpochConsensusNodes) 387 require.NoError(t, err) 388 389 selectionFromWeightful, err := ComputeLeaderSelection(0, rng_copy, count, votingConsensusNodes) 390 require.NoError(t, err) 391 392 for i := 0; i < count; i++ { 393 nodeIDFromAll, err := selectionFromAll.LeaderForView(uint64(i)) 394 require.NoError(t, err) 395 396 nodeIDFromWeightful, err := selectionFromWeightful.LeaderForView(uint64(i)) 397 require.NoError(t, err) 398 399 // the selection should be the same 400 require.Equal(t, nodeIDFromWeightful, nodeIDFromAll) 401 } 402 403 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) { 404 toolRng := getPRG(t, someSeed) 405 406 identities := unittest.IdentityListFixture(1000, unittest.WithInitialWeight(0)).ToSkeleton() 407 408 n := rng.UintN(1000) 409 weight := n + 1 410 identities[n].InitialWeight = weight 411 onlyNodeWithWeight := identities[n] 412 413 selections, err := ComputeLeaderSelection(0, toolRng, 1000, identities) 414 require.NoError(t, err) 415 416 for i := 0; i < 1000; i++ { 417 nodeID, err := selections.LeaderForView(uint64(i)) 418 require.NoError(t, err) 419 require.Equal(t, onlyNodeWithWeight.NodeID, nodeID) 420 } 421 }) 422 }) 423 }