From 1c4c226ade5aface0b9b2fe154c15f63739171ce Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:10:45 +0100 Subject: [PATCH] cmd: skip inactive validators in exits (#4205) This PR solves 2 issues: 1. Upon signing exits for all validators, Charon does not check if all validators are active. We are changing that to sign only exits for validators that are either queued for activation or active. 2. Upon broadcasting exits for all validators, Charon does not check if all validators are active. We are changing that to broadcast only exits for validators that are active. category: bug ticket: none --- 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 50841413a1..bd5da4f65d 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 f3c1e59322..3670d431ba 100644 --- a/cmd/exit_sign.go +++ b/cmd/exit_sign.go @@ -224,11 +224,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 { @@ -236,14 +238,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))) @@ -276,7 +278,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)) } @@ -300,7 +302,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())) } @@ -314,11 +316,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)