From bfda29f2ad3ee3b47257ad431768ba58e48cbfed Mon Sep 17 00:00:00 2001 From: Preston Van Loon Date: Tue, 21 Jan 2020 15:29:04 -0800 Subject: [PATCH] Implement voluntary exits pool (#4610) --- .../operations/voluntaryexits/BUILD.bazel | 31 ++ beacon-chain/operations/voluntaryexits/doc.go | 2 + .../operations/voluntaryexits/service.go | 94 ++++ .../operations/voluntaryexits/service_test.go | 411 ++++++++++++++++++ 4 files changed, 538 insertions(+) create mode 100644 beacon-chain/operations/voluntaryexits/BUILD.bazel create mode 100644 beacon-chain/operations/voluntaryexits/doc.go create mode 100644 beacon-chain/operations/voluntaryexits/service.go create mode 100644 beacon-chain/operations/voluntaryexits/service_test.go diff --git a/beacon-chain/operations/voluntaryexits/BUILD.bazel b/beacon-chain/operations/voluntaryexits/BUILD.bazel new file mode 100644 index 00000000000..2fc60fdfcb8 --- /dev/null +++ b/beacon-chain/operations/voluntaryexits/BUILD.bazel @@ -0,0 +1,31 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "doc.go", + "service.go", + ], + importpath = "github.com/prysmaticlabs/prysm/beacon-chain/operations/voluntaryexits", + visibility = ["//beacon-chain:__subpackages__"], + deps = [ + "//beacon-chain/blockchain:go_default_library", + "//beacon-chain/core/helpers:go_default_library", + "//shared/params:go_default_library", + "@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library", + ], +) + +go_test( + name = "go_default_test", + size = "small", + srcs = ["service_test.go"], + embed = [":go_default_library"], + deps = [ + "//beacon-chain/blockchain/testing:go_default_library", + "//proto/beacon/p2p/v1:go_default_library", + "//shared/params:go_default_library", + "@com_github_gogo_protobuf//proto:go_default_library", + "@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library", + ], +) diff --git a/beacon-chain/operations/voluntaryexits/doc.go b/beacon-chain/operations/voluntaryexits/doc.go new file mode 100644 index 00000000000..0fe438fa29e --- /dev/null +++ b/beacon-chain/operations/voluntaryexits/doc.go @@ -0,0 +1,2 @@ +// Package voluntaryexits defines the operations management of voluntary exits. +package voluntaryexits diff --git a/beacon-chain/operations/voluntaryexits/service.go b/beacon-chain/operations/voluntaryexits/service.go new file mode 100644 index 00000000000..d81371f5d7a --- /dev/null +++ b/beacon-chain/operations/voluntaryexits/service.go @@ -0,0 +1,94 @@ +package voluntaryexits + +import ( + "context" + "sort" + "sync" + + ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" + "github.com/prysmaticlabs/prysm/beacon-chain/blockchain" + "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/shared/params" +) + +// Pool implements a struct to maintain pending and recently included voluntary exits. This pool +// is used by proposers to insert into new blocks. +type Pool struct { + lock sync.RWMutex + pending []*ethpb.SignedVoluntaryExit + included map[uint64]bool + chain blockchain.HeadFetcher +} + +// NewPool accepts a head fetcher (for reading the validator set) and returns an initialized +// voluntary exit pool. +func NewPool(chain blockchain.HeadFetcher) *Pool { + return &Pool{ + pending: make([]*ethpb.SignedVoluntaryExit, 0), + included: make(map[uint64]bool), + chain: chain, + } +} + +// PendingExits returns exits that are ready for inclusion at the given slot. +func (p *Pool) PendingExits(slot uint64) []*ethpb.SignedVoluntaryExit { + p.lock.RLock() + defer p.lock.RUnlock() + pending := make([]*ethpb.SignedVoluntaryExit, 0) + for _, e := range p.pending { + if e.Exit.Epoch > helpers.SlotToEpoch(slot) { + continue + } + pending = append(pending, e) + } + return pending +} + +// InsertVoluntaryExit into the pool. This method is a no-op if the pending exit already exists, +// has been included recently, or the validator is already exited. +func (p *Pool) InsertVoluntaryExit(ctx context.Context, exit *ethpb.SignedVoluntaryExit) { + p.lock.Lock() + defer p.lock.Unlock() + + // Has this validator index been included recently? + if p.included[exit.Exit.ValidatorIndex] { + return + } + + // Has the validator been exited already? + if h, _ := p.chain.HeadState(ctx); h == nil || len(h.Validators) <= int(exit.Exit.ValidatorIndex) || h.Validators[exit.Exit.ValidatorIndex].ExitEpoch != params.BeaconConfig().FarFutureEpoch { + return + } + + // Does this validator exist in the list already? Use binary search to find the answer. + if found := sort.Search(len(p.pending), func(i int) bool { + e := p.pending[i].Exit + return e.ValidatorIndex == exit.Exit.ValidatorIndex + }); found != len(p.pending) { + // If an exit exists with this validator index, prefer one with an earlier exit epoch. + if p.pending[found].Exit.Epoch > exit.Exit.Epoch { + p.pending[found] = exit + } + return + } + + // Insert into pending list and sort again. + p.pending = append(p.pending, exit) + sort.Slice(p.pending, func(i, j int) bool { + return p.pending[i].Exit.ValidatorIndex < p.pending[j].Exit.ValidatorIndex + }) +} + +// MarkIncluded is used when an exit has been included in a beacon block. Every block seen by this +// node should call this method to include the exit. +func (p *Pool) MarkIncluded(exit *ethpb.SignedVoluntaryExit) { + p.lock.Lock() + defer p.lock.Unlock() + i := sort.Search(len(p.pending), func(i int) bool { + return p.pending[i].Exit.ValidatorIndex == exit.Exit.ValidatorIndex + }) + if i != len(p.pending) { + p.pending = append(p.pending[:i], p.pending[i+1:]...) + } + p.included[exit.Exit.ValidatorIndex] = true +} diff --git a/beacon-chain/operations/voluntaryexits/service_test.go b/beacon-chain/operations/voluntaryexits/service_test.go new file mode 100644 index 00000000000..d1ef1227fdf --- /dev/null +++ b/beacon-chain/operations/voluntaryexits/service_test.go @@ -0,0 +1,411 @@ +package voluntaryexits + +import ( + "context" + "reflect" + "testing" + + "github.com/gogo/protobuf/proto" + ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" + mock "github.com/prysmaticlabs/prysm/beacon-chain/blockchain/testing" + pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" + "github.com/prysmaticlabs/prysm/shared/params" +) + +func TestPool_InsertVoluntaryExit(t *testing.T) { + type fields struct { + pending []*ethpb.SignedVoluntaryExit + included map[uint64]bool + } + type args struct { + exit *ethpb.SignedVoluntaryExit + } + tests := []struct { + name string + fields fields + args args + want []*ethpb.SignedVoluntaryExit + }{ + { + name: "Empty list", + fields: fields{ + pending: make([]*ethpb.SignedVoluntaryExit, 0), + included: make(map[uint64]bool), + }, + args: args{ + exit: ðpb.SignedVoluntaryExit{ + Exit: ðpb.VoluntaryExit{ + Epoch: 12, + ValidatorIndex: 1, + }, + }, + }, + want: []*ethpb.SignedVoluntaryExit{ + { + Exit: ðpb.VoluntaryExit{ + Epoch: 12, + ValidatorIndex: 1, + }, + }, + }, + }, + { + name: "Duplicate identical exit", + fields: fields{ + pending: []*ethpb.SignedVoluntaryExit{ + { + Exit: ðpb.VoluntaryExit{ + Epoch: 12, + ValidatorIndex: 1, + }, + }, + }, + included: make(map[uint64]bool), + }, + args: args{ + exit: ðpb.SignedVoluntaryExit{ + Exit: ðpb.VoluntaryExit{ + Epoch: 12, + ValidatorIndex: 1, + }, + }, + }, + want: []*ethpb.SignedVoluntaryExit{ + { + Exit: ðpb.VoluntaryExit{ + Epoch: 12, + ValidatorIndex: 1, + }, + }, + }, + }, + { + name: "Duplicate exit with lower epoch", + fields: fields{ + pending: []*ethpb.SignedVoluntaryExit{ + { + Exit: ðpb.VoluntaryExit{ + Epoch: 12, + ValidatorIndex: 1, + }, + }, + }, + included: make(map[uint64]bool), + }, + args: args{ + exit: ðpb.SignedVoluntaryExit{ + Exit: ðpb.VoluntaryExit{ + Epoch: 10, + ValidatorIndex: 1, + }, + }, + }, + want: []*ethpb.SignedVoluntaryExit{ + { + Exit: ðpb.VoluntaryExit{ + Epoch: 10, + ValidatorIndex: 1, + }, + }, + }, + }, + { + name: "Exit for already exited validator", + fields: fields{ + pending: []*ethpb.SignedVoluntaryExit{}, + included: make(map[uint64]bool), + }, + args: args{ + exit: ðpb.SignedVoluntaryExit{ + Exit: ðpb.VoluntaryExit{ + Epoch: 12, + ValidatorIndex: 2, + }, + }, + }, + want: []*ethpb.SignedVoluntaryExit{}, + }, + { + name: "Maintains sorted order", + fields: fields{ + pending: []*ethpb.SignedVoluntaryExit{ + { + Exit: ðpb.VoluntaryExit{ + Epoch: 12, + ValidatorIndex: 0, + }, + }, + { + Exit: ðpb.VoluntaryExit{ + Epoch: 12, + ValidatorIndex: 2, + }, + }, + }, + included: make(map[uint64]bool), + }, + args: args{ + exit: ðpb.SignedVoluntaryExit{ + Exit: ðpb.VoluntaryExit{ + Epoch: 10, + ValidatorIndex: 1, + }, + }, + }, + want: []*ethpb.SignedVoluntaryExit{ + { + Exit: ðpb.VoluntaryExit{ + Epoch: 12, + ValidatorIndex: 0, + }, + }, + { + Exit: ðpb.VoluntaryExit{ + Epoch: 10, + ValidatorIndex: 1, + }, + }, + { + Exit: ðpb.VoluntaryExit{ + Epoch: 12, + ValidatorIndex: 2, + }, + }, + }, + }, + { + name: "Already included", + fields: fields{ + pending: make([]*ethpb.SignedVoluntaryExit, 0), + included: map[uint64]bool{ + 1: true, + }, + }, + args: args{ + exit: ðpb.SignedVoluntaryExit{ + Exit: ðpb.VoluntaryExit{ + Epoch: 12, + ValidatorIndex: 1, + }, + }, + }, + want: []*ethpb.SignedVoluntaryExit{}, + }, + } + ctx := context.Background() + chain := &mock.ChainService{ + State: &pb.BeaconState{ + Validators: []*ethpb.Validator{ + { // 0 + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + }, + { // 1 + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + }, + { // 2 - Already exited. + ExitEpoch: 15, + }, + { // 3 + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Pool{ + pending: tt.fields.pending, + included: tt.fields.included, + chain: chain, + } + p.InsertVoluntaryExit(ctx, tt.args.exit) + if len(p.pending) != len(tt.want) { + t.Fatalf("Mismatched lengths of pending list. Got %d, wanted %d.", len(p.pending), len(tt.want)) + } + for i := range p.pending { + if !proto.Equal(p.pending[i], tt.want[i]) { + t.Errorf("Pending exit at index %d does not match expected. Got=%v wanted=%v", i, p.pending[i], tt.want[i]) + } + } + }) + } +} + +func TestPool_MarkIncluded(t *testing.T) { + type fields struct { + pending []*ethpb.SignedVoluntaryExit + included map[uint64]bool + } + type args struct { + exit *ethpb.SignedVoluntaryExit + } + tests := []struct { + name string + fields fields + args args + want fields + }{ + { + name: "Included, does not exist in pending", + fields: fields{ + pending: []*ethpb.SignedVoluntaryExit{ + { + Exit: ðpb.VoluntaryExit{ValidatorIndex: 2}, + }, + }, + included: make(map[uint64]bool), + }, + args: args{ + exit: ðpb.SignedVoluntaryExit{ + Exit: ðpb.VoluntaryExit{ValidatorIndex: 3}, + }, + }, + want: fields{ + pending: []*ethpb.SignedVoluntaryExit{ + { + Exit: ðpb.VoluntaryExit{ValidatorIndex: 2}, + }, + }, + included: map[uint64]bool{ + 3: true, + }, + }, + }, + { + name: "Removes from pending list", + fields: fields{ + pending: []*ethpb.SignedVoluntaryExit{ + { + Exit: ðpb.VoluntaryExit{ValidatorIndex: 1}, + }, + { + Exit: ðpb.VoluntaryExit{ValidatorIndex: 2}, + }, + { + Exit: ðpb.VoluntaryExit{ValidatorIndex: 3}, + }, + }, + included: map[uint64]bool{ + 0: true, + }, + }, + args: args{ + exit: ðpb.SignedVoluntaryExit{ + Exit: ðpb.VoluntaryExit{ValidatorIndex: 2}, + }, + }, + want: fields{ + pending: []*ethpb.SignedVoluntaryExit{ + { + Exit: ðpb.VoluntaryExit{ValidatorIndex: 1}, + }, + { + Exit: ðpb.VoluntaryExit{ValidatorIndex: 3}, + }, + }, + included: map[uint64]bool{ + 0: true, + 2: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Pool{ + pending: tt.fields.pending, + included: tt.fields.included, + } + p.MarkIncluded(tt.args.exit) + if len(p.pending) != len(tt.want.pending) { + t.Fatalf("Mismatched lengths of pending list. Got %d, wanted %d.", len(p.pending), len(tt.want.pending)) + } + for i := range p.pending { + if !proto.Equal(p.pending[i], tt.want.pending[i]) { + t.Errorf("Pending exit at index %d does not match expected. Got=%v wanted=%v", i, p.pending[i], tt.want.pending[i]) + } + } + if !reflect.DeepEqual(p.included, tt.want.included) { + t.Errorf("Included map is not as expected. Got=%v wanted=%v", p.included, tt.want.included) + } + }) + } +} + +func TestPool_PendingExits(t *testing.T) { + type fields struct { + pending []*ethpb.SignedVoluntaryExit + } + type args struct { + slot uint64 + } + tests := []struct { + name string + fields fields + args args + want []*ethpb.SignedVoluntaryExit + }{ + { + name: "Empty list", + fields: fields{ + pending: []*ethpb.SignedVoluntaryExit{}, + }, + args: args{ + slot: 100000, + }, + want: []*ethpb.SignedVoluntaryExit{}, + }, + { + name: "All eligible", + fields: fields{ + pending: []*ethpb.SignedVoluntaryExit{ + {Exit: ðpb.VoluntaryExit{Epoch: 0}}, + {Exit: ðpb.VoluntaryExit{Epoch: 1}}, + {Exit: ðpb.VoluntaryExit{Epoch: 2}}, + {Exit: ðpb.VoluntaryExit{Epoch: 3}}, + {Exit: ðpb.VoluntaryExit{Epoch: 4}}, + }, + }, + args: args{ + slot: 1000000, + }, + want: []*ethpb.SignedVoluntaryExit{ + {Exit: ðpb.VoluntaryExit{Epoch: 0}}, + {Exit: ðpb.VoluntaryExit{Epoch: 1}}, + {Exit: ðpb.VoluntaryExit{Epoch: 2}}, + {Exit: ðpb.VoluntaryExit{Epoch: 3}}, + {Exit: ðpb.VoluntaryExit{Epoch: 4}}, + }, + }, + { + name: "Some eligible", + fields: fields{ + pending: []*ethpb.SignedVoluntaryExit{ + {Exit: ðpb.VoluntaryExit{Epoch: 0}}, + {Exit: ðpb.VoluntaryExit{Epoch: 3}}, + {Exit: ðpb.VoluntaryExit{Epoch: 4}}, + {Exit: ðpb.VoluntaryExit{Epoch: 2}}, + {Exit: ðpb.VoluntaryExit{Epoch: 1}}, + }, + }, + args: args{ + slot: 2 * params.BeaconConfig().SlotsPerEpoch, + }, + want: []*ethpb.SignedVoluntaryExit{ + {Exit: ðpb.VoluntaryExit{Epoch: 0}}, + {Exit: ðpb.VoluntaryExit{Epoch: 2}}, + {Exit: ðpb.VoluntaryExit{Epoch: 1}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Pool{ + pending: tt.fields.pending, + } + if got := p.PendingExits(tt.args.slot); !reflect.DeepEqual(got, tt.want) { + t.Errorf("PendingExits() = %v, want %v", got, tt.want) + } + }) + } +}