Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

app: wire synthetic block proposals #1499

Merged
merged 2 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 30 additions & 17 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,22 @@ import (
const eth2ClientTimeout = time.Second * 2

type Config struct {
P2P p2p.Config
Log log.Config
Feature featureset.Config
LockFile string
NoVerify bool
PrivKeyFile string
MonitoringAddr string
ValidatorAPIAddr string
BeaconNodeAddrs []string
JaegerAddr string
JaegerService string
SimnetBMock bool
SimnetVMock bool
SimnetValidatorKeysDir string
BuilderAPI bool
P2P p2p.Config
Log log.Config
Feature featureset.Config
LockFile string
NoVerify bool
PrivKeyFile string
MonitoringAddr string
ValidatorAPIAddr string
BeaconNodeAddrs []string
JaegerAddr string
JaegerService string
SimnetBMock bool
SimnetVMock bool
SimnetValidatorKeysDir string
SyntheticBlockProposals bool
BuilderAPI bool

TestConfig TestConfig
}
Expand Down Expand Up @@ -637,10 +638,12 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager,
opts := []beaconmock.Option{
beaconmock.WithSlotDuration(time.Second),
beaconmock.WithDeterministicAttesterDuties(dutyFactor),
beaconmock.WithDeterministicProposerDuties(dutyFactor),
beaconmock.WithDeterministicSyncCommDuties(2, 8), // First 2 epochs of every 8
beaconmock.WithValidatorSet(createMockValidators(pubkeys)),
}
if !conf.SyntheticBlockProposals { // Only add deterministic proposals if synthetic duties are disabled.
opts = append(opts, beaconmock.WithDeterministicProposerDuties(dutyFactor))
}
opts = append(opts, conf.TestConfig.SimnetBMockOpts...)
bmock, err := beaconmock.New(opts...)
if err != nil {
Expand All @@ -652,6 +655,11 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager,
return nil, err
}

if conf.SyntheticBlockProposals {
log.Info(ctx, "Synthetic block proposals enabled")
wrap = eth2wrap.WithSyntheticDuties(wrap, pubkeys)
}

life.RegisterStop(lifecycle.StopBeaconMock, lifecycle.HookFuncErr(bmock.Close))

return wrap, nil
Expand All @@ -661,11 +669,16 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager,
return nil, errors.New("beacon node endpoints empty")
}

eth2Cl, err := eth2wrap.NewMultiHTTP(ctx, eth2ClientTimeout, conf.BeaconNodeAddrs)
eth2Cl, err := eth2wrap.NewMultiHTTP(ctx, eth2ClientTimeout, conf.BeaconNodeAddrs...)
if err != nil {
return nil, errors.Wrap(err, "new eth2 http client")
}

if conf.SyntheticBlockProposals {
log.Info(ctx, "Synthetic block proposals enabled")
eth2Cl = eth2wrap.WithSyntheticDuties(eth2Cl, pubkeys)
}

return eth2Cl, nil
}

Expand Down
12 changes: 10 additions & 2 deletions app/eth2wrap/eth2wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,16 @@ func Instrument(clients ...Client) (Client, error) {
return multi{clients: clients}, nil
}

// WithSyntheticDuties wraps the provided client adding synthetic duties.
func WithSyntheticDuties(cl Client, pubkeys []eth2p0.BLSPubKey) Client {
return &synthWrapper{
Client: cl,
synthProposerCache: newSynthProposerCache(pubkeys),
}
}

// NewMultiHTTP returns a new instrumented multi eth2 http client.
func NewMultiHTTP(ctx context.Context, timeout time.Duration, addresses []string, opts ...Option) (Client, error) {
func NewMultiHTTP(ctx context.Context, timeout time.Duration, addresses ...string) (Client, error) {
var clients []Client
for _, address := range addresses {
eth2Svc, err := eth2http.New(ctx,
Expand All @@ -83,7 +91,7 @@ func NewMultiHTTP(ctx context.Context, timeout time.Duration, addresses []string
return nil, errors.New("invalid eth2 http service")
}

clients = append(clients, AdaptEth2HTTP(eth2Http, timeout, opts...))
clients = append(clients, AdaptEth2HTTP(eth2Http, timeout))
}

return Instrument(clients...)
Expand Down
10 changes: 5 additions & 5 deletions app/eth2wrap/eth2wrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func TestSyncState(t *testing.T) {
func TestErrors(t *testing.T) {
ctx := context.Background()
t.Run("network dial error", func(t *testing.T) {
_, err := eth2wrap.NewMultiHTTP(ctx, time.Hour, []string{"localhost:22222"})
_, err := eth2wrap.NewMultiHTTP(ctx, time.Hour, "localhost:22222")
log.Error(ctx, "See this error log for fields", err)
require.Error(t, err)
require.ErrorContains(t, err, "beacon api new eth2 client: network operation error: dial: connect: connection refused")
Expand All @@ -186,7 +186,7 @@ func TestErrors(t *testing.T) {
}))

t.Run("http timeout", func(t *testing.T) {
_, err := eth2wrap.NewMultiHTTP(ctx, time.Millisecond, []string{srv.URL})
_, err := eth2wrap.NewMultiHTTP(ctx, time.Millisecond, srv.URL)
log.Error(ctx, "See this error log for fields", err)
require.Error(t, err)
require.ErrorContains(t, err, "beacon api new eth2 client: http request timeout: context deadline exceeded")
Expand All @@ -195,7 +195,7 @@ func TestErrors(t *testing.T) {
t.Run("caller cancelled", func(t *testing.T) {
ctx, cancel := context.WithCancel(ctx)
cancel()
_, err := eth2wrap.NewMultiHTTP(ctx, time.Millisecond, []string{srv.URL})
_, err := eth2wrap.NewMultiHTTP(ctx, time.Millisecond, srv.URL)
log.Error(ctx, "See this error log for fields", err)
require.Error(t, err)
require.ErrorContains(t, err, "beacon api new eth2 client: caller cancelled http request: context canceled")
Expand All @@ -204,7 +204,7 @@ func TestErrors(t *testing.T) {
t.Run("go-eth2-client http error", func(t *testing.T) {
bmock, err := beaconmock.New()
require.NoError(t, err)
eth2Cl, err := eth2wrap.NewMultiHTTP(ctx, time.Second, []string{bmock.Address()})
eth2Cl, err := eth2wrap.NewMultiHTTP(ctx, time.Second, bmock.Address())
require.NoError(t, err)

_, err = eth2Cl.AggregateAttestation(ctx, 0, eth2p0.Root{})
Expand Down Expand Up @@ -235,7 +235,7 @@ func TestCtxCancel(t *testing.T) {

bmock, err := beaconmock.New()
require.NoError(t, err)
eth2Cl, err := eth2wrap.NewMultiHTTP(ctx, time.Second, []string{bmock.Address()})
eth2Cl, err := eth2wrap.NewMultiHTTP(ctx, time.Second, bmock.Address())
require.NoError(t, err)

cancel() // Cancel context before calling method.
Expand Down
126 changes: 8 additions & 118 deletions app/eth2wrap/httpwrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,10 @@ import (
"testing"
"time"

"github.com/attestantio/go-eth2-client/api"
eth2v1 "github.com/attestantio/go-eth2-client/api/v1"
eth2http "github.com/attestantio/go-eth2-client/http"
"github.com/attestantio/go-eth2-client/spec"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/z"
"github.com/obolnetwork/charon/eth2util/eth2exp"
)
Expand All @@ -48,139 +44,33 @@ type BlockAttestationsProvider interface {
BlockAttestations(ctx context.Context, stateID string) ([]*eth2p0.Attestation, error)
}

type Option func(*httpAdapter)

// WithSyntheticDuties returns an option that enables synthetic duties.
func WithSyntheticDuties(pubkeys []eth2p0.BLSPubKey) Option {
return func(a *httpAdapter) {
a.syntheticDuties = true
a.synthProposerCache = newSynthProposerCache(pubkeys)
}
}

// NewHTTPAdapterForT returns a http adapter for testing non-eth2service methods as it is nil.
func NewHTTPAdapterForT(_ *testing.T, address string, timeout time.Duration, opts ...Option) *httpAdapter {
return newHTTPAdapter(nil, address, timeout, opts...)
func NewHTTPAdapterForT(_ *testing.T, address string, timeout time.Duration) *httpAdapter {
return newHTTPAdapter(nil, address, timeout)
}

// AdaptEth2HTTP returns a Client wrapping an eth2http service by adding experimental endpoints.
// Note that the returned client doesn't wrap errors, so they are unstructured without stacktraces.
func AdaptEth2HTTP(eth2Svc *eth2http.Service, timeout time.Duration, opts ...Option) Client {
return newHTTPAdapter(eth2Svc, eth2Svc.Address(), timeout, opts...)
func AdaptEth2HTTP(eth2Svc *eth2http.Service, timeout time.Duration) Client {
return newHTTPAdapter(eth2Svc, eth2Svc.Address(), timeout)
}

// newHTTPAdapter returns a new http adapter.
func newHTTPAdapter(ethSvc *eth2http.Service, address string, timeout time.Duration, opts ...Option) *httpAdapter {
a := &httpAdapter{
func newHTTPAdapter(ethSvc *eth2http.Service, address string, timeout time.Duration) *httpAdapter {
return &httpAdapter{
Service: ethSvc,
address: address,
timeout: timeout,
}

for _, opt := range opts {
opt(a)
}

return a
}

// httpAdapter implements Client by wrapping and adding the following to eth2http.Service:
// - experimental interfaces not present in go-eth2-client
// - synthetic duties
type httpAdapter struct {
*eth2http.Service
address string
timeout time.Duration
syntheticDuties bool
synthProposerCache *synthProposerCache
}

// ProposerDuties returns upstream proposer duties for the provided validator indexes or
// upstream proposer duties and synthetic duties for all cluster validators if enabled.
func (h *httpAdapter) ProposerDuties(ctx context.Context, epoch eth2p0.Epoch, valIdxs []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) {
if h.syntheticDuties {
return h.synthProposerCache.Duties(ctx, h.Service, epoch)
}

return h.Service.ProposerDuties(ctx, epoch, valIdxs)
}

// BeaconBlockProposal returns an unsigned beacon block, possibly marked as synthetic.
func (h *httpAdapter) BeaconBlockProposal(ctx context.Context, slot eth2p0.Slot, randao eth2p0.BLSSignature, graffiti []byte) (*spec.VersionedBeaconBlock, error) {
if h.syntheticDuties {
ok, err := h.synthProposerCache.IsSynthetic(ctx, h.Service, slot)
if err != nil {
return nil, err
}

if ok {
graffiti = []byte(syntheticBlockGraffiti)
}
}

return h.Service.BeaconBlockProposal(ctx, slot, randao, graffiti)
}

// BlindedBeaconBlockProposal returns an unsigned blinded beacon block, possibly marked as synthetic.
func (h *httpAdapter) BlindedBeaconBlockProposal(ctx context.Context, slot eth2p0.Slot, randao eth2p0.BLSSignature, graffiti []byte) (*api.VersionedBlindedBeaconBlock, error) {
if h.syntheticDuties {
ok, err := h.synthProposerCache.IsSynthetic(ctx, h.Service, slot)
if err != nil {
return nil, err
}

if ok {
graffiti = []byte(syntheticBlockGraffiti)
}
}

return h.Service.BlindedBeaconBlockProposal(ctx, slot, randao, graffiti)
}

// SubmitBlindedBeaconBlock submits a blinded beacon block or swallows it if marked as synthetic.
func (h *httpAdapter) SubmitBlindedBeaconBlock(ctx context.Context, block *api.VersionedSignedBlindedBeaconBlock) error {
var graffiti [32]byte
switch block.Version {
case spec.DataVersionBellatrix:
graffiti = block.Bellatrix.Message.Body.Graffiti
default:
return errors.New("unknown block version")
}

var synthGraffiti [32]byte
copy(synthGraffiti[:], syntheticBlockGraffiti)
if graffiti == synthGraffiti {
log.Debug(ctx, "Synthetic blinded beacon block swallowed")
return nil
}

return h.Service.SubmitBlindedBeaconBlock(ctx, block)
}

// SubmitBeaconBlock submits a beacon block or swallows it if marked as synthetic.
func (h *httpAdapter) SubmitBeaconBlock(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
var graffiti [32]byte
switch block.Version {
case spec.DataVersionPhase0:
graffiti = block.Phase0.Message.Body.Graffiti
case spec.DataVersionAltair:
graffiti = block.Altair.Message.Body.Graffiti
case spec.DataVersionBellatrix:
graffiti = block.Bellatrix.Message.Body.Graffiti
case spec.DataVersionCapella:
graffiti = block.Capella.Message.Body.Graffiti
default:
return errors.New("unknown block version")
}

var synthGraffiti [32]byte
copy(synthGraffiti[:], syntheticBlockGraffiti)
if graffiti == synthGraffiti {
log.Debug(ctx, "Synthetic beacon block swallowed")
return nil
}

return h.Service.SubmitBeaconBlock(ctx, block)
address string
timeout time.Duration
}

// AggregateBeaconCommitteeSelections implements eth2exp.BeaconCommitteeSelectionAggregator.
Expand Down
Loading