mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-05-02 03:02:54 -04:00
Implement voluntary exits pool (#4610)
This commit is contained in:
committed by
terence tsao
parent
e96c2f4949
commit
bfda29f2ad
31
beacon-chain/operations/voluntaryexits/BUILD.bazel
Normal file
31
beacon-chain/operations/voluntaryexits/BUILD.bazel
Normal file
@@ -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",
|
||||
],
|
||||
)
|
||||
2
beacon-chain/operations/voluntaryexits/doc.go
Normal file
2
beacon-chain/operations/voluntaryexits/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package voluntaryexits defines the operations management of voluntary exits.
|
||||
package voluntaryexits
|
||||
94
beacon-chain/operations/voluntaryexits/service.go
Normal file
94
beacon-chain/operations/voluntaryexits/service.go
Normal file
@@ -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
|
||||
}
|
||||
411
beacon-chain/operations/voluntaryexits/service_test.go
Normal file
411
beacon-chain/operations/voluntaryexits/service_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user