package blossomsub import ( "bytes" "context" "fmt" "sync" "testing" "time" "github.com/libp2p/go-libp2p/core/peer" ) func TestRegisterUnregisterValidator(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() hosts := getNetHosts(t, ctx, 1) psubs := getPubsubs(ctx, hosts) err := psubs[0].RegisterBitmaskValidator([]byte{0xf0, 0x00}, func(context.Context, peer.ID, *Message) bool { return true }) if err != nil { t.Fatal(err) } err = psubs[0].UnregisterBitmaskValidator([]byte{0xf0, 0x00}) if err != nil { t.Fatal(err) } err = psubs[0].UnregisterBitmaskValidator([]byte{0xf0, 0x00}) if err == nil { t.Fatal("Unregistered bogus bitmask validator") } } func TestRegisterValidatorEx(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() hosts := getNetHosts(t, ctx, 3) psubs := getPubsubs(ctx, hosts) err := psubs[0].RegisterBitmaskValidator([]byte{0x7e, 0x57}, Validator(func(context.Context, peer.ID, *Message) bool { return true })) if err != nil { t.Fatal(err) } err = psubs[1].RegisterBitmaskValidator([]byte{0x7e, 0x57}, ValidatorEx(func(context.Context, peer.ID, *Message) ValidationResult { return ValidationAccept })) if err != nil { t.Fatal(err) } err = psubs[2].RegisterBitmaskValidator([]byte{0x7e, 0x57}, "bogus") if err == nil { t.Fatal("expected error") } } func TestValidate(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() hosts := getNetHosts(t, ctx, 2) psubs := getPubsubs(ctx, hosts) connect(t, hosts[0], hosts[1]) bitmask := []byte{0xf0, 0x0b, 0xa1, 0x20} err := psubs[1].RegisterBitmaskValidator(bitmask, func(ctx context.Context, from peer.ID, msg *Message) bool { return !bytes.Contains(msg.Data, []byte("illegal")) }) if err != nil { t.Fatal(err) } sub, err := psubs[1].Subscribe(bitmask) if err != nil { t.Fatal(err) } 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(bitmask, tc.msg) 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") } 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() hosts := getNetHosts(t, ctx, 1) psubs := getPubsubs(ctx, hosts) bitmask := []byte{0xf0, 0x0b, 0xa1, 0x20} err := psubs[0].RegisterBitmaskValidator(bitmask, 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}, } for _, tc := range msgs { err := psubs[0].Publish(bitmask, 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 } tcs := []struct { msgs []msg maxConcurrency int }{ { maxConcurrency: 10, msgs: []msg{ {msg: []byte("this is a legal message"), validates: true}, {msg: []byte("but subversive actors will use leetspeek to spread 1ll3g4l content"), validates: true}, {msg: []byte("there also is nothing controversial about this message"), validates: true}, {msg: []byte("also fine"), validates: true}, {msg: []byte("still, all good"), validates: true}, {msg: []byte("this is getting boring"), validates: true}, {msg: []byte([]byte{0xf0, 0x00}), validates: true}, {msg: []byte([]byte{0xf0, 0x0b, 0xa1, 0x20}), validates: true}, {msg: []byte("foofoo"), validates: true}, {msg: []byte("barfoo"), validates: true}, {msg: []byte("oh no!"), validates: false}, }, }, { maxConcurrency: 2, msgs: []msg{ {msg: []byte("this is a legal message"), validates: true}, {msg: []byte("but subversive actors will use leetspeek to spread 1ll3g4l content"), validates: true}, {msg: []byte("oh no!"), validates: false}, }, }, } for tci, tc := range tcs { t.Run(fmt.Sprintf("%d", tci), func(t *testing.T) { hosts := getNetHosts(t, ctx, 2) psubs := getPubsubs(ctx, hosts) connect(t, hosts[0], hosts[1]) bitmask := []byte{0xf0, 0x0b, 0xa1, 0x20} block := make(chan struct{}) err := psubs[1].RegisterBitmaskValidator(bitmask, func(ctx context.Context, from peer.ID, msg *Message) bool { <-block return true }, WithValidatorConcurrency(tc.maxConcurrency)) if err != nil { t.Fatal(err) } sub, err := psubs[1].Subscribe(bitmask) if err != nil { t.Fatal(err) } time.Sleep(time.Millisecond * 50) 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) } 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() }() for _, tmsg := range tc.msgs { err := p.Publish(bitmask, tmsg.msg) if err != nil { t.Fatal(err) } } // wait a bit before unblocking the validator goroutines time.Sleep(500 * time.Millisecond) close(block) 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() hosts := getNetHosts(t, ctx, 10) psubs := getPubsubs(ctx, hosts, WithValidateQueueSize(10), WithValidateThrottle(10), WithValidateWorkers(10)) sparseConnect(t, hosts) for _, psub := range psubs { err := psub.RegisterBitmaskValidator([]byte{0xff, 0x00, 0x00, 0x00}, func(context.Context, peer.ID, *Message) bool { return true }, WithValidatorTimeout(100*time.Millisecond)) if err != nil { t.Fatal(err) } err = psub.RegisterBitmaskValidator([]byte{0x00, 0xff, 0x00, 0x00}, func(context.Context, peer.ID, *Message) bool { return true }, WithValidatorInline(true)) if err != nil { t.Fatal(err) } } var subs1, subs2 []*Subscription for _, ps := range psubs { sub, err := ps.Subscribe([]byte{0xff, 0x00, 0x00, 0x00}) if err != nil { t.Fatal(err) } subs1 = append(subs1, sub) sub, err = ps.Subscribe([]byte{0x00, 0xff, 0x00, 0x00}) if err != nil { t.Fatal(err) } subs2 = append(subs2, sub) } time.Sleep(time.Second) for i := 0; i < 10; i++ { msg := []byte(fmt.Sprintf("message %d", i)) psubs[i].Publish([]byte{0xff, 0x00, 0x00, 0x00}, msg) for _, sub := range subs1 { assertReceive(t, sub, msg) } psubs[i].Publish([]byte{0x00, 0xff, 0x00, 0x00}, msg) for _, sub := range subs2 { assertReceive(t, sub, msg) } } }