Skip to content

Commit

Permalink
Simpler and Safer Attester Slashing Protection (#8086)
Browse files Browse the repository at this point in the history
* att sign validations

* eliminate old cached methods and use a simple approach in the db

* redefined db methods

* db package builds

* add multilock to attest and propose

* gaz

* removed concurrency tests that are no longer relevant

* add cache to db functions for attesting history checks

* passing

* add in feature flag --disable-attesting-history-db-cache

* remove lock

* Revert "remove lock"

This reverts commit b1a6502.

* comment

* gaz
  • Loading branch information
rauljordan committed Dec 11, 2020
1 parent 2e18df6 commit 1fbfd52
Show file tree
Hide file tree
Showing 27 changed files with 193 additions and 498 deletions.
5 changes: 5 additions & 0 deletions shared/featureconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type Flags struct {
EnableSyncBacktracking bool // EnableSyncBacktracking enables backtracking algorithm when searching for alternative forks during initial sync.
EnableLargerGossipHistory bool // EnableLargerGossipHistory increases the gossip history we store in our caches.
WriteWalletPasswordOnWebOnboarding bool // WriteWalletPasswordOnWebOnboarding writes the password to disk after Prysm web signup.
DisableAttestingHistoryDBCache bool // DisableAttestingHistoryDBCache for the validator client increases disk reads/writes.

// Logging related toggles.
DisableGRPCConnectionLogs bool // Disables logging when a new grpc client has connected.
Expand Down Expand Up @@ -213,6 +214,10 @@ func ConfigureValidator(ctx *cli.Context) {
"upon completing web onboarding.")
cfg.WriteWalletPasswordOnWebOnboarding = true
}
if ctx.Bool(disableAttestingHistoryDBCache.Name) {
log.Warn("Disabled attesting history DB cache, likely increasing disk reads and writes significantly")
cfg.DisableAttestingHistoryDBCache = true
}
cfg.EnableBlst = true
if ctx.Bool(disableBlst.Name) {
log.Warn("Disabling new BLS library blst")
Expand Down
6 changes: 6 additions & 0 deletions shared/featureconfig/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ var (
Usage: "(Danger): Writes the wallet password to the wallet directory on completing Prysm web onboarding. " +
"We recommend against this flag unless you are an advanced user.",
}
disableAttestingHistoryDBCache = &cli.BoolFlag{
Name: "disable-attesting-history-db-cache",
Usage: "(Danger): Disables the cache for attesting history in the validator DB, greatly increasing " +
"disk reads and writes as well as increasing time required for attestations to be produced",
}
)

// devModeFlags holds list of flags that are set when development mode is on.
Expand All @@ -100,6 +105,7 @@ var devModeFlags = []cli.Flag{
var ValidatorFlags = append(deprecatedFlags, []cli.Flag{
writeWalletPasswordOnWebOnboarding,
enableExternalSlasherProtectionFlag,
disableAttestingHistoryDBCache,
ToledoTestnet,
PyrmontTestnet,
Mainnet,
Expand Down
1 change: 1 addition & 0 deletions validator/client/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ go_library(
"//shared/grpcutils:go_default_library",
"//shared/hashutil:go_default_library",
"//shared/mathutil:go_default_library",
"//shared/mputil:go_default_library",
"//shared/params:go_default_library",
"//shared/rand:go_default_library",
"//shared/slotutil:go_default_library",
Expand Down
30 changes: 18 additions & 12 deletions validator/client/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
validatorpb "github.com/prysmaticlabs/prysm/proto/validator/accounts/v2"
"github.com/prysmaticlabs/prysm/shared/bytesutil"
"github.com/prysmaticlabs/prysm/shared/hashutil"
"github.com/prysmaticlabs/prysm/shared/mputil"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/slotutil"
"github.com/prysmaticlabs/prysm/shared/timeutils"
Expand All @@ -29,6 +30,9 @@ func (v *validator) SubmitAttestation(ctx context.Context, slot uint64, pubKey [
ctx, span := trace.StartSpan(ctx, "validator.SubmitAttestation")
defer span.End()
span.AddAttributes(trace.StringAttribute("validator", fmt.Sprintf("%#x", pubKey)))
lock := mputil.NewMultilock(string(pubKey[:]))
lock.Lock()
defer lock.Unlock()

fmtKey := fmt.Sprintf("%#x", pubKey[:])
log := log.WithField("pubKey", fmt.Sprintf("%#x", bytesutil.Trunc(pubKey[:]))).WithField("slot", slot)
Expand Down Expand Up @@ -64,15 +68,25 @@ func (v *validator) SubmitAttestation(ctx context.Context, slot uint64, pubKey [
AttestingIndices: []uint64{duty.ValidatorIndex},
Data: data,
}
if err := v.preAttSignValidations(ctx, indexedAtt, pubKey); err != nil {

_, signingRoot, err := v.getDomainAndSigningRoot(ctx, indexedAtt.Data)
if err != nil {
log.WithError(err).Error("Could not get domain and signing root from attestation")
if v.emitAccountMetrics {
ValidatorAttestFailVec.WithLabelValues(fmtKey).Inc()
}
return
}

if err := v.slashableAttestationCheck(ctx, indexedAtt, pubKey, signingRoot); err != nil {
log.WithError(err).Error("Failed attestation slashing protection check")
log.WithFields(
attestationLogFields(pubKey, indexedAtt),
).Debug("Attempted slashable attestation details")
return
}

sig, signingRoot, err := v.signAtt(ctx, pubKey, data)
sig, _, err := v.signAtt(ctx, pubKey, data)
if err != nil {
log.WithError(err).Error("Could not sign attestation")
if v.emitAccountMetrics {
Expand Down Expand Up @@ -106,17 +120,9 @@ func (v *validator) SubmitAttestation(ctx context.Context, slot uint64, pubKey [
Signature: sig,
}

// Set the signature of the attestation and send it out to the beacon node.
indexedAtt.Signature = sig
if err := v.postAttSignUpdate(ctx, indexedAtt, pubKey, signingRoot); err != nil {
log.WithError(err).Error("Failed attestation slashing protection check")
log.WithFields(
attestationLogFields(pubKey, indexedAtt),
).Debug("Attempted slashable attestation details")
return
}
if err := v.SaveProtection(ctx, pubKey); err != nil {
log.WithError(err).Errorf("Could not save validator: %#x protection", pubKey)
}

attResp, err := v.validatorClient.ProposeAttestation(ctx, attestation)
if err != nil {
log.WithError(err).Error("Could not submit attestation to beacon node")
Expand Down
107 changes: 30 additions & 77 deletions validator/client/attest_protect.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,79 +18,22 @@ var failedAttLocalProtectionErr = "attempted to make slashable attestation, reje
var failedPreAttSignExternalErr = "attempted to make slashable attestation, rejected by external slasher service"
var failedPostAttSignExternalErr = "external slasher service detected a submitted slashable attestation"

func (v *validator) preAttSignValidations(ctx context.Context, indexedAtt *ethpb.IndexedAttestation, pubKey [48]byte) error {
ctx, span := trace.StartSpan(ctx, "validator.preAttSignUpdate")
defer span.End()

fmtKey := fmt.Sprintf("%#x", pubKey[:])
v.attesterHistoryByPubKeyLock.RLock()
attesterHistory, ok := v.attesterHistoryByPubKey[pubKey]
v.attesterHistoryByPubKeyLock.RUnlock()
if !ok {
AttestationMapMiss.Inc()
attesterHistoryMap, err := v.db.AttestationHistoryForPubKeysV2(ctx, [][48]byte{pubKey})
if err != nil {
return errors.Wrap(err, "could not get attester history")
}
attesterHistory, ok = attesterHistoryMap[pubKey]
if !ok {
log.WithField("publicKey", fmtKey).Debug("Could not get local slashing protection data for validator in pre validation")
}
} else {
AttestationMapHit.Inc()
}
_, sr, err := v.getDomainAndSigningRoot(ctx, indexedAtt.Data)
if err != nil {
log.WithError(err).Error("Could not get domain and signing root from attestation")
return err
}
slashable, err := isNewAttSlashable(
ctx,
attesterHistory,
indexedAtt.Data.Source.Epoch,
indexedAtt.Data.Target.Epoch,
sr,
)
if err != nil {
return errors.Wrap(err, "could not check if attestation is slashable")
}
if slashable {
if v.emitAccountMetrics {
ValidatorAttestFailVec.WithLabelValues(fmtKey).Inc()
}
return errors.New(failedAttLocalProtectionErr)
}
if featureconfig.Get().SlasherProtection && v.protector != nil {
if !v.protector.CheckAttestationSafety(ctx, indexedAtt) {
if v.emitAccountMetrics {
ValidatorAttestFailVecSlasher.WithLabelValues(fmtKey).Inc()
}
return errors.New(failedPreAttSignExternalErr)
}
}
return nil
}

func (v *validator) postAttSignUpdate(ctx context.Context, indexedAtt *ethpb.IndexedAttestation, pubKey [48]byte, signingRoot [32]byte) error {
// Checks if an attestation is slashable by comparing it with the attesting
// history for the given public key in our DB. If it is not, we then update the history
// with new values and save it to the database.
func (v *validator) slashableAttestationCheck(
ctx context.Context,
indexedAtt *ethpb.IndexedAttestation,
pubKey [48]byte,
signingRoot [32]byte,
) error {
ctx, span := trace.StartSpan(ctx, "validator.postAttSignUpdate")
defer span.End()

fmtKey := fmt.Sprintf("%#x", pubKey[:])
v.attesterHistoryByPubKeyLock.Lock()
defer v.attesterHistoryByPubKeyLock.Unlock()
attesterHistory, ok := v.attesterHistoryByPubKey[pubKey]
if !ok {
AttestationMapMiss.Inc()
attesterHistoryMap, err := v.db.AttestationHistoryForPubKeysV2(ctx, [][48]byte{pubKey})
if err != nil {
return errors.Wrap(err, "could not get attester history")
}
attesterHistory, ok = attesterHistoryMap[pubKey]
if !ok {
log.WithField("publicKey", fmtKey).Debug("Could not get local slashing protection data for validator in post validation")
}
} else {
AttestationMapHit.Inc()
attesterHistory, err := v.db.AttestationHistoryForPubKeyV2(ctx, pubKey)
if err != nil {
return errors.Wrap(err, "could not get attester history")
}
slashable, err := isNewAttSlashable(
ctx,
Expand Down Expand Up @@ -120,23 +63,33 @@ func (v *validator) postAttSignUpdate(ctx context.Context, indexedAtt *ethpb.Ind
if err != nil {
return errors.Wrapf(err, "could not mark epoch %d as attested", indexedAtt.Data.Target.Epoch)
}
v.attesterHistoryByPubKey[pubKey] = newHistory
if err := v.db.SaveAttestationHistoryForPubKeyV2(ctx, pubKey, newHistory); err != nil {
return errors.Wrapf(err, "could not save attestation history for public key: %#x", pubKey)
}

// Save source and target epochs to satisfy EIP3076 requirements.
// The DB methods below will replace the lowest epoch in DB if necessary.
if err := v.db.SaveLowestSignedSourceEpoch(ctx, pubKey, indexedAtt.Data.Source.Epoch); err != nil {
return err
}
if err := v.db.SaveLowestSignedTargetEpoch(ctx, pubKey, indexedAtt.Data.Target.Epoch); err != nil {
return err
}
if featureconfig.Get().SlasherProtection && v.protector != nil {
if !v.protector.CheckAttestationSafety(ctx, indexedAtt) {
if v.emitAccountMetrics {
ValidatorAttestFailVecSlasher.WithLabelValues(fmtKey).Inc()
}
return errors.New(failedPreAttSignExternalErr)
}
if !v.protector.CommitAttestation(ctx, indexedAtt) {
if v.emitAccountMetrics {
ValidatorAttestFailVecSlasher.WithLabelValues(fmtKey).Inc()
}
return errors.New(failedPostAttSignExternalErr)
}
}

// Save source and target epochs to satisfy EIP3076 requirements.
// The DB methods below will replace the lowest epoch in DB if necessary.
if err := v.db.SaveLowestSignedSourceEpoch(ctx, pubKey, indexedAtt.Data.Source.Epoch); err != nil {
return err
}
return v.db.SaveLowestSignedTargetEpoch(ctx, pubKey, indexedAtt.Data.Target.Epoch)
return nil
}

// isNewAttSlashable uses the attestation history to determine if an attestation of sourceEpoch
Expand Down

0 comments on commit 1fbfd52

Please sign in to comment.