mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2024-12-25 12:57:18 +00:00
f46ee335bd
* adding proto messages * adding fields * adding new service * new changes * checking in file from syncFix * fixing test * add test * adding to node * goimports * adding query service to sync services * sync working * finally works * fix test * lint * fix build * fix test
280 lines
8.6 KiB
Go
280 lines
8.6 KiB
Go
// Package initialsync is run by the beacon node when the local chain is
|
|
// behind the network's longest chain. Initial sync works as follows:
|
|
// The node requests for the slot number of the most recent finalized block.
|
|
// The node then builds from the most recent finalized block by requesting for subsequent
|
|
// blocks by slot number. Once the service detects that the local chain is caught up with
|
|
// the network, the service hands over control to the regular sync service.
|
|
// Note: The behavior of initialsync will likely change as the specification changes.
|
|
// The most significant and highly probable change will be determining where to sync from.
|
|
// The beacon chain may sync from a block in the pasts X months in order to combat long-range attacks
|
|
// (see here: https://github.com/ethereum/wiki/wiki/Proof-of-Stake-FAQs#what-is-weak-subjectivity)
|
|
package initialsync
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
"github.com/prysmaticlabs/prysm/beacon-chain/types"
|
|
pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1"
|
|
"github.com/prysmaticlabs/prysm/shared/event"
|
|
"github.com/prysmaticlabs/prysm/shared/p2p"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var log = logrus.WithField("prefix", "initial-sync")
|
|
|
|
// Config defines the configurable properties of InitialSync.
|
|
//
|
|
type Config struct {
|
|
SyncPollingInterval time.Duration
|
|
BlockBufferSize int
|
|
CrystallizedStateBufferSize int
|
|
BeaconDB beaconDB
|
|
P2P p2pAPI
|
|
SyncService syncService
|
|
QueryService queryService
|
|
}
|
|
|
|
// DefaultConfig provides the default configuration for a sync service.
|
|
// SyncPollingInterval determines how frequently the service checks that initial sync is complete.
|
|
// BlockBufferSize determines that buffer size of the `blockBuf` channel.
|
|
// CrystallizedStateBufferSize determines the buffer size of thhe `crystallizedStateBuf` channel.
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
SyncPollingInterval: 1 * time.Second,
|
|
BlockBufferSize: 100,
|
|
CrystallizedStateBufferSize: 100,
|
|
}
|
|
}
|
|
|
|
type p2pAPI interface {
|
|
Subscribe(msg proto.Message, channel chan p2p.Message) event.Subscription
|
|
Send(msg proto.Message, peer p2p.Peer)
|
|
Broadcast(msg proto.Message)
|
|
}
|
|
|
|
type beaconDB interface {
|
|
SaveBlock(*types.Block) error
|
|
}
|
|
|
|
// SyncService is the interface for the Sync service.
|
|
// InitialSync calls `Start` when initial sync completes.
|
|
type syncService interface {
|
|
Start()
|
|
ResumeSync()
|
|
IsSyncedWithNetwork() bool
|
|
}
|
|
|
|
type queryService interface {
|
|
IsSynced() (bool, error)
|
|
}
|
|
|
|
// InitialSync defines the main class in this package.
|
|
// See the package comments for a general description of the service's functions.
|
|
type InitialSync struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
p2p p2pAPI
|
|
syncService syncService
|
|
queryService queryService
|
|
db beaconDB
|
|
blockBuf chan p2p.Message
|
|
crystallizedStateBuf chan p2p.Message
|
|
currentSlot uint64
|
|
syncPollingInterval time.Duration
|
|
initialCrystallizedStateRoot [32]byte
|
|
}
|
|
|
|
// NewInitialSyncService constructs a new InitialSyncService.
|
|
// This method is normally called by the main node.
|
|
func NewInitialSyncService(ctx context.Context,
|
|
cfg Config,
|
|
) *InitialSync {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
blockBuf := make(chan p2p.Message, cfg.BlockBufferSize)
|
|
crystallizedStateBuf := make(chan p2p.Message, cfg.CrystallizedStateBufferSize)
|
|
|
|
return &InitialSync{
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
p2p: cfg.P2P,
|
|
syncService: cfg.SyncService,
|
|
db: cfg.BeaconDB,
|
|
blockBuf: blockBuf,
|
|
crystallizedStateBuf: crystallizedStateBuf,
|
|
syncPollingInterval: cfg.SyncPollingInterval,
|
|
queryService: cfg.QueryService,
|
|
}
|
|
}
|
|
|
|
// Start begins the goroutine.
|
|
func (s *InitialSync) Start() {
|
|
synced, err := s.queryService.IsSynced()
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
|
|
if synced {
|
|
// TODO(#661): Bail out of the sync service if the chain is only partially synced.
|
|
log.Info("Chain fully synced, exiting initial sync")
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
ticker := time.NewTicker(s.syncPollingInterval)
|
|
s.run(ticker.C)
|
|
ticker.Stop()
|
|
}()
|
|
}
|
|
|
|
// Stop kills the initial sync goroutine.
|
|
func (s *InitialSync) Stop() error {
|
|
log.Info("Stopping service")
|
|
s.cancel()
|
|
return nil
|
|
}
|
|
|
|
// run is the main goroutine for the initial sync service.
|
|
// delayChan is explicitly passed into this function to facilitate tests that don't require a timeout.
|
|
// It is assumed that the goroutine `run` is only called once per instance.
|
|
func (s *InitialSync) run(delaychan <-chan time.Time) {
|
|
blockSub := s.p2p.Subscribe(&pb.BeaconBlockResponse{}, s.blockBuf)
|
|
crystallizedStateSub := s.p2p.Subscribe(&pb.CrystallizedStateResponse{}, s.crystallizedStateBuf)
|
|
defer func() {
|
|
blockSub.Unsubscribe()
|
|
crystallizedStateSub.Unsubscribe()
|
|
close(s.blockBuf)
|
|
close(s.crystallizedStateBuf)
|
|
}()
|
|
|
|
highestObservedSlot := uint64(0)
|
|
|
|
for {
|
|
select {
|
|
case <-s.ctx.Done():
|
|
log.Debug("Exiting goroutine")
|
|
return
|
|
case <-delaychan:
|
|
if highestObservedSlot == s.currentSlot {
|
|
log.Info("Exiting initial sync and starting normal sync")
|
|
s.syncService.ResumeSync()
|
|
// TODO(#661): Resume sync after completion of initial sync.
|
|
return
|
|
}
|
|
case msg := <-s.blockBuf:
|
|
data := msg.Data.(*pb.BeaconBlockResponse)
|
|
|
|
if data.Block.GetSlot() > highestObservedSlot {
|
|
highestObservedSlot = data.Block.GetSlot()
|
|
}
|
|
|
|
if s.currentSlot == 0 {
|
|
if s.initialCrystallizedStateRoot != [32]byte{} {
|
|
continue
|
|
}
|
|
if err := s.setBlockForInitialSync(data); err != nil {
|
|
log.Errorf("Could not set block for initial sync: %v", err)
|
|
}
|
|
if err := s.requestCrystallizedStateFromPeer(data, msg.Peer); err != nil {
|
|
log.Errorf("Could not request crystallized state from peer: %v", err)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
if data.Block.GetSlot() != (s.currentSlot + 1) {
|
|
continue
|
|
}
|
|
|
|
if err := s.validateAndSaveNextBlock(data); err != nil {
|
|
log.Errorf("Unable to save block: %v", err)
|
|
}
|
|
s.requestNextBlock()
|
|
case msg := <-s.crystallizedStateBuf:
|
|
data := msg.Data.(*pb.CrystallizedStateResponse)
|
|
|
|
if s.initialCrystallizedStateRoot == [32]byte{} {
|
|
continue
|
|
}
|
|
|
|
crystallizedState := types.NewCrystallizedState(data.CrystallizedState)
|
|
hash, err := crystallizedState.Hash()
|
|
if err != nil {
|
|
log.Errorf("Unable to hash crytsallized state: %v", err)
|
|
}
|
|
|
|
if hash != s.initialCrystallizedStateRoot {
|
|
continue
|
|
}
|
|
|
|
s.currentSlot = crystallizedState.LastFinalizedSlot()
|
|
s.requestNextBlock()
|
|
crystallizedStateSub.Unsubscribe()
|
|
}
|
|
}
|
|
}
|
|
|
|
// requestCrystallizedStateFromPeer sends a request to a peer for the corresponding crystallized state
|
|
// for a beacon block.
|
|
func (s *InitialSync) requestCrystallizedStateFromPeer(data *pb.BeaconBlockResponse, peer p2p.Peer) error {
|
|
block := types.NewBlock(data.Block)
|
|
h := block.CrystallizedStateRoot()
|
|
log.Debugf("Successfully processed incoming block with crystallized state hash: %#x", h)
|
|
s.p2p.Send(&pb.CrystallizedStateRequest{Hash: h[:]}, peer)
|
|
return nil
|
|
}
|
|
|
|
// setBlockForInitialSync sets the first received block as the base finalized
|
|
// block for initial sync.
|
|
func (s *InitialSync) setBlockForInitialSync(data *pb.BeaconBlockResponse) error {
|
|
block := types.NewBlock(data.Block)
|
|
|
|
h, err := block.Hash()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.WithField("blockhash", fmt.Sprintf("%#x", h)).Debug("Crystallized state hash exists locally")
|
|
|
|
if err := s.writeBlockToDB(block); err != nil {
|
|
return err
|
|
}
|
|
|
|
s.initialCrystallizedStateRoot = block.CrystallizedStateRoot()
|
|
|
|
log.Infof("Saved block with hash %#x for initial sync", h)
|
|
return nil
|
|
}
|
|
|
|
// requestNextBlock broadcasts a request for a block with the next slotnumber.
|
|
func (s *InitialSync) requestNextBlock() {
|
|
s.p2p.Broadcast(&pb.BeaconBlockRequestBySlotNumber{SlotNumber: (s.currentSlot + 1)})
|
|
}
|
|
|
|
// validateAndSaveNextBlock will validate whether blocks received from the blockfetcher
|
|
// routine can be added to the chain.
|
|
func (s *InitialSync) validateAndSaveNextBlock(data *pb.BeaconBlockResponse) error {
|
|
block := types.NewBlock(data.Block)
|
|
|
|
if s.currentSlot == uint64(0) {
|
|
return errors.New("invalid slot number for syncing")
|
|
}
|
|
|
|
if (s.currentSlot + 1) == block.SlotNumber() {
|
|
|
|
if err := s.writeBlockToDB(block); err != nil {
|
|
return err
|
|
}
|
|
s.currentSlot = block.SlotNumber()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// writeBlockToDB saves the corresponding block to the local DB.
|
|
func (s *InitialSync) writeBlockToDB(block *types.Block) error {
|
|
return s.db.SaveBlock(block)
|
|
}
|