Merge branch 'attestation-strategy'

This commit is contained in:
Jim McDonald 2020-11-17 13:16:25 +00:00
commit 581938ef45
No known key found for this signature in database
GPG Key ID: 89CEB61B2AD2A5E7
13 changed files with 1017 additions and 0 deletions

View File

@ -303,6 +303,86 @@ func (m *SignedBeaconBlockProvider) SignedBeaconBlock(ctx context.Context, state
}, 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 spec.Slot, committeeIndex spec.CommitteeIndex) (*spec.AttestationData, error) {
return &spec.AttestationData{
Slot: slot,
Index: committeeIndex,
BeaconBlockRoot: spec.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,
}),
Source: &spec.Checkpoint{
Epoch: 1,
Root: spec.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,
}),
},
Target: &spec.Checkpoint{
Epoch: 2,
Root: spec.Root([32]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 spec.Slot, committeeIndex spec.CommitteeIndex) (*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 spec.Slot, committeeIndex spec.CommitteeIndex) (*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 spec.Slot, committeeIndex spec.CommitteeIndex) (*spec.AttestationData, error) {
time.Sleep(m.wait)
return m.next.AttestationData(ctx, slot, committeeIndex)
}
// BeaconProposerDomainProvider is a mock for eth2client.BeaconProposerDomainProvider.
type BeaconProposerDomainProvider struct{}

View File

@ -0,0 +1,87 @@
// 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 best
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
type attestationDataResponse struct {
attestationData *spec.AttestationData
score float64
}
// AttestationData provides the best attestation data from a number of beacon nodes.
func (s *Service) AttestationData(ctx context.Context, slot spec.Slot, committeeIndex spec.CommitteeIndex) (*spec.AttestationData, error) {
started := time.Now()
// We create a cancelable context with a timeout. If the context times out we take the best to date.
ctx, cancel := context.WithTimeout(ctx, s.timeout)
respCh := make(chan *attestationDataResponse, len(s.attestationDataProviders))
errCh := make(chan error, len(s.attestationDataProviders))
// Kick off the requests.
for name, provider := range s.attestationDataProviders {
go func(ctx context.Context, name string, provider eth2client.AttestationDataProvider, respCh chan *attestationDataResponse, errCh chan error) {
attestationData, err := provider.AttestationData(ctx, slot, committeeIndex)
s.clientMonitor.ClientOperation(name, "attestation data", err == nil, time.Since(started))
if err != nil {
errCh <- err
return
}
log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained attestation data")
if attestationData == nil {
return
}
score := s.scoreAttestationData(ctx, name, attestationData)
respCh <- &attestationDataResponse{
attestationData: attestationData,
score: score,
}
}(ctx, name, provider, respCh, errCh)
}
// Wait for all responses (or context done).
responded := 0
errored := 0
bestScore := float64(0)
var bestAttestationData *spec.AttestationData
for responded+errored != len(s.attestationDataProviders) {
select {
case <-ctx.Done():
// Anyone not responded by now is considered errored.
errored = len(s.attestationDataProviders) - responded
log.Info().Dur("elapsed", time.Since(started)).Int("responded", responded).Int("errored", errored).Msg("Timed out waiting for responses")
case <-errCh:
errored++
case resp := <-respCh:
responded++
if bestAttestationData == nil || resp.score > bestScore {
bestAttestationData = resp.attestationData
}
}
}
cancel()
if bestAttestationData == nil {
return nil, errors.New("no attestations received")
}
return bestAttestationData, nil
}

View File

@ -0,0 +1,103 @@
// 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 best_test
import (
"context"
"testing"
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/vouch/mock"
"github.com/attestantio/vouch/strategies/attestationdata/best"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
)
func TestAttestationData(t *testing.T) {
tests := []struct {
name string
params []best.Parameter
slot spec.Slot
committeeIndex spec.CommitteeIndex
err string
}{
{
name: "Good",
params: []best.Parameter{
best.WithLogLevel(zerolog.Disabled),
best.WithAttestationDataProviders(map[string]eth2client.AttestationDataProvider{
"good": mock.NewAttestationDataProvider(),
}),
},
slot: 12345,
committeeIndex: 3,
},
{
name: "Timeout",
params: []best.Parameter{
best.WithLogLevel(zerolog.Disabled),
best.WithTimeout(time.Second),
best.WithAttestationDataProviders(map[string]eth2client.AttestationDataProvider{
"sleepy": mock.NewSleepyAttestationDataProvider(5*time.Second, mock.NewAttestationDataProvider()),
}),
},
slot: 12345,
committeeIndex: 3,
err: "no attestations received",
},
{
name: "NilResponse",
params: []best.Parameter{
best.WithLogLevel(zerolog.Disabled),
best.WithTimeout(time.Second),
best.WithAttestationDataProviders(map[string]eth2client.AttestationDataProvider{
"nil": mock.NewNilAttestationDataProvider(),
}),
},
slot: 12345,
committeeIndex: 3,
err: "no attestations received",
},
{
name: "GoodMixed",
params: []best.Parameter{
best.WithLogLevel(zerolog.Disabled),
best.WithTimeout(2 * time.Second),
best.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 := best.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)
}
})
}
}

View File

@ -0,0 +1,112 @@
// 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 best is a strategy that obtains attestation from multiple
// nodes and selects the best one.
package best
import (
"context"
"runtime"
"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
processConcurrency int64
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
})
}
// WithProcessConcurrency sets the concurrency for the service.
func WithProcessConcurrency(concurrency int64) Parameter {
return parameterFunc(func(p *parameters) {
p.processConcurrency = concurrency
})
}
// 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()),
processConcurrency: int64(runtime.GOMAXPROCS(-1)),
}
for _, p := range params {
if params != nil {
p.apply(&parameters)
}
}
if parameters.timeout == 0 {
return nil, errors.New("no timeout specified")
}
if parameters.clientMonitor == nil {
return nil, errors.New("no client monitor specified")
}
if parameters.processConcurrency == 0 {
return nil, errors.New("no process concurrency specified")
}
if len(parameters.attestationDataProviders) == 0 {
return nil, errors.New("no attestation data providers specified")
}
return &parameters, nil
}

View File

@ -0,0 +1,41 @@
// 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 best
import (
"context"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
)
// scoreAttestationData generates a score for attestation data.
// The score is relative to the reward expected by proposing the block.
func (s *Service) scoreAttestationData(ctx context.Context, name string, attestationData *spec.AttestationData) float64 {
if attestationData == nil {
return 0
}
// log.Trace().
// Uint64("slot", blockProposal.Slot).
// Str("provider", name).
// Float64("immediate_attestations", immediateAttestationScore).
// Float64("attestations", attestationScore).
// Float64("proposer_slashings", proposerSlashingScore).
// Float64("attester_slashings", attesterSlashingScore).
// Float64("total", attestationScore+proposerSlashingScore+attesterSlashingScore).
// Msg("Scored block")
//
// return attestationScore + proposerSlashingScore + attesterSlashingScore
return 0
}

View File

@ -0,0 +1,59 @@
// 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 best
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"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
processConcurrency int64
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", "best").Logger()
if parameters.logLevel != log.GetLevel() {
log = log.Level(parameters.logLevel)
}
s := &Service{
timeout: parameters.timeout,
clientMonitor: parameters.clientMonitor,
processConcurrency: parameters.processConcurrency,
attestationDataProviders: parameters.attestationDataProviders,
}
return s, nil
}

View File

@ -0,0 +1,105 @@
// 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 best_test
import (
"context"
"testing"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/vouch/mock"
"github.com/attestantio/vouch/strategies/attestationdata/best"
"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 []best.Parameter
err string
}{
{
name: "TimeoutZero",
params: []best.Parameter{
best.WithLogLevel(zerolog.TraceLevel),
best.WithTimeout(0),
best.WithAttestationDataProviders(attestationDataProviders),
},
err: "problem with parameters: no timeout specified",
},
{
name: "ClientMonitorMissing",
params: []best.Parameter{
best.WithLogLevel(zerolog.TraceLevel),
best.WithClientMonitor(nil),
best.WithAttestationDataProviders(attestationDataProviders),
},
err: "problem with parameters: no client monitor specified",
},
{
name: "AttestationDataProvidersNil",
params: []best.Parameter{
best.WithLogLevel(zerolog.TraceLevel),
best.WithAttestationDataProviders(nil),
},
err: "problem with parameters: no attestation data providers specified",
},
{
name: "AttestationDataProvidersEmpty",
params: []best.Parameter{
best.WithLogLevel(zerolog.TraceLevel),
best.WithAttestationDataProviders(map[string]eth2client.AttestationDataProvider{}),
},
err: "problem with parameters: no attestation data providers specified",
},
{
name: "Good",
params: []best.Parameter{
best.WithLogLevel(zerolog.TraceLevel),
best.WithTimeout(10 * time.Second),
best.WithAttestationDataProviders(attestationDataProviders),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := best.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 := best.New(context.Background(),
best.WithLogLevel(zerolog.Disabled),
best.WithAttestationDataProviders(attestationDataProviders),
)
require.NoError(t, err)
require.Implements(t, (*eth2client.AttestationDataProvider)(nil), s)
}

View File

@ -0,0 +1,62 @@
// 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/pkg/errors"
)
// AttestationData provides the first attestation data from a number of beacon nodes.
func (s *Service) AttestationData(ctx context.Context, slot spec.Slot, committeeIndex spec.CommitteeIndex) (*spec.AttestationData, error) {
started := time.Now()
// We create a cancelable context with a timeout. When a provider responds we cancel the context to cancel the other requests.
ctx, cancel := context.WithTimeout(ctx, s.timeout)
respCh := 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", uint64(slot)).Logger()
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, respCh)
}
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 := <-respCh:
cancel()
return attestationData, nil
}
}

View File

@ -0,0 +1,104 @@
// 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"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/vouch/mock"
"github.com/attestantio/vouch/strategies/attestationdata/first"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
)
func TestAttestationData(t *testing.T) {
tests := []struct {
name string
params []first.Parameter
slot spec.Slot
committeeIndex spec.CommitteeIndex
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)
}
})
}
}

View File

@ -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(&parameters)
}
}
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 &parameters, nil
}

View File

@ -0,0 +1,57 @@
// 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"
"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
}

View File

@ -0,0 +1,105 @@
// 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)
}

View File

@ -56,6 +56,9 @@ func (s *Service) BeaconBlockProposal(ctx context.Context, slot spec.Slot, randa
}
log.Trace().Dur("elapsed", time.Since(started)).Msg("Obtained beacon block proposal")
cancel()
if proposal == nil {
return
}
// 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.