github.com/MetalBlockchain/metalgo@v1.11.9/network/p2p/gossip/gossip_test.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package gossip 5 6 import ( 7 "context" 8 "sync" 9 "testing" 10 "time" 11 12 "github.com/prometheus/client_golang/prometheus" 13 "github.com/stretchr/testify/require" 14 "golang.org/x/exp/maps" 15 "google.golang.org/protobuf/proto" 16 17 "github.com/MetalBlockchain/metalgo/ids" 18 "github.com/MetalBlockchain/metalgo/network/p2p" 19 "github.com/MetalBlockchain/metalgo/proto/pb/sdk" 20 "github.com/MetalBlockchain/metalgo/snow/engine/common" 21 "github.com/MetalBlockchain/metalgo/snow/validators" 22 "github.com/MetalBlockchain/metalgo/utils/constants" 23 "github.com/MetalBlockchain/metalgo/utils/logging" 24 "github.com/MetalBlockchain/metalgo/utils/set" 25 "github.com/MetalBlockchain/metalgo/utils/units" 26 ) 27 28 func TestGossiperShutdown(*testing.T) { 29 gossiper := NewPullGossiper[*testTx]( 30 logging.NoLog{}, 31 nil, 32 nil, 33 nil, 34 Metrics{}, 35 0, 36 ) 37 ctx, cancel := context.WithCancel(context.Background()) 38 39 wg := &sync.WaitGroup{} 40 wg.Add(1) 41 42 go func() { 43 Every(ctx, logging.NoLog{}, gossiper, time.Second) 44 wg.Done() 45 }() 46 47 cancel() 48 wg.Wait() 49 } 50 51 func TestGossiperGossip(t *testing.T) { 52 tests := []struct { 53 name string 54 targetResponseSize int 55 requester []*testTx // what we have 56 responder []*testTx // what the peer we're requesting gossip from has 57 expectedPossibleValues []*testTx // possible values we can have 58 expectedLen int 59 }{ 60 { 61 name: "no gossip - no one knows anything", 62 }, 63 { 64 name: "no gossip - requester knows more than responder", 65 targetResponseSize: 1024, 66 requester: []*testTx{{id: ids.ID{0}}}, 67 expectedPossibleValues: []*testTx{{id: ids.ID{0}}}, 68 expectedLen: 1, 69 }, 70 { 71 name: "no gossip - requester knows everything responder knows", 72 targetResponseSize: 1024, 73 requester: []*testTx{{id: ids.ID{0}}}, 74 responder: []*testTx{{id: ids.ID{0}}}, 75 expectedPossibleValues: []*testTx{{id: ids.ID{0}}}, 76 expectedLen: 1, 77 }, 78 { 79 name: "gossip - requester knows nothing", 80 targetResponseSize: 1024, 81 responder: []*testTx{{id: ids.ID{0}}}, 82 expectedPossibleValues: []*testTx{{id: ids.ID{0}}}, 83 expectedLen: 1, 84 }, 85 { 86 name: "gossip - requester knows less than responder", 87 targetResponseSize: 1024, 88 requester: []*testTx{{id: ids.ID{0}}}, 89 responder: []*testTx{{id: ids.ID{0}}, {id: ids.ID{1}}}, 90 expectedPossibleValues: []*testTx{{id: ids.ID{0}}, {id: ids.ID{1}}}, 91 expectedLen: 2, 92 }, 93 { 94 name: "gossip - target response size exceeded", 95 targetResponseSize: 32, 96 responder: []*testTx{{id: ids.ID{0}}, {id: ids.ID{1}}, {id: ids.ID{2}}}, 97 expectedPossibleValues: []*testTx{{id: ids.ID{0}}, {id: ids.ID{1}}, {id: ids.ID{2}}}, 98 expectedLen: 2, 99 }, 100 } 101 102 for _, tt := range tests { 103 t.Run(tt.name, func(t *testing.T) { 104 require := require.New(t) 105 ctx := context.Background() 106 107 responseSender := &common.FakeSender{ 108 SentAppResponse: make(chan []byte, 1), 109 } 110 responseNetwork, err := p2p.NewNetwork(logging.NoLog{}, responseSender, prometheus.NewRegistry(), "") 111 require.NoError(err) 112 113 responseBloom, err := NewBloomFilter(prometheus.NewRegistry(), "", 1000, 0.01, 0.05) 114 require.NoError(err) 115 responseSet := &testSet{ 116 txs: make(map[ids.ID]*testTx), 117 bloom: responseBloom, 118 } 119 for _, item := range tt.responder { 120 require.NoError(responseSet.Add(item)) 121 } 122 123 metrics, err := NewMetrics(prometheus.NewRegistry(), "") 124 require.NoError(err) 125 marshaller := testMarshaller{} 126 handler := NewHandler[*testTx]( 127 logging.NoLog{}, 128 marshaller, 129 responseSet, 130 metrics, 131 tt.targetResponseSize, 132 ) 133 require.NoError(err) 134 require.NoError(responseNetwork.AddHandler(0x0, handler)) 135 136 requestSender := &common.FakeSender{ 137 SentAppRequest: make(chan []byte, 1), 138 } 139 140 requestNetwork, err := p2p.NewNetwork(logging.NoLog{}, requestSender, prometheus.NewRegistry(), "") 141 require.NoError(err) 142 require.NoError(requestNetwork.Connected(context.Background(), ids.EmptyNodeID, nil)) 143 144 bloom, err := NewBloomFilter(prometheus.NewRegistry(), "", 1000, 0.01, 0.05) 145 require.NoError(err) 146 requestSet := &testSet{ 147 txs: make(map[ids.ID]*testTx), 148 bloom: bloom, 149 } 150 for _, item := range tt.requester { 151 require.NoError(requestSet.Add(item)) 152 } 153 154 requestClient := requestNetwork.NewClient(0x0) 155 156 require.NoError(err) 157 gossiper := NewPullGossiper[*testTx]( 158 logging.NoLog{}, 159 marshaller, 160 requestSet, 161 requestClient, 162 metrics, 163 1, 164 ) 165 require.NoError(err) 166 received := set.Set[*testTx]{} 167 requestSet.onAdd = func(tx *testTx) { 168 received.Add(tx) 169 } 170 171 require.NoError(gossiper.Gossip(ctx)) 172 require.NoError(responseNetwork.AppRequest(ctx, ids.EmptyNodeID, 1, time.Time{}, <-requestSender.SentAppRequest)) 173 require.NoError(requestNetwork.AppResponse(ctx, ids.EmptyNodeID, 1, <-responseSender.SentAppResponse)) 174 175 require.Len(requestSet.txs, tt.expectedLen) 176 require.Subset(tt.expectedPossibleValues, maps.Values(requestSet.txs)) 177 178 // we should not receive anything that we already had before we 179 // requested the gossip 180 for _, tx := range tt.requester { 181 require.NotContains(received, tx) 182 } 183 }) 184 } 185 } 186 187 func TestEvery(*testing.T) { 188 ctx, cancel := context.WithCancel(context.Background()) 189 calls := 0 190 gossiper := &TestGossiper{ 191 GossipF: func(context.Context) error { 192 if calls >= 10 { 193 cancel() 194 return nil 195 } 196 197 calls++ 198 return nil 199 }, 200 } 201 202 go Every(ctx, logging.NoLog{}, gossiper, time.Millisecond) 203 <-ctx.Done() 204 } 205 206 func TestValidatorGossiper(t *testing.T) { 207 require := require.New(t) 208 209 nodeID := ids.GenerateTestNodeID() 210 211 validators := testValidatorSet{ 212 validators: set.Of(nodeID), 213 } 214 215 calls := 0 216 gossiper := ValidatorGossiper{ 217 Gossiper: &TestGossiper{ 218 GossipF: func(context.Context) error { 219 calls++ 220 return nil 221 }, 222 }, 223 NodeID: nodeID, 224 Validators: validators, 225 } 226 227 // we are a validator, so we should request gossip 228 require.NoError(gossiper.Gossip(context.Background())) 229 require.Equal(1, calls) 230 231 // we are not a validator, so we should not request gossip 232 validators.validators = set.Set[ids.NodeID]{} 233 require.NoError(gossiper.Gossip(context.Background())) 234 require.Equal(2, calls) 235 } 236 237 func TestPushGossiperNew(t *testing.T) { 238 tests := []struct { 239 name string 240 gossipParams BranchingFactor 241 regossipParams BranchingFactor 242 discardedSize int 243 targetGossipSize int 244 maxRegossipFrequency time.Duration 245 expected error 246 }{ 247 { 248 name: "invalid gossip num validators", 249 gossipParams: BranchingFactor{ 250 Validators: -1, 251 }, 252 regossipParams: BranchingFactor{ 253 Peers: 1, 254 }, 255 expected: ErrInvalidNumValidators, 256 }, 257 { 258 name: "invalid gossip num non-validators", 259 gossipParams: BranchingFactor{ 260 NonValidators: -1, 261 }, 262 regossipParams: BranchingFactor{ 263 Peers: 1, 264 }, 265 expected: ErrInvalidNumNonValidators, 266 }, 267 { 268 name: "invalid gossip num peers", 269 gossipParams: BranchingFactor{ 270 Peers: -1, 271 }, 272 regossipParams: BranchingFactor{ 273 Peers: 1, 274 }, 275 expected: ErrInvalidNumPeers, 276 }, 277 { 278 name: "invalid gossip num to gossip", 279 gossipParams: BranchingFactor{}, 280 regossipParams: BranchingFactor{ 281 Peers: 1, 282 }, 283 expected: ErrInvalidNumToGossip, 284 }, 285 { 286 name: "invalid regossip num validators", 287 gossipParams: BranchingFactor{ 288 Validators: 1, 289 }, 290 regossipParams: BranchingFactor{ 291 Validators: -1, 292 }, 293 expected: ErrInvalidNumValidators, 294 }, 295 { 296 name: "invalid regossip num non-validators", 297 gossipParams: BranchingFactor{ 298 Validators: 1, 299 }, 300 regossipParams: BranchingFactor{ 301 NonValidators: -1, 302 }, 303 expected: ErrInvalidNumNonValidators, 304 }, 305 { 306 name: "invalid regossip num peers", 307 gossipParams: BranchingFactor{ 308 Validators: 1, 309 }, 310 regossipParams: BranchingFactor{ 311 Peers: -1, 312 }, 313 expected: ErrInvalidNumPeers, 314 }, 315 { 316 name: "invalid regossip num to gossip", 317 gossipParams: BranchingFactor{ 318 Validators: 1, 319 }, 320 regossipParams: BranchingFactor{}, 321 expected: ErrInvalidNumToGossip, 322 }, 323 { 324 name: "invalid discarded size", 325 gossipParams: BranchingFactor{ 326 Validators: 1, 327 }, 328 regossipParams: BranchingFactor{ 329 Validators: 1, 330 }, 331 discardedSize: -1, 332 expected: ErrInvalidDiscardedSize, 333 }, 334 { 335 name: "invalid target gossip size", 336 gossipParams: BranchingFactor{ 337 Validators: 1, 338 }, 339 regossipParams: BranchingFactor{ 340 Validators: 1, 341 }, 342 targetGossipSize: -1, 343 expected: ErrInvalidTargetGossipSize, 344 }, 345 { 346 name: "invalid max re-gossip frequency", 347 gossipParams: BranchingFactor{ 348 Validators: 1, 349 }, 350 regossipParams: BranchingFactor{ 351 Validators: 1, 352 }, 353 maxRegossipFrequency: -1, 354 expected: ErrInvalidRegossipFrequency, 355 }, 356 } 357 358 for _, tt := range tests { 359 t.Run(tt.name, func(t *testing.T) { 360 _, err := NewPushGossiper[*testTx]( 361 nil, 362 nil, 363 nil, 364 nil, 365 Metrics{}, 366 tt.gossipParams, 367 tt.regossipParams, 368 tt.discardedSize, 369 tt.targetGossipSize, 370 tt.maxRegossipFrequency, 371 ) 372 require.ErrorIs(t, err, tt.expected) 373 }) 374 } 375 } 376 377 // Tests that the outgoing gossip is equivalent to what was accumulated 378 func TestPushGossiper(t *testing.T) { 379 type cycle struct { 380 toAdd []*testTx 381 expected [][]*testTx 382 } 383 tests := []struct { 384 name string 385 cycles []cycle 386 shouldRegossip bool 387 }{ 388 { 389 name: "single cycle with regossip", 390 cycles: []cycle{ 391 { 392 toAdd: []*testTx{ 393 { 394 id: ids.ID{0}, 395 }, 396 { 397 id: ids.ID{1}, 398 }, 399 { 400 id: ids.ID{2}, 401 }, 402 }, 403 expected: [][]*testTx{ 404 { 405 { 406 id: ids.ID{0}, 407 }, 408 { 409 id: ids.ID{1}, 410 }, 411 { 412 id: ids.ID{2}, 413 }, 414 }, 415 }, 416 }, 417 }, 418 shouldRegossip: true, 419 }, 420 { 421 name: "multiple cycles with regossip", 422 cycles: []cycle{ 423 { 424 toAdd: []*testTx{ 425 { 426 id: ids.ID{0}, 427 }, 428 }, 429 expected: [][]*testTx{ 430 { 431 { 432 id: ids.ID{0}, 433 }, 434 }, 435 }, 436 }, 437 { 438 toAdd: []*testTx{ 439 { 440 id: ids.ID{1}, 441 }, 442 }, 443 expected: [][]*testTx{ 444 { 445 { 446 id: ids.ID{1}, 447 }, 448 }, 449 { 450 { 451 id: ids.ID{0}, 452 }, 453 }, 454 }, 455 }, 456 { 457 toAdd: []*testTx{ 458 { 459 id: ids.ID{2}, 460 }, 461 }, 462 expected: [][]*testTx{ 463 { 464 { 465 id: ids.ID{2}, 466 }, 467 }, 468 { 469 { 470 id: ids.ID{1}, 471 }, 472 { 473 id: ids.ID{0}, 474 }, 475 }, 476 }, 477 }, 478 }, 479 shouldRegossip: true, 480 }, 481 { 482 name: "verify that we don't gossip empty messages", 483 cycles: []cycle{ 484 { 485 toAdd: []*testTx{ 486 { 487 id: ids.ID{0}, 488 }, 489 }, 490 expected: [][]*testTx{ 491 { 492 { 493 id: ids.ID{0}, 494 }, 495 }, 496 }, 497 }, 498 { 499 toAdd: []*testTx{}, 500 expected: [][]*testTx{}, 501 }, 502 }, 503 shouldRegossip: false, 504 }, 505 } 506 507 for _, tt := range tests { 508 t.Run(tt.name, func(t *testing.T) { 509 require := require.New(t) 510 ctx := context.Background() 511 512 sender := &common.FakeSender{ 513 SentAppGossip: make(chan []byte, 2), 514 } 515 network, err := p2p.NewNetwork( 516 logging.NoLog{}, 517 sender, 518 prometheus.NewRegistry(), 519 "", 520 ) 521 require.NoError(err) 522 client := network.NewClient(0) 523 validators := p2p.NewValidators( 524 &p2p.Peers{}, 525 logging.NoLog{}, 526 constants.PrimaryNetworkID, 527 &validators.TestState{ 528 GetCurrentHeightF: func(context.Context) (uint64, error) { 529 return 1, nil 530 }, 531 GetValidatorSetF: func(context.Context, uint64, ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { 532 return nil, nil 533 }, 534 }, 535 time.Hour, 536 ) 537 metrics, err := NewMetrics(prometheus.NewRegistry(), "") 538 require.NoError(err) 539 marshaller := testMarshaller{} 540 541 regossipTime := time.Hour 542 if tt.shouldRegossip { 543 regossipTime = time.Nanosecond 544 } 545 546 gossiper, err := NewPushGossiper[*testTx]( 547 marshaller, 548 FullSet[*testTx]{}, 549 validators, 550 client, 551 metrics, 552 BranchingFactor{ 553 Validators: 1, 554 }, 555 BranchingFactor{ 556 Validators: 1, 557 }, 558 0, // the discarded cache size doesn't matter for this test 559 units.MiB, 560 regossipTime, 561 ) 562 require.NoError(err) 563 564 for _, cycle := range tt.cycles { 565 gossiper.Add(cycle.toAdd...) 566 require.NoError(gossiper.Gossip(ctx)) 567 568 for _, expected := range cycle.expected { 569 want := &sdk.PushGossip{ 570 Gossip: make([][]byte, 0, len(expected)), 571 } 572 573 for _, gossipable := range expected { 574 bytes, err := marshaller.MarshalGossip(gossipable) 575 require.NoError(err) 576 577 want.Gossip = append(want.Gossip, bytes) 578 } 579 580 if len(want.Gossip) > 0 { 581 // remove the handler prefix 582 sentMsg := <-sender.SentAppGossip 583 got := &sdk.PushGossip{} 584 require.NoError(proto.Unmarshal(sentMsg[1:], got)) 585 586 require.Equal(want.Gossip, got.Gossip) 587 } else { 588 select { 589 case <-sender.SentAppGossip: 590 require.FailNow("unexpectedly sent gossip message") 591 default: 592 } 593 } 594 } 595 596 if tt.shouldRegossip { 597 // Ensure that subsequent calls to `time.Now()` are 598 // sufficient for regossip. 599 time.Sleep(regossipTime + time.Nanosecond) 600 } 601 } 602 }) 603 } 604 } 605 606 type testValidatorSet struct { 607 validators set.Set[ids.NodeID] 608 } 609 610 func (t testValidatorSet) Has(_ context.Context, nodeID ids.NodeID) bool { 611 return t.validators.Contains(nodeID) 612 }