From 8ae0ee225e531ee41fa69e0fb0d9ae584d24762d Mon Sep 17 00:00:00 2001 From: Kaloyan Tanev Date: Tue, 6 Jan 2026 14:38:21 +0200 Subject: [PATCH] Add guards for signing and broadcasting exits for inactive validators --- cmd/exit_broadcast.go | 39 ++++++++++++++++++++++++++++++++++++++- cmd/exit_sign.go | 23 +++++++++++++---------- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/cmd/exit_broadcast.go b/cmd/exit_broadcast.go index 6fa8a298b..ca6557249 100644 --- a/cmd/exit_broadcast.go +++ b/cmd/exit_broadcast.go @@ -9,9 +9,11 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" "time" + eth2v1 "github.com/attestantio/go-eth2-client/api/v1" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" libp2plog "github.com/ipfs/go-log/v2" @@ -252,9 +254,42 @@ func fetchFullExit(ctx context.Context, exitFilePath string, config exitConfig, } func broadcastExitsToBeacon(ctx context.Context, eth2Cl eth2wrap.Client, exits map[core.PubKey]eth2p0.SignedVoluntaryExit) error { + blsKeys := []eth2p0.BLSPubKey{} + + for key := range exits { + blsKey, err := key.ToETH2() + if err != nil { + return errors.Wrap(err, "convert core pubkey to eth2 pubkey", z.Str("core_pubkey", key.String())) + } + + blsKeys = append(blsKeys, blsKey) + } + + rawValData, err := queryBeaconForValidator(ctx, eth2Cl, blsKeys, nil, []eth2v1.ValidatorState{eth2v1.ValidatorStateActiveOngoing}) + if err != nil { + return errors.Wrap(err, "fetch all validators indices from beacon") + } + + activePubKeys := []eth2p0.BLSPubKey{} + for _, val := range rawValData.Data { + activePubKeys = append(activePubKeys, val.Validator.PublicKey) + } + + activeExits := make(map[core.PubKey]eth2p0.SignedVoluntaryExit) + for validator, fullExit := range exits { valCtx := log.WithCtx(ctx, z.Str("validator", validator.String())) + eth2Key, err := validator.ToETH2() + if err != nil { + return errors.Wrap(err, "convert core pubkey to eth2 pubkey", z.Str("core_pubkey", validator.String())) + } + + if !slices.Contains(activePubKeys, eth2Key) { + log.Info(valCtx, "Validator is not active, skipping broadcast") + continue + } + rawPkBytes, err := validator.Bytes() if err != nil { return errors.Wrap(err, "serialize validator key bytes", z.Str("validator", validator.String())) @@ -284,9 +319,11 @@ func broadcastExitsToBeacon(ctx context.Context, eth2Cl eth2wrap.Client, exits m if err := tbls.Verify(pubkey, exitRoot[:], signature); err != nil { return errors.Wrap(err, "exit message signature not verified") } + + activeExits[validator] = fullExit } - for validator, fullExit := range exits { + for validator, fullExit := range activeExits { valCtx := log.WithCtx(ctx, z.Str("validator", validator.String())) if err := eth2Cl.SubmitVoluntaryExit(valCtx, &fullExit); err != nil { return errors.Wrap(err, "submit voluntary exit") diff --git a/cmd/exit_sign.go b/cmd/exit_sign.go index 04ea5224c..f019d68b3 100644 --- a/cmd/exit_sign.go +++ b/cmd/exit_sign.go @@ -225,11 +225,13 @@ func signAllValidatorsExits(ctx context.Context, config exitConfig, eth2Cl eth2w valsEth2 = append(valsEth2, eth2PK) } - rawValData, err := queryBeaconForValidator(ctx, eth2Cl, valsEth2, nil) + rawValData, err := queryBeaconForValidator(ctx, eth2Cl, valsEth2, nil, []eth2v1.ValidatorState{eth2v1.ValidatorStatePendingQueued, eth2v1.ValidatorStateActiveOngoing}) if err != nil { return nil, errors.Wrap(err, "fetch all validators indices from beacon") } + activeShares := make(keystore.ValidatorShares) + for _, val := range rawValData.Data { share, ok := shares[core.PubKeyFrom48Bytes(val.Validator.PublicKey)] if !ok { @@ -237,14 +239,14 @@ func signAllValidatorsExits(ctx context.Context, config exitConfig, eth2Cl eth2w } share.Index = int(val.Index) - shares[core.PubKeyFrom48Bytes(val.Validator.PublicKey)] = share + activeShares[core.PubKeyFrom48Bytes(val.Validator.PublicKey)] = share } - log.Info(ctx, "Signing partial exit message for all active validators") + log.Info(ctx, "Signing partial exit message for all active validators", z.Int("active_validators", len(activeShares)), z.Int("inactive_validators", len(shares)-len(activeShares))) var exitBlobs []obolapi.ExitBlob - for pk, share := range shares { + for pk, share := range activeShares { exitMsg, err := signExit(ctx, eth2Cl, eth2p0.ValidatorIndex(share.Index), share.Share, eth2p0.Epoch(config.ExitEpoch)) if err != nil { return nil, errors.Wrap(err, "sign partial exit message", z.Str("validator_public_key", pk.String()), z.Int("validator_index", share.Index), z.Int("exit_epoch", int(config.ExitEpoch))) @@ -277,7 +279,7 @@ func fetchValidatorBLSPubKey(ctx context.Context, config exitConfig, eth2Cl eth2 return valEth2, nil } - rawValData, err := queryBeaconForValidator(ctx, eth2Cl, nil, []eth2p0.ValidatorIndex{eth2p0.ValidatorIndex(config.ValidatorIndex)}) + rawValData, err := queryBeaconForValidator(ctx, eth2Cl, nil, []eth2p0.ValidatorIndex{eth2p0.ValidatorIndex(config.ValidatorIndex)}, nil) if err != nil { return eth2p0.BLSPubKey{}, errors.Wrap(err, "fetch validator pubkey from beacon", z.Str("beacon_address", eth2Cl.Address()), z.U64("validator_index", config.ValidatorIndex)) } @@ -301,7 +303,7 @@ func fetchValidatorIndex(ctx context.Context, config exitConfig, eth2Cl eth2wrap return 0, errors.Wrap(err, "convert core pubkey to eth2 pubkey", z.Str("core_pubkey", config.ValidatorPubkey)) } - rawValData, err := queryBeaconForValidator(ctx, eth2Cl, []eth2p0.BLSPubKey{valEth2}, nil) + rawValData, err := queryBeaconForValidator(ctx, eth2Cl, []eth2p0.BLSPubKey{valEth2}, nil, nil) if err != nil { return 0, errors.Wrap(err, "fetch validator index from beacon", z.Str("beacon_address", eth2Cl.Address()), z.Str("validator_pubkey", valEth2.String())) } @@ -315,11 +317,12 @@ func fetchValidatorIndex(ctx context.Context, config exitConfig, eth2Cl eth2wrap return 0, errors.New("validator public key not found in beacon node response", z.Str("beacon_address", eth2Cl.Address()), z.Str("validator_pubkey", valEth2.String()), z.Any("raw_response", rawValData)) } -func queryBeaconForValidator(ctx context.Context, eth2Cl eth2wrap.Client, pubKeys []eth2p0.BLSPubKey, indices []eth2p0.ValidatorIndex) (*eth2api.Response[map[eth2p0.ValidatorIndex]*eth2v1.Validator], error) { +func queryBeaconForValidator(ctx context.Context, eth2Cl eth2wrap.Client, pubKeys []eth2p0.BLSPubKey, indices []eth2p0.ValidatorIndex, states []eth2v1.ValidatorState) (*eth2api.Response[map[eth2p0.ValidatorIndex]*eth2v1.Validator], error) { valAPICallOpts := ð2api.ValidatorsOpts{ - State: "head", - PubKeys: pubKeys, - Indices: indices, + State: "head", + PubKeys: pubKeys, + Indices: indices, + ValidatorStates: states, } rawValData, err := eth2Cl.Validators(ctx, valAPICallOpts)