2020-02-14 19:03:25 +00:00
|
|
|
package detection
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2020-07-07 17:11:07 +00:00
|
|
|
"errors"
|
2020-02-14 19:03:25 +00:00
|
|
|
|
|
|
|
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
|
|
|
|
"github.com/prysmaticlabs/prysm/shared/event"
|
|
|
|
"github.com/prysmaticlabs/prysm/slasher/beaconclient"
|
2020-02-19 22:26:14 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/slasher/db"
|
2020-02-27 17:22:39 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/slasher/detection/attestations"
|
|
|
|
"github.com/prysmaticlabs/prysm/slasher/detection/attestations/iface"
|
2020-03-19 11:59:35 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/slasher/detection/proposals"
|
|
|
|
proposerIface "github.com/prysmaticlabs/prysm/slasher/detection/proposals/iface"
|
2020-02-14 19:03:25 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2020-02-19 22:26:14 +00:00
|
|
|
"go.opencensus.io/trace"
|
2020-02-14 19:03:25 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var log = logrus.WithField("prefix", "detection")
|
|
|
|
|
2020-07-07 17:11:07 +00:00
|
|
|
// Status detection statuses type.
|
|
|
|
type Status int
|
|
|
|
|
|
|
|
const (
|
|
|
|
// None slasher was not initialised.
|
|
|
|
None Status = iota
|
|
|
|
// Started service start has been called,
|
|
|
|
Started
|
|
|
|
// Syncing beacon client is still syncing.
|
|
|
|
Syncing
|
|
|
|
// HistoricalDetection slasher is replaying all attestations that
|
|
|
|
// were included in the canonical chain.
|
|
|
|
HistoricalDetection
|
|
|
|
// Ready slasher is ready to detect requests.
|
|
|
|
Ready
|
|
|
|
)
|
|
|
|
|
|
|
|
// String returns the string value of the status
|
|
|
|
func (s Status) String() string {
|
|
|
|
strings := [...]string{"None", "Started", "Syncing", "HistoricalDetection", "Ready"}
|
|
|
|
|
|
|
|
// prevent panicking in case of status is out-of-range
|
|
|
|
if s < None || s > Ready {
|
|
|
|
return "Unknown"
|
|
|
|
}
|
|
|
|
|
|
|
|
return strings[s]
|
|
|
|
}
|
|
|
|
|
2020-02-14 19:03:25 +00:00
|
|
|
// Service struct for the detection service of the slasher.
|
|
|
|
type Service struct {
|
|
|
|
ctx context.Context
|
|
|
|
cancel context.CancelFunc
|
2020-02-19 22:26:14 +00:00
|
|
|
slasherDB db.Database
|
2020-02-14 19:03:25 +00:00
|
|
|
blocksChan chan *ethpb.SignedBeaconBlock
|
2020-02-27 17:22:39 +00:00
|
|
|
attsChan chan *ethpb.IndexedAttestation
|
2020-02-14 19:03:25 +00:00
|
|
|
notifier beaconclient.Notifier
|
2020-02-19 22:26:14 +00:00
|
|
|
chainFetcher beaconclient.ChainFetcher
|
|
|
|
beaconClient *beaconclient.Service
|
2020-02-14 19:03:25 +00:00
|
|
|
attesterSlashingsFeed *event.Feed
|
|
|
|
proposerSlashingsFeed *event.Feed
|
2020-02-27 17:22:39 +00:00
|
|
|
minMaxSpanDetector iface.SpanDetector
|
2020-03-19 11:59:35 +00:00
|
|
|
proposalsDetector proposerIface.ProposalsDetector
|
2020-07-20 20:23:48 +00:00
|
|
|
historicalDetection bool
|
2020-07-07 17:11:07 +00:00
|
|
|
status Status
|
2020-02-14 19:03:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Config options for the detection service.
|
|
|
|
type Config struct {
|
|
|
|
Notifier beaconclient.Notifier
|
2020-02-19 22:26:14 +00:00
|
|
|
SlasherDB db.Database
|
|
|
|
ChainFetcher beaconclient.ChainFetcher
|
|
|
|
BeaconClient *beaconclient.Service
|
2020-02-14 19:03:25 +00:00
|
|
|
AttesterSlashingsFeed *event.Feed
|
|
|
|
ProposerSlashingsFeed *event.Feed
|
2020-07-20 20:23:48 +00:00
|
|
|
HistoricalDetection bool
|
2020-02-14 19:03:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewDetectionService instantiation.
|
|
|
|
func NewDetectionService(ctx context.Context, cfg *Config) *Service {
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
return &Service{
|
|
|
|
ctx: ctx,
|
|
|
|
cancel: cancel,
|
|
|
|
notifier: cfg.Notifier,
|
2020-02-19 22:26:14 +00:00
|
|
|
chainFetcher: cfg.ChainFetcher,
|
|
|
|
slasherDB: cfg.SlasherDB,
|
|
|
|
beaconClient: cfg.BeaconClient,
|
2020-02-14 19:03:25 +00:00
|
|
|
blocksChan: make(chan *ethpb.SignedBeaconBlock, 1),
|
2020-02-27 17:22:39 +00:00
|
|
|
attsChan: make(chan *ethpb.IndexedAttestation, 1),
|
2020-02-14 19:03:25 +00:00
|
|
|
attesterSlashingsFeed: cfg.AttesterSlashingsFeed,
|
|
|
|
proposerSlashingsFeed: cfg.ProposerSlashingsFeed,
|
2020-03-05 18:11:54 +00:00
|
|
|
minMaxSpanDetector: attestations.NewSpanDetector(cfg.SlasherDB),
|
2020-03-19 11:59:35 +00:00
|
|
|
proposalsDetector: proposals.NewProposeDetector(cfg.SlasherDB),
|
2020-07-20 20:23:48 +00:00
|
|
|
historicalDetection: cfg.HistoricalDetection,
|
2020-07-07 17:11:07 +00:00
|
|
|
status: None,
|
2020-02-14 19:03:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stop the notifier service.
|
|
|
|
func (ds *Service) Stop() error {
|
|
|
|
ds.cancel()
|
|
|
|
log.Info("Stopping service")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-07 17:11:07 +00:00
|
|
|
// Status returns an error if detection service is not ready yet.
|
2020-02-14 19:03:25 +00:00
|
|
|
func (ds *Service) Status() error {
|
2020-07-07 17:11:07 +00:00
|
|
|
if ds.status == Ready {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return errors.New(ds.status.String())
|
2020-02-14 19:03:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Start the detection service runtime.
|
|
|
|
func (ds *Service) Start() {
|
2020-02-19 22:26:14 +00:00
|
|
|
// We wait for the gRPC beacon client to be ready and the beacon node
|
|
|
|
// to be fully synced before proceeding.
|
2020-07-07 17:11:07 +00:00
|
|
|
ds.status = Started
|
2020-02-19 22:26:14 +00:00
|
|
|
ch := make(chan bool)
|
|
|
|
sub := ds.notifier.ClientReadyFeed().Subscribe(ch)
|
2020-07-07 17:11:07 +00:00
|
|
|
ds.status = Syncing
|
2020-02-19 22:26:14 +00:00
|
|
|
<-ch
|
|
|
|
sub.Unsubscribe()
|
|
|
|
|
2020-07-20 20:23:48 +00:00
|
|
|
if ds.historicalDetection {
|
2020-05-06 22:17:36 +00:00
|
|
|
// The detection service runs detection on all historical
|
|
|
|
// chain data since genesis.
|
2020-07-07 17:11:07 +00:00
|
|
|
ds.status = HistoricalDetection
|
2020-07-07 02:57:40 +00:00
|
|
|
ds.detectHistoricalChainData(ds.ctx)
|
2020-05-06 22:17:36 +00:00
|
|
|
}
|
2020-07-07 17:11:07 +00:00
|
|
|
ds.status = Ready
|
2020-07-07 02:57:40 +00:00
|
|
|
// We listen to a stream of blocks and attestations from the beacon node.
|
|
|
|
go ds.beaconClient.ReceiveBlocks(ds.ctx)
|
|
|
|
go ds.beaconClient.ReceiveAttestations(ds.ctx)
|
2020-02-19 22:26:14 +00:00
|
|
|
// We subscribe to incoming blocks from the beacon node via
|
|
|
|
// our gRPC client to keep detecting slashable offenses.
|
2020-02-14 19:03:25 +00:00
|
|
|
go ds.detectIncomingBlocks(ds.ctx, ds.blocksChan)
|
|
|
|
go ds.detectIncomingAttestations(ds.ctx, ds.attsChan)
|
|
|
|
}
|
2020-02-19 22:26:14 +00:00
|
|
|
|
|
|
|
func (ds *Service) detectHistoricalChainData(ctx context.Context) {
|
|
|
|
ctx, span := trace.StartSpan(ctx, "detection.detectHistoricalChainData")
|
|
|
|
defer span.End()
|
|
|
|
// We fetch both the latest persisted chain head in our DB as well
|
|
|
|
// as the current chain head from the beacon node via gRPC.
|
|
|
|
latestStoredHead, err := ds.slasherDB.ChainHead(ctx)
|
|
|
|
if err != nil {
|
2020-06-28 06:34:21 +00:00
|
|
|
log.WithError(err).Error("Could not retrieve chain head from DB")
|
|
|
|
return
|
2020-02-19 22:26:14 +00:00
|
|
|
}
|
|
|
|
currentChainHead, err := ds.chainFetcher.ChainHead(ctx)
|
|
|
|
if err != nil {
|
2020-06-28 06:34:21 +00:00
|
|
|
log.WithError(err).Error("Cannot retrieve chain head from beacon node")
|
|
|
|
return
|
2020-02-19 22:26:14 +00:00
|
|
|
}
|
|
|
|
var latestStoredEpoch uint64
|
|
|
|
if latestStoredHead != nil {
|
|
|
|
latestStoredEpoch = latestStoredHead.HeadEpoch
|
|
|
|
}
|
2020-05-28 14:24:54 +00:00
|
|
|
log.Infof("Performing historical detection from epoch %d to %d", latestStoredEpoch, currentChainHead.HeadEpoch)
|
2020-03-03 18:08:21 +00:00
|
|
|
|
2020-02-19 22:26:14 +00:00
|
|
|
// We retrieve historical chain data from the last persisted chain head in the
|
|
|
|
// slasher DB up to the current beacon node's head epoch we retrieved via gRPC.
|
|
|
|
// If no data was persisted from previous sessions, we request data starting from
|
|
|
|
// the genesis epoch.
|
2020-05-20 19:52:18 +00:00
|
|
|
var storedEpoch uint64
|
2020-02-27 17:22:39 +00:00
|
|
|
for epoch := latestStoredEpoch; epoch < currentChainHead.HeadEpoch; epoch++ {
|
2020-06-28 06:34:21 +00:00
|
|
|
if ctx.Err() != nil {
|
|
|
|
log.WithError(err).Errorf("Could not fetch attestations for epoch: %d", epoch)
|
|
|
|
return
|
|
|
|
}
|
2020-02-27 17:22:39 +00:00
|
|
|
indexedAtts, err := ds.beaconClient.RequestHistoricalAttestations(ctx, epoch)
|
2020-02-19 22:26:14 +00:00
|
|
|
if err != nil {
|
2020-02-27 17:22:39 +00:00
|
|
|
log.WithError(err).Errorf("Could not fetch attestations for epoch: %d", epoch)
|
2020-07-20 20:23:48 +00:00
|
|
|
return
|
2020-05-28 14:24:54 +00:00
|
|
|
}
|
|
|
|
if err := ds.slasherDB.SaveIndexedAttestations(ctx, indexedAtts); err != nil {
|
|
|
|
log.WithError(err).Error("could not save indexed attestations")
|
2020-07-20 20:23:48 +00:00
|
|
|
return
|
2020-02-19 22:26:14 +00:00
|
|
|
}
|
2020-03-03 18:08:21 +00:00
|
|
|
|
2020-02-27 17:22:39 +00:00
|
|
|
for _, att := range indexedAtts {
|
2020-05-12 18:32:42 +00:00
|
|
|
if ctx.Err() == context.Canceled {
|
|
|
|
log.WithError(ctx.Err()).Error("context has been canceled, ending detection")
|
|
|
|
return
|
|
|
|
}
|
2020-03-24 18:30:21 +00:00
|
|
|
slashings, err := ds.DetectAttesterSlashings(ctx, att)
|
2020-02-27 17:22:39 +00:00
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("Could not detect attester slashings")
|
|
|
|
continue
|
|
|
|
}
|
2020-05-12 18:32:42 +00:00
|
|
|
if len(slashings) < 1 {
|
|
|
|
if err := ds.minMaxSpanDetector.UpdateSpans(ctx, att); err != nil {
|
|
|
|
log.WithError(err).Error("Could not update spans")
|
|
|
|
}
|
|
|
|
}
|
2020-03-17 21:53:08 +00:00
|
|
|
ds.submitAttesterSlashings(ctx, slashings)
|
2020-02-27 17:22:39 +00:00
|
|
|
}
|
2020-04-20 16:11:53 +00:00
|
|
|
latestStoredHead = ðpb.ChainHead{HeadEpoch: epoch}
|
|
|
|
if err := ds.slasherDB.SaveChainHead(ctx, latestStoredHead); err != nil {
|
|
|
|
log.WithError(err).Error("Could not persist chain head to disk")
|
|
|
|
}
|
2020-05-20 19:52:18 +00:00
|
|
|
storedEpoch = epoch
|
2020-05-22 19:44:48 +00:00
|
|
|
ds.slasherDB.RemoveOldestFromCache(ctx)
|
2020-06-28 06:34:21 +00:00
|
|
|
if epoch == currentChainHead.HeadEpoch-1 {
|
|
|
|
currentChainHead, err = ds.chainFetcher.ChainHead(ctx)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("Cannot retrieve chain head from beacon node")
|
2020-07-20 20:23:48 +00:00
|
|
|
return
|
2020-06-28 06:34:21 +00:00
|
|
|
}
|
|
|
|
if epoch != currentChainHead.HeadEpoch-1 {
|
|
|
|
log.Infof("Continuing historical detection from epoch %d to %d", epoch, currentChainHead.HeadEpoch)
|
|
|
|
}
|
|
|
|
}
|
2020-02-19 22:26:14 +00:00
|
|
|
}
|
2020-05-20 19:52:18 +00:00
|
|
|
log.Infof("Completed slashing detection on historical chain data up to epoch %d", storedEpoch)
|
2020-02-19 22:26:14 +00:00
|
|
|
}
|
2020-02-27 17:22:39 +00:00
|
|
|
|
2020-03-17 21:53:08 +00:00
|
|
|
func (ds *Service) submitAttesterSlashings(ctx context.Context, slashings []*ethpb.AttesterSlashing) {
|
2020-03-08 17:56:43 +00:00
|
|
|
ctx, span := trace.StartSpan(ctx, "detection.submitAttesterSlashings")
|
|
|
|
defer span.End()
|
2020-03-09 18:14:19 +00:00
|
|
|
for i := 0; i < len(slashings); i++ {
|
2020-05-22 15:19:10 +00:00
|
|
|
ds.attesterSlashingsFeed.Send(slashings[i])
|
2020-03-09 18:14:19 +00:00
|
|
|
}
|
2020-02-27 17:22:39 +00:00
|
|
|
}
|
2020-04-14 20:27:03 +00:00
|
|
|
|
|
|
|
func (ds *Service) submitProposerSlashing(ctx context.Context, slashing *ethpb.ProposerSlashing) {
|
|
|
|
ctx, span := trace.StartSpan(ctx, "detection.submitProposerSlashing")
|
|
|
|
defer span.End()
|
|
|
|
if slashing != nil && slashing.Header_1 != nil && slashing.Header_2 != nil {
|
|
|
|
log.WithFields(logrus.Fields{
|
|
|
|
"header1Slot": slashing.Header_1.Header.Slot,
|
|
|
|
"header2Slot": slashing.Header_2.Header.Slot,
|
|
|
|
"proposerIdxHeader1": slashing.Header_1.Header.ProposerIndex,
|
|
|
|
"proposerIdxHeader2": slashing.Header_2.Header.ProposerIndex,
|
|
|
|
}).Info("Found a proposer slashing! Submitting to beacon node")
|
|
|
|
ds.proposerSlashingsFeed.Send(slashing)
|
|
|
|
}
|
|
|
|
}
|