From 3ad5ce38e80585e6237d92de95173107174a8555 Mon Sep 17 00:00:00 2001 From: gammazero <11790789+gammazero@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:51:50 -0700 Subject: [PATCH] Include optional Ipni-Cid-Schema-Type HTTP header Same as #221 applied to Release-0.5.x branch. - update go-libp2p to latest --- dagsync/announce_test.go | 7 ++- dagsync/ipnisync/cid_schema_hint.go | 71 ++++++++++++++++++++++++ dagsync/ipnisync/cid_schema_hint_test.go | 46 +++++++++++++++ dagsync/ipnisync/publisher.go | 32 ++++++++++- dagsync/ipnisync/sync.go | 35 ++++++++++-- dagsync/ipnisync/sync_test.go | 61 ++++++++++++++++++++ dagsync/option.go | 9 +++ dagsync/subscriber.go | 25 +++++++++ dagsync/test/util.go | 8 ++- go.mod | 2 +- go.sum | 4 +- 11 files changed, 285 insertions(+), 15 deletions(-) create mode 100644 dagsync/ipnisync/cid_schema_hint.go create mode 100644 dagsync/ipnisync/cid_schema_hint_test.go diff --git a/dagsync/announce_test.go b/dagsync/announce_test.go index b262be5..28cd655 100644 --- a/dagsync/announce_test.go +++ b/dagsync/announce_test.go @@ -36,7 +36,7 @@ func TestAnnounceReplace(t *testing.T) { } sub, err := dagsync.NewSubscriber(dstHost, dstStore, dstLnkS, testTopic, dagsync.RecvAnnounce(), - dagsync.BlockHook(blockHook)) + dagsync.BlockHook(blockHook), dagsync.WithCidSchemaHint(false)) require.NoError(t, err) defer sub.Close() @@ -377,7 +377,8 @@ func TestPublisherRejectsPeer(t *testing.T) { dstHost.Peerstore().AddAddrs(srcHost.ID(), srcHost.Addrs(), time.Hour) dstLnkS := test.MkLinkSystem(dstStore) - sub, err := dagsync.NewSubscriber(dstHost, dstStore, dstLnkS, testTopic, dagsync.RecvAnnounce(announce.WithTopic(topics[1]))) + sub, err := dagsync.NewSubscriber(dstHost, dstStore, dstLnkS, testTopic, + dagsync.RecvAnnounce(announce.WithTopic(topics[1]))) require.NoError(t, err) defer sub.Close() @@ -457,7 +458,7 @@ func initPubSub(t *testing.T, srcStore, dstStore datastore.Batching, allowPeer f dstHost.Peerstore().AddAddrs(srcHost.ID(), srcHost.Addrs(), time.Hour) dstLnkS := test.MkLinkSystem(dstStore) - sub, err := dagsync.NewSubscriber(dstHost, dstStore, dstLnkS, testTopic, + sub, err := dagsync.NewSubscriber(dstHost, dstStore, dstLnkS, testTopic, dagsync.WithCidSchemaHint(false), dagsync.RecvAnnounce(announce.WithTopic(topics[1]), announce.WithAllowPeer(allowPeer))) require.NoError(t, err) diff --git a/dagsync/ipnisync/cid_schema_hint.go b/dagsync/ipnisync/cid_schema_hint.go new file mode 100644 index 0000000..461c033 --- /dev/null +++ b/dagsync/ipnisync/cid_schema_hint.go @@ -0,0 +1,71 @@ +package ipnisync + +import ( + "context" + "errors" +) + +const ( + // CidSchemaHeader is the HTTP header used as an optional hint about the + // type of data requested by a CID. + CidSchemaHeader = "Ipni-Cid-Schema-Type" + // CidSchemaAdvertisement is a value for the CidSchemaHeader specifying + // advertiesement data is being requested. Referrs to Advertisement in + // https://github.com/ipni/go-libipni/blob/main/ingest/schema/schema.ipldsch + CidSchemaAdvertisement = "Advertisement" + // CidSchemaEntries is a value for the CidSchemaHeader specifying + // advertisement entries (multihash chunks) data is being requested. + // Referrs to Entry chunk in + // https://github.com/ipni/go-libipni/blob/main/ingest/schema/schema.ipldsch + CidSchemaEntryChunk = "EntryChunk" +) + +var ErrUnknownCidSchema = errors.New("unknown cid schema type value") + +// cidSchemaTypeKey is the type used for the key of CidSchemaHeader when set as +// a context value. +type cidSchemaTypeCtxKey string + +// cidSchemaCtxKey is used to get the key used to store or extract the cid +// schema value in a context. +const cidSchemaCtxKey cidSchemaTypeCtxKey = CidSchemaHeader + +// CidSchemaFromCtx extracts the CID schema name from the context. If the +// scheam value is not set, then returns "". If the schema value is set, but is +// not recognized, then ErrUnknownCidSchema is returned along with the value. +// +// Returning unrecognized values with an error allows consumers to retrieved +// newer values that are not recognized by an older version of this library. +func CidSchemaFromCtx(ctx context.Context) (string, error) { + cidSchemaType, ok := ctx.Value(cidSchemaCtxKey).(string) + if !ok { + return "", nil + } + + var err error + switch cidSchemaType { + case CidSchemaAdvertisement, CidSchemaEntryChunk: + default: + err = ErrUnknownCidSchema + } + return cidSchemaType, err +} + +// CtxWithCidSchema creates a derived context that has the specified value for +// the CID schema type. +// +// Setting an unrecognized value, even when an error is retruned, allows +// producers to set context values that are not recognized by an older version +// of this library. +func CtxWithCidSchema(ctx context.Context, cidSchemaType string) (context.Context, error) { + if cidSchemaType == "" { + return ctx, nil + } + var err error + switch cidSchemaType { + case CidSchemaAdvertisement, CidSchemaEntryChunk: + default: + err = ErrUnknownCidSchema + } + return context.WithValue(ctx, cidSchemaCtxKey, cidSchemaType), err +} diff --git a/dagsync/ipnisync/cid_schema_hint_test.go b/dagsync/ipnisync/cid_schema_hint_test.go new file mode 100644 index 0000000..8462cc7 --- /dev/null +++ b/dagsync/ipnisync/cid_schema_hint_test.go @@ -0,0 +1,46 @@ +package ipnisync_test + +import ( + "context" + "testing" + + "github.com/ipni/go-libipni/dagsync/ipnisync" + "github.com/stretchr/testify/require" +) + +func TestCtxWithCidSchema(t *testing.T) { + ctxOrig := context.Background() + ctx, err := ipnisync.CtxWithCidSchema(ctxOrig, "") + require.NoError(t, err) + require.Equal(t, ctxOrig, ctx) + + ctx, err = ipnisync.CtxWithCidSchema(ctxOrig, ipnisync.CidSchemaAdvertisement) + require.NoError(t, err) + require.NotEqual(t, ctxOrig, ctx) + + value, err := ipnisync.CidSchemaFromCtx(ctx) + require.NoError(t, err) + require.Equal(t, ipnisync.CidSchemaAdvertisement, value) + + ctx, err = ipnisync.CtxWithCidSchema(ctx, ipnisync.CidSchemaEntryChunk) + require.NoError(t, err) + value, err = ipnisync.CidSchemaFromCtx(ctx) + require.NoError(t, err) + require.Equal(t, ipnisync.CidSchemaEntryChunk, value) + + value, err = ipnisync.CidSchemaFromCtx(ctxOrig) + require.NoError(t, err) + require.Empty(t, value) + + const unknownVal = "unknown" + + // Setting unknown value returns error as well as context with value. + ctx, err = ipnisync.CtxWithCidSchema(ctxOrig, unknownVal) + require.ErrorIs(t, err, ipnisync.ErrUnknownCidSchema) + require.NotNil(t, ctxOrig, ctx) + + // Getting unknown value returns error as well as value. + value, err = ipnisync.CidSchemaFromCtx(ctx) + require.ErrorIs(t, err, ipnisync.ErrUnknownCidSchema) + require.Equal(t, unknownVal, value) +} diff --git a/dagsync/ipnisync/publisher.go b/dagsync/ipnisync/publisher.go index 358f01e..f52f9c5 100644 --- a/dagsync/ipnisync/publisher.go +++ b/dagsync/ipnisync/publisher.go @@ -1,6 +1,7 @@ package ipnisync import ( + "context" "errors" "fmt" "net/http" @@ -12,9 +13,11 @@ import ( "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec/dagjson" + "github.com/ipld/go-ipld-prime/datamodel" cidlink "github.com/ipld/go-ipld-prime/linking/cid" basicnode "github.com/ipld/go-ipld-prime/node/basic" headschema "github.com/ipni/go-libipni/dagsync/ipnisync/head" + "github.com/ipni/go-libipni/ingest/schema" "github.com/ipni/go-libipni/maurl" ic "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" @@ -23,6 +26,10 @@ import ( ) // Publisher serves an advertisement chain over HTTP. +// +// If the publisher receives a request that contains a valid CidSchemaHeader +// header, then the ipld.Context passed to the lsys Load function contains a +// context that has that header's value retrievable with CidSchemaFromCtx. type Publisher struct { lsys ipld.LinkSystem handlerPath string @@ -218,7 +225,30 @@ func (p *Publisher) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "invalid request: not a cid", http.StatusBadRequest) return } - item, err := p.lsys.Load(ipld.LinkContext{}, cidlink.Link{Cid: c}, basicnode.Prototype.Any) + + ipldCtx := ipld.LinkContext{} + reqType := r.Header.Get(CidSchemaHeader) + if reqType != "" { + log.Debug("sync request has cid schema type hint", "hint", reqType) + ipldCtx.Ctx, err = CtxWithCidSchema(context.Background(), reqType) + if err != nil { + // Log warning about unknown cid schema type, but continue on since + // the linksystem might recognize it. + log.Warnw(err.Error(), "value", reqType) + } + } + + var ipldProto datamodel.NodePrototype + switch reqType { + case CidSchemaAdvertisement: + ipldProto = schema.AdvertisementPrototype + case CidSchemaEntryChunk: + ipldProto = schema.EntryChunkPrototype + default: + ipldProto = basicnode.Prototype.Any + } + + item, err := p.lsys.Load(ipldCtx, cidlink.Link{Cid: c}, ipldProto) if err != nil { if errors.Is(err, ipld.ErrNotExists{}) { http.Error(w, "cid not found", http.StatusNotFound) diff --git a/dagsync/ipnisync/sync.go b/dagsync/ipnisync/sync.go index 59937b7..9ec3363 100644 --- a/dagsync/ipnisync/sync.go +++ b/dagsync/ipnisync/sync.go @@ -22,6 +22,7 @@ import ( "github.com/ipld/go-ipld-prime/traversal" "github.com/ipld/go-ipld-prime/traversal/selector" headschema "github.com/ipni/go-libipni/dagsync/ipnisync/head" + "github.com/ipni/go-libipni/ingest/schema" "github.com/ipni/go-libipni/maurl" "github.com/ipni/go-libipni/mautil" "github.com/libp2p/go-libp2p/core/network" @@ -242,7 +243,23 @@ func (s *Syncer) Sync(ctx context.Context, nextCid cid.Cid, sel ipld.Node) error return fmt.Errorf("failed to compile selector: %w", err) } - cids, err := s.walkFetch(ctx, nextCid, xsel) + // Check for valid cid schema type if set. + reqType, err := CidSchemaFromCtx(ctx) + if err != nil { + return err + } + + var ipldProto datamodel.NodePrototype + switch reqType { + case CidSchemaAdvertisement: + ipldProto = schema.AdvertisementPrototype + case CidSchemaEntryChunk: + ipldProto = schema.EntryChunkPrototype + default: + ipldProto = basicnode.Prototype.Any + } + + cids, err := s.walkFetch(ctx, nextCid, xsel, ipldProto) if err != nil { return fmt.Errorf("failed to traverse requested dag: %w", err) } @@ -268,7 +285,7 @@ func (s *Syncer) Sync(ctx context.Context, nextCid cid.Cid, sel ipld.Node) error // walkFetch is run by a traversal of the selector. For each block that the // selector walks over, walkFetch will look to see if it can find it in the // local data store. If it cannot, it will then go and get it over HTTP. -func (s *Syncer) walkFetch(ctx context.Context, rootCid cid.Cid, sel selector.Selector) ([]cid.Cid, error) { +func (s *Syncer) walkFetch(ctx context.Context, rootCid cid.Cid, sel selector.Selector, ipldProto datamodel.NodePrototype) ([]cid.Cid, error) { // Track the order of cids seen during traversal so that the block hook // function gets called in the same order. var traversalOrder []cid.Cid @@ -279,7 +296,7 @@ func (s *Syncer) walkFetch(ctx context.Context, rootCid cid.Cid, sel selector.Se getMissingLs.StorageReadOpener = func(lc ipld.LinkContext, l ipld.Link) (io.Reader, error) { c := l.(cidlink.Link).Cid // fetchBlock checks if the node is already present in storage. - err := s.fetchBlock(ctx, c) + err := s.fetchBlock(ctx, c, ipldProto) if err != nil { return nil, fmt.Errorf("failed to fetch block for cid %s: %w", c, err) } @@ -301,7 +318,7 @@ func (s *Syncer) walkFetch(ctx context.Context, rootCid cid.Cid, sel selector.Se } // get the direct node. - rootNode, err := getMissingLs.Load(ipld.LinkContext{Ctx: ctx}, cidlink.Link{Cid: rootCid}, basicnode.Prototype.Any) + rootNode, err := getMissingLs.Load(ipld.LinkContext{Ctx: ctx}, cidlink.Link{Cid: rootCid}, ipldProto) if err != nil { return nil, fmt.Errorf("failed to load node for root cid %s: %w", rootCid, err) } @@ -323,6 +340,12 @@ retry: return err } + // Error already checked in Sync. + reqType, _ := CidSchemaFromCtx(ctx) + if reqType != "" { + req.Header.Set(CidSchemaHeader, reqType) + } + resp, err := s.client.Do(req) if err != nil { if len(s.urls) != 0 { @@ -378,8 +401,8 @@ retry: } // fetchBlock fetches an item into the datastore at c if not locally available. -func (s *Syncer) fetchBlock(ctx context.Context, c cid.Cid) error { - n, err := s.sync.lsys.Load(ipld.LinkContext{Ctx: ctx}, cidlink.Link{Cid: c}, basicnode.Prototype.Any) +func (s *Syncer) fetchBlock(ctx context.Context, c cid.Cid, ipldProto datamodel.NodePrototype) error { + n, err := s.sync.lsys.Load(ipld.LinkContext{Ctx: ctx}, cidlink.Link{Cid: c}, ipldProto) // node is already present. if n != nil && err == nil { return nil diff --git a/dagsync/ipnisync/sync_test.go b/dagsync/ipnisync/sync_test.go index c748b27..18606e1 100644 --- a/dagsync/ipnisync/sync_test.go +++ b/dagsync/ipnisync/sync_test.go @@ -230,3 +230,64 @@ func TestIPNIsync_NotFoundReturnsContentNotFoundErr(t *testing.T) { require.NotNil(t, err) require.Contains(t, err.Error(), "content not found") } + +func TestRequestTypeHint(t *testing.T) { + pubPrK, _, err := crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, rand.Reader) + require.NoError(t, err) + pubID, err := peer.IDFromPrivateKey(pubPrK) + require.NoError(t, err) + + var lastReqTypeHint string + + // Instantiate a dagsync publisher. + publs := cidlink.DefaultLinkSystem() + + publs.StorageReadOpener = func(lnkCtx linking.LinkContext, lnk datamodel.Link) (io.Reader, error) { + if lnkCtx.Ctx != nil { + hint, err := ipnisync.CidSchemaFromCtx(lnkCtx.Ctx) + require.NoError(t, err) + require.NotEmpty(t, hint) + lastReqTypeHint = hint + } else { + lastReqTypeHint = "" + } + + require.NotEmpty(t, lastReqTypeHint, "missing expected context value") + return nil, ipld.ErrNotExists{} + } + + pub, err := ipnisync.NewPublisher(publs, pubPrK, ipnisync.WithHTTPListenAddrs("0.0.0.0:0")) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, pub.Close()) }) + + ls := cidlink.DefaultLinkSystem() + store := &memstore.Store{} + ls.SetWriteStorage(store) + ls.SetReadStorage(store) + + sync := ipnisync.NewSync(ls, nil) + pubInfo := peer.AddrInfo{ + ID: pubID, + Addrs: pub.Addrs(), + } + syncer, err := sync.NewSyncer(pubInfo) + require.NoError(t, err) + + testCid, err := cid.Decode(sampleNFTStorageCid) + require.NoError(t, err) + + ctx, err := ipnisync.CtxWithCidSchema(context.Background(), ipnisync.CidSchemaAdvertisement) + require.NoError(t, err) + _ = syncer.Sync(ctx, testCid, selectorparse.CommonSelector_MatchPoint) + require.Equal(t, ipnisync.CidSchemaAdvertisement, lastReqTypeHint) + + ctx, err = ipnisync.CtxWithCidSchema(context.Background(), ipnisync.CidSchemaEntryChunk) + require.NoError(t, err) + _ = syncer.Sync(ctx, testCid, selectorparse.CommonSelector_MatchPoint) + require.Equal(t, ipnisync.CidSchemaEntryChunk, lastReqTypeHint) + + ctx, err = ipnisync.CtxWithCidSchema(context.Background(), "bad") + require.ErrorIs(t, err, ipnisync.ErrUnknownCidSchema) + err = syncer.Sync(ctx, testCid, selectorparse.CommonSelector_MatchPoint) + require.ErrorIs(t, err, ipnisync.ErrUnknownCidSchema) +} diff --git a/dagsync/option.go b/dagsync/option.go index 45096e2..8370d37 100644 --- a/dagsync/option.go +++ b/dagsync/option.go @@ -53,6 +53,7 @@ type config struct { gsMaxInRequests uint64 gsMaxOutRequests uint64 + cidSchemaHint bool strictAdsSelSeq bool httpTimeout time.Duration @@ -73,6 +74,7 @@ func getOpts(opts []Option) (config, error) { segDepthLimit: defaultSegDepthLimit, gsMaxInRequests: defaultGsMaxInRequests, gsMaxOutRequests: defaultGsMaxOutRequests, + cidSchemaHint: true, strictAdsSelSeq: true, } @@ -355,3 +357,10 @@ func MakeGeneralBlockHook(prevAdCid func(adCid cid.Cid) (cid.Cid, error)) BlockH } } } + +func WithCidSchemaHint(enable bool) Option { + return func(c *config) error { + c.cidSchemaHint = enable + return nil + } +} diff --git a/dagsync/subscriber.go b/dagsync/subscriber.go index dea7d66..8f557dd 100644 --- a/dagsync/subscriber.go +++ b/dagsync/subscriber.go @@ -105,6 +105,10 @@ type Subscriber struct { receiver *announce.Receiver topicName string + // cidSchemaHint enables sending the cid schema type hint as + // an HTTP header in sync requests. + cidSchemaHint bool + // Track explicit Sync calls in progress and allow them to complete before // closing subscriber. expSyncClosed bool @@ -244,6 +248,8 @@ func NewSubscriber(host host.Host, ds datastore.Batching, lsys ipld.LinkSystem, ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) { efsb.Insert("Next", ssb.ExploreRecursiveEdge()) // Next field in EntryChunk })).Node(), + + cidSchemaHint: opts.cidSchemaHint, } if opts.strictAdsSelSeq { @@ -252,6 +258,7 @@ func NewSubscriber(host host.Host, ds datastore.Batching, lsys ipld.LinkSystem, }).Node() } else { s.adsSelectorSeq = ssb.ExploreAll(ssb.ExploreRecursiveEdge()).Node() + s.cidSchemaHint = false } if opts.hasRcvr { @@ -497,6 +504,12 @@ func (s *Subscriber) SyncAdChain(ctx context.Context, peerInfo peer.AddrInfo, op sel := ExploreRecursiveWithStopNode(depthLimit, s.adsSelectorSeq, stopLnk) + if s.cidSchemaHint { + ctx, err = ipnisync.CtxWithCidSchema(ctx, ipnisync.CidSchemaAdvertisement) + if err != nil { + panic(err.Error()) + } + } syncCount, err := hnd.handle(ctx, nextCid, sel, syncer, opts.blockHook, segdl, stopAtCid) if err != nil { return cid.Undef, fmt.Errorf("sync handler failed: %w", err) @@ -580,6 +593,12 @@ func (s *Subscriber) syncEntries(ctx context.Context, peerInfo peer.AddrInfo, en log.Debugw("Start entries sync", "peer", peerInfo.ID, "cid", entCid) + if s.cidSchemaHint { + ctx, err = ipnisync.CtxWithCidSchema(ctx, ipnisync.CidSchemaAdvertisement) + if err != nil { + panic(err.Error()) + } + } _, err = hnd.handle(ctx, entCid, sel, syncer, bh, segdl, cid.Undef) if err != nil { return fmt.Errorf("sync handler failed: %w", err) @@ -882,6 +901,12 @@ func (h *handler) asyncSyncAdChain(ctx context.Context) { log.Errorw("Cannot make syncer for announce", "err", err, "peer", h.peerID) return } + if h.subscriber.cidSchemaHint { + ctx, err = ipnisync.CtxWithCidSchema(ctx, ipnisync.CidSchemaAdvertisement) + if err != nil { + panic(err.Error()) + } + } sel := ExploreRecursiveWithStopNode(adsDepthLimit, h.subscriber.adsSelectorSeq, latestSyncLink) syncCount, err := h.handle(ctx, nextCid, sel, syncer, h.subscriber.generalBlockHook, h.subscriber.segDepthLimit, stopAtCid) if err != nil { diff --git a/dagsync/test/util.go b/dagsync/test/util.go index a45b499..b111395 100644 --- a/dagsync/test/util.go +++ b/dagsync/test/util.go @@ -170,8 +170,12 @@ func encode(lsys ipld.LinkSystem, n ipld.Node) (ipld.Node, ipld.Link) { func MkLinkSystem(ds datastore.Batching) ipld.LinkSystem { lsys := cidlink.DefaultLinkSystem() - lsys.StorageReadOpener = func(_ ipld.LinkContext, lnk ipld.Link) (io.Reader, error) { - val, err := ds.Get(context.Background(), datastore.NewKey(lnk.String())) + lsys.StorageReadOpener = func(ipldCtx ipld.LinkContext, lnk ipld.Link) (io.Reader, error) { + ctx := ipldCtx.Ctx + if ctx == nil { + ctx = context.Background() + } + val, err := ds.Get(ctx, datastore.NewKey(lnk.String())) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 394ebdc..857bb42 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/ipfs/go-log/v2 v2.5.1 github.com/ipfs/go-test v0.0.4 github.com/ipld/go-ipld-prime v0.21.0 - github.com/libp2p/go-libp2p v0.36.2 + github.com/libp2p/go-libp2p v0.36.3 github.com/libp2p/go-libp2p-pubsub v0.12.0 github.com/libp2p/go-msgio v0.3.0 github.com/mr-tron/base58 v1.2.0 diff --git a/go.sum b/go.sum index d975495..3dad43c 100644 --- a/go.sum +++ b/go.sum @@ -325,8 +325,8 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= -github.com/libp2p/go-libp2p v0.36.2 h1:BbqRkDaGC3/5xfaJakLV/BrpjlAuYqSB0lRvtzL3B/U= -github.com/libp2p/go-libp2p v0.36.2/go.mod h1:XO3joasRE4Eup8yCTTP/+kX+g92mOgRaadk46LmPhHY= +github.com/libp2p/go-libp2p v0.36.3 h1:NHz30+G7D8Y8YmznrVZZla0ofVANrvBl2c+oARfMeDQ= +github.com/libp2p/go-libp2p v0.36.3/go.mod h1:4Y5vFyCUiJuluEPmpnKYf6WFx5ViKPUYs/ixe9ANFZ8= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-pubsub v0.12.0 h1:PENNZjSfk8KYxANRlpipdS7+BfLmOl3L2E/6vSNjbdI=