Regular Sync - First Pass (#3201)

* checkpoint

* checkpoint

* varint prefix for ssz

* move the encoding API around a little bit to support reader writer

* add a simple test for the happy path subscribe

* move wait timeout to testutil

* Add inverted topic mapping

* Add varint prefixing to ssz network encoder

* fix spacing

* fix comments

* fix comments

* make anon methods more clear

* clean up log fields

* move topic mapping, reformat TODOs, get ready for brutal team review

* lint

* lint

* lint

* Update beacon-chain/p2p/gossip_topic_mappings.go

Co-Authored-By: Nishant Das <nishdas93@gmail.com>

* PR feedback

* PR feedback

* PR feedback

* PR feedback

* PR feedback

* Update WORKSPACE

* Update WORKSPACE
This commit is contained in:
Preston Van Loon
2019-08-16 13:13:04 -04:00
committed by GitHub
parent 6ec9d7e6e2
commit 81f868bd48
21 changed files with 673 additions and 1 deletions

View File

@@ -205,7 +205,7 @@ go_repository(
go_repository(
name = "com_github_prysmaticlabs_go_ssz",
commit = "4c975a4b9dc1575778ccd3d02740fc81b977b540",
commit = "de7d8d169d83bf8ca5ac66739fc49685fd6b05e0",
importpath = "github.com/prysmaticlabs/go-ssz",
)

View File

@@ -3,17 +3,25 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"broadcaster.go",
"config.go",
"doc.go",
"gossip_topic_mappings.go",
"interfaces.go",
"log.go",
"service.go",
],
importpath = "github.com/prysmaticlabs/prysm/beacon-chain/p2p",
visibility = ["//beacon-chain:__subpackages__"],
deps = [
"//beacon-chain/p2p/encoder:go_default_library",
"//proto/eth/v1alpha1:go_default_library",
"//shared:go_default_library",
"@com_github_gogo_protobuf//proto:go_default_library",
"@com_github_libp2p_go_libp2p//:go_default_library",
"@com_github_libp2p_go_libp2p_core//host:go_default_library",
"@com_github_libp2p_go_libp2p_core//network:go_default_library",
"@com_github_libp2p_go_libp2p_core//protocol:go_default_library",
"@com_github_libp2p_go_libp2p_pubsub//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",

View File

@@ -0,0 +1,8 @@
package p2p
import "github.com/gogo/protobuf/proto"
// Broadcast a message to the p2p network.
func (s *Service) Broadcast(msg proto.Message) {
// TODO(3147): implement
}

View File

@@ -0,0 +1,28 @@
package p2p
import (
"reflect"
"github.com/gogo/protobuf/proto"
pb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1"
)
// GossipTopicMappings represent the protocol ID to protobuf message type map for easy
// lookup.
var GossipTopicMappings = map[string]proto.Message{
"/eth2/beacon_block": &pb.BeaconBlock{},
"/eth2/beacon_attestation": &pb.Attestation{},
"/eth2/voluntary_exit": &pb.VoluntaryExit{},
"/eth2/proposer_slashing": &pb.ProposerSlashing{},
"/eth2/attester_slashing": &pb.AttesterSlashing{},
}
// GossipTypeMapping is the inverse of GossipTopicMappings so that an arbitrary protobuf message
// can be mapped to a protocol ID string.
var GossipTypeMapping = make(map[reflect.Type]string)
func init() {
for k, v := range GossipTopicMappings {
GossipTypeMapping[reflect.TypeOf(v)] = k
}
}

View File

@@ -0,0 +1,36 @@
package p2p
import (
"github.com/gogo/protobuf/proto"
"github.com/libp2p/go-libp2p-core/network"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/prysmaticlabs/prysm/beacon-chain/p2p/encoder"
)
// P2P represents the full p2p interface composed of all of the sub-interfaces.
type P2P interface {
Broadcaster
SetStreamHandler
EncodingProvider
PubSubProvider
}
// Broadcaster broadcasts messages to peers over the p2p pubsub protocol.
type Broadcaster interface {
Broadcast(proto.Message)
}
// SetStreamHandler configures p2p to handle streams of a certain topic ID.
type SetStreamHandler interface {
SetStreamHandler(topic string, handler network.StreamHandler)
}
// EncodingProvider provides p2p network encoding.
type EncodingProvider interface {
Encoding() encoder.NetworkEncoding
}
// PubSubProvider provides the p2p pubsub protocol.
type PubSubProvider interface {
PubSub() *pubsub.PubSub
}

View File

@@ -5,8 +5,11 @@ import (
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p-core/host"
network "github.com/libp2p/go-libp2p-core/network"
"github.com/libp2p/go-libp2p-core/protocol"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/beacon-chain/p2p/encoder"
"github.com/prysmaticlabs/prysm/shared"
)
@@ -75,3 +78,19 @@ func (s *Service) Status() error {
}
return nil
}
// Encoding returns the configured networking encoding.
func (s *Service) Encoding() encoder.NetworkEncoding {
return nil
}
// PubSub returns the p2p pubsub framework.
func (s *Service) PubSub() *pubsub.PubSub {
return s.pubsub
}
// SetStreamHandler sets the protocol handler on the p2p host multiplexer.
// This method is a pass through to libp2pcore.Host.SetStreamHandler.
func (s *Service) SetStreamHandler(topic string, handler network.StreamHandler) {
s.host.SetStreamHandler(protocol.ID(topic), handler)
}

View File

@@ -0,0 +1,20 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
testonly = True,
srcs = ["p2p.go"],
importpath = "github.com/prysmaticlabs/prysm/beacon-chain/p2p/testing",
visibility = ["//beacon-chain:__subpackages__"],
deps = [
"//beacon-chain/p2p:go_default_library",
"//beacon-chain/p2p/encoder:go_default_library",
"@com_github_gogo_protobuf//proto:go_default_library",
"@com_github_libp2p_go_libp2p_blankhost//:go_default_library",
"@com_github_libp2p_go_libp2p_core//host:go_default_library",
"@com_github_libp2p_go_libp2p_core//network:go_default_library",
"@com_github_libp2p_go_libp2p_core//protocol:go_default_library",
"@com_github_libp2p_go_libp2p_pubsub//:go_default_library",
"@com_github_libp2p_go_libp2p_swarm//testing:go_default_library",
],
)

View File

@@ -0,0 +1,128 @@
package testing
import (
"bytes"
"context"
"testing"
"time"
"github.com/gogo/protobuf/proto"
bhost "github.com/libp2p/go-libp2p-blankhost"
"github.com/libp2p/go-libp2p-core/host"
"github.com/libp2p/go-libp2p-core/network"
"github.com/libp2p/go-libp2p-core/protocol"
pubsub "github.com/libp2p/go-libp2p-pubsub"
swarmt "github.com/libp2p/go-libp2p-swarm/testing"
"github.com/prysmaticlabs/prysm/beacon-chain/p2p"
"github.com/prysmaticlabs/prysm/beacon-chain/p2p/encoder"
)
var _ = p2p.P2P(&TestP2P{})
// TestP2P represents a p2p implementation that can be used for testing.
type TestP2P struct {
t *testing.T
host host.Host
pubsub *pubsub.PubSub
}
// NewTestP2P initializes a new p2p test service.
func NewTestP2P(t *testing.T) *TestP2P {
ctx := context.Background()
h := bhost.NewBlankHost(swarmt.GenSwarm(t, ctx))
ps, err := pubsub.NewFloodSub(ctx, h,
pubsub.WithMessageSigning(false),
pubsub.WithStrictSignatureVerification(false),
)
if err != nil {
t.Fatal(err)
}
return &TestP2P{
t: t,
host: h,
pubsub: ps,
}
}
// Connect two test peers together.
func (p *TestP2P) Connect(b *TestP2P) {
if err := connect(p.host, b.host); err != nil {
p.t.Fatal(err)
}
}
func connect(a, b host.Host) error {
pinfo := a.Peerstore().PeerInfo(a.ID())
return b.Connect(context.Background(), pinfo)
}
// ReceiveRPC simulates an incoming RPC.
func (p *TestP2P) ReceiveRPC(topic string, msg proto.Message) {
h := bhost.NewBlankHost(swarmt.GenSwarm(p.t, context.Background()))
if err := connect(h, p.host); err != nil {
p.t.Fatalf("Failed to connect two peers for RPC: %v", err)
}
s, err := h.NewStream(context.Background(), p.host.ID(), protocol.ID(topic+p.Encoding().ProtocolSuffix()))
if err != nil {
p.t.Fatalf("Failed to open stream %v", err)
}
defer s.Close()
n, err := p.Encoding().Encode(s, msg)
if err != nil {
p.t.Fatalf("Failed to encode message: %v", err)
}
p.t.Logf("Wrote %d bytes", n)
}
// ReceivePubSub simulates an incoming message over pubsub on a given topic.
func (p *TestP2P) ReceivePubSub(topic string, msg proto.Message) {
h := bhost.NewBlankHost(swarmt.GenSwarm(p.t, context.Background()))
ps, err := pubsub.NewFloodSub(context.Background(), h,
pubsub.WithMessageSigning(false),
pubsub.WithStrictSignatureVerification(false),
)
if err != nil {
p.t.Fatalf("Failed to create flood sub: %v", err)
}
if err := connect(h, p.host); err != nil {
p.t.Fatalf("Failed to connect two peers for RPC: %v", err)
}
// PubSub requires some delay after connecting for the (*PubSub).processLoop method to
// pick up the newly connected peer.
time.Sleep(time.Millisecond * 100)
buf := new(bytes.Buffer)
if _, err := p.Encoding().Encode(buf, msg); err != nil {
p.t.Fatalf("Failed to encode message: %v", err)
}
if err := ps.Publish(topic+p.Encoding().ProtocolSuffix(), buf.Bytes()); err != nil {
p.t.Fatalf("Failed to publish message; %v", err)
}
}
// Broadcast a message.
func (p *TestP2P) Broadcast(msg proto.Message) {
// TODO(3147): implement
}
// SetStreamHandler for RPC.
func (p *TestP2P) SetStreamHandler(topic string, handler network.StreamHandler) {
p.host.SetStreamHandler(protocol.ID(topic), handler)
}
// Encoding returns ssz encoding.
func (p *TestP2P) Encoding() encoder.NetworkEncoding {
return &encoder.SszNetworkEncoder{}
}
// PubSub returns reference underlying floodsub. This test library uses floodsub
// to ensure all connected peers receive the message.
func (p *TestP2P) PubSub() *pubsub.PubSub {
return p.pubsub
}

View File

@@ -0,0 +1,44 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"log.go",
"metrics.go",
"rpc.go",
"rpc_hello.go",
"service.go",
"subscriber.go",
],
importpath = "github.com/prysmaticlabs/prysm/beacon-chain/sync",
visibility = ["//beacon-chain:__subpackages__"],
deps = [
"//beacon-chain/p2p:go_default_library",
"//proto/beacon/p2p/v1:go_default_library",
"//shared:go_default_library",
"//shared/roughtime:go_default_library",
"@com_github_gogo_protobuf//proto:go_default_library",
"@com_github_libp2p_go_libp2p_core//:go_default_library",
"@com_github_libp2p_go_libp2p_core//network:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
],
)
go_test(
name = "go_default_test",
size = "small",
srcs = [
"rpc_test.go",
"subscriber_test.go",
],
embed = [":go_default_library"],
deps = [
"//beacon-chain/p2p/testing:go_default_library",
"//proto/eth/v1alpha1:go_default_library",
"//proto/testing:go_default_library",
"//shared/testutil:go_default_library",
"@com_github_gogo_protobuf//proto:go_default_library",
"@com_github_libp2p_go_libp2p_core//:go_default_library",
],
)

4
beacon-chain/sync/doc.go Normal file
View File

@@ -0,0 +1,4 @@
/*
Package sync TODO(3147): Add details on how sync works.
*/
package sync

5
beacon-chain/sync/log.go Normal file
View File

@@ -0,0 +1,5 @@
package sync
import "github.com/sirupsen/logrus"
var log = logrus.WithField("prefix", "sync")

View File

@@ -0,0 +1,3 @@
package sync
// TODO(3147): Add metrics for RPC & subscription success/error.

80
beacon-chain/sync/rpc.go Normal file
View File

@@ -0,0 +1,80 @@
package sync
import (
"context"
"errors"
"time"
"github.com/gogo/protobuf/proto"
libp2pcore "github.com/libp2p/go-libp2p-core"
"github.com/libp2p/go-libp2p-core/network"
pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1"
"github.com/prysmaticlabs/prysm/shared/roughtime"
)
// Time to first byte timeout. The maximum time to wait for first byte of
// request response (time-to-first-byte). The client is expected to give up if
// they don't receive the first byte within 5 seconds.
var ttfbTimeout = 5 * time.Second
// rpcHandler is responsible for handling and responding to any incoming message.
// This method may return an error to internal monitoring, but the error will
// not be relayed to the peer.
type rpcHandler func(context.Context, proto.Message, libp2pcore.Stream) error
// TODO(3147): Delete after all handlers implemented.
func notImplementedRPCHandler(_ context.Context, _ proto.Message, _ libp2pcore.Stream) error {
return errors.New("not implemented")
}
// registerRPCHandlers for p2p RPC.
func (r *RegularSync) registerRPCHandlers() {
r.registerRPC(
"/eth2/beacon_chain/req/hello/1",
&pb.Hello{},
r.helloRPCHandler,
)
r.registerRPC(
"/eth2/beacon_chain/req/goodbye/1",
nil,
notImplementedRPCHandler, // TODO(3147): Implement.
)
r.registerRPC(
"/eth2/beacon_chain/req/recent_beacon_blocks/1",
nil,
notImplementedRPCHandler, // TODO(3147): Implement.
)
r.registerRPC(
"/eth2/beacon_chain/req/beacon_blocks/1",
nil,
notImplementedRPCHandler, // TODO(3147): Implement.
)
}
// registerRPC for a given topic with an expected protobuf message type.
func (r *RegularSync) registerRPC(topic string, base proto.Message, handle rpcHandler) {
topic += r.p2p.Encoding().ProtocolSuffix()
log := log.WithField("topic", topic)
r.p2p.SetStreamHandler(topic, func(stream network.Stream) {
ctx, cancel := context.WithTimeout(r.ctx, ttfbTimeout)
defer cancel()
defer stream.Close()
if err := stream.SetReadDeadline(roughtime.Now().Add(ttfbTimeout)); err != nil {
log.WithError(err).Error("Could not set stream read deadline")
return
}
// Clone the base message type so we have a newly initialized message as the decoding
// destination.
msg := proto.Clone(base)
if err := r.p2p.Encoding().Decode(stream, msg); err != nil {
log.WithError(err).Error("Failed to decode stream message")
return
}
if err := handle(ctx, msg, stream); err != nil {
// TODO(3147): Update metrics
log.WithError(err).Error("Failed to handle p2p RPC")
}
})
}

View File

@@ -0,0 +1,13 @@
package sync
import (
"context"
"github.com/gogo/protobuf/proto"
libp2pcore "github.com/libp2p/go-libp2p-core"
)
func (r *RegularSync) helloRPCHandler(ctx context.Context, msg proto.Message, stream libp2pcore.Stream) error {
// TODO(3147): Implement
return nil
}

View File

@@ -0,0 +1,43 @@
package sync
import (
"bytes"
"context"
"sync"
"testing"
"time"
"github.com/gogo/protobuf/proto"
libp2pcore "github.com/libp2p/go-libp2p-core"
p2ptest "github.com/prysmaticlabs/prysm/beacon-chain/p2p/testing"
pb "github.com/prysmaticlabs/prysm/proto/testing"
"github.com/prysmaticlabs/prysm/shared/testutil"
)
func TestRegisterRPC_ReceivesValidMessage(t *testing.T) {
p2p := p2ptest.NewTestP2P(t)
r := &RegularSync{
ctx: context.Background(),
p2p: p2p,
}
var wg sync.WaitGroup
wg.Add(1)
topic := "/testing/foobar/1"
handler := func(ctx context.Context, msg proto.Message, stream libp2pcore.Stream) error {
m := msg.(*pb.TestSimpleMessage)
if !bytes.Equal(m.Foo, []byte("foo")) {
t.Errorf("Unexpected incoming message: %+v", m)
}
wg.Done()
return nil
}
r.registerRPC(topic, &pb.TestSimpleMessage{}, handler)
p2p.ReceiveRPC(topic, &pb.TestSimpleMessage{Foo: []byte("foo")})
if testutil.WaitTimeout(&wg, time.Second) {
t.Fatal("Did not receive RPC in 1 second")
}
}

View File

@@ -0,0 +1,33 @@
package sync
import (
"context"
"github.com/prysmaticlabs/prysm/beacon-chain/p2p"
"github.com/prysmaticlabs/prysm/shared"
)
var _ = shared.Service(&RegularSync{})
// RegularSync service is responsible for handling all run time p2p related operations as the
// main entry point for network messages.
type RegularSync struct {
ctx context.Context
p2p p2p.P2P
}
// Start the regular sync service by initializing all of the p2p sync handlers.
func (r *RegularSync) Start() {
r.registerRPCHandlers()
r.registerSubscribers()
}
// Stop the regular sync service.
func (r *RegularSync) Stop() error {
return nil
}
// Status of the currently running regular sync service.
func (r *RegularSync) Status() error {
return nil
}

View File

@@ -0,0 +1,122 @@
package sync
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/gogo/protobuf/proto"
"github.com/prysmaticlabs/prysm/beacon-chain/p2p"
)
// subHandler represents handler for a given subscription.
type subHandler func(context.Context, proto.Message) error
func notImplementedSubHandler(_ context.Context, _ proto.Message) error {
return errors.New("not implemented")
}
// validator should verify the contents of the message, propagate the message
// as expected, and return true or false to continue the message processing
// pipeline.
type validator func(context.Context, proto.Message, p2p.Broadcaster) bool
// noopValidator is a no-op that always returns true and does not propagate any
// message.
func noopValidator(_ context.Context, _ proto.Message, _ p2p.Broadcaster) bool {
return true
}
// Register PubSub subscribers
func (r *RegularSync) registerSubscribers() {
r.subscribe(
"/eth2/beacon_block",
noopValidator,
notImplementedSubHandler, // TODO(3147): Implement.
)
r.subscribe(
"/eth2/beacon_attestation",
noopValidator,
notImplementedSubHandler, // TODO(3147): Implement.
)
r.subscribe(
"/eth2/voluntary_exit",
noopValidator,
notImplementedSubHandler, // TODO(3147): Implement.
)
r.subscribe(
"/eth2/proposer_slashing",
noopValidator,
notImplementedSubHandler, // TODO(3147): Implement.
)
r.subscribe(
"/eth2/attester_slashing",
noopValidator,
notImplementedSubHandler, // TODO(3147): Implement.
)
}
// subscribe to a given topic with a given validator and subscription handler.
// The base protobuf message is used to initialize new messages for decoding.
func (r *RegularSync) subscribe(topic string, validate validator, handle subHandler) {
base := p2p.GossipTopicMappings[topic]
if base == nil {
panic(fmt.Sprintf("%s is not mapped to any message in GossipTopicMappings", topic))
}
topic += r.p2p.Encoding().ProtocolSuffix()
log := log.WithField("topic", topic)
sub, err := r.p2p.PubSub().Subscribe(topic)
if err != nil {
// Any error subscribing to a PubSub topic would be the result of a misconfiguration of
// libp2p PubSub library. This should not happen at normal runtime, unless the config
// changes to a fatal configuration.
panic(err)
}
// Pipeline decodes the incoming subscription data, runs the validation, and handles the
// message.
pipeline := func(data []byte) {
if data == nil {
log.Warn("Received nil message on pubsub")
return
}
msg := proto.Clone(base)
if err := r.p2p.Encoding().Decode(bytes.NewBuffer(data), msg); err != nil {
log.WithError(err).Warn("Failed to decode pubsub message")
return
}
if !validate(r.ctx, msg, r.p2p) {
log.WithField("message", msg.String()).Debug("Message did not verify")
// TODO(3147): Increment metrics.
return
}
if err := handle(r.ctx, msg); err != nil {
// TODO(3147): Increment metrics.
log.WithError(err).Error("Failed to handle p2p pubsub")
return
}
}
// The main message loop for receiving incoming messages from this subscription.
messageLoop := func() {
for {
msg, err := sub.Next(r.ctx)
if err != nil {
log.WithError(err).Error("Subscription next failed")
// TODO(3147): Mark status unhealthy.
return
}
go pipeline(msg.Data)
}
}
go messageLoop()
}

View File

@@ -0,0 +1,40 @@
package sync
import (
"context"
"sync"
"testing"
"time"
"github.com/gogo/protobuf/proto"
p2ptest "github.com/prysmaticlabs/prysm/beacon-chain/p2p/testing"
pb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1"
"github.com/prysmaticlabs/prysm/shared/testutil"
)
func TestSubscribe_ReceivesValidMessage(t *testing.T) {
p2p := p2ptest.NewTestP2P(t)
r := RegularSync{
ctx: context.Background(),
p2p: p2p,
}
topic := "/eth2/voluntary_exit"
var wg sync.WaitGroup
wg.Add(1)
r.subscribe(topic, noopValidator, func(_ context.Context, msg proto.Message) error {
m := msg.(*pb.VoluntaryExit)
if m.Epoch != 55 {
t.Errorf("Unexpected incoming message: %+v", m)
}
wg.Done()
return nil
})
p2p.ReceivePubSub(topic, &pb.VoluntaryExit{Epoch: 55})
if testutil.WaitTimeout(&wg, time.Second) {
t.Fatal("Did not receive PubSub in 1 second")
}
}

View File

@@ -88,6 +88,21 @@ func TestBlockHeaderSigningRoot(t *testing.T) {
}
}
// See: https://github.com/prysmaticlabs/go-ssz/pull/69
func TestBeaconBlock(t *testing.T) {
block := &ethpb.BeaconBlock{
Slot: 55,
}
enc, err := ssz.Marshal(block)
if err != nil {
t.Fatal(err)
}
dec := &ethpb.BeaconBlock{}
if err := ssz.Unmarshal(enc, dec); err != nil {
t.Fatal(err)
}
}
func hexDecodeOrDie(t *testing.T, h string) []byte {
b, err := hex.DecodeString(h)
if err != nil {

View File

@@ -10,6 +10,7 @@ go_library(
"json_to_pb_converter.go",
"log.go",
"tempdir.go",
"wait_timeout.go",
],
importpath = "github.com/prysmaticlabs/prysm/shared/testutil",
visibility = ["//visibility:public"],

View File

@@ -0,0 +1,22 @@
package testutil
import (
"sync"
"time"
)
// WaitTimeout will wait for a WaitGroup to resolve within a timeout interval.
// Returns true if the waitgroup exceeded the timeout.
func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
ch := make(chan struct{})
go func() {
defer close(ch)
wg.Wait()
}()
select {
case <-ch:
return false
case <-time.After(timeout):
return true
}
}