From 1402e7bdeb6e5c64efac293a7b58e5ccdf4ad779 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Mon, 17 Jun 2024 21:51:30 +0300 Subject: [PATCH 01/19] removed deprecated import --- core/dht.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/dht.go b/core/dht.go index cc3278e..37b414c 100644 --- a/core/dht.go +++ b/core/dht.go @@ -6,13 +6,12 @@ import ( "github.com/amirylm/p2pmq/commons" dht "github.com/libp2p/go-libp2p-kad-dht" - dhtopts "github.com/libp2p/go-libp2p-kad-dht/opts" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/routing" ) -func (c *Controller) dhtRoutingFactory(ctx context.Context, opts ...dhtopts.Option) func(host.Host) (routing.PeerRouting, error) { +func (c *Controller) dhtRoutingFactory(ctx context.Context, opts ...dht.Option) func(host.Host) (routing.PeerRouting, error) { return func(h host.Host) (routing.PeerRouting, error) { dhtInst, err := dht.New(ctx, h, opts...) if err != nil { From 4bd7ef9dad87da175b7f67ba99164b1f2a7e0540 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Mon, 17 Jun 2024 21:51:55 +0300 Subject: [PATCH 02/19] make protoc --- Makefile | 3 +++ scripts/proto-gen.sh | 5 +---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index fa30e0c..bcad572 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,9 @@ CFG_PATH?=./resources/config/default.p2pmq.yaml TEST_PKG?=./core/... TEST_TIMEOUT?=2m +protoc: + ./scripts/proto-gen.sh + lint: @docker run --rm -v $(shell pwd):/app -w /app golangci/golangci-lint:v1.54 golangci-lint run -v --timeout=5m ./... diff --git a/scripts/proto-gen.sh b/scripts/proto-gen.sh index 133871d..f634567 100755 --- a/scripts/proto-gen.sh +++ b/scripts/proto-gen.sh @@ -1,7 +1,4 @@ #!/bin/bash protoc --go_out=. --go_opt=paths=source_relative \ - --go-grpc_out=. --go-grpc_opt=paths=source_relative ./proto/*.proto - -protoc --go_out=. --go_opt=paths=source_relative \ - --go-grpc_out=. --go-grpc_opt=paths=source_relative ./proto/**/*.proto \ No newline at end of file + --go-grpc_out=. --go-grpc_opt=paths=source_relative ./**/*.proto From 2ab0edeb4722e1f01b8369238c030f8a14530d73 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Mon, 17 Jun 2024 21:52:14 +0300 Subject: [PATCH 03/19] rename build target to 'pmq' --- Dockerfile | 6 ++++-- cmd/{p2pmq => pmq}/main.go | 16 +--------------- 2 files changed, 5 insertions(+), 17 deletions(-) rename cmd/{p2pmq => pmq}/main.go (84%) diff --git a/Dockerfile b/Dockerfile index 867d1e5..69de218 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN apt-get update && \ ARG APP_VERSION=nightly ARG APP_NAME=p2pmq -ARG BUILD_TARGET=p2pmq +ARG BUILD_TARGET=pmq WORKDIR /p2pmq @@ -23,7 +23,7 @@ RUN GOOS=linux CGO_ENABLED=0 go build -tags netgo -a -v -o ./bin/${BUILD_TARGET} FROM alpine:latest as runner -ARG BUILD_TARGET=p2pmq +ARG BUILD_TARGET=pmq RUN apk --no-cache --upgrade add ca-certificates bash @@ -32,3 +32,5 @@ WORKDIR /p2pmq COPY --from=builder /p2pmq/.env* ./ COPY --from=builder /p2pmq/resources/config/*.p2pmq.yaml ./ COPY --from=builder /p2pmq/bin/${BUILD_TARGET} ./app + +CMD ["./app"] \ No newline at end of file diff --git a/cmd/p2pmq/main.go b/cmd/pmq/main.go similarity index 84% rename from cmd/p2pmq/main.go rename to cmd/pmq/main.go index 48a4a42..1468c9d 100644 --- a/cmd/p2pmq/main.go +++ b/cmd/pmq/main.go @@ -17,7 +17,7 @@ import ( func main() { app := &cli.App{ - Name: "p2pmq", + Name: "pmq", Flags: []cli.Flag{ cli.IntFlag{ Name: "grpc-port", @@ -92,20 +92,6 @@ func main() { ctrl.Start(ctx) defer ctrl.Close() - // <-time.After(time.Second * 10) - - // if cfg.Pubsub != nil { - // if err := ctrl.Subscribe(ctx, "test-1"); err != nil { - // lggr.Errorw("could not subscribe to topic", "topic", "test-1", "err", err) - // } - // for i := 0; i < 10; i++ { - // <-time.After(time.Second * 5) - // if err := ctrl.Publish(ctx, "test-1", []byte(fmt.Sprintf("test-data-%d-%s", i, ctrl.ID()))); err != nil { - // lggr.Errorw("could not subscribe to topic", "topic", "test-1", "err", err) - // } - // } - // } - return grpcapi.ListenGrpc(srv, c.Int("grpc-port")) }, From 120565f6737db4372d43fe31f33923f94bb50939 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:43:39 +0300 Subject: [PATCH 04/19] pubsub updates --- commons/config_pubsub.go | 8 +- core/ctrl.go | 54 +++- core/gossipsub_test.go | 559 +++++++++++++++++++++++++++++++++++++++ core/pubsub.go | 65 +++-- core/pubsub_trace.go | 283 ++++++++++++++++++++ core/testutils.go | 136 +++++++--- core/topic.go | 21 +- 7 files changed, 1042 insertions(+), 84 deletions(-) create mode 100644 core/gossipsub_test.go create mode 100644 core/pubsub_trace.go diff --git a/commons/config_pubsub.go b/commons/config_pubsub.go index 422b197..f25150b 100644 --- a/commons/config_pubsub.go +++ b/commons/config_pubsub.go @@ -18,7 +18,7 @@ type PubsubConfig struct { Scoring *ScoringParams `json:"scoring,omitempty" yaml:"scoring,omitempty"` MsgValidator *MsgValidationConfig `json:"msgValidator,omitempty" yaml:"msgValidator,omitempty"` MsgIDFnConfig *MsgIDFnConfig `json:"msgIDFn,omitempty" yaml:"msgIDFn,omitempty"` - Trace bool `json:"trace,omitempty" yaml:"trace,omitempty"` + Trace *PubsubTraceConfig `json:"trace,omitempty" yaml:"trace,omitempty"` } func (psc PubsubConfig) GetTopicConfig(name string) (TopicConfig, bool) { @@ -30,6 +30,12 @@ func (psc PubsubConfig) GetTopicConfig(name string) (TopicConfig, bool) { return TopicConfig{}, false } +type PubsubTraceConfig struct { + Skiplist []string `json:"skiplist,omitempty" yaml:"skiplist,omitempty"` + JsonFile string `json:"jsonFile,omitempty" yaml:"jsonFile,omitempty"` + Debug bool `json:"debug,omitempty" yaml:"debug,omitempty"` +} + type MsgIDFnConfig struct { Type string `json:"type,omitempty" yaml:"type,omitempty"` Size int `json:"size,omitempty" yaml:"size,omitempty"` diff --git a/core/ctrl.go b/core/ctrl.go index 0ca959e..832b586 100644 --- a/core/ctrl.go +++ b/core/ctrl.go @@ -3,6 +3,7 @@ package core import ( "context" "fmt" + "sync/atomic" "github.com/amirylm/p2pmq/commons" "github.com/amirylm/p2pmq/commons/utils" @@ -12,6 +13,8 @@ import ( pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/host" + libp2pnetwork "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/protocol" "github.com/libp2p/go-libp2p/p2p/discovery/mdns" "github.com/libp2p/go-libp2p/p2p/net/connmgr" @@ -33,9 +36,11 @@ type Controller struct { mdnsSvc mdns.Service pubsub *pubsub.PubSub - topicManager *topicManager - denylist pubsub.Blacklist - subFilter pubsub.SubscriptionFilter + topicManager *topicManager + denylist pubsub.Blacklist + subFilter pubsub.SubscriptionFilter + psTracer *psTracer + pubsubRpcCounter *atomic.Uint64 valRouter MsgRouter[pubsub.ValidationResult] msgRouter MsgRouter[error] @@ -46,15 +51,16 @@ func NewController( cfg commons.Config, msgRouter MsgRouter[error], valRouter MsgRouter[pubsub.ValidationResult], - lggrNS string, + name string, ) (*Controller, error) { d := &Controller{ - threadControl: utils.NewThreadControl(), - lggr: lggr.Named(lggrNS).Named("controller"), - cfg: cfg, - valRouter: valRouter, - msgRouter: msgRouter, - topicManager: newTopicManager(), + threadControl: utils.NewThreadControl(), + lggr: lggr.Named(name).Named("ctrl"), + cfg: cfg, + valRouter: valRouter, + msgRouter: msgRouter, + topicManager: newTopicManager(), + pubsubRpcCounter: new(atomic.Uint64), } err := d.setup(ctx, cfg) @@ -65,6 +71,21 @@ func (c *Controller) ID() string { return c.host.ID().String() } +func (c *Controller) Connect(ctx context.Context, dest *Controller) error { + ai := peer.AddrInfo{ + ID: dest.host.ID(), + Addrs: dest.host.Addrs(), + } + switch c.host.Network().Connectedness(ai.ID) { + case libp2pnetwork.Connected: + return nil + case libp2pnetwork.CannotConnect: + return fmt.Errorf("cannot connect to %s", ai.ID) + default: + } + return c.host.Connect(ctx, ai) +} + func (c *Controller) RefreshRouters(msgHandler func(*MsgWrapper[error]), valHandler func(*MsgWrapper[pubsub.ValidationResult])) { if c.valRouter != nil { c.valRouter.RefreshHandler(valHandler) @@ -78,7 +99,7 @@ func (c *Controller) RefreshRouters(msgHandler func(*MsgWrapper[error]), valHand func (c *Controller) Start(ctx context.Context) { c.StartOnce(func() { - // d.lggr.Debugf("starting controller with host %s", d.host.ID()) + c.lggr.Debugf("starting ctrl") if c.msgRouter != nil { c.threadControl.Go(c.msgRouter.Start) @@ -98,7 +119,7 @@ func (c *Controller) Start(ctx context.Context) { c.connect(b) } if err := c.dht.Bootstrap(ctx); err != nil { - c.lggr.Panicf("failed to start discovery: %w", err) + c.lggr.Panicf("failed to start dht: %w", err) } } if c.mdnsSvc != nil { @@ -111,7 +132,8 @@ func (c *Controller) Start(ctx context.Context) { func (c *Controller) Close() { c.StopOnce(func() { - c.lggr.Debugf("closing controller with host %s", c.host.ID()) + h := c.host.ID() + c.lggr.Debugf("closing controller with host %s", h) c.threadControl.Close() if c.dht != nil { if err := c.dht.Close(); err != nil { @@ -126,6 +148,7 @@ func (c *Controller) Close() { if err := c.host.Close(); err != nil { c.lggr.Errorf("failed to close host: %w", err) } + c.lggr.Debugf("closed controller with host %s", h) }) } @@ -194,7 +217,8 @@ func (c *Controller) setup(ctx context.Context, cfg commons.Config) (err error) return err } c.host = h - c.lggr.Infow("created libp2p host", "peerID", h.ID(), "addrs", h.Addrs()) + c.lggr = c.lggr.With("peerID", h.ID()) + c.lggr.Debugw("created libp2p host", "addrs", h.Addrs()) if len(cfg.MdnsTag) > 0 { c.setupMdnsDiscovery(ctx, h, cfg.MdnsTag) @@ -207,5 +231,7 @@ func (c *Controller) setup(ctx context.Context, cfg commons.Config) (err error) } } + c.lggr.Infow("ctrl setup done", "addrs", h.Addrs()) + return nil } diff --git a/core/gossipsub_test.go b/core/gossipsub_test.go new file mode 100644 index 0000000..e670a03 --- /dev/null +++ b/core/gossipsub_test.go @@ -0,0 +1,559 @@ +package core + +import ( + "context" + "fmt" + "strconv" + "strings" + "sync" + "testing" + "text/template" + "time" + + "github.com/amirylm/p2pmq/commons" + "github.com/amirylm/p2pmq/commons/utils" + "github.com/amirylm/p2pmq/core/gossip" + logging "github.com/ipfs/go-log" + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" +) + +var ( + traceEmptySkipList = []string{} + traceMsgEventSkipList = []string{ + "ADD_PEER", + "REMOVE_PEER", + "JOIN", + "LEAVE", + "GRAFT", + "PRUNE", + "DROP_RPC", + } + traceGossipEventSkipList = []string{ + "ADD_PEER", + "REMOVE_PEER", + "JOIN", + "LEAVE", + } +) + +func TestGossipMsgThroughput(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + require.NoError(t, logging.SetLogLevelRegex("p2pmq", "error")) + + groupsCfgSimple, groupsCfgMedium := testGroupSimple(), testGroupMedium() + + tests := []struct { + name string + n int + pubsubConfig *commons.PubsubConfig + gen *testGen + groupsCfg map[string]groupCfg + conns connectivity + flows []flow + topicsToCheck []string + }{ + { + name: "simple_11", + n: 11, + gen: &testGen{ + hitMaps: map[string]*nodeHitMap{}, + routingFn: func(m *pubsub.Message) {}, + validationFn: func(p peer.ID, m *pubsub.Message) pubsub.ValidationResult { + return pubsub.ValidationAccept + }, + pubsubConfig: &commons.PubsubConfig{ + MsgValidator: &commons.MsgValidationConfig{}, + Trace: &commons.PubsubTraceConfig{ + // JsonFile: fmt.Sprintf("../.output/trace/node-%d.json", i+1), + Skiplist: traceEmptySkipList, + }, + Overlay: &commons.OverlayParams{ + D: 3, + Dlow: 2, + Dhi: 5, + Dlazy: 3, + }, + }, + }, + groupsCfg: groupsCfgSimple, + conns: map[int][]int{ + 0: append(append(groupsCfgSimple["a"].ids, groupsCfgSimple["relayers"].ids...), + 5, 7, 9, // group b + ), + 1: append(append(groupsCfgSimple["a"].ids, groupsCfgSimple["relayers"].ids...), + 4, 6, 8, // group b + ), + 2: append(append(groupsCfgSimple["a"].ids, groupsCfgSimple["relayers"].ids...), + 4, 5, 7, // group b + ), + 3: append(append(groupsCfgSimple["a"].ids, groupsCfgSimple["relayers"].ids...), + 4, 6, 8, // group b + ), + 4: append(groupsCfgSimple["b"].ids, groupsCfgSimple["relayers"].ids...), + 5: append(groupsCfgSimple["b"].ids, groupsCfgSimple["relayers"].ids...), + 6: append(groupsCfgSimple["b"].ids, groupsCfgSimple["relayers"].ids...), + 7: append(groupsCfgSimple["b"].ids, groupsCfgSimple["relayers"].ids...), + 8: append(groupsCfgSimple["b"].ids, groupsCfgSimple["relayers"].ids...), + 9: append(groupsCfgSimple["b"].ids, groupsCfgSimple["relayers"].ids...), + }, + flows: []flow{ + { + name: "actions a->b", + events: []flowEvent{ + { + srcGroup: "a", + topic: "b.action.req", + pattern: "dummy-request-{{.i}}", + interval: time.Millisecond * 10, + wait: true, + }, + { + srcGroup: "b", + topic: "b.action.res", + pattern: "dummy-response-{{.i}}", + }, + }, + iterations: 1, + interval: time.Millisecond * 250, + }, + { + name: "triggers b", + events: []flowEvent{ + { + srcGroup: "b", + topic: "b.trigger", + pattern: "dummy-trigger-{{.group}}-{{.i}}", + interval: time.Millisecond * 10, + }, + }, + iterations: 2, + interval: time.Millisecond * 10, + }, + }, + }, + { + name: "medium_29", + n: 29, + gen: &testGen{ + hitMaps: map[string]*nodeHitMap{}, + routingFn: func(m *pubsub.Message) {}, + validationFn: func(p peer.ID, m *pubsub.Message) pubsub.ValidationResult { + return pubsub.ValidationAccept + }, + pubsubConfig: &commons.PubsubConfig{ + MsgValidator: &commons.MsgValidationConfig{}, + Trace: &commons.PubsubTraceConfig{ + // JsonFile: fmt.Sprintf("../.output/trace/node-%d.json", i+1), + Skiplist: traceEmptySkipList, + }, + Overlay: &commons.OverlayParams{ + D: 3, + Dlow: 2, + Dhi: 5, + Dlazy: 3, + }, + }, + }, + groupsCfg: groupsCfgMedium, + conns: map[int][]int{ + 0: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), + 10, 11, 12, // group b + 20, 21, 22, // group c + ), + 1: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), + 14, 11, 15, // group b + 24, 21, 25, // group c + ), + 2: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), + 13, 12, 16, // group b + 23, 22, 26, // group c + ), + 3: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), + 14, 15, 17, // group b + 24, 25, 27, // group c + ), + 4: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), + 10, 11, 12, // group b + 20, 21, 22, // group c + ), + 5: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), + 14, 11, 15, // group b + 21, 20, // group c + ), + 6: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), + 13, 12, 16, // group b + 23, 22, // group c + ), + 7: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), + 14, 15, 17, // group b + 25, // group c + ), + 8: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), + 10, 11, 12, // group b + 20, 21, // group c + ), + 9: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), + 14, 19, 15, // group b + 24, // group c + ), + 10: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), + 24, 22, // group c + ), + 11: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), + 22, // group c + ), + 12: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), + 22, 23, // group c + ), + 13: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), + 24, 21, // group c + ), + 14: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), + 20, // group c + ), + 15: append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), + 16: append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), + 17: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), + 22, // group c + ), + 18: append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), + 19: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), + 20, // group c + ), + 20: append(groupsCfgMedium["c"].ids, groupsCfgMedium["relayers"].ids...), + 21: append(groupsCfgMedium["c"].ids, groupsCfgMedium["relayers"].ids...), + 22: append(groupsCfgMedium["c"].ids, groupsCfgMedium["relayers"].ids...), + 23: append(groupsCfgMedium["c"].ids, groupsCfgMedium["relayers"].ids...), + 24: append(groupsCfgMedium["c"].ids, groupsCfgMedium["relayers"].ids...), + 25: append(groupsCfgMedium["c"].ids, groupsCfgMedium["relayers"].ids...), + 26: groupsCfgMedium["relayers"].ids, + 27: groupsCfgMedium["relayers"].ids, + 28: groupsCfgMedium["relayers"].ids, + }, + flows: []flow{ + { + name: "actions a->b", + events: []flowEvent{ + { + srcGroup: "a", + topic: "b.action.req", + pattern: "dummy-request-{{.i}}", + interval: time.Millisecond * 10, + wait: true, + }, + { + srcGroup: "b", + topic: "b.action.res", + pattern: "dummy-response-{{.i}}", + }, + }, + iterations: 5, + interval: time.Millisecond * 250, + }, + { + name: "triggers b", + events: []flowEvent{ + { + srcGroup: "b", + topic: "b.trigger", + pattern: "dummy-trigger-{{.group}}-{{.i}}", + interval: time.Millisecond * 10, + }, + }, + iterations: 10, + interval: time.Millisecond * 10, + }, + { + name: "triggers b+c", + events: []flowEvent{ + { + srcGroup: "b", + topic: "b.trigger", + pattern: "xdummy-trigger-{{.group}}-{{.i}}", + interval: time.Millisecond * 10, + }, + { + srcGroup: "c", + topic: "c.trigger", + pattern: "xdummy-trigger-{{.group}}-{{.i}}", + interval: time.Millisecond * 10, + }, + }, + iterations: 10, + interval: time.Millisecond * 10, + }, + }, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + ctrls, _, _, done, err := StartControllers(ctx, tc.n, tc.gen) + defer done() + require.NoError(t, err) + + t.Logf("%d controllers were started, connecting peers...", tc.n) + + require.NoError(t, tc.conns.connect(ctx, ctrls)) + + <-time.After(time.Second * 2) // TODO: avoid timeout + + t.Log("subscribing to topics...") + groups := make(map[string][]*Controller) + for name, groupCfg := range tc.groupsCfg { + for _, i := range groupCfg.ids { + if i >= tc.n { + continue + } + ctrl := ctrls[i] + groups[name] = append(groups[name], ctrl) + for _, topic := range groupCfg.subs { + require.NoError(t, ctrl.Subscribe(ctx, topic)) + } + for _, topic := range groupCfg.relays { + require.NoError(t, ctrl.Relay(topic)) + } + } + } + + <-time.After(time.Second * 4) // TODO: avoid timeout + + // starting fresh trace after subscriptions + startupFaucets := traceFaucets{} + for _, ctrl := range ctrls { + startupFaucets.add(ctrl.psTracer.faucets) + ctrl.psTracer.Reset() + } + t.Logf("\n [%s] all trace faucets (startup): %+v\n", tc.name, startupFaucets) + + t.Log("starting flows...") + for _, f := range tc.flows { + flow := f + flowTestName := fmt.Sprintf("%s-x%d", flow.name, flow.iterations) + t.Run(flowTestName, func(t *testing.T) { + threadCtrl := utils.NewThreadControl() + defer threadCtrl.Close() + + for i := 0; i < flow.iterations; i++ { + for _, e := range flow.events { + event := e + group, ok := groups[event.srcGroup] + if !ok || len(group) == 0 { + continue + } + var wg sync.WaitGroup + for _, c := range group { + _i := i + ctrl := c + wg.Add(1) + threadCtrl.Go(func(ctx context.Context) { + defer wg.Done() + args := map[string]interface{}{ + "i": _i, + "group": event.srcGroup, + "ctrl": ctrl.lggr.Desugar().Name(), + "flow": flow.name, + } + require.NoError(t, ctrl.Publish(ctx, event.topic, []byte(event.Msg(args)))) + }) + } + if event.wait { + wg.Wait() + } + } + <-time.After(flow.interval) + } + + faucets := traceFaucets{} + for node, hitMap := range tc.gen.hitMaps { + for _, topic := range tc.topicsToCheck { + t.Logf("[%s] %s messages: %d; validations: %d", node, topic, hitMap.messages(topic), hitMap.validations(topic)) + } + + nodeIndex, err := strconv.Atoi(node[5:]) + require.NoError(t, err) + tracer := ctrls[nodeIndex-1].psTracer + nodeFaucets := tracer.faucets + t.Logf("[%s] trace faucets: %+v", node, nodeFaucets) + faucets.add(nodeFaucets) + // traceEvents := tracer.Events() + // eventsJson, err := MarshalTraceEvents(traceEvents) + // require.NoError(t, err) + // require.NoError(t, os.WriteFile(fmt.Sprintf("../.output/trace/%s-%s-%s.json", tc.name, node, flow.name), eventsJson, 0644)) + tracer.Reset() + } + for _, ctrl := range groups["relayers"] { + nodeFaucets := ctrl.psTracer.faucets + t.Logf("[%s] trace faucets: %+v", ctrl.lggr.Desugar().Name(), nodeFaucets) + faucets.add(nodeFaucets) + } + t.Logf("\n [%s/%s] all trace faucets: %+v\n", tc.name, flowTestName, faucets) + }) + } + }) + } +} + +type connectivity map[int][]int + +func (connect connectivity) connect(ctx context.Context, ctrls []*Controller) error { + for i, ctrl := range ctrls { + conns := connect[i] + for _, c := range conns { + if i == c { + continue + } + if err := ctrl.Connect(ctx, ctrls[c]); err != nil { + return err + } + } + } + return nil +} + +type groupCfg struct { + ids []int + subs []string + relays []string +} + +func testGroupSimple() map[string]groupCfg { + return map[string]groupCfg{ + "a": { + ids: []int{0, 1, 2, 3}, + subs: []string{"b.action.res", "b.trigger"}, + }, + "b": { + ids: []int{4, 5, 6, 7, 8, 9}, + subs: []string{"b.action.req"}, + }, + "relayers": { + ids: []int{10}, + relays: []string{"b.action.req", "b.action.res", "b.trigger"}, + }, + } +} + +func testGroupMedium() map[string]groupCfg { + return map[string]groupCfg{ + "a": { + ids: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + subs: []string{"b.action.res", "b.trigger", "c.trigger"}, + }, + "b": { + ids: []int{10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, + subs: []string{"b.action.req", "c.trigger"}, + }, + "c": { + ids: []int{20, 21, 22, 23, 24, 25}, + subs: []string{"b.trigger"}, + }, + "relayers": { + ids: []int{26, 27, 28}, + relays: []string{"b.action.req", "b.action.res", "b.trigger", "c.trigger"}, + }, + } +} + +type flowEvent struct { + srcGroup string + topic string + pattern string + interval time.Duration + wait bool +} + +func (fe flowEvent) Msg(args map[string]interface{}) string { + tmpl, err := template.New("msg").Parse(fe.pattern) + if err != nil { + return "" + } + // create io.Buffer for the message and executing template + sb := new(strings.Builder) + if err := tmpl.Execute(sb, args); err != nil { + return "" + } + return sb.String() +} + +type flow struct { + name string + events []flowEvent + iterations int + interval time.Duration +} + +type nodeHitMap struct { + lock sync.RWMutex + valHitMap map[string]uint32 + msgHitMap map[string]uint32 +} + +func (n *nodeHitMap) validations(topic string) uint32 { + n.lock.RLock() + defer n.lock.RUnlock() + + return n.valHitMap[topic] +} + +func (n *nodeHitMap) messages(topic string) uint32 { + n.lock.RLock() + defer n.lock.RUnlock() + + return n.msgHitMap[topic] +} + +func (n *nodeHitMap) addValidation(topic string) { + n.lock.Lock() + defer n.lock.Unlock() + + n.valHitMap[topic] += 1 +} + +func (n *nodeHitMap) addMessage(topic string) { + n.lock.Lock() + defer n.lock.Unlock() + + n.msgHitMap[topic] += 1 +} + +type testGen struct { + hitMaps map[string]*nodeHitMap + routingFn func(*pubsub.Message) + validationFn func(peer.ID, *pubsub.Message) pubsub.ValidationResult + pubsubConfig *commons.PubsubConfig +} + +func (g *testGen) NextConfig(i int) (commons.Config, MsgRouter[error], MsgRouter[pubsub.ValidationResult], string) { + cfg := commons.Config{ + ListenAddrs: []string{ + "/ip4/127.0.0.1/tcp/0", + }, + Pubsub: g.pubsubConfig, + } + + name := fmt.Sprintf("node-%d", i+1) + + hitMap := &nodeHitMap{ + valHitMap: make(map[string]uint32), + msgHitMap: make(map[string]uint32), + } + g.hitMaps[name] = hitMap + + msgRouter := NewMsgRouter(1024, 4, func(mw *MsgWrapper[error]) { + hitMap.addMessage(mw.Msg.GetTopic()) + g.routingFn(mw.Msg) + }, gossip.DefaultMsgIDFn) + + valRouter := NewMsgRouter(1024, 4, func(mw *MsgWrapper[pubsub.ValidationResult]) { + hitMap.addValidation(mw.Msg.GetTopic()) + res := g.validationFn(mw.Peer, mw.Msg) + mw.Result = res + }, gossip.DefaultMsgIDFn) + + return cfg, msgRouter, valRouter, name +} diff --git a/core/pubsub.go b/core/pubsub.go index 5815525..8f87078 100644 --- a/core/pubsub.go +++ b/core/pubsub.go @@ -9,9 +9,7 @@ import ( "github.com/amirylm/p2pmq/commons" "github.com/amirylm/p2pmq/core/gossip" pubsub "github.com/libp2p/go-libp2p-pubsub" - pubsub_pb "github.com/libp2p/go-libp2p-pubsub/pb" "github.com/libp2p/go-libp2p/core/peer" - "go.uber.org/zap" ) var ( @@ -43,6 +41,11 @@ func (c *Controller) setupPubsubRouter(ctx context.Context, cfg commons.Config) opts = append(opts, pubsub.WithSeenMessagesTTL(cfg.Pubsub.Overlay.SeenTtl)) } + opts = append(opts, pubsub.WithAppSpecificRpcInspector(func(p peer.ID, rpc *pubsub.RPC) error { + c.pubsubRpcCounter.Add(1) + return nil + })) + denylist := pubsub.NewMapBlacklist() opts = append(opts, pubsub.WithBlacklist(denylist)) @@ -59,8 +62,18 @@ func (c *Controller) setupPubsubRouter(ctx context.Context, cfg commons.Config) opts = append(opts, pubsub.WithSubscriptionFilter(sf)) } - if cfg.Pubsub.Trace { - opts = append(opts, pubsub.WithEventTracer(newPubsubTracer(c.lggr.Named("PubsubTracer")))) + if cfg.Pubsub.Trace != nil { + var jtracer pubsub.EventTracer + if len(cfg.Pubsub.Trace.JsonFile) > 0 { + var err error + jtracer, err = pubsub.NewJSONTracer(cfg.Pubsub.Trace.JsonFile) + if err != nil { + return err + } + } + tracer := newPubsubTracer(c.lggr.Named("PubsubTracer"), cfg.Pubsub.Trace.Debug, cfg.Pubsub.Trace.Skiplist, jtracer) + c.psTracer = tracer.(*psTracer) + opts = append(opts, pubsub.WithEventTracer(tracer)) } ps, err := pubsub.NewGossipSub(ctx, c.host, opts...) @@ -79,7 +92,7 @@ func (c *Controller) Publish(ctx context.Context, topicName string, data []byte) if err != nil { return err } - // d.lggr.Debugw("publishing on topic", "topic", topicName, "data", string(data)) + c.lggr.Debugw("publishing on topic", "topic", topicName, "data", string(data)) return topic.Publish(ctx, data) } @@ -128,6 +141,28 @@ func (c *Controller) Subscribe(ctx context.Context, topicName string) error { return nil } +func (c *Controller) Relay(topicName string) error { + topic, err := c.tryJoin(topicName) + if err != nil { + return err + } + cancel, err := topic.Relay() + if err != nil { + return err + } + c.topicManager.setTopicRelayCancelFn(topicName, cancel) + return nil +} + +func (c *Controller) Unrelay(topicName string) error { + tw := c.topicManager.getTopicWrapper(topicName) + if tw.state.Load() == topicStateUnknown { + return nil // TODO: topic not found? + } + tw.relayCancelFn() + return nil +} + func (c *Controller) listenSubscription(ctx context.Context, sub *pubsub.Subscription) { c.lggr.Debugw("listening on topic", "topic", sub.Topic()) @@ -182,7 +217,7 @@ func (c *Controller) tryJoin(topicName string) (*pubsub.Topic, error) { if cfg.MsgValidator != nil || c.cfg.Pubsub.MsgValidator != nil { msgValConfig := (&commons.MsgValidationConfig{}).Defaults(c.cfg.Pubsub.MsgValidator) - if cfg.MsgValidator != nil { + if cfg.MsgValidator != nil { // specific topic validator config msgValConfig = msgValConfig.Defaults(cfg.MsgValidator) } valOpts := []pubsub.ValidatorOpt{ @@ -237,21 +272,3 @@ func (c *Controller) validateMsg(ctx context.Context, p peer.ID, msg *pubsub.Mes func (c *Controller) inspectPeerScores(map[peer.ID]*pubsub.PeerScoreSnapshot) { // TODO } - -// psTracer helps to trace pubsub events, implements pubsublibp2p.EventTracer -type psTracer struct { - lggr *zap.SugaredLogger -} - -// NewTracer creates an instance of pubsub tracer -func newPubsubTracer(lggr *zap.SugaredLogger) pubsub.EventTracer { - return &psTracer{ - lggr: lggr.Named("PubsubTracer"), - } -} - -// Trace handles events, implementation of pubsub.EventTracer -func (pst *psTracer) Trace(evt *pubsub_pb.TraceEvent) { - eType := evt.GetType().String() - pst.lggr.Debugw("pubsub event", "type", eType) -} diff --git a/core/pubsub_trace.go b/core/pubsub_trace.go new file mode 100644 index 0000000..cf71696 --- /dev/null +++ b/core/pubsub_trace.go @@ -0,0 +1,283 @@ +package core + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + "sync" + "time" + + pubsub "github.com/libp2p/go-libp2p-pubsub" + pubsub_pb "github.com/libp2p/go-libp2p-pubsub/pb" + "github.com/libp2p/go-libp2p/core/peer" + "go.uber.org/zap" +) + +type traceFaucets struct { + join, leave, publish, deliver, reject, duplicate, addPeer, removePeer, graft, prune, sendRPC, dropRPC, recvRPC int +} + +func (tf *traceFaucets) add(other traceFaucets) { + tf.join += other.join + tf.leave += other.leave + tf.publish += other.publish + tf.deliver += other.deliver + tf.reject += other.reject + tf.duplicate += other.duplicate + tf.addPeer += other.addPeer + tf.removePeer += other.removePeer + tf.graft += other.graft + tf.prune += other.prune + tf.sendRPC += other.sendRPC + tf.dropRPC += other.dropRPC + tf.recvRPC += other.recvRPC +} + +type eventFields map[string]string + +func MarshalTraceEvents(events []eventFields) ([]byte, error) { + return json.Marshal(events) +} + +func UnmarshalTraceEvents(data []byte) ([]eventFields, error) { + var events []eventFields + err := json.Unmarshal(data, &events) + return events, err +} + +// psTracer helps to trace pubsub events, implements pubsublibp2p.EventTracer +type psTracer struct { + lggr *zap.SugaredLogger + subTracer pubsub.EventTracer + skiplist []string + faucets traceFaucets + lock sync.Mutex + events []eventFields + debug bool +} + +// NewTracer creates an instance of pubsub tracer +func newPubsubTracer(lggr *zap.SugaredLogger, debug bool, skiplist []string, subTracer pubsub.EventTracer) pubsub.EventTracer { + return &psTracer{ + lggr: lggr.Named("PubsubTracer"), + subTracer: subTracer, + skiplist: skiplist, + debug: debug, + } +} + +func (pst *psTracer) Reset() { + pst.lock.Lock() + defer pst.lock.Unlock() + + pst.events = nil + pst.faucets = traceFaucets{} +} + +func (pst *psTracer) Events() []eventFields { + pst.lock.Lock() + defer pst.lock.Unlock() + + return pst.events +} + +func (pst *psTracer) Faucets() traceFaucets { + return pst.faucets +} + +// Trace handles events, implementation of pubsub.EventTracer +func (pst *psTracer) Trace(evt *pubsub_pb.TraceEvent) { + fields := eventFields{} + fields["type"] = evt.GetType().String() + fields["time"] = time.Now().Format(time.RFC3339) + pid, err := peer.IDFromBytes(evt.GetPeerID()) + if err != nil { + fields["peerID"] = "error" + } + fields["peerID"] = pid.String() + eventType := evt.GetType() + switch eventType { + case pubsub_pb.TraceEvent_PUBLISH_MESSAGE: + pst.faucets.publish++ + msg := evt.GetPublishMessage() + evt.GetPeerID() + fields["msgID"] = hex.EncodeToString(msg.GetMessageID()) + fields["topic"] = msg.GetTopic() + case pubsub_pb.TraceEvent_REJECT_MESSAGE: + pst.faucets.reject++ + msg := evt.GetRejectMessage() + pid, err := peer.IDFromBytes(msg.GetReceivedFrom()) + if err == nil { + fields["receivedFrom"] = pid.String() + } + fields["msgID"] = hex.EncodeToString(msg.GetMessageID()) + fields["topic"] = msg.GetTopic() + fields["reason"] = msg.GetReason() + case pubsub_pb.TraceEvent_DUPLICATE_MESSAGE: + pst.faucets.duplicate++ + msg := evt.GetDuplicateMessage() + pid, err := peer.IDFromBytes(msg.GetReceivedFrom()) + if err == nil { + fields["receivedFrom"] = pid.String() + } + fields["msgID"] = hex.EncodeToString(msg.GetMessageID()) + fields["topic"] = msg.GetTopic() + case pubsub_pb.TraceEvent_DELIVER_MESSAGE: + pst.faucets.deliver++ + msg := evt.GetDeliverMessage() + pid, err := peer.IDFromBytes(msg.GetReceivedFrom()) + if err == nil { + fields["receivedFrom"] = pid.String() + } + fields["msgID"] = hex.EncodeToString(msg.GetMessageID()) + fields["topic"] = msg.GetTopic() + case pubsub_pb.TraceEvent_ADD_PEER: + pst.faucets.addPeer++ + pid, err := peer.IDFromBytes(evt.GetAddPeer().GetPeerID()) + if err == nil { + fields["targetPeer"] = pid.String() + } + case pubsub_pb.TraceEvent_REMOVE_PEER: + pst.faucets.removePeer++ + pid, err := peer.IDFromBytes(evt.GetRemovePeer().GetPeerID()) + if err == nil { + fields["targetPeer"] = pid.String() + } + case pubsub_pb.TraceEvent_JOIN: + pst.faucets.join++ + fields["topic"] = evt.GetJoin().GetTopic() + case pubsub_pb.TraceEvent_LEAVE: + pst.faucets.leave++ + fields["topic"] = evt.GetLeave().GetTopic() + case pubsub_pb.TraceEvent_GRAFT: + pst.faucets.graft++ + msg := evt.GetGraft() + pid, err := peer.IDFromBytes(msg.GetPeerID()) + if err == nil { + fields["graftPeer"] = pid.String() + } + fields["topic"] = msg.GetTopic() + case pubsub_pb.TraceEvent_PRUNE: + pst.faucets.prune++ + msg := evt.GetPrune() + pid, err := peer.IDFromBytes(msg.GetPeerID()) + if err == nil { + fields["prunePeer"] = pid.String() + } + fields["topic"] = msg.GetTopic() + case pubsub_pb.TraceEvent_SEND_RPC: + pst.faucets.sendRPC++ + msg := evt.GetSendRPC() + pid, err := peer.IDFromBytes(msg.GetSendTo()) + if err == nil { + fields["targetPeer"] = pid.String() + } + if meta := msg.GetMeta(); meta != nil { + if ctrl := meta.Control; ctrl != nil { + fields = appendIHave(fields, ctrl.GetIhave()) + fields = appendIWant(fields, "self", ctrl.GetIwant()) + // ctrl.GetGraft() + // ctrl.GetPrune() + } + var subs []string + for _, sub := range meta.Subscription { + subs = append(subs, sub.GetTopic()) + } + fields["subs"] = strings.Join(subs, ",") + } + case pubsub_pb.TraceEvent_DROP_RPC: + pst.faucets.dropRPC++ + msg := evt.GetDropRPC() + pid, err := peer.IDFromBytes(msg.GetSendTo()) + if err == nil { + fields["targetPeer"] = pid.String() + } + case pubsub_pb.TraceEvent_RECV_RPC: + pst.faucets.recvRPC++ + msg := evt.GetRecvRPC() + pid, err := peer.IDFromBytes(msg.GetReceivedFrom()) + if err == nil { + fields["receivedFrom"] = pid.String() + } + if meta := msg.GetMeta(); meta != nil { + if ctrl := meta.Control; ctrl != nil { + fields = appendIHave(fields, ctrl.GetIhave()) + fields = appendIWant(fields, pid.String(), ctrl.GetIwant()) + } + var subs []string + for _, sub := range meta.Subscription { + subs = append(subs, sub.GetTopic()) + } + fields["subs"] = strings.Join(subs, ",") + } + default: + } + + if pst.shouldSkip(eventType.String()) { + return + } + + pst.debugEvent(fields) + pst.storeEvent(fields) + + if pst.subTracer != nil { + pst.subTracer.Trace(evt) + } +} + +func (pst *psTracer) debugEvent(fields eventFields) { + if pst.debug { + pst.lggr.Debugf("pubsub trace event: %+v", fields) + } +} + +func (pst *psTracer) storeEvent(fields eventFields) { + pst.lock.Lock() + defer pst.lock.Unlock() + + pst.events = append(pst.events, fields) +} + +func (pst *psTracer) shouldSkip(eventType string) bool { + pst.lock.Lock() + defer pst.lock.Unlock() + + for _, skip := range pst.skiplist { + if eventType == skip { + return true + } + } + return false +} + +func appendIHave(fields map[string]string, ihave []*pubsub_pb.TraceEvent_ControlIHaveMeta) map[string]string { + if len(ihave) > 0 { + fields["ihaveCount"] = strconv.Itoa(len(ihave)) + for _, im := range ihave { + var mids []string + msgids := im.GetMessageIDs() + for _, mid := range msgids { + mids = append(mids, hex.EncodeToString(mid)) + } + fields[fmt.Sprintf("%s-IHAVEmsgIDs", im.GetTopic())] = strings.Join(mids, ",") + } + } + return fields +} + +func appendIWant(fields map[string]string, peer string, iwant []*pubsub_pb.TraceEvent_ControlIWantMeta) map[string]string { + if len(iwant) > 0 { + fields["iwantCount"] = strconv.Itoa(len(iwant)) + var mids []string + for _, im := range iwant { + msgids := im.GetMessageIDs() + for _, mid := range msgids { + mids = append(mids, hex.EncodeToString(mid)) + } + } + fields[fmt.Sprintf("%s-IWANTmsgIDs", peer)] = strings.Join(mids, ",") + } + return fields +} diff --git a/core/testutils.go b/core/testutils.go index df48819..203a515 100644 --- a/core/testutils.go +++ b/core/testutils.go @@ -4,11 +4,11 @@ import ( "context" "fmt" "math/rand" - "sync/atomic" "testing" "time" pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/host" libp2pnetwork "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" "github.com/stretchr/testify/require" @@ -37,57 +37,68 @@ func SetupTestControllers(ctx context.Context, t *testing.T, n int, routingFn fu <-time.After(time.Second * 2) - hitMap := map[string]*atomic.Int32{} - for i := 0; i < n; i++ { - hitMap[fmt.Sprintf("test-%d", i+1)] = &atomic.Int32{} + gen := &DefaultGenerator{ + BootAddr: fmt.Sprintf("%s/p2p/%s", bootAddr, boot.host.ID()), + RoutingFn: routingFn, + ValidationFn: valFn, } + controllers, msgRouters, valRouters, done, err := StartControllers(ctx, n, gen) + require.NoError(t, err) + + t.Logf("created %d controllers", n) - controllers := make([]*Controller, n) - msgRouters := make([]MsgRouter[error], n) - valRouters := make([]MsgRouter[pubsub.ValidationResult], n) + waitControllersConnected(n) + + return controllers, msgRouters, valRouters, func() { + done() + boot.Close() + } +} + +func StartControllers(ctx context.Context, n int, gen Generator) ([]*Controller, []MsgRouter[error], []MsgRouter[pubsub.ValidationResult], func(), error) { + controllers := make([]*Controller, 0, n) + msgRouters := make([]MsgRouter[error], 0, n) + valRouters := make([]MsgRouter[pubsub.ValidationResult], 0, n) + done := func() { + for _, c := range controllers { + c.Close() + } + } for i := 0; i < n; i++ { - cfg := commons.Config{ - ListenAddrs: []string{ - "/ip4/127.0.0.1/tcp/0", - }, - // MdnsTag: "p2pmq/mdns/test", - Discovery: &commons.DiscoveryConfig{ - Mode: commons.ModeServer, - ProtocolPrefix: "p2pmq/kad/test", - Bootstrappers: []string{ - fmt.Sprintf("%s/p2p/%s", bootAddr, boot.host.ID()), - }, - }, - Pubsub: &commons.PubsubConfig{ - MsgValidator: &commons.MsgValidationConfig{}, - }, + cfg, msgRouter, valRouter, name := gen.NextConfig(i) + c, err := NewController(ctx, cfg, msgRouter, valRouter, name) + if err != nil { + return controllers, msgRouters, valRouters, done, err } - msgRouter := NewMsgRouter(1024, 4, func(mw *MsgWrapper[error]) { - routingFn(mw.Msg) - }, gossip.DefaultMsgIDFn) - valRouter := NewMsgRouter(1024, 4, func(mw *MsgWrapper[pubsub.ValidationResult]) { - res := valFn(mw.Peer, mw.Msg) - mw.Result = res - }, gossip.DefaultMsgIDFn) - c, err := NewController(ctx, cfg, msgRouter, valRouter, fmt.Sprintf("peer-%d", i+1)) - require.NoError(t, err) - controllers[i] = c - msgRouters[i] = msgRouter - valRouters[i] = valRouter - t.Logf("created controller %d: %s", i+1, c.host.ID()) + controllers = append(controllers, c) + msgRouters = append(msgRouters, msgRouter) + valRouters = append(valRouters, valRouter) } - for i, c := range controllers { + for _, c := range controllers { c.Start(ctx) - t.Logf("started controller %d: %s", i+1, c.host.ID()) } - waitControllersConnected(n) + return controllers, msgRouters, valRouters, done, nil +} - return controllers, msgRouters, valRouters, func() { - go boot.Close() // closing bootstrapper in the background - for _, c := range controllers { - c.Close() +func waitMinConnected(ctx context.Context, minConnected func(i int) int, backoff time.Duration, hosts ...host.Host) { + for i, h := range hosts { + connected := make([]peer.ID, 0) + min := minConnected(i) + for len(connected) < min && ctx.Err() == nil { + peers := h.Network().Peers() + for _, pid := range peers { + switch h.Network().Connectedness(pid) { + case libp2pnetwork.Connected: + connected = append(connected, pid) + default: + } + } + if len(connected) < min { + fmt.Printf("host %s connected to %d peers, waiting for %d\n", h.ID(), len(connected), min) + } + time.Sleep(backoff) } } } @@ -108,3 +119,44 @@ func waitControllersConnected(n int, controllers ...*Controller) { } } } + +type Generator interface { + NextConfig(i int) (commons.Config, MsgRouter[error], MsgRouter[pubsub.ValidationResult], string) +} + +type DefaultGenerator struct { + BootAddr string + RoutingFn func(*pubsub.Message) + ValidationFn func(peer.ID, *pubsub.Message) pubsub.ValidationResult +} + +func (g *DefaultGenerator) NextConfig(i int) (commons.Config, MsgRouter[error], MsgRouter[pubsub.ValidationResult], string) { + cfg := commons.Config{ + ListenAddrs: []string{ + "/ip4/127.0.0.1/tcp/0", + }, + MdnsTag: "p2pmq/mdns/test", + // Discovery: &commons.DiscoveryConfig{ + // Mode: commons.ModeServer, + // ProtocolPrefix: "p2pmq/kad/test", + // Bootstrappers: []string{ + // g.BootAddr, + // // fmt.Sprintf("%s/p2p/%s", g.BootAddr, boot.host.ID()), + // }, + // }, + Pubsub: &commons.PubsubConfig{ + MsgValidator: &commons.MsgValidationConfig{}, + }, + } + + msgRouter := NewMsgRouter(1024, 4, func(mw *MsgWrapper[error]) { + g.RoutingFn(mw.Msg) + }, gossip.DefaultMsgIDFn) + + valRouter := NewMsgRouter(1024, 4, func(mw *MsgWrapper[pubsub.ValidationResult]) { + res := g.ValidationFn(mw.Peer, mw.Msg) + mw.Result = res + }, gossip.DefaultMsgIDFn) + + return cfg, msgRouter, valRouter, fmt.Sprintf("node-%d", i+1) +} diff --git a/core/topic.go b/core/topic.go index 33a812e..4e559e2 100644 --- a/core/topic.go +++ b/core/topic.go @@ -19,9 +19,10 @@ func newTopicManager() *topicManager { } type topicWrapper struct { - state atomic.Int32 - topic *pubsub.Topic - sub *pubsub.Subscription + state atomic.Int32 + topic *pubsub.Topic + sub *pubsub.Subscription + relayCancelFn pubsub.RelayCancelFunc } const ( @@ -57,6 +58,20 @@ func (tm *topicManager) upgradeTopic(name string, topic *pubsub.Topic) bool { return true } +func (tm *topicManager) setTopicRelayCancelFn(name string, fn pubsub.RelayCancelFunc) bool { + tm.lock.Lock() + defer tm.lock.Unlock() + + tw, ok := tm.topics[name] + if !ok { + return false + } + tw.relayCancelFn = fn + tm.topics[name] = tw + + return true +} + func (tm *topicManager) getTopicWrapper(topic string) *topicWrapper { tm.lock.RLock() defer tm.lock.RUnlock() From 0a039b1aa3124cc86066840a34727bd52ef1f768 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:15:40 +0300 Subject: [PATCH 05/19] align gossipsub tests --- core/gossipsub_test.go | 606 ++++++++++++++++++++++------------------- 1 file changed, 328 insertions(+), 278 deletions(-) diff --git a/core/gossipsub_test.go b/core/gossipsub_test.go index e670a03..2f3dc99 100644 --- a/core/gossipsub_test.go +++ b/core/gossipsub_test.go @@ -3,9 +3,9 @@ package core import ( "context" "fmt" - "strconv" "strings" "sync" + "sync/atomic" "testing" "text/template" "time" @@ -43,23 +43,82 @@ func TestGossipMsgThroughput(t *testing.T) { defer cancel() require.NoError(t, logging.SetLogLevelRegex("p2pmq", "error")) - groupsCfgSimple, groupsCfgMedium := testGroupSimple(), testGroupMedium() + // groupsCfgSimple11 := testGroupSimple(11, 4, 6, 1) + groupsCfgSimple18 := testGroupSimple(18, 10, 6, 2) + groupsCfgSimple36 := testGroupSimple(36, 16, 16, 4) + groupsCfgSimple54 := testGroupSimple(54, 32, 16, 6) + + benchFlows := []flow{ + // flowTrigger("a", 1, time.Millisecond*10), + flowActionA2B(1, time.Millisecond*1, time.Millisecond*250), + flowActionA2B(10, time.Millisecond*1, time.Millisecond*1), + // // flowActionA2B(50, time.Millisecond*1, time.Millisecond*1), + flowActionA2B(100, time.Millisecond*1, time.Millisecond*10), + flowActionA2B(1000, time.Millisecond*10, time.Millisecond*10), + flowTrigger("b", 1, time.Millisecond*10), + flowTrigger("b", 10, time.Millisecond*1), + // flowTrigger("b", 50, time.Millisecond*1), + flowTrigger("b", 100, time.Millisecond*10), + flowTrigger("b", 1000, time.Millisecond*10), + flowTrigger("a", 1, time.Millisecond*10), + flowTrigger("a", 10, time.Millisecond*10), + // // flowTrigger("b", 50, time.Millisecond*1), + flowTrigger("a", 100, time.Millisecond*10), + flowTrigger("a", 1000, time.Millisecond*10), + } tests := []struct { - name string - n int - pubsubConfig *commons.PubsubConfig - gen *testGen - groupsCfg map[string]groupCfg - conns connectivity - flows []flow - topicsToCheck []string + name string + n int + pubsubConfig *commons.PubsubConfig + gen *testGen + groupsCfg groupsCfg + conns connectivity + flows []flow }{ + // { + // name: "simple_11", + // n: 11, + // gen: &testGen{ + // // hitMaps: map[string]*nodeHitMap{}, + // routingFn: func(m *pubsub.Message) {}, + // validationFn: func(p peer.ID, m *pubsub.Message) pubsub.ValidationResult { + // return pubsub.ValidationAccept + // }, + // pubsubConfig: &commons.PubsubConfig{ + // MsgValidator: &commons.MsgValidationConfig{}, + // Trace: &commons.PubsubTraceConfig{ + // // JsonFile: fmt.Sprintf("../.output/trace/node-%d.json", i+1), + // Skiplist: traceEmptySkipList, + // }, + // Overlay: &commons.OverlayParams{ + // D: 3, + // Dlow: 2, + // Dhi: 5, + // Dlazy: 3, + // }, + // }, + // }, + // groupsCfg: groupsCfgSimple11, + // conns: func() connectivity { + // conns := groupsCfgSimple11.baseConnectivity() + // // add some extra connections between a and b + // conns[0] = append(conns[0], 5, 7, 9) + // conns[1] = append(conns[1], 4, 6, 8) + // conns[2] = append(conns[2], 4, 5, 7) + // conns[3] = append(conns[3], 4, 6, 8) + // return conns + // }(), + // flows: []flow{ + // flowActionA2B(1, time.Millisecond*1, time.Millisecond*250), + // flowTrigger("b", 2, time.Millisecond*10), + // }, + // }, { - name: "simple_11", - n: 11, + name: "simple_18", + n: 18, gen: &testGen{ - hitMaps: map[string]*nodeHitMap{}, + // hitMaps: map[string]*nodeHitMap{}, routingFn: func(m *pubsub.Message) {}, validationFn: func(p peer.ID, m *pubsub.Message) pubsub.ValidationResult { return pubsub.ValidationAccept @@ -67,7 +126,6 @@ func TestGossipMsgThroughput(t *testing.T) { pubsubConfig: &commons.PubsubConfig{ MsgValidator: &commons.MsgValidationConfig{}, Trace: &commons.PubsubTraceConfig{ - // JsonFile: fmt.Sprintf("../.output/trace/node-%d.json", i+1), Skiplist: traceEmptySkipList, }, Overlay: &commons.OverlayParams{ @@ -78,67 +136,15 @@ func TestGossipMsgThroughput(t *testing.T) { }, }, }, - groupsCfg: groupsCfgSimple, - conns: map[int][]int{ - 0: append(append(groupsCfgSimple["a"].ids, groupsCfgSimple["relayers"].ids...), - 5, 7, 9, // group b - ), - 1: append(append(groupsCfgSimple["a"].ids, groupsCfgSimple["relayers"].ids...), - 4, 6, 8, // group b - ), - 2: append(append(groupsCfgSimple["a"].ids, groupsCfgSimple["relayers"].ids...), - 4, 5, 7, // group b - ), - 3: append(append(groupsCfgSimple["a"].ids, groupsCfgSimple["relayers"].ids...), - 4, 6, 8, // group b - ), - 4: append(groupsCfgSimple["b"].ids, groupsCfgSimple["relayers"].ids...), - 5: append(groupsCfgSimple["b"].ids, groupsCfgSimple["relayers"].ids...), - 6: append(groupsCfgSimple["b"].ids, groupsCfgSimple["relayers"].ids...), - 7: append(groupsCfgSimple["b"].ids, groupsCfgSimple["relayers"].ids...), - 8: append(groupsCfgSimple["b"].ids, groupsCfgSimple["relayers"].ids...), - 9: append(groupsCfgSimple["b"].ids, groupsCfgSimple["relayers"].ids...), - }, - flows: []flow{ - { - name: "actions a->b", - events: []flowEvent{ - { - srcGroup: "a", - topic: "b.action.req", - pattern: "dummy-request-{{.i}}", - interval: time.Millisecond * 10, - wait: true, - }, - { - srcGroup: "b", - topic: "b.action.res", - pattern: "dummy-response-{{.i}}", - }, - }, - iterations: 1, - interval: time.Millisecond * 250, - }, - { - name: "triggers b", - events: []flowEvent{ - { - srcGroup: "b", - topic: "b.trigger", - pattern: "dummy-trigger-{{.group}}-{{.i}}", - interval: time.Millisecond * 10, - }, - }, - iterations: 2, - interval: time.Millisecond * 10, - }, - }, + groupsCfg: groupsCfgSimple18, + conns: groupsCfgSimple18.allToAllConnectivity(), + flows: benchFlows[:], }, { - name: "medium_29", - n: 29, + name: "simple_36", + n: 36, gen: &testGen{ - hitMaps: map[string]*nodeHitMap{}, + // hitMaps: map[string]*nodeHitMap{}, routingFn: func(m *pubsub.Message) {}, validationFn: func(p peer.ID, m *pubsub.Message) pubsub.ValidationResult { return pubsub.ValidationAccept @@ -146,153 +152,68 @@ func TestGossipMsgThroughput(t *testing.T) { pubsubConfig: &commons.PubsubConfig{ MsgValidator: &commons.MsgValidationConfig{}, Trace: &commons.PubsubTraceConfig{ - // JsonFile: fmt.Sprintf("../.output/trace/node-%d.json", i+1), Skiplist: traceEmptySkipList, }, Overlay: &commons.OverlayParams{ - D: 3, + D: 4, Dlow: 2, - Dhi: 5, + Dhi: 6, Dlazy: 3, }, }, }, - groupsCfg: groupsCfgMedium, - conns: map[int][]int{ - 0: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), - 10, 11, 12, // group b - 20, 21, 22, // group c - ), - 1: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), - 14, 11, 15, // group b - 24, 21, 25, // group c - ), - 2: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), - 13, 12, 16, // group b - 23, 22, 26, // group c - ), - 3: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), - 14, 15, 17, // group b - 24, 25, 27, // group c - ), - 4: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), - 10, 11, 12, // group b - 20, 21, 22, // group c - ), - 5: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), - 14, 11, 15, // group b - 21, 20, // group c - ), - 6: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), - 13, 12, 16, // group b - 23, 22, // group c - ), - 7: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), - 14, 15, 17, // group b - 25, // group c - ), - 8: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), - 10, 11, 12, // group b - 20, 21, // group c - ), - 9: append(append(groupsCfgMedium["a"].ids, groupsCfgMedium["relayers"].ids...), - 14, 19, 15, // group b - 24, // group c - ), - 10: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), - 24, 22, // group c - ), - 11: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), - 22, // group c - ), - 12: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), - 22, 23, // group c - ), - 13: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), - 24, 21, // group c - ), - 14: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), - 20, // group c - ), - 15: append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), - 16: append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), - 17: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), - 22, // group c - ), - 18: append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), - 19: append(append(groupsCfgMedium["b"].ids, groupsCfgMedium["relayers"].ids...), - 20, // group c - ), - 20: append(groupsCfgMedium["c"].ids, groupsCfgMedium["relayers"].ids...), - 21: append(groupsCfgMedium["c"].ids, groupsCfgMedium["relayers"].ids...), - 22: append(groupsCfgMedium["c"].ids, groupsCfgMedium["relayers"].ids...), - 23: append(groupsCfgMedium["c"].ids, groupsCfgMedium["relayers"].ids...), - 24: append(groupsCfgMedium["c"].ids, groupsCfgMedium["relayers"].ids...), - 25: append(groupsCfgMedium["c"].ids, groupsCfgMedium["relayers"].ids...), - 26: groupsCfgMedium["relayers"].ids, - 27: groupsCfgMedium["relayers"].ids, - 28: groupsCfgMedium["relayers"].ids, - }, - flows: []flow{ - { - name: "actions a->b", - events: []flowEvent{ - { - srcGroup: "a", - topic: "b.action.req", - pattern: "dummy-request-{{.i}}", - interval: time.Millisecond * 10, - wait: true, - }, - { - srcGroup: "b", - topic: "b.action.res", - pattern: "dummy-response-{{.i}}", - }, - }, - iterations: 5, - interval: time.Millisecond * 250, + groupsCfg: groupsCfgSimple36, + conns: groupsCfgSimple36.allToAllConnectivity(), + flows: benchFlows[:], + }, + { + name: "simple_36_with_default_overlay_params", + n: 36, + gen: &testGen{ + // hitMaps: map[string]*nodeHitMap{}, + routingFn: func(m *pubsub.Message) {}, + validationFn: func(p peer.ID, m *pubsub.Message) pubsub.ValidationResult { + return pubsub.ValidationAccept }, - { - name: "triggers b", - events: []flowEvent{ - { - srcGroup: "b", - topic: "b.trigger", - pattern: "dummy-trigger-{{.group}}-{{.i}}", - interval: time.Millisecond * 10, - }, + pubsubConfig: &commons.PubsubConfig{ + MsgValidator: &commons.MsgValidationConfig{}, + Trace: &commons.PubsubTraceConfig{ + Skiplist: traceEmptySkipList, }, - iterations: 10, - interval: time.Millisecond * 10, }, - { - name: "triggers b+c", - events: []flowEvent{ - { - srcGroup: "b", - topic: "b.trigger", - pattern: "xdummy-trigger-{{.group}}-{{.i}}", - interval: time.Millisecond * 10, - }, - { - srcGroup: "c", - topic: "c.trigger", - pattern: "xdummy-trigger-{{.group}}-{{.i}}", - interval: time.Millisecond * 10, - }, + }, + groupsCfg: groupsCfgSimple36, + conns: groupsCfgSimple36.allToAllConnectivity(), + flows: benchFlows[:], + }, + { + name: "simple_54_with_default_overlay_params", + n: 54, + gen: &testGen{ + // hitMaps: map[string]*nodeHitMap{}, + routingFn: func(m *pubsub.Message) {}, + validationFn: func(p peer.ID, m *pubsub.Message) pubsub.ValidationResult { + return pubsub.ValidationAccept + }, + pubsubConfig: &commons.PubsubConfig{ + MsgValidator: &commons.MsgValidationConfig{}, + Trace: &commons.PubsubTraceConfig{ + Skiplist: traceEmptySkipList, }, - iterations: 10, - interval: time.Millisecond * 10, }, }, + groupsCfg: groupsCfgSimple54, + conns: groupsCfgSimple54.allToAllConnectivity(), + flows: benchFlows[:], }, } + var outputs []gossipTestOutput for _, tt := range tests { tc := tt t.Run(tc.name, func(t *testing.T) { - ctrls, _, _, done, err := StartControllers(ctx, tc.n, tc.gen) + tgen := tc.gen + ctrls, _, _, done, err := StartControllers(ctx, tc.n, tgen) defer done() require.NoError(t, err) @@ -320,17 +241,40 @@ func TestGossipMsgThroughput(t *testing.T) { } } + // waiting for nodes to setup subscriptions <-time.After(time.Second * 4) // TODO: avoid timeout // starting fresh trace after subscriptions startupFaucets := traceFaucets{} + pubsubRpcCount := new(atomic.Uint64) for _, ctrl := range ctrls { startupFaucets.add(ctrl.psTracer.faucets) ctrl.psTracer.Reset() + pubsubRpcCount.Store(ctrl.pubsubRpcCounter.Swap(0)) + } + // t.Logf("\n [%s] startup in_rpc_count: %d\n; trace faucets: %+v\n", tc.name, startupFaucets, pubsubRpcCount.Load()) + + d := uint(6) + if tgen.pubsubConfig != nil && tgen.pubsubConfig.Overlay != nil { + d = uint(tgen.pubsubConfig.Overlay.D) } - t.Logf("\n [%s] all trace faucets (startup): %+v\n", tc.name, startupFaucets) + baseTestOutput := gossipTestOutput{ + Name: tc.name, + N: uint(tc.n), + A: uint(len(groups["a"])), + B: uint(len(groups["b"])), + R: uint(len(groups["relayers"])), + D: uint(d), + } + + startupTestOutput := baseTestOutput + startupTestOutput.Iterations = 1 + startupTestOutput.InboundRPC = pubsubRpcCount.Load() + startupTestOutput.Name = fmt.Sprintf("%s-startup", tc.name) + outputs = append(outputs, startupTestOutput) t.Log("starting flows...") + for _, f := range tc.flows { flow := f flowTestName := fmt.Sprintf("%s-x%d", flow.name, flow.iterations) @@ -338,6 +282,8 @@ func TestGossipMsgThroughput(t *testing.T) { threadCtrl := utils.NewThreadControl() defer threadCtrl.Close() + start := time.Now() + for i := 0; i < flow.iterations; i++ { for _, e := range flow.events { event := e @@ -349,53 +295,70 @@ func TestGossipMsgThroughput(t *testing.T) { for _, c := range group { _i := i ctrl := c + ctrlName := strings.Replace(ctrl.lggr.Desugar().Name(), ".ctrl", "", 1) + ctrlName = strings.Replace(ctrlName, "p2pmq.", "", 1) wg.Add(1) threadCtrl.Go(func(ctx context.Context) { defer wg.Done() args := map[string]interface{}{ "i": _i, "group": event.srcGroup, - "ctrl": ctrl.lggr.Desugar().Name(), + "ctrl": ctrlName, "flow": flow.name, } - require.NoError(t, ctrl.Publish(ctx, event.topic, []byte(event.Msg(args)))) + msg := event.Msg(args) + require.NoError(t, ctrl.Publish(ctx, event.topic, []byte(msg))) + // msgID := gossip.DefaultMsgIDFn(&pubsub_pb.Message{Data: []byte(msg)}) + // hmap.addSent(msgID) }) } if event.wait { wg.Wait() } + if event.interval > 0 { + <-time.After(event.interval) + } } <-time.After(flow.interval) } - faucets := traceFaucets{} - for node, hitMap := range tc.gen.hitMaps { - for _, topic := range tc.topicsToCheck { - t.Logf("[%s] %s messages: %d; validations: %d", node, topic, hitMap.messages(topic), hitMap.validations(topic)) - } + <-time.After(time.Second * 2) // TODO: avoid timeout - nodeIndex, err := strconv.Atoi(node[5:]) - require.NoError(t, err) - tracer := ctrls[nodeIndex-1].psTracer - nodeFaucets := tracer.faucets - t.Logf("[%s] trace faucets: %+v", node, nodeFaucets) - faucets.add(nodeFaucets) - // traceEvents := tracer.Events() - // eventsJson, err := MarshalTraceEvents(traceEvents) - // require.NoError(t, err) - // require.NoError(t, os.WriteFile(fmt.Sprintf("../.output/trace/%s-%s-%s.json", tc.name, node, flow.name), eventsJson, 0644)) - tracer.Reset() - } - for _, ctrl := range groups["relayers"] { + faucets := traceFaucets{} + pubsubRpcCount := new(atomic.Uint64) + for _, ctrl := range ctrls { nodeFaucets := ctrl.psTracer.faucets - t.Logf("[%s] trace faucets: %+v", ctrl.lggr.Desugar().Name(), nodeFaucets) + // t.Logf("[%s] trace faucets: %+v", ctrl.lggr.Desugar().Name(), nodeFaucets) faucets.add(nodeFaucets) + pubsubRpcCount.Add(ctrl.pubsubRpcCounter.Swap(0)) + ctrl.psTracer.Reset() } - t.Logf("\n [%s/%s] all trace faucets: %+v\n", tc.name, flowTestName, faucets) + testOutput := baseTestOutput + testOutput.Name = flow.name + testOutput.Iterations = uint(flow.iterations) + testOutput.Faucets = msgTraceFaucetsOutput{ + Publish: faucets.publish, + Deliver: faucets.deliver, + Reject: faucets.reject, + DropRPC: faucets.dropRPC, + SendRPC: faucets.sendRPC, + RecvRPC: faucets.recvRPC, + } + testOutput.InboundRPC = pubsubRpcCount.Load() + testOutput.TotalTime = time.Since(start) + outputs = append(outputs, testOutput) + t.Logf("output: %+v", testOutput) }) } }) } + + // outDir := t.TempDir() // ../.output + // outputsJson, err := json.Marshal(outputs) + // require.NoError(t, err) + // outputFileName := fmt.Sprintf("%s/test-%d.json", outDir, time.Now().UnixMilli()) + // require.NoError(t, os.WriteFile(outputFileName, outputsJson, 0644)) + // t.Logf("outputs saved in %s", outputFileName) } type connectivity map[int][]int @@ -415,47 +378,123 @@ func (connect connectivity) connect(ctx context.Context, ctrls []*Controller) er return nil } +type msgTraceFaucetsOutput struct { + Publish int `json:"publish,omitempty"` + Deliver int `json:"deliver,omitempty"` + Reject int `json:"reject,omitempty"` + DropRPC int `json:"drop_rpc,omitempty"` + SendRPC int `json:"send_rpc,omitempty"` + RecvRPC int `json:"recv_rpc,omitempty"` +} + +type gossipTestOutput struct { + Name string + N, A, B, R, D uint + Iterations uint + Faucets msgTraceFaucetsOutput + InboundRPC uint64 + TotalTime time.Duration +} + +type groupsCfg map[string]groupCfg + +// baseConnectivity returns a base connectivity map for the groups, +// where each group member is connected to all other members of the group +// and to the relayers. +func (groups groupsCfg) baseConnectivity() connectivity { + conns := make(connectivity) + var relayerIDs []int + relayers, ok := groups["relayers"] + if ok { + relayerIDs = relayers.ids + } + for _, cfg := range groups { + connectIDs := append(cfg.ids, relayerIDs...) + for _, i := range cfg.ids { + conns[i] = connectIDs + } + } + return conns +} + +func (groups groupsCfg) allToAllConnectivity() connectivity { + conns := make(connectivity) + var allIDs []int + for _, cfg := range groups { + allIDs = append(allIDs, cfg.ids...) + } + for _, id := range allIDs { + conns[id] = allIDs + } + return conns +} + type groupCfg struct { ids []int subs []string relays []string } -func testGroupSimple() map[string]groupCfg { - return map[string]groupCfg{ +// testGroupSimple creates a simple test group configuration with n nodes: +// group a: a nodes +// group b: b nodes +// relayers: r nodes +// NOTE: n >= a + b + r must hold +func testGroupSimple(n, a, b, r int) groupsCfg { + ids := make([]int, n) + for i := 0; i < n; i++ { + ids[i] = i + } + return groupsCfg{ "a": { - ids: []int{0, 1, 2, 3}, + ids: ids[:a], subs: []string{"b.action.res", "b.trigger"}, }, "b": { - ids: []int{4, 5, 6, 7, 8, 9}, - subs: []string{"b.action.req"}, + ids: ids[a : a+b], + subs: []string{"a.trigger", "b.action.req"}, }, "relayers": { - ids: []int{10}, - relays: []string{"b.action.req", "b.action.res", "b.trigger"}, + ids: ids[a+b : a+b+r], + relays: []string{"a.trigger", "b.action.req", "b.action.res", "b.trigger"}, }, } } -func testGroupMedium() map[string]groupCfg { - return map[string]groupCfg{ - "a": { - ids: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, - subs: []string{"b.action.res", "b.trigger", "c.trigger"}, - }, - "b": { - ids: []int{10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, - subs: []string{"b.action.req", "c.trigger"}, - }, - "c": { - ids: []int{20, 21, 22, 23, 24, 25}, - subs: []string{"b.trigger"}, +func flowActionA2B(iterations int, interval, waitAfterReq time.Duration) flow { + return flow{ + name: "action a->b", + events: []flowEvent{ + { + srcGroup: "a", + topic: "b.action.req", + pattern: "dummy-request-{{.group}}-{{.i}}", + interval: waitAfterReq, + wait: true, + }, + { + srcGroup: "b", + topic: "b.action.res", + pattern: "dummy-response-{{.group}}-{{.i}}", + }, }, - "relayers": { - ids: []int{26, 27, 28}, - relays: []string{"b.action.req", "b.action.res", "b.trigger", "c.trigger"}, + iterations: iterations, + interval: interval, + } +} + +func flowTrigger(src string, iterations int, interval time.Duration) flow { + return flow{ + name: fmt.Sprintf("trigger %s", src), + events: []flowEvent{ + { + srcGroup: src, + topic: fmt.Sprintf("%s.trigger", src), + pattern: "dummy-trigger-{{.group}}-{{.i}}", + }, }, + iterations: iterations, + interval: interval, } } @@ -487,42 +526,51 @@ type flow struct { interval time.Duration } -type nodeHitMap struct { - lock sync.RWMutex - valHitMap map[string]uint32 - msgHitMap map[string]uint32 -} +// type nodeHitMap struct { +// lock sync.RWMutex +// valHitMap map[string]uint32 +// msgHitMap map[string]uint32 +// sent, recieved map[string]time.Time +// } -func (n *nodeHitMap) validations(topic string) uint32 { - n.lock.RLock() - defer n.lock.RUnlock() +// func (n *nodeHitMap) validations(topic string) uint32 { +// n.lock.RLock() +// defer n.lock.RUnlock() - return n.valHitMap[topic] -} +// return n.valHitMap[topic] +// } -func (n *nodeHitMap) messages(topic string) uint32 { - n.lock.RLock() - defer n.lock.RUnlock() +// func (n *nodeHitMap) messages(topic string) uint32 { +// n.lock.RLock() +// defer n.lock.RUnlock() - return n.msgHitMap[topic] -} +// return n.msgHitMap[topic] +// } -func (n *nodeHitMap) addValidation(topic string) { - n.lock.Lock() - defer n.lock.Unlock() +// func (n *nodeHitMap) addValidation(topic string) { +// n.lock.Lock() +// defer n.lock.Unlock() - n.valHitMap[topic] += 1 -} +// n.valHitMap[topic] += 1 +// } -func (n *nodeHitMap) addMessage(topic string) { - n.lock.Lock() - defer n.lock.Unlock() +// func (n *nodeHitMap) addMessage(topic, msgID string) { +// n.lock.Lock() +// defer n.lock.Unlock() - n.msgHitMap[topic] += 1 -} +// n.msgHitMap[topic] += 1 +// n.recieved[msgID] = time.Now() +// } + +// func (n *nodeHitMap) addSent(msgID string) { +// n.lock.Lock() +// defer n.lock.Unlock() + +// n.sent[msgID] = time.Now() +// } type testGen struct { - hitMaps map[string]*nodeHitMap + // hitMaps map[string]*nodeHitMap routingFn func(*pubsub.Message) validationFn func(peer.ID, *pubsub.Message) pubsub.ValidationResult pubsubConfig *commons.PubsubConfig @@ -538,19 +586,21 @@ func (g *testGen) NextConfig(i int) (commons.Config, MsgRouter[error], MsgRouter name := fmt.Sprintf("node-%d", i+1) - hitMap := &nodeHitMap{ - valHitMap: make(map[string]uint32), - msgHitMap: make(map[string]uint32), - } - g.hitMaps[name] = hitMap + // hitMap := &nodeHitMap{ + // valHitMap: make(map[string]uint32), + // msgHitMap: make(map[string]uint32), + // recieved: make(map[string]time.Time), + // sent: make(map[string]time.Time), + // } + // g.hitMaps[name] = hitMap msgRouter := NewMsgRouter(1024, 4, func(mw *MsgWrapper[error]) { - hitMap.addMessage(mw.Msg.GetTopic()) + // hitMap.addMessage(mw.Msg.GetTopic(), mw.Msg.ID) g.routingFn(mw.Msg) }, gossip.DefaultMsgIDFn) valRouter := NewMsgRouter(1024, 4, func(mw *MsgWrapper[pubsub.ValidationResult]) { - hitMap.addValidation(mw.Msg.GetTopic()) + // hitMap.addValidation(mw.Msg.GetTopic()) res := g.validationFn(mw.Peer, mw.Msg) mw.Result = res }, gossip.DefaultMsgIDFn) From cf2e37bd03b42bc68157614737be8d7e0df9bc51 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:15:51 +0300 Subject: [PATCH 06/19] update gitignore --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 283fb37..ca28aa4 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,9 @@ go.work.sum bin cover.out -cover.html \ No newline at end of file +cover.html + +.output + +.DS_Store +*.log \ No newline at end of file From 5cfa510b752958b9a7384f17bed1f84c29accd05 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:20:37 +0300 Subject: [PATCH 07/19] put rpc counter inside trace --- core/pubsub.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/pubsub.go b/core/pubsub.go index 8f87078..2efaf7f 100644 --- a/core/pubsub.go +++ b/core/pubsub.go @@ -41,11 +41,6 @@ func (c *Controller) setupPubsubRouter(ctx context.Context, cfg commons.Config) opts = append(opts, pubsub.WithSeenMessagesTTL(cfg.Pubsub.Overlay.SeenTtl)) } - opts = append(opts, pubsub.WithAppSpecificRpcInspector(func(p peer.ID, rpc *pubsub.RPC) error { - c.pubsubRpcCounter.Add(1) - return nil - })) - denylist := pubsub.NewMapBlacklist() opts = append(opts, pubsub.WithBlacklist(denylist)) @@ -74,6 +69,11 @@ func (c *Controller) setupPubsubRouter(ctx context.Context, cfg commons.Config) tracer := newPubsubTracer(c.lggr.Named("PubsubTracer"), cfg.Pubsub.Trace.Debug, cfg.Pubsub.Trace.Skiplist, jtracer) c.psTracer = tracer.(*psTracer) opts = append(opts, pubsub.WithEventTracer(tracer)) + // TODO: config? + opts = append(opts, pubsub.WithAppSpecificRpcInspector(func(p peer.ID, rpc *pubsub.RPC) error { + c.pubsubRpcCounter.Add(1) + return nil + })) } ps, err := pubsub.NewGossipSub(ctx, c.host, opts...) From 341c6433eacd2628e50ab6992a659c70b66f6f07 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:51:23 +0300 Subject: [PATCH 08/19] msg args --- core/gossipsub_test.go | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/core/gossipsub_test.go b/core/gossipsub_test.go index 2f3dc99..3a81b16 100644 --- a/core/gossipsub_test.go +++ b/core/gossipsub_test.go @@ -300,13 +300,12 @@ func TestGossipMsgThroughput(t *testing.T) { wg.Add(1) threadCtrl.Go(func(ctx context.Context) { defer wg.Done() - args := map[string]interface{}{ - "i": _i, - "group": event.srcGroup, - "ctrl": ctrlName, - "flow": flow.name, - } - msg := event.Msg(args) + msg := event.Msg(msgArgs{ + i: _i, + group: event.srcGroup, + ctrl: ctrlName, + flow: flow.name, + }) require.NoError(t, ctrl.Publish(ctx, event.topic, []byte(msg))) // msgID := gossip.DefaultMsgIDFn(&pubsub_pb.Message{Data: []byte(msg)}) // hmap.addSent(msgID) @@ -506,12 +505,18 @@ type flowEvent struct { wait bool } -func (fe flowEvent) Msg(args map[string]interface{}) string { +type msgArgs struct { + i int + group string + ctrl string + flow string +} + +func (fe flowEvent) Msg(args msgArgs) string { tmpl, err := template.New("msg").Parse(fe.pattern) if err != nil { return "" } - // create io.Buffer for the message and executing template sb := new(strings.Builder) if err := tmpl.Execute(sb, args); err != nil { return "" From bb6a3d3172265a37756b1927d9bcb46b5cfc1949 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:32:42 +0300 Subject: [PATCH 09/19] rm unused cmd --- cmd/pqclient/main.go | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 cmd/pqclient/main.go diff --git a/cmd/pqclient/main.go b/cmd/pqclient/main.go deleted file mode 100644 index 7905807..0000000 --- a/cmd/pqclient/main.go +++ /dev/null @@ -1,5 +0,0 @@ -package main - -func main() { - -} From 225186d33bf63e859bb76aad2c156f7bbd095086 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:04:49 +0300 Subject: [PATCH 10/19] align gossip simulation test --- Makefile | 7 +++ core/gossipsub_test.go | 99 ++++++++++++++++++------------------------ 2 files changed, 49 insertions(+), 57 deletions(-) diff --git a/Makefile b/Makefile index bcad572..7e956f5 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ APP_VERSION?=$(git describe --tags $(git rev-list --tags --max-count=1) 2> /dev/ CFG_PATH?=./resources/config/default.p2pmq.yaml TEST_PKG?=./core/... TEST_TIMEOUT?=2m +GOSSIP_OUT_DIR=../.output protoc: ./scripts/proto-gen.sh @@ -49,3 +50,9 @@ docker-run-default: docker-run-boot: @docker run -d --restart unless-stopped --name "${APP_NAME}" -p "${TCP_PORT}":"${TCP_PORT}" -p "${GRPC_PORT}":"${GRPC_PORT}" -e "GRPC_PORT=${GRPC_PORT}" -it "${BUILD_IMG}" /p2pmq/app -config=./bootstrapper.p2pmq.yaml + +gossip-sim: + @mkdir -p "${GOSSIP_OUT_DIR}" \ + && export GOSSIP_SIMULATION=full \ + && export GOSSIP_OUT_DIR="${GOSSIP_OUT_DIR}" \ + && go test -v -timeout 10m ./core -run TestGossipSimulation \ No newline at end of file diff --git a/core/gossipsub_test.go b/core/gossipsub_test.go index 3a81b16..976ed9c 100644 --- a/core/gossipsub_test.go +++ b/core/gossipsub_test.go @@ -2,7 +2,9 @@ package core import ( "context" + "encoding/json" "fmt" + "os" "strings" "sync" "sync/atomic" @@ -38,31 +40,26 @@ var ( } ) -func TestGossipMsgThroughput(t *testing.T) { +func TestGossipSimulation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() require.NoError(t, logging.SetLogLevelRegex("p2pmq", "error")) - // groupsCfgSimple11 := testGroupSimple(11, 4, 6, 1) groupsCfgSimple18 := testGroupSimple(18, 10, 6, 2) groupsCfgSimple36 := testGroupSimple(36, 16, 16, 4) groupsCfgSimple54 := testGroupSimple(54, 32, 16, 6) benchFlows := []flow{ - // flowTrigger("a", 1, time.Millisecond*10), flowActionA2B(1, time.Millisecond*1, time.Millisecond*250), - flowActionA2B(10, time.Millisecond*1, time.Millisecond*1), - // // flowActionA2B(50, time.Millisecond*1, time.Millisecond*1), + flowActionA2B(10, time.Millisecond*1, time.Millisecond*10), flowActionA2B(100, time.Millisecond*1, time.Millisecond*10), flowActionA2B(1000, time.Millisecond*10, time.Millisecond*10), flowTrigger("b", 1, time.Millisecond*10), - flowTrigger("b", 10, time.Millisecond*1), - // flowTrigger("b", 50, time.Millisecond*1), + flowTrigger("b", 10, time.Millisecond*10), flowTrigger("b", 100, time.Millisecond*10), flowTrigger("b", 1000, time.Millisecond*10), flowTrigger("a", 1, time.Millisecond*10), flowTrigger("a", 10, time.Millisecond*10), - // // flowTrigger("b", 50, time.Millisecond*1), flowTrigger("a", 100, time.Millisecond*10), flowTrigger("a", 1000, time.Millisecond*10), } @@ -76,44 +73,6 @@ func TestGossipMsgThroughput(t *testing.T) { conns connectivity flows []flow }{ - // { - // name: "simple_11", - // n: 11, - // gen: &testGen{ - // // hitMaps: map[string]*nodeHitMap{}, - // routingFn: func(m *pubsub.Message) {}, - // validationFn: func(p peer.ID, m *pubsub.Message) pubsub.ValidationResult { - // return pubsub.ValidationAccept - // }, - // pubsubConfig: &commons.PubsubConfig{ - // MsgValidator: &commons.MsgValidationConfig{}, - // Trace: &commons.PubsubTraceConfig{ - // // JsonFile: fmt.Sprintf("../.output/trace/node-%d.json", i+1), - // Skiplist: traceEmptySkipList, - // }, - // Overlay: &commons.OverlayParams{ - // D: 3, - // Dlow: 2, - // Dhi: 5, - // Dlazy: 3, - // }, - // }, - // }, - // groupsCfg: groupsCfgSimple11, - // conns: func() connectivity { - // conns := groupsCfgSimple11.baseConnectivity() - // // add some extra connections between a and b - // conns[0] = append(conns[0], 5, 7, 9) - // conns[1] = append(conns[1], 4, 6, 8) - // conns[2] = append(conns[2], 4, 5, 7) - // conns[3] = append(conns[3], 4, 6, 8) - // return conns - // }(), - // flows: []flow{ - // flowActionA2B(1, time.Millisecond*1, time.Millisecond*250), - // flowTrigger("b", 2, time.Millisecond*10), - // }, - // }, { name: "simple_18", n: 18, @@ -208,6 +167,29 @@ func TestGossipMsgThroughput(t *testing.T) { }, } + outDir := os.Getenv("GOSSIP_OUT_DIR") + if len(outDir) == 0 { + outDir = t.TempDir() + } + gossipSimulation := os.Getenv("GOSSIP_SIMULATION") + switch gossipSimulation { + case "full": + t.Log("running full simulation") + default: + t.Log("running limited simulation (only for 18 and 36 nodes and flows with less than 100 iterations)") + tests = tests[:3] + for i, tc := range tests { + newFlows := make([]flow, 0, len(tc.flows)) + for _, f := range tc.flows { + if f.iterations < 100 { + newFlows = append(newFlows, f) + } + } + tc.flows = newFlows + tests[i] = tc + } + } + var outputs []gossipTestOutput for _, tt := range tests { tc := tt @@ -324,12 +306,12 @@ func TestGossipMsgThroughput(t *testing.T) { <-time.After(time.Second * 2) // TODO: avoid timeout faucets := traceFaucets{} - pubsubRpcCount := new(atomic.Uint64) + var pubsubRpcCount uint64 for _, ctrl := range ctrls { nodeFaucets := ctrl.psTracer.faucets // t.Logf("[%s] trace faucets: %+v", ctrl.lggr.Desugar().Name(), nodeFaucets) faucets.add(nodeFaucets) - pubsubRpcCount.Add(ctrl.pubsubRpcCounter.Swap(0)) + pubsubRpcCount += ctrl.pubsubRpcCounter.Swap(0) ctrl.psTracer.Reset() } testOutput := baseTestOutput @@ -343,7 +325,7 @@ func TestGossipMsgThroughput(t *testing.T) { SendRPC: faucets.sendRPC, RecvRPC: faucets.recvRPC, } - testOutput.InboundRPC = pubsubRpcCount.Load() + testOutput.InboundRPC = pubsubRpcCount testOutput.TotalTime = time.Since(start) outputs = append(outputs, testOutput) t.Logf("output: %+v", testOutput) @@ -351,13 +333,11 @@ func TestGossipMsgThroughput(t *testing.T) { } }) } - - // outDir := t.TempDir() // ../.output - // outputsJson, err := json.Marshal(outputs) - // require.NoError(t, err) - // outputFileName := fmt.Sprintf("%s/test-%d.json", outDir, time.Now().UnixMilli()) - // require.NoError(t, os.WriteFile(outputFileName, outputsJson, 0644)) - // t.Logf("outputs saved in %s", outputFileName) + outputsJson, err := json.Marshal(outputs) + require.NoError(t, err) + outputFileName := fmt.Sprintf("%s/test-%d.json", outDir, time.Now().UnixMilli()) + require.NoError(t, os.WriteFile(outputFileName, outputsJson, 0644)) + t.Logf("outputs saved in %s", outputFileName) } type connectivity map[int][]int @@ -518,7 +498,12 @@ func (fe flowEvent) Msg(args msgArgs) string { return "" } sb := new(strings.Builder) - if err := tmpl.Execute(sb, args); err != nil { + if err := tmpl.Execute(sb, map[string]interface{}{ + "i": args.i, + "group": args.group, + "ctrl": args.ctrl, + "flow": args.flow, + }); err != nil { return "" } return sb.String() From 73506d1c1f01e2a881fbd34385fd00450befd415 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:10:49 +0300 Subject: [PATCH 11/19] lint --- core/gossipsub_test.go | 65 ++++++++++++------------------------------ core/testutils.go | 22 -------------- 2 files changed, 19 insertions(+), 68 deletions(-) diff --git a/core/gossipsub_test.go b/core/gossipsub_test.go index 976ed9c..9ba9c9e 100644 --- a/core/gossipsub_test.go +++ b/core/gossipsub_test.go @@ -21,25 +21,6 @@ import ( "github.com/stretchr/testify/require" ) -var ( - traceEmptySkipList = []string{} - traceMsgEventSkipList = []string{ - "ADD_PEER", - "REMOVE_PEER", - "JOIN", - "LEAVE", - "GRAFT", - "PRUNE", - "DROP_RPC", - } - traceGossipEventSkipList = []string{ - "ADD_PEER", - "REMOVE_PEER", - "JOIN", - "LEAVE", - } -) - func TestGossipSimulation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -84,9 +65,7 @@ func TestGossipSimulation(t *testing.T) { }, pubsubConfig: &commons.PubsubConfig{ MsgValidator: &commons.MsgValidationConfig{}, - Trace: &commons.PubsubTraceConfig{ - Skiplist: traceEmptySkipList, - }, + Trace: &commons.PubsubTraceConfig{}, Overlay: &commons.OverlayParams{ D: 3, Dlow: 2, @@ -110,9 +89,7 @@ func TestGossipSimulation(t *testing.T) { }, pubsubConfig: &commons.PubsubConfig{ MsgValidator: &commons.MsgValidationConfig{}, - Trace: &commons.PubsubTraceConfig{ - Skiplist: traceEmptySkipList, - }, + Trace: &commons.PubsubTraceConfig{}, Overlay: &commons.OverlayParams{ D: 4, Dlow: 2, @@ -136,9 +113,7 @@ func TestGossipSimulation(t *testing.T) { }, pubsubConfig: &commons.PubsubConfig{ MsgValidator: &commons.MsgValidationConfig{}, - Trace: &commons.PubsubTraceConfig{ - Skiplist: traceEmptySkipList, - }, + Trace: &commons.PubsubTraceConfig{}, }, }, groupsCfg: groupsCfgSimple36, @@ -156,9 +131,7 @@ func TestGossipSimulation(t *testing.T) { }, pubsubConfig: &commons.PubsubConfig{ MsgValidator: &commons.MsgValidationConfig{}, - Trace: &commons.PubsubTraceConfig{ - Skiplist: traceEmptySkipList, - }, + Trace: &commons.PubsubTraceConfig{}, }, }, groupsCfg: groupsCfgSimple54, @@ -380,21 +353,21 @@ type groupsCfg map[string]groupCfg // baseConnectivity returns a base connectivity map for the groups, // where each group member is connected to all other members of the group // and to the relayers. -func (groups groupsCfg) baseConnectivity() connectivity { - conns := make(connectivity) - var relayerIDs []int - relayers, ok := groups["relayers"] - if ok { - relayerIDs = relayers.ids - } - for _, cfg := range groups { - connectIDs := append(cfg.ids, relayerIDs...) - for _, i := range cfg.ids { - conns[i] = connectIDs - } - } - return conns -} +// func (groups groupsCfg) baseConnectivity() connectivity { +// conns := make(connectivity) +// var relayerIDs []int +// relayers, ok := groups["relayers"] +// if ok { +// relayerIDs = relayers.ids +// } +// for _, cfg := range groups { +// connectIDs := append(cfg.ids, relayerIDs...) +// for _, i := range cfg.ids { +// conns[i] = connectIDs +// } +// } +// return conns +// } func (groups groupsCfg) allToAllConnectivity() connectivity { conns := make(connectivity) diff --git a/core/testutils.go b/core/testutils.go index 203a515..a4d810e 100644 --- a/core/testutils.go +++ b/core/testutils.go @@ -8,7 +8,6 @@ import ( "time" pubsub "github.com/libp2p/go-libp2p-pubsub" - "github.com/libp2p/go-libp2p/core/host" libp2pnetwork "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" "github.com/stretchr/testify/require" @@ -82,27 +81,6 @@ func StartControllers(ctx context.Context, n int, gen Generator) ([]*Controller, return controllers, msgRouters, valRouters, done, nil } -func waitMinConnected(ctx context.Context, minConnected func(i int) int, backoff time.Duration, hosts ...host.Host) { - for i, h := range hosts { - connected := make([]peer.ID, 0) - min := minConnected(i) - for len(connected) < min && ctx.Err() == nil { - peers := h.Network().Peers() - for _, pid := range peers { - switch h.Network().Connectedness(pid) { - case libp2pnetwork.Connected: - connected = append(connected, pid) - default: - } - } - if len(connected) < min { - fmt.Printf("host %s connected to %d peers, waiting for %d\n", h.ID(), len(connected), min) - } - time.Sleep(backoff) - } - } -} - func waitControllersConnected(n int, controllers ...*Controller) { for _, c := range controllers { connected := make([]peer.ID, 0) From c68c4e550f9a85762d51c072de5f979addcf5687 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:32:41 +0300 Subject: [PATCH 12/19] race --- core/gossipsub_test.go | 12 +++---- core/pubsub_trace.go | 74 +++++++++++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/core/gossipsub_test.go b/core/gossipsub_test.go index 9ba9c9e..6393f7c 100644 --- a/core/gossipsub_test.go +++ b/core/gossipsub_test.go @@ -291,12 +291,12 @@ func TestGossipSimulation(t *testing.T) { testOutput.Name = flow.name testOutput.Iterations = uint(flow.iterations) testOutput.Faucets = msgTraceFaucetsOutput{ - Publish: faucets.publish, - Deliver: faucets.deliver, - Reject: faucets.reject, - DropRPC: faucets.dropRPC, - SendRPC: faucets.sendRPC, - RecvRPC: faucets.recvRPC, + Publish: int(faucets.publish.Load()), + Deliver: int(faucets.deliver.Load()), + Reject: int(faucets.reject.Load()), + DropRPC: int(faucets.dropRPC.Load()), + SendRPC: int(faucets.sendRPC.Load()), + RecvRPC: int(faucets.recvRPC.Load()), } testOutput.InboundRPC = pubsubRpcCount testOutput.TotalTime = time.Since(start) diff --git a/core/pubsub_trace.go b/core/pubsub_trace.go index cf71696..32017ad 100644 --- a/core/pubsub_trace.go +++ b/core/pubsub_trace.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" pubsub "github.com/libp2p/go-libp2p-pubsub" @@ -16,23 +17,41 @@ import ( ) type traceFaucets struct { - join, leave, publish, deliver, reject, duplicate, addPeer, removePeer, graft, prune, sendRPC, dropRPC, recvRPC int + join, leave, publish, deliver, reject, duplicate, addPeer, removePeer, graft, prune, sendRPC, dropRPC, recvRPC *atomic.Uint64 +} + +func newTraceFaucets() traceFaucets { + return traceFaucets{ + join: new(atomic.Uint64), + leave: new(atomic.Uint64), + publish: new(atomic.Uint64), + deliver: new(atomic.Uint64), + reject: new(atomic.Uint64), + duplicate: new(atomic.Uint64), + addPeer: new(atomic.Uint64), + removePeer: new(atomic.Uint64), + graft: new(atomic.Uint64), + prune: new(atomic.Uint64), + sendRPC: new(atomic.Uint64), + dropRPC: new(atomic.Uint64), + recvRPC: new(atomic.Uint64), + } } func (tf *traceFaucets) add(other traceFaucets) { - tf.join += other.join - tf.leave += other.leave - tf.publish += other.publish - tf.deliver += other.deliver - tf.reject += other.reject - tf.duplicate += other.duplicate - tf.addPeer += other.addPeer - tf.removePeer += other.removePeer - tf.graft += other.graft - tf.prune += other.prune - tf.sendRPC += other.sendRPC - tf.dropRPC += other.dropRPC - tf.recvRPC += other.recvRPC + tf.join.Add(other.join.Load()) + tf.leave.Add(other.leave.Load()) + tf.publish.Add(other.publish.Load()) + tf.deliver.Add(other.deliver.Load()) + tf.reject.Add(other.reject.Load()) + tf.duplicate.Add(other.duplicate.Load()) + tf.addPeer.Add(other.addPeer.Load()) + tf.removePeer.Add(other.removePeer.Load()) + tf.graft.Add(other.graft.Load()) + tf.prune.Add(other.prune.Load()) + tf.sendRPC.Add(other.sendRPC.Load()) + tf.dropRPC.Add(other.dropRPC.Load()) + tf.recvRPC.Add(other.recvRPC.Load()) } type eventFields map[string]string @@ -65,6 +84,7 @@ func newPubsubTracer(lggr *zap.SugaredLogger, debug bool, skiplist []string, sub subTracer: subTracer, skiplist: skiplist, debug: debug, + faucets: newTraceFaucets(), } } @@ -100,13 +120,13 @@ func (pst *psTracer) Trace(evt *pubsub_pb.TraceEvent) { eventType := evt.GetType() switch eventType { case pubsub_pb.TraceEvent_PUBLISH_MESSAGE: - pst.faucets.publish++ + pst.faucets.publish.Add(1) msg := evt.GetPublishMessage() evt.GetPeerID() fields["msgID"] = hex.EncodeToString(msg.GetMessageID()) fields["topic"] = msg.GetTopic() case pubsub_pb.TraceEvent_REJECT_MESSAGE: - pst.faucets.reject++ + pst.faucets.reject.Add(1) msg := evt.GetRejectMessage() pid, err := peer.IDFromBytes(msg.GetReceivedFrom()) if err == nil { @@ -116,7 +136,7 @@ func (pst *psTracer) Trace(evt *pubsub_pb.TraceEvent) { fields["topic"] = msg.GetTopic() fields["reason"] = msg.GetReason() case pubsub_pb.TraceEvent_DUPLICATE_MESSAGE: - pst.faucets.duplicate++ + pst.faucets.duplicate.Add(1) msg := evt.GetDuplicateMessage() pid, err := peer.IDFromBytes(msg.GetReceivedFrom()) if err == nil { @@ -125,7 +145,7 @@ func (pst *psTracer) Trace(evt *pubsub_pb.TraceEvent) { fields["msgID"] = hex.EncodeToString(msg.GetMessageID()) fields["topic"] = msg.GetTopic() case pubsub_pb.TraceEvent_DELIVER_MESSAGE: - pst.faucets.deliver++ + pst.faucets.deliver.Add(1) msg := evt.GetDeliverMessage() pid, err := peer.IDFromBytes(msg.GetReceivedFrom()) if err == nil { @@ -134,25 +154,25 @@ func (pst *psTracer) Trace(evt *pubsub_pb.TraceEvent) { fields["msgID"] = hex.EncodeToString(msg.GetMessageID()) fields["topic"] = msg.GetTopic() case pubsub_pb.TraceEvent_ADD_PEER: - pst.faucets.addPeer++ + pst.faucets.addPeer.Add(1) pid, err := peer.IDFromBytes(evt.GetAddPeer().GetPeerID()) if err == nil { fields["targetPeer"] = pid.String() } case pubsub_pb.TraceEvent_REMOVE_PEER: - pst.faucets.removePeer++ + pst.faucets.removePeer.Add(1) pid, err := peer.IDFromBytes(evt.GetRemovePeer().GetPeerID()) if err == nil { fields["targetPeer"] = pid.String() } case pubsub_pb.TraceEvent_JOIN: - pst.faucets.join++ + pst.faucets.join.Add(1) fields["topic"] = evt.GetJoin().GetTopic() case pubsub_pb.TraceEvent_LEAVE: - pst.faucets.leave++ + pst.faucets.leave.Add(1) fields["topic"] = evt.GetLeave().GetTopic() case pubsub_pb.TraceEvent_GRAFT: - pst.faucets.graft++ + pst.faucets.graft.Add(1) msg := evt.GetGraft() pid, err := peer.IDFromBytes(msg.GetPeerID()) if err == nil { @@ -160,7 +180,7 @@ func (pst *psTracer) Trace(evt *pubsub_pb.TraceEvent) { } fields["topic"] = msg.GetTopic() case pubsub_pb.TraceEvent_PRUNE: - pst.faucets.prune++ + pst.faucets.prune.Add(1) msg := evt.GetPrune() pid, err := peer.IDFromBytes(msg.GetPeerID()) if err == nil { @@ -168,7 +188,7 @@ func (pst *psTracer) Trace(evt *pubsub_pb.TraceEvent) { } fields["topic"] = msg.GetTopic() case pubsub_pb.TraceEvent_SEND_RPC: - pst.faucets.sendRPC++ + pst.faucets.sendRPC.Add(1) msg := evt.GetSendRPC() pid, err := peer.IDFromBytes(msg.GetSendTo()) if err == nil { @@ -188,14 +208,14 @@ func (pst *psTracer) Trace(evt *pubsub_pb.TraceEvent) { fields["subs"] = strings.Join(subs, ",") } case pubsub_pb.TraceEvent_DROP_RPC: - pst.faucets.dropRPC++ + pst.faucets.dropRPC.Add(1) msg := evt.GetDropRPC() pid, err := peer.IDFromBytes(msg.GetSendTo()) if err == nil { fields["targetPeer"] = pid.String() } case pubsub_pb.TraceEvent_RECV_RPC: - pst.faucets.recvRPC++ + pst.faucets.recvRPC.Add(1) msg := evt.GetRecvRPC() pid, err := peer.IDFromBytes(msg.GetReceivedFrom()) if err == nil { From 336aea5d600ae0eeadaee851c6dea20d435cdfee Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:33:31 +0300 Subject: [PATCH 13/19] small fixes to bls example --- examples/bls/bls_test.go | 37 ++++++++++++++++---------------- examples/bls/node_internalnet.go | 2 +- examples/bls/share.go | 6 ++++++ 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/examples/bls/bls_test.go b/examples/bls/bls_test.go index b9495ce..7f43a93 100644 --- a/examples/bls/bls_test.go +++ b/examples/bls/bls_test.go @@ -232,26 +232,27 @@ func triggerReports(pctx context.Context, t *testing.T, net string, interval tim if !ok { continue } - if node.getLeader(net, nextSeq) == share.SignerID { - node.threadC.Go(func(ctx context.Context) { - report := &SignedReport{ - Network: net, - SeqNumber: nextSeq, - Data: []byte(fmt.Sprintf("report for %s, seq %d", net, nextSeq)), - } - share.Sign(report) - if pctx.Err() != nil || ctx.Err() != nil { // ctx might be canceled by the time we get here + if node.getLeader(net, nextSeq) != share.SignerID { + continue + } + node.threadC.Go(func(ctx context.Context) { + report := &SignedReport{ + Network: net, + SeqNumber: nextSeq, + Data: []byte(fmt.Sprintf("report for %s, seq %d", net, nextSeq)), + } + share.Sign(report) + if pctx.Err() != nil || ctx.Err() != nil { // ctx might be canceled by the time we get here + return + } + if err := node.Broadcast(ctx, *report); ctx.Err() == nil && pctx.Err() == nil { + if err != nil && strings.Contains(err.Error(), "context canceled") { return } - if err := node.Broadcast(ctx, *report); ctx.Err() == nil && pctx.Err() == nil { - if err != nil && strings.Contains(err.Error(), "context canceled") { - return - } - require.NoError(t, err) - reports.Add(net, *report) - } - }) - } + require.NoError(t, err) + reports.Add(net, *report) + } + }) } } } diff --git a/examples/bls/node_internalnet.go b/examples/bls/node_internalnet.go index 8b7e066..a449cd8 100644 --- a/examples/bls/node_internalnet.go +++ b/examples/bls/node_internalnet.go @@ -125,7 +125,7 @@ func (n *Node) getLeader(net string, seq uint64) uint64 { if !ok { return 0 } - return (seq % uint64(len(share.Signers))) + 1 + return RoundRobinLeader(seq, share.Signers) } // isProcessable ensures that we sign once and only leaders can trigger a new sequence diff --git a/examples/bls/share.go b/examples/bls/share.go index 8738c16..d235cbf 100644 --- a/examples/bls/share.go +++ b/examples/bls/share.go @@ -38,6 +38,12 @@ func (share *Share) QuorumCount() int { return Threshold(len(share.Signers)) } +type LeaderSelector func(seq uint64, signers map[uint64]*bls.PublicKey) uint64 + +func RoundRobinLeader(seq uint64, signers map[uint64]*bls.PublicKey) uint64 { + return (seq % uint64(len(signers))) + 1 +} + func Threshold(count int) int { f := (count - 1) / 3 From aa1153ed9d0791eb14966d5c37098326990c3a91 Mon Sep 17 00:00:00 2001 From: amirylm <83904651+amirylm@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:57:17 +0300 Subject: [PATCH 14/19] Squashed docs branch --- README.md | 15 ++-- examples/README.md | 8 ++ examples/bls/README.md | 3 +- resources/docs/APPENDIX_LIBP2P.md | 64 +++++++++++++ resources/docs/README.md | 130 +++++++++++++++++++++++++++ resources/docs/THREAT_ANALYSIS.md | 115 ++++++++++++++++++++++++ resources/docs/arch-node.png | Bin 0 -> 105526 bytes resources/docs/overlays-topology.png | Bin 0 -> 150237 bytes resources/img/clnode-pmq.png | Bin 125411 -> 0 bytes resources/img/composer-p2pmq.png | Bin 132926 -> 0 bytes 10 files changed, 324 insertions(+), 11 deletions(-) create mode 100644 examples/README.md create mode 100644 resources/docs/APPENDIX_LIBP2P.md create mode 100644 resources/docs/README.md create mode 100644 resources/docs/THREAT_ANALYSIS.md create mode 100644 resources/docs/arch-node.png create mode 100644 resources/docs/overlays-topology.png delete mode 100644 resources/img/clnode-pmq.png delete mode 100644 resources/img/composer-p2pmq.png diff --git a/README.md b/README.md index 41c5fb8..e593e55 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,12 @@
-**NOTE: This is an experimental work in progress. DO NOT USE** +**WARNING: This is an experimental work in progress. DO NOT USE** -## Overview +## Documentation -**DME** is a distributed, permissionless messaging engine for cross oracle communication. +You can find documentation in [./resources/docs](./resources/docs). -A network of agents is capable of the following: -- Broadcast messages over topics with optimal latency -- Pluggable and decoupled message validation using gRPC -- Scoring for protection from bad actors -- Syncing peers with the latest messages to recover from -restarts, network partition, etc. +## Usage + +Usage examples are available in the [examples](./examples) folder. \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..822e312 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,8 @@ +# Decentralized Message Engine: Examples + +This folder contains examples of how the DME can be used to facilitate cross network communication. + +The following examples are available: + +- [OCR based cryptography](./don) (Chainlink Oracles) +- [BLS based networks](./bls) \ No newline at end of file diff --git a/examples/bls/README.md b/examples/bls/README.md index 4b918c1..4c1561c 100644 --- a/examples/bls/README.md +++ b/examples/bls/README.md @@ -1,10 +1,9 @@ # BLS example -This package shows a high level integration with BLS based networks. +This package shows a high level integration with BLS based networks, using [herumi/bls-eth-go-binary](https://github.com/herumi/bls-eth-go-binary) package for BLS cryptography. ## Links -- [github.com/herumi/bls-eth-go-binary](https://github.com/herumi/bls-eth-go-binary) is the library used for BLS signatures. - [BLS Multi-Signatures With Public-Key Aggregation](https://crypto.stanford.edu/~dabo/pubs/papers/BLSmultisig.html) - [BLS Signatures Part 1 — Overview](https://alonmuroch-65570.medium.com/bls-signatures-part-1-overview-47d9eebf1c75) - [BLS signatures in Solidity](https://ethresear.ch/t/bls-signatures-in-solidity/7919) diff --git a/resources/docs/APPENDIX_LIBP2P.md b/resources/docs/APPENDIX_LIBP2P.md new file mode 100644 index 0000000..afe16a8 --- /dev/null +++ b/resources/docs/APPENDIX_LIBP2P.md @@ -0,0 +1,64 @@ +# Appendix: Libp2p + +## Links + +- [Libp2p specs](https://github.com/libp2p/specs) +- [Gossipsub v1.1 spec](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md) +- [Gossipsub v1.1 Evaluation Report](https://gateway.ipfs.io/ipfs/QmRAFP5DBnvNjdYSbWhEhVRJJDFCLpPyvew5GwCCB4VxM4) + +## Overview + +Libp2p is a modular networking framework designed for peer-to-peer communication in decentralized systems. It provides a foundation for building decentralized applications and systems by offering a range of essential components. + +Libp2p was chosen because it provides a battle tested, complete yet extensible networking framework for a distributed message engine. + +## Libp2p Protocols + +The following libp2p protocols are utilized by the DME: + +| Name | Description | Links | +| --- | --- | --- | +| Gossipsub (v1.1) | Pubsub messaging | https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md | +| Kad DHT | Distributed hash table for discovery | https://github.com/libp2p/specs/tree/master/kad-dht | +| Noise | Protocol for securing transport | https://github.com/libp2p/specs/blob/master/noise/README.md | +| Yamux | Protocol for multiplexing transport | https://github.com/libp2p/specs/blob/master/yamux/README.md | + +## Security + +Libp2p provides strong cryptographic support, including secure key management and encryption. It offers options for transport-level encryption and authentication, ensuring the confidentiality and integrity of data exchanged between peers. + +Secure channels in libp2p are established with the help of a transport upgrader, which providers layers of security and stream multiplexing over "raw" connections such as TCP sockets. + +## Kad DHT + +[KadDHT](https://github.com/libp2p/specs/tree/master/kad-dht) is used for peer discovery. It requires to deploy a set of bootstrapper nodes, which are used by new peers to join the network. + +**NOTE:** bootstrappers should managed by multiple parties for decentralization. + +## Pubsub + +### Key Concepts of Gossiping + +1. **Fanout Groups:** In a gossip-based network like Gossipsub, peers are organized into "fanout groups" based on their interests, i.e., the topics they subscribe to. Each fanout group represents a set of peers interested in the same topic. Messages are gossiped within these fanout groups to ensure that relevant information reaches its intended audience efficiently. +2. **Mesh Peers:** Within each fanout group, a subset of peers forms a "mesh." The mesh represents a more tightly connected subgroup where every member is directly connected to every other member. Messages are initially sent to mesh peers, who play a crucial role in the efficient propagation of messages within their fanout group. +3. **Message Propagation:** Gossiping relies on a "gossip" mechanism where mesh peers share messages they have received with other mesh peers within the same fanout group. This propagation method ensures that messages quickly spread to all members of the group without overloading the network with redundant copies. Gossiping reduces the number of messages sent while maximizing their reach. +4. **Heartbeat Interval:** Gossipsub introduces a "heartbeat" mechanism that defines a regular interval at which peers exchange heartbeat messages with their mesh peers. Heartbeats help maintain the freshness of the mesh and enable the discovery of new topics or subscriptions. During heartbeats, peers can also inform each other about their latest state, including the topics they are interested in. +5. **Message Validation:** To maintain the integrity and authenticity of messages, Gossipsub requires that all messages by signed by their publishers. [Extended validators](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#extended-validators) +allows to aid-in a custom, decoupled validation process. Invalid messages are promptly discarded to prevent the spread of misinformation, and the sending peers are punished with low score. +**NOTE:** in our case the validation is being facilitated over duplex stream and verifies that all messages have a reasonable sequence number, and were signed by a quorum of signers as part of OCR. +6. **Peer Scoring:** Gossipsub incorporates a "peer scoring" mechanism to evaluate the behavior and trustworthiness of network peers. Peers are assigned scores based on various factors, including their message forwarding reliability, responsiveness, and adherence to protocol rules. Low-scoring peers may be disconnected from the network to maintain its health and reliability. +7. **Pruning:** To further optimize the network's efficiency, Gossipsub employs a "pruning" mechanism. This mechanism removes peers from the mesh and fanout groups when they are no longer interested in a particular topic. Pruning ensures that resources are allocated efficiently, and peers focus on propagating messages relevant to their subscriptions. + +### How Gossipsub Works + +- **Message Exchange:** When a node wishes to publish a message to a topic, it first sends the message to its mesh peers within the corresponding fanout group. Mesh peers validate the message and, if valid, forward it to their mesh peers and so on. Messages propagate through the mesh until they reach all mesh peers within the group. +- **Heartbeat:** Each peer runs a periodic stabilization process called the "heartbeat procedure" +at regular intervals (1s is the default). The heartbeat serves three functions: +1. [mesh maintenance](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.0.md#mesh-maintenance) to keep mesh fresh and discover new subscriptions +2. [fanout maintenance](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.0.md#fanout-maintenance) to efficiently adjust fanout groups based on changing interests +3. [gossip emission](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.0.md#gossip-emission) to a random selection of peers for each topic (that are not already members of the topic mesh) +- **Piggybacking:** Gossipsub employs [piggybacking](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.0.md#control-message-piggybacking) - a technique where acknowledgment (ACK) messages carry new messages. When a peer sends an ACK for a message, it may include new messages it has received. This optimizes bandwidth usage by combining acknowledgment and message propagation in a single step. + +To summarize, `gossipsub` leverages a combination of fanout groups, mesh peers, message propagation, heartbeat intervals, peer scoring, message validation, and piggybacking to efficiently distribute messages within decentralized networks. These mechanisms ensure that messages reach their intended recipients while maintaining network health and minimizing redundancy. + + diff --git a/resources/docs/README.md b/resources/docs/README.md new file mode 100644 index 0000000..199ee8c --- /dev/null +++ b/resources/docs/README.md @@ -0,0 +1,130 @@ +# Decentralized Messaging Engine + +This document describes a solution for cross networks communication, including oracle, offchain computation or blockchain networks. + +## Links + +- [Threat Analysis](./THREAT_ANALYSIS.md) +- [Appendix: Libp2p](./APPENDIX_LIBP2P.md) +- Examples: + - [OCR based cryptography](https://github.com/amirylm/p2pmq/tree/main/examples/don) (Chainlink Oracles) + - [BLS based networks](https://github.com/amirylm/p2pmq/tree/main/examples/bls) + +## Table of Contents + +- [Overview](#overview) + - [Goals](#goals) + - [Background: Libp2p](#background-libp2p) +- [High Level Design](#high-level-design) + - [Technical Overview](#technical-overview) + - [Architecture](#architecture) + - [API](#api) + - [Network Topology](#network-topology) + - [Message Validation](#message-validation) + +## Overview + +By introducing a decentralized messaging engine (DME) that facilitates the secure exchange of verifiable messages across networks, we enable the formation of a global, collaborative network that consists of multiple overlay networks. + +The resulting protocol leverages libp2p and gossipsub v1.1 in order to provide robust networking and message propagation while ensuring the integrity and authenticity of transmitted data by outsourcing the process of cryptographic and sequence validation. + +The following diagram visualizes the topology of a such a global network, +where the dashed lines represent overlay networks and the solid lines represent the underlying/standalone networks: + +![overlays-topology.png](./overlays-topology.png) + +### Goals + +- Enable secure communication layer across networks +- Enable exchange of messages while facilitating validation and authentication via an outsourced verification mechanism +- Provie efficient and reliable network communication and message propagation by utilizing proven protocols such as gossipsub +- Provide a flexible & extensible API that serves an array of use cases, while maintaining a simple and robust protocol that can withstand scaling and high throughput + +### Background: Libp2p + +Libp2p is a modular networking framework designed for peer-to-peer communication in decentralized systems. It provides a foundation for building decentralized applications and systems by offering a range of essential components. Among its core features are [pubsub](./APPENDIX_LIBP2P.md#pubsub), [peer discovery](./APPENDIX_LIBP2P.md#kad-dht), abstracted transport layer and a complete [cryptography suite](./APPENDIX_LIBP2P.md#security). + +[gossipsub v1.1](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md) is a dynamic and efficient message propagation protocol, it is based on randomized topic meshes and gossip, with moderate amplification factors and good scaling properties. + +Gossipsub is designed to be extensible by more specialized routers and provides an optimized environment for a distributed protocol that runs over a trust-less p2p network. + +Libp2p was chosen because it provides a battle tested, complete yet extensible networking framework for a distributed message engine. + +**NOTE:** For more information see the [libp2p appendix](./APPENDIX_LIBP2P.md). + +## High Level Design + +### Technical Overview + +Agents runs within some parent nodes, and enables the node to gossip messages in an overlay network (topic), while using an outsourced message validation to avoid introducing additional dependencies for the agent such as requiring validation keys. + +Sending verified-only information enables to achieve optimal latency and throughput due to having no additional signing involved. Additionally, consensus is not needed for sharing a sequential, signed-by-quorum data, which is usually stored on-chain or some public storage. + +Message validation is decoupled from the agents, which queue messages for validation and processing, thus enabling the implementation of any specifically tailored, custom validation logic according to each network requirements. + +Peer scoring is facilitated by the pubsub router according to validation results, messaging rate and overall behaviour, however it is still required that network specific setting fit the topology, expected message rate and the strictness of the validation requirements. + +### Architecture + +Agents are separate processes running within some parent node, interaction is done via a gRPC API, following [go-plugin](https://github.com/hashicorp/go-plugin) in order to achieve modularity. + +The following diagram illustrates the system architecture: + +![arch-node.png](./arch-node.png) + +Agents manages libp2p components such as the gossipsub router, which is responsible for message propagation and peer scoring. Additionally, the following services run within the agent: + +- **validation router** for message validation and processing +- **message router** for consuming messages +- **control service** for managing subscriptions, publishing messages and more. + +The node implements: +- verifiers for validating messages +- processors for processing incoming messages from other networks, and broadcasting messages on behalf of the node to other networks. + +### API + +The following gRPC services are used by the clients from within the parent node, for interacting with the agent: + +```protobuf +service ControlService { + rpc Publish(PublishRequest) returns (PublishResponse); + rpc Subscribe(SubscribeRequest) returns (SubscribeResponse); + rpc Unsubscribe(UnsubscribeRequest) returns (UnsubscribeResponse); +} + +service MsgRouter { + rpc Listen(ListenRequest) returns (stream Message) {} +} + +service ValidationRouter { + rpc Handle(stream Message) returns (stream ValidatedMessage) {} +} +``` + +### Network Topology + +Each network creates an overlay network for outbound traffic (i.e. pubsub topic), where other networks can subscribe for messages. + +There might be multiple overlay networks for broadcasting messages from the same network, depending on versioning and business logic. +For instance, in case the encoding of the messages has changed, a new overlay network will be created for the new version and the old overlay network will be neglected until it is no longer needed or used. + +The amount of overlay connections to nodes in other networks might be changed dynamically, depending on the network topology and the amount of nodes in each network. Gossipsub allows to configure the amount of peers per topic while facilitating decent propagation of messages. This property enables the scale of the global network to a large quantity of nodes w/o flooding the wires and consuming too much resources. + +### Message Validation + +Due to the validation being outsourced to the parent node, we rely on the security properties of an existing infrastructure for the processes of signing and verifying messages. + +**NOTE:** Having validation within the agent introduces significant complexity, dependencies and aaditional vulnerabilities. + +All the messages are propagated through the pipes must be valid. +In case of invalid messages, the message will be dropped and the sender, regardless of the message origin, will be penalized by the gossipsub router. + +E.g. for an oracles (or other offchain computing) network, messages must comply with the following strict rules: + +- The message was **signed by a quorum** of a standalone-network nodes +- The message has a **sequence number** that match the current order, taking into account potential gaps that might be created due to bad network conditions. + - Messages that are older than **Validity Threshold** are considered invalid and will result in a low score for the sender, which can be either the origin peer or a gossiper peer + - Messages that are older than **Skip Threshold** will be ignored w/o affecting sender scores + +**NOTE:** validity and skip thresholds should be set within the parent node, according to the network topology and expected message rate. diff --git a/resources/docs/THREAT_ANALYSIS.md b/resources/docs/THREAT_ANALYSIS.md new file mode 100644 index 0000000..738bcf3 --- /dev/null +++ b/resources/docs/THREAT_ANALYSIS.md @@ -0,0 +1,115 @@ +# Threat Analysis + +In order to create a robust and secure messaging solution for cross network interoperability, the following threats were considered: + +### 1. Message Spamming + +Attackers flood the network with invalid or malicious messages, consuming bandwidth and degrading network performance. + +**Severity:** Very high \ +**Impact:** Network congestion, performance degradation, resource exhaustion. + +**Mitigation:** + +- Require message validation with cryptographic signatures to ensure message authenticity and integrity +- Require message sequence validation, where unrealistic sequences are considered invalid. Use caching, where the key is based on content hashing, so the same message won't exhaust resources + +### 2. Message Spamming: Validation Queue Flooding + +Attackers can overload the validation queue by sending spam messages at a very high rate. Legitimate messages get dropped, resulting in a denial of service as messages are ignored. + +**Severity:** Very high \ +**Impact:** Denial of service, message loss. + +**Mitigation:** + +Implement a circuit breaker before the validation queue that makes informed decisions based on message origin IP and a probabilistic strategy to drop messages. See [gossipsub v1.1: validation-queue-protection](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/red.md#gossipsub-v11-functional-extension-for-validation-queue-protection). + + +### 3. Censorship Attacks + +Malicious nodes selectively block messages to suppress certain information. Countermeasures include redundancy and diversification of message propagation paths. + +**Severity:** Very high \ +**Impact:** Information suppression, network manipulation. + +**Mitigation:** + +- Maintain a diverse set of mesh peers to maintain network resilience +- Use redundancy in message propagation paths to counter censorship attacks +- Employ mechanisms to detect and mitigate Sybil nodes, such as peer scoring and validation + + +### 4. Denial of Service (DoS) + +Adversaries flood the network with malicious traffic or connections to disrupt its operation. + +**Severity:** High \ +**Impact:** Network disruption, resource exhaustion. + +**Mitigation:** + +Implement rate limiting, connection policies, and adaptive firewall mechanisms to protect against DoS attacks. + + +### 5. Partition Attacks + +Adversaries attempt to partition the network by disrupting communication between mesh peers. + +**Severity:** High \ +**Impact:** Network fragmentation, reduced communication. + +**Mitigation:** + +Ensuring a diverse set of well-behaved mesh peers can help prevent this. Implement a robust peer scoring system to detect and disconnect poorly performing or malicious peers. See [gossipsub v1.1: peer scoring](https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#peer-scoring). + + +### 6. Sybil Attacks + +Attackers create multiple fake identities (Sybil nodes) to manipulate the network. + +**Severity:** Medium \ +**Impact:** Potential network manipulation, disruption. + +**Mitigation:** + +Relies on the effectiveness of peer discovery mechanisms, including whitelisting which in controlled by the parent node, which should have access to that information. + + +### 7. Eclipse Attacks + +Malicious nodes attempt to control a target node's connections, isolating it from the legitimate peers. + +**Severity:** Medium \ +**Impact:** Network isolation, potential data manipulation. + +**Mitigation:** + +Ensure diverse connectivity by utilizing peer discovery methods and continuously change connected peers. + + +### 8. DHT Pollution: connections + +Malicious nodes flood the DHT with malicious entries as part of an eclipse attack. + +**Severity:** Medium \ +**Impact:** Network isolation. + +**Mitigation:** + +- Implement DHT security mechanisms to prevent unauthorized writes and ensure data validity +- Regularly check the integrity of DHT data and remove or quarantine polluted entries + + +### 9. DHT Pollution: storage + +Malicious nodes flood the DHT with irrelevant data, potentially disrupting the network's ability to perform efficient content retrieval. + +**Severity:** Medium \ +**Impact:** Degraded performance in content retrieval, network congestion, resource exhaustion. + +**Mitigation:** + +- Implement DHT security mechanisms to prevent unauthorized writes and ensure data validity +- Regularly check the integrity of DHT data and remove or quarantine polluted entries +- Implement rate limiting and access controls for DHT writes to mitigate pollution attempts diff --git a/resources/docs/arch-node.png b/resources/docs/arch-node.png new file mode 100644 index 0000000000000000000000000000000000000000..a0c3d1e2e4619af5c8aa8750d6f9dbf4646e29fd GIT binary patch literal 105526 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