diff --git a/CHANGELOG.md b/CHANGELOG.md index 1edd928..c132de6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ - fetch wallet accounts from Dirk in parallel - fetch process-concurrency configuration value from most specific point in hierarchy - add metrics to track strategy operation results + - support Altair: + - support updated `go-eth2-client` for versioned data + - manage sync committee operations: + - generate sync committee messages + - act as sync committee aggregator as required + - added metrics to track strategy operation results - provide release metric in `vouch_release` - provide ready metric in `vouch_ready` - handle chain reorganisations, updating duties as appropriate diff --git a/go.mod b/go.mod index 09780f8..b8b60c9 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,13 @@ require ( github.com/aws/aws-sdk-go v1.38.30 github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/mitchellh/go-homedir v1.1.0 + github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect github.com/opentracing/opentracing-go v1.2.0 github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.9.0 github.com/prometheus/procfs v0.6.0 // indirect - github.com/prysmaticlabs/go-bitfield v0.0.0-20210202205921-7fcea7c45dc8 + github.com/prysmaticlabs/go-bitfield v0.0.0-20210607200045-4da71aaf6c2d github.com/rs/zerolog v1.21.0 github.com/sasha-s/go-deadlock v0.2.0 github.com/sirupsen/logrus v1.6.0 @@ -39,3 +40,5 @@ require ( google.golang.org/grpc v1.38.0 gotest.tools v2.2.0+incompatible ) + +replace github.com/attestantio/go-eth2-client => ../go-eth2-client diff --git a/go.sum b/go.sum index 3e294b1..31d5120 100644 --- a/go.sum +++ b/go.sum @@ -226,6 +226,8 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -403,6 +405,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA= +github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -482,6 +486,8 @@ github.com/prysmaticlabs/go-bitfield v0.0.0-20200322041314-62c2aee71669/go.mod h github.com/prysmaticlabs/go-bitfield v0.0.0-20200618145306-2ae0807bef65/go.mod h1:hCwmef+4qXWjv0jLDbQdWnL0Ol7cS7/lCSS26WR+u6s= github.com/prysmaticlabs/go-bitfield v0.0.0-20210202205921-7fcea7c45dc8 h1:18+Qqobq3HAUY0hgIhPGSqmLFnaLLocemmU7+Sj2aYQ= github.com/prysmaticlabs/go-bitfield v0.0.0-20210202205921-7fcea7c45dc8/go.mod h1:hCwmef+4qXWjv0jLDbQdWnL0Ol7cS7/lCSS26WR+u6s= +github.com/prysmaticlabs/go-bitfield v0.0.0-20210607200045-4da71aaf6c2d h1:46gKr69IlRpv/ENdlzG0SWo5nMLKJxS3tI5NOSdZndQ= +github.com/prysmaticlabs/go-bitfield v0.0.0-20210607200045-4da71aaf6c2d/go.mod h1:hCwmef+4qXWjv0jLDbQdWnL0Ol7cS7/lCSS26WR+u6s= github.com/r3labs/sse/v2 v2.3.0 h1:R/UMa0ML6AYKQ8irQNHhY+204lz1LytDIdKhCxSVAd8= github.com/r3labs/sse/v2 v2.3.0/go.mod h1:hUrYMKfu9WquG9MyI0r6TKiNH+6Sw/QPKm2YbNbU5g8= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -542,6 +548,7 @@ github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5J github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/main.go b/main.go index 4e374d6..a1a441c 100644 --- a/main.go +++ b/main.go @@ -56,6 +56,12 @@ import ( "github.com/attestantio/vouch/services/submitter" immediatesubmitter "github.com/attestantio/vouch/services/submitter/immediate" multinodesubmitter "github.com/attestantio/vouch/services/submitter/multinode" + "github.com/attestantio/vouch/services/synccommitteeaggregator" + standardsynccommitteeaggregator "github.com/attestantio/vouch/services/synccommitteeaggregator/standard" + "github.com/attestantio/vouch/services/synccommitteemessenger" + standardsynccommitteemessenger "github.com/attestantio/vouch/services/synccommitteemessenger/standard" + "github.com/attestantio/vouch/services/synccommitteesubscriber" + standardsynccommitteesubscriber "github.com/attestantio/vouch/services/synccommitteesubscriber/standard" "github.com/attestantio/vouch/services/validatorsmanager" standardvalidatorsmanager "github.com/attestantio/vouch/services/validatorsmanager/standard" bestaggregateattestationstrategy "github.com/attestantio/vouch/strategies/aggregateattestation/best" @@ -191,7 +197,7 @@ func fetchConfig() error { viper.AutomaticEnv() // Defaults. - viper.SetDefault("process-concurrency", 16) + viper.SetDefault("process-concurrency", int64(runtime.GOMAXPROCS(-1))) viper.SetDefault("eth2client.timeout", 2*time.Minute) viper.SetDefault("controller.max-attestation-delay", 4*time.Second) @@ -390,24 +396,91 @@ func startServices(ctx context.Context, majordomo majordomo.Service) error { return errors.Wrap(err, "failed to start beacon committee subscriber service") } + // Decide if the ETH2 client is capable of Altair. + altairCapable := false + spec, err := eth2Client.(eth2client.SpecProvider).Spec(ctx) + if err != nil { + return errors.Wrap(err, "failed to obtain spec") + } + if _, exists := spec["INACTIVITY_PENALTY_QUOTIENT_ALTAIR"]; exists { + altairCapable = true + log.Info().Msg("Client is Altair-capable") + } else { + log.Info().Msg("Client is not Altair-capable") + } + + // The following items are for Altair. These are optional. + var syncCommitteeSubscriber synccommitteesubscriber.Service + var syncCommitteeMessenger synccommitteemessenger.Service + var syncCommitteeAggregator synccommitteeaggregator.Service + if altairCapable { + log.Trace().Msg("Starting sync committee subscriber service") + syncCommitteeSubscriber, err = standardsynccommitteesubscriber.New(ctx, + standardsynccommitteesubscriber.WithLogLevel(logLevel(viper.GetString("synccommiteesubscriber.log-level"))), + standardsynccommitteesubscriber.WithMonitor(monitor.(metrics.SyncCommitteeSubscriptionMonitor)), + standardsynccommitteesubscriber.WithSyncCommitteeSubmitter(submitterStrategy.(submitter.SyncCommitteeSubscriptionsSubmitter)), + ) + if err != nil { + return errors.Wrap(err, "failed to start beacon committee subscriber service") + } + + log.Trace().Msg("Starting sync committee aggregator") + syncCommitteeAggregator, err = standardsynccommitteeaggregator.New(ctx, + standardsynccommitteeaggregator.WithLogLevel(logLevel(viper.GetString("synccommitteeaggregator.log-level"))), + standardsynccommitteeaggregator.WithMonitor(monitor.(metrics.SyncCommitteeAggregationMonitor)), + standardsynccommitteeaggregator.WithSpecProvider(eth2Client.(eth2client.SpecProvider)), + standardsynccommitteeaggregator.WithBeaconBlockRootProvider(eth2Client.(eth2client.BeaconBlockRootProvider)), + standardsynccommitteeaggregator.WithContributionAndProofSigner(signerSvc.(signer.ContributionAndProofSigner)), + standardsynccommitteeaggregator.WithValidatingAccountsProvider(accountManager.(accountmanager.ValidatingAccountsProvider)), + standardsynccommitteeaggregator.WithSyncCommitteeContributionProvider(eth2Client.(eth2client.SyncCommitteeContributionProvider)), + standardsynccommitteeaggregator.WithSyncCommitteeContributionsSubmitter(submitterStrategy.(submitter.SyncCommitteeContributionsSubmitter)), + ) + if err != nil { + return errors.Wrap(err, "failed to start sync committee aggregator service") + } + + log.Trace().Msg("Starting sync committee messenger") + syncCommitteeMessenger, err = standardsynccommitteemessenger.New(ctx, + standardsynccommitteemessenger.WithLogLevel(logLevel(viper.GetString("synccommitteemessenger.log-level"))), + standardsynccommitteemessenger.WithProcessConcurrency(viper.GetInt64("process-concurrency")), + standardsynccommitteemessenger.WithMonitor(monitor.(metrics.SyncCommitteeMessageMonitor)), + standardsynccommitteemessenger.WithSpecProvider(eth2Client.(eth2client.SpecProvider)), + standardsynccommitteemessenger.WithChainTimeService(chainTime), + standardsynccommitteemessenger.WithSyncCommitteeAggregator(syncCommitteeAggregator), + standardsynccommitteemessenger.WithBeaconBlockRootProvider(eth2Client.(eth2client.BeaconBlockRootProvider)), + standardsynccommitteemessenger.WithSyncCommitteeMessagesSubmitter(submitterStrategy.(submitter.SyncCommitteeMessagesSubmitter)), + standardsynccommitteemessenger.WithValidatingAccountsProvider(accountManager.(accountmanager.ValidatingAccountsProvider)), + standardsynccommitteemessenger.WithSyncCommitteeRootSigner(signerSvc.(signer.SyncCommitteeRootSigner)), + standardsynccommitteemessenger.WithSyncCommitteeSelectionSigner(signerSvc.(signer.SyncCommitteeSelectionSigner)), + standardsynccommitteemessenger.WithSyncCommitteeSubscriptionsSubmitter(submitterStrategy.(submitter.SyncCommitteeSubscriptionsSubmitter)), + ) + if err != nil { + return errors.Wrap(err, "failed to start sync committee messenger service") + } + } + log.Trace().Msg("Starting controller") _, err = standardcontroller.New(ctx, standardcontroller.WithLogLevel(logLevel(viper.GetString("controller.log-level"))), standardcontroller.WithMonitor(monitor.(metrics.ControllerMonitor)), - standardcontroller.WithSlotDurationProvider(eth2Client.(eth2client.SlotDurationProvider)), - standardcontroller.WithSlotsPerEpochProvider(eth2Client.(eth2client.SlotsPerEpochProvider)), + standardcontroller.WithSpecProvider(eth2Client.(eth2client.SpecProvider)), standardcontroller.WithChainTimeService(chainTime), standardcontroller.WithProposerDutiesProvider(eth2Client.(eth2client.ProposerDutiesProvider)), standardcontroller.WithAttesterDutiesProvider(eth2Client.(eth2client.AttesterDutiesProvider)), + standardcontroller.WithSyncCommitteeDutiesProvider(eth2Client.(eth2client.SyncCommitteeDutiesProvider)), standardcontroller.WithEventsProvider(eth2Client.(eth2client.EventsProvider)), standardcontroller.WithScheduler(scheduler), standardcontroller.WithValidatingAccountsProvider(accountManager.(accountmanager.ValidatingAccountsProvider)), standardcontroller.WithAttester(attester), + standardcontroller.WithSyncCommitteeMessenger(syncCommitteeMessenger), + standardcontroller.WithSyncCommitteeAggregator(syncCommitteeAggregator), standardcontroller.WithBeaconBlockProposer(beaconBlockProposer), standardcontroller.WithAttestationAggregator(attestationAggregator), standardcontroller.WithBeaconCommitteeSubscriber(beaconCommitteeSubscriber), + standardcontroller.WithSyncCommitteeSubscriber(syncCommitteeSubscriber), standardcontroller.WithAccountsRefresher(accountManager.(accountmanager.Refresher)), standardcontroller.WithMaxAttestationDelay(viper.GetDuration("controller.max-attestation-delay")), + standardcontroller.WithMaxSyncCommitteeMessageDelay(viper.GetDuration("controller.max-sync-committee-message-delay")), standardcontroller.WithReorgs(viper.GetBool("controller.reorgs")), ) if err != nil { @@ -597,12 +670,7 @@ func startSigner(ctx context.Context, monitor metrics.Service, eth2Client eth2cl standardsigner.WithLogLevel(logLevel(viper.GetString("signer.log-level"))), standardsigner.WithMonitor(monitor.(metrics.SignerMonitor)), standardsigner.WithClientMonitor(monitor.(metrics.ClientMonitor)), - standardsigner.WithSlotsPerEpochProvider(eth2Client.(eth2client.SlotsPerEpochProvider)), - standardsigner.WithBeaconProposerDomainTypeProvider(eth2Client.(eth2client.BeaconProposerDomainProvider)), - standardsigner.WithBeaconAttesterDomainTypeProvider(eth2Client.(eth2client.BeaconAttesterDomainProvider)), - standardsigner.WithRANDAODomainTypeProvider(eth2Client.(eth2client.RANDAODomainProvider)), - standardsigner.WithSelectionProofDomainTypeProvider(eth2Client.(eth2client.SelectionProofDomainProvider)), - standardsigner.WithAggregateAndProofDomainTypeProvider(eth2Client.(eth2client.AggregateAndProofDomainProvider)), + standardsigner.WithSpecProvider(eth2Client.(eth2client.SpecProvider)), standardsigner.WithDomainProvider(eth2Client.(eth2client.DomainProvider)), ) @@ -873,6 +941,9 @@ func selectSubmitterStrategy(ctx context.Context, monitor metrics.Service, eth2C attestationsSubmitters := make(map[string]eth2client.AttestationsSubmitter) aggregateAttestationSubmitters := make(map[string]eth2client.AggregateAttestationsSubmitter) beaconCommitteeSubscriptionsSubmitters := make(map[string]eth2client.BeaconCommitteeSubscriptionsSubmitter) + syncCommitteeMessagesSubmitters := make(map[string]eth2client.SyncCommitteeMessagesSubmitter) + syncCommitteeContributionsSubmitters := make(map[string]eth2client.SyncCommitteeContributionsSubmitter) + syncCommitteeSubscriptionsSubmitters := make(map[string]eth2client.SyncCommitteeSubscriptionsSubmitter) for _, address := range viper.GetStringSlice("submitter.beacon-node-addresses") { client, err := fetchClient(ctx, address) if err != nil { @@ -882,6 +953,9 @@ func selectSubmitterStrategy(ctx context.Context, monitor metrics.Service, eth2C attestationsSubmitters[address] = client.(eth2client.AttestationsSubmitter) aggregateAttestationSubmitters[address] = client.(eth2client.AggregateAttestationsSubmitter) beaconCommitteeSubscriptionsSubmitters[address] = client.(eth2client.BeaconCommitteeSubscriptionsSubmitter) + syncCommitteeMessagesSubmitters[address] = client.(eth2client.SyncCommitteeMessagesSubmitter) + syncCommitteeContributionsSubmitters[address] = client.(eth2client.SyncCommitteeContributionsSubmitter) + syncCommitteeSubscriptionsSubmitters[address] = client.(eth2client.SyncCommitteeSubscriptionsSubmitter) } submitter, err = multinodesubmitter.New(ctx, multinodesubmitter.WithClientMonitor(monitor.(metrics.ClientMonitor)), @@ -889,6 +963,9 @@ func selectSubmitterStrategy(ctx context.Context, monitor metrics.Service, eth2C multinodesubmitter.WithLogLevel(logLevel(viper.GetString("submitter.log-level"))), multinodesubmitter.WithBeaconBlockSubmitters(beaconBlockSubmitters), multinodesubmitter.WithAttestationsSubmitters(attestationsSubmitters), + multinodesubmitter.WithSyncCommitteeMessagesSubmitters(syncCommitteeMessagesSubmitters), + multinodesubmitter.WithSyncCommitteeContributionsSubmitters(syncCommitteeContributionsSubmitters), + multinodesubmitter.WithSyncCommitteeSubscriptionsSubmitters(syncCommitteeSubscriptionsSubmitters), multinodesubmitter.WithAggregateAttestationsSubmitters(aggregateAttestationSubmitters), multinodesubmitter.WithBeaconCommitteeSubscriptionsSubmitters(beaconCommitteeSubscriptionsSubmitters), ) @@ -899,6 +976,9 @@ func selectSubmitterStrategy(ctx context.Context, monitor metrics.Service, eth2C immediatesubmitter.WithClientMonitor(monitor.(metrics.ClientMonitor)), immediatesubmitter.WithBeaconBlockSubmitter(eth2Client.(eth2client.BeaconBlockSubmitter)), immediatesubmitter.WithAttestationsSubmitter(eth2Client.(eth2client.AttestationsSubmitter)), + immediatesubmitter.WithSyncCommitteeMessagesSubmitter(eth2Client.(eth2client.SyncCommitteeMessagesSubmitter)), + immediatesubmitter.WithSyncCommitteeContributionsSubmitter(eth2Client.(eth2client.SyncCommitteeContributionsSubmitter)), + immediatesubmitter.WithSyncCommitteeSubscriptionsSubmitter(eth2Client.(eth2client.SyncCommitteeSubscriptionsSubmitter)), immediatesubmitter.WithBeaconCommitteeSubscriptionsSubmitter(eth2Client.(eth2client.BeaconCommitteeSubscriptionsSubmitter)), immediatesubmitter.WithAggregateAttestationsSubmitter(eth2Client.(eth2client.AggregateAttestationsSubmitter)), ) diff --git a/mock/eth2client.go b/mock/eth2client.go index b9664b6..11cc00a 100644 --- a/mock/eth2client.go +++ b/mock/eth2client.go @@ -22,6 +22,8 @@ import ( eth2client "github.com/attestantio/go-eth2-client" api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/prysmaticlabs/go-bitfield" ) @@ -146,6 +148,97 @@ func (m *AttesterDutiesProvider) AttesterDuties(ctx context.Context, epoch phase return make([]*api.AttesterDuty, 0), nil } +// SyncCommitteeDutiesProvider is a mock for eth2client.SyncCommitteeDutiesProvider. +type SyncCommitteeDutiesProvider struct{} + +// NewSyncCommitteeDutiesProvider returns a mock attester duties provider. +func NewSyncCommitteeDutiesProvider() eth2client.SyncCommitteeDutiesProvider { + return &SyncCommitteeDutiesProvider{} +} + +// SyncCommitteeDuties is a mock. +func (m *SyncCommitteeDutiesProvider) SyncCommitteeDuties(ctx context.Context, epoch phase0.Epoch, validatorIndices []phase0.ValidatorIndex) ([]*api.SyncCommitteeDuty, error) { + return make([]*api.SyncCommitteeDuty, 0), nil +} + +// SyncCommitteeSubscriptionsSubmitter is a mock for eth2client.SyncCommitteeSubscriptionsSubmitter. +type SyncCommitteeSubscriptionsSubmitter struct{} + +// NewSyncCommitteeSubscriptionsSubmitter returns a mock attester duties submitter. +func NewSyncCommitteeSubscriptionsSubmitter() eth2client.SyncCommitteeSubscriptionsSubmitter { + return &SyncCommitteeSubscriptionsSubmitter{} +} + +// SubmitSyncCommitteeSubscriptions is a mock +func (m *SyncCommitteeSubscriptionsSubmitter) SubmitSyncCommitteeSubscriptions(ctx context.Context, subscriptions []*api.SyncCommitteeSubscription) error { + return nil +} + +// ErroringSyncCommitteeSubscriptionsSubmitter is a mock for eth2client.SyncCommitteeSubscriptionsSubmitter. +type ErroringSyncCommitteeSubscriptionsSubmitter struct{} + +// NewErroringSyncCommitteeSubscriptionsSubmitter returns a mock attester duties submitter. +func NewErroringSyncCommitteeSubscriptionsSubmitter() eth2client.SyncCommitteeSubscriptionsSubmitter { + return &ErroringSyncCommitteeSubscriptionsSubmitter{} +} + +// SubmitSyncCommitteeSubscriptions is a mock +func (m *ErroringSyncCommitteeSubscriptionsSubmitter) SubmitSyncCommitteeSubscriptions(ctx context.Context, subscriptions []*api.SyncCommitteeSubscription) error { + return errors.New("error") +} + +// SyncCommitteeMessagesSubmitter is a mock for eth2client.SyncCommitteeMessagesSubmitter. +type SyncCommitteeMessagesSubmitter struct{} + +// NewSyncCommitteeMessagesSubmitter returns a mock attester duties submitter. +func NewSyncCommitteeMessagesSubmitter() eth2client.SyncCommitteeMessagesSubmitter { + return &SyncCommitteeMessagesSubmitter{} +} + +// SubmitSyncCommitteeMessages submits sync committee messages. +func (m *SyncCommitteeMessagesSubmitter) SubmitSyncCommitteeMessages(ctx context.Context, messages []*altair.SyncCommitteeMessage) error { + return nil +} + +// ErroringSyncCommitteeMessagesSubmitter is a mock for eth2client.SyncCommitteeMessagesSubmitter. +type ErroringSyncCommitteeMessagesSubmitter struct{} + +// NewErroringSyncCommitteeMessagesSubmitter returns a mock attester duties submitter. +func NewErroringSyncCommitteeMessagesSubmitter() eth2client.SyncCommitteeMessagesSubmitter { + return &ErroringSyncCommitteeMessagesSubmitter{} +} + +// SubmitSyncCommitteeMessages submits sync committee messages. +func (m *ErroringSyncCommitteeMessagesSubmitter) SubmitSyncCommitteeMessages(ctx context.Context, messages []*altair.SyncCommitteeMessage) error { + return errors.New("error") +} + +// SyncCommitteeContributionsSubmitter is a mock for eth2client.SyncCommitteeContributionsSubmitter. +type SyncCommitteeContributionsSubmitter struct{} + +// NewSyncCommitteeContributionsSubmitter returns a mock attester duties submitter. +func NewSyncCommitteeContributionsSubmitter() eth2client.SyncCommitteeContributionsSubmitter { + return &SyncCommitteeContributionsSubmitter{} +} + +// SubmitSyncCommitteeContributions submits sync committee contributions. +func (m *SyncCommitteeContributionsSubmitter) SubmitSyncCommitteeContributions(ctx context.Context, contributionAndProofs []*altair.SignedContributionAndProof) error { + return nil +} + +// ErroringSyncCommitteeContributionsSubmitter is a mock for eth2client.SyncCommitteeContributionsSubmitter. +type ErroringSyncCommitteeContributionsSubmitter struct{} + +// NewErroringSyncCommitteeContributionsSubmitter returns a mock attester duties submitter. +func NewErroringSyncCommitteeContributionsSubmitter() eth2client.SyncCommitteeContributionsSubmitter { + return &ErroringSyncCommitteeContributionsSubmitter{} +} + +// SubmitSyncCommitteeContributions submits sync committee contributions. +func (m *ErroringSyncCommitteeContributionsSubmitter) SubmitSyncCommitteeContributions(ctx context.Context, contributionAndProofs []*altair.SignedContributionAndProof) error { + return errors.New("error") +} + // EventsProvider is a mock for eth2client.EventsProvider. type EventsProvider struct{} @@ -194,7 +287,7 @@ func NewBeaconBlockSubmitter() eth2client.BeaconBlockSubmitter { } // SubmitBeaconBlock is a mock. -func (m *BeaconBlockSubmitter) SubmitBeaconBlock(ctx context.Context, bloc *phase0.SignedBeaconBlock) error { +func (m *BeaconBlockSubmitter) SubmitBeaconBlock(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error { return nil } @@ -207,7 +300,7 @@ func NewErroringBeaconBlockSubmitter() eth2client.BeaconBlockSubmitter { } // SubmitBeaconBlock is a mock. -func (m *ErroringBeaconBlockSubmitter) SubmitBeaconBlock(ctx context.Context, bloc *phase0.SignedBeaconBlock) error { +func (m *ErroringBeaconBlockSubmitter) SubmitBeaconBlock(ctx context.Context, bloc *spec.VersionedSignedBeaconBlock) error { return errors.New("error") } @@ -272,7 +365,7 @@ func NewBeaconBlockProposalProvider() eth2client.BeaconBlockProposalProvider { } // BeaconBlockProposal is a mock. -func (m *BeaconBlockProposalProvider) BeaconBlockProposal(ctx context.Context, slot phase0.Slot, randaoReveal phase0.BLSSignature, graffiti []byte) (*phase0.BeaconBlock, error) { +func (m *BeaconBlockProposalProvider) BeaconBlockProposal(ctx context.Context, slot phase0.Slot, randaoReveal phase0.BLSSignature, graffiti []byte) (*spec.VersionedBeaconBlock, error) { // Graffiti should be 32 bytes. fixedGraffiti := make([]byte, 32) copy(fixedGraffiti, graffiti) @@ -319,36 +412,39 @@ func (m *BeaconBlockProposalProvider) BeaconBlockProposal(ctx context.Context, s } } - block := &phase0.BeaconBlock{ - Slot: slot, - ProposerIndex: 1, - ParentRoot: phase0.Root([32]byte{ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, - }), - StateRoot: phase0.Root([32]byte{ - 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, - }), - Body: &phase0.BeaconBlockBody{ - RANDAOReveal: randaoReveal, - ETH1Data: &phase0.ETH1Data{ - DepositRoot: phase0.Root([32]byte{ - 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, - 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, - }), - DepositCount: 16384, - BlockHash: []byte{ - 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, - 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, + block := &spec.VersionedBeaconBlock{ + Version: spec.DataVersionPhase0, + Phase0: &phase0.BeaconBlock{ + Slot: slot, + ProposerIndex: 1, + ParentRoot: phase0.Root([32]byte{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + }), + StateRoot: phase0.Root([32]byte{ + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + }), + Body: &phase0.BeaconBlockBody{ + RANDAOReveal: randaoReveal, + ETH1Data: &phase0.ETH1Data{ + DepositRoot: phase0.Root([32]byte{ + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + }), + DepositCount: 16384, + BlockHash: []byte{ + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, + }, }, + Graffiti: fixedGraffiti, + ProposerSlashings: []*phase0.ProposerSlashing{}, + AttesterSlashings: []*phase0.AttesterSlashing{}, + Attestations: attestations, + Deposits: []*phase0.Deposit{}, + VoluntaryExits: []*phase0.SignedVoluntaryExit{}, }, - Graffiti: fixedGraffiti, - ProposerSlashings: []*phase0.ProposerSlashing{}, - AttesterSlashings: []*phase0.AttesterSlashing{}, - Attestations: attestations, - Deposits: []*phase0.Deposit{}, - VoluntaryExits: []*phase0.SignedVoluntaryExit{}, }, } @@ -364,10 +460,13 @@ func NewSignedBeaconBlockProvider() eth2client.SignedBeaconBlockProvider { } // SignedBeaconBlock is a mock. -func (m *SignedBeaconBlockProvider) SignedBeaconBlock(ctx context.Context, stateID string) (*phase0.SignedBeaconBlock, error) { - return &phase0.SignedBeaconBlock{ - Message: &phase0.BeaconBlock{ - Slot: 123, +func (m *SignedBeaconBlockProvider) SignedBeaconBlock(ctx context.Context, stateID string) (*spec.VersionedSignedBeaconBlock, error) { + return &spec.VersionedSignedBeaconBlock{ + Version: spec.DataVersionPhase0, + Phase0: &phase0.SignedBeaconBlock{ + Message: &phase0.BeaconBlock{ + Slot: 123, + }, }, }, nil } @@ -550,186 +649,48 @@ func (m *SleepyAggregateAttestationProvider) AggregateAttestation(ctx context.Co return m.next.AggregateAttestation(ctx, slot, attestationDataRoot) } -// BeaconProposerDomainProvider is a mock for eth2client.BeaconProposerDomainProvider. -type BeaconProposerDomainProvider struct{} +// ErroringSpecProvider is a mock for eth2client.SpecProvider. +type ErroringSpecProvider struct{} -// NewBeaconProposerDomainProvider returns a mock beacon proposer domain provider. -func NewBeaconProposerDomainProvider() eth2client.BeaconProposerDomainProvider { - return &BeaconProposerDomainProvider{} +// NewErroringSpecProvider returns a mock spec provider. +func NewErroringSpecProvider() eth2client.SpecProvider { + return &ErroringSpecProvider{} } -// BeaconProposerDomain is a mock. -func (m *BeaconProposerDomainProvider) BeaconProposerDomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{0x00, 0x00, 0x00, 0x00}, nil +// Spec is a mock. +func (m *ErroringSpecProvider) Spec(ctx context.Context) (map[string]interface{}, error) { + return nil, errors.New("error") } -// ErroringBeaconProposerDomainProvider is a mock for eth2client.BeaconProposerDomainProvider. -type ErroringBeaconProposerDomainProvider struct{} +// SpecProvider is a mock for eth2client.SpecProvider. +type SpecProvider struct{} -// NewErroringBeaconProposerDomainProvider returns a mock beacon proposer domain provider that errors. -func NewErroringBeaconProposerDomainProvider() eth2client.BeaconProposerDomainProvider { - return &ErroringBeaconProposerDomainProvider{} +// NewSpecProvider returns a mock spec provider. +func NewSpecProvider() eth2client.SpecProvider { + return &SpecProvider{} } -// BeaconProposerDomain is a mock. -func (m *ErroringBeaconProposerDomainProvider) BeaconProposerDomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{}, errors.New("error") -} - -// BeaconAttesterDomainProvider is a mock for eth2client.BeaconAttesterDomainProvider. -type BeaconAttesterDomainProvider struct{} - -// NewBeaconAttesterDomainProvider returns a mock beacon attester domain provider. -func NewBeaconAttesterDomainProvider() eth2client.BeaconAttesterDomainProvider { - return &BeaconAttesterDomainProvider{} -} - -// BeaconAttesterDomain is a mock. -func (m *BeaconAttesterDomainProvider) BeaconAttesterDomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{0x01, 0x00, 0x00, 0x00}, nil -} - -// ErroringBeaconAttesterDomainProvider is a mock for eth2client.BeaconAttesterDomainProvider. -type ErroringBeaconAttesterDomainProvider struct{} - -// NewErroringBeaconAttesterDomainProvider returns a mock beacon attester domain provider that errors. -func NewErroringBeaconAttesterDomainProvider() eth2client.BeaconAttesterDomainProvider { - return &ErroringBeaconAttesterDomainProvider{} -} - -// BeaconAttesterDomain is a mock. -func (m *ErroringBeaconAttesterDomainProvider) BeaconAttesterDomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{}, errors.New("error") -} - -// RANDAODomainProvider is a mock for eth2client.RANDAODomainProvider. -type RANDAODomainProvider struct{} - -// NewRANDAODomainProvider returns a mock RANDAO domain provider. -func NewRANDAODomainProvider() eth2client.RANDAODomainProvider { - return &RANDAODomainProvider{} -} - -// RANDAODomain is a mock. -func (m *RANDAODomainProvider) RANDAODomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{0x02, 0x00, 0x00, 0x00}, nil -} - -// ErroringRANDAODomainProvider is a mock for eth2client.RANDAODomainProvider. -type ErroringRANDAODomainProvider struct{} - -// NewErroringRANDAODomainProvider returns a mock RANDAO domain provider that errors. -func NewErroringRANDAODomainProvider() eth2client.RANDAODomainProvider { - return &ErroringRANDAODomainProvider{} -} - -// RANDAODomain is a mock. -func (m *ErroringRANDAODomainProvider) RANDAODomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{}, errors.New("error") -} - -// DepositDomainProvider is a mock for eth2client.DepositDomainProvider. -type DepositDomainProvider struct{} - -// NewDepositDomainProvider returns a mock deposit domain provider. -func NewDepositDomainProvider() eth2client.DepositDomainProvider { - return &DepositDomainProvider{} -} - -// DepositDomain is a mock. -func (m *DepositDomainProvider) DepositDomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{0x03, 0x00, 0x00, 0x00}, nil -} - -// ErroringDepositDomainProvider is a mock for eth2client.DepositDomainProvider. -type ErroringDepositDomainProvider struct{} - -// NewErroringDepositDomainProvider returns a mock deposit domain provider that errors. -func NewErroringDepositDomainProvider() eth2client.DepositDomainProvider { - return &DepositDomainProvider{} -} - -// DepositDomain is a mock. -func (m *ErroringDepositDomainProvider) DepositDomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{}, errors.New("error") -} - -// VoluntaryExitDomainProvider is a mock for eth2client.VoluntaryExitDomainProvider. -type VoluntaryExitDomainProvider struct{} - -// NewVoluntaryExitDomainProvider returns a mock voluntary exit domain provider. -func NewVoluntaryExitDomainProvider() eth2client.VoluntaryExitDomainProvider { - return &VoluntaryExitDomainProvider{} -} - -// VoluntaryExitDomain is a mock. -func (m *VoluntaryExitDomainProvider) VoluntaryExitDomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{0x04, 0x00, 0x00, 0x00}, nil -} - -// ErroringVoluntaryExitDomainProvider is a mock for eth2client.VoluntaryExitDomainProvider. -type ErroringVoluntaryExitDomainProvider struct{} - -// NewErroringVoluntaryExitDomainProvider returns a mock voluntary exit domain provider that errors. -func NewErroringVoluntaryExitDomainProvider() eth2client.VoluntaryExitDomainProvider { - return &VoluntaryExitDomainProvider{} -} - -// VoluntaryExitDomain is a mock. -func (m *ErroringVoluntaryExitDomainProvider) VoluntaryExitDomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{}, errors.New("error") -} - -// SelectionProofDomainProvider is a mock for eth2client.SelectionProofDomainProvider. -type SelectionProofDomainProvider struct{} - -// NewSelectionProofDomainProvider returns a mock selection proof domain provider. -func NewSelectionProofDomainProvider() eth2client.SelectionProofDomainProvider { - return &SelectionProofDomainProvider{} -} - -// SelectionProofDomain is a mock. -func (m *SelectionProofDomainProvider) SelectionProofDomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{0x05, 0x00, 0x00, 0x00}, nil -} - -// ErroringSelectionProofDomainProvider is a mock for eth2client.SelectionProofDomainProvider. -type ErroringSelectionProofDomainProvider struct{} - -// NewErroringSelectionProofDomainProvider returns a mock selection proof domain provider that errors. -func NewErroringSelectionProofDomainProvider() eth2client.SelectionProofDomainProvider { - return &ErroringSelectionProofDomainProvider{} -} - -// SelectionProofDomain is a mock. -func (m *ErroringSelectionProofDomainProvider) SelectionProofDomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{}, errors.New("error") -} - -// AggregateAndProofDomainProvider is a mock for eth2client.AggregateAndProofDomainProvider. -type AggregateAndProofDomainProvider struct{} - -// NewAggregateAndProofDomainProvider returns a mock aggregate and proof domain provider. -func NewAggregateAndProofDomainProvider() eth2client.AggregateAndProofDomainProvider { - return &AggregateAndProofDomainProvider{} -} - -// AggregateAndProofDomain is a mock. -func (m *AggregateAndProofDomainProvider) AggregateAndProofDomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{0x06, 0x00, 0x00, 0x00}, nil -} - -// ErroringAggregateAndProofDomainProvider is a mock for eth2client.AggregateAndProofDomainProvider. -type ErroringAggregateAndProofDomainProvider struct{} - -// NewErroringAggregateAndProofDomainProvider returns a mock aggregate and proof domain provider that errors. -func NewErroringAggregateAndProofDomainProvider() eth2client.AggregateAndProofDomainProvider { - return &ErroringAggregateAndProofDomainProvider{} -} - -// AggregateAndProofDomain is a mock. -func (m *ErroringAggregateAndProofDomainProvider) AggregateAndProofDomain(ctx context.Context) (phase0.DomainType, error) { - return phase0.DomainType{}, errors.New("error") +// Spec is a mock. +func (m *SpecProvider) Spec(ctx context.Context) (map[string]interface{}, error) { + return map[string]interface{}{ + // Mainnet params (give or take). + "DOMAIN_AGGREGATE_AND_PROOF": phase0.DomainType{0x06, 0x00, 0x00, 0x00}, + "DOMAIN_BEACON_ATTESTER": phase0.DomainType{0x00, 0x00, 0x00, 0x00}, + "DOMAIN_BEACON_PROPOSER": phase0.DomainType{0x01, 0x00, 0x00, 0x00}, + "DOMAIN_CONTRIBUTION_AND_PROOF": phase0.DomainType{0x09, 0x00, 0x00, 0x00}, + "DOMAIN_DEPOSIT": phase0.DomainType{0x03, 0x00, 0x00, 0x00}, + "DOMAIN_RANDAO": phase0.DomainType{0x02, 0x00, 0x00, 0x00}, + "DOMAIN_SELECTION_PROOF": phase0.DomainType{0x05, 0x00, 0x00, 0x00}, + "DOMAIN_SYNC_COMMITTEE": phase0.DomainType{0x07, 0x00, 0x00, 0x00}, + "DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF": phase0.DomainType{0x08, 0x00, 0x00, 0x00}, + "DOMAIN_VOLUNTARY_EXIT": phase0.DomainType{0x04, 0x00, 0x00, 0x00}, + "EPOCHS_PER_SYNC_COMMITTEE_PERIOD": uint64(256), + "SECONDS_PER_SLOT": 12 * time.Second, + "SLOTS_PER_EPOCH": uint64(32), + "SYNC_COMMITTEE_SIZE": uint64(512), + "SYNC_COMMITTEE_SUBNET_COUNT": uint64(4), + "TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE": uint64(16), + }, nil } // DomainProvider is a mock for eth2client.DomainProvider. diff --git a/services/accountmanager/dirk/parameters.go b/services/accountmanager/dirk/parameters.go index b97c109..4efac14 100644 --- a/services/accountmanager/dirk/parameters.go +++ b/services/accountmanager/dirk/parameters.go @@ -156,6 +156,9 @@ func parseAndCheckParameters(params ...Parameter) (*parameters, error) { } } + if parameters.processConcurrency == 0 { + return nil, errors.New("no process concurrency specified") + } if parameters.monitor == nil { return nil, errors.New("no monitor specified") } diff --git a/services/accountmanager/dirk/service.go b/services/accountmanager/dirk/service.go index b7b53f1..3696e41 100644 --- a/services/accountmanager/dirk/service.go +++ b/services/accountmanager/dirk/service.go @@ -1,4 +1,4 @@ -// Copyright © 2020 Attestant Limited. +// Copyright © 2020, 2021 Attestant Limited. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/services/beaconblockproposer/standard/service.go b/services/beaconblockproposer/standard/service.go index 3b721aa..98ca79f 100644 --- a/services/beaconblockproposer/standard/service.go +++ b/services/beaconblockproposer/standard/service.go @@ -19,6 +19,8 @@ import ( "time" eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/attestantio/vouch/services/accountmanager" "github.com/attestantio/vouch/services/beaconblockproposer" @@ -156,25 +158,46 @@ func (s *Service) Propose(ctx context.Context, data interface{}) { } log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained proposal") - if proposal.Slot != duty.Slot() { - log.Error().Uint64("proposal_slot", uint64(proposal.Slot)).Msg("Proposal data for incorrect slot; not proceeding") + proposalSlot, err := proposal.Slot() + if err != nil { + log.Error().Str("version", proposal.Version.String()).Err(err).Msg("Unknown proposal version") s.monitor.BeaconBlockProposalCompleted(started, "failed") return } - bodyRoot, err := proposal.Body.HashTreeRoot() + if proposalSlot != duty.Slot() { + log.Error().Uint64("proposal_slot", uint64(proposalSlot)).Msg("Proposal data for incorrect slot; not proceeding") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + + bodyRoot, err := proposal.BodyRoot() if err != nil { log.Error().Err(err).Msg("Failed to calculate hash tree root of block") s.monitor.BeaconBlockProposalCompleted(started, "failed") return } + parentRoot, err := proposal.ParentRoot() + if err != nil { + log.Error().Err(err).Msg("Failed to obtain parent root of block") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + + stateRoot, err := proposal.StateRoot() + if err != nil { + log.Error().Err(err).Msg("Failed to obtain state root of block") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return + } + sig, err := s.beaconBlockSigner.SignBeaconBlockProposal(ctx, duty.Account(), - proposal.Slot, + proposalSlot, duty.ValidatorIndex(), - proposal.ParentRoot, - proposal.StateRoot, + parentRoot, + stateRoot, bodyRoot) if err != nil { log.Error().Err(err).Msg("Failed to sign beacon block proposal") @@ -183,9 +206,24 @@ func (s *Service) Propose(ctx context.Context, data interface{}) { } log.Trace().Dur("elapsed", time.Since(started)).Msg("Signed proposal") - signedBlock := &phase0.SignedBeaconBlock{ - Message: proposal, - Signature: sig, + signedBlock := &spec.VersionedSignedBeaconBlock{ + Version: proposal.Version, + } + switch signedBlock.Version { + case spec.DataVersionPhase0: + signedBlock.Phase0 = &phase0.SignedBeaconBlock{ + Message: proposal.Phase0, + Signature: sig, + } + case spec.DataVersionAltair: + signedBlock.Altair = &altair.SignedBeaconBlock{ + Message: proposal.Altair, + Signature: sig, + } + default: + log.Error().Str("version", proposal.Version.String()).Msg("Unknown proposal version") + s.monitor.BeaconBlockProposalCompleted(started, "failed") + return } // Submit the block. diff --git a/services/controller/standard/attester.go b/services/controller/standard/attester.go index 535f2bb..4783e00 100644 --- a/services/controller/standard/attester.go +++ b/services/controller/standard/attester.go @@ -96,7 +96,7 @@ func (s *Service) scheduleAttestations(ctx context.Context, } go func(duty *attester.Duty) { // Adding 200 ms to ensure that head is up to date before we fetch attester duties. - jobTime := s.chainTimeService.StartOfSlot(duty.Slot()).Add(s.maxAttestationDelay).Add(200 * time.Millisecond) + jobTime := s.chainTimeService.StartOfSlot(duty.Slot()).Add(s.maxSyncCommitteeMessageDelay).Add(200 * time.Millisecond) if err := s.scheduler.ScheduleJob(ctx, fmt.Sprintf("Attestations for slot %d", duty.Slot()), jobTime, diff --git a/services/controller/standard/events.go b/services/controller/standard/events.go index 2f65968..39a9881 100644 --- a/services/controller/standard/events.go +++ b/services/controller/standard/events.go @@ -1,4 +1,4 @@ -// Copyright © 2020 Attestant Limited. +// Copyright © 2020, 2021 Attestant Limited. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -105,6 +105,11 @@ func (s *Service) HandleHeadEvent(event *api.Event) { log.Trace().Msg("Kicking off attestations for slot early due to receiving relevant block") s.scheduler.RunJobIfExists(ctx, jobName) } + jobName = fmt.Sprintf("Sync committee contributions for slot %d", data.Slot) + if s.scheduler.JobExists(ctx, jobName) { + log.Trace().Msg("Kicking off sync committee contributions for slot early due to receiving relevant block") + s.scheduler.RunJobIfExists(ctx, jobName) + } // Remove old subscriptions if present. delete(s.subscriptionInfos, s.chainTimeService.SlotToEpoch(data.Slot)-2) @@ -113,17 +118,29 @@ func (s *Service) HandleHeadEvent(event *api.Event) { // handlePreviousDependentRootChanged handles the situation where the previous // dependent root changed. func (s *Service) handlePreviousDependentRootChanged(ctx context.Context) { + // Refreshes run in parallel. + // We need to refresh the attester duties for this epoch. - s.refreshAttesterDutiesForEpoch(ctx, s.chainTimeService.CurrentEpoch()) + go s.refreshAttesterDutiesForEpoch(ctx, s.chainTimeService.CurrentEpoch()) } // handlePreviousDependentRootChanged handles the situation where the current // dependent root changed. func (s *Service) handleCurrentDependentRootChanged(ctx context.Context) { + // Refreshes run in parallel. + // We need to refresh the proposer duties for this epoch. - s.refreshProposerDutiesForEpoch(ctx, s.chainTimeService.CurrentEpoch()) + go s.refreshProposerDutiesForEpoch(ctx, s.chainTimeService.CurrentEpoch()) + // We need to refresh the sync committee duties for this epoch if we are + // at the appropriate boundary. + if uint64(s.chainTimeService.CurrentEpoch())%s.epochsPerSyncCommitteePeriod == 0 { + // TODO is this correct? + // Check if this is the correct sync committee period (should it be the next one?) + // Check if this should only be recalculated on the sync committee period boundary. + go s.refreshSyncCommitteeDutiesForEpoch(ctx, s.chainTimeService.CurrentEpoch()) + } // We need to refresh the attester duties for the next epoch. - s.refreshAttesterDutiesForEpoch(ctx, s.chainTimeService.CurrentEpoch()+1) + go s.refreshAttesterDutiesForEpoch(ctx, s.chainTimeService.CurrentEpoch()+1) } func (s *Service) refreshProposerDutiesForEpoch(ctx context.Context, epoch phase0.Epoch) { @@ -181,3 +198,36 @@ func (s *Service) refreshAttesterDutiesForEpoch(ctx context.Context, epoch phase s.subscriptionInfos[epoch] = subscriptionInfo s.subscriptionInfosMutex.Unlock() } + +// TODO this should refresh for the entire period. +func (s *Service) refreshSyncCommitteeDutiesForEpoch(ctx context.Context, epoch phase0.Epoch) { + if !s.handlingAltair { + // Not handling Altair, nothing to do. + return + } + + // First thing we do is cancel all scheduled sync committee message jobs. + firstSlot := s.chainTimeService.FirstSlotOfEpoch(epoch) + syncCommitteePeriod := uint64(s.chainTimeService.SlotToEpoch(firstSlot)) / s.epochsPerSyncCommitteePeriod + lastSlot := s.chainTimeService.FirstSlotOfEpoch(phase0.Epoch((syncCommitteePeriod+1)*s.epochsPerSyncCommitteePeriod)) - 1 + for slot := firstSlot; slot <= lastSlot; slot++ { + if err := s.scheduler.CancelJob(ctx, fmt.Sprintf("Sync committee messages for slot %d", slot)); err != nil { + log.Debug().Err(err).Msg("Failed to cancel sync committee message job") + } + } + + _, validatorIndices, err := s.accountsAndIndicesForEpoch(ctx, epoch) + if err != nil { + log.Error().Err(err).Uint64("epoch", uint64(epoch)).Msg("Failed to obtain active validators for epoch") + return + } + + // Expect at least one validator. + if len(validatorIndices) == 0 { + log.Warn().Msg("No active validators; not validating") + return + } + + // Reschedule sync committee messages. + go s.scheduleSyncCommitteeMessages(ctx, epoch, validatorIndices) +} diff --git a/services/controller/standard/parameters.go b/services/controller/standard/parameters.go index 1eee7ed..0859150 100644 --- a/services/controller/standard/parameters.go +++ b/services/controller/standard/parameters.go @@ -14,6 +14,7 @@ package standard import ( + "context" "time" eth2client "github.com/attestantio/go-eth2-client" @@ -25,28 +26,35 @@ import ( "github.com/attestantio/vouch/services/chaintime" "github.com/attestantio/vouch/services/metrics" "github.com/attestantio/vouch/services/scheduler" + "github.com/attestantio/vouch/services/synccommitteeaggregator" + "github.com/attestantio/vouch/services/synccommitteemessenger" + "github.com/attestantio/vouch/services/synccommitteesubscriber" "github.com/pkg/errors" "github.com/rs/zerolog" ) type parameters struct { - logLevel zerolog.Level - monitor metrics.ControllerMonitor - slotDurationProvider eth2client.SlotDurationProvider - slotsPerEpochProvider eth2client.SlotsPerEpochProvider - chainTimeService chaintime.Service - proposerDutiesProvider eth2client.ProposerDutiesProvider - attesterDutiesProvider eth2client.AttesterDutiesProvider - validatingAccountsProvider accountmanager.ValidatingAccountsProvider - scheduler scheduler.Service - eventsProvider eth2client.EventsProvider - attester attester.Service - beaconBlockProposer beaconblockproposer.Service - attestationAggregator attestationaggregator.Service - beaconCommitteeSubscriber beaconcommitteesubscriber.Service - accountsRefresher accountmanager.Refresher - maxAttestationDelay time.Duration - reorgs bool + logLevel zerolog.Level + monitor metrics.ControllerMonitor + specProvider eth2client.SpecProvider + chainTimeService chaintime.Service + proposerDutiesProvider eth2client.ProposerDutiesProvider + attesterDutiesProvider eth2client.AttesterDutiesProvider + syncCommitteeDutiesProvider eth2client.SyncCommitteeDutiesProvider + syncCommitteesSubscriber synccommitteesubscriber.Service + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + scheduler scheduler.Service + eventsProvider eth2client.EventsProvider + attester attester.Service + syncCommitteeMessenger synccommitteemessenger.Service + syncCommitteeAggregator synccommitteeaggregator.Service + beaconBlockProposer beaconblockproposer.Service + attestationAggregator attestationaggregator.Service + beaconCommitteeSubscriber beaconcommitteesubscriber.Service + accountsRefresher accountmanager.Refresher + maxAttestationDelay time.Duration + maxSyncCommitteeMessageDelay time.Duration + reorgs bool } // Parameter is the interface for service parameters. @@ -74,17 +82,10 @@ func WithMonitor(monitor metrics.ControllerMonitor) Parameter { }) } -// WithSlotDurationProvider sets the slot duration provider. -func WithSlotDurationProvider(provider eth2client.SlotDurationProvider) Parameter { +// WithSpecProvider sets the spec provider. +func WithSpecProvider(provider eth2client.SpecProvider) Parameter { return parameterFunc(func(p *parameters) { - p.slotDurationProvider = provider - }) -} - -// WithSlotsPerEpochProvider sets the slots per epoch provider. -func WithSlotsPerEpochProvider(provider eth2client.SlotsPerEpochProvider) Parameter { - return parameterFunc(func(p *parameters) { - p.slotsPerEpochProvider = provider + p.specProvider = provider }) } @@ -109,6 +110,20 @@ func WithAttesterDutiesProvider(provider eth2client.AttesterDutiesProvider) Para }) } +// WithSyncCommitteeDutiesProvider sets the sync committee duties provider. +func WithSyncCommitteeDutiesProvider(provider eth2client.SyncCommitteeDutiesProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeDutiesProvider = provider + }) +} + +// WithSyncCommitteeSubscriber sets the sync committee subscriber. +func WithSyncCommitteeSubscriber(subscriber synccommitteesubscriber.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteesSubscriber = subscriber + }) +} + // WithEventsProvider sets the events provider. func WithEventsProvider(provider eth2client.EventsProvider) Parameter { return parameterFunc(func(p *parameters) { @@ -137,6 +152,20 @@ func WithAttester(attester attester.Service) Parameter { }) } +// WithSyncCommitteeMessenger sets the sync committee messenger. +func WithSyncCommitteeMessenger(messenger synccommitteemessenger.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeMessenger = messenger + }) +} + +// WithSyncCommitteeAggregator sets the sync committee aggregator. +func WithSyncCommitteeAggregator(aggregator synccommitteeaggregator.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeAggregator = aggregator + }) +} + // WithBeaconBlockProposer sets the beacon block propser. func WithBeaconBlockProposer(proposer beaconblockproposer.Service) Parameter { return parameterFunc(func(p *parameters) { @@ -172,6 +201,13 @@ func WithMaxAttestationDelay(delay time.Duration) Parameter { }) } +// WithMaxSyncCommitteeMessageDelay sets the maximum delay before generating sync committee messages. +func WithMaxSyncCommitteeMessageDelay(delay time.Duration) Parameter { + return parameterFunc(func(p *parameters) { + p.maxSyncCommitteeMessageDelay = delay + }) +} + // WithReorgs sets or unsets reorgs. func WithReorgs(reorgs bool) Parameter { return parameterFunc(func(p *parameters) { @@ -182,8 +218,7 @@ func WithReorgs(reorgs bool) Parameter { // parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. func parseAndCheckParameters(params ...Parameter) (*parameters, error) { parameters := parameters{ - logLevel: zerolog.GlobalLevel(), - maxAttestationDelay: 4 * time.Second, + logLevel: zerolog.GlobalLevel(), } for _, p := range params { if params != nil { @@ -194,11 +229,8 @@ func parseAndCheckParameters(params ...Parameter) (*parameters, error) { if parameters.monitor == nil { return nil, errors.New("no monitor specified") } - if parameters.slotDurationProvider == nil { - return nil, errors.New("no slot duration provider specified") - } - if parameters.slotsPerEpochProvider == nil { - return nil, errors.New("no slots per epoch provider specified") + if parameters.specProvider == nil { + return nil, errors.New("no spec provider specified") } if parameters.chainTimeService == nil { return nil, errors.New("no chain time service specified") @@ -233,9 +265,41 @@ func parseAndCheckParameters(params ...Parameter) (*parameters, error) { if parameters.accountsRefresher == nil { return nil, errors.New("no accounts refresher specified") } + var spec map[string]interface{} + var err error if parameters.maxAttestationDelay == 0 { - return nil, errors.New("no maximum attestation delay specified") + spec, err = parameters.specProvider.Spec(context.Background()) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain spec") + } + tmp, exists := spec["SECONDS_PER_SLOT"] + if !exists { + return nil, errors.New("SECONDS_PER_SLOT not found in spec") + } + slotDuration, ok := tmp.(time.Duration) + if !ok { + return nil, errors.New("SECONDS_PER_SLOT of unexpected type") + } + parameters.maxAttestationDelay = slotDuration / 3 } + if parameters.maxSyncCommitteeMessageDelay == 0 { + if spec == nil { + spec, err = parameters.specProvider.Spec(context.Background()) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain spec") + } + } + tmp, exists := spec["SECONDS_PER_SLOT"] + if !exists { + return nil, errors.New("SECONDS_PER_SLOT not found in spec") + } + slotDuration, ok := tmp.(time.Duration) + if !ok { + return nil, errors.New("SECONDS_PER_SLOT of unexpected type") + } + parameters.maxSyncCommitteeMessageDelay = slotDuration / 3 + } + // Sync committee duties provider/messenger/aggregator/subscriber are optional so no checks here. return ¶meters, nil } diff --git a/services/controller/standard/service.go b/services/controller/standard/service.go index 60ab0e7..d99c1c2 100644 --- a/services/controller/standard/service.go +++ b/services/controller/standard/service.go @@ -29,6 +29,9 @@ import ( "github.com/attestantio/vouch/services/chaintime" "github.com/attestantio/vouch/services/metrics" "github.com/attestantio/vouch/services/scheduler" + "github.com/attestantio/vouch/services/synccommitteeaggregator" + "github.com/attestantio/vouch/services/synccommitteemessenger" + "github.com/attestantio/vouch/services/synccommitteesubscriber" "github.com/pkg/errors" "github.com/rs/zerolog" zerologger "github.com/rs/zerolog/log" @@ -39,24 +42,32 @@ import ( // It runs purely against clock events, setting up jobs for the validator's processes of block proposal, attestation // creation and attestation aggregation. type Service struct { - monitor metrics.ControllerMonitor - slotDuration time.Duration - slotsPerEpoch uint64 - chainTimeService chaintime.Service - proposerDutiesProvider eth2client.ProposerDutiesProvider - attesterDutiesProvider eth2client.AttesterDutiesProvider - validatingAccountsProvider accountmanager.ValidatingAccountsProvider - scheduler scheduler.Service - attester attester.Service - beaconBlockProposer beaconblockproposer.Service - attestationAggregator attestationaggregator.Service - beaconCommitteeSubscriber beaconcommitteesubscriber.Service - activeValidators int - subscriptionInfos map[phase0.Epoch]map[phase0.Slot]map[phase0.CommitteeIndex]*beaconcommitteesubscriber.Subscription - subscriptionInfosMutex sync.Mutex - accountsRefresher accountmanager.Refresher - maxAttestationDelay time.Duration - reorgs bool + monitor metrics.ControllerMonitor + slotDuration time.Duration + slotsPerEpoch uint64 + epochsPerSyncCommitteePeriod uint64 + chainTimeService chaintime.Service + proposerDutiesProvider eth2client.ProposerDutiesProvider + attesterDutiesProvider eth2client.AttesterDutiesProvider + syncCommitteeDutiesProvider eth2client.SyncCommitteeDutiesProvider + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + scheduler scheduler.Service + attester attester.Service + syncCommitteeMessenger synccommitteemessenger.Service + syncCommitteeAggregator synccommitteeaggregator.Service + syncCommitteesSubscriber synccommitteesubscriber.Service + beaconBlockProposer beaconblockproposer.Service + attestationAggregator attestationaggregator.Service + beaconCommitteeSubscriber beaconcommitteesubscriber.Service + activeValidators int + subscriptionInfos map[phase0.Epoch]map[phase0.Slot]map[phase0.CommitteeIndex]*beaconcommitteesubscriber.Subscription + subscriptionInfosMutex sync.Mutex + accountsRefresher accountmanager.Refresher + maxSyncCommitteeMessageDelay time.Duration + reorgs bool + + // Hard fork control + handlingAltair bool // Tracking for reorgs. lastBlockRoot phase0.Root @@ -81,33 +92,64 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) { log = log.Level(parameters.logLevel) } - slotDuration, err := parameters.slotDurationProvider.SlotDuration(ctx) + spec, err := parameters.specProvider.Spec(ctx) if err != nil { - return nil, errors.Wrap(err, "failed to obtain slot duration") + return nil, errors.Wrap(err, "failed to obtain spec") } - slotsPerEpoch, err := parameters.slotsPerEpochProvider.SlotsPerEpoch(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to obtain slots per epoch") + tmp, exists := spec["SECONDS_PER_SLOT"] + if !exists { + return nil, errors.New("SECONDS_PER_SLOT not found in spec") } + slotDuration, ok := tmp.(time.Duration) + if !ok { + return nil, errors.New("SECONDS_PER_SLOT of unexpected type") + } + + tmp, exists = spec["SLOTS_PER_EPOCH"] + if !exists { + return nil, errors.New("SLOTS_PER_EPOCH not found in spec") + } + slotsPerEpoch, ok := tmp.(uint64) + if !ok { + return nil, errors.New("SLOTS_PER_EPOCH of unexpected type") + } + + var epochsPerSyncCommitteePeriod uint64 + if tmp, exists := spec["EPOCHS_PER_SYNC_COMMITTEE_PERIOD"]; exists { + tmp2, ok := tmp.(uint64) + if !ok { + return nil, errors.New("EPOCHS_PER_SYNC_COMMITTEE_PERIOD of unexpected type") + } + epochsPerSyncCommitteePeriod = tmp2 + } + + // Handling altair if we have the service and spec to do so. + handlingAltair := parameters.syncCommitteeAggregator != nil && epochsPerSyncCommitteePeriod != 0 s := &Service{ - monitor: parameters.monitor, - slotDuration: slotDuration, - slotsPerEpoch: slotsPerEpoch, - chainTimeService: parameters.chainTimeService, - proposerDutiesProvider: parameters.proposerDutiesProvider, - attesterDutiesProvider: parameters.attesterDutiesProvider, - validatingAccountsProvider: parameters.validatingAccountsProvider, - scheduler: parameters.scheduler, - attester: parameters.attester, - beaconBlockProposer: parameters.beaconBlockProposer, - attestationAggregator: parameters.attestationAggregator, - beaconCommitteeSubscriber: parameters.beaconCommitteeSubscriber, - accountsRefresher: parameters.accountsRefresher, - maxAttestationDelay: parameters.maxAttestationDelay, - reorgs: parameters.reorgs, - subscriptionInfos: make(map[phase0.Epoch]map[phase0.Slot]map[phase0.CommitteeIndex]*beaconcommitteesubscriber.Subscription), + monitor: parameters.monitor, + slotDuration: slotDuration, + slotsPerEpoch: slotsPerEpoch, + epochsPerSyncCommitteePeriod: epochsPerSyncCommitteePeriod, + chainTimeService: parameters.chainTimeService, + proposerDutiesProvider: parameters.proposerDutiesProvider, + attesterDutiesProvider: parameters.attesterDutiesProvider, + syncCommitteeDutiesProvider: parameters.syncCommitteeDutiesProvider, + syncCommitteesSubscriber: parameters.syncCommitteesSubscriber, + validatingAccountsProvider: parameters.validatingAccountsProvider, + scheduler: parameters.scheduler, + attester: parameters.attester, + syncCommitteeMessenger: parameters.syncCommitteeMessenger, + syncCommitteeAggregator: parameters.syncCommitteeAggregator, + beaconBlockProposer: parameters.beaconBlockProposer, + attestationAggregator: parameters.attestationAggregator, + beaconCommitteeSubscriber: parameters.beaconCommitteeSubscriber, + accountsRefresher: parameters.accountsRefresher, + maxSyncCommitteeMessageDelay: parameters.maxSyncCommitteeMessageDelay, + reorgs: parameters.reorgs, + subscriptionInfos: make(map[phase0.Epoch]map[phase0.Slot]map[phase0.CommitteeIndex]*beaconcommitteesubscriber.Subscription), + handlingAltair: handlingAltair, } // Subscribe to head events. This allows us to go early for attestations if a block arrives, as well as @@ -137,6 +179,11 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) { } go s.scheduleProposals(ctx, epoch, validatorIndices, true /* notCurrentSlot */) go s.scheduleAttestations(ctx, epoch, validatorIndices, true /* notCurrentSlot */) + if handlingAltair { + go s.scheduleSyncCommitteeMessages(ctx, epoch, validatorIndices) + nextSyncCommitteePeriodStartEpoch := phase0.Epoch((uint64(epoch)/s.epochsPerSyncCommitteePeriod + 1) * s.epochsPerSyncCommitteePeriod) + go s.scheduleSyncCommitteeMessages(ctx, nextSyncCommitteePeriodStartEpoch, validatorIndices) + } go s.scheduleAttestations(ctx, epoch+1, nextEpochValidatorIndices, true /* notCurrentSlot */) // Update beacon committee subscriptions this and the next epoch. go func() { @@ -264,6 +311,12 @@ func (s *Service) epochTicker(ctx context.Context, data interface{}) { go s.scheduleProposals(ctx, currentEpoch, validatorIndices, false /* notCurrentSlot */) go s.scheduleAttestations(ctx, currentEpoch+1, nextEpochValidatorIndices, false /* notCurrentSlot */) + if s.handlingAltair { + // Only update if we are on an EPOCHS_PER_SYNC_COMMITTEE_PERIOD boundary. + if uint64(currentEpoch)%s.epochsPerSyncCommitteePeriod == 0 { + go s.scheduleSyncCommitteeMessages(ctx, currentEpoch, validatorIndices) + } + } go func() { // Update beacon committee subscriptions for the next epoch. subscriptionInfo, err := s.beaconCommitteeSubscriber.Subscribe(ctx, currentEpoch+1, nextEpochAccounts) diff --git a/services/controller/standard/service_test.go b/services/controller/standard/service_test.go index 603f189..de0f410 100644 --- a/services/controller/standard/service_test.go +++ b/services/controller/standard/service_test.go @@ -1,4 +1,4 @@ -// Copyright © 2020 Attestant Limited. +// Copyright © 2020, 2021 Attestant Limited. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -28,6 +28,8 @@ import ( "github.com/attestantio/vouch/services/controller/standard" nullmetrics "github.com/attestantio/vouch/services/metrics/null" mockscheduler "github.com/attestantio/vouch/services/scheduler/mock" + mocksynccommitteemessenger "github.com/attestantio/vouch/services/synccommitteemessenger/mock" + mocksynccommitteesubscriber "github.com/attestantio/vouch/services/synccommitteesubscriber/mock" "github.com/attestantio/vouch/testing/logger" "github.com/rs/zerolog" "github.com/stretchr/testify/require" @@ -39,14 +41,18 @@ func TestService(t *testing.T) { genesisTime := time.Now() slotDuration := 12 * time.Second slotsPerEpoch := uint64(32) - mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime) - mockSlotDurationProvider := mock.NewSlotDurationProvider(slotDuration) - mockSlotsPerEpochProvider := mock.NewSlotsPerEpochProvider(slotsPerEpoch) + genesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime) + slotDurationProvider := mock.NewSlotDurationProvider(slotDuration) + slotsPerEpochProvider := mock.NewSlotsPerEpochProvider(slotsPerEpoch) + specProvider := mock.NewSpecProvider() proposerDutiesProvider := mock.NewProposerDutiesProvider() attesterDutiesProvider := mock.NewAttesterDutiesProvider() + syncCommitteeDutiesProvider := mock.NewSyncCommitteeDutiesProvider() mockScheduler := mockscheduler.New() mockAttester := mockattester.New() + mockSyncCommitteeMessenger := mocksynccommitteemessenger.New() + mockSyncCommitteeSubscriber := mocksynccommitteesubscriber.New() mockAttestationAggregator := mockattestationaggregator.New() mockValidatingAccountsProvider := mockaccountmanager.NewValidatingAccountsProvider() mockAccountsRefresher := mockaccountmanager.NewRefresher() @@ -55,9 +61,9 @@ func TestService(t *testing.T) { mockBeaconCommitteeSubscriber := mockbeaconcommitteesubscriber.New() chainTime, err := standardchaintime.New(ctx, - standardchaintime.WithGenesisTimeProvider(mockGenesisTimeProvider), - standardchaintime.WithSlotDurationProvider(mockSlotDurationProvider), - standardchaintime.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standardchaintime.WithGenesisTimeProvider(genesisTimeProvider), + standardchaintime.WithSlotDurationProvider(slotDurationProvider), + standardchaintime.WithSlotsPerEpochProvider(slotsPerEpochProvider), ) require.NoError(t, err) @@ -71,15 +77,17 @@ func TestService(t *testing.T) { name: "MonitorNil", params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), @@ -89,104 +97,67 @@ func TestService(t *testing.T) { err: "problem with parameters: no monitor specified", }, { - name: "SlotDurationProviderNotSpecified", + name: "SpecProviderMissing", params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), standard.WithAccountsRefresher(mockAccountsRefresher), standard.WithMaxAttestationDelay(4 * time.Second), }, - err: "problem with parameters: no slot duration provider specified", + err: "problem with parameters: no spec provider specified", }, { - name: "SlotDurationProviderErrors", + name: "SpecProviderErrors", params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mock.NewErroringSlotDurationProvider()), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(mock.NewErroringSpecProvider()), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), standard.WithAccountsRefresher(mockAccountsRefresher), standard.WithMaxAttestationDelay(4 * time.Second), }, - err: "failed to obtain slot duration: mock", - }, - { - name: "SlotsPerEpochProviderMissing", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithChainTimeService(chainTime), - standard.WithProposerDutiesProvider(proposerDutiesProvider), - standard.WithAttesterDutiesProvider(attesterDutiesProvider), - standard.WithEventsProvider(mockEventsProvider), - standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), - standard.WithScheduler(mockScheduler), - standard.WithAttester(mockAttester), - standard.WithBeaconBlockProposer(mockBeaconBlockProposer), - standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), - standard.WithAttestationAggregator(mockAttestationAggregator), - standard.WithAccountsRefresher(mockAccountsRefresher), - standard.WithMaxAttestationDelay(4 * time.Second), - }, - err: "problem with parameters: no slots per epoch provider specified", - }, - { - name: "SlotsPerEpochProviderErrors", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mock.NewErroringSlotsPerEpochProvider()), - standard.WithChainTimeService(chainTime), - standard.WithProposerDutiesProvider(proposerDutiesProvider), - standard.WithAttesterDutiesProvider(attesterDutiesProvider), - standard.WithEventsProvider(mockEventsProvider), - standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), - standard.WithScheduler(mockScheduler), - standard.WithAttester(mockAttester), - standard.WithBeaconBlockProposer(mockBeaconBlockProposer), - standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), - standard.WithAttestationAggregator(mockAttestationAggregator), - standard.WithAccountsRefresher(mockAccountsRefresher), - standard.WithMaxAttestationDelay(4 * time.Second), - }, - err: "failed to obtain slots per epoch: error", + err: "problem with parameters: failed to obtain spec: error", }, { name: "ChainTimeServiceMissing", params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), @@ -200,14 +171,16 @@ func TestService(t *testing.T) { params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithChainTimeService(chainTime), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), @@ -221,14 +194,16 @@ func TestService(t *testing.T) { params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), @@ -242,14 +217,16 @@ func TestService(t *testing.T) { params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), @@ -263,14 +240,16 @@ func TestService(t *testing.T) { params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), @@ -284,14 +263,16 @@ func TestService(t *testing.T) { params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), @@ -305,14 +286,16 @@ func TestService(t *testing.T) { params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), @@ -326,15 +309,17 @@ func TestService(t *testing.T) { params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), standard.WithAccountsRefresher(mockAccountsRefresher), @@ -347,15 +332,17 @@ func TestService(t *testing.T) { params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithAttestationAggregator(mockAttestationAggregator), standard.WithAccountsRefresher(mockAccountsRefresher), @@ -368,15 +355,17 @@ func TestService(t *testing.T) { params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAccountsRefresher(mockAccountsRefresher), @@ -389,15 +378,17 @@ func TestService(t *testing.T) { params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), @@ -405,47 +396,51 @@ func TestService(t *testing.T) { }, err: "problem with parameters: no accounts refresher specified", }, - { - name: "MaxAttestationDelayZero", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), - standard.WithChainTimeService(chainTime), - standard.WithProposerDutiesProvider(proposerDutiesProvider), - standard.WithAttesterDutiesProvider(attesterDutiesProvider), - standard.WithEventsProvider(mockEventsProvider), - standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), - standard.WithScheduler(mockScheduler), - standard.WithAttester(mockAttester), - standard.WithBeaconBlockProposer(mockBeaconBlockProposer), - standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), - standard.WithAttestationAggregator(mockAttestationAggregator), - standard.WithAccountsRefresher(mockAccountsRefresher), - standard.WithMaxAttestationDelay(0), - }, - err: "problem with parameters: no maximum attestation delay specified", - }, { name: "Good", params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(ctx)), - standard.WithSlotDurationProvider(mockSlotDurationProvider), - standard.WithSlotsPerEpochProvider(mockSlotsPerEpochProvider), + standard.WithSpecProvider(specProvider), standard.WithChainTimeService(chainTime), standard.WithProposerDutiesProvider(proposerDutiesProvider), standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), standard.WithEventsProvider(mockEventsProvider), standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), standard.WithScheduler(mockScheduler), standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), standard.WithBeaconBlockProposer(mockBeaconBlockProposer), standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), standard.WithAttestationAggregator(mockAttestationAggregator), standard.WithAccountsRefresher(mockAccountsRefresher), standard.WithMaxAttestationDelay(4 * time.Second), + standard.WithReorgs(false), + }, + }, + { + name: "GoodDefaultMaxAttestationDelay", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithSpecProvider(specProvider), + standard.WithChainTimeService(chainTime), + standard.WithProposerDutiesProvider(proposerDutiesProvider), + standard.WithAttesterDutiesProvider(attesterDutiesProvider), + standard.WithSyncCommitteeDutiesProvider(syncCommitteeDutiesProvider), + standard.WithEventsProvider(mockEventsProvider), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithScheduler(mockScheduler), + standard.WithAttester(mockAttester), + standard.WithSyncCommitteeMessenger(mockSyncCommitteeMessenger), + standard.WithSyncCommitteeSubscriber(mockSyncCommitteeSubscriber), + standard.WithBeaconBlockProposer(mockBeaconBlockProposer), + standard.WithBeaconCommitteeSubscriber(mockBeaconCommitteeSubscriber), + standard.WithAttestationAggregator(mockAttestationAggregator), + standard.WithAccountsRefresher(mockAccountsRefresher), + standard.WithReorgs(true), }, }, } diff --git a/services/controller/standard/synccommitteemessenger.go b/services/controller/standard/synccommitteemessenger.go new file mode 100644 index 0000000..da07412 --- /dev/null +++ b/services/controller/standard/synccommitteemessenger.go @@ -0,0 +1,175 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + "fmt" + "time" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/vouch/services/synccommitteeaggregator" + "github.com/attestantio/vouch/services/synccommitteemessenger" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" +) + +// scheduleSyncCommitteeMessages schedules sync committee messages for the given epoch and validator indices. +func (s *Service) scheduleSyncCommitteeMessages(ctx context.Context, + epoch phase0.Epoch, + validatorIndices []phase0.ValidatorIndex, +) { + if len(validatorIndices) == 0 { + // Nothing to do. + return + } + + started := time.Now() + log.Trace().Uint64("epoch", uint64(epoch)).Msg("Scheduling sync committee messages") + + resp, err := s.syncCommitteeDutiesProvider.SyncCommitteeDuties(ctx, epoch, validatorIndices) + if err != nil { + log.Error().Err(err).Msg("Failed to fetch sync committee message duties") + return + } + log.Trace().Dur("elapsed", time.Since(started)).Int("duties", len(resp)).Msg("Fetched sync committee message duties") + + // We combine the duties for the epoch. + messageIndices := make(map[phase0.ValidatorIndex][]phase0.CommitteeIndex, len(resp)) + for _, duty := range resp { + messageIndices[duty.ValidatorIndex] = duty.ValidatorSyncCommitteeIndices + } + // log.Trace().Dur("elapsed", time.Since(started)).Str("duties", duty.String()).Msg("Generated sync committee message indices") + + // Obtain the accounts for the validator indices. + accounts, err := s.validatingAccountsProvider.ValidatingAccountsForEpochByIndex(ctx, epoch, validatorIndices) + if err != nil { + log.Error().Err(err).Msg("Failed to obtain validating accounts for epoch") + return + } + + // Now we have the messages we can subscribe to the relevant subnets. + + syncCommitteePeriodFirstSlot := s.chainTimeService.FirstSlotOfEpoch(phase0.Epoch((uint64(epoch) / s.epochsPerSyncCommitteePeriod) * s.epochsPerSyncCommitteePeriod)) + syncCommitteePeriodLastSlot := syncCommitteePeriodFirstSlot + phase0.Slot(s.slotsPerEpoch*s.epochsPerSyncCommitteePeriod) - 1 + if syncCommitteePeriodFirstSlot < s.chainTimeService.CurrentSlot() { + syncCommitteePeriodFirstSlot = s.chainTimeService.CurrentSlot() + } + log.Trace(). + Uint64("first_slot", uint64(syncCommitteePeriodFirstSlot)). + Uint64("last_slot", uint64(syncCommitteePeriodLastSlot)). + Msg("Setting sync committee duties for period") + + for slot := syncCommitteePeriodFirstSlot; slot <= syncCommitteePeriodLastSlot; slot++ { + go func(duty *synccommitteemessenger.Duty, accounts map[phase0.ValidatorIndex]e2wtypes.Account) { + for _, validatorIndex := range duty.ValidatorIndices() { + account, exists := accounts[validatorIndex] + if !exists { + log.Error().Uint64("validator_index", uint64(validatorIndex)).Msg("No validating account; cannot continue") + // Continue regardless of error, to attempt to schedule as many valid jobs as possible. + } else { + duty.SetAccount(validatorIndex, account) + } + } + + prepareJobTime := s.chainTimeService.StartOfSlot(duty.Slot()).Add(-1 * time.Minute) + if err := s.scheduler.ScheduleJob(ctx, + fmt.Sprintf("Prepare sync committee messages for slot %d", duty.Slot()), + prepareJobTime, + s.prepareMessageSyncCommittee, + duty, + ); err != nil { + log.Error().Err(err).Msg("Failed to schedule prepare sync committee messages") + return + } + jobTime := s.chainTimeService.StartOfSlot(duty.Slot()).Add(s.maxSyncCommitteeMessageDelay) + if err := s.scheduler.ScheduleJob(ctx, + fmt.Sprintf("Sync committee messages for slot %d", duty.Slot()), + jobTime, + s.messageSyncCommittee, + duty, + ); err != nil { + // Don't return here; we want to try to set up as many sync committee message jobs as possible. + log.Error().Err(err).Msg("Failed to schedule sync committee messages") + } + }(synccommitteemessenger.NewDuty(slot, messageIndices), accounts) + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Scheduled sync committee messages") + + // TODO Obi-wan? + if err := s.syncCommitteesSubscriber.Subscribe(ctx, s.chainTimeService.SlotToEpoch(syncCommitteePeriodLastSlot), resp); err != nil { + log.Error().Err(err).Msg("Failed to submit sync committee subscribers") + return + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Submitted sync committee subscribers") +} + +func (s *Service) prepareMessageSyncCommittee(ctx context.Context, data interface{}) { + started := time.Now() + duty, ok := data.(*synccommitteemessenger.Duty) + if !ok { + log.Error().Msg("Passed invalid data") + return + } + log := log.With().Uint64("slot", uint64(s.chainTimeService.CurrentSlot())).Logger() + + if err := s.syncCommitteeMessenger.Prepare(ctx, duty); err != nil { + log.Error().Uint64("sync_committee_slot", uint64(duty.Slot())).Err(err).Msg("Failed to prepare sync committee message") + return + } + + // At this point we can schedule an aggregation job if reqiured. + aggregateValidatorIndices := make([]phase0.ValidatorIndex, 0) + selectionProofs := make(map[phase0.ValidatorIndex]map[uint64]phase0.BLSSignature) + for _, validatorIndex := range duty.ValidatorIndices() { + aggregationIndices := duty.AggregatorSubcommittees(validatorIndex) + if len(aggregationIndices) > 0 { + aggregateValidatorIndices = append(aggregateValidatorIndices, validatorIndex) + selectionProofs[validatorIndex] = aggregationIndices + } + } + if len(aggregateValidatorIndices) > 0 { + aggregatorDuty := &synccommitteeaggregator.Duty{ + Slot: duty.Slot(), + ValidatorIndices: aggregateValidatorIndices, + SelectionProofs: selectionProofs, + Accounts: duty.Accounts(), + } + if err := s.scheduler.ScheduleJob(ctx, + fmt.Sprintf("Sync committee aggregation for slot %d", duty.Slot()), + s.chainTimeService.StartOfSlot(duty.Slot()).Add(s.slotDuration*2/3), + s.syncCommitteeAggregator.Aggregate, + aggregatorDuty, + ); err != nil { + log.Error().Err(err).Msg("Failed to schedule sync committee attestation aggregation job") + } + } + + log.Trace().Dur("elapsed", time.Since(started)).Msg("Prepared") +} +func (s *Service) messageSyncCommittee(ctx context.Context, data interface{}) { + started := time.Now() + duty, ok := data.(*synccommitteemessenger.Duty) + if !ok { + log.Error().Msg("Passed invalid data") + return + } + log := log.With().Uint64("slot", uint64(s.chainTimeService.CurrentSlot())).Logger() + + _, err := s.syncCommitteeMessenger.Message(ctx, duty) + if err != nil { + log.Warn().Err(err).Msg("Failed to submit sync committee message") + return + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Messaged") +} diff --git a/services/metrics/null/service.go b/services/metrics/null/service.go index 0c988d3..01ae259 100644 --- a/services/metrics/null/service.go +++ b/services/metrics/null/service.go @@ -81,3 +81,11 @@ func (s *Service) ClientOperation(provider string, name string, succeeded bool, // StrategyOperation provides a generic monitor for strategy operations. func (s *Service) StrategyOperation(strategy string, provider string, operation string, duration time.Duration) { } + +// SyncCommitteeAggregationsCompleted is called when a sync committee aggregation process has completed. +func (s *Service) SyncCommitteeAggregationsCompleted(started time.Time, count int, result string) { +} + +// SyncCommitteeMessagesCompleted is called when a sync committee message process has completed. +func (s *Service) SyncCommitteeMessagesCompleted(started time.Time, count int, result string) { +} diff --git a/services/metrics/prometheus/service.go b/services/metrics/prometheus/service.go index 74965f1..853d129 100644 --- a/services/metrics/prometheus/service.go +++ b/services/metrics/prometheus/service.go @@ -43,11 +43,21 @@ type Service struct { attestationAggregationProcessRequests *prometheus.CounterVec attestationAggregationCoverageRatio prometheus.Histogram + syncCommitteeMessageProcessTimer prometheus.Histogram + syncCommitteeMessageProcessRequests *prometheus.CounterVec + + syncCommitteeAggregationProcessTimer prometheus.Histogram + syncCommitteeAggregationProcessRequests *prometheus.CounterVec + beaconCommitteeSubscriptionProcessTimer prometheus.Histogram beaconCommitteeSubscriptionProcessRequests *prometheus.CounterVec beaconCommitteeSubscribers prometheus.Gauge beaconCommitteeAggregators prometheus.Gauge + syncCommitteeSubscriptionProcessTimer prometheus.Histogram + syncCommitteeSubscriptionProcessRequests *prometheus.CounterVec + syncCommitteeSubscribers prometheus.Gauge + accountManagerAccounts *prometheus.GaugeVec clientOperationCounter *prometheus.CounterVec @@ -89,9 +99,18 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) { if err := s.setupAttestationAggregationMetrics(); err != nil { return nil, errors.Wrap(err, "failed to set up attestation aggregation metrics") } + if err := s.setupSyncCommitteeMessageMetrics(); err != nil { + return nil, errors.Wrap(err, "failed to set up sync committee message metrics") + } + if err := s.setupSyncCommitteeAggregationMetrics(); err != nil { + return nil, errors.Wrap(err, "failed to set up sync committee aggregation metrics") + } if err := s.setupBeaconCommitteeSubscriptionMetrics(); err != nil { return nil, errors.Wrap(err, "failed to set up beacon committee subscription metrics") } + if err := s.setupSyncCommitteeSubscriptionMetrics(); err != nil { + return nil, errors.Wrap(err, "failed to set up sync committee subscription metrics") + } if err := s.setupAccountManagerMetrics(); err != nil { return nil, errors.Wrap(err, "failed to set up account manager metrics") } diff --git a/services/metrics/prometheus/synccommitteeaggregation.go b/services/metrics/prometheus/synccommitteeaggregation.go new file mode 100644 index 0000000..ebaf5ff --- /dev/null +++ b/services/metrics/prometheus/synccommitteeaggregation.go @@ -0,0 +1,54 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func (s *Service) setupSyncCommitteeAggregationMetrics() error { + s.syncCommitteeAggregationProcessTimer = + prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: "vouch", + Subsystem: "sync_committee_aggregation_process", + Name: "duration_seconds", + Help: "The time vouch spends from starting the sync committee aggregation process to submitting the sync committee aggregations.", + Buckets: []float64{ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, + 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, + }, + }) + if err := prometheus.Register(s.syncCommitteeAggregationProcessTimer); err != nil { + return err + } + + s.syncCommitteeAggregationProcessRequests = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "vouch", + Subsystem: "sync_committee_aggregation_process", + Name: "requests_total", + Help: "The number of sync committee aggregation processes.", + }, []string{"result"}) + return prometheus.Register(s.syncCommitteeAggregationProcessRequests) +} + +// SyncCommitteeAggregationsCompleted is called when a sync committee aggregation process has completed. +func (s *Service) SyncCommitteeAggregationsCompleted(started time.Time, count int, result string) { + duration := time.Since(started).Seconds() + for i := 0; i < count; i++ { + s.syncCommitteeAggregationProcessTimer.Observe(duration) + } + s.syncCommitteeAggregationProcessRequests.WithLabelValues(result).Add(float64(count)) +} diff --git a/services/metrics/prometheus/synccommitteemessenger.go b/services/metrics/prometheus/synccommitteemessenger.go new file mode 100644 index 0000000..26d04f3 --- /dev/null +++ b/services/metrics/prometheus/synccommitteemessenger.go @@ -0,0 +1,54 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func (s *Service) setupSyncCommitteeMessageMetrics() error { + s.syncCommitteeMessageProcessTimer = + prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: "vouch", + Subsystem: "sync_committee_message_process", + Name: "duration_seconds", + Help: "The time vouch spends from starting the sync committee message process to submitting the sync committee messages.", + Buckets: []float64{ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, + 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, + }, + }) + if err := prometheus.Register(s.syncCommitteeMessageProcessTimer); err != nil { + return err + } + + s.syncCommitteeMessageProcessRequests = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "vouch", + Subsystem: "sync_committee_message_process", + Name: "requests_total", + Help: "The number of sync committee message processes.", + }, []string{"result"}) + return prometheus.Register(s.syncCommitteeMessageProcessRequests) +} + +// SyncCommitteeMessagesCompleted is called when a sync committee message process has completed. +func (s *Service) SyncCommitteeMessagesCompleted(started time.Time, count int, result string) { + duration := time.Since(started).Seconds() + for i := 0; i < count; i++ { + s.syncCommitteeMessageProcessTimer.Observe(duration) + } + s.syncCommitteeMessageProcessRequests.WithLabelValues(result).Add(float64(count)) +} diff --git a/services/metrics/prometheus/synccommitteesubscriber.go b/services/metrics/prometheus/synccommitteesubscriber.go new file mode 100644 index 0000000..d979f63 --- /dev/null +++ b/services/metrics/prometheus/synccommitteesubscriber.go @@ -0,0 +1,66 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +func (s *Service) setupSyncCommitteeSubscriptionMetrics() error { + s.syncCommitteeSubscriptionProcessTimer = + prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: "vouch", + Subsystem: "synccommitteesubscription_process", + Name: "duration_seconds", + Help: "The time vouch spends from starting the sync committee subscription process to submitting the subscription request.", + Buckets: []float64{ + 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, + 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, + }, + }) + if err := prometheus.Register(s.syncCommitteeSubscriptionProcessTimer); err != nil { + return err + } + + s.syncCommitteeSubscriptionProcessRequests = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "vouch", + Subsystem: "synccommitteesubscription_process", + Name: "requests_total", + Help: "The number of sync committee subscription processes.", + }, []string{"result"}) + if err := prometheus.Register(s.syncCommitteeSubscriptionProcessRequests); err != nil { + return err + } + + s.syncCommitteeSubscribers = prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "vouch", + Subsystem: "synccommitteesubscription", + Name: "subscribers_total", + Help: "The number of sync committee subscribed.", + }) + return prometheus.Register(s.syncCommitteeSubscribers) +} + +// SyncCommitteeSubscriptionCompleted is called when an sync committee subscription process has completed. +func (s *Service) SyncCommitteeSubscriptionCompleted(started time.Time, result string) { + s.syncCommitteeSubscriptionProcessTimer.Observe(time.Since(started).Seconds()) + s.syncCommitteeSubscriptionProcessRequests.WithLabelValues(result).Inc() +} + +// SyncCommitteeSubscribers sets the number of sync committees to which our validators are subscribed. +func (s *Service) SyncCommitteeSubscribers(subscribers int) { + s.syncCommitteeSubscribers.Set(float64(subscribers)) +} diff --git a/services/metrics/service.go b/services/metrics/service.go index 5ba3a0e..e180d6f 100644 --- a/services/metrics/service.go +++ b/services/metrics/service.go @@ -63,9 +63,21 @@ type AttestationAggregationMonitor interface { AttestationAggregationCoverage(frac float64) } +// SyncCommitteeMessageMonitor provides methods to monitor the sync committee message process. +type SyncCommitteeMessageMonitor interface { + // SyncCommitteeMessagesCompleted is called when a sync committee message process has completed. + SyncCommitteeMessagesCompleted(started time.Time, count int, result string) +} + +// SyncCommitteeAggregationMonitor provides methods to monitor the sync committee aggregation process. +type SyncCommitteeAggregationMonitor interface { + // SyncCommitteeAggregationsCompleted is called when a sync committee aggregation process has completed. + SyncCommitteeAggregationsCompleted(started time.Time, count int, result string) +} + // BeaconCommitteeSubscriptionMonitor provides methods to monitor the outcome of beacon committee subscriptions. type BeaconCommitteeSubscriptionMonitor interface { - // BeaconCommitteeSubscriptionCompleted is called when an beacon committee subscription process has completed. + // BeaconCommitteeSubscriptionCompleted is called when a beacon committee subscription process has completed. BeaconCommitteeSubscriptionCompleted(started time.Time, result string) // BeaconCommitteeSubscribers sets the number of beacon committees to which our validators are subscribed. BeaconCommitteeSubscribers(subscribers int) @@ -73,6 +85,14 @@ type BeaconCommitteeSubscriptionMonitor interface { BeaconCommitteeAggregators(aggregators int) } +// SyncCommitteeSubscriptionMonitor provides methods to monitor the outcome of sync committee subscriptions. +type SyncCommitteeSubscriptionMonitor interface { + // SyncCommitteeSubscriptionCompleted is called when a sync committee subscription process has completed. + SyncCommitteeSubscriptionCompleted(started time.Time, result string) + // SyncCommitteeSubscribers sets the number of sync committees to which our validators are subscribed. + SyncCommitteeSubscribers(subscribers int) +} + // AccountManagerMonitor provides methods to monitor the account manager. type AccountManagerMonitor interface { // Accounts sets the number of accounts in a given state. diff --git a/services/signer/mock/service.go b/services/signer/mock/service.go index 457c781..aa7c95b 100644 --- a/services/signer/mock/service.go +++ b/services/signer/mock/service.go @@ -16,6 +16,7 @@ package mock import ( "context" + "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/phase0" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" ) @@ -112,3 +113,40 @@ func (s *Service) SignSlotSelection(ctx context.Context, ) { return phase0.BLSSignature{}, nil } + +// SignContributionAndProof signs a sync committee contribution for given slot and root. +func (s *Service) SignContributionAndProof(ctx context.Context, + account e2wtypes.Account, + contributionAndProof *altair.ContributionAndProof, +) ( + phase0.BLSSignature, + error, +) { + return phase0.BLSSignature{}, nil +} + +// SignSyncCommitteeRoot returns a root signature. +// This signs a beacon block root with the "sync committee" domain. +func (s *Service) SignSyncCommitteeRoot(ctx context.Context, + account e2wtypes.Account, + epoch phase0.Epoch, + root phase0.Root, +) ( + phase0.BLSSignature, + error, +) { + return phase0.BLSSignature{}, nil +} + +// SignSyncCommitteeSelection returns a sync committee selection signature. +// This signs a slot and subcommittee with the "sync committee selection proof" domain. +func (s *Service) SignSyncCommitteeSelection(ctx context.Context, + account e2wtypes.Account, + slot phase0.Slot, + subcommitteeIndex uint64, +) ( + phase0.BLSSignature, + error, +) { + return phase0.BLSSignature{}, nil +} diff --git a/services/signer/service.go b/services/signer/service.go index 45b60e7..f29261b 100644 --- a/services/signer/service.go +++ b/services/signer/service.go @@ -1,4 +1,4 @@ -// Copyright © 2020 Attestant Limited. +// Copyright © 2021 Attestant Limited. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -17,6 +17,7 @@ package signer import ( "context" + "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/phase0" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" ) @@ -114,3 +115,43 @@ type SlotSelectionSigner interface { error, ) } + +// SyncCommitteeRootSigner provides methods to sign a sync committee root. +type SyncCommitteeRootSigner interface { + // SignSyncCommittee returns a root signature. + // This signs a beacon block root with the "sync committee" domain. + SignSyncCommitteeRoot(ctx context.Context, + account e2wtypes.Account, + epoch phase0.Epoch, + root phase0.Root, + ) ( + phase0.BLSSignature, + error, + ) +} + +// SyncCommitteeSelectionSigner provides methods to sign sync committee selections. +type SyncCommitteeSelectionSigner interface { + // SignSyncCommitteeSelection returns a sync committee selection signature. + // This signs a slot and subcommittee with the "sync committee selection proof" domain. + SignSyncCommitteeSelection(ctx context.Context, + account e2wtypes.Account, + slot phase0.Slot, + subcommitteeIndex uint64, + ) ( + phase0.BLSSignature, + error, + ) +} + +// ContributionAndProofSigner provides methods to sign contribution and proofs. +type ContributionAndProofSigner interface { + // SignContributionAndProof signs a sync committee contribution for given slot and root. + SignContributionAndProof(ctx context.Context, + account e2wtypes.Account, + contributionAndProof *altair.ContributionAndProof, + ) ( + phase0.BLSSignature, + error, + ) +} diff --git a/services/signer/standard/parameters.go b/services/signer/standard/parameters.go index 116cc8a..ea67498 100644 --- a/services/signer/standard/parameters.go +++ b/services/signer/standard/parameters.go @@ -24,16 +24,11 @@ import ( ) type parameters struct { - logLevel zerolog.Level - monitor metrics.SignerMonitor - clientMonitor metrics.ClientMonitor - slotsPerEpochProvider eth2client.SlotsPerEpochProvider - beaconProposerDomainTypeProvider eth2client.BeaconProposerDomainProvider - beaconAttesterDomainTypeProvider eth2client.BeaconAttesterDomainProvider - randaoDomainTypeProvider eth2client.RANDAODomainProvider - selectionProofDomainTypeProvider eth2client.SelectionProofDomainProvider - aggregateAndProofDomainTypeProvider eth2client.AggregateAndProofDomainProvider - domainProvider eth2client.DomainProvider + logLevel zerolog.Level + monitor metrics.SignerMonitor + clientMonitor metrics.ClientMonitor + specProvider eth2client.SpecProvider + domainProvider eth2client.DomainProvider } // Parameter is the interface for service parameters. @@ -68,45 +63,10 @@ func WithClientMonitor(clientMonitor metrics.ClientMonitor) Parameter { }) } -// WithSlotsPerEpochProvider sets the slots per epoch provider. -func WithSlotsPerEpochProvider(provider eth2client.SlotsPerEpochProvider) Parameter { +// WithSpecProvider sets the spec provider. +func WithSpecProvider(provider eth2client.SpecProvider) Parameter { return parameterFunc(func(p *parameters) { - p.slotsPerEpochProvider = provider - }) -} - -// WithBeaconProposerDomainTypeProvider sets the beacon proposer domain provider. -func WithBeaconProposerDomainTypeProvider(provider eth2client.BeaconProposerDomainProvider) Parameter { - return parameterFunc(func(p *parameters) { - p.beaconProposerDomainTypeProvider = provider - }) -} - -// WithBeaconAttesterDomainTypeProvider sets the beacon attester domain provider. -func WithBeaconAttesterDomainTypeProvider(provider eth2client.BeaconAttesterDomainProvider) Parameter { - return parameterFunc(func(p *parameters) { - p.beaconAttesterDomainTypeProvider = provider - }) -} - -// WithRANDAODomainTypeProvider sets the RANDAO domain provider. -func WithRANDAODomainTypeProvider(provider eth2client.RANDAODomainProvider) Parameter { - return parameterFunc(func(p *parameters) { - p.randaoDomainTypeProvider = provider - }) -} - -// WithSelectionProofDomainTypeProvider sets the RANDAO domain provider. -func WithSelectionProofDomainTypeProvider(provider eth2client.SelectionProofDomainProvider) Parameter { - return parameterFunc(func(p *parameters) { - p.selectionProofDomainTypeProvider = provider - }) -} - -// WithAggregateAndProofDomainTypeProvider sets the aggregate and proof domain provider. -func WithAggregateAndProofDomainTypeProvider(provider eth2client.AggregateAndProofDomainProvider) Parameter { - return parameterFunc(func(p *parameters) { - p.aggregateAndProofDomainTypeProvider = provider + p.specProvider = provider }) } @@ -136,23 +96,8 @@ func parseAndCheckParameters(params ...Parameter) (*parameters, error) { if parameters.clientMonitor == nil { return nil, errors.New("no client monitor specified") } - if parameters.slotsPerEpochProvider == nil { - return nil, errors.New("no slots per epoch provider specified") - } - if parameters.beaconProposerDomainTypeProvider == nil { - return nil, errors.New("no beacon proposer domain type provider specified") - } - if parameters.beaconAttesterDomainTypeProvider == nil { - return nil, errors.New("no beacon attester domain type provider specified") - } - if parameters.randaoDomainTypeProvider == nil { - return nil, errors.New("no RANDAO domain type provider specified") - } - if parameters.selectionProofDomainTypeProvider == nil { - return nil, errors.New("no selection proof domain type provider specified") - } - if parameters.aggregateAndProofDomainTypeProvider == nil { - return nil, errors.New("no aggregate and proof domain type provider specified") + if parameters.specProvider == nil { + return nil, errors.New("no spec provider specified") } if parameters.domainProvider == nil { return nil, errors.New("no domain provider specified") diff --git a/services/signer/standard/service.go b/services/signer/standard/service.go index 8333128..278f367 100644 --- a/services/signer/standard/service.go +++ b/services/signer/standard/service.go @@ -15,6 +15,7 @@ package standard import ( "context" + "fmt" eth2client "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/spec/phase0" @@ -26,15 +27,18 @@ import ( // Service is the manager for signers. type Service struct { - monitor metrics.SignerMonitor - clientMonitor metrics.ClientMonitor - slotsPerEpoch phase0.Slot - beaconProposerDomainType phase0.DomainType - beaconAttesterDomainType phase0.DomainType - randaoDomainType phase0.DomainType - selectionProofDomainType phase0.DomainType - aggregateAndProofDomainType phase0.DomainType - domainProvider eth2client.DomainProvider + monitor metrics.SignerMonitor + clientMonitor metrics.ClientMonitor + slotsPerEpoch phase0.Slot + beaconProposerDomainType phase0.DomainType + beaconAttesterDomainType phase0.DomainType + randaoDomainType phase0.DomainType + selectionProofDomainType phase0.DomainType + aggregateAndProofDomainType phase0.DomainType + syncCommitteeDomainType *phase0.DomainType + syncCommitteeSelectionProofDomainType *phase0.DomainType + contributionAndProofDomainType *phase0.DomainType + domainProvider eth2client.DomainProvider } // module-wide log. @@ -53,42 +57,87 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) { log = log.Level(parameters.logLevel) } - slotsPerEpoch, err := parameters.slotsPerEpochProvider.SlotsPerEpoch(ctx) + spec, err := parameters.specProvider.Spec(ctx) if err != nil { - return nil, errors.Wrap(err, "failed to obtain slots per epoch") + return nil, errors.Wrap(err, "failed to obtain spec") } - beaconAttesterDomainType, err := parameters.beaconAttesterDomainTypeProvider.BeaconAttesterDomain(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to obtain beacon attester domain type") + + tmp, exists := spec["SLOTS_PER_EPOCH"] + if !exists { + return nil, errors.New("SLOTS_PER_EPOCH not found in spec") } - beaconProposerDomainType, err := parameters.beaconProposerDomainTypeProvider.BeaconProposerDomain(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to obtain beacon proposer domain type") + slotsPerEpoch, ok := tmp.(uint64) + if !ok { + return nil, errors.New("SLOTS_PER_EPOCH of unexpected type") } - randaoDomainType, err := parameters.randaoDomainTypeProvider.RANDAODomain(ctx) + + beaconAttesterDomainType, err := domainType(spec, "DOMAIN_BEACON_ATTESTER") if err != nil { - return nil, errors.Wrap(err, "failed to obtain RANDAO domain type") + return nil, err } - selectionProofDomainType, err := parameters.selectionProofDomainTypeProvider.SelectionProofDomain(ctx) + + beaconProposerDomainType, err := domainType(spec, "DOMAIN_BEACON_PROPOSER") if err != nil { - return nil, errors.Wrap(err, "failed to obtain selection proof domain type") + return nil, err } - aggregateAndProofDomainType, err := parameters.aggregateAndProofDomainTypeProvider.AggregateAndProofDomain(ctx) + + randaoDomainType, err := domainType(spec, "DOMAIN_RANDAO") if err != nil { - return nil, errors.Wrap(err, "failed to obtain aggregate and proof domain type") + return nil, err + } + + selectionProofDomainType, err := domainType(spec, "DOMAIN_SELECTION_PROOF") + if err != nil { + return nil, err + } + + aggregateAndProofDomainType, err := domainType(spec, "DOMAIN_AGGREGATE_AND_PROOF") + if err != nil { + return nil, err + } + + // The following are optional. + var syncCommitteeDomainType *phase0.DomainType + if tmp, err := domainType(spec, "DOMAIN_SYNC_COMMITTEE"); err == nil { + syncCommitteeDomainType = &tmp + } + + var syncCommitteeSelectionProofDomainType *phase0.DomainType + if tmp, err := domainType(spec, "DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF"); err == nil { + syncCommitteeSelectionProofDomainType = &tmp + } + + var contributionAndProofDomainType *phase0.DomainType + if tmp, err := domainType(spec, "DOMAIN_CONTRIBUTION_AND_PROOF"); err == nil { + contributionAndProofDomainType = &tmp } s := &Service{ - monitor: parameters.monitor, - clientMonitor: parameters.clientMonitor, - slotsPerEpoch: phase0.Slot(slotsPerEpoch), - beaconAttesterDomainType: beaconAttesterDomainType, - beaconProposerDomainType: beaconProposerDomainType, - randaoDomainType: randaoDomainType, - selectionProofDomainType: selectionProofDomainType, - aggregateAndProofDomainType: aggregateAndProofDomainType, - domainProvider: parameters.domainProvider, + monitor: parameters.monitor, + clientMonitor: parameters.clientMonitor, + slotsPerEpoch: phase0.Slot(slotsPerEpoch), + beaconAttesterDomainType: beaconAttesterDomainType, + beaconProposerDomainType: beaconProposerDomainType, + randaoDomainType: randaoDomainType, + selectionProofDomainType: selectionProofDomainType, + aggregateAndProofDomainType: aggregateAndProofDomainType, + syncCommitteeDomainType: syncCommitteeDomainType, + syncCommitteeSelectionProofDomainType: syncCommitteeSelectionProofDomainType, + contributionAndProofDomainType: contributionAndProofDomainType, + domainProvider: parameters.domainProvider, } return s, nil } + +func domainType(spec map[string]interface{}, input string) (phase0.DomainType, error) { + tmp, exists := spec[input] + if !exists { + return phase0.DomainType{}, fmt.Errorf("%v not found in spec", input) + } + domainType, ok := tmp.(phase0.DomainType) + if !ok { + return phase0.DomainType{}, fmt.Errorf("%v of unexpected type", input) + } + return domainType, nil +} diff --git a/services/signer/standard/service_test.go b/services/signer/standard/service_test.go index 33db2f2..79c32c6 100644 --- a/services/signer/standard/service_test.go +++ b/services/signer/standard/service_test.go @@ -26,12 +26,7 @@ import ( ) func TestService(t *testing.T) { - slotsPerEpochProvider := mock.NewSlotsPerEpochProvider(32) - beaconProposerDomainTypeProvider := mock.NewBeaconProposerDomainProvider() - beaconAttesterDomainTypeProvider := mock.NewBeaconAttesterDomainProvider() - randaoDomainTypeProvider := mock.NewRANDAODomainProvider() - selectionProofDomainTypeProvider := mock.NewSelectionProofDomainProvider() - aggregateAndProofDomainTypeProvider := mock.NewAggregateAndProofDomainProvider() + specProvider := mock.NewSpecProvider() domainProvider := mock.NewDomainProvider() tests := []struct { @@ -46,12 +41,7 @@ func TestService(t *testing.T) { standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nil), standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), + standard.WithSpecProvider(specProvider), standard.WithDomainProvider(domainProvider), }, err: "problem with parameters: no monitor specified", @@ -62,216 +52,31 @@ func TestService(t *testing.T) { standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(context.Background())), standard.WithClientMonitor(nil), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), + standard.WithSpecProvider(specProvider), standard.WithDomainProvider(domainProvider), }, err: "problem with parameters: no client monitor specified", }, { - name: "SlotsPerEpochProviderMissing", + name: "SpecProviderMissing", params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(context.Background())), standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), standard.WithDomainProvider(domainProvider), }, - err: "problem with parameters: no slots per epoch provider specified", + err: "problem with parameters: no spec provider specified", }, { - name: "SlotsPerEpochProviderErrors", + name: "SpecProviderErrors", params: []standard.Parameter{ standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(context.Background())), standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(mock.NewErroringSlotsPerEpochProvider()), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), + standard.WithSpecProvider(mock.NewErroringSpecProvider()), standard.WithDomainProvider(domainProvider), }, - err: "failed to obtain slots per epoch: error", - }, - { - name: "BeaonProposerDomainTypeProviderMissing", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(context.Background())), - standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), - standard.WithDomainProvider(domainProvider), - }, - err: "problem with parameters: no beacon proposer domain type provider specified", - }, - { - name: "BeaonProposerDomainTypeProviderErrors", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(context.Background())), - standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(mock.NewErroringBeaconProposerDomainProvider()), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), - standard.WithDomainProvider(domainProvider), - }, - err: "failed to obtain beacon proposer domain type: error", - }, - { - name: "BeaonAttesterDomainTypeMissing", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(context.Background())), - standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), - standard.WithDomainProvider(domainProvider), - }, - err: "problem with parameters: no beacon attester domain type provider specified", - }, - { - name: "BeaonAttesterDomainTypeErrors", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(context.Background())), - standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(mock.NewErroringBeaconAttesterDomainProvider()), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), - standard.WithDomainProvider(domainProvider), - }, - err: "failed to obtain beacon attester domain type: error", - }, - { - name: "RANDAODomainTypeProviderMissing", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(context.Background())), - standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), - standard.WithDomainProvider(domainProvider), - }, - err: "problem with parameters: no RANDAO domain type provider specified", - }, - { - name: "RANDAODomainTypeProviderErrors", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(context.Background())), - standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(mock.NewErroringRANDAODomainProvider()), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), - standard.WithDomainProvider(domainProvider), - }, - err: "failed to obtain RANDAO domain type: error", - }, - { - name: "SelectionProofDomianTypeProviderMissing", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(context.Background())), - standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), - standard.WithDomainProvider(domainProvider), - }, - err: "problem with parameters: no selection proof domain type provider specified", - }, - { - name: "SelectionProofDomianTypeProviderErrors", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(context.Background())), - standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(mock.NewErroringSelectionProofDomainProvider()), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), - standard.WithDomainProvider(domainProvider), - }, - err: "failed to obtain selection proof domain type: error", - }, - { - name: "AggregateAndProofDomianTypeProviderMissing", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(context.Background())), - standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithDomainProvider(domainProvider), - }, - err: "problem with parameters: no aggregate and proof domain type provider specified", - }, - { - name: "AggregateAndProofDomianTypeProviderErrors", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(context.Background())), - standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(mock.NewErroringAggregateAndProofDomainProvider()), - standard.WithDomainProvider(domainProvider), - }, - err: "failed to obtain aggregate and proof domain type: error", - }, - { - name: "DomainProviderMissing", - params: []standard.Parameter{ - standard.WithLogLevel(zerolog.Disabled), - standard.WithMonitor(nullmetrics.New(context.Background())), - standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), - }, - err: "problem with parameters: no domain provider specified", + err: "failed to obtain spec: error", }, { name: "Good", @@ -279,12 +84,7 @@ func TestService(t *testing.T) { standard.WithLogLevel(zerolog.Disabled), standard.WithMonitor(nullmetrics.New(context.Background())), standard.WithClientMonitor(nullmetrics.New(context.Background())), - standard.WithSlotsPerEpochProvider(slotsPerEpochProvider), - standard.WithBeaconProposerDomainTypeProvider(beaconProposerDomainTypeProvider), - standard.WithBeaconAttesterDomainTypeProvider(beaconAttesterDomainTypeProvider), - standard.WithRANDAODomainTypeProvider(randaoDomainTypeProvider), - standard.WithSelectionProofDomainTypeProvider(selectionProofDomainTypeProvider), - standard.WithAggregateAndProofDomainTypeProvider(aggregateAndProofDomainTypeProvider), + standard.WithSpecProvider(specProvider), standard.WithDomainProvider(domainProvider), }, }, diff --git a/services/signer/standard/signcontributonandproof.go b/services/signer/standard/signcontributonandproof.go new file mode 100644 index 0000000..cb27ce5 --- /dev/null +++ b/services/signer/standard/signcontributonandproof.go @@ -0,0 +1,55 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" +) + +// SignContributionAndProof signs a sync committee contribution for given slot and root. +func (s *Service) SignContributionAndProof(ctx context.Context, + account e2wtypes.Account, + contributionAndProof *altair.ContributionAndProof, +) ( + phase0.BLSSignature, + error, +) { + if s.contributionAndProofDomainType == nil { + return phase0.BLSSignature{}, errors.New("no contribution and proof domain type available; cannot sign") + } + + root, err := contributionAndProof.HashTreeRoot() + if err != nil { + return phase0.BLSSignature{}, errors.Wrap(err, "failed to calculate hash tree root") + } + + // Calculate the domain. + epoch := phase0.Epoch(contributionAndProof.Contribution.Slot / s.slotsPerEpoch) + domain, err := s.domainProvider.Domain(ctx, *s.contributionAndProofDomainType, epoch) + if err != nil { + return phase0.BLSSignature{}, errors.Wrap(err, "failed to obtain signature domain for contribution and proof") + } + + sig, err := s.sign(ctx, account, root, domain) + if err != nil { + return phase0.BLSSignature{}, errors.Wrap(err, "failed to sign contribution and proof") + } + + return sig, nil +} diff --git a/services/signer/standard/signsynccommitteeroot.go b/services/signer/standard/signsynccommitteeroot.go new file mode 100644 index 0000000..6201851 --- /dev/null +++ b/services/signer/standard/signsynccommitteeroot.go @@ -0,0 +1,50 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" +) + +// SignSyncCommitteeRoot returns a root signature. +// This signs a beacon block root with the "sync committee" domain. +func (s *Service) SignSyncCommitteeRoot(ctx context.Context, + account e2wtypes.Account, + epoch phase0.Epoch, + root phase0.Root, +) ( + phase0.BLSSignature, + error, +) { + if s.syncCommitteeDomainType == nil { + return phase0.BLSSignature{}, errors.New("no sync committee domain type available; cannot sign") + } + + // Calculate the domain. + domain, err := s.domainProvider.Domain(ctx, *s.syncCommitteeDomainType, epoch) + if err != nil { + return phase0.BLSSignature{}, errors.Wrap(err, "failed to obtain signature domain for sync committee") + } + + sig, err := s.sign(ctx, account, root, domain) + if err != nil { + return phase0.BLSSignature{}, errors.Wrap(err, "failed to sign sync committee root") + } + + return sig, nil +} diff --git a/services/signer/standard/signsynccommitteeselection.go b/services/signer/standard/signsynccommitteeselection.go new file mode 100644 index 0000000..7aa0f73 --- /dev/null +++ b/services/signer/standard/signsynccommitteeselection.go @@ -0,0 +1,62 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" +) + +// SignSyncCommitteeSelection returns a sync committee selection signature. +// This signs a slot and subcommittee with the "sync committee selection proof" domain. +func (s *Service) SignSyncCommitteeSelection(ctx context.Context, + account e2wtypes.Account, + slot phase0.Slot, + subcommitteeIndex uint64, +) ( + phase0.BLSSignature, + error, +) { + if s.syncCommitteeSelectionProofDomainType == nil { + return phase0.BLSSignature{}, errors.New("no sync committee selection proof domain type, cannot sign") + } + + // Calculate the domain. + domain, err := s.domainProvider.Domain(ctx, + *s.syncCommitteeSelectionProofDomainType, + phase0.Epoch(slot/s.slotsPerEpoch)) + if err != nil { + return phase0.BLSSignature{}, errors.Wrap(err, "failed to obtain signature domain for sync committee selection proof") + } + + selectionData := &altair.SyncAggregatorSelectionData{ + Slot: slot, + SubcommitteeIndex: subcommitteeIndex, + } + root, err := selectionData.HashTreeRoot() + if err != nil { + return phase0.BLSSignature{}, errors.Wrap(err, "failed to obtain hash tree root of sync aggregator selection data") + } + + sig, err := s.sign(ctx, account, root, domain) + if err != nil { + return phase0.BLSSignature{}, errors.Wrap(err, "failed to sign sync committee selection proof") + } + + return sig, nil +} diff --git a/services/submitter/immediate/parameters.go b/services/submitter/immediate/parameters.go index db6ebc2..beb073a 100644 --- a/services/submitter/immediate/parameters.go +++ b/services/submitter/immediate/parameters.go @@ -31,6 +31,9 @@ type parameters struct { attestationsSubmitter eth2client.AttestationsSubmitter beaconCommitteeSubscriptionsSubmitter eth2client.BeaconCommitteeSubscriptionsSubmitter aggregateAttestationsSubmitter eth2client.AggregateAttestationsSubmitter + syncCommitteeMessagesSubmitter eth2client.SyncCommitteeMessagesSubmitter + syncCommitteeSubscriptionsSubmitter eth2client.SyncCommitteeSubscriptionsSubmitter + syncCommitteeContributionsSubmitter eth2client.SyncCommitteeContributionsSubmitter } // Parameter is the interface for service parameters. @@ -72,6 +75,27 @@ func WithAttestationsSubmitter(submitter eth2client.AttestationsSubmitter) Param }) } +// WithSyncCommitteeMessagesSubmitter sets the sync committee messages submitter. +func WithSyncCommitteeMessagesSubmitter(submitter eth2client.SyncCommitteeMessagesSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeMessagesSubmitter = submitter + }) +} + +// WithSyncCommitteeSubscriptionsSubmitter sets the sync committee subscriptions submitter +func WithSyncCommitteeSubscriptionsSubmitter(submitter eth2client.SyncCommitteeSubscriptionsSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeSubscriptionsSubmitter = submitter + }) +} + +// WithSyncCommitteeContributionsSubmitter sets the sync committee contributions submitter +func WithSyncCommitteeContributionsSubmitter(submitter eth2client.SyncCommitteeContributionsSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeContributionsSubmitter = submitter + }) +} + // WithBeaconCommitteeSubscriptionsSubmitter sets the attestation subnet subscriptions submitter func WithBeaconCommitteeSubscriptionsSubmitter(submitter eth2client.BeaconCommitteeSubscriptionsSubmitter) Parameter { return parameterFunc(func(p *parameters) { @@ -107,6 +131,15 @@ func parseAndCheckParameters(params ...Parameter) (*parameters, error) { if parameters.attestationsSubmitter == nil { return nil, errors.New("no attestations submitter specified") } + if parameters.syncCommitteeMessagesSubmitter == nil { + return nil, errors.New("no sync committee messages submitter specified") + } + if parameters.syncCommitteeSubscriptionsSubmitter == nil { + return nil, errors.New("no sync committee subscriptions submitter specified") + } + if parameters.syncCommitteeContributionsSubmitter == nil { + return nil, errors.New("no sync committee contributions submitter specified") + } if parameters.beaconCommitteeSubscriptionsSubmitter == nil { return nil, errors.New("no beacon committee subscriptions submitter specified") } diff --git a/services/submitter/immediate/service.go b/services/submitter/immediate/service.go index e865e60..1d9c835 100644 --- a/services/submitter/immediate/service.go +++ b/services/submitter/immediate/service.go @@ -20,6 +20,8 @@ import ( eth2client "github.com/attestantio/go-eth2-client" api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/attestantio/vouch/services/metrics" "github.com/pkg/errors" @@ -34,6 +36,9 @@ type Service struct { beaconBlockSubmitter eth2client.BeaconBlockSubmitter beaconCommitteeSubscriptionsSubmitter eth2client.BeaconCommitteeSubscriptionsSubmitter aggregateAttestationsSubmitter eth2client.AggregateAttestationsSubmitter + syncCommitteeMessagesSubmitter eth2client.SyncCommitteeMessagesSubmitter + syncCommitteeSubscriptionsSubmitter eth2client.SyncCommitteeSubscriptionsSubmitter + syncCommitteeContributionsSubmitter eth2client.SyncCommitteeContributionsSubmitter } // module-wide log. @@ -58,13 +63,16 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) { beaconBlockSubmitter: parameters.beaconBlockSubmitter, beaconCommitteeSubscriptionsSubmitter: parameters.beaconCommitteeSubscriptionsSubmitter, aggregateAttestationsSubmitter: parameters.aggregateAttestationsSubmitter, + syncCommitteeMessagesSubmitter: parameters.syncCommitteeMessagesSubmitter, + syncCommitteeSubscriptionsSubmitter: parameters.syncCommitteeSubscriptionsSubmitter, + syncCommitteeContributionsSubmitter: parameters.syncCommitteeContributionsSubmitter, } return s, nil } // SubmitBeaconBlock submits a block. -func (s *Service) SubmitBeaconBlock(ctx context.Context, block *phase0.SignedBeaconBlock) error { +func (s *Service) SubmitBeaconBlock(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error { if block == nil { return errors.New("no beacon block supplied") } @@ -187,3 +195,84 @@ func (s *Service) SubmitAggregateAttestations(ctx context.Context, aggregates [] return nil } + +// SubmitSyncCommitteeMessages submits sync committee messages. +func (s *Service) SubmitSyncCommitteeMessages(ctx context.Context, messages []*altair.SyncCommitteeMessage) error { + if len(messages) == 0 { + return errors.New("no sync committee messages supplied") + } + + started := time.Now() + err := s.syncCommitteeMessagesSubmitter.SubmitSyncCommitteeMessages(ctx, messages) + if service, isService := s.aggregateAttestationsSubmitter.(eth2client.Service); isService { + s.clientMonitor.ClientOperation(service.Address(), "submit sync committee messages", err == nil, time.Since(started)) + } else { + s.clientMonitor.ClientOperation("", "submit sync committee messages", err == nil, time.Since(started)) + } + if err != nil { + return errors.Wrap(err, "failed to submit sync committee messages") + } + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(messages) + if err == nil { + e.Str("messages", string(data)).Msg("Submitted sync committee messages") + } + } + + return nil +} + +// SubmitSyncCommitteeSubscriptions submits a batch of beacon committee subscriptions. +func (s *Service) SubmitSyncCommitteeSubscriptions(ctx context.Context, subscriptions []*api.SyncCommitteeSubscription) error { + if len(subscriptions) == 0 { + return errors.New("no sync committee subscriptions supplied") + } + + started := time.Now() + err := s.syncCommitteeSubscriptionsSubmitter.SubmitSyncCommitteeSubscriptions(ctx, subscriptions) + if service, isService := s.syncCommitteeSubscriptionsSubmitter.(eth2client.Service); isService { + s.clientMonitor.ClientOperation(service.Address(), "submit sync committee subscription", err == nil, time.Since(started)) + } else { + s.clientMonitor.ClientOperation("", "submit sync committee subscription", err == nil, time.Since(started)) + } + if err != nil { + return errors.Wrap(err, "failed to submit sync committee subscriptions") + } + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(subscriptions) + if err == nil { + e.Str("subscriptions", string(data)).Int("subscribing", len(subscriptions)).Msg("Submitted subscriptions") + } + } + + return nil +} + +// SubmitSyncCommitteeContributions submits sync committee contributions. +func (s *Service) SubmitSyncCommitteeContributions(ctx context.Context, contributionAndProofs []*altair.SignedContributionAndProof) error { + if len(contributionAndProofs) == 0 { + return errors.New("no sync committee contribution and proofs supplied") + } + + started := time.Now() + err := s.syncCommitteeContributionsSubmitter.SubmitSyncCommitteeContributions(ctx, contributionAndProofs) + if service, isService := s.syncCommitteeContributionsSubmitter.(eth2client.Service); isService { + s.clientMonitor.ClientOperation(service.Address(), "submit sync committee contribution and proofs", err == nil, time.Since(started)) + } else { + s.clientMonitor.ClientOperation("", "submit sync committee contribution and proofs", err == nil, time.Since(started)) + } + if err != nil { + return errors.Wrap(err, "failed to submit sync committee contribution and proofs") + } + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(contributionAndProofs) + if err == nil { + e.Str("contributionAndProofs", string(data)).Msg("Submitted contribution and proofs") + } + } + + return nil +} diff --git a/services/submitter/immediate/service_test.go b/services/submitter/immediate/service_test.go index eef826b..cd8d5dc 100644 --- a/services/submitter/immediate/service_test.go +++ b/services/submitter/immediate/service_test.go @@ -18,6 +18,8 @@ import ( "testing" api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/attestantio/vouch/mock" "github.com/attestantio/vouch/services/submitter" @@ -31,12 +33,30 @@ func TestService(t *testing.T) { beaconBlockSubmitter := mock.NewBeaconBlockSubmitter() beaconCommitteeSubscriptionSubmitter := mock.NewBeaconCommitteeSubscriptionsSubmitter() aggregateAttestationSubmitter := mock.NewAggregateAttestationsSubmitter() + syncCommitteeMessagesSubmitter := mock.NewSyncCommitteeMessagesSubmitter() + syncCommitteeSubscriptionsSubmitter := mock.NewSyncCommitteeSubscriptionsSubmitter() + syncCommitteeContributionsSubmitter := mock.NewSyncCommitteeContributionsSubmitter() tests := []struct { name string params []immediate.Parameter err string }{ + { + name: "ClientMonitorMisssing", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.TraceLevel), + immediate.WithClientMonitor(nil), + immediate.WithAttestationsSubmitter(attestationsSubmitter), + immediate.WithBeaconBlockSubmitter(beaconBlockSubmitter), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(beaconCommitteeSubscriptionSubmitter), + immediate.WithAggregateAttestationsSubmitter(aggregateAttestationSubmitter), + immediate.WithSyncCommitteeSubscriptionsSubmitter(syncCommitteeSubscriptionsSubmitter), + immediate.WithSyncCommitteeMessagesSubmitter(syncCommitteeMessagesSubmitter), + immediate.WithSyncCommitteeContributionsSubmitter(syncCommitteeContributionsSubmitter), + }, + err: "problem with parameters: no client monitor specified", + }, { name: "AttestationsSubmitterMissing", params: []immediate.Parameter{ @@ -44,6 +64,9 @@ func TestService(t *testing.T) { immediate.WithBeaconBlockSubmitter(beaconBlockSubmitter), immediate.WithBeaconCommitteeSubscriptionsSubmitter(beaconCommitteeSubscriptionSubmitter), immediate.WithAggregateAttestationsSubmitter(aggregateAttestationSubmitter), + immediate.WithSyncCommitteeSubscriptionsSubmitter(syncCommitteeSubscriptionsSubmitter), + immediate.WithSyncCommitteeMessagesSubmitter(syncCommitteeMessagesSubmitter), + immediate.WithSyncCommitteeContributionsSubmitter(syncCommitteeContributionsSubmitter), }, err: "problem with parameters: no attestations submitter specified", }, @@ -54,6 +77,9 @@ func TestService(t *testing.T) { immediate.WithAttestationsSubmitter(attestationsSubmitter), immediate.WithBeaconCommitteeSubscriptionsSubmitter(beaconCommitteeSubscriptionSubmitter), immediate.WithAggregateAttestationsSubmitter(aggregateAttestationSubmitter), + immediate.WithSyncCommitteeSubscriptionsSubmitter(syncCommitteeSubscriptionsSubmitter), + immediate.WithSyncCommitteeMessagesSubmitter(syncCommitteeMessagesSubmitter), + immediate.WithSyncCommitteeContributionsSubmitter(syncCommitteeContributionsSubmitter), }, err: "problem with parameters: no beacon block submitter specified", }, @@ -64,6 +90,9 @@ func TestService(t *testing.T) { immediate.WithAttestationsSubmitter(attestationsSubmitter), immediate.WithBeaconBlockSubmitter(beaconBlockSubmitter), immediate.WithAggregateAttestationsSubmitter(aggregateAttestationSubmitter), + immediate.WithSyncCommitteeSubscriptionsSubmitter(syncCommitteeSubscriptionsSubmitter), + immediate.WithSyncCommitteeMessagesSubmitter(syncCommitteeMessagesSubmitter), + immediate.WithSyncCommitteeContributionsSubmitter(syncCommitteeContributionsSubmitter), }, err: "problem with parameters: no beacon committee subscriptions submitter specified", }, @@ -74,9 +103,50 @@ func TestService(t *testing.T) { immediate.WithAttestationsSubmitter(attestationsSubmitter), immediate.WithBeaconBlockSubmitter(beaconBlockSubmitter), immediate.WithBeaconCommitteeSubscriptionsSubmitter(beaconCommitteeSubscriptionSubmitter), + immediate.WithSyncCommitteeSubscriptionsSubmitter(syncCommitteeSubscriptionsSubmitter), + immediate.WithSyncCommitteeMessagesSubmitter(syncCommitteeMessagesSubmitter), + immediate.WithSyncCommitteeContributionsSubmitter(syncCommitteeContributionsSubmitter), }, err: "problem with parameters: no aggregate attestations submitter specified", }, + { + name: "SyncCommitteeSubscriptionsSubmitterMissing", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.TraceLevel), + immediate.WithAttestationsSubmitter(attestationsSubmitter), + immediate.WithBeaconBlockSubmitter(beaconBlockSubmitter), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(beaconCommitteeSubscriptionSubmitter), + immediate.WithSyncCommitteeMessagesSubmitter(syncCommitteeMessagesSubmitter), + immediate.WithSyncCommitteeContributionsSubmitter(syncCommitteeContributionsSubmitter), + }, + err: "problem with parameters: no sync committee subscriptions submitter specified", + }, + { + name: "SyncCommitteeMessagesSubmitterMissing", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.TraceLevel), + immediate.WithAttestationsSubmitter(attestationsSubmitter), + immediate.WithBeaconBlockSubmitter(beaconBlockSubmitter), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(beaconCommitteeSubscriptionSubmitter), + immediate.WithAggregateAttestationsSubmitter(aggregateAttestationSubmitter), + immediate.WithSyncCommitteeSubscriptionsSubmitter(syncCommitteeSubscriptionsSubmitter), + immediate.WithSyncCommitteeContributionsSubmitter(syncCommitteeContributionsSubmitter), + }, + err: "problem with parameters: no sync committee messages submitter specified", + }, + { + name: "SyncCommitteeContributionsSubmitterMissing", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.TraceLevel), + immediate.WithAttestationsSubmitter(attestationsSubmitter), + immediate.WithBeaconBlockSubmitter(beaconBlockSubmitter), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(beaconCommitteeSubscriptionSubmitter), + immediate.WithAggregateAttestationsSubmitter(aggregateAttestationSubmitter), + immediate.WithSyncCommitteeSubscriptionsSubmitter(syncCommitteeSubscriptionsSubmitter), + immediate.WithSyncCommitteeMessagesSubmitter(syncCommitteeMessagesSubmitter), + }, + err: "problem with parameters: no sync committee contributions submitter specified", + }, { name: "Good", params: []immediate.Parameter{ @@ -85,6 +155,9 @@ func TestService(t *testing.T) { immediate.WithBeaconBlockSubmitter(beaconBlockSubmitter), immediate.WithBeaconCommitteeSubscriptionsSubmitter(beaconCommitteeSubscriptionSubmitter), immediate.WithAggregateAttestationsSubmitter(aggregateAttestationSubmitter), + immediate.WithSyncCommitteeSubscriptionsSubmitter(syncCommitteeSubscriptionsSubmitter), + immediate.WithSyncCommitteeMessagesSubmitter(syncCommitteeMessagesSubmitter), + immediate.WithSyncCommitteeContributionsSubmitter(syncCommitteeContributionsSubmitter), }, }, } @@ -107,6 +180,9 @@ func TestInterfaces(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), ) require.NoError(t, err) require.Implements(t, (*submitter.BeaconBlockSubmitter)(nil), s) @@ -119,7 +195,7 @@ func TestSubmitBeaconBlock(t *testing.T) { tests := []struct { name string params []immediate.Parameter - block *phase0.SignedBeaconBlock + block *spec.VersionedSignedBeaconBlock err string }{ { @@ -130,6 +206,9 @@ func TestSubmitBeaconBlock(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, err: "no beacon block supplied", }, @@ -141,8 +220,11 @@ func TestSubmitBeaconBlock(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, - block: &phase0.SignedBeaconBlock{}, + block: &spec.VersionedSignedBeaconBlock{}, }, { name: "Erroring", @@ -152,8 +234,11 @@ func TestSubmitBeaconBlock(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewErroringBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, - block: &phase0.SignedBeaconBlock{}, + block: &spec.VersionedSignedBeaconBlock{}, err: "failed to submit beacon block: error", }, { @@ -164,8 +249,11 @@ func TestSubmitBeaconBlock(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, - block: &phase0.SignedBeaconBlock{}, + block: &spec.VersionedSignedBeaconBlock{}, }, } @@ -199,6 +287,9 @@ func TestSubmitAttestations(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, err: "no attestations supplied", }, @@ -210,6 +301,9 @@ func TestSubmitAttestations(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, attestations: []*phase0.Attestation{}, err: "no attestations supplied", @@ -222,6 +316,9 @@ func TestSubmitAttestations(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, attestations: []*phase0.Attestation{{}}, err: "failed to submit attestations: error", @@ -234,6 +331,9 @@ func TestSubmitAttestations(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, attestations: []*phase0.Attestation{{}}, }, @@ -269,6 +369,9 @@ func TestSubmitAggregateAttestations(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, err: "no aggregate attestations supplied", }, @@ -280,6 +383,9 @@ func TestSubmitAggregateAttestations(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, aggregates: []*phase0.SignedAggregateAndProof{}, err: "no aggregate attestations supplied", @@ -292,6 +398,9 @@ func TestSubmitAggregateAttestations(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewErroringAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, aggregates: []*phase0.SignedAggregateAndProof{ {}, @@ -306,6 +415,9 @@ func TestSubmitAggregateAttestations(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, aggregates: []*phase0.SignedAggregateAndProof{ {}, @@ -343,6 +455,9 @@ func TestSubmitBeaconCommitteeSubscriptions(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, err: "no beacon committee subscriptions supplied", }, @@ -354,6 +469,9 @@ func TestSubmitBeaconCommitteeSubscriptions(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, subscriptions: []*api.BeaconCommitteeSubscription{}, err: "no beacon committee subscriptions supplied", @@ -366,6 +484,9 @@ func TestSubmitBeaconCommitteeSubscriptions(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewErroringBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, subscriptions: []*api.BeaconCommitteeSubscription{ {}, @@ -380,6 +501,9 @@ func TestSubmitBeaconCommitteeSubscriptions(t *testing.T) { immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), }, subscriptions: []*api.BeaconCommitteeSubscription{ {}, @@ -401,3 +525,261 @@ func TestSubmitBeaconCommitteeSubscriptions(t *testing.T) { }) } } + +func TestSubmitSyncCommitteeSubscriptions(t *testing.T) { + tests := []struct { + name string + params []immediate.Parameter + subscriptions []*api.SyncCommitteeSubscription + err string + }{ + { + name: "Nil", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationsSubmitter(mock.NewAttestationsSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), + }, + err: "no sync committee subscriptions supplied", + }, + { + name: "Empty", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationsSubmitter(mock.NewAttestationsSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), + }, + subscriptions: []*api.SyncCommitteeSubscription{}, + err: "no sync committee subscriptions supplied", + }, + { + name: "Erroring", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationsSubmitter(mock.NewAttestationsSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewErroringSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), + }, + subscriptions: []*api.SyncCommitteeSubscription{ + {}, + }, + err: "failed to submit sync committee subscriptions: error", + }, + { + name: "Good", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.TraceLevel), + immediate.WithAttestationsSubmitter(mock.NewAttestationsSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), + }, + subscriptions: []*api.SyncCommitteeSubscription{ + {}, + }, + }, + } + + for _, test := range tests { + s, err := immediate.New(context.Background(), test.params...) + require.NoError(t, err) + + t.Run(test.name, func(t *testing.T) { + err := s.SubmitSyncCommitteeSubscriptions(context.Background(), test.subscriptions) + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestSubmitSyncCommitteeMessages(t *testing.T) { + tests := []struct { + name string + params []immediate.Parameter + messages []*altair.SyncCommitteeMessage + err string + }{ + { + name: "Nil", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationsSubmitter(mock.NewAttestationsSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), + }, + err: "no sync committee messages supplied", + }, + { + name: "Empty", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationsSubmitter(mock.NewAttestationsSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), + }, + messages: []*altair.SyncCommitteeMessage{}, + err: "no sync committee messages supplied", + }, + { + name: "Erroring", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationsSubmitter(mock.NewAttestationsSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewErroringSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), + }, + messages: []*altair.SyncCommitteeMessage{ + {}, + }, + err: "failed to submit sync committee messages: error", + }, + { + name: "Good", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.TraceLevel), + immediate.WithAttestationsSubmitter(mock.NewAttestationsSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), + }, + messages: []*altair.SyncCommitteeMessage{ + {}, + }, + }, + } + + for _, test := range tests { + s, err := immediate.New(context.Background(), test.params...) + require.NoError(t, err) + + t.Run(test.name, func(t *testing.T) { + err := s.SubmitSyncCommitteeMessages(context.Background(), test.messages) + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestSubmitSyncCommitteeContributions(t *testing.T) { + tests := []struct { + name string + params []immediate.Parameter + contributions []*altair.SignedContributionAndProof + err string + }{ + { + name: "Nil", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationsSubmitter(mock.NewAttestationsSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), + }, + err: "no sync committee contribution and proofs supplied", + }, + { + name: "Empty", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationsSubmitter(mock.NewAttestationsSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), + }, + contributions: []*altair.SignedContributionAndProof{}, + err: "no sync committee contribution and proofs supplied", + }, + { + name: "Erroring", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.Disabled), + immediate.WithAttestationsSubmitter(mock.NewAttestationsSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewErroringSyncCommitteeContributionsSubmitter()), + }, + contributions: []*altair.SignedContributionAndProof{ + {}, + }, + err: "failed to submit sync committee contribution and proofs: error", + }, + { + name: "Good", + params: []immediate.Parameter{ + immediate.WithLogLevel(zerolog.TraceLevel), + immediate.WithAttestationsSubmitter(mock.NewAttestationsSubmitter()), + immediate.WithBeaconBlockSubmitter(mock.NewBeaconBlockSubmitter()), + immediate.WithBeaconCommitteeSubscriptionsSubmitter(mock.NewBeaconCommitteeSubscriptionsSubmitter()), + immediate.WithAggregateAttestationsSubmitter(mock.NewAggregateAttestationsSubmitter()), + immediate.WithSyncCommitteeSubscriptionsSubmitter(mock.NewSyncCommitteeSubscriptionsSubmitter()), + immediate.WithSyncCommitteeMessagesSubmitter(mock.NewSyncCommitteeMessagesSubmitter()), + immediate.WithSyncCommitteeContributionsSubmitter(mock.NewSyncCommitteeContributionsSubmitter()), + }, + contributions: []*altair.SignedContributionAndProof{ + {}, + }, + }, + } + + for _, test := range tests { + s, err := immediate.New(context.Background(), test.params...) + require.NoError(t, err) + + t.Run(test.name, func(t *testing.T) { + err := s.SubmitSyncCommitteeContributions(context.Background(), test.contributions) + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/services/submitter/multinode/parameters.go b/services/submitter/multinode/parameters.go index 4b277f6..7c97a92 100644 --- a/services/submitter/multinode/parameters.go +++ b/services/submitter/multinode/parameters.go @@ -33,6 +33,9 @@ type parameters struct { attestationsSubmitters map[string]eth2client.AttestationsSubmitter aggregateAttestationsSubmitters map[string]eth2client.AggregateAttestationsSubmitter beaconCommitteeSubscriptionsSubmitters map[string]eth2client.BeaconCommitteeSubscriptionsSubmitter + syncCommitteeMessagesSubmitter map[string]eth2client.SyncCommitteeMessagesSubmitter + syncCommitteeSubscriptionsSubmitters map[string]eth2client.SyncCommitteeSubscriptionsSubmitter + syncCommitteeContributionsSubmitters map[string]eth2client.SyncCommitteeContributionsSubmitter } // Parameter is the interface for service parameters. @@ -95,6 +98,27 @@ func WithBeaconCommitteeSubscriptionsSubmitters(submitters map[string]eth2client }) } +// WithSyncCommitteeMessagesSubmitters sets the sync committee messages submitters. +func WithSyncCommitteeMessagesSubmitters(submitters map[string]eth2client.SyncCommitteeMessagesSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeMessagesSubmitter = submitters + }) +} + +// WithSyncCommitteeSubscriptionsSubmitters sets the sync committee subscriptions submitters. +func WithSyncCommitteeSubscriptionsSubmitters(submitters map[string]eth2client.SyncCommitteeSubscriptionsSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeSubscriptionsSubmitters = submitters + }) +} + +// WithSyncCommitteeContributionsSubmitters sets the sync committee contributions submitters. +func WithSyncCommitteeContributionsSubmitters(submitters map[string]eth2client.SyncCommitteeContributionsSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeContributionsSubmitters = submitters + }) +} + // parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. func parseAndCheckParameters(params ...Parameter) (*parameters, error) { parameters := parameters{ @@ -125,6 +149,15 @@ func parseAndCheckParameters(params ...Parameter) (*parameters, error) { if len(parameters.beaconCommitteeSubscriptionsSubmitters) == 0 { return nil, errors.New("no beacon committee subscription submitters specified") } + if len(parameters.syncCommitteeMessagesSubmitter) == 0 { + return nil, errors.New("no sync committee messages submitters specified") + } + if len(parameters.syncCommitteeSubscriptionsSubmitters) == 0 { + return nil, errors.New("no sync committee subscription submitters specified") + } + if len(parameters.syncCommitteeContributionsSubmitters) == 0 { + return nil, errors.New("no sync committee subscription contributions specified") + } return ¶meters, nil } diff --git a/services/submitter/multinode/service.go b/services/submitter/multinode/service.go index a47c99c..2016211 100644 --- a/services/submitter/multinode/service.go +++ b/services/submitter/multinode/service.go @@ -31,6 +31,9 @@ type Service struct { attestationsSubmitters map[string]eth2client.AttestationsSubmitter aggregateAttestationsSubmitters map[string]eth2client.AggregateAttestationsSubmitter beaconCommitteeSubscriptionSubmitters map[string]eth2client.BeaconCommitteeSubscriptionsSubmitter + syncCommitteeMessagesSubmitter map[string]eth2client.SyncCommitteeMessagesSubmitter + syncCommitteeSubscriptionSubmitters map[string]eth2client.SyncCommitteeSubscriptionsSubmitter + syncCommitteeContributionsSubmitters map[string]eth2client.SyncCommitteeContributionsSubmitter } // module-wide log. @@ -56,6 +59,9 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) { attestationsSubmitters: parameters.attestationsSubmitters, aggregateAttestationsSubmitters: parameters.aggregateAttestationsSubmitters, beaconCommitteeSubscriptionSubmitters: parameters.beaconCommitteeSubscriptionsSubmitters, + syncCommitteeMessagesSubmitter: parameters.syncCommitteeMessagesSubmitter, + syncCommitteeSubscriptionSubmitters: parameters.syncCommitteeSubscriptionsSubmitters, + syncCommitteeContributionsSubmitters: parameters.syncCommitteeContributionsSubmitters, } log.Trace().Int64("process_concurrency", s.processConcurrency).Msg("Set process concurrency") diff --git a/services/submitter/multinode/submitbeaconblock.go b/services/submitter/multinode/submitbeaconblock.go index 06cbe02..e96fc9c 100644 --- a/services/submitter/multinode/submitbeaconblock.go +++ b/services/submitter/multinode/submitbeaconblock.go @@ -19,18 +19,23 @@ import ( "time" eth2client "github.com/attestantio/go-eth2-client" - "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/go-eth2-client/spec" "github.com/pkg/errors" "golang.org/x/sync/semaphore" ) // SubmitBeaconBlock submits a beacon block. -func (s *Service) SubmitBeaconBlock(ctx context.Context, block *phase0.SignedBeaconBlock) error { +func (s *Service) SubmitBeaconBlock(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error { if block == nil { return errors.New("no beacon block supplied") } - log := log.With().Uint64("slot", uint64(block.Message.Slot)).Logger() + blockSlot, err := block.Slot() + if err != nil { + return err + } + + log := log.With().Uint64("slot", uint64(blockSlot)).Logger() sem := semaphore.NewWeighted(s.processConcurrency) var wg sync.WaitGroup for name, submitter := range s.beaconBlockSubmitters { diff --git a/services/submitter/multinode/submitsynccommitteecontributions.go b/services/submitter/multinode/submitsynccommitteecontributions.go new file mode 100644 index 0000000..b30b57b --- /dev/null +++ b/services/submitter/multinode/submitsynccommitteecontributions.go @@ -0,0 +1,71 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multinode + +import ( + "context" + "encoding/json" + "sync" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/pkg/errors" + "golang.org/x/sync/semaphore" +) + +// SubmitSyncCommitteeContributions submits sync committee contributions. +func (s *Service) SubmitSyncCommitteeContributions(ctx context.Context, contributionAndProofs []*altair.SignedContributionAndProof) error { + if len(contributionAndProofs) == 0 { + return errors.New("no contribution and proofs supplied") + } + + sem := semaphore.NewWeighted(s.processConcurrency) + var wg sync.WaitGroup + for name, submitter := range s.syncCommitteeContributionsSubmitters { + wg.Add(1) + go func(ctx context.Context, + sem *semaphore.Weighted, + wg *sync.WaitGroup, + name string, + submitter eth2client.SyncCommitteeContributionsSubmitter, + ) { + defer wg.Done() + log := log.With().Str("beacon_node_address", name).Uint64("slot", uint64(contributionAndProofs[0].Message.Contribution.Slot)).Logger() + if err := sem.Acquire(ctx, 1); err != nil { + log.Error().Err(err).Msg("Failed to acquire semaphore") + return + } + defer sem.Release(1) + + _, address := s.serviceInfo(ctx, submitter) + started := time.Now() + err := submitter.SubmitSyncCommitteeContributions(ctx, contributionAndProofs) + s.clientMonitor.ClientOperation(address, "submit contribution and proofs", err == nil, time.Since(started)) + if err != nil { + log.Warn().Err(err).Msg("Failed to submit contribution and proofs") + } else { + data, err := json.Marshal(contributionAndProofs) + if err != nil { + log.Error().Err(err).Msg("Failed to marshal JSON") + } else { + log.Trace().Str("data", string(data)).Msg("Submitted contribution and proofs") + } + } + }(ctx, sem, &wg, name, submitter) + } + wg.Wait() + + return nil +} diff --git a/services/submitter/multinode/submitsynccommitteemessages.go b/services/submitter/multinode/submitsynccommitteemessages.go new file mode 100644 index 0000000..e55a570 --- /dev/null +++ b/services/submitter/multinode/submitsynccommitteemessages.go @@ -0,0 +1,71 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multinode + +import ( + "context" + "encoding/json" + "sync" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/pkg/errors" + "golang.org/x/sync/semaphore" +) + +// SubmitSyncCommitteeMessages submits sync committee messages. +func (s *Service) SubmitSyncCommitteeMessages(ctx context.Context, messages []*altair.SyncCommitteeMessage) error { + if len(messages) == 0 { + return errors.New("no messages supplied") + } + + sem := semaphore.NewWeighted(s.processConcurrency) + var wg sync.WaitGroup + for name, submitter := range s.syncCommitteeMessagesSubmitter { + wg.Add(1) + go func(ctx context.Context, + sem *semaphore.Weighted, + wg *sync.WaitGroup, + name string, + submitter eth2client.SyncCommitteeMessagesSubmitter, + ) { + defer wg.Done() + log := log.With().Str("beacon_node_address", name).Uint64("slot", uint64(messages[0].Slot)).Logger() + if err := sem.Acquire(ctx, 1); err != nil { + log.Error().Err(err).Msg("Failed to acquire semaphore") + return + } + defer sem.Release(1) + + _, address := s.serviceInfo(ctx, submitter) + started := time.Now() + err := submitter.SubmitSyncCommitteeMessages(ctx, messages) + s.clientMonitor.ClientOperation(address, "submit messages", err == nil, time.Since(started)) + if err != nil { + log.Warn().Err(err).Msg("Failed to submit messages") + } else { + data, err := json.Marshal(messages) + if err != nil { + log.Error().Err(err).Msg("Failed to marshal JSON") + } else { + log.Trace().Str("data", string(data)).Msg("Submitted messages") + } + } + }(ctx, sem, &wg, name, submitter) + } + wg.Wait() + + return nil +} diff --git a/services/submitter/multinode/submitsynccommitteesubscriptions.go b/services/submitter/multinode/submitsynccommitteesubscriptions.go new file mode 100644 index 0000000..7fa050a --- /dev/null +++ b/services/submitter/multinode/submitsynccommitteesubscriptions.go @@ -0,0 +1,71 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multinode + +import ( + "context" + "encoding/json" + "sync" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/pkg/errors" + "golang.org/x/sync/semaphore" +) + +// SubmitSyncCommitteeSubscriptions submits a batch of sync committee subscriptions. +func (s *Service) SubmitSyncCommitteeSubscriptions(ctx context.Context, subscriptions []*api.SyncCommitteeSubscription) error { + if len(subscriptions) == 0 { + return errors.New("no subscriptions supplied") + } + + sem := semaphore.NewWeighted(s.processConcurrency) + var wg sync.WaitGroup + for name, submitter := range s.syncCommitteeSubscriptionSubmitters { + wg.Add(1) + go func(ctx context.Context, + sem *semaphore.Weighted, + wg *sync.WaitGroup, + name string, + submitter eth2client.SyncCommitteeSubscriptionsSubmitter, + ) { + defer wg.Done() + log := log.With().Str("beacon_node_address", name).Int("subscriptions", len(subscriptions)).Logger() + if err := sem.Acquire(ctx, 1); err != nil { + log.Error().Err(err).Msg("Failed to acquire semaphore") + return + } + defer sem.Release(1) + + _, address := s.serviceInfo(ctx, submitter) + started := time.Now() + err := submitter.SubmitSyncCommitteeSubscriptions(ctx, subscriptions) + s.clientMonitor.ClientOperation(address, "submit sync committee subscriptions", err == nil, time.Since(started)) + if err != nil { + log.Warn().Err(err).Msg("Failed to submit subscriptions") + } else { + data, err := json.Marshal(submitter) + if err != nil { + log.Error().Err(err).Msg("Failed to marshal JSON") + } else { + log.Trace().Str("data", string(data)).Msg("Submitted subscriptions") + } + } + }(ctx, sem, &wg, name, submitter) + } + wg.Wait() + + return nil +} diff --git a/services/submitter/null/service.go b/services/submitter/null/service.go index 2c85ccb..0c0cc63 100644 --- a/services/submitter/null/service.go +++ b/services/submitter/null/service.go @@ -18,6 +18,8 @@ import ( "encoding/json" api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -49,7 +51,7 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) { } // SubmitBeaconBlock submits a block. -func (s *Service) SubmitBeaconBlock(ctx context.Context, block *phase0.SignedBeaconBlock) error { +func (s *Service) SubmitBeaconBlock(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error { if block == nil { return errors.New("no beacon block supplied") } @@ -119,3 +121,51 @@ func (s *Service) SubmitAggregateAttestations(ctx context.Context, aggregates [] return nil } + +// SubmitSyncCommitteeMessages submits sync committee messages. +func (s *Service) SubmitSyncCommitteeMessages(ctx context.Context, messages []*altair.SyncCommitteeMessage) error { + if len(messages) == 0 { + return errors.New("no sync committee messages supplied") + } + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(messages) + if err == nil { + e.Str("messages", string(data)).Msg("Not submitting sync committee messages") + } + } + + return nil +} + +// SubmitSyncCommitteeSubscriptions submits a batch of sync committee subscriptions. +func (s *Service) SubmitSyncCommitteeSubscriptions(ctx context.Context, subscriptions []*api.SyncCommitteeSubscription) error { + if len(subscriptions) == 0 { + return errors.New("no sync committee subscriptions supplied") + } + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(subscriptions) + if err == nil { + e.Str("subscriptions", string(data)).Msg("Not submitting sync committee subscriptions") + } + } + + return nil +} + +// SubmitSyncCommitteeContributions submits sync committee contributions. +func (s *Service) SubmitSyncCommitteeContributions(ctx context.Context, contributionAndProofs []*altair.SignedContributionAndProof) error { + if len(contributionAndProofs) == 0 { + return errors.New("no sync committee contribution and proofs supplied") + } + + if e := log.Trace(); e.Enabled() { + data, err := json.Marshal(contributionAndProofs) + if err == nil { + e.Str("contribution_and_proofs", string(data)).Msg("Not submitting sync committee contribution and proofs") + } + } + + return nil +} diff --git a/services/submitter/service.go b/services/submitter/service.go index 5ec58c4..6a860ee 100644 --- a/services/submitter/service.go +++ b/services/submitter/service.go @@ -1,4 +1,4 @@ -// Copyright © 2020 Attestant Limited. +// Copyright © 2020, 2021 Attestant Limited. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -17,6 +17,8 @@ import ( "context" api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/phase0" ) @@ -32,7 +34,7 @@ type AttestationsSubmitter interface { // BeaconBlockSubmitter is the interface for a submitter of beacon blocks. type BeaconBlockSubmitter interface { // SubmitBeaconBlock submits a block. - SubmitBeaconBlock(ctx context.Context, block *phase0.SignedBeaconBlock) error + SubmitBeaconBlock(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error } // BeaconCommitteeSubscriptionsSubmitter is the interface for a submitter of beacon committee subscriptions. @@ -46,3 +48,21 @@ type AggregateAttestationsSubmitter interface { // SubmitAggregateAttestations submits aggregate attestations. SubmitAggregateAttestations(ctx context.Context, aggregateAttestations []*phase0.SignedAggregateAndProof) error } + +// SyncCommitteeMessagesSubmitter is the interface for a submitter of sync committee messages. +type SyncCommitteeMessagesSubmitter interface { + // SubmitSyncCommitteeMessages submits sync committee messages. + SubmitSyncCommitteeMessages(ctx context.Context, messages []*altair.SyncCommitteeMessage) error +} + +// SyncCommitteeSubscriptionsSubmitter is the interface for a submitter of sync committee subscriptions. +type SyncCommitteeSubscriptionsSubmitter interface { + // SubmitSyncCommitteeSubscription submits a batch of sync committee subscriptions. + SubmitSyncCommitteeSubscriptions(ctx context.Context, subscriptions []*api.SyncCommitteeSubscription) error +} + +// SyncCommitteeContributionsSubmitter is the interface for a submitter of sync committee contributions. +type SyncCommitteeContributionsSubmitter interface { + // SubmitSyncCommitteeContributions submits sync committee contributions. + SubmitSyncCommitteeContributions(ctx context.Context, contributionAndProofs []*altair.SignedContributionAndProof) error +} diff --git a/services/synccommitteeaggregator/mock/service.go b/services/synccommitteeaggregator/mock/service.go new file mode 100644 index 0000000..4f9e4ad --- /dev/null +++ b/services/synccommitteeaggregator/mock/service.go @@ -0,0 +1,36 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import ( + "context" + + "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// Service is a mock sync committee aggregator. +type Service struct{} + +// New creates a new mock sync committee aggregator. +func New() *Service { + return &Service{} +} + +// SetBeaconBlockRoot sets the beacon block root used for a given slot. +func (s *Service) SetBeaconBlockRoot(slot phase0.Slot, root phase0.Root) { +} + +// Aggregate carries out aggregation for a slot and committee. +func (s *Service) Aggregate(ctx context.Context, details interface{}) { +} diff --git a/services/synccommitteeaggregator/service.go b/services/synccommitteeaggregator/service.go new file mode 100644 index 0000000..d4872ab --- /dev/null +++ b/services/synccommitteeaggregator/service.go @@ -0,0 +1,45 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package synccommitteeaggregator + +import ( + "context" + + "github.com/attestantio/go-eth2-client/spec/phase0" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" +) + +// Duty contains information about a sync committee aggregation duty. +type Duty struct { + // Slot is the slot of the sync committee aggregation; required for obtaining the aggregate. + Slot phase0.Slot + + // ValidatorIndices are the validators that aggregate for this slot. + ValidatorIndices []phase0.ValidatorIndex + + // SelectionProofs are the selection proofs of the subcommittees for which each validator aggregates. + SelectionProofs map[phase0.ValidatorIndex]map[uint64]phase0.BLSSignature + + // Accounts is used to sign the sync committee contribution and proof. + Accounts map[phase0.ValidatorIndex]e2wtypes.Account +} + +// Service is the sync committee aggregation service. +type Service interface { + // SetBeaconBlockRoot sets the beacon block root used for a given slot. + SetBeaconBlockRoot(slot phase0.Slot, root phase0.Root) + + // Aggregate carries out aggregation for a slot and committee. + Aggregate(ctx context.Context, details interface{}) +} diff --git a/services/synccommitteeaggregator/standard/parameters.go b/services/synccommitteeaggregator/standard/parameters.go new file mode 100644 index 0000000..9afeec8 --- /dev/null +++ b/services/synccommitteeaggregator/standard/parameters.go @@ -0,0 +1,138 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/signer" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + monitor metrics.SyncCommitteeAggregationMonitor + specProvider eth2client.SpecProvider + beaconBlockRootProvider eth2client.BeaconBlockRootProvider + contributionAndProofSigner signer.ContributionAndProofSigner + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + syncCommitteeContributionProvider eth2client.SyncCommitteeContributionProvider + syncCommitteeContributionsSubmitter submitter.SyncCommitteeContributionsSubmitter +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithMonitor sets the monitor for this module. +func WithMonitor(monitor metrics.SyncCommitteeAggregationMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.monitor = monitor + }) +} + +// WithSpecProvider sets the spec provider. +func WithSpecProvider(provider eth2client.SpecProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.specProvider = provider + }) +} + +// WithBeaconBlockRootProvider sets the beacon block root provider. +func WithBeaconBlockRootProvider(provider eth2client.BeaconBlockRootProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconBlockRootProvider = provider + }) +} + +// WithContributionAndProofSigner sets the contribution and proof submitter. +func WithContributionAndProofSigner(signer signer.ContributionAndProofSigner) Parameter { + return parameterFunc(func(p *parameters) { + p.contributionAndProofSigner = signer + }) +} + +// WithValidatingAccountsProvider sets the account manager. +func WithValidatingAccountsProvider(provider accountmanager.ValidatingAccountsProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.validatingAccountsProvider = provider + }) +} + +// WithSyncCommitteeContributionProvider sets the sync committee contribution provider. +func WithSyncCommitteeContributionProvider(provider eth2client.SyncCommitteeContributionProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeContributionProvider = provider + }) +} + +// WithSyncCommitteeContributionsSubmitter sets the sync committee contributions submitter. +func WithSyncCommitteeContributionsSubmitter(submitter submitter.SyncCommitteeContributionsSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeContributionsSubmitter = submitter + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.monitor == nil { + return nil, errors.New("no monitor specified") + } + if parameters.specProvider == nil { + return nil, errors.New("no spec provider specified") + } + if parameters.beaconBlockRootProvider == nil { + return nil, errors.New("no beacon block root provider specified") + } + if parameters.contributionAndProofSigner == nil { + return nil, errors.New("no contribution and proof signer specified") + } + if parameters.validatingAccountsProvider == nil { + return nil, errors.New("no validating accounts provider specified") + } + if parameters.syncCommitteeContributionProvider == nil { + return nil, errors.New("no sync committee contribution provider specified") + } + if parameters.syncCommitteeContributionsSubmitter == nil { + return nil, errors.New("no sync committee contributions submitter specified") + } + + return ¶meters, nil +} diff --git a/services/synccommitteeaggregator/standard/service.go b/services/synccommitteeaggregator/standard/service.go new file mode 100644 index 0000000..bb54332 --- /dev/null +++ b/services/synccommitteeaggregator/standard/service.go @@ -0,0 +1,211 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + "fmt" + "sync" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/signer" + "github.com/attestantio/vouch/services/synccommitteeaggregator" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" +) + +// Service is a sync committee aggregator. +type Service struct { + monitor metrics.SyncCommitteeAggregationMonitor + slotsPerEpoch uint64 + syncCommitteeSize uint64 + syncCommitteeSubnetCount uint64 + targetAggregatorsPerSyncSubcommittee uint64 + beaconBlockRootProvider eth2client.BeaconBlockRootProvider + contributionAndProofSigner signer.ContributionAndProofSigner + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + syncCommitteeContributionProvider eth2client.SyncCommitteeContributionProvider + syncCommitteeContributionsSubmitter eth2client.SyncCommitteeContributionsSubmitter + beaconBlockRoots map[phase0.Slot]phase0.Root + beaconBlockRootsMu sync.Mutex +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new sync committee aggregator. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "synccommitteeaggregator").Str("impl", "standard").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + spec, err := parameters.specProvider.Spec(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain spec") + } + + tmp, exists := spec["SLOTS_PER_EPOCH"] + if !exists { + return nil, errors.New("SLOTS_PER_EPOCH not found in spec") + } + slotsPerEpoch, ok := tmp.(uint64) + if !ok { + return nil, errors.New("SLOTS_PER_EPOCH of unexpected type") + } + + tmp, exists = spec["SYNC_COMMITTEE_SIZE"] + if !exists { + return nil, errors.New("SYNC_COMMITTEE_SIZE not found in spec") + } + syncCommitteeSize, ok := tmp.(uint64) + if !ok { + return nil, errors.New("SYNC_COMMITTEE_SIZE of unexpected type") + } + + tmp, exists = spec["SYNC_COMMITTEE_SUBNET_COUNT"] + if !exists { + return nil, errors.New("SYNC_COMMITTEE_SUBNET_COUNT not found in spec") + } + syncCommitteeSubnetCount, ok := tmp.(uint64) + if !ok { + return nil, errors.New("SYNC_COMMITTEE_SUBNET_COUNT of unexpected type") + } + + tmp, exists = spec["TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE"] + if !exists { + return nil, errors.New("TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE not found in spec") + } + targetAggregatorsPerSyncSubcommittee, ok := tmp.(uint64) + if !ok { + return nil, errors.New("TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE of unexpected type") + } + + s := &Service{ + monitor: parameters.monitor, + slotsPerEpoch: slotsPerEpoch, + syncCommitteeSize: syncCommitteeSize, + syncCommitteeSubnetCount: syncCommitteeSubnetCount, + targetAggregatorsPerSyncSubcommittee: targetAggregatorsPerSyncSubcommittee, + beaconBlockRootProvider: parameters.beaconBlockRootProvider, + contributionAndProofSigner: parameters.contributionAndProofSigner, + validatingAccountsProvider: parameters.validatingAccountsProvider, + syncCommitteeContributionProvider: parameters.syncCommitteeContributionProvider, + syncCommitteeContributionsSubmitter: parameters.syncCommitteeContributionsSubmitter, + beaconBlockRoots: map[phase0.Slot]phase0.Root{}, + } + + return s, nil +} + +// SetBeaconBlockRoot sets the beacon block root used for a given slot. +// Set by the sync committee messenger when it is creating the messages for the slot. +func (s *Service) SetBeaconBlockRoot(slot phase0.Slot, root phase0.Root) { + s.beaconBlockRootsMu.Lock() + s.beaconBlockRoots[slot] = root + s.beaconBlockRootsMu.Unlock() +} + +// Aggregate aggregates the attestations for a given slot/committee combination. +func (s *Service) Aggregate(ctx context.Context, data interface{}) { + started := time.Now() + + duty, ok := data.(*synccommitteeaggregator.Duty) + if !ok { + log.Error().Msg("Passed invalid data structure") + return + } + log := log.With().Uint64("slot", uint64(duty.Slot)).Int("validators", len(duty.ValidatorIndices)).Logger() + log.Trace().Msg("Aggregating") + + var beaconBlockRoot *phase0.Root + var err error + + s.beaconBlockRootsMu.Lock() + if tmp, exists := s.beaconBlockRoots[duty.Slot]; exists { + beaconBlockRoot = &tmp + delete(s.beaconBlockRoots, duty.Slot) + s.beaconBlockRootsMu.Unlock() + log.Trace().Msg("Obtained beacon block root from cache") + } else { + s.beaconBlockRootsMu.Unlock() + log.Debug().Msg("Failed to obtain beacon block root from cache; using head") + beaconBlockRoot, err = s.beaconBlockRootProvider.BeaconBlockRoot(ctx, "head") + if err != nil { + log.Warn().Err(err).Msg("Failed to obtain beacon block root") + s.monitor.SyncCommitteeAggregationsCompleted(started, len(duty.ValidatorIndices), "failed") + return + } + if beaconBlockRoot == nil { + log.Warn().Msg("Returned empty beacon block root") + s.monitor.SyncCommitteeAggregationsCompleted(started, len(duty.ValidatorIndices), "failed") + return + } + } + log.Trace().Dur("elapsed", time.Since(started)).Str("beacon_block_root", fmt.Sprintf("%#x", *beaconBlockRoot)).Msg("Obtained beacon block root") + + for _, validatorIndex := range duty.ValidatorIndices { + for subcommitteeIndex := range duty.SelectionProofs[validatorIndex] { + log.Warn().Uint64("validator_index", uint64(validatorIndex)).Uint64("subcommittee_index", subcommitteeIndex).Str("beacon_block_root", fmt.Sprintf("%#x", *beaconBlockRoot)).Msg("jgm Aggregating") + contribution, err := s.syncCommitteeContributionProvider.SyncCommitteeContribution(ctx, duty.Slot, subcommitteeIndex, *beaconBlockRoot) + if err != nil { + log.Warn().Err(err).Msg("Failed to obtain sync committee contribution") + s.monitor.SyncCommitteeAggregationsCompleted(started, len(duty.ValidatorIndices), "failed") + return + } + if contribution == nil { + log.Warn().Msg("Returned empty contribution") + s.monitor.SyncCommitteeAggregationsCompleted(started, len(duty.ValidatorIndices), "failed") + return + } + contributionAndProof := &altair.ContributionAndProof{ + AggregatorIndex: validatorIndex, + Contribution: contribution, + SelectionProof: duty.SelectionProofs[validatorIndex][subcommitteeIndex], + } + sig, err := s.contributionAndProofSigner.SignContributionAndProof(ctx, duty.Accounts[validatorIndex], contributionAndProof) + if err != nil { + log.Warn().Err(err).Msg("Failed to obtain signature of contribution and proof") + s.monitor.SyncCommitteeAggregationsCompleted(started, len(duty.ValidatorIndices), "failed") + return + } + + signedContributionAndProof := &altair.SignedContributionAndProof{ + Message: contributionAndProof, + Signature: sig, + } + + if err := s.syncCommitteeContributionsSubmitter.SubmitSyncCommitteeContributions(ctx, []*altair.SignedContributionAndProof{signedContributionAndProof}); err != nil { + log.Warn().Err(err).Msg("Failed to submit signed contribution and proof") + s.monitor.SyncCommitteeAggregationsCompleted(started, len(duty.ValidatorIndices), "failed") + return + } + + log.Trace().Msg("Submitted signed contribution and proof") + s.monitor.SyncCommitteeAggregationsCompleted(started, len(duty.ValidatorIndices), "succeeded") + } + } +} diff --git a/services/synccommitteeaggregator/standard/service_test.go b/services/synccommitteeaggregator/standard/service_test.go new file mode 100644 index 0000000..4041d17 --- /dev/null +++ b/services/synccommitteeaggregator/standard/service_test.go @@ -0,0 +1,170 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard_test + +import ( + "context" + "testing" + + mocketh2client "github.com/attestantio/go-eth2-client/mock" + "github.com/attestantio/vouch/mock" + mockaccountmanager "github.com/attestantio/vouch/services/accountmanager/mock" + nullmetrics "github.com/attestantio/vouch/services/metrics/null" + mocksigner "github.com/attestantio/vouch/services/signer/mock" + nullsubmitter "github.com/attestantio/vouch/services/submitter/null" + "github.com/attestantio/vouch/services/synccommitteeaggregator/standard" + "github.com/attestantio/vouch/testing/logger" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" +) + +func TestService(t *testing.T) { + ctx := context.Background() + + specProvider := mock.NewSpecProvider() + + mockSigner := mocksigner.New() + nullSubmitter, err := nullsubmitter.New(ctx) + require.NoError(t, err) + mockETH2Client, err := mocketh2client.New(ctx) + require.NoError(t, err) + mockValidatingAccountsProvider := mockaccountmanager.NewValidatingAccountsProvider() + + tests := []struct { + name string + params []standard.Parameter + err string + logEntry string + }{ + { + name: "MonitorMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithContributionAndProofSigner(mockSigner), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeContributionProvider(mockETH2Client), + standard.WithSyncCommitteeContributionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no monitor specified", + }, + { + name: "SpecProviderMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithContributionAndProofSigner(mockSigner), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeContributionProvider(mockETH2Client), + standard.WithSyncCommitteeContributionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no spec provider specified", + }, + { + name: "BeaconBlockRootProviderMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithSpecProvider(specProvider), + standard.WithContributionAndProofSigner(mockSigner), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeContributionProvider(mockETH2Client), + standard.WithSyncCommitteeContributionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no beacon block root provider specified", + }, + { + name: "ContributionAndProofSignerMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeContributionProvider(mockETH2Client), + standard.WithSyncCommitteeContributionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no contribution and proof signer specified", + }, + { + name: "ValidatingAccountsProviderMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithContributionAndProofSigner(mockSigner), + standard.WithSyncCommitteeContributionProvider(mockETH2Client), + standard.WithSyncCommitteeContributionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no validating accounts provider specified", + }, + { + name: "SyncCommitteeContributionProviderMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithContributionAndProofSigner(mockSigner), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeContributionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no sync committee contribution provider specified", + }, + { + name: "SyncCommitteeContributionsSubmitterMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithContributionAndProofSigner(mockSigner), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeContributionProvider(mockETH2Client), + }, + err: "problem with parameters: no sync committee contributions submitter specified", + }, + { + name: "Good", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithContributionAndProofSigner(mockSigner), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeContributionProvider(mockETH2Client), + standard.WithSyncCommitteeContributionsSubmitter(nullSubmitter), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + capture := logger.NewLogCapture() + _, err := standard.New(ctx, test.params...) + if test.err != "" { + require.EqualError(t, err, test.err) + if test.logEntry != "" { + capture.AssertHasEntry(t, test.logEntry) + } + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/services/synccommitteemessenger/mock/service.go b/services/synccommitteemessenger/mock/service.go new file mode 100644 index 0000000..8ea72de --- /dev/null +++ b/services/synccommitteemessenger/mock/service.go @@ -0,0 +1,39 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import ( + "context" + + "github.com/attestantio/go-eth2-client/spec/altair" +) + +// Service is a mock sync committee contributor. +type Service struct{} + +// New creates a new mock sync committee contributor. +func New() *Service { + return &Service{} +} + +// Prepare prepares in advance of a sync committee message. +func (s *Service) Prepare(ctx context.Context, data interface{}) error { + return nil +} + +// Message generates and broadcasts sync committee messages for a slot. +// It returns a list of messages made. +func (s *Service) Message(ctx context.Context, data interface{}) ([]*altair.SyncCommitteeMessage, error) { + return make([]*altair.SyncCommitteeMessage, 0), nil +} diff --git a/services/synccommitteemessenger/service.go b/services/synccommitteemessenger/service.go new file mode 100644 index 0000000..a9605bd --- /dev/null +++ b/services/synccommitteemessenger/service.go @@ -0,0 +1,135 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package synccommitteemessenger + +import ( + "context" + "fmt" + + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/phase0" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" +) + +// Duty contains information about a sync committee contribution duty. +type Duty struct { + // Details for the duty. + slot phase0.Slot + validatorIndices []phase0.ValidatorIndex + contributionIndices map[phase0.ValidatorIndex][]phase0.CommitteeIndex + + // account is used to sign the sync committee contribution; can be pre-fetched. + accounts map[phase0.ValidatorIndex]e2wtypes.Account + + // aggregatorSubcommittees are the subcommittees for which the validator must aggregate. + aggregatorSubcommittees map[phase0.ValidatorIndex]map[uint64]phase0.BLSSignature +} + +// NewDuty creates a new sync committee contribution duty. +func NewDuty(slot phase0.Slot, contributionIndices map[phase0.ValidatorIndex][]phase0.CommitteeIndex) *Duty { + validatorIndices := make([]phase0.ValidatorIndex, 0, len(contributionIndices)) + for k := range contributionIndices { + validatorIndices = append(validatorIndices, k) + } + + return &Duty{ + slot: slot, + validatorIndices: validatorIndices, + contributionIndices: contributionIndices, + accounts: make(map[phase0.ValidatorIndex]e2wtypes.Account, len(contributionIndices)), + aggregatorSubcommittees: make(map[phase0.ValidatorIndex]map[uint64]phase0.BLSSignature), + } +} + +// Slot provides the slot for the sync committee messenger. +func (d *Duty) Slot() phase0.Slot { + return d.slot +} + +// ValidatorIndices provides the validator indices for the sync committee messenger. +func (d *Duty) ValidatorIndices() []phase0.ValidatorIndex { + return d.validatorIndices +} + +// ContributionIndices provides the contribution indices for the sync committee messenger. +func (d *Duty) ContributionIndices() map[phase0.ValidatorIndex][]phase0.CommitteeIndex { + return d.contributionIndices +} + +// String provides a friendly string for the struct. +func (d *Duty) String() string { + return fmt.Sprintf("sync committee contributions %v", d.Tuples()) +} + +// Tuples returns a slice of (validator index, committee indices) strings. +func (d *Duty) Tuples() []string { + res := make([]string, 0, len(d.contributionIndices)) + for k, v := range d.contributionIndices { + res = append(res, fmt.Sprintf("(%d,%v)", k, v)) + } + return res +} + +// // SetRandaoReveal sets the RANDAO reveal. +// func (d *Duty) SetRandaoReveal(randaoReveal spec.BLSSignature) { +// d.randaoReveal = randaoReveal +// } +// +// // RANDAOReveal provides the RANDAO reveal. +// func (d *Duty) RANDAOReveal() spec.BLSSignature { +// return d.randaoReveal +// } + +// SetAccount sets the account. +func (d *Duty) SetAccount(index phase0.ValidatorIndex, account e2wtypes.Account) { + d.accounts[index] = account +} + +// Accounts provides all accounts. +func (d *Duty) Accounts() map[phase0.ValidatorIndex]e2wtypes.Account { + return d.accounts +} + +// Account provides a specific account. +func (d *Duty) Account(index phase0.ValidatorIndex) e2wtypes.Account { + return d.accounts[index] +} + +// SetAggregatorSubcommittees sets the aggregator state for a validator. +func (d *Duty) SetAggregatorSubcommittees(index phase0.ValidatorIndex, subcommittee uint64, selectionProof phase0.BLSSignature) { + _, exists := d.aggregatorSubcommittees[index] + if !exists { + d.aggregatorSubcommittees[index] = make(map[uint64]phase0.BLSSignature) + } + d.aggregatorSubcommittees[index][subcommittee] = selectionProof +} + +// AggregatorSubcommittees returns the map of subcommittees for which the supplied index is an aggregator. +func (d *Duty) AggregatorSubcommittees(index phase0.ValidatorIndex) map[uint64]phase0.BLSSignature { + aggregatorSubcommittees, exists := d.aggregatorSubcommittees[index] + if !exists { + return make(map[uint64]phase0.BLSSignature) + } + return aggregatorSubcommittees +} + +// Service is the sync committee messenger service. +type Service interface { + // Prepare prepares in advance of a sync committee message. + Prepare(ctx context.Context, data interface{}) error + + // Message generates and broadcasts sync committee messages for a slot. + // It returns a list of messages made. + Message(ctx context.Context, data interface{}) ([]*altair.SyncCommitteeMessage, error) +} diff --git a/services/synccommitteemessenger/standard/parameters.go b/services/synccommitteemessenger/standard/parameters.go new file mode 100644 index 0000000..95da047 --- /dev/null +++ b/services/synccommitteemessenger/standard/parameters.go @@ -0,0 +1,188 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/chaintime" + "github.com/attestantio/vouch/services/metrics" + nullmetrics "github.com/attestantio/vouch/services/metrics/null" + "github.com/attestantio/vouch/services/signer" + "github.com/attestantio/vouch/services/submitter" + "github.com/attestantio/vouch/services/synccommitteeaggregator" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + processConcurrency int64 + monitor metrics.SyncCommitteeMessageMonitor + chainTimeService chaintime.Service + syncCommitteeAggregator synccommitteeaggregator.Service + specProvider eth2client.SpecProvider + beaconBlockRootProvider eth2client.BeaconBlockRootProvider + syncCommitteeMessagesSubmitter submitter.SyncCommitteeMessagesSubmitter + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + syncCommitteeRootSigner signer.SyncCommitteeRootSigner + syncCommitteeSelectionSigner signer.SyncCommitteeSelectionSigner + syncCommitteeSubscriptionsSubmitter submitter.SyncCommitteeSubscriptionsSubmitter +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithProcessConcurrency sets the concurrency for the service. +func WithProcessConcurrency(concurrency int64) Parameter { + return parameterFunc(func(p *parameters) { + p.processConcurrency = concurrency + }) +} + +// WithMonitor sets the monitor for this module. +func WithMonitor(monitor metrics.SyncCommitteeMessageMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.monitor = monitor + }) +} + +// WithChainTimeService sets the chaintime service. +func WithChainTimeService(service chaintime.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.chainTimeService = service + }) +} + +// WithSyncCommitteeAggregator sets the sync committee aggregator. +func WithSyncCommitteeAggregator(aggregator synccommitteeaggregator.Service) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeAggregator = aggregator + }) +} + +// WithSpecProvider sets the spec provider. +func WithSpecProvider(provider eth2client.SpecProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.specProvider = provider + }) +} + +// WithBeaconBlockRootProvider sets the beacon block root provider. +func WithBeaconBlockRootProvider(provider eth2client.BeaconBlockRootProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.beaconBlockRootProvider = provider + }) +} + +// WithSyncCommitteeMessagesSubmitter sets the sync committee messages submitter. +func WithSyncCommitteeMessagesSubmitter(submitter submitter.SyncCommitteeMessagesSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeMessagesSubmitter = submitter + }) +} + +// WithValidatingAccountsProvider sets the account manager. +func WithValidatingAccountsProvider(provider accountmanager.ValidatingAccountsProvider) Parameter { + return parameterFunc(func(p *parameters) { + p.validatingAccountsProvider = provider + }) +} + +// WithSyncCommitteeRootSigner sets the sync committee root signer. +func WithSyncCommitteeRootSigner(signer signer.SyncCommitteeRootSigner) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeRootSigner = signer + }) +} + +// WithSyncCommitteeSelectionSigner sets the sync committee selection signer. +func WithSyncCommitteeSelectionSigner(signer signer.SyncCommitteeSelectionSigner) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeSelectionSigner = signer + }) +} + +// WithSyncCommitteeSubscriptionsSubmitter sets the sync committee subscriptions submitter. +func WithSyncCommitteeSubscriptionsSubmitter(submitter submitter.SyncCommitteeSubscriptionsSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeSubscriptionsSubmitter = submitter + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + monitor: nullmetrics.New(context.Background()), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.processConcurrency == 0 { + return nil, errors.New("no process concurrency specified") + } + if parameters.monitor == nil { + return nil, errors.New("no monitor specified") + } + if parameters.specProvider == nil { + return nil, errors.New("no spec provider specified") + } + if parameters.chainTimeService == nil { + return nil, errors.New("no chain time service specified") + } + if parameters.syncCommitteeAggregator == nil { + return nil, errors.New("no sync committee aggregator specified") + } + if parameters.beaconBlockRootProvider == nil { + return nil, errors.New("no beacon block root provider specified") + } + if parameters.syncCommitteeMessagesSubmitter == nil { + return nil, errors.New("no sync committee messages submitter specified") + } + if parameters.syncCommitteeSubscriptionsSubmitter == nil { + return nil, errors.New("no sync committee subscriptions submitter specified") + } + if parameters.validatingAccountsProvider == nil { + return nil, errors.New("no validating accounts provider specified") + } + if parameters.syncCommitteeSelectionSigner == nil { + return nil, errors.New("no sync committee selection signer specified") + } + if parameters.syncCommitteeRootSigner == nil { + return nil, errors.New("no sync committee root signer specified") + } + + return ¶meters, nil +} diff --git a/services/synccommitteemessenger/standard/service.go b/services/synccommitteemessenger/standard/service.go new file mode 100644 index 0000000..3f332c1 --- /dev/null +++ b/services/synccommitteemessenger/standard/service.go @@ -0,0 +1,296 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + "crypto/sha256" + "encoding/binary" + "fmt" + "sync" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/vouch/services/accountmanager" + "github.com/attestantio/vouch/services/chaintime" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/signer" + "github.com/attestantio/vouch/services/submitter" + "github.com/attestantio/vouch/services/synccommitteeaggregator" + "github.com/attestantio/vouch/services/synccommitteemessenger" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" + "golang.org/x/sync/semaphore" +) + +// Service is a beacon block attester. +type Service struct { + monitor metrics.SyncCommitteeMessageMonitor + processConcurrency int64 + slotsPerEpoch uint64 + syncCommitteeSize uint64 + syncCommitteeSubnetCount uint64 + targetAggregatorsPerSyncCommittee uint64 + chainTimeService chaintime.Service + syncCommitteeAggregator synccommitteeaggregator.Service + validatingAccountsProvider accountmanager.ValidatingAccountsProvider + beaconBlockRootProvider eth2client.BeaconBlockRootProvider + syncCommitteeMessagesSubmitter submitter.SyncCommitteeMessagesSubmitter + syncCommitteeSelectionSigner signer.SyncCommitteeSelectionSigner + syncCommitteeRootSigner signer.SyncCommitteeRootSigner +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new sync committee messenger. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "synccommitteemessenger").Str("impl", "standard").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + spec, err := parameters.specProvider.Spec(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain spec") + } + + slotsPerEpoch, err := specUint64(spec, "SLOTS_PER_EPOCH") + if err != nil { + return nil, errors.Wrap(err, "failed to obtain SLOTS_PER_EPOCH from spec") + } + + syncCommitteeSize, err := specUint64(spec, "SYNC_COMMITTEE_SIZE") + if err != nil { + return nil, errors.Wrap(err, "failed to obtain SYNC_COMMITTEE_SIZE from spec") + } + + syncCommitteeSubnetCount, err := specUint64(spec, "SYNC_COMMITTEE_SUBNET_COUNT") + if err != nil { + return nil, errors.Wrap(err, "failed to obtain SYNC_COMMITTEE_SUBNET_COUNT from spec") + } + + targetAggregatorsPerSyncCommittee, err := specUint64(spec, "TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE") + if err != nil { + return nil, errors.Wrap(err, "failed to obtain TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE from spec") + } + + s := &Service{ + monitor: parameters.monitor, + processConcurrency: parameters.processConcurrency, + slotsPerEpoch: slotsPerEpoch, + syncCommitteeSize: syncCommitteeSize, + syncCommitteeSubnetCount: syncCommitteeSubnetCount, + targetAggregatorsPerSyncCommittee: targetAggregatorsPerSyncCommittee, + chainTimeService: parameters.chainTimeService, + syncCommitteeAggregator: parameters.syncCommitteeAggregator, + validatingAccountsProvider: parameters.validatingAccountsProvider, + beaconBlockRootProvider: parameters.beaconBlockRootProvider, + syncCommitteeMessagesSubmitter: parameters.syncCommitteeMessagesSubmitter, + syncCommitteeSelectionSigner: parameters.syncCommitteeSelectionSigner, + syncCommitteeRootSigner: parameters.syncCommitteeRootSigner, + } + + return s, nil +} + +// Prepare prepares in advance of a sync committee message. +func (s *Service) Prepare(ctx context.Context, data interface{}) error { + started := time.Now() + + duty, ok := data.(*synccommitteemessenger.Duty) + if !ok { + s.monitor.SyncCommitteeMessagesCompleted(started, len(duty.ValidatorIndices()), "failed") + return errors.New("passed invalid data structure") + } + + // Decide if we are an aggregator. + for _, validatorIndex := range duty.ValidatorIndices() { + subcommittees := make(map[uint64]bool) + for _, contributionIndex := range duty.ContributionIndices()[validatorIndex] { + subcommittee := uint64(contributionIndex) / (s.syncCommitteeSize / s.syncCommitteeSubnetCount) + subcommittees[subcommittee] = true + } + for subcommittee := range subcommittees { + isAggregator, sig, err := s.isAggregator(ctx, duty.Account(validatorIndex), duty.Slot(), subcommittee) + if err != nil { + return errors.Wrap(err, "failed to calculate if this is an aggregator") + } + if isAggregator { + duty.SetAggregatorSubcommittees(validatorIndex, subcommittee, sig) + } + } + } + + return nil +} + +// Message generates and broadcasts sync committee messages for a slot. +// It returns a list of messages made. +func (s *Service) Message(ctx context.Context, data interface{}) ([]*altair.SyncCommitteeMessage, error) { + started := time.Now() + + duty, ok := data.(*synccommitteemessenger.Duty) + if !ok { + s.monitor.SyncCommitteeMessagesCompleted(started, len(duty.ValidatorIndices()), "failed") + return nil, errors.New("passed invalid data structure") + } + + // Fetch the beacon block root. + beaconBlockRoot, err := s.beaconBlockRootProvider.BeaconBlockRoot(ctx, "head") + if err != nil { + s.monitor.SyncCommitteeMessagesCompleted(started, len(duty.ValidatorIndices()), "failed") + return nil, errors.Wrap(err, "failed to obtain beacon block root") + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained beacon block root") + s.syncCommitteeAggregator.SetBeaconBlockRoot(duty.Slot(), *beaconBlockRoot) + + // Sign in parallel. + msgs := make([]*altair.SyncCommitteeMessage, 0, len(duty.ContributionIndices())) + var msgsMu sync.Mutex + validatorIndices := make([]phase0.ValidatorIndex, 0, len(duty.ContributionIndices())) + for validatorIndex := range duty.ContributionIndices() { + validatorIndices = append(validatorIndices, validatorIndex) + } + sem := semaphore.NewWeighted(s.processConcurrency) + var wg sync.WaitGroup + for i := range validatorIndices { + wg.Add(1) + go func(ctx context.Context, + sem *semaphore.Weighted, + wg *sync.WaitGroup, + i int, + ) { + defer wg.Done() + sig, err := s.contribute(ctx, duty.Account(validatorIndices[i]), s.chainTimeService.SlotToEpoch(duty.Slot()), *beaconBlockRoot) + if err != nil { + log.Error().Err(err).Msg("Failed to sign sync committee message") + return + } + log.Trace().Str("signature", fmt.Sprintf("%#x", sig)).Msg("Signed sync committee message") + + msg := &altair.SyncCommitteeMessage{ + Slot: duty.Slot(), + BeaconBlockRoot: *beaconBlockRoot, + ValidatorIndex: validatorIndices[i], + Signature: sig, + } + msgsMu.Lock() + msgs = append(msgs, msg) + msgsMu.Unlock() + }(ctx, sem, &wg, i) + } + + // msgs := make([]*altair.SyncCommitteeMessage, 0, len(duty.ContributionIndices())) + // validatorIndices := make([]phase0.ValidatorIndex, 0, len(duty.ContributionIndices())) + // for validatorIndex := range duty.ContributionIndices() { + // validatorIndices = append(validatorIndices, validatorIndex) + // } + // _, err = util.Scatter(len(duty.ContributionIndices()), func(offset int, entries int, mu *sync.RWMutex) (interface{}, error) { + // for i := offset; i < offset+entries; i++ { + // sig, err := s.contribute(ctx, duty.Account(validatorIndices[i]), s.chainTimeService.SlotToEpoch(duty.Slot()), *beaconBlockRoot) + // if err != nil { + // log.Error().Err(err).Msg("Failed to sign sync committee message") + // continue + // } + // log.Trace().Str("signature", fmt.Sprintf("%#x", sig)).Msg("Signed sync committee message") + // + // msg := &altair.SyncCommitteeMessage{ + // Slot: duty.Slot(), + // BeaconBlockRoot: *beaconBlockRoot, + // ValidatorIndex: validatorIndices[i], + // Signature: sig, + // } + // mu.Lock() + // msgs = append(msgs, msg) + // mu.Unlock() + // } + // return nil, nil + // }) + // if err != nil { + // s.monitor.SyncCommitteeMessagesCompleted(started, len(msgs), "failed") + // log.Error().Err(err).Str("result", "failed").Msg("Failed to obtain committee messages") + // } + + if err := s.syncCommitteeMessagesSubmitter.SubmitSyncCommitteeMessages(ctx, msgs); err != nil { + s.monitor.SyncCommitteeMessagesCompleted(started, len(msgs), "failed") + return nil, errors.Wrap(err, "failed to submit sync committee messages") + } + log.Trace().Dur("elapsed", time.Since(started)).Msg("Submitted sync committee messages") + + return msgs, nil +} + +func (s *Service) contribute(ctx context.Context, + account e2wtypes.Account, + epoch phase0.Epoch, + root phase0.Root, +) ( + phase0.BLSSignature, + error, +) { + sig, err := s.syncCommitteeRootSigner.SignSyncCommitteeRoot(ctx, account, epoch, root) + if err != nil { + return phase0.BLSSignature{}, err + } + return sig, err +} + +func (s *Service) isAggregator(ctx context.Context, account e2wtypes.Account, slot phase0.Slot, subcommitteeIndex uint64) (bool, phase0.BLSSignature, error) { + modulo := s.syncCommitteeSize / s.syncCommitteeSubnetCount / s.targetAggregatorsPerSyncCommittee + if modulo < 1 { + modulo = 1 + } + + // Sign the slot. + signature, err := s.syncCommitteeSelectionSigner.SignSyncCommitteeSelection(ctx, account, slot, subcommitteeIndex) + if err != nil { + return false, phase0.BLSSignature{}, errors.Wrap(err, "failed to sign the slot") + } + + // Hash the signature. + sigHash := sha256.New() + n, err := sigHash.Write(signature[:]) + if err != nil { + return false, phase0.BLSSignature{}, errors.Wrap(err, "failed to hash the slot signature") + } + if n != len(signature) { + return false, phase0.BLSSignature{}, errors.New("failed to write all bytes of the slot signature to the hash") + } + hash := sigHash.Sum(nil) + + return binary.LittleEndian.Uint64(hash[:8])%modulo == 0, signature, nil +} + +func specUint64(spec map[string]interface{}, item string) (uint64, error) { + tmp, exists := spec[item] + if !exists { + return 0, fmt.Errorf("%s not found in spec", item) + } + val, ok := tmp.(uint64) + if !ok { + return 0, fmt.Errorf("%s of unexpected type", item) + } + return val, nil +} diff --git a/services/synccommitteemessenger/standard/service_test.go b/services/synccommitteemessenger/standard/service_test.go new file mode 100644 index 0000000..488e29b --- /dev/null +++ b/services/synccommitteemessenger/standard/service_test.go @@ -0,0 +1,289 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard_test + +import ( + "context" + "testing" + "time" + + mocketh2client "github.com/attestantio/go-eth2-client/mock" + "github.com/attestantio/vouch/mock" + mockaccountmanager "github.com/attestantio/vouch/services/accountmanager/mock" + standardchaintime "github.com/attestantio/vouch/services/chaintime/standard" + nullmetrics "github.com/attestantio/vouch/services/metrics/null" + mocksigner "github.com/attestantio/vouch/services/signer/mock" + nullsubmitter "github.com/attestantio/vouch/services/submitter/null" + mocksynccommitteeaggregator "github.com/attestantio/vouch/services/synccommitteeaggregator/mock" + "github.com/attestantio/vouch/services/synccommitteemessenger/standard" + "github.com/attestantio/vouch/testing/logger" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" +) + +func TestService(t *testing.T) { + ctx := context.Background() + + genesisTime := time.Now() + slotDuration := 12 * time.Second + slotsPerEpoch := uint64(32) + genesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime) + slotDurationProvider := mock.NewSlotDurationProvider(slotDuration) + slotsPerEpochProvider := mock.NewSlotsPerEpochProvider(slotsPerEpoch) + specProvider := mock.NewSpecProvider() + + mockSyncCommitteeAggregator := mocksynccommitteeaggregator.New() + mockSigner := mocksigner.New() + nullSubmitter, err := nullsubmitter.New(ctx) + require.NoError(t, err) + mockETH2Client, err := mocketh2client.New(ctx) + require.NoError(t, err) + mockValidatingAccountsProvider := mockaccountmanager.NewValidatingAccountsProvider() + + chainTime, err := standardchaintime.New(ctx, + standardchaintime.WithGenesisTimeProvider(genesisTimeProvider), + standardchaintime.WithSlotDurationProvider(slotDurationProvider), + standardchaintime.WithSlotsPerEpochProvider(slotsPerEpochProvider), + ) + + require.NoError(t, err) + tests := []struct { + name string + params []standard.Parameter + err string + logEntry string + }{ + { + name: "ProcessConcurrencyBad", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithProcessConcurrency(-1), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithChainTimeService(chainTime), + standard.WithSyncCommitteeAggregator(mockSyncCommitteeAggregator), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithSyncCommitteeMessagesSubmitter(nullSubmitter), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeRootSigner(mockSigner), + standard.WithSyncCommitteeSelectionSigner(mockSigner), + standard.WithSyncCommitteeSubscriptionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no process concurrency specified", + }, + { + name: "MonitorMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithProcessConcurrency(1), + standard.WithMonitor(nil), + standard.WithChainTimeService(chainTime), + standard.WithSyncCommitteeAggregator(mockSyncCommitteeAggregator), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithSyncCommitteeMessagesSubmitter(nullSubmitter), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeRootSigner(mockSigner), + standard.WithSyncCommitteeSelectionSigner(mockSigner), + standard.WithSyncCommitteeSubscriptionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no monitor specified", + }, + { + name: "ChainTimeMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithProcessConcurrency(1), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithSyncCommitteeAggregator(mockSyncCommitteeAggregator), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithSyncCommitteeMessagesSubmitter(nullSubmitter), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeRootSigner(mockSigner), + standard.WithSyncCommitteeSelectionSigner(mockSigner), + standard.WithSyncCommitteeSubscriptionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no chain time service specified", + }, + { + name: "SyncCommitteeAggregatorMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithProcessConcurrency(1), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithChainTimeService(chainTime), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithSyncCommitteeMessagesSubmitter(nullSubmitter), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeRootSigner(mockSigner), + standard.WithSyncCommitteeSelectionSigner(mockSigner), + standard.WithSyncCommitteeSubscriptionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no sync committee aggregator specified", + }, + { + name: "SpecProviderMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithProcessConcurrency(1), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithChainTimeService(chainTime), + standard.WithSyncCommitteeAggregator(mockSyncCommitteeAggregator), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithSyncCommitteeMessagesSubmitter(nullSubmitter), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeRootSigner(mockSigner), + standard.WithSyncCommitteeSelectionSigner(mockSigner), + standard.WithSyncCommitteeSubscriptionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no spec provider specified", + }, + { + name: "BeaconBlockRootProviderMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithProcessConcurrency(1), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithChainTimeService(chainTime), + standard.WithSyncCommitteeAggregator(mockSyncCommitteeAggregator), + standard.WithSpecProvider(specProvider), + standard.WithSyncCommitteeMessagesSubmitter(nullSubmitter), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeRootSigner(mockSigner), + standard.WithSyncCommitteeSelectionSigner(mockSigner), + standard.WithSyncCommitteeSubscriptionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no beacon block root provider specified", + }, + { + name: "SyncCommitteeMessagesSubmitterMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithProcessConcurrency(1), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithChainTimeService(chainTime), + standard.WithSyncCommitteeAggregator(mockSyncCommitteeAggregator), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeRootSigner(mockSigner), + standard.WithSyncCommitteeSelectionSigner(mockSigner), + standard.WithSyncCommitteeSubscriptionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no sync committee messages submitter specified", + }, + { + name: "ValidatingAccountsProviderMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithProcessConcurrency(1), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithChainTimeService(chainTime), + standard.WithSyncCommitteeAggregator(mockSyncCommitteeAggregator), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithSyncCommitteeMessagesSubmitter(nullSubmitter), + standard.WithSyncCommitteeRootSigner(mockSigner), + standard.WithSyncCommitteeSelectionSigner(mockSigner), + standard.WithSyncCommitteeSubscriptionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no validating accounts provider specified", + }, + { + name: "SyncCommitteeRootSignerMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithProcessConcurrency(1), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithChainTimeService(chainTime), + standard.WithSyncCommitteeAggregator(mockSyncCommitteeAggregator), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithSyncCommitteeMessagesSubmitter(nullSubmitter), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeSelectionSigner(mockSigner), + standard.WithSyncCommitteeSubscriptionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no sync committee root signer specified", + }, + { + name: "SyncCommitteeSelectionSignerMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithProcessConcurrency(1), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithChainTimeService(chainTime), + standard.WithSyncCommitteeAggregator(mockSyncCommitteeAggregator), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithSyncCommitteeMessagesSubmitter(nullSubmitter), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeRootSigner(mockSigner), + standard.WithSyncCommitteeSubscriptionsSubmitter(nullSubmitter), + }, + err: "problem with parameters: no sync committee selection signer specified", + }, + { + name: "SynccommitteeSubscriptionsSubmitterMissing", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithProcessConcurrency(1), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithChainTimeService(chainTime), + standard.WithSyncCommitteeAggregator(mockSyncCommitteeAggregator), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithSyncCommitteeMessagesSubmitter(nullSubmitter), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeRootSigner(mockSigner), + standard.WithSyncCommitteeSelectionSigner(mockSigner), + }, + err: "problem with parameters: no sync committee subscriptions submitter specified", + }, + { + name: "Good", + params: []standard.Parameter{ + standard.WithLogLevel(zerolog.Disabled), + standard.WithProcessConcurrency(1), + standard.WithMonitor(nullmetrics.New(ctx)), + standard.WithChainTimeService(chainTime), + standard.WithSyncCommitteeAggregator(mockSyncCommitteeAggregator), + standard.WithSpecProvider(specProvider), + standard.WithBeaconBlockRootProvider(mockETH2Client), + standard.WithSyncCommitteeMessagesSubmitter(nullSubmitter), + standard.WithValidatingAccountsProvider(mockValidatingAccountsProvider), + standard.WithSyncCommitteeRootSigner(mockSigner), + standard.WithSyncCommitteeSelectionSigner(mockSigner), + standard.WithSyncCommitteeSubscriptionsSubmitter(nullSubmitter), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + capture := logger.NewLogCapture() + _, err := standard.New(ctx, test.params...) + if test.err != "" { + require.EqualError(t, err, test.err) + if test.logEntry != "" { + capture.AssertHasEntry(t, test.logEntry) + } + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/services/synccommitteesubscriber/mock/service.go b/services/synccommitteesubscriber/mock/service.go new file mode 100644 index 0000000..8101328 --- /dev/null +++ b/services/synccommitteesubscriber/mock/service.go @@ -0,0 +1,37 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import ( + "context" + + api "github.com/attestantio/go-eth2-client/api/v1" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/vouch/services/synccommitteesubscriber" +) + +type service struct{} + +// New is a mock sync committee subscriber service. +func New() synccommitteesubscriber.Service { + return &service{} +} + +// Subscribe is a mock. +func (s *service) Subscribe(ctx context.Context, + endEpoch spec.Epoch, + duties []*api.SyncCommitteeDuty, +) error { + return nil +} diff --git a/services/synccommitteesubscriber/service.go b/services/synccommitteesubscriber/service.go new file mode 100644 index 0000000..6fea374 --- /dev/null +++ b/services/synccommitteesubscriber/service.go @@ -0,0 +1,31 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package synccommitteesubscriber is a package that manages subscriptions for sync committees. +package synccommitteesubscriber + +import ( + "context" + + api "github.com/attestantio/go-eth2-client/api/v1" + spec "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// Service is the sync committee subscriber service. +type Service interface { + // Subscribe subscribes to sync committees given a set of duties. + Subscribe(ctx context.Context, + endEpoch spec.Epoch, + duties []*api.SyncCommitteeDuty, + ) error +} diff --git a/services/synccommitteesubscriber/standard/parameters.go b/services/synccommitteesubscriber/standard/parameters.go new file mode 100644 index 0000000..96e0454 --- /dev/null +++ b/services/synccommitteesubscriber/standard/parameters.go @@ -0,0 +1,81 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +type parameters struct { + logLevel zerolog.Level + monitor metrics.SyncCommitteeSubscriptionMonitor + syncCommitteeSubmitter submitter.SyncCommitteeSubscriptionsSubmitter +} + +// Parameter is the interface for service parameters. +type Parameter interface { + apply(*parameters) +} + +type parameterFunc func(*parameters) + +func (f parameterFunc) apply(p *parameters) { + f(p) +} + +// WithLogLevel sets the log level for the module. +func WithLogLevel(logLevel zerolog.Level) Parameter { + return parameterFunc(func(p *parameters) { + p.logLevel = logLevel + }) +} + +// WithMonitor sets the monitor for the module. +func WithMonitor(monitor metrics.SyncCommitteeSubscriptionMonitor) Parameter { + return parameterFunc(func(p *parameters) { + p.monitor = monitor + }) +} + +// WithSyncCommitteeSubmitter sets the sync committee subscriptions provider. +func WithSyncCommitteeSubmitter(provider eth2client.SyncCommitteeSubscriptionsSubmitter) Parameter { + return parameterFunc(func(p *parameters) { + p.syncCommitteeSubmitter = provider + }) +} + +// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. +func parseAndCheckParameters(params ...Parameter) (*parameters, error) { + parameters := parameters{ + logLevel: zerolog.GlobalLevel(), + } + for _, p := range params { + if params != nil { + p.apply(¶meters) + } + } + + if parameters.monitor == nil { + return nil, errors.New("no monitor specified") + } + if parameters.syncCommitteeSubmitter == nil { + return nil, errors.New("no sync committee submitter specified") + } + + return ¶meters, nil +} diff --git a/services/synccommitteesubscriber/standard/service.go b/services/synccommitteesubscriber/standard/service.go new file mode 100644 index 0000000..8fcdcd1 --- /dev/null +++ b/services/synccommitteesubscriber/standard/service.go @@ -0,0 +1,91 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package standard + +import ( + "context" + "time" + + api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/vouch/services/metrics" + "github.com/attestantio/vouch/services/submitter" + "github.com/pkg/errors" + "github.com/rs/zerolog" + zerologger "github.com/rs/zerolog/log" +) + +// Service is an beacon committee subscriber. +type Service struct { + monitor metrics.SyncCommitteeSubscriptionMonitor + submitter submitter.SyncCommitteeSubscriptionsSubmitter +} + +// module-wide log. +var log zerolog.Logger + +// New creates a new sync committee subscriber. +func New(ctx context.Context, params ...Parameter) (*Service, error) { + parameters, err := parseAndCheckParameters(params...) + if err != nil { + return nil, errors.Wrap(err, "problem with parameters") + } + + // Set logging. + log = zerologger.With().Str("service", "synccommitteesubscriber").Str("impl", "standard").Logger() + if parameters.logLevel != log.GetLevel() { + log = log.Level(parameters.logLevel) + } + + s := &Service{ + monitor: parameters.monitor, + submitter: parameters.syncCommitteeSubmitter, + } + + return s, nil +} + +// Subscribe subscribes to sync committees given a set of duties. +func (s *Service) Subscribe(ctx context.Context, + endEpoch phase0.Epoch, + duties []*api.SyncCommitteeDuty, +) error { + if len(duties) == 0 { + // Nothing to do. + return nil + } + + started := time.Now() + log := log.With().Uint64("end_epoch", uint64(endEpoch)).Logger() + log.Trace().Msg("Subscribing") + + subscriptions := make([]*api.SyncCommitteeSubscription, 0, len(duties)) + for _, duty := range duties { + subscriptions = append(subscriptions, &api.SyncCommitteeSubscription{ + ValidatorIndex: duty.ValidatorIndex, + SyncCommitteeIndices: duty.ValidatorSyncCommitteeIndices, + UntilEpoch: endEpoch, + }) + } + + if err := s.submitter.SubmitSyncCommitteeSubscriptions(ctx, subscriptions); err != nil { + s.monitor.SyncCommitteeSubscriptionCompleted(started, "failed") + return errors.Wrap(err, "failed to subscribe to sync committees") + } + + log.Trace().Dur("elapsed", time.Since(started)).Msg("Submitted subscription request") + s.monitor.SyncCommitteeSubscriptionCompleted(started, "succeeded") + + return nil +} diff --git a/strategies/attestationdata/best/score.go b/strategies/attestationdata/best/score.go index d27da1b..c9e1548 100644 --- a/strategies/attestationdata/best/score.go +++ b/strategies/attestationdata/best/score.go @@ -50,11 +50,11 @@ func (s *Service) scoreAttestationData(ctx context.Context, log.Warn().Str("block_root", fmt.Sprintf("%#x", attestationData.BeaconBlockRoot)).Msg("No block returned by provider") return float64(attestationData.Source.Epoch + attestationData.Target.Epoch) } - if block.Message == nil { + slot, err = block.Slot() + if err != nil { log.Warn().Str("block_root", fmt.Sprintf("%#x", attestationData.BeaconBlockRoot)).Msg("Empty block returned by provider") return float64(attestationData.Source.Epoch + attestationData.Target.Epoch) } - slot = block.Message.Slot } else { log.Warn().Msg("Cannot score attestation") return float64(attestationData.Source.Epoch + attestationData.Target.Epoch) diff --git a/strategies/beaconblockproposal/best/beaconblockproposal.go b/strategies/beaconblockproposal/best/beaconblockproposal.go index 9ceb6df..b64204e 100644 --- a/strategies/beaconblockproposal/best/beaconblockproposal.go +++ b/strategies/beaconblockproposal/best/beaconblockproposal.go @@ -20,15 +20,16 @@ import ( "time" eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/phase0" "golang.org/x/sync/semaphore" ) // BeaconBlockProposal provides the best beacon block proposal from a number of beacon nodes. -func (s *Service) BeaconBlockProposal(ctx context.Context, slot phase0.Slot, randaoReveal phase0.BLSSignature, graffiti []byte) (*phase0.BeaconBlock, error) { +func (s *Service) BeaconBlockProposal(ctx context.Context, slot phase0.Slot, randaoReveal phase0.BLSSignature, graffiti []byte) (*spec.VersionedBeaconBlock, error) { var mu sync.Mutex bestScore := float64(0) - var bestProposal *phase0.BeaconBlock + var bestProposal *spec.VersionedBeaconBlock bestProvider := "" started := time.Now() @@ -63,17 +64,34 @@ func (s *Service) BeaconBlockProposal(ctx context.Context, slot phase0.Slot, ran // Obtain the slot of the block to which the proposal refers. // We use this to allow the scorer to score blocks with earlier parents lower. + parentRoot, err := proposal.ParentRoot() + if err != nil { + log.Error().Str("version", proposal.Version.String()).Msg("Failed to obtain parent root") + return + } var parentSlot phase0.Slot - parentBlock, err := s.signedBeaconBlockProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%#x", proposal.ParentRoot[:])) + parentBlock, err := s.signedBeaconBlockProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%#x", parentRoot[:])) switch { case err != nil: log.Warn().Err(err).Msg("Failed to obtain parent block") - parentSlot = proposal.Slot - 1 + slot, err := proposal.Slot() + if err != nil { + log.Error().Str("version", proposal.Version.String()).Err(err).Msg("Failed to obtain proposal slot") + } + parentSlot = slot - 1 case parentBlock == nil: - log.Warn().Err(err).Msg("Failed to obtain parent block") - parentSlot = proposal.Slot - 1 + log.Warn().Msg("Empty parent block") + slot, err := proposal.Slot() + if err != nil { + log.Error().Str("version", proposal.Version.String()).Err(err).Msg("Failed to obtain proposal slot") + } + parentSlot = slot - 1 default: - parentSlot = parentBlock.Message.Slot + slot, err := parentBlock.Slot() + if err != nil { + log.Error().Str("version", proposal.Version.String()).Err(err).Msg("Failed to obtain proposal slot for parent block") + } + parentSlot = slot - 1 } mu.Lock() diff --git a/strategies/beaconblockproposal/best/parameters.go b/strategies/beaconblockproposal/best/parameters.go index c843cea..9feac3f 100644 --- a/strategies/beaconblockproposal/best/parameters.go +++ b/strategies/beaconblockproposal/best/parameters.go @@ -17,7 +17,6 @@ package best import ( "context" - "runtime" "time" eth2client "github.com/attestantio/go-eth2-client" @@ -92,10 +91,9 @@ func WithSignedBeaconBlockProvider(provider eth2client.SignedBeaconBlockProvider // parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct. func parseAndCheckParameters(params ...Parameter) (*parameters, error) { parameters := parameters{ - logLevel: zerolog.GlobalLevel(), - timeout: 2 * time.Second, - clientMonitor: nullmetrics.New(context.Background()), - processConcurrency: int64(runtime.GOMAXPROCS(-1)), + logLevel: zerolog.GlobalLevel(), + timeout: 2 * time.Second, + clientMonitor: nullmetrics.New(context.Background()), } for _, p := range params { if params != nil { diff --git a/strategies/beaconblockproposal/best/score.go b/strategies/beaconblockproposal/best/score.go index 7f7d71d..8e51146 100644 --- a/strategies/beaconblockproposal/best/score.go +++ b/strategies/beaconblockproposal/best/score.go @@ -1,4 +1,4 @@ -// Copyright © 2020 Attestant Limited. +// Copyright © 2020, 2021 Attestant Limited. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -17,16 +17,17 @@ import ( "context" "sort" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/phase0" ) // scoreBeaconBlockPropsal generates a score for a beacon block. // The score is relative to the reward expected by proposing the block. -func scoreBeaconBlockProposal(ctx context.Context, name string, parentSlot phase0.Slot, blockProposal *phase0.BeaconBlock) float64 { +func scoreBeaconBlockProposal(ctx context.Context, name string, parentSlot phase0.Slot, blockProposal *spec.VersionedBeaconBlock) float64 { if blockProposal == nil { return 0 } - if blockProposal.Body == nil { + if blockProposal.IsEmpty() { return 0 } @@ -36,7 +37,12 @@ func scoreBeaconBlockProposal(ctx context.Context, name string, parentSlot phase // We need to avoid duplicates in attestations. // Map is slot -> committee index -> validator committee index -> attested. attested := make(map[phase0.Slot]map[phase0.CommitteeIndex]map[uint64]bool) - for _, attestation := range blockProposal.Body.Attestations { + attestations, err := blockProposal.Attestations() + if err != nil { + log.Warn().Err(err).Msg("Failed to obtain attestations") + return 0 + } + for _, attestation := range attestations { slotAttested, exists := attested[attestation.Data.Slot] if !exists { slotAttested = make(map[phase0.CommitteeIndex]map[uint64]bool) @@ -54,9 +60,15 @@ func scoreBeaconBlockProposal(ctx context.Context, name string, parentSlot phase } } + blockProposalSlot, err := blockProposal.Slot() + if err != nil { + log.Error().Str("version", blockProposal.Version.String()).Msg("Unknown proposal version") + return 0 + } + // Calculate inclusion score. for slot, slotAttested := range attested { - inclusionDistance := float64(blockProposal.Slot - slot) + inclusionDistance := float64(blockProposalSlot - slot) for _, committeeAttested := range slotAttested { attestationScore += float64(len(committeeAttested)) * (float64(0.75) + float64(0.25)/inclusionDistance) if inclusionDistance == 1 { @@ -73,38 +85,56 @@ func scoreBeaconBlockProposal(ctx context.Context, name string, parentSlot phase slashingWeight := float64(700) // Add proposer slashing scores. - proposerSlashingScore := float64(len(blockProposal.Body.ProposerSlashings)) * slashingWeight + proposerSlashings, err := blockProposal.ProposerSlashings() + if err != nil { + log.Warn().Err(err).Msg("Failed to obtain proposer slashings") + proposerSlashings = make([]*phase0.ProposerSlashing, 0) + } + proposerSlashingScore := float64(len(proposerSlashings)) * slashingWeight // Add attester slashing scores. + attesterSlashings, err := blockProposal.AttesterSlashings() + if err != nil { + log.Warn().Err(err).Msg("Failed to obtain attester slashings") + attesterSlashings = make([]*phase0.AttesterSlashing, 0) + } indicesSlashed := 0 - for i := range blockProposal.Body.AttesterSlashings { - slashing := blockProposal.Body.AttesterSlashings[i] + for i := range attesterSlashings { + slashing := attesterSlashings[i] indicesSlashed += len(intersection(slashing.Attestation1.AttestingIndices, slashing.Attestation2.AttestingIndices)) } attesterSlashingScore := slashingWeight * float64(indicesSlashed) + // Add sync committee score. + syncCommitteeScore := float64(0) + if blockProposal.Version == spec.DataVersionAltair { + // An individual sync proposal is worth roughly 0.3 of a fully correct attestation. + syncCommitteeScore = float64(blockProposal.Altair.Body.SyncAggregate.SyncCommitteeBits.Count()) * 0.3 + } + // Scale scores by the distance between the proposal and parent slots. var scale uint64 - if blockProposal.Slot <= parentSlot { - log.Warn().Uint64("slot", uint64(blockProposal.Slot)).Uint64("parent_slot", uint64(parentSlot)).Msg("Invalid parent slot for proposal") + if blockProposalSlot <= parentSlot { + log.Warn().Uint64("slot", uint64(blockProposalSlot)).Uint64("parent_slot", uint64(parentSlot)).Msg("Invalid parent slot for proposal") scale = 32 } else { - scale = uint64(blockProposal.Slot - parentSlot) + scale = uint64(blockProposalSlot - parentSlot) } log.Trace(). - Uint64("slot", uint64(blockProposal.Slot)). + Uint64("slot", uint64(blockProposalSlot)). Uint64("parent_slot", uint64(parentSlot)). Str("provider", name). Float64("immediate_attestations", immediateAttestationScore). Float64("attestations", attestationScore). Float64("proposer_slashings", proposerSlashingScore). Float64("attester_slashings", attesterSlashingScore). + Float64("sync_committee", syncCommitteeScore). Uint64("scale", scale). Float64("total", (attestationScore+proposerSlashingScore+attesterSlashingScore)/float64(scale)). Msg("Scored block") - return (attestationScore + proposerSlashingScore + attesterSlashingScore) / float64(scale) + return (attestationScore + proposerSlashingScore + attesterSlashingScore + syncCommitteeScore) / float64(scale) } // intersection returns a list of items common between the two sets. diff --git a/strategies/beaconblockproposal/best/score_internal_test.go b/strategies/beaconblockproposal/best/score_internal_test.go index f63c2fa..45546ba 100644 --- a/strategies/beaconblockproposal/best/score_internal_test.go +++ b/strategies/beaconblockproposal/best/score_internal_test.go @@ -17,6 +17,7 @@ import ( "context" "testing" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/attestantio/vouch/testutil" "github.com/prysmaticlabs/go-bitfield" @@ -42,7 +43,7 @@ func specificAggregationBits(set []uint64, total uint64) bitfield.Bitlist { func TestScore(t *testing.T) { tests := []struct { name string - block *phase0.BeaconBlock + block *spec.VersionedBeaconBlock parentSlot phase0.Slot score float64 err string @@ -54,20 +55,23 @@ func TestScore(t *testing.T) { }, { name: "Empty", - block: &phase0.BeaconBlock{}, + block: &spec.VersionedBeaconBlock{}, parentSlot: 1, score: 0, }, { name: "SingleAttestation", - block: &phase0.BeaconBlock{ - Slot: 12345, - Body: &phase0.BeaconBlockBody{ - Attestations: []*phase0.Attestation{ - { - AggregationBits: aggregationBits(1, 128), - Data: &phase0.AttestationData{ - Slot: 12344, + block: &spec.VersionedBeaconBlock{ + Version: spec.DataVersionPhase0, + Phase0: &phase0.BeaconBlock{ + Slot: 12345, + Body: &phase0.BeaconBlockBody{ + Attestations: []*phase0.Attestation{ + { + AggregationBits: aggregationBits(1, 128), + Data: &phase0.AttestationData{ + Slot: 12344, + }, }, }, }, @@ -78,14 +82,17 @@ func TestScore(t *testing.T) { }, { name: "SingleAttestationParentRootDistance2", - block: &phase0.BeaconBlock{ - Slot: 12345, - Body: &phase0.BeaconBlockBody{ - Attestations: []*phase0.Attestation{ - { - AggregationBits: aggregationBits(1, 128), - Data: &phase0.AttestationData{ - Slot: 12344, + block: &spec.VersionedBeaconBlock{ + Version: spec.DataVersionPhase0, + Phase0: &phase0.BeaconBlock{ + Slot: 12345, + Body: &phase0.BeaconBlockBody{ + Attestations: []*phase0.Attestation{ + { + AggregationBits: aggregationBits(1, 128), + Data: &phase0.AttestationData{ + Slot: 12344, + }, }, }, }, @@ -96,14 +103,17 @@ func TestScore(t *testing.T) { }, { name: "SingleAttestationDistance2", - block: &phase0.BeaconBlock{ - Slot: 12345, - Body: &phase0.BeaconBlockBody{ - Attestations: []*phase0.Attestation{ - { - AggregationBits: aggregationBits(1, 128), - Data: &phase0.AttestationData{ - Slot: 12343, + block: &spec.VersionedBeaconBlock{ + Version: spec.DataVersionPhase0, + Phase0: &phase0.BeaconBlock{ + Slot: 12345, + Body: &phase0.BeaconBlockBody{ + Attestations: []*phase0.Attestation{ + { + AggregationBits: aggregationBits(1, 128), + Data: &phase0.AttestationData{ + Slot: 12343, + }, }, }, }, @@ -114,20 +124,23 @@ func TestScore(t *testing.T) { }, { name: "TwoAttestations", - block: &phase0.BeaconBlock{ - Slot: 12345, - Body: &phase0.BeaconBlockBody{ - Attestations: []*phase0.Attestation{ - { - AggregationBits: aggregationBits(2, 128), - Data: &phase0.AttestationData{ - Slot: 12344, + block: &spec.VersionedBeaconBlock{ + Version: spec.DataVersionPhase0, + Phase0: &phase0.BeaconBlock{ + Slot: 12345, + Body: &phase0.BeaconBlockBody{ + Attestations: []*phase0.Attestation{ + { + AggregationBits: aggregationBits(2, 128), + Data: &phase0.AttestationData{ + Slot: 12344, + }, }, - }, - { - AggregationBits: aggregationBits(1, 128), - Data: &phase0.AttestationData{ - Slot: 12341, + { + AggregationBits: aggregationBits(1, 128), + Data: &phase0.AttestationData{ + Slot: 12341, + }, }, }, }, @@ -138,24 +151,27 @@ func TestScore(t *testing.T) { }, { name: "AttesterSlashing", - block: &phase0.BeaconBlock{ - Slot: 12345, - Body: &phase0.BeaconBlockBody{ - Attestations: []*phase0.Attestation{ - { - AggregationBits: aggregationBits(50, 128), - Data: &phase0.AttestationData{ - Slot: 12344, + block: &spec.VersionedBeaconBlock{ + Version: spec.DataVersionPhase0, + Phase0: &phase0.BeaconBlock{ + Slot: 12345, + Body: &phase0.BeaconBlockBody{ + Attestations: []*phase0.Attestation{ + { + AggregationBits: aggregationBits(50, 128), + Data: &phase0.AttestationData{ + Slot: 12344, + }, }, }, - }, - AttesterSlashings: []*phase0.AttesterSlashing{ - { - Attestation1: &phase0.IndexedAttestation{ - AttestingIndices: []uint64{1, 2, 3}, - }, - Attestation2: &phase0.IndexedAttestation{ - AttestingIndices: []uint64{2, 3, 4}, + AttesterSlashings: []*phase0.AttesterSlashing{ + { + Attestation1: &phase0.IndexedAttestation{ + AttestingIndices: []uint64{1, 2, 3}, + }, + Attestation2: &phase0.IndexedAttestation{ + AttestingIndices: []uint64{2, 3, 4}, + }, }, }, }, @@ -166,20 +182,23 @@ func TestScore(t *testing.T) { }, { name: "DuplicateAttestations", - block: &phase0.BeaconBlock{ - Slot: 12345, - Body: &phase0.BeaconBlockBody{ - Attestations: []*phase0.Attestation{ - { - AggregationBits: specificAggregationBits([]uint64{1, 2, 3}, 128), - Data: &phase0.AttestationData{ - Slot: 12344, + block: &spec.VersionedBeaconBlock{ + Version: spec.DataVersionPhase0, + Phase0: &phase0.BeaconBlock{ + Slot: 12345, + Body: &phase0.BeaconBlockBody{ + Attestations: []*phase0.Attestation{ + { + AggregationBits: specificAggregationBits([]uint64{1, 2, 3}, 128), + Data: &phase0.AttestationData{ + Slot: 12344, + }, }, - }, - { - AggregationBits: specificAggregationBits([]uint64{2, 3, 4}, 128), - Data: &phase0.AttestationData{ - Slot: 12344, + { + AggregationBits: specificAggregationBits([]uint64{2, 3, 4}, 128), + Data: &phase0.AttestationData{ + Slot: 12344, + }, }, }, }, @@ -190,48 +209,51 @@ func TestScore(t *testing.T) { }, { name: "Full", - block: &phase0.BeaconBlock{ - Slot: 12345, - Body: &phase0.BeaconBlockBody{ - Attestations: []*phase0.Attestation{ - { - AggregationBits: aggregationBits(50, 128), - Data: &phase0.AttestationData{ - Slot: 12344, + block: &spec.VersionedBeaconBlock{ + Version: spec.DataVersionPhase0, + Phase0: &phase0.BeaconBlock{ + Slot: 12345, + Body: &phase0.BeaconBlockBody{ + Attestations: []*phase0.Attestation{ + { + AggregationBits: aggregationBits(50, 128), + Data: &phase0.AttestationData{ + Slot: 12344, + }, }, }, - }, - AttesterSlashings: []*phase0.AttesterSlashing{ - { - Attestation1: &phase0.IndexedAttestation{ - AttestingIndices: []uint64{1, 2, 3}, - }, - Attestation2: &phase0.IndexedAttestation{ - AttestingIndices: []uint64{2, 3, 4}, + AttesterSlashings: []*phase0.AttesterSlashing{ + { + Attestation1: &phase0.IndexedAttestation{ + AttestingIndices: []uint64{1, 2, 3}, + }, + Attestation2: &phase0.IndexedAttestation{ + AttestingIndices: []uint64{2, 3, 4}, + }, }, }, - }, - ProposerSlashings: []*phase0.ProposerSlashing{ - { - SignedHeader1: &phase0.SignedBeaconBlockHeader{ - Message: &phase0.BeaconBlockHeader{ - Slot: 10, - ProposerIndex: 1, - ParentRoot: testutil.HexToRoot("0x0101010101010101010101010101010101010101010101010101010101010101"), - StateRoot: testutil.HexToRoot("0x0202020202020202020202020202020202020202020202020202020202020202"), - BodyRoot: testutil.HexToRoot("0x0303030303030303030303030303030303030303030303030303030303030303"), + ProposerSlashings: []*phase0.ProposerSlashing{ + { + SignedHeader1: &phase0.SignedBeaconBlockHeader{ + Message: &phase0.BeaconBlockHeader{ + Slot: 10, + ProposerIndex: 1, + ParentRoot: testutil.HexToRoot("0x0101010101010101010101010101010101010101010101010101010101010101"), + StateRoot: testutil.HexToRoot("0x0202020202020202020202020202020202020202020202020202020202020202"), + BodyRoot: testutil.HexToRoot("0x0303030303030303030303030303030303030303030303030303030303030303"), + }, + Signature: testutil.HexToSignature("0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404"), }, - Signature: testutil.HexToSignature("0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404"), - }, - SignedHeader2: &phase0.SignedBeaconBlockHeader{ - Message: &phase0.BeaconBlockHeader{ - Slot: 10, - ProposerIndex: 1, - ParentRoot: testutil.HexToRoot("0x0404040404040404040404040404040404040404040404040404040404040404"), - StateRoot: testutil.HexToRoot("0x0202020202020202020202020202020202020202020202020202020202020202"), - BodyRoot: testutil.HexToRoot("0x0303030303030303030303030303030303030303030303030303030303030303"), + SignedHeader2: &phase0.SignedBeaconBlockHeader{ + Message: &phase0.BeaconBlockHeader{ + Slot: 10, + ProposerIndex: 1, + ParentRoot: testutil.HexToRoot("0x0404040404040404040404040404040404040404040404040404040404040404"), + StateRoot: testutil.HexToRoot("0x0202020202020202020202020202020202020202020202020202020202020202"), + BodyRoot: testutil.HexToRoot("0x0303030303030303030303030303030303030303030303030303030303030303"), + }, + Signature: testutil.HexToSignature("0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404"), }, - Signature: testutil.HexToSignature("0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404"), }, }, }, @@ -242,48 +264,51 @@ func TestScore(t *testing.T) { }, { name: "FullParentRootDistance2", - block: &phase0.BeaconBlock{ - Slot: 12345, - Body: &phase0.BeaconBlockBody{ - Attestations: []*phase0.Attestation{ - { - AggregationBits: aggregationBits(50, 128), - Data: &phase0.AttestationData{ - Slot: 12344, + block: &spec.VersionedBeaconBlock{ + Version: spec.DataVersionPhase0, + Phase0: &phase0.BeaconBlock{ + Slot: 12345, + Body: &phase0.BeaconBlockBody{ + Attestations: []*phase0.Attestation{ + { + AggregationBits: aggregationBits(50, 128), + Data: &phase0.AttestationData{ + Slot: 12344, + }, }, }, - }, - AttesterSlashings: []*phase0.AttesterSlashing{ - { - Attestation1: &phase0.IndexedAttestation{ - AttestingIndices: []uint64{1, 2, 3}, - }, - Attestation2: &phase0.IndexedAttestation{ - AttestingIndices: []uint64{2, 3, 4}, + AttesterSlashings: []*phase0.AttesterSlashing{ + { + Attestation1: &phase0.IndexedAttestation{ + AttestingIndices: []uint64{1, 2, 3}, + }, + Attestation2: &phase0.IndexedAttestation{ + AttestingIndices: []uint64{2, 3, 4}, + }, }, }, - }, - ProposerSlashings: []*phase0.ProposerSlashing{ - { - SignedHeader1: &phase0.SignedBeaconBlockHeader{ - Message: &phase0.BeaconBlockHeader{ - Slot: 10, - ProposerIndex: 1, - ParentRoot: testutil.HexToRoot("0x0101010101010101010101010101010101010101010101010101010101010101"), - StateRoot: testutil.HexToRoot("0x0202020202020202020202020202020202020202020202020202020202020202"), - BodyRoot: testutil.HexToRoot("0x0303030303030303030303030303030303030303030303030303030303030303"), + ProposerSlashings: []*phase0.ProposerSlashing{ + { + SignedHeader1: &phase0.SignedBeaconBlockHeader{ + Message: &phase0.BeaconBlockHeader{ + Slot: 10, + ProposerIndex: 1, + ParentRoot: testutil.HexToRoot("0x0101010101010101010101010101010101010101010101010101010101010101"), + StateRoot: testutil.HexToRoot("0x0202020202020202020202020202020202020202020202020202020202020202"), + BodyRoot: testutil.HexToRoot("0x0303030303030303030303030303030303030303030303030303030303030303"), + }, + Signature: testutil.HexToSignature("0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404"), }, - Signature: testutil.HexToSignature("0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404"), - }, - SignedHeader2: &phase0.SignedBeaconBlockHeader{ - Message: &phase0.BeaconBlockHeader{ - Slot: 10, - ProposerIndex: 1, - ParentRoot: testutil.HexToRoot("0x0404040404040404040404040404040404040404040404040404040404040404"), - StateRoot: testutil.HexToRoot("0x0202020202020202020202020202020202020202020202020202020202020202"), - BodyRoot: testutil.HexToRoot("0x0303030303030303030303030303030303030303030303030303030303030303"), + SignedHeader2: &phase0.SignedBeaconBlockHeader{ + Message: &phase0.BeaconBlockHeader{ + Slot: 10, + ProposerIndex: 1, + ParentRoot: testutil.HexToRoot("0x0404040404040404040404040404040404040404040404040404040404040404"), + StateRoot: testutil.HexToRoot("0x0202020202020202020202020202020202020202020202020202020202020202"), + BodyRoot: testutil.HexToRoot("0x0303030303030303030303030303030303030303030303030303030303030303"), + }, + Signature: testutil.HexToSignature("0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404"), }, - Signature: testutil.HexToSignature("0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404"), }, }, }, @@ -294,48 +319,51 @@ func TestScore(t *testing.T) { }, { name: "FullParentRootDistance4", - block: &phase0.BeaconBlock{ - Slot: 12345, - Body: &phase0.BeaconBlockBody{ - Attestations: []*phase0.Attestation{ - { - AggregationBits: aggregationBits(50, 128), - Data: &phase0.AttestationData{ - Slot: 12344, + block: &spec.VersionedBeaconBlock{ + Version: spec.DataVersionPhase0, + Phase0: &phase0.BeaconBlock{ + Slot: 12345, + Body: &phase0.BeaconBlockBody{ + Attestations: []*phase0.Attestation{ + { + AggregationBits: aggregationBits(50, 128), + Data: &phase0.AttestationData{ + Slot: 12344, + }, }, }, - }, - AttesterSlashings: []*phase0.AttesterSlashing{ - { - Attestation1: &phase0.IndexedAttestation{ - AttestingIndices: []uint64{1, 2, 3}, - }, - Attestation2: &phase0.IndexedAttestation{ - AttestingIndices: []uint64{2, 3, 4}, + AttesterSlashings: []*phase0.AttesterSlashing{ + { + Attestation1: &phase0.IndexedAttestation{ + AttestingIndices: []uint64{1, 2, 3}, + }, + Attestation2: &phase0.IndexedAttestation{ + AttestingIndices: []uint64{2, 3, 4}, + }, }, }, - }, - ProposerSlashings: []*phase0.ProposerSlashing{ - { - SignedHeader1: &phase0.SignedBeaconBlockHeader{ - Message: &phase0.BeaconBlockHeader{ - Slot: 10, - ProposerIndex: 1, - ParentRoot: testutil.HexToRoot("0x0101010101010101010101010101010101010101010101010101010101010101"), - StateRoot: testutil.HexToRoot("0x0202020202020202020202020202020202020202020202020202020202020202"), - BodyRoot: testutil.HexToRoot("0x0303030303030303030303030303030303030303030303030303030303030303"), + ProposerSlashings: []*phase0.ProposerSlashing{ + { + SignedHeader1: &phase0.SignedBeaconBlockHeader{ + Message: &phase0.BeaconBlockHeader{ + Slot: 10, + ProposerIndex: 1, + ParentRoot: testutil.HexToRoot("0x0101010101010101010101010101010101010101010101010101010101010101"), + StateRoot: testutil.HexToRoot("0x0202020202020202020202020202020202020202020202020202020202020202"), + BodyRoot: testutil.HexToRoot("0x0303030303030303030303030303030303030303030303030303030303030303"), + }, + Signature: testutil.HexToSignature("0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404"), }, - Signature: testutil.HexToSignature("0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404"), - }, - SignedHeader2: &phase0.SignedBeaconBlockHeader{ - Message: &phase0.BeaconBlockHeader{ - Slot: 10, - ProposerIndex: 1, - ParentRoot: testutil.HexToRoot("0x0404040404040404040404040404040404040404040404040404040404040404"), - StateRoot: testutil.HexToRoot("0x0202020202020202020202020202020202020202020202020202020202020202"), - BodyRoot: testutil.HexToRoot("0x0303030303030303030303030303030303030303030303030303030303030303"), + SignedHeader2: &phase0.SignedBeaconBlockHeader{ + Message: &phase0.BeaconBlockHeader{ + Slot: 10, + ProposerIndex: 1, + ParentRoot: testutil.HexToRoot("0x0404040404040404040404040404040404040404040404040404040404040404"), + StateRoot: testutil.HexToRoot("0x0202020202020202020202020202020202020202020202020202020202020202"), + BodyRoot: testutil.HexToRoot("0x0303030303030303030303030303030303030303030303030303030303030303"), + }, + Signature: testutil.HexToSignature("0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404"), }, - Signature: testutil.HexToSignature("0x040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404040404"), }, }, }, diff --git a/strategies/beaconblockproposal/first/service.go b/strategies/beaconblockproposal/first/service.go index 8887b16..701118e 100644 --- a/strategies/beaconblockproposal/first/service.go +++ b/strategies/beaconblockproposal/first/service.go @@ -18,6 +18,7 @@ import ( "time" eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/attestantio/vouch/services/metrics" "github.com/pkg/errors" @@ -58,14 +59,14 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) { } // BeaconBlockProposal provides the first beacon block proposal from a number of beacon nodes. -func (s *Service) BeaconBlockProposal(ctx context.Context, slot phase0.Slot, randaoReveal phase0.BLSSignature, graffiti []byte) (*phase0.BeaconBlock, error) { +func (s *Service) BeaconBlockProposal(ctx context.Context, slot phase0.Slot, randaoReveal phase0.BLSSignature, graffiti []byte) (*spec.VersionedBeaconBlock, error) { // We create a cancelable context with a timeout. As soon as the first provider has responded we // cancel the context to cancel the other requests. ctx, cancel := context.WithTimeout(ctx, s.timeout) - proposalCh := make(chan *phase0.BeaconBlock, 1) + proposalCh := make(chan *spec.VersionedBeaconBlock, 1) for name, provider := range s.beaconBlockProposalProviders { - go func(ctx context.Context, name string, provider eth2client.BeaconBlockProposalProvider, ch chan *phase0.BeaconBlock) { + go func(ctx context.Context, name string, provider eth2client.BeaconBlockProposalProvider, ch chan *spec.VersionedBeaconBlock) { log := log.With().Str("provider", name).Uint64("slot", uint64(slot)).Logger() started := time.Now()