Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update proposer protection to v0.11 #5292

Merged
merged 13 commits into from
Apr 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
310 changes: 42 additions & 268 deletions proto/slashing/slashing.pb.go

Large diffs are not rendered by default.

9 changes: 0 additions & 9 deletions proto/slashing/slashing.proto
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,6 @@ message EpochSpanMap {
map<uint64, MinMaxEpochSpan> epoch_span_map = 1;
}

// ProposalHistory defines the structure for recording a validator's historical proposals.
// Using a bitlist to represent the epochs and an uint64 to mark the latest marked
// epoch of the bitlist, we can easily store which epochs a validator has proposed
// a block for while pruning the older data.
message ProposalHistory {
bytes epoch_bits = 1 [(gogoproto.casttype) = "github.com/prysmaticlabs/go-bitfield.Bitlist"];
uint64 latest_epoch_written = 2;
}

// AttestationHistory defines the structure for recording a validator's historical attestation.
// Using a map[uint64]uint64 to map its target epoch to its source epoch, in order to detect if a
// vote being created is not a double vote and surrounded by, or surrounding any other votes.
Expand Down
54 changes: 6 additions & 48 deletions validator/client/validator_propose.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/prysmaticlabs/go-ssz"
"github.com/prysmaticlabs/prysm/beacon-chain/core/helpers"
slashpb "github.com/prysmaticlabs/prysm/proto/slashing"
"github.com/prysmaticlabs/prysm/shared/bls"
"github.com/prysmaticlabs/prysm/shared/bytesutil"
"github.com/prysmaticlabs/prysm/shared/featureconfig"
Expand Down Expand Up @@ -87,7 +86,7 @@ func (v *validator) ProposeBlock(ctx context.Context, slot uint64, pubKey [48]by
}

if featureconfig.Get().ProtectProposer {
history, err := v.db.ProposalHistory(ctx, pubKey[:])
slotBits, err := v.db.ProposalHistoryForEpoch(ctx, pubKey[:], epoch)
if err != nil {
log.WithError(err).Error("Failed to get proposal history")
if v.emitAccountMetrics {
Expand All @@ -96,7 +95,8 @@ func (v *validator) ProposeBlock(ctx context.Context, slot uint64, pubKey [48]by
return
}

if HasProposedForEpoch(history, epoch) {
// If the bit for the current slot is marked, do not propose.
if slotBits.BitAt(slot % params.BeaconConfig().SlotsPerEpoch) {
log.WithField("epoch", epoch).Error("Tried to sign a double proposal, rejected")
if v.emitAccountMetrics {
validatorProposeFailVec.WithLabelValues(fmtKey).Inc()
Expand Down Expand Up @@ -130,16 +130,16 @@ func (v *validator) ProposeBlock(ctx context.Context, slot uint64, pubKey [48]by
}

if featureconfig.Get().ProtectProposer {
history, err := v.db.ProposalHistory(ctx, pubKey[:])
slotBits, err := v.db.ProposalHistoryForEpoch(ctx, pubKey[:], epoch)
if err != nil {
log.WithError(err).Error("Failed to get proposal history")
if v.emitAccountMetrics {
validatorProposeFailVec.WithLabelValues(fmtKey).Inc()
}
return
}
history = SetProposedForEpoch(history, epoch)
if err := v.db.SaveProposalHistory(ctx, pubKey[:], history); err != nil {
slotBits.SetBitAt(slot%params.BeaconConfig().SlotsPerEpoch, true)
if err := v.db.SaveProposalHistoryForEpoch(ctx, pubKey[:], epoch, slotBits); err != nil {
log.WithError(err).Error("Failed to save updated proposal history")
if v.emitAccountMetrics {
validatorProposeFailVec.WithLabelValues(fmtKey).Inc()
Expand Down Expand Up @@ -220,45 +220,3 @@ func (v *validator) signBlock(ctx context.Context, pubKey [48]byte, epoch uint64
}
return sig.Marshal(), nil
}

// HasProposedForEpoch returns whether a validators proposal history has been marked for the entered epoch.
// If the request is more in the future than what the history contains, it will return false.
// If the request is from the past, and likely previously pruned it will return false.
func HasProposedForEpoch(history *slashpb.ProposalHistory, epoch uint64) bool {
wsPeriod := params.BeaconConfig().WeakSubjectivityPeriod
// Previously pruned, we should return false.
if int(epoch) <= int(history.LatestEpochWritten)-int(wsPeriod) {
return false
}
// Accessing future proposals that haven't been marked yet. Needs to return false.
if epoch > history.LatestEpochWritten {
return false
}
return history.EpochBits.BitAt(epoch % wsPeriod)
}

// SetProposedForEpoch updates the proposal history to mark the indicated epoch in the bitlist
// and updates the last epoch written if needed.
// Returns the modified proposal history.
func SetProposedForEpoch(history *slashpb.ProposalHistory, epoch uint64) *slashpb.ProposalHistory {
wsPeriod := params.BeaconConfig().WeakSubjectivityPeriod

if epoch > history.LatestEpochWritten {
// If the history is empty, just update the latest written and mark the epoch.
// This is for the first run of a validator.
if history.EpochBits.Count() < 1 {
history.LatestEpochWritten = epoch
history.EpochBits.SetBitAt(epoch%wsPeriod, true)
return history
}
// If the epoch to mark is ahead of latest written epoch, override the old votes 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 < epoch && i <= maxToWrite; i++ {
history.EpochBits.SetBitAt(i%wsPeriod, false)
}
history.LatestEpochWritten = epoch
}
history.EpochBits.SetBitAt(epoch%wsPeriod, true)
return history
}
156 changes: 39 additions & 117 deletions validator/client/validator_propose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (

"github.com/golang/mock/gomock"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/prysmaticlabs/go-bitfield"
slashpb "github.com/prysmaticlabs/prysm/proto/slashing"
"github.com/prysmaticlabs/prysm/shared/bytesutil"
"github.com/prysmaticlabs/prysm/shared/featureconfig"
"github.com/prysmaticlabs/prysm/shared/params"
Expand Down Expand Up @@ -224,6 +222,45 @@ func TestProposeBlock_AllowsPastProposals(t *testing.T) {
testutil.AssertLogsDoNotContain(t, hook, "Tried to sign a double proposal")
}

func TestProposeBlock_AllowsSameEpoch(t *testing.T) {
cfg := &featureconfig.Flags{
ProtectProposer: true,
}
featureconfig.Init(cfg)
hook := logTest.NewGlobal()
validator, m, finish := setup(t)
defer finish()
defer db.TeardownDB(t, validator.db)

m.validatorClient.EXPECT().DomainData(
gomock.Any(), // ctx
gomock.Any(), //epoch
).Times(2).Return(&ethpb.DomainResponse{}, nil /*err*/)

m.validatorClient.EXPECT().GetBlock(
gomock.Any(), // ctx
gomock.Any(),
).Times(2).Return(&ethpb.BeaconBlock{Body: &ethpb.BeaconBlockBody{}}, nil /*err*/)

m.validatorClient.EXPECT().DomainData(
gomock.Any(), // ctx
gomock.Any(), //epoch
).Times(2).Return(&ethpb.DomainResponse{}, nil /*err*/)

m.validatorClient.EXPECT().ProposeBlock(
gomock.Any(), // ctx
gomock.AssignableToTypeOf(&ethpb.SignedBeaconBlock{}),
).Times(2).Return(&ethpb.ProposeResponse{}, nil /*error*/)

pubKey := bytesutil.ToBytes48(validatorPubKey.Marshal())
farAhead := (params.BeaconConfig().WeakSubjectivityPeriod + 9) * params.BeaconConfig().SlotsPerEpoch
validator.ProposeBlock(context.Background(), farAhead, pubKey)
testutil.AssertLogsDoNotContain(t, hook, "Tried to sign a double proposal")

validator.ProposeBlock(context.Background(), farAhead-4, pubKey)
testutil.AssertLogsDoNotContain(t, hook, "Tried to sign a double proposal")
}

func TestProposeBlock_BroadcastsBlock(t *testing.T) {
validator, m, finish := setup(t)
defer finish()
Expand Down Expand Up @@ -288,118 +325,3 @@ func TestProposeBlock_BroadcastsBlock_WithGraffiti(t *testing.T) {
t.Errorf("Block was broadcast with the wrong graffiti field, wanted \"%v\", got \"%v\"", string(validator.graffiti), string(sentBlock.Block.Body.Graffiti))
}
}

func TestSetProposedForEpoch_SetsBit(t *testing.T) {
wsPeriod := params.BeaconConfig().WeakSubjectivityPeriod
proposals := &slashpb.ProposalHistory{
EpochBits: bitfield.NewBitlist(wsPeriod),
LatestEpochWritten: 0,
}
epoch := uint64(4)
proposals = SetProposedForEpoch(proposals, epoch)
proposed := HasProposedForEpoch(proposals, epoch)
if !proposed {
t.Fatal("Expected epoch 4 to be marked as proposed")
}
// Make sure no other bits are changed.
for i := uint64(1); i <= wsPeriod; i++ {
if i == epoch {
continue
}
if HasProposedForEpoch(proposals, i) {
t.Fatalf("Expected epoch %d to not be marked as proposed", i)
}
}
}

func TestSetProposedForEpoch_PrunesOverWSPeriod(t *testing.T) {
wsPeriod := params.BeaconConfig().WeakSubjectivityPeriod
proposals := &slashpb.ProposalHistory{
EpochBits: bitfield.NewBitlist(wsPeriod),
LatestEpochWritten: 0,
}
prunedEpoch := uint64(3)
proposals = SetProposedForEpoch(proposals, prunedEpoch)

if proposals.LatestEpochWritten != prunedEpoch {
t.Fatalf("Expected latest epoch written to be %d, received %d", prunedEpoch, proposals.LatestEpochWritten)
}

epoch := wsPeriod + 4
proposals = SetProposedForEpoch(proposals, epoch)
if !HasProposedForEpoch(proposals, epoch) {
t.Fatalf("Expected to be marked as proposed for epoch %d", epoch)
}
if proposals.LatestEpochWritten != epoch {
t.Fatalf("Expected latest written epoch to be %d, received %d", epoch, proposals.LatestEpochWritten)
}

if HasProposedForEpoch(proposals, epoch-wsPeriod+prunedEpoch) {
t.Fatalf("Expected the bit of pruned epoch %d to not be marked as proposed", epoch)
}
// Make sure no other bits are changed.
for i := epoch - wsPeriod + 1; i <= epoch; i++ {
if i == epoch {
continue
}
if HasProposedForEpoch(proposals, i) {
t.Fatalf("Expected epoch %d to not be marked as proposed", i)
}
}
}

func TestSetProposedForEpoch_KeepsHistory(t *testing.T) {
wsPeriod := params.BeaconConfig().WeakSubjectivityPeriod
proposals := &slashpb.ProposalHistory{
EpochBits: bitfield.NewBitlist(wsPeriod),
LatestEpochWritten: 0,
}
randomIndexes := []uint64{23, 423, 8900, 11347, 25033, 52225, 53999}
for i := 0; i < len(randomIndexes); i++ {
proposals = SetProposedForEpoch(proposals, randomIndexes[i])
}
if proposals.LatestEpochWritten != 53999 {
t.Fatalf("Expected latest epoch written to be %d, received %d", 53999, proposals.LatestEpochWritten)
}

// Make sure no other bits are changed.
for i := uint64(0); i < wsPeriod; i++ {
setIndex := false
for r := 0; r < len(randomIndexes); r++ {
if i == randomIndexes[r] {
setIndex = true
break
}
}

if setIndex != HasProposedForEpoch(proposals, i) {
t.Fatalf("Expected epoch %d to be marked as %t", i, setIndex)
}
}

// Set a past epoch as proposed, and make sure the recent data isn't changed.
proposals = SetProposedForEpoch(proposals, randomIndexes[1]+5)
if proposals.LatestEpochWritten != 53999 {
t.Fatalf("Expected last epoch written to not change after writing a past epoch, received %d", proposals.LatestEpochWritten)
}
// Proposal just marked should be true.
if !HasProposedForEpoch(proposals, randomIndexes[1]+5) {
t.Fatal("Expected marked past epoch to be true, received false")
}
// Previously marked proposal should stay true.
if !HasProposedForEpoch(proposals, randomIndexes[1]) {
t.Fatal("Expected marked past epoch to be true, received false")
}
}

func TestSetProposedForEpoch_PreventsProposingFutureEpochs(t *testing.T) {
wsPeriod := params.BeaconConfig().WeakSubjectivityPeriod
proposals := &slashpb.ProposalHistory{
EpochBits: bitfield.NewBitlist(wsPeriod),
LatestEpochWritten: 0,
}
proposals = SetProposedForEpoch(proposals, 200)
if HasProposedForEpoch(proposals, wsPeriod+200) {
t.Fatalf("Expected epoch %d to not be marked as proposed", wsPeriod+200)
}
}
2 changes: 2 additions & 0 deletions validator/db/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ go_library(
"@com_github_pkg_errors//:go_default_library",
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_wealdtech_go_bytesutil//:go_default_library",
"@io_etcd_go_bbolt//:go_default_library",
"@io_opencensus_go//trace:go_default_library",
],
Expand All @@ -33,6 +34,7 @@ go_test(
],
embed = [":go_default_library"],
deps = [
"//beacon-chain/core/helpers:go_default_library",
"//proto/slashing:go_default_library",
"//shared/params:go_default_library",
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
Expand Down
23 changes: 6 additions & 17 deletions validator/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"time"

"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
slashpb "github.com/prysmaticlabs/prysm/proto/slashing"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/validator/db/iface"
Expand Down Expand Up @@ -68,7 +67,7 @@ func createBuckets(tx *bolt.Tx, buckets ...[]byte) error {
// NewKVStore initializes a new boltDB key-value store at the directory
// path specified, creates the kv-buckets based on the schema, and stores
// an open connection db object as a property of the Store struct.
func NewKVStore(dirPath string, pubkeys [][48]byte) (*Store, error) {
func NewKVStore(dirPath string, pubKeys [][48]byte) (*Store, error) {
if err := os.MkdirAll(dirPath, 0700); err != nil {
return nil, err
}
Expand All @@ -93,21 +92,11 @@ func NewKVStore(dirPath string, pubkeys [][48]byte) (*Store, error) {
return nil, err
}

// Initialize the required pubkeys into the DB to ensure they're not empty.
for _, pubkey := range pubkeys {
proHistory, err := kv.ProposalHistory(context.Background(), pubkey[:])
if err != nil {
return nil, err
}
if proHistory == nil {
cleanHistory := &slashpb.ProposalHistory{
EpochBits: bitfield.NewBitlist(params.BeaconConfig().WeakSubjectivityPeriod),
}
if err := kv.SaveProposalHistory(context.Background(), pubkey[:], cleanHistory); err != nil {
return nil, err
}
}

// Initialize the required pubKeys into the DB to ensure they're not empty.
if err := kv.initializeSubBuckets(pubKeys); err != nil {
return nil, err
}
for _, pubkey := range pubKeys {
attHistory, err := kv.AttestationHistory(context.Background(), pubkey[:])
if err != nil {
return nil, err
Expand Down
5 changes: 4 additions & 1 deletion validator/db/iface/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ go_library(
importpath = "github.com/prysmaticlabs/prysm/validator/db/iface",
# Other packages must use github.com/prysmaticlabs/prysm/validator/db.Database alias.
visibility = ["//validator/db:__subpackages__"],
deps = ["//proto/slashing:go_default_library"],
deps = [
"//proto/slashing:go_default_library",
"@com_github_prysmaticlabs_go_bitfield//:go_default_library",
],
)
5 changes: 3 additions & 2 deletions validator/db/iface/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"io"

"github.com/prysmaticlabs/go-bitfield"
slashpb "github.com/prysmaticlabs/prysm/proto/slashing"
)

Expand All @@ -14,8 +15,8 @@ type ValidatorDB interface {
DatabasePath() string
ClearDB() error
// Proposer protection related methods.
ProposalHistory(ctx context.Context, publicKey []byte) (*slashpb.ProposalHistory, error)
SaveProposalHistory(ctx context.Context, publicKey []byte, history *slashpb.ProposalHistory) error
ProposalHistoryForEpoch(ctx context.Context, publicKey []byte, epoch uint64) (bitfield.Bitlist, error)
SaveProposalHistoryForEpoch(ctx context.Context, publicKey []byte, epoch uint64, history bitfield.Bitlist) error
DeleteProposalHistory(ctx context.Context, publicKey []byte) error
// Attester protection related methods.
AttestationHistory(ctx context.Context, publicKey []byte) (*slashpb.AttestationHistory, error)
Expand Down
Loading