Skip to content

Commit

Permalink
Implement attester protection into validator client (#4598)
Browse files Browse the repository at this point in the history
* Add flag for attester protection

* Remove flags

* Add attestation history DB functions to validator client

* Fix comments

* Update interface to new funcs

* Fix test

* Add flags

* Implement most of attester protection

* Fix tests

* Add test for pruning

* Add more test cases for prunes

* Remove todo comment

* Fix comments

* Rename functions

* Fix logs

Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>
  • Loading branch information
0xKiwi and rauljordan committed Jan 23, 2020
1 parent ee9b9e6 commit ed3ab82
Show file tree
Hide file tree
Showing 7 changed files with 395 additions and 15 deletions.
7 changes: 4 additions & 3 deletions proto/slashing/slashing.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 9 additions & 4 deletions shared/featureconfig/config.go
Expand Up @@ -35,7 +35,8 @@ type Flags struct {
EnableSnappyDBCompression bool // EnableSnappyDBCompression in the database.
InitSyncCacheState bool // InitSyncCacheState caches state during initial sync.
KafkaBootstrapServers string // KafkaBootstrapServers to find kafka servers to stream blocks, attestations, etc.
BlockDoubleProposals bool // BlockDoubleProposals prevents the validator client from signing any proposals that would be considered a slashable offense.
ProtectProposer bool // ProtectProposer prevents the validator client from signing any proposals that would be considered a slashable offense.
ProtectAttester bool // ProtectAttester prevents the validator client from signing any attestations that would be considered a slashable offense.

// DisableForkChoice disables using LMD-GHOST fork choice to update
// the head of the chain based on attestations and instead accepts any valid received block
Expand Down Expand Up @@ -150,9 +151,13 @@ func ConfigureValidator(ctx *cli.Context) {
log.Warn("Using minimal config")
cfg.MinimalConfig = true
}
if ctx.GlobalBool(blockDoubleProposals.Name) {
log.Warn("Enabled validator double proposal slashing protection.")
cfg.BlockDoubleProposals = true
if ctx.GlobalBool(protectProposerFlag.Name) {
log.Warn("Enabled validator proposal slashing protection.")
cfg.ProtectProposer = true
}
if ctx.GlobalBool(protectAttesterFlag.Name) {
log.Warn("Enabled validator attestation slashing protection.")
cfg.ProtectAttester = true
}
Init(cfg)
}
Expand Down
12 changes: 9 additions & 3 deletions shared/featureconfig/flags.go
Expand Up @@ -83,11 +83,16 @@ var (
Name: "cache-proposer-indices",
Usage: "Cache proposer indices on per epoch basis.",
}
blockDoubleProposals = cli.BoolFlag{
Name: "block-double-proposals",
protectProposerFlag = cli.BoolFlag{
Name: "protect-proposer",
Usage: "Prevent the validator client from signing and broadcasting 2 different block " +
"proposals in the same epoch. Protects from slashing.",
}
protectAttesterFlag = cli.BoolFlag{
Name: "protect-attester",
Usage: "Prevent the validator client from signing and broadcasting 2 any slashable attestations. " +
"Protects from slashing.",
}
)

// Deprecated flags list.
Expand Down Expand Up @@ -199,7 +204,8 @@ var deprecatedFlags = []cli.Flag{
// ValidatorFlags contains a list of all the feature flags that apply to the validator client.
var ValidatorFlags = append(deprecatedFlags, []cli.Flag{
minimalConfigFlag,
blockDoubleProposals,
protectAttesterFlag,
protectProposerFlag,
}...)

// BeaconChainFlags contains a list of all the feature flags that apply to the beacon-chain client.
Expand Down
96 changes: 96 additions & 0 deletions validator/client/validator_attest.go
Expand Up @@ -10,11 +10,14 @@ import (
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/prysmaticlabs/go-bitfield"
"github.com/prysmaticlabs/go-ssz"
slashpb "github.com/prysmaticlabs/prysm/proto/slashing"
"github.com/prysmaticlabs/prysm/shared/bytesutil"
"github.com/prysmaticlabs/prysm/shared/featureconfig"
"github.com/prysmaticlabs/prysm/shared/hashutil"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/roughtime"
"github.com/prysmaticlabs/prysm/shared/slotutil"
"github.com/sirupsen/logrus"
"go.opencensus.io/trace"
)

Expand Down Expand Up @@ -55,6 +58,21 @@ func (v *validator) SubmitAttestation(ctx context.Context, slot uint64, pubKey [
return
}

if featureconfig.Get().ProtectAttester {
history, err := v.db.AttestationHistory(ctx, pubKey[:])
if err != nil {
log.Errorf("Could not get attestation history from DB: %v", err)
return
}
if isNewAttSlashable(history, data.Source.Epoch, data.Target.Epoch) {
log.WithFields(logrus.Fields{
"sourceEpoch": data.Source.Epoch,
"targetEpoch": data.Target.Epoch,
}).Error("Attempted to make a slashable attestation, rejected")
return
}
}

sig, err := v.signAtt(ctx, pubKey, data)
if err != nil {
log.WithError(err).Error("Could not sign attestation")
Expand All @@ -75,6 +93,19 @@ func (v *validator) SubmitAttestation(ctx context.Context, slot uint64, pubKey [
return
}

if featureconfig.Get().ProtectAttester {
history, err := v.db.AttestationHistory(ctx, pubKey[:])
if err != nil {
log.Errorf("Could not get attestation history from DB: %v", err)
return
}
history = markAttestationForTargetEpoch(history, data.Source.Epoch, data.Target.Epoch)
if err := v.db.SaveAttestationHistory(ctx, pubKey[:], history); err != nil {
log.Errorf("Could not save attestation history to DB: %v", err)
return
}
}

if err := v.saveAttesterIndexToData(data, validatorIndex); err != nil {
log.WithError(err).Error("Could not save validator index for logging")
return
Expand Down Expand Up @@ -180,3 +211,68 @@ func (v *validator) saveAttesterIndexToData(data *ethpb.AttestationData, index u

return nil
}

// isNewAttSlashable uses the attestation history to determine if an attestation of sourceEpoch
// and targetEpoch would be slashable. It can detect double, surrounding, and surrounded votes.
func isNewAttSlashable(history *slashpb.AttestationHistory, sourceEpoch uint64, targetEpoch uint64) bool {
farFuture := params.BeaconConfig().FarFutureEpoch
wsPeriod := params.BeaconConfig().WeakSubjectivityPeriod

// Previously pruned, we should return false.
if int(targetEpoch) <= int(history.LatestEpochWritten)-int(wsPeriod) {
return false
}

// Check if there has already been a vote for this target epoch.
if safeTargetToSource(history, targetEpoch) != farFuture {
return true
}

// Check if the new attestation would be surrounding another attestation.
for i := sourceEpoch; i <= targetEpoch; i++ {
// Unattested for epochs are marked as FAR_FUTURE_EPOCH.
if safeTargetToSource(history, i) == farFuture {
continue
}
if history.TargetToSource[i%wsPeriod] > sourceEpoch {
return true
}
}

// Check if the new attestation is being surrounded.
for i := targetEpoch; i <= history.LatestEpochWritten; i++ {
if safeTargetToSource(history, i) < sourceEpoch {
return true
}
}

return false
}

// markAttestationForTargetEpoch returns the modified attestation history with the passed-in epochs marked
// as attested for. This is done to prevent the validator client from signing any slashable attestations.
func markAttestationForTargetEpoch(history *slashpb.AttestationHistory, sourceEpoch uint64, targetEpoch uint64) *slashpb.AttestationHistory {
wsPeriod := params.BeaconConfig().WeakSubjectivityPeriod

if targetEpoch > history.LatestEpochWritten {
// If the target epoch to mark is ahead of latest written epoch, override the old targets and mark the requested epoch.
// Limit the overwriting to one weak subjectivity period as further is not needed.
maxToWrite := history.LatestEpochWritten + wsPeriod
for i := history.LatestEpochWritten + 1; i < targetEpoch && i <= maxToWrite; i++ {
history.TargetToSource[i%wsPeriod] = params.BeaconConfig().FarFutureEpoch
}
history.LatestEpochWritten = targetEpoch
}
history.TargetToSource[targetEpoch%wsPeriod] = sourceEpoch
return history
}

// safeTargetToSource makes sure the epoch accessed is within bounds, and if it's not it at
// returns the "default" FAR_FUTURE_EPOCH value.
func safeTargetToSource(history *slashpb.AttestationHistory, targetEpoch uint64) uint64 {
wsPeriod := params.BeaconConfig().WeakSubjectivityPeriod
if targetEpoch > history.LatestEpochWritten || int(targetEpoch) < int(history.LatestEpochWritten)-int(wsPeriod) {
return params.BeaconConfig().FarFutureEpoch
}
return history.TargetToSource[targetEpoch%wsPeriod]
}

0 comments on commit ed3ab82

Please sign in to comment.