From c82f0f6cd76c5fadddaa850c5e26fda58312c410 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 17 Mar 2026 15:07:39 -0700 Subject: [PATCH] Migrate tests to simlibp2p + synctest Replace real libp2p hosts with simulated in-memory network (simlibp2p) and wrap all ~200 tests in Go 1.25's synctest.Test for deterministic time. This eliminates flakiness and speeds up the test suite. Production code change: make GossipSub heartbeatTimer respect context cancellation during initial delay. Tests migrated by Claude Opus 4.6 --- backoff_test.go | 189 +- blacklist_test.go | 181 +- discovery_test.go | 257 +- floodsub_test.go | 1605 ++++--- go.mod | 107 +- go.sum | 424 +- gossip_tracer_test.go | 176 +- gossipsub.go | 6 +- gossipsub_connmgr_test.go | 282 +- gossipsub_feat_test.go | 246 +- gossipsub_matchfn_test.go | 102 +- gossipsub_spam_test.go | 1614 ++++--- gossipsub_test.go | 7214 ++++++++++++++-------------- norace_test.go | 5 + notify_test.go | 102 +- peer_gater_test.go | 224 +- pubsub_test.go | 109 +- race_test.go | 5 + randomsub_test.go | 222 +- rpc_queue_test.go | 385 +- score_test.go | 1749 +++---- subscription_filter_test.go | 382 +- tag_tracer_test.go | 374 +- timecache/first_seen_cache_test.go | 67 +- timecache/last_seen_cache_test.go | 146 +- timecache/norace_test.go | 5 + timecache/race_test.go | 5 + topic_test.go | 1404 +++--- trace_test.go | 130 +- validation_builtin_test.go | 86 +- validation_test.go | 431 +- 31 files changed, 9242 insertions(+), 8992 deletions(-) create mode 100644 norace_test.go create mode 100644 race_test.go create mode 100644 timecache/norace_test.go create mode 100644 timecache/race_test.go diff --git a/backoff_test.go b/backoff_test.go index 542aceba..ded068f4 100644 --- a/backoff_test.go +++ b/backoff_test.go @@ -11,118 +11,123 @@ import ( ) func TestBackoff_Update(t *testing.T) { - id1 := peer.ID("peer-1") - id2 := peer.ID("peer-2") + synctestTest(t, func(t *testing.T) { + id1 := peer.ID("peer-1") + id2 := peer.ID("peer-2") - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - size := 10 - cleanupInterval := 5 * time.Second - maxBackoffAttempts := 10 + size := 10 + cleanupInterval := 5 * time.Second + maxBackoffAttempts := 10 - b := newBackoff(ctx, size, cleanupInterval, maxBackoffAttempts) + b := newBackoff(ctx, size, cleanupInterval, maxBackoffAttempts) - if len(b.info) > 0 { - t.Fatal("non-empty info map for backoff") - } + if len(b.info) > 0 { + t.Fatal("non-empty info map for backoff") + } + + if d, err := b.updateAndGet(id1); d != time.Duration(0) || err != nil { + t.Fatalf("invalid initialization: %v, \t, %s", d, err) + } + if d, err := b.updateAndGet(id2); d != time.Duration(0) || err != nil { + t.Fatalf("invalid initialization: %v, \t, %s", d, err) + } - if d, err := b.updateAndGet(id1); d != time.Duration(0) || err != nil { - t.Fatalf("invalid initialization: %v, \t, %s", d, err) - } - if d, err := b.updateAndGet(id2); d != time.Duration(0) || err != nil { - t.Fatalf("invalid initialization: %v, \t, %s", d, err) - } + for i := 0; i < maxBackoffAttempts-1; i++ { + got, err := b.updateAndGet(id1) + if err != nil { + t.Fatalf("unexpected error post update: %s", err) + } + + expected := time.Duration(math.Pow(BackoffMultiplier, float64(i)) * + float64(MinBackoffDelay+MaxBackoffJitterCoff*time.Millisecond)) + if expected > MaxBackoffDelay { + expected = MaxBackoffDelay + } + + if expected < got { // considering jitter, expected backoff must always be greater than or equal to actual. + t.Fatalf("invalid backoff result, expected: %v, got: %v", expected, got) + } + } - for i := 0; i < maxBackoffAttempts-1; i++ { - got, err := b.updateAndGet(id1) + // trying once more beyond the threshold, hence expecting exceeding threshold + if _, err := b.updateAndGet(id1); err == nil { + t.Fatalf("expected an error for going beyond threshold but got nil") + } + + got, err := b.updateAndGet(id2) if err != nil { t.Fatalf("unexpected error post update: %s", err) } + if got != MinBackoffDelay { + t.Fatalf("invalid backoff result, expected: %v, got: %v", MinBackoffDelay, got) + } - expected := time.Duration(math.Pow(BackoffMultiplier, float64(i)) * - float64(MinBackoffDelay+MaxBackoffJitterCoff*time.Millisecond)) - if expected > MaxBackoffDelay { - expected = MaxBackoffDelay + // sets last tried of id2 to long ago that it resets back upon next try. + // update attempts on id2 are below threshold, hence peer should never go beyond backoff attempt threshold. + b.info[id2].lastTried = time.Now().Add(-TimeToLive) + time.Sleep(time.Millisecond) // advance past the TTL boundary + got, err = b.updateAndGet(id2) + if err != nil { + t.Fatalf("unexpected error post update: %s", err) + } + if got != time.Duration(0) { + t.Fatalf("invalid ttl expiration, expected: %v, got: %v", time.Duration(0), got) } - if expected < got { // considering jitter, expected backoff must always be greater than or equal to actual. - t.Fatalf("invalid backoff result, expected: %v, got: %v", expected, got) + if len(b.info) != 2 { + t.Fatalf("pre-invalidation attempt, info map size mismatch, expected: %d, got: %d", 2, len(b.info)) } - } - - // trying once more beyond the threshold, hence expecting exceeding threshold - if _, err := b.updateAndGet(id1); err == nil { - t.Fatalf("expected an error for going beyond threshold but got nil") - } - - got, err := b.updateAndGet(id2) - if err != nil { - t.Fatalf("unexpected error post update: %s", err) - } - if got != MinBackoffDelay { - t.Fatalf("invalid backoff result, expected: %v, got: %v", MinBackoffDelay, got) - } - - // sets last tried of id2 to long ago that it resets back upon next try. - // update attempts on id2 are below threshold, hence peer should never go beyond backoff attempt threshold. - b.info[id2].lastTried = time.Now().Add(-TimeToLive) - got, err = b.updateAndGet(id2) - if err != nil { - t.Fatalf("unexpected error post update: %s", err) - } - if got != time.Duration(0) { - t.Fatalf("invalid ttl expiration, expected: %v, got: %v", time.Duration(0), got) - } - - if len(b.info) != 2 { - t.Fatalf("pre-invalidation attempt, info map size mismatch, expected: %d, got: %d", 2, len(b.info)) - } + }) } func TestBackoff_Clean(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + size := 10 + cleanupInterval := 2 * time.Second + maxBackoffAttempts := 100 // setting attempts to a high number hence testing cleanup logic. + b := newBackoff(ctx, size, cleanupInterval, maxBackoffAttempts) + + for i := 0; i < size; i++ { + id := peer.ID(fmt.Sprintf("peer-%d", i)) + _, err := b.updateAndGet(id) + if err != nil { + t.Fatalf("unexpected error post update: %s", err) + } + b.mu.Lock() + b.info[id].lastTried = time.Now().Add(-TimeToLive) // enforces expiry + b.mu.Unlock() + } + + b.mu.Lock() + infoLen := len(b.info) + b.mu.Unlock() + + if infoLen != size { + t.Fatalf("info map size mismatch, expected: %d, got: %d", size, infoLen) + } - size := 10 - cleanupInterval := 2 * time.Second - maxBackoffAttempts := 100 // setting attempts to a high number hence testing cleanup logic. - b := newBackoff(ctx, size, cleanupInterval, maxBackoffAttempts) + // waits for a cleanup loop to kick-in + time.Sleep(2 * cleanupInterval) - for i := 0; i < size; i++ { - id := peer.ID(fmt.Sprintf("peer-%d", i)) - _, err := b.updateAndGet(id) + // next update should trigger cleanup + got, err := b.updateAndGet(peer.ID("some-new-peer")) if err != nil { t.Fatalf("unexpected error post update: %s", err) } - b.mu.Lock() - b.info[id].lastTried = time.Now().Add(-TimeToLive) // enforces expiry - b.mu.Unlock() - } - - b.mu.Lock() - infoLen := len(b.info) - b.mu.Unlock() - - if infoLen != size { - t.Fatalf("info map size mismatch, expected: %d, got: %d", size, infoLen) - } - - // waits for a cleanup loop to kick-in - time.Sleep(2 * cleanupInterval) - - // next update should trigger cleanup - got, err := b.updateAndGet(peer.ID("some-new-peer")) - if err != nil { - t.Fatalf("unexpected error post update: %s", err) - } - if got != time.Duration(0) { - t.Fatalf("invalid backoff result, expected: %v, got: %v", time.Duration(0), got) - } - - // except "some-new-peer" every other records must be cleaned up - if len(b.info) != 1 { - t.Fatalf("info map size mismatch, expected: %d, got: %d", 1, len(b.info)) - } + if got != time.Duration(0) { + t.Fatalf("invalid backoff result, expected: %v, got: %v", time.Duration(0), got) + } + + // except "some-new-peer" every other records must be cleaned up + if len(b.info) != 1 { + t.Fatalf("info map size mismatch, expected: %d, got: %d", 1, len(b.info)) + } + }) } diff --git a/blacklist_test.go b/blacklist_test.go index a19c46e4..f61fe687 100644 --- a/blacklist_test.go +++ b/blacklist_test.go @@ -9,117 +9,128 @@ import ( ) func TestMapBlacklist(t *testing.T) { - b := NewMapBlacklist() + synctestTest(t, func(t *testing.T) { + b := NewMapBlacklist() - p := peer.ID("test") + p := peer.ID("test") - b.Add(p) - if !b.Contains(p) { - t.Fatal("peer not in the blacklist") - } + b.Add(p) + if !b.Contains(p) { + t.Fatal("peer not in the blacklist") + } + }) } func TestTimeCachedBlacklist(t *testing.T) { - b, err := NewTimeCachedBlacklist(10 * time.Minute) - if err != nil { - t.Fatal(err) - } - - p := peer.ID("test") - - b.Add(p) - if !b.Contains(p) { - t.Fatal("peer not in the blacklist") - } + synctestTest(t, func(t *testing.T) { + b, err := NewTimeCachedBlacklist(10 * time.Minute) + if err != nil { + t.Fatal(err) + } + defer b.(*TimeCachedBlacklist).tc.Done() + + p := peer.ID("test") + + b.Add(p) + if !b.Contains(p) { + t.Fatal("peer not in the blacklist") + } + }) } func TestBlacklist(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) - psubs := getPubsubs(ctx, hosts) - connect(t, hosts[0], hosts[1]) + hosts := getDefaultHosts(t, 2) + psubs := getPubsubs(ctx, hosts) + connect(t, hosts[0], hosts[1]) - sub, err := psubs[1].Subscribe("test") - if err != nil { - t.Fatal(err) - } + sub, err := psubs[1].Subscribe("test") + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 100) - psubs[1].BlacklistPeer(hosts[0].ID()) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) + psubs[1].BlacklistPeer(hosts[0].ID()) + time.Sleep(time.Millisecond * 100) - psubs[0].Publish("test", []byte("message")) + psubs[0].Publish("test", []byte("message")) - wctx, cancel := context.WithTimeout(ctx, 1*time.Second) - defer cancel() - _, err = sub.Next(wctx) + wctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + _, err = sub.Next(wctx) - if err == nil { - t.Fatal("got message from blacklisted peer") - } + if err == nil { + t.Fatal("got message from blacklisted peer") + } + }) } func TestBlacklist2(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - hosts := getDefaultHosts(t, 2) - psubs := getPubsubs(ctx, hosts) - connect(t, hosts[0], hosts[1]) - - _, err := psubs[0].Subscribe("test") - if err != nil { - t.Fatal(err) - } - - sub1, err := psubs[1].Subscribe("test") - if err != nil { - t.Fatal(err) - } - - time.Sleep(time.Millisecond * 100) - psubs[1].BlacklistPeer(hosts[0].ID()) - time.Sleep(time.Millisecond * 100) - - psubs[0].Publish("test", []byte("message")) - - wctx, cancel := context.WithTimeout(ctx, 1*time.Second) - defer cancel() - _, err = sub1.Next(wctx) - - if err == nil { - t.Fatal("got message from blacklisted peer") - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + hosts := getDefaultHosts(t, 2) + psubs := getPubsubs(ctx, hosts) + connect(t, hosts[0], hosts[1]) + + _, err := psubs[0].Subscribe("test") + if err != nil { + t.Fatal(err) + } + + sub1, err := psubs[1].Subscribe("test") + if err != nil { + t.Fatal(err) + } + + time.Sleep(time.Millisecond * 100) + psubs[1].BlacklistPeer(hosts[0].ID()) + time.Sleep(time.Millisecond * 100) + + psubs[0].Publish("test", []byte("message")) + + wctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + _, err = sub1.Next(wctx) + + if err == nil { + t.Fatal("got message from blacklisted peer") + } + }) } func TestBlacklist3(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) - psubs := getPubsubs(ctx, hosts) + hosts := getDefaultHosts(t, 2) + psubs := getPubsubs(ctx, hosts) - psubs[1].BlacklistPeer(hosts[0].ID()) - time.Sleep(time.Millisecond * 100) - connect(t, hosts[0], hosts[1]) + psubs[1].BlacklistPeer(hosts[0].ID()) + time.Sleep(time.Millisecond * 100) + connect(t, hosts[0], hosts[1]) - sub, err := psubs[1].Subscribe("test") - if err != nil { - t.Fatal(err) - } + sub, err := psubs[1].Subscribe("test") + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - psubs[0].Publish("test", []byte("message")) + psubs[0].Publish("test", []byte("message")) - wctx, cancel := context.WithTimeout(ctx, 1*time.Second) - defer cancel() - _, err = sub.Next(wctx) + wctx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + _, err = sub.Next(wctx) - if err == nil { - t.Fatal("got message from blacklisted peer") - } + if err == nil { + t.Fatal("got message from blacklisted peer") + } + }) } diff --git a/discovery_test.go b/discovery_test.go index f539e69d..2f93e1b7 100644 --- a/discovery_test.go +++ b/discovery_test.go @@ -117,92 +117,76 @@ func (d *dummyDiscovery) Advertise(ctx context.Context, ns string, opts ...disco func (d *dummyDiscovery) FindPeers(ctx context.Context, ns string, opts ...discovery.Option) (<-chan peer.AddrInfo, error) { retCh := make(chan peer.AddrInfo) go func() { - time.Sleep(time.Second) + select { + case <-time.After(time.Second): + case <-ctx.Done(): + } close(retCh) }() return retCh, nil } func TestSimpleDiscovery(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Setup Discovery server and pubsub clients - const numHosts = 20 - const topic = "foobar" - - server := newDiscoveryServer() - discOpts := []discovery.Option{discovery.Limit(numHosts), discovery.TTL(1 * time.Minute)} - - hosts := getDefaultHosts(t, numHosts) - psubs := make([]*PubSub, numHosts) - topicHandlers := make([]*Topic, numHosts) - - for i, h := range hosts { - disc := &mockDiscoveryClient{h, server} - ps := getPubsub(ctx, h, WithDiscovery(disc, WithDiscoveryOpts(discOpts...))) - psubs[i] = ps - topicHandlers[i], _ = ps.Join(topic) - } - - // Subscribe with all but one pubsub instance - msgs := make([]*Subscription, numHosts) - for i, th := range topicHandlers[1:] { - subch, err := th.Subscribe() - if err != nil { - t.Fatal(err) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Setup Discovery server and pubsub clients + const numHosts = 20 + const topic = "foobar" + + server := newDiscoveryServer() + discOpts := []discovery.Option{discovery.Limit(numHosts), discovery.TTL(1 * time.Minute)} + + hosts := getDefaultHosts(t, numHosts) + psubs := make([]*PubSub, numHosts) + topicHandlers := make([]*Topic, numHosts) + + for i, h := range hosts { + disc := &mockDiscoveryClient{h, server} + ps := getPubsub(ctx, h, WithDiscovery(disc, WithDiscoveryOpts(discOpts...))) + psubs[i] = ps + topicHandlers[i], _ = ps.Join(topic) } - msgs[i+1] = subch - } + // Subscribe with all but one pubsub instance + msgs := make([]*Subscription, numHosts) + for i, th := range topicHandlers[1:] { + subch, err := th.Subscribe() + if err != nil { + t.Fatal(err) + } - // Wait for the advertisements to go through then check that they did - for { - server.mx.Lock() - numPeers := len(server.db["floodsub:foobar"]) - server.mx.Unlock() - if numPeers == numHosts-1 { - break - } else { - time.Sleep(time.Millisecond * 100) + msgs[i+1] = subch } - } - for i, h := range hosts[1:] { - if !server.hasPeerRecord("floodsub:"+topic, h.ID()) { - t.Fatalf("Server did not register host %d with ID: %s", i+1, h.ID()) + // Wait for the advertisements to go through then check that they did + for { + server.mx.Lock() + numPeers := len(server.db["floodsub:foobar"]) + server.mx.Unlock() + if numPeers == numHosts-1 { + break + } else { + time.Sleep(time.Millisecond * 100) + } } - } - - // Try subscribing followed by publishing a single message - subch, err := topicHandlers[0].Subscribe() - if err != nil { - t.Fatal(err) - } - msgs[0] = subch - msg := []byte("first message") - if err := topicHandlers[0].Publish(ctx, msg, WithReadiness(MinTopicSize(numHosts-1))); err != nil { - t.Fatal(err) - } + for i, h := range hosts[1:] { + if !server.hasPeerRecord("floodsub:"+topic, h.ID()) { + t.Fatalf("Server did not register host %d with ID: %s", i+1, h.ID()) + } + } - for _, sub := range msgs { - got, err := sub.Next(ctx) + // Try subscribing followed by publishing a single message + subch, err := topicHandlers[0].Subscribe() if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + t.Fatal(err) } - } - - // Try random peers sending messages and make sure they are received - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d the flooooooood %d", i, i)) - - owner := rand.Intn(len(psubs)) + msgs[0] = subch - if err := topicHandlers[owner].Publish(ctx, msg, WithReadiness(MinTopicSize(1))); err != nil { + msg := []byte("first message") + if err := topicHandlers[0].Publish(ctx, msg, WithReadiness(MinTopicSize(numHosts-1))); err != nil { t.Fatal(err) } @@ -215,81 +199,108 @@ func TestSimpleDiscovery(t *testing.T) { t.Fatal("got wrong message!") } } - } + + // Try random peers sending messages and make sure they are received + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d the flooooooood %d", i, i)) + + owner := rand.Intn(len(psubs)) + + if err := topicHandlers[owner].Publish(ctx, msg, WithReadiness(MinTopicSize(1))); err != nil { + t.Fatal(err) + } + + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } + } + } + }) } func TestGossipSubDiscoveryAfterBootstrap(t *testing.T) { - t.Skip("flaky test disabled") - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - // Setup Discovery server and pubsub clients - partitionSize := GossipSubDlo - 1 - numHosts := partitionSize * 2 - const ttl = 1 * time.Minute + // Setup Discovery server and pubsub clients + partitionSize := GossipSubDlo - 1 + numHosts := partitionSize * 2 + const ttl = 1 * time.Minute - const topic = "foobar" + const topic = "foobar" - server1, server2 := newDiscoveryServer(), newDiscoveryServer() - discOpts := []discovery.Option{discovery.Limit(numHosts), discovery.TTL(ttl)} + server1, server2 := newDiscoveryServer(), newDiscoveryServer() + discOpts := []discovery.Option{discovery.Limit(numHosts), discovery.TTL(ttl)} - // Put the pubsub clients into two partitions - hosts := getDefaultHosts(t, numHosts) - psubs := make([]*PubSub, numHosts) - topicHandlers := make([]*Topic, numHosts) + // Put the pubsub clients into two partitions + hosts := getDefaultHosts(t, numHosts) + psubs := make([]*PubSub, numHosts) + topicHandlers := make([]*Topic, numHosts) - for i, h := range hosts { - s := server1 - if i >= partitionSize { - s = server2 + for i, h := range hosts { + s := server1 + if i >= partitionSize { + s = server2 + } + disc := &mockDiscoveryClient{h, s} + ps := getGossipsub(ctx, h, WithDiscovery(disc, WithDiscoveryOpts(discOpts...))) + psubs[i] = ps + topicHandlers[i], _ = ps.Join(topic) } - disc := &mockDiscoveryClient{h, s} - ps := getGossipsub(ctx, h, WithDiscovery(disc, WithDiscoveryOpts(discOpts...))) - psubs[i] = ps - topicHandlers[i], _ = ps.Join(topic) - } - msgs := make([]*Subscription, numHosts) - for i, th := range topicHandlers { - subch, err := th.Subscribe() - if err != nil { - t.Fatal(err) - } + msgs := make([]*Subscription, numHosts) + for i, th := range topicHandlers { + subch, err := th.Subscribe() + if err != nil { + t.Fatal(err) + } - msgs[i] = subch - } + msgs[i] = subch + } - // Wait for network to finish forming then join the partitions via discovery - for _, ps := range psubs { - waitUntilGossipsubMeshCount(ps, topic, partitionSize-1) - } + // Wait for network to finish forming then join the partitions via discovery + for _, ps := range psubs { + waitUntilGossipsubMeshCount(ps, topic, partitionSize-1) + } - for i := 0; i < partitionSize; i++ { - if _, err := server1.Advertise("floodsub:"+topic, *host.InfoFromHost(hosts[i+partitionSize]), ttl); err != nil { - t.Fatal(err) + for i := 0; i < partitionSize; i++ { + if _, err := server1.Advertise("floodsub:"+topic, *host.InfoFromHost(hosts[i+partitionSize]), ttl); err != nil { + t.Fatal(err) + } } - } - // test the mesh - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + // Wait for discovery to connect the partitions + time.Sleep(5 * time.Second) - owner := rand.Intn(numHosts) + // test the mesh + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - if err := topicHandlers[owner].Publish(ctx, msg, WithReadiness(MinTopicSize(numHosts-1))); err != nil { - t.Fatal(err) - } + owner := rand.Intn(numHosts) - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) + // Use partitionSize as readiness threshold: enough to confirm + // cross-partition discovery worked, but within GossipSubD mesh limit. + if err := topicHandlers[owner].Publish(ctx, msg, WithReadiness(MinTopicSize(partitionSize))); err != nil { + t.Fatal(err) } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } //lint:ignore U1000 used only by skipped tests at present diff --git a/floodsub_test.go b/floodsub_test.go index fcdb12c4..f8b0cf93 100644 --- a/floodsub_test.go +++ b/floodsub_test.go @@ -9,7 +9,6 @@ import ( "fmt" "io" mrand "math/rand" - "os" "sort" "sync" "testing" @@ -141,241 +140,253 @@ func assertNeverReceives(t *testing.T, ch *Subscription, timeout time.Duration) } func TestBasicFloodsub(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 20) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 20) - psubs := getPubsubs(ctx, hosts) + psubs := getPubsubs(ctx, hosts) - var msgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + var msgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - msgs = append(msgs, subch) - } + msgs = append(msgs, subch) + } - // connectAll(t, hosts) - sparseConnect(t, hosts) + // connectAll(t, hosts) + sparseConnect(t, hosts) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d the flooooooood %d", i, i)) + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d the flooooooood %d", i, i)) - owner := mrand.Intn(len(psubs)) + owner := mrand.Intn(len(psubs)) - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } func TestMultihops(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 6) + hosts := getDefaultHosts(t, 6) - psubs := getPubsubs(ctx, hosts) + psubs := getPubsubs(ctx, hosts) - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) - connect(t, hosts[2], hosts[3]) - connect(t, hosts[3], hosts[4]) - connect(t, hosts[4], hosts[5]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) + connect(t, hosts[2], hosts[3]) + connect(t, hosts[3], hosts[4]) + connect(t, hosts[4], hosts[5]) - var subs []*Subscription - for i := 1; i < 6; i++ { - ch, err := psubs[i].Subscribe("foobar") - if err != nil { - t.Fatal(err) + var subs []*Subscription + for i := 1; i < 6; i++ { + ch, err := psubs[i].Subscribe("foobar") + if err != nil { + t.Fatal(err) + } + subs = append(subs, ch) } - subs = append(subs, ch) - } - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - msg := []byte("i like cats") - err := psubs[0].Publish("foobar", msg) - if err != nil { - t.Fatal(err) - } + msg := []byte("i like cats") + err := psubs[0].Publish("foobar", msg) + if err != nil { + t.Fatal(err) + } - // last node in the chain should get the message - select { - case out := <-subs[4].ch: - if !bytes.Equal(out.GetData(), msg) { - t.Fatal("got wrong data") + // last node in the chain should get the message + select { + case out := <-subs[4].ch: + if !bytes.Equal(out.GetData(), msg) { + t.Fatal("got wrong data") + } + case <-time.After(time.Second * 5): + t.Fatal("timed out waiting for message") } - case <-time.After(time.Second * 5): - t.Fatal("timed out waiting for message") - } + }) } func TestReconnects(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 3) + hosts := getDefaultHosts(t, 3) - psubs := getPubsubs(ctx, hosts) + psubs := getPubsubs(ctx, hosts) - connect(t, hosts[0], hosts[1]) - connect(t, hosts[0], hosts[2]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[2]) - A, err := psubs[1].Subscribe("cats") - if err != nil { - t.Fatal(err) - } + A, err := psubs[1].Subscribe("cats") + if err != nil { + t.Fatal(err) + } - B, err := psubs[2].Subscribe("cats") - if err != nil { - t.Fatal(err) - } + B, err := psubs[2].Subscribe("cats") + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - msg := []byte("apples and oranges") - err = psubs[0].Publish("cats", msg) - if err != nil { - t.Fatal(err) - } + msg := []byte("apples and oranges") + err = psubs[0].Publish("cats", msg) + if err != nil { + t.Fatal(err) + } - assertReceive(t, A, msg) - assertReceive(t, B, msg) + assertReceive(t, A, msg) + assertReceive(t, B, msg) - B.Cancel() + B.Cancel() - time.Sleep(time.Millisecond * 50) + time.Sleep(time.Millisecond * 50) - msg2 := []byte("potato") - err = psubs[0].Publish("cats", msg2) - if err != nil { - t.Fatal(err) - } + msg2 := []byte("potato") + err = psubs[0].Publish("cats", msg2) + if err != nil { + t.Fatal(err) + } - assertReceive(t, A, msg2) - select { - case _, ok := <-B.ch: - if ok { - t.Fatal("shouldnt have gotten data on this channel") + assertReceive(t, A, msg2) + select { + case _, ok := <-B.ch: + if ok { + t.Fatal("shouldnt have gotten data on this channel") + } + case <-time.After(time.Second): + t.Fatal("timed out waiting for B chan to be closed") } - case <-time.After(time.Second): - t.Fatal("timed out waiting for B chan to be closed") - } - nSubs := make(chan int) - psubs[2].eval <- func() { - nSubs <- len(psubs[2].mySubs["cats"]) - } - if <-nSubs > 0 { - t.Fatal(`B should have 0 subscribers for channel "cats", has`, nSubs) - } + nSubs := make(chan int) + psubs[2].eval <- func() { + nSubs <- len(psubs[2].mySubs["cats"]) + } + if <-nSubs > 0 { + t.Fatal(`B should have 0 subscribers for channel "cats", has`, nSubs) + } - ch2, err := psubs[2].Subscribe("cats") - if err != nil { - t.Fatal(err) - } + ch2, err := psubs[2].Subscribe("cats") + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - nextmsg := []byte("ifps is kul") - err = psubs[0].Publish("cats", nextmsg) - if err != nil { - t.Fatal(err) - } + nextmsg := []byte("ifps is kul") + err = psubs[0].Publish("cats", nextmsg) + if err != nil { + t.Fatal(err) + } - assertReceive(t, ch2, nextmsg) + assertReceive(t, ch2, nextmsg) + }) } // make sure messages arent routed between nodes who arent subscribed func TestNoConnection(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 10) + hosts := getDefaultHosts(t, 10) - psubs := getPubsubs(ctx, hosts) + psubs := getPubsubs(ctx, hosts) - ch, err := psubs[5].Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + ch, err := psubs[5].Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - err = psubs[0].Publish("foobar", []byte("TESTING")) - if err != nil { - t.Fatal(err) - } + err = psubs[0].Publish("foobar", []byte("TESTING")) + if err != nil { + t.Fatal(err) + } - select { - case <-ch.ch: - t.Fatal("shouldnt have gotten a message") - case <-time.After(time.Millisecond * 200): - } + select { + case <-ch.ch: + t.Fatal("shouldnt have gotten a message") + case <-time.After(time.Millisecond * 200): + } + }) } func TestSelfReceive(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - host := getDefaultHosts(t, 1)[0] + host := getDefaultHosts(t, 1)[0] - psub, err := NewFloodSub(ctx, host) - if err != nil { - t.Fatal(err) - } + psub, err := NewFloodSub(ctx, host) + if err != nil { + t.Fatal(err) + } - msg := []byte("hello world") + msg := []byte("hello world") - err = psub.Publish("foobar", msg) - if err != nil { - t.Fatal(err) - } + err = psub.Publish("foobar", msg) + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 10) + time.Sleep(time.Millisecond * 10) - ch, err := psub.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + ch, err := psub.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - msg2 := []byte("goodbye world") - err = psub.Publish("foobar", msg2) - if err != nil { - t.Fatal(err) - } + msg2 := []byte("goodbye world") + err = psub.Publish("foobar", msg2) + if err != nil { + t.Fatal(err) + } - assertReceive(t, ch, msg2) + assertReceive(t, ch, msg2) + }) } func TestOneToOne(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) - psubs := getPubsubs(ctx, hosts) + hosts := getDefaultHosts(t, 2) + psubs := getPubsubs(ctx, hosts) - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - sub, err := psubs[1].Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + sub, err := psubs[1].Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 50) + time.Sleep(time.Millisecond * 50) - checkMessageRouting(t, "foobar", psubs, []*Subscription{sub}) + checkMessageRouting(t, "foobar", psubs, []*Subscription{sub}) + }) } func assertPeerLists(t *testing.T, hosts []host.Host, ps *PubSub, has ...int) { @@ -393,49 +404,51 @@ func assertPeerLists(t *testing.T, hosts []host.Host, ps *PubSub, has ...int) { } func TestTreeTopology(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - hosts := getDefaultHosts(t, 10) - psubs := getPubsubs(ctx, hosts) - - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) - connect(t, hosts[1], hosts[4]) - connect(t, hosts[2], hosts[3]) - connect(t, hosts[0], hosts[5]) - connect(t, hosts[5], hosts[6]) - connect(t, hosts[5], hosts[8]) - connect(t, hosts[6], hosts[7]) - connect(t, hosts[8], hosts[9]) - - /* - [0] -> [1] -> [2] -> [3] - | L->[4] - v - [5] -> [6] -> [7] - | - v - [8] -> [9] - */ - - var chs []*Subscription - for _, ps := range psubs { - ch, err := ps.Subscribe("fizzbuzz") - if err != nil { - t.Fatal(err) - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - chs = append(chs, ch) - } + hosts := getDefaultHosts(t, 10) + psubs := getPubsubs(ctx, hosts) + + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) + connect(t, hosts[1], hosts[4]) + connect(t, hosts[2], hosts[3]) + connect(t, hosts[0], hosts[5]) + connect(t, hosts[5], hosts[6]) + connect(t, hosts[5], hosts[8]) + connect(t, hosts[6], hosts[7]) + connect(t, hosts[8], hosts[9]) + + /* + [0] -> [1] -> [2] -> [3] + | L->[4] + v + [5] -> [6] -> [7] + | + v + [8] -> [9] + */ + + var chs []*Subscription + for _, ps := range psubs { + ch, err := ps.Subscribe("fizzbuzz") + if err != nil { + t.Fatal(err) + } + + chs = append(chs, ch) + } - time.Sleep(time.Millisecond * 50) + time.Sleep(time.Millisecond * 50) - assertPeerLists(t, hosts, psubs[0], 1, 5) - assertPeerLists(t, hosts, psubs[1], 0, 2, 4) - assertPeerLists(t, hosts, psubs[2], 1, 3) + assertPeerLists(t, hosts, psubs[0], 1, 5) + assertPeerLists(t, hosts, psubs[1], 0, 2, 4) + assertPeerLists(t, hosts, psubs[2], 1, 3) - checkMessageRouting(t, "fizzbuzz", []*PubSub{psubs[9], psubs[3]}, chs) + checkMessageRouting(t, "fizzbuzz", []*PubSub{psubs[9], psubs[3]}, chs) + }) } func assertHasTopics(t *testing.T, ps *PubSub, exptopics ...string) { @@ -456,71 +469,75 @@ func assertHasTopics(t *testing.T, ps *PubSub, exptopics ...string) { func TestFloodSubPluggableProtocol(t *testing.T) { t.Run("multi-procol router acts like a hub", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 3) + hosts := getDefaultHosts(t, 3) - psubA := mustCreatePubSub(ctx, t, hosts[0], "/esh/floodsub", "/lsr/floodsub") - psubB := mustCreatePubSub(ctx, t, hosts[1], "/esh/floodsub") - psubC := mustCreatePubSub(ctx, t, hosts[2], "/lsr/floodsub") + psubA := mustCreatePubSub(ctx, t, hosts[0], "/esh/floodsub", "/lsr/floodsub") + psubB := mustCreatePubSub(ctx, t, hosts[1], "/esh/floodsub") + psubC := mustCreatePubSub(ctx, t, hosts[2], "/lsr/floodsub") - subA := mustSubscribe(t, psubA, "foobar") - defer subA.Cancel() + subA := mustSubscribe(t, psubA, "foobar") + defer subA.Cancel() - subB := mustSubscribe(t, psubB, "foobar") - defer subB.Cancel() + subB := mustSubscribe(t, psubB, "foobar") + defer subB.Cancel() - subC := mustSubscribe(t, psubC, "foobar") - defer subC.Cancel() + subC := mustSubscribe(t, psubC, "foobar") + defer subC.Cancel() - // B --> A, C --> A - connect(t, hosts[1], hosts[0]) - connect(t, hosts[2], hosts[0]) + // B --> A, C --> A + connect(t, hosts[1], hosts[0]) + connect(t, hosts[2], hosts[0]) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - psubC.Publish("foobar", []byte("bar")) + psubC.Publish("foobar", []byte("bar")) - assertReceive(t, subA, []byte("bar")) - assertReceive(t, subB, []byte("bar")) - assertReceive(t, subC, []byte("bar")) + assertReceive(t, subA, []byte("bar")) + assertReceive(t, subB, []byte("bar")) + assertReceive(t, subC, []byte("bar")) + }) }) t.Run("won't talk to routers with no protocol overlap", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) + hosts := getDefaultHosts(t, 2) - psubA := mustCreatePubSub(ctx, t, hosts[0], "/esh/floodsub") - psubB := mustCreatePubSub(ctx, t, hosts[1], "/lsr/floodsub") + psubA := mustCreatePubSub(ctx, t, hosts[0], "/esh/floodsub") + psubB := mustCreatePubSub(ctx, t, hosts[1], "/lsr/floodsub") - subA := mustSubscribe(t, psubA, "foobar") - defer subA.Cancel() + subA := mustSubscribe(t, psubA, "foobar") + defer subA.Cancel() - subB := mustSubscribe(t, psubB, "foobar") - defer subB.Cancel() + subB := mustSubscribe(t, psubB, "foobar") + defer subB.Cancel() - connect(t, hosts[1], hosts[0]) + connect(t, hosts[1], hosts[0]) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - psubA.Publish("foobar", []byte("bar")) + psubA.Publish("foobar", []byte("bar")) - assertReceive(t, subA, []byte("bar")) + assertReceive(t, subA, []byte("bar")) - pass := false - select { - case <-subB.ch: - t.Fatal("different protocols: should not have received message") - case <-time.After(time.Second * 1): - pass = true - } + pass := false + select { + case <-subB.ch: + t.Fatal("different protocols: should not have received message") + case <-time.After(time.Second * 1): + pass = true + } - if !pass { - t.Fatal("should have timed out waiting for message") - } + if !pass { + t.Fatal("should have timed out waiting for message") + } + }) }) } @@ -543,180 +560,188 @@ func mustSubscribe(t *testing.T, ps *PubSub, topic string) *Subscription { } func TestSubReporting(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - host := getDefaultHosts(t, 1)[0] - psub, err := NewFloodSub(ctx, host) - if err != nil { - t.Fatal(err) - } + host := getDefaultHosts(t, 1)[0] + psub, err := NewFloodSub(ctx, host) + if err != nil { + t.Fatal(err) + } - fooSub, err := psub.Subscribe("foo") - if err != nil { - t.Fatal(err) - } + fooSub, err := psub.Subscribe("foo") + if err != nil { + t.Fatal(err) + } - barSub, err := psub.Subscribe("bar") - if err != nil { - t.Fatal(err) - } + barSub, err := psub.Subscribe("bar") + if err != nil { + t.Fatal(err) + } - assertHasTopics(t, psub, "foo", "bar") + assertHasTopics(t, psub, "foo", "bar") - _, err = psub.Subscribe("baz") - if err != nil { - t.Fatal(err) - } + _, err = psub.Subscribe("baz") + if err != nil { + t.Fatal(err) + } - assertHasTopics(t, psub, "foo", "bar", "baz") + assertHasTopics(t, psub, "foo", "bar", "baz") - barSub.Cancel() - assertHasTopics(t, psub, "foo", "baz") - fooSub.Cancel() - assertHasTopics(t, psub, "baz") + barSub.Cancel() + assertHasTopics(t, psub, "foo", "baz") + fooSub.Cancel() + assertHasTopics(t, psub, "baz") - _, err = psub.Subscribe("fish") - if err != nil { - t.Fatal(err) - } + _, err = psub.Subscribe("fish") + if err != nil { + t.Fatal(err) + } - assertHasTopics(t, psub, "baz", "fish") + assertHasTopics(t, psub, "baz", "fish") + }) } func TestPeerTopicReporting(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 4) - psubs := getPubsubs(ctx, hosts) + hosts := getDefaultHosts(t, 4) + psubs := getPubsubs(ctx, hosts) - connect(t, hosts[0], hosts[1]) - connect(t, hosts[0], hosts[2]) - connect(t, hosts[0], hosts[3]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[2]) + connect(t, hosts[0], hosts[3]) - _, err := psubs[1].Subscribe("foo") - if err != nil { - t.Fatal(err) - } - _, err = psubs[1].Subscribe("bar") - if err != nil { - t.Fatal(err) - } - _, err = psubs[1].Subscribe("baz") - if err != nil { - t.Fatal(err) - } + _, err := psubs[1].Subscribe("foo") + if err != nil { + t.Fatal(err) + } + _, err = psubs[1].Subscribe("bar") + if err != nil { + t.Fatal(err) + } + _, err = psubs[1].Subscribe("baz") + if err != nil { + t.Fatal(err) + } - _, err = psubs[2].Subscribe("foo") - if err != nil { - t.Fatal(err) - } - _, err = psubs[2].Subscribe("ipfs") - if err != nil { - t.Fatal(err) - } + _, err = psubs[2].Subscribe("foo") + if err != nil { + t.Fatal(err) + } + _, err = psubs[2].Subscribe("ipfs") + if err != nil { + t.Fatal(err) + } - _, err = psubs[3].Subscribe("baz") - if err != nil { - t.Fatal(err) - } - _, err = psubs[3].Subscribe("ipfs") - if err != nil { - t.Fatal(err) - } + _, err = psubs[3].Subscribe("baz") + if err != nil { + t.Fatal(err) + } + _, err = psubs[3].Subscribe("ipfs") + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 200) + time.Sleep(time.Millisecond * 200) - peers := psubs[0].ListPeers("ipfs") - assertPeerList(t, peers, hosts[2].ID(), hosts[3].ID()) + peers := psubs[0].ListPeers("ipfs") + assertPeerList(t, peers, hosts[2].ID(), hosts[3].ID()) - peers = psubs[0].ListPeers("foo") - assertPeerList(t, peers, hosts[1].ID(), hosts[2].ID()) + peers = psubs[0].ListPeers("foo") + assertPeerList(t, peers, hosts[1].ID(), hosts[2].ID()) - peers = psubs[0].ListPeers("baz") - assertPeerList(t, peers, hosts[1].ID(), hosts[3].ID()) + peers = psubs[0].ListPeers("baz") + assertPeerList(t, peers, hosts[1].ID(), hosts[3].ID()) - peers = psubs[0].ListPeers("bar") - assertPeerList(t, peers, hosts[1].ID()) + peers = psubs[0].ListPeers("bar") + assertPeerList(t, peers, hosts[1].ID()) + }) } func TestSubscribeMultipleTimes(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) - psubs := getPubsubs(ctx, hosts) + hosts := getDefaultHosts(t, 2) + psubs := getPubsubs(ctx, hosts) - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - sub1, err := psubs[0].Subscribe("foo") - if err != nil { - t.Fatal(err) - } - sub2, err := psubs[0].Subscribe("foo") - if err != nil { - t.Fatal(err) - } + sub1, err := psubs[0].Subscribe("foo") + if err != nil { + t.Fatal(err) + } + sub2, err := psubs[0].Subscribe("foo") + if err != nil { + t.Fatal(err) + } - // make sure subscribing is finished by the time we publish - time.Sleep(10 * time.Millisecond) + // make sure subscribing is finished by the time we publish + time.Sleep(10 * time.Millisecond) - psubs[1].Publish("foo", []byte("bar")) + psubs[1].Publish("foo", []byte("bar")) - msg, err := sub1.Next(ctx) - if err != nil { - t.Fatalf("unexpected error: %v.", err) - } + msg, err := sub1.Next(ctx) + if err != nil { + t.Fatalf("unexpected error: %v.", err) + } - data := string(msg.GetData()) + data := string(msg.GetData()) - if data != "bar" { - t.Fatalf("data is %s, expected %s.", data, "bar") - } + if data != "bar" { + t.Fatalf("data is %s, expected %s.", data, "bar") + } - msg, err = sub2.Next(ctx) - if err != nil { - t.Fatalf("unexpected error: %v.", err) - } - data = string(msg.GetData()) + msg, err = sub2.Next(ctx) + if err != nil { + t.Fatalf("unexpected error: %v.", err) + } + data = string(msg.GetData()) - if data != "bar" { - t.Fatalf("data is %s, expected %s.", data, "bar") - } + if data != "bar" { + t.Fatalf("data is %s, expected %s.", data, "bar") + } + }) } func TestPeerDisconnect(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) - psubs := getPubsubs(ctx, hosts) + hosts := getDefaultHosts(t, 2) + psubs := getPubsubs(ctx, hosts) - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - _, err := psubs[0].Subscribe("foo") - if err != nil { - t.Fatal(err) - } + _, err := psubs[0].Subscribe("foo") + if err != nil { + t.Fatal(err) + } - _, err = psubs[1].Subscribe("foo") - if err != nil { - t.Fatal(err) - } + _, err = psubs[1].Subscribe("foo") + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 300) - peers := psubs[0].ListPeers("foo") - assertPeerList(t, peers, hosts[1].ID()) - for _, c := range hosts[1].Network().ConnsToPeer(hosts[0].ID()) { - c.Close() - } + peers := psubs[0].ListPeers("foo") + assertPeerList(t, peers, hosts[1].ID()) + for _, c := range hosts[1].Network().ConnsToPeer(hosts[0].ID()) { + c.Close() + } - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 300) - peers = psubs[0].ListPeers("foo") - assertPeerList(t, peers) + peers = psubs[0].ListPeers("foo") + assertPeerList(t, peers) + }) } func assertPeerList(t *testing.T, peers []peer.ID, expected ...peer.ID) { @@ -735,345 +760,360 @@ func assertPeerList(t *testing.T, peers []peer.ID, expected ...peer.ID) { } func TestWithNoSigning(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) - psubs := getPubsubs(ctx, hosts, WithNoAuthor(), WithMessageIdFn(func(pmsg *pb.Message) string { - // silly content-based test message-ID: just use the data as whole - return base64.URLEncoding.EncodeToString(pmsg.Data) - })) + hosts := getDefaultHosts(t, 2) + psubs := getPubsubs(ctx, hosts, WithNoAuthor(), WithMessageIdFn(func(pmsg *pb.Message) string { + // silly content-based test message-ID: just use the data as whole + return base64.URLEncoding.EncodeToString(pmsg.Data) + })) - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - topic := "foobar" - data := []byte("this is a message") + topic := "foobar" + data := []byte("this is a message") - sub, err := psubs[1].Subscribe(topic) - if err != nil { - t.Fatal(err) - } + sub, err := psubs[1].Subscribe(topic) + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 10) + time.Sleep(time.Millisecond * 10) - err = psubs[0].Publish(topic, data) - if err != nil { - t.Fatal(err) - } + err = psubs[0].Publish(topic, data) + if err != nil { + t.Fatal(err) + } - msg, err := sub.Next(ctx) - if err != nil { - t.Fatal(err) - } - if msg.Signature != nil { - t.Fatal("signature in message") - } - if msg.From != nil { - t.Fatal("from in message") - } - if msg.Seqno != nil { - t.Fatal("seqno in message") - } - if string(msg.Data) != string(data) { - t.Fatalf("unexpected data: %s", string(msg.Data)) - } + msg, err := sub.Next(ctx) + if err != nil { + t.Fatal(err) + } + if msg.Signature != nil { + t.Fatal("signature in message") + } + if msg.From != nil { + t.Fatal("from in message") + } + if msg.Seqno != nil { + t.Fatal("seqno in message") + } + if string(msg.Data) != string(data) { + t.Fatalf("unexpected data: %s", string(msg.Data)) + } + }) } func TestWithSigning(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) - psubs := getPubsubs(ctx, hosts, WithStrictSignatureVerification(true)) + hosts := getDefaultHosts(t, 2) + psubs := getPubsubs(ctx, hosts, WithStrictSignatureVerification(true)) - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - topic := "foobar" - data := []byte("this is a message") + topic := "foobar" + data := []byte("this is a message") - sub, err := psubs[1].Subscribe(topic) - if err != nil { - t.Fatal(err) - } + sub, err := psubs[1].Subscribe(topic) + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 10) + time.Sleep(time.Millisecond * 10) - err = psubs[0].Publish(topic, data) - if err != nil { - t.Fatal(err) - } + err = psubs[0].Publish(topic, data) + if err != nil { + t.Fatal(err) + } - msg, err := sub.Next(ctx) - if err != nil { - t.Fatal(err) - } - if msg.Signature == nil { - t.Fatal("no signature in message") - } - if msg.From == nil { - t.Fatal("from not in message") - } - if msg.Seqno == nil { - t.Fatal("seqno not in message") - } - if string(msg.Data) != string(data) { - t.Fatalf("unexpected data: %s", string(msg.Data)) - } + msg, err := sub.Next(ctx) + if err != nil { + t.Fatal(err) + } + if msg.Signature == nil { + t.Fatal("no signature in message") + } + if msg.From == nil { + t.Fatal("from not in message") + } + if msg.Seqno == nil { + t.Fatal("seqno not in message") + } + if string(msg.Data) != string(data) { + t.Fatalf("unexpected data: %s", string(msg.Data)) + } + }) } func TestImproperlySignedMessageRejected(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - hosts := getDefaultHosts(t, 2) - adversary := hosts[0] - honestPeer := hosts[1] - - // The adversary enables signing, but disables verification to let through - // an incorrectly signed message. - adversaryPubSub := getPubsub( - ctx, - adversary, - WithMessageSigning(true), - WithStrictSignatureVerification(false), - ) - honestPubSub := getPubsub( - ctx, - honestPeer, - WithStrictSignatureVerification(true), - ) - - connect(t, adversary, honestPeer) - - var ( - topic = "foobar" - correctMessage = []byte("this is a correct message") - incorrectMessage = []byte("this is the incorrect message") - ) - - adversarySubscription, err := adversaryPubSub.Subscribe(topic) - if err != nil { - t.Fatal(err) - } - honestPeerSubscription, err := honestPubSub.Subscribe(topic) - if err != nil { - t.Fatal(err) - } - time.Sleep(time.Millisecond * 50) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - // First the adversary sends the correct message. - err = adversaryPubSub.Publish(topic, correctMessage) - if err != nil { - t.Fatal(err) - } + hosts := getDefaultHosts(t, 2) + adversary := hosts[0] + honestPeer := hosts[1] + + // The adversary enables signing, but disables verification to let through + // an incorrectly signed message. + adversaryPubSub := getPubsub( + ctx, + adversary, + WithMessageSigning(true), + WithStrictSignatureVerification(false), + ) + honestPubSub := getPubsub( + ctx, + honestPeer, + WithStrictSignatureVerification(true), + ) - // Change the sign key for the adversarial peer, and send the second, - // incorrectly signed, message. - adversaryPubSub.signID = honestPubSub.signID - adversaryPubSub.signKey = honestPubSub.host.Peerstore().PrivKey(honestPubSub.signID) - err = adversaryPubSub.Publish(topic, incorrectMessage) - if err != nil { - t.Fatal(err) - } + connect(t, adversary, honestPeer) - adversaryMessagesCh := make(chan []*Message) + var ( + topic = "foobar" + correctMessage = []byte("this is a correct message") + incorrectMessage = []byte("this is the incorrect message") + ) - adversaryContext, adversaryCancel := context.WithCancel(ctx) - go func(ctx context.Context) { - var adversaryMessages []*Message - defer func() { - adversaryMessagesCh <- adversaryMessages - }() - for { - select { - case <-ctx.Done(): - return - default: - msg, err := adversarySubscription.Next(ctx) - if err != nil { - return - } - adversaryMessages = append(adversaryMessages, msg) - } + adversarySubscription, err := adversaryPubSub.Subscribe(topic) + if err != nil { + t.Fatal(err) + } + honestPeerSubscription, err := honestPubSub.Subscribe(topic) + if err != nil { + t.Fatal(err) } - }(adversaryContext) + time.Sleep(time.Millisecond * 50) - <-time.After(1 * time.Second) - adversaryCancel() - adversaryMessages := <-adversaryMessagesCh + // First the adversary sends the correct message. + err = adversaryPubSub.Publish(topic, correctMessage) + if err != nil { + t.Fatal(err) + } - // Ensure the adversary successfully publishes the incorrectly signed - // message. If the adversary "sees" this, we successfully got through - // their local validation. - if len(adversaryMessages) != 2 { - t.Fatalf("got %d messages, expected 2", len(adversaryMessages)) - } + // Change the sign key for the adversarial peer, and send the second, + // incorrectly signed, message. + adversaryPubSub.signID = honestPubSub.signID + adversaryPubSub.signKey = honestPubSub.host.Peerstore().PrivKey(honestPubSub.signID) + err = adversaryPubSub.Publish(topic, incorrectMessage) + if err != nil { + t.Fatal(err) + } - // the honest peer's validation process will drop the message; - // next will never furnish the incorrect message. - honestPeerMessagesCh := make(chan []*Message) - honestPeerContext, honestPeerCancel := context.WithCancel(ctx) - go func(ctx context.Context) { - var honestPeerMessages []*Message - defer func() { - honestPeerMessagesCh <- honestPeerMessages - }() - for { - select { - case <-ctx.Done(): - return - default: - msg, err := honestPeerSubscription.Next(ctx) - if err != nil { + adversaryMessagesCh := make(chan []*Message) + + adversaryContext, adversaryCancel := context.WithCancel(ctx) + go func(ctx context.Context) { + var adversaryMessages []*Message + defer func() { + adversaryMessagesCh <- adversaryMessages + }() + for { + select { + case <-ctx.Done(): return + default: + msg, err := adversarySubscription.Next(ctx) + if err != nil { + return + } + adversaryMessages = append(adversaryMessages, msg) } - honestPeerMessages = append(honestPeerMessages, msg) } - } - }(honestPeerContext) - - <-time.After(1 * time.Second) - honestPeerCancel() + }(adversaryContext) - honestPeerMessages := <-honestPeerMessagesCh - if len(honestPeerMessages) != 1 { - t.Fatalf("got %d messages, expected 1", len(honestPeerMessages)) - } - if string(honestPeerMessages[0].GetData()) != string(correctMessage) { - t.Fatalf( - "got %s, expected message %s", - honestPeerMessages[0].GetData(), - correctMessage, - ) - } -} + <-time.After(1 * time.Second) + adversaryCancel() + adversaryMessages := <-adversaryMessagesCh -func TestMessageSender(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + // Ensure the adversary successfully publishes the incorrectly signed + // message. If the adversary "sees" this, we successfully got through + // their local validation. + if len(adversaryMessages) != 2 { + t.Fatalf("got %d messages, expected 2", len(adversaryMessages)) + } - const topic = "foobar" + // the honest peer's validation process will drop the message; + // next will never furnish the incorrect message. + honestPeerMessagesCh := make(chan []*Message) + honestPeerContext, honestPeerCancel := context.WithCancel(ctx) + go func(ctx context.Context) { + var honestPeerMessages []*Message + defer func() { + honestPeerMessagesCh <- honestPeerMessages + }() + for { + select { + case <-ctx.Done(): + return + default: + msg, err := honestPeerSubscription.Next(ctx) + if err != nil { + return + } + honestPeerMessages = append(honestPeerMessages, msg) + } + } + }(honestPeerContext) - hosts := getDefaultHosts(t, 3) - psubs := getPubsubs(ctx, hosts) + <-time.After(1 * time.Second) + honestPeerCancel() - var msgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + honestPeerMessages := <-honestPeerMessagesCh + if len(honestPeerMessages) != 1 { + t.Fatalf("got %d messages, expected 1", len(honestPeerMessages)) + } + if string(honestPeerMessages[0].GetData()) != string(correctMessage) { + t.Fatalf( + "got %s, expected message %s", + honestPeerMessages[0].GetData(), + correctMessage, + ) } + }) +} - msgs = append(msgs, subch) - } +func TestMessageSender(t *testing.T) { + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) + const topic = "foobar" - time.Sleep(time.Millisecond * 100) + hosts := getDefaultHosts(t, 3) + psubs := getPubsubs(ctx, hosts) - for i := 0; i < 3; i++ { - for j := 0; j < 100; j++ { - msg := []byte(fmt.Sprintf("%d sent %d", i, j)) + var msgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } - psubs[i].Publish(topic, msg) + msgs = append(msgs, subch) + } - for k, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") - } + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) - var expectedHost int - if i == k { - expectedHost = i - } else if k != 1 { - expectedHost = 1 - } else { - expectedHost = i - } + time.Sleep(time.Millisecond * 100) - if got.ReceivedFrom != hosts[expectedHost].ID() { - t.Fatal("got wrong message sender") + for i := 0; i < 3; i++ { + for j := 0; j < 100; j++ { + msg := []byte(fmt.Sprintf("%d sent %d", i, j)) + + psubs[i].Publish(topic, msg) + + for k, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } + + var expectedHost int + if i == k { + expectedHost = i + } else if k != 1 { + expectedHost = 1 + } else { + expectedHost = i + } + + if got.ReceivedFrom != hosts[expectedHost].ID() { + t.Fatal("got wrong message sender") + } } } } - } + }) } func TestConfigurableMaxMessageSize(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 10) + hosts := getDefaultHosts(t, 10) - // use a 4mb limit; default is 1mb; we'll test with a 2mb payload. - psubs := getPubsubs(ctx, hosts, WithMaxMessageSize(1<<22)) + // use a 4mb limit; default is 1mb; we'll test with a 2mb payload. + psubs := getPubsubs(ctx, hosts, WithMaxMessageSize(1<<22)) - sparseConnect(t, hosts) - time.Sleep(time.Millisecond * 100) + sparseConnect(t, hosts) - const topic = "foobar" - var subs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + const topic = "foobar" + var subs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } + subs = append(subs, subch) } - subs = append(subs, subch) - } - // 2mb payload. - msg := make([]byte, 1<<21) - crand.Read(msg) - err := psubs[0].Publish(topic, msg) - if err != nil { - t.Fatal(err) - } + // Wait for subscriptions to propagate through the sparse network + time.Sleep(time.Second) + + // 2mb payload. + msg := make([]byte, 1<<21) + crand.Read(msg) - // make sure that all peers received the message. - for _, sub := range subs { - got, err := sub.Next(ctx) + err := psubs[0].Publish(topic, msg) if err != nil { - t.Fatal(sub.err) + t.Fatal(err) } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + + // make sure that all peers received the message. + for _, sub := range subs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } - } + }) } func TestAnnounceRetry(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) - ps := getPubsub(ctx, hosts[0]) - watcher := &announceWatcher{} - hosts[1].SetStreamHandler(FloodSubID, watcher.handleStream) + hosts := getDefaultHosts(t, 2) + ps := getPubsub(ctx, hosts[0]) + watcher := &announceWatcher{} + hosts[1].SetStreamHandler(FloodSubID, watcher.handleStream) - _, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) - } + _, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } - // connect the watcher to the pubsub - connect(t, hosts[0], hosts[1]) + // connect the watcher to the pubsub + connect(t, hosts[0], hosts[1]) - // wait a bit for the first subscription to be emitted and trigger announce retry - time.Sleep(100 * time.Millisecond) - go ps.announceRetry(hosts[1].ID(), "test", true) + // wait a bit for the first subscription to be emitted and trigger announce retry + time.Sleep(100 * time.Millisecond) + go ps.announceRetry(hosts[1].ID(), "test", true) - // wait a bit for the subscription to propagate and ensure it was received twice - time.Sleep(time.Second + 100*time.Millisecond) - count := watcher.countSubs() - if count != 2 { - t.Fatalf("expected 2 subscription messages, but got %d", count) - } + // wait a bit for the subscription to propagate and ensure it was received twice + time.Sleep(time.Second + 100*time.Millisecond) + count := watcher.countSubs() + if count != 2 { + t.Fatalf("expected 2 subscription messages, but got %d", count) + } + }) } type announceWatcher struct { @@ -1115,186 +1155,193 @@ func (aw *announceWatcher) countSubs() int { func TestPubsubWithAssortedOptions(t *testing.T) { // this test uses assorted options that are not covered in other tests - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - hashMsgID := func(m *pb.Message) string { - hash := sha256.Sum256(m.Data) - return string(hash[:]) - } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + hashMsgID := func(m *pb.Message) string { + hash := sha256.Sum256(m.Data) + return string(hash[:]) + } - hosts := getDefaultHosts(t, 2) - psubs := getPubsubs(ctx, hosts, - WithMessageIdFn(hashMsgID), - WithPeerOutboundQueueSize(10), - WithMessageAuthor(""), - WithBlacklist(NewMapBlacklist())) + hosts := getDefaultHosts(t, 2) + psubs := getPubsubs(ctx, hosts, + WithMessageIdFn(hashMsgID), + WithPeerOutboundQueueSize(10), + WithMessageAuthor(""), + WithBlacklist(NewMapBlacklist())) - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - var subs []*Subscription - for _, ps := range psubs { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + var subs []*Subscription + for _, ps := range psubs { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } + subs = append(subs, sub) } - subs = append(subs, sub) - } - time.Sleep(time.Second) + time.Sleep(time.Second) - for i := 0; i < 2; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[i].Publish("test", msg) + for i := 0; i < 2; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[i].Publish("test", msg) - for _, sub := range subs { - assertReceive(t, sub, msg) + for _, sub := range subs { + assertReceive(t, sub, msg) + } } - } + }) } func TestWithInvalidMessageAuthor(t *testing.T) { // this test exercises the failure path in the WithMessageAuthor option - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - h := getDefaultHosts(t, 1)[0] - _, err := NewFloodSub(ctx, h, WithMessageAuthor("bogotr0n")) - if err == nil { - t.Fatal("expected error") - } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + h := getDefaultHosts(t, 1)[0] + _, err := NewFloodSub(ctx, h, WithMessageAuthor("bogotr0n")) + if err == nil { + t.Fatal("expected error") + } + }) } func TestPreconnectedNodes(t *testing.T) { - if os.Getenv("CI") != "" { - t.Skip("Flaky test in CI") - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - // If this test fails it may hang so set a timeout - ctx, cancel = context.WithTimeout(ctx, time.Second*10) - defer cancel() - - // Create hosts - hosts := getDefaultHosts(t, 2) - h1 := hosts[0] - h2 := hosts[1] - - opts := []Option{WithDiscovery(&dummyDiscovery{})} - // Setup first PubSub - p1, err := NewFloodSub(ctx, h1, opts...) - if err != nil { - t.Fatal(err) - } - - // Connect the two hosts together - connect(t, h2, h1) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + // If this test fails it may hang so set a timeout + ctx, cancel = context.WithTimeout(ctx, time.Second*10) + defer cancel() - // Setup the second DHT - p2, err := NewFloodSub(ctx, h2, opts...) - if err != nil { - t.Fatal(err) - } + // Create hosts + hosts := getDefaultHosts(t, 2) + h1 := hosts[0] + h2 := hosts[1] - // See if it works - p2Topic, err := p2.Join("test") - if err != nil { - t.Fatal(err) - } + opts := []Option{WithDiscovery(&dummyDiscovery{})} + // Setup first PubSub + p1, err := NewFloodSub(ctx, h1, opts...) + if err != nil { + t.Fatal(err) + } - p1Topic, err := p1.Join("test") - if err != nil { - t.Fatal(err) - } + // Connect the two hosts together + connect(t, h2, h1) - testPublish := func(publisher, receiver *Topic, msg []byte) { - receiverSub, err := receiver.Subscribe() + // Setup the second DHT + p2, err := NewFloodSub(ctx, h2, opts...) if err != nil { t.Fatal(err) } - if err := publisher.Publish(ctx, msg, WithReadiness(MinTopicSize(1))); err != nil { + // See if it works + p2Topic, err := p2.Join("test") + if err != nil { t.Fatal(err) } - m, err := receiverSub.Next(ctx) + p1Topic, err := p1.Join("test") if err != nil { t.Fatal(err) } - if receivedData := m.GetData(); !bytes.Equal(receivedData, msg) { - t.Fatalf("expected message %v, got %v", msg, receivedData) + testPublish := func(publisher, receiver *Topic, msg []byte) { + receiverSub, err := receiver.Subscribe() + if err != nil { + t.Fatal(err) + } + + if err := publisher.Publish(ctx, msg, WithReadiness(MinTopicSize(1))); err != nil { + t.Fatal(err) + } + + m, err := receiverSub.Next(ctx) + if err != nil { + t.Fatal(err) + } + + if receivedData := m.GetData(); !bytes.Equal(receivedData, msg) { + t.Fatalf("expected message %v, got %v", msg, receivedData) + } } - } - // Test both directions since PubSub uses one directional streams - testPublish(p1Topic, p2Topic, []byte("test1-to-2")) - testPublish(p1Topic, p2Topic, []byte("test2-to-1")) + // Test both directions since PubSub uses one directional streams + testPublish(p1Topic, p2Topic, []byte("test1-to-2")) + testPublish(p1Topic, p2Topic, []byte("test2-to-1")) + }) } func TestDedupInboundStreams(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) - h1 := hosts[0] - h2 := hosts[1] + hosts := getDefaultHosts(t, 2) + h1 := hosts[0] + h2 := hosts[1] - _, err := NewFloodSub(ctx, h1) - if err != nil { - t.Fatal(err) - } + _, err := NewFloodSub(ctx, h1) + if err != nil { + t.Fatal(err) + } - // Connect the two hosts together - connect(t, h1, h2) + // Connect the two hosts together + connect(t, h1, h2) - // open a few streams and make sure all but the last one get reset - s1, err := h2.NewStream(ctx, h1.ID(), FloodSubID) - if err != nil { - t.Fatal(err) - } - _, err = s1.Read(nil) // force protocol negotiation to complete - if err != nil { - t.Fatal(err) - } - time.Sleep(100 * time.Millisecond) + // open a few streams and make sure all but the last one get reset + s1, err := h2.NewStream(ctx, h1.ID(), FloodSubID) + if err != nil { + t.Fatal(err) + } + _, err = s1.Read(nil) // force protocol negotiation to complete + if err != nil { + t.Fatal(err) + } + time.Sleep(100 * time.Millisecond) - s2, err := h2.NewStream(ctx, h1.ID(), FloodSubID) - if err != nil { - t.Fatal(err) - } - _, err = s2.Read(nil) // force protocol negotiation to complete - if err != nil { - t.Fatal(err) - } - time.Sleep(100 * time.Millisecond) + s2, err := h2.NewStream(ctx, h1.ID(), FloodSubID) + if err != nil { + t.Fatal(err) + } + _, err = s2.Read(nil) // force protocol negotiation to complete + if err != nil { + t.Fatal(err) + } + time.Sleep(100 * time.Millisecond) - s3, err := h2.NewStream(ctx, h1.ID(), FloodSubID) - if err != nil { - t.Fatal(err) - } - _, err = s3.Read(nil) // force protocol negotiation to complete - if err != nil { - t.Fatal(err) - } - time.Sleep(100 * time.Millisecond) + s3, err := h2.NewStream(ctx, h1.ID(), FloodSubID) + if err != nil { + t.Fatal(err) + } + _, err = s3.Read(nil) // force protocol negotiation to complete + if err != nil { + t.Fatal(err) + } + time.Sleep(100 * time.Millisecond) - // check that s1 and s2 have been reset - _, err = s1.Read([]byte{0}) - if err == nil { - t.Fatal("expected s1 to be reset") - } + // check that s1 and s2 have been reset + _, err = s1.Read([]byte{0}) + if err == nil { + t.Fatal("expected s1 to be reset") + } - _, err = s2.Read([]byte{0}) - if err == nil { - t.Fatal("expected s2 to be reset") - } + _, err = s2.Read([]byte{0}) + if err == nil { + t.Fatal("expected s2 to be reset") + } - // check that s3 is readable and simply times out - s3.SetReadDeadline(time.Now().Add(time.Millisecond)) - _, err = s3.Read([]byte{0}) - err2, ok := err.(interface{ Timeout() bool }) - if !ok || !err2.Timeout() { - t.Fatal(err) - } + // check that s3 is readable and simply times out + s3.SetReadDeadline(time.Now().Add(time.Millisecond)) + _, err = s3.Read([]byte{0}) + err2, ok := err.(interface{ Timeout() bool }) + if !ok || !err2.Timeout() { + t.Fatal(err) + } + }) } diff --git a/go.mod b/go.mod index d2cf54ad..270d267e 100644 --- a/go.mod +++ b/go.mod @@ -1,54 +1,43 @@ module github.com/libp2p/go-libp2p-pubsub -go 1.24 +go 1.25 require ( - github.com/benbjohnson/clock v1.3.5 github.com/gogo/protobuf v1.3.2 github.com/libp2p/go-buffer-pool v0.1.0 - github.com/libp2p/go-libp2p v0.39.1 + github.com/libp2p/go-libp2p v0.47.0 github.com/libp2p/go-libp2p-testing v0.12.0 github.com/libp2p/go-msgio v0.3.0 - github.com/multiformats/go-multiaddr v0.14.0 + github.com/marcopolo/simnet v0.0.4 + github.com/multiformats/go-multiaddr v0.16.0 github.com/multiformats/go-varint v0.0.7 ) require ( + github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/cgroups v1.1.0 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/elastic/gosigar v0.14.3 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dunglas/httpsfv v1.1.0 // indirect github.com/flynn/noise v1.1.0 // indirect - github.com/francoispqt/gojay v1.2.13 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/google/gopacket v1.1.19 // indirect - github.com/google/pprof v0.0.0-20250202011525-fc3143867406 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/ipfs/go-cid v0.5.0 // indirect - github.com/ipfs/go-log/v2 v2.5.1 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect - github.com/koron/go-ssdp v0.0.5 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/koron/go-ssdp v0.0.6 // indirect github.com/libp2p/go-flow-metrics v0.2.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-nat v0.2.0 // indirect - github.com/libp2p/go-netroute v0.2.2 // indirect + github.com/libp2p/go-netroute v0.3.0 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect - github.com/libp2p/go-yamux/v4 v4.0.2 // indirect + github.com/libp2p/go-yamux/v5 v5.0.1 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/miekg/dns v1.1.63 // indirect + github.com/miekg/dns v1.1.66 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/sha256-simd v1.0.1 // indirect @@ -58,62 +47,56 @@ require ( github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect - github.com/multiformats/go-multicodec v0.9.0 // indirect + github.com/multiformats/go-multicodec v0.9.1 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-multistream v0.6.0 // indirect + github.com/multiformats/go-multistream v0.6.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.22.2 // indirect - github.com/opencontainers/runtime-spec v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/dtls/v3 v3.0.4 // indirect - github.com/pion/ice/v2 v2.3.37 // indirect - github.com/pion/ice/v4 v4.0.6 // indirect - github.com/pion/interceptor v0.1.37 // indirect + github.com/pion/dtls/v3 v3.0.6 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect github.com/pion/logging v0.2.3 // indirect - github.com/pion/mdns v0.0.12 // indirect github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/rtcp v1.2.15 // indirect - github.com/pion/rtp v1.8.11 // indirect - github.com/pion/sctp v1.8.35 // indirect - github.com/pion/sdp/v3 v3.0.10 // indirect - github.com/pion/srtp/v3 v3.0.4 // indirect + github.com/pion/rtp v1.8.19 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.13 // indirect + github.com/pion/srtp/v3 v3.0.6 // indirect github.com/pion/stun v0.6.1 // indirect github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect - github.com/pion/turn/v2 v2.1.6 // indirect - github.com/pion/turn/v4 v4.0.0 // indirect - github.com/pion/webrtc/v4 v4.0.8 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pion/turn/v4 v4.0.2 // indirect + github.com/pion/webrtc/v4 v4.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.20.5 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.49.0 // indirect - github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect - github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/quic-go/webtransport-go v0.10.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/stretchr/testify v1.10.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/wlynxg/anet v0.0.5 // indirect - go.uber.org/dig v1.18.0 // indirect - go.uber.org/fx v1.23.0 // indirect - go.uber.org/mock v0.5.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/fx v1.24.0 // indirect + go.uber.org/mock v0.5.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect - golang.org/x/tools v0.29.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.36.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/blake3 v1.3.0 // indirect + lukechampine.com/blake3 v1.4.1 // indirect ) diff --git a/go.sum b/go.sum index 01c2c04a..8add21d6 100644 --- a/go.sum +++ b/go.sum @@ -1,132 +1,50 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= -dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= -dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= -dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= -dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= -github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= -github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= -github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= -github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= -github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= -github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= -github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= -github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20250202011525-fc3143867406 h1:wlQI2cYY0BsWmmPPAnxfQ8SDW0S3Jasn+4B8kXFxprg= -github.com/google/pprof v0.0.0-20250202011525-fc3143867406/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= -github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= -github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= -github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= -github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk= -github.com/koron/go-ssdp v0.0.5/go.mod h1:Qm59B7hpKpDqfyRNWRNr00jGwLdXjDyZh6y7rH6VS0w= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= +github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -134,33 +52,26 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= -github.com/libp2p/go-libp2p v0.39.1 h1:1Ur6rPCf3GR+g8jkrnaQaM0ha2IGespsnNlCqJLLALE= -github.com/libp2p/go-libp2p v0.39.1/go.mod h1:3zicI8Lp7Isun+Afo/JOACUbbJqqR2owK6RQWFsVAbI= +github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0yc= +github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= -github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk= -github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk= -github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8= -github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE= +github.com/libp2p/go-netroute v0.3.0 h1:nqPCXHmeNmgTJnktosJ/sIef9hvwYCrsLxXmfNks/oc= +github.com/libp2p/go-netroute v0.3.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= -github.com/libp2p/go-yamux/v4 v4.0.2 h1:nrLh89LN/LEiqcFiqdKDRHjGstN300C1269K/EX0CPU= -github.com/libp2p/go-yamux/v4 v4.0.2/go.mod h1:C808cCRgOs1iBwY4S71T5oxgMxgLmqUw56qh4AeBW2o= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= +github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= +github.com/marcopolo/simnet v0.0.4 h1:50Kx4hS9kFGSRIbrt9xUS3NJX33EyPqHVmpXvaKLqrY= +github.com/marcopolo/simnet v0.0.4/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= +github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= @@ -171,8 +82,6 @@ github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8Rv github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= @@ -181,35 +90,25 @@ github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYg github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= -github.com/multiformats/go-multiaddr v0.14.0 h1:bfrHrJhrRuh/NXH5mCnemjpbGjzRw/b+tJFOD41g2tU= -github.com/multiformats/go-multiaddr v0.14.0/go.mod h1:6EkVAxtznq2yC3QT5CM1UTAwG0GTP3EWAIcjHuzQ+r4= +github.com/multiformats/go-multiaddr v0.16.0 h1:oGWEVKioVQcdIOBlYM8BH1rZDWOGJSqr9/BKl6zQ4qc= +github.com/multiformats/go-multiaddr v0.16.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= -github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= -github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo= +github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo= github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= -github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= +github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= -github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= -github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= -github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= @@ -217,33 +116,29 @@ github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oL github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U= -github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg= -github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0= -github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= -github.com/pion/ice/v4 v4.0.6 h1:jmM9HwI9lfetQV/39uD0nY4y++XZNPhvzIPCb8EwxUM= -github.com/pion/ice/v4 v4.0.6/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= -github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= -github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= +github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= +github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= -github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= -github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= -github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk= -github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= -github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA= -github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg= -github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA= -github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= -github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= -github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= +github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= +github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= +github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= @@ -252,120 +147,60 @@ github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1A github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= -github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= -github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= -github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= -github.com/pion/webrtc/v4 v4.0.8 h1:T1ZmnT9qxIJIt4d8XoiMOBrTClGHDDXNg9e/fh018Qc= -github.com/pion/webrtc/v4 v4.0.8/go.mod h1:HHBeUVBAC+j4ZFnYhovEFStF02Arb1EyD4G7e7HBTJw= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= +github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94= -github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s= -github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= -github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= -github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= -github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= +github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= -github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= -github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= -github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= -github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= -github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= -github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= -github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= -github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= -github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= -github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= -github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= -github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= -github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= -github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= -github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= -github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= -github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= -go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= -go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= -golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -375,92 +210,53 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= -golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -469,75 +265,35 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= -golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= -google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= -lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= -sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= diff --git a/gossip_tracer_test.go b/gossip_tracer_test.go index bed327cc..58def1d9 100644 --- a/gossip_tracer_test.go +++ b/gossip_tracer_test.go @@ -11,93 +11,99 @@ import ( func TestBrokenPromises(t *testing.T) { // tests that unfullfilled promises are tracked correctly - gt := newGossipTracer() - gt.followUpTime = 100 * time.Millisecond - - peerA := peer.ID("A") - peerB := peer.ID("B") - peerC := peer.ID("C") - - var mids []string - for i := 0; i < 100; i++ { - m := makeTestMessage(i) - m.From = []byte(peerA) - mid := DefaultMsgIdFn(m) - mids = append(mids, mid) - } - - gt.AddPromise(peerA, mids) - gt.AddPromise(peerB, mids) - gt.AddPromise(peerC, mids) - - // no broken promises yet - brokenPromises := gt.GetBrokenPromises() - if brokenPromises != nil { - t.Fatal("expected no broken promises") - } - - // throttle one of the peers to save his promises - gt.ThrottlePeer(peerC) - - // make promises break - time.Sleep(gt.followUpTime + time.Millisecond) - - brokenPromises = gt.GetBrokenPromises() - if len(brokenPromises) != 2 { - t.Fatalf("expected 2 broken prmises, got %d", len(brokenPromises)) - } - - brokenPromisesA := brokenPromises[peerA] - if brokenPromisesA != 1 { - t.Fatalf("expected 1 broken promise from A, got %d", brokenPromisesA) - } - - brokenPromisesB := brokenPromises[peerB] - if brokenPromisesB != 1 { - t.Fatalf("expected 1 broken promise from A, got %d", brokenPromisesB) - } - - // verify that the peerPromises map has been vacated - if len(gt.peerPromises) != 0 { - t.Fatal("expected empty peerPromises map") - } + synctestTest(t, func(t *testing.T) { + + gt := newGossipTracer() + gt.followUpTime = 100 * time.Millisecond + + peerA := peer.ID("A") + peerB := peer.ID("B") + peerC := peer.ID("C") + + var mids []string + for i := 0; i < 100; i++ { + m := makeTestMessage(i) + m.From = []byte(peerA) + mid := DefaultMsgIdFn(m) + mids = append(mids, mid) + } + + gt.AddPromise(peerA, mids) + gt.AddPromise(peerB, mids) + gt.AddPromise(peerC, mids) + + // no broken promises yet + brokenPromises := gt.GetBrokenPromises() + if brokenPromises != nil { + t.Fatal("expected no broken promises") + } + + // throttle one of the peers to save his promises + gt.ThrottlePeer(peerC) + + // make promises break + time.Sleep(gt.followUpTime + time.Millisecond) + + brokenPromises = gt.GetBrokenPromises() + if len(brokenPromises) != 2 { + t.Fatalf("expected 2 broken prmises, got %d", len(brokenPromises)) + } + + brokenPromisesA := brokenPromises[peerA] + if brokenPromisesA != 1 { + t.Fatalf("expected 1 broken promise from A, got %d", brokenPromisesA) + } + + brokenPromisesB := brokenPromises[peerB] + if brokenPromisesB != 1 { + t.Fatalf("expected 1 broken promise from A, got %d", brokenPromisesB) + } + + // verify that the peerPromises map has been vacated + if len(gt.peerPromises) != 0 { + t.Fatal("expected empty peerPromises map") + } + }) } func TestNoBrokenPromises(t *testing.T) { // like above, but this time we deliver messages to fullfil the promises - gt := newGossipTracer() - gt.followUpTime = 100 * time.Millisecond - - peerA := peer.ID("A") - peerB := peer.ID("B") - - var msgs []*pb.Message - var mids []string - for i := 0; i < 100; i++ { - m := makeTestMessage(i) - m.From = []byte(peerA) - msgs = append(msgs, m) - mid := DefaultMsgIdFn(m) - mids = append(mids, mid) - } - - gt.AddPromise(peerA, mids) - gt.AddPromise(peerB, mids) - - for _, m := range msgs { - gt.DeliverMessage(&Message{Message: m}) - } - - time.Sleep(gt.followUpTime + time.Millisecond) - - // there should be no broken promises - brokenPromises := gt.GetBrokenPromises() - if brokenPromises != nil { - t.Fatal("expected no broken promises") - } - - // verify that the peerPromises map has been vacated - if len(gt.peerPromises) != 0 { - t.Fatal("expected empty peerPromises map") - } + synctestTest(t, func(t *testing.T) { + + gt := newGossipTracer() + gt.followUpTime = 100 * time.Millisecond + + peerA := peer.ID("A") + peerB := peer.ID("B") + + var msgs []*pb.Message + var mids []string + for i := 0; i < 100; i++ { + m := makeTestMessage(i) + m.From = []byte(peerA) + msgs = append(msgs, m) + mid := DefaultMsgIdFn(m) + mids = append(mids, mid) + } + + gt.AddPromise(peerA, mids) + gt.AddPromise(peerB, mids) + + for _, m := range msgs { + gt.DeliverMessage(&Message{Message: m}) + } + + time.Sleep(gt.followUpTime + time.Millisecond) + + // there should be no broken promises + brokenPromises := gt.GetBrokenPromises() + if brokenPromises != nil { + t.Fatal("expected no broken promises") + } + + // verify that the peerPromises map has been vacated + if len(gt.peerPromises) != 0 { + t.Fatal("expected empty peerPromises map") + } + }) } diff --git a/gossipsub.go b/gossipsub.go index 925e274c..a6641533 100644 --- a/gossipsub.go +++ b/gossipsub.go @@ -1592,7 +1592,11 @@ func (gs *GossipSubRouter) doSendRPC(rpc *RPC, p peer.ID, q *rpcQueue, urgent bo } func (gs *GossipSubRouter) heartbeatTimer() { - time.Sleep(gs.params.HeartbeatInitialDelay) + select { + case <-time.After(gs.params.HeartbeatInitialDelay): + case <-gs.p.ctx.Done(): + return + } select { case gs.p.eval <- gs.heartbeat: case <-gs.p.ctx.Done(): diff --git a/gossipsub_connmgr_test.go b/gossipsub_connmgr_test.go index 10650a0e..b9f7bb85 100644 --- a/gossipsub_connmgr_test.go +++ b/gossipsub_connmgr_test.go @@ -5,161 +5,179 @@ import ( "testing" "time" - "github.com/benbjohnson/clock" - "github.com/libp2p/go-libp2p/core/host" - - "github.com/libp2p/go-libp2p" - "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/p2p/net/connmgr" + "github.com/libp2p/go-libp2p/x/simlibp2p" + "github.com/marcopolo/simnet" ) func TestGossipsubConnTagMessageDeliveries(t *testing.T) { - t.Skip("flaky test disabled") - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - oldGossipSubD := GossipSubD - oldGossipSubDlo := GossipSubDlo - oldGossipSubDHi := GossipSubDhi - oldGossipSubConnTagDecayInterval := GossipSubConnTagDecayInterval - oldGossipSubConnTagMessageDeliveryCap := GossipSubConnTagMessageDeliveryCap - - // set the gossipsub D parameters low, so that we have some peers outside the mesh - GossipSubDlo = 3 - GossipSubD = 3 - GossipSubDhi = 3 - // also set the tag decay interval so we don't have to wait forever for tests - GossipSubConnTagDecayInterval = time.Second - - // set the cap for deliveries above GossipSubConnTagValueMeshPeer, so the sybils - // will be forced out even if they end up in someone's mesh - GossipSubConnTagMessageDeliveryCap = 50 - - // reset globals after test - defer func() { - GossipSubD = oldGossipSubD - GossipSubDlo = oldGossipSubDlo - GossipSubDhi = oldGossipSubDHi - GossipSubConnTagDecayInterval = oldGossipSubConnTagDecayInterval - GossipSubConnTagMessageDeliveryCap = oldGossipSubConnTagMessageDeliveryCap - }() - - decayClock := clock.NewMock() - decayCfg := connmgr.DecayerCfg{ - Resolution: time.Second, - Clock: decayClock, - } - - nHonest := 5 - nSquatter := 10 - connLimit := 10 - - connmgrs := make([]*connmgr.BasicConnMgr, nHonest) - honestHosts := make([]host.Host, nHonest) - honestPeers := make(map[peer.ID]struct{}) - - for i := 0; i < nHonest; i++ { - var err error - connmgrs[i], err = connmgr.NewConnManager(nHonest, connLimit, - connmgr.WithGracePeriod(0), - connmgr.WithSilencePeriod(time.Millisecond), - connmgr.DecayerConfig(&decayCfg), - ) - if err != nil { - t.Fatal(err) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + oldGossipSubD := GossipSubD + oldGossipSubDlo := GossipSubDlo + oldGossipSubDHi := GossipSubDhi + oldGossipSubDscore := GossipSubDscore + oldGossipSubConnTagDecayInterval := GossipSubConnTagDecayInterval + oldGossipSubConnTagMessageDeliveryCap := GossipSubConnTagMessageDeliveryCap + + // set the gossipsub D parameters low, so that we have some peers outside the mesh + GossipSubDlo = 3 + GossipSubD = 3 + GossipSubDhi = 3 + GossipSubDscore = 3 + // also set the tag decay interval so we don't have to wait forever for tests + GossipSubConnTagDecayInterval = time.Second + + // set the cap for deliveries above GossipSubConnTagValueMeshPeer, so the sybils + // will be forced out even if they end up in someone's mesh + GossipSubConnTagMessageDeliveryCap = 50 + + // reset globals after test + defer func() { + GossipSubD = oldGossipSubD + GossipSubDlo = oldGossipSubDlo + GossipSubDhi = oldGossipSubDHi + GossipSubDscore = oldGossipSubDscore + GossipSubConnTagDecayInterval = oldGossipSubConnTagDecayInterval + GossipSubConnTagMessageDeliveryCap = oldGossipSubConnTagMessageDeliveryCap + }() + + nHonest := 5 + nSquatter := 10 + connLimit := 10 + + connmgrs := make([]*connmgr.BasicConnMgr, nHonest) + for i := 0; i < nHonest; i++ { + var err error + connmgrs[i], err = connmgr.NewConnManager(nHonest, connLimit, + connmgr.WithGracePeriod(0), + connmgr.WithSilencePeriod(time.Millisecond), + ) + if err != nil { + t.Fatal(err) + } } - h, err := libp2p.New( - libp2p.ResourceManager(&network.NullResourceManager{}), - libp2p.ConnectionManager(connmgrs[i]), + totalNodes := nHonest + nSquatter + net, meta, err := simlibp2p.SimpleLibp2pNetwork( + []simlibp2p.NodeLinkSettingsAndCount{{ + LinkSettings: simnet.NodeBiDiLinkSettings{ + Downlink: simnet.LinkSettings{BitsPerSecond: 20 * simlibp2p.OneMbps}, + Uplink: simnet.LinkSettings{BitsPerSecond: 20 * simlibp2p.OneMbps}, + }, + Count: totalNodes, + }}, + simnet.StaticLatency(time.Millisecond), + simlibp2p.NetworkSettings{ + UseBlankHost: true, + BlankHostOptsForHostIdx: func(idx int) simlibp2p.BlankHostOpts { + if idx < nHonest { + return simlibp2p.BlankHostOpts{ConnMgr: connmgrs[idx]} + } + return simlibp2p.BlankHostOpts{} + }, + }, ) if err != nil { t.Fatal(err) } - t.Cleanup(func() { h.Close() }) - honestHosts[i] = h - honestPeers[h.ID()] = struct{}{} - } - - // use flood publishing, so non-mesh peers will still be delivering messages - // to everyone - psubs := getGossipsubs(ctx, honestHosts, - WithFloodPublish(true)) - - // sybil squatters to be connected later - sybilHosts := getDefaultHosts(t, nSquatter) - for _, h := range sybilHosts { - squatter := &sybilSquatter{h: h, ignoreErrors: true} - h.SetStreamHandler(GossipSubID_v10, squatter.handleStream) - } - - // connect the honest hosts - connectAll(t, honestHosts) - - for _, h := range honestHosts { - if len(h.Network().Peers()) != nHonest-1 { - t.Errorf("expected to have conns to all honest peers, have %d", len(h.Network().Conns())) - } - } + net.Start() + t.Cleanup(func() { + for _, h := range meta.Nodes { + h.Close() + } + net.Close() + }) - // subscribe everyone to the topic - topic := "test" - for _, ps := range psubs { - _, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + honestHosts := meta.Nodes[:nHonest] + sybilHosts := meta.Nodes[nHonest:] + + honestPeers := make(map[peer.ID]struct{}) + for _, h := range honestHosts { + honestPeers[h.ID()] = struct{}{} } - } - // sleep to allow meshes to form - time.Sleep(2 * time.Second) + // use flood publishing, so non-mesh peers will still be delivering messages + // to everyone + psubs := getGossipsubs(ctx, honestHosts, + WithFloodPublish(true)) - // have all the hosts publish enough messages to ensure that they get some delivery credit - nMessages := GossipSubConnTagMessageDeliveryCap * 2 - for _, ps := range psubs { - for i := 0; i < nMessages; i++ { - ps.Publish(topic, []byte("hello")) + // sybil squatters to be connected later + for _, h := range sybilHosts { + squatter := &sybilSquatter{h: h, ignoreErrors: true} + h.SetStreamHandler(GossipSubID_v10, squatter.handleStream) } - } - // advance the fake time for the tag decay - decayClock.Add(time.Second) + // connect the honest hosts + connectAll(t, honestHosts) - // verify that they've given each other delivery connection tags - tag := "pubsub-deliveries:test" - for _, h := range honestHosts { - for _, h2 := range honestHosts { - if h.ID() == h2.ID() { - continue + for _, h := range honestHosts { + if len(h.Network().Peers()) != nHonest-1 { + t.Errorf("expected to have conns to all honest peers, have %d", len(h.Network().Conns())) } - val := getTagValue(h.ConnManager(), h2.ID(), tag) - if val == 0 { - t.Errorf("Expected non-zero delivery tag value for peer %s", h2.ID()) + } + + // subscribe everyone to the topic + topic := "test" + for _, ps := range psubs { + _, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) } } - } - - // now connect the sybils to put pressure on the real hosts' connection managers - allHosts := append(honestHosts, sybilHosts...) - connectAll(t, allHosts) - - // we should still have conns to all the honest peers, but not the sybils - for _, h := range honestHosts { - nHonestConns := 0 - nDishonestConns := 0 - for _, p := range h.Network().Peers() { - if _, ok := honestPeers[p]; !ok { - nDishonestConns++ - } else { - nHonestConns++ + + // sleep to allow meshes to form + time.Sleep(2 * time.Second) + + // have all the hosts publish enough messages to ensure that they get some delivery credit + nMessages := GossipSubConnTagMessageDeliveryCap * 2 + for _, ps := range psubs { + for i := 0; i < nMessages; i++ { + ps.Publish(topic, []byte("hello")) } } - if nDishonestConns > connLimit-nHonest { - t.Errorf("expected most dishonest conns to be pruned, have %d", nDishonestConns) + + // advance time for the tag decay (under synctest, time.Sleep advances the fake clock) + time.Sleep(time.Second) + + // verify that they've given each other delivery connection tags + tag := "pubsub-deliveries:test" + for _, h := range honestHosts { + for _, h2 := range honestHosts { + if h.ID() == h2.ID() { + continue + } + val := getTagValue(h.ConnManager(), h2.ID(), tag) + if val == 0 { + t.Errorf("Expected non-zero delivery tag value for peer %s", h2.ID()) + } + } } - if nHonestConns != nHonest-1 { - t.Errorf("expected all honest conns to be preserved, have %d", nHonestConns) + + // now connect the sybils to put pressure on the real hosts' connection managers + allHosts := append(honestHosts, sybilHosts...) + connectAll(t, allHosts) + + // we should still have conns to all the honest peers, but not the sybils + for _, h := range honestHosts { + nHonestConns := 0 + nDishonestConns := 0 + for _, p := range h.Network().Peers() { + if _, ok := honestPeers[p]; !ok { + nDishonestConns++ + } else { + nHonestConns++ + } + } + if nDishonestConns > connLimit-nHonest { + t.Errorf("expected most dishonest conns to be pruned, have %d", nDishonestConns) + } + if nHonestConns != nHonest-1 { + t.Errorf("expected all honest conns to be preserved, have %d", nHonestConns) + } } - } + }) } diff --git a/gossipsub_feat_test.go b/gossipsub_feat_test.go index 5f790e2f..29bf9b08 100644 --- a/gossipsub_feat_test.go +++ b/gossipsub_feat_test.go @@ -12,148 +12,152 @@ import ( ) func TestDefaultGossipSubFeatures(t *testing.T) { - if GossipSubDefaultFeatures(GossipSubFeatureMesh, FloodSubID) { - t.Fatal("floodsub should not support Mesh") - } - if !GossipSubDefaultFeatures(GossipSubFeatureMesh, GossipSubID_v10) { - t.Fatal("gossipsub-v1.0 should support Mesh") - } - if !GossipSubDefaultFeatures(GossipSubFeatureMesh, GossipSubID_v11) { - t.Fatal("gossipsub-v1.1 should support Mesh") - } - if !GossipSubDefaultFeatures(GossipSubFeatureMesh, GossipSubID_v12) { - t.Fatal("gossipsub-v1.2 should support Mesh") - } - if !GossipSubDefaultFeatures(GossipSubFeatureMesh, GossipSubID_v13) { - t.Fatal("gossipsub-v1.3 should support Mesh") - } - - if GossipSubDefaultFeatures(GossipSubFeaturePX, FloodSubID) { - t.Fatal("floodsub should not support PX") - } - if GossipSubDefaultFeatures(GossipSubFeaturePX, GossipSubID_v10) { - t.Fatal("gossipsub-v1.0 should not support PX") - } - if !GossipSubDefaultFeatures(GossipSubFeaturePX, GossipSubID_v11) { - t.Fatal("gossipsub-v1.1 should support PX") - } - if !GossipSubDefaultFeatures(GossipSubFeaturePX, GossipSubID_v12) { - t.Fatal("gossipsub-v1.2 should support PX") - } - if !GossipSubDefaultFeatures(GossipSubFeaturePX, GossipSubID_v13) { - t.Fatal("gossipsub-v1.3 should support PX") - } - - if GossipSubDefaultFeatures(GossipSubFeatureIdontwant, FloodSubID) { - t.Fatal("floodsub should not support IDONTWANT") - } - if GossipSubDefaultFeatures(GossipSubFeatureIdontwant, GossipSubID_v10) { - t.Fatal("gossipsub-v1.0 should not support IDONTWANT") - } - if GossipSubDefaultFeatures(GossipSubFeatureIdontwant, GossipSubID_v11) { - t.Fatal("gossipsub-v1.1 should not support IDONTWANT") - } - if !GossipSubDefaultFeatures(GossipSubFeatureIdontwant, GossipSubID_v12) { - t.Fatal("gossipsub-v1.2 should support IDONTWANT") - } - if !GossipSubDefaultFeatures(GossipSubFeatureIdontwant, GossipSubID_v13) { - t.Fatal("gossipsub-v1.3 should support IDONTWANT") - } - - if GossipSubDefaultFeatures(GossipSubFeatureExtensions, FloodSubID) { - t.Fatal("floodsub should not support Extensions") - } - if GossipSubDefaultFeatures(GossipSubFeatureExtensions, GossipSubID_v10) { - t.Fatal("gossipsub-v1.0 should not support Extensions") - } - if GossipSubDefaultFeatures(GossipSubFeatureExtensions, GossipSubID_v11) { - t.Fatal("gossipsub-v1.1 should not support Extensions") - } - if GossipSubDefaultFeatures(GossipSubFeatureExtensions, GossipSubID_v12) { - t.Fatal("gossipsub-v1.2 should not support Extensions") - } - if !GossipSubDefaultFeatures(GossipSubFeatureExtensions, GossipSubID_v13) { - t.Fatal("gossipsub-v1.3 should support Extensions") - } + synctestTest(t, func(t *testing.T) { + if GossipSubDefaultFeatures(GossipSubFeatureMesh, FloodSubID) { + t.Fatal("floodsub should not support Mesh") + } + if !GossipSubDefaultFeatures(GossipSubFeatureMesh, GossipSubID_v10) { + t.Fatal("gossipsub-v1.0 should support Mesh") + } + if !GossipSubDefaultFeatures(GossipSubFeatureMesh, GossipSubID_v11) { + t.Fatal("gossipsub-v1.1 should support Mesh") + } + if !GossipSubDefaultFeatures(GossipSubFeatureMesh, GossipSubID_v12) { + t.Fatal("gossipsub-v1.2 should support Mesh") + } + if !GossipSubDefaultFeatures(GossipSubFeatureMesh, GossipSubID_v13) { + t.Fatal("gossipsub-v1.3 should support Mesh") + } + + if GossipSubDefaultFeatures(GossipSubFeaturePX, FloodSubID) { + t.Fatal("floodsub should not support PX") + } + if GossipSubDefaultFeatures(GossipSubFeaturePX, GossipSubID_v10) { + t.Fatal("gossipsub-v1.0 should not support PX") + } + if !GossipSubDefaultFeatures(GossipSubFeaturePX, GossipSubID_v11) { + t.Fatal("gossipsub-v1.1 should support PX") + } + if !GossipSubDefaultFeatures(GossipSubFeaturePX, GossipSubID_v12) { + t.Fatal("gossipsub-v1.2 should support PX") + } + if !GossipSubDefaultFeatures(GossipSubFeaturePX, GossipSubID_v13) { + t.Fatal("gossipsub-v1.3 should support PX") + } + + if GossipSubDefaultFeatures(GossipSubFeatureIdontwant, FloodSubID) { + t.Fatal("floodsub should not support IDONTWANT") + } + if GossipSubDefaultFeatures(GossipSubFeatureIdontwant, GossipSubID_v10) { + t.Fatal("gossipsub-v1.0 should not support IDONTWANT") + } + if GossipSubDefaultFeatures(GossipSubFeatureIdontwant, GossipSubID_v11) { + t.Fatal("gossipsub-v1.1 should not support IDONTWANT") + } + if !GossipSubDefaultFeatures(GossipSubFeatureIdontwant, GossipSubID_v12) { + t.Fatal("gossipsub-v1.2 should support IDONTWANT") + } + if !GossipSubDefaultFeatures(GossipSubFeatureIdontwant, GossipSubID_v13) { + t.Fatal("gossipsub-v1.3 should support IDONTWANT") + } + + if GossipSubDefaultFeatures(GossipSubFeatureExtensions, FloodSubID) { + t.Fatal("floodsub should not support Extensions") + } + if GossipSubDefaultFeatures(GossipSubFeatureExtensions, GossipSubID_v10) { + t.Fatal("gossipsub-v1.0 should not support Extensions") + } + if GossipSubDefaultFeatures(GossipSubFeatureExtensions, GossipSubID_v11) { + t.Fatal("gossipsub-v1.1 should not support Extensions") + } + if GossipSubDefaultFeatures(GossipSubFeatureExtensions, GossipSubID_v12) { + t.Fatal("gossipsub-v1.2 should not support Extensions") + } + if !GossipSubDefaultFeatures(GossipSubFeatureExtensions, GossipSubID_v13) { + t.Fatal("gossipsub-v1.3 should support Extensions") + } + }) } func TestGossipSubCustomProtocols(t *testing.T) { - customsub := protocol.ID("customsub/1.0.0") - protos := []protocol.ID{customsub, FloodSubID} - features := func(feat GossipSubFeature, proto protocol.ID) bool { - return proto == customsub - } + synctestTest(t, func(t *testing.T) { + customsub := protocol.ID("customsub/1.0.0") + protos := []protocol.ID{customsub, FloodSubID} + features := func(feat GossipSubFeature, proto protocol.ID) bool { + return proto == customsub + } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 3) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 3) - gsubs := getGossipsubs(ctx, hosts[:2], WithGossipSubProtocols(protos, features)) - fsub := getPubsub(ctx, hosts[2]) - psubs := append(gsubs, fsub) + gsubs := getGossipsubs(ctx, hosts[:2], WithGossipSubProtocols(protos, features)) + fsub := getPubsub(ctx, hosts[2]) + psubs := append(gsubs, fsub) - connectAll(t, hosts) + connectAll(t, hosts) - topic := "test" - var subs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) - } + topic := "test" + var subs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } - subs = append(subs, subch) - } + subs = append(subs, subch) + } - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - // check the meshes of the gsubs, the gossipsub meshes should include each other but not the - // floddsub peer - gsubs[0].eval <- func() { - gs := gsubs[0].rt.(*GossipSubRouter) + // check the meshes of the gsubs, the gossipsub meshes should include each other but not the + // floddsub peer + gsubs[0].eval <- func() { + gs := gsubs[0].rt.(*GossipSubRouter) - _, ok := gs.mesh[topic][hosts[1].ID()] - if !ok { - t.Fatal("expected gs0 to have gs1 in its mesh") - } + _, ok := gs.mesh[topic][hosts[1].ID()] + if !ok { + t.Fatal("expected gs0 to have gs1 in its mesh") + } - _, ok = gs.mesh[topic][hosts[2].ID()] - if ok { - t.Fatal("expected gs0 to not have fs in its mesh") + _, ok = gs.mesh[topic][hosts[2].ID()] + if ok { + t.Fatal("expected gs0 to not have fs in its mesh") + } } - } - gsubs[1].eval <- func() { - gs := gsubs[1].rt.(*GossipSubRouter) + gsubs[1].eval <- func() { + gs := gsubs[1].rt.(*GossipSubRouter) - _, ok := gs.mesh[topic][hosts[0].ID()] - if !ok { - t.Fatal("expected gs1 to have gs0 in its mesh") - } + _, ok := gs.mesh[topic][hosts[0].ID()] + if !ok { + t.Fatal("expected gs1 to have gs0 in its mesh") + } - _, ok = gs.mesh[topic][hosts[2].ID()] - if ok { - t.Fatal("expected gs1 to not have fs in its mesh") + _, ok = gs.mesh[topic][hosts[2].ID()] + if ok { + t.Fatal("expected gs1 to not have fs in its mesh") + } } - } - // send some messages - for i := 0; i < 10; i++ { - msg := []byte(fmt.Sprintf("%d it's not quite a floooooood %d", i, i)) + // send some messages + for i := 0; i < 10; i++ { + msg := []byte(fmt.Sprintf("%d it's not quite a floooooood %d", i, i)) - owner := rand.Intn(len(psubs)) + owner := rand.Intn(len(psubs)) - psubs[owner].Publish(topic, msg) + psubs[owner].Publish(topic, msg) - for _, sub := range subs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range subs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } diff --git a/gossipsub_matchfn_test.go b/gossipsub_matchfn_test.go index 4d688d25..680f98a7 100644 --- a/gossipsub_matchfn_test.go +++ b/gossipsub_matchfn_test.go @@ -10,69 +10,71 @@ import ( ) func TestGossipSubMatchingFn(t *testing.T) { - customsubA100 := protocol.ID("/customsub_a/1.0.0") - customsubA101Beta := protocol.ID("/customsub_a/1.0.1-beta") - customsubB100 := protocol.ID("/customsub_b/1.0.0") + synctestTest(t, func(t *testing.T) { + customsubA100 := protocol.ID("/customsub_a/1.0.0") + customsubA101Beta := protocol.ID("/customsub_a/1.0.1-beta") + customsubB100 := protocol.ID("/customsub_b/1.0.0") - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - h := getDefaultHosts(t, 4) - psubs := []*PubSub{ - getGossipsub(ctx, h[0], WithProtocolMatchFn(protocolNameMatch), WithGossipSubProtocols([]protocol.ID{customsubA100, GossipSubID_v11}, GossipSubDefaultFeatures)), - getGossipsub(ctx, h[1], WithProtocolMatchFn(protocolNameMatch), WithGossipSubProtocols([]protocol.ID{customsubA101Beta}, GossipSubDefaultFeatures)), - getGossipsub(ctx, h[2], WithProtocolMatchFn(protocolNameMatch), WithGossipSubProtocols([]protocol.ID{GossipSubID_v11}, GossipSubDefaultFeatures)), - getGossipsub(ctx, h[3], WithProtocolMatchFn(protocolNameMatch), WithGossipSubProtocols([]protocol.ID{customsubB100}, GossipSubDefaultFeatures)), - } + h := getDefaultHosts(t, 4) + psubs := []*PubSub{ + getGossipsub(ctx, h[0], WithProtocolMatchFn(protocolNameMatch), WithGossipSubProtocols([]protocol.ID{customsubA100, GossipSubID_v11}, GossipSubDefaultFeatures)), + getGossipsub(ctx, h[1], WithProtocolMatchFn(protocolNameMatch), WithGossipSubProtocols([]protocol.ID{customsubA101Beta}, GossipSubDefaultFeatures)), + getGossipsub(ctx, h[2], WithProtocolMatchFn(protocolNameMatch), WithGossipSubProtocols([]protocol.ID{GossipSubID_v11}, GossipSubDefaultFeatures)), + getGossipsub(ctx, h[3], WithProtocolMatchFn(protocolNameMatch), WithGossipSubProtocols([]protocol.ID{customsubB100}, GossipSubDefaultFeatures)), + } - connect(t, h[0], h[1]) - connect(t, h[0], h[2]) - connect(t, h[0], h[3]) + connect(t, h[0], h[1]) + connect(t, h[0], h[2]) + connect(t, h[0], h[3]) - // verify that the peers are connected - time.Sleep(2 * time.Second) - for i := 1; i < len(h); i++ { - if len(h[0].Network().ConnsToPeer(h[i].ID())) == 0 { - t.Fatal("expected a connection between peers") + // verify that the peers are connected + time.Sleep(2 * time.Second) + for i := 1; i < len(h); i++ { + if len(h[0].Network().ConnsToPeer(h[i].ID())) == 0 { + t.Fatal("expected a connection between peers") + } } - } - // build the mesh - var subs []*Subscription - for _, ps := range psubs { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + // build the mesh + var subs []*Subscription + for _, ps := range psubs { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } + subs = append(subs, sub) } - subs = append(subs, sub) - } - time.Sleep(time.Second) + time.Sleep(time.Second) - // publish a message - msg := []byte("message") - psubs[0].Publish("test", msg) + // publish a message + msg := []byte("message") + psubs[0].Publish("test", msg) - assertReceive(t, subs[0], msg) - assertReceive(t, subs[1], msg) // Should match via semver over CustomSub name, ignoring the version - assertReceive(t, subs[2], msg) // Should match via GossipSubID_v11 + assertReceive(t, subs[0], msg) + assertReceive(t, subs[1], msg) // Should match via semver over CustomSub name, ignoring the version + assertReceive(t, subs[2], msg) // Should match via GossipSubID_v11 - // No message should be received because customsubA and customsubB have different names - ctxTimeout, timeoutCancel := context.WithTimeout(context.Background(), 1*time.Second) - defer timeoutCancel() - received := false - for { - msg, err := subs[3].Next(ctxTimeout) - if err != nil { - break + // No message should be received because customsubA and customsubB have different names + ctxTimeout, timeoutCancel := context.WithTimeout(context.Background(), 1*time.Second) + defer timeoutCancel() + received := false + for { + msg, err := subs[3].Next(ctxTimeout) + if err != nil { + break + } + if msg != nil { + received = true + } } - if msg != nil { - received = true + if received { + t.Fatal("Should not have received a message") } - } - if received { - t.Fatal("Should not have received a message") - } + }) } func protocolNameMatch(base protocol.ID) func(protocol.ID) bool { diff --git a/gossipsub_spam_test.go b/gossipsub_spam_test.go index e42b8859..bd446678 100644 --- a/gossipsub_spam_test.go +++ b/gossipsub_spam_test.go @@ -24,584 +24,592 @@ import ( // Test that when Gossipsub receives too many IWANT messages from a peer // for the same message ID, it cuts off the peer func TestGossipsubAttackSpamIWANT(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Create legitimate and attacker hosts - hosts := getDefaultHosts(t, 2) - legit := hosts[0] - attacker := hosts[1] - - // Set up gossipsub on the legit host - ps, err := NewGossipSub(ctx, legit) - if err != nil { - t.Fatal(err) - } - - // Subscribe to mytopic on the legit host - mytopic := "mytopic" - _, err = ps.Subscribe(mytopic) - if err != nil { - t.Fatal(err) - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - // Used to publish a message with random data - publishMsg := func() { - data := make([]byte, 16) - rand.Read(data) + // Create legitimate and attacker hosts + hosts := getDefaultHosts(t, 2) + legit := hosts[0] + attacker := hosts[1] - if err = ps.Publish(mytopic, data); err != nil { + // Set up gossipsub on the legit host + ps, err := NewGossipSub(ctx, legit) + if err != nil { t.Fatal(err) } - } - // Wait a bit after the last message before checking we got the - // right number of messages - msgWaitMax := time.Second - msgCount := 0 - msgTimer := time.NewTimer(msgWaitMax) - - // Checks we received the right number of messages - checkMsgCount := func() { - // After the original message from the legit host, we keep sending - // IWANT until it stops replying. So the number of messages is - // + GossipSubGossipRetransmission - exp := 1 + GossipSubGossipRetransmission - if msgCount != exp { - t.Fatalf("Expected %d messages, got %d", exp, msgCount) + // Subscribe to mytopic on the legit host + mytopic := "mytopic" + _, err = ps.Subscribe(mytopic) + if err != nil { + t.Fatal(err) } - } - // Wait for the timer to expire - go func() { - select { - case <-msgTimer.C: - checkMsgCount() - cancel() - return - case <-ctx.Done(): - checkMsgCount() - } - }() - - newMockGS(ctx, t, attacker, func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - // When the legit host connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and grafting to the peer - writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, - }) - - go func() { - // Wait for a short interval to make sure the legit host - // received and processed the subscribe + graft - time.Sleep(100 * time.Millisecond) - - // Publish a message from the legit host - publishMsg() - }() + // Used to publish a message with random data + publishMsg := func() { + data := make([]byte, 16) + rand.Read(data) + + if err = ps.Publish(mytopic, data); err != nil { + t.Fatal(err) } } - // Each time the legit host sends a message - for _, msg := range irpc.GetPublish() { - // Increment the number of messages and reset the timer - msgCount++ - msgTimer.Reset(msgWaitMax) - - // Shouldn't get more than the expected number of messages + // Wait a bit after the last message before checking we got the + // right number of messages + msgWaitMax := time.Second + msgCount := 0 + msgTimer := time.NewTimer(msgWaitMax) + + // Checks we received the right number of messages + checkMsgCount := func() { + // After the original message from the legit host, we keep sending + // IWANT until it stops replying. So the number of messages is + // + GossipSubGossipRetransmission exp := 1 + GossipSubGossipRetransmission - if msgCount > exp { + if msgCount != exp { + t.Fatalf("Expected %d messages, got %d", exp, msgCount) + } + } + + // Wait for the timer to expire + go func() { + select { + case <-msgTimer.C: + checkMsgCount() cancel() - t.Fatal("Received too many responses") + return + case <-ctx.Done(): + checkMsgCount() } + }() - // Send an IWANT with the message ID, causing the legit host - // to send another message (until it cuts off the attacker for - // being spammy) - iwantlst := []string{DefaultMsgIdFn(msg)} - iwant := []*pb.ControlIWant{{MessageIDs: iwantlst}} - orpc := rpcWithControl(nil, nil, iwant, nil, nil, nil) - writeMsg(&orpc.RPC) - } - }) + newMockGS(ctx, t, attacker, func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + // When the legit host connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and grafting to the peer + writeMsg(&pb.RPC{ + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, + }) + + go func() { + // Wait for a short interval to make sure the legit host + // received and processed the subscribe + graft + time.Sleep(100 * time.Millisecond) - connect(t, hosts[0], hosts[1]) + // Publish a message from the legit host + publishMsg() + }() + } + } + + // Each time the legit host sends a message + for _, msg := range irpc.GetPublish() { + // Increment the number of messages and reset the timer + msgCount++ + msgTimer.Reset(msgWaitMax) - <-ctx.Done() + // Shouldn't get more than the expected number of messages + exp := 1 + GossipSubGossipRetransmission + if msgCount > exp { + cancel() + t.Fatal("Received too many responses") + } + + // Send an IWANT with the message ID, causing the legit host + // to send another message (until it cuts off the attacker for + // being spammy) + iwantlst := []string{DefaultMsgIdFn(msg)} + iwant := []*pb.ControlIWant{{MessageIDs: iwantlst}} + orpc := rpcWithControl(nil, nil, iwant, nil, nil, nil) + writeMsg(&orpc.RPC) + } + }) + + connect(t, hosts[0], hosts[1]) + + <-ctx.Done() + }) } // Test that Gossipsub only responds to IHAVE with IWANT once per heartbeat func TestGossipsubAttackSpamIHAVE(t *testing.T) { - originalGossipSubIWantFollowupTime := GossipSubIWantFollowupTime - GossipSubIWantFollowupTime = 10 * time.Second - defer func() { - GossipSubIWantFollowupTime = originalGossipSubIWantFollowupTime - }() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Create legitimate and attacker hosts - hosts := getDefaultHosts(t, 2) - legit := hosts[0] - attacker := hosts[1] - - // Set up gossipsub on the legit host - ps, err := NewGossipSub(ctx, legit, - WithPeerScore( - &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - BehaviourPenaltyWeight: -1, - BehaviourPenaltyDecay: ScoreParameterDecay(time.Minute), - DecayInterval: DefaultDecayInterval, - DecayToZero: DefaultDecayToZero, - }, - &PeerScoreThresholds{ - GossipThreshold: -100, - PublishThreshold: -500, - GraylistThreshold: -1000, - })) - if err != nil { - t.Fatal(err) - } + synctestTest(t, func(t *testing.T) { + originalGossipSubIWantFollowupTime := GossipSubIWantFollowupTime + GossipSubIWantFollowupTime = 10 * time.Second + defer func() { + GossipSubIWantFollowupTime = originalGossipSubIWantFollowupTime + }() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Create legitimate and attacker hosts + hosts := getDefaultHosts(t, 2) + legit := hosts[0] + attacker := hosts[1] + + // Set up gossipsub on the legit host + ps, err := NewGossipSub(ctx, legit, + WithPeerScore( + &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + BehaviourPenaltyWeight: -1, + BehaviourPenaltyDecay: ScoreParameterDecay(time.Minute), + DecayInterval: DefaultDecayInterval, + DecayToZero: DefaultDecayToZero, + }, + &PeerScoreThresholds{ + GossipThreshold: -100, + PublishThreshold: -500, + GraylistThreshold: -1000, + })) + if err != nil { + t.Fatal(err) + } - // Subscribe to mytopic on the legit host - mytopic := "mytopic" - _, err = ps.Subscribe(mytopic) - if err != nil { - t.Fatal(err) - } + // Subscribe to mytopic on the legit host + mytopic := "mytopic" + _, err = ps.Subscribe(mytopic) + if err != nil { + t.Fatal(err) + } - iWantCount := 0 - iWantCountMx := sync.Mutex{} - getIWantCount := func() int { - iWantCountMx.Lock() - defer iWantCountMx.Unlock() - return iWantCount - } - addIWantCount := func(i int) { - iWantCountMx.Lock() - defer iWantCountMx.Unlock() - iWantCount += i - } + iWantCount := 0 + iWantCountMx := sync.Mutex{} + getIWantCount := func() int { + iWantCountMx.Lock() + defer iWantCountMx.Unlock() + return iWantCount + } + addIWantCount := func(i int) { + iWantCountMx.Lock() + defer iWantCountMx.Unlock() + iWantCount += i + } - newMockGS(ctx, t, attacker, func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - // When the legit host connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and grafting to the peer - writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, - }) - - sub := sub - go func() { - defer cancel() - - // Wait for a short interval to make sure the legit host - // received and processed the subscribe + graft - time.Sleep(20 * time.Millisecond) - - // Send a bunch of IHAVEs - for i := 0; i < 3*GossipSubMaxIHaveLength; i++ { - ihavelst := []string{"someid" + strconv.Itoa(i)} - ihave := []*pb.ControlIHave{{TopicID: sub.Topicid, MessageIDs: ihavelst}} - orpc := rpcWithControl(nil, ihave, nil, nil, nil, nil) - writeMsg(&orpc.RPC) - } - - select { - case <-ctx.Done(): - return - case <-time.After(GossipSubHeartbeatInterval): - } - - // Should have hit the maximum number of IWANTs per peer - // per heartbeat - iwc := getIWantCount() - if iwc > GossipSubMaxIHaveLength { - t.Errorf("Expecting max %d IWANTs per heartbeat but received %d", GossipSubMaxIHaveLength, iwc) - return // cannot call t.Fatalf in a non-test goroutine - } - firstBatchCount := iwc - - // the score should still be 0 because we haven't broken any promises yet - score := ps.rt.(*GossipSubRouter).score.Score(attacker.ID()) - if score != 0 { - t.Errorf("Expected 0 score, but got %f", score) - return // cannot call t.Fatalf in a non-test goroutine - } - - // Send a bunch of IHAVEs - for i := 0; i < 3*GossipSubMaxIHaveLength; i++ { - ihavelst := []string{"someid" + strconv.Itoa(i+100)} - ihave := []*pb.ControlIHave{{TopicID: sub.Topicid, MessageIDs: ihavelst}} - orpc := rpcWithControl(nil, ihave, nil, nil, nil, nil) - writeMsg(&orpc.RPC) - } - - select { - case <-ctx.Done(): - return - case <-time.After(GossipSubHeartbeatInterval): - } - - // Should have sent more IWANTs after the heartbeat - iwc = getIWantCount() - if iwc == firstBatchCount { - t.Error("Expecting to receive more IWANTs after heartbeat but did not") - return // cannot call t.Fatalf in a non-test goroutine - } - // Should not be more than the maximum per heartbeat - if iwc-firstBatchCount > GossipSubMaxIHaveLength { - t.Errorf("Expecting max %d IWANTs per heartbeat but received %d", GossipSubMaxIHaveLength, iwc-firstBatchCount) - return // cannot call t.Fatalf in a non-test goroutine - } - - select { - case <-ctx.Done(): - return - case <-time.After(GossipSubIWantFollowupTime): - } - - // The score should now be negative because of broken promises - score = ps.rt.(*GossipSubRouter).score.Score(attacker.ID()) - if score >= 0 { - t.Errorf("Expected negative score, but got %f", score) - return // cannot call t.Fatalf in a non-test goroutine - } - }() + newMockGS(ctx, t, attacker, func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + // When the legit host connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and grafting to the peer + writeMsg(&pb.RPC{ + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, + }) + + sub := sub + go func() { + defer cancel() + + // Wait for a short interval to make sure the legit host + // received and processed the subscribe + graft + time.Sleep(20 * time.Millisecond) + + // Send a bunch of IHAVEs + for i := 0; i < 3*GossipSubMaxIHaveLength; i++ { + ihavelst := []string{"someid" + strconv.Itoa(i)} + ihave := []*pb.ControlIHave{{TopicID: sub.Topicid, MessageIDs: ihavelst}} + orpc := rpcWithControl(nil, ihave, nil, nil, nil, nil) + writeMsg(&orpc.RPC) + } + + select { + case <-ctx.Done(): + return + case <-time.After(GossipSubHeartbeatInterval): + } + + // Should have hit the maximum number of IWANTs per peer + // per heartbeat + iwc := getIWantCount() + if iwc > GossipSubMaxIHaveLength { + t.Errorf("Expecting max %d IWANTs per heartbeat but received %d", GossipSubMaxIHaveLength, iwc) + return // cannot call t.Fatalf in a non-test goroutine + } + firstBatchCount := iwc + + // the score should still be 0 because we haven't broken any promises yet + score := ps.rt.(*GossipSubRouter).score.Score(attacker.ID()) + if score != 0 { + t.Errorf("Expected 0 score, but got %f", score) + return // cannot call t.Fatalf in a non-test goroutine + } + + // Send a bunch of IHAVEs + for i := 0; i < 3*GossipSubMaxIHaveLength; i++ { + ihavelst := []string{"someid" + strconv.Itoa(i+100)} + ihave := []*pb.ControlIHave{{TopicID: sub.Topicid, MessageIDs: ihavelst}} + orpc := rpcWithControl(nil, ihave, nil, nil, nil, nil) + writeMsg(&orpc.RPC) + } + + select { + case <-ctx.Done(): + return + case <-time.After(GossipSubHeartbeatInterval): + } + + // Should have sent more IWANTs after the heartbeat + iwc = getIWantCount() + if iwc == firstBatchCount { + t.Error("Expecting to receive more IWANTs after heartbeat but did not") + return // cannot call t.Fatalf in a non-test goroutine + } + // Should not be more than the maximum per heartbeat + if iwc-firstBatchCount > GossipSubMaxIHaveLength { + t.Errorf("Expecting max %d IWANTs per heartbeat but received %d", GossipSubMaxIHaveLength, iwc-firstBatchCount) + return // cannot call t.Fatalf in a non-test goroutine + } + + select { + case <-ctx.Done(): + return + case <-time.After(GossipSubIWantFollowupTime): + } + + // The score should now be negative because of broken promises + score = ps.rt.(*GossipSubRouter).score.Score(attacker.ID()) + if score >= 0 { + t.Errorf("Expected negative score, but got %f", score) + return // cannot call t.Fatalf in a non-test goroutine + } + }() + } } - } - // Record the count of received IWANT messages - if ctl := irpc.GetControl(); ctl != nil { - addIWantCount(len(ctl.GetIwant())) - } - }) + // Record the count of received IWANT messages + if ctl := irpc.GetControl(); ctl != nil { + addIWantCount(len(ctl.GetIwant())) + } + }) - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - <-ctx.Done() + <-ctx.Done() + }) } // Test that when Gossipsub receives GRAFT for an unknown topic, it ignores // the request func TestGossipsubAttackGRAFTNonExistentTopic(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Create legitimate and attacker hosts - hosts := getDefaultHosts(t, 2) - legit := hosts[0] - attacker := hosts[1] - - // Set up gossipsub on the legit host - ps, err := NewGossipSub(ctx, legit) - if err != nil { - t.Fatal(err) - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - // Subscribe to mytopic on the legit host - mytopic := "mytopic" - _, err = ps.Subscribe(mytopic) - if err != nil { - t.Fatal(err) - } + // Create legitimate and attacker hosts + hosts := getDefaultHosts(t, 2) + legit := hosts[0] + attacker := hosts[1] - // Checks that we haven't received any PRUNE message - pruneCount := 0 - checkForPrune := func() { - // We send a GRAFT for a non-existent topic so we shouldn't - // receive a PRUNE in response - if pruneCount != 0 { - t.Fatalf("Got %d unexpected PRUNE messages", pruneCount) + // Set up gossipsub on the legit host + ps, err := NewGossipSub(ctx, legit) + if err != nil { + t.Fatal(err) } - } - newMockGS(ctx, t, attacker, func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - // When the legit host connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and grafting to the peer - writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, - }) - - // Graft to the peer on a non-existent topic - nonExistentTopic := "non-existent" - writeMsg(&pb.RPC{ - Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: &nonExistentTopic}}}, - }) - - go func() { - // Wait for a short interval to make sure the legit host - // received and processed the subscribe + graft - time.Sleep(100 * time.Millisecond) - - // We shouldn't get any prune messages becaue the topic - // doesn't exist - checkForPrune() - cancel() - }() - } + // Subscribe to mytopic on the legit host + mytopic := "mytopic" + _, err = ps.Subscribe(mytopic) + if err != nil { + t.Fatal(err) } - // Record the count of received PRUNE messages - if ctl := irpc.GetControl(); ctl != nil { - pruneCount += len(ctl.GetPrune()) + // Checks that we haven't received any PRUNE message + pruneCount := 0 + checkForPrune := func() { + // We send a GRAFT for a non-existent topic so we shouldn't + // receive a PRUNE in response + if pruneCount != 0 { + t.Fatalf("Got %d unexpected PRUNE messages", pruneCount) + } } - }) - connect(t, hosts[0], hosts[1]) + newMockGS(ctx, t, attacker, func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + // When the legit host connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and grafting to the peer + writeMsg(&pb.RPC{ + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, + }) + + // Graft to the peer on a non-existent topic + nonExistentTopic := "non-existent" + writeMsg(&pb.RPC{ + Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: &nonExistentTopic}}}, + }) + + go func() { + // Wait for a short interval to make sure the legit host + // received and processed the subscribe + graft + time.Sleep(100 * time.Millisecond) + + // We shouldn't get any prune messages becaue the topic + // doesn't exist + checkForPrune() + cancel() + }() + } + } - <-ctx.Done() + // Record the count of received PRUNE messages + if ctl := irpc.GetControl(); ctl != nil { + pruneCount += len(ctl.GetPrune()) + } + }) + + connect(t, hosts[0], hosts[1]) + + <-ctx.Done() + }) } // Test that when Gossipsub receives GRAFT for a peer that has been PRUNED, // it penalizes through P7 and eventually graylists and ignores the requests if the // GRAFTs are coming too fast func TestGossipsubAttackGRAFTDuringBackoff(t *testing.T) { - originalGossipSubPruneBackoff := GossipSubPruneBackoff - GossipSubPruneBackoff = 200 * time.Millisecond - originalGossipSubGraftFloodThreshold := GossipSubGraftFloodThreshold - GossipSubGraftFloodThreshold = 100 * time.Millisecond - defer func() { - GossipSubPruneBackoff = originalGossipSubPruneBackoff - GossipSubGraftFloodThreshold = originalGossipSubGraftFloodThreshold - }() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Create legitimate and attacker hosts - hosts := getDefaultHosts(t, 2) - legit := hosts[0] - attacker := hosts[1] - - // Set up gossipsub on the legit host - ps, err := NewGossipSub(ctx, legit, - WithPeerScore( - &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - BehaviourPenaltyWeight: -100, - BehaviourPenaltyDecay: ScoreParameterDecay(time.Minute), - DecayInterval: DefaultDecayInterval, - DecayToZero: DefaultDecayToZero, - }, - &PeerScoreThresholds{ - GossipThreshold: -100, - PublishThreshold: -500, - GraylistThreshold: -1000, - })) - if err != nil { - t.Fatal(err) - } + synctestTest(t, func(t *testing.T) { + originalGossipSubPruneBackoff := GossipSubPruneBackoff + GossipSubPruneBackoff = 200 * time.Millisecond + originalGossipSubGraftFloodThreshold := GossipSubGraftFloodThreshold + GossipSubGraftFloodThreshold = 100 * time.Millisecond + defer func() { + GossipSubPruneBackoff = originalGossipSubPruneBackoff + GossipSubGraftFloodThreshold = originalGossipSubGraftFloodThreshold + }() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Create legitimate and attacker hosts + hosts := getDefaultHosts(t, 2) + legit := hosts[0] + attacker := hosts[1] + + // Set up gossipsub on the legit host + ps, err := NewGossipSub(ctx, legit, + WithPeerScore( + &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + BehaviourPenaltyWeight: -100, + BehaviourPenaltyDecay: ScoreParameterDecay(time.Minute), + DecayInterval: DefaultDecayInterval, + DecayToZero: DefaultDecayToZero, + }, + &PeerScoreThresholds{ + GossipThreshold: -100, + PublishThreshold: -500, + GraylistThreshold: -1000, + })) + if err != nil { + t.Fatal(err) + } - // Subscribe to mytopic on the legit host - mytopic := "mytopic" - _, err = ps.Subscribe(mytopic) - if err != nil { - t.Fatal(err) - } + // Subscribe to mytopic on the legit host + mytopic := "mytopic" + _, err = ps.Subscribe(mytopic) + if err != nil { + t.Fatal(err) + } - pruneCount := 0 - pruneCountMx := sync.Mutex{} - getPruneCount := func() int { - pruneCountMx.Lock() - defer pruneCountMx.Unlock() - return pruneCount - } - addPruneCount := func(i int) { - pruneCountMx.Lock() - defer pruneCountMx.Unlock() - pruneCount += i - } + pruneCount := 0 + pruneCountMx := sync.Mutex{} + getPruneCount := func() int { + pruneCountMx.Lock() + defer pruneCountMx.Unlock() + return pruneCount + } + addPruneCount := func(i int) { + pruneCountMx.Lock() + defer pruneCountMx.Unlock() + pruneCount += i + } - newMockGS(ctx, t, attacker, func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - // When the legit host connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and grafting to the peer - graft := []*pb.ControlGraft{{TopicID: sub.Topicid}} - writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Graft: graft}, - }) - - sub := sub - go func() { - defer cancel() - - // Wait for a short interval to make sure the legit host - // received and processed the subscribe + graft - time.Sleep(20 * time.Millisecond) - - // No PRUNE should have been sent at this stage - pc := getPruneCount() - if pc != 0 { - t.Errorf("Expected %d PRUNE messages but got %d", 0, pc) - return // cannot call t.Fatalf in a non-test goroutine - } - - // Send a PRUNE to remove the attacker node from the legit - // host's mesh - var prune []*pb.ControlPrune - prune = append(prune, &pb.ControlPrune{TopicID: sub.Topicid}) + newMockGS(ctx, t, attacker, func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + // When the legit host connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and grafting to the peer + graft := []*pb.ControlGraft{{TopicID: sub.Topicid}} writeMsg(&pb.RPC{ - Control: &pb.ControlMessage{Prune: prune}, + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Graft: graft}, }) - select { - case <-ctx.Done(): - return - case <-time.After(20 * time.Millisecond): - } + sub := sub + go func() { + defer cancel() - // No PRUNE should have been sent at this stage - pc = getPruneCount() - if pc != 0 { - t.Errorf("Expected %d PRUNE messages but got %d", 0, pc) - return // cannot call t.Fatalf in a non-test goroutine + // Wait for a short interval to make sure the legit host + // received and processed the subscribe + graft + time.Sleep(20 * time.Millisecond) - } + // No PRUNE should have been sent at this stage + pc := getPruneCount() + if pc != 0 { + t.Errorf("Expected %d PRUNE messages but got %d", 0, pc) + return // cannot call t.Fatalf in a non-test goroutine + } - // wait for the GossipSubGraftFloodThreshold to pass before attempting another graft - time.Sleep(GossipSubGraftFloodThreshold + time.Millisecond) + // Send a PRUNE to remove the attacker node from the legit + // host's mesh + var prune []*pb.ControlPrune + prune = append(prune, &pb.ControlPrune{TopicID: sub.Topicid}) + writeMsg(&pb.RPC{ + Control: &pb.ControlMessage{Prune: prune}, + }) - // Send a GRAFT to attempt to rejoin the mesh - writeMsg(&pb.RPC{ - Control: &pb.ControlMessage{Graft: graft}, - }) + select { + case <-ctx.Done(): + return + case <-time.After(20 * time.Millisecond): + } - select { - case <-ctx.Done(): - return - case <-time.After(20 * time.Millisecond): - } - - // We should have been peanalized by the peer for sending before the backoff has expired - // but should still receive a PRUNE because we haven't dropped below GraylistThreshold - // yet. - pc = getPruneCount() - if pc != 1 { - t.Errorf("Expected %d PRUNE messages but got %d", 1, pc) - return // cannot call t.Fatalf in a non-test goroutine - } - - score1 := ps.rt.(*GossipSubRouter).score.Score(attacker.ID()) - if score1 >= 0 { - t.Errorf("Expected negative score, but got %f", score1) - return // cannot call t.Fatalf in a non-test goroutine - } - - // Send a GRAFT again to attempt to rejoin the mesh - writeMsg(&pb.RPC{ - Control: &pb.ControlMessage{Graft: graft}, - }) + // No PRUNE should have been sent at this stage + pc = getPruneCount() + if pc != 0 { + t.Errorf("Expected %d PRUNE messages but got %d", 0, pc) + return // cannot call t.Fatalf in a non-test goroutine - select { - case <-ctx.Done(): - return - case <-time.After(20 * time.Millisecond): - } - - // we are before the flood threshold so we should be penalized twice, but still get - // a PRUNE because we are before the flood threshold - pc = getPruneCount() - if pc != 2 { - t.Errorf("Expected %d PRUNE messages but got %d", 2, pc) - return // cannot call t.Fatalf in a non-test goroutine - } - - score2 := ps.rt.(*GossipSubRouter).score.Score(attacker.ID()) - if score2 >= score1 { - t.Errorf("Expected score below %f, but got %f", score1, score2) - return // cannot call t.Fatalf in a non-test goroutine - } - - // Send another GRAFT; this should get us a PRUNE, but penalize us below the graylist threshold - writeMsg(&pb.RPC{ - Control: &pb.ControlMessage{Graft: graft}, - }) + } - select { - case <-ctx.Done(): - return - case <-time.After(20 * time.Millisecond): - } - - pc = getPruneCount() - if pc != 3 { - t.Errorf("Expected %d PRUNE messages but got %d", 3, pc) - return // cannot call t.Fatalf in a non-test goroutine - } - - score3 := ps.rt.(*GossipSubRouter).score.Score(attacker.ID()) - if score3 >= score2 { - t.Errorf("Expected score below %f, but got %f", score2, score3) - return // cannot call t.Fatalf in a non-test goroutine - } - if score3 >= -1000 { - t.Errorf("Expected score below %f, but got %f", -1000.0, score3) - return // cannot call t.Fatalf in a non-test goroutine - } - - // Wait for the PRUNE backoff to expire and try again; this time we should fail - // because we are below the graylist threshold, so our RPC should be ignored and - // we should get no PRUNE back - select { - case <-ctx.Done(): - return - case <-time.After(GossipSubPruneBackoff + time.Millisecond): - } + // wait for the GossipSubGraftFloodThreshold to pass before attempting another graft + time.Sleep(GossipSubGraftFloodThreshold + time.Millisecond) - writeMsg(&pb.RPC{ - Control: &pb.ControlMessage{Graft: graft}, - }) + // Send a GRAFT to attempt to rejoin the mesh + writeMsg(&pb.RPC{ + Control: &pb.ControlMessage{Graft: graft}, + }) - select { - case <-ctx.Done(): - return - case <-time.After(20 * time.Millisecond): - } - - pc = getPruneCount() - if pc != 3 { - t.Errorf("Expected %d PRUNE messages but got %d", 3, pc) - return // cannot call t.Fatalf in a non-test goroutine - } - - // make sure we are _not_ in the mesh - res := make(chan bool) - ps.eval <- func() { - mesh := ps.rt.(*GossipSubRouter).mesh[mytopic] - _, inMesh := mesh[attacker.ID()] - res <- inMesh - } - - inMesh := <-res - if inMesh { - t.Error("Expected to not be in the mesh of the legitimate host") - return // cannot call t.Fatal in a non-test goroutine - } - }() + select { + case <-ctx.Done(): + return + case <-time.After(20 * time.Millisecond): + } + + // We should have been peanalized by the peer for sending before the backoff has expired + // but should still receive a PRUNE because we haven't dropped below GraylistThreshold + // yet. + pc = getPruneCount() + if pc != 1 { + t.Errorf("Expected %d PRUNE messages but got %d", 1, pc) + return // cannot call t.Fatalf in a non-test goroutine + } + + score1 := ps.rt.(*GossipSubRouter).score.Score(attacker.ID()) + if score1 >= 0 { + t.Errorf("Expected negative score, but got %f", score1) + return // cannot call t.Fatalf in a non-test goroutine + } + + // Send a GRAFT again to attempt to rejoin the mesh + writeMsg(&pb.RPC{ + Control: &pb.ControlMessage{Graft: graft}, + }) + + select { + case <-ctx.Done(): + return + case <-time.After(20 * time.Millisecond): + } + + // we are before the flood threshold so we should be penalized twice, but still get + // a PRUNE because we are before the flood threshold + pc = getPruneCount() + if pc != 2 { + t.Errorf("Expected %d PRUNE messages but got %d", 2, pc) + return // cannot call t.Fatalf in a non-test goroutine + } + + score2 := ps.rt.(*GossipSubRouter).score.Score(attacker.ID()) + if score2 >= score1 { + t.Errorf("Expected score below %f, but got %f", score1, score2) + return // cannot call t.Fatalf in a non-test goroutine + } + + // Send another GRAFT; this should get us a PRUNE, but penalize us below the graylist threshold + writeMsg(&pb.RPC{ + Control: &pb.ControlMessage{Graft: graft}, + }) + + select { + case <-ctx.Done(): + return + case <-time.After(20 * time.Millisecond): + } + + pc = getPruneCount() + if pc != 3 { + t.Errorf("Expected %d PRUNE messages but got %d", 3, pc) + return // cannot call t.Fatalf in a non-test goroutine + } + + score3 := ps.rt.(*GossipSubRouter).score.Score(attacker.ID()) + if score3 >= score2 { + t.Errorf("Expected score below %f, but got %f", score2, score3) + return // cannot call t.Fatalf in a non-test goroutine + } + if score3 >= -1000 { + t.Errorf("Expected score below %f, but got %f", -1000.0, score3) + return // cannot call t.Fatalf in a non-test goroutine + } + + // Wait for the PRUNE backoff to expire and try again; this time we should fail + // because we are below the graylist threshold, so our RPC should be ignored and + // we should get no PRUNE back + select { + case <-ctx.Done(): + return + case <-time.After(GossipSubPruneBackoff + time.Millisecond): + } + + writeMsg(&pb.RPC{ + Control: &pb.ControlMessage{Graft: graft}, + }) + + select { + case <-ctx.Done(): + return + case <-time.After(20 * time.Millisecond): + } + + pc = getPruneCount() + if pc != 3 { + t.Errorf("Expected %d PRUNE messages but got %d", 3, pc) + return // cannot call t.Fatalf in a non-test goroutine + } + + // make sure we are _not_ in the mesh + res := make(chan bool) + ps.eval <- func() { + mesh := ps.rt.(*GossipSubRouter).mesh[mytopic] + _, inMesh := mesh[attacker.ID()] + res <- inMesh + } + + inMesh := <-res + if inMesh { + t.Error("Expected to not be in the mesh of the legitimate host") + return // cannot call t.Fatal in a non-test goroutine + } + }() + } } - } - if ctl := irpc.GetControl(); ctl != nil { - addPruneCount(len(ctl.GetPrune())) - } - }) + if ctl := irpc.GetControl(); ctl != nil { + addPruneCount(len(ctl.GetPrune())) + } + }) - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - <-ctx.Done() + <-ctx.Done() + }) } type gsAttackInvalidMsgTracer struct { @@ -618,332 +626,338 @@ func (t *gsAttackInvalidMsgTracer) Trace(evt *pb.TraceEvent) { // Test that when Gossipsub receives a lot of invalid messages from // a peer it should graylist the peer func TestGossipsubAttackInvalidMessageSpam(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Create legitimate and attacker hosts - hosts := getDefaultHosts(t, 2) - legit := hosts[0] - attacker := hosts[1] - - mytopic := "mytopic" - - // Create parameters with reasonable default values - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - IPColocationFactorWeight: 0, - IPColocationFactorThreshold: 1, - DecayInterval: 5 * time.Second, - DecayToZero: 0.01, - RetainScore: 10 * time.Second, - Topics: make(map[string]*TopicScoreParams), - } - params.Topics[mytopic] = &TopicScoreParams{ - TopicWeight: 0.25, - TimeInMeshWeight: 0.0027, - TimeInMeshQuantum: time.Second, - TimeInMeshCap: 3600, - FirstMessageDeliveriesWeight: 0.664, - FirstMessageDeliveriesDecay: 0.9916, - FirstMessageDeliveriesCap: 1500, - MeshMessageDeliveriesWeight: -0.25, - MeshMessageDeliveriesDecay: 0.97, - MeshMessageDeliveriesCap: 400, - MeshMessageDeliveriesThreshold: 100, - MeshMessageDeliveriesActivation: 30 * time.Second, - MeshMessageDeliveriesWindow: 5 * time.Minute, - MeshFailurePenaltyWeight: -0.25, - MeshFailurePenaltyDecay: 0.997, - InvalidMessageDeliveriesWeight: -99, - InvalidMessageDeliveriesDecay: 0.9994, - } - thresholds := &PeerScoreThresholds{ - GossipThreshold: -100, - PublishThreshold: -200, - GraylistThreshold: -300, - AcceptPXThreshold: 0, - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Create legitimate and attacker hosts + hosts := getDefaultHosts(t, 2) + legit := hosts[0] + attacker := hosts[1] + + mytopic := "mytopic" + + // Create parameters with reasonable default values + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + IPColocationFactorWeight: 0, + IPColocationFactorThreshold: 1, + DecayInterval: 5 * time.Second, + DecayToZero: 0.01, + RetainScore: 10 * time.Second, + Topics: make(map[string]*TopicScoreParams), + } + params.Topics[mytopic] = &TopicScoreParams{ + TopicWeight: 0.25, + TimeInMeshWeight: 0.0027, + TimeInMeshQuantum: time.Second, + TimeInMeshCap: 3600, + FirstMessageDeliveriesWeight: 0.664, + FirstMessageDeliveriesDecay: 0.9916, + FirstMessageDeliveriesCap: 1500, + MeshMessageDeliveriesWeight: -0.25, + MeshMessageDeliveriesDecay: 0.97, + MeshMessageDeliveriesCap: 400, + MeshMessageDeliveriesThreshold: 100, + MeshMessageDeliveriesActivation: 30 * time.Second, + MeshMessageDeliveriesWindow: 5 * time.Minute, + MeshFailurePenaltyWeight: -0.25, + MeshFailurePenaltyDecay: 0.997, + InvalidMessageDeliveriesWeight: -99, + InvalidMessageDeliveriesDecay: 0.9994, + } + thresholds := &PeerScoreThresholds{ + GossipThreshold: -100, + PublishThreshold: -200, + GraylistThreshold: -300, + AcceptPXThreshold: 0, + } - // Set up gossipsub on the legit host - tracer := &gsAttackInvalidMsgTracer{} - ps, err := NewGossipSub(ctx, legit, - WithEventTracer(tracer), - WithPeerScore(params, thresholds), - ) - if err != nil { - t.Fatal(err) - } + // Set up gossipsub on the legit host + tracer := &gsAttackInvalidMsgTracer{} + ps, err := NewGossipSub(ctx, legit, + WithEventTracer(tracer), + WithPeerScore(params, thresholds), + ) + if err != nil { + t.Fatal(err) + } - attackerScore := func() float64 { - return ps.rt.(*GossipSubRouter).score.Score(attacker.ID()) - } + attackerScore := func() float64 { + return ps.rt.(*GossipSubRouter).score.Score(attacker.ID()) + } - // Subscribe to mytopic on the legit host - _, err = ps.Subscribe(mytopic) - if err != nil { - t.Fatal(err) - } + // Subscribe to mytopic on the legit host + _, err = ps.Subscribe(mytopic) + if err != nil { + t.Fatal(err) + } - pruneCount := 0 - pruneCountMx := sync.Mutex{} - getPruneCount := func() int { - pruneCountMx.Lock() - defer pruneCountMx.Unlock() - return pruneCount - } - addPruneCount := func(i int) { - pruneCountMx.Lock() - defer pruneCountMx.Unlock() - pruneCount += i - } + pruneCount := 0 + pruneCountMx := sync.Mutex{} + getPruneCount := func() int { + pruneCountMx.Lock() + defer pruneCountMx.Unlock() + return pruneCount + } + addPruneCount := func(i int) { + pruneCountMx.Lock() + defer pruneCountMx.Unlock() + pruneCount += i + } - newMockGS(ctx, t, attacker, func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - // When the legit host connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and grafting to the peer - writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, - }) - - go func() { - defer cancel() - - // Attacker score should start at zero - if attackerScore() != 0 { - t.Errorf("Expected attacker score to be zero but it's %f", attackerScore()) - return // cannot call t.Fatalf in a non-test goroutine - } - - // Send a bunch of messages with no signature (these will - // fail validation and reduce the attacker's score) - for i := 0; i < 100; i++ { - msg := &pb.Message{ - Data: []byte("some data" + strconv.Itoa(i)), - Topic: &mytopic, - From: []byte(attacker.ID()), - Seqno: []byte{byte(i + 1)}, + newMockGS(ctx, t, attacker, func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + // When the legit host connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and grafting to the peer + writeMsg(&pb.RPC{ + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, + }) + + go func() { + defer cancel() + + // Attacker score should start at zero + if attackerScore() != 0 { + t.Errorf("Expected attacker score to be zero but it's %f", attackerScore()) + return // cannot call t.Fatalf in a non-test goroutine } - writeMsg(&pb.RPC{ - Publish: []*pb.Message{msg}, - }) - } - - // Wait for the initial heartbeat, plus a bit of padding - select { - case <-ctx.Done(): - return - case <-time.After(100*time.Millisecond + GossipSubHeartbeatInitialDelay): - } - - // The attackers score should now have fallen below zero - if attackerScore() >= 0 { - t.Errorf("Expected attacker score to be less than zero but it's %f", attackerScore()) - return // cannot call t.Fatalf in a non-test goroutine - } - // There should be several rejected messages (because the signature was invalid) - if tracer.rejectCount == 0 { - t.Error("Expected message rejection but got none") - return // cannot call t.Fatal in a non-test goroutine - } - // The legit node should have sent a PRUNE message - pc := getPruneCount() - if pc == 0 { - t.Error("Expected attacker node to be PRUNED when score drops low enough") - return // cannot call t.Fatal in a non-test goroutine - } - }() + + // Send a bunch of messages with no signature (these will + // fail validation and reduce the attacker's score) + for i := 0; i < 100; i++ { + msg := &pb.Message{ + Data: []byte("some data" + strconv.Itoa(i)), + Topic: &mytopic, + From: []byte(attacker.ID()), + Seqno: []byte{byte(i + 1)}, + } + writeMsg(&pb.RPC{ + Publish: []*pb.Message{msg}, + }) + } + + // Wait for the initial heartbeat, plus a bit of padding + select { + case <-ctx.Done(): + return + case <-time.After(100*time.Millisecond + GossipSubHeartbeatInitialDelay): + } + + // The attackers score should now have fallen below zero + if attackerScore() >= 0 { + t.Errorf("Expected attacker score to be less than zero but it's %f", attackerScore()) + return // cannot call t.Fatalf in a non-test goroutine + } + // There should be several rejected messages (because the signature was invalid) + if tracer.rejectCount == 0 { + t.Error("Expected message rejection but got none") + return // cannot call t.Fatal in a non-test goroutine + } + // The legit node should have sent a PRUNE message + pc := getPruneCount() + if pc == 0 { + t.Error("Expected attacker node to be PRUNED when score drops low enough") + return // cannot call t.Fatal in a non-test goroutine + } + }() + } } - } - if ctl := irpc.GetControl(); ctl != nil { - addPruneCount(len(ctl.GetPrune())) - } - }) + if ctl := irpc.GetControl(); ctl != nil { + addPruneCount(len(ctl.GetPrune())) + } + }) - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - <-ctx.Done() + <-ctx.Done() + }) } // Test that when Gossipsub receives too many IDONTWANT messages from a peer func TestGossipsubAttackSpamIDONTWANT(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 3) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 3) + + msgID := func(pmsg *pb.Message) string { + // silly content-based test message-ID: just use the data as whole + return base64.URLEncoding.EncodeToString(pmsg.Data) + } - msgID := func(pmsg *pb.Message) string { - // silly content-based test message-ID: just use the data as whole - return base64.URLEncoding.EncodeToString(pmsg.Data) - } + psubs := make([]*PubSub, 2) + psubs[0] = getGossipsub(ctx, hosts[0], WithMessageIdFn(msgID)) + psubs[1] = getGossipsub(ctx, hosts[1], WithMessageIdFn(msgID)) - psubs := make([]*PubSub, 2) - psubs[0] = getGossipsub(ctx, hosts[0], WithMessageIdFn(msgID)) - psubs[1] = getGossipsub(ctx, hosts[1], WithMessageIdFn(msgID)) + topic := "foobar" + for _, ps := range psubs { + _, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } + } - topic := "foobar" - for _, ps := range psubs { - _, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + // Wait a bit after the last message before checking the result + msgWaitMax := time.Second + GossipSubHeartbeatInterval + msgTimer := time.NewTimer(msgWaitMax) + + // Checks we received some messages + var expMid string + var actMids []string + var mu sync.Mutex + checkMsgs := func() { + mu.Lock() + defer mu.Unlock() + if len(actMids) == 0 { + t.Fatalf("Expected some messages when the maximum number of IDONTWANTs is reached") + } + if actMids[0] != expMid { + t.Fatalf("The expected message is incorrect") + } + if len(actMids) > 1 { + t.Fatalf("The spam prevention should be reset after the heartbeat") + } } - } - // Wait a bit after the last message before checking the result - msgWaitMax := time.Second + GossipSubHeartbeatInterval - msgTimer := time.NewTimer(msgWaitMax) + // Wait for the timer to expire + go func() { + select { + case <-msgTimer.C: + checkMsgs() + cancel() + return + case <-ctx.Done(): + checkMsgs() + } + }() + + newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + mu.Lock() + defer mu.Unlock() + // Each time the host receives a message + for _, msg := range irpc.GetPublish() { + actMids = append(actMids, msgID(msg)) + } + // When the middle peer connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and grafting to the middle peer + writeMsg(&pb.RPC{ + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, + }) - // Checks we received some messages - var expMid string - var actMids []string - var mu sync.Mutex - checkMsgs := func() { - mu.Lock() - defer mu.Unlock() - if len(actMids) == 0 { - t.Fatalf("Expected some messages when the maximum number of IDONTWANTs is reached") - } - if actMids[0] != expMid { - t.Fatalf("The expected message is incorrect") - } - if len(actMids) > 1 { - t.Fatalf("The spam prevention should be reset after the heartbeat") - } - } + go func() { + // Wait for a short interval to make sure the middle peer + // received and processed the subscribe + graft + time.Sleep(100 * time.Millisecond) + + // Generate a message and send IDONTWANT to the middle peer + data := make([]byte, 16) + var mid string + for i := 0; i < 1+GossipSubMaxIDontWantMessages; i++ { + rand.Read(data) + mid = msgID(&pb.Message{Data: data}) + writeMsg(&pb.RPC{ + Control: &pb.ControlMessage{Idontwant: []*pb.ControlIDontWant{{MessageIDs: []string{mid}}}}, + }) + } + // The host should receives this message id because the maximum was reached + mu.Lock() + expMid = mid + mu.Unlock() + + // Wait for a short interval to make sure the middle peer + // received and processed the IDONTWANTs + time.Sleep(100 * time.Millisecond) + + // Publish the message from the first peer + if err := psubs[0].Publish(topic, data); err != nil { + t.Error(err) + return // cannot call t.Fatal in a non-test goroutine + } - // Wait for the timer to expire - go func() { - select { - case <-msgTimer.C: - checkMsgs() - cancel() - return - case <-ctx.Done(): - checkMsgs() - } - }() - - newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - mu.Lock() - defer mu.Unlock() - // Each time the host receives a message - for _, msg := range irpc.GetPublish() { - actMids = append(actMids, msgID(msg)) - } - // When the middle peer connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and grafting to the middle peer - writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, - }) - - go func() { - // Wait for a short interval to make sure the middle peer - // received and processed the subscribe + graft - time.Sleep(100 * time.Millisecond) - - // Generate a message and send IDONTWANT to the middle peer - data := make([]byte, 16) - var mid string - for i := 0; i < 1+GossipSubMaxIDontWantMessages; i++ { + // Wait for the next heartbeat so that the prevention will be reset + select { + case <-ctx.Done(): + return + case <-time.After(GossipSubHeartbeatInterval): + } + + // Test IDONTWANT again to see that it now works again rand.Read(data) mid = msgID(&pb.Message{Data: data}) writeMsg(&pb.RPC{ Control: &pb.ControlMessage{Idontwant: []*pb.ControlIDontWant{{MessageIDs: []string{mid}}}}, }) - } - // The host should receives this message id because the maximum was reached - mu.Lock() - expMid = mid - mu.Unlock() - - // Wait for a short interval to make sure the middle peer - // received and processed the IDONTWANTs - time.Sleep(100 * time.Millisecond) - - // Publish the message from the first peer - if err := psubs[0].Publish(topic, data); err != nil { - t.Error(err) - return // cannot call t.Fatal in a non-test goroutine - } - - // Wait for the next heartbeat so that the prevention will be reset - select { - case <-ctx.Done(): - return - case <-time.After(GossipSubHeartbeatInterval): - } - - // Test IDONTWANT again to see that it now works again - rand.Read(data) - mid = msgID(&pb.Message{Data: data}) - writeMsg(&pb.RPC{ - Control: &pb.ControlMessage{Idontwant: []*pb.ControlIDontWant{{MessageIDs: []string{mid}}}}, - }) - time.Sleep(100 * time.Millisecond) - if err := psubs[0].Publish(topic, data); err != nil { - t.Error(err) - return // cannot call t.Fatal in a non-test goroutine - } - }() + time.Sleep(100 * time.Millisecond) + if err := psubs[0].Publish(topic, data); err != nil { + t.Error(err) + return // cannot call t.Fatal in a non-test goroutine + } + }() + } } - } - }) + }) - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) - <-ctx.Done() + <-ctx.Done() + }) } func TestGossipsubHandleIDontwantSpam(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 2) - - msgID := func(pmsg *pb.Message) string { - // silly content-based test message-ID: just use the data as whole - return base64.URLEncoding.EncodeToString(pmsg.Data) - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 2) + + msgID := func(pmsg *pb.Message) string { + // silly content-based test message-ID: just use the data as whole + return base64.URLEncoding.EncodeToString(pmsg.Data) + } - psubs := make([]*PubSub, 2) - psubs[0] = getGossipsub(ctx, hosts[0], WithMessageIdFn(msgID)) - psubs[1] = getGossipsub(ctx, hosts[1], WithMessageIdFn(msgID)) + psubs := make([]*PubSub, 2) + psubs[0] = getGossipsub(ctx, hosts[0], WithMessageIdFn(msgID)) + psubs[1] = getGossipsub(ctx, hosts[1], WithMessageIdFn(msgID)) - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - topic := "foobar" - for _, ps := range psubs { - _, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + topic := "foobar" + for _, ps := range psubs { + _, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } } - } - exceededIDWLength := GossipSubMaxIDontWantLength + 1 - var idwIds []string - for i := 0; i < exceededIDWLength; i++ { - idwIds = append(idwIds, fmt.Sprintf("idontwant-%d", i)) - } - rPid := hosts[1].ID() - ctrlMessage := &pb.ControlMessage{Idontwant: []*pb.ControlIDontWant{{MessageIDs: idwIds}}} - grt := psubs[0].rt.(*GossipSubRouter) - grt.handleIDontWant(rPid, ctrlMessage) + exceededIDWLength := GossipSubMaxIDontWantLength + 1 + var idwIds []string + for i := 0; i < exceededIDWLength; i++ { + idwIds = append(idwIds, fmt.Sprintf("idontwant-%d", i)) + } + rPid := hosts[1].ID() + ctrlMessage := &pb.ControlMessage{Idontwant: []*pb.ControlIDontWant{{MessageIDs: idwIds}}} + grt := psubs[0].rt.(*GossipSubRouter) + grt.handleIDontWant(rPid, ctrlMessage) - if grt.peerdontwant[rPid] != 1 { - t.Errorf("Wanted message count of %d but received %d", 1, grt.peerdontwant[rPid]) - } - mid := fmt.Sprintf("idontwant-%d", GossipSubMaxIDontWantLength-1) - if _, ok := grt.unwanted[rPid][computeChecksum(mid)]; !ok { - t.Errorf("Desired message id was not stored in the unwanted map: %s", mid) - } + if grt.peerdontwant[rPid] != 1 { + t.Errorf("Wanted message count of %d but received %d", 1, grt.peerdontwant[rPid]) + } + mid := fmt.Sprintf("idontwant-%d", GossipSubMaxIDontWantLength-1) + if _, ok := grt.unwanted[rPid][computeChecksum(mid)]; !ok { + t.Errorf("Desired message id was not stored in the unwanted map: %s", mid) + } - mid = fmt.Sprintf("idontwant-%d", GossipSubMaxIDontWantLength) - if _, ok := grt.unwanted[rPid][computeChecksum(mid)]; ok { - t.Errorf("Unwanted message id was stored in the unwanted map: %s", mid) - } + mid = fmt.Sprintf("idontwant-%d", GossipSubMaxIDontWantLength) + if _, ok := grt.unwanted[rPid][computeChecksum(mid)]; ok { + t.Errorf("Unwanted message id was stored in the unwanted map: %s", mid) + } + }) } type mockGSOnRead func(writeMsg func(*pb.RPC), irpc *pb.RPC) diff --git a/gossipsub_test.go b/gossipsub_test.go index 8e96b98c..e3cd4985 100644 --- a/gossipsub_test.go +++ b/gossipsub_test.go @@ -66,24 +66,28 @@ func getGossipsubs(ctx context.Context, hs []host.Host, opts ...Option) []*PubSu } func TestGossipSubParamsValidate(t *testing.T) { - params := DefaultGossipSubParams() - params.Dhi = 1 - params.Dscore = 10 - if params.validate() == nil { - t.Fatal("Params should be invalid") - } + synctestTest(t, func(t *testing.T) { + params := DefaultGossipSubParams() + params.Dhi = 1 + params.Dscore = 10 + if params.validate() == nil { + t.Fatal("Params should be invalid") + } + }) } func TestGossipSubBootstrapParamsValidate(t *testing.T) { - params := DefaultGossipSubParams() - params.D = 0 - params.Dlo = 0 - params.Dhi = 0 - params.Dout = 0 - params.Dscore = 0 - if err := params.validate(); err != nil { - t.Fatalf("Params should be valid: %v", err) - } + synctestTest(t, func(t *testing.T) { + params := DefaultGossipSubParams() + params.D = 0 + params.Dlo = 0 + params.Dhi = 0 + params.Dout = 0 + params.Dscore = 0 + if err := params.validate(); err != nil { + t.Fatalf("Params should be valid: %v", err) + } + }) } func getGossipsubsOptFn(ctx context.Context, hs []host.Host, optFn func(int, host.Host) []Option) []*PubSub { @@ -96,2000 +100,2069 @@ func getGossipsubsOptFn(ctx context.Context, hs []host.Host, optFn func(int, hos } func TestSparseGossipsub(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 20) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 20) - psubs := getGossipsubsOptFn(ctx, hosts, func(i int, h host.Host) []Option { - lh := slog.NewJSONHandler(os.Stdout, nil) - logger := slog.New(lh.WithAttrs([]slog.Attr{slog.String("id", h.ID().String())})) - return []Option{ - WithRPCLogger(logger), - } - }) + psubs := getGossipsubsOptFn(ctx, hosts, func(i int, h host.Host) []Option { + lh := slog.NewJSONHandler(os.Stdout, nil) + logger := slog.New(lh.WithAttrs([]slog.Attr{slog.String("id", h.ID().String())})) + return []Option{ + WithRPCLogger(logger), + } + }) - var msgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + var msgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - msgs = append(msgs, subch) - } + msgs = append(msgs, subch) + } - sparseConnect(t, hosts) + sparseConnect(t, hosts) - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := mrand.Intn(len(psubs)) + owner := mrand.Intn(len(psubs)) - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } func TestDenseGossipsub(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 20) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts) + psubs := getGossipsubs(ctx, hosts) - var msgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + var msgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - msgs = append(msgs, subch) - } + msgs = append(msgs, subch) + } - denseConnect(t, hosts) + denseConnect(t, hosts) - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := mrand.Intn(len(psubs)) + owner := mrand.Intn(len(psubs)) - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } func TestGossipsubFanout(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 20) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts) + psubs := getGossipsubs(ctx, hosts) - var msgs []*Subscription - for _, ps := range psubs[1:] { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + var msgs []*Subscription + for _, ps := range psubs[1:] { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - msgs = append(msgs, subch) - } + msgs = append(msgs, subch) + } - denseConnect(t, hosts) + denseConnect(t, hosts) - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := 0 + owner := 0 - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } - // subscribe the owner - subch, err := psubs[0].Subscribe("foobar") - if err != nil { - t.Fatal(err) - } - msgs = append(msgs, subch) + // subscribe the owner + subch, err := psubs[0].Subscribe("foobar") + if err != nil { + t.Fatal(err) + } + msgs = append(msgs, subch) - // wait for a heartbeat - time.Sleep(time.Second * 1) + // wait for a heartbeat + time.Sleep(time.Second * 1) - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := 0 + owner := 0 - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } func TestGossipsubFanoutMaintenance(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 20) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts) + psubs := getGossipsubs(ctx, hosts) - var msgs []*Subscription - for _, ps := range psubs[1:] { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) + var msgs []*Subscription + for _, ps := range psubs[1:] { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } + + msgs = append(msgs, subch) } - msgs = append(msgs, subch) - } + denseConnect(t, hosts) - denseConnect(t, hosts) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + owner := 0 - owner := 0 + psubs[owner].Publish("foobar", msg) - psubs[owner].Publish("foobar", msg) + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } + } + } + // unsubscribe all peers to exercise fanout maintenance for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") - } + sub.Cancel() } - } + msgs = nil - // unsubscribe all peers to exercise fanout maintenance - for _, sub := range msgs { - sub.Cancel() - } - msgs = nil + // wait for heartbeats + time.Sleep(time.Second * 2) - // wait for heartbeats - time.Sleep(time.Second * 2) + // resubscribe and repeat + for _, ps := range psubs[1:] { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - // resubscribe and repeat - for _, ps := range psubs[1:] { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) + msgs = append(msgs, subch) } - msgs = append(msgs, subch) - } - - time.Sleep(time.Second * 2) + time.Sleep(time.Second * 2) - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := 0 + owner := 0 - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } func TestGossipsubFanoutExpiry(t *testing.T) { - GossipSubFanoutTTL = 1 * time.Second - defer func() { - GossipSubFanoutTTL = 60 * time.Second - }() + synctestTest(t, func(t *testing.T) { + GossipSubFanoutTTL = 1 * time.Second + defer func() { + GossipSubFanoutTTL = 60 * time.Second + }() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 10) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 10) - psubs := getGossipsubs(ctx, hosts) + psubs := getGossipsubs(ctx, hosts) - var msgs []*Subscription - for _, ps := range psubs[1:] { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + var msgs []*Subscription + for _, ps := range psubs[1:] { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - msgs = append(msgs, subch) - } + msgs = append(msgs, subch) + } - denseConnect(t, hosts) + denseConnect(t, hosts) - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - for i := 0; i < 5; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 5; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := 0 + owner := 0 - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } - psubs[0].eval <- func() { - if len(psubs[0].rt.(*GossipSubRouter).fanout) == 0 { - t.Fatal("owner has no fanout") + psubs[0].eval <- func() { + if len(psubs[0].rt.(*GossipSubRouter).fanout) == 0 { + t.Fatal("owner has no fanout") + } } - } - // wait for TTL to expire fanout peers in owner - time.Sleep(time.Second * 2) + // wait for TTL to expire fanout peers in owner + time.Sleep(time.Second * 2) - psubs[0].eval <- func() { - if len(psubs[0].rt.(*GossipSubRouter).fanout) > 0 { - t.Fatal("fanout hasn't expired") + psubs[0].eval <- func() { + if len(psubs[0].rt.(*GossipSubRouter).fanout) > 0 { + t.Fatal("fanout hasn't expired") + } } - } - // wait for it to run in the event loop - time.Sleep(10 * time.Millisecond) + // wait for it to run in the event loop + time.Sleep(10 * time.Millisecond) + }) } func TestGossipsubGossip(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 20) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts) + psubs := getGossipsubs(ctx, hosts) - var msgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + var msgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - msgs = append(msgs, subch) - } + msgs = append(msgs, subch) + } - denseConnect(t, hosts) + denseConnect(t, hosts) - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := mrand.Intn(len(psubs)) + owner := mrand.Intn(len(psubs)) - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } - } - // wait a bit to have some gossip interleaved - time.Sleep(time.Millisecond * 100) - } + // wait a bit to have some gossip interleaved + time.Sleep(time.Millisecond * 100) + } - // and wait for some gossip flushing - time.Sleep(time.Second * 2) + // and wait for some gossip flushing + time.Sleep(time.Second * 2) + }) } func TestGossipsubGossipPiggyback(t *testing.T) { - t.Skip("test no longer relevant; gossip propagation has become eager") - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 20) + synctestTest(t, func(t *testing.T) { + t.Skip("test no longer relevant; gossip propagation has become eager") + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts) + psubs := getGossipsubs(ctx, hosts) - var msgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) + var msgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } + + msgs = append(msgs, subch) } - msgs = append(msgs, subch) - } + var xmsgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe("bazcrux") + if err != nil { + t.Fatal(err) + } - var xmsgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe("bazcrux") - if err != nil { - t.Fatal(err) + xmsgs = append(xmsgs, subch) } - xmsgs = append(xmsgs, subch) - } - - denseConnect(t, hosts) + denseConnect(t, hosts) - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := mrand.Intn(len(psubs)) + owner := mrand.Intn(len(psubs)) - psubs[owner].Publish("foobar", msg) - psubs[owner].Publish("bazcrux", msg) + psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("bazcrux", msg) - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } - } - for _, sub := range xmsgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range xmsgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } - } - // wait a bit to have some gossip interleaved - time.Sleep(time.Millisecond * 100) - } + // wait a bit to have some gossip interleaved + time.Sleep(time.Millisecond * 100) + } - // and wait for some gossip flushing - time.Sleep(time.Second * 2) + // and wait for some gossip flushing + time.Sleep(time.Second * 2) + }) } func TestGossipsubGossipPropagation(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts) + hosts := getDefaultHosts(t, 20) + psubs := getGossipsubs(ctx, hosts) - hosts1 := hosts[:GossipSubD+1] - hosts2 := append(hosts[GossipSubD+1:], hosts[0]) + hosts1 := hosts[:GossipSubD+1] + hosts2 := append(hosts[GossipSubD+1:], hosts[0]) - denseConnect(t, hosts1) - denseConnect(t, hosts2) + denseConnect(t, hosts1) + denseConnect(t, hosts2) - var msgs1 []*Subscription - for _, ps := range psubs[1 : GossipSubD+1] { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + var msgs1 []*Subscription + for _, ps := range psubs[1 : GossipSubD+1] { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - msgs1 = append(msgs1, subch) - } + msgs1 = append(msgs1, subch) + } - time.Sleep(time.Second * 1) + time.Sleep(time.Second * 1) - for i := 0; i < 10; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 10; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := 0 + owner := 0 - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs1 { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs1 { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } - - time.Sleep(time.Millisecond * 100) - - var msgs2 []*Subscription - for _, ps := range psubs[GossipSubD+1:] { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } - msgs2 = append(msgs2, subch) - } + time.Sleep(time.Millisecond * 100) - var collect [][]byte - for i := 0; i < 10; i++ { - for _, sub := range msgs2 { - got, err := sub.Next(ctx) + var msgs2 []*Subscription + for _, ps := range psubs[GossipSubD+1:] { + subch, err := ps.Subscribe("foobar") if err != nil { - t.Fatal(sub.err) + t.Fatal(err) } - collect = append(collect, got.Data) + + msgs2 = append(msgs2, subch) } - } - for i := 0; i < 10; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - gotit := false - for j := 0; j < len(collect); j++ { - if bytes.Equal(msg, collect[j]) { - gotit = true - break + var collect [][]byte + for i := 0; i < 10; i++ { + for _, sub := range msgs2 { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + collect = append(collect, got.Data) } } - if !gotit { - t.Fatalf("Didn't get message %s", string(msg)) + + for i := 0; i < 10; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + gotit := false + for j := 0; j < len(collect); j++ { + if bytes.Equal(msg, collect[j]) { + gotit = true + break + } + } + if !gotit { + t.Fatalf("Didn't get message %s", string(msg)) + } } - } + }) } func TestGossipsubPrune(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 20) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts) + psubs := getGossipsubs(ctx, hosts) - var msgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + var msgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - msgs = append(msgs, subch) - } + msgs = append(msgs, subch) + } - denseConnect(t, hosts) + denseConnect(t, hosts) - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - // disconnect some peers from the mesh to get some PRUNEs - for _, sub := range msgs[:5] { - sub.Cancel() - } + // disconnect some peers from the mesh to get some PRUNEs + for _, sub := range msgs[:5] { + sub.Cancel() + } - // wait a bit to take effect - time.Sleep(time.Millisecond * 100) + // wait a bit to take effect + time.Sleep(time.Millisecond * 100) - for i := 0; i < 10; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 10; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := mrand.Intn(len(psubs)) + owner := mrand.Intn(len(psubs)) - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs[5:] { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs[5:] { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } func TestGossipsubPruneBackoffTime(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 10) - - // App specific score that we'll change later. - currentScoreForHost0 := int32(0) - - params := DefaultGossipSubParams() - params.HeartbeatInitialDelay = time.Millisecond * 10 - params.HeartbeatInterval = time.Millisecond * 100 - - psubs := getGossipsubs(ctx, hosts, WithGossipSubParams(params), WithPeerScore( - &PeerScoreParams{ - AppSpecificScore: func(p peer.ID) float64 { - if p == hosts[0].ID() { - return float64(atomic.LoadInt32(¤tScoreForHost0)) - } else { - return 0 - } - }, - AppSpecificWeight: 1, - DecayInterval: time.Second, - DecayToZero: 0.01, - }, - &PeerScoreThresholds{ - GossipThreshold: -1, - PublishThreshold: -1, - GraylistThreshold: -1, - })) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 10) - var msgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + // App specific score that we'll change later. + currentScoreForHost0 := int32(0) - msgs = append(msgs, subch) - } + params := DefaultGossipSubParams() + params.HeartbeatInitialDelay = time.Millisecond * 10 + params.HeartbeatInterval = time.Millisecond * 100 + + psubs := getGossipsubs(ctx, hosts, WithGossipSubParams(params), WithPeerScore( + &PeerScoreParams{ + AppSpecificScore: func(p peer.ID) float64 { + if p == hosts[0].ID() { + return float64(atomic.LoadInt32(¤tScoreForHost0)) + } else { + return 0 + } + }, + AppSpecificWeight: 1, + DecayInterval: time.Second, + DecayToZero: 0.01, + }, + &PeerScoreThresholds{ + GossipThreshold: -1, + PublishThreshold: -1, + GraylistThreshold: -1, + })) + + var msgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - connectAll(t, hosts) + msgs = append(msgs, subch) + } - // wait for heartbeats to build mesh - time.Sleep(time.Second) + connectAll(t, hosts) - pruneTime := time.Now() - // Flip the score. Host 0 should be pruned from everyone - atomic.StoreInt32(¤tScoreForHost0, -1000) + // wait for heartbeats to build mesh + time.Sleep(time.Second) - // wait for heartbeats to run and prune - time.Sleep(time.Second) + pruneTime := time.Now() + // Flip the score. Host 0 should be pruned from everyone + atomic.StoreInt32(¤tScoreForHost0, -1000) - wg := sync.WaitGroup{} - var missingBackoffs uint32 = 0 - for i := 1; i < 10; i++ { - wg.Add(1) - // Copy i so this func keeps the correct value in the closure. - var idx = i - // Run this check in the eval thunk so that we don't step over the heartbeat goroutine and trigger a race. - psubs[idx].rt.(*GossipSubRouter).p.eval <- func() { - defer wg.Done() - backoff, ok := psubs[idx].rt.(*GossipSubRouter).backoff["foobar"][hosts[0].ID()] - if !ok { - atomic.AddUint32(&missingBackoffs, 1) - } - if ok && backoff.Sub(pruneTime)-params.PruneBackoff > time.Second { - t.Errorf("backoff time should be equal to prune backoff (with some slack) was %v", backoff.Sub(pruneTime)-params.PruneBackoff) + // wait for heartbeats to run and prune + time.Sleep(time.Second) + + wg := sync.WaitGroup{} + var missingBackoffs uint32 = 0 + for i := 1; i < 10; i++ { + wg.Add(1) + // Copy i so this func keeps the correct value in the closure. + var idx = i + // Run this check in the eval thunk so that we don't step over the heartbeat goroutine and trigger a race. + psubs[idx].rt.(*GossipSubRouter).p.eval <- func() { + defer wg.Done() + backoff, ok := psubs[idx].rt.(*GossipSubRouter).backoff["foobar"][hosts[0].ID()] + if !ok { + atomic.AddUint32(&missingBackoffs, 1) + } + if ok && backoff.Sub(pruneTime)-params.PruneBackoff > time.Second { + t.Errorf("backoff time should be equal to prune backoff (with some slack) was %v", backoff.Sub(pruneTime)-params.PruneBackoff) + } } } - } - wg.Wait() + wg.Wait() - // Sometimes not all the peers will have updated their backoffs by this point. If the majority haven't we'll fail this test. - if missingBackoffs >= 5 { - t.Errorf("missing too many backoffs: %v", missingBackoffs) - } + // Sometimes not all the peers will have updated their backoffs by this point. If the majority haven't we'll fail this test. + if missingBackoffs >= 5 { + t.Errorf("missing too many backoffs: %v", missingBackoffs) + } - for i := 0; i < 10; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 10; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - // Don't publish from host 0, since everyone should have pruned it. - owner := mrand.Intn(len(psubs)-1) + 1 + // Don't publish from host 0, since everyone should have pruned it. + owner := mrand.Intn(len(psubs)-1) + 1 - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs[1:] { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs[1:] { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } func TestGossipsubGraft(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 20) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts) + psubs := getGossipsubs(ctx, hosts) - sparseConnect(t, hosts) + sparseConnect(t, hosts) - time.Sleep(time.Second * 1) + time.Sleep(time.Second * 1) - var msgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + var msgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - msgs = append(msgs, subch) + msgs = append(msgs, subch) - // wait for announce to propagate - time.Sleep(time.Millisecond * 100) - } + // wait for announce to propagate + time.Sleep(time.Millisecond * 100) + } - time.Sleep(time.Second * 1) + time.Sleep(time.Second * 1) - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := mrand.Intn(len(psubs)) + owner := mrand.Intn(len(psubs)) - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } func TestGossipsubRemovePeer(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 20) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts) + psubs := getGossipsubs(ctx, hosts) - var msgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + var msgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - msgs = append(msgs, subch) - } + msgs = append(msgs, subch) + } - denseConnect(t, hosts) + denseConnect(t, hosts) - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - // disconnect some peers to exercise RemovePeer paths - for _, host := range hosts[:5] { - host.Close() - } + // disconnect some peers to exercise RemovePeer paths + for _, host := range hosts[:5] { + host.Close() + } - // wait a heartbeat - time.Sleep(time.Second * 1) + // wait a heartbeat + time.Sleep(time.Second * 1) - for i := 0; i < 10; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 10; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := 5 + mrand.Intn(len(psubs)-5) + owner := 5 + mrand.Intn(len(psubs)-5) - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs[5:] { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs[5:] { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } func TestGossipsubGraftPruneRetry(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 10) - psubs := getGossipsubs(ctx, hosts) - denseConnect(t, hosts) + hosts := getDefaultHosts(t, 10) + psubs := getGossipsubs(ctx, hosts) + denseConnect(t, hosts) - var topics []string - var msgs [][]*Subscription - for i := 0; i < 35; i++ { - topic := fmt.Sprintf("topic%d", i) - topics = append(topics, topic) + var topics []string + var msgs [][]*Subscription + for i := 0; i < 35; i++ { + topic := fmt.Sprintf("topic%d", i) + topics = append(topics, topic) - var subs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) - } + var subs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } - subs = append(subs, subch) + subs = append(subs, subch) + } + msgs = append(msgs, subs) } - msgs = append(msgs, subs) - } - // wait for heartbeats to build meshes - time.Sleep(time.Second * 5) + // wait for heartbeats to build meshes + time.Sleep(time.Second * 5) - for i, topic := range topics { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i, topic := range topics { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := mrand.Intn(len(psubs)) + owner := mrand.Intn(len(psubs)) - psubs[owner].Publish(topic, msg) + psubs[owner].Publish(topic, msg) - for _, sub := range msgs[i] { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs[i] { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } func TestGossipsubControlPiggyback(t *testing.T) { - t.Skip("travis regularly fails on this test") - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 10) - psubs := getGossipsubs(ctx, hosts) - denseConnect(t, hosts) + hosts := getDefaultHosts(t, 10) + psubs := getGossipsubs(ctx, hosts) + denseConnect(t, hosts) - for _, ps := range psubs { - subch, err := ps.Subscribe("flood") - if err != nil { - t.Fatal(err) - } - go func(sub *Subscription) { - for { - _, err := sub.Next(ctx) - if err != nil { - break + for _, ps := range psubs { + subch, err := ps.Subscribe("flood") + if err != nil { + t.Fatal(err) + } + go func(sub *Subscription) { + for { + _, err := sub.Next(ctx) + if err != nil { + break + } } + }(subch) + } + + time.Sleep(time.Second * 1) + + // create a background flood of messages that overloads the queues + done := make(chan struct{}) + go func() { + owner := mrand.Intn(len(psubs)) + for i := 0; i < 10000; i++ { + msg := []byte("background flooooood") + psubs[owner].Publish("flood", msg) } - }(subch) - } + done <- struct{}{} + }() - time.Sleep(time.Second * 1) + // subscribe to a bunch of topics in the meantime -- this should + // result in some dropped control messages, with subsequent piggybacking + // in the background flood + var topics []string + var msgs [][]*Subscription + for i := 0; i < 5; i++ { + topic := fmt.Sprintf("topic%d", i) + topics = append(topics, topic) - // create a background flood of messages that overloads the queues - done := make(chan struct{}) - go func() { - owner := mrand.Intn(len(psubs)) - for i := 0; i < 10000; i++ { - msg := []byte("background flooooood") - psubs[owner].Publish("flood", msg) - } - done <- struct{}{} - }() - - time.Sleep(time.Millisecond * 20) - - // and subscribe to a bunch of topics in the meantime -- this should - // result in some dropped control messages, with subsequent piggybacking - // in the background flood - var topics []string - var msgs [][]*Subscription - for i := 0; i < 5; i++ { - topic := fmt.Sprintf("topic%d", i) - topics = append(topics, topic) + var subs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } - var subs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + subs = append(subs, subch) } - - subs = append(subs, subch) + msgs = append(msgs, subs) } - msgs = append(msgs, subs) - } - // wait for the flood to stop - <-done + // wait for the flood to stop + <-done - // and test that we have functional overlays - for i, topic := range topics { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + // wait for heartbeats to establish meshes for the new topics + time.Sleep(time.Second * 2) - owner := mrand.Intn(len(psubs)) + // and test that we have functional overlays + for i, topic := range topics { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - psubs[owner].Publish(topic, msg) + owner := mrand.Intn(len(psubs)) - for _, sub := range msgs[i] { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + psubs[owner].Publish(topic, msg) + + for _, sub := range msgs[i] { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } func TestMixedGossipsub(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 30) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 30) - gsubs := getGossipsubs(ctx, hosts[:20]) - fsubs := getPubsubs(ctx, hosts[20:]) - psubs := append(gsubs, fsubs...) + gsubs := getGossipsubs(ctx, hosts[:20]) + fsubs := getPubsubs(ctx, hosts[20:]) + psubs := append(gsubs, fsubs...) - var msgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) - } + var msgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - msgs = append(msgs, subch) - } + msgs = append(msgs, subch) + } - sparseConnect(t, hosts) + sparseConnect(t, hosts) - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - for i := 0; i < 100; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + for i := 0; i < 100; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - owner := mrand.Intn(len(psubs)) + owner := mrand.Intn(len(psubs)) - psubs[owner].Publish("foobar", msg) + psubs[owner].Publish("foobar", msg) - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) } func TestGossipsubMultihops(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 6) + hosts := getDefaultHosts(t, 6) - psubs := getGossipsubs(ctx, hosts) + psubs := getGossipsubs(ctx, hosts) - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) - connect(t, hosts[2], hosts[3]) - connect(t, hosts[3], hosts[4]) - connect(t, hosts[4], hosts[5]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) + connect(t, hosts[2], hosts[3]) + connect(t, hosts[3], hosts[4]) + connect(t, hosts[4], hosts[5]) - var subs []*Subscription - for i := 1; i < 6; i++ { - ch, err := psubs[i].Subscribe("foobar") - if err != nil { - t.Fatal(err) + var subs []*Subscription + for i := 1; i < 6; i++ { + ch, err := psubs[i].Subscribe("foobar") + if err != nil { + t.Fatal(err) + } + subs = append(subs, ch) } - subs = append(subs, ch) - } - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - msg := []byte("i like cats") - err := psubs[0].Publish("foobar", msg) - if err != nil { - t.Fatal(err) - } + msg := []byte("i like cats") + err := psubs[0].Publish("foobar", msg) + if err != nil { + t.Fatal(err) + } - // last node in the chain should get the message - select { - case out := <-subs[4].ch: - if !bytes.Equal(out.GetData(), msg) { - t.Fatal("got wrong data") + // last node in the chain should get the message + select { + case out := <-subs[4].ch: + if !bytes.Equal(out.GetData(), msg) { + t.Fatal("got wrong data") + } + case <-time.After(time.Second * 5): + t.Fatal("timed out waiting for message") } - case <-time.After(time.Second * 5): - t.Fatal("timed out waiting for message") - } + }) } func TestGossipsubTreeTopology(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - hosts := getDefaultHosts(t, 10) - psubs := getGossipsubs(ctx, hosts) - - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) - connect(t, hosts[1], hosts[4]) - connect(t, hosts[2], hosts[3]) - connect(t, hosts[0], hosts[5]) - connect(t, hosts[5], hosts[6]) - connect(t, hosts[5], hosts[8]) - connect(t, hosts[6], hosts[7]) - connect(t, hosts[8], hosts[9]) - - /* - [0] -> [1] -> [2] -> [3] - | L->[4] - v - [5] -> [6] -> [7] - | - v - [8] -> [9] - */ - - var chs []*Subscription - for _, ps := range psubs { - ch, err := ps.Subscribe("fizzbuzz") - if err != nil { - t.Fatal(err) - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - chs = append(chs, ch) - } + hosts := getDefaultHosts(t, 10) + psubs := getGossipsubs(ctx, hosts) + + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) + connect(t, hosts[1], hosts[4]) + connect(t, hosts[2], hosts[3]) + connect(t, hosts[0], hosts[5]) + connect(t, hosts[5], hosts[6]) + connect(t, hosts[5], hosts[8]) + connect(t, hosts[6], hosts[7]) + connect(t, hosts[8], hosts[9]) + + /* + [0] -> [1] -> [2] -> [3] + | L->[4] + v + [5] -> [6] -> [7] + | + v + [8] -> [9] + */ + + var chs []*Subscription + for _, ps := range psubs { + ch, err := ps.Subscribe("fizzbuzz") + if err != nil { + t.Fatal(err) + } + + chs = append(chs, ch) + } - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - assertPeerLists(t, hosts, psubs[0], 1, 5) - assertPeerLists(t, hosts, psubs[1], 0, 2, 4) - assertPeerLists(t, hosts, psubs[2], 1, 3) + assertPeerLists(t, hosts, psubs[0], 1, 5) + assertPeerLists(t, hosts, psubs[1], 0, 2, 4) + assertPeerLists(t, hosts, psubs[2], 1, 3) - checkMessageRouting(t, "fizzbuzz", []*PubSub{psubs[9], psubs[3]}, chs) + checkMessageRouting(t, "fizzbuzz", []*PubSub{psubs[9], psubs[3]}, chs) + }) } // this tests overlay bootstrapping through px in Gossipsub v1.1 // we start with a star topology and rely on px through prune to build the mesh func TestGossipsubStarTopology(t *testing.T) { - originalGossipSubD := GossipSubD - GossipSubD = 4 - originalGossipSubDhi := GossipSubDhi - GossipSubDhi = GossipSubD + 1 - originalGossipSubDlo := GossipSubDlo - GossipSubDlo = GossipSubD - 1 - originalGossipSubDscore := GossipSubDscore - GossipSubDscore = GossipSubDlo - defer func() { - GossipSubD = originalGossipSubD - GossipSubDhi = originalGossipSubDhi - GossipSubDlo = originalGossipSubDlo - GossipSubDscore = originalGossipSubDscore - }() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts, WithPeerExchange(true), WithFloodPublish(true)) - - // configure the center of the star with a very low D - psubs[0].eval <- func() { - gs := psubs[0].rt.(*GossipSubRouter) - gs.params.D = 0 - gs.params.Dlo = 0 - gs.params.Dhi = 0 - gs.params.Dscore = 0 - } + synctestTest(t, func(t *testing.T) { + originalGossipSubD := GossipSubD + GossipSubD = 4 + originalGossipSubDhi := GossipSubDhi + GossipSubDhi = GossipSubD + 1 + originalGossipSubDlo := GossipSubDlo + GossipSubDlo = GossipSubD - 1 + originalGossipSubDscore := GossipSubDscore + GossipSubDscore = GossipSubDlo + defer func() { + GossipSubD = originalGossipSubD + GossipSubDhi = originalGossipSubDhi + GossipSubDlo = originalGossipSubDlo + GossipSubDscore = originalGossipSubDscore + }() - // add all peer addresses to the peerstores - // this is necessary because we can't have signed address records witout identify - // pushing them - for i := range hosts { - for j := range hosts { - if i == j { - continue + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + hosts := getDefaultHosts(t, 20) + psubs := getGossipsubs(ctx, hosts, WithPeerExchange(true), WithFloodPublish(true)) + + // configure the center of the star with a very low D + psubs[0].eval <- func() { + gs := psubs[0].rt.(*GossipSubRouter) + gs.params.D = 0 + gs.params.Dlo = 0 + gs.params.Dhi = 0 + gs.params.Dscore = 0 + } + + // add all peer addresses to the peerstores + // this is necessary because we can't have signed address records witout identify + // pushing them + for i := range hosts { + for j := range hosts { + if i == j { + continue + } + hosts[i].Peerstore().AddAddrs(hosts[j].ID(), hosts[j].Addrs(), peerstore.PermanentAddrTTL) } - hosts[i].Peerstore().AddAddrs(hosts[j].ID(), hosts[j].Addrs(), peerstore.PermanentAddrTTL) } - } - // build the star - for i := 1; i < 20; i++ { - connect(t, hosts[0], hosts[i]) - } + // build the star + for i := 1; i < 20; i++ { + connect(t, hosts[0], hosts[i]) + } - time.Sleep(time.Second) + time.Sleep(time.Second) - // build the mesh - var subs []*Subscription - for _, ps := range psubs { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + // build the mesh + var subs []*Subscription + for _, ps := range psubs { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } + subs = append(subs, sub) } - subs = append(subs, sub) - } - // wait a bit for the mesh to build - time.Sleep(10 * time.Second) + // wait a bit for the mesh to build + time.Sleep(10 * time.Second) - // check that all peers have > 1 connection - for i, h := range hosts { - if len(h.Network().Conns()) == 1 { - t.Errorf("peer %d has ony a single connection", i) + // check that all peers have > 1 connection + for i, h := range hosts { + if len(h.Network().Conns()) == 1 { + t.Errorf("peer %d has ony a single connection", i) + } } - } - // send a message from each peer and assert it was propagated - for i := 0; i < 20; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[i].Publish("test", msg) + // send a message from each peer and assert it was propagated + for i := 0; i < 20; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[i].Publish("test", msg) - for _, sub := range subs { - assertReceive(t, sub, msg) + for _, sub := range subs { + assertReceive(t, sub, msg) + } } - } + }) } // this tests overlay bootstrapping through px in Gossipsub v1.1, with addresses // exchanged in signed peer records. // we start with a star topology and rely on px through prune to build the mesh func TestGossipsubStarTopologyWithSignedPeerRecords(t *testing.T) { - originalGossipSubD := GossipSubD - GossipSubD = 4 - originalGossipSubDhi := GossipSubDhi - GossipSubDhi = GossipSubD + 1 - originalGossipSubDlo := GossipSubDlo - GossipSubDlo = GossipSubD - 1 - originalGossipSubDscore := GossipSubDscore - GossipSubDscore = GossipSubDlo - defer func() { - GossipSubD = originalGossipSubD - GossipSubDhi = originalGossipSubDhi - GossipSubDlo = originalGossipSubDlo - GossipSubDscore = originalGossipSubDscore - }() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts, WithPeerExchange(true), WithFloodPublish(true)) - - // configure the center of the star with a very low D - psubs[0].eval <- func() { - gs := psubs[0].rt.(*GossipSubRouter) - gs.params.D = 0 - gs.params.Dlo = 0 - gs.params.Dhi = 0 - gs.params.Dscore = 0 - } + synctestTest(t, func(t *testing.T) { + originalGossipSubD := GossipSubD + GossipSubD = 4 + originalGossipSubDhi := GossipSubDhi + GossipSubDhi = GossipSubD + 1 + originalGossipSubDlo := GossipSubDlo + GossipSubDlo = GossipSubD - 1 + originalGossipSubDscore := GossipSubDscore + GossipSubDscore = GossipSubDlo + defer func() { + GossipSubD = originalGossipSubD + GossipSubDhi = originalGossipSubDhi + GossipSubDlo = originalGossipSubDlo + GossipSubDscore = originalGossipSubDscore + }() - // manually create signed peer records for each host and add them to the - // peerstore of the center of the star, which is doing the bootstrapping - for i := range hosts[1:] { - privKey := hosts[i].Peerstore().PrivKey(hosts[i].ID()) - if privKey == nil { - t.Fatalf("unable to get private key for host %s", hosts[i].ID()) - } - ai := host.InfoFromHost(hosts[i]) - rec := peer.PeerRecordFromAddrInfo(*ai) - signedRec, err := record.Seal(rec, privKey) - if err != nil { - t.Fatalf("error creating signed peer record: %s", err) - } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - cab, ok := peerstore.GetCertifiedAddrBook(hosts[0].Peerstore()) - if !ok { - t.Fatal("peerstore does not implement CertifiedAddrBook") + hosts := getDefaultHosts(t, 20) + psubs := getGossipsubs(ctx, hosts, WithPeerExchange(true), WithFloodPublish(true)) + + // configure the center of the star with a very low D + psubs[0].eval <- func() { + gs := psubs[0].rt.(*GossipSubRouter) + gs.params.D = 0 + gs.params.Dlo = 0 + gs.params.Dhi = 0 + gs.params.Dscore = 0 } - _, err = cab.ConsumePeerRecord(signedRec, peerstore.PermanentAddrTTL) - if err != nil { - t.Fatalf("error adding signed peer record: %s", err) + + // manually create signed peer records for each host and add them to the + // peerstore of the center of the star, which is doing the bootstrapping + for i := range hosts[1:] { + privKey := hosts[i].Peerstore().PrivKey(hosts[i].ID()) + if privKey == nil { + t.Fatalf("unable to get private key for host %s", hosts[i].ID()) + } + ai := host.InfoFromHost(hosts[i]) + rec := peer.PeerRecordFromAddrInfo(*ai) + signedRec, err := record.Seal(rec, privKey) + if err != nil { + t.Fatalf("error creating signed peer record: %s", err) + } + + cab, ok := peerstore.GetCertifiedAddrBook(hosts[0].Peerstore()) + if !ok { + t.Fatal("peerstore does not implement CertifiedAddrBook") + } + _, err = cab.ConsumePeerRecord(signedRec, peerstore.PermanentAddrTTL) + if err != nil { + t.Fatalf("error adding signed peer record: %s", err) + } } - } - // build the star - for i := 1; i < 20; i++ { - connect(t, hosts[0], hosts[i]) - } + // build the star + for i := 1; i < 20; i++ { + connect(t, hosts[0], hosts[i]) + } - time.Sleep(time.Second) + time.Sleep(time.Second) - // build the mesh - var subs []*Subscription - for _, ps := range psubs { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + // build the mesh + var subs []*Subscription + for _, ps := range psubs { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } + subs = append(subs, sub) } - subs = append(subs, sub) - } - // wait a bit for the mesh to build - time.Sleep(10 * time.Second) + // wait a bit for the mesh to build + time.Sleep(10 * time.Second) - // check that all peers have > 1 connection - for i, h := range hosts { - if len(h.Network().Conns()) == 1 { - t.Errorf("peer %d has ony a single connection", i) + // check that all peers have > 1 connection + for i, h := range hosts { + if len(h.Network().Conns()) == 1 { + t.Errorf("peer %d has ony a single connection", i) + } } - } - // send a message from each peer and assert it was propagated - for i := 0; i < 20; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[i].Publish("test", msg) + // send a message from each peer and assert it was propagated + for i := 0; i < 20; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[i].Publish("test", msg) - for _, sub := range subs { - assertReceive(t, sub, msg) + for _, sub := range subs { + assertReceive(t, sub, msg) + } } - } + }) } func TestGossipsubDirectPeers(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - h := getDefaultHosts(t, 3) - psubs := []*PubSub{ - getGossipsub(ctx, h[0], WithDirectConnectTicks(2)), - getGossipsub(ctx, h[1], WithDirectPeers([]peer.AddrInfo{{ID: h[2].ID(), Addrs: h[2].Addrs()}}), WithDirectConnectTicks(2)), - getGossipsub(ctx, h[2], WithDirectPeers([]peer.AddrInfo{{ID: h[1].ID(), Addrs: h[1].Addrs()}}), WithDirectConnectTicks(2)), - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - connect(t, h[0], h[1]) - connect(t, h[0], h[2]) + h := getDefaultHosts(t, 3) + psubs := []*PubSub{ + getGossipsub(ctx, h[0], WithDirectConnectTicks(2)), + getGossipsub(ctx, h[1], WithDirectPeers([]peer.AddrInfo{{ID: h[2].ID(), Addrs: h[2].Addrs()}}), WithDirectConnectTicks(2)), + getGossipsub(ctx, h[2], WithDirectPeers([]peer.AddrInfo{{ID: h[1].ID(), Addrs: h[1].Addrs()}}), WithDirectConnectTicks(2)), + } - // verify that the direct peers connected - time.Sleep(2 * time.Second) - if len(h[1].Network().ConnsToPeer(h[2].ID())) == 0 { - t.Fatal("expected a connection between direct peers") - } + connect(t, h[0], h[1]) + connect(t, h[0], h[2]) - // build the mesh - var subs []*Subscription - for _, ps := range psubs { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + // verify that the direct peers connected + time.Sleep(2 * time.Second) + if len(h[1].Network().ConnsToPeer(h[2].ID())) == 0 { + t.Fatal("expected a connection between direct peers") + } + + // build the mesh + var subs []*Subscription + for _, ps := range psubs { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } + subs = append(subs, sub) } - subs = append(subs, sub) - } - time.Sleep(time.Second) + time.Sleep(time.Second) - // publish some messages - for i := 0; i < 3; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[i].Publish("test", msg) + // publish some messages + for i := 0; i < 3; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[i].Publish("test", msg) - for _, sub := range subs { - assertReceive(t, sub, msg) + for _, sub := range subs { + assertReceive(t, sub, msg) + } } - } - // disconnect the direct peers to test reconnection - for _, c := range h[1].Network().ConnsToPeer(h[2].ID()) { - c.Close() - } + // disconnect the direct peers to test reconnection + for _, c := range h[1].Network().ConnsToPeer(h[2].ID()) { + c.Close() + } - time.Sleep(5 * time.Second) + time.Sleep(5 * time.Second) - if len(h[1].Network().ConnsToPeer(h[2].ID())) == 0 { - t.Fatal("expected a connection between direct peers") - } + if len(h[1].Network().ConnsToPeer(h[2].ID())) == 0 { + t.Fatal("expected a connection between direct peers") + } - // publish some messages - for i := 0; i < 3; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[i].Publish("test", msg) + // publish some messages + for i := 0; i < 3; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[i].Publish("test", msg) - for _, sub := range subs { - assertReceive(t, sub, msg) + for _, sub := range subs { + assertReceive(t, sub, msg) + } } - } + }) } func TestGossipsubDynamicDirectPeers(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - h := getDefaultHosts(t, 3) - psubs := []*PubSub{ - getGossipsub(ctx, h[0], WithDirectConnectTicks(2)), - getGossipsub(ctx, h[1], WithDirectConnectTicks(2)), - getGossipsub(ctx, h[2], WithDirectConnectTicks(2)), - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - listDirectPeers := func(psb *PubSub) int { - directPeers := 0 - gspRt, _ := psb.rt.(*GossipSubRouter) - fn := func() { - directPeers = len(gspRt.direct) + h := getDefaultHosts(t, 3) + psubs := []*PubSub{ + getGossipsub(ctx, h[0], WithDirectConnectTicks(2)), + getGossipsub(ctx, h[1], WithDirectConnectTicks(2)), + getGossipsub(ctx, h[2], WithDirectConnectTicks(2)), } - psb.syncEval(fn) - return directPeers - } - // test dinamic addition of direct-peers to h[1] and h[2] - psubs[1].AddDirectPeer(peer.AddrInfo{ID: h[2].ID(), Addrs: h[2].Addrs()}) - psubs[2].AddDirectPeer(peer.AddrInfo{ID: h[1].ID(), Addrs: h[1].Addrs()}) + listDirectPeers := func(psb *PubSub) int { + directPeers := 0 + gspRt, _ := psb.rt.(*GossipSubRouter) + fn := func() { + directPeers = len(gspRt.direct) + } + psb.syncEval(fn) + return directPeers + } - // give enough time to the state machine to process the direct additions - time.Sleep(time.Second) + // test dinamic addition of direct-peers to h[1] and h[2] + psubs[1].AddDirectPeer(peer.AddrInfo{ID: h[2].ID(), Addrs: h[2].Addrs()}) + psubs[2].AddDirectPeer(peer.AddrInfo{ID: h[1].ID(), Addrs: h[1].Addrs()}) - if listDirectPeers(psubs[1]) < 1 || listDirectPeers(psubs[2]) < 1 { - t.Fatal("expected 1 direct peer at both gsp rts") - } + // give enough time to the state machine to process the direct additions + time.Sleep(time.Second) - // remove peer from direct from directPeers - psubs[1].RemoveDirectPeer(h[2].ID()) - psubs[2].RemoveDirectPeer(h[1].ID()) + if listDirectPeers(psubs[1]) < 1 || listDirectPeers(psubs[2]) < 1 { + t.Fatal("expected 1 direct peer at both gsp rts") + } - // give enough time to the state machine to process the direct additions - time.Sleep(time.Second) + // remove peer from direct from directPeers + psubs[1].RemoveDirectPeer(h[2].ID()) + psubs[2].RemoveDirectPeer(h[1].ID()) - if listDirectPeers(psubs[1]) > 0 || listDirectPeers(psubs[2]) > 0 { - t.Fatal("expected no direct peers both gsp rts") - } + // give enough time to the state machine to process the direct additions + time.Sleep(time.Second) + + if listDirectPeers(psubs[1]) > 0 || listDirectPeers(psubs[2]) > 0 { + t.Fatal("expected no direct peers both gsp rts") + } + }) } func TestGossipSubPeerFilter(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - h := getDefaultHosts(t, 3) - psubs := []*PubSub{ - getGossipsub(ctx, h[0], WithPeerFilter(func(pid peer.ID, topic string) bool { - return pid == h[1].ID() - })), - getGossipsub(ctx, h[1], WithPeerFilter(func(pid peer.ID, topic string) bool { - return pid == h[0].ID() - })), - getGossipsub(ctx, h[2]), - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - connect(t, h[0], h[1]) - connect(t, h[0], h[2]) + h := getDefaultHosts(t, 3) + psubs := []*PubSub{ + getGossipsub(ctx, h[0], WithPeerFilter(func(pid peer.ID, topic string) bool { + return pid == h[1].ID() + })), + getGossipsub(ctx, h[1], WithPeerFilter(func(pid peer.ID, topic string) bool { + return pid == h[0].ID() + })), + getGossipsub(ctx, h[2]), + } - // Join all peers - var subs []*Subscription - for _, ps := range psubs { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + connect(t, h[0], h[1]) + connect(t, h[0], h[2]) + + // Join all peers + var subs []*Subscription + for _, ps := range psubs { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } + subs = append(subs, sub) } - subs = append(subs, sub) - } - time.Sleep(time.Second) + time.Sleep(time.Second) - msg := []byte("message") + msg := []byte("message") - psubs[0].Publish("test", msg) - assertReceive(t, subs[1], msg) - assertNeverReceives(t, subs[2], time.Second) + psubs[0].Publish("test", msg) + assertReceive(t, subs[1], msg) + assertNeverReceives(t, subs[2], time.Second) - psubs[1].Publish("test", msg) - assertReceive(t, subs[0], msg) - assertNeverReceives(t, subs[2], time.Second) + psubs[1].Publish("test", msg) + assertReceive(t, subs[0], msg) + assertNeverReceives(t, subs[2], time.Second) + }) } func TestGossipsubDirectPeersFanout(t *testing.T) { // regression test for #371 - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - h := getDefaultHosts(t, 3) - psubs := []*PubSub{ - getGossipsub(ctx, h[0]), - getGossipsub(ctx, h[1], WithDirectPeers([]peer.AddrInfo{{ID: h[2].ID(), Addrs: h[2].Addrs()}})), - getGossipsub(ctx, h[2], WithDirectPeers([]peer.AddrInfo{{ID: h[1].ID(), Addrs: h[1].Addrs()}})), - } + synctestTest(t, func(t *testing.T) { - connect(t, h[0], h[1]) - connect(t, h[0], h[2]) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - // Join all peers except h2 - var subs []*Subscription - for _, ps := range psubs[:2] { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + h := getDefaultHosts(t, 3) + psubs := []*PubSub{ + getGossipsub(ctx, h[0]), + getGossipsub(ctx, h[1], WithDirectPeers([]peer.AddrInfo{{ID: h[2].ID(), Addrs: h[2].Addrs()}})), + getGossipsub(ctx, h[2], WithDirectPeers([]peer.AddrInfo{{ID: h[1].ID(), Addrs: h[1].Addrs()}})), } - subs = append(subs, sub) - } - time.Sleep(time.Second) + connect(t, h[0], h[1]) + connect(t, h[0], h[2]) + + // Join all peers except h2 + var subs []*Subscription + for _, ps := range psubs[:2] { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } + subs = append(subs, sub) + } + + time.Sleep(time.Second) - // h2 publishes some messages to build a fanout - for i := 0; i < 3; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[2].Publish("test", msg) + // h2 publishes some messages to build a fanout + for i := 0; i < 3; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[2].Publish("test", msg) - for _, sub := range subs { - assertReceive(t, sub, msg) + for _, sub := range subs { + assertReceive(t, sub, msg) + } } - } - // verify that h0 is in the fanout of h2, but not h1 who is a direct peer - result := make(chan bool, 2) - psubs[2].eval <- func() { - rt := psubs[2].rt.(*GossipSubRouter) - fanout := rt.fanout["test"] - _, ok := fanout[h[0].ID()] - result <- ok - _, ok = fanout[h[1].ID()] - result <- ok - } + // verify that h0 is in the fanout of h2, but not h1 who is a direct peer + result := make(chan bool, 2) + psubs[2].eval <- func() { + rt := psubs[2].rt.(*GossipSubRouter) + fanout := rt.fanout["test"] + _, ok := fanout[h[0].ID()] + result <- ok + _, ok = fanout[h[1].ID()] + result <- ok + } - inFanout := <-result - if !inFanout { - t.Fatal("expected peer 0 to be in fanout") - } + inFanout := <-result + if !inFanout { + t.Fatal("expected peer 0 to be in fanout") + } - inFanout = <-result - if inFanout { - t.Fatal("expected peer 1 to not be in fanout") - } + inFanout = <-result + if inFanout { + t.Fatal("expected peer 1 to not be in fanout") + } - // now subscribe h2 too and verify tht h0 is in the mesh but not h1 - _, err := psubs[2].Subscribe("test") - if err != nil { - t.Fatal(err) - } + // now subscribe h2 too and verify tht h0 is in the mesh but not h1 + _, err := psubs[2].Subscribe("test") + if err != nil { + t.Fatal(err) + } - time.Sleep(2 * time.Second) + time.Sleep(2 * time.Second) - psubs[2].eval <- func() { - rt := psubs[2].rt.(*GossipSubRouter) - mesh := rt.mesh["test"] - _, ok := mesh[h[0].ID()] - result <- ok - _, ok = mesh[h[1].ID()] - result <- ok - } + psubs[2].eval <- func() { + rt := psubs[2].rt.(*GossipSubRouter) + mesh := rt.mesh["test"] + _, ok := mesh[h[0].ID()] + result <- ok + _, ok = mesh[h[1].ID()] + result <- ok + } - inMesh := <-result - if !inMesh { - t.Fatal("expected peer 0 to be in mesh") - } + inMesh := <-result + if !inMesh { + t.Fatal("expected peer 0 to be in mesh") + } - inMesh = <-result - if inMesh { - t.Fatal("expected peer 1 to not be in mesh") - } + inMesh = <-result + if inMesh { + t.Fatal("expected peer 1 to not be in mesh") + } + }) } func TestGossipsubFloodPublish(t *testing.T) { // uses a star topology without PX and publishes from the star to verify that all // messages get received - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts, WithFloodPublish(true)) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - // build the star - for i := 1; i < 20; i++ { - connect(t, hosts[0], hosts[i]) - } + hosts := getDefaultHosts(t, 20) + psubs := getGossipsubs(ctx, hosts, WithFloodPublish(true)) - // build the (partial, unstable) mesh - var subs []*Subscription - for _, ps := range psubs { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + // build the star + for i := 1; i < 20; i++ { + connect(t, hosts[0], hosts[i]) } - subs = append(subs, sub) - } - time.Sleep(time.Second) + // build the (partial, unstable) mesh + var subs []*Subscription + for _, ps := range psubs { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } + subs = append(subs, sub) + } - // send a message from the star and assert it was received - for i := 0; i < 20; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[0].Publish("test", msg) + time.Sleep(time.Second) + + // send a message from the star and assert it was received + for i := 0; i < 20; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[0].Publish("test", msg) - for _, sub := range subs { - assertReceive(t, sub, msg) + for _, sub := range subs { + assertReceive(t, sub, msg) + } } - } + }) } func TestGossipsubEnoughPeers(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts) + hosts := getDefaultHosts(t, 20) + psubs := getGossipsubs(ctx, hosts) - for _, ps := range psubs { - _, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + for _, ps := range psubs { + _, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } } - } - // at this point we have no connections and no mesh, so EnoughPeers should return false - res := make(chan bool, 1) - psubs[0].eval <- func() { - res <- psubs[0].rt.EnoughPeers("test", 0) - } - enough := <-res - if enough { - t.Fatal("should not have enough peers") - } + // at this point we have no connections and no mesh, so EnoughPeers should return false + res := make(chan bool, 1) + psubs[0].eval <- func() { + res <- psubs[0].rt.EnoughPeers("test", 0) + } + enough := <-res + if enough { + t.Fatal("should not have enough peers") + } - // connect them densly to build up the mesh - denseConnect(t, hosts) + // connect them densly to build up the mesh + denseConnect(t, hosts) - time.Sleep(3 * time.Second) + time.Sleep(3 * time.Second) - psubs[0].eval <- func() { - res <- psubs[0].rt.EnoughPeers("test", 0) - } - enough = <-res - if !enough { - t.Fatal("should have enough peers") - } + psubs[0].eval <- func() { + res <- psubs[0].rt.EnoughPeers("test", 0) + } + enough = <-res + if !enough { + t.Fatal("should have enough peers") + } + }) } func TestGossipsubCustomParams(t *testing.T) { // in this test we score sinkhole a peer to exercise code paths relative to negative scores - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - params := DefaultGossipSubParams() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - wantedFollowTime := 1 * time.Second - params.IWantFollowupTime = wantedFollowTime + params := DefaultGossipSubParams() - customGossipFactor := 0.12 - params.GossipFactor = customGossipFactor + wantedFollowTime := 1 * time.Second + params.IWantFollowupTime = wantedFollowTime - wantedMaxPendingConns := 23 - params.MaxPendingConnections = wantedMaxPendingConns - hosts := getDefaultHosts(t, 1) - psubs := getGossipsubs(ctx, hosts, - WithGossipSubParams(params)) + customGossipFactor := 0.12 + params.GossipFactor = customGossipFactor - if len(psubs) != 1 { - t.Fatalf("incorrect number of pusbub objects received: wanted %d but got %d", 1, len(psubs)) - } + wantedMaxPendingConns := 23 + params.MaxPendingConnections = wantedMaxPendingConns + hosts := getDefaultHosts(t, 1) + psubs := getGossipsubs(ctx, hosts, + WithGossipSubParams(params)) - rt, ok := psubs[0].rt.(*GossipSubRouter) - if !ok { - t.Fatal("Did not get gossip sub router from pub sub object") - } + if len(psubs) != 1 { + t.Fatalf("incorrect number of pusbub objects received: wanted %d but got %d", 1, len(psubs)) + } - if rt.params.IWantFollowupTime != wantedFollowTime { - t.Errorf("Wanted %d of param GossipSubIWantFollowupTime but got %d", wantedFollowTime, rt.params.IWantFollowupTime) - } - if rt.params.GossipFactor != customGossipFactor { - t.Errorf("Wanted %f of param GossipSubGossipFactor but got %f", customGossipFactor, rt.params.GossipFactor) - } - if rt.params.MaxPendingConnections != wantedMaxPendingConns { - t.Errorf("Wanted %d of param GossipSubMaxPendingConnections but got %d", wantedMaxPendingConns, rt.params.MaxPendingConnections) - } + rt, ok := psubs[0].rt.(*GossipSubRouter) + if !ok { + t.Fatal("Did not get gossip sub router from pub sub object") + } + + if rt.params.IWantFollowupTime != wantedFollowTime { + t.Errorf("Wanted %d of param GossipSubIWantFollowupTime but got %d", wantedFollowTime, rt.params.IWantFollowupTime) + } + if rt.params.GossipFactor != customGossipFactor { + t.Errorf("Wanted %f of param GossipSubGossipFactor but got %f", customGossipFactor, rt.params.GossipFactor) + } + if rt.params.MaxPendingConnections != wantedMaxPendingConns { + t.Errorf("Wanted %d of param GossipSubMaxPendingConnections but got %d", wantedMaxPendingConns, rt.params.MaxPendingConnections) + } + }) } func TestGossipsubNegativeScore(t *testing.T) { // in this test we score sinkhole a peer to exercise code paths relative to negative scores - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - hosts := getDefaultHosts(t, 20) - psubs := getGossipsubs(ctx, hosts, - WithPeerScore( - &PeerScoreParams{ - AppSpecificScore: func(p peer.ID) float64 { - if p == hosts[0].ID() { - return -1000 - } else { - return 0 - } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + hosts := getDefaultHosts(t, 20) + psubs := getGossipsubs(ctx, hosts, + WithPeerScore( + &PeerScoreParams{ + AppSpecificScore: func(p peer.ID) float64 { + if p == hosts[0].ID() { + return -1000 + } else { + return 0 + } + }, + AppSpecificWeight: 1, + DecayInterval: time.Second, + DecayToZero: 0.01, }, - AppSpecificWeight: 1, - DecayInterval: time.Second, - DecayToZero: 0.01, - }, - &PeerScoreThresholds{ - GossipThreshold: -10, - PublishThreshold: -100, - GraylistThreshold: -10000, - })) + &PeerScoreThresholds{ + GossipThreshold: -10, + PublishThreshold: -100, + GraylistThreshold: -10000, + })) - denseConnect(t, hosts) + denseConnect(t, hosts) - var subs []*Subscription - for _, ps := range psubs { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + var subs []*Subscription + for _, ps := range psubs { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } + subs = append(subs, sub) } - subs = append(subs, sub) - } - time.Sleep(3 * time.Second) + time.Sleep(3 * time.Second) - for i := 0; i < 20; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[i%20].Publish("test", msg) - time.Sleep(20 * time.Millisecond) - } + for i := 0; i < 20; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[i%20].Publish("test", msg) + time.Sleep(20 * time.Millisecond) + } - // let the sinkholed peer try to emit gossip as well - time.Sleep(2 * time.Second) + // let the sinkholed peer try to emit gossip as well + time.Sleep(2 * time.Second) - // checks: - // 1. peer 0 should only receive its own message - // 2. peers 1-20 should not receive a message from peer 0, because it's not part of the mesh - // and its gossip is rejected - collectAll := func(sub *Subscription) []*Message { - var res []*Message - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel() + // checks: + // 1. peer 0 should only receive its own message + // 2. peers 1-20 should not receive a message from peer 0, because it's not part of the mesh + // and its gossip is rejected + collectAll := func(sub *Subscription) []*Message { + var res []*Message + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() - for { - msg, err := sub.Next(ctx) - if err != nil { - break + for { + msg, err := sub.Next(ctx) + if err != nil { + break + } + + res = append(res, msg) } - res = append(res, msg) + return res } - return res - } - - count := len(collectAll(subs[0])) - if count != 1 { - t.Fatalf("expected 1 message but got %d instead", count) - } + count := len(collectAll(subs[0])) + if count != 1 { + t.Fatalf("expected 1 message but got %d instead", count) + } - for _, sub := range subs[1:] { - all := collectAll(sub) - for _, m := range all { - if m.ReceivedFrom == hosts[0].ID() { - t.Fatal("received message from sinkholed peer") + for _, sub := range subs[1:] { + all := collectAll(sub) + for _, m := range all { + if m.ReceivedFrom == hosts[0].ID() { + t.Fatal("received message from sinkholed peer") + } } } - } + }) } func TestGossipsubScoreValidatorEx(t *testing.T) { // this is a test that of the two message drop responses from a validator - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - hosts := getDefaultHosts(t, 3) - psubs := getGossipsubs(ctx, hosts, - WithPeerScore( - &PeerScoreParams{ - AppSpecificScore: func(p peer.ID) float64 { return 0 }, - DecayInterval: time.Second, - DecayToZero: 0.01, - Topics: map[string]*TopicScoreParams{ - "test": { - TopicWeight: 1, - TimeInMeshQuantum: time.Second, - InvalidMessageDeliveriesWeight: -1, - InvalidMessageDeliveriesDecay: 0.9999, + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + hosts := getDefaultHosts(t, 3) + psubs := getGossipsubs(ctx, hosts, + WithPeerScore( + &PeerScoreParams{ + AppSpecificScore: func(p peer.ID) float64 { return 0 }, + DecayInterval: time.Second, + DecayToZero: 0.01, + Topics: map[string]*TopicScoreParams{ + "test": { + TopicWeight: 1, + TimeInMeshQuantum: time.Second, + InvalidMessageDeliveriesWeight: -1, + InvalidMessageDeliveriesDecay: 0.9999, + }, }, }, - }, - &PeerScoreThresholds{ - GossipThreshold: -10, - PublishThreshold: -100, - GraylistThreshold: -10000, - })) + &PeerScoreThresholds{ + GossipThreshold: -10, + PublishThreshold: -100, + GraylistThreshold: -10000, + })) - connectAll(t, hosts) + connectAll(t, hosts) - err := psubs[0].RegisterTopicValidator("test", func(ctx context.Context, p peer.ID, msg *Message) ValidationResult { - // we ignore host1 and reject host2 - if p == hosts[1].ID() { - return ValidationIgnore - } - if p == hosts[2].ID() { - return ValidationReject - } + err := psubs[0].RegisterTopicValidator("test", func(ctx context.Context, p peer.ID, msg *Message) ValidationResult { + // we ignore host1 and reject host2 + if p == hosts[1].ID() { + return ValidationIgnore + } + if p == hosts[2].ID() { + return ValidationReject + } - return ValidationAccept - }) - if err != nil { - t.Fatal(err) - } + return ValidationAccept + }) + if err != nil { + t.Fatal(err) + } - sub, err := psubs[0].Subscribe("test") - if err != nil { - t.Fatal(err) - } + sub, err := psubs[0].Subscribe("test") + if err != nil { + t.Fatal(err) + } - time.Sleep(100 * time.Millisecond) + time.Sleep(100 * time.Millisecond) - expectNoMessage := func(sub *Subscription) { - ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) - defer cancel() + expectNoMessage := func(sub *Subscription) { + ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) + defer cancel() - m, err := sub.Next(ctx) - if err == nil { - t.Fatal("expected no message, but got ", string(m.Data)) + m, err := sub.Next(ctx) + if err == nil { + t.Fatal("expected no message, but got ", string(m.Data)) + } } - } - psubs[1].Publish("test", []byte("i am not a walrus")) - psubs[2].Publish("test", []byte("i am not a walrus either")) + psubs[1].Publish("test", []byte("i am not a walrus")) + psubs[2].Publish("test", []byte("i am not a walrus either")) - // assert no messages - expectNoMessage(sub) + // assert no messages + expectNoMessage(sub) - // assert that peer1's score is still 0 (its message was ignored) while peer2 should have - // a negative score (its message got rejected) - res := make(chan float64, 1) - psubs[0].eval <- func() { - res <- psubs[0].rt.(*GossipSubRouter).score.Score(hosts[1].ID()) - } - score := <-res - if score != 0 { - t.Fatalf("expected 0 score for peer1, but got %f", score) - } + // assert that peer1's score is still 0 (its message was ignored) while peer2 should have + // a negative score (its message got rejected) + res := make(chan float64, 1) + psubs[0].eval <- func() { + res <- psubs[0].rt.(*GossipSubRouter).score.Score(hosts[1].ID()) + } + score := <-res + if score != 0 { + t.Fatalf("expected 0 score for peer1, but got %f", score) + } - psubs[0].eval <- func() { - res <- psubs[0].rt.(*GossipSubRouter).score.Score(hosts[2].ID()) - } - score = <-res - if score >= 0 { - t.Fatalf("expected negative score for peer2, but got %f", score) - } + psubs[0].eval <- func() { + res <- psubs[0].rt.(*GossipSubRouter).score.Score(hosts[2].ID()) + } + score = <-res + if score >= 0 { + t.Fatalf("expected negative score for peer2, but got %f", score) + } + }) } func TestGossipsubPiggybackControl(t *testing.T) { // this is a direct test of the piggybackControl function as we can't reliably // trigger it on travis - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - h := getDefaultHosts(t, 1)[0] - ps := getGossipsub(ctx, h) - - blah := peer.ID("bogotr0n") - - res := make(chan *RPC, 1) - ps.eval <- func() { - gs := ps.rt.(*GossipSubRouter) - test1 := "test1" - test2 := "test2" - test3 := "test3" - gs.mesh[test1] = make(map[peer.ID]struct{}) - gs.mesh[test2] = make(map[peer.ID]struct{}) - gs.mesh[test1][blah] = struct{}{} - - rpc := &RPC{RPC: pb.RPC{}} - gs.piggybackControl(blah, rpc, &pb.ControlMessage{ - Graft: []*pb.ControlGraft{{TopicID: &test1}, {TopicID: &test2}, {TopicID: &test3}}, - Prune: []*pb.ControlPrune{{TopicID: &test1}, {TopicID: &test2}, {TopicID: &test3}}, - }) - res <- rpc - } + synctestTest(t, func(t *testing.T) { - rpc := <-res - if rpc.Control == nil { - t.Fatal("expected non-nil control message") - } - if len(rpc.Control.Graft) != 1 { - t.Fatal("expected 1 GRAFT") - } - if rpc.Control.Graft[0].GetTopicID() != "test1" { - t.Fatal("expected test1 as graft topic ID") - } - if len(rpc.Control.Prune) != 2 { - t.Fatal("expected 2 PRUNEs") - } - if rpc.Control.Prune[0].GetTopicID() != "test2" { - t.Fatal("expected test2 as prune topic ID") - } - if rpc.Control.Prune[1].GetTopicID() != "test3" { - t.Fatal("expected test3 as prune topic ID") - } -} + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() -func TestGossipsubMultipleGraftTopics(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + h := getDefaultHosts(t, 1)[0] + ps := getGossipsub(ctx, h) + + blah := peer.ID("bogotr0n") + + res := make(chan *RPC, 1) + ps.eval <- func() { + gs := ps.rt.(*GossipSubRouter) + test1 := "test1" + test2 := "test2" + test3 := "test3" + gs.mesh[test1] = make(map[peer.ID]struct{}) + gs.mesh[test2] = make(map[peer.ID]struct{}) + gs.mesh[test1][blah] = struct{}{} + + rpc := &RPC{RPC: pb.RPC{}} + gs.piggybackControl(blah, rpc, &pb.ControlMessage{ + Graft: []*pb.ControlGraft{{TopicID: &test1}, {TopicID: &test2}, {TopicID: &test3}}, + Prune: []*pb.ControlPrune{{TopicID: &test1}, {TopicID: &test2}, {TopicID: &test3}}, + }) + res <- rpc + } - hosts := getDefaultHosts(t, 2) - psubs := getGossipsubs(ctx, hosts) - sparseConnect(t, hosts) + rpc := <-res + if rpc.Control == nil { + t.Fatal("expected non-nil control message") + } + if len(rpc.Control.Graft) != 1 { + t.Fatal("expected 1 GRAFT") + } + if rpc.Control.Graft[0].GetTopicID() != "test1" { + t.Fatal("expected test1 as graft topic ID") + } + if len(rpc.Control.Prune) != 2 { + t.Fatal("expected 2 PRUNEs") + } + if rpc.Control.Prune[0].GetTopicID() != "test2" { + t.Fatal("expected test2 as prune topic ID") + } + if rpc.Control.Prune[1].GetTopicID() != "test3" { + t.Fatal("expected test3 as prune topic ID") + } + }) +} - time.Sleep(time.Second * 1) +func TestGossipsubMultipleGraftTopics(t *testing.T) { + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - firstTopic := "topic1" - secondTopic := "topic2" - thirdTopic := "topic3" + hosts := getDefaultHosts(t, 2) + psubs := getGossipsubs(ctx, hosts) + sparseConnect(t, hosts) - firstPeer := hosts[0].ID() - secondPeer := hosts[1].ID() + time.Sleep(time.Second * 1) - p1Sub := psubs[0] - p2Sub := psubs[1] - p1Router := psubs[0].rt.(*GossipSubRouter) - p2Router := psubs[1].rt.(*GossipSubRouter) + firstTopic := "topic1" + secondTopic := "topic2" + thirdTopic := "topic3" - finChan := make(chan struct{}) + firstPeer := hosts[0].ID() + secondPeer := hosts[1].ID() - p2Sub.eval <- func() { - // Add topics to second peer - p2Router.mesh[firstTopic] = map[peer.ID]struct{}{} - p2Router.mesh[secondTopic] = map[peer.ID]struct{}{} - p2Router.mesh[thirdTopic] = map[peer.ID]struct{}{} + p1Sub := psubs[0] + p2Sub := psubs[1] + p1Router := psubs[0].rt.(*GossipSubRouter) + p2Router := psubs[1].rt.(*GossipSubRouter) - finChan <- struct{}{} - } - <-finChan - - // Send multiple GRAFT messages to second peer from - // 1st peer - wait := make(chan struct{}) - p1Sub.eval <- func() { - defer close(wait) - p1Router.sendGraftPrune(map[peer.ID][]string{ - secondPeer: {firstTopic, secondTopic, thirdTopic}, - }, map[peer.ID][]string{}, map[peer.ID]bool{}) - } - <-wait + finChan := make(chan struct{}) - time.Sleep(time.Second * 1) + p2Sub.eval <- func() { + // Add topics to second peer + p2Router.mesh[firstTopic] = map[peer.ID]struct{}{} + p2Router.mesh[secondTopic] = map[peer.ID]struct{}{} + p2Router.mesh[thirdTopic] = map[peer.ID]struct{}{} - p2Sub.eval <- func() { - if _, ok := p2Router.mesh[firstTopic][firstPeer]; !ok { - t.Errorf("First peer wasnt added to mesh of the second peer for the topic %s", firstTopic) + finChan <- struct{}{} } - if _, ok := p2Router.mesh[secondTopic][firstPeer]; !ok { - t.Errorf("First peer wasnt added to mesh of the second peer for the topic %s", secondTopic) + <-finChan + + // Send multiple GRAFT messages to second peer from + // 1st peer + wait := make(chan struct{}) + p1Sub.eval <- func() { + defer close(wait) + p1Router.sendGraftPrune(map[peer.ID][]string{ + secondPeer: {firstTopic, secondTopic, thirdTopic}, + }, map[peer.ID][]string{}, map[peer.ID]bool{}) } - if _, ok := p2Router.mesh[thirdTopic][firstPeer]; !ok { - t.Errorf("First peer wasnt added to mesh of the second peer for the topic %s", thirdTopic) + <-wait + + time.Sleep(time.Second * 1) + + p2Sub.eval <- func() { + if _, ok := p2Router.mesh[firstTopic][firstPeer]; !ok { + t.Errorf("First peer wasnt added to mesh of the second peer for the topic %s", firstTopic) + } + if _, ok := p2Router.mesh[secondTopic][firstPeer]; !ok { + t.Errorf("First peer wasnt added to mesh of the second peer for the topic %s", secondTopic) + } + if _, ok := p2Router.mesh[thirdTopic][firstPeer]; !ok { + t.Errorf("First peer wasnt added to mesh of the second peer for the topic %s", thirdTopic) + } + finChan <- struct{}{} } - finChan <- struct{}{} - } - <-finChan + <-finChan + }) } func TestGossipsubOpportunisticGrafting(t *testing.T) { - originalGossipSubPruneBackoff := GossipSubPruneBackoff - GossipSubPruneBackoff = 500 * time.Millisecond - originalGossipSubGraftFloodThreshold := GossipSubGraftFloodThreshold - GossipSubGraftFloodThreshold = 100 * time.Millisecond - originalGossipSubOpportunisticGraftTicks := GossipSubOpportunisticGraftTicks - GossipSubOpportunisticGraftTicks = 2 - defer func() { - GossipSubPruneBackoff = originalGossipSubPruneBackoff - GossipSubGraftFloodThreshold = originalGossipSubGraftFloodThreshold - GossipSubOpportunisticGraftTicks = originalGossipSubOpportunisticGraftTicks - }() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - hosts := getDefaultHosts(t, 50) - // pubsubs for the first 10 hosts - psubs := getGossipsubs(ctx, hosts[:10], - WithFloodPublish(true), - WithPeerScore( - &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - AppSpecificWeight: 0, - DecayInterval: time.Second, - DecayToZero: 0.01, - Topics: map[string]*TopicScoreParams{ - "test": { - TopicWeight: 1, - TimeInMeshWeight: 0.0002777, - TimeInMeshQuantum: time.Second, - TimeInMeshCap: 3600, - FirstMessageDeliveriesWeight: 1, - FirstMessageDeliveriesDecay: 0.9997, - FirstMessageDeliveriesCap: 100, - InvalidMessageDeliveriesDecay: 0.99997, + synctestTest(t, func(t *testing.T) { + originalGossipSubPruneBackoff := GossipSubPruneBackoff + GossipSubPruneBackoff = 500 * time.Millisecond + originalGossipSubGraftFloodThreshold := GossipSubGraftFloodThreshold + GossipSubGraftFloodThreshold = 100 * time.Millisecond + originalGossipSubOpportunisticGraftTicks := GossipSubOpportunisticGraftTicks + GossipSubOpportunisticGraftTicks = 2 + defer func() { + GossipSubPruneBackoff = originalGossipSubPruneBackoff + GossipSubGraftFloodThreshold = originalGossipSubGraftFloodThreshold + GossipSubOpportunisticGraftTicks = originalGossipSubOpportunisticGraftTicks + }() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + hosts := getDefaultHosts(t, 50) + // pubsubs for the first 10 hosts + psubs := getGossipsubs(ctx, hosts[:10], + WithFloodPublish(true), + WithPeerScore( + &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + AppSpecificWeight: 0, + DecayInterval: time.Second, + DecayToZero: 0.01, + Topics: map[string]*TopicScoreParams{ + "test": { + TopicWeight: 1, + TimeInMeshWeight: 0.0002777, + TimeInMeshQuantum: time.Second, + TimeInMeshCap: 3600, + FirstMessageDeliveriesWeight: 1, + FirstMessageDeliveriesDecay: 0.9997, + FirstMessageDeliveriesCap: 100, + InvalidMessageDeliveriesDecay: 0.99997, + }, }, }, - }, - &PeerScoreThresholds{ - GossipThreshold: -10, - PublishThreshold: -100, - GraylistThreshold: -10000, - OpportunisticGraftThreshold: 1, - })) + &PeerScoreThresholds{ + GossipThreshold: -10, + PublishThreshold: -100, + GraylistThreshold: -10000, + OpportunisticGraftThreshold: 1, + })) - // connect the real hosts with degree 5 - connectSome(t, hosts[:10], 5) + // connect the real hosts with degree 5 + connectSome(t, hosts[:10], 5) - // sybil squatters for the remaining 40 hosts - for _, h := range hosts[10:] { - squatter := &sybilSquatter{h: h} - h.SetStreamHandler(GossipSubID_v10, squatter.handleStream) - } + // sybil squatters for the remaining 40 hosts + for _, h := range hosts[10:] { + squatter := &sybilSquatter{h: h} + h.SetStreamHandler(GossipSubID_v10, squatter.handleStream) + } - // connect all squatters to every real host - for _, squatter := range hosts[10:] { - for _, real := range hosts[:10] { - connect(t, squatter, real) + // connect all squatters to every real host + for _, squatter := range hosts[10:] { + for _, real := range hosts[:10] { + connect(t, squatter, real) + } } - } - // wait a bit for the connections to propagate events to the pubsubs - time.Sleep(time.Second) + // wait a bit for the connections to propagate events to the pubsubs + time.Sleep(time.Second) - // ask the real pubsus to join the topic - for _, ps := range psubs { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) - } - // consume the messages - go func(sub *Subscription) { - for { - _, err := sub.Next(ctx) - if err != nil { - return - } + // ask the real pubsus to join the topic + for _, ps := range psubs { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) } - }(sub) - } + // consume the messages + go func(sub *Subscription) { + for { + _, err := sub.Next(ctx) + if err != nil { + return + } + } + }(sub) + } - // publish a bunch of messages from the real hosts - for i := 0; i < 1000; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[i%10].Publish("test", msg) - time.Sleep(20 * time.Millisecond) - } + // publish a bunch of messages from the real hosts + for i := 0; i < 1000; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[i%10].Publish("test", msg) + time.Sleep(20 * time.Millisecond) + } - // now wait a few of oppgraft cycles - time.Sleep(7 * time.Second) + // now wait a few of oppgraft cycles + time.Sleep(7 * time.Second) - // check the honest peer meshes, they should have at least 3 honest peers each - res := make(chan int, 1) - for _, ps := range psubs { - ps.eval <- func() { - gs := ps.rt.(*GossipSubRouter) - count := 0 - for _, h := range hosts[:10] { - _, ok := gs.mesh["test"][h.ID()] - if ok { - count++ + // check the honest peer meshes, they should have at least 3 honest peers each + res := make(chan int, 1) + for _, ps := range psubs { + ps.eval <- func() { + gs := ps.rt.(*GossipSubRouter) + count := 0 + for _, h := range hosts[:10] { + _, ok := gs.mesh["test"][h.ID()] + if ok { + count++ + } } + res <- count } - res <- count - } - count := <-res - if count < 3 { - t.Fatalf("expected at least 3 honest peers, got %d", count) + count := <-res + if count < 3 { + t.Fatalf("expected at least 3 honest peers, got %d", count) + } } - } + }) } func TestGossipSubLeaveTopic(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - h := getDefaultHosts(t, 2) - psubs := []*PubSub{ - getGossipsub(ctx, h[0]), - getGossipsub(ctx, h[1]), - } + h := getDefaultHosts(t, 2) + psubs := []*PubSub{ + getGossipsub(ctx, h[0]), + getGossipsub(ctx, h[1]), + } - connect(t, h[0], h[1]) + connect(t, h[0], h[1]) - // Join all peers - for _, ps := range psubs { - _, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + // Join all peers + for _, ps := range psubs { + _, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } } - } - time.Sleep(time.Second) + time.Sleep(time.Second) - leaveTime := time.Now() - done := make(chan struct{}) + leaveTime := time.Now() + done := make(chan struct{}) - psubs[0].rt.(*GossipSubRouter).p.eval <- func() { - defer close(done) - psubs[0].rt.Leave("test") - time.Sleep(time.Second) - peerMap := psubs[0].rt.(*GossipSubRouter).backoff["test"] - if len(peerMap) != 1 { - t.Fatalf("No peer is populated in the backoff map for peer 0") - } - _, ok := peerMap[h[1].ID()] - if !ok { - t.Errorf("Expected peer does not exist in the backoff map") - } + psubs[0].rt.(*GossipSubRouter).p.eval <- func() { + defer close(done) + psubs[0].rt.Leave("test") + time.Sleep(time.Second) + peerMap := psubs[0].rt.(*GossipSubRouter).backoff["test"] + if len(peerMap) != 1 { + t.Fatalf("No peer is populated in the backoff map for peer 0") + } + _, ok := peerMap[h[1].ID()] + if !ok { + t.Errorf("Expected peer does not exist in the backoff map") + } - backoffTime := peerMap[h[1].ID()].Sub(leaveTime) - // Check that the backoff time is roughly the unsubscribebackoff time (with a slack of 1s) - if backoffTime-GossipSubUnsubscribeBackoff > time.Second { - t.Error("Backoff time should be set to GossipSubUnsubscribeBackoff.") + backoffTime := peerMap[h[1].ID()].Sub(leaveTime) + // Check that the backoff time is roughly the unsubscribebackoff time (with a slack of 1s) + if backoffTime-GossipSubUnsubscribeBackoff > time.Second { + t.Error("Backoff time should be set to GossipSubUnsubscribeBackoff.") + } } - } - <-done + <-done - done = make(chan struct{}) - // Ensure that remote peer 1 also applies the backoff appropriately - // for peer 0. - psubs[1].rt.(*GossipSubRouter).p.eval <- func() { - defer close(done) - peerMap2 := psubs[1].rt.(*GossipSubRouter).backoff["test"] - if len(peerMap2) != 1 { - t.Fatalf("No peer is populated in the backoff map for peer 1") - } - _, ok := peerMap2[h[0].ID()] - if !ok { - t.Errorf("Expected peer does not exist in the backoff map") - } + done = make(chan struct{}) + // Ensure that remote peer 1 also applies the backoff appropriately + // for peer 0. + psubs[1].rt.(*GossipSubRouter).p.eval <- func() { + defer close(done) + peerMap2 := psubs[1].rt.(*GossipSubRouter).backoff["test"] + if len(peerMap2) != 1 { + t.Fatalf("No peer is populated in the backoff map for peer 1") + } + _, ok := peerMap2[h[0].ID()] + if !ok { + t.Errorf("Expected peer does not exist in the backoff map") + } - backoffTime := peerMap2[h[0].ID()].Sub(leaveTime) - // Check that the backoff time is roughly the unsubscribebackoff time (with a slack of 1s) - if backoffTime-GossipSubUnsubscribeBackoff > time.Second { - t.Error("Backoff time should be set to GossipSubUnsubscribeBackoff.") + backoffTime := peerMap2[h[0].ID()].Sub(leaveTime) + // Check that the backoff time is roughly the unsubscribebackoff time (with a slack of 1s) + if backoffTime-GossipSubUnsubscribeBackoff > time.Second { + t.Error("Backoff time should be set to GossipSubUnsubscribeBackoff.") + } } - } - <-done + <-done + }) } // withRouter is a race-free way of accessing state from the PubSubRouter. @@ -2114,48 +2187,50 @@ func withGSRouter(p *PubSub, f func(r *GossipSubRouter)) { } func TestGossipSubJoinTopic(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - h := getDefaultHosts(t, 3) - psubs := []*PubSub{ - getGossipsub(ctx, h[0]), - getGossipsub(ctx, h[1]), - getGossipsub(ctx, h[2]), - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - connect(t, h[0], h[1]) - connect(t, h[0], h[2]) + h := getDefaultHosts(t, 3) + psubs := []*PubSub{ + getGossipsub(ctx, h[0]), + getGossipsub(ctx, h[1]), + getGossipsub(ctx, h[2]), + } - // Add in backoff for peer. - peerMap := make(map[peer.ID]time.Time) - withGSRouter(psubs[0], func(router0 *GossipSubRouter) { - peerMap[h[1].ID()] = time.Now().Add(router0.params.UnsubscribeBackoff) - }) + connect(t, h[0], h[1]) + connect(t, h[0], h[2]) - withGSRouter(psubs[0], func(router0 *GossipSubRouter) { - router0.backoff["test"] = peerMap - }) + // Add in backoff for peer. + peerMap := make(map[peer.ID]time.Time) + withGSRouter(psubs[0], func(router0 *GossipSubRouter) { + peerMap[h[1].ID()] = time.Now().Add(router0.params.UnsubscribeBackoff) + }) - // Join all peers - for _, ps := range psubs { - _, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + withGSRouter(psubs[0], func(router0 *GossipSubRouter) { + router0.backoff["test"] = peerMap + }) + + // Join all peers + for _, ps := range psubs { + _, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } } - } - time.Sleep(time.Second) + time.Sleep(time.Second) - withGSRouter(psubs[0], func(router0 *GossipSubRouter) { - meshMap := router0.mesh["test"] - if len(meshMap) != 1 { - t.Fatalf("Unexpect peer included in the mesh") - } - _, ok := meshMap[h[1].ID()] - if ok { - t.Fatalf("Peer that was to be backed off is included in the mesh") - } + withGSRouter(psubs[0], func(router0 *GossipSubRouter) { + meshMap := router0.mesh["test"] + if len(meshMap) != 1 { + t.Fatalf("Unexpect peer included in the mesh") + } + _, ok := meshMap[h[1].ID()] + if ok { + t.Fatalf("Peer that was to be backed off is included in the mesh") + } + }) }) } @@ -2204,165 +2279,174 @@ func (sq *sybilSquatter) handleStream(s network.Stream) { func TestGossipsubPeerScoreInspect(t *testing.T) { // this test exercises the code path sof peer score inspection - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - hosts := getDefaultHosts(t, 2) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - inspector := &mockPeerScoreInspector{} - psub1 := getGossipsub(ctx, hosts[0], - WithPeerScore( - &PeerScoreParams{ - Topics: map[string]*TopicScoreParams{ - "test": { - TopicWeight: 1, - TimeInMeshQuantum: time.Second, - FirstMessageDeliveriesWeight: 1, - FirstMessageDeliveriesDecay: 0.999, - FirstMessageDeliveriesCap: 100, - InvalidMessageDeliveriesWeight: -1, - InvalidMessageDeliveriesDecay: 0.9999, + hosts := getDefaultHosts(t, 2) + + inspector := &mockPeerScoreInspector{} + psub1 := getGossipsub(ctx, hosts[0], + WithPeerScore( + &PeerScoreParams{ + Topics: map[string]*TopicScoreParams{ + "test": { + TopicWeight: 1, + TimeInMeshQuantum: time.Second, + FirstMessageDeliveriesWeight: 1, + FirstMessageDeliveriesDecay: 0.999, + FirstMessageDeliveriesCap: 100, + InvalidMessageDeliveriesWeight: -1, + InvalidMessageDeliveriesDecay: 0.9999, + }, }, + AppSpecificScore: func(peer.ID) float64 { return 0 }, + DecayInterval: time.Second, + DecayToZero: 0.01, }, - AppSpecificScore: func(peer.ID) float64 { return 0 }, - DecayInterval: time.Second, - DecayToZero: 0.01, - }, - &PeerScoreThresholds{ - GossipThreshold: -1, - PublishThreshold: -10, - GraylistThreshold: -1000, - }), - WithPeerScoreInspect(inspector.inspect, time.Second)) - psub2 := getGossipsub(ctx, hosts[1]) - psubs := []*PubSub{psub1, psub2} + &PeerScoreThresholds{ + GossipThreshold: -1, + PublishThreshold: -10, + GraylistThreshold: -1000, + }), + WithPeerScoreInspect(inspector.inspect, time.Second)) + psub2 := getGossipsub(ctx, hosts[1]) + psubs := []*PubSub{psub1, psub2} - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - for _, ps := range psubs { - _, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + for _, ps := range psubs { + _, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } } - } - time.Sleep(time.Second) + time.Sleep(time.Second) - for i := 0; i < 20; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[i%2].Publish("test", msg) - time.Sleep(20 * time.Millisecond) - } + for i := 0; i < 20; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[i%2].Publish("test", msg) + time.Sleep(20 * time.Millisecond) + } - time.Sleep(time.Second + 200*time.Millisecond) + time.Sleep(time.Second + 200*time.Millisecond) - score2 := inspector.score(hosts[1].ID()) - if score2 < 9 { - t.Fatalf("expected score to be at least 9, instead got %f", score2) - } + score2 := inspector.score(hosts[1].ID()) + if score2 < 9 { + t.Fatalf("expected score to be at least 9, instead got %f", score2) + } + }) } func TestGossipsubPeerFeedback(t *testing.T) { // this test exercises the code path sof peer score inspection - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - hosts := getDefaultHosts(t, 2) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - inspector := &mockPeerScoreInspector{} - psub1 := getGossipsub(ctx, hosts[0], - WithPeerScore( - &PeerScoreParams{ - Topics: map[string]*TopicScoreParams{ - "test": { - TopicWeight: 1, - TimeInMeshQuantum: time.Second, - FirstMessageDeliveriesWeight: 10, - FirstMessageDeliveriesDecay: 0.999, - FirstMessageDeliveriesCap: 100, - InvalidMessageDeliveriesWeight: -1, - InvalidMessageDeliveriesDecay: 0.9999, + hosts := getDefaultHosts(t, 2) + + inspector := &mockPeerScoreInspector{} + psub1 := getGossipsub(ctx, hosts[0], + WithPeerScore( + &PeerScoreParams{ + Topics: map[string]*TopicScoreParams{ + "test": { + TopicWeight: 1, + TimeInMeshQuantum: time.Second, + FirstMessageDeliveriesWeight: 10, + FirstMessageDeliveriesDecay: 0.999, + FirstMessageDeliveriesCap: 100, + InvalidMessageDeliveriesWeight: -1, + InvalidMessageDeliveriesDecay: 0.9999, + }, }, + AppSpecificScore: func(peer.ID) float64 { return 0 }, + DecayInterval: time.Second, + DecayToZero: 0.01, }, - AppSpecificScore: func(peer.ID) float64 { return 0 }, - DecayInterval: time.Second, - DecayToZero: 0.01, - }, - &PeerScoreThresholds{ - GossipThreshold: -1, - PublishThreshold: -10, - GraylistThreshold: -1000, - }), - WithPeerScoreInspect(inspector.inspect, time.Second)) - _ = getGossipsub(ctx, hosts[1]) - - connect(t, hosts[0], hosts[1]) - time.Sleep(500 * time.Millisecond) // Wait for nodes to connect - - var err error - err = errors.Join(err, psub1.PeerFeedback("test", hosts[1].ID(), PeerFeedbackUsefulMessage)) - err = errors.Join(err, psub1.PeerFeedback("test", hosts[1].ID(), PeerFeedbackUsefulMessage)) - err = errors.Join(err, psub1.PeerFeedback("test", hosts[1].ID(), PeerFeedbackUsefulMessage)) - if err != nil { - t.Fatal(err) - } - time.Sleep(500 * time.Millisecond) // Wait for feedback to be incorporated + &PeerScoreThresholds{ + GossipThreshold: -1, + PublishThreshold: -10, + GraylistThreshold: -1000, + }), + WithPeerScoreInspect(inspector.inspect, time.Second)) + _ = getGossipsub(ctx, hosts[1]) + + connect(t, hosts[0], hosts[1]) + time.Sleep(500 * time.Millisecond) // Wait for nodes to connect + + var err error + err = errors.Join(err, psub1.PeerFeedback("test", hosts[1].ID(), PeerFeedbackUsefulMessage)) + err = errors.Join(err, psub1.PeerFeedback("test", hosts[1].ID(), PeerFeedbackUsefulMessage)) + err = errors.Join(err, psub1.PeerFeedback("test", hosts[1].ID(), PeerFeedbackUsefulMessage)) + if err != nil { + t.Fatal(err) + } + time.Sleep(500 * time.Millisecond) // Wait for feedback to be incorporated - score2 := inspector.score(hosts[1].ID()) - if score2 < 9 { - t.Fatalf("expected score to be at least 9, instead got %f", score2) - } + score2 := inspector.score(hosts[1].ID()) + if score2 < 9 { + t.Fatalf("expected score to be at least 9, instead got %f", score2) + } + }) } func TestGossipsubPeerScoreResetTopicParams(t *testing.T) { // this test exercises the code path sof peer score inspection - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - hosts := getDefaultHosts(t, 1) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - ps := getGossipsub(ctx, hosts[0], - WithPeerScore( - &PeerScoreParams{ - Topics: map[string]*TopicScoreParams{ - "test": { - TopicWeight: 1, - TimeInMeshQuantum: time.Second, - FirstMessageDeliveriesWeight: 1, - FirstMessageDeliveriesDecay: 0.999, - FirstMessageDeliveriesCap: 100, - InvalidMessageDeliveriesWeight: -1, - InvalidMessageDeliveriesDecay: 0.9999, + hosts := getDefaultHosts(t, 1) + + ps := getGossipsub(ctx, hosts[0], + WithPeerScore( + &PeerScoreParams{ + Topics: map[string]*TopicScoreParams{ + "test": { + TopicWeight: 1, + TimeInMeshQuantum: time.Second, + FirstMessageDeliveriesWeight: 1, + FirstMessageDeliveriesDecay: 0.999, + FirstMessageDeliveriesCap: 100, + InvalidMessageDeliveriesWeight: -1, + InvalidMessageDeliveriesDecay: 0.9999, + }, }, + AppSpecificScore: func(peer.ID) float64 { return 0 }, + DecayInterval: time.Second, + DecayToZero: 0.01, }, - AppSpecificScore: func(peer.ID) float64 { return 0 }, - DecayInterval: time.Second, - DecayToZero: 0.01, - }, - &PeerScoreThresholds{ - GossipThreshold: -1, - PublishThreshold: -10, - GraylistThreshold: -1000, - })) + &PeerScoreThresholds{ + GossipThreshold: -1, + PublishThreshold: -10, + GraylistThreshold: -1000, + })) - topic, err := ps.Join("test") - if err != nil { - t.Fatal(err) - } + topic, err := ps.Join("test") + if err != nil { + t.Fatal(err) + } - err = topic.SetScoreParams( - &TopicScoreParams{ - TopicWeight: 1, - TimeInMeshQuantum: time.Second, - FirstMessageDeliveriesWeight: 1, - FirstMessageDeliveriesDecay: 0.999, - FirstMessageDeliveriesCap: 200, - InvalidMessageDeliveriesWeight: -1, - InvalidMessageDeliveriesDecay: 0.9999, - }) - if err != nil { - t.Fatal(err) - } + err = topic.SetScoreParams( + &TopicScoreParams{ + TopicWeight: 1, + TimeInMeshQuantum: time.Second, + FirstMessageDeliveriesWeight: 1, + FirstMessageDeliveriesDecay: 0.999, + FirstMessageDeliveriesCap: 200, + InvalidMessageDeliveriesWeight: -1, + InvalidMessageDeliveriesDecay: 0.9999, + }) + if err != nil { + t.Fatal(err) + } + }) } type mockPeerScoreInspector struct { @@ -2383,60 +2467,62 @@ func (ps *mockPeerScoreInspector) score(p peer.ID) float64 { } func TestGossipsubRPCFragmentation(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) - ps := getGossipsub(ctx, hosts[0]) + hosts := getDefaultHosts(t, 2) + ps := getGossipsub(ctx, hosts[0]) - // make a fake peer that requests everything through IWANT gossip - iwe := iwantEverything{h: hosts[1]} - iwe.h.SetStreamHandler(GossipSubID_v10, iwe.handleStream) + // make a fake peer that requests everything through IWANT gossip + iwe := iwantEverything{h: hosts[1]} + iwe.h.SetStreamHandler(GossipSubID_v10, iwe.handleStream) - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - // have the real pubsub join the test topic - _, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) - } + // have the real pubsub join the test topic + _, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } - // wait for the real pubsub to connect and try to graft to the faker - time.Sleep(time.Second) - - // publish a bunch of fairly large messages from the real host - nMessages := 1000 - msgSize := 20000 - for i := 0; i < nMessages; i++ { - msg := make([]byte, msgSize) - crand.Read(msg) - ps.Publish("test", msg) - time.Sleep(20 * time.Millisecond) - } + // wait for the real pubsub to connect and try to graft to the faker + time.Sleep(time.Second) - // wait a bit for them to be received via gossip by the fake peer - time.Sleep(5 * time.Second) - iwe.lk.Lock() - defer iwe.lk.Unlock() + // publish a bunch of fairly large messages from the real host + nMessages := 1000 + msgSize := 20000 + for i := 0; i < nMessages; i++ { + msg := make([]byte, msgSize) + crand.Read(msg) + ps.Publish("test", msg) + time.Sleep(20 * time.Millisecond) + } - // we should have received all the messages - if iwe.msgsReceived != nMessages { - t.Fatalf("expected fake gossipsub peer to receive all messages, got %d / %d", iwe.msgsReceived, nMessages) - } + // wait a bit for them to be received via gossip by the fake peer + time.Sleep(5 * time.Second) + iwe.lk.Lock() + defer iwe.lk.Unlock() - // and we should have seen an IHAVE message for each of them - if iwe.ihavesReceived != nMessages { - t.Fatalf("expected to get IHAVEs for every message, got %d / %d", iwe.ihavesReceived, nMessages) - } + // we should have received all the messages + if iwe.msgsReceived != nMessages { + t.Fatalf("expected fake gossipsub peer to receive all messages, got %d / %d", iwe.msgsReceived, nMessages) + } - // If everything were fragmented with maximum efficiency, we would expect to get - // (nMessages * msgSize) / ps.maxMessageSize total RPCs containing the messages we sent IWANTs for. - // The actual number will probably be larger, since there's some overhead for the RPC itself, and - // we probably aren't packing each RPC to it's maximum size - minExpectedRPCS := (nMessages * msgSize) / ps.maxMessageSize - if iwe.rpcsWithMessages < minExpectedRPCS { - t.Fatalf("expected to receive at least %d RPCs containing messages, got %d", minExpectedRPCS, iwe.rpcsWithMessages) - } + // and we should have seen an IHAVE message for each of them + if iwe.ihavesReceived != nMessages { + t.Fatalf("expected to get IHAVEs for every message, got %d / %d", iwe.ihavesReceived, nMessages) + } + + // If everything were fragmented with maximum efficiency, we would expect to get + // (nMessages * msgSize) / ps.maxMessageSize total RPCs containing the messages we sent IWANTs for. + // The actual number will probably be larger, since there's some overhead for the RPC itself, and + // we probably aren't packing each RPC to it's maximum size + minExpectedRPCS := (nMessages * msgSize) / ps.maxMessageSize + if iwe.rpcsWithMessages < minExpectedRPCS { + t.Fatalf("expected to receive at least %d RPCs containing messages, got %d", minExpectedRPCS, iwe.rpcsWithMessages) + } + }) } // iwantEverything is a simple gossipsub client that never grafts onto a mesh, @@ -2533,213 +2619,215 @@ func validRPCSizes(slice []RPC, limit int) bool { } func TestFragmentRPCFunction(t *testing.T) { - fragmentRPC := func(rpc *RPC, limit int) ([]RPC, error) { - rpcs := slices.Collect(rpc.split(limit)) - if allValid := validRPCSizes(rpcs, limit); !allValid { - return rpcs, fmt.Errorf("RPC size exceeds limit") - } - return rpcs, nil - } - - p := peer.ID("some-peer") - topic := "test" - rpc := &RPC{from: p} - limit := 1024 - - mkMsg := func(size int) *pb.Message { - msg := &pb.Message{} - msg.Data = make([]byte, size-4) // subtract the protobuf overhead, so msg.Size() returns requested size - crand.Read(msg.Data) - return msg - } - - ensureBelowLimit := func(rpcs []RPC) { - for _, r := range rpcs { - if r.Size() > limit { - t.Fatalf("expected fragmented RPC to be below %d bytes, was %d", limit, r.Size()) + synctestTest(t, func(t *testing.T) { + fragmentRPC := func(rpc *RPC, limit int) ([]RPC, error) { + rpcs := slices.Collect(rpc.split(limit)) + if allValid := validRPCSizes(rpcs, limit); !allValid { + return rpcs, fmt.Errorf("RPC size exceeds limit") } + return rpcs, nil } - } - - // it should not fragment if everything fits in one RPC - rpc.Publish = []*pb.Message{} - rpc.Publish = []*pb.Message{mkMsg(10), mkMsg(10)} - results, err := fragmentRPC(rpc, limit) - if err != nil { - t.Fatal(err) - } - if len(results) != 1 { - t.Fatalf("expected single RPC if input is < limit, got %d %#v", len(results), results) - } - - // if there's a message larger than the limit, we should fail - rpc.Publish = []*pb.Message{mkMsg(10), mkMsg(limit * 2)} - results, err = fragmentRPC(rpc, limit) - if err == nil { - t.Fatalf("expected an error if a message exceeds limit, got %d RPCs instead", len(results)) - } - - // if the individual messages are below the limit, but the RPC as a whole is larger, we should fragment - nMessages := 100 - msgSize := 200 - truth := true - rpc.Subscriptions = []*pb.RPC_SubOpts{ - { - Subscribe: &truth, - Topicid: &topic, - }, - } - rpc.Publish = make([]*pb.Message, nMessages) - for i := 0; i < nMessages; i++ { - rpc.Publish[i] = mkMsg(msgSize) - } - results, err = fragmentRPC(rpc, limit) - if err != nil { - t.Fatal(err) - } - ensureBelowLimit(results) - msgsPerRPC := limit / msgSize - expectedRPCs := nMessages / msgsPerRPC - if len(results) > expectedRPCs+1 { - t.Fatalf("expected around %d RPC messages in output, got %d", expectedRPCs, len(results)) - } - var nMessagesFragmented int - var nSubscriptions int - for _, r := range results { - nMessagesFragmented += len(r.Publish) - nSubscriptions += len(r.Subscriptions) - } - if nMessagesFragmented != nMessages { - t.Fatalf("expected fragemented RPCs to contain same number of messages as input, got %d / %d", nMessagesFragmented, nMessages) - } - if nSubscriptions != 1 { - t.Fatal("expected subscription to be present in one of the fragmented messages, but not found") - } - - // if we're fragmenting, and the input RPC has control messages, - // the control messages should be in a separate RPC at the end - // reuse RPC from prev test, but add a control message - rpc.Control = &pb.ControlMessage{ - Graft: []*pb.ControlGraft{{TopicID: &topic}}, - Prune: []*pb.ControlPrune{{TopicID: &topic}}, - Ihave: []*pb.ControlIHave{{MessageIDs: []string{"foo"}}}, - Iwant: []*pb.ControlIWant{{MessageIDs: []string{"bar"}}}, - } - results, err = fragmentRPC(rpc, limit) - if err != nil { - t.Fatal(err) - } - ensureBelowLimit(results) - // we expect one more RPC than last time, with the final one containing the control messages - expectedCtrl := 1 - expectedRPCs = (nMessages / msgsPerRPC) + expectedCtrl - if len(results) != expectedRPCs { - t.Fatalf("expected %d RPC messages in output, got %d", expectedRPCs, len(results)) - } - ctl := results[len(results)-1].Control - if ctl == nil { - t.Fatal("expected final fragmented RPC to contain control messages, but .Control was nil") - } - // since it was not altered, the original control message should be identical to the output control message - originalBytes, err := rpc.Control.Marshal() - if err != nil { - t.Fatal(err) - } - receivedBytes, err := ctl.Marshal() - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(originalBytes, receivedBytes) { - t.Fatal("expected control message to be unaltered if it fits within one RPC message") - } - - // if the control message is too large to fit into a single RPC, it should be split into multiple RPCs - nTopics := 5 // pretend we're subscribed to multiple topics and sending IHAVE / IWANTs for each - messageIdSize := 32 - msgsPerTopic := 100 // enough that a single IHAVE or IWANT will exceed the limit - rpc.Control.Ihave = make([]*pb.ControlIHave, nTopics) - rpc.Control.Iwant = make([]*pb.ControlIWant, nTopics) - for i := 0; i < nTopics; i++ { - messageIds := make([]string, msgsPerTopic) - for m := 0; m < msgsPerTopic; m++ { - mid := make([]byte, messageIdSize) - crand.Read(mid) - messageIds[m] = string(mid) - } - rpc.Control.Ihave[i] = &pb.ControlIHave{MessageIDs: messageIds} - rpc.Control.Iwant[i] = &pb.ControlIWant{MessageIDs: messageIds} - } - results, err = fragmentRPC(rpc, limit) - if err != nil { - t.Fatal(err) - } - ensureBelowLimit(results) - minExpectedCtl := rpc.Control.Size() / limit - minExpectedRPCs := (nMessages / msgsPerRPC) + minExpectedCtl - if len(results) < minExpectedRPCs { - t.Fatalf("expected at least %d total RPCs (at least %d with control messages), got %d total", expectedRPCs, expectedCtrl, len(results)) - } - // Test the pathological case where a single gossip message ID exceeds the limit. - // It should not be present in the fragmented messages, but smaller IDs should be - rpc.Reset() - giantIdBytes := make([]byte, limit*2) - crand.Read(giantIdBytes) - rpc.Control = &pb.ControlMessage{ - Iwant: []*pb.ControlIWant{ - {MessageIDs: []string{"hello", string(giantIdBytes)}}, - }, - } - results, _ = fragmentRPC(rpc, limit) + p := peer.ID("some-peer") + topic := "test" + rpc := &RPC{from: p} + limit := 1024 - // The old behavior would silently drop the giant ID. - // Now we return a the giant ID in a RPC by itself so that it can be - // dropped before actually sending the RPC. This lets us log the anamoly. - // To keep this test useful, we implement the old behavior here. - filtered := make([]RPC, 0, len(results)) - for _, r := range results { - if r.Size() < limit { - filtered = append(filtered, r) + mkMsg := func(size int) *pb.Message { + msg := &pb.Message{} + msg.Data = make([]byte, size-4) // subtract the protobuf overhead, so msg.Size() returns requested size + crand.Read(msg.Data) + return msg } - } - results = filtered - err = nil - if !validRPCSizes(results, limit) { - err = fmt.Errorf("RPC size exceeds limit") - } - if err != nil { - t.Fatal(err) - } - if len(results) != 1 { - t.Fatalf("expected 1 RPC, got %d", len(results)) - } - if len(results[0].Control.Iwant) != 1 { - t.Fatalf("expected 1 IWANT, got %d", len(results[0].Control.Iwant)) - } - if results[0].Control.Iwant[0].MessageIDs[0] != "hello" { - t.Fatalf("expected small message ID to be included unaltered, got %s instead", - results[0].Control.Iwant[0].MessageIDs[0]) - } -} + ensureBelowLimit := func(rpcs []RPC) { + for _, r := range rpcs { + if r.Size() > limit { + t.Fatalf("expected fragmented RPC to be below %d bytes, was %d", limit, r.Size()) + } + } + } -func FuzzRPCSplit(f *testing.F) { - minMaxMsgSize := 100 - maxMaxMsgSize := 2048 - f.Fuzz(func(t *testing.T, data []byte) { - maxSize := int(generateU16(&data)) % maxMaxMsgSize - if maxSize < minMaxMsgSize { - maxSize = minMaxMsgSize + // it should not fragment if everything fits in one RPC + rpc.Publish = []*pb.Message{} + rpc.Publish = []*pb.Message{mkMsg(10), mkMsg(10)} + results, err := fragmentRPC(rpc, limit) + if err != nil { + t.Fatal(err) + } + if len(results) != 1 { + t.Fatalf("expected single RPC if input is < limit, got %d %#v", len(results), results) } - rpc := generateRPC(data, maxSize) - originalControl := compressedRPC{ihave: make(map[string][]string)} - originalControl.append(&rpc.RPC) - mergedControl := compressedRPC{ihave: make(map[string][]string)} + // if there's a message larger than the limit, we should fail + rpc.Publish = []*pb.Message{mkMsg(10), mkMsg(limit * 2)} + results, err = fragmentRPC(rpc, limit) + if err == nil { + t.Fatalf("expected an error if a message exceeds limit, got %d RPCs instead", len(results)) + } - for rpc := range rpc.split(maxSize) { - if rpc.Size() > maxSize { - t.Fatalf("invalid RPC size %v %d (max=%d)", rpc, rpc.Size(), maxSize) + // if the individual messages are below the limit, but the RPC as a whole is larger, we should fragment + nMessages := 100 + msgSize := 200 + truth := true + rpc.Subscriptions = []*pb.RPC_SubOpts{ + { + Subscribe: &truth, + Topicid: &topic, + }, + } + rpc.Publish = make([]*pb.Message, nMessages) + for i := 0; i < nMessages; i++ { + rpc.Publish[i] = mkMsg(msgSize) + } + results, err = fragmentRPC(rpc, limit) + if err != nil { + t.Fatal(err) + } + ensureBelowLimit(results) + msgsPerRPC := limit / msgSize + expectedRPCs := nMessages / msgsPerRPC + if len(results) > expectedRPCs+1 { + t.Fatalf("expected around %d RPC messages in output, got %d", expectedRPCs, len(results)) + } + var nMessagesFragmented int + var nSubscriptions int + for _, r := range results { + nMessagesFragmented += len(r.Publish) + nSubscriptions += len(r.Subscriptions) + } + if nMessagesFragmented != nMessages { + t.Fatalf("expected fragemented RPCs to contain same number of messages as input, got %d / %d", nMessagesFragmented, nMessages) + } + if nSubscriptions != 1 { + t.Fatal("expected subscription to be present in one of the fragmented messages, but not found") + } + + // if we're fragmenting, and the input RPC has control messages, + // the control messages should be in a separate RPC at the end + // reuse RPC from prev test, but add a control message + rpc.Control = &pb.ControlMessage{ + Graft: []*pb.ControlGraft{{TopicID: &topic}}, + Prune: []*pb.ControlPrune{{TopicID: &topic}}, + Ihave: []*pb.ControlIHave{{MessageIDs: []string{"foo"}}}, + Iwant: []*pb.ControlIWant{{MessageIDs: []string{"bar"}}}, + } + results, err = fragmentRPC(rpc, limit) + if err != nil { + t.Fatal(err) + } + ensureBelowLimit(results) + // we expect one more RPC than last time, with the final one containing the control messages + expectedCtrl := 1 + expectedRPCs = (nMessages / msgsPerRPC) + expectedCtrl + if len(results) != expectedRPCs { + t.Fatalf("expected %d RPC messages in output, got %d", expectedRPCs, len(results)) + } + ctl := results[len(results)-1].Control + if ctl == nil { + t.Fatal("expected final fragmented RPC to contain control messages, but .Control was nil") + } + // since it was not altered, the original control message should be identical to the output control message + originalBytes, err := rpc.Control.Marshal() + if err != nil { + t.Fatal(err) + } + receivedBytes, err := ctl.Marshal() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(originalBytes, receivedBytes) { + t.Fatal("expected control message to be unaltered if it fits within one RPC message") + } + + // if the control message is too large to fit into a single RPC, it should be split into multiple RPCs + nTopics := 5 // pretend we're subscribed to multiple topics and sending IHAVE / IWANTs for each + messageIdSize := 32 + msgsPerTopic := 100 // enough that a single IHAVE or IWANT will exceed the limit + rpc.Control.Ihave = make([]*pb.ControlIHave, nTopics) + rpc.Control.Iwant = make([]*pb.ControlIWant, nTopics) + for i := 0; i < nTopics; i++ { + messageIds := make([]string, msgsPerTopic) + for m := 0; m < msgsPerTopic; m++ { + mid := make([]byte, messageIdSize) + crand.Read(mid) + messageIds[m] = string(mid) + } + rpc.Control.Ihave[i] = &pb.ControlIHave{MessageIDs: messageIds} + rpc.Control.Iwant[i] = &pb.ControlIWant{MessageIDs: messageIds} + } + results, err = fragmentRPC(rpc, limit) + if err != nil { + t.Fatal(err) + } + ensureBelowLimit(results) + minExpectedCtl := rpc.Control.Size() / limit + minExpectedRPCs := (nMessages / msgsPerRPC) + minExpectedCtl + if len(results) < minExpectedRPCs { + t.Fatalf("expected at least %d total RPCs (at least %d with control messages), got %d total", expectedRPCs, expectedCtrl, len(results)) + } + + // Test the pathological case where a single gossip message ID exceeds the limit. + // It should not be present in the fragmented messages, but smaller IDs should be + rpc.Reset() + giantIdBytes := make([]byte, limit*2) + crand.Read(giantIdBytes) + rpc.Control = &pb.ControlMessage{ + Iwant: []*pb.ControlIWant{ + {MessageIDs: []string{"hello", string(giantIdBytes)}}, + }, + } + results, _ = fragmentRPC(rpc, limit) + + // The old behavior would silently drop the giant ID. + // Now we return a the giant ID in a RPC by itself so that it can be + // dropped before actually sending the RPC. This lets us log the anamoly. + // To keep this test useful, we implement the old behavior here. + filtered := make([]RPC, 0, len(results)) + for _, r := range results { + if r.Size() < limit { + filtered = append(filtered, r) + } + } + results = filtered + err = nil + if !validRPCSizes(results, limit) { + err = fmt.Errorf("RPC size exceeds limit") + } + + if err != nil { + t.Fatal(err) + } + if len(results) != 1 { + t.Fatalf("expected 1 RPC, got %d", len(results)) + } + if len(results[0].Control.Iwant) != 1 { + t.Fatalf("expected 1 IWANT, got %d", len(results[0].Control.Iwant)) + } + if results[0].Control.Iwant[0].MessageIDs[0] != "hello" { + t.Fatalf("expected small message ID to be included unaltered, got %s instead", + results[0].Control.Iwant[0].MessageIDs[0]) + } + }) +} + +func FuzzRPCSplit(f *testing.F) { + minMaxMsgSize := 100 + maxMaxMsgSize := 2048 + f.Fuzz(func(t *testing.T, data []byte) { + maxSize := int(generateU16(&data)) % maxMaxMsgSize + if maxSize < minMaxMsgSize { + maxSize = minMaxMsgSize + } + rpc := generateRPC(data, maxSize) + + originalControl := compressedRPC{ihave: make(map[string][]string)} + originalControl.append(&rpc.RPC) + mergedControl := compressedRPC{ihave: make(map[string][]string)} + + for rpc := range rpc.split(maxSize) { + if rpc.Size() > maxSize { + t.Fatalf("invalid RPC size %v %d (max=%d)", rpc, rpc.Size(), maxSize) } mergedControl.append(&rpc.RPC) } @@ -2829,267 +2917,273 @@ func (c *compressedRPC) append(rpc *pb.RPC) { } func TestGossipsubManagesAnAddressBook(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - // Create a pair of hosts - hosts := getDefaultHosts(t, 2) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + // Create a pair of hosts + hosts := getDefaultHosts(t, 2) - psubs := getGossipsubs(ctx, hosts) - connectAll(t, hosts) + psubs := getGossipsubs(ctx, hosts) + connectAll(t, hosts) - // wait for identify events to propagate - time.Sleep(time.Second) + // wait for identify events to propagate + time.Sleep(time.Second) - // Check that the address book is populated - cab, ok := peerstore.GetCertifiedAddrBook(psubs[0].rt.(*GossipSubRouter).cab) - if !ok { - t.Fatalf("expected a certified address book") - } + // Check that the address book is populated + cab, ok := peerstore.GetCertifiedAddrBook(psubs[0].rt.(*GossipSubRouter).cab) + if !ok { + t.Fatalf("expected a certified address book") + } - env := cab.GetPeerRecord(hosts[1].ID()) - if env == nil { - t.Fatalf("expected a peer record for host 1") - } + env := cab.GetPeerRecord(hosts[1].ID()) + if env == nil { + t.Fatalf("expected a peer record for host 1") + } - // Disconnect host 1. Host 0 should then update the TTL of the address book - for _, c := range hosts[1].Network().Conns() { - c.Close() - } - time.Sleep(time.Second) - - // This only updates addrs that are marked as recently connected, which should be all of them - psubs[0].rt.(*GossipSubRouter).cab.UpdateAddrs(hosts[1].ID(), peerstore.RecentlyConnectedAddrTTL, 0) - addrs := psubs[0].rt.(*GossipSubRouter).cab.Addrs(hosts[1].ID()) - // There should be no Addrs left because we cleared all recently connected ones. - if len(addrs) != 0 { - t.Fatalf("expected no addrs, got %d addrs", len(addrs)) - } + // Disconnect host 1. Host 0 should then update the TTL of the address book + for _, c := range hosts[1].Network().Conns() { + c.Close() + } + time.Sleep(time.Second) + + // This only updates addrs that are marked as recently connected, which should be all of them + psubs[0].rt.(*GossipSubRouter).cab.UpdateAddrs(hosts[1].ID(), peerstore.RecentlyConnectedAddrTTL, 0) + addrs := psubs[0].rt.(*GossipSubRouter).cab.Addrs(hosts[1].ID()) + // There should be no Addrs left because we cleared all recently connected ones. + if len(addrs) != 0 { + t.Fatalf("expected no addrs, got %d addrs", len(addrs)) + } + }) } func TestGossipsubIdontwantSend(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 3) - - msgID := func(pmsg *pb.Message) string { - // silly content-based test message-ID: just use the data as whole - return base64.URLEncoding.EncodeToString(pmsg.Data) - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 3) - var validated atomic.Bool - validate := func(context.Context, peer.ID, *Message) bool { - time.Sleep(100 * time.Millisecond) - validated.Store(true) - return true - } + msgID := func(pmsg *pb.Message) string { + // silly content-based test message-ID: just use the data as whole + return base64.URLEncoding.EncodeToString(pmsg.Data) + } - params := DefaultGossipSubParams() - params.IDontWantMessageThreshold = 16 - - psubs := make([]*PubSub, 2) - psubs[0] = getGossipsub(ctx, hosts[0], - WithGossipSubParams(params), - WithMessageIdFn(msgID)) - psubs[1] = getGossipsub(ctx, hosts[1], - WithGossipSubParams(params), - WithMessageIdFn(msgID), - WithDefaultValidator(validate)) - - topic := "foobar" - for _, ps := range psubs { - _, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + var validated atomic.Bool + validate := func(context.Context, peer.ID, *Message) bool { + time.Sleep(100 * time.Millisecond) + validated.Store(true) + return true } - } - var midsMu sync.Mutex - var expMids []string - var actMids []string + params := DefaultGossipSubParams() + params.IDontWantMessageThreshold = 16 - // Used to publish a message with random data - publishMsg := func() { - midsMu.Lock() - defer midsMu.Unlock() - data := make([]byte, 16) - crand.Read(data) - m := &pb.Message{Data: data} - expMids = append(expMids, msgID(m)) + psubs := make([]*PubSub, 2) + psubs[0] = getGossipsub(ctx, hosts[0], + WithGossipSubParams(params), + WithMessageIdFn(msgID)) + psubs[1] = getGossipsub(ctx, hosts[1], + WithGossipSubParams(params), + WithMessageIdFn(msgID), + WithDefaultValidator(validate)) - if err := psubs[0].Publish(topic, data); err != nil { - t.Fatal(err) + topic := "foobar" + for _, ps := range psubs { + _, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } } - } - // Wait a bit after the last message before checking we got the right messages - msgWaitMax := time.Second - msgTimer := time.NewTimer(msgWaitMax) + var midsMu sync.Mutex + var expMids []string + var actMids []string - // Checks we received the right IDONTWANT messages - checkMsgs := func() { - midsMu.Lock() - defer midsMu.Unlock() - sort.Strings(actMids) - sort.Strings(expMids) + // Used to publish a message with random data + publishMsg := func() { + midsMu.Lock() + defer midsMu.Unlock() + data := make([]byte, 16) + crand.Read(data) + m := &pb.Message{Data: data} + expMids = append(expMids, msgID(m)) - if len(actMids) != len(expMids) { - t.Fatalf("Expected %d IDONTWANT messages, got %d", len(expMids), len(actMids)) - } - for i, expMid := range expMids { - actMid := actMids[i] - if actMid != expMid { - t.Fatalf("Expected the id of %s in the %d'th IDONTWANT messages, got %s", expMid, i+1, actMid) + if err := psubs[0].Publish(topic, data); err != nil { + t.Fatal(err) } } - } - - // Wait for the timer to expire - go func() { - select { - case <-msgTimer.C: - checkMsgs() - cancel() - return - case <-ctx.Done(): - checkMsgs() - } - }() - newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - // When the middle peer connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and grafting to the middle peer - writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, - }) + // Wait a bit after the last message before checking we got the right messages + msgWaitMax := time.Second + msgTimer := time.NewTimer(msgWaitMax) - go func() { - // Wait for a short interval to make sure the middle peer - // received and processed the subscribe + graft - time.Sleep(100 * time.Millisecond) + // Checks we received the right IDONTWANT messages + checkMsgs := func() { + midsMu.Lock() + defer midsMu.Unlock() + sort.Strings(actMids) + sort.Strings(expMids) - // Publish messages from the first peer - for i := 0; i < 10; i++ { - publishMsg() - } - }() + if len(actMids) != len(expMids) { + t.Fatalf("Expected %d IDONTWANT messages, got %d", len(expMids), len(actMids)) + } + for i, expMid := range expMids { + actMid := actMids[i] + if actMid != expMid { + t.Fatalf("Expected the id of %s in the %d'th IDONTWANT messages, got %s", expMid, i+1, actMid) + } } } - // Each time the middle peer sends an IDONTWANT message - for _, idonthave := range irpc.GetControl().GetIdontwant() { - // If true, it means that, when we get IDONTWANT, the middle peer has done validation - // already, which should not be the case - if validated.Load() { - t.Fatalf("IDONTWANT should be sent before doing validation") + // Wait for the timer to expire + go func() { + select { + case <-msgTimer.C: + checkMsgs() + cancel() + return + case <-ctx.Done(): + checkMsgs() } - for _, mid := range idonthave.GetMessageIDs() { - // Add the message to the list and reset the timer - actMids = append(actMids, mid) - msgTimer.Reset(msgWaitMax) + }() + + newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + // When the middle peer connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and grafting to the middle peer + writeMsg(&pb.RPC{ + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, + }) + + go func() { + // Wait for a short interval to make sure the middle peer + // received and processed the subscribe + graft + time.Sleep(100 * time.Millisecond) + + // Publish messages from the first peer + for i := 0; i < 10; i++ { + publishMsg() + } + }() + } } - } - }) - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) + // Each time the middle peer sends an IDONTWANT message + for _, idonthave := range irpc.GetControl().GetIdontwant() { + // If true, it means that, when we get IDONTWANT, the middle peer has done validation + // already, which should not be the case + if validated.Load() { + t.Fatalf("IDONTWANT should be sent before doing validation") + } + for _, mid := range idonthave.GetMessageIDs() { + // Add the message to the list and reset the timer + actMids = append(actMids, mid) + msgTimer.Reset(msgWaitMax) + } + } + }) + + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) - <-ctx.Done() + <-ctx.Done() + }) } func TestGossipsubIdontwantReceive(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 3) - - msgID := func(pmsg *pb.Message) string { - // silly content-based test message-ID: just use the data as whole - return base64.URLEncoding.EncodeToString(pmsg.Data) - } - - psubs := make([]*PubSub, 2) - psubs[0] = getGossipsub(ctx, hosts[0], WithMessageIdFn(msgID)) - psubs[1] = getGossipsub(ctx, hosts[1], WithMessageIdFn(msgID)) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 3) - topic := "foobar" - for _, ps := range psubs { - _, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + msgID := func(pmsg *pb.Message) string { + // silly content-based test message-ID: just use the data as whole + return base64.URLEncoding.EncodeToString(pmsg.Data) } - } - // Wait a bit after the last message before checking the result - msgWaitMax := time.Second - msgTimer := time.NewTimer(msgWaitMax) + psubs := make([]*PubSub, 2) + psubs[0] = getGossipsub(ctx, hosts[0], WithMessageIdFn(msgID)) + psubs[1] = getGossipsub(ctx, hosts[1], WithMessageIdFn(msgID)) - // Checks we received no messages - received := false - checkMsgs := func() { - if received { - t.Fatalf("Expected no messages received after IDONWANT") + topic := "foobar" + for _, ps := range psubs { + _, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } } - } - // Wait for the timer to expire - go func() { - select { - case <-msgTimer.C: - checkMsgs() - cancel() - return - case <-ctx.Done(): - checkMsgs() - } - }() + // Wait a bit after the last message before checking the result + msgWaitMax := time.Second + msgTimer := time.NewTimer(msgWaitMax) - newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - // Check if it receives any message - if len(irpc.GetPublish()) > 0 { - received = true + // Checks we received no messages + received := false + checkMsgs := func() { + if received { + t.Fatalf("Expected no messages received after IDONWANT") + } } - // When the middle peer connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and grafting to the middle peer - writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, - }) - go func() { - // Wait for a short interval to make sure the middle peer - // received and processed the subscribe + graft - time.Sleep(100 * time.Millisecond) - - // Generate a message and send IDONTWANT to the middle peer - data := make([]byte, 16) - crand.Read(data) - mid := msgID(&pb.Message{Data: data}) + // Wait for the timer to expire + go func() { + select { + case <-msgTimer.C: + checkMsgs() + cancel() + return + case <-ctx.Done(): + checkMsgs() + } + }() + + newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + // Check if it receives any message + if len(irpc.GetPublish()) > 0 { + received = true + } + // When the middle peer connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and grafting to the middle peer writeMsg(&pb.RPC{ - Control: &pb.ControlMessage{Idontwant: []*pb.ControlIDontWant{{MessageIDs: []string{mid}}}}, + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, }) - // Wait for a short interval to make sure the middle peer - // received and processed the IDONTWANTs - time.Sleep(100 * time.Millisecond) - - // Publish the message from the first peer - if err := psubs[0].Publish(topic, data); err != nil { - t.Error(err) - return // cannot call t.Fatal in a non-test goroutine - } - }() + go func() { + // Wait for a short interval to make sure the middle peer + // received and processed the subscribe + graft + time.Sleep(100 * time.Millisecond) + + // Generate a message and send IDONTWANT to the middle peer + data := make([]byte, 16) + crand.Read(data) + mid := msgID(&pb.Message{Data: data}) + writeMsg(&pb.RPC{ + Control: &pb.ControlMessage{Idontwant: []*pb.ControlIDontWant{{MessageIDs: []string{mid}}}}, + }) + + // Wait for a short interval to make sure the middle peer + // received and processed the IDONTWANTs + time.Sleep(100 * time.Millisecond) + + // Publish the message from the first peer + if err := psubs[0].Publish(topic, data); err != nil { + t.Error(err) + return // cannot call t.Fatal in a non-test goroutine + } + }() + } } - } - }) + }) - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) - <-ctx.Done() + <-ctx.Done() + }) } type mockRawTracer struct { @@ -3120,622 +3214,638 @@ func (m *mockRawTracer) ValidateMessage(msg *Message) {} var _ RawTracer = &mockRawTracer{} func TestGossipsubNoIDONTWANTToMessageSender(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 2) - denseConnect(t, hosts) - - psubs := make([]*PubSub, 2) - - receivedIDONTWANT := make(chan struct{}) - psubs[0] = getGossipsub(ctx, hosts[0], WithRawTracer(&mockRawTracer{ - onRecvRPC: func(rpc *RPC) { - if len(rpc.GetControl().GetIdontwant()) > 0 { - close(receivedIDONTWANT) - } - }, - })) - psubs[1] = getGossipsub(ctx, hosts[1]) - - topicString := "foobar" - var topics []*Topic - for _, ps := range psubs { - topic, err := ps.Join(topicString) - if err != nil { - t.Fatal(err) - } - topics = append(topics, topic) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 2) + denseConnect(t, hosts) - _, err = ps.Subscribe(topicString) - if err != nil { - t.Fatal(err) + psubs := make([]*PubSub, 2) + + receivedIDONTWANT := make(chan struct{}) + psubs[0] = getGossipsub(ctx, hosts[0], WithRawTracer(&mockRawTracer{ + onRecvRPC: func(rpc *RPC) { + if len(rpc.GetControl().GetIdontwant()) > 0 { + close(receivedIDONTWANT) + } + }, + })) + psubs[1] = getGossipsub(ctx, hosts[1]) + + topicString := "foobar" + var topics []*Topic + for _, ps := range psubs { + topic, err := ps.Join(topicString) + if err != nil { + t.Fatal(err) + } + topics = append(topics, topic) + + _, err = ps.Subscribe(topicString) + if err != nil { + t.Fatal(err) + } } - } - time.Sleep(time.Second) + time.Sleep(time.Second) - msg := make([]byte, GossipSubIDontWantMessageThreshold+1) - topics[0].Publish(ctx, msg) + msg := make([]byte, GossipSubIDontWantMessageThreshold+1) + topics[0].Publish(ctx, msg) - select { - case <-receivedIDONTWANT: - t.Fatal("IDONTWANT should not be sent to the message sender") - case <-time.After(time.Second): - } + select { + case <-receivedIDONTWANT: + t.Fatal("IDONTWANT should not be sent to the message sender") + case <-time.After(time.Second): + } + }) } func TestGossipsubIDONTWANTBeforeFirstPublish(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 2) - denseConnect(t, hosts) - - psubs := make([]*PubSub, 2) - - psubs[0] = getGossipsub(ctx, hosts[0]) - rpcsReceived := make(chan string) - psubs[1] = getGossipsub(ctx, hosts[1], WithRawTracer(&mockRawTracer{ - onRecvRPC: func(rpc *RPC) { - if len(rpc.GetControl().GetIdontwant()) > 0 { - rpcsReceived <- "idontwant" - } - if len(rpc.GetPublish()) > 0 { - rpcsReceived <- "publish" - } - }, - })) - - topicString := "foobar" - var topics []*Topic - for _, ps := range psubs { - topic, err := ps.Join(topicString) - if err != nil { - t.Fatal(err) - } - topics = append(topics, topic) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 2) + denseConnect(t, hosts) - _, err = ps.Subscribe(topicString) - if err != nil { - t.Fatal(err) + psubs := make([]*PubSub, 2) + + psubs[0] = getGossipsub(ctx, hosts[0]) + rpcsReceived := make(chan string) + psubs[1] = getGossipsub(ctx, hosts[1], WithRawTracer(&mockRawTracer{ + onRecvRPC: func(rpc *RPC) { + if len(rpc.GetControl().GetIdontwant()) > 0 { + rpcsReceived <- "idontwant" + } + if len(rpc.GetPublish()) > 0 { + rpcsReceived <- "publish" + } + }, + })) + + topicString := "foobar" + var topics []*Topic + for _, ps := range psubs { + topic, err := ps.Join(topicString) + if err != nil { + t.Fatal(err) + } + topics = append(topics, topic) + + _, err = ps.Subscribe(topicString) + if err != nil { + t.Fatal(err) + } } - } - time.Sleep(2 * time.Second) + time.Sleep(2 * time.Second) - msg := make([]byte, GossipSubIDontWantMessageThreshold+1) - _ = topics[0].Publish(ctx, msg) + msg := make([]byte, GossipSubIDontWantMessageThreshold+1) + _ = topics[0].Publish(ctx, msg) - timeout := time.After(5 * time.Second) + timeout := time.After(5 * time.Second) - select { - case kind := <-rpcsReceived: - if kind == "publish" { - t.Fatal("IDONTWANT should be sent before publish") + select { + case kind := <-rpcsReceived: + if kind == "publish" { + t.Fatal("IDONTWANT should be sent before publish") + } + case <-timeout: + t.Fatal("IDONTWANT should be sent on first publish") } - case <-timeout: - t.Fatal("IDONTWANT should be sent on first publish") - } - select { - case kind := <-rpcsReceived: - if kind != "publish" { + select { + case kind := <-rpcsReceived: + if kind != "publish" { + t.Fatal("Expected publish after IDONTWANT") + } + case <-timeout: t.Fatal("Expected publish after IDONTWANT") } - case <-timeout: - t.Fatal("Expected publish after IDONTWANT") - } + }) } // Test that non-mesh peers will not get IDONTWANT func TestGossipsubIdontwantNonMesh(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 3) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 3) - params := DefaultGossipSubParams() - params.IDontWantMessageThreshold = 16 - psubs := getGossipsubs(ctx, hosts[:2], WithGossipSubParams(params)) + params := DefaultGossipSubParams() + params.IDontWantMessageThreshold = 16 + psubs := getGossipsubs(ctx, hosts[:2], WithGossipSubParams(params)) - topic := "foobar" - for _, ps := range psubs { - _, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + topic := "foobar" + for _, ps := range psubs { + _, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } } - } - // Used to publish a message with random data - publishMsg := func() { - data := make([]byte, 16) - crand.Read(data) + // Used to publish a message with random data + publishMsg := func() { + data := make([]byte, 16) + crand.Read(data) - if err := psubs[0].Publish(topic, data); err != nil { - t.Fatal(err) + if err := psubs[0].Publish(topic, data); err != nil { + t.Fatal(err) + } } - } - // Wait a bit after the last message before checking we got the right messages - msgWaitMax := time.Second - msgTimer := time.NewTimer(msgWaitMax) - received := false + // Wait a bit after the last message before checking we got the right messages + msgWaitMax := time.Second + msgTimer := time.NewTimer(msgWaitMax) + received := false - // Checks if we received any IDONTWANT - checkMsgs := func() { - if received { - t.Fatalf("No IDONTWANT is expected") + // Checks if we received any IDONTWANT + checkMsgs := func() { + if received { + t.Fatalf("No IDONTWANT is expected") + } } - } - // Wait for the timer to expire - go func() { - select { - case <-msgTimer.C: - checkMsgs() - cancel() - return - case <-ctx.Done(): - checkMsgs() - } - }() + // Wait for the timer to expire + go func() { + select { + case <-msgTimer.C: + checkMsgs() + cancel() + return + case <-ctx.Done(): + checkMsgs() + } + }() - newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - // When the middle peer connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and pruning to the middle peer to make sure - // that it's not in the mesh - writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Prune: []*pb.ControlPrune{{TopicID: sub.Topicid}}}, - }) + newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + // When the middle peer connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and pruning to the middle peer to make sure + // that it's not in the mesh + writeMsg(&pb.RPC{ + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Prune: []*pb.ControlPrune{{TopicID: sub.Topicid}}}, + }) - go func() { - // Wait for a short interval to make sure the middle peer - // received and processed the subscribe - time.Sleep(100 * time.Millisecond) + go func() { + // Wait for a short interval to make sure the middle peer + // received and processed the subscribe + time.Sleep(100 * time.Millisecond) - // Publish messages from the first peer - for i := 0; i < 10; i++ { - publishMsg() - } - }() + // Publish messages from the first peer + for i := 0; i < 10; i++ { + publishMsg() + } + }() + } } - } - // Each time the middle peer sends an IDONTWANT message - for range irpc.GetControl().GetIdontwant() { - received = true - } - }) + // Each time the middle peer sends an IDONTWANT message + for range irpc.GetControl().GetIdontwant() { + received = true + } + }) - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) - <-ctx.Done() + <-ctx.Done() + }) } // Test that peers with incompatible versions will not get IDONTWANT func TestGossipsubIdontwantIncompat(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 3) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 3) - params := DefaultGossipSubParams() - params.IDontWantMessageThreshold = 16 - psubs := getGossipsubs(ctx, hosts[:2], WithGossipSubParams(params)) + params := DefaultGossipSubParams() + params.IDontWantMessageThreshold = 16 + psubs := getGossipsubs(ctx, hosts[:2], WithGossipSubParams(params)) - topic := "foobar" - for _, ps := range psubs { - _, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + topic := "foobar" + for _, ps := range psubs { + _, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } } - } - // Used to publish a message with random data - publishMsg := func() { - data := make([]byte, 16) - crand.Read(data) + // Used to publish a message with random data + publishMsg := func() { + data := make([]byte, 16) + crand.Read(data) - if err := psubs[0].Publish(topic, data); err != nil { - t.Fatal(err) + if err := psubs[0].Publish(topic, data); err != nil { + t.Fatal(err) + } } - } - // Wait a bit after the last message before checking we got the right messages - msgWaitMax := time.Second - msgTimer := time.NewTimer(msgWaitMax) - received := false + // Wait a bit after the last message before checking we got the right messages + msgWaitMax := time.Second + msgTimer := time.NewTimer(msgWaitMax) + received := false - // Checks if we received any IDONTWANT - checkMsgs := func() { - if received { - t.Fatalf("No IDONTWANT is expected") + // Checks if we received any IDONTWANT + checkMsgs := func() { + if received { + t.Fatalf("No IDONTWANT is expected") + } } - } - // Wait for the timer to expire - go func() { - select { - case <-msgTimer.C: - checkMsgs() - cancel() - return - case <-ctx.Done(): - checkMsgs() - } - }() + // Wait for the timer to expire + go func() { + select { + case <-msgTimer.C: + checkMsgs() + cancel() + return + case <-ctx.Done(): + checkMsgs() + } + }() - // Use the old GossipSub version - newMockGSWithVersion(ctx, t, hosts[2], protocol.ID("/meshsub/1.1.0"), func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - // When the middle peer connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and grafting to the middle peer - writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, - }) + // Use the old GossipSub version + newMockGSWithVersion(ctx, t, hosts[2], protocol.ID("/meshsub/1.1.0"), func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + // When the middle peer connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and grafting to the middle peer + writeMsg(&pb.RPC{ + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, + }) - go func() { - // Wait for a short interval to make sure the middle peer - // received and processed the subscribe + graft - time.Sleep(100 * time.Millisecond) + go func() { + // Wait for a short interval to make sure the middle peer + // received and processed the subscribe + graft + time.Sleep(100 * time.Millisecond) - // Publish messages from the first peer - for i := 0; i < 10; i++ { - publishMsg() - } - }() + // Publish messages from the first peer + for i := 0; i < 10; i++ { + publishMsg() + } + }() + } } - } - // Each time the middle peer sends an IDONTWANT message - for range irpc.GetControl().GetIdontwant() { - received = true - } - }) + // Each time the middle peer sends an IDONTWANT message + for range irpc.GetControl().GetIdontwant() { + received = true + } + }) - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) - <-ctx.Done() + <-ctx.Done() + }) } // Test that IDONTWANT will not be sent for small messages func TestGossipsubIdontwantSmallMessage(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 3) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 3) - params := DefaultGossipSubParams() - params.IDontWantMessageThreshold = 16 - psubs := getGossipsubs(ctx, hosts[:2], WithGossipSubParams(params)) + params := DefaultGossipSubParams() + params.IDontWantMessageThreshold = 16 + psubs := getGossipsubs(ctx, hosts[:2], WithGossipSubParams(params)) - topic := "foobar" - for _, ps := range psubs { - _, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + topic := "foobar" + for _, ps := range psubs { + _, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } } - } - // Used to publish a message with random data - publishMsg := func() { - data := make([]byte, 8) - crand.Read(data) + // Used to publish a message with random data + publishMsg := func() { + data := make([]byte, 8) + crand.Read(data) - if err := psubs[0].Publish(topic, data); err != nil { - t.Fatal(err) + if err := psubs[0].Publish(topic, data); err != nil { + t.Fatal(err) + } } - } - // Wait a bit after the last message before checking we got the right messages - msgWaitMax := time.Second - msgTimer := time.NewTimer(msgWaitMax) - received := false + // Wait a bit after the last message before checking we got the right messages + msgWaitMax := time.Second + msgTimer := time.NewTimer(msgWaitMax) + received := false - // Checks if we received any IDONTWANT - checkMsgs := func() { - if received { - t.Fatalf("No IDONTWANT is expected") + // Checks if we received any IDONTWANT + checkMsgs := func() { + if received { + t.Fatalf("No IDONTWANT is expected") + } } - } - // Wait for the timer to expire - go func() { - select { - case <-msgTimer.C: - checkMsgs() - cancel() - return - case <-ctx.Done(): - checkMsgs() - } - }() + // Wait for the timer to expire + go func() { + select { + case <-msgTimer.C: + checkMsgs() + cancel() + return + case <-ctx.Done(): + checkMsgs() + } + }() - newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - // When the middle peer connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and pruning to the middle peer to make sure - // that it's not in the mesh - writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, - }) + newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + // When the middle peer connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and pruning to the middle peer to make sure + // that it's not in the mesh + writeMsg(&pb.RPC{ + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, + }) - go func() { - // Wait for a short interval to make sure the middle peer - // received and processed the subscribe - time.Sleep(100 * time.Millisecond) + go func() { + // Wait for a short interval to make sure the middle peer + // received and processed the subscribe + time.Sleep(100 * time.Millisecond) - // Publish messages from the first peer - for i := 0; i < 10; i++ { - publishMsg() - } - }() + // Publish messages from the first peer + for i := 0; i < 10; i++ { + publishMsg() + } + }() + } } - } - // Each time the middle peer sends an IDONTWANT message - for range irpc.GetControl().GetIdontwant() { - received = true - } - }) + // Each time the middle peer sends an IDONTWANT message + for range irpc.GetControl().GetIdontwant() { + received = true + } + }) - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) - <-ctx.Done() + <-ctx.Done() + }) } // Test that IWANT will have no effect after IDONTWANT is sent func TestGossipsubIdontwantBeforeIwant(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 3) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 3) - msgID := func(pmsg *pb.Message) string { - // silly content-based test message-ID: just use the data as whole - return base64.URLEncoding.EncodeToString(pmsg.Data) - } + msgID := func(pmsg *pb.Message) string { + // silly content-based test message-ID: just use the data as whole + return base64.URLEncoding.EncodeToString(pmsg.Data) + } - psubs := make([]*PubSub, 2) - psubs[0] = getGossipsub(ctx, hosts[0], WithMessageIdFn(msgID)) - psubs[1] = getGossipsub(ctx, hosts[1], WithMessageIdFn(msgID)) + psubs := make([]*PubSub, 2) + psubs[0] = getGossipsub(ctx, hosts[0], WithMessageIdFn(msgID)) + psubs[1] = getGossipsub(ctx, hosts[1], WithMessageIdFn(msgID)) - topic := "foobar" - for _, ps := range psubs { - _, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + topic := "foobar" + for _, ps := range psubs { + _, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } } - } - // Wait a bit after the last message before checking the result - msgWaitMax := 2 * time.Second - msgTimer := time.NewTimer(msgWaitMax) + // Wait a bit after the last message before checking the result + msgWaitMax := 2 * time.Second + msgTimer := time.NewTimer(msgWaitMax) - // Checks we received right messages - var msgReceived atomic.Bool - var ihaveReceived atomic.Bool - checkMsgs := func() { - if msgReceived.Load() { - t.Fatalf("Expected no messages received after IDONWANT") - } - if !ihaveReceived.Load() { - t.Fatalf("Expected IHAVE received") + // Checks we received right messages + var msgReceived atomic.Bool + var ihaveReceived atomic.Bool + checkMsgs := func() { + if msgReceived.Load() { + t.Fatalf("Expected no messages received after IDONWANT") + } + if !ihaveReceived.Load() { + t.Fatalf("Expected IHAVE received") + } } - } - // Wait for the timer to expire - go func() { - select { - case <-msgTimer.C: - checkMsgs() - cancel() - return - case <-ctx.Done(): - checkMsgs() - } - }() + // Wait for the timer to expire + go func() { + select { + case <-msgTimer.C: + checkMsgs() + cancel() + return + case <-ctx.Done(): + checkMsgs() + } + }() - newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - // Check if it receives any message - if len(irpc.GetPublish()) > 0 { - msgReceived.Store(true) - } - // The middle peer is supposed to send IHAVE - for _, ihave := range irpc.GetControl().GetIhave() { - ihaveReceived.Store(true) - mids := ihave.GetMessageIDs() + newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + // Check if it receives any message + if len(irpc.GetPublish()) > 0 { + msgReceived.Store(true) + } + // The middle peer is supposed to send IHAVE + for _, ihave := range irpc.GetControl().GetIhave() { + ihaveReceived.Store(true) + mids := ihave.GetMessageIDs() - writeMsg(&pb.RPC{ - Control: &pb.ControlMessage{Idontwant: []*pb.ControlIDontWant{{MessageIDs: mids}}}, - }) - // Wait for the middle peer to process IDONTWANT - time.Sleep(100 * time.Millisecond) - writeMsg(&pb.RPC{ - Control: &pb.ControlMessage{Iwant: []*pb.ControlIWant{{MessageIDs: mids}}}, - }) - } - // When the middle peer connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and pruning to the middle peer to make sure - // that it's not in the mesh writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Prune: []*pb.ControlPrune{{TopicID: sub.Topicid}}}, + Control: &pb.ControlMessage{Idontwant: []*pb.ControlIDontWant{{MessageIDs: mids}}}, }) + // Wait for the middle peer to process IDONTWANT + time.Sleep(100 * time.Millisecond) + writeMsg(&pb.RPC{ + Control: &pb.ControlMessage{Iwant: []*pb.ControlIWant{{MessageIDs: mids}}}, + }) + } + // When the middle peer connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and pruning to the middle peer to make sure + // that it's not in the mesh + writeMsg(&pb.RPC{ + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Prune: []*pb.ControlPrune{{TopicID: sub.Topicid}}}, + }) - go func() { - // Wait for an interval to make sure the middle peer - // received and processed the subscribe - time.Sleep(100 * time.Millisecond) + go func() { + // Wait for an interval to make sure the middle peer + // received and processed the subscribe + time.Sleep(100 * time.Millisecond) - data := make([]byte, 16) - crand.Read(data) + data := make([]byte, 16) + crand.Read(data) - // Publish the message from the first peer - if err := psubs[0].Publish(topic, data); err != nil { - t.Error(err) - return // cannot call t.Fatal in a non-test goroutine - } - }() + // Publish the message from the first peer + if err := psubs[0].Publish(topic, data); err != nil { + t.Error(err) + return // cannot call t.Fatal in a non-test goroutine + } + }() + } } - } - }) + }) - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) - <-ctx.Done() + <-ctx.Done() + }) } // Test that IDONTWANT will cleared when it's old enough func TestGossipsubIdontwantClear(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 3) - - msgID := func(pmsg *pb.Message) string { - // silly content-based test message-ID: just use the data as whole - return base64.URLEncoding.EncodeToString(pmsg.Data) - } - - psubs := make([]*PubSub, 2) - psubs[0] = getGossipsub(ctx, hosts[0], WithMessageIdFn(msgID)) - psubs[1] = getGossipsub(ctx, hosts[1], WithMessageIdFn(msgID)) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 3) - topic := "foobar" - for _, ps := range psubs { - _, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + msgID := func(pmsg *pb.Message) string { + // silly content-based test message-ID: just use the data as whole + return base64.URLEncoding.EncodeToString(pmsg.Data) } - } - // Wait a bit after the last message before checking the result - msgWaitMax := 5 * time.Second - msgTimer := time.NewTimer(msgWaitMax) + psubs := make([]*PubSub, 2) + psubs[0] = getGossipsub(ctx, hosts[0], WithMessageIdFn(msgID)) + psubs[1] = getGossipsub(ctx, hosts[1], WithMessageIdFn(msgID)) - // Checks we received some message after the IDONTWANT is cleared - var received atomic.Bool - checkMsgs := func() { - if !received.Load() { - t.Fatalf("Expected some message after the IDONTWANT is cleared") + topic := "foobar" + for _, ps := range psubs { + _, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } } - } - // Wait for the timer to expire - go func() { - select { - case <-msgTimer.C: - checkMsgs() - cancel() - return - case <-ctx.Done(): - checkMsgs() - } - }() + // Wait a bit after the last message before checking the result + msgWaitMax := 5 * time.Second + msgTimer := time.NewTimer(msgWaitMax) - newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { - // Check if it receives any message - if len(irpc.GetPublish()) > 0 { - received.Store(true) + // Checks we received some message after the IDONTWANT is cleared + var received atomic.Bool + checkMsgs := func() { + if !received.Load() { + t.Fatalf("Expected some message after the IDONTWANT is cleared") + } } - // When the middle peer connects it will send us its subscriptions - for _, sub := range irpc.GetSubscriptions() { - if sub.GetSubscribe() { - // Reply by subcribing to the topic and grafting to the middle peer - writeMsg(&pb.RPC{ - Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, - Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, - }) - go func() { - // Wait for a short interval to make sure the middle peer - // received and processed the subscribe + graft - time.Sleep(100 * time.Millisecond) - - // Generate a message and send IDONTWANT to the middle peer - data := make([]byte, 16) - crand.Read(data) - mid := msgID(&pb.Message{Data: data}) + // Wait for the timer to expire + go func() { + select { + case <-msgTimer.C: + checkMsgs() + cancel() + return + case <-ctx.Done(): + checkMsgs() + } + }() + + newMockGS(ctx, t, hosts[2], func(writeMsg func(*pb.RPC), irpc *pb.RPC) { + // Check if it receives any message + if len(irpc.GetPublish()) > 0 { + received.Store(true) + } + // When the middle peer connects it will send us its subscriptions + for _, sub := range irpc.GetSubscriptions() { + if sub.GetSubscribe() { + // Reply by subcribing to the topic and grafting to the middle peer writeMsg(&pb.RPC{ - Control: &pb.ControlMessage{Idontwant: []*pb.ControlIDontWant{{MessageIDs: []string{mid}}}}, + Subscriptions: []*pb.RPC_SubOpts{{Subscribe: sub.Subscribe, Topicid: sub.Topicid}}, + Control: &pb.ControlMessage{Graft: []*pb.ControlGraft{{TopicID: sub.Topicid}}}, }) - // Wait for a short interval to make sure the middle peer - // received and processed the IDONTWANTs - time.Sleep(100 * time.Millisecond) - - // Wait for 4 heartbeats to make sure the IDONTWANT is cleared - time.Sleep(4 * time.Second) - - // Publish the message from the first peer - if err := psubs[0].Publish(topic, data); err != nil { - t.Error(err) - return // cannot call t.Fatal in a non-test goroutine - } - }() + go func() { + // Wait for a short interval to make sure the middle peer + // received and processed the subscribe + graft + time.Sleep(100 * time.Millisecond) + + // Generate a message and send IDONTWANT to the middle peer + data := make([]byte, 16) + crand.Read(data) + mid := msgID(&pb.Message{Data: data}) + writeMsg(&pb.RPC{ + Control: &pb.ControlMessage{Idontwant: []*pb.ControlIDontWant{{MessageIDs: []string{mid}}}}, + }) + + // Wait for a short interval to make sure the middle peer + // received and processed the IDONTWANTs + time.Sleep(100 * time.Millisecond) + + // Wait for 4 heartbeats to make sure the IDONTWANT is cleared + time.Sleep(4 * time.Second) + + // Publish the message from the first peer + if err := psubs[0].Publish(topic, data); err != nil { + t.Error(err) + return // cannot call t.Fatal in a non-test goroutine + } + }() + } } - } - }) + }) - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) - <-ctx.Done() + <-ctx.Done() + }) } func TestGossipsubPruneMeshCorrectly(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 9) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 9) - msgID := func(pmsg *pb.Message) string { - // silly content-based test message-ID: just use the data as whole - return base64.URLEncoding.EncodeToString(pmsg.Data) - } + msgID := func(pmsg *pb.Message) string { + // silly content-based test message-ID: just use the data as whole + return base64.URLEncoding.EncodeToString(pmsg.Data) + } - params := DefaultGossipSubParams() - params.Dhi = 8 + params := DefaultGossipSubParams() + params.Dhi = 8 - psubs := make([]*PubSub, 9) - for i := 0; i < 9; i++ { - psubs[i] = getGossipsub(ctx, hosts[i], - WithGossipSubParams(params), - WithMessageIdFn(msgID)) - } + psubs := make([]*PubSub, 9) + for i := 0; i < 9; i++ { + psubs[i] = getGossipsub(ctx, hosts[i], + WithGossipSubParams(params), + WithMessageIdFn(msgID)) + } - topic := "foobar" - for _, ps := range psubs { - _, err := ps.Subscribe(topic) - if err != nil { - t.Fatal(err) + topic := "foobar" + for _, ps := range psubs { + _, err := ps.Subscribe(topic) + if err != nil { + t.Fatal(err) + } } - } - // Connect first peer with the rest of the 8 other - // peers. - for i := 1; i < 9; i++ { - connect(t, hosts[0], hosts[i]) - } + // Connect first peer with the rest of the 8 other + // peers. + for i := 1; i < 9; i++ { + connect(t, hosts[0], hosts[i]) + } - // Wait for 2 heartbeats to be able to prune excess peers back down to D. - totalTimeToWait := params.HeartbeatInitialDelay + 2*params.HeartbeatInterval - time.Sleep(totalTimeToWait) + // Wait for 2 heartbeats to be able to prune excess peers back down to D. + totalTimeToWait := params.HeartbeatInitialDelay + 2*params.HeartbeatInterval + time.Sleep(totalTimeToWait) - withGSRouter(psubs[0], func(rt *GossipSubRouter) { - meshPeers, ok := rt.mesh[topic] - if !ok { - t.Fatal("mesh does not exist for topic") - } - if len(meshPeers) != params.D { - t.Fatalf("mesh does not have the correct number of peers. Wanted %d but got %d", params.D, len(meshPeers)) - } + withGSRouter(psubs[0], func(rt *GossipSubRouter) { + meshPeers, ok := rt.mesh[topic] + if !ok { + t.Fatal("mesh does not exist for topic") + } + if len(meshPeers) != params.D { + t.Fatalf("mesh does not have the correct number of peers. Wanted %d but got %d", params.D, len(meshPeers)) + } + }) }) } @@ -3748,95 +3858,97 @@ func BenchmarkAllocDoDropRPC(b *testing.B) { } func TestRoundRobinMessageIDScheduler(t *testing.T) { - const maxNumPeers = 256 - const maxNumMessages = 1_000 + synctestTest(t, func(t *testing.T) { + const maxNumPeers = 256 + const maxNumMessages = 1_000 - err := quick.Check(func(numPeers uint16, numMessages uint16) bool { - numPeers = numPeers % maxNumPeers - numMessages = numMessages % maxNumMessages + err := quick.Check(func(numPeers uint16, numMessages uint16) bool { + numPeers = numPeers % maxNumPeers + numMessages = numMessages % maxNumMessages - output := make([]pendingRPC, 0, numMessages*numPeers) + output := make([]pendingRPC, 0, numMessages*numPeers) - var strategy RoundRobinMessageIDScheduler + var strategy RoundRobinMessageIDScheduler - peers := make([]peer.ID, numPeers) - for i := 0; i < int(numPeers); i++ { - peers[i] = peer.ID(fmt.Sprintf("peer%d", i)) - } + peers := make([]peer.ID, numPeers) + for i := 0; i < int(numPeers); i++ { + peers[i] = peer.ID(fmt.Sprintf("peer%d", i)) + } - getID := func(r pendingRPC) string { - return string(r.rpc.Publish[0].Data) - } + getID := func(r pendingRPC) string { + return string(r.rpc.Publish[0].Data) + } - for i := range int(numMessages) { - for j := range int(numPeers) { - strategy.AddRPC(peers[j], fmt.Sprintf("msg%d", i), &RPC{ - RPC: pb.RPC{ - Publish: []*pb.Message{ - { - Data: []byte(fmt.Sprintf("msg%d", i)), + for i := range int(numMessages) { + for j := range int(numPeers) { + strategy.AddRPC(peers[j], fmt.Sprintf("msg%d", i), &RPC{ + RPC: pb.RPC{ + Publish: []*pb.Message{ + { + Data: []byte(fmt.Sprintf("msg%d", i)), + }, }, }, - }, - }) + }) + } } - } - - for p, rpc := range strategy.All() { - output = append(output, pendingRPC{ - peer: p, - rpc: rpc, - }) - } - // Check invariants - // 1. The published rpcs count is the same as the number of messages added - // 2. Before all message IDs are seen, no message ID may be repeated - // 3. The set of message ID + peer ID combinations should be the same as the input + for p, rpc := range strategy.All() { + output = append(output, pendingRPC{ + peer: p, + rpc: rpc, + }) + } - // 1. - expectedCount := int(numMessages) * int(numPeers) - if len(output) != expectedCount { - t.Logf("Expected %d RPCs, got %d", expectedCount, len(output)) - return false - } + // Check invariants + // 1. The published rpcs count is the same as the number of messages added + // 2. Before all message IDs are seen, no message ID may be repeated + // 3. The set of message ID + peer ID combinations should be the same as the input - // 2. - seen := make(map[string]bool) - expected := make(map[string]bool) - for i := 0; i < int(numMessages); i++ { - expected[fmt.Sprintf("msg%d", i)] = true - } + // 1. + expectedCount := int(numMessages) * int(numPeers) + if len(output) != expectedCount { + t.Logf("Expected %d RPCs, got %d", expectedCount, len(output)) + return false + } - for _, rpc := range output { - if expected[getID(rpc)] { - delete(expected, getID(rpc)) + // 2. + seen := make(map[string]bool) + expected := make(map[string]bool) + for i := 0; i < int(numMessages); i++ { + expected[fmt.Sprintf("msg%d", i)] = true } - if seen[getID(rpc)] && len(expected) > 0 { - t.Logf("Message ID %s repeated before all message IDs are seen", getID(rpc)) - return false + + for _, rpc := range output { + if expected[getID(rpc)] { + delete(expected, getID(rpc)) + } + if seen[getID(rpc)] && len(expected) > 0 { + t.Logf("Message ID %s repeated before all message IDs are seen", getID(rpc)) + return false + } + seen[getID(rpc)] = true } - seen[getID(rpc)] = true - } - // 3. - inputSet := make(map[string]bool) - for i := range int(numMessages) { - for j := range int(numPeers) { - inputSet[fmt.Sprintf("msg%d:peer%d", i, j)] = true + // 3. + inputSet := make(map[string]bool) + for i := range int(numMessages) { + for j := range int(numPeers) { + inputSet[fmt.Sprintf("msg%d:peer%d", i, j)] = true + } } - } - for _, rpc := range output { - if !inputSet[getID(rpc)+":"+string(rpc.peer)] { - t.Logf("Message ID %s not in input", getID(rpc)) - return false + for _, rpc := range output { + if !inputSet[getID(rpc)+":"+string(rpc.peer)] { + t.Logf("Message ID %s not in input", getID(rpc)) + return false + } } + return true + }, &quick.Config{MaxCount: 32}) + if err != nil { + t.Fatal(err) } - return true - }, &quick.Config{MaxCount: 32}) - if err != nil { - t.Fatal(err) - } + }) } func BenchmarkRoundRobinMessageIDScheduler(b *testing.B) { @@ -3871,105 +3983,109 @@ func TestMessageBatchPublish(t *testing.T) { concurrentAdds := []bool{false, true} for _, concurrentAdd := range concurrentAdds { t.Run(fmt.Sprintf("WithConcurrentAdd=%v", concurrentAdd), func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - hosts := getDefaultHosts(t, 20) - - msgIDFn := func(msg *pb.Message) string { - hdr := string(msg.Data[0:16]) - msgID := strings.SplitN(hdr, " ", 2) - return msgID[0] - } - const numMessages = 100 - // +8 to account for the gossiping overhead - psubs := getGossipsubs(ctx, hosts, WithMessageIdFn(msgIDFn), WithPeerOutboundQueueSize(numMessages+8), WithValidateQueueSize(numMessages+8)) - - var topics []*Topic - var msgs []*Subscription - for _, ps := range psubs { - topic, err := ps.Join("foobar") - if err != nil { - t.Fatal(err) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + hosts := getDefaultHosts(t, 20) + + msgIDFn := func(msg *pb.Message) string { + hdr := string(msg.Data[0:16]) + msgID := strings.SplitN(hdr, " ", 2) + return msgID[0] } - topics = append(topics, topic) + const numMessages = 100 + // +8 to account for the gossiping overhead + psubs := getGossipsubs(ctx, hosts, WithMessageIdFn(msgIDFn), WithPeerOutboundQueueSize(numMessages+8), WithValidateQueueSize(numMessages+8)) + + var topics []*Topic + var msgs []*Subscription + for _, ps := range psubs { + topic, err := ps.Join("foobar") + if err != nil { + t.Fatal(err) + } + topics = append(topics, topic) - subch, err := topic.Subscribe(WithBufferSize(numMessages + 8)) - if err != nil { - t.Fatal(err) - } + subch, err := topic.Subscribe(WithBufferSize(numMessages + 8)) + if err != nil { + t.Fatal(err) + } - msgs = append(msgs, subch) - } + msgs = append(msgs, subch) + } - sparseConnect(t, hosts) + sparseConnect(t, hosts) - // wait for heartbeats to build mesh - time.Sleep(time.Second * 2) + // wait for heartbeats to build mesh + time.Sleep(time.Second * 2) - var batch MessageBatch - var wg sync.WaitGroup - for i := 0; i < numMessages; i++ { - msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) - if concurrentAdd { - wg.Add(1) - go func() { - defer wg.Done() + var batch MessageBatch + var wg sync.WaitGroup + for i := 0; i < numMessages; i++ { + msg := []byte(fmt.Sprintf("%d it's not a floooooood %d", i, i)) + if concurrentAdd { + wg.Add(1) + go func() { + defer wg.Done() + err := topics[0].AddToBatch(ctx, &batch, msg) + if err != nil { + t.Log(err) + t.Fail() + } + }() + } else { err := topics[0].AddToBatch(ctx, &batch, msg) if err != nil { - t.Log(err) - t.Fail() + t.Fatal(err) } - }() - } else { - err := topics[0].AddToBatch(ctx, &batch, msg) - if err != nil { - t.Fatal(err) } } - } - wg.Wait() - err := psubs[0].PublishBatch(&batch) - if err != nil { - t.Fatal(err) - } + wg.Wait() + err := psubs[0].PublishBatch(&batch) + if err != nil { + t.Fatal(err) + } - for range numMessages { - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(err) - } - id := msgIDFn(got.Message) - expected := []byte(fmt.Sprintf("%s it's not a floooooood %s", id, id)) - if !bytes.Equal(expected, got.Data) { - t.Fatal("got wrong message!") + for range numMessages { + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(err) + } + id := msgIDFn(got.Message) + expected := []byte(fmt.Sprintf("%s it's not a floooooood %s", id, id)) + if !bytes.Equal(expected, got.Data) { + t.Fatal("got wrong message!") + } } } - } + }) }) } } func TestPublishDuplicateMessage(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 1) - psubs := getGossipsubs(ctx, hosts, WithMessageIdFn(func(msg *pb.Message) string { - return string(msg.Data) - })) - topic, err := psubs[0].Join("foobar") - if err != nil { - t.Fatal(err) - } - err = topic.Publish(ctx, []byte("hello")) - if err != nil { - t.Fatal(err) - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 1) + psubs := getGossipsubs(ctx, hosts, WithMessageIdFn(func(msg *pb.Message) string { + return string(msg.Data) + })) + topic, err := psubs[0].Join("foobar") + if err != nil { + t.Fatal(err) + } + err = topic.Publish(ctx, []byte("hello")) + if err != nil { + t.Fatal(err) + } - err = topic.Publish(ctx, []byte("hello")) - if err != nil { - t.Fatal("Duplicate message should not return an error") - } + err = topic.Publish(ctx, []byte("hello")) + if err != nil { + t.Fatal("Duplicate message should not return an error") + } + }) } func genNRpcs(tb testing.TB, n int, maxSize int) []*RPC { @@ -4386,114 +4502,116 @@ func newSkeletonGossipsub(ctx context.Context, h host.Host) *skeletonGossipsub { func TestExtensionsControlMessage(t *testing.T) { for _, wellBehaved := range []bool{true, false} { t.Run(fmt.Sprintf("wellBehaved=%t", wellBehaved), func(t *testing.T) { - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - hosts := getDefaultHosts(t, 2) - psub0 := getGossipsub(ctx, hosts[0], - WithPeerScore( - &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - BehaviourPenaltyWeight: -1, - BehaviourPenaltyDecay: ScoreParameterDecay(time.Minute), - DecayInterval: DefaultDecayInterval, - DecayToZero: DefaultDecayToZero, - }, - &PeerScoreThresholds{ - GossipThreshold: -100, - PublishThreshold: -500, - GraylistThreshold: -1000, - }), - WithMessageIdFn(func(msg *pb.Message) string { - return string(msg.Data) - })) - - psub1 := newSkeletonGossipsub(ctx, hosts[1]) - - connect(t, hosts[0], hosts[1]) - time.Sleep(time.Second) - - loopTimes := 3 - - for i := range loopTimes { - rpcToSend := &pb.RPC{ - Control: &pb.ControlMessage{ - Extensions: &pb.ControlExtensions{}, - }, - } - if wellBehaved && i > 0 { - // A well behaved node does not repeat the control - // extension message - rpcToSend.Control.Extensions = nil + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + hosts := getDefaultHosts(t, 2) + psub0 := getGossipsub(ctx, hosts[0], + WithPeerScore( + &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + BehaviourPenaltyWeight: -1, + BehaviourPenaltyDecay: ScoreParameterDecay(time.Minute), + DecayInterval: DefaultDecayInterval, + DecayToZero: DefaultDecayToZero, + }, + &PeerScoreThresholds{ + GossipThreshold: -100, + PublishThreshold: -500, + GraylistThreshold: -1000, + }), + WithMessageIdFn(func(msg *pb.Message) string { + return string(msg.Data) + })) + + psub1 := newSkeletonGossipsub(ctx, hosts[1]) + + connect(t, hosts[0], hosts[1]) + time.Sleep(time.Second) + + loopTimes := 3 + + for i := range loopTimes { + rpcToSend := &pb.RPC{ + Control: &pb.ControlMessage{ + Extensions: &pb.ControlExtensions{}, + }, + } + if wellBehaved && i > 0 { + // A well behaved node does not repeat the control + // extension message + rpcToSend.Control.Extensions = nil + } + psub1.inRPC <- rpcToSend } - psub1.inRPC <- rpcToSend - } - time.Sleep(time.Second) + time.Sleep(time.Second) - peerScore := psub0.rt.(*GossipSubRouter).score.Score(hosts[1].ID()) - t.Log("Peer score:", peerScore) - if wellBehaved { - if peerScore < 0 { - t.Fatal("Peer score should not be negative") - } - } else { - if peerScore >= 0 { - t.Fatal("Peer score should not be positive") + peerScore := psub0.rt.(*GossipSubRouter).score.Score(hosts[1].ID()) + t.Log("Peer score:", peerScore) + if wellBehaved { + if peerScore < 0 { + t.Fatal("Peer score should not be negative") + } + } else { + if peerScore >= 0 { + t.Fatal("Peer score should not be positive") + } } - } + }) }) } } func TestTestExtension(t *testing.T) { - hosts := getDefaultHosts(t, 2) - var receivedTestExtension atomic.Bool - c := TestExtensionConfig{ - OnReceiveTestExtension: func(_ peer.ID) { - receivedTestExtension.Store(true) - }, - } - psub := getGossipsub(context.Background(), hosts[0], WithTestExtension(c)) - _ = psub - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - psub1 := newSkeletonGossipsub(ctx, hosts[1]) + synctestTest(t, func(t *testing.T) { + hosts := getDefaultHosts(t, 2) + var receivedTestExtension atomic.Bool + c := TestExtensionConfig{ + OnReceiveTestExtension: func(_ peer.ID) { + receivedTestExtension.Store(true) + }, + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + psub := getGossipsub(ctx, hosts[0], WithTestExtension(c)) + _ = psub + psub1 := newSkeletonGossipsub(ctx, hosts[1]) - connect(t, hosts[0], hosts[1]) + connect(t, hosts[0], hosts[1]) - const timeout = 3 * time.Second - select { - case <-time.After(timeout): - t.Fatal("Timeout") - case r := <-psub1.outRPC: - if !*r.Control.Extensions.TestExtension { - t.Fatal("Unexpected RPC. First RPC should be the Extensions Control Message") + const timeout = 3 * time.Second + select { + case <-time.After(timeout): + t.Fatal("Timeout") + case r := <-psub1.outRPC: + if !*r.Control.Extensions.TestExtension { + t.Fatal("Unexpected RPC. First RPC should be the Extensions Control Message") + } } - } - truePtr := true - psub1.inRPC <- &pb.RPC{ - Control: &pb.ControlMessage{ - Extensions: &pb.ControlExtensions{ - TestExtension: &truePtr, + truePtr := true + psub1.inRPC <- &pb.RPC{ + Control: &pb.ControlMessage{ + Extensions: &pb.ControlExtensions{ + TestExtension: &truePtr, + }, }, - }, - } + } - select { - case <-time.After(timeout): - t.Fatal("Timeout") - case r := <-psub1.outRPC: - if r.TestExtension == nil { - t.Fatal("Unexpected RPC. Next RPC should be the TestExtension Message") + select { + case <-time.After(timeout): + t.Fatal("Timeout") + case r := <-psub1.outRPC: + if r.TestExtension == nil { + t.Fatal("Unexpected RPC. Next RPC should be the TestExtension Message") + } } - } - if !receivedTestExtension.Load() { - t.Fatal("TestExtension not received") - } + if !receivedTestExtension.Load() { + t.Fatal("TestExtension not received") + } + }) } type minimalTestPartialMessage struct { @@ -4604,116 +4722,118 @@ type peerState struct { } func TestPartialMessages(t *testing.T) { - topic := "test-topic" - const hostCount = 5 - hosts := getDefaultHosts(t, hostCount) - psubs := make([]*PubSub, 0, len(hosts)) - - gossipsubCtx, closeGossipsub := context.WithCancel(context.Background()) - go func() { - <-gossipsubCtx.Done() - for _, h := range hosts { - h.Close() - } - }() - - partialExt := make([]*partialmessages.PartialMessagesExtension[peerState], hostCount) - logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})) - - // A list of maps from topic+groupID to partialMessage. One map per peer - // var partialMessageStoreMu sync.Mutex - partialMessageStore := make([]map[string]*minimalTestPartialMessage, hostCount) - for i := range hostCount { - partialMessageStore[i] = make(map[string]*minimalTestPartialMessage) - } + synctestTest(t, func(t *testing.T) { + topic := "test-topic" + const hostCount = 5 + hosts := getDefaultHosts(t, hostCount) + psubs := make([]*PubSub, 0, len(hosts)) - for i := range partialExt { - partialExt[i] = &partialmessages.PartialMessagesExtension[peerState]{ - Logger: logger.With("id", i), - OnEmitGossip: func(topic string, groupID []byte, gossipPeers []peer.ID, peerStates map[peer.ID]peerState) { - pm := partialMessageStore[i][topic+string(groupID)] - if pm == nil { - return - } - partialExt[i].PublishPartial(topic, groupID, pm.publishActions) - }, - OnIncomingRPC: func(from peer.ID, peerStates map[peer.ID]peerState, rpc *pb.PartialMessagesExtension) error { - peerState := peerStates[from] - groupID := rpc.GroupID - pm, ok := partialMessageStore[i][topic+string(groupID)] - if !ok { - pm = &minimalTestPartialMessage{ - Group: groupID, + gossipsubCtx, closeGossipsub := context.WithCancel(context.Background()) + go func() { + <-gossipsubCtx.Done() + for _, h := range hosts { + h.Close() + } + }() + + partialExt := make([]*partialmessages.PartialMessagesExtension[peerState], hostCount) + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})) + + // A list of maps from topic+groupID to partialMessage. One map per peer + // var partialMessageStoreMu sync.Mutex + partialMessageStore := make([]map[string]*minimalTestPartialMessage, hostCount) + for i := range hostCount { + partialMessageStore[i] = make(map[string]*minimalTestPartialMessage) + } + + for i := range partialExt { + partialExt[i] = &partialmessages.PartialMessagesExtension[peerState]{ + Logger: logger.With("id", i), + OnEmitGossip: func(topic string, groupID []byte, gossipPeers []peer.ID, peerStates map[peer.ID]peerState) { + pm := partialMessageStore[i][topic+string(groupID)] + if pm == nil { + return } - partialMessageStore[i][topic+string(groupID)] = pm - } - if rpc.PartsMetadata != nil { - peerState.recvd = bitmap.Merge(peerState.recvd, rpc.PartsMetadata) - } - prevMeta := slices.Clone(pm.PartsMetadata()) - shouldRepublish := pm.onIncomingRPC(from, rpc) - if !bytes.Equal(prevMeta, pm.PartsMetadata()) { - peerState.sent = bitmap.Merge(peerState.sent, pm.PartsMetadata()) - } - peerStates[from] = peerState - if shouldRepublish { - go PublishPartial(psubs[i], topic, pm.GroupID(), pm.publishActions) - } - return nil - }, + partialExt[i].PublishPartial(topic, groupID, pm.publishActions) + }, + OnIncomingRPC: func(from peer.ID, peerStates map[peer.ID]peerState, rpc *pb.PartialMessagesExtension) error { + peerState := peerStates[from] + groupID := rpc.GroupID + pm, ok := partialMessageStore[i][topic+string(groupID)] + if !ok { + pm = &minimalTestPartialMessage{ + Group: groupID, + } + partialMessageStore[i][topic+string(groupID)] = pm + } + if rpc.PartsMetadata != nil { + peerState.recvd = bitmap.Merge(peerState.recvd, rpc.PartsMetadata) + } + prevMeta := slices.Clone(pm.PartsMetadata()) + shouldRepublish := pm.onIncomingRPC(from, rpc) + if !bytes.Equal(prevMeta, pm.PartsMetadata()) { + peerState.sent = bitmap.Merge(peerState.sent, pm.PartsMetadata()) + } + peerStates[from] = peerState + if shouldRepublish { + go PublishPartial(psubs[i], topic, pm.GroupID(), pm.publishActions) + } + return nil + }, + } } - } - for i, h := range hosts { - psub := getGossipsub(gossipsubCtx, h, WithPartialMessagesExtension(partialExt[i])) - topic, err := psub.Join(topic, RequestPartialMessages()) - if err != nil { - t.Fatal(err) + for i, h := range hosts { + psub := getGossipsub(gossipsubCtx, h, WithPartialMessagesExtension(partialExt[i])) + topic, err := psub.Join(topic, RequestPartialMessages()) + if err != nil { + t.Fatal(err) + } + _, err = topic.Subscribe() + if err != nil { + t.Fatal(err) + } + psubs = append(psubs, psub) } - _, err = topic.Subscribe() + + denseConnect(t, hosts) + time.Sleep(2 * time.Second) + + group := []byte("test-group") + msg1 := &minimalTestPartialMessage{ + Group: group, + Parts: [2][]byte{ + []byte("Hello"), + []byte("World"), + }, + } + partialMessageStore[0][topic+string(group)] = msg1 + err := PublishPartial(psubs[0], topic, msg1.GroupID(), msg1.publishActions) if err != nil { t.Fatal(err) } - psubs = append(psubs, psub) - } - - denseConnect(t, hosts) - time.Sleep(2 * time.Second) - - group := []byte("test-group") - msg1 := &minimalTestPartialMessage{ - Group: group, - Parts: [2][]byte{ - []byte("Hello"), - []byte("World"), - }, - } - partialMessageStore[0][topic+string(group)] = msg1 - err := PublishPartial(psubs[0], topic, msg1.GroupID(), msg1.publishActions) - if err != nil { - t.Fatal(err) - } - time.Sleep(2 * time.Second) + time.Sleep(2 * time.Second) - // Close gossipsub before we inspect the state to avoid race conditions - closeGossipsub() - time.Sleep(1 * time.Second) + // Close gossipsub before we inspect the state to avoid race conditions + closeGossipsub() + time.Sleep(1 * time.Second) - if len(partialMessageStore) != hostCount { - t.Errorf("One host is missing the partial message") - } - - for i, msgStore := range partialMessageStore { - if len(msgStore) == 0 { - t.Errorf("Host %d is missing the partial message", i) + if len(partialMessageStore) != hostCount { + t.Errorf("One host is missing the partial message") } - for _, partialMessage := range msgStore { - if !partialMessage.complete() { - t.Errorf("expected complete message, but %v is incomplete", partialMessage) + + for i, msgStore := range partialMessageStore { + if len(msgStore) == 0 { + t.Errorf("Host %d is missing the partial message", i) + } + for _, partialMessage := range msgStore { + if !partialMessage.complete() { + t.Errorf("expected complete message, but %v is incomplete", partialMessage) + } } } - } + }) } func TestPeerSupportsPartialMessages(t *testing.T) { @@ -4728,332 +4848,338 @@ func TestPeerSupportsPartialMessages(t *testing.T) { // The rest of the peers then publish the full message. The peer that // supports partial messages should have received the request from peer 0, // and can sent the missing parts right away. + synctestTest(t, func(t *testing.T) { - topic := "test-topic" - const hostCount = 5 - hosts := getDefaultHosts(t, hostCount) - psubs := make([]*PubSub, 0, len(hosts)) - topics := make([]*Topic, 0, len(hosts)) - subs := make([]*Subscription, 0, len(hosts)) - - gossipsubCtx, closeGossipsub := context.WithCancel(context.Background()) - go func() { - <-gossipsubCtx.Done() - for _, h := range hosts { - h.Close() - } - }() - - partialExt := make([]*partialmessages.PartialMessagesExtension[peerState], hostCount) - logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})) - - // A list of maps from topic+groupID to partialMessage. One map per peer - // var partialMessageStoreMu sync.Mutex - partialMessageStore := make([]map[string]*minimalTestPartialMessage, hostCount) - for i := range hostCount { - partialMessageStore[i] = make(map[string]*minimalTestPartialMessage) - } + topic := "test-topic" + const hostCount = 5 + hosts := getDefaultHosts(t, hostCount) + psubs := make([]*PubSub, 0, len(hosts)) + topics := make([]*Topic, 0, len(hosts)) + subs := make([]*Subscription, 0, len(hosts)) - for i := range partialExt { - partialExt[i] = &partialmessages.PartialMessagesExtension[peerState]{ - Logger: logger.With("id", i), - OnEmitGossip: func(topic string, groupID []byte, gossipPeers []peer.ID, peerStates map[peer.ID]peerState) { - pm := partialMessageStore[i][topic+string(groupID)] - if pm == nil { - return - } - partialExt[i].PublishPartial(topic, groupID, pm.publishActions) - }, - OnIncomingRPC: func(from peer.ID, peerStates map[peer.ID]peerState, rpc *pb.PartialMessagesExtension) error { - peerState := peerStates[from] - if from == hosts[1].ID() { - panic("peer 1 does not support partial messages, so should not send a partial message RPC") - } + gossipsubCtx, closeGossipsub := context.WithCancel(context.Background()) + go func() { + <-gossipsubCtx.Done() + for _, h := range hosts { + h.Close() + } + }() - if i == 0 && rpc.PartialMessage == nil { - // The first incoming rpc to the peer requesting a partial - // message should contain data since we made sure to send - // the request first. - panic("expected to receive a partial message from a supporting peer") - } + partialExt := make([]*partialmessages.PartialMessagesExtension[peerState], hostCount) + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})) - groupID := rpc.GroupID - pm, ok := partialMessageStore[i][topic+string(groupID)] - if !ok { - pm = &minimalTestPartialMessage{ - Group: groupID, + // A list of maps from topic+groupID to partialMessage. One map per peer + // var partialMessageStoreMu sync.Mutex + partialMessageStore := make([]map[string]*minimalTestPartialMessage, hostCount) + for i := range hostCount { + partialMessageStore[i] = make(map[string]*minimalTestPartialMessage) + } + + for i := range partialExt { + partialExt[i] = &partialmessages.PartialMessagesExtension[peerState]{ + Logger: logger.With("id", i), + OnEmitGossip: func(topic string, groupID []byte, gossipPeers []peer.ID, peerStates map[peer.ID]peerState) { + pm := partialMessageStore[i][topic+string(groupID)] + if pm == nil { + return } - partialMessageStore[i][topic+string(groupID)] = pm - } - if rpc.PartsMetadata != nil { - peerState.recvd = bitmap.Merge(peerState.recvd, rpc.PartsMetadata) - } - prevMeta := slices.Clone(pm.PartsMetadata()) - shouldRepublish := pm.onIncomingRPC(from, rpc) - if !bytes.Equal(prevMeta, pm.PartsMetadata()) { - peerState.sent = bitmap.Merge(peerState.sent, pm.PartsMetadata()) - } - peerStates[from] = peerState - if shouldRepublish { - go PublishPartial(psubs[i], topic, pm.GroupID(), pm.publishActions) - if pm.complete() { - encoded, _ := json.Marshal(pm) - go func() { - err := psubs[i].Publish(topic, encoded) - if err != nil { - panic(err) - } - }() + partialExt[i].PublishPartial(topic, groupID, pm.publishActions) + }, + OnIncomingRPC: func(from peer.ID, peerStates map[peer.ID]peerState, rpc *pb.PartialMessagesExtension) error { + peerState := peerStates[from] + if from == hosts[1].ID() { + panic("peer 1 does not support partial messages, so should not send a partial message RPC") } - } - return nil - }, - } - } - for i, h := range hosts { - var topicOpts []TopicOpt - if i == 0 { - topicOpts = append(topicOpts, RequestPartialMessages()) - } else if i == 1 { - // The right neighbor doesn't support partial messages - } else { - topicOpts = append(topicOpts, SupportsPartialMessages()) - } + if i == 0 && rpc.PartialMessage == nil { + // The first incoming rpc to the peer requesting a partial + // message should contain data since we made sure to send + // the request first. + panic("expected to receive a partial message from a supporting peer") + } - psub := getGossipsub(gossipsubCtx, h, WithPartialMessagesExtension(partialExt[i])) - topic, err := psub.Join(topic, topicOpts...) - if err != nil { - t.Fatal(err) + groupID := rpc.GroupID + pm, ok := partialMessageStore[i][topic+string(groupID)] + if !ok { + pm = &minimalTestPartialMessage{ + Group: groupID, + } + partialMessageStore[i][topic+string(groupID)] = pm + } + if rpc.PartsMetadata != nil { + peerState.recvd = bitmap.Merge(peerState.recvd, rpc.PartsMetadata) + } + prevMeta := slices.Clone(pm.PartsMetadata()) + shouldRepublish := pm.onIncomingRPC(from, rpc) + if !bytes.Equal(prevMeta, pm.PartsMetadata()) { + peerState.sent = bitmap.Merge(peerState.sent, pm.PartsMetadata()) + } + peerStates[from] = peerState + if shouldRepublish { + go PublishPartial(psubs[i], topic, pm.GroupID(), pm.publishActions) + if pm.complete() { + encoded, _ := json.Marshal(pm) + go func() { + err := psubs[i].Publish(topic, encoded) + if err != nil { + panic(err) + } + }() + } + } + return nil + }, + } } - sub, err := topic.Subscribe() - if err != nil { - t.Fatal(err) + + for i, h := range hosts { + var topicOpts []TopicOpt + if i == 0 { + topicOpts = append(topicOpts, RequestPartialMessages()) + } else if i == 1 { + // The right neighbor doesn't support partial messages + } else { + topicOpts = append(topicOpts, SupportsPartialMessages()) + } + + psub := getGossipsub(gossipsubCtx, h, WithPartialMessagesExtension(partialExt[i])) + topic, err := psub.Join(topic, topicOpts...) + if err != nil { + t.Fatal(err) + } + sub, err := topic.Subscribe() + if err != nil { + t.Fatal(err) + } + psubs = append(psubs, psub) + topics = append(topics, topic) + subs = append(subs, sub) } - psubs = append(psubs, psub) - topics = append(topics, topic) - subs = append(subs, sub) - } - ringConnect(t, hosts) - time.Sleep(2 * time.Second) + ringConnect(t, hosts) + time.Sleep(2 * time.Second) - group := []byte("test-group") - emptyMsg := &minimalTestPartialMessage{ - Group: group, - } - fullMsg := &minimalTestPartialMessage{ - Group: group, - Parts: [2][]byte{ - []byte("Hello"), - []byte("World"), - }, - } + group := []byte("test-group") + emptyMsg := &minimalTestPartialMessage{ + Group: group, + } + fullMsg := &minimalTestPartialMessage{ + Group: group, + Parts: [2][]byte{ + []byte("Hello"), + []byte("World"), + }, + } - for i := range hosts { - if i <= 1 { - continue + for i := range hosts { + if i <= 1 { + continue + } + copiedMsg := *fullMsg + partialMessageStore[i][topic+string(group)] = &copiedMsg } - copiedMsg := *fullMsg - partialMessageStore[i][topic+string(group)] = &copiedMsg - } - // Have the first host publish the empty partial message to send a partial - // message request to peers that support partial messages. - partialMessageStore[0][topic+string(group)] = emptyMsg - // first host has no data - err := PublishPartial(psubs[0], topic, emptyMsg.GroupID(), emptyMsg.publishActions) - if err != nil { - t.Fatal(err) - } + // Have the first host publish the empty partial message to send a partial + // message request to peers that support partial messages. + partialMessageStore[0][topic+string(group)] = emptyMsg + // first host has no data + err := PublishPartial(psubs[0], topic, emptyMsg.GroupID(), emptyMsg.publishActions) + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Second) + time.Sleep(time.Second) - for i := range hosts { - if i == 0 { - continue - } else { - if i != 1 { - pm := partialMessageStore[i][topic+string(group)] - err := PublishPartial(psubs[i], topic, pm.GroupID(), pm.publishActions) + for i := range hosts { + if i == 0 { + continue + } else { + if i != 1 { + pm := partialMessageStore[i][topic+string(group)] + err := PublishPartial(psubs[i], topic, pm.GroupID(), pm.publishActions) + if err != nil { + t.Fatal(err) + } + } + encoded, err := json.Marshal(fullMsg) + if err != nil { + t.Fatal(err) + } + err = topics[i].Publish(context.Background(), encoded) if err != nil { t.Fatal(err) } - } - encoded, err := json.Marshal(fullMsg) - if err != nil { - t.Fatal(err) - } - err = topics[i].Publish(context.Background(), encoded) - if err != nil { - t.Fatal(err) } } - } - time.Sleep(2 * time.Second) + time.Sleep(2 * time.Second) - // Close gossipsub before we inspect the state to avoid race conditions - closeGossipsub() - time.Sleep(1 * time.Second) + // Close gossipsub before we inspect the state to avoid race conditions + closeGossipsub() + time.Sleep(1 * time.Second) - if len(partialMessageStore) != hostCount { - t.Errorf("One host is missing the partial message") - } - - for i, msgStore := range partialMessageStore { - if i == 1 { - // Host 1 doesn't support partial messages - continue + if len(partialMessageStore) != hostCount { + t.Errorf("One host is missing the partial message") } - if len(msgStore) == 0 { - t.Errorf("Host %d is missing the partial message", i) - } - for _, partialMessage := range msgStore { - if !partialMessage.complete() { - t.Errorf("expected complete message, but %v is incomplete at host %d", partialMessage, i) + + for i, msgStore := range partialMessageStore { + if i == 1 { + // Host 1 doesn't support partial messages + continue + } + if len(msgStore) == 0 { + t.Errorf("Host %d is missing the partial message", i) + } + for _, partialMessage := range msgStore { + if !partialMessage.complete() { + t.Errorf("expected complete message, but %v is incomplete at host %d", partialMessage, i) + } } } - } - for i, sub := range subs { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - _, err := sub.Next(ctx) - if err != nil { - t.Errorf("failed to receive message: %v at host %d", err, i) + for i, sub := range subs { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + _, err := sub.Next(ctx) + if err != nil { + t.Errorf("failed to receive message: %v at host %d", err, i) + } } - } + }) } func TestSkipPublishingToPeersRequestingPartialMessages(t *testing.T) { - topicName := "test-topic" - - // 3 hosts. - // hosts[0]: Publisher. Requests partial messages - // hosts[1]: Subscriber. Requests partial messages - // hosts[2]: Alternate publisher. Does not support partial messages. Only - // connected to hosts[0] - hosts := getDefaultHosts(t, 3) - - const hostsWithPartialMessageSupport = 2 - partialExt := make([]*partialmessages.PartialMessagesExtension[peerState], hostsWithPartialMessageSupport) - // A list of maps from topic+groupID to partialMessage. One map per peer - partialMessageStore := make([]map[string]*minimalTestPartialMessage, hostsWithPartialMessageSupport) - for i := range hostsWithPartialMessageSupport { - partialMessageStore[i] = make(map[string]*minimalTestPartialMessage) - } + synctestTest(t, func(t *testing. + + // 3 hosts. + // hosts[0]: Publisher. Requests partial messages + // hosts[1]: Subscriber. Requests partial messages + // hosts[2]: Alternate publisher. Does not support partial messages. Only + // connected to hosts[0] + T) { + topicName := "test-topic" + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - // Only hosts with partial message support - psubs := make([]*PubSub, 0, len(hosts)-1) + hosts := getDefaultHosts(t, 3) - logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ - Level: slog.LevelDebug, - })) + const hostsWithPartialMessageSupport = 2 + partialExt := make([]*partialmessages.PartialMessagesExtension[peerState], hostsWithPartialMessageSupport) + // A list of maps from topic+groupID to partialMessage. One map per peer + partialMessageStore := make([]map[string]*minimalTestPartialMessage, hostsWithPartialMessageSupport) + for i := range hostsWithPartialMessageSupport { + partialMessageStore[i] = make(map[string]*minimalTestPartialMessage) + } - for i := range partialExt { - partialExt[i] = &partialmessages.PartialMessagesExtension[peerState]{ - Logger: logger, - OnEmitGossip: func(topic string, groupID []byte, gossipPeers []peer.ID, peerStates map[peer.ID]peerState) { - pm := partialMessageStore[i][topic+string(groupID)] - if pm == nil { - return - } - partialExt[i].PublishPartial(topic, groupID, pm.publishActions) - }, - OnIncomingRPC: func(from peer.ID, peerStates map[peer.ID]peerState, rpc *pb.PartialMessagesExtension) error { - peerState := peerStates[from] - topicID := rpc.GetTopicID() - groupID := rpc.GetGroupID() - pm, ok := partialMessageStore[i][topicID+string(groupID)] - if !ok { - pm = &minimalTestPartialMessage{ - Group: groupID, + // Only hosts with partial message support + psubs := make([]*PubSub, 0, len(hosts)-1) + + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + + for i := range partialExt { + partialExt[i] = &partialmessages.PartialMessagesExtension[peerState]{ + Logger: logger, + OnEmitGossip: func(topic string, groupID []byte, gossipPeers []peer.ID, peerStates map[peer.ID]peerState) { + pm := partialMessageStore[i][topic+string(groupID)] + if pm == nil { + return } - partialMessageStore[i][topicID+string(groupID)] = pm - } - if rpc.PartsMetadata != nil { - peerState.recvd = bitmap.Merge(peerState.recvd, rpc.PartsMetadata) - } - prevMeta := slices.Clone(pm.PartsMetadata()) - shouldRepublish := pm.onIncomingRPC(from, rpc) - if !bytes.Equal(prevMeta, pm.PartsMetadata()) { - peerState.sent = bitmap.Merge(peerState.sent, pm.PartsMetadata()) - } - peerStates[from] = peerState - if shouldRepublish { - go PublishPartial(psubs[i], topicID, pm.GroupID(), pm.publishActions) - } - return nil - }, + partialExt[i].PublishPartial(topic, groupID, pm.publishActions) + }, + OnIncomingRPC: func(from peer.ID, peerStates map[peer.ID]peerState, rpc *pb.PartialMessagesExtension) error { + peerState := peerStates[from] + topicID := rpc.GetTopicID() + groupID := rpc.GetGroupID() + pm, ok := partialMessageStore[i][topicID+string(groupID)] + if !ok { + pm = &minimalTestPartialMessage{ + Group: groupID, + } + partialMessageStore[i][topicID+string(groupID)] = pm + } + if rpc.PartsMetadata != nil { + peerState.recvd = bitmap.Merge(peerState.recvd, rpc.PartsMetadata) + } + prevMeta := slices.Clone(pm.PartsMetadata()) + shouldRepublish := pm.onIncomingRPC(from, rpc) + if !bytes.Equal(prevMeta, pm.PartsMetadata()) { + peerState.sent = bitmap.Merge(peerState.sent, pm.PartsMetadata()) + } + peerStates[from] = peerState + if shouldRepublish { + go PublishPartial(psubs[i], topicID, pm.GroupID(), pm.publishActions) + } + return nil + }, + } } - } - for i, h := range hosts[:2] { - psub := getGossipsub(context.Background(), h, WithPartialMessagesExtension(partialExt[i])) - psubs = append(psubs, psub) - } + for i, h := range hosts[:2] { + psub := getGossipsub(ctx, h, WithPartialMessagesExtension(partialExt[i])) + psubs = append(psubs, psub) + } - nonPartialPubsub := getGossipsub(context.Background(), hosts[2]) + nonPartialPubsub := getGossipsub(ctx, hosts[2]) - denseConnect(t, hosts[:2]) - time.Sleep(2 * time.Second) + denseConnect(t, hosts[:2]) + time.Sleep(2 * time.Second) - // Connect nonPartialPubsub to the publisher - connect(t, hosts[0], hosts[2]) + // Connect nonPartialPubsub to the publisher + connect(t, hosts[0], hosts[2]) - var topics []*Topic - var subs []*Subscription - for _, psub := range psubs { - topic, err := psub.Join(topicName, RequestPartialMessages()) - if err != nil { - t.Fatal(err) + var topics []*Topic + var subs []*Subscription + for _, psub := range psubs { + topic, err := psub.Join(topicName, RequestPartialMessages()) + if err != nil { + t.Fatal(err) + } + topics = append(topics, topic) + s, err := topic.Subscribe() + if err != nil { + t.Fatal(err) + } + subs = append(subs, s) } - topics = append(topics, topic) - s, err := topic.Subscribe() + + topicForNonPartial, err := nonPartialPubsub.Join(topicName) if err != nil { t.Fatal(err) } - subs = append(subs, s) - } - topicForNonPartial, err := nonPartialPubsub.Join(topicName) - if err != nil { - t.Fatal(err) - } + // Wait for subscriptions to propagate + time.Sleep(time.Second) - // Wait for subscriptions to propagate - time.Sleep(time.Second) + topics[0].Publish(ctx, []byte("Hello")) - topics[0].Publish(context.Background(), []byte("Hello")) + // Publish from another peer, the publisher (psub[0]) should not forward this to psub[1]. + // The application has to handle the interaction of getting a standard + // gossipsub message and republishing it with partial messages. + topicForNonPartial.Publish(ctx, []byte("from non-partial")) - // Publish from another peer, the publisher (psub[0]) should not forward this to psub[1]. - // The application has to handle the interaction of getting a standard - // gossipsub message and republishing it with partial messages. - topicForNonPartial.Publish(context.Background(), []byte("from non-partial")) + recvdMessage := make(chan struct{}, 1) + go func() { + msg, err := subs[1].Next(ctx) + if err == context.Canceled { + return + } + if err != nil { + t.Log(err) + t.Fail() + return + } + t.Log("Received msg", string(msg.Data)) + recvdMessage <- struct{}{} + }() - recvdMessage := make(chan struct{}, 1) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - go func() { - msg, err := subs[1].Next(ctx) - if err == context.Canceled { - return - } - if err != nil { - t.Log(err) - t.Fail() - return + select { + case <-recvdMessage: + t.Fatal("Received message") + case <-time.After(2 * time.Second): } - t.Log("Received msg", string(msg.Data)) - recvdMessage <- struct{}{} - }() - - select { - case <-recvdMessage: - t.Fatal("Received message") - case <-time.After(2 * time.Second): - } + }) } func TestPairwiseInteractionWithPartialMessages(t *testing.T) { @@ -5082,415 +5208,425 @@ func TestPairwiseInteractionWithPartialMessages(t *testing.T) { for _, tc := range tcs { t.Run(fmt.Sprintf("Host Support: %v. Publisher: %d", tc.hostSupport, tc.publisherIdx), func(t *testing.T) { - topic := "test-topic" - hostCount := len(tc.hostSupport) - hosts := getDefaultHosts(t, hostCount) - topics := make([]*Topic, 0, len(hosts)) - psubs := make([]*PubSub, 0, len(hosts)) - - gossipsubCtx, closeGossipsub := context.WithCancel(context.Background()) - defer closeGossipsub() - go func() { - <-gossipsubCtx.Done() - for _, h := range hosts { - h.Close() - } - }() - - partialExt := make([]*partialmessages.PartialMessagesExtension[peerState], hostCount) - logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})) - - // A list of maps from topic+groupID to partialMessage. One map per peer - // var partialMessageStoreMu sync.Mutex - partialMessageStore := make([]map[string]*minimalTestPartialMessage, hostCount) - for i := range hostCount { - partialMessageStore[i] = make(map[string]*minimalTestPartialMessage) - } + synctestTest(t, func(t *testing.T) { + topic := "test-topic" + hostCount := len(tc.hostSupport) + hosts := getDefaultHosts(t, hostCount) + topics := make([]*Topic, 0, len(hosts)) + psubs := make([]*PubSub, 0, len(hosts)) + + gossipsubCtx, closeGossipsub := context.WithCancel(context.Background()) + defer closeGossipsub() + go func() { + <-gossipsubCtx.Done() + for _, h := range hosts { + h.Close() + } + }() - receivedMessage := make(chan struct{}, hostCount) + partialExt := make([]*partialmessages.PartialMessagesExtension[peerState], hostCount) + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})) - for i := range partialExt { - if tc.hostSupport[i] == NoPartialMessages { - continue + // A list of maps from topic+groupID to partialMessage. One map per peer + // var partialMessageStoreMu sync.Mutex + partialMessageStore := make([]map[string]*minimalTestPartialMessage, hostCount) + for i := range hostCount { + partialMessageStore[i] = make(map[string]*minimalTestPartialMessage) } - partialExt[i] = &partialmessages.PartialMessagesExtension[peerState]{ - Logger: logger.With("id", i), - OnEmitGossip: func(topic string, groupID []byte, gossipPeers []peer.ID, peerStates map[peer.ID]peerState) { - pm := partialMessageStore[i][topic+string(groupID)] - if pm == nil { - return - } - partialExt[i].PublishPartial(topic, groupID, pm.publishActions) - }, - OnIncomingRPC: func(from peer.ID, peerStates map[peer.ID]peerState, rpc *pb.PartialMessagesExtension) error { - peerState := peerStates[from] - if tc.hostSupport[i] == PeerSupportsPartialMessages && len(rpc.PartialMessage) > 0 { - panic("This host should not have received partial message data") - } - groupID := rpc.GroupID - pm, ok := partialMessageStore[i][topic+string(groupID)] - if !ok { - pm = &minimalTestPartialMessage{ - Group: groupID, + receivedMessage := make(chan struct{}, hostCount) + + for i := range partialExt { + if tc.hostSupport[i] == NoPartialMessages { + continue + } + partialExt[i] = &partialmessages.PartialMessagesExtension[peerState]{ + Logger: logger.With("id", i), + OnEmitGossip: func(topic string, groupID []byte, gossipPeers []peer.ID, peerStates map[peer.ID]peerState) { + pm := partialMessageStore[i][topic+string(groupID)] + if pm == nil { + return + } + partialExt[i].PublishPartial(topic, groupID, pm.publishActions) + }, + OnIncomingRPC: func(from peer.ID, peerStates map[peer.ID]peerState, rpc *pb.PartialMessagesExtension) error { + peerState := peerStates[from] + if tc.hostSupport[i] == PeerSupportsPartialMessages && len(rpc.PartialMessage) > 0 { + panic("This host should not have received partial message data") } - partialMessageStore[i][topic+string(groupID)] = pm - } - if rpc.PartsMetadata != nil { - peerState.recvd = bitmap.Merge(peerState.recvd, rpc.PartsMetadata) - } - prevMeta := slices.Clone(pm.PartsMetadata()) - prevComplete := pm.complete() - shouldRepublish := pm.onIncomingRPC(from, rpc) - if !bytes.Equal(prevMeta, pm.PartsMetadata()) { - peerState.sent = bitmap.Merge(peerState.sent, pm.PartsMetadata()) - } - peerStates[from] = peerState - if shouldRepublish { - if !prevComplete && pm.complete() { - t.Log("host", i, "received partial message") - receivedMessage <- struct{}{} + groupID := rpc.GroupID + pm, ok := partialMessageStore[i][topic+string(groupID)] + if !ok { + pm = &minimalTestPartialMessage{ + Group: groupID, + } + partialMessageStore[i][topic+string(groupID)] = pm + } + if rpc.PartsMetadata != nil { + peerState.recvd = bitmap.Merge(peerState.recvd, rpc.PartsMetadata) } + prevMeta := slices.Clone(pm.PartsMetadata()) + prevComplete := pm.complete() + shouldRepublish := pm.onIncomingRPC(from, rpc) + if !bytes.Equal(prevMeta, pm.PartsMetadata()) { + peerState.sent = bitmap.Merge(peerState.sent, pm.PartsMetadata()) + } + peerStates[from] = peerState + if shouldRepublish { + if !prevComplete && pm.complete() { + t.Log("host", i, "received partial message") - go PublishPartial(psubs[i], topic, pm.GroupID(), pm.publishActions) - } - return nil - }, - } - } + receivedMessage <- struct{}{} + } - for i, h := range hosts { - var opts []Option - // For debugging: - // slog.SetLogLoggerLevel(slog.LevelDebug) - // opts = append(opts, WithRPCLogger(slog.Default().With("host_idx", i))) - var topicOpts []TopicOpt - switch tc.hostSupport[i] { - case NoPartialMessages: - case PartialMessagesExtensionNoSupport: - opts = append(opts, WithPartialMessagesExtension(partialExt[i])) - case PeerSupportsPartialMessages: - opts = append(opts, WithPartialMessagesExtension(partialExt[i])) - topicOpts = append(topicOpts, SupportsPartialMessages()) - case PeerRequestsPartialMessages: - opts = append(opts, WithPartialMessagesExtension(partialExt[i])) - topicOpts = append(topicOpts, RequestPartialMessages()) + go PublishPartial(psubs[i], topic, pm.GroupID(), pm.publishActions) + } + return nil + }, + } } - psub := getGossipsub(gossipsubCtx, h, opts...) - topic, err := psub.Join(topic, topicOpts...) - if err != nil { - t.Fatal(err) - } - topics = append(topics, topic) - sub, err := topic.Subscribe() - if err != nil { - t.Fatal(err) - } - psubs = append(psubs, psub) - go func() { - _, err := sub.Next(gossipsubCtx) - if err == context.Canceled { - return + for i, h := range hosts { + var opts []Option + // For debugging: + // slog.SetLogLoggerLevel(slog.LevelDebug) + // opts = append(opts, WithRPCLogger(slog.Default().With("host_idx", i))) + var topicOpts []TopicOpt + switch tc.hostSupport[i] { + case NoPartialMessages: + case PartialMessagesExtensionNoSupport: + opts = append(opts, WithPartialMessagesExtension(partialExt[i])) + case PeerSupportsPartialMessages: + opts = append(opts, WithPartialMessagesExtension(partialExt[i])) + topicOpts = append(topicOpts, SupportsPartialMessages()) + case PeerRequestsPartialMessages: + opts = append(opts, WithPartialMessagesExtension(partialExt[i])) + topicOpts = append(topicOpts, RequestPartialMessages()) } + + psub := getGossipsub(gossipsubCtx, h, opts...) + topic, err := psub.Join(topic, topicOpts...) if err != nil { - panic(err) + t.Fatal(err) } + topics = append(topics, topic) + sub, err := topic.Subscribe() + if err != nil { + t.Fatal(err) + } + psubs = append(psubs, psub) + go func() { + _, err := sub.Next(gossipsubCtx) + if err == context.Canceled { + return + } + if err != nil { + panic(err) + } - t.Log("host", i, "received message") - receivedMessage <- struct{}{} - }() - } - - denseConnect(t, hosts) - time.Sleep(time.Second) + t.Log("host", i, "received message") + receivedMessage <- struct{}{} + }() + } - group := []byte("test-group") - msg1 := &minimalTestPartialMessage{ - Group: group, - Parts: [2][]byte{ - []byte("Hello"), - []byte("World"), - }, - } + denseConnect(t, hosts) + time.Sleep(time.Second) - for i := range hostCount { - if i != tc.publisherIdx { - continue + group := []byte("test-group") + msg1 := &minimalTestPartialMessage{ + Group: group, + Parts: [2][]byte{ + []byte("Hello"), + []byte("World"), + }, } - partialMessageStore[i][topic+string(group)] = msg1 + for i := range hostCount { + if i != tc.publisherIdx { + continue + } - encoded, err := json.Marshal(msg1) - if err != nil { - t.Fatal(err) - } - err = topics[i].Publish(context.Background(), encoded) - if err != nil { - t.Fatal(err) - } + partialMessageStore[i][topic+string(group)] = msg1 - switch tc.hostSupport[i] { - case PeerSupportsPartialMessages, PeerRequestsPartialMessages: - err = PublishPartial(psubs[i], topic, msg1.GroupID(), msg1.publishActions) + encoded, err := json.Marshal(msg1) if err != nil { t.Fatal(err) } + err = topics[i].Publish(context.Background(), encoded) + if err != nil { + t.Fatal(err) + } + + switch tc.hostSupport[i] { + case PeerSupportsPartialMessages, PeerRequestsPartialMessages: + err = PublishPartial(psubs[i], topic, msg1.GroupID(), msg1.publishActions) + if err != nil { + t.Fatal(err) + } + } + } + + for range hostCount { + select { + case <-receivedMessage: + case <-time.After(time.Second): + t.Fatalf("At least one message was not received") + } } - } - for range hostCount { select { case <-receivedMessage: - case <-time.After(time.Second): - t.Fatalf("At least one message was not received") + t.Fatalf("An extra message was received") + case <-time.After(100 * time.Millisecond): } - } - - select { - case <-receivedMessage: - t.Fatalf("An extra message was received") - case <-time.After(100 * time.Millisecond): - } + }) }) } } func TestNoIDONTWANTWithPartialMessage(t *testing.T) { - hs := getDefaultHosts(t, 3) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + hs := getDefaultHosts(t, 3) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - type rpcWithMeta struct { - from, to peer.ID - rpc *RPC - } + type rpcWithMeta struct { + from, to peer.ID + rpc *RPC + } - receivedRPCs := make(chan *rpcWithMeta, 20) - - pubsubs := getGossipsubsOptFn(ctx, hs, - func(i int, h host.Host) []Option { - tracer := &mockRawTracer{ - onRecvRPC: func(r *RPC) { - copy := *r - receivedRPCs <- &rpcWithMeta{ - from: r.from, - to: h.ID(), - rpc: ©, - } - }, - } - return []Option{ - WithPartialMessagesExtension( - &partialmessages.PartialMessagesExtension[peerState]{ - OnEmitGossip: func(topic string, groupID []byte, gossipPeers []peer.ID, peerStates map[peer.ID]peerState) { - }, - Logger: slog.Default(), - OnIncomingRPC: func(from peer.ID, peerStates map[peer.ID]peerState, rpc *pb.PartialMessagesExtension) error { - return nil - }, + receivedRPCs := make(chan *rpcWithMeta, 20) + + pubsubs := getGossipsubsOptFn(ctx, hs, + func(i int, h host.Host) []Option { + tracer := &mockRawTracer{ + onRecvRPC: func(r *RPC) { + copy := *r + receivedRPCs <- &rpcWithMeta{ + from: r.from, + to: h.ID(), + rpc: ©, + } }, - ), - WithRawTracer(tracer), - } - }) + } + return []Option{ + WithPartialMessagesExtension( + &partialmessages.PartialMessagesExtension[peerState]{ + OnEmitGossip: func(topic string, groupID []byte, gossipPeers []peer.ID, peerStates map[peer.ID]peerState) { + }, + Logger: slog.Default(), + OnIncomingRPC: func(from peer.ID, peerStates map[peer.ID]peerState, rpc *pb.PartialMessagesExtension) error { + return nil + }, + }, + ), + WithRawTracer(tracer), + } + }) - for i, h := range hs { - t.Logf("Host ID of node %v: %s\n", i, h.ID()) - } + for i, h := range hs { + t.Logf("Host ID of node %v: %s\n", i, h.ID()) + } - hs[0].Connect(ctx, peer.AddrInfo{ - ID: hs[1].ID(), - Addrs: hs[1].Addrs(), - }) - hs[1].Connect(ctx, peer.AddrInfo{ - ID: hs[2].ID(), - Addrs: hs[2].Addrs(), - }) + hs[0].Connect(ctx, peer.AddrInfo{ + ID: hs[1].ID(), + Addrs: hs[1].Addrs(), + }) + hs[1].Connect(ctx, peer.AddrInfo{ + ID: hs[2].ID(), + Addrs: hs[2].Addrs(), + }) - var topics []*Topic - var receivedMessage [3]atomic.Bool - const topicStr = "test-topic" - for i, ps := range pubsubs { - var topicOpts []TopicOpt - switch i { - case 0: - topicOpts = append(topicOpts, SupportsPartialMessages()) - case 1: - topicOpts = append(topicOpts, RequestPartialMessages()) - } - topic, err := ps.Join(topicStr, topicOpts...) - if err != nil { - t.Fatal(err) + var topics []*Topic + var receivedMessage [3]atomic.Bool + const topicStr = "test-topic" + for i, ps := range pubsubs { + var topicOpts []TopicOpt + switch i { + case 0: + topicOpts = append(topicOpts, SupportsPartialMessages()) + case 1: + topicOpts = append(topicOpts, RequestPartialMessages()) + } + topic, err := ps.Join(topicStr, topicOpts...) + if err != nil { + t.Fatal(err) + } + topics = append(topics, topic) + sub, err := topic.Subscribe() + if err != nil { + t.Fatal(err) + } + go func() { + for { + msg, err := sub.Next(ctx) + if errors.Is(err, context.Canceled) { + return + } + if err != nil { + panic(err) + } + t.Logf("%v Received message from %s\n", i, msg.GetFrom()) + receivedMessage[i].Store(true) + } + }() } - topics = append(topics, topic) - sub, err := topic.Subscribe() + + // Let mesh form + time.Sleep(time.Second) + msg := make([]byte, GossipSubIDontWantMessageThreshold) + err := topics[2].Publish(ctx, msg) if err != nil { t.Fatal(err) } - go func() { - for { - msg, err := sub.Next(ctx) - if errors.Is(err, context.Canceled) { - return - } - if err != nil { - panic(err) + + outer: + for { + select { + case rpc := <-receivedRPCs: + // Peer 1 shouldn't send any IDONTWANTS. + // because: + // - Peer 2 send the message, so shouldn't receive an IDONTWANT. + // - Peer 0 supports partial messages and peer 1 requested partial + // messages. It will not receive a full message from peer 1. + if rpc.from == hs[1].ID() && len(rpc.rpc.Control.GetIdontwant()) > 0 { + t.Fatalf("Received unexpected IDONTWANT from %s", rpc.from) } - t.Logf("%v Received message from %s\n", i, msg.GetFrom()) - receivedMessage[i].Store(true) + t.Logf("Received RPC: %s->%s %+v\n", rpc.from, rpc.to, rpc.rpc) + case <-time.After(2 * time.Second): + break outer } - }() - } - - // Let mesh form - time.Sleep(time.Second) - msg := make([]byte, GossipSubIDontWantMessageThreshold) - err := topics[2].Publish(ctx, msg) - if err != nil { - t.Fatal(err) - } - -outer: - for { - select { - case rpc := <-receivedRPCs: - // Peer 1 shouldn't send any IDONTWANTS. - // because: - // - Peer 2 send the message, so shouldn't receive an IDONTWANT. - // - Peer 0 supports partial messages and peer 1 requested partial - // messages. It will not receive a full message from peer 1. - if rpc.from == hs[1].ID() && len(rpc.rpc.Control.GetIdontwant()) > 0 { - t.Fatalf("Received unexpected IDONTWANT from %s", rpc.from) - } - t.Logf("Received RPC: %s->%s %+v\n", rpc.from, rpc.to, rpc.rpc) - case <-time.After(2 * time.Second): - break outer } - } - for i := range receivedMessage { - if !receivedMessage[i].Load() { - t.Fatal("Peer did not receive a full message") + for i := range receivedMessage { + if !receivedMessage[i].Load() { + t.Fatal("Peer did not receive a full message") + } } - } + }) } func TestGossipsubFanoutOnly(t *testing.T) { // Test that a fanout-only topic can publish to the network but its // subscriber only receives locally published messages. - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - hosts := getDefaultHosts(t, 5) - psubs := getGossipsubs(ctx, hosts) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - topicID := "foobar" + hosts := getDefaultHosts(t, 5) + psubs := getGossipsubs(ctx, hosts) - // hosts[0] joins with FanoutOnly - it should be able to publish but never - // join the mesh, so its subscriber should not receive remote messages. - fanoutTopic, err := psubs[0].Join(topicID, FanoutOnly()) - if err != nil { - t.Fatal(err) - } + topicID := "foobar" - // The rest subscribe normally. - var normalSubs []*Subscription - for _, ps := range psubs[1:] { - sub, err := ps.Subscribe(topicID) + // hosts[0] joins with FanoutOnly - it should be able to publish but never + // join the mesh, so its subscriber should not receive remote messages. + fanoutTopic, err := psubs[0].Join(topicID, FanoutOnly()) if err != nil { t.Fatal(err) } - normalSubs = append(normalSubs, sub) - } - // Also subscribe on the fanout-only topic. Since it's fanout-only, this - // must not trigger a p2p subscription. - fanoutSub, err := fanoutTopic.Subscribe() - if err != nil { - t.Fatal(err) - } + // The rest subscribe normally. + var normalSubs []*Subscription + for _, ps := range psubs[1:] { + sub, err := ps.Subscribe(topicID) + if err != nil { + t.Fatal(err) + } + normalSubs = append(normalSubs, sub) + } - denseConnect(t, hosts) + // Also subscribe on the fanout-only topic. Since it's fanout-only, this + // must not trigger a p2p subscription. + fanoutSub, err := fanoutTopic.Subscribe() + if err != nil { + t.Fatal(err) + } - // Wait for heartbeats to build mesh. - time.Sleep(2 * time.Second) + denseConnect(t, hosts) - // Publish from the fanout-only node. Normal subscribers should receive it - // because the router uses fanout to forward the message. - fanoutMsg := []byte("from fanout-only node") - if err := fanoutTopic.Publish(ctx, fanoutMsg); err != nil { - t.Fatal(err) - } + // Wait for heartbeats to build mesh. + time.Sleep(2 * time.Second) - for i, sub := range normalSubs { + // Publish from the fanout-only node. Normal subscribers should receive it + // because the router uses fanout to forward the message. + fanoutMsg := []byte("from fanout-only node") + if err := fanoutTopic.Publish(ctx, fanoutMsg); err != nil { + t.Fatal(err) + } + + for i, sub := range normalSubs { + tctx, tcancel := context.WithTimeout(ctx, 5*time.Second) + got, err := sub.Next(tctx) + tcancel() + if err != nil { + t.Fatalf("normal sub %d did not receive fanout message: %v", i, err) + } + if !bytes.Equal(got.Data, fanoutMsg) { + t.Fatalf("normal sub %d got wrong message", i) + } + } + + // The fanout subscriber should also get the locally published message. tctx, tcancel := context.WithTimeout(ctx, 5*time.Second) - got, err := sub.Next(tctx) + got, err := fanoutSub.Next(tctx) tcancel() if err != nil { - t.Fatalf("normal sub %d did not receive fanout message: %v", i, err) + t.Fatal("fanout subscriber did not receive locally published message:", err) } if !bytes.Equal(got.Data, fanoutMsg) { - t.Fatalf("normal sub %d got wrong message", i) + t.Fatal("fanout subscriber got wrong message") } - } - // The fanout subscriber should also get the locally published message. - tctx, tcancel := context.WithTimeout(ctx, 5*time.Second) - got, err := fanoutSub.Next(tctx) - tcancel() - if err != nil { - t.Fatal("fanout subscriber did not receive locally published message:", err) - } - if !bytes.Equal(got.Data, fanoutMsg) { - t.Fatal("fanout subscriber got wrong message") - } + // Now publish from a normal node. The fanout subscriber must NOT receive + // it because the fanout-only topic never joined the mesh. + remoteMsg := []byte("from normal node") + if err := psubs[1].Publish(topicID, remoteMsg); err != nil { + t.Fatal(err) + } - // Now publish from a normal node. The fanout subscriber must NOT receive - // it because the fanout-only topic never joined the mesh. - remoteMsg := []byte("from normal node") - if err := psubs[1].Publish(topicID, remoteMsg); err != nil { - t.Fatal(err) - } + // The other normal subscribers should receive it. + for i, sub := range normalSubs[1:] { + tctx, tcancel := context.WithTimeout(ctx, 5*time.Second) + got, err := sub.Next(tctx) + tcancel() + if err != nil { + t.Fatalf("normal sub %d did not receive remote message: %v", i+1, err) + } + if !bytes.Equal(got.Data, remoteMsg) { + t.Fatalf("normal sub %d got wrong message", i+1) + } + } - // The other normal subscribers should receive it. - for i, sub := range normalSubs[1:] { - tctx, tcancel := context.WithTimeout(ctx, 5*time.Second) - got, err := sub.Next(tctx) + // The fanout subscriber should NOT receive the remote message. + tctx, tcancel = context.WithTimeout(ctx, time.Second) + _, err = fanoutSub.Next(tctx) tcancel() - if err != nil { - t.Fatalf("normal sub %d did not receive remote message: %v", i+1, err) - } - if !bytes.Equal(got.Data, remoteMsg) { - t.Fatalf("normal sub %d got wrong message", i+1) + if err == nil { + t.Fatal("fanout subscriber received a remote message but should not have") } - } - - // The fanout subscriber should NOT receive the remote message. - tctx, tcancel = context.WithTimeout(ctx, time.Second) - _, err = fanoutSub.Next(tctx) - tcancel() - if err == nil { - t.Fatal("fanout subscriber received a remote message but should not have") - } + }) } func TestGossipsubFanoutOnlyRelay(t *testing.T) { // Test that Relay() returns an error on a fanout-only topic. - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - hosts := getDefaultHosts(t, 1) - ps := getGossipsub(ctx, hosts[0]) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - topic, err := ps.Join("foobar", FanoutOnly()) - if err != nil { - t.Fatal(err) - } + hosts := getDefaultHosts(t, 1) + ps := getGossipsub(ctx, hosts[0]) - _, err = topic.Relay() - if !errors.Is(err, ErrFanoutOnlyTopic) { - t.Fatalf("expected ErrFanoutOnlyTopic, got: %v", err) - } + topic, err := ps.Join("foobar", FanoutOnly()) + if err != nil { + t.Fatal(err) + } + + _, err = topic.Relay() + if !errors.Is(err, ErrFanoutOnlyTopic) { + t.Fatalf("expected ErrFanoutOnlyTopic, got: %v", err) + } + }) } diff --git a/norace_test.go b/norace_test.go new file mode 100644 index 00000000..1dbc2aa4 --- /dev/null +++ b/norace_test.go @@ -0,0 +1,5 @@ +//go:build !race + +package pubsub + +const raceEnabled = false diff --git a/notify_test.go b/notify_test.go index fa5b755a..9b8d425c 100644 --- a/notify_test.go +++ b/notify_test.go @@ -9,68 +9,70 @@ import ( ) func TestNotifyPeerProtocolsUpdated(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) + hosts := getDefaultHosts(t, 2) - // Initialize id services. - { - ids1, err := identify.NewIDService(hosts[0]) + // Initialize id services. + { + ids1, err := identify.NewIDService(hosts[0]) + if err != nil { + t.Fatal(err) + } + ids1.Start() + defer ids1.Close() + + ids2, err := identify.NewIDService(hosts[1]) + if err != nil { + t.Fatal(err) + } + ids2.Start() + defer ids2.Close() + } + + psubs0 := getPubsub(ctx, hosts[0]) + connect(t, hosts[0], hosts[1]) + // Delay to make sure that peers are connected. + <-time.After(time.Millisecond * 100) + psubs1 := getPubsub(ctx, hosts[1]) + + // Pubsub 0 joins topic "test". + topic0, err := psubs0.Join("test") if err != nil { t.Fatal(err) } - ids1.Start() - defer ids1.Close() + defer topic0.Close() - ids2, err := identify.NewIDService(hosts[1]) + sub0, err := topic0.Subscribe() if err != nil { t.Fatal(err) } - ids2.Start() - defer ids2.Close() - } + defer sub0.Cancel() - psubs0 := getPubsub(ctx, hosts[0]) - connect(t, hosts[0], hosts[1]) - // Delay to make sure that peers are connected. - <-time.After(time.Millisecond * 100) - psubs1 := getPubsub(ctx, hosts[1]) - - // Pubsub 0 joins topic "test". - topic0, err := psubs0.Join("test") - if err != nil { - t.Fatal(err) - } - defer topic0.Close() - - sub0, err := topic0.Subscribe() - if err != nil { - t.Fatal(err) - } - defer sub0.Cancel() - - // Pubsub 1 joins topic "test". - topic1, err := psubs1.Join("test") - if err != nil { - t.Fatal(err) - } - defer topic1.Close() + // Pubsub 1 joins topic "test". + topic1, err := psubs1.Join("test") + if err != nil { + t.Fatal(err) + } + defer topic1.Close() - sub1, err := topic1.Subscribe() - if err != nil { - t.Fatal(err) - } - defer sub1.Cancel() + sub1, err := topic1.Subscribe() + if err != nil { + t.Fatal(err) + } + defer sub1.Cancel() - // Delay before checking results (similar to most tests). - <-time.After(time.Millisecond * 100) + // Delay before checking results (similar to most tests). + <-time.After(time.Millisecond * 100) - if len(topic0.ListPeers()) == 0 { - t.Fatalf("topic0 should at least have 1 peer") - } + if len(topic0.ListPeers()) == 0 { + t.Fatalf("topic0 should at least have 1 peer") + } - if len(topic1.ListPeers()) == 0 { - t.Fatalf("topic1 should at least have 1 peer") - } + if len(topic1.ListPeers()) == 0 { + t.Fatalf("topic1 should at least have 1 peer") + } + }) } diff --git a/peer_gater_test.go b/peer_gater_test.go index 73152fde..73eb3927 100644 --- a/peer_gater_test.go +++ b/peer_gater_test.go @@ -10,120 +10,122 @@ import ( ) func TestPeerGater(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - peerA := peer.ID("A") - peerAip := "1.2.3.4" - - params := NewPeerGaterParams(.1, .9, .999) - err := params.validate() - if err != nil { - t.Fatal(err) - } - - pg := newPeerGater(ctx, nil, params, slog.Default()) - pg.getIP = func(p peer.ID) string { - switch p { - case peerA: - return peerAip - default: - return "" - } - } - - pg.AddPeer(peerA, "") - - status := pg.AcceptFrom(peerA) - if status != AcceptAll { - t.Fatal("expected AcceptAll") - } - - msg := &Message{ReceivedFrom: peerA} - - pg.ValidateMessage(msg) - status = pg.AcceptFrom(peerA) - if status != AcceptAll { - t.Fatal("expected AcceptAll") - } - - pg.RejectMessage(msg, RejectValidationQueueFull) - status = pg.AcceptFrom(peerA) - if status != AcceptAll { - t.Fatal("expected AcceptAll") - } - - pg.RejectMessage(msg, RejectValidationThrottled) - status = pg.AcceptFrom(peerA) - if status != AcceptAll { - t.Fatal("expected AcceptAll") - } - - for i := 0; i < 100; i++ { - pg.RejectMessage(msg, RejectValidationIgnored) - pg.RejectMessage(msg, RejectValidationFailed) - } - - accepted := false - for i := 0; !accepted && i < 1000; i++ { + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + peerA := peer.ID("A") + peerAip := "1.2.3.4" + + params := NewPeerGaterParams(.1, .9, .999) + err := params.validate() + if err != nil { + t.Fatal(err) + } + + pg := newPeerGater(ctx, nil, params, slog.Default()) + pg.getIP = func(p peer.ID) string { + switch p { + case peerA: + return peerAip + default: + return "" + } + } + + pg.AddPeer(peerA, "") + + status := pg.AcceptFrom(peerA) + if status != AcceptAll { + t.Fatal("expected AcceptAll") + } + + msg := &Message{ReceivedFrom: peerA} + + pg.ValidateMessage(msg) status = pg.AcceptFrom(peerA) - if status == AcceptControl { - accepted = true + if status != AcceptAll { + t.Fatal("expected AcceptAll") + } + + pg.RejectMessage(msg, RejectValidationQueueFull) + status = pg.AcceptFrom(peerA) + if status != AcceptAll { + t.Fatal("expected AcceptAll") + } + + pg.RejectMessage(msg, RejectValidationThrottled) + status = pg.AcceptFrom(peerA) + if status != AcceptAll { + t.Fatal("expected AcceptAll") + } + + for i := 0; i < 100; i++ { + pg.RejectMessage(msg, RejectValidationIgnored) + pg.RejectMessage(msg, RejectValidationFailed) } - } - if !accepted { - t.Fatal("expected AcceptControl") - } - for i := 0; i < 100; i++ { - pg.DeliverMessage(msg) - } + accepted := false + for i := 0; !accepted && i < 1000; i++ { + status = pg.AcceptFrom(peerA) + if status == AcceptControl { + accepted = true + } + } + if !accepted { + t.Fatal("expected AcceptControl") + } + + for i := 0; i < 100; i++ { + pg.DeliverMessage(msg) + } + + accepted = false + for i := 0; !accepted && i < 1000; i++ { + status = pg.AcceptFrom(peerA) + if status == AcceptAll { + accepted = true + } + } + if !accepted { + t.Fatal("expected to accept at least once") + } + + for i := 0; i < 100; i++ { + pg.decayStats() + } - accepted = false - for i := 0; !accepted && i < 1000; i++ { status = pg.AcceptFrom(peerA) - if status == AcceptAll { - accepted = true - } - } - if !accepted { - t.Fatal("expected to accept at least once") - } - - for i := 0; i < 100; i++ { - pg.decayStats() - } - - status = pg.AcceptFrom(peerA) - if status != AcceptAll { - t.Fatal("expected AcceptAll") - } - - pg.RemovePeer(peerA) - pg.Lock() - _, ok := pg.peerStats[peerA] - pg.Unlock() - if ok { - t.Fatal("still have a stat record for peerA") - } - - pg.Lock() - _, ok = pg.ipStats[peerAip] - pg.Unlock() - if !ok { - t.Fatal("expected to still have a stat record for peerA's ip") - } - - pg.Lock() - pg.ipStats[peerAip].expire = time.Now() - pg.Unlock() - - time.Sleep(2 * time.Second) - - pg.Lock() - _, ok = pg.ipStats["1.2.3.4"] - pg.Unlock() - if ok { - t.Fatal("still have a stat record for peerA's ip") - } + if status != AcceptAll { + t.Fatal("expected AcceptAll") + } + + pg.RemovePeer(peerA) + pg.Lock() + _, ok := pg.peerStats[peerA] + pg.Unlock() + if ok { + t.Fatal("still have a stat record for peerA") + } + + pg.Lock() + _, ok = pg.ipStats[peerAip] + pg.Unlock() + if !ok { + t.Fatal("expected to still have a stat record for peerA's ip") + } + + pg.Lock() + pg.ipStats[peerAip].expire = time.Now() + pg.Unlock() + + time.Sleep(2 * time.Second) + + pg.Lock() + _, ok = pg.ipStats["1.2.3.4"] + pg.Unlock() + if ok { + t.Fatal("still have a stat record for peerA's ip") + } + }) } diff --git a/pubsub_test.go b/pubsub_test.go index 37fbbf21..e27bdbf6 100644 --- a/pubsub_test.go +++ b/pubsub_test.go @@ -2,69 +2,94 @@ package pubsub import ( "context" + "runtime" "testing" + "testing/synctest" "time" - "github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p/core/host" - "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/x/simlibp2p" + "github.com/marcopolo/simnet" ) -func getDefaultHosts(t *testing.T, n int) []host.Host { - var out []host.Host - - for i := 0; i < n; i++ { - h, err := libp2p.New(libp2p.ResourceManager(&network.NullResourceManager{})) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { h.Close() }) - out = append(out, h) +// synctestTest wraps synctest.Test with GOMAXPROCS(1) to work around a Go +// runtime bug where concurrent bubble timer firing corrupts TSan state. +// https://github.com/golang/go/issues/78156 +func synctestTest(t *testing.T, f func(t *testing.T)) { + if raceEnabled { + prev := runtime.GOMAXPROCS(1) + t.Cleanup(func() { runtime.GOMAXPROCS(prev) }) } + synctest.Test(t, f) +} - return out +func getDefaultHosts(t *testing.T, n int) []host.Host { + net, meta, err := simlibp2p.SimpleLibp2pNetwork( + []simlibp2p.NodeLinkSettingsAndCount{{ + LinkSettings: simnet.NodeBiDiLinkSettings{ + Downlink: simnet.LinkSettings{BitsPerSecond: 20 * simlibp2p.OneMbps}, + Uplink: simnet.LinkSettings{BitsPerSecond: 20 * simlibp2p.OneMbps}, + }, + Count: n, + }}, + simnet.StaticLatency(time.Millisecond), + simlibp2p.NetworkSettings{}, + ) + if err != nil { + t.Fatal(err) + } + net.Start() + t.Cleanup(func() { + for _, h := range meta.Nodes { + h.Close() + } + net.Close() + }) + return meta.Nodes } // See https://github.com/libp2p/go-libp2p-pubsub/issues/426 func TestPubSubRemovesBlacklistedPeer(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) - hosts := getDefaultHosts(t, 2) + hosts := getDefaultHosts(t, 2) - bl := NewMapBlacklist() + bl := NewMapBlacklist() - psubs0 := getPubsub(ctx, hosts[0]) - psubs1 := getPubsub(ctx, hosts[1], WithBlacklist(bl)) - connect(t, hosts[0], hosts[1]) + psubs0 := getPubsub(ctx, hosts[0]) + psubs1 := getPubsub(ctx, hosts[1], WithBlacklist(bl)) + connect(t, hosts[0], hosts[1]) - // Bad peer is blacklisted after it has connected. - // Calling p.BlacklistPeer directly does the right thing but we should also clean - // up the peer if it has been added the the blacklist by another means. - withRouter(psubs1, func(r PubSubRouter) { - bl.Add(hosts[0].ID()) - }) + // Bad peer is blacklisted after it has connected. + // Calling p.BlacklistPeer directly does the right thing but we should also clean + // up the peer if it has been added the the blacklist by another means. + withRouter(psubs1, func(r PubSubRouter) { + bl.Add(hosts[0].ID()) + }) - _, err := psubs0.Subscribe("test") - if err != nil { - t.Fatal(err) - } + _, err := psubs0.Subscribe("test") + if err != nil { + t.Fatal(err) + } - sub1, err := psubs1.Subscribe("test") - if err != nil { - t.Fatal(err) - } + sub1, err := psubs1.Subscribe("test") + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - psubs0.Publish("test", []byte("message")) + psubs0.Publish("test", []byte("message")) - wctx, cancel2 := context.WithTimeout(ctx, 1*time.Second) - defer cancel2() + wctx, cancel2 := context.WithTimeout(ctx, 1*time.Second) + defer cancel2() - _, _ = sub1.Next(wctx) + _, _ = sub1.Next(wctx) - // Explicitly cancel context so PubSub cleans up peer channels. - // Issue 426 reports a panic due to a peer channel being closed twice. - cancel() - time.Sleep(time.Millisecond * 100) + // Explicitly cancel context so PubSub cleans up peer channels. + // Issue 426 reports a panic due to a peer channel being closed twice. + cancel() + time.Sleep(time.Millisecond * 100) + }) } diff --git a/race_test.go b/race_test.go new file mode 100644 index 00000000..130fcc62 --- /dev/null +++ b/race_test.go @@ -0,0 +1,5 @@ +//go:build race + +package pubsub + +const raceEnabled = true diff --git a/randomsub_test.go b/randomsub_test.go index 5c817b7c..f79fdd0c 100644 --- a/randomsub_test.go +++ b/randomsub_test.go @@ -37,156 +37,164 @@ func tryReceive(sub *Subscription) *Message { } func TestRandomsubSmall(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 10) - psubs := getRandomsubs(ctx, hosts, 10) + hosts := getDefaultHosts(t, 10) + psubs := getRandomsubs(ctx, hosts, 10) - connectAll(t, hosts) + connectAll(t, hosts) - var subs []*Subscription - for _, ps := range psubs { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + var subs []*Subscription + for _, ps := range psubs { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } + subs = append(subs, sub) } - subs = append(subs, sub) - } - time.Sleep(time.Second) + time.Sleep(time.Second) - count := 0 - for i := 0; i < 10; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[i].Publish("test", msg) + count := 0 + for i := 0; i < 10; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[i].Publish("test", msg) - for _, sub := range subs { - if tryReceive(sub) != nil { - count++ + for _, sub := range subs { + if tryReceive(sub) != nil { + count++ + } } } - } - if count < 7*len(hosts) { - t.Fatalf("received too few messages; expected at least %d but got %d", 9*len(hosts), count) - } + if count < 7*len(hosts) { + t.Fatalf("received too few messages; expected at least %d but got %d", 9*len(hosts), count) + } + }) } func TestRandomsubBig(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 50) - psubs := getRandomsubs(ctx, hosts, 50) + hosts := getDefaultHosts(t, 50) + psubs := getRandomsubs(ctx, hosts, 50) - connectSome(t, hosts, 12) + connectSome(t, hosts, 12) - var subs []*Subscription - for _, ps := range psubs { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + var subs []*Subscription + for _, ps := range psubs { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } + subs = append(subs, sub) } - subs = append(subs, sub) - } - time.Sleep(time.Second) + time.Sleep(time.Second) - count := 0 - for i := 0; i < 10; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[i].Publish("test", msg) + count := 0 + for i := 0; i < 10; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[i].Publish("test", msg) - for _, sub := range subs { - if tryReceive(sub) != nil { - count++ + for _, sub := range subs { + if tryReceive(sub) != nil { + count++ + } } } - } - if count < 7*len(hosts) { - t.Fatalf("received too few messages; expected at least %d but got %d", 9*len(hosts), count) - } + if count < 7*len(hosts) { + t.Fatalf("received too few messages; expected at least %d but got %d", 9*len(hosts), count) + } + }) } func TestRandomsubMixed(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - hosts := getDefaultHosts(t, 40) - fsubs := getPubsubs(ctx, hosts[:10]) - rsubs := getRandomsubs(ctx, hosts[10:], 30) - psubs := append(fsubs, rsubs...) - - connectSome(t, hosts, 12) - - var subs []*Subscription - for _, ps := range psubs { - sub, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + hosts := getDefaultHosts(t, 40) + fsubs := getPubsubs(ctx, hosts[:10]) + rsubs := getRandomsubs(ctx, hosts[10:], 30) + psubs := append(fsubs, rsubs...) + + connectSome(t, hosts, 12) + + var subs []*Subscription + for _, ps := range psubs { + sub, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } + subs = append(subs, sub) } - subs = append(subs, sub) - } - time.Sleep(time.Second) + time.Sleep(time.Second) - count := 0 - for i := 0; i < 10; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) - psubs[i].Publish("test", msg) + count := 0 + for i := 0; i < 10; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) + psubs[i].Publish("test", msg) - for _, sub := range subs { - if tryReceive(sub) != nil { - count++ + for _, sub := range subs { + if tryReceive(sub) != nil { + count++ + } } } - } - if count < 7*len(hosts) { - t.Fatalf("received too few messages; expected at least %d but got %d", 9*len(hosts), count) - } + if count < 7*len(hosts) { + t.Fatalf("received too few messages; expected at least %d but got %d", 9*len(hosts), count) + } + }) } func TestRandomsubEnoughPeers(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 40) - fsubs := getPubsubs(ctx, hosts[:10]) - rsubs := getRandomsubs(ctx, hosts[10:], 30) - psubs := append(fsubs, rsubs...) + hosts := getDefaultHosts(t, 40) + fsubs := getPubsubs(ctx, hosts[:10]) + rsubs := getRandomsubs(ctx, hosts[10:], 30) + psubs := append(fsubs, rsubs...) - connectSome(t, hosts, 12) + connectSome(t, hosts, 12) - for _, ps := range psubs { - _, err := ps.Subscribe("test") - if err != nil { - t.Fatal(err) + for _, ps := range psubs { + _, err := ps.Subscribe("test") + if err != nil { + t.Fatal(err) + } } - } - time.Sleep(time.Second) + time.Sleep(time.Second) - res := make(chan bool, 1) - rsubs[0].eval <- func() { - rs := rsubs[0].rt.(*RandomSubRouter) - res <- rs.EnoughPeers("test", 0) - } + res := make(chan bool, 1) + rsubs[0].eval <- func() { + rs := rsubs[0].rt.(*RandomSubRouter) + res <- rs.EnoughPeers("test", 0) + } - enough := <-res - if !enough { - t.Fatal("expected enough peers") - } + enough := <-res + if !enough { + t.Fatal("expected enough peers") + } - rsubs[0].eval <- func() { - rs := rsubs[0].rt.(*RandomSubRouter) - res <- rs.EnoughPeers("test", 100) - } + rsubs[0].eval <- func() { + rs := rsubs[0].rt.(*RandomSubRouter) + res <- rs.EnoughPeers("test", 100) + } - enough = <-res - if !enough { - t.Fatal("expected enough peers") - } + enough = <-res + if !enough { + t.Fatal("expected enough peers") + } + }) } diff --git a/rpc_queue_test.go b/rpc_queue_test.go index 6e92ee56..6380d383 100644 --- a/rpc_queue_test.go +++ b/rpc_queue_test.go @@ -7,223 +7,250 @@ import ( ) func TestNewRpcQueue(t *testing.T) { - maxSize := 32 - q := newRpcQueue(maxSize) - if q.maxSize != maxSize { - t.Fatalf("rpc queue has wrong max size, expected %d but got %d", maxSize, q.maxSize) - } - if q.dataAvailable.L != &q.queueMu { - t.Fatalf("the dataAvailable field of rpc queue has an incorrect mutex") - } - if q.spaceAvailable.L != &q.queueMu { - t.Fatalf("the spaceAvailable field of rpc queue has an incorrect mutex") - } + synctestTest(t, func(t *testing.T) { + maxSize := 32 + q := newRpcQueue(maxSize) + if q.maxSize != maxSize { + t.Fatalf("rpc queue has wrong max size, expected %d but got %d", maxSize, q.maxSize) + } + if q.dataAvailable.L != &q.queueMu { + t.Fatalf("the dataAvailable field of rpc queue has an incorrect mutex") + } + if q.spaceAvailable.L != &q.queueMu { + t.Fatalf("the spaceAvailable field of rpc queue has an incorrect mutex") + } + }) } func TestRpcQueueUrgentPush(t *testing.T) { - maxSize := 32 - q := newRpcQueue(maxSize) - - rpc1 := &RPC{} - rpc2 := &RPC{} - rpc3 := &RPC{} - rpc4 := &RPC{} - q.Push(rpc1, true) - q.UrgentPush(rpc2, true) - q.Push(rpc3, true) - q.UrgentPush(rpc4, true) - pop1, err := q.Pop(context.Background()) - if err != nil { - t.Fatal(err) - } - pop2, err := q.Pop(context.Background()) - if err != nil { - t.Fatal(err) - } - pop3, err := q.Pop(context.Background()) - if err != nil { - t.Fatal(err) - } - pop4, err := q.Pop(context.Background()) - if err != nil { - t.Fatal(err) - } - if pop1 != rpc2 { - t.Fatalf("get wrong item from rpc queue Pop") - } - if pop2 != rpc4 { - t.Fatalf("get wrong item from rpc queue Pop") - } - if pop3 != rpc1 { - t.Fatalf("get wrong item from rpc queue Pop") - } - if pop4 != rpc3 { - t.Fatalf("get wrong item from rpc queue Pop") - } + synctestTest(t, func(t *testing.T) { + maxSize := 32 + q := newRpcQueue(maxSize) + + rpc1 := &RPC{} + rpc2 := &RPC{} + rpc3 := &RPC{} + rpc4 := &RPC{} + q.Push(rpc1, true) + q.UrgentPush(rpc2, true) + q.Push(rpc3, true) + q.UrgentPush(rpc4, true) + pop1, err := q.Pop(context.Background()) + if err != nil { + t.Fatal(err) + } + pop2, err := q.Pop(context.Background()) + if err != nil { + t.Fatal(err) + } + pop3, err := q.Pop(context.Background()) + if err != nil { + t.Fatal(err) + } + pop4, err := q.Pop(context.Background()) + if err != nil { + t.Fatal(err) + } + if pop1 != rpc2 { + t.Fatalf("get wrong item from rpc queue Pop") + } + if pop2 != rpc4 { + t.Fatalf("get wrong item from rpc queue Pop") + } + if pop3 != rpc1 { + t.Fatalf("get wrong item from rpc queue Pop") + } + if pop4 != rpc3 { + t.Fatalf("get wrong item from rpc queue Pop") + } + }) } func TestRpcQueuePushThenPop(t *testing.T) { - maxSize := 32 - q := newRpcQueue(maxSize) - - rpc1 := &RPC{} - rpc2 := &RPC{} - q.Push(rpc1, true) - q.Push(rpc2, true) - pop1, err := q.Pop(context.Background()) - if err != nil { - t.Fatal(err) - } - pop2, err := q.Pop(context.Background()) - if err != nil { - t.Fatal(err) - } - if pop1 != rpc1 { - t.Fatalf("get wrong item from rpc queue Pop") - } - if pop2 != rpc2 { - t.Fatalf("get wrong item from rpc queue Pop") - } + synctestTest(t, func(t *testing.T) { + maxSize := 32 + q := newRpcQueue(maxSize) + + rpc1 := &RPC{} + rpc2 := &RPC{} + q.Push(rpc1, true) + q.Push(rpc2, true) + pop1, err := q.Pop(context.Background()) + if err != nil { + t.Fatal(err) + } + pop2, err := q.Pop(context.Background()) + if err != nil { + t.Fatal(err) + } + if pop1 != rpc1 { + t.Fatalf("get wrong item from rpc queue Pop") + } + if pop2 != rpc2 { + t.Fatalf("get wrong item from rpc queue Pop") + } + }) } func TestRpcQueuePopThenPush(t *testing.T) { - maxSize := 32 - q := newRpcQueue(maxSize) + synctestTest(t, func(t *testing.T) { + maxSize := 32 + q := newRpcQueue(maxSize) - rpc1 := &RPC{} - rpc2 := &RPC{} - go func() { - // Wait to make sure the main goroutine is blocked. - time.Sleep(1 * time.Millisecond) - q.Push(rpc1, true) - q.Push(rpc2, true) - }() - pop1, err := q.Pop(context.Background()) - if err != nil { - t.Fatal(err) - } - pop2, err := q.Pop(context.Background()) - if err != nil { - t.Fatal(err) - } - if pop1 != rpc1 { - t.Fatalf("get wrong item from rpc queue Pop") - } - if pop2 != rpc2 { - t.Fatalf("get wrong item from rpc queue Pop") - } + rpc1 := &RPC{} + rpc2 := &RPC{} + go func() { + // Wait to make sure the main goroutine is blocked. + time.Sleep(1 * time.Millisecond) + q.Push(rpc1, true) + q.Push(rpc2, true) + }() + pop1, err := q.Pop(context.Background()) + if err != nil { + t.Fatal(err) + } + pop2, err := q.Pop(context.Background()) + if err != nil { + t.Fatal(err) + } + if pop1 != rpc1 { + t.Fatalf("get wrong item from rpc queue Pop") + } + if pop2 != rpc2 { + t.Fatalf("get wrong item from rpc queue Pop") + } + }) } func TestRpcQueueBlockPushWhenFull(t *testing.T) { - maxSize := 1 - q := newRpcQueue(maxSize) + synctestTest(t, func(t *testing.T) { + maxSize := 1 + q := newRpcQueue(maxSize) - finished := make(chan struct{}) - q.Push(&RPC{}, true) - go func() { + finished := make(chan struct{}) q.Push(&RPC{}, true) - finished <- struct{}{} - }() - // Wait to make sure the goroutine is blocked. - time.Sleep(1 * time.Millisecond) - select { - case <-finished: - t.Fatalf("blocking rpc queue Push is not blocked when it is full") - default: - } + go func() { + defer func() { recover() }() // recover from close + q.Push(&RPC{}, true) + finished <- struct{}{} + }() + // Wait to make sure the goroutine is blocked. + time.Sleep(1 * time.Millisecond) + select { + case <-finished: + t.Fatalf("blocking rpc queue Push is not blocked when it is full") + default: + } + // Unblock the goroutine so synctest can exit cleanly + q.Close() + }) } func TestRpcQueueNonblockPushWhenFull(t *testing.T) { - maxSize := 1 - q := newRpcQueue(maxSize) - - q.Push(&RPC{}, true) - err := q.Push(&RPC{}, false) - if err != ErrQueueFull { - t.Fatalf("non-blocking rpc queue Push returns wrong error when it is full") - } + synctestTest(t, func(t *testing.T) { + maxSize := 1 + q := newRpcQueue(maxSize) + + q.Push(&RPC{}, true) + err := q.Push(&RPC{}, false) + if err != ErrQueueFull { + t.Fatalf("non-blocking rpc queue Push returns wrong error when it is full") + } + }) } func TestRpcQueuePushAfterClose(t *testing.T) { - maxSize := 32 - q := newRpcQueue(maxSize) - q.Close() + synctestTest(t, func(t *testing.T) { + maxSize := 32 + q := newRpcQueue(maxSize) + q.Close() - defer func() { - if r := recover(); r == nil { - t.Fatalf("rpc queue Push does not panick after closed") - } - }() - q.Push(&RPC{}, true) + defer func() { + if r := recover(); r == nil { + t.Fatalf("rpc queue Push does not panick after closed") + } + }() + q.Push(&RPC{}, true) + }) } func TestRpcQueuePopAfterClose(t *testing.T) { - maxSize := 32 - q := newRpcQueue(maxSize) - q.Close() - _, err := q.Pop(context.Background()) - if err != ErrQueueClosed { - t.Fatalf("rpc queue Pop returns wrong error after closed") - } + synctestTest(t, func(t *testing.T) { + maxSize := 32 + q := newRpcQueue(maxSize) + q.Close() + _, err := q.Pop(context.Background()) + if err != ErrQueueClosed { + t.Fatalf("rpc queue Pop returns wrong error after closed") + } + }) } func TestRpcQueueCloseWhilePush(t *testing.T) { - maxSize := 1 - q := newRpcQueue(maxSize) - q.Push(&RPC{}, true) + synctestTest(t, func(t *testing.T) { + maxSize := 1 + q := newRpcQueue(maxSize) + q.Push(&RPC{}, true) - defer func() { - if r := recover(); r == nil { - t.Fatalf("rpc queue Push does not panick when it's closed on the fly") - } - }() + defer func() { + if r := recover(); r == nil { + t.Fatalf("rpc queue Push does not panick when it's closed on the fly") + } + }() - go func() { - // Wait to make sure the main goroutine is blocked. - time.Sleep(1 * time.Millisecond) - q.Close() - }() - q.Push(&RPC{}, true) + go func() { + // Wait to make sure the main goroutine is blocked. + time.Sleep(1 * time.Millisecond) + q.Close() + }() + q.Push(&RPC{}, true) + }) } func TestRpcQueueCloseWhilePop(t *testing.T) { - maxSize := 32 - q := newRpcQueue(maxSize) - go func() { - // Wait to make sure the main goroutine is blocked. - time.Sleep(1 * time.Millisecond) - q.Close() - }() - _, err := q.Pop(context.Background()) - if err != ErrQueueClosed { - t.Fatalf("rpc queue Pop returns wrong error when it's closed on the fly") - } + synctestTest(t, func(t *testing.T) { + maxSize := 32 + q := newRpcQueue(maxSize) + go func() { + // Wait to make sure the main goroutine is blocked. + time.Sleep(1 * time.Millisecond) + q.Close() + }() + _, err := q.Pop(context.Background()) + if err != ErrQueueClosed { + t.Fatalf("rpc queue Pop returns wrong error when it's closed on the fly") + } + }) } func TestRpcQueuePushWhenFullThenPop(t *testing.T) { - maxSize := 1 - q := newRpcQueue(maxSize) + synctestTest(t, func(t *testing.T) { + maxSize := 1 + q := newRpcQueue(maxSize) - q.Push(&RPC{}, true) - go func() { - // Wait to make sure the main goroutine is blocked. - time.Sleep(1 * time.Millisecond) - q.Pop(context.Background()) - }() - q.Push(&RPC{}, true) + q.Push(&RPC{}, true) + go func() { + // Wait to make sure the main goroutine is blocked. + time.Sleep(1 * time.Millisecond) + q.Pop(context.Background()) + }() + q.Push(&RPC{}, true) + }) } func TestRpcQueueCancelPop(t *testing.T) { - maxSize := 32 - q := newRpcQueue(maxSize) - ctx, cancel := context.WithCancel(context.Background()) - go func() { - // Wait to make sure the main goroutine is blocked. - time.Sleep(1 * time.Millisecond) - cancel() - }() - _, err := q.Pop(ctx) - if err != ErrQueueCancelled { - t.Fatalf("rpc queue Pop returns wrong error when it's cancelled") - } + synctestTest(t, func(t *testing.T) { + maxSize := 32 + q := newRpcQueue(maxSize) + ctx, cancel := context.WithCancel(context.Background()) + go func() { + // Wait to make sure the main goroutine is blocked. + time.Sleep(1 * time.Millisecond) + cancel() + }() + _, err := q.Pop(ctx) + if err != ErrQueueCancelled { + t.Fatalf("rpc queue Pop returns wrong error when it's cancelled") + } + }) } diff --git a/score_test.go b/score_test.go index 93369a9e..ac4c3b59 100644 --- a/score_test.go +++ b/score_test.go @@ -13,1053 +13,1112 @@ import ( func TestScoreTimeInMesh(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } - topicScoreParams := &TopicScoreParams{ - TopicWeight: 0.5, - TimeInMeshWeight: 1, - TimeInMeshQuantum: time.Millisecond, - TimeInMeshCap: 3600, - } - params.Topics[mytopic] = topicScoreParams + synctestTest(t, func(t *testing.T) { - peerA := peer.ID("A") + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } + topicScoreParams := &TopicScoreParams{ + TopicWeight: 0.5, + TimeInMeshWeight: 1, + TimeInMeshQuantum: time.Millisecond, + TimeInMeshCap: 3600, + } + params.Topics[mytopic] = topicScoreParams - // Peer score should start at 0 - ps := newPeerScore(params, slog.Default()) - ps.AddPeer(peerA, "myproto") + peerA := peer.ID("A") - aScore := ps.Score(peerA) - if aScore != 0 { - t.Fatal("expected score to start at zero") - } + // Peer score should start at 0 + ps := newPeerScore(params, slog.Default()) + ps.AddPeer(peerA, "myproto") - // The time in mesh depends on how long the peer has been grafted - ps.Graft(peerA, mytopic) - elapsed := topicScoreParams.TimeInMeshQuantum * 200 - time.Sleep(elapsed) + aScore := ps.Score(peerA) + if aScore != 0 { + t.Fatal("expected score to start at zero") + } - ps.refreshScores() - aScore = ps.Score(peerA) - expected := topicScoreParams.TopicWeight * topicScoreParams.TimeInMeshWeight * float64(elapsed/topicScoreParams.TimeInMeshQuantum) - if aScore < expected { - t.Fatalf("Score: %f. Expected >= %f", aScore, expected) - } + // The time in mesh depends on how long the peer has been grafted + ps.Graft(peerA, mytopic) + elapsed := topicScoreParams.TimeInMeshQuantum * 200 + time.Sleep(elapsed) + + ps.refreshScores() + aScore = ps.Score(peerA) + expected := topicScoreParams.TopicWeight * topicScoreParams.TimeInMeshWeight * float64(elapsed/topicScoreParams.TimeInMeshQuantum) + if aScore < expected { + t.Fatalf("Score: %f. Expected >= %f", aScore, expected) + } + }) } func TestScoreTimeInMeshCap(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } - topicScoreParams := &TopicScoreParams{ - TopicWeight: 0.5, - TimeInMeshWeight: 1, - TimeInMeshQuantum: time.Millisecond, - TimeInMeshCap: 10, - } + synctestTest(t, func(t *testing.T) { + + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } + topicScoreParams := &TopicScoreParams{ + TopicWeight: 0.5, + TimeInMeshWeight: 1, + TimeInMeshQuantum: time.Millisecond, + TimeInMeshCap: 10, + } - params.Topics[mytopic] = topicScoreParams + params.Topics[mytopic] = topicScoreParams - peerA := peer.ID("A") + peerA := peer.ID("A") - ps := newPeerScore(params, slog.Default()) - ps.AddPeer(peerA, "myproto") - ps.Graft(peerA, mytopic) - elapsed := topicScoreParams.TimeInMeshQuantum * 40 - time.Sleep(elapsed) + ps := newPeerScore(params, slog.Default()) + ps.AddPeer(peerA, "myproto") + ps.Graft(peerA, mytopic) + elapsed := topicScoreParams.TimeInMeshQuantum * 40 + time.Sleep(elapsed) - // The time in mesh score has a cap - ps.refreshScores() - aScore := ps.Score(peerA) - expected := topicScoreParams.TopicWeight * topicScoreParams.TimeInMeshWeight * topicScoreParams.TimeInMeshCap - variance := 0.5 - if !withinVariance(aScore, expected, variance) { - t.Fatalf("Score: %f. Expected %f ± %f", aScore, expected, variance*expected) - } + // The time in mesh score has a cap + ps.refreshScores() + aScore := ps.Score(peerA) + expected := topicScoreParams.TopicWeight * topicScoreParams.TimeInMeshWeight * topicScoreParams.TimeInMeshCap + variance := 0.5 + if !withinVariance(aScore, expected, variance) { + t.Fatalf("Score: %f. Expected %f ± %f", aScore, expected, variance*expected) + } + }) } func TestScoreFirstMessageDeliveries(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } - topicScoreParams := &TopicScoreParams{ - TopicWeight: 1, - FirstMessageDeliveriesWeight: 1, - FirstMessageDeliveriesDecay: 1.0, // test without decay for now - FirstMessageDeliveriesCap: 2000, - TimeInMeshQuantum: time.Second, - } - - params.Topics[mytopic] = topicScoreParams - peerA := peer.ID("A") + synctestTest(t, func(t *testing.T) { - ps := newPeerScore(params, slog.Default()) - ps.AddPeer(peerA, "myproto") - ps.Graft(peerA, mytopic) + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } + topicScoreParams := &TopicScoreParams{ + TopicWeight: 1, + FirstMessageDeliveriesWeight: 1, + FirstMessageDeliveriesDecay: 1.0, // test without decay for now + FirstMessageDeliveriesCap: 2000, + TimeInMeshQuantum: time.Second, + } - // deliver a bunch of messages from peer A - nMessages := 100 - for i := 0; i < nMessages; i++ { - pbMsg := makeTestMessage(i) - pbMsg.Topic = &mytopic - msg := Message{ReceivedFrom: peerA, Message: pbMsg} - ps.ValidateMessage(&msg) - ps.DeliverMessage(&msg) - } + params.Topics[mytopic] = topicScoreParams + peerA := peer.ID("A") + + ps := newPeerScore(params, slog.Default()) + ps.AddPeer(peerA, "myproto") + ps.Graft(peerA, mytopic) + + // deliver a bunch of messages from peer A + nMessages := 100 + for i := 0; i < nMessages; i++ { + pbMsg := makeTestMessage(i) + pbMsg.Topic = &mytopic + msg := Message{ReceivedFrom: peerA, Message: pbMsg} + ps.ValidateMessage(&msg) + ps.DeliverMessage(&msg) + } - ps.refreshScores() - aScore := ps.Score(peerA) - expected := topicScoreParams.TopicWeight * topicScoreParams.FirstMessageDeliveriesWeight * float64(nMessages) - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + ps.refreshScores() + aScore := ps.Score(peerA) + expected := topicScoreParams.TopicWeight * topicScoreParams.FirstMessageDeliveriesWeight * float64(nMessages) + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } + }) } func TestScoreFirstMessageDeliveriesCap(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } - topicScoreParams := &TopicScoreParams{ - TopicWeight: 1, - FirstMessageDeliveriesWeight: 1, - FirstMessageDeliveriesDecay: 1.0, // test without decay for now - FirstMessageDeliveriesCap: 50, - TimeInMeshQuantum: time.Second, - } - - params.Topics[mytopic] = topicScoreParams - peerA := peer.ID("A") + synctestTest(t, func(t *testing.T) { - ps := newPeerScore(params, slog.Default()) - ps.AddPeer(peerA, "myproto") - ps.Graft(peerA, mytopic) + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } + topicScoreParams := &TopicScoreParams{ + TopicWeight: 1, + FirstMessageDeliveriesWeight: 1, + FirstMessageDeliveriesDecay: 1.0, // test without decay for now + FirstMessageDeliveriesCap: 50, + TimeInMeshQuantum: time.Second, + } - // deliver a bunch of messages from peer A - nMessages := 100 - for i := 0; i < nMessages; i++ { - pbMsg := makeTestMessage(i) - pbMsg.Topic = &mytopic - msg := Message{ReceivedFrom: peerA, Message: pbMsg} - ps.ValidateMessage(&msg) - ps.DeliverMessage(&msg) - } + params.Topics[mytopic] = topicScoreParams + peerA := peer.ID("A") + + ps := newPeerScore(params, slog.Default()) + ps.AddPeer(peerA, "myproto") + ps.Graft(peerA, mytopic) + + // deliver a bunch of messages from peer A + nMessages := 100 + for i := 0; i < nMessages; i++ { + pbMsg := makeTestMessage(i) + pbMsg.Topic = &mytopic + msg := Message{ReceivedFrom: peerA, Message: pbMsg} + ps.ValidateMessage(&msg) + ps.DeliverMessage(&msg) + } - ps.refreshScores() - aScore := ps.Score(peerA) - expected := topicScoreParams.TopicWeight * topicScoreParams.FirstMessageDeliveriesWeight * topicScoreParams.FirstMessageDeliveriesCap - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + ps.refreshScores() + aScore := ps.Score(peerA) + expected := topicScoreParams.TopicWeight * topicScoreParams.FirstMessageDeliveriesWeight * topicScoreParams.FirstMessageDeliveriesCap + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } + }) } func TestScoreFirstMessageDeliveriesDecay(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } - topicScoreParams := &TopicScoreParams{ - TopicWeight: 1, - FirstMessageDeliveriesWeight: 1, - FirstMessageDeliveriesDecay: 0.9, // decay 10% per decay interval - FirstMessageDeliveriesCap: 2000, - TimeInMeshQuantum: time.Second, - } - - params.Topics[mytopic] = topicScoreParams - peerA := peer.ID("A") + synctestTest(t, func(t *testing.T) { - ps := newPeerScore(params, slog.Default()) - ps.AddPeer(peerA, "myproto") - ps.Graft(peerA, mytopic) - - // deliver a bunch of messages from peer A - nMessages := 100 - for i := 0; i < nMessages; i++ { - pbMsg := makeTestMessage(i) - pbMsg.Topic = &mytopic - msg := Message{ReceivedFrom: peerA, Message: pbMsg} - ps.ValidateMessage(&msg) - ps.DeliverMessage(&msg) - } + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } + topicScoreParams := &TopicScoreParams{ + TopicWeight: 1, + FirstMessageDeliveriesWeight: 1, + FirstMessageDeliveriesDecay: 0.9, // decay 10% per decay interval + FirstMessageDeliveriesCap: 2000, + TimeInMeshQuantum: time.Second, + } - ps.refreshScores() - aScore := ps.Score(peerA) - expected := topicScoreParams.TopicWeight * topicScoreParams.FirstMessageDeliveriesWeight * topicScoreParams.FirstMessageDeliveriesDecay * float64(nMessages) - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + params.Topics[mytopic] = topicScoreParams + peerA := peer.ID("A") + + ps := newPeerScore(params, slog.Default()) + ps.AddPeer(peerA, "myproto") + ps.Graft(peerA, mytopic) + + // deliver a bunch of messages from peer A + nMessages := 100 + for i := 0; i < nMessages; i++ { + pbMsg := makeTestMessage(i) + pbMsg.Topic = &mytopic + msg := Message{ReceivedFrom: peerA, Message: pbMsg} + ps.ValidateMessage(&msg) + ps.DeliverMessage(&msg) + } - // refreshing the scores applies the decay param - decayIntervals := 10 - for i := 0; i < decayIntervals; i++ { ps.refreshScores() - expected *= topicScoreParams.FirstMessageDeliveriesDecay - } - aScore = ps.Score(peerA) - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + aScore := ps.Score(peerA) + expected := topicScoreParams.TopicWeight * topicScoreParams.FirstMessageDeliveriesWeight * topicScoreParams.FirstMessageDeliveriesDecay * float64(nMessages) + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } + + // refreshing the scores applies the decay param + decayIntervals := 10 + for i := 0; i < decayIntervals; i++ { + ps.refreshScores() + expected *= topicScoreParams.FirstMessageDeliveriesDecay + } + aScore = ps.Score(peerA) + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } + }) } func TestScoreMeshMessageDeliveries(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } - topicScoreParams := &TopicScoreParams{ - TopicWeight: 1, - MeshMessageDeliveriesWeight: -1, - MeshMessageDeliveriesActivation: time.Second, - MeshMessageDeliveriesWindow: 10 * time.Millisecond, - MeshMessageDeliveriesThreshold: 20, - MeshMessageDeliveriesCap: 100, - MeshMessageDeliveriesDecay: 1.0, // no decay for this test - - FirstMessageDeliveriesWeight: 0, - TimeInMeshQuantum: time.Second, - } - - params.Topics[mytopic] = topicScoreParams - - // peer A always delivers the message first. - // peer B delivers next (within the delivery window). - // peer C delivers outside the delivery window. - // we expect peers A and B to have a score of zero, since all other parameter weights are zero. - // Peer C should have a negative score. - peerA := peer.ID("A") - peerB := peer.ID("B") - peerC := peer.ID("C") - peers := []peer.ID{peerA, peerB, peerC} - - ps := newPeerScore(params, slog.Default()) - for _, p := range peers { - ps.AddPeer(p, "myproto") - ps.Graft(p, mytopic) - } + synctestTest(t, func(t *testing.T) { - // assert that nobody has been penalized yet for not delivering messages before activation time - ps.refreshScores() - for _, p := range peers { - score := ps.Score(p) - if score < 0 { - t.Fatalf("expected no mesh delivery penalty before activation time, got score %f", score) + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } + topicScoreParams := &TopicScoreParams{ + TopicWeight: 1, + MeshMessageDeliveriesWeight: -1, + MeshMessageDeliveriesActivation: time.Second, + MeshMessageDeliveriesWindow: 10 * time.Millisecond, + MeshMessageDeliveriesThreshold: 20, + MeshMessageDeliveriesCap: 100, + MeshMessageDeliveriesDecay: 1.0, // no decay for this test + + FirstMessageDeliveriesWeight: 0, + TimeInMeshQuantum: time.Second, } - } - // wait for the activation time to kick in - time.Sleep(topicScoreParams.MeshMessageDeliveriesActivation) - - // deliver a bunch of messages from peer A, with duplicates within the window from peer B, - // and duplicates outside the window from peer C. - nMessages := 100 - wg := sync.WaitGroup{} - for i := 0; i < nMessages; i++ { - pbMsg := makeTestMessage(i) - pbMsg.Topic = &mytopic - msg := Message{ReceivedFrom: peerA, Message: pbMsg} - ps.ValidateMessage(&msg) - ps.DeliverMessage(&msg) - msg.ReceivedFrom = peerB - ps.DuplicateMessage(&msg) + params.Topics[mytopic] = topicScoreParams + + // peer A always delivers the message first. + // peer B delivers next (within the delivery window). + // peer C delivers outside the delivery window. + // we expect peers A and B to have a score of zero, since all other parameter weights are zero. + // Peer C should have a negative score. + peerA := peer.ID("A") + peerB := peer.ID("B") + peerC := peer.ID("C") + peers := []peer.ID{peerA, peerB, peerC} + + ps := newPeerScore(params, slog.Default()) + for _, p := range peers { + ps.AddPeer(p, "myproto") + ps.Graft(p, mytopic) + } - // deliver duplicate from peerC after the window - wg.Add(1) - time.AfterFunc(topicScoreParams.MeshMessageDeliveriesWindow+(20*time.Millisecond), func() { - msg.ReceivedFrom = peerC + // assert that nobody has been penalized yet for not delivering messages before activation time + ps.refreshScores() + for _, p := range peers { + score := ps.Score(p) + if score < 0 { + t.Fatalf("expected no mesh delivery penalty before activation time, got score %f", score) + } + } + // wait for the activation time to kick in + time.Sleep(topicScoreParams.MeshMessageDeliveriesActivation) + + // deliver a bunch of messages from peer A, with duplicates within the window from peer B, + // and duplicates outside the window from peer C. + nMessages := 100 + wg := sync.WaitGroup{} + for i := 0; i < nMessages; i++ { + pbMsg := makeTestMessage(i) + pbMsg.Topic = &mytopic + msg := Message{ReceivedFrom: peerA, Message: pbMsg} + ps.ValidateMessage(&msg) + ps.DeliverMessage(&msg) + + msg.ReceivedFrom = peerB ps.DuplicateMessage(&msg) - wg.Done() - }) - } - wg.Wait() - - ps.refreshScores() - aScore := ps.Score(peerA) - bScore := ps.Score(peerB) - cScore := ps.Score(peerC) - if aScore < 0 { - t.Fatalf("Expected non-negative score for peer A, got %f", aScore) - } - if bScore < 0 { - t.Fatalf("Expected non-negative score for peer B, got %f", aScore) - } - // the penalty is the difference between the threshold and the actual mesh deliveries, squared. - // since we didn't deliver anything, this is just the value of the threshold - penalty := topicScoreParams.MeshMessageDeliveriesThreshold * topicScoreParams.MeshMessageDeliveriesThreshold - expected := topicScoreParams.TopicWeight * topicScoreParams.MeshMessageDeliveriesWeight * penalty - if cScore != expected { - t.Fatalf("Score: %f. Expected %f", cScore, expected) - } + // deliver duplicate from peerC after the window + wg.Add(1) + time.AfterFunc(topicScoreParams.MeshMessageDeliveriesWindow+(20*time.Millisecond), func() { + msg.ReceivedFrom = peerC + ps.DuplicateMessage(&msg) + wg.Done() + }) + } + wg.Wait() + + ps.refreshScores() + aScore := ps.Score(peerA) + bScore := ps.Score(peerB) + cScore := ps.Score(peerC) + if aScore < 0 { + t.Fatalf("Expected non-negative score for peer A, got %f", aScore) + } + if bScore < 0 { + t.Fatalf("Expected non-negative score for peer B, got %f", aScore) + } + + // the penalty is the difference between the threshold and the actual mesh deliveries, squared. + // since we didn't deliver anything, this is just the value of the threshold + penalty := topicScoreParams.MeshMessageDeliveriesThreshold * topicScoreParams.MeshMessageDeliveriesThreshold + expected := topicScoreParams.TopicWeight * topicScoreParams.MeshMessageDeliveriesWeight * penalty + if cScore != expected { + t.Fatalf("Score: %f. Expected %f", cScore, expected) + } + }) } func TestScoreMeshMessageDeliveriesDecay(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } - topicScoreParams := &TopicScoreParams{ - TopicWeight: 1, - MeshMessageDeliveriesWeight: -1, - MeshMessageDeliveriesActivation: 0, - MeshMessageDeliveriesWindow: 10 * time.Millisecond, - MeshMessageDeliveriesThreshold: 20, - MeshMessageDeliveriesCap: 100, - MeshMessageDeliveriesDecay: 0.9, - - FirstMessageDeliveriesWeight: 0, - TimeInMeshQuantum: time.Second, - } + synctestTest(t, func(t *testing.T) { - params.Topics[mytopic] = topicScoreParams + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } + topicScoreParams := &TopicScoreParams{ + TopicWeight: 1, + MeshMessageDeliveriesWeight: -1, + MeshMessageDeliveriesActivation: 0, + MeshMessageDeliveriesWindow: 10 * time.Millisecond, + MeshMessageDeliveriesThreshold: 20, + MeshMessageDeliveriesCap: 100, + MeshMessageDeliveriesDecay: 0.9, + + FirstMessageDeliveriesWeight: 0, + TimeInMeshQuantum: time.Second, + } - peerA := peer.ID("A") + params.Topics[mytopic] = topicScoreParams - ps := newPeerScore(params, slog.Default()) - ps.AddPeer(peerA, "myproto") - ps.Graft(peerA, mytopic) + peerA := peer.ID("A") - // deliver messages from peer A - nMessages := 40 - for i := 0; i < nMessages; i++ { - pbMsg := makeTestMessage(i) - pbMsg.Topic = &mytopic - msg := Message{ReceivedFrom: peerA, Message: pbMsg} - ps.ValidateMessage(&msg) - ps.DeliverMessage(&msg) - } + ps := newPeerScore(params, slog.Default()) + ps.AddPeer(peerA, "myproto") + ps.Graft(peerA, mytopic) - // we should have a positive score, since we delivered more messages than the threshold - ps.refreshScores() - aScore := ps.Score(peerA) - if aScore < 0 { - t.Fatalf("Expected non-negative score for peer A, got %f", aScore) - } + // deliver messages from peer A + nMessages := 40 + for i := 0; i < nMessages; i++ { + pbMsg := makeTestMessage(i) + pbMsg.Topic = &mytopic + msg := Message{ReceivedFrom: peerA, Message: pbMsg} + ps.ValidateMessage(&msg) + ps.DeliverMessage(&msg) + } - // we need to refresh enough times for the decay to bring us below the threshold - decayedDeliveryCount := float64(nMessages) * topicScoreParams.MeshMessageDeliveriesDecay - for i := 0; i < 20; i++ { + // advance time so meshMessageDeliveriesActive becomes true (meshTime > activation) + time.Sleep(time.Millisecond) + + // we should have a positive score, since we delivered more messages than the threshold ps.refreshScores() - decayedDeliveryCount *= topicScoreParams.MeshMessageDeliveriesDecay - } - aScore = ps.Score(peerA) - // the penalty is the difference between the threshold and the (decayed) mesh deliveries, squared. - deficit := topicScoreParams.MeshMessageDeliveriesThreshold - decayedDeliveryCount - penalty := deficit * deficit - expected := topicScoreParams.TopicWeight * topicScoreParams.MeshMessageDeliveriesWeight * penalty - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + aScore := ps.Score(peerA) + if aScore < 0 { + t.Fatalf("Expected non-negative score for peer A, got %f", aScore) + } + + // we need to refresh enough times for the decay to bring us below the threshold + decayedDeliveryCount := float64(nMessages) * topicScoreParams.MeshMessageDeliveriesDecay + for i := 0; i < 20; i++ { + ps.refreshScores() + decayedDeliveryCount *= topicScoreParams.MeshMessageDeliveriesDecay + } + aScore = ps.Score(peerA) + // the penalty is the difference between the threshold and the (decayed) mesh deliveries, squared. + deficit := topicScoreParams.MeshMessageDeliveriesThreshold - decayedDeliveryCount + penalty := deficit * deficit + expected := topicScoreParams.TopicWeight * topicScoreParams.MeshMessageDeliveriesWeight * penalty + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } + }) } func TestScoreMeshFailurePenalty(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } + synctestTest(t, func(t *testing.T) { - // the mesh failure penalty is applied when a peer is pruned while their - // mesh deliveries are under the threshold. - // for this test, we set the mesh delivery threshold, but set - // MeshMessageDeliveriesWeight to zero, so the only affect on the score - // is from the mesh failure penalty - topicScoreParams := &TopicScoreParams{ - TopicWeight: 1, - MeshFailurePenaltyWeight: -1, - MeshFailurePenaltyDecay: 1.0, - - MeshMessageDeliveriesActivation: 0, - MeshMessageDeliveriesWindow: 10 * time.Millisecond, - MeshMessageDeliveriesThreshold: 20, - MeshMessageDeliveriesCap: 100, - MeshMessageDeliveriesDecay: 1.0, - - MeshMessageDeliveriesWeight: 0, - FirstMessageDeliveriesWeight: 0, - TimeInMeshQuantum: time.Second, - } + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } - params.Topics[mytopic] = topicScoreParams + // the mesh failure penalty is applied when a peer is pruned while their + // mesh deliveries are under the threshold. + // for this test, we set the mesh delivery threshold, but set + // MeshMessageDeliveriesWeight to zero, so the only affect on the score + // is from the mesh failure penalty + topicScoreParams := &TopicScoreParams{ + TopicWeight: 1, + MeshFailurePenaltyWeight: -1, + MeshFailurePenaltyDecay: 1.0, + + MeshMessageDeliveriesActivation: 0, + MeshMessageDeliveriesWindow: 10 * time.Millisecond, + MeshMessageDeliveriesThreshold: 20, + MeshMessageDeliveriesCap: 100, + MeshMessageDeliveriesDecay: 1.0, + + MeshMessageDeliveriesWeight: 0, + FirstMessageDeliveriesWeight: 0, + TimeInMeshQuantum: time.Second, + } - peerA := peer.ID("A") - peerB := peer.ID("B") - peers := []peer.ID{peerA, peerB} + params.Topics[mytopic] = topicScoreParams - ps := newPeerScore(params, slog.Default()) - for _, p := range peers { - ps.AddPeer(p, "myproto") - ps.Graft(p, mytopic) - } + peerA := peer.ID("A") + peerB := peer.ID("B") + peers := []peer.ID{peerA, peerB} - // deliver messages from peer A. peer B does nothing - nMessages := 100 - for i := 0; i < nMessages; i++ { - pbMsg := makeTestMessage(i) - pbMsg.Topic = &mytopic - msg := Message{ReceivedFrom: peerA, Message: pbMsg} - ps.ValidateMessage(&msg) - ps.DeliverMessage(&msg) - } + ps := newPeerScore(params, slog.Default()) + for _, p := range peers { + ps.AddPeer(p, "myproto") + ps.Graft(p, mytopic) + } - // peers A and B should both have zero scores, since the failure penalty hasn't been applied yet - ps.refreshScores() - aScore := ps.Score(peerA) - bScore := ps.Score(peerB) - if aScore != 0 { - t.Errorf("expected peer A to have score 0.0, got %f", aScore) - } - if bScore != 0 { - t.Errorf("expected peer B to have score 0.0, got %f", bScore) - } + // deliver messages from peer A. peer B does nothing + nMessages := 100 + for i := 0; i < nMessages; i++ { + pbMsg := makeTestMessage(i) + pbMsg.Topic = &mytopic + msg := Message{ReceivedFrom: peerA, Message: pbMsg} + ps.ValidateMessage(&msg) + ps.DeliverMessage(&msg) + } - // prune peer B to apply the penalty - ps.Prune(peerB, mytopic) - ps.refreshScores() - aScore = ps.Score(peerA) - bScore = ps.Score(peerB) + // advance time so meshMessageDeliveriesActive becomes true (meshTime > activation) + time.Sleep(time.Millisecond) - if aScore != 0 { - t.Errorf("expected peer A to have score 0.0, got %f", aScore) - } + // peers A and B should both have zero scores, since the failure penalty hasn't been applied yet + ps.refreshScores() + aScore := ps.Score(peerA) + bScore := ps.Score(peerB) + if aScore != 0 { + t.Errorf("expected peer A to have score 0.0, got %f", aScore) + } + if bScore != 0 { + t.Errorf("expected peer B to have score 0.0, got %f", bScore) + } - // penalty calculation is the same as for MeshMessageDeliveries, but multiplied by MeshFailurePenaltyWeight - // instead of MeshMessageDeliveriesWeight - penalty := topicScoreParams.MeshMessageDeliveriesThreshold * topicScoreParams.MeshMessageDeliveriesThreshold - expected := topicScoreParams.TopicWeight * topicScoreParams.MeshFailurePenaltyWeight * penalty - if bScore != expected { - t.Fatalf("Score: %f. Expected %f", bScore, expected) - } + // prune peer B to apply the penalty + ps.Prune(peerB, mytopic) + ps.refreshScores() + aScore = ps.Score(peerA) + bScore = ps.Score(peerB) + + if aScore != 0 { + t.Errorf("expected peer A to have score 0.0, got %f", aScore) + } + + // penalty calculation is the same as for MeshMessageDeliveries, but multiplied by MeshFailurePenaltyWeight + // instead of MeshMessageDeliveriesWeight + penalty := topicScoreParams.MeshMessageDeliveriesThreshold * topicScoreParams.MeshMessageDeliveriesThreshold + expected := topicScoreParams.TopicWeight * topicScoreParams.MeshFailurePenaltyWeight * penalty + if bScore != expected { + t.Fatalf("Score: %f. Expected %f", bScore, expected) + } + }) } func TestScoreInvalidMessageDeliveries(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } - topicScoreParams := &TopicScoreParams{ - TopicWeight: 1, - TimeInMeshQuantum: time.Second, - InvalidMessageDeliveriesWeight: -1, - InvalidMessageDeliveriesDecay: 1.0, - } - params.Topics[mytopic] = topicScoreParams + synctestTest(t, func(t *testing.T) { - peerA := peer.ID("A") + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } + topicScoreParams := &TopicScoreParams{ + TopicWeight: 1, + TimeInMeshQuantum: time.Second, + InvalidMessageDeliveriesWeight: -1, + InvalidMessageDeliveriesDecay: 1.0, + } + params.Topics[mytopic] = topicScoreParams - ps := newPeerScore(params, slog.Default()) - ps.AddPeer(peerA, "myproto") - ps.Graft(peerA, mytopic) + peerA := peer.ID("A") - nMessages := 100 - for i := 0; i < nMessages; i++ { - pbMsg := makeTestMessage(i) - pbMsg.Topic = &mytopic - msg := Message{ReceivedFrom: peerA, Message: pbMsg} - ps.RejectMessage(&msg, RejectInvalidSignature) - } + ps := newPeerScore(params, slog.Default()) + ps.AddPeer(peerA, "myproto") + ps.Graft(peerA, mytopic) - ps.refreshScores() - aScore := ps.Score(peerA) - expected := topicScoreParams.TopicWeight * topicScoreParams.InvalidMessageDeliveriesWeight * float64(nMessages*nMessages) - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + nMessages := 100 + for i := 0; i < nMessages; i++ { + pbMsg := makeTestMessage(i) + pbMsg.Topic = &mytopic + msg := Message{ReceivedFrom: peerA, Message: pbMsg} + ps.RejectMessage(&msg, RejectInvalidSignature) + } + + ps.refreshScores() + aScore := ps.Score(peerA) + expected := topicScoreParams.TopicWeight * topicScoreParams.InvalidMessageDeliveriesWeight * float64(nMessages*nMessages) + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } + }) } func TestScoreInvalidMessageDeliveriesDecay(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } - topicScoreParams := &TopicScoreParams{ - TopicWeight: 1, - TimeInMeshQuantum: time.Second, - InvalidMessageDeliveriesWeight: -1, - InvalidMessageDeliveriesDecay: 0.9, - } - params.Topics[mytopic] = topicScoreParams + synctestTest(t, func(t *testing.T) { - peerA := peer.ID("A") + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } + topicScoreParams := &TopicScoreParams{ + TopicWeight: 1, + TimeInMeshQuantum: time.Second, + InvalidMessageDeliveriesWeight: -1, + InvalidMessageDeliveriesDecay: 0.9, + } + params.Topics[mytopic] = topicScoreParams - ps := newPeerScore(params, slog.Default()) - ps.AddPeer(peerA, "myproto") - ps.Graft(peerA, mytopic) + peerA := peer.ID("A") - nMessages := 100 - for i := 0; i < nMessages; i++ { - pbMsg := makeTestMessage(i) - pbMsg.Topic = &mytopic - msg := Message{ReceivedFrom: peerA, Message: pbMsg} - ps.RejectMessage(&msg, RejectInvalidSignature) - } + ps := newPeerScore(params, slog.Default()) + ps.AddPeer(peerA, "myproto") + ps.Graft(peerA, mytopic) - ps.refreshScores() - aScore := ps.Score(peerA) - expected := topicScoreParams.TopicWeight * topicScoreParams.InvalidMessageDeliveriesWeight * math.Pow(topicScoreParams.InvalidMessageDeliveriesDecay*float64(nMessages), 2) - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + nMessages := 100 + for i := 0; i < nMessages; i++ { + pbMsg := makeTestMessage(i) + pbMsg.Topic = &mytopic + msg := Message{ReceivedFrom: peerA, Message: pbMsg} + ps.RejectMessage(&msg, RejectInvalidSignature) + } - // refresh scores a few times to apply decay - for i := 0; i < 10; i++ { ps.refreshScores() - expected *= math.Pow(topicScoreParams.InvalidMessageDeliveriesDecay, 2) - } - aScore = ps.Score(peerA) - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + aScore := ps.Score(peerA) + expected := topicScoreParams.TopicWeight * topicScoreParams.InvalidMessageDeliveriesWeight * math.Pow(topicScoreParams.InvalidMessageDeliveriesDecay*float64(nMessages), 2) + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } + + // refresh scores a few times to apply decay + for i := 0; i < 10; i++ { + ps.refreshScores() + expected *= math.Pow(topicScoreParams.InvalidMessageDeliveriesDecay, 2) + } + aScore = ps.Score(peerA) + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } + }) } func TestScoreRejectMessageDeliveries(t *testing.T) { // this tests adds coverage for the dark corners of rejection tracing - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } - topicScoreParams := &TopicScoreParams{ - TopicWeight: 1, - TimeInMeshQuantum: time.Second, - InvalidMessageDeliveriesWeight: -1, - InvalidMessageDeliveriesDecay: 1.0, - } - params.Topics[mytopic] = topicScoreParams + synctestTest(t, func(t *testing.T) { + + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } + topicScoreParams := &TopicScoreParams{ + TopicWeight: 1, + TimeInMeshQuantum: time.Second, + InvalidMessageDeliveriesWeight: -1, + InvalidMessageDeliveriesDecay: 1.0, + } + params.Topics[mytopic] = topicScoreParams - peerA := peer.ID("A") - peerB := peer.ID("B") + peerA := peer.ID("A") + peerB := peer.ID("B") - ps := newPeerScore(params, slog.Default()) - ps.AddPeer(peerA, "myproto") - ps.AddPeer(peerB, "myproto") + ps := newPeerScore(params, slog.Default()) + ps.AddPeer(peerA, "myproto") + ps.AddPeer(peerB, "myproto") - pbMsg := makeTestMessage(0) - pbMsg.Topic = &mytopic - msg := Message{ReceivedFrom: peerA, Message: pbMsg} - msg2 := Message{ReceivedFrom: peerB, Message: pbMsg} + pbMsg := makeTestMessage(0) + pbMsg.Topic = &mytopic + msg := Message{ReceivedFrom: peerA, Message: pbMsg} + msg2 := Message{ReceivedFrom: peerB, Message: pbMsg} - // these should have no effect in the score - ps.RejectMessage(&msg, RejectBlacklstedPeer) - ps.RejectMessage(&msg, RejectBlacklistedSource) - ps.RejectMessage(&msg, RejectValidationQueueFull) + // these should have no effect in the score + ps.RejectMessage(&msg, RejectBlacklstedPeer) + ps.RejectMessage(&msg, RejectBlacklistedSource) + ps.RejectMessage(&msg, RejectValidationQueueFull) - aScore := ps.Score(peerA) - expected := 0.0 - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + aScore := ps.Score(peerA) + expected := 0.0 + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } - // insert a record in the message deliveries - ps.ValidateMessage(&msg) + // insert a record in the message deliveries + ps.ValidateMessage(&msg) - // this should have no effect in the score, and subsequent duplicate messages should have no - // effect either - ps.RejectMessage(&msg, RejectValidationThrottled) - ps.DuplicateMessage(&msg2) + // this should have no effect in the score, and subsequent duplicate messages should have no + // effect either + ps.RejectMessage(&msg, RejectValidationThrottled) + ps.DuplicateMessage(&msg2) - aScore = ps.Score(peerA) - expected = 0.0 - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + aScore = ps.Score(peerA) + expected = 0.0 + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } - bScore := ps.Score(peerB) - expected = 0.0 - if bScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + bScore := ps.Score(peerB) + expected = 0.0 + if bScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } - // now clear the delivery record - ps.deliveries.head.expire = time.Now() - time.Sleep(1 * time.Millisecond) - ps.deliveries.gc() + // now clear the delivery record + ps.deliveries.head.expire = time.Now() + time.Sleep(1 * time.Millisecond) + ps.deliveries.gc() - // insert a record in the message deliveries - ps.ValidateMessage(&msg) + // insert a record in the message deliveries + ps.ValidateMessage(&msg) - // this should have no effect in the score, and subsequent duplicate messages should have no - // effect either - ps.RejectMessage(&msg, RejectValidationIgnored) - ps.DuplicateMessage(&msg2) + // this should have no effect in the score, and subsequent duplicate messages should have no + // effect either + ps.RejectMessage(&msg, RejectValidationIgnored) + ps.DuplicateMessage(&msg2) - aScore = ps.Score(peerA) - expected = 0.0 - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + aScore = ps.Score(peerA) + expected = 0.0 + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } - bScore = ps.Score(peerB) - expected = 0.0 - if bScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + bScore = ps.Score(peerB) + expected = 0.0 + if bScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } - // now clear the delivery record - ps.deliveries.head.expire = time.Now() - time.Sleep(1 * time.Millisecond) - ps.deliveries.gc() + // now clear the delivery record + ps.deliveries.head.expire = time.Now() + time.Sleep(1 * time.Millisecond) + ps.deliveries.gc() - // insert a new record in the message deliveries - ps.ValidateMessage(&msg) + // insert a new record in the message deliveries + ps.ValidateMessage(&msg) - // and reject the message to make sure duplicates are also penalized - ps.RejectMessage(&msg, RejectValidationFailed) - ps.DuplicateMessage(&msg2) + // and reject the message to make sure duplicates are also penalized + ps.RejectMessage(&msg, RejectValidationFailed) + ps.DuplicateMessage(&msg2) - aScore = ps.Score(peerA) - expected = -1.0 - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + aScore = ps.Score(peerA) + expected = -1.0 + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } - bScore = ps.Score(peerB) - expected = -1.0 - if bScore != expected { - t.Fatalf("Score: %f. Expected %f", bScore, expected) - } + bScore = ps.Score(peerB) + expected = -1.0 + if bScore != expected { + t.Fatalf("Score: %f. Expected %f", bScore, expected) + } - // now clear the delivery record again - ps.deliveries.head.expire = time.Now() - time.Sleep(1 * time.Millisecond) - ps.deliveries.gc() + // now clear the delivery record again + ps.deliveries.head.expire = time.Now() + time.Sleep(1 * time.Millisecond) + ps.deliveries.gc() - // insert a new record in the message deliveries - ps.ValidateMessage(&msg) + // insert a new record in the message deliveries + ps.ValidateMessage(&msg) - // and reject the message after a duplciate has arrived - ps.DuplicateMessage(&msg2) - ps.RejectMessage(&msg, RejectValidationFailed) + // and reject the message after a duplciate has arrived + ps.DuplicateMessage(&msg2) + ps.RejectMessage(&msg, RejectValidationFailed) - aScore = ps.Score(peerA) - expected = -4.0 - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + aScore = ps.Score(peerA) + expected = -4.0 + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } - bScore = ps.Score(peerB) - expected = -4.0 - if bScore != expected { - t.Fatalf("Score: %f. Expected %f", bScore, expected) - } + bScore = ps.Score(peerB) + expected = -4.0 + if bScore != expected { + t.Fatalf("Score: %f. Expected %f", bScore, expected) + } + }) } func TestScoreApplicationScore(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" + synctestTest(t, func(t *testing.T) { - var appScoreValue float64 - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return appScoreValue }, - AppSpecificWeight: 0.5, - Topics: make(map[string]*TopicScoreParams), - } + mytopic := "mytopic" + + var appScoreValue float64 + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return appScoreValue }, + AppSpecificWeight: 0.5, + Topics: make(map[string]*TopicScoreParams), + } - peerA := peer.ID("A") + peerA := peer.ID("A") - ps := newPeerScore(params, slog.Default()) - ps.AddPeer(peerA, "myproto") - ps.Graft(peerA, mytopic) + ps := newPeerScore(params, slog.Default()) + ps.AddPeer(peerA, "myproto") + ps.Graft(peerA, mytopic) - for i := -100; i < 100; i++ { - appScoreValue = float64(i) - ps.refreshScores() - aScore := ps.Score(peerA) - expected := float64(i) * params.AppSpecificWeight - if aScore != expected { - t.Errorf("expected peer score to equal app-specific score %f, got %f", expected, aScore) + for i := -100; i < 100; i++ { + appScoreValue = float64(i) + ps.refreshScores() + aScore := ps.Score(peerA) + expected := float64(i) * params.AppSpecificWeight + if aScore != expected { + t.Errorf("expected peer score to equal app-specific score %f, got %f", expected, aScore) + } } - } + }) } func TestScoreIPColocation(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" + synctestTest(t, func(t *testing.T) { - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - IPColocationFactorThreshold: 1, - IPColocationFactorWeight: -1, - Topics: make(map[string]*TopicScoreParams), - } + mytopic := "mytopic" - peerA := peer.ID("A") - peerB := peer.ID("B") - peerC := peer.ID("C") - peerD := peer.ID("D") - peers := []peer.ID{peerA, peerB, peerC, peerD} + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + IPColocationFactorThreshold: 1, + IPColocationFactorWeight: -1, + Topics: make(map[string]*TopicScoreParams), + } - ps := newPeerScore(params, slog.Default()) - for _, p := range peers { - ps.AddPeer(p, "myproto") - ps.Graft(p, mytopic) - } + peerA := peer.ID("A") + peerB := peer.ID("B") + peerC := peer.ID("C") + peerD := peer.ID("D") + peers := []peer.ID{peerA, peerB, peerC, peerD} - // peerA should have no penalty, but B, C, and D should be penalized for sharing an IP - setIPsForPeer(t, ps, peerA, "1.2.3.4") - setIPsForPeer(t, ps, peerB, "2.3.4.5") - setIPsForPeer(t, ps, peerC, "2.3.4.5", "3.4.5.6") - setIPsForPeer(t, ps, peerD, "2.3.4.5") + ps := newPeerScore(params, slog.Default()) + for _, p := range peers { + ps.AddPeer(p, "myproto") + ps.Graft(p, mytopic) + } - ps.refreshScores() - aScore := ps.Score(peerA) - bScore := ps.Score(peerB) - cScore := ps.Score(peerC) - dScore := ps.Score(peerD) + // peerA should have no penalty, but B, C, and D should be penalized for sharing an IP + setIPsForPeer(t, ps, peerA, "1.2.3.4") + setIPsForPeer(t, ps, peerB, "2.3.4.5") + setIPsForPeer(t, ps, peerC, "2.3.4.5", "3.4.5.6") + setIPsForPeer(t, ps, peerD, "2.3.4.5") - if aScore != 0 { - t.Errorf("expected peer A to have score 0.0, got %f", aScore) - } + ps.refreshScores() + aScore := ps.Score(peerA) + bScore := ps.Score(peerB) + cScore := ps.Score(peerC) + dScore := ps.Score(peerD) - nShared := 3 - ipSurplus := nShared - params.IPColocationFactorThreshold - penalty := ipSurplus * ipSurplus - expected := params.IPColocationFactorWeight * float64(penalty) - for _, score := range []float64{bScore, cScore, dScore} { - if score != expected { - t.Fatalf("Score: %f. Expected %f", score, expected) + if aScore != 0 { + t.Errorf("expected peer A to have score 0.0, got %f", aScore) } - } + + nShared := 3 + ipSurplus := nShared - params.IPColocationFactorThreshold + penalty := ipSurplus * ipSurplus + expected := params.IPColocationFactorWeight * float64(penalty) + for _, score := range []float64{bScore, cScore, dScore} { + if score != expected { + t.Fatalf("Score: %f. Expected %f", score, expected) + } + } + }) } func TestScoreIPColocationWhitelist(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" + synctestTest(t, func(t *testing.T) { - _, ipNet, err := net.ParseCIDR("2.3.0.0/16") - if err != nil { - t.Fatal(err) - } + mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - IPColocationFactorThreshold: 1, - IPColocationFactorWeight: -1, - IPColocationFactorWhitelist: []*net.IPNet{ipNet}, - Topics: make(map[string]*TopicScoreParams), - } + _, ipNet, err := net.ParseCIDR("2.3.0.0/16") + if err != nil { + t.Fatal(err) + } - peerA := peer.ID("A") - peerB := peer.ID("B") - peerC := peer.ID("C") - peerD := peer.ID("D") - peers := []peer.ID{peerA, peerB, peerC, peerD} + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + IPColocationFactorThreshold: 1, + IPColocationFactorWeight: -1, + IPColocationFactorWhitelist: []*net.IPNet{ipNet}, + Topics: make(map[string]*TopicScoreParams), + } - ps := newPeerScore(params, slog.Default()) - for _, p := range peers { - ps.AddPeer(p, "myproto") - ps.Graft(p, mytopic) - } + peerA := peer.ID("A") + peerB := peer.ID("B") + peerC := peer.ID("C") + peerD := peer.ID("D") + peers := []peer.ID{peerA, peerB, peerC, peerD} - // peerA should have no penalty, but B, C, and D should be penalized for sharing an IP - setIPsForPeer(t, ps, peerA, "1.2.3.4") - setIPsForPeer(t, ps, peerB, "2.3.4.5") - setIPsForPeer(t, ps, peerC, "2.3.4.5", "3.4.5.6") - setIPsForPeer(t, ps, peerD, "2.3.4.5") + ps := newPeerScore(params, slog.Default()) + for _, p := range peers { + ps.AddPeer(p, "myproto") + ps.Graft(p, mytopic) + } - ps.refreshScores() - aScore := ps.Score(peerA) - bScore := ps.Score(peerB) - cScore := ps.Score(peerC) - dScore := ps.Score(peerD) + // peerA should have no penalty, but B, C, and D should be penalized for sharing an IP + setIPsForPeer(t, ps, peerA, "1.2.3.4") + setIPsForPeer(t, ps, peerB, "2.3.4.5") + setIPsForPeer(t, ps, peerC, "2.3.4.5", "3.4.5.6") + setIPsForPeer(t, ps, peerD, "2.3.4.5") - if aScore != 0 { - t.Errorf("expected peer A to have score 0.0, got %f", aScore) - } + ps.refreshScores() + aScore := ps.Score(peerA) + bScore := ps.Score(peerB) + cScore := ps.Score(peerC) + dScore := ps.Score(peerD) - if bScore != 0 { - t.Errorf("expected peer B to have score 0.0, got %f", aScore) - } + if aScore != 0 { + t.Errorf("expected peer A to have score 0.0, got %f", aScore) + } - if cScore != 0 { - t.Errorf("expected peer C to have score 0.0, got %f", aScore) - } + if bScore != 0 { + t.Errorf("expected peer B to have score 0.0, got %f", aScore) + } - if dScore != 0 { - t.Errorf("expected peer D to have score 0.0, got %f", aScore) - } + if cScore != 0 { + t.Errorf("expected peer C to have score 0.0, got %f", aScore) + } + + if dScore != 0 { + t.Errorf("expected peer D to have score 0.0, got %f", aScore) + } + }) } func TestScoreBehaviourPenalty(t *testing.T) { - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - BehaviourPenaltyWeight: -1, - BehaviourPenaltyDecay: 0.99, - } + synctestTest(t, func(t *testing.T) { + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + BehaviourPenaltyWeight: -1, + BehaviourPenaltyDecay: 0.99, + } - peerA := peer.ID("A") + peerA := peer.ID("A") - var ps *peerScore + var ps *peerScore - // first check AddPenalty on a nil peerScore - ps.AddPenalty(peerA, 1) - aScore := ps.Score(peerA) - if aScore != 0 { - t.Errorf("expected peer score to be 0, got %f", aScore) - } + // first check AddPenalty on a nil peerScore + ps.AddPenalty(peerA, 1) + aScore := ps.Score(peerA) + if aScore != 0 { + t.Errorf("expected peer score to be 0, got %f", aScore) + } - // instantiate the peerScore - ps = newPeerScore(params, slog.Default()) + // instantiate the peerScore + ps = newPeerScore(params, slog.Default()) - // next AddPenalty on a non-existent peer - ps.AddPenalty(peerA, 1) - aScore = ps.Score(peerA) - if aScore != 0 { - t.Errorf("expected peer score to be 0, got %f", aScore) - } + // next AddPenalty on a non-existent peer + ps.AddPenalty(peerA, 1) + aScore = ps.Score(peerA) + if aScore != 0 { + t.Errorf("expected peer score to be 0, got %f", aScore) + } - // add the peer and test penalties - ps.AddPeer(peerA, "myproto") + // add the peer and test penalties + ps.AddPeer(peerA, "myproto") - aScore = ps.Score(peerA) - if aScore != 0 { - t.Errorf("expected peer score to be 0, got %f", aScore) - } + aScore = ps.Score(peerA) + if aScore != 0 { + t.Errorf("expected peer score to be 0, got %f", aScore) + } - ps.AddPenalty(peerA, 1) - aScore = ps.Score(peerA) - if aScore != -1 { - t.Errorf("expected peer score to be -1, got %f", aScore) - } + ps.AddPenalty(peerA, 1) + aScore = ps.Score(peerA) + if aScore != -1 { + t.Errorf("expected peer score to be -1, got %f", aScore) + } - ps.AddPenalty(peerA, 1) - aScore = ps.Score(peerA) - if aScore != -4 { - t.Errorf("expected peer score to be -4, got %f", aScore) - } + ps.AddPenalty(peerA, 1) + aScore = ps.Score(peerA) + if aScore != -4 { + t.Errorf("expected peer score to be -4, got %f", aScore) + } - ps.refreshScores() + ps.refreshScores() - aScore = ps.Score(peerA) - if aScore != -3.9204 { - t.Errorf("expected peer score to be -3.9204, got %f", aScore) - } + aScore = ps.Score(peerA) + if aScore != -3.9204 { + t.Errorf("expected peer score to be -3.9204, got %f", aScore) + } + }) } func TestScoreRetention(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" + synctestTest(t, func(t *testing.T) { - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return -1000 }, - AppSpecificWeight: 1.0, - Topics: make(map[string]*TopicScoreParams), - RetainScore: time.Second, - } + mytopic := "mytopic" + + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return -1000 }, + AppSpecificWeight: 1.0, + Topics: make(map[string]*TopicScoreParams), + RetainScore: time.Second, + } - peerA := peer.ID("A") + peerA := peer.ID("A") - ps := newPeerScore(params, slog.Default()) - ps.AddPeer(peerA, "myproto") - ps.Graft(peerA, mytopic) + ps := newPeerScore(params, slog.Default()) + ps.AddPeer(peerA, "myproto") + ps.Graft(peerA, mytopic) - // score should equal -1000 (app specific score) - expected := float64(-1000) - ps.refreshScores() - aScore := ps.Score(peerA) - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + // score should equal -1000 (app specific score) + expected := float64(-1000) + ps.refreshScores() + aScore := ps.Score(peerA) + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } - // disconnect & wait half of RetainScore time. should still have negative score - ps.RemovePeer(peerA) - delay := params.RetainScore / time.Duration(2) - time.Sleep(delay) - ps.refreshScores() - aScore = ps.Score(peerA) - if aScore != expected { - t.Fatalf("Score: %f. Expected %f", aScore, expected) - } + // disconnect & wait half of RetainScore time. should still have negative score + ps.RemovePeer(peerA) + delay := params.RetainScore / time.Duration(2) + time.Sleep(delay) + ps.refreshScores() + aScore = ps.Score(peerA) + if aScore != expected { + t.Fatalf("Score: %f. Expected %f", aScore, expected) + } - // wait remaining time (plus a little slop) and the score should reset to zero - time.Sleep(delay + (50 * time.Millisecond)) - ps.refreshScores() - aScore = ps.Score(peerA) - if aScore != 0 { - t.Fatalf("Score: %f. Expected 0.0", aScore) - } + // wait remaining time (plus a little slop) and the score should reset to zero + time.Sleep(delay + (50 * time.Millisecond)) + ps.refreshScores() + aScore = ps.Score(peerA) + if aScore != 0 { + t.Fatalf("Score: %f. Expected 0.0", aScore) + } + }) } func TestScoreRecapTopicParams(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } - topicScoreParams := &TopicScoreParams{ - TopicWeight: 1, + synctestTest(t, func(t *testing.T) { - MeshMessageDeliveriesWeight: -1, - MeshMessageDeliveriesActivation: time.Second, - MeshMessageDeliveriesWindow: 10 * time.Millisecond, - MeshMessageDeliveriesThreshold: 20, - MeshMessageDeliveriesCap: 100, - MeshMessageDeliveriesDecay: 1.0, // no decay for this test + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } + topicScoreParams := &TopicScoreParams{ + TopicWeight: 1, - FirstMessageDeliveriesWeight: 10, - FirstMessageDeliveriesDecay: 1.0, // no decay for this test - FirstMessageDeliveriesCap: 100, + MeshMessageDeliveriesWeight: -1, + MeshMessageDeliveriesActivation: time.Second, + MeshMessageDeliveriesWindow: 10 * time.Millisecond, + MeshMessageDeliveriesThreshold: 20, + MeshMessageDeliveriesCap: 100, + MeshMessageDeliveriesDecay: 1.0, // no decay for this test - TimeInMeshQuantum: time.Second, - } + FirstMessageDeliveriesWeight: 10, + FirstMessageDeliveriesDecay: 1.0, // no decay for this test + FirstMessageDeliveriesCap: 100, - params.Topics[mytopic] = topicScoreParams - - // peer A always delivers the message first. - // peer B delivers next (within the delivery window). - // peer C delivers outside the delivery window. - // we expect peers A and B to have a score of zero, since all other parameter weights are zero. - // Peer C should have a negative score. - peerA := peer.ID("A") - peerB := peer.ID("B") - peers := []peer.ID{peerA, peerB} - - ps := newPeerScore(params, slog.Default()) - for _, p := range peers { - ps.AddPeer(p, "myproto") - ps.Graft(p, mytopic) - } + TimeInMeshQuantum: time.Second, + } - // deliver a bunch of messages from peer A, with duplicates within the window from peer B, - nMessages := 100 - for i := 0; i < nMessages; i++ { - pbMsg := makeTestMessage(i) - pbMsg.Topic = &mytopic - msg := Message{ReceivedFrom: peerA, Message: pbMsg} - ps.ValidateMessage(&msg) - ps.DeliverMessage(&msg) + params.Topics[mytopic] = topicScoreParams + + // peer A always delivers the message first. + // peer B delivers next (within the delivery window). + // peer C delivers outside the delivery window. + // we expect peers A and B to have a score of zero, since all other parameter weights are zero. + // Peer C should have a negative score. + peerA := peer.ID("A") + peerB := peer.ID("B") + peers := []peer.ID{peerA, peerB} + + ps := newPeerScore(params, slog.Default()) + for _, p := range peers { + ps.AddPeer(p, "myproto") + ps.Graft(p, mytopic) + } - msg.ReceivedFrom = peerB - ps.DuplicateMessage(&msg) - } + // deliver a bunch of messages from peer A, with duplicates within the window from peer B, + nMessages := 100 + for i := 0; i < nMessages; i++ { + pbMsg := makeTestMessage(i) + pbMsg.Topic = &mytopic + msg := Message{ReceivedFrom: peerA, Message: pbMsg} + ps.ValidateMessage(&msg) + ps.DeliverMessage(&msg) - // check that the FirstMessageDeliveries for peerA and MeshMessageDeliveries for PeerB is - // at 100 - if ps.peerStats[peerA].topics[mytopic].firstMessageDeliveries != 100 { - t.Fatalf("expected 100 FirstMessageDeliveries for peerA, but got %f", ps.peerStats[peerA].topics[mytopic].firstMessageDeliveries) - } - // check that the MeshMessageDeliveries for peerB and MeshMessageDeliveries for PeerB is - // at 100 - if ps.peerStats[peerB].topics[mytopic].meshMessageDeliveries != 100 { - t.Fatalf("expected 100 MeshMessageDeliveries for peerB, but got %f", ps.peerStats[peerB].topics[mytopic].meshMessageDeliveries) - } + msg.ReceivedFrom = peerB + ps.DuplicateMessage(&msg) + } - // reset the topic paramaters recapping the deliveries counters - newTopicScoreParams := &TopicScoreParams{ - TopicWeight: 1, + // check that the FirstMessageDeliveries for peerA and MeshMessageDeliveries for PeerB is + // at 100 + if ps.peerStats[peerA].topics[mytopic].firstMessageDeliveries != 100 { + t.Fatalf("expected 100 FirstMessageDeliveries for peerA, but got %f", ps.peerStats[peerA].topics[mytopic].firstMessageDeliveries) + } + // check that the MeshMessageDeliveries for peerB and MeshMessageDeliveries for PeerB is + // at 100 + if ps.peerStats[peerB].topics[mytopic].meshMessageDeliveries != 100 { + t.Fatalf("expected 100 MeshMessageDeliveries for peerB, but got %f", ps.peerStats[peerB].topics[mytopic].meshMessageDeliveries) + } - MeshMessageDeliveriesWeight: -1, - MeshMessageDeliveriesActivation: time.Second, - MeshMessageDeliveriesWindow: 10 * time.Millisecond, - MeshMessageDeliveriesThreshold: 20, - MeshMessageDeliveriesCap: 50, - MeshMessageDeliveriesDecay: 1.0, // no decay for this test + // reset the topic paramaters recapping the deliveries counters + newTopicScoreParams := &TopicScoreParams{ + TopicWeight: 1, - FirstMessageDeliveriesWeight: 10, - FirstMessageDeliveriesDecay: 1.0, // no decay for this test - FirstMessageDeliveriesCap: 50, + MeshMessageDeliveriesWeight: -1, + MeshMessageDeliveriesActivation: time.Second, + MeshMessageDeliveriesWindow: 10 * time.Millisecond, + MeshMessageDeliveriesThreshold: 20, + MeshMessageDeliveriesCap: 50, + MeshMessageDeliveriesDecay: 1.0, // no decay for this test - TimeInMeshQuantum: time.Second, - } + FirstMessageDeliveriesWeight: 10, + FirstMessageDeliveriesDecay: 1.0, // no decay for this test + FirstMessageDeliveriesCap: 50, - err := ps.SetTopicScoreParams(mytopic, newTopicScoreParams) - if err != nil { - t.Fatal(err) - } + TimeInMeshQuantum: time.Second, + } - // verify that the counters got recapped - if ps.peerStats[peerA].topics[mytopic].firstMessageDeliveries != 50 { - t.Fatalf("expected 50 FirstMessageDeliveries for peerA, but got %f", ps.peerStats[peerA].topics[mytopic].firstMessageDeliveries) - } - if ps.peerStats[peerB].topics[mytopic].meshMessageDeliveries != 50 { - t.Fatalf("expected 50 MeshMessageDeliveries for peerB, but got %f", ps.peerStats[peerB].topics[mytopic].meshMessageDeliveries) - } + err := ps.SetTopicScoreParams(mytopic, newTopicScoreParams) + if err != nil { + t.Fatal(err) + } + + // verify that the counters got recapped + if ps.peerStats[peerA].topics[mytopic].firstMessageDeliveries != 50 { + t.Fatalf("expected 50 FirstMessageDeliveries for peerA, but got %f", ps.peerStats[peerA].topics[mytopic].firstMessageDeliveries) + } + if ps.peerStats[peerB].topics[mytopic].meshMessageDeliveries != 50 { + t.Fatalf("expected 50 MeshMessageDeliveries for peerB, but got %f", ps.peerStats[peerB].topics[mytopic].meshMessageDeliveries) + } + }) } func TestScoreResetTopicParams(t *testing.T) { // Create parameters with reasonable default values - mytopic := "mytopic" - params := &PeerScoreParams{ - AppSpecificScore: func(peer.ID) float64 { return 0 }, - Topics: make(map[string]*TopicScoreParams), - } - topicScoreParams := &TopicScoreParams{ - TopicWeight: 1, - TimeInMeshQuantum: time.Second, - InvalidMessageDeliveriesWeight: -1, - InvalidMessageDeliveriesDecay: 1.0, - } + synctestTest(t, func(t *testing.T) { - params.Topics[mytopic] = topicScoreParams - - // peer A always delivers the message first. - // peer B delivers next (within the delivery window). - // peer C delivers outside the delivery window. - // we expect peers A and B to have a score of zero, since all other parameter weights are zero. - // Peer C should have a negative score. - peerA := peer.ID("A") - - ps := newPeerScore(params, slog.Default()) - ps.AddPeer(peerA, "myproto") + mytopic := "mytopic" + params := &PeerScoreParams{ + AppSpecificScore: func(peer.ID) float64 { return 0 }, + Topics: make(map[string]*TopicScoreParams), + } + topicScoreParams := &TopicScoreParams{ + TopicWeight: 1, + TimeInMeshQuantum: time.Second, + InvalidMessageDeliveriesWeight: -1, + InvalidMessageDeliveriesDecay: 1.0, + } - // reject a bunch of messages - nMessages := 100 - for i := 0; i < nMessages; i++ { - pbMsg := makeTestMessage(i) - pbMsg.Topic = &mytopic - msg := Message{ReceivedFrom: peerA, Message: pbMsg} - ps.ValidateMessage(&msg) - ps.RejectMessage(&msg, RejectValidationFailed) - } + params.Topics[mytopic] = topicScoreParams + + // peer A always delivers the message first. + // peer B delivers next (within the delivery window). + // peer C delivers outside the delivery window. + // we expect peers A and B to have a score of zero, since all other parameter weights are zero. + // Peer C should have a negative score. + peerA := peer.ID("A") + + ps := newPeerScore(params, slog.Default()) + ps.AddPeer(peerA, "myproto") + + // reject a bunch of messages + nMessages := 100 + for i := 0; i < nMessages; i++ { + pbMsg := makeTestMessage(i) + pbMsg.Topic = &mytopic + msg := Message{ReceivedFrom: peerA, Message: pbMsg} + ps.ValidateMessage(&msg) + ps.RejectMessage(&msg, RejectValidationFailed) + } - // check the topic score - aScore := ps.Score(peerA) - if aScore != -10000 { - t.Fatalf("expected a -10000 score, but got %f instead", aScore) - } + // check the topic score + aScore := ps.Score(peerA) + if aScore != -10000 { + t.Fatalf("expected a -10000 score, but got %f instead", aScore) + } - // reset the topic paramaters recapping the deliveries counters - newTopicScoreParams := &TopicScoreParams{ - TopicWeight: 1, - TimeInMeshQuantum: time.Second, - InvalidMessageDeliveriesWeight: -10, - InvalidMessageDeliveriesDecay: 1.0, - } + // reset the topic paramaters recapping the deliveries counters + newTopicScoreParams := &TopicScoreParams{ + TopicWeight: 1, + TimeInMeshQuantum: time.Second, + InvalidMessageDeliveriesWeight: -10, + InvalidMessageDeliveriesDecay: 1.0, + } - err := ps.SetTopicScoreParams(mytopic, newTopicScoreParams) - if err != nil { - t.Fatal(err) - } + err := ps.SetTopicScoreParams(mytopic, newTopicScoreParams) + if err != nil { + t.Fatal(err) + } - // verify the topic score was adjusted - aScore = ps.Score(peerA) - if aScore != -100000 { - t.Fatalf("expected a -1000000 score, but got %f instead", aScore) - } + // verify the topic score was adjusted + aScore = ps.Score(peerA) + if aScore != -100000 { + t.Fatalf("expected a -1000000 score, but got %f instead", aScore) + } + }) } func withinVariance(score float64, expected float64, variance float64) bool { diff --git a/subscription_filter_test.go b/subscription_filter_test.go index 0057cdcf..f7711077 100644 --- a/subscription_filter_test.go +++ b/subscription_filter_test.go @@ -12,199 +12,205 @@ import ( ) func TestBasicSubscriptionFilter(t *testing.T) { - peerA := peer.ID("A") - - topic1 := "test1" - topic2 := "test2" - topic3 := "test3" - yes := true - subs := []*pb.RPC_SubOpts{ - { - Topicid: &topic1, - Subscribe: &yes, - }, - { - Topicid: &topic2, - Subscribe: &yes, - }, - { - Topicid: &topic3, - Subscribe: &yes, - }, - } - - filter := NewAllowlistSubscriptionFilter(topic1, topic2) - canSubscribe := filter.CanSubscribe(topic1) - if !canSubscribe { - t.Fatal("expected allowed subscription") - } - canSubscribe = filter.CanSubscribe(topic2) - if !canSubscribe { - t.Fatal("expected allowed subscription") - } - canSubscribe = filter.CanSubscribe(topic3) - if canSubscribe { - t.Fatal("expected disallowed subscription") - } - allowedSubs, err := filter.FilterIncomingSubscriptions(peerA, subs) - if err != nil { - t.Fatal(err) - } - if len(allowedSubs) != 2 { - t.Fatalf("expected 2 allowed subscriptions but got %d", len(allowedSubs)) - } - for _, sub := range allowedSubs { - if sub.GetTopicid() == topic3 { - t.Fatal("unpexted subscription to test3") - } - } - - limitFilter := WrapLimitSubscriptionFilter(filter, 2) - _, err = limitFilter.FilterIncomingSubscriptions(peerA, subs) - if err != ErrTooManySubscriptions { - t.Fatal("expected rejection because of too many subscriptions") - } - - filter = NewRegexpSubscriptionFilter(regexp.MustCompile("test[12]")) - canSubscribe = filter.CanSubscribe(topic1) - if !canSubscribe { - t.Fatal("expected allowed subscription") - } - canSubscribe = filter.CanSubscribe(topic2) - if !canSubscribe { - t.Fatal("expected allowed subscription") - } - canSubscribe = filter.CanSubscribe(topic3) - if canSubscribe { - t.Fatal("expected disallowed subscription") - } - allowedSubs, err = filter.FilterIncomingSubscriptions(peerA, subs) - if err != nil { - t.Fatal(err) - } - if len(allowedSubs) != 2 { - t.Fatalf("expected 2 allowed subscriptions but got %d", len(allowedSubs)) - } - for _, sub := range allowedSubs { - if sub.GetTopicid() == topic3 { - t.Fatal("unexpected subscription") - } - } - - limitFilter = WrapLimitSubscriptionFilter(filter, 2) - _, err = limitFilter.FilterIncomingSubscriptions(peerA, subs) - if err != ErrTooManySubscriptions { - t.Fatal("expected rejection because of too many subscriptions") - } + synctestTest(t, func(t *testing.T) { + peerA := peer.ID("A") + + topic1 := "test1" + topic2 := "test2" + topic3 := "test3" + yes := true + subs := []*pb.RPC_SubOpts{ + { + Topicid: &topic1, + Subscribe: &yes, + }, + { + Topicid: &topic2, + Subscribe: &yes, + }, + { + Topicid: &topic3, + Subscribe: &yes, + }, + } + + filter := NewAllowlistSubscriptionFilter(topic1, topic2) + canSubscribe := filter.CanSubscribe(topic1) + if !canSubscribe { + t.Fatal("expected allowed subscription") + } + canSubscribe = filter.CanSubscribe(topic2) + if !canSubscribe { + t.Fatal("expected allowed subscription") + } + canSubscribe = filter.CanSubscribe(topic3) + if canSubscribe { + t.Fatal("expected disallowed subscription") + } + allowedSubs, err := filter.FilterIncomingSubscriptions(peerA, subs) + if err != nil { + t.Fatal(err) + } + if len(allowedSubs) != 2 { + t.Fatalf("expected 2 allowed subscriptions but got %d", len(allowedSubs)) + } + for _, sub := range allowedSubs { + if sub.GetTopicid() == topic3 { + t.Fatal("unpexted subscription to test3") + } + } + + limitFilter := WrapLimitSubscriptionFilter(filter, 2) + _, err = limitFilter.FilterIncomingSubscriptions(peerA, subs) + if err != ErrTooManySubscriptions { + t.Fatal("expected rejection because of too many subscriptions") + } + + filter = NewRegexpSubscriptionFilter(regexp.MustCompile("test[12]")) + canSubscribe = filter.CanSubscribe(topic1) + if !canSubscribe { + t.Fatal("expected allowed subscription") + } + canSubscribe = filter.CanSubscribe(topic2) + if !canSubscribe { + t.Fatal("expected allowed subscription") + } + canSubscribe = filter.CanSubscribe(topic3) + if canSubscribe { + t.Fatal("expected disallowed subscription") + } + allowedSubs, err = filter.FilterIncomingSubscriptions(peerA, subs) + if err != nil { + t.Fatal(err) + } + if len(allowedSubs) != 2 { + t.Fatalf("expected 2 allowed subscriptions but got %d", len(allowedSubs)) + } + for _, sub := range allowedSubs { + if sub.GetTopicid() == topic3 { + t.Fatal("unexpected subscription") + } + } + + limitFilter = WrapLimitSubscriptionFilter(filter, 2) + _, err = limitFilter.FilterIncomingSubscriptions(peerA, subs) + if err != ErrTooManySubscriptions { + t.Fatal("expected rejection because of too many subscriptions") + } + }) } func TestSubscriptionFilterDeduplication(t *testing.T) { - peerA := peer.ID("A") - - topic1 := "test1" - topic2 := "test2" - topic3 := "test3" - yes := true - no := false - subs := []*pb.RPC_SubOpts{ - { - Topicid: &topic1, - Subscribe: &yes, - }, - { - Topicid: &topic1, - Subscribe: &yes, - }, - - { - Topicid: &topic2, - Subscribe: &yes, - }, - { - Topicid: &topic2, - Subscribe: &no, - }, - { - Topicid: &topic3, - Subscribe: &yes, - }, - } - - filter := NewAllowlistSubscriptionFilter(topic1, topic2) - allowedSubs, err := filter.FilterIncomingSubscriptions(peerA, subs) - if err != nil { - t.Fatal(err) - } - if len(allowedSubs) != 1 { - t.Fatalf("expected 2 allowed subscriptions but got %d", len(allowedSubs)) - } - for _, sub := range allowedSubs { - if sub.GetTopicid() == topic3 || sub.GetTopicid() == topic2 { - t.Fatal("unexpected subscription") - } - } + synctestTest(t, func(t *testing.T) { + peerA := peer.ID("A") + + topic1 := "test1" + topic2 := "test2" + topic3 := "test3" + yes := true + no := false + subs := []*pb.RPC_SubOpts{ + { + Topicid: &topic1, + Subscribe: &yes, + }, + { + Topicid: &topic1, + Subscribe: &yes, + }, + + { + Topicid: &topic2, + Subscribe: &yes, + }, + { + Topicid: &topic2, + Subscribe: &no, + }, + { + Topicid: &topic3, + Subscribe: &yes, + }, + } + + filter := NewAllowlistSubscriptionFilter(topic1, topic2) + allowedSubs, err := filter.FilterIncomingSubscriptions(peerA, subs) + if err != nil { + t.Fatal(err) + } + if len(allowedSubs) != 1 { + t.Fatalf("expected 2 allowed subscriptions but got %d", len(allowedSubs)) + } + for _, sub := range allowedSubs { + if sub.GetTopicid() == topic3 || sub.GetTopicid() == topic2 { + t.Fatal("unexpected subscription") + } + } + }) } func TestSubscriptionFilterRPC(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - hosts := getDefaultHosts(t, 2) - ps1 := getPubsub(ctx, hosts[0], WithSubscriptionFilter(NewAllowlistSubscriptionFilter("test1", "test2"))) - ps2 := getPubsub(ctx, hosts[1], WithSubscriptionFilter(NewAllowlistSubscriptionFilter("test2", "test3"))) - - _ = mustSubscribe(t, ps1, "test1") - _ = mustSubscribe(t, ps1, "test2") - _ = mustSubscribe(t, ps2, "test2") - _ = mustSubscribe(t, ps2, "test3") - - // check the rejection as well - _, err := ps1.Join("test3") - if err == nil { - t.Fatal("expected subscription error") - } - - connect(t, hosts[0], hosts[1]) - - time.Sleep(time.Second) - - var sub1, sub2, sub3 bool - ready := make(chan struct{}) - - ps1.eval <- func() { - _, sub1 = ps1.topics["test1"][hosts[1].ID()] - _, sub2 = ps1.topics["test2"][hosts[1].ID()] - _, sub3 = ps1.topics["test3"][hosts[1].ID()] - ready <- struct{}{} - } - <-ready - - if sub1 { - t.Fatal("expected no subscription for test1") - } - if !sub2 { - t.Fatal("expected subscription for test2") - } - if sub3 { - t.Fatal("expected no subscription for test1") - } - - ps2.eval <- func() { - _, sub1 = ps2.topics["test1"][hosts[0].ID()] - _, sub2 = ps2.topics["test2"][hosts[0].ID()] - _, sub3 = ps2.topics["test3"][hosts[0].ID()] - ready <- struct{}{} - } - <-ready - - if sub1 { - t.Fatal("expected no subscription for test1") - } - if !sub2 { - t.Fatal("expected subscription for test1") - } - if sub3 { - t.Fatal("expected no subscription for test1") - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + hosts := getDefaultHosts(t, 2) + ps1 := getPubsub(ctx, hosts[0], WithSubscriptionFilter(NewAllowlistSubscriptionFilter("test1", "test2"))) + ps2 := getPubsub(ctx, hosts[1], WithSubscriptionFilter(NewAllowlistSubscriptionFilter("test2", "test3"))) + + _ = mustSubscribe(t, ps1, "test1") + _ = mustSubscribe(t, ps1, "test2") + _ = mustSubscribe(t, ps2, "test2") + _ = mustSubscribe(t, ps2, "test3") + + // check the rejection as well + _, err := ps1.Join("test3") + if err == nil { + t.Fatal("expected subscription error") + } + + connect(t, hosts[0], hosts[1]) + + time.Sleep(time.Second) + + var sub1, sub2, sub3 bool + ready := make(chan struct{}) + + ps1.eval <- func() { + _, sub1 = ps1.topics["test1"][hosts[1].ID()] + _, sub2 = ps1.topics["test2"][hosts[1].ID()] + _, sub3 = ps1.topics["test3"][hosts[1].ID()] + ready <- struct{}{} + } + <-ready + + if sub1 { + t.Fatal("expected no subscription for test1") + } + if !sub2 { + t.Fatal("expected subscription for test2") + } + if sub3 { + t.Fatal("expected no subscription for test1") + } + + ps2.eval <- func() { + _, sub1 = ps2.topics["test1"][hosts[0].ID()] + _, sub2 = ps2.topics["test2"][hosts[0].ID()] + _, sub3 = ps2.topics["test3"][hosts[0].ID()] + ready <- struct{}{} + } + <-ready + + if sub1 { + t.Fatal("expected no subscription for test1") + } + if !sub2 { + t.Fatal("expected subscription for test1") + } + if sub3 { + t.Fatal("expected no subscription for test1") + } + }) } diff --git a/tag_tracer_test.go b/tag_tracer_test.go index 81a8c961..e0bcb3d5 100644 --- a/tag_tracer_test.go +++ b/tag_tracer_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - "github.com/benbjohnson/clock" connmgri "github.com/libp2p/go-libp2p/core/connmgr" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/p2p/net/connmgr" @@ -15,229 +14,227 @@ import ( func TestTagTracerMeshTags(t *testing.T) { // test that tags are applied when the tagTracer sees graft and prune events + synctestTest(t, func(t *testing.T) { + // test that tags are applied when the tagTracer sees graft and prune events + cmgr, err := connmgr.NewConnManager(5, 10, connmgr.WithGracePeriod(time.Minute)) + if err != nil { + t.Fatal(err) + } + defer cmgr.Close() + tt := newTagTracer(cmgr) - cmgr, err := connmgr.NewConnManager(5, 10, connmgr.WithGracePeriod(time.Minute)) - if err != nil { - t.Fatal(err) - } - tt := newTagTracer(cmgr) - - p := peer.ID("a-peer") - topic := "a-topic" + p := peer.ID("a-peer") + topic := "a-topic" - tt.Join(topic) - tt.Graft(p, topic) + tt.Join(topic) + tt.Graft(p, topic) - tag := "pubsub:" + topic - if !cmgr.IsProtected(p, tag) { - t.Fatal("expected the mesh peer to be protected") - } + tag := "pubsub:" + topic + if !cmgr.IsProtected(p, tag) { + t.Fatal("expected the mesh peer to be protected") + } - tt.Prune(p, topic) - if cmgr.IsProtected(p, tag) { - t.Fatal("expected the former mesh peer to be unprotected") - } + tt.Prune(p, topic) + if cmgr.IsProtected(p, tag) { + t.Fatal("expected the former mesh peer to be unprotected") + } + }) } func TestTagTracerDirectPeerTags(t *testing.T) { // test that we add a tag to direct peers - cmgr, err := connmgr.NewConnManager(5, 10, connmgr.WithGracePeriod(time.Minute)) - if err != nil { - t.Fatal(err) - } - tt := newTagTracer(cmgr) + synctestTest(t, func(t *testing.T) { + // test that we add a tag to direct peers + cmgr, err := connmgr.NewConnManager(5, 10, connmgr.WithGracePeriod(time.Minute)) + if err != nil { + t.Fatal(err) + } + defer cmgr.Close() + tt := newTagTracer(cmgr) - p1 := peer.ID("1") - p2 := peer.ID("2") - p3 := peer.ID("3") + p1 := peer.ID("1") + p2 := peer.ID("2") + p3 := peer.ID("3") - tt.protectDirect(p1) + tt.protectDirect(p1) - tt.AddPeer(p1, GossipSubID_v10) - tt.AddPeer(p2, GossipSubID_v10) - tt.AddPeer(p3, GossipSubID_v10) + tt.AddPeer(p1, GossipSubID_v10) + tt.AddPeer(p2, GossipSubID_v10) + tt.AddPeer(p3, GossipSubID_v10) - tag := "pubsub:" - if !cmgr.IsProtected(p1, tag) { - t.Fatal("expected direct peer to be protected") - } + tag := "pubsub:" + if !cmgr.IsProtected(p1, tag) { + t.Fatal("expected direct peer to be protected") + } - for _, p := range []peer.ID{p2, p3} { - if cmgr.IsProtected(p, tag) { - t.Fatal("expected non-direct peer to be unprotected") + for _, p := range []peer.ID{p2, p3} { + if cmgr.IsProtected(p, tag) { + t.Fatal("expected non-direct peer to be unprotected") + } } - } - tt.unprotectDirect(p1) - if cmgr.IsProtected(p1, tag) { - t.Fatal("expected direct peer to not be protected") - } + tt.unprotectDirect(p1) + if cmgr.IsProtected(p1, tag) { + t.Fatal("expected direct peer to not be protected") + } + }) } func TestTagTracerDeliveryTags(t *testing.T) { - t.Skip("flaky test temporarily disabled; TODO: fixme") - // test decaying delivery tags - - // use fake time to test the tag decay - clk := clock.NewMock() - decayCfg := &connmgr.DecayerCfg{ - Clock: clk, - Resolution: time.Minute, - } - cmgr, err := connmgr.NewConnManager(5, 10, connmgr.WithGracePeriod(time.Minute), connmgr.DecayerConfig(decayCfg)) - if err != nil { - t.Fatal(err) - } - - tt := newTagTracer(cmgr) - - topic1 := "topic-1" - topic2 := "topic-2" - - p := peer.ID("a-peer") - - tt.Join(topic1) - tt.Join(topic2) - - for i := 0; i < 20; i++ { - // deliver only 5 messages to topic 2 (less than the cap) - topic := &topic1 - if i < 5 { - topic = &topic2 + synctestTest(t, func(t *testing.T) { + // test decaying delivery tags + // Under synctest, time is deterministic so we don't need a mock clock. + cmgr, err := connmgr.NewConnManager(5, 10, connmgr.WithGracePeriod(time.Minute)) + if err != nil { + t.Fatal(err) } - msg := &Message{ - ReceivedFrom: p, - Message: &pb.Message{ - From: []byte(p), - Data: []byte("hello"), - Topic: topic, - }, + defer cmgr.Close() + + tt := newTagTracer(cmgr) + + topic1 := "topic-1" + topic2 := "topic-2" + + p := peer.ID("a-peer") + + tt.Join(topic1) + tt.Join(topic2) + + for i := 0; i < 20; i++ { + // deliver only 5 messages to topic 2 (less than the cap) + topic := &topic1 + if i < 5 { + topic = &topic2 + } + msg := &Message{ + ReceivedFrom: p, + Message: &pb.Message{ + From: []byte(p), + Data: []byte("hello"), + Topic: topic, + }, + } + tt.DeliverMessage(msg) } - tt.DeliverMessage(msg) - } - // we have to tick the fake clock once to apply the bump - clk.Add(time.Minute) + // advance time to apply the bump + time.Sleep(time.Minute) - tag1 := "pubsub-deliveries:topic-1" - tag2 := "pubsub-deliveries:topic-2" + tag1 := "pubsub-deliveries:topic-1" + tag2 := "pubsub-deliveries:topic-2" - // the tag value for topic-1 should be capped at GossipSubConnTagMessageDeliveryCap (default 15) - val := getTagValue(cmgr, p, tag1) - expected := GossipSubConnTagMessageDeliveryCap - if val != expected { - t.Errorf("expected delivery tag to be capped at %d, was %d", expected, val) - } + // the tag value for topic-1 should be capped at GossipSubConnTagMessageDeliveryCap (default 15) + val := getTagValue(cmgr, p, tag1) + expected := GossipSubConnTagMessageDeliveryCap + if val != expected { + t.Errorf("expected delivery tag to be capped at %d, was %d", expected, val) + } - // the value for topic-2 should equal the number of messages delivered (5), since it was less than the cap - val = getTagValue(cmgr, p, tag2) - expected = 5 - if val != expected { - t.Errorf("expected delivery tag value = %d, got %d", expected, val) - } + // the value for topic-2 should equal the number of messages delivered (5), since it was less than the cap + val = getTagValue(cmgr, p, tag2) + expected = 5 + if val != expected { + t.Errorf("expected delivery tag value = %d, got %d", expected, val) + } - // if we jump forward a few minutes, we should see the tags decrease by 1 / 10 minutes - clk.Add(50 * time.Minute) - time.Sleep(2 * time.Second) - - val = getTagValue(cmgr, p, tag1) - expected = GossipSubConnTagMessageDeliveryCap - 5 - // the actual expected value should be GossipSubConnTagMessageDeliveryCap - 5, - // however due to timing issues on Travis, we consistently get GossipSubConnTagMessageDeliveryCap - 4 - // there instead. So our assertion checks for the expected value +/- 1 - if val > expected+1 || val < expected-1 { - t.Errorf("expected delivery tag value = %d ± 1, got %d", expected, val) - } + // if we jump forward a few minutes, we should see the tags decrease by 1 / 10 minutes + time.Sleep(50 * time.Minute) - // the tag for topic-2 should have reset to zero by now, but again we add one for Travis since it's slow... - val = getTagValue(cmgr, p, tag2) - expected = 0 - if val > expected+1 || val < expected-1 { - t.Errorf("expected delivery tag value = %d ± 1, got %d", expected, val) - } + val = getTagValue(cmgr, p, tag1) + expected = GossipSubConnTagMessageDeliveryCap - 5 + if val > expected+1 || val < expected-1 { + t.Errorf("expected delivery tag value = %d ± 1, got %d", expected, val) + } - // leaving the topic should remove the tag - if !tagExists(cmgr, p, tag1) { - t.Errorf("expected delivery tag %s to be applied to peer %s", tag1, p) - } - tt.Leave(topic1) - // advance the real clock a bit to allow the connmgr to remove the tag async - time.Sleep(time.Second) - if tagExists(cmgr, p, tag1) { - t.Errorf("expected delivery tag %s to be removed after leaving the topic", tag1) - } + // the tag for topic-2 should have reset to zero by now + val = getTagValue(cmgr, p, tag2) + expected = 0 + if val > expected+1 || val < expected-1 { + t.Errorf("expected delivery tag value = %d ± 1, got %d", expected, val) + } + + // leaving the topic should remove the tag + if !tagExists(cmgr, p, tag1) { + t.Errorf("expected delivery tag %s to be applied to peer %s", tag1, p) + } + tt.Leave(topic1) + // advance time to allow the connmgr to remove the tag async + time.Sleep(time.Second) + if tagExists(cmgr, p, tag1) { + t.Errorf("expected delivery tag %s to be removed after leaving the topic", tag1) + } + }) } func TestTagTracerDeliveryTagsNearFirst(t *testing.T) { // use fake time to test the tag decay - clk := clock.NewMock() - decayCfg := &connmgr.DecayerCfg{ - Clock: clk, - Resolution: time.Minute, - } - cmgr, err := connmgr.NewConnManager(5, 10, connmgr.WithGracePeriod(time.Minute), connmgr.DecayerConfig(decayCfg)) - if err != nil { - t.Fatal(err) - } - - tt := newTagTracer(cmgr) - - topic := "test" - - p := peer.ID("a-peer") - p2 := peer.ID("another-peer") - p3 := peer.ID("slow-peer") - - tt.Join(topic) - - for i := 0; i < GossipSubConnTagMessageDeliveryCap+5; i++ { - msg := &Message{ - ReceivedFrom: p, - Message: &pb.Message{ - From: []byte(p), - Data: []byte(fmt.Sprintf("msg-%d", i)), - Topic: &topic, - Seqno: []byte(fmt.Sprintf("%d", i)), - }, + synctestTest(t, func(t *testing.T) { + // Under synctest, time is deterministic so we don't need a mock clock. + cmgr, err := connmgr.NewConnManager(5, 10, connmgr.WithGracePeriod(time.Minute)) + if err != nil { + t.Fatal(err) } - - // a duplicate of the message, received from p2 - dup := &Message{ - ReceivedFrom: p2, - Message: msg.Message, + defer cmgr.Close() + + tt := newTagTracer(cmgr) + + topic := "test" + + p := peer.ID("a-peer") + p2 := peer.ID("another-peer") + p3 := peer.ID("slow-peer") + + tt.Join(topic) + + for i := 0; i < GossipSubConnTagMessageDeliveryCap+5; i++ { + msg := &Message{ + ReceivedFrom: p, + Message: &pb.Message{ + From: []byte(p), + Data: []byte(fmt.Sprintf("msg-%d", i)), + Topic: &topic, + Seqno: []byte(fmt.Sprintf("%d", i)), + }, + } + + // a duplicate of the message, received from p2 + dup := &Message{ + ReceivedFrom: p2, + Message: msg.Message, + } + + // the message starts validating as soon as we receive it from p + tt.ValidateMessage(msg) + // p2 should get near-first credit for the duplicate message that arrives before + // validation is complete + tt.DuplicateMessage(dup) + // DeliverMessage gets called when validation is complete + tt.DeliverMessage(msg) + + // p3 delivers a duplicate after validation completes & gets no credit + dup.ReceivedFrom = p3 + tt.DuplicateMessage(dup) } - // the message starts validating as soon as we receive it from p - tt.ValidateMessage(msg) - // p2 should get near-first credit for the duplicate message that arrives before - // validation is complete - tt.DuplicateMessage(dup) - // DeliverMessage gets called when validation is complete - tt.DeliverMessage(msg) - - // p3 delivers a duplicate after validation completes & gets no credit - dup.ReceivedFrom = p3 - tt.DuplicateMessage(dup) - } - - clk.Add(time.Minute) + time.Sleep(time.Minute) - // both p and p2 should get delivery tags equal to the cap - tag := "pubsub-deliveries:test" - val := getTagValue(cmgr, p, tag) - if val != GossipSubConnTagMessageDeliveryCap { - t.Errorf("expected tag %s to have val %d, was %d", tag, GossipSubConnTagMessageDeliveryCap, val) - } - val = getTagValue(cmgr, p2, tag) - if val != GossipSubConnTagMessageDeliveryCap { - t.Errorf("expected tag %s for near-first peer to have val %d, was %d", tag, GossipSubConnTagMessageDeliveryCap, val) - } + // both p and p2 should get delivery tags equal to the cap + tag := "pubsub-deliveries:test" + val := getTagValue(cmgr, p, tag) + if val != GossipSubConnTagMessageDeliveryCap { + t.Errorf("expected tag %s to have val %d, was %d", tag, GossipSubConnTagMessageDeliveryCap, val) + } + val = getTagValue(cmgr, p2, tag) + if val != GossipSubConnTagMessageDeliveryCap { + t.Errorf("expected tag %s for near-first peer to have val %d, was %d", tag, GossipSubConnTagMessageDeliveryCap, val) + } - // p3 should have no delivery tag credit - val = getTagValue(cmgr, p3, tag) - if val != 0 { - t.Errorf("expected tag %s for slow peer to have val %d, was %d", tag, 0, val) - } + // p3 should have no delivery tag credit + val = getTagValue(cmgr, p3, tag) + if val != 0 { + t.Errorf("expected tag %s for slow peer to have val %d, was %d", tag, 0, val) + } + }) } func getTagValue(mgr connmgri.ConnManager, p peer.ID, tag string) int { @@ -252,7 +249,6 @@ func getTagValue(mgr connmgri.ConnManager, p peer.ID, tag string) int { return val } -//lint:ignore U1000 used only by skipped tests at present func tagExists(mgr connmgri.ConnManager, p peer.ID, tag string) bool { info := mgr.GetTagInfo(p) if info == nil { diff --git a/timecache/first_seen_cache_test.go b/timecache/first_seen_cache_test.go index 10f69c93..65698713 100644 --- a/timecache/first_seen_cache_test.go +++ b/timecache/first_seen_cache_test.go @@ -2,41 +2,66 @@ package timecache import ( "fmt" + "runtime" "testing" + "testing/synctest" "time" ) +// synctestTest wraps synctest.Test with GOMAXPROCS(1) to work around a Go +// runtime bug where concurrent bubble timer firing corrupts TSan state. +// https://github.com/golang/go/issues/78156 +func synctestTest(t *testing.T, f func(t *testing.T)) { + if raceEnabled { + prev := runtime.GOMAXPROCS(1) + t.Cleanup(func() { runtime.GOMAXPROCS(prev) }) + } + synctest.Test(t, f) +} + func TestFirstSeenCacheFound(t *testing.T) { - tc := newFirstSeenCache(time.Minute) + synctestTest(t, func(t *testing.T) { + tc := newFirstSeenCache(time.Minute) + defer tc.Done() - tc.Add("test") + tc.Add("test") - if !tc.Has("test") { - t.Fatal("should have this key") - } + if !tc.Has("test") { + t.Fatal("should have this key") + } + }) } func TestFirstSeenCacheExpire(t *testing.T) { - tc := newFirstSeenCacheWithSweepInterval(time.Second, time.Second) - for i := 0; i < 10; i++ { - tc.Add(fmt.Sprint(i)) - time.Sleep(time.Millisecond * 100) - } + synctestTest(t, func(t *testing.T) { + tc := newFirstSeenCacheWithSweepInterval(time.Second, time.Second) + defer tc.Done() - time.Sleep(2 * time.Second) - for i := 0; i < 10; i++ { - if tc.Has(fmt.Sprint(i)) { - t.Fatalf("should have dropped this key: %s from the cache already", fmt.Sprint(i)) + for i := 0; i < 10; i++ { + tc.Add(fmt.Sprint(i)) + time.Sleep(time.Millisecond * 100) } - } + + time.Sleep(2 * time.Second) + for i := 0; i < 10; i++ { + if tc.Has(fmt.Sprint(i)) { + t.Fatalf("should have dropped this key: %s from the cache already", fmt.Sprint(i)) + } + } + }) } func TestFirstSeenCacheNotFoundAfterExpire(t *testing.T) { - tc := newFirstSeenCacheWithSweepInterval(time.Second, time.Second) - tc.Add(fmt.Sprint(0)) + synctestTest(t, func(t *testing.T) { + tc := newFirstSeenCacheWithSweepInterval(time.Second, 500*time.Millisecond) + defer tc.Done() - time.Sleep(2 * time.Second) - if tc.Has(fmt.Sprint(0)) { - t.Fatal("should have dropped this from the cache already") - } + tc.Add(fmt.Sprint(0)) + + // Sleep past expiry + sweep interval to ensure the background sweep has removed the entry + time.Sleep(2 * time.Second) + if tc.Has(fmt.Sprint(0)) { + t.Fatal("should have dropped this from the cache already") + } + }) } diff --git a/timecache/last_seen_cache_test.go b/timecache/last_seen_cache_test.go index a3200937..39366a09 100644 --- a/timecache/last_seen_cache_test.go +++ b/timecache/last_seen_cache_test.go @@ -7,83 +7,97 @@ import ( ) func TestLastSeenCacheFound(t *testing.T) { - tc := newLastSeenCache(time.Minute) + synctestTest(t, func(t *testing.T) { + tc := newLastSeenCache(time.Minute) + defer tc.Done() - tc.Add("test") + tc.Add("test") - if !tc.Has("test") { - t.Fatal("should have this key") - } + if !tc.Has("test") { + t.Fatal("should have this key") + } + }) } func TestLastSeenCacheExpire(t *testing.T) { - tc := newLastSeenCacheWithSweepInterval(time.Second, time.Second) - for i := 0; i < 11; i++ { - tc.Add(fmt.Sprint(i)) - time.Sleep(time.Millisecond * 100) - } - - time.Sleep(2 * time.Second) - for i := 0; i < 11; i++ { - if tc.Has(fmt.Sprint(i)) { - t.Fatalf("should have dropped this key: %s from the cache already", fmt.Sprint(i)) + synctestTest(t, func(t *testing.T) { + tc := newLastSeenCacheWithSweepInterval(time.Second, time.Second) + defer tc.Done() + + for i := 0; i < 11; i++ { + tc.Add(fmt.Sprint(i)) + time.Sleep(time.Millisecond * 100) + } + + time.Sleep(2 * time.Second) + for i := 0; i < 11; i++ { + if tc.Has(fmt.Sprint(i)) { + t.Fatalf("should have dropped this key: %s from the cache already", fmt.Sprint(i)) + } } - } + }) } func TestLastSeenCacheSlideForward(t *testing.T) { - t.Skip("timing is too fine grained to run in CI") - - tc := newLastSeenCache(time.Second) - i := 0 - - // T0ms: Add 8 entries with a 100ms sleep after each - for i < 8 { - tc.Add(fmt.Sprint(i)) - time.Sleep(time.Millisecond * 100) - i++ - } - - // T800ms: Lookup the first entry - this should slide the entry forward so that its expiration is a full second - // later. - if !tc.Has(fmt.Sprint(0)) { - t.Fatal("should have this key") - } - - // T800ms: Wait till after the first and second entries would have normally expired (had we not looked the first - // entry up). - time.Sleep(time.Millisecond * 400) - - // T1200ms: The first entry should still be present in the cache - this will also slide the entry forward. - if !tc.Has(fmt.Sprint(0)) { - t.Fatal("should still have this key") - } - - // T1200ms: The second entry should have expired - if tc.Has(fmt.Sprint(1)) { - t.Fatal("should have dropped this from the cache already") - } - - // T1200ms: Sleep till the first entry actually expires - time.Sleep(time.Millisecond * 1100) - - // T2300ms: Now the first entry should have expired - if tc.Has(fmt.Sprint(0)) { - t.Fatal("should have dropped this from the cache already") - } - - // And it should not have been added back - if tc.Has(fmt.Sprint(0)) { - t.Fatal("should have dropped this from the cache already") - } + synctestTest(t, func(t *testing.T) { + tc := newLastSeenCacheWithSweepInterval(time.Second, 100*time.Millisecond) + defer tc.Done() + + i := 0 + + // T0ms: Add 8 entries with a 100ms sleep after each + for i < 8 { + tc.Add(fmt.Sprint(i)) + time.Sleep(time.Millisecond * 100) + i++ + } + + // T800ms: Lookup the first entry - this should slide the entry forward so that its expiration is a full second + // later. + if !tc.Has(fmt.Sprint(0)) { + t.Fatal("should have this key") + } + + // T800ms: Wait till after the first and second entries would have normally expired (had we not looked the first + // entry up). Sleep a bit extra so the background sweep has time to remove expired entries. + time.Sleep(time.Millisecond * 410) + + // ~T1210ms: The first entry should still be present in the cache - this will also slide the entry forward. + if !tc.Has(fmt.Sprint(0)) { + t.Fatal("should still have this key") + } + + // ~T1210ms: The second entry should have expired (added at T100ms, expired at T1100ms, sweep removed it) + if tc.Has(fmt.Sprint(1)) { + t.Fatal("should have dropped this from the cache already") + } + + // Sleep till the first entry actually expires (slid to ~T1210ms + 1s = T2210ms) + time.Sleep(time.Millisecond * 1110) + + // Now the first entry should have expired + if tc.Has(fmt.Sprint(0)) { + t.Fatal("should have dropped this from the cache already") + } + + // And it should not have been added back + if tc.Has(fmt.Sprint(0)) { + t.Fatal("should have dropped this from the cache already") + } + }) } func TestLastSeenCacheNotFoundAfterExpire(t *testing.T) { - tc := newLastSeenCacheWithSweepInterval(time.Second, time.Second) - tc.Add(fmt.Sprint(0)) + synctestTest(t, func(t *testing.T) { + tc := newLastSeenCacheWithSweepInterval(time.Second, 500*time.Millisecond) + defer tc.Done() - time.Sleep(2 * time.Second) - if tc.Has(fmt.Sprint(0)) { - t.Fatal("should have dropped this from the cache already") - } + tc.Add(fmt.Sprint(0)) + + // Sleep past expiry + sweep interval to ensure the background sweep has removed the entry + time.Sleep(2 * time.Second) + if tc.Has(fmt.Sprint(0)) { + t.Fatal("should have dropped this from the cache already") + } + }) } diff --git a/timecache/norace_test.go b/timecache/norace_test.go new file mode 100644 index 00000000..029bf49a --- /dev/null +++ b/timecache/norace_test.go @@ -0,0 +1,5 @@ +//go:build !race + +package timecache + +const raceEnabled = false diff --git a/timecache/race_test.go b/timecache/race_test.go new file mode 100644 index 00000000..615ee03e --- /dev/null +++ b/timecache/race_test.go @@ -0,0 +1,5 @@ +//go:build race + +package timecache + +const raceEnabled = true diff --git a/topic_test.go b/topic_test.go index aa96cf5b..f533d013 100644 --- a/topic_test.go +++ b/topic_test.go @@ -46,51 +46,57 @@ func getTopicEvts(topics []*Topic, opts ...TopicEventHandlerOpt) []*TopicEventHa } func TestTopicCloseWithOpenSubscription(t *testing.T) { - var sub *Subscription - var err error - testTopicCloseWithOpenResource(t, - func(topic *Topic) { - sub, err = topic.Subscribe() - if err != nil { - t.Fatal(err) - } - }, - func() { - sub.Cancel() - }, - ) + synctestTest(t, func(t *testing.T) { + var sub *Subscription + var err error + testTopicCloseWithOpenResource(t, + func(topic *Topic) { + sub, err = topic.Subscribe() + if err != nil { + t.Fatal(err) + } + }, + func() { + sub.Cancel() + }, + ) + }) } func TestTopicCloseWithOpenEventHandler(t *testing.T) { - var evts *TopicEventHandler - var err error - testTopicCloseWithOpenResource(t, - func(topic *Topic) { - evts, err = topic.EventHandler() - if err != nil { - t.Fatal(err) - } - }, - func() { - evts.Cancel() - }, - ) + synctestTest(t, func(t *testing.T) { + var evts *TopicEventHandler + var err error + testTopicCloseWithOpenResource(t, + func(topic *Topic) { + evts, err = topic.EventHandler() + if err != nil { + t.Fatal(err) + } + }, + func() { + evts.Cancel() + }, + ) + }) } func TestTopicCloseWithOpenRelay(t *testing.T) { - var relayCancel RelayCancelFunc - var err error - testTopicCloseWithOpenResource(t, - func(topic *Topic) { - relayCancel, err = topic.Relay() - if err != nil { - t.Fatal(err) - } - }, - func() { - relayCancel() - }, - ) + synctestTest(t, func(t *testing.T) { + var relayCancel RelayCancelFunc + var err error + testTopicCloseWithOpenResource(t, + func(topic *Topic) { + relayCancel, err = topic.Relay() + if err != nil { + t.Fatal(err) + } + }, + func() { + relayCancel() + }, + ) + }) } func testTopicCloseWithOpenResource(t *testing.T, openResource func(topic *Topic), closeResource func()) { @@ -134,588 +140,607 @@ func testTopicCloseWithOpenResource(t *testing.T, openResource func(topic *Topic } func TestTopicReuse(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - const numHosts = 2 - topicID := "foobar" - hosts := getDefaultHosts(t, numHosts) - - sender := getPubsub(ctx, hosts[0], WithDiscovery(&dummyDiscovery{})) - receiver := getPubsub(ctx, hosts[1]) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - connectAll(t, hosts) + const numHosts = 2 + topicID := "foobar" + hosts := getDefaultHosts(t, numHosts) - // Sender creates topic - sendTopic, err := sender.Join(topicID) - if err != nil { - t.Fatal(err) - } + sender := getPubsub(ctx, hosts[0], WithDiscovery(&dummyDiscovery{})) + receiver := getPubsub(ctx, hosts[1]) - // Receiver creates and subscribes to the topic - receiveTopic, err := receiver.Join(topicID) - if err != nil { - t.Fatal(err) - } + connectAll(t, hosts) - sub, err := receiveTopic.Subscribe() - if err != nil { - t.Fatal(err) - } + // Sender creates topic + sendTopic, err := sender.Join(topicID) + if err != nil { + t.Fatal(err) + } - firstMsg := []byte("1") - if err := sendTopic.Publish(ctx, firstMsg, WithReadiness(MinTopicSize(1))); err != nil { - t.Fatal(err) - } + // Receiver creates and subscribes to the topic + receiveTopic, err := receiver.Join(topicID) + if err != nil { + t.Fatal(err) + } - msg, err := sub.Next(ctx) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(msg.GetData(), firstMsg) { - t.Fatal("received incorrect message") - } + sub, err := receiveTopic.Subscribe() + if err != nil { + t.Fatal(err) + } - if err := sendTopic.Close(); err != nil { - t.Fatal(err) - } + firstMsg := []byte("1") + if err := sendTopic.Publish(ctx, firstMsg, WithReadiness(MinTopicSize(1))); err != nil { + t.Fatal(err) + } - // Recreate the same topic - newSendTopic, err := sender.Join(topicID) - if err != nil { - t.Fatal(err) - } + msg, err := sub.Next(ctx) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(msg.GetData(), firstMsg) { + t.Fatal("received incorrect message") + } - // Try sending data with original topic - illegalSend := []byte("illegal") - if err := sendTopic.Publish(ctx, illegalSend); err != ErrTopicClosed { - t.Fatal(err) - } + if err := sendTopic.Close(); err != nil { + t.Fatal(err) + } - timeoutCtx, timeoutCancel := context.WithTimeout(ctx, time.Second*2) - defer timeoutCancel() - msg, err = sub.Next(timeoutCtx) - if err != context.DeadlineExceeded { + // Recreate the same topic + newSendTopic, err := sender.Join(topicID) if err != nil { t.Fatal(err) } - if !bytes.Equal(msg.GetData(), illegalSend) { - t.Fatal("received incorrect message from illegal topic") + + // Try sending data with original topic + illegalSend := []byte("illegal") + if err := sendTopic.Publish(ctx, illegalSend); err != ErrTopicClosed { + t.Fatal(err) } - t.Fatal("received message sent by illegal topic") - } - timeoutCancel() - // Try cancelling the new topic by using the original topic - if err := sendTopic.Close(); err != nil { - t.Fatal(err) - } + timeoutCtx, timeoutCancel := context.WithTimeout(ctx, time.Second*2) + defer timeoutCancel() + msg, err = sub.Next(timeoutCtx) + if err != context.DeadlineExceeded { + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(msg.GetData(), illegalSend) { + t.Fatal("received incorrect message from illegal topic") + } + t.Fatal("received message sent by illegal topic") + } + timeoutCancel() - secondMsg := []byte("2") - if err := newSendTopic.Publish(ctx, secondMsg); err != nil { - t.Fatal(err) - } + // Try cancelling the new topic by using the original topic + if err := sendTopic.Close(); err != nil { + t.Fatal(err) + } - timeoutCtx, timeoutCancel = context.WithTimeout(ctx, time.Second*2) - defer timeoutCancel() - msg, err = sub.Next(timeoutCtx) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(msg.GetData(), secondMsg) { - t.Fatal("received incorrect message") - } + secondMsg := []byte("2") + if err := newSendTopic.Publish(ctx, secondMsg); err != nil { + t.Fatal(err) + } + + timeoutCtx, timeoutCancel = context.WithTimeout(ctx, time.Second*2) + defer timeoutCancel() + msg, err = sub.Next(timeoutCtx) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(msg.GetData(), secondMsg) { + t.Fatal("received incorrect message") + } + }) } func TestTopicEventHandlerCancel(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - const numHosts = 5 - topicID := "foobar" - hosts := getDefaultHosts(t, numHosts) - ps := getPubsub(ctx, hosts[0]) + const numHosts = 5 + topicID := "foobar" + hosts := getDefaultHosts(t, numHosts) + ps := getPubsub(ctx, hosts[0]) - // Try create and cancel topic - topic, err := ps.Join(topicID) - if err != nil { - t.Fatal(err) - } + // Try create and cancel topic + topic, err := ps.Join(topicID) + if err != nil { + t.Fatal(err) + } - evts, err := topic.EventHandler() - if err != nil { - t.Fatal(err) - } - evts.Cancel() - timeoutCtx, timeoutCancel := context.WithTimeout(ctx, time.Second*2) - defer timeoutCancel() - connectAll(t, hosts) - _, err = evts.NextPeerEvent(timeoutCtx) - if err != context.DeadlineExceeded { + evts, err := topic.EventHandler() if err != nil { t.Fatal(err) } - t.Fatal("received event after cancel") - } + evts.Cancel() + timeoutCtx, timeoutCancel := context.WithTimeout(ctx, time.Second*2) + defer timeoutCancel() + connectAll(t, hosts) + _, err = evts.NextPeerEvent(timeoutCtx) + if err != context.DeadlineExceeded { + if err != nil { + t.Fatal(err) + } + t.Fatal("received event after cancel") + } + }) } func TestSubscriptionJoinNotification(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - const numLateSubscribers = 10 - const numHosts = 20 - hosts := getDefaultHosts(t, numHosts) - topics := getTopics(getPubsubs(ctx, hosts), "foobar") - evts := getTopicEvts(topics) + const numLateSubscribers = 10 + const numHosts = 20 + hosts := getDefaultHosts(t, numHosts) + topics := getTopics(getPubsubs(ctx, hosts), "foobar") + evts := getTopicEvts(topics) - subs := make([]*Subscription, numHosts) - topicPeersFound := make([]map[peer.ID]struct{}, numHosts) + subs := make([]*Subscription, numHosts) + topicPeersFound := make([]map[peer.ID]struct{}, numHosts) - // Have some peers subscribe earlier than other peers. - // This exercises whether we get subscription notifications from - // existing peers. - for i, topic := range topics[numLateSubscribers:] { - subch, err := topic.Subscribe() - if err != nil { - t.Fatal(err) + // Have some peers subscribe earlier than other peers. + // This exercises whether we get subscription notifications from + // existing peers. + for i, topic := range topics[numLateSubscribers:] { + subch, err := topic.Subscribe() + if err != nil { + t.Fatal(err) + } + + subs[i] = subch } - subs[i] = subch - } + connectAll(t, hosts) - connectAll(t, hosts) + time.Sleep(time.Millisecond * 100) - time.Sleep(time.Millisecond * 100) + // Have the rest subscribe + for i, topic := range topics[:numLateSubscribers] { + subch, err := topic.Subscribe() + if err != nil { + t.Fatal(err) + } - // Have the rest subscribe - for i, topic := range topics[:numLateSubscribers] { - subch, err := topic.Subscribe() - if err != nil { - t.Fatal(err) + subs[i+numLateSubscribers] = subch } - subs[i+numLateSubscribers] = subch - } - - wg := sync.WaitGroup{} - for i := 0; i < numHosts; i++ { - peersFound := make(map[peer.ID]struct{}) - topicPeersFound[i] = peersFound - evt := evts[i] - wg.Add(1) - go func(peersFound map[peer.ID]struct{}) { - defer wg.Done() - for len(peersFound) < numHosts-1 { - event, err := evt.NextPeerEvent(ctx) - if err != nil { - panic(err) + wg := sync.WaitGroup{} + for i := 0; i < numHosts; i++ { + peersFound := make(map[peer.ID]struct{}) + topicPeersFound[i] = peersFound + evt := evts[i] + wg.Add(1) + go func(peersFound map[peer.ID]struct{}) { + defer wg.Done() + for len(peersFound) < numHosts-1 { + event, err := evt.NextPeerEvent(ctx) + if err != nil { + panic(err) + } + if event.Type == PeerJoin { + peersFound[event.Peer] = struct{}{} + } } - if event.Type == PeerJoin { - peersFound[event.Peer] = struct{}{} - } - } - }(peersFound) - } + }(peersFound) + } - wg.Wait() - for _, peersFound := range topicPeersFound { - if len(peersFound) != numHosts-1 { - t.Fatal("incorrect number of peers found") + wg.Wait() + for _, peersFound := range topicPeersFound { + if len(peersFound) != numHosts-1 { + t.Fatal("incorrect number of peers found") + } } - } + }) } func TestSubscriptionLeaveNotification(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - const numHosts = 20 - hosts := getDefaultHosts(t, numHosts) - psubs := getPubsubs(ctx, hosts) - topics := getTopics(psubs, "foobar") - evts := getTopicEvts(topics) + const numHosts = 20 + hosts := getDefaultHosts(t, numHosts) + psubs := getPubsubs(ctx, hosts) + topics := getTopics(psubs, "foobar") + evts := getTopicEvts(topics) - subs := make([]*Subscription, numHosts) - topicPeersFound := make([]map[peer.ID]struct{}, numHosts) + subs := make([]*Subscription, numHosts) + topicPeersFound := make([]map[peer.ID]struct{}, numHosts) - // Subscribe all peers and wait until they've all been found - for i, topic := range topics { - subch, err := topic.Subscribe() - if err != nil { - t.Fatal(err) - } + // Subscribe all peers and wait until they've all been found + for i, topic := range topics { + subch, err := topic.Subscribe() + if err != nil { + t.Fatal(err) + } - subs[i] = subch - } + subs[i] = subch + } - connectAll(t, hosts) + connectAll(t, hosts) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - wg := sync.WaitGroup{} - for i := 0; i < numHosts; i++ { - peersFound := make(map[peer.ID]struct{}) - topicPeersFound[i] = peersFound - evt := evts[i] - wg.Add(1) - go func(peersFound map[peer.ID]struct{}) { - defer wg.Done() - for len(peersFound) < numHosts-1 { - event, err := evt.NextPeerEvent(ctx) - if err != nil { - panic(err) - } - if event.Type == PeerJoin { - peersFound[event.Peer] = struct{}{} + wg := sync.WaitGroup{} + for i := 0; i < numHosts; i++ { + peersFound := make(map[peer.ID]struct{}) + topicPeersFound[i] = peersFound + evt := evts[i] + wg.Add(1) + go func(peersFound map[peer.ID]struct{}) { + defer wg.Done() + for len(peersFound) < numHosts-1 { + event, err := evt.NextPeerEvent(ctx) + if err != nil { + panic(err) + } + if event.Type == PeerJoin { + peersFound[event.Peer] = struct{}{} + } } - } - }(peersFound) - } + }(peersFound) + } - wg.Wait() - for _, peersFound := range topicPeersFound { - if len(peersFound) != numHosts-1 { - t.Fatal("incorrect number of peers found") + wg.Wait() + for _, peersFound := range topicPeersFound { + if len(peersFound) != numHosts-1 { + t.Fatal("incorrect number of peers found") + } } - } - // Test removing peers and verifying that they cause events - subs[1].Cancel() - _ = hosts[2].Close() - psubs[0].BlacklistPeer(hosts[3].ID()) + // Test removing peers and verifying that they cause events + subs[1].Cancel() + _ = hosts[2].Close() + psubs[0].BlacklistPeer(hosts[3].ID()) - leavingPeers := make(map[peer.ID]struct{}) - for len(leavingPeers) < 3 { - event, err := evts[0].NextPeerEvent(ctx) - if err != nil { - t.Fatal(err) - } - if event.Type == PeerLeave { - leavingPeers[event.Peer] = struct{}{} + leavingPeers := make(map[peer.ID]struct{}) + for len(leavingPeers) < 3 { + event, err := evts[0].NextPeerEvent(ctx) + if err != nil { + t.Fatal(err) + } + if event.Type == PeerLeave { + leavingPeers[event.Peer] = struct{}{} + } } - } - if _, ok := leavingPeers[hosts[1].ID()]; !ok { - t.Fatal(fmt.Errorf("canceling subscription did not cause a leave event")) - } - if _, ok := leavingPeers[hosts[2].ID()]; !ok { - t.Fatal(fmt.Errorf("closing host did not cause a leave event")) - } - if _, ok := leavingPeers[hosts[3].ID()]; !ok { - t.Fatal(fmt.Errorf("blacklisting peer did not cause a leave event")) - } + if _, ok := leavingPeers[hosts[1].ID()]; !ok { + t.Fatal(fmt.Errorf("canceling subscription did not cause a leave event")) + } + if _, ok := leavingPeers[hosts[2].ID()]; !ok { + t.Fatal(fmt.Errorf("closing host did not cause a leave event")) + } + if _, ok := leavingPeers[hosts[3].ID()]; !ok { + t.Fatal(fmt.Errorf("blacklisting peer did not cause a leave event")) + } + }) } func TestSubscriptionManyNotifications(t *testing.T) { - t.Skip("flaky test disabled") + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + const topic = "foobar" - const topic = "foobar" + const numHosts = 33 + hosts := getDefaultHosts(t, numHosts) + topics := getTopics(getPubsubs(ctx, hosts), topic) + evts := getTopicEvts(topics) - const numHosts = 33 - hosts := getDefaultHosts(t, numHosts) - topics := getTopics(getPubsubs(ctx, hosts), topic) - evts := getTopicEvts(topics) + subs := make([]*Subscription, numHosts) + topicPeersFound := make([]map[peer.ID]struct{}, numHosts) - subs := make([]*Subscription, numHosts) - topicPeersFound := make([]map[peer.ID]struct{}, numHosts) + // Subscribe all peers except one and wait until they've all been found + for i := 1; i < numHosts; i++ { + subch, err := topics[i].Subscribe() + if err != nil { + t.Fatal(err) + } - // Subscribe all peers except one and wait until they've all been found - for i := 1; i < numHosts; i++ { - subch, err := topics[i].Subscribe() - if err != nil { - t.Fatal(err) + subs[i] = subch } - subs[i] = subch - } - - connectAll(t, hosts) + connectAll(t, hosts) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - wg := sync.WaitGroup{} - for i := 1; i < numHosts; i++ { - peersFound := make(map[peer.ID]struct{}) - topicPeersFound[i] = peersFound - evt := evts[i] - wg.Add(1) - go func(peersFound map[peer.ID]struct{}) { - defer wg.Done() - for len(peersFound) < numHosts-2 { - event, err := evt.NextPeerEvent(ctx) - if err != nil { - panic(err) + wg := sync.WaitGroup{} + for i := 1; i < numHosts; i++ { + peersFound := make(map[peer.ID]struct{}) + topicPeersFound[i] = peersFound + evt := evts[i] + wg.Add(1) + go func(peersFound map[peer.ID]struct{}) { + defer wg.Done() + for len(peersFound) < numHosts-2 { + event, err := evt.NextPeerEvent(ctx) + if err != nil { + panic(err) + } + if event.Type == PeerJoin { + peersFound[event.Peer] = struct{}{} + } } - if event.Type == PeerJoin { - peersFound[event.Peer] = struct{}{} - } - } - }(peersFound) - } + }(peersFound) + } - wg.Wait() - for _, peersFound := range topicPeersFound[1:] { - if len(peersFound) != numHosts-2 { - t.Fatalf("found %d peers, expected %d", len(peersFound), numHosts-2) + wg.Wait() + for _, peersFound := range topicPeersFound[1:] { + if len(peersFound) != numHosts-2 { + t.Fatalf("found %d peers, expected %d", len(peersFound), numHosts-2) + } } - } - // Wait for remaining peer to find other peers - remPeerTopic, remPeerEvts := topics[0], evts[0] - for len(remPeerTopic.ListPeers()) < numHosts-1 { - time.Sleep(time.Millisecond * 100) - } + // Wait for remaining peer to find other peers + remPeerTopic, remPeerEvts := topics[0], evts[0] + for len(remPeerTopic.ListPeers()) < numHosts-1 { + time.Sleep(time.Millisecond * 100) + } - // Subscribe the remaining peer and check that all the events came through - sub, err := remPeerTopic.Subscribe() - if err != nil { - t.Fatal(err) - } + // Subscribe the remaining peer and check that all the events came through + sub, err := remPeerTopic.Subscribe() + if err != nil { + t.Fatal(err) + } - subs[0] = sub + subs[0] = sub - peerState := readAllQueuedEvents(ctx, t, remPeerEvts) + peerState := readAllQueuedEvents(ctx, t, remPeerEvts) - if len(peerState) != numHosts-1 { - t.Fatal("incorrect number of peers found") - } + if len(peerState) != numHosts-1 { + t.Fatal("incorrect number of peers found") + } - for _, e := range peerState { - if e != PeerJoin { - t.Fatal("non Join event occurred") + for _, e := range peerState { + if e != PeerJoin { + t.Fatal("non Join event occurred") + } } - } - // Unsubscribe all peers except one and check that all the events came through - for i := 1; i < numHosts; i++ { - subs[i].Cancel() - } + // Unsubscribe all peers except one and check that all the events came through + for i := 1; i < numHosts; i++ { + subs[i].Cancel() + } - // Wait for remaining peer to disconnect from the other peers - for len(topics[0].ListPeers()) != 0 { - time.Sleep(time.Millisecond * 100) - } + // Wait for remaining peer to disconnect from the other peers + for len(topics[0].ListPeers()) != 0 { + time.Sleep(time.Millisecond * 100) + } - peerState = readAllQueuedEvents(ctx, t, remPeerEvts) + peerState = readAllQueuedEvents(ctx, t, remPeerEvts) - if len(peerState) != numHosts-1 { - t.Fatal("incorrect number of peers found") - } + if len(peerState) != numHosts-1 { + t.Fatal("incorrect number of peers found") + } - for _, e := range peerState { - if e != PeerLeave { - t.Fatal("non Leave event occurred") + for _, e := range peerState { + if e != PeerLeave { + t.Fatal("non Leave event occurred") + } } - } + }) } func TestSubscriptionNotificationSubUnSub(t *testing.T) { // Resubscribe and Unsubscribe a peers and check the state for consistency - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - const topic = "foobar" + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - const numHosts = 35 - hosts := getDefaultHosts(t, numHosts) - topics := getTopics(getPubsubs(ctx, hosts), topic) + const topic = "foobar" - for i := 1; i < numHosts; i++ { - connect(t, hosts[0], hosts[i]) - } - time.Sleep(time.Millisecond * 100) + const numHosts = 35 + hosts := getDefaultHosts(t, numHosts) + topics := getTopics(getPubsubs(ctx, hosts), topic) - notifSubThenUnSub(ctx, t, topics) + for i := 1; i < numHosts; i++ { + connect(t, hosts[0], hosts[i]) + } + time.Sleep(time.Millisecond * 100) + + notifSubThenUnSub(ctx, t, topics) + }) } func TestTopicRelay(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() - const topic = "foobar" - const numHosts = 5 + const topic = "foobar" + const numHosts = 5 - hosts := getDefaultHosts(t, numHosts) - topics := getTopics(getPubsubs(ctx, hosts), topic) + hosts := getDefaultHosts(t, numHosts) + topics := getTopics(getPubsubs(ctx, hosts), topic) - // [0.Rel] - [1.Rel] - [2.Sub] - // | - // [3.Rel] - [4.Sub] + // [0.Rel] - [1.Rel] - [2.Sub] + // | + // [3.Rel] - [4.Sub] - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) - connect(t, hosts[1], hosts[3]) - connect(t, hosts[3], hosts[4]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) + connect(t, hosts[1], hosts[3]) + connect(t, hosts[3], hosts[4]) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - var subs []*Subscription + var subs []*Subscription - for i, topic := range topics { - if i == 2 || i == 4 { - sub, err := topic.Subscribe() - if err != nil { - t.Fatal(err) - } + for i, topic := range topics { + if i == 2 || i == 4 { + sub, err := topic.Subscribe() + if err != nil { + t.Fatal(err) + } - subs = append(subs, sub) - } else { - _, err := topic.Relay() - if err != nil { - t.Fatal(err) + subs = append(subs, sub) + } else { + _, err := topic.Relay() + if err != nil { + t.Fatal(err) + } } } - } - - time.Sleep(time.Millisecond * 100) - for i := 0; i < 100; i++ { - msg := []byte("message") + time.Sleep(time.Millisecond * 100) - owner := rand.Intn(len(topics)) + for i := 0; i < 100; i++ { + msg := []byte("message") - err := topics[owner].Publish(ctx, msg) - if err != nil { - t.Fatal(err) - } + owner := rand.Intn(len(topics)) - for _, sub := range subs { - received, err := sub.Next(ctx) + err := topics[owner].Publish(ctx, msg) if err != nil { t.Fatal(err) } - if !bytes.Equal(msg, received.Data) { - t.Fatal("received message is other than expected") + for _, sub := range subs { + received, err := sub.Next(ctx) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(msg, received.Data) { + t.Fatal("received message is other than expected") + } } } - } + }) } func TestTopicRelayReuse(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - const topic = "foobar" - const numHosts = 1 + const topic = "foobar" + const numHosts = 1 - hosts := getDefaultHosts(t, numHosts) - pubsubs := getPubsubs(ctx, hosts) - topics := getTopics(pubsubs, topic) + hosts := getDefaultHosts(t, numHosts) + pubsubs := getPubsubs(ctx, hosts) + topics := getTopics(pubsubs, topic) - relay1Cancel, err := topics[0].Relay() - if err != nil { - t.Fatal(err) - } + relay1Cancel, err := topics[0].Relay() + if err != nil { + t.Fatal(err) + } - relay2Cancel, err := topics[0].Relay() - if err != nil { - t.Fatal(err) - } + relay2Cancel, err := topics[0].Relay() + if err != nil { + t.Fatal(err) + } - relay3Cancel, err := topics[0].Relay() - if err != nil { - t.Fatal(err) - } + relay3Cancel, err := topics[0].Relay() + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - res := make(chan bool, 1) - pubsubs[0].eval <- func() { - res <- pubsubs[0].myRelays[topic] == 3 - } + res := make(chan bool, 1) + pubsubs[0].eval <- func() { + res <- pubsubs[0].myRelays[topic] == 3 + } - isCorrectNumber := <-res - if !isCorrectNumber { - t.Fatal("incorrect number of relays") - } + isCorrectNumber := <-res + if !isCorrectNumber { + t.Fatal("incorrect number of relays") + } - // only the first invocation should take effect - relay1Cancel() - relay1Cancel() - relay1Cancel() + // only the first invocation should take effect + relay1Cancel() + relay1Cancel() + relay1Cancel() - pubsubs[0].eval <- func() { - res <- pubsubs[0].myRelays[topic] == 2 - } + pubsubs[0].eval <- func() { + res <- pubsubs[0].myRelays[topic] == 2 + } - isCorrectNumber = <-res - if !isCorrectNumber { - t.Fatal("incorrect number of relays") - } + isCorrectNumber = <-res + if !isCorrectNumber { + t.Fatal("incorrect number of relays") + } - relay2Cancel() - relay3Cancel() + relay2Cancel() + relay3Cancel() - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 100) - pubsubs[0].eval <- func() { - res <- pubsubs[0].myRelays[topic] == 0 - } + pubsubs[0].eval <- func() { + res <- pubsubs[0].myRelays[topic] == 0 + } - isCorrectNumber = <-res - if !isCorrectNumber { - t.Fatal("incorrect number of relays") - } + isCorrectNumber = <-res + if !isCorrectNumber { + t.Fatal("incorrect number of relays") + } + }) } func TestTopicRelayOnClosedTopic(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - const topic = "foobar" - const numHosts = 1 + const topic = "foobar" + const numHosts = 1 - hosts := getDefaultHosts(t, numHosts) - topics := getTopics(getPubsubs(ctx, hosts), topic) + hosts := getDefaultHosts(t, numHosts) + topics := getTopics(getPubsubs(ctx, hosts), topic) - err := topics[0].Close() - if err != nil { - t.Fatal(err) - } + err := topics[0].Close() + if err != nil { + t.Fatal(err) + } - _, err = topics[0].Relay() - if err == nil { - t.Fatalf("error should be returned") - } + _, err = topics[0].Relay() + if err == nil { + t.Fatalf("error should be returned") + } + }) } func TestProducePanic(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - const numHosts = 5 - topicID := "foobar" - hosts := getDefaultHosts(t, numHosts) - ps := getPubsub(ctx, hosts[0]) + const numHosts = 5 + topicID := "foobar" + hosts := getDefaultHosts(t, numHosts) + ps := getPubsub(ctx, hosts[0]) - // Create topic - topic, err := ps.Join(topicID) - if err != nil { - t.Fatal(err) - } + // Create topic + topic, err := ps.Join(topicID) + if err != nil { + t.Fatal(err) + } - // Create subscription we're going to cancel - s, err := topic.Subscribe() - if err != nil { - t.Fatal(err) - } - // Create second subscription to keep us alive on the subscription map - // after the first one is canceled - s2, err := topic.Subscribe() - if err != nil { - t.Fatal(err) - } - _ = s2 + // Create subscription we're going to cancel + s, err := topic.Subscribe() + if err != nil { + t.Fatal(err) + } + // Create second subscription to keep us alive on the subscription map + // after the first one is canceled + s2, err := topic.Subscribe() + if err != nil { + t.Fatal(err) + } + _ = s2 - s.Cancel() - time.Sleep(time.Second) - s.Cancel() - time.Sleep(time.Second) + s.Cancel() + time.Sleep(time.Second) + s.Cancel() + time.Sleep(time.Second) + }) } func notifSubThenUnSub(ctx context.Context, t *testing.T, topics []*Topic) { @@ -787,164 +812,167 @@ func readAllQueuedEvents(ctx context.Context, t *testing.T, evt *TopicEventHandl } func TestMinTopicSizeNoDiscovery(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() - const numHosts = 3 - topicID := "foobar" - hosts := getDefaultHosts(t, numHosts) + const numHosts = 3 + topicID := "foobar" + hosts := getDefaultHosts(t, numHosts) - sender := getPubsub(ctx, hosts[0]) - receiver1 := getPubsub(ctx, hosts[1]) - receiver2 := getPubsub(ctx, hosts[2]) + sender := getPubsub(ctx, hosts[0]) + receiver1 := getPubsub(ctx, hosts[1]) + receiver2 := getPubsub(ctx, hosts[2]) - connectAll(t, hosts) + connectAll(t, hosts) - // Sender creates topic - sendTopic, err := sender.Join(topicID) - if err != nil { - t.Fatal(err) - } + // Sender creates topic + sendTopic, err := sender.Join(topicID) + if err != nil { + t.Fatal(err) + } - // Receiver creates and subscribes to the topic - receiveTopic1, err := receiver1.Join(topicID) - if err != nil { - t.Fatal(err) - } + // Receiver creates and subscribes to the topic + receiveTopic1, err := receiver1.Join(topicID) + if err != nil { + t.Fatal(err) + } - sub1, err := receiveTopic1.Subscribe() - if err != nil { - t.Fatal(err) - } + sub1, err := receiveTopic1.Subscribe() + if err != nil { + t.Fatal(err) + } - oneMsg := []byte("minimum one") - if err := sendTopic.Publish(ctx, oneMsg, WithReadiness(MinTopicSize(1))); err != nil { - t.Fatal(err) - } + oneMsg := []byte("minimum one") + if err := sendTopic.Publish(ctx, oneMsg, WithReadiness(MinTopicSize(1))); err != nil { + t.Fatal(err) + } - if msg, err := sub1.Next(ctx); err != nil { - t.Fatal(err) - } else if !bytes.Equal(msg.GetData(), oneMsg) { - t.Fatal("received incorrect message") - } + if msg, err := sub1.Next(ctx); err != nil { + t.Fatal(err) + } else if !bytes.Equal(msg.GetData(), oneMsg) { + t.Fatal("received incorrect message") + } - twoMsg := []byte("minimum two") + twoMsg := []byte("minimum two") - // Attempting to publish with a minimum topic size of two should fail. - { - ctx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - if err := sendTopic.Publish(ctx, twoMsg, WithReadiness(MinTopicSize(2))); !errors.Is(err, context.DeadlineExceeded) { + // Attempting to publish with a minimum topic size of two should fail. + { + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + if err := sendTopic.Publish(ctx, twoMsg, WithReadiness(MinTopicSize(2))); !errors.Is(err, context.DeadlineExceeded) { + t.Fatal(err) + } + } + + // Subscribe the second receiver; the publish should now work. + receiveTopic2, err := receiver2.Join(topicID) + if err != nil { t.Fatal(err) } - } - // Subscribe the second receiver; the publish should now work. - receiveTopic2, err := receiver2.Join(topicID) - if err != nil { - t.Fatal(err) - } + sub2, err := receiveTopic2.Subscribe() + if err != nil { + t.Fatal(err) + } - sub2, err := receiveTopic2.Subscribe() - if err != nil { - t.Fatal(err) - } + { + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + if err := sendTopic.Publish(ctx, twoMsg, WithReadiness(MinTopicSize(2))); err != nil { + t.Fatal(err) + } + } - { - ctx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - if err := sendTopic.Publish(ctx, twoMsg, WithReadiness(MinTopicSize(2))); err != nil { + if msg, err := sub2.Next(ctx); err != nil { t.Fatal(err) + } else if !bytes.Equal(msg.GetData(), twoMsg) { + t.Fatal("received incorrect message") } - } - - if msg, err := sub2.Next(ctx); err != nil { - t.Fatal(err) - } else if !bytes.Equal(msg.GetData(), twoMsg) { - t.Fatal("received incorrect message") - } + }) } func TestWithTopicMsgIdFunction(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - const topicA, topicB = "foobarA", "foobarB" - const numHosts = 2 + const topicA, topicB = "foobarA", "foobarB" + const numHosts = 2 - hosts := getDefaultHosts(t, numHosts) - pubsubs := getPubsubs(ctx, hosts, WithMessageIdFn(func(pmsg *pb.Message) string { - hash := sha256.Sum256(pmsg.Data) - return string(hash[:]) - })) - connectAll(t, hosts) + hosts := getDefaultHosts(t, numHosts) + pubsubs := getPubsubs(ctx, hosts, WithMessageIdFn(func(pmsg *pb.Message) string { + hash := sha256.Sum256(pmsg.Data) + return string(hash[:]) + })) + connectAll(t, hosts) - topicsA := getTopics(pubsubs, topicA) // uses global msgIdFn - topicsB := getTopics(pubsubs, topicB, WithTopicMessageIdFn(func(pmsg *pb.Message) string { // uses custom - hash := sha1.Sum(pmsg.Data) - return string(hash[:]) - })) + topicsA := getTopics(pubsubs, topicA) // uses global msgIdFn + topicsB := getTopics(pubsubs, topicB, WithTopicMessageIdFn(func(pmsg *pb.Message) string { // uses custom + hash := sha1.Sum(pmsg.Data) + return string(hash[:]) + })) - payload := []byte("pubsub rocks") + payload := []byte("pubsub rocks") - subA, err := topicsA[0].Subscribe() - if err != nil { - t.Fatal(err) - } + subA, err := topicsA[0].Subscribe() + if err != nil { + t.Fatal(err) + } - err = topicsA[1].Publish(ctx, payload, WithReadiness(MinTopicSize(1))) - if err != nil { - t.Fatal(err) - } + err = topicsA[1].Publish(ctx, payload, WithReadiness(MinTopicSize(1))) + if err != nil { + t.Fatal(err) + } - msgA, err := subA.Next(ctx) - if err != nil { - t.Fatal(err) - } + msgA, err := subA.Next(ctx) + if err != nil { + t.Fatal(err) + } - subB, err := topicsB[0].Subscribe() - if err != nil { - t.Fatal(err) - } + subB, err := topicsB[0].Subscribe() + if err != nil { + t.Fatal(err) + } - err = topicsB[1].Publish(ctx, payload, WithReadiness(MinTopicSize(1))) - if err != nil { - t.Fatal(err) - } + err = topicsB[1].Publish(ctx, payload, WithReadiness(MinTopicSize(1))) + if err != nil { + t.Fatal(err) + } - msgB, err := subB.Next(ctx) - if err != nil { - t.Fatal(err) - } + msgB, err := subB.Next(ctx) + if err != nil { + t.Fatal(err) + } - if msgA.ID == msgB.ID { - t.Fatal("msg ids are equal") - } + if msgA.ID == msgB.ID { + t.Fatal("msg ids are equal") + } + }) } func TestTopicPublishWithKeyInvalidParameters(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() - const topic = "foobar" - const numHosts = 5 + const topic = "foobar" + const numHosts = 5 - virtualPeer := tnet.RandPeerNetParamsOrFatal(t) - hosts := getDefaultHosts(t, numHosts) - topics := getTopics(getPubsubs(ctx, hosts), topic) + virtualPeer := tnet.RandPeerNetParamsOrFatal(t) + hosts := getDefaultHosts(t, numHosts) + topics := getTopics(getPubsubs(ctx, hosts), topic) - t.Run("nil sign private key should error", func(t *testing.T) { + // nil sign private key should error withVirtualKey := WithSecretKeyAndPeerId(nil, virtualPeer.ID) err := topics[0].Publish(ctx, []byte("buff"), withVirtualKey) if err != ErrNilSignKey { t.Fatal("error should have been of type errNilSignKey") } - }) - t.Run("empty peer ID should error", func(t *testing.T) { - withVirtualKey := WithSecretKeyAndPeerId(virtualPeer.PrivKey, "") - err := topics[0].Publish(ctx, []byte("buff"), withVirtualKey) + + // empty peer ID should error + withVirtualKey = WithSecretKeyAndPeerId(virtualPeer.PrivKey, "") + err = topics[0].Publish(ctx, []byte("buff"), withVirtualKey) if err != ErrEmptyPeerID { t.Fatal("error should have been of type errEmptyPeerID") } @@ -952,132 +980,146 @@ func TestTopicPublishWithKeyInvalidParameters(t *testing.T) { } func TestTopicPublishWithContextCanceled(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - const topic = "foobar" - const numHosts = 5 + const topic = "foobar" + const numHosts = 5 - hosts := getDefaultHosts(t, numHosts) - topics := getTopics(getPubsubs(ctx, hosts), topic) - cancel() + hosts := getDefaultHosts(t, numHosts) + topics := getTopics(getPubsubs(ctx, hosts), topic) + cancel() - err := topics[0].Publish(ctx, []byte("buff")) - if err != context.Canceled { - t.Fatal("error should have been of type context.Canceled", err) - } + err := topics[0].Publish(ctx, []byte("buff")) + if err != context.Canceled { + t.Fatal("error should have been of type context.Canceled", err) + } + }) } func TestTopicRelayPublishWithKey(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - const topic = "foobar" - const numHosts = 5 + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() - virtualPeer := tnet.RandPeerNetParamsOrFatal(t) - hosts := getDefaultHosts(t, numHosts) - topics := getTopics(getPubsubs(ctx, hosts), topic) + const topic = "foobar" + const numHosts = 5 + + virtualPeer := tnet.RandPeerNetParamsOrFatal(t) + hosts := getDefaultHosts(t, numHosts) + // Create PubSubs with a small sleep between each so that each gets a + // different time.Now().UnixNano() for its seqno counter. This avoids + // message ID collisions when multiple hosts publish with the same + // virtual peer ID (From+Seqno must be unique). + var psubs []*PubSub + for _, h := range hosts { + time.Sleep(time.Millisecond) + psubs = append(psubs, getPubsub(ctx, h)) + } + topics := getTopics(psubs, topic) - // [0.Rel] - [1.Rel] - [2.Sub] - // | - // [3.Rel] - [4.Sub] + // [0.Rel] - [1.Rel] - [2.Sub] + // | + // [3.Rel] - [4.Sub] - connect(t, hosts[0], hosts[1]) - connect(t, hosts[1], hosts[2]) - connect(t, hosts[1], hosts[3]) - connect(t, hosts[3], hosts[4]) + connect(t, hosts[0], hosts[1]) + connect(t, hosts[1], hosts[2]) + connect(t, hosts[1], hosts[3]) + connect(t, hosts[3], hosts[4]) - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Second) - var subs []*Subscription + var subs []*Subscription - for i, topicValue := range topics { - if i == 2 || i == 4 { - sub, err := topicValue.Subscribe() - if err != nil { - t.Fatal(err) - } + for i, topicValue := range topics { + if i == 2 || i == 4 { + sub, err := topicValue.Subscribe() + if err != nil { + t.Fatal(err) + } - subs = append(subs, sub) - } else { - _, err := topicValue.Relay() - if err != nil { - t.Fatal(err) + subs = append(subs, sub) + } else { + _, err := topicValue.Relay() + if err != nil { + t.Fatal(err) + } } } - } - - time.Sleep(time.Millisecond * 100) - - for i := 0; i < 100; i++ { - msg := []byte("message") - owner := rand.Intn(len(topics)) + time.Sleep(time.Second) - withVirtualKey := WithSecretKeyAndPeerId(virtualPeer.PrivKey, virtualPeer.ID) - err := topics[owner].Publish(ctx, msg, withVirtualKey) - if err != nil { - t.Fatal(err) - } + for i := 0; i < 100; i++ { + msg := []byte("message") - for _, sub := range subs { - received, errSub := sub.Next(ctx) - if errSub != nil { - t.Fatal(errSub) + owner := rand.Intn(len(topics)) + withVirtualKey := WithSecretKeyAndPeerId(virtualPeer.PrivKey, virtualPeer.ID) + err := topics[owner].Publish(ctx, msg, withVirtualKey) + if err != nil { + t.Fatal(err) } - if !bytes.Equal(msg, received.Data) { - t.Fatal("received message is other than expected") - } - if string(received.From) != string(virtualPeer.ID) { - t.Fatal("received message is not from the virtual peer") + for _, sub := range subs { + received, errSub := sub.Next(ctx) + if errSub != nil { + t.Fatal(errSub) + } + + if !bytes.Equal(msg, received.Data) { + t.Fatal("received message is other than expected") + } + if string(received.From) != string(virtualPeer.ID) { + t.Fatal("received message is not from the virtual peer") + } } } - } + }) } func TestWithLocalPublication(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - const topic = "test" + const topic = "test" - hosts := getDefaultHosts(t, 2) - pubsubs := getPubsubs(ctx, hosts) - topics := getTopics(pubsubs, topic) - connectAll(t, hosts) + hosts := getDefaultHosts(t, 2) + pubsubs := getPubsubs(ctx, hosts) + topics := getTopics(pubsubs, topic) + connectAll(t, hosts) - payload := []byte("pubsub smashes") + payload := []byte("pubsub smashes") - local, err := topics[0].Subscribe() - if err != nil { - t.Fatal(err) - } + local, err := topics[0].Subscribe() + if err != nil { + t.Fatal(err) + } - remote, err := topics[1].Subscribe() - if err != nil { - t.Fatal(err) - } + remote, err := topics[1].Subscribe() + if err != nil { + t.Fatal(err) + } - err = topics[0].Publish(ctx, payload, WithLocalPublication(true)) - if err != nil { - t.Fatal(err) - } + err = topics[0].Publish(ctx, payload, WithLocalPublication(true)) + if err != nil { + t.Fatal(err) + } - remoteCtx, cancel := context.WithTimeout(ctx, time.Millisecond*100) - defer cancel() + remoteCtx, cancel := context.WithTimeout(ctx, time.Millisecond*100) + defer cancel() - msg, err := remote.Next(remoteCtx) - if msg != nil || err == nil { - t.Fatal("unexpected msg") - } + msg, err := remote.Next(remoteCtx) + if msg != nil || err == nil { + t.Fatal("unexpected msg") + } - msg, err = local.Next(ctx) - if err != nil { - t.Fatal(err) - } - if !msg.Local || !bytes.Equal(msg.Data, payload) { - t.Fatal("wrong message") - } + msg, err = local.Next(ctx) + if err != nil { + t.Fatal(err) + } + if !msg.Local || !bytes.Equal(msg.Data, payload) { + t.Fatal("wrong message") + } + }) } diff --git a/trace_test.go b/trace_test.go index 0d4dafee..f1afe3cf 100644 --- a/trace_test.go +++ b/trace_test.go @@ -192,69 +192,73 @@ func (ts *traceStats) check(t *testing.T) { } func TestJSONTracer(t *testing.T) { - tracer, err := NewJSONTracer("/tmp/trace.out.json") - if err != nil { - t.Fatal(err) - } - - testWithTracer(t, tracer) - time.Sleep(time.Second) - tracer.Close() + synctestTest(t, func(t *testing.T) { + tracer, err := NewJSONTracer("/tmp/trace.out.json") + if err != nil { + t.Fatal(err) + } - var stats traceStats - var evt pb.TraceEvent + testWithTracer(t, tracer) + time.Sleep(time.Second) + tracer.Close() - f, err := os.Open("/tmp/trace.out.json") - if err != nil { - t.Fatal(err) - } - defer f.Close() + var stats traceStats + var evt pb.TraceEvent - dec := json.NewDecoder(f) - for { - evt.Reset() - err := dec.Decode(&evt) + f, err := os.Open("/tmp/trace.out.json") if err != nil { - break + t.Fatal(err) } + defer f.Close() + + dec := json.NewDecoder(f) + for { + evt.Reset() + err := dec.Decode(&evt) + if err != nil { + break + } - stats.process(&evt) - } + stats.process(&evt) + } - stats.check(t) + stats.check(t) + }) } func TestPBTracer(t *testing.T) { - tracer, err := NewPBTracer("/tmp/trace.out.pb") - if err != nil { - t.Fatal(err) - } - - testWithTracer(t, tracer) - time.Sleep(time.Second) - tracer.Close() + synctestTest(t, func(t *testing.T) { + tracer, err := NewPBTracer("/tmp/trace.out.pb") + if err != nil { + t.Fatal(err) + } - var stats traceStats - var evt pb.TraceEvent + testWithTracer(t, tracer) + time.Sleep(time.Second) + tracer.Close() - f, err := os.Open("/tmp/trace.out.pb") - if err != nil { - t.Fatal(err) - } - defer f.Close() + var stats traceStats + var evt pb.TraceEvent - r := protoio.NewDelimitedReader(f, 1<<20) - for { - evt.Reset() - err := r.ReadMsg(&evt) + f, err := os.Open("/tmp/trace.out.pb") if err != nil { - break + t.Fatal(err) } + defer f.Close() + + r := protoio.NewDelimitedReader(f, 1<<20) + for { + evt.Reset() + err := r.ReadMsg(&evt) + if err != nil { + break + } - stats.process(&evt) - } + stats.process(&evt) + } - stats.check(t) + stats.check(t) + }) } type mockRemoteTracer struct { @@ -298,24 +302,28 @@ func (mrt *mockRemoteTracer) check(t *testing.T) { } func TestRemoteTracer(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 2) - h1 := hosts[0] - h2 := hosts[1] + hosts := getDefaultHosts(t, 2) + h1 := hosts[0] + h2 := hosts[1] - mrt := &mockRemoteTracer{} - h1.SetStreamHandler(RemoteTracerProtoID, mrt.handleStream) + mrt := &mockRemoteTracer{} + h1.SetStreamHandler(RemoteTracerProtoID, mrt.handleStream) - tracer, err := NewRemoteTracer(ctx, h2, peer.AddrInfo{ID: h1.ID(), Addrs: h1.Addrs()}, slog.Default()) - if err != nil { - t.Fatal(err) - } + tracer, err := NewRemoteTracer(ctx, h2, peer.AddrInfo{ID: h1.ID(), Addrs: h1.Addrs()}, slog.Default()) + if err != nil { + t.Fatal(err) + } - testWithTracer(t, tracer) - time.Sleep(time.Second) - tracer.Close() + testWithTracer(t, tracer) + time.Sleep(time.Second) + tracer.Close() + // Allow the doWrite goroutine to complete its sleep loop and exit + time.Sleep(2 * time.Second) - mrt.check(t) + mrt.check(t) + }) } diff --git a/validation_builtin_test.go b/validation_builtin_test.go index fa7419aa..ebea37c6 100644 --- a/validation_builtin_test.go +++ b/validation_builtin_test.go @@ -41,11 +41,15 @@ func init() { } func TestBasicSeqnoValidator1(t *testing.T) { - testBasicSeqnoValidator(t, time.Minute) + synctestTest(t, func(t *testing.T) { + testBasicSeqnoValidator(t, time.Minute) + }) } func TestBasicSeqnoValidator2(t *testing.T) { - testBasicSeqnoValidator(t, time.Nanosecond) + synctestTest(t, func(t *testing.T) { + testBasicSeqnoValidator(t, time.Nanosecond) + }) } func testBasicSeqnoValidator(t *testing.T, ttl time.Duration) { @@ -97,55 +101,57 @@ func testBasicSeqnoValidator(t *testing.T, ttl time.Duration) { } func TestBasicSeqnoValidatorReplay(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - hosts := getDefaultHosts(t, 20) - psubs := getPubsubsWithOptionC(ctx, hosts[:19], - func(i int) Option { - return WithDefaultValidator(NewBasicSeqnoValidator(newMockPeerMetadataStore(), slog.Default())) - }, - func(i int) Option { - return WithSeenMessagesTTL(time.Nanosecond) - }, - ) - _ = newReplayActor(t, ctx, hosts[19]) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + hosts := getDefaultHosts(t, 20) + psubs := getPubsubsWithOptionC(ctx, hosts[:19], + func(i int) Option { + return WithDefaultValidator(NewBasicSeqnoValidator(newMockPeerMetadataStore(), slog.Default())) + }, + func(i int) Option { + return WithSeenMessagesTTL(time.Nanosecond) + }, + ) + _ = newReplayActor(t, ctx, hosts[19]) + + var msgs []*Subscription + for _, ps := range psubs { + subch, err := ps.Subscribe("foobar") + if err != nil { + t.Fatal(err) + } - var msgs []*Subscription - for _, ps := range psubs { - subch, err := ps.Subscribe("foobar") - if err != nil { - t.Fatal(err) + msgs = append(msgs, subch) } - msgs = append(msgs, subch) - } + sparseConnect(t, hosts) - sparseConnect(t, hosts) + time.Sleep(time.Millisecond * 100) - time.Sleep(time.Millisecond * 100) + for i := 0; i < 10; i++ { + msg := []byte(fmt.Sprintf("%d the flooooooood %d", i, i)) - for i := 0; i < 10; i++ { - msg := []byte(fmt.Sprintf("%d the flooooooood %d", i, i)) + owner := rng.Intn(len(psubs)) - owner := rng.Intn(len(psubs)) + psubs[owner].Publish("foobar", msg) - psubs[owner].Publish("foobar", msg) - - for _, sub := range msgs { - got, err := sub.Next(ctx) - if err != nil { - t.Fatal(sub.err) - } - if !bytes.Equal(msg, got.Data) { - t.Fatal("got wrong message!") + for _, sub := range msgs { + got, err := sub.Next(ctx) + if err != nil { + t.Fatal(sub.err) + } + if !bytes.Equal(msg, got.Data) { + t.Fatal("got wrong message!") + } } } - } - for _, sub := range msgs { - assertNeverReceives(t, sub, time.Second) - } + for _, sub := range msgs { + assertNeverReceives(t, sub, time.Second) + } + }) } type mockPeerMetadataStore struct { diff --git a/validation_test.go b/validation_test.go index 0a09f70b..683504db 100644 --- a/validation_test.go +++ b/validation_test.go @@ -12,157 +12,162 @@ import ( ) func TestRegisterUnregisterValidator(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 1) - psubs := getPubsubs(ctx, hosts) + hosts := getDefaultHosts(t, 1) + psubs := getPubsubs(ctx, hosts) - err := psubs[0].RegisterTopicValidator("foo", func(context.Context, peer.ID, *Message) bool { - return true - }) - if err != nil { - t.Fatal(err) - } + err := psubs[0].RegisterTopicValidator("foo", func(context.Context, peer.ID, *Message) bool { + return true + }) + if err != nil { + t.Fatal(err) + } - err = psubs[0].UnregisterTopicValidator("foo") - if err != nil { - t.Fatal(err) - } + err = psubs[0].UnregisterTopicValidator("foo") + if err != nil { + t.Fatal(err) + } - err = psubs[0].UnregisterTopicValidator("foo") - if err == nil { - t.Fatal("Unregistered bogus topic validator") - } + err = psubs[0].UnregisterTopicValidator("foo") + if err == nil { + t.Fatal("Unregistered bogus topic validator") + } + }) } func TestRegisterValidatorEx(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 3) - psubs := getPubsubs(ctx, hosts) + hosts := getDefaultHosts(t, 3) + psubs := getPubsubs(ctx, hosts) - err := psubs[0].RegisterTopicValidator("test", - Validator(func(context.Context, peer.ID, *Message) bool { - return true - })) - if err != nil { - t.Fatal(err) - } + err := psubs[0].RegisterTopicValidator("test", + Validator(func(context.Context, peer.ID, *Message) bool { + return true + })) + if err != nil { + t.Fatal(err) + } - err = psubs[1].RegisterTopicValidator("test", - ValidatorEx(func(context.Context, peer.ID, *Message) ValidationResult { - return ValidationAccept - })) - if err != nil { - t.Fatal(err) - } + err = psubs[1].RegisterTopicValidator("test", + ValidatorEx(func(context.Context, peer.ID, *Message) ValidationResult { + return ValidationAccept + })) + if err != nil { + t.Fatal(err) + } - err = psubs[2].RegisterTopicValidator("test", "bogus") - if err == nil { - t.Fatal("expected error") - } + err = psubs[2].RegisterTopicValidator("test", "bogus") + if err == nil { + t.Fatal("expected error") + } + }) } func TestValidate(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - hosts := getDefaultHosts(t, 2) - psubs := getPubsubs(ctx, hosts) - - connect(t, hosts[0], hosts[1]) - topic := "foobar" - - err := psubs[1].RegisterTopicValidator(topic, func(ctx context.Context, from peer.ID, msg *Message) bool { - return !bytes.Contains(msg.Data, []byte("illegal")) - }) - if err != nil { - t.Fatal(err) - } + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - sub, err := psubs[1].Subscribe(topic) - if err != nil { - t.Fatal(err) - } + hosts := getDefaultHosts(t, 2) + psubs := getPubsubs(ctx, hosts) - time.Sleep(time.Millisecond * 50) + connect(t, hosts[0], hosts[1]) + topic := "foobar" - msgs := []struct { - msg []byte - validates bool - }{ - {msg: []byte("this is a legal message"), validates: true}, - {msg: []byte("there also is nothing controversial about this message"), validates: true}, - {msg: []byte("openly illegal content will be censored"), validates: false}, - {msg: []byte("but subversive actors will use leetspeek to spread 1ll3g4l content"), validates: true}, - } + err := psubs[1].RegisterTopicValidator(topic, func(ctx context.Context, from peer.ID, msg *Message) bool { + return !bytes.Contains(msg.Data, []byte("illegal")) + }) + if err != nil { + t.Fatal(err) + } - for _, tc := range msgs { - err := psubs[0].Publish(topic, tc.msg) + sub, err := psubs[1].Subscribe(topic) if err != nil { t.Fatal(err) } - select { - case msg := <-sub.ch: - if !tc.validates { - t.Log(msg) - t.Error("expected message validation to filter out the message") + time.Sleep(time.Millisecond * 50) + + msgs := []struct { + msg []byte + validates bool + }{ + {msg: []byte("this is a legal message"), validates: true}, + {msg: []byte("there also is nothing controversial about this message"), validates: true}, + {msg: []byte("openly illegal content will be censored"), validates: false}, + {msg: []byte("but subversive actors will use leetspeek to spread 1ll3g4l content"), validates: true}, + } + + for _, tc := range msgs { + err := psubs[0].Publish(topic, tc.msg) + if err != nil { + t.Fatal(err) } - case <-time.After(333 * time.Millisecond): - if tc.validates { - t.Error("expected message validation to accept the message") + + select { + case msg := <-sub.ch: + if !tc.validates { + t.Log(msg) + t.Error("expected message validation to filter out the message") + } + case <-time.After(333 * time.Millisecond): + if tc.validates { + t.Error("expected message validation to accept the message") + } } } - } + }) } func TestValidate2(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - hosts := getDefaultHosts(t, 1) - psubs := getPubsubs(ctx, hosts) + hosts := getDefaultHosts(t, 1) + psubs := getPubsubs(ctx, hosts) - topic := "foobar" + topic := "foobar" - err := psubs[0].RegisterTopicValidator(topic, func(ctx context.Context, from peer.ID, msg *Message) bool { - return !bytes.Contains(msg.Data, []byte("illegal")) - }) - if err != nil { - t.Fatal(err) - } + err := psubs[0].RegisterTopicValidator(topic, func(ctx context.Context, from peer.ID, msg *Message) bool { + return !bytes.Contains(msg.Data, []byte("illegal")) + }) + if err != nil { + t.Fatal(err) + } - msgs := []struct { - msg []byte - validates bool - }{ - {msg: []byte("this is a legal message"), validates: true}, - {msg: []byte("there also is nothing controversial about this message"), validates: true}, - {msg: []byte("openly illegal content will be censored"), validates: false}, - {msg: []byte("but subversive actors will use leetspeek to spread 1ll3g4l content"), validates: true}, - } + msgs := []struct { + msg []byte + validates bool + }{ + {msg: []byte("this is a legal message"), validates: true}, + {msg: []byte("there also is nothing controversial about this message"), validates: true}, + {msg: []byte("openly illegal content will be censored"), validates: false}, + {msg: []byte("but subversive actors will use leetspeek to spread 1ll3g4l content"), validates: true}, + } - for _, tc := range msgs { - err := psubs[0].Publish(topic, tc.msg) - if tc.validates { - if err != nil { - t.Fatal(err) - } - } else { - if err == nil { - t.Fatal("expected validation to fail for this message") + for _, tc := range msgs { + err := psubs[0].Publish(topic, tc.msg) + if tc.validates { + if err != nil { + t.Fatal(err) + } + } else { + if err == nil { + t.Fatal("expected validation to fail for this message") + } } } - } + }) } func TestValidateOverload(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - type msg struct { msg []byte validates bool @@ -201,134 +206,142 @@ func TestValidateOverload(t *testing.T) { for tci, tc := range tcs { t.Run(fmt.Sprintf("%d", tci), func(t *testing.T) { - hosts := getDefaultHosts(t, 2) - psubs := getPubsubs(ctx, hosts) + synctestTest(t, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - connect(t, hosts[0], hosts[1]) - topic := "foobar" + hosts := getDefaultHosts(t, 2) + psubs := getPubsubs(ctx, hosts) - block := make(chan struct{}) + connect(t, hosts[0], hosts[1]) + topic := "foobar" - err := psubs[1].RegisterTopicValidator(topic, - func(ctx context.Context, from peer.ID, msg *Message) bool { - <-block - return true - }, - WithValidatorConcurrency(tc.maxConcurrency)) + block := make(chan struct{}) - if err != nil { - t.Fatal(err) - } + err := psubs[1].RegisterTopicValidator(topic, + func(ctx context.Context, from peer.ID, msg *Message) bool { + <-block + return true + }, + WithValidatorConcurrency(tc.maxConcurrency)) - sub, err := psubs[1].Subscribe(topic) - if err != nil { - t.Fatal(err) - } + if err != nil { + t.Fatal(err) + } - time.Sleep(time.Millisecond * 50) + sub, err := psubs[1].Subscribe(topic) + if err != nil { + t.Fatal(err) + } - if len(tc.msgs) != tc.maxConcurrency+1 { - t.Fatalf("expected number of messages sent to be maxConcurrency+1. Got %d, expected %d", len(tc.msgs), tc.maxConcurrency+1) - } + time.Sleep(time.Millisecond * 50) - p := psubs[0] + if len(tc.msgs) != tc.maxConcurrency+1 { + t.Fatalf("expected number of messages sent to be maxConcurrency+1. Got %d, expected %d", len(tc.msgs), tc.maxConcurrency+1) + } - var wg sync.WaitGroup - wg.Add(1) - go func() { - for _, tmsg := range tc.msgs { - select { - case msg := <-sub.ch: - if !tmsg.validates { - t.Log(msg) - t.Error("expected message validation to drop the message because all validator goroutines are taken") - } - case <-time.After(time.Second): - if tmsg.validates { - t.Error("expected message validation to accept the message") + p := psubs[0] + + var wg sync.WaitGroup + wg.Add(1) + go func() { + for _, tmsg := range tc.msgs { + select { + case msg := <-sub.ch: + if !tmsg.validates { + t.Log(msg) + t.Error("expected message validation to drop the message because all validator goroutines are taken") + } + case <-time.After(time.Second): + if tmsg.validates { + t.Error("expected message validation to accept the message") + } } } - } - wg.Done() - }() + wg.Done() + }() - for _, tmsg := range tc.msgs { - err := p.Publish(topic, tmsg.msg) - if err != nil { - t.Fatal(err) + for _, tmsg := range tc.msgs { + err := p.Publish(topic, tmsg.msg) + if err != nil { + t.Fatal(err) + } } - } - // wait a bit before unblocking the validator goroutines - time.Sleep(500 * time.Millisecond) - close(block) + // wait a bit before unblocking the validator goroutines + time.Sleep(500 * time.Millisecond) + close(block) - wg.Wait() + wg.Wait() + }) }) } } func TestValidateAssortedOptions(t *testing.T) { // this test adds coverage for various options that are not covered in other tests - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + synctestTest(t, func(t *testing.T) { - hosts := getDefaultHosts(t, 10) - psubs := getPubsubs(ctx, hosts, - WithValidateQueueSize(10), - WithValidateThrottle(10), - WithValidateWorkers(10)) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - sparseConnect(t, hosts) + hosts := getDefaultHosts(t, 10) + psubs := getPubsubs(ctx, hosts, + WithValidateQueueSize(10), + WithValidateThrottle(10), + WithValidateWorkers(10)) - for _, psub := range psubs { - err := psub.RegisterTopicValidator("test1", - func(context.Context, peer.ID, *Message) bool { - return true - }, - WithValidatorTimeout(100*time.Millisecond)) - if err != nil { - t.Fatal(err) - } + sparseConnect(t, hosts) - err = psub.RegisterTopicValidator("test2", - func(context.Context, peer.ID, *Message) bool { - return true - }, - WithValidatorInline(true)) - if err != nil { - t.Fatal(err) - } - } + for _, psub := range psubs { + err := psub.RegisterTopicValidator("test1", + func(context.Context, peer.ID, *Message) bool { + return true + }, + WithValidatorTimeout(100*time.Millisecond)) + if err != nil { + t.Fatal(err) + } - var subs1, subs2 []*Subscription - for _, ps := range psubs { - sub, err := ps.Subscribe("test1") - if err != nil { - t.Fatal(err) + err = psub.RegisterTopicValidator("test2", + func(context.Context, peer.ID, *Message) bool { + return true + }, + WithValidatorInline(true)) + if err != nil { + t.Fatal(err) + } } - subs1 = append(subs1, sub) - sub, err = ps.Subscribe("test2") - if err != nil { - t.Fatal(err) + var subs1, subs2 []*Subscription + for _, ps := range psubs { + sub, err := ps.Subscribe("test1") + if err != nil { + t.Fatal(err) + } + subs1 = append(subs1, sub) + + sub, err = ps.Subscribe("test2") + if err != nil { + t.Fatal(err) + } + subs2 = append(subs2, sub) } - subs2 = append(subs2, sub) - } - time.Sleep(time.Second) + time.Sleep(time.Second) - for i := 0; i < 10; i++ { - msg := []byte(fmt.Sprintf("message %d", i)) + for i := 0; i < 10; i++ { + msg := []byte(fmt.Sprintf("message %d", i)) - psubs[i].Publish("test1", msg) - for _, sub := range subs1 { - assertReceive(t, sub, msg) - } + psubs[i].Publish("test1", msg) + for _, sub := range subs1 { + assertReceive(t, sub, msg) + } - psubs[i].Publish("test2", msg) - for _, sub := range subs2 { - assertReceive(t, sub, msg) + psubs[i].Publish("test2", msg) + for _, sub := range subs2 { + assertReceive(t, sub, msg) + } } - } + }) }