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

Bounce attack check #3989

Merged
merged 7 commits into from
Nov 13, 2019
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
7 changes: 5 additions & 2 deletions beacon-chain/blockchain/chain_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ func TestFinalizedCheckpt_CanRetrieve(t *testing.T) {
ctx := context.Background()

c := setupBeaconChain(t, db)

if err := c.forkChoiceStore.GenesisStore(ctx, &ethpb.Checkpoint{}, &ethpb.Checkpoint{}); err != nil {
r := [32]byte{'g'}
if err := db.SaveState(ctx, &pb.BeaconState{}, r); err != nil {
t.Fatal(err)
}
if err := c.forkChoiceStore.GenesisStore(ctx, &ethpb.Checkpoint{Root: r[:]}, &ethpb.Checkpoint{Root: r[:]}); err != nil {
t.Fatal(err)
}

Expand Down
6 changes: 2 additions & 4 deletions beacon-chain/blockchain/forkchoice/lmd_ghost_yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,10 @@ func TestGetHeadFromYaml(t *testing.T) {

s := &pb.BeaconState{Validators: validators}

if err := store.GenesisStore(ctx, &ethpb.Checkpoint{}, &ethpb.Checkpoint{}); err != nil {
if err := store.db.SaveState(ctx, s, bytesutil.ToBytes32(blksRoot[0])); err != nil {
t.Fatal(err)
}

store.justifiedCheckpt.Root = blksRoot[0]
if err := store.db.SaveState(ctx, s, bytesutil.ToBytes32(blksRoot[0])); err != nil {
if err := store.GenesisStore(ctx, &ethpb.Checkpoint{Root: blksRoot[0]}, &ethpb.Checkpoint{Root: blksRoot[0]}); err != nil {
t.Fatal(err)
}

Expand Down
2 changes: 2 additions & 0 deletions beacon-chain/blockchain/forkchoice/process_attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ func (s *Store) OnAttestation(ctx context.Context, a *ethpb.Attestation) (uint64
return 0, err
}

s.updateJustifiedCheckpoint()

return tgtSlot, nil
}

Expand Down
13 changes: 10 additions & 3 deletions beacon-chain/blockchain/forkchoice/process_attestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ func TestStore_OnAttestation(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := store.GenesisStore(ctx, &ethpb.Checkpoint{}, &ethpb.Checkpoint{}); err != nil {
if err := store.GenesisStore(
ctx,
&ethpb.Checkpoint{Root: BlkWithValidStateRoot[:]},
&ethpb.Checkpoint{Root: BlkWithValidStateRoot[:]}); err != nil {
t.Fatal(err)
}

Expand Down Expand Up @@ -131,7 +134,11 @@ func TestStore_SaveCheckpointState(t *testing.T) {
Slashings: make([]uint64, params.BeaconConfig().EpochsPerSlashingsVector),
FinalizedCheckpoint: &ethpb.Checkpoint{},
}
if err := store.GenesisStore(ctx, &ethpb.Checkpoint{}, &ethpb.Checkpoint{}); err != nil {
r := [32]byte{'g'}
if err := store.db.SaveState(ctx, s, r); err != nil {
t.Fatal(err)
}
if err := store.GenesisStore(ctx, &ethpb.Checkpoint{Root: r[:]}, &ethpb.Checkpoint{Root: r[:]}); err != nil {
t.Fatal(err)
}

Expand Down Expand Up @@ -178,7 +185,7 @@ func TestStore_SaveCheckpointState(t *testing.T) {
}

s.Slot = params.BeaconConfig().SlotsPerEpoch + 1
if err := store.GenesisStore(ctx, &ethpb.Checkpoint{}, &ethpb.Checkpoint{}); err != nil {
if err := store.GenesisStore(ctx, &ethpb.Checkpoint{Root: r[:]}, &ethpb.Checkpoint{Root: r[:]}); err != nil {
t.Fatal(err)
}
cp3 := &ethpb.Checkpoint{Epoch: 1, Root: []byte{'C'}}
Expand Down
55 changes: 54 additions & 1 deletion beacon-chain/blockchain/forkchoice/process_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/hex"
"fmt"
"time"

"github.com/pkg/errors"
"github.com/prysmaticlabs/go-ssz"
Expand Down Expand Up @@ -89,8 +90,16 @@ func (s *Store) OnBlock(ctx context.Context, b *ethpb.BeaconBlock) error {
}

// Update justified check point.
s.updateJustifiedCheckpoint()
if postState.CurrentJustifiedCheckpoint.Epoch > s.JustifiedCheckpt().Epoch {
s.justifiedCheckpt = postState.CurrentJustifiedCheckpoint
s.bestJustifiedCheckpt = s.justifiedCheckpt
canUpdate, err := s.shouldUpdateJustified(ctx, postState.CurrentJustifiedCheckpoint)
if err != nil {
return err
}
if canUpdate {
s.justifiedCheckpt = postState.CurrentJustifiedCheckpoint
}
if err := s.db.SaveJustifiedCheckpoint(ctx, postState.CurrentJustifiedCheckpoint); err != nil {
return errors.Wrap(err, "could not save justified checkpoint")
}
Expand Down Expand Up @@ -440,3 +449,47 @@ func (s *Store) rmStatesOlderThanLastFinalized(ctx context.Context, startSlot ui

return nil
}

// shouldUpdateJustified prevents bouncing attack, by only update conflicting justified
// checkpoints in the fork choice if in the early slots of the epoch.
// Otherwise, delay incorporation of new justified checkpoint until next epoch boundary.
// See https://ethresear.ch/t/prevention-of-bouncing-attack-on-ffg/6114 for more detailed analysis and discussion.
func (s *Store) shouldUpdateJustified(ctx context.Context, newJustifiedCheckpt *ethpb.Checkpoint) (bool, error) {
if helpers.SlotsSinceEpochStarts(s.currentSlot()) < params.BeaconConfig().SafeSlotsToUpdateJustified {
return true, nil
}
newJustifiedBlock, err := s.db.Block(ctx, bytesutil.ToBytes32(newJustifiedCheckpt.Root))
if err != nil {
return false, err
}
if newJustifiedBlock.Slot <= helpers.StartSlot(s.justifiedCheckpt.Epoch) {
return false, nil
}
justifiedBlock, err := s.db.Block(ctx, bytesutil.ToBytes32(s.justifiedCheckpt.Root))
if err != nil {
return false, err
}
b, err := s.ancestor(ctx, newJustifiedCheckpt.Root, justifiedBlock.Slot)
if err != nil {
return false, err
}
if !bytes.Equal(b, s.justifiedCheckpt.Root) {
return false, nil
}
return true, nil
}

// currentSlot returns the current slot based on time.
func (s *Store) currentSlot() uint64 {
return (uint64(time.Now().Unix()) - s.genesisTime) / params.BeaconConfig().SecondsPerSlot
}

// updates justified check point in store if a better check point is known
func (s *Store) updateJustifiedCheckpoint() {
if helpers.SlotsSinceEpochStarts(s.currentSlot()) == 0 {
return
}
if s.bestJustifiedCheckpt.Epoch > s.justifiedCheckpt.Epoch {
s.justifiedCheckpt = s.bestJustifiedCheckpt
}
}
14 changes: 7 additions & 7 deletions beacon-chain/blockchain/forkchoice/process_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@ func TestStore_OnBlock(t *testing.T) {
t.Fatal(err)
}

randomParentRoot := []byte{'a'}
if err := store.db.SaveState(ctx, &pb.BeaconState{}, bytesutil.ToBytes32(randomParentRoot)); err != nil {
randomParentRoot := [32]byte{'a'}
if err := store.db.SaveState(ctx, &pb.BeaconState{}, randomParentRoot); err != nil {
t.Fatal(err)
}
randomParentRoot2 := roots[1]
if err := store.db.SaveState(ctx, &pb.BeaconState{}, bytesutil.ToBytes32(randomParentRoot2)); err != nil {
t.Fatal(err)
}
validGenesisRoot := []byte{'g'}
if err := store.db.SaveState(ctx, &pb.BeaconState{}, bytesutil.ToBytes32(validGenesisRoot)); err != nil {
validGenesisRoot := [32]byte{'g'}
if err := store.db.SaveState(ctx, &pb.BeaconState{}, validGenesisRoot); err != nil {
t.Fatal(err)
}

Expand All @@ -67,13 +67,13 @@ func TestStore_OnBlock(t *testing.T) {
},
{
name: "block is from the feature",
blk: &ethpb.BeaconBlock{ParentRoot: randomParentRoot, Slot: params.BeaconConfig().FarFutureEpoch},
blk: &ethpb.BeaconBlock{ParentRoot: randomParentRoot[:], Slot: params.BeaconConfig().FarFutureEpoch},
s: &pb.BeaconState{},
wantErrString: "could not process slot from the future",
},
{
name: "could not get finalized block",
blk: &ethpb.BeaconBlock{ParentRoot: randomParentRoot},
blk: &ethpb.BeaconBlock{ParentRoot: randomParentRoot[:]},
s: &pb.BeaconState{},
wantErrString: "block from slot 0 is not a descendent of the current finalized block",
},
Expand All @@ -87,7 +87,7 @@ func TestStore_OnBlock(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := store.GenesisStore(ctx, &ethpb.Checkpoint{}, &ethpb.Checkpoint{}); err != nil {
if err := store.GenesisStore(ctx, &ethpb.Checkpoint{Root: validGenesisRoot[:]}, &ethpb.Checkpoint{Root: validGenesisRoot[:]}); err != nil {
t.Fatal(err)
}
store.finalizedCheckpt.Root = roots[0]
Expand Down
5 changes: 5 additions & 0 deletions beacon-chain/blockchain/forkchoice/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ type Store struct {
attsQueueLock sync.Mutex
seenAtts map[[32]byte]bool
seenAttsLock sync.Mutex
genesisTime uint64
bestJustifiedCheckpt *ethpb.Checkpoint
}

// NewForkChoiceService instantiates a new service instance that will
Expand Down Expand Up @@ -82,6 +84,7 @@ func (s *Store) GenesisStore(
finalizedCheckpoint *ethpb.Checkpoint) error {

s.justifiedCheckpt = proto.Clone(justifiedCheckpoint).(*ethpb.Checkpoint)
s.bestJustifiedCheckpt = proto.Clone(justifiedCheckpoint).(*ethpb.Checkpoint)
s.finalizedCheckpt = proto.Clone(finalizedCheckpoint).(*ethpb.Checkpoint)
s.prevFinalizedCheckpt = proto.Clone(finalizedCheckpoint).(*ethpb.Checkpoint)

Expand All @@ -97,6 +100,8 @@ func (s *Store) GenesisStore(
return errors.Wrap(err, "could not save genesis state in check point cache")
}

s.genesisTime = justifiedState.GenesisTime

return nil
}

Expand Down
3 changes: 3 additions & 0 deletions beacon-chain/blockchain/forkchoice/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ func TestStore_GetHead(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := store.db.SaveState(ctx, s, blkRoot); err != nil {
t.Fatal(err)
}

checkPoint := &ethpb.Checkpoint{Root: blkRoot[:]}

Expand Down
16 changes: 11 additions & 5 deletions beacon-chain/blockchain/receive_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ func TestReceiveBlock_ProcessCorrectly(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := db.SaveState(ctx, beaconState, genesisBlkRoot); err != nil {
t.Fatal(err)
}
cp := &ethpb.Checkpoint{Root: genesisBlkRoot[:]}
if err := chainService.forkChoiceStore.GenesisStore(ctx, cp, cp); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -203,7 +206,14 @@ func TestReceiveBlockNoPubsubForkchoice_ProcessCorrectly(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := chainService.forkChoiceStore.GenesisStore(ctx, &ethpb.Checkpoint{}, &ethpb.Checkpoint{}); err != nil {
parentRoot, err := ssz.SigningRoot(genesis)
if err != nil {
t.Fatal(err)
}
if err := db.SaveState(ctx, beaconState, parentRoot); err != nil {
t.Fatal(err)
}
if err := chainService.forkChoiceStore.GenesisStore(ctx, &ethpb.Checkpoint{Root: parentRoot[:]}, &ethpb.Checkpoint{Root: parentRoot[:]}); err != nil {
t.Fatal(err)
}

Expand All @@ -216,10 +226,6 @@ func TestReceiveBlockNoPubsubForkchoice_ProcessCorrectly(t *testing.T) {
if err := chainService.beaconDB.SaveBlock(ctx, genesis); err != nil {
t.Fatalf("Could not save block to db: %v", err)
}
parentRoot, err := ssz.SigningRoot(genesis)
if err != nil {
t.Fatal(err)
}

if err := db.SaveState(ctx, beaconState, parentRoot); err != nil {
t.Fatal(err)
Expand Down
5 changes: 5 additions & 0 deletions beacon-chain/core/helpers/slot_epoch.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ func IsEpochEnd(slot uint64) bool {
return IsEpochStart(slot + 1)
}

// SlotsSinceEpochStarts returns number of slots since the start of the epoch.
func SlotsSinceEpochStarts(slot uint64) uint64 {
return slot - StartSlot(SlotToEpoch(slot))
}

// Allow for slots "from the future" within a certain tolerance.
const timeShiftTolerance = 10 // ms

Expand Down
3 changes: 3 additions & 0 deletions shared/params/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type BeaconChainConfig struct {
PersistentCommitteePeriod uint64 `yaml:"PERSISTENT_COMMITTEE_PERIOD"` // PersistentCommitteePeriod is the minimum amount of epochs a validator must participate before exitting.
MinEpochsToInactivityPenalty uint64 `yaml:"MIN_EPOCHS_TO_INACTIVITY_PENALTY"` // MinEpochsToInactivityPenalty defines the minimum amount of epochs since finality to begin penalizing inactivity.
Eth1FollowDistance uint64 // Eth1FollowDistance is the number of eth1.0 blocks to wait before considering a new deposit for voting. This only applies after the chain as been started.
SafeSlotsToUpdateJustified uint64 // SafeSlotsToUpdateJustified is the minimal slots needed to update justified check point.

// State list lengths
EpochsPerHistoricalVector uint64 `yaml:"EPOCHS_PER_HISTORICAL_VECTOR"` // EpochsPerHistoricalVector defines max length in epoch to store old historical stats in beacon state.
Expand Down Expand Up @@ -143,6 +144,7 @@ var defaultBeaconConfig = &BeaconChainConfig{
PersistentCommitteePeriod: 2048,
MinEpochsToInactivityPenalty: 4,
Eth1FollowDistance: 1024,
SafeSlotsToUpdateJustified: 8,

// State list length constants.
EpochsPerHistoricalVector: 65536,
Expand Down Expand Up @@ -265,6 +267,7 @@ func MinimalSpecConfig() *BeaconChainConfig {
minimalConfig.MinValidatorWithdrawabilityDelay = 256
minimalConfig.PersistentCommitteePeriod = 2048
minimalConfig.MinEpochsToInactivityPenalty = 4
minimalConfig.SafeSlotsToUpdateJustified = 2

// State vector lengths
minimalConfig.EpochsPerHistoricalVector = 64
Expand Down