Files
prysm/validator/client/registration.go
james-prysm 101dd55710 gloas validator client proposer preferences (#16548)
**What type of PR is this?**

Feature

**What does this PR do? Why is it needed?**

adds validator client call to proposer preferences. adds todo comments
for when we fully replace prepare beacon proposer endpoint with proposer
preferences.

**Which issues(s) does this PR fix?**
partially addresses https://github.com/OffchainLabs/prysm/issues/16545

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [ ] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-03-23 21:54:36 +00:00

196 lines
6.6 KiB
Go

package client
import (
"context"
"strings"
"github.com/OffchainLabs/prysm/v7/beacon-chain/builder"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
validatorpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1/validator-client"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/OffchainLabs/prysm/v7/validator/client/iface"
"github.com/OffchainLabs/prysm/v7/validator/keymanager"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/pkg/errors"
)
// SubmitValidatorRegistrations signs validator registration objects and submits it to the beacon node by batch of validatorRegsBatchSize size maximum.
// If at least one error occurs during a registration call to the beacon node, the last error is returned.
func SubmitValidatorRegistrations(
ctx context.Context,
validatorClient iface.ValidatorClient,
signedRegs []*ethpb.SignedValidatorRegistrationV1,
validatorRegsBatchSize int,
) error {
ctx, span := trace.StartSpan(ctx, "validator.SubmitValidatorRegistrations")
defer span.End()
if len(signedRegs) == 0 {
return nil
}
chunks := chunkSignedValidatorRegistrationV1(signedRegs, validatorRegsBatchSize)
var lastErr error
for _, chunk := range chunks {
innerSignerRegs := ethpb.SignedValidatorRegistrationsV1{
Messages: chunk,
}
if _, err := validatorClient.SubmitValidatorRegistrations(ctx, &innerSignerRegs); err != nil {
lastErr = errors.Wrap(err, "could not submit signed registrations to beacon node")
if strings.Contains(err.Error(), builder.ErrNoBuilder.Error()) {
log.Warnln("Beacon node does not utilize a custom builder via the --http-mev-relay flag. Validator registration skipped.")
// We stop early the loop here, since if the builder endpoint is not configured for this chunk, it is useless to check the following chunks
break
}
}
}
if lastErr == nil {
log.Debugln("Submitted builder validator registration settings for custom builders")
} else {
log.WithError(lastErr).Warn("Could not submit all signed registrations to beacon node")
}
return lastErr
}
// Sings validator registration obj with the proposer domain and private key.
func signValidatorRegistration(ctx context.Context, signer iface.SigningFunc, reg *ethpb.ValidatorRegistrationV1) ([]byte, error) {
ctx, span := trace.StartSpan(ctx, "validator.signValidatorRegistration")
defer span.End()
// Per spec, we want the fork version and genesis validator to be nil.
// Which is genesis value and zero by default.
d, err := signing.ComputeDomain(
params.BeaconConfig().DomainApplicationBuilder,
nil, /* fork version */
nil /* genesis val root */)
if err != nil {
return nil, err
}
r, err := signing.ComputeSigningRoot(reg, d)
if err != nil {
return nil, errors.Wrap(err, signingRootErr)
}
sig, err := signer(ctx, &validatorpb.SignRequest{
PublicKey: reg.Pubkey,
SigningRoot: r[:],
SignatureDomain: d,
Object: &validatorpb.SignRequest_Registration{Registration: reg},
})
if err != nil {
return nil, errors.Wrap(err, "could not sign validator registration")
}
return sig.Marshal(), nil
}
func (v *validator) signProposerPreferences(
ctx context.Context,
km keymanager.IKeymanager,
pubkey [fieldparams.BLSPubkeyLength]byte,
pref *ethpb.ProposerPreferences,
) (*ethpb.SignedProposerPreferences, error) {
ctx, span := trace.StartSpan(ctx, "validator.signProposerPreferences")
defer span.End()
epoch := slots.ToEpoch(pref.ProposalSlot)
resp, err := v.domainData(ctx, epoch, params.BeaconConfig().DomainProposerPreferences[:])
if err != nil {
return nil, errors.Wrap(err, "could not get proposer preferences domain data")
}
domain := resp.SignatureDomain
r, err := signing.ComputeSigningRoot(pref, domain)
if err != nil {
return nil, errors.Wrap(err, signingRootErr)
}
sig, err := km.Sign(ctx, &validatorpb.SignRequest{
PublicKey: pubkey[:],
SigningRoot: r[:],
SignatureDomain: domain,
Object: &validatorpb.SignRequest_ProposerPreference{ProposerPreference: pref},
})
if err != nil {
return nil, errors.Wrap(err, "could not sign proposer preferences")
}
return &ethpb.SignedProposerPreferences{
Message: pref,
Signature: sig.Marshal(),
}, nil
}
// SignValidatorRegistrationRequest compares and returns either the cached validator registration request or signs a new one.
func (v *validator) SignValidatorRegistrationRequest(ctx context.Context, signer iface.SigningFunc, newValidatorRegistration *ethpb.ValidatorRegistrationV1) (*ethpb.SignedValidatorRegistrationV1, bool /* isCached */, error) {
signedReg, ok := v.signedValidatorRegistrations[bytesutil.ToBytes48(newValidatorRegistration.Pubkey)]
if ok && isValidatorRegistrationSame(signedReg.Message, newValidatorRegistration) {
return signedReg, true, nil
} else {
sig, err := signValidatorRegistration(ctx, signer, newValidatorRegistration)
if err != nil {
return nil, false, err
}
newRequest := &ethpb.SignedValidatorRegistrationV1{
Message: newValidatorRegistration,
Signature: sig,
}
v.signedValidatorRegistrations[bytesutil.ToBytes48(newValidatorRegistration.Pubkey)] = newRequest
return newRequest, false, nil
}
}
func isValidatorRegistrationSame(cachedVR *ethpb.ValidatorRegistrationV1, newVR *ethpb.ValidatorRegistrationV1) bool {
isSame := true
if cachedVR.GasLimit != newVR.GasLimit {
isSame = false
}
if hexutil.Encode(cachedVR.FeeRecipient) != hexutil.Encode(newVR.FeeRecipient) {
isSame = false
}
return isSame
}
// chunkSignedValidatorRegistrationV1 chunks regs into chunks of size chunkSize (the last chunk may be smaller). If chunkSize is non-positive, returns only one chunk.
func chunkSignedValidatorRegistrationV1(regs []*ethpb.SignedValidatorRegistrationV1, chunkSize int) [][]*ethpb.SignedValidatorRegistrationV1 {
if chunkSize <= 0 {
chunkSize = len(regs)
}
regsCount := len(regs)
chunksCount := (regsCount + chunkSize - 1) / chunkSize
lastChunkSize := regsCount % chunkSize
if lastChunkSize == 0 {
lastChunkSize = chunkSize
}
chunks := make([][]*ethpb.SignedValidatorRegistrationV1, chunksCount)
for i := 0; i < chunksCount-1; i++ {
chunks[i] = make([]*ethpb.SignedValidatorRegistrationV1, chunkSize)
}
chunks[chunksCount-1] = make([]*ethpb.SignedValidatorRegistrationV1, lastChunkSize)
for i, reg := range regs {
chunkIndex := i / chunkSize
chunkOffset := i % chunkSize
chunks[chunkIndex][chunkOffset] = reg
}
return chunks
}