Skip to content

Commit

Permalink
Update proposer protection to v0.11 (#5292)
Browse files Browse the repository at this point in the history
* Complete most of changes

* Fix other tests

* Test progress

* Tests

* Finish tests

* update pbs

* Fix mocked tests

* Gazelle

* pt 2

* Fix

* Fixes

* Fix tests wit hwrong copying
  • Loading branch information
0xKiwi authored Apr 3, 2020
1 parent c1946f8 commit 579a4e1
Show file tree
Hide file tree
Showing 10 changed files with 354 additions and 576 deletions.
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

0 comments on commit 579a4e1

Please sign in to comment.