Allow validators registration batching on Builder API /eth/v1/builder/validators (#13178)

* builder `NewClient`: Simplify + fix some typos.

* Validator client: Implement `validator-registration-batch-size` option

* Address Potuz comments

* Address Potuz's comments

---------

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
This commit is contained in:
Manu NALEPA
2023-11-28 01:23:48 +01:00
committed by GitHub
parent 7cc05401ca
commit da2212f6cc
12 changed files with 260 additions and 50 deletions

View File

@@ -16,11 +16,13 @@ import (
"go.opencensus.io/trace"
)
// SubmitValidatorRegistrations signs validator registration objects and submits it to the beacon node.
// 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()
@@ -29,17 +31,33 @@ func SubmitValidatorRegistrations(
return nil
}
if _, err := validatorClient.SubmitValidatorRegistrations(ctx, &ethpb.SignedValidatorRegistrationsV1{
Messages: signedRegs,
}); err != nil {
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.")
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
}
}
return errors.Wrap(err, "could not submit signed registrations to beacon node")
}
log.Infoln("Submitted builder validator registration settings for custom builders")
return nil
if lastErr == nil {
log.Infoln("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.
@@ -100,3 +118,35 @@ func isValidatorRegistrationSame(cachedVR *ethpb.ValidatorRegistrationV1, newVR
}
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
}

View File

@@ -21,27 +21,76 @@ func TestSubmitValidatorRegistrations(t *testing.T) {
defer finish()
ctx := context.Background()
require.NoError(t, nil, SubmitValidatorRegistrations(ctx, m.validatorClient, []*ethpb.SignedValidatorRegistrationV1{}))
validatorRegsBatchSize := 2
require.NoError(t, nil, SubmitValidatorRegistrations(ctx, m.validatorClient, []*ethpb.SignedValidatorRegistrationV1{}, validatorRegsBatchSize))
reg := &ethpb.ValidatorRegistrationV1{
FeeRecipient: bytesutil.PadTo([]byte("fee"), 20),
GasLimit: 123456,
Timestamp: uint64(time.Now().Unix()),
Pubkey: validatorKey.PublicKey().Marshal(),
regs := [...]*ethpb.ValidatorRegistrationV1{
{
FeeRecipient: bytesutil.PadTo([]byte("fee"), 20),
GasLimit: 123,
Timestamp: uint64(time.Now().Unix()),
Pubkey: validatorKey.PublicKey().Marshal(),
},
{
FeeRecipient: bytesutil.PadTo([]byte("fee"), 20),
GasLimit: 456,
Timestamp: uint64(time.Now().Unix()),
Pubkey: validatorKey.PublicKey().Marshal(),
},
{
FeeRecipient: bytesutil.PadTo([]byte("fee"), 20),
GasLimit: 789,
Timestamp: uint64(time.Now().Unix()),
Pubkey: validatorKey.PublicKey().Marshal(),
},
}
m.validatorClient.EXPECT().
SubmitValidatorRegistrations(gomock.Any(), &ethpb.SignedValidatorRegistrationsV1{
Messages: []*ethpb.SignedValidatorRegistrationV1{
{Message: reg,
Signature: params.BeaconConfig().ZeroHash[:]},
gomock.InOrder(
m.validatorClient.EXPECT().
SubmitValidatorRegistrations(gomock.Any(), &ethpb.SignedValidatorRegistrationsV1{
Messages: []*ethpb.SignedValidatorRegistrationV1{
{
Message: regs[0],
Signature: params.BeaconConfig().ZeroHash[:],
},
{
Message: regs[1],
Signature: params.BeaconConfig().ZeroHash[:],
},
},
}).
Return(nil, nil),
m.validatorClient.EXPECT().
SubmitValidatorRegistrations(gomock.Any(), &ethpb.SignedValidatorRegistrationsV1{
Messages: []*ethpb.SignedValidatorRegistrationV1{
{
Message: regs[2],
Signature: params.BeaconConfig().ZeroHash[:],
},
},
}).
Return(nil, nil),
)
require.NoError(t, nil, SubmitValidatorRegistrations(
ctx, m.validatorClient,
[]*ethpb.SignedValidatorRegistrationV1{
{
Message: regs[0],
Signature: params.BeaconConfig().ZeroHash[:],
},
}).
Return(nil, nil)
require.NoError(t, nil, SubmitValidatorRegistrations(ctx, m.validatorClient, []*ethpb.SignedValidatorRegistrationV1{
{Message: reg,
Signature: params.BeaconConfig().ZeroHash[:]},
}))
{
Message: regs[1],
Signature: params.BeaconConfig().ZeroHash[:],
},
{
Message: regs[2],
Signature: params.BeaconConfig().ZeroHash[:],
},
},
validatorRegsBatchSize,
))
}
func TestSubmitValidatorRegistration_CantSign(t *testing.T) {
@@ -49,6 +98,7 @@ func TestSubmitValidatorRegistration_CantSign(t *testing.T) {
defer finish()
ctx := context.Background()
validatorRegsBatchSize := 500
reg := &ethpb.ValidatorRegistrationV1{
FeeRecipient: bytesutil.PadTo([]byte("fee"), 20),
GasLimit: 123456,
@@ -67,7 +117,7 @@ func TestSubmitValidatorRegistration_CantSign(t *testing.T) {
require.ErrorContains(t, "could not sign", SubmitValidatorRegistrations(ctx, m.validatorClient, []*ethpb.SignedValidatorRegistrationV1{
{Message: reg,
Signature: params.BeaconConfig().ZeroHash[:]},
}))
}, validatorRegsBatchSize))
}
func Test_signValidatorRegistration(t *testing.T) {
@@ -225,3 +275,113 @@ func TestValidator_SignValidatorRegistrationRequest(t *testing.T) {
})
}
}
func TestChunkSignedValidatorRegistrationV1(t *testing.T) {
tests := map[string]struct {
regs []*ethpb.SignedValidatorRegistrationV1
chunkSize int
expected [][]*ethpb.SignedValidatorRegistrationV1
}{
"All buckets are full": {
regs: []*ethpb.SignedValidatorRegistrationV1{
{Signature: []byte("1")},
{Signature: []byte("2")},
{Signature: []byte("3")},
{Signature: []byte("4")},
{Signature: []byte("5")},
{Signature: []byte("6")},
},
chunkSize: 3,
expected: [][]*ethpb.SignedValidatorRegistrationV1{
{
{Signature: []byte("1")},
{Signature: []byte("2")},
{Signature: []byte("3")},
},
{
{Signature: []byte("4")},
{Signature: []byte("5")},
{Signature: []byte("6")},
},
},
},
"Last bucket is not full": {
regs: []*ethpb.SignedValidatorRegistrationV1{
{Signature: []byte("1")},
{Signature: []byte("2")},
{Signature: []byte("3")},
{Signature: []byte("4")},
{Signature: []byte("5")},
{Signature: []byte("6")},
{Signature: []byte("7")},
},
chunkSize: 3,
expected: [][]*ethpb.SignedValidatorRegistrationV1{
{
{Signature: []byte("1")},
{Signature: []byte("2")},
{Signature: []byte("3")},
},
{
{Signature: []byte("4")},
{Signature: []byte("5")},
{Signature: []byte("6")},
},
{
{Signature: []byte("7")},
},
},
},
"Not enough items": {
regs: []*ethpb.SignedValidatorRegistrationV1{
{Signature: []byte("1")},
{Signature: []byte("2")},
{Signature: []byte("3")},
},
chunkSize: 42,
expected: [][]*ethpb.SignedValidatorRegistrationV1{
{
{Signature: []byte("1")},
{Signature: []byte("2")},
{Signature: []byte("3")},
},
},
},
"Null chunk size": {
regs: []*ethpb.SignedValidatorRegistrationV1{
{Signature: []byte("1")},
{Signature: []byte("2")},
{Signature: []byte("3")},
},
chunkSize: 0,
expected: [][]*ethpb.SignedValidatorRegistrationV1{
{
{Signature: []byte("1")},
{Signature: []byte("2")},
{Signature: []byte("3")},
},
},
},
"Negative chunk size": {
regs: []*ethpb.SignedValidatorRegistrationV1{
{Signature: []byte("1")},
{Signature: []byte("2")},
{Signature: []byte("3")},
},
chunkSize: -1,
expected: [][]*ethpb.SignedValidatorRegistrationV1{
{
{Signature: []byte("1")},
{Signature: []byte("2")},
{Signature: []byte("3")},
},
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
require.DeepEqual(t, test.expected, chunkSignedValidatorRegistrationV1(test.regs, test.chunkSize))
})
}
}

View File

@@ -74,6 +74,7 @@ type ValidatorService struct {
graffiti []byte
Web3SignerConfig *remoteweb3signer.SetupConfig
proposerSettings *validatorserviceconfig.ProposerSettings
validatorRegBatchSize int
}
// Config for the validator service.
@@ -99,6 +100,7 @@ type Config struct {
ProposerSettings *validatorserviceconfig.ProposerSettings
BeaconApiEndpoint string
BeaconApiTimeout time.Duration
ValidatorRegBatchSize int
}
// NewValidatorService creates a new validator service for the service
@@ -127,6 +129,7 @@ func NewValidatorService(ctx context.Context, cfg *Config) (*ValidatorService, e
graffitiStruct: cfg.GraffitiStruct,
Web3SignerConfig: cfg.Web3SignerConfig,
proposerSettings: cfg.ProposerSettings,
validatorRegBatchSize: cfg.ValidatorRegBatchSize,
}
dialOpts := ConstructDialOptions(
@@ -220,6 +223,7 @@ func (v *ValidatorService) Start() {
proposerSettings: v.proposerSettings,
walletInitializedChannel: make(chan *wallet.Wallet, 1),
prysmBeaconClient: prysmBeaconClient,
validatorRegBatchSize: v.validatorRegBatchSize,
}
// To resolve a race condition at startup due to the interface

View File

@@ -105,6 +105,7 @@ type validator struct {
proposerSettings *validatorserviceconfig.ProposerSettings
walletInitializedChannel chan *wallet.Wallet
prysmBeaconClient iface.PrysmBeaconChainClient
validatorRegBatchSize int
}
type validatorStatus struct {
@@ -1043,7 +1044,7 @@ func (v *validator) PushProposerSettings(ctx context.Context, km keymanager.IKey
if err != nil {
return err
}
if err := SubmitValidatorRegistrations(ctx, v.validatorClient, signedRegReqs); err != nil {
if err := SubmitValidatorRegistrations(ctx, v.validatorClient, signedRegReqs, v.validatorRegBatchSize); err != nil {
return errors.Wrap(ErrBuilderValidatorRegistration, err.Error())
}

View File

@@ -439,6 +439,7 @@ func (c *ValidatorClient) registerValidatorService(cliCtx *cli.Context) error {
ProposerSettings: bpc,
BeaconApiTimeout: time.Second * 30,
BeaconApiEndpoint: c.cliCtx.String(flags.BeaconRESTApiProviderFlag.Name),
ValidatorRegBatchSize: c.cliCtx.Int(flags.ValidatorRegistrationBatchSizeFlag.Name),
})
if err != nil {
return errors.Wrap(err, "could not initialize validator service")