mirror of https://github.com/certusone/vouch.git
Add 'first' attestation data strategy
This commit is contained in:
parent
c21866491a
commit
b08baf16d4
|
@ -286,6 +286,86 @@ func (m *BeaconBlockProposalProvider) BeaconBlockProposal(ctx context.Context, s
|
|||
return block, nil
|
||||
}
|
||||
|
||||
// AttestationDataProvider is a mock for eth2client.AttestationDataProvider.
|
||||
type AttestationDataProvider struct{}
|
||||
|
||||
// NewAttestationDataProvider returns a mock attestation data provider.
|
||||
func NewAttestationDataProvider() eth2client.AttestationDataProvider {
|
||||
return &AttestationDataProvider{}
|
||||
}
|
||||
|
||||
// AttestationData is a mock.
|
||||
func (m *AttestationDataProvider) AttestationData(ctx context.Context, slot uint64, committeeIndex uint64) (*spec.AttestationData, error) {
|
||||
return &spec.AttestationData{
|
||||
Slot: slot,
|
||||
Index: committeeIndex,
|
||||
BeaconBlockRoot: []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,
|
||||
},
|
||||
Source: &spec.Checkpoint{
|
||||
Epoch: 1,
|
||||
Root: []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,
|
||||
},
|
||||
},
|
||||
Target: &spec.Checkpoint{
|
||||
Epoch: 2,
|
||||
Root: []byte{
|
||||
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
|
||||
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ErroringAttestationDataProvider is a mock for eth2client.AttestationDataProvider.
|
||||
type ErroringAttestationDataProvider struct{}
|
||||
|
||||
// NewErroringAttestationDataProvider returns a mock attestation data provider.
|
||||
func NewErroringAttestationDataProvider() eth2client.AttestationDataProvider {
|
||||
return &ErroringAttestationDataProvider{}
|
||||
}
|
||||
|
||||
// AttestationData is a mock.
|
||||
func (m *ErroringAttestationDataProvider) AttestationData(ctx context.Context, slot uint64, committeeIndex uint64) (*spec.AttestationData, error) {
|
||||
return nil, errors.New("mock error")
|
||||
}
|
||||
|
||||
// NilAttestationDataProvider is a mock for eth2client.AttestationDataProvider.
|
||||
type NilAttestationDataProvider struct{}
|
||||
|
||||
// NewNilAttestationDataProvider returns a mock attestation data provider.
|
||||
func NewNilAttestationDataProvider() eth2client.AttestationDataProvider {
|
||||
return &NilAttestationDataProvider{}
|
||||
}
|
||||
|
||||
// AttestationData is a mock.
|
||||
func (m *NilAttestationDataProvider) AttestationData(ctx context.Context, slot uint64, committeeIndex uint64) (*spec.AttestationData, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SleepyAttestationDataProvider is a mock for eth2client.AttestationDataProvider.
|
||||
type SleepyAttestationDataProvider struct {
|
||||
wait time.Duration
|
||||
next eth2client.AttestationDataProvider
|
||||
}
|
||||
|
||||
// NewSleepyAttestationDataProvider returns a mock attestation data provider.
|
||||
func NewSleepyAttestationDataProvider(wait time.Duration, next eth2client.AttestationDataProvider) eth2client.AttestationDataProvider {
|
||||
return &SleepyAttestationDataProvider{
|
||||
wait: wait,
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
// AttestationData is a mock.
|
||||
func (m *SleepyAttestationDataProvider) AttestationData(ctx context.Context, slot uint64, committeeIndex uint64) (*spec.AttestationData, error) {
|
||||
time.Sleep(m.wait)
|
||||
return m.next.AttestationData(ctx, slot, committeeIndex)
|
||||
}
|
||||
|
||||
// BeaconProposerDomainProvider is a mock for eth2client.BeaconProposerDomainProvider.
|
||||
type BeaconProposerDomainProvider struct{}
|
||||
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
// 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 first is a strategy that obtains attestation from multiple
|
||||
// nodes and selects the first one returned.
|
||||
package first
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/vouch/services/metrics"
|
||||
nullmetrics "github.com/attestantio/vouch/services/metrics/null"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type parameters struct {
|
||||
logLevel zerolog.Level
|
||||
clientMonitor metrics.ClientMonitor
|
||||
attestationDataProviders map[string]eth2client.AttestationDataProvider
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
}
|
||||
|
||||
// WithClientMonitor sets the client monitor for the service.
|
||||
func WithClientMonitor(monitor metrics.ClientMonitor) Parameter {
|
||||
return parameterFunc(func(p *parameters) {
|
||||
p.clientMonitor = monitor
|
||||
})
|
||||
}
|
||||
|
||||
// WithAttestationDataProviders sets the beacon block proposal providers.
|
||||
func WithAttestationDataProviders(providers map[string]eth2client.AttestationDataProvider) Parameter {
|
||||
return parameterFunc(func(p *parameters) {
|
||||
p.attestationDataProviders = providers
|
||||
})
|
||||
}
|
||||
|
||||
// WithTimeout sets the timeout for beacon block proposal requests.
|
||||
func WithTimeout(timeout time.Duration) Parameter {
|
||||
return parameterFunc(func(p *parameters) {
|
||||
p.timeout = timeout
|
||||
})
|
||||
}
|
||||
|
||||
// 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()),
|
||||
}
|
||||
for _, p := range params {
|
||||
if params != nil {
|
||||
p.apply(¶meters)
|
||||
}
|
||||
}
|
||||
|
||||
if parameters.timeout == 0 {
|
||||
return nil, errors.New("no timeout specified")
|
||||
}
|
||||
if parameters.clientMonitor == nil {
|
||||
return nil, errors.New("no client monitor specified")
|
||||
}
|
||||
if len(parameters.attestationDataProviders) == 0 {
|
||||
return nil, errors.New("no attestation data providers specified")
|
||||
}
|
||||
|
||||
return ¶meters, nil
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
// 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 first
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
eth2client "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"
|
||||
"github.com/rs/zerolog"
|
||||
zerologger "github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Service is the provider for attestation data.
|
||||
type Service struct {
|
||||
clientMonitor metrics.ClientMonitor
|
||||
attestationDataProviders map[string]eth2client.AttestationDataProvider
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// module-wide log.
|
||||
var log zerolog.Logger
|
||||
|
||||
// New creates a new attestation data strategy.
|
||||
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("strategy", "attestationdata").Str("impl", "first").Logger()
|
||||
if parameters.logLevel != log.GetLevel() {
|
||||
log = log.Level(parameters.logLevel)
|
||||
}
|
||||
|
||||
s := &Service{
|
||||
attestationDataProviders: parameters.attestationDataProviders,
|
||||
timeout: parameters.timeout,
|
||||
clientMonitor: parameters.clientMonitor,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// AttestationData provides the first attestation data from a number of beacon nodes.
|
||||
func (s *Service) AttestationData(ctx context.Context, slot uint64, committeeIndex uint64) (*spec.AttestationData, 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)
|
||||
|
||||
attestationDataCh := make(chan *spec.AttestationData, 1)
|
||||
for name, provider := range s.attestationDataProviders {
|
||||
go func(ctx context.Context, name string, provider eth2client.AttestationDataProvider, ch chan *spec.AttestationData) {
|
||||
log := log.With().Str("provider", name).Uint64("slot", slot).Logger()
|
||||
|
||||
started := time.Now()
|
||||
attestationData, err := provider.AttestationData(ctx, slot, committeeIndex)
|
||||
s.clientMonitor.ClientOperation(name, "attestation data", err == nil, time.Since(started))
|
||||
if err != nil {
|
||||
log.Warn().Dur("elapsed", time.Since(started)).Err(err).Msg("Failed to obtain attestation data")
|
||||
return
|
||||
}
|
||||
if attestationData == nil {
|
||||
log.Warn().Dur("elapsed", time.Since(started)).Err(err).Msg("Returned empty attestation data")
|
||||
return
|
||||
}
|
||||
log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained attestation data")
|
||||
|
||||
ch <- attestationData
|
||||
}(ctx, name, provider, attestationDataCh)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
log.Warn().Msg("Failed to obtain attestation data before timeout")
|
||||
return nil, errors.New("failed to obtain attestation data before timeout")
|
||||
case attestationData := <-attestationDataCh:
|
||||
cancel()
|
||||
return attestationData, nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
// 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 first_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
eth2client "github.com/attestantio/go-eth2-client"
|
||||
"github.com/attestantio/vouch/mock"
|
||||
"github.com/attestantio/vouch/strategies/attestationdata/first"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
attestationDataProviders := map[string]eth2client.AttestationDataProvider{
|
||||
"localhost:1": mock.NewAttestationDataProvider(),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
params []first.Parameter
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "TimeoutZero",
|
||||
params: []first.Parameter{
|
||||
first.WithLogLevel(zerolog.TraceLevel),
|
||||
first.WithTimeout(0),
|
||||
first.WithAttestationDataProviders(attestationDataProviders),
|
||||
},
|
||||
err: "problem with parameters: no timeout specified",
|
||||
},
|
||||
{
|
||||
name: "ClientMonitorMissing",
|
||||
params: []first.Parameter{
|
||||
first.WithLogLevel(zerolog.TraceLevel),
|
||||
first.WithClientMonitor(nil),
|
||||
first.WithAttestationDataProviders(attestationDataProviders),
|
||||
},
|
||||
err: "problem with parameters: no client monitor specified",
|
||||
},
|
||||
{
|
||||
name: "AttestationDataProvidersNil",
|
||||
params: []first.Parameter{
|
||||
first.WithLogLevel(zerolog.TraceLevel),
|
||||
first.WithAttestationDataProviders(nil),
|
||||
},
|
||||
err: "problem with parameters: no attestation data providers specified",
|
||||
},
|
||||
{
|
||||
name: "AttestationDataProvidersEmpty",
|
||||
params: []first.Parameter{
|
||||
first.WithLogLevel(zerolog.TraceLevel),
|
||||
first.WithAttestationDataProviders(map[string]eth2client.AttestationDataProvider{}),
|
||||
},
|
||||
err: "problem with parameters: no attestation data providers specified",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
params: []first.Parameter{
|
||||
first.WithLogLevel(zerolog.TraceLevel),
|
||||
first.WithTimeout(10 * time.Second),
|
||||
first.WithAttestationDataProviders(attestationDataProviders),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := first.New(context.Background(), test.params...)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaces(t *testing.T) {
|
||||
attestationDataProviders := map[string]eth2client.AttestationDataProvider{
|
||||
"localhost:1": mock.NewAttestationDataProvider(),
|
||||
}
|
||||
|
||||
s, err := first.New(context.Background(),
|
||||
first.WithLogLevel(zerolog.Disabled),
|
||||
first.WithAttestationDataProviders(attestationDataProviders),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Implements(t, (*eth2client.AttestationDataProvider)(nil), s)
|
||||
}
|
||||
|
||||
func TestAttestationData(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params []first.Parameter
|
||||
slot uint64
|
||||
committeeIndex uint64
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "Good",
|
||||
params: []first.Parameter{
|
||||
first.WithLogLevel(zerolog.Disabled),
|
||||
first.WithAttestationDataProviders(map[string]eth2client.AttestationDataProvider{
|
||||
"good": mock.NewAttestationDataProvider(),
|
||||
}),
|
||||
},
|
||||
slot: 12345,
|
||||
committeeIndex: 3,
|
||||
},
|
||||
{
|
||||
name: "Timeout",
|
||||
params: []first.Parameter{
|
||||
first.WithLogLevel(zerolog.Disabled),
|
||||
first.WithTimeout(time.Second),
|
||||
first.WithAttestationDataProviders(map[string]eth2client.AttestationDataProvider{
|
||||
"sleepy": mock.NewSleepyAttestationDataProvider(5*time.Second, mock.NewAttestationDataProvider()),
|
||||
}),
|
||||
},
|
||||
slot: 12345,
|
||||
committeeIndex: 3,
|
||||
err: "failed to obtain attestation data before timeout",
|
||||
},
|
||||
{
|
||||
name: "NilResponse",
|
||||
params: []first.Parameter{
|
||||
first.WithLogLevel(zerolog.Disabled),
|
||||
first.WithTimeout(time.Second),
|
||||
first.WithAttestationDataProviders(map[string]eth2client.AttestationDataProvider{
|
||||
"nil": mock.NewNilAttestationDataProvider(),
|
||||
}),
|
||||
},
|
||||
slot: 12345,
|
||||
committeeIndex: 3,
|
||||
// Nil response is invalid, so expect a timeout.
|
||||
err: "failed to obtain attestation data before timeout",
|
||||
},
|
||||
{
|
||||
name: "GoodMixed",
|
||||
params: []first.Parameter{
|
||||
first.WithLogLevel(zerolog.Disabled),
|
||||
first.WithTimeout(2 * time.Second),
|
||||
first.WithAttestationDataProviders(map[string]eth2client.AttestationDataProvider{
|
||||
"error": mock.NewErroringAttestationDataProvider(),
|
||||
"sleepy": mock.NewSleepyAttestationDataProvider(time.Second, mock.NewAttestationDataProvider()),
|
||||
}),
|
||||
},
|
||||
slot: 12345,
|
||||
committeeIndex: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
s, err := first.New(context.Background(), test.params...)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
attestationData, err := s.AttestationData(context.Background(), test.slot, test.committeeIndex)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, attestationData)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue