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

Initial work on voluntary exit #4207

Merged
merged 12 commits into from
Dec 13, 2019
1 change: 1 addition & 0 deletions beacon-chain/blockchain/testing/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ go_library(
importpath = "github.com/prysmaticlabs/prysm/beacon-chain/blockchain/testing",
visibility = ["//beacon-chain:__subpackages__"],
deps = [
"//beacon-chain/core/feed/operation:go_default_library",
"//beacon-chain/core/feed/state:go_default_library",
"//beacon-chain/db:go_default_library",
"//proto/beacon/p2p/v1:go_default_library",
Expand Down
23 changes: 23 additions & 0 deletions beacon-chain/blockchain/testing/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/pkg/errors"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/prysmaticlabs/go-ssz"
opfeed "github.com/prysmaticlabs/prysm/beacon-chain/core/feed/operation"
statefeed "github.com/prysmaticlabs/prysm/beacon-chain/core/feed/state"
"github.com/prysmaticlabs/prysm/beacon-chain/db"
pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1"
Expand All @@ -26,6 +27,7 @@ type ChainService struct {
Fork *pb.Fork
DB db.Database
stateNotifier statefeed.Notifier
opNotifier opfeed.Notifier
}

// StateNotifier mocks the same method in the chain service.
Expand All @@ -49,6 +51,27 @@ func (msn *MockStateNotifier) StateFeed() *event.Feed {
return msn.feed
}

// OperationNotifier mocks the same method in the chain service.
func (ms *ChainService) OperationNotifier() opfeed.Notifier {
if ms.opNotifier == nil {
ms.opNotifier = &MockOperationNotifier{}
}
return ms.opNotifier
}

// MockOperationNotifier mocks the operation notifier.
type MockOperationNotifier struct {
feed *event.Feed
}

// OperationFeed returns an operation feed.
func (mon *MockOperationNotifier) OperationFeed() *event.Feed {
if mon.feed == nil {
mon.feed = new(event.Feed)
}
return mon.feed
}

// ReceiveBlock mocks ReceiveBlock method in chain service.
func (ms *ChainService) ReceiveBlock(ctx context.Context, block *ethpb.BeaconBlock) error {
return nil
Expand Down
38 changes: 38 additions & 0 deletions beacon-chain/core/exit/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["validation.go"],
importpath = "github.com/prysmaticlabs/prysm/beacon-chain/core/exit",
visibility = [
"//beacon-chain:__subpackages__",
],
deps = [
"//beacon-chain/core/helpers:go_default_library",
"//proto/beacon/p2p/v1:go_default_library",
"//shared/bls:go_default_library",
"//shared/mathutil:go_default_library",
"//shared/params:go_default_library",
"//shared/roughtime:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library",
"@com_github_prysmaticlabs_go_ssz//:go_default_library",
],
)

go_test(
name = "go_default_test",
srcs = ["validation_test.go"],
embed = [":go_default_library"],
deps = [
"//beacon-chain/blockchain/testing:go_default_library",
"//beacon-chain/core/blocks:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/core/state:go_default_library",
"//beacon-chain/db/testing:go_default_library",
"//shared/params:go_default_library",
"//shared/testutil:go_default_library",
"@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library",
"@com_github_prysmaticlabs_go_ssz//:go_default_library",
],
)
62 changes: 62 additions & 0 deletions beacon-chain/core/exit/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package exit

import (
"fmt"
"time"

"github.com/pkg/errors"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/prysmaticlabs/go-ssz"
"github.com/prysmaticlabs/prysm/beacon-chain/core/helpers"
pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1"
"github.com/prysmaticlabs/prysm/shared/bls"
"github.com/prysmaticlabs/prysm/shared/mathutil"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/roughtime"
)

// ValidateVoluntaryExit validates the voluntary exit.
// If it is invalid for some reason an error, if valid it will return no error.
func ValidateVoluntaryExit(state *pb.BeaconState, genesisTime time.Time, ve *ethpb.VoluntaryExit) error {
if ve.ValidatorIndex >= uint64(len(state.Validators)) {
return fmt.Errorf("unknown validator index %d", ve.ValidatorIndex)
}
validator := state.Validators[ve.ValidatorIndex]

if !helpers.IsActiveValidator(validator, ve.Epoch) {
return fmt.Errorf("validator %d not active at epoch %d", ve.ValidatorIndex, ve.Epoch)
}
if validator.ExitEpoch != params.BeaconConfig().FarFutureEpoch {
return fmt.Errorf("validator %d already exiting or exited", ve.ValidatorIndex)
}

secondsPerEpoch := params.BeaconConfig().SecondsPerSlot * params.BeaconConfig().SlotsPerEpoch
currentEpoch := uint64(roughtime.Now().Unix()-genesisTime.Unix()) / secondsPerEpoch
earliestRequestedExitEpoch := mathutil.Max(ve.Epoch, currentEpoch)
earliestExitEpoch := validator.ActivationEpoch + params.BeaconConfig().PersistentCommitteePeriod
if earliestRequestedExitEpoch < earliestExitEpoch {
return fmt.Errorf("validator %d cannot exit before epoch %d", ve.ValidatorIndex, earliestExitEpoch)
}

// Confirm signature is valid
root, err := ssz.SigningRoot(ve)
if err != nil {
return errors.Wrap(err, "cannot confirm signature")
}
sig, err := bls.SignatureFromBytes(ve.Signature)
if err != nil {
return errors.Wrap(err, "malformed signature")
}
validatorPubKey, err := bls.PublicKeyFromBytes(validator.PublicKey)
if err != nil {
return errors.Wrap(err, "invalid validator public key")
}
domain := bls.ComputeDomain(params.BeaconConfig().DomainVoluntaryExit)
verified := sig.Verify(root[:], validatorPubKey, domain)
if !verified {
return errors.New("incorrect signature")
}

// Parameters are valid.
return nil
}
118 changes: 118 additions & 0 deletions beacon-chain/core/exit/validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package exit_test

import (
"context"
"errors"
"testing"
"time"

ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/prysmaticlabs/go-ssz"
mockChain "github.com/prysmaticlabs/prysm/beacon-chain/blockchain/testing"
blk "github.com/prysmaticlabs/prysm/beacon-chain/core/blocks"
"github.com/prysmaticlabs/prysm/beacon-chain/core/exit"
"github.com/prysmaticlabs/prysm/beacon-chain/core/helpers"
"github.com/prysmaticlabs/prysm/beacon-chain/core/state"
dbutil "github.com/prysmaticlabs/prysm/beacon-chain/db/testing"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/testutil"
)

func TestValidation(t *testing.T) {
tests := []struct {
name string
epoch uint64
validatorIndex uint64
signature []byte
err error
}{
{
name: "MissingValidator",
epoch: 2048,
validatorIndex: 16,
err: errors.New("unknown validator index 16"),
},
{
name: "EarlyExit",
epoch: 2047,
validatorIndex: 0,
err: errors.New("validator 0 cannot exit before epoch 2048"),
},
{
name: "NoSignature",
epoch: 2048,
validatorIndex: 0,
err: errors.New("malformed signature: signature must be 96 bytes"),
},
{
name: "InvalidSignature",
epoch: 2048,
validatorIndex: 0,
signature: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
err: errors.New("malformed signature: could not unmarshal bytes into signature: err blsSignatureDeserialize 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"),
},
{
name: "IncorrectSignature",
epoch: 2048,
validatorIndex: 0,
signature: []byte{0xab, 0xb0, 0x12, 0x4c, 0x75, 0x74, 0xf2, 0x81, 0xa2, 0x93, 0xf4, 0x18, 0x5c, 0xad, 0x3c, 0xb2, 0x26, 0x81, 0xd5, 0x20, 0x91, 0x7c, 0xe4, 0x66, 0x65, 0x24, 0x3e, 0xac, 0xb0, 0x51, 0x00, 0x0d, 0x8b, 0xac, 0xf7, 0x5e, 0x14, 0x51, 0x87, 0x0c, 0xa6, 0xb3, 0xb9, 0xe6, 0xc9, 0xd4, 0x1a, 0x7b, 0x02, 0xea, 0xd2, 0x68, 0x5a, 0x84, 0x18, 0x8a, 0x4f, 0xaf, 0xd3, 0x82, 0x5d, 0xaf, 0x6a, 0x98, 0x96, 0x25, 0xd7, 0x19, 0xcc, 0xd2, 0xd8, 0x3a, 0x40, 0x10, 0x1f, 0x4a, 0x45, 0x3f, 0xca, 0x62, 0x87, 0x8c, 0x89, 0x0e, 0xca, 0x62, 0x23, 0x63, 0xf9, 0xdd, 0xb8, 0xf3, 0x67, 0xa9, 0x1e, 0x84},
err: errors.New("incorrect signature"),
},
{
name: "Good",
epoch: 2048,
validatorIndex: 0,
signature: []byte{0xb3, 0xe1, 0x9d, 0xc6, 0x7c, 0x78, 0x6c, 0xcf, 0x33, 0x1d, 0xb9, 0x6f, 0x59, 0x64, 0x44, 0xe1, 0x29, 0xd0, 0x87, 0x03, 0x26, 0x6e, 0x49, 0x1c, 0x05, 0xae, 0x16, 0x7b, 0x04, 0x0f, 0x3f, 0xf8, 0x82, 0x77, 0x60, 0xfc, 0xcf, 0x2f, 0x59, 0xc7, 0x40, 0x0b, 0x2c, 0xa9, 0x23, 0x8a, 0x6c, 0x8d, 0x01, 0x21, 0x5e, 0xa8, 0xac, 0x36, 0x70, 0x31, 0xb0, 0xe1, 0xa8, 0xb8, 0x8f, 0x93, 0x8c, 0x1c, 0xa2, 0x86, 0xe7, 0x22, 0x00, 0x6a, 0x7d, 0x36, 0xc0, 0x2b, 0x86, 0x2c, 0xf5, 0xf9, 0x10, 0xb9, 0xf2, 0xbd, 0x5e, 0xa6, 0x5f, 0x12, 0x86, 0x43, 0x20, 0x4d, 0xa2, 0x9d, 0x8b, 0xe6, 0x6f, 0x09},
},
}

db := dbutil.SetupDB(t)
defer dbutil.TeardownDB(t, db)
ctx := context.Background()
helpers.ClearAllCaches()
deposits, _, _ := testutil.DeterministicDepositsAndKeys(8)
beaconState, err := state.GenesisBeaconState(deposits, 0, &ethpb.Eth1Data{BlockHash: make([]byte, 32)})
if err != nil {
t.Fatal(err)
}
block := blk.NewGenesisBlock([]byte{})
if err := db.SaveBlock(ctx, block); err != nil {
t.Fatalf("Could not save genesis block: %v", err)
}
genesisRoot, err := ssz.SigningRoot(block)
if err != nil {
t.Fatalf("Could not get signing root %v", err)
}

// Set genesis time to be 100 epochs ago
genesisTime := time.Now().Add(time.Duration(-100*int64(params.BeaconConfig().SecondsPerSlot*params.BeaconConfig().SlotsPerEpoch)) * time.Second)
mockChainService := &mockChain.ChainService{State: beaconState, Root: genesisRoot[:], Genesis: genesisTime}
headState, err := mockChainService.HeadState(context.Background())
if err != nil {
t.Fatal("Failed to obtain head state")
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := &ethpb.VoluntaryExit{
Epoch: test.epoch,
ValidatorIndex: test.validatorIndex,
Signature: test.signature,
}

err := exit.ValidateVoluntaryExit(headState, genesisTime, req)
if test.err == nil {
if err != nil {
t.Errorf("Unexpected error: received %v", err)
}
} else {
if err == nil {
t.Error("Failed to receive expected error")
}
if err.Error() != test.err.Error() {
t.Errorf("Unexpected error: expected %s, received %s", test.err.Error(), err.Error())
}
}
})
}
}
4 changes: 2 additions & 2 deletions beacon-chain/core/feed/operation/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ type AggregatedAttReceivedData struct {
Attestation *pb.AggregateAndProof
}

// ExitRecievedData is the data sent with ExitReceived events.
type ExitRecievedData struct {
// ExitReceivedData is the data sent with ExitReceived events.
type ExitReceivedData struct {
// Exit is the voluntary exit object.
Exit *ethpb.VoluntaryExit
}
8 changes: 8 additions & 0 deletions beacon-chain/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type BeaconNode struct {
attestationPool attestations.Pool
depositCache *depositcache.DepositCache
stateFeed *event.Feed
opFeed *event.Feed
}

// NewBeaconNode creates a new node instance, sets up configuration options, and registers
Expand All @@ -84,6 +85,7 @@ func NewBeaconNode(ctx *cli.Context) (*BeaconNode, error) {
services: registry,
stop: make(chan struct{}),
stateFeed: new(event.Feed),
opFeed: new(event.Feed),
attestationPool: attestations.NewPool(),
}

Expand Down Expand Up @@ -160,6 +162,11 @@ func (b *BeaconNode) StateFeed() *event.Feed {
return b.stateFeed
}

// OperationFeed implements opfeed.Notifier.
func (b *BeaconNode) OperationFeed() *event.Feed {
return b.opFeed
}

// Start the BeaconNode and kicks off every registered service.
func (b *BeaconNode) Start() {
b.lock.Lock()
Expand Down Expand Up @@ -464,6 +471,7 @@ func (b *BeaconNode) registerRPCService(ctx *cli.Context) error {
DepositFetcher: depositFetcher,
PendingDepositFetcher: b.depositCache,
StateNotifier: b,
OperationNotifier: b,
})

return b.services.RegisterService(rpcService)
Expand Down
1 change: 1 addition & 0 deletions beacon-chain/rpc/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ go_library(
"//beacon-chain/blockchain:go_default_library",
"//beacon-chain/cache:go_default_library",
"//beacon-chain/cache/depositcache:go_default_library",
"//beacon-chain/core/feed/operation:go_default_library",
"//beacon-chain/core/feed/state:go_default_library",
"//beacon-chain/db:go_default_library",
"//beacon-chain/operations/attestations:go_default_library",
Expand Down
6 changes: 6 additions & 0 deletions beacon-chain/rpc/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/prysmaticlabs/prysm/beacon-chain/blockchain"
"github.com/prysmaticlabs/prysm/beacon-chain/cache"
"github.com/prysmaticlabs/prysm/beacon-chain/cache/depositcache"
opfeed "github.com/prysmaticlabs/prysm/beacon-chain/core/feed/operation"
statefeed "github.com/prysmaticlabs/prysm/beacon-chain/core/feed/state"
"github.com/prysmaticlabs/prysm/beacon-chain/db"
"github.com/prysmaticlabs/prysm/beacon-chain/operations/attestations"
Expand Down Expand Up @@ -73,6 +74,7 @@ type Service struct {
depositFetcher depositcache.DepositFetcher
pendingDepositFetcher depositcache.PendingDepositsFetcher
stateNotifier statefeed.Notifier
operationNotifier opfeed.Notifier
}

// Config options for the beacon node RPC server.
Expand All @@ -97,6 +99,7 @@ type Config struct {
DepositFetcher depositcache.DepositFetcher
PendingDepositFetcher depositcache.PendingDepositsFetcher
StateNotifier statefeed.Notifier
OperationNotifier opfeed.Notifier
}

// NewService instantiates a new RPC service instance that will
Expand Down Expand Up @@ -128,6 +131,7 @@ func NewService(ctx context.Context, cfg *Config) *Service {
canonicalStateChan: make(chan *pbp2p.BeaconState, params.BeaconConfig().DefaultBufferSize),
incomingAttestation: make(chan *ethpb.Attestation, params.BeaconConfig().DefaultBufferSize),
stateNotifier: cfg.StateNotifier,
operationNotifier: cfg.OperationNotifier,
}
}

Expand Down Expand Up @@ -184,11 +188,13 @@ func (s *Service) Start() {
Eth1InfoFetcher: s.powChainService,
SyncChecker: s.syncService,
StateNotifier: s.stateNotifier,
OperationNotifier: s.operationNotifier,
P2P: s.p2p,
BlockReceiver: s.blockReceiver,
MockEth1Votes: s.mockEth1Votes,
Eth1BlockFetcher: s.powChainService,
PendingDepositsFetcher: s.pendingDepositFetcher,
GenesisTimeFetcher: s.genesisTimeFetcher,
}
nodeServer := &node.Server{
BeaconDB: s.beaconDB,
Expand Down
Loading