Compare commits

...

8 Commits

Author SHA1 Message Date
nisdas
039b86bd89 debug logs 2023-01-25 21:49:25 +08:00
Potuz
9619170df1 Return error structures from bls changes endpoint (#11908)
Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
2023-01-24 15:58:43 +00:00
james-prysm
4da0abace7 validator exit test (#11910)
* adding exit test

* fixing deep source errors
2023-01-24 15:26:58 +00:00
Patrice Vignola
d919f800e3 Add REST implementation for Validator's GetDuties (#11893)
* Add REST implementation for `MultipleValidatorStatus`

* Fix PR comments

* WIP

* Add REST implementation for Validator's SubscribeCommitteeSubnets

* Remove redundant test

* Initialize dutiesProvider

* Address PR comments

* Remove duplicate import

* Fix build break

* WIP

* WIP

* WIP

* Add valid test case for getDutiesForEpoch

* Add failure tests for getDuties

* Revert test change

* Address PR comments

* Address PR comments

Co-authored-by: Emmanuel NALEPA <emmanuel.nalepa@kiln.fi>
Co-authored-by: Radosław Kapka <rkapka@wp.pl>
Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
2023-01-24 14:33:29 +00:00
james-prysm
38f095d556 prysmctl: changeblstoexecutionaddress ( withdrawals) (#11790)
* adding unit test for withdrawal

* updating to handle api changes

* Update cmd/prysmctl/withdrawal/withdrawal.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/withdrawal/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/withdrawal/withdrawal.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/withdrawal/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/withdrawal/withdrawal.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/withdrawal/withdrawal.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/withdrawal/withdrawal.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* updating based on feedback

* rewording prints

* Update cmd/prysmctl/withdrawal/withdrawal.go

Co-authored-by: Potuz <potuz@prysmaticlabs.com>

* add error handling when status not ok

* addressing some of the feedback, adding new tests

* adding unit test with error return

* adjusting based on review comments

* addressing more comments and fixing unit test

* updating prompts

* migrating some functions to api/client

* removing unused function

* adding review feedback

* removing unused functions

* migrating command, old command under accounts still exist

* reverting tos.go

* fixing bazel lint

* fixing build

* resolving unused parameter caught by deepsource

* fixing unit tests

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Potuz <potuz@prysmaticlabs.com>

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Potuz <potuz@prysmaticlabs.com>

* adding in website

* addressing review comment

* adding new verify only flag

* Update cmd/prysmctl/validator/withdraw.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/withdraw.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/withdraw.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/withdraw.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update api/client/beacon/client.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update api/client/beacon/client.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update api/client/beacon/client.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update api/client/beacon/client.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/withdraw.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* addressing comments

* fixing unit test

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* address comments

* Update cmd/prysmctl/validator/cmd.go

Co-authored-by: Radosław Kapka <rkapka@wp.pl>

* adjusting object for new provided format

* adding omit empty

* fixing unit test, and linting

* adjusting based on cli changes

* fixing test data and metadata property

* addressing backwards compatability

* reverting based on review comment

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
Co-authored-by: Potuz <potuz@prysmaticlabs.com>
Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>
Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
2023-01-24 10:05:55 +00:00
Potuz
04010d45c8 Do not check headstate being Capella for BLS changes on gossip (#11902)
* Do not check headstate being Capella for BLS changes on gossip

* fix test

Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
2023-01-24 00:11:26 +00:00
Radosław Kapka
1a048a2f2a Refactor BLS change pool (#11894)
* Refactor BLS change pool

* update mock's signature

* do not return error from `MarkIncluded`

* fix tests

* fix mock's signature

Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
2023-01-23 14:11:45 +00:00
terencechain
3df2dedbb2 Exit properly with terminal block hash (#11892) 2023-01-23 14:27:36 +01:00
33 changed files with 2677 additions and 222 deletions

View File

@@ -28,13 +28,14 @@ import (
)
const (
getSignedBlockPath = "/eth/v2/beacon/blocks"
getBlockRootPath = "/eth/v1/beacon/blocks/{{.Id}}/root"
getForkForStatePath = "/eth/v1/beacon/states/{{.Id}}/fork"
getWeakSubjectivityPath = "/eth/v1/beacon/weak_subjectivity"
getForkSchedulePath = "/eth/v1/config/fork_schedule"
getStatePath = "/eth/v2/debug/beacon/states"
getNodeVersionPath = "/eth/v1/node/version"
getSignedBlockPath = "/eth/v2/beacon/blocks"
getBlockRootPath = "/eth/v1/beacon/blocks/{{.Id}}/root"
getForkForStatePath = "/eth/v1/beacon/states/{{.Id}}/fork"
getWeakSubjectivityPath = "/eth/v1/beacon/weak_subjectivity"
getForkSchedulePath = "/eth/v1/config/fork_schedule"
getStatePath = "/eth/v2/debug/beacon/states"
getNodeVersionPath = "/eth/v1/node/version"
changeBLStoExecutionPath = "/eth/v1/beacon/pool/bls_to_execution_changes"
)
// StateOrBlockId represents the block_id / state_id parameters that several of the Eth Beacon API methods accept.
@@ -146,7 +147,6 @@ func withSSZEncoding() reqOption {
// get is a generic, opinionated GET function to reduce boilerplate amongst the getters in this package.
func (c *Client) get(ctx context.Context, path string, opts ...reqOption) ([]byte, error) {
u := c.baseURL.ResolveReference(&url.URL{Path: path})
log.Printf("requesting %s", u.String())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
@@ -343,6 +343,60 @@ func (c *Client) GetWeakSubjectivity(ctx context.Context) (*WeakSubjectivityData
}, nil
}
// SubmitChangeBLStoExecution calls a beacon API endpoint to set the withdrawal addresses based on the given signed messages.
// If the API responds with something other than OK there will be failure messages associated to the corresponding request message.
func (c *Client) SubmitChangeBLStoExecution(ctx context.Context, request []*apimiddleware.SignedBLSToExecutionChangeJson) error {
u := c.baseURL.ResolveReference(&url.URL{Path: changeBLStoExecutionPath})
body, err := json.Marshal(request)
if err != nil {
return errors.Wrap(err, "failed to marshal JSON")
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewBuffer(body))
if err != nil {
return errors.Wrap(err, "invalid format, failed to create new POST request object")
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.hc.Do(req)
if err != nil {
return err
}
defer func() {
err = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
decoder := json.NewDecoder(resp.Body)
decoder.DisallowUnknownFields()
errorJson := &apimiddleware.IndexedVerificationFailureErrorJson{}
if err := decoder.Decode(errorJson); err != nil {
return errors.Wrapf(err, "failed to decode error JSON for %s", resp.Request.URL)
}
for _, failure := range errorJson.Failures {
w := request[failure.Index].Message
log.WithFields(log.Fields{
"validator_index": w.ValidatorIndex,
"withdrawal_address": w.ToExecutionAddress,
}).Error(failure.Message)
}
return errors.Errorf("POST error %d: %s", errorJson.Code, errorJson.Message)
}
return nil
}
// GetBLStoExecutionChanges gets all the set withdrawal messages in the node's operation pool.
// Returns a struct representation of json response.
func (c *Client) GetBLStoExecutionChanges(ctx context.Context) (*apimiddleware.BLSToExecutionChangesPoolResponseJson, error) {
body, err := c.get(ctx, changeBLStoExecutionPath)
if err != nil {
return nil, err
}
poolResponse := &apimiddleware.BLSToExecutionChangesPoolResponseJson{}
err = json.Unmarshal(body, poolResponse)
if err != nil {
return nil, err
}
return poolResponse, nil
}
func non200Err(response *http.Response) error {
bodyBytes, err := io.ReadAll(response.Body)
var body string

View File

@@ -49,9 +49,13 @@ func (s *Service) validateMergeBlock(ctx context.Context, b interfaces.SignedBea
if payload.IsNil() {
return errors.New("nil execution payload")
}
if err := validateTerminalBlockHash(b.Block().Slot(), payload); err != nil {
ok, err := canUseValidatedTerminalBlockHash(b.Block().Slot(), payload)
if err != nil {
return errors.Wrap(err, "could not validate terminal block hash")
}
if ok {
return nil
}
mergeBlockParentHash, mergeBlockTD, err := s.getBlkParentHashAndTD(ctx, payload.ParentHash())
if err != nil {
return errors.Wrap(err, "could not get merge block parent hash and total difficulty")
@@ -105,7 +109,7 @@ func (s *Service) getBlkParentHashAndTD(ctx context.Context, blkHash []byte) ([]
return blk.ParentHash[:], blkTDUint256, nil
}
// validateTerminalBlockHash validates if the merge block is a valid terminal PoW block.
// canUseValidatedTerminalBlockHash validates if the merge block is a valid terminal PoW block.
// spec code:
// if TERMINAL_BLOCK_HASH != Hash32():
//
@@ -113,17 +117,17 @@ func (s *Service) getBlkParentHashAndTD(ctx context.Context, blkHash []byte) ([]
// assert compute_epoch_at_slot(block.slot) >= TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH
// assert block.body.execution_payload.parent_hash == TERMINAL_BLOCK_HASH
// return
func validateTerminalBlockHash(blkSlot types.Slot, payload interfaces.ExecutionData) error {
func canUseValidatedTerminalBlockHash(blkSlot types.Slot, payload interfaces.ExecutionData) (bool, error) {
if bytesutil.ToBytes32(params.BeaconConfig().TerminalBlockHash.Bytes()) == [32]byte{} {
return nil
return false, nil
}
if params.BeaconConfig().TerminalBlockHashActivationEpoch > slots.ToEpoch(blkSlot) {
return errors.New("terminal block hash activation epoch not reached")
return false, errors.New("terminal block hash activation epoch not reached")
}
if !bytes.Equal(payload.ParentHash(), params.BeaconConfig().TerminalBlockHash.Bytes()) {
return errors.New("parent hash does not match terminal block hash")
return false, errors.New("parent hash does not match terminal block hash")
}
return nil
return true, nil
}
// validateTerminalBlockDifficulties validates terminal pow block by comparing own total difficulty with parent's total difficulty.

View File

@@ -18,6 +18,7 @@ import (
enginev1 "github.com/prysmaticlabs/prysm/v3/proto/engine/v1"
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v3/testing/require"
"github.com/prysmaticlabs/prysm/v3/testing/util"
)
func Test_validTerminalPowBlock(t *testing.T) {
@@ -213,20 +214,42 @@ func Test_getBlkParentHashAndTD(t *testing.T) {
func Test_validateTerminalBlockHash(t *testing.T) {
wrapped, err := blocks.WrappedExecutionPayload(&enginev1.ExecutionPayload{})
require.NoError(t, err)
require.NoError(t, validateTerminalBlockHash(1, wrapped))
ok, err := canUseValidatedTerminalBlockHash(1, wrapped)
require.NoError(t, err)
require.Equal(t, false, ok)
cfg := params.BeaconConfig()
cfg.TerminalBlockHash = [32]byte{0x01}
params.OverrideBeaconConfig(cfg)
require.ErrorContains(t, "terminal block hash activation epoch not reached", validateTerminalBlockHash(1, wrapped))
ok, err = canUseValidatedTerminalBlockHash(1, wrapped)
require.ErrorContains(t, "terminal block hash activation epoch not reached", err)
require.Equal(t, false, ok)
cfg.TerminalBlockHashActivationEpoch = 0
params.OverrideBeaconConfig(cfg)
require.ErrorContains(t, "parent hash does not match terminal block hash", validateTerminalBlockHash(1, wrapped))
ok, err = canUseValidatedTerminalBlockHash(1, wrapped)
require.ErrorContains(t, "parent hash does not match terminal block hash", err)
require.Equal(t, false, ok)
wrapped, err = blocks.WrappedExecutionPayload(&enginev1.ExecutionPayload{
ParentHash: cfg.TerminalBlockHash.Bytes(),
})
require.NoError(t, err)
require.NoError(t, validateTerminalBlockHash(1, wrapped))
ok, err = canUseValidatedTerminalBlockHash(1, wrapped)
require.NoError(t, err)
require.Equal(t, true, ok)
ctx := context.Background()
beaconDB := testDB.SetupDB(t)
opts := []Option{
WithDatabase(beaconDB),
WithStateGen(stategen.New(beaconDB, doublylinkedtree.New())),
}
service, err := NewService(ctx, opts...)
require.NoError(t, err)
blk, err := blocks.NewSignedBeaconBlock(util.HydrateSignedBeaconBlockBellatrix(&ethpb.SignedBeaconBlockBellatrix{}))
require.NoError(t, err)
blk.Block().SetSlot(1)
require.NoError(t, blk.Block().Body().SetExecution(wrapped))
require.NoError(t, service.validateMergeBlock(ctx, blk))
}

View File

@@ -557,22 +557,6 @@ func (s *Service) handleBlockAttestations(ctx context.Context, blk interfaces.Be
return nil
}
func (s *Service) handleBlockBLSToExecChanges(blk interfaces.BeaconBlock) error {
if blk.Version() < version.Capella {
return nil
}
changes, err := blk.Body().BLSToExecutionChanges()
if err != nil {
return errors.Wrap(err, "could not get BLSToExecutionChanges")
}
for _, change := range changes {
if err := s.cfg.BLSToExecPool.MarkIncluded(change); err != nil {
return errors.Wrap(err, "could not mark BLSToExecutionChange as included")
}
}
return nil
}
// InsertSlashingsToForkChoiceStore inserts attester slashing indices to fork choice store.
// To call this function, it's caller's responsibility to ensure the slashing object is valid.
func (s *Service) InsertSlashingsToForkChoiceStore(ctx context.Context, slashings []*ethpb.AttesterSlashing) {

View File

@@ -26,7 +26,6 @@ import (
doublylinkedtree "github.com/prysmaticlabs/prysm/v3/beacon-chain/forkchoice/doubly-linked-tree"
forkchoicetypes "github.com/prysmaticlabs/prysm/v3/beacon-chain/forkchoice/types"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/attestations"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/blstoexec"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/state"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/state/stategen"
fieldparams "github.com/prysmaticlabs/prysm/v3/config/fieldparams"
@@ -2305,65 +2304,6 @@ func TestFillMissingBlockPayloadId_DiffSlotExitEarly(t *testing.T) {
require.NoError(t, service.fillMissingBlockPayloadId(ctx, time.Unix(int64(params.BeaconConfig().SecondsPerSlot/2), 0)))
}
func TestHandleBBlockBLSToExecutionChanges(t *testing.T) {
ctx := context.Background()
beaconDB := testDB.SetupDB(t)
fc := doublylinkedtree.New()
pool := blstoexec.NewPool()
opts := []Option{
WithDatabase(beaconDB),
WithStateGen(stategen.New(beaconDB, fc)),
WithForkChoiceStore(fc),
WithStateNotifier(&mock.MockStateNotifier{}),
WithBLSToExecPool(pool),
}
service, err := NewService(ctx, opts...)
require.NoError(t, err)
t.Run("pre Capella block", func(t *testing.T) {
body := &ethpb.BeaconBlockBodyBellatrix{}
pbb := &ethpb.BeaconBlockBellatrix{
Body: body,
}
blk, err := consensusblocks.NewBeaconBlock(pbb)
require.NoError(t, err)
require.NoError(t, service.handleBlockBLSToExecChanges(blk))
})
t.Run("Post Capella no changes", func(t *testing.T) {
body := &ethpb.BeaconBlockBodyCapella{}
pbb := &ethpb.BeaconBlockCapella{
Body: body,
}
blk, err := consensusblocks.NewBeaconBlock(pbb)
require.NoError(t, err)
require.NoError(t, service.handleBlockBLSToExecChanges(blk))
})
t.Run("Post Capella some changes", func(t *testing.T) {
idx := types.ValidatorIndex(123)
change := &ethpb.BLSToExecutionChange{
ValidatorIndex: idx,
}
signedChange := &ethpb.SignedBLSToExecutionChange{
Message: change,
}
body := &ethpb.BeaconBlockBodyCapella{
BlsToExecutionChanges: []*ethpb.SignedBLSToExecutionChange{signedChange},
}
pbb := &ethpb.BeaconBlockCapella{
Body: body,
}
blk, err := consensusblocks.NewBeaconBlock(pbb)
require.NoError(t, err)
pool.InsertBLSToExecChange(signedChange)
require.Equal(t, true, pool.ValidatorExists(idx))
require.NoError(t, service.handleBlockBLSToExecChanges(blk))
require.Equal(t, false, pool.ValidatorExists(idx))
})
}
// Helper function to simulate the block being on time or delayed for proposer
// boost. It alters the genesisTime tracked by the store.
func driftGenesisTime(s *Service, slot int64, delay int64) {

View File

@@ -10,6 +10,7 @@ import (
types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives"
"github.com/prysmaticlabs/prysm/v3/monitoring/tracing"
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v3/runtime/version"
"github.com/prysmaticlabs/prysm/v3/time"
"github.com/prysmaticlabs/prysm/v3/time/slots"
"go.opencensus.io/trace"
@@ -150,6 +151,11 @@ func (s *Service) handlePostBlockOperations(b interfaces.BeaconBlock) error {
s.cfg.ExitPool.MarkIncluded(e)
}
// Mark block BLS changes as seen so we don't include same ones in future blocks.
if err := s.handleBlockBLSToExecChanges(b); err != nil {
return errors.Wrap(err, "could not process BLSToExecutionChanges")
}
// Mark attester slashings as seen so we don't include same ones in future blocks.
for _, as := range b.Body().AttesterSlashings() {
s.cfg.SlashingPool.MarkIncludedAttesterSlashing(as)
@@ -157,6 +163,20 @@ func (s *Service) handlePostBlockOperations(b interfaces.BeaconBlock) error {
return nil
}
func (s *Service) handleBlockBLSToExecChanges(blk interfaces.BeaconBlock) error {
if blk.Version() < version.Capella {
return nil
}
changes, err := blk.Body().BLSToExecutionChanges()
if err != nil {
return errors.Wrap(err, "could not get BLSToExecutionChanges")
}
for _, change := range changes {
s.cfg.BLSToExecPool.MarkIncluded(change)
}
return nil
}
// This checks whether it's time to start saving hot state to DB.
// It's time when there's `epochsSinceFinalitySaveHotStateDB` epochs of non-finality.
func (s *Service) checkSaveHotStateDB(ctx context.Context) error {

View File

@@ -10,6 +10,7 @@ import (
testDB "github.com/prysmaticlabs/prysm/v3/beacon-chain/db/testing"
doublylinkedtree "github.com/prysmaticlabs/prysm/v3/beacon-chain/forkchoice/doubly-linked-tree"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/attestations"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/blstoexec"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/operations/voluntaryexits"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/state/stategen"
"github.com/prysmaticlabs/prysm/v3/config/params"
@@ -332,3 +333,62 @@ func TestCheckSaveHotStateDB_Overflow(t *testing.T) {
require.NoError(t, s.checkSaveHotStateDB(context.Background()))
assert.LogsDoNotContain(t, hook, "Entering mode to save hot states in DB")
}
func TestHandleBlockBLSToExecutionChanges(t *testing.T) {
ctx := context.Background()
beaconDB := testDB.SetupDB(t)
fc := doublylinkedtree.New()
pool := blstoexec.NewPool()
opts := []Option{
WithDatabase(beaconDB),
WithStateGen(stategen.New(beaconDB, fc)),
WithForkChoiceStore(fc),
WithStateNotifier(&blockchainTesting.MockStateNotifier{}),
WithBLSToExecPool(pool),
}
service, err := NewService(ctx, opts...)
require.NoError(t, err)
t.Run("pre Capella block", func(t *testing.T) {
body := &ethpb.BeaconBlockBodyBellatrix{}
pbb := &ethpb.BeaconBlockBellatrix{
Body: body,
}
blk, err := blocks.NewBeaconBlock(pbb)
require.NoError(t, err)
require.NoError(t, service.handleBlockBLSToExecChanges(blk))
})
t.Run("Post Capella no changes", func(t *testing.T) {
body := &ethpb.BeaconBlockBodyCapella{}
pbb := &ethpb.BeaconBlockCapella{
Body: body,
}
blk, err := blocks.NewBeaconBlock(pbb)
require.NoError(t, err)
require.NoError(t, service.handleBlockBLSToExecChanges(blk))
})
t.Run("Post Capella some changes", func(t *testing.T) {
idx := types.ValidatorIndex(123)
change := &ethpb.BLSToExecutionChange{
ValidatorIndex: idx,
}
signedChange := &ethpb.SignedBLSToExecutionChange{
Message: change,
}
body := &ethpb.BeaconBlockBodyCapella{
BlsToExecutionChanges: []*ethpb.SignedBLSToExecutionChange{signedChange},
}
pbb := &ethpb.BeaconBlockCapella{
Body: body,
}
blk, err := blocks.NewBeaconBlock(pbb)
require.NoError(t, err)
pool.InsertBLSToExecChange(signedChange)
require.Equal(t, true, pool.ValidatorExists(idx))
require.NoError(t, service.handleBlockBLSToExecChanges(blk))
require.Equal(t, false, pool.ValidatorExists(idx))
})
}

View File

@@ -131,3 +131,118 @@ func TestProcessVoluntaryExits_AppliesCorrectStatus(t *testing.T) {
helpers.ActivationExitEpoch(types.Epoch(state.Slot()/params.BeaconConfig().SlotsPerEpoch)), newRegistry[0].ExitEpoch)
}
}
func TestVerifyExitAndSignature(t *testing.T) {
type args struct {
currentSlot types.Slot
}
tests := []struct {
name string
args args
setup func() (*ethpb.Validator, *ethpb.SignedVoluntaryExit, *ethpb.Fork, []byte, error)
wantErr string
}{
{
name: "Empty Exit",
args: args{
currentSlot: 0,
},
setup: func() (*ethpb.Validator, *ethpb.SignedVoluntaryExit, *ethpb.Fork, []byte, error) {
fork := &ethpb.Fork{
PreviousVersion: params.BeaconConfig().GenesisForkVersion,
CurrentVersion: params.BeaconConfig().GenesisForkVersion,
Epoch: 0,
}
genesisRoot := [32]byte{'a'}
return &ethpb.Validator{}, &ethpb.SignedVoluntaryExit{}, fork, genesisRoot[:], nil
},
wantErr: "nil exit",
},
{
name: "Happy Path",
args: args{
currentSlot: (params.BeaconConfig().SlotsPerEpoch * 2) + 1,
},
setup: func() (*ethpb.Validator, *ethpb.SignedVoluntaryExit, *ethpb.Fork, []byte, error) {
fork := &ethpb.Fork{
PreviousVersion: params.BeaconConfig().GenesisForkVersion,
CurrentVersion: params.BeaconConfig().GenesisForkVersion,
Epoch: 0,
}
signedExit := &ethpb.SignedVoluntaryExit{
Exit: &ethpb.VoluntaryExit{
Epoch: 2,
ValidatorIndex: 0,
},
}
bs, keys := util.DeterministicGenesisState(t, 1)
validator := bs.Validators()[0]
validator.ActivationEpoch = 1
err := bs.UpdateValidatorAtIndex(0, validator)
require.NoError(t, err)
sb, err := signing.ComputeDomainAndSign(bs, signedExit.Exit.Epoch, signedExit.Exit, params.BeaconConfig().DomainVoluntaryExit, keys[0])
require.NoError(t, err)
sig, err := bls.SignatureFromBytes(sb)
require.NoError(t, err)
signedExit.Signature = sig.Marshal()
return validator, signedExit, fork, bs.GenesisValidatorsRoot(), nil
},
},
{
name: "bad signature",
args: args{
currentSlot: (params.BeaconConfig().SlotsPerEpoch * 2) + 1,
},
setup: func() (*ethpb.Validator, *ethpb.SignedVoluntaryExit, *ethpb.Fork, []byte, error) {
fork := &ethpb.Fork{
PreviousVersion: params.BeaconConfig().GenesisForkVersion,
CurrentVersion: params.BeaconConfig().GenesisForkVersion,
Epoch: 0,
}
signedExit := &ethpb.SignedVoluntaryExit{
Exit: &ethpb.VoluntaryExit{
Epoch: 2,
ValidatorIndex: 0,
},
}
bs, keys := util.DeterministicGenesisState(t, 1)
validator := bs.Validators()[0]
validator.ActivationEpoch = 1
sb, err := signing.ComputeDomainAndSign(bs, signedExit.Exit.Epoch, signedExit.Exit, params.BeaconConfig().DomainVoluntaryExit, keys[0])
require.NoError(t, err)
sig, err := bls.SignatureFromBytes(sb)
require.NoError(t, err)
signedExit.Signature = sig.Marshal()
genesisRoot := [32]byte{'a'}
// use wrong genesis root and don't update validator
return validator, signedExit, fork, genesisRoot[:], nil
},
wantErr: "signature did not verify",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := params.BeaconConfig().ShardCommitteePeriod
params.BeaconConfig().ShardCommitteePeriod = 0
validator, signedExit, fork, genesisRoot, err := tt.setup()
require.NoError(t, err)
rvalidator, err := state_native.NewValidator(validator)
require.NoError(t, err)
err = blocks.VerifyExitAndSignature(
rvalidator,
tt.args.currentSlot,
fork,
signedExit,
genesisRoot,
)
if tt.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, tt.wantErr, err)
}
params.BeaconConfig().ShardCommitteePeriod = c // prevent contamination
})
}
}

View File

@@ -18,7 +18,6 @@ go_library(
"//container/doubly-linked-list:go_default_library",
"//crypto/bls/blst:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
],
)

View File

@@ -17,7 +17,7 @@ func (m *PoolMock) PendingBLSToExecChanges() ([]*eth.SignedBLSToExecutionChange,
}
// BLSToExecChangesForInclusion --
func (m *PoolMock) BLSToExecChangesForInclusion(_ state.BeaconState) ([]*eth.SignedBLSToExecutionChange, error) {
func (m *PoolMock) BLSToExecChangesForInclusion(_ state.ReadOnlyBeaconState) ([]*eth.SignedBLSToExecutionChange, error) {
return m.Changes, nil
}
@@ -27,7 +27,7 @@ func (m *PoolMock) InsertBLSToExecChange(change *eth.SignedBLSToExecutionChange)
}
// MarkIncluded --
func (*PoolMock) MarkIncluded(_ *eth.SignedBLSToExecutionChange) error {
func (*PoolMock) MarkIncluded(_ *eth.SignedBLSToExecutionChange) {
panic("implement me")
}

View File

@@ -4,7 +4,6 @@ import (
"math"
"sync"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/core/blocks"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/state"
"github.com/prysmaticlabs/prysm/v3/config/params"
@@ -19,9 +18,9 @@ import (
// This pool is used by proposers to insert BLS-to-execution-change objects into new blocks.
type PoolManager interface {
PendingBLSToExecChanges() ([]*ethpb.SignedBLSToExecutionChange, error)
BLSToExecChangesForInclusion(state.BeaconState) ([]*ethpb.SignedBLSToExecutionChange, error)
BLSToExecChangesForInclusion(beaconState state.ReadOnlyBeaconState) ([]*ethpb.SignedBLSToExecutionChange, error)
InsertBLSToExecChange(change *ethpb.SignedBLSToExecutionChange)
MarkIncluded(change *ethpb.SignedBLSToExecutionChange) error
MarkIncluded(change *ethpb.SignedBLSToExecutionChange)
ValidatorExists(idx types.ValidatorIndex) bool
}
@@ -61,9 +60,9 @@ func (p *Pool) PendingBLSToExecChanges() ([]*ethpb.SignedBLSToExecutionChange, e
return result, nil
}
// BLSToExecChangesForInclusion returns objects that are ready for inclusion at the given slot.
// BLSToExecChangesForInclusion returns objects that are ready for inclusion.
// This method will not return more than the block enforced MaxBlsToExecutionChanges.
func (p *Pool) BLSToExecChangesForInclusion(st state.BeaconState) ([]*ethpb.SignedBLSToExecutionChange, error) {
func (p *Pool) BLSToExecChangesForInclusion(st state.ReadOnlyBeaconState) ([]*ethpb.SignedBLSToExecutionChange, error) {
p.lock.RLock()
length := int(math.Min(float64(params.BeaconConfig().MaxBlsToExecutionChanges), float64(p.pending.Len())))
result := make([]*ethpb.SignedBLSToExecutionChange, 0, length)
@@ -79,9 +78,7 @@ func (p *Pool) BLSToExecChangesForInclusion(st state.BeaconState) ([]*ethpb.Sign
logrus.WithError(err).Warning("removing invalid BLSToExecutionChange from pool")
// MarkIncluded removes the invalid change from the pool
p.lock.RUnlock()
if err := p.MarkIncluded(change); err != nil {
return nil, errors.Wrap(err, "could not mark BLSToExecutionChange as included")
}
p.MarkIncluded(change)
p.lock.RLock()
} else {
result = append(result, change)
@@ -118,9 +115,7 @@ func (p *Pool) BLSToExecChangesForInclusion(st state.BeaconState) ([]*ethpb.Sign
}
if !signature.Verify(cSet.PublicKeys[i], cSet.Messages[i][:]) {
logrus.Warning("removing BLSToExecutionChange with invalid signature from pool")
if err := p.MarkIncluded(result[i]); err != nil {
return nil, errors.Wrap(err, "could not mark BLSToExecutionChange as included")
}
p.MarkIncluded(result[i])
} else {
verified = append(verified, result[i])
}
@@ -143,19 +138,18 @@ func (p *Pool) InsertBLSToExecChange(change *ethpb.SignedBLSToExecutionChange) {
}
// MarkIncluded is used when an object has been included in a beacon block. Every block seen by this
// listNode should call this method to include the object. This will remove the object from the pool.
func (p *Pool) MarkIncluded(change *ethpb.SignedBLSToExecutionChange) error {
// node should call this method to include the object. This will remove the object from the pool.
func (p *Pool) MarkIncluded(change *ethpb.SignedBLSToExecutionChange) {
p.lock.Lock()
defer p.lock.Unlock()
node := p.m[change.Message.ValidatorIndex]
if node == nil {
return nil
return
}
delete(p.m, change.Message.ValidatorIndex)
p.pending.Remove(node)
return nil
}
// ValidatorExists checks if the bls to execution change object exists

View File

@@ -237,7 +237,7 @@ func TestMarkIncluded(t *testing.T) {
ValidatorIndex: types.ValidatorIndex(0),
}}
pool.InsertBLSToExecChange(change)
require.NoError(t, pool.MarkIncluded(change))
pool.MarkIncluded(change)
assert.Equal(t, 0, pool.pending.Len())
_, ok := pool.m[0]
assert.Equal(t, false, ok)
@@ -259,7 +259,7 @@ func TestMarkIncluded(t *testing.T) {
pool.InsertBLSToExecChange(first)
pool.InsertBLSToExecChange(second)
pool.InsertBLSToExecChange(third)
require.NoError(t, pool.MarkIncluded(first))
pool.MarkIncluded(first)
require.Equal(t, 2, pool.pending.Len())
_, ok := pool.m[0]
assert.Equal(t, false, ok)
@@ -281,7 +281,7 @@ func TestMarkIncluded(t *testing.T) {
pool.InsertBLSToExecChange(first)
pool.InsertBLSToExecChange(second)
pool.InsertBLSToExecChange(third)
require.NoError(t, pool.MarkIncluded(third))
pool.MarkIncluded(third)
require.Equal(t, 2, pool.pending.Len())
_, ok := pool.m[2]
assert.Equal(t, false, ok)
@@ -303,7 +303,7 @@ func TestMarkIncluded(t *testing.T) {
pool.InsertBLSToExecChange(first)
pool.InsertBLSToExecChange(second)
pool.InsertBLSToExecChange(third)
require.NoError(t, pool.MarkIncluded(second))
pool.MarkIncluded(second)
require.Equal(t, 2, pool.pending.Len())
_, ok := pool.m[1]
assert.Equal(t, false, ok)
@@ -324,7 +324,7 @@ func TestMarkIncluded(t *testing.T) {
}}
pool.InsertBLSToExecChange(first)
pool.InsertBLSToExecChange(second)
require.NoError(t, pool.MarkIncluded(change))
pool.MarkIncluded(change)
require.Equal(t, 2, pool.pending.Len())
_, ok := pool.m[0]
require.Equal(t, true, ok)
@@ -378,7 +378,7 @@ func TestValidatorExists(t *testing.T) {
ValidatorIndex: types.ValidatorIndex(0),
}}
pool.InsertBLSToExecChange(change)
require.NoError(t, pool.MarkIncluded(change))
pool.MarkIncluded(change)
assert.Equal(t, false, pool.ValidatorExists(0))
})
t.Run("multiple validators added to pool and removed", func(t *testing.T) {
@@ -399,8 +399,8 @@ func TestValidatorExists(t *testing.T) {
}}
pool.InsertBLSToExecChange(thirdChange)
assert.NoError(t, pool.MarkIncluded(firstChange))
assert.NoError(t, pool.MarkIncluded(thirdChange))
pool.MarkIncluded(firstChange)
pool.MarkIncluded(thirdChange)
assert.Equal(t, false, pool.ValidatorExists(0))
assert.Equal(t, true, pool.ValidatorExists(10))

View File

@@ -166,6 +166,7 @@ func (_ *BeaconEndpointFactory) Create(path string) (*apimiddleware.Endpoint, er
case "/eth/v1/beacon/pool/bls_to_execution_changes":
endpoint.PostRequest = &SubmitBLSToExecutionChangesRequest{}
endpoint.GetResponse = &BLSToExecutionChangesPoolResponseJson{}
endpoint.Err = &IndexedVerificationFailureErrorJson{}
endpoint.Hooks = apimiddleware.HookCollection{
OnPreDeserializeRequestBodyIntoContainer: wrapBLSChangesArray,
}

View File

@@ -3,6 +3,7 @@ package stategen
import (
"context"
"fmt"
"runtime/debug"
"time"
"github.com/pkg/errors"
@@ -41,7 +42,8 @@ func (_ *State) replayBlocks(
"endSlot": targetSlot,
"diff": targetSlot - state.Slot(),
})
log.Debug("Replaying state")
log.Debugf("Replaying state at %s", debug.Stack())
// The input block list is sorted in decreasing slots order.
if len(signed) > 0 {
for i := len(signed) - 1; i >= 0; i-- {

View File

@@ -8,7 +8,6 @@ import (
"github.com/prysmaticlabs/prysm/v3/beacon-chain/core/blocks"
"github.com/prysmaticlabs/prysm/v3/monitoring/tracing"
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v3/runtime/version"
"go.opencensus.io/trace"
)
@@ -46,11 +45,6 @@ func (s *Service) validateBlsToExecutionChange(ctx context.Context, pid peer.ID,
if err != nil {
return pubsub.ValidationIgnore, err
}
// Ignore messages if our current head state doesn't support
// capella.
if st.Version() < version.Capella {
return pubsub.ValidationIgnore, nil
}
// Validate that the execution change object is valid.
_, err = blocks.ValidateBLSToExecutionChange(st, blsChange)
if err != nil {

View File

@@ -148,7 +148,7 @@ func TestService_ValidateBlsToExecutionChange(t *testing.T) {
want: pubsub.ValidationIgnore,
},
{
name: "Non-capella Head state",
name: "Non-Capella HeadState Valid Execution Change Message",
svc: NewService(context.Background(),
WithP2P(mockp2p.NewTestP2P(t)),
WithInitialSync(&mockSync.Sync{IsSyncing: false}),
@@ -161,13 +161,23 @@ func TestService_ValidateBlsToExecutionChange(t *testing.T) {
s.cfg.stateGen = stategen.New(beaconDB, doublylinkedtree.New())
s.cfg.beaconDB = beaconDB
s.initCaches()
st, _ := util.DeterministicGenesisStateBellatrix(t, 128)
st, keys := util.DeterministicGenesisStateBellatrix(t, 128)
s.cfg.chain = &mockChain.ChainService{
ValidatorsRoot: [32]byte{'A'},
Genesis: time.Now().Add(-time.Second * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Duration(10)),
State: st,
}
msg.Message.ValidatorIndex = 50
// Provide invalid withdrawal key for validator
msg.Message.FromBlsPubkey = keys[51].PublicKey().Marshal()
msg.Message.ToExecutionAddress = wantedExecAddress
epoch := slots.ToEpoch(st.Slot())
domain, err := signing.Domain(st.Fork(), epoch, params.BeaconConfig().DomainBLSToExecutionChange, st.GenesisValidatorsRoot())
assert.NoError(t, err)
htr, err := signing.SigningData(msg.Message.HashTreeRoot, domain)
assert.NoError(t, err)
msg.Signature = keys[51].Sign(htr[:]).Marshal()
return s, topic
},
args: args{
@@ -182,7 +192,7 @@ func TestService_ValidateBlsToExecutionChange(t *testing.T) {
},
Signature: emptySig[:],
}},
want: pubsub.ValidationIgnore,
want: pubsub.ValidationAccept,
},
{
name: "Non-existent Validator Index",

View File

@@ -15,8 +15,8 @@ go_library(
"//cmd/prysmctl/db:go_default_library",
"//cmd/prysmctl/deprecated:go_default_library",
"//cmd/prysmctl/p2p:go_default_library",
"//cmd/prysmctl/signing:go_default_library",
"//cmd/prysmctl/testnet:go_default_library",
"//cmd/prysmctl/validator:go_default_library",
"//cmd/prysmctl/weaksubjectivity:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",

View File

@@ -7,8 +7,8 @@ import (
"github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/db"
"github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/deprecated"
"github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/p2p"
"github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/signing"
"github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/testnet"
"github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/validator"
"github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/weaksubjectivity"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
@@ -36,5 +36,5 @@ func init() {
prysmctlCommands = append(prysmctlCommands, p2p.Commands...)
prysmctlCommands = append(prysmctlCommands, testnet.Commands...)
prysmctlCommands = append(prysmctlCommands, weaksubjectivity.Commands...)
prysmctlCommands = append(prysmctlCommands, signing.Commands...)
prysmctlCommands = append(prysmctlCommands, validator.Commands...)
}

View File

@@ -1,17 +0,0 @@
load("@prysm//tools/go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["cmd.go"],
importpath = "github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/signing",
visibility = ["//visibility:public"],
deps = [
"//cmd:go_default_library",
"//cmd/validator/accounts:go_default_library",
"//cmd/validator/flags:go_default_library",
"//config/features:go_default_library",
"//runtime/tos:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
],
)

View File

@@ -1,64 +0,0 @@
package signing
import (
"os"
"github.com/prysmaticlabs/prysm/v3/cmd"
"github.com/prysmaticlabs/prysm/v3/cmd/validator/accounts"
"github.com/prysmaticlabs/prysm/v3/cmd/validator/flags"
"github.com/prysmaticlabs/prysm/v3/config/features"
"github.com/prysmaticlabs/prysm/v3/runtime/tos"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var Commands = []*cli.Command{
{
Name: "sign",
Usage: "signs a message and broadcasts it to the network through the beacon node",
Subcommands: []*cli.Command{
{
Name: "voluntary-exit",
Description: "Performs a voluntary exit on selected accounts",
Flags: cmd.WrapFlags([]cli.Flag{
flags.WalletDirFlag,
flags.WalletPasswordFileFlag,
flags.AccountPasswordFileFlag,
flags.VoluntaryExitPublicKeysFlag,
flags.BeaconRPCProviderFlag,
flags.Web3SignerURLFlag,
flags.Web3SignerPublicValidatorKeysFlag,
flags.InteropNumValidators,
flags.InteropStartIndex,
cmd.GrpcMaxCallRecvMsgSizeFlag,
flags.CertFlag,
flags.GrpcHeadersFlag,
flags.GrpcRetriesFlag,
flags.GrpcRetryDelayFlag,
flags.ExitAllFlag,
flags.ForceExitFlag,
features.Mainnet,
features.PraterTestnet,
features.RopstenTestnet,
features.SepoliaTestnet,
cmd.AcceptTosFlag,
}),
Before: func(cliCtx *cli.Context) error {
if err := cmd.LoadFlagsFromConfig(cliCtx, cliCtx.Command.Flags); err != nil {
return err
}
if err := tos.VerifyTosAcceptedOrPrompt(cliCtx); err != nil {
return err
}
return features.ConfigureValidator(cliCtx)
},
Action: func(cliCtx *cli.Context) error {
if err := accounts.AccountsExit(cliCtx, os.Stdin); err != nil {
log.WithError(err).Fatal("Could not perform voluntary exit")
}
return nil
},
},
},
},
}

View File

@@ -0,0 +1,41 @@
load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"cmd.go",
"withdraw.go",
],
importpath = "github.com/prysmaticlabs/prysm/v3/cmd/prysmctl/validator",
visibility = ["//visibility:public"],
deps = [
"//api/client/beacon:go_default_library",
"//beacon-chain/rpc/apimiddleware:go_default_library",
"//cmd:go_default_library",
"//cmd/validator/accounts:go_default_library",
"//cmd/validator/flags:go_default_library",
"//config/features:go_default_library",
"//config/fieldparams:go_default_library",
"//runtime/tos:go_default_library",
"@com_github_ethereum_go_ethereum//common:go_default_library",
"@com_github_logrusorgru_aurora//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
"@io_opencensus_go//trace:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["withdraw_test.go"],
data = glob(["testdata/**"]),
embed = [":go_default_library"],
deps = [
"//beacon-chain/rpc/apimiddleware:go_default_library",
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"@com_github_sirupsen_logrus//hooks/test:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
],
)

View File

@@ -0,0 +1,137 @@
package validator
import (
"fmt"
"os"
"github.com/logrusorgru/aurora"
"github.com/prysmaticlabs/prysm/v3/cmd"
"github.com/prysmaticlabs/prysm/v3/cmd/validator/accounts"
"github.com/prysmaticlabs/prysm/v3/cmd/validator/flags"
"github.com/prysmaticlabs/prysm/v3/config/features"
"github.com/prysmaticlabs/prysm/v3/runtime/tos"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var (
BeaconHostFlag = &cli.StringFlag{
Name: "beacon-node-host",
Usage: "host:port for beacon node to query",
Value: "127.0.0.1:3500",
}
PathFlag = &cli.StringFlag{
Name: "path",
Aliases: []string{"p"},
Usage: "path to the signed withdrawal messages JSON",
}
ConfirmFlag = &cli.BoolFlag{
Name: "confirm",
Aliases: []string{"c"},
Usage: "WARNING: User confirms and accepts responsibility of all input data provided and actions for setting their withdrawal address for their validator key. " +
"This action is not reversible and withdrawal addresses can not be changed once set.",
}
VerifyOnlyFlag = &cli.BoolFlag{
Name: "verify-only",
Aliases: []string{"vo"},
Usage: "overrides withdrawal command to only verify whether requests are in the pool and does not submit withdrawal requests",
}
)
var Commands = []*cli.Command{
{
Name: "validator",
Aliases: []string{"v", "sign"}, // remove sign command should be depreciated but having as backwards compatability.
Usage: "commands that affect the state of validators such as exiting or withdrawing",
Subcommands: []*cli.Command{
{
Name: "withdraw",
Aliases: []string{"w"},
Usage: "Assign Ethereum withdrawal addresses to validator keys. WARNING: once set values are included they can no longer be updated.",
Flags: []cli.Flag{
BeaconHostFlag,
PathFlag,
ConfirmFlag,
VerifyOnlyFlag,
cmd.ConfigFileFlag,
cmd.AcceptTosFlag,
},
Before: func(cliCtx *cli.Context) error {
if err := cmd.LoadFlagsFromConfig(cliCtx, cliCtx.Command.Flags); err != nil {
return err
}
au := aurora.NewAurora(true)
if !cliCtx.Bool(cmd.AcceptTosFlag.Name) || !cliCtx.Bool(ConfirmFlag.Name) {
fmt.Println(au.Red("===============IMPORTANT==============="))
fmt.Println(au.Red("Please read the following carefully"))
fmt.Print("This action will allow the partial withdrawal of amounts over the 32 staked ETH in your active validator balance. \n" +
"You will also be entitled to the full withdrawal of the entire validator balance if your validator has exited. \n" +
"Please navigate to our website (https://docs.prylabs.network/) and make sure you understand the full implications of setting your withdrawal address. \n")
fmt.Println(au.Red("THIS ACTION WILL NOT BE REVERSIBLE ONCE INCLUDED. "))
fmt.Println(au.Red("You will NOT be able to change the address again once changed. "))
return fmt.Errorf("both the `--%s` and `--%s` flags are required to run this command. \n"+
"By providing these flags the user has read and accepts the TERMS AND CONDITIONS: https://github.com/prysmaticlabs/prysm/blob/master/TERMS_OF_SERVICE.md "+
"and confirms the action of setting withdrawals addresses", cmd.AcceptTosFlag.Name, ConfirmFlag.Name)
} else {
return nil
}
},
Action: func(cliCtx *cli.Context) error {
if cliCtx.Bool(VerifyOnlyFlag.Name) {
if err := verifyWithdrawalsInPool(cliCtx); err != nil {
log.WithError(err).Fatal("Could not verify withdrawal addresses")
}
} else {
if err := setWithdrawalAddresses(cliCtx); err != nil {
log.WithError(err).Fatal("Could not set withdrawal addresses")
}
}
return nil
},
},
{
Name: "exit",
Aliases: []string{"e", "voluntary-exit"},
Usage: "Performs a voluntary exit on selected accounts",
Flags: cmd.WrapFlags([]cli.Flag{
flags.WalletDirFlag,
flags.WalletPasswordFileFlag,
flags.AccountPasswordFileFlag,
flags.VoluntaryExitPublicKeysFlag,
flags.BeaconRPCProviderFlag,
flags.Web3SignerURLFlag,
flags.Web3SignerPublicValidatorKeysFlag,
flags.InteropNumValidators,
flags.InteropStartIndex,
cmd.GrpcMaxCallRecvMsgSizeFlag,
flags.CertFlag,
flags.GrpcHeadersFlag,
flags.GrpcRetriesFlag,
flags.GrpcRetryDelayFlag,
flags.ExitAllFlag,
flags.ForceExitFlag,
features.Mainnet,
features.PraterTestnet,
features.RopstenTestnet,
features.SepoliaTestnet,
cmd.AcceptTosFlag,
}),
Before: func(cliCtx *cli.Context) error {
if err := cmd.LoadFlagsFromConfig(cliCtx, cliCtx.Command.Flags); err != nil {
return err
}
if err := tos.VerifyTosAcceptedOrPrompt(cliCtx); err != nil {
return err
}
return features.ConfigureValidator(cliCtx)
},
Action: func(cliCtx *cli.Context) error {
if err := accounts.AccountsExit(cliCtx, os.Stdin); err != nil {
log.WithError(err).Fatal("Could not perform voluntary exit")
}
return nil
},
},
},
},
}

View File

@@ -0,0 +1 @@
[{"message":{"validator_index":"0","from_bls_pubkey":"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","to_execution_address":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"0xb6e640f0fc58e9f22585dbf434b6a0e8fc36b98e2f2a963e158716cfc84034141289f7898027de1ec56754937f1a837e01c7b066a6a56af7a379f8aec823d050788a5ecc799e9bc39f73d45b7c389c961cbaace61823e4c7bf2f93bd06c03127"},{"message":{"validator_index":"1","from_bls_pubkey":"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","to_execution_address":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"0xa97103e15d3dbdaa75fb15cea782e4a11329eea77d155864ec682d7907b3b70c7771960bef7be1b1c4e08fe735888b950c1a22053f6049b35736f48e6dd018392efa3896c9e427ea4e100e86e9131b5ea2673388a4bf188407a630ba405b7dc5"}]

View File

@@ -0,0 +1 @@
[{"message":{"validator_index":"3","from_bls_pubkey":"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","to_execution_address":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"0xb6e640f0fc58e9f22585dbf434b6a0e8fc36b98e2f2a963e158716cfc84034141289f7898027de1ec56754937f1a837e01c7b066a6a56af7a379f8aec823d050788a5ecc799e9bc39f73d45b7c389c961cbaace61823e4c7bf2f93bd06c03127"},{"message":{"validator_index":"5","from_bls_pubkey":"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","to_execution_address":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"0xa97103e15d3dbdaa75fb15cea782e4a11329eea77d155864ec682d7907b3b70c7771960bef7be1b1c4e08fe735888b950c1a22053f6049b35736f48e6dd018392efa3896c9e427ea4e100e86e9131b5ea2673388a4bf188407a630ba405b7dc5"}]

View File

@@ -0,0 +1 @@
[{"message":{"validator_index":"1","from_bls_pubkey":"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","to_execution_address":"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"0xa97103e15d3dbdaa75fb15cea782e4a11329eea77d155864ec682d7907b3b70c7771960bef7be1b1c4e08fe735888b950c1a22053f6049b35736f48e6dd018392efa3896c9e427ea4e100e86e9131b5ea2673388a4bf188407a630ba405b7dc5"}]

View File

@@ -0,0 +1 @@
[{"message":{"validator_index":"0","from_bls_pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","to_execution_address":"a94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"b6e640f0fc58e9f22585dbf434b6a0e8fc36b98e2f2a963e158716cfc84034141289f7898027de1ec56754937f1a837e01c7b066a6a56af7a379f8aec823d050788a5ecc799e9bc39f73d45b7c389c961cbaace61823e4c7bf2f93bd06c03127", "metadata":{ "network_name": "mainnet", "genesis_validators_root": "4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95", "deposit_cli_version": "2.4.0"}},{"message":{"validator_index":"1","from_bls_pubkey":"b89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","to_execution_address":"a94f5374fce5edbc8e2a8697c15331677e6ebf0b"},"signature":"a97103e15d3dbdaa75fb15cea782e4a11329eea77d155864ec682d7907b3b70c7771960bef7be1b1c4e08fe735888b950c1a22053f6049b35736f48e6dd018392efa3896c9e427ea4e100e86e9131b5ea2673388a4bf188407a630ba405b7dc5", "metadata":{ "network_name": "mainnet", "genesis_validators_root": "4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95", "deposit_cli_version": "2.4.0"}}]

View File

@@ -0,0 +1,171 @@
package validator
import (
"context"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/logrusorgru/aurora"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v3/api/client/beacon"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
fieldparams "github.com/prysmaticlabs/prysm/v3/config/fieldparams"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"go.opencensus.io/trace"
)
func setWithdrawalAddresses(c *cli.Context) error {
ctx, span := trace.StartSpan(c.Context, "withdrawal.setWithdrawalAddresses")
defer span.End()
au := aurora.NewAurora(true)
beaconNodeHost := c.String(BeaconHostFlag.Name)
if !c.IsSet(PathFlag.Name) {
return fmt.Errorf("no --%s flag value was provided", PathFlag.Name)
}
setWithdrawalAddressJsons, err := getWithdrawalMessagesFromPathFlag(c)
if err != nil {
return err
}
for _, request := range setWithdrawalAddressJsons {
fmt.Println("SETTING VALIDATOR INDEX " + au.Red(request.Message.ValidatorIndex).String() + " TO WITHDRAWAL ADDRESS " + au.Red(request.Message.ToExecutionAddress).String())
}
return callWithdrawalEndpoints(ctx, beaconNodeHost, setWithdrawalAddressJsons)
}
func getWithdrawalMessagesFromPathFlag(c *cli.Context) ([]*apimiddleware.SignedBLSToExecutionChangeJson, error) {
setWithdrawalAddressJsons := make([]*apimiddleware.SignedBLSToExecutionChangeJson, 0)
foundFilePaths, err := findWithdrawalFiles(c.String(PathFlag.Name))
if err != nil {
return setWithdrawalAddressJsons, errors.Wrap(err, "failed to find withdrawal files")
}
for _, foundFilePath := range foundFilePaths {
b, err := os.ReadFile(filepath.Clean(foundFilePath))
if err != nil {
return setWithdrawalAddressJsons, errors.Wrap(err, "failed to open file")
}
var to []*apimiddleware.SignedBLSToExecutionChangeJson
if err := json.Unmarshal(b, &to); err != nil {
log.Warnf("provided file: %s, is not a list of signed withdrawal messages", foundFilePath)
continue
}
// verify 0x from file and add if needed
for i, obj := range to {
if len(obj.Message.FromBLSPubkey) == fieldparams.BLSPubkeyLength*2 {
to[i].Message.FromBLSPubkey = fmt.Sprintf("0x%s", obj.Message.FromBLSPubkey)
}
if len(obj.Message.ToExecutionAddress) == common.AddressLength*2 {
to[i].Message.ToExecutionAddress = fmt.Sprintf("0x%s", obj.Message.ToExecutionAddress)
}
if len(obj.Signature) == fieldparams.BLSSignatureLength*2 {
to[i].Signature = fmt.Sprintf("0x%s", obj.Signature)
}
setWithdrawalAddressJsons = append(setWithdrawalAddressJsons, &apimiddleware.SignedBLSToExecutionChangeJson{
Message: &apimiddleware.BLSToExecutionChangeJson{
ValidatorIndex: to[i].Message.ValidatorIndex,
FromBLSPubkey: to[i].Message.FromBLSPubkey,
ToExecutionAddress: to[i].Message.ToExecutionAddress,
},
Signature: to[i].Signature,
})
}
}
if len(setWithdrawalAddressJsons) == 0 {
return setWithdrawalAddressJsons, errors.New("the list of signed requests is empty")
}
return setWithdrawalAddressJsons, nil
}
func callWithdrawalEndpoints(ctx context.Context, host string, request []*apimiddleware.SignedBLSToExecutionChangeJson) error {
client, err := beacon.NewClient(host)
if err != nil {
return err
}
if err := client.SubmitChangeBLStoExecution(ctx, request); err != nil {
return err
}
log.Infof("Successfully published messages to update %d withdrawal addresses.", len(request))
return checkIfWithdrawsAreInPool(ctx, client, request)
}
func checkIfWithdrawsAreInPool(ctx context.Context, client *beacon.Client, request []*apimiddleware.SignedBLSToExecutionChangeJson) error {
log.Info("Verifying requested withdrawal messages known to node...")
poolResponse, err := client.GetBLStoExecutionChanges(ctx)
if err != nil {
return err
}
requestMap := make(map[string]string)
for _, w := range request {
requestMap[w.Message.ValidatorIndex] = w.Message.ToExecutionAddress
}
totalMessages := len(requestMap)
for _, resp := range poolResponse.Data {
value, found := requestMap[resp.Message.ValidatorIndex]
if found && value == resp.Message.ToExecutionAddress {
delete(requestMap, resp.Message.ValidatorIndex)
}
}
if len(requestMap) != 0 {
for key, address := range requestMap {
log.WithFields(log.Fields{
"validator_index": key,
"execution_address:": address,
}).Warn("Set withdrawal address message not found in the node's operations pool.")
}
log.Warn("Please check before resubmitting. Set withdrawal address messages that were not found in the pool may have been already included into a block.")
} else {
log.Infof("All (total:%d) signed withdrawal messages were found in the pool.", totalMessages)
}
return nil
}
func findWithdrawalFiles(path string) ([]string, error) {
var foundpaths []string
maxdepth := 3
cleanpath := filepath.Clean(path)
if err := filepath.WalkDir(cleanpath, func(s string, d fs.DirEntry, e error) error {
if e != nil {
return e
}
if d.IsDir() && strings.Count(cleanpath, string(os.PathSeparator)) > maxdepth {
return fs.SkipDir
}
if filepath.Ext(d.Name()) == ".json" {
foundpaths = append(foundpaths, s)
}
return nil
}); err != nil {
return nil, errors.Wrap(err, "unable to find compatible files")
}
if len(foundpaths) == 0 {
return nil, errors.New("no compatible files were found")
}
log.Infof("found JSON files for setting withdrawals: %v", foundpaths)
return foundpaths, nil
}
func verifyWithdrawalsInPool(c *cli.Context) error {
ctx, span := trace.StartSpan(c.Context, "withdrawal.verifyWithdrawalsInPool")
defer span.End()
beaconNodeHost := c.String(BeaconHostFlag.Name)
if !c.IsSet(PathFlag.Name) {
return fmt.Errorf("no --%s flag value was provided", PathFlag.Name)
}
client, err := beacon.NewClient(beaconNodeHost)
if err != nil {
return err
}
request, err := getWithdrawalMessagesFromPathFlag(c)
if err != nil {
return err
}
return checkIfWithdrawsAreInPool(ctx, client, request)
}

View File

@@ -0,0 +1,305 @@
package validator
import (
"encoding/json"
"flag"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
"github.com/prysmaticlabs/prysm/v3/testing/assert"
"github.com/prysmaticlabs/prysm/v3/testing/require"
logtest "github.com/sirupsen/logrus/hooks/test"
"github.com/urfave/cli/v2"
)
func TestCallWithdrawalEndpoint(t *testing.T) {
file := "./testdata/change-operations.json"
baseurl := "127.0.0.1:3500"
l, err := net.Listen("tcp", baseurl)
require.NoError(t, err)
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
b, err := os.ReadFile(filepath.Clean(file))
require.NoError(t, err)
var to []*apimiddleware.SignedBLSToExecutionChangeJson
err = json.Unmarshal(b, &to)
require.NoError(t, err)
err = json.NewEncoder(w).Encode(&apimiddleware.BLSToExecutionChangesPoolResponseJson{
Data: to,
})
require.NoError(t, err)
}
}))
err = srv.Listener.Close()
require.NoError(t, err)
srv.Listener = l
srv.Start()
defer srv.Close()
hook := logtest.NewGlobal()
app := cli.App{}
set := flag.NewFlagSet("test", 0)
set.String("beacon-node-host", baseurl, "")
set.String("path", file, "")
set.Bool("confirm", true, "")
set.Bool("accept-terms-of-use", true, "")
assert.NoError(t, set.Set("beacon-node-host", baseurl))
assert.NoError(t, set.Set("path", file))
cliCtx := cli.NewContext(&app, set, nil)
err = setWithdrawalAddresses(cliCtx)
require.NoError(t, err)
assert.LogsContain(t, hook, "Successfully published")
}
func TestCallWithdrawalEndpoint_Mutiple(t *testing.T) {
file := "./testdata/change-operations-multiple.json"
baseurl := "127.0.0.1:3500"
l, err := net.Listen("tcp", baseurl)
require.NoError(t, err)
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
b, err := os.ReadFile(filepath.Clean(file))
require.NoError(t, err)
var to []*apimiddleware.SignedBLSToExecutionChangeJson
err = json.Unmarshal(b, &to)
require.NoError(t, err)
err = json.NewEncoder(w).Encode(&apimiddleware.BLSToExecutionChangesPoolResponseJson{
Data: to,
})
require.NoError(t, err)
}
}))
err = srv.Listener.Close()
require.NoError(t, err)
srv.Listener = l
srv.Start()
defer srv.Close()
hook := logtest.NewGlobal()
app := cli.App{}
set := flag.NewFlagSet("test", 0)
set.String("beacon-node-host", baseurl, "")
set.String("path", file, "")
set.Bool("confirm", true, "")
set.Bool("accept-terms-of-use", true, "")
assert.NoError(t, set.Set("beacon-node-host", baseurl))
assert.NoError(t, set.Set("path", file))
cliCtx := cli.NewContext(&app, set, nil)
err = setWithdrawalAddresses(cliCtx)
require.NoError(t, err)
assert.LogsContain(t, hook, "Successfully published")
assert.LogsContain(t, hook, "to update 2 withdrawal")
assert.LogsContain(t, hook, "All (total:2) signed withdrawal messages were found in the pool.")
assert.LogsDoNotContain(t, hook, "Set withdrawal address message not found in the node's operations pool.")
}
func TestCallWithdrawalEndpoint_Mutiple_stakingcli(t *testing.T) {
stakingcliFile := "./testdata/staking-cli-change-operations-multiple.json"
file := "./testdata/change-operations-multiple.json"
baseurl := "127.0.0.1:3500"
l, err := net.Listen("tcp", baseurl)
require.NoError(t, err)
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
b, err := os.ReadFile(filepath.Clean(file))
require.NoError(t, err)
var to []*apimiddleware.SignedBLSToExecutionChangeJson
err = json.Unmarshal(b, &to)
require.NoError(t, err)
err = json.NewEncoder(w).Encode(&apimiddleware.BLSToExecutionChangesPoolResponseJson{
Data: to,
})
require.NoError(t, err)
}
}))
err = srv.Listener.Close()
require.NoError(t, err)
srv.Listener = l
srv.Start()
defer srv.Close()
hook := logtest.NewGlobal()
app := cli.App{}
set := flag.NewFlagSet("test", 0)
set.String("beacon-node-host", baseurl, "")
set.String("path", stakingcliFile, "")
set.Bool("confirm", true, "")
set.Bool("accept-terms-of-use", true, "")
assert.NoError(t, set.Set("beacon-node-host", baseurl))
assert.NoError(t, set.Set("path", stakingcliFile))
cliCtx := cli.NewContext(&app, set, nil)
err = setWithdrawalAddresses(cliCtx)
require.NoError(t, err)
assert.LogsContain(t, hook, "Successfully published")
assert.LogsContain(t, hook, "to update 2 withdrawal")
assert.LogsContain(t, hook, "All (total:2) signed withdrawal messages were found in the pool.")
assert.LogsDoNotContain(t, hook, "Set withdrawal address message not found in the node's operations pool.")
}
func TestCallWithdrawalEndpoint_Mutiple_notfound(t *testing.T) {
respFile := "./testdata/change-operations-multiple_notfound.json"
file := "./testdata/change-operations-multiple.json"
baseurl := "127.0.0.1:3500"
l, err := net.Listen("tcp", baseurl)
require.NoError(t, err)
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
b, err := os.ReadFile(filepath.Clean(respFile))
require.NoError(t, err)
var to []*apimiddleware.SignedBLSToExecutionChangeJson
err = json.Unmarshal(b, &to)
require.NoError(t, err)
err = json.NewEncoder(w).Encode(&apimiddleware.BLSToExecutionChangesPoolResponseJson{
Data: to,
})
require.NoError(t, err)
}
}))
err = srv.Listener.Close()
require.NoError(t, err)
srv.Listener = l
srv.Start()
defer srv.Close()
hook := logtest.NewGlobal()
app := cli.App{}
set := flag.NewFlagSet("test", 0)
set.String("beacon-node-host", baseurl, "")
set.String("path", file, "")
set.Bool("confirm", true, "")
set.Bool("accept-terms-of-use", true, "")
assert.NoError(t, set.Set("beacon-node-host", baseurl))
assert.NoError(t, set.Set("path", file))
cliCtx := cli.NewContext(&app, set, nil)
err = setWithdrawalAddresses(cliCtx)
require.NoError(t, err)
assert.LogsContain(t, hook, "Successfully published")
assert.LogsContain(t, hook, "to update 2 withdrawal")
assert.LogsContain(t, hook, "Set withdrawal address message not found in the node's operations pool.")
assert.LogsContain(t, hook, "Please check before resubmitting. Set withdrawal address messages that were not found in the pool may have been already included into a block.")
assert.LogsDoNotContain(t, hook, "Set withdrawal address message found in the node's operations pool.")
}
func TestCallWithdrawalEndpoint_Empty(t *testing.T) {
baseurl := "127.0.0.1:3500"
content := []byte("[]")
tmpfile, err := os.CreateTemp("./testdata", "*.json")
require.NoError(t, err)
_, err = tmpfile.Write(content)
require.NoError(t, err)
defer func() {
err := os.Remove(tmpfile.Name())
require.NoError(t, err)
}()
app := cli.App{}
set := flag.NewFlagSet("test", 0)
set.String("beacon-node-host", baseurl, "")
set.String("path", tmpfile.Name(), "")
set.Bool("confirm", true, "")
set.Bool("accept-terms-of-use", true, "")
assert.NoError(t, set.Set("beacon-node-host", baseurl))
assert.NoError(t, set.Set("path", tmpfile.Name()))
cliCtx := cli.NewContext(&app, set, nil)
err = setWithdrawalAddresses(cliCtx)
assert.ErrorContains(t, "the list of signed requests is empty", err)
}
func TestCallWithdrawalEndpoint_Errors(t *testing.T) {
file := "./testdata/change-operations.json"
baseurl := "127.0.0.1:3500"
l, err := net.Listen("tcp", baseurl)
require.NoError(t, err)
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(400)
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(&apimiddleware.IndexedVerificationFailureErrorJson{
Failures: []*apimiddleware.SingleIndexedVerificationFailureJson{
{Index: 0, Message: "Could not validate SignedBLSToExecutionChange"},
},
})
require.NoError(t, err)
}))
err = srv.Listener.Close()
require.NoError(t, err)
srv.Listener = l
srv.Start()
defer srv.Close()
hook := logtest.NewGlobal()
app := cli.App{}
set := flag.NewFlagSet("test", 0)
set.String("beacon-node-host", baseurl, "")
set.String("path", file, "")
set.Bool("confirm", true, "")
set.Bool("accept-terms-of-use", true, "")
assert.NoError(t, set.Set("beacon-node-host", baseurl))
assert.NoError(t, set.Set("path", file))
cliCtx := cli.NewContext(&app, set, nil)
err = setWithdrawalAddresses(cliCtx)
assert.ErrorContains(t, "POST error", err)
assert.LogsContain(t, hook, "Could not validate SignedBLSToExecutionChange")
}
func TestVerifyWithdrawal_Mutiple(t *testing.T) {
file := "./testdata/change-operations-multiple.json"
baseurl := "127.0.0.1:3500"
l, err := net.Listen("tcp", baseurl)
require.NoError(t, err)
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
b, err := os.ReadFile(filepath.Clean(file))
require.NoError(t, err)
var to []*apimiddleware.SignedBLSToExecutionChangeJson
err = json.Unmarshal(b, &to)
require.NoError(t, err)
err = json.NewEncoder(w).Encode(&apimiddleware.BLSToExecutionChangesPoolResponseJson{
Data: to,
})
require.NoError(t, err)
}
}))
err = srv.Listener.Close()
require.NoError(t, err)
srv.Listener = l
srv.Start()
defer srv.Close()
hook := logtest.NewGlobal()
app := cli.App{}
set := flag.NewFlagSet("test", 0)
set.String("beacon-node-host", baseurl, "")
set.String("path", file, "")
set.Bool("confirm", true, "")
set.Bool("accept-terms-of-use", true, "")
set.Bool("verify-only", true, "")
assert.NoError(t, set.Set("beacon-node-host", baseurl))
assert.NoError(t, set.Set("path", file))
cliCtx := cli.NewContext(&app, set, nil)
err = verifyWithdrawalsInPool(cliCtx)
require.NoError(t, err)
assert.LogsContain(t, hook, "All (total:2) signed withdrawal messages were found in the pool.")
assert.LogsDoNotContain(t, hook, "set withdrawal address message not found in the node's operations pool.")
}

View File

@@ -186,7 +186,7 @@ var Commands = &cli.Command{
return features.ConfigureValidator(cliCtx)
},
Action: func(cliCtx *cli.Context) error {
log.Info("This command will be deprecated in the future in favor of `prysmctl sign validator-exit`")
log.Info("This command will be deprecated in the future in favor of `prysmctl validator exit`")
if err := AccountsExit(cliCtx, os.Stdin); err != nil {
log.WithError(err).Fatal("Could not perform voluntary exit")
}

View File

@@ -42,12 +42,7 @@ func NewBeaconApiValidatorClientWithFallback(host string, timeout time.Duration,
}
func (c *beaconApiValidatorClient) GetDuties(ctx context.Context, in *ethpb.DutiesRequest) (*ethpb.DutiesResponse, error) {
if c.fallbackClient != nil {
return c.fallbackClient.GetDuties(ctx, in)
}
// TODO: Implement me
panic("beaconApiValidatorClient.GetDuties is not implemented. To use a fallback client, create this validator with NewBeaconApiValidatorClientWithFallback instead.")
return c.getDuties(ctx, in)
}
func (c *beaconApiValidatorClient) CheckDoppelGanger(ctx context.Context, in *ethpb.DoppelGangerRequest) (*ethpb.DoppelGangerResponse, error) {

View File

@@ -5,21 +5,224 @@ import (
"context"
"encoding/json"
"fmt"
"net/url"
"strconv"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
"github.com/prysmaticlabs/prysm/v3/config/params"
types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives"
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
)
type dutiesProvider interface {
GetAttesterDuties(ctx context.Context, epoch types.Epoch, validatorIndices []types.ValidatorIndex) ([]*apimiddleware.AttesterDutyJson, error)
GetProposerDuties(ctx context.Context, epoch types.Epoch) ([]*apimiddleware.ProposerDutyJson, error)
GetSyncDuties(ctx context.Context, epoch types.Epoch, validatorIndices []types.ValidatorIndex) ([]*apimiddleware.SyncCommitteeDuty, error)
GetCommittees(ctx context.Context, epoch types.Epoch) ([]*apimiddleware.CommitteeJson, error)
}
type beaconApiDutiesProvider struct {
jsonRestHandler jsonRestHandler
}
type committeeIndexSlotPair struct {
committeeIndex types.CommitteeIndex
slot types.Slot
}
func (c beaconApiValidatorClient) getDuties(ctx context.Context, in *ethpb.DutiesRequest) (*ethpb.DutiesResponse, error) {
multipleValidatorStatus, err := c.multipleValidatorStatus(ctx, &ethpb.MultipleValidatorStatusRequest{PublicKeys: in.PublicKeys})
if err != nil {
return nil, errors.Wrap(err, "failed to get validator status")
}
// Sync committees are an Altair feature
fetchSyncDuties := in.Epoch >= params.BeaconConfig().AltairForkEpoch
currentEpochDuties, err := c.getDutiesForEpoch(ctx, in.Epoch, multipleValidatorStatus, fetchSyncDuties)
if err != nil {
return nil, errors.Wrapf(err, "failed to get duties for current epoch `%d`", in.Epoch)
}
nextEpochDuties, err := c.getDutiesForEpoch(ctx, in.Epoch+1, multipleValidatorStatus, fetchSyncDuties)
if err != nil {
return nil, errors.Wrapf(err, "failed to get duties for next epoch `%d`", in.Epoch+1)
}
return &ethpb.DutiesResponse{
Duties: currentEpochDuties,
CurrentEpochDuties: currentEpochDuties,
NextEpochDuties: nextEpochDuties,
}, nil
}
func (c beaconApiValidatorClient) getDutiesForEpoch(
ctx context.Context,
epoch types.Epoch,
multipleValidatorStatus *ethpb.MultipleValidatorStatusResponse,
fetchSyncDuties bool,
) ([]*ethpb.DutiesResponse_Duty, error) {
attesterDuties, err := c.dutiesProvider.GetAttesterDuties(ctx, epoch, multipleValidatorStatus.Indices)
if err != nil {
return nil, errors.Wrapf(err, "failed to get attester duties for epoch `%d`", epoch)
}
var syncDuties []*apimiddleware.SyncCommitteeDuty
if fetchSyncDuties {
if syncDuties, err = c.dutiesProvider.GetSyncDuties(ctx, epoch, multipleValidatorStatus.Indices); err != nil {
return nil, errors.Wrapf(err, "failed to get sync duties for epoch `%d`", epoch)
}
}
var proposerDuties []*apimiddleware.ProposerDutyJson
if proposerDuties, err = c.dutiesProvider.GetProposerDuties(ctx, epoch); err != nil {
return nil, errors.Wrapf(err, "failed to get proposer duties for epoch `%d`", epoch)
}
committees, err := c.dutiesProvider.GetCommittees(ctx, epoch)
if err != nil {
return nil, errors.Wrapf(err, "failed to get committees for epoch `%d`", epoch)
}
// Mapping from a validator index to its attesting committee's index and slot
attesterDutiesMapping := make(map[types.ValidatorIndex]committeeIndexSlotPair)
for _, attesterDuty := range attesterDuties {
validatorIndex, err := strconv.ParseUint(attesterDuty.ValidatorIndex, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse attester validator index `%s`", attesterDuty.ValidatorIndex)
}
slot, err := strconv.ParseUint(attesterDuty.Slot, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse attester slot `%s`", attesterDuty.Slot)
}
committeeIndex, err := strconv.ParseUint(attesterDuty.CommitteeIndex, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse attester committee index `%s`", attesterDuty.CommitteeIndex)
}
attesterDutiesMapping[types.ValidatorIndex(validatorIndex)] = committeeIndexSlotPair{
slot: types.Slot(slot),
committeeIndex: types.CommitteeIndex(committeeIndex),
}
}
// Mapping from a validator index to its proposal slot
proposerDutySlots := make(map[types.ValidatorIndex][]types.Slot)
for _, proposerDuty := range proposerDuties {
validatorIndex, err := strconv.ParseUint(proposerDuty.ValidatorIndex, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse proposer validator index `%s`", proposerDuty.ValidatorIndex)
}
slot, err := strconv.ParseUint(proposerDuty.Slot, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse proposer slot `%s`", proposerDuty.Slot)
}
proposerDutySlots[types.ValidatorIndex(validatorIndex)] = append(proposerDutySlots[types.ValidatorIndex(validatorIndex)], types.Slot(slot))
}
// Set containing all validator indices that are part of a sync committee for this epoch
syncDutiesMapping := make(map[types.ValidatorIndex]bool)
for _, syncDuty := range syncDuties {
validatorIndex, err := strconv.ParseUint(syncDuty.ValidatorIndex, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse sync validator index `%s`", syncDuty.ValidatorIndex)
}
syncDutiesMapping[types.ValidatorIndex(validatorIndex)] = true
}
// Mapping from the {committeeIndex, slot} to each of the committee's validator indices
committeeMapping := make(map[committeeIndexSlotPair][]types.ValidatorIndex)
for _, committee := range committees {
committeeIndex, err := strconv.ParseUint(committee.Index, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse committee index `%s`", committee.Index)
}
slot, err := strconv.ParseUint(committee.Slot, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse slot `%s`", committee.Slot)
}
validatorIndices := make([]types.ValidatorIndex, len(committee.Validators))
for index, validatorIndexString := range committee.Validators {
validatorIndex, err := strconv.ParseUint(validatorIndexString, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse committee validator index `%s`", validatorIndexString)
}
validatorIndices[index] = types.ValidatorIndex(validatorIndex)
}
key := committeeIndexSlotPair{
committeeIndex: types.CommitteeIndex(committeeIndex),
slot: types.Slot(slot),
}
committeeMapping[key] = validatorIndices
}
duties := make([]*ethpb.DutiesResponse_Duty, len(multipleValidatorStatus.Statuses))
for index, validatorStatus := range multipleValidatorStatus.Statuses {
validatorIndex := multipleValidatorStatus.Indices[index]
pubkey := multipleValidatorStatus.PublicKeys[index]
var attesterSlot types.Slot
var committeeIndex types.CommitteeIndex
var committeeValidatorIndices []types.ValidatorIndex
if committeeMappingKey, ok := attesterDutiesMapping[validatorIndex]; ok {
committeeIndex = committeeMappingKey.committeeIndex
attesterSlot = committeeMappingKey.slot
if committeeValidatorIndices, ok = committeeMapping[committeeMappingKey]; !ok {
return nil, errors.Errorf("failed to find validators for committee index `%d` and slot `%d`", committeeIndex, attesterSlot)
}
}
duties[index] = &ethpb.DutiesResponse_Duty{
Committee: committeeValidatorIndices,
CommitteeIndex: committeeIndex,
AttesterSlot: attesterSlot,
ProposerSlots: proposerDutySlots[types.ValidatorIndex(validatorIndex)],
PublicKey: pubkey,
Status: validatorStatus.Status,
ValidatorIndex: types.ValidatorIndex(validatorIndex),
IsSyncCommittee: syncDutiesMapping[types.ValidatorIndex(validatorIndex)],
}
}
return duties, nil
}
// GetCommittees retrieves the committees for the given epoch
func (c beaconApiDutiesProvider) GetCommittees(ctx context.Context, epoch types.Epoch) ([]*apimiddleware.CommitteeJson, error) {
committeeParams := url.Values{}
committeeParams.Add("epoch", strconv.FormatUint(uint64(epoch), 10))
committeesRequest := buildURL("/eth/v1/beacon/states/head/committees", committeeParams)
var stateCommittees apimiddleware.StateCommitteesResponseJson
if _, err := c.jsonRestHandler.GetRestJsonResponse(ctx, committeesRequest, &stateCommittees); err != nil {
return nil, errors.Wrapf(err, "failed to query committees for epoch `%d`", epoch)
}
if stateCommittees.Data == nil {
return nil, errors.New("state committees data is nil")
}
for index, committee := range stateCommittees.Data {
if committee == nil {
return nil, errors.Errorf("committee at index `%d` is nil", index)
}
}
return stateCommittees.Data, nil
}
// GetAttesterDuties retrieves the attester duties for the given epoch and validatorIndices
func (c beaconApiDutiesProvider) GetAttesterDuties(ctx context.Context, epoch types.Epoch, validatorIndices []types.ValidatorIndex) ([]*apimiddleware.AttesterDutyJson, error) {
jsonValidatorIndices := make([]string, len(validatorIndices))
@@ -45,3 +248,53 @@ func (c beaconApiDutiesProvider) GetAttesterDuties(ctx context.Context, epoch ty
return attesterDuties.Data, nil
}
// GetProposerDuties retrieves the proposer duties for the given epoch
func (c beaconApiDutiesProvider) GetProposerDuties(ctx context.Context, epoch types.Epoch) ([]*apimiddleware.ProposerDutyJson, error) {
proposerDuties := apimiddleware.ProposerDutiesResponseJson{}
if _, err := c.jsonRestHandler.GetRestJsonResponse(ctx, fmt.Sprintf("/eth/v1/validator/duties/proposer/%d", epoch), &proposerDuties); err != nil {
return nil, errors.Wrapf(err, "failed to query proposer duties for epoch `%d`", epoch)
}
if proposerDuties.Data == nil {
return nil, errors.New("proposer duties data is nil")
}
for index, proposerDuty := range proposerDuties.Data {
if proposerDuty == nil {
return nil, errors.Errorf("proposer duty at index `%d` is nil", index)
}
}
return proposerDuties.Data, nil
}
// GetSyncDuties retrieves the sync committee duties for the given epoch and validatorIndices
func (c beaconApiDutiesProvider) GetSyncDuties(ctx context.Context, epoch types.Epoch, validatorIndices []types.ValidatorIndex) ([]*apimiddleware.SyncCommitteeDuty, error) {
jsonValidatorIndices := make([]string, len(validatorIndices))
for index, validatorIndex := range validatorIndices {
jsonValidatorIndices[index] = strconv.FormatUint(uint64(validatorIndex), 10)
}
validatorIndicesBytes, err := json.Marshal(jsonValidatorIndices)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal validator indices")
}
syncDuties := apimiddleware.SyncCommitteeDutiesResponseJson{}
if _, err := c.jsonRestHandler.PostRestJson(ctx, fmt.Sprintf("/eth/v1/validator/duties/sync/%d", epoch), nil, bytes.NewBuffer(validatorIndicesBytes), &syncDuties); err != nil {
return nil, errors.Wrap(err, "failed to send POST data to REST endpoint")
}
if syncDuties.Data == nil {
return nil, errors.New("sync duties data is nil")
}
for index, syncDuty := range syncDuties.Data {
if syncDuty == nil {
return nil, errors.Errorf("sync duty at index `%d` is nil", index)
}
}
return syncDuties.Data, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -50,3 +50,48 @@ func (mr *MockdutiesProviderMockRecorder) GetAttesterDuties(ctx, epoch, validato
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttesterDuties", reflect.TypeOf((*MockdutiesProvider)(nil).GetAttesterDuties), ctx, epoch, validatorIndices)
}
// GetCommittees mocks base method.
func (m *MockdutiesProvider) GetCommittees(ctx context.Context, epoch types.Epoch) ([]*apimiddleware.CommitteeJson, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCommittees", ctx, epoch)
ret0, _ := ret[0].([]*apimiddleware.CommitteeJson)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetCommittees indicates an expected call of GetCommittees.
func (mr *MockdutiesProviderMockRecorder) GetCommittees(ctx, epoch interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommittees", reflect.TypeOf((*MockdutiesProvider)(nil).GetCommittees), ctx, epoch)
}
// GetProposerDuties mocks base method.
func (m *MockdutiesProvider) GetProposerDuties(ctx context.Context, epoch types.Epoch) ([]*apimiddleware.ProposerDutyJson, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetProposerDuties", ctx, epoch)
ret0, _ := ret[0].([]*apimiddleware.ProposerDutyJson)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetProposerDuties indicates an expected call of GetProposerDuties.
func (mr *MockdutiesProviderMockRecorder) GetProposerDuties(ctx, epoch interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProposerDuties", reflect.TypeOf((*MockdutiesProvider)(nil).GetProposerDuties), ctx, epoch)
}
// GetSyncDuties mocks base method.
func (m *MockdutiesProvider) GetSyncDuties(ctx context.Context, epoch types.Epoch, validatorIndices []types.ValidatorIndex) ([]*apimiddleware.SyncCommitteeDuty, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSyncDuties", ctx, epoch, validatorIndices)
ret0, _ := ret[0].([]*apimiddleware.SyncCommitteeDuty)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSyncDuties indicates an expected call of GetSyncDuties.
func (mr *MockdutiesProviderMockRecorder) GetSyncDuties(ctx, epoch, validatorIndices interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSyncDuties", reflect.TypeOf((*MockdutiesProvider)(nil).GetSyncDuties), ctx, epoch, validatorIndices)
}