diff --git a/network/p2p/gossip/bloom.go b/network/p2p/gossip/bloom.go new file mode 100644 index 000000000000..5b1c6bc8c390 --- /dev/null +++ b/network/p2p/gossip/bloom.go @@ -0,0 +1,130 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gossip + +import ( + "crypto/rand" + "encoding/binary" + "hash" + + bloomfilter "github.com/holiman/bloomfilter/v2" + + "github.com/ava-labs/avalanchego/ids" +) + +var _ hash.Hash64 = (*hasher)(nil) + +// NewBloomFilter returns a new instance of a bloom filter with at most +// [maxExpectedElements] elements anticipated at any moment, and a false +// positive probability of [falsePositiveProbability]. +func NewBloomFilter( + maxExpectedElements uint64, + falsePositiveProbability float64, +) (*BloomFilter, error) { + bloom, err := bloomfilter.NewOptimal( + maxExpectedElements, + falsePositiveProbability, + ) + if err != nil { + return nil, err + } + + salt, err := randomSalt() + return &BloomFilter{ + Bloom: bloom, + Salt: salt, + }, err +} + +type BloomFilter struct { + Bloom *bloomfilter.Filter + // Salt is provided to eventually unblock collisions in Bloom. It's possible + // that conflicting Gossipable items collide in the bloom filter, so a salt + // is generated to eventually resolve collisions. + Salt ids.ID +} + +func (b *BloomFilter) Add(gossipable Gossipable) { + h := gossipable.GetID() + salted := &hasher{ + hash: h[:], + salt: b.Salt, + } + b.Bloom.Add(salted) +} + +func (b *BloomFilter) Has(gossipable Gossipable) bool { + h := gossipable.GetID() + salted := &hasher{ + hash: h[:], + salt: b.Salt, + } + return b.Bloom.Contains(salted) +} + +// ResetBloomFilterIfNeeded resets a bloom filter if it breaches a target false +// positive probability. Returns true if the bloom filter was reset. +func ResetBloomFilterIfNeeded( + bloomFilter *BloomFilter, + falsePositiveProbability float64, +) (bool, error) { + if bloomFilter.Bloom.FalsePosititveProbability() < falsePositiveProbability { + return false, nil + } + + newBloom, err := bloomfilter.New(bloomFilter.Bloom.M(), bloomFilter.Bloom.K()) + if err != nil { + return false, err + } + salt, err := randomSalt() + if err != nil { + return false, err + } + + bloomFilter.Bloom = newBloom + bloomFilter.Salt = salt + return true, nil +} + +func randomSalt() (ids.ID, error) { + salt := ids.ID{} + _, err := rand.Read(salt[:]) + return salt, err +} + +type hasher struct { + hash []byte + salt ids.ID +} + +func (h *hasher) Write(p []byte) (n int, err error) { + h.hash = append(h.hash, p...) + return len(p), nil +} + +func (h *hasher) Sum(b []byte) []byte { + h.hash = append(h.hash, b...) + return h.hash +} + +func (h *hasher) Reset() { + h.hash = ids.Empty[:] +} + +func (*hasher) BlockSize() int { + return ids.IDLen +} + +func (h *hasher) Sum64() uint64 { + salted := ids.ID{} + for i := 0; i < len(h.hash) && i < ids.IDLen; i++ { + salted[i] = h.hash[i] ^ h.salt[i] + } + + return binary.BigEndian.Uint64(salted[:]) +} + +func (h *hasher) Size() int { + return len(h.hash) +} diff --git a/network/p2p/gossip/bloom_test.go b/network/p2p/gossip/bloom_test.go new file mode 100644 index 000000000000..860d2d5e936e --- /dev/null +++ b/network/p2p/gossip/bloom_test.go @@ -0,0 +1,68 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gossip + +import ( + "testing" + + bloomfilter "github.com/holiman/bloomfilter/v2" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" +) + +func TestBloomFilterRefresh(t *testing.T) { + tests := []struct { + name string + falsePositiveProbability float64 + add []*testTx + expected []*testTx + }{ + { + name: "no refresh", + falsePositiveProbability: 1, + add: []*testTx{ + {id: ids.ID{0}}, + }, + expected: []*testTx{ + {id: ids.ID{0}}, + }, + }, + { + name: "refresh", + falsePositiveProbability: 0.1, + add: []*testTx{ + {id: ids.ID{0}}, + {id: ids.ID{1}}, + }, + expected: []*testTx{ + {id: ids.ID{1}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + b, err := bloomfilter.New(10, 1) + require.NoError(err) + bloom := BloomFilter{ + Bloom: b, + } + + for _, item := range tt.add { + _, err = ResetBloomFilterIfNeeded(&bloom, tt.falsePositiveProbability) + require.NoError(err) + bloom.Add(item) + } + + require.Equal(uint64(len(tt.expected)), bloom.Bloom.N()) + + for _, expected := range tt.expected { + require.True(bloom.Has(expected)) + } + }) + } +} diff --git a/network/p2p/gossip/gossip.go b/network/p2p/gossip/gossip.go new file mode 100644 index 000000000000..3bf08904fa5b --- /dev/null +++ b/network/p2p/gossip/gossip.go @@ -0,0 +1,141 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gossip + +import ( + "context" + "time" + + "go.uber.org/zap" + + "google.golang.org/protobuf/proto" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p" + "github.com/ava-labs/avalanchego/proto/pb/sdk" + "github.com/ava-labs/avalanchego/utils/logging" +) + +// GossipableAny exists to help create non-nil pointers to a concrete Gossipable +// ref: https://stackoverflow.com/questions/69573113/how-can-i-instantiate-a-non-nil-pointer-of-type-argument-with-generic-go +type GossipableAny[T any] interface { + *T + Gossipable +} + +type Config struct { + Frequency time.Duration + PollSize int +} + +func NewGossiper[T any, U GossipableAny[T]]( + config Config, + log logging.Logger, + set Set[U], + client *p2p.Client, +) *Gossiper[T, U] { + return &Gossiper[T, U]{ + config: config, + log: log, + set: set, + client: client, + } +} + +type Gossiper[T any, U GossipableAny[T]] struct { + config Config + log logging.Logger + set Set[U] + client *p2p.Client +} + +func (g *Gossiper[_, _]) Gossip(ctx context.Context) { + gossipTicker := time.NewTicker(g.config.Frequency) + defer gossipTicker.Stop() + + for { + select { + case <-gossipTicker.C: + if err := g.gossip(ctx); err != nil { + g.log.Warn("failed to gossip", zap.Error(err)) + } + case <-ctx.Done(): + g.log.Debug("shutting down gossip") + return + } + } +} + +func (g *Gossiper[_, _]) gossip(ctx context.Context) error { + bloom, salt, err := g.set.GetFilter() + if err != nil { + return err + } + + request := &sdk.PullGossipRequest{ + Filter: bloom, + Salt: salt, + } + msgBytes, err := proto.Marshal(request) + if err != nil { + return err + } + + for i := 0; i < g.config.PollSize; i++ { + if err := g.client.AppRequestAny(ctx, msgBytes, g.handleResponse); err != nil { + return err + } + } + + return nil +} + +func (g *Gossiper[T, U]) handleResponse( + nodeID ids.NodeID, + responseBytes []byte, + err error, +) { + if err != nil { + g.log.Debug( + "failed gossip request", + zap.Stringer("nodeID", nodeID), + zap.Error(err), + ) + return + } + + response := &sdk.PullGossipResponse{} + if err := proto.Unmarshal(responseBytes, response); err != nil { + g.log.Debug("failed to unmarshal gossip response", zap.Error(err)) + return + } + + for _, bytes := range response.Gossip { + gossipable := U(new(T)) + if err := gossipable.Unmarshal(bytes); err != nil { + g.log.Debug( + "failed to unmarshal gossip", + zap.Stringer("nodeID", nodeID), + zap.Error(err), + ) + continue + } + + hash := gossipable.GetID() + g.log.Debug( + "received gossip", + zap.Stringer("nodeID", nodeID), + zap.Stringer("id", hash), + ) + if err := g.set.Add(gossipable); err != nil { + g.log.Debug( + "failed to add gossip to the known set", + zap.Stringer("nodeID", nodeID), + zap.Stringer("id", hash), + zap.Error(err), + ) + continue + } + } +} diff --git a/network/p2p/gossip/gossip_test.go b/network/p2p/gossip/gossip_test.go new file mode 100644 index 000000000000..ab8dab502eca --- /dev/null +++ b/network/p2p/gossip/gossip_test.go @@ -0,0 +1,168 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gossip + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" +) + +func TestGossiperShutdown(_ *testing.T) { + config := Config{Frequency: time.Second} + gossiper := NewGossiper[testTx](config, logging.NoLog{}, nil, nil) + ctx, cancel := context.WithCancel(context.Background()) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + gossiper.Gossip(ctx) + wg.Done() + }() + + cancel() + wg.Wait() +} + +func TestGossiperGossip(t *testing.T) { + tests := []struct { + name string + maxResponseSize int + requester []*testTx // what we have + responder []*testTx // what the peer we're requesting gossip from has + expectedPossibleValues []*testTx // possible values we can have + expectedLen int + }{ + { + name: "no gossip - no one knows anything", + }, + { + name: "no gossip - requester knows more than responder", + maxResponseSize: 1024, + requester: []*testTx{{id: ids.ID{0}}}, + expectedPossibleValues: []*testTx{{id: ids.ID{0}}}, + expectedLen: 1, + }, + { + name: "no gossip - requester knows everything responder knows", + maxResponseSize: 1024, + requester: []*testTx{{id: ids.ID{0}}}, + responder: []*testTx{{id: ids.ID{0}}}, + expectedPossibleValues: []*testTx{{id: ids.ID{0}}}, + expectedLen: 1, + }, + { + name: "gossip - requester knows nothing", + maxResponseSize: 1024, + responder: []*testTx{{id: ids.ID{0}}}, + expectedPossibleValues: []*testTx{{id: ids.ID{0}}}, + expectedLen: 1, + }, + { + name: "gossip - requester knows less than responder", + maxResponseSize: 1024, + requester: []*testTx{{id: ids.ID{0}}}, + responder: []*testTx{{id: ids.ID{0}}, {id: ids.ID{1}}}, + expectedPossibleValues: []*testTx{{id: ids.ID{0}}, {id: ids.ID{1}}}, + expectedLen: 2, + }, + { + name: "gossip - max response size exceeded", + maxResponseSize: 32, + responder: []*testTx{{id: ids.ID{0}}, {id: ids.ID{1}}}, + expectedPossibleValues: []*testTx{{id: ids.ID{0}}, {id: ids.ID{1}}}, + expectedLen: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + responseSender := common.NewMockSender(ctrl) + responseRouter := p2p.NewRouter(logging.NoLog{}, responseSender) + responseBloom, err := NewBloomFilter(1000, 0.01) + require.NoError(err) + responseSet := testSet{ + set: set.Set[*testTx]{}, + bloom: responseBloom, + } + for _, item := range tt.responder { + require.NoError(responseSet.Add(item)) + } + peers := &p2p.Peers{} + require.NoError(peers.Connected(context.Background(), ids.EmptyNodeID, nil)) + + handler := NewHandler[*testTx](responseSet, tt.maxResponseSize) + _, err = responseRouter.RegisterAppProtocol(0x0, handler, peers) + require.NoError(err) + + requestSender := common.NewMockSender(ctrl) + requestRouter := p2p.NewRouter(logging.NoLog{}, requestSender) + + gossiped := make(chan struct{}) + requestSender.EXPECT().SendAppRequest(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Do(func(ctx context.Context, nodeIDs set.Set[ids.NodeID], requestID uint32, request []byte) { + go func() { + require.NoError(responseRouter.AppRequest(ctx, ids.EmptyNodeID, requestID, time.Time{}, request)) + }() + }).AnyTimes() + + responseSender.EXPECT(). + SendAppResponse(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Do(func(ctx context.Context, nodeID ids.NodeID, requestID uint32, appResponseBytes []byte) { + require.NoError(requestRouter.AppResponse(ctx, nodeID, requestID, appResponseBytes)) + close(gossiped) + }).AnyTimes() + + bloom, err := NewBloomFilter(1000, 0.01) + require.NoError(err) + requestSet := testSet{ + set: set.Set[*testTx]{}, + bloom: bloom, + } + for _, item := range tt.requester { + require.NoError(requestSet.Add(item)) + } + + requestClient, err := requestRouter.RegisterAppProtocol(0x0, nil, peers) + require.NoError(err) + + config := Config{ + Frequency: 500 * time.Millisecond, + PollSize: 1, + } + gossiper := NewGossiper[testTx, *testTx](config, logging.NoLog{}, requestSet, requestClient) + received := set.Set[*testTx]{} + requestSet.onAdd = func(tx *testTx) { + received.Add(tx) + } + + require.NoError(gossiper.gossip(context.Background())) + <-gossiped + + require.Len(requestSet.set, tt.expectedLen) + require.Subset(tt.expectedPossibleValues, requestSet.set.List()) + + // we should not receive anything that we already had before we + // requested the gossip + for _, tx := range tt.requester { + require.NotContains(received, tx) + } + }) + } +} diff --git a/network/p2p/gossip/gossipable.go b/network/p2p/gossip/gossipable.go new file mode 100644 index 000000000000..84c37e2d6b84 --- /dev/null +++ b/network/p2p/gossip/gossipable.go @@ -0,0 +1,24 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gossip + +import "github.com/ava-labs/avalanchego/ids" + +// Gossipable is an item that can be gossiped across the network +type Gossipable interface { + GetID() ids.ID + Marshal() ([]byte, error) + Unmarshal(bytes []byte) error +} + +// Set holds a set of known Gossipable items +type Set[T Gossipable] interface { + // Add adds a Gossipable to the set + Add(gossipable T) error + // Iterate iterates over elements until [f] returns false + Iterate(f func(gossipable T) bool) + // GetFilter returns the byte representation of bloom filter and its + // corresponding salt. + GetFilter() (bloom []byte, salt []byte, err error) +} diff --git a/network/p2p/gossip/handler.go b/network/p2p/gossip/handler.go new file mode 100644 index 000000000000..934701c9160f --- /dev/null +++ b/network/p2p/gossip/handler.go @@ -0,0 +1,93 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gossip + +import ( + "context" + "errors" + "time" + + bloomfilter "github.com/holiman/bloomfilter/v2" + + "google.golang.org/protobuf/proto" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/p2p" + "github.com/ava-labs/avalanchego/proto/pb/sdk" +) + +var ( + _ p2p.Handler = (*Handler[Gossipable])(nil) + + ErrInvalidID = errors.New("invalid id") +) + +func NewHandler[T Gossipable](set Set[T], maxResponseSize int) *Handler[T] { + return &Handler[T]{ + Handler: p2p.NoOpHandler{}, + set: set, + maxResponseSize: maxResponseSize, + } +} + +type Handler[T Gossipable] struct { + p2p.Handler + set Set[T] + maxResponseSize int +} + +func (h Handler[T]) AppRequest(_ context.Context, _ ids.NodeID, _ time.Time, requestBytes []byte) ([]byte, error) { + request := &sdk.PullGossipRequest{} + if err := proto.Unmarshal(requestBytes, request); err != nil { + return nil, err + } + + salt, err := ids.ToID(request.Salt) + if err != nil { + return nil, err + } + + filter := &BloomFilter{ + Bloom: &bloomfilter.Filter{}, + Salt: salt, + } + if err := filter.Bloom.UnmarshalBinary(request.Filter); err != nil { + return nil, err + } + + responseSize := 0 + gossipBytes := make([][]byte, 0) + h.set.Iterate(func(gossipable T) bool { + // filter out what the requesting peer already knows about + if filter.Has(gossipable) { + return true + } + + var bytes []byte + bytes, err = gossipable.Marshal() + if err != nil { + return false + } + + // check that this doesn't exceed our maximum configured response size + responseSize += len(bytes) + if responseSize > h.maxResponseSize { + return false + } + + gossipBytes = append(gossipBytes, bytes) + + return true + }) + + if err != nil { + return nil, err + } + + response := &sdk.PullGossipResponse{ + Gossip: gossipBytes, + } + + return proto.Marshal(response) +} diff --git a/network/p2p/gossip/test_gossip.go b/network/p2p/gossip/test_gossip.go new file mode 100644 index 000000000000..ba114adf3774 --- /dev/null +++ b/network/p2p/gossip/test_gossip.go @@ -0,0 +1,61 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package gossip + +import ( + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/set" +) + +var ( + _ Gossipable = (*testTx)(nil) + _ Set[*testTx] = (*testSet)(nil) +) + +type testTx struct { + id ids.ID +} + +func (t *testTx) GetID() ids.ID { + return t.id +} + +func (t *testTx) Marshal() ([]byte, error) { + return t.id[:], nil +} + +func (t *testTx) Unmarshal(bytes []byte) error { + t.id = ids.ID{} + copy(t.id[:], bytes) + return nil +} + +type testSet struct { + set set.Set[*testTx] + bloom *BloomFilter + onAdd func(tx *testTx) +} + +func (t testSet) Add(gossipable *testTx) error { + t.set.Add(gossipable) + t.bloom.Add(gossipable) + if t.onAdd != nil { + t.onAdd(gossipable) + } + + return nil +} + +func (t testSet) Iterate(f func(gossipable *testTx) bool) { + for tx := range t.set { + if !f(tx) { + return + } + } +} + +func (t testSet) GetFilter() ([]byte, []byte, error) { + bloom, err := t.bloom.Bloom.MarshalBinary() + return bloom, t.bloom.Salt[:], err +} diff --git a/proto/pb/sdk/sdk.pb.go b/proto/pb/sdk/sdk.pb.go new file mode 100644 index 000000000000..120974ee5976 --- /dev/null +++ b/proto/pb/sdk/sdk.pb.go @@ -0,0 +1,216 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc (unknown) +// source: sdk/sdk.proto + +package sdk + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PullGossipRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Filter []byte `protobuf:"bytes,1,opt,name=filter,proto3" json:"filter,omitempty"` + Salt []byte `protobuf:"bytes,2,opt,name=salt,proto3" json:"salt,omitempty"` +} + +func (x *PullGossipRequest) Reset() { + *x = PullGossipRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_sdk_sdk_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PullGossipRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PullGossipRequest) ProtoMessage() {} + +func (x *PullGossipRequest) ProtoReflect() protoreflect.Message { + mi := &file_sdk_sdk_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PullGossipRequest.ProtoReflect.Descriptor instead. +func (*PullGossipRequest) Descriptor() ([]byte, []int) { + return file_sdk_sdk_proto_rawDescGZIP(), []int{0} +} + +func (x *PullGossipRequest) GetFilter() []byte { + if x != nil { + return x.Filter + } + return nil +} + +func (x *PullGossipRequest) GetSalt() []byte { + if x != nil { + return x.Salt + } + return nil +} + +type PullGossipResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Gossip [][]byte `protobuf:"bytes,1,rep,name=gossip,proto3" json:"gossip,omitempty"` +} + +func (x *PullGossipResponse) Reset() { + *x = PullGossipResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_sdk_sdk_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PullGossipResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PullGossipResponse) ProtoMessage() {} + +func (x *PullGossipResponse) ProtoReflect() protoreflect.Message { + mi := &file_sdk_sdk_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PullGossipResponse.ProtoReflect.Descriptor instead. +func (*PullGossipResponse) Descriptor() ([]byte, []int) { + return file_sdk_sdk_proto_rawDescGZIP(), []int{1} +} + +func (x *PullGossipResponse) GetGossip() [][]byte { + if x != nil { + return x.Gossip + } + return nil +} + +var File_sdk_sdk_proto protoreflect.FileDescriptor + +var file_sdk_sdk_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x73, 0x64, 0x6b, 0x2f, 0x73, 0x64, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x03, 0x73, 0x64, 0x6b, 0x22, 0x3f, 0x0a, 0x11, 0x50, 0x75, 0x6c, 0x6c, 0x47, 0x6f, 0x73, 0x73, + 0x69, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x04, 0x73, 0x61, 0x6c, 0x74, 0x22, 0x2c, 0x0a, 0x12, 0x50, 0x75, 0x6c, 0x6c, 0x47, 0x6f, 0x73, + 0x73, 0x69, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x67, + 0x6f, 0x73, 0x73, 0x69, 0x70, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x06, 0x67, 0x6f, 0x73, + 0x73, 0x69, 0x70, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x61, 0x76, 0x61, 0x2d, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x61, 0x76, 0x61, 0x6c, 0x61, + 0x6e, 0x63, 0x68, 0x65, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x62, 0x2f, + 0x73, 0x64, 0x6b, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_sdk_sdk_proto_rawDescOnce sync.Once + file_sdk_sdk_proto_rawDescData = file_sdk_sdk_proto_rawDesc +) + +func file_sdk_sdk_proto_rawDescGZIP() []byte { + file_sdk_sdk_proto_rawDescOnce.Do(func() { + file_sdk_sdk_proto_rawDescData = protoimpl.X.CompressGZIP(file_sdk_sdk_proto_rawDescData) + }) + return file_sdk_sdk_proto_rawDescData +} + +var file_sdk_sdk_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_sdk_sdk_proto_goTypes = []interface{}{ + (*PullGossipRequest)(nil), // 0: sdk.PullGossipRequest + (*PullGossipResponse)(nil), // 1: sdk.PullGossipResponse +} +var file_sdk_sdk_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_sdk_sdk_proto_init() } +func file_sdk_sdk_proto_init() { + if File_sdk_sdk_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_sdk_sdk_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PullGossipRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sdk_sdk_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PullGossipResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_sdk_sdk_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_sdk_sdk_proto_goTypes, + DependencyIndexes: file_sdk_sdk_proto_depIdxs, + MessageInfos: file_sdk_sdk_proto_msgTypes, + }.Build() + File_sdk_sdk_proto = out.File + file_sdk_sdk_proto_rawDesc = nil + file_sdk_sdk_proto_goTypes = nil + file_sdk_sdk_proto_depIdxs = nil +} diff --git a/proto/sdk/sdk.proto b/proto/sdk/sdk.proto new file mode 100644 index 000000000000..20bfca081856 --- /dev/null +++ b/proto/sdk/sdk.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package sdk; + +option go_package = "github.com/ava-labs/avalanchego/proto/pb/sdk"; + +message PullGossipRequest { + bytes filter = 1; + bytes salt = 2; +} + +message PullGossipResponse { + repeated bytes gossip = 1; +}