From 11e3000c72b6f825a4b4d27a61994d266eb742c7 Mon Sep 17 00:00:00 2001 From: Andrei Smirnov Date: Mon, 6 May 2024 14:04:37 +0300 Subject: [PATCH] *: integrating go-eth2-client@v0.21.1 (#2986) Integrating the new version of go-eth2-client, resolving breaking changes, etc. Known issues: ~https://github.com/attestantio/go-eth2-client/issues/118~ & ~https://github.com/attestantio/go-eth2-client/issues/119~ TODOs - [x] Integrate v0.21.0 - [x] Integrate v0.21.1 (with bugfixes) - [x] Test with simnet - [x] Improve test coverage - [x] Investigate latest teku VC not querying BN category: feature ticket: #2936 --- app/app.go | 2 +- app/eth2wrap/eth2wrap.go | 151 +-- app/eth2wrap/eth2wrap_gen.go | 47 +- app/eth2wrap/eth2wrap_test.go | 11 +- app/eth2wrap/lazy.go | 30 + app/eth2wrap/lazy_test.go | 147 +++ app/eth2wrap/mocks/client.go | 1157 +++++++++++++++++ app/eth2wrap/multi.go | 188 +++ app/eth2wrap/multi_test.go | 186 +++ app/eth2wrap/synthproposer.go | 171 +-- app/eth2wrap/synthproposer_test.go | 102 +- codecov.yml | 1 + core/bcast/bcast.go | 24 +- core/bcast/bcast_test.go | 45 +- core/deadline_test.go | 1 - core/dutydb/memory.go | 110 +- core/dutydb/memory_internal_test.go | 6 - core/dutydb/memory_test.go | 152 +-- core/fetcher/fetcher.go | 108 +- core/fetcher/fetcher_internal_test.go | 84 ++ core/fetcher/fetcher_test.go | 135 +- core/interfaces.go | 12 - core/parsigex/parsigex_test.go | 2 +- core/proto.go | 6 +- core/proto_test.go | 36 +- core/scheduler/scheduler.go | 14 +- core/scheduler/scheduler_test.go | 7 +- core/serialise_test.go | 1 - core/sigagg/sigagg_test.go | 24 +- core/signeddata.go | 131 +- core/signeddata_test.go | 363 ++++++ core/ssz.go | 204 ++- core/ssz_test.go | 209 ++- ...VersionedSignedBlindedProposal.json.golden | 3 +- ...sation_VersionedBlindedProposal.ssz.golden | Bin 7317 -> 0 bytes ...Serialisation_VersionedProposal.ssz.golden | Bin 5659 -> 5660 bytes ..._VersionedSignedBlindedProposal.ssz.golden | Bin 8813 -> 8814 bytes ...isation_VersionedSignedProposal.ssz.golden | Bin 6862 -> 6863 bytes core/tracker/inclusion.go | 53 +- core/tracker/inclusion_internal_test.go | 5 +- core/tracker/tracker.go | 2 +- core/tracker/tracker_internal_test.go | 1 - core/types.go | 24 +- core/types_test.go | 123 ++ core/unsigneddata.go | 211 ++- core/unsigneddata_test.go | 203 +++ core/validatorapi/eth2types.go | 60 +- core/validatorapi/router.go | 298 ++--- core/validatorapi/router_internal_test.go | 427 ++++-- core/validatorapi/validatorapi.go | 88 +- core/validatorapi/validatorapi_test.go | 153 +-- docs/architecture.md | 39 +- go.mod | 3 +- go.sum | 4 +- testutil/beaconmock/beaconmock.go | 30 +- testutil/beaconmock/beaconmock_fuzz.go | 7 - testutil/beaconmock/options.go | 44 +- testutil/compose/smoke/smoke_test.go | 68 +- testutil/fuzz.go | 14 +- testutil/integration/simnet_test.go | 28 +- testutil/random.go | 22 +- testutil/validatormock/component.go | 13 +- testutil/validatormock/propose.go | 154 +-- testutil/validatormock/propose_test.go | 4 +- tools.go | 3 + 65 files changed, 3880 insertions(+), 2071 deletions(-) create mode 100644 app/eth2wrap/lazy_test.go create mode 100644 app/eth2wrap/mocks/client.go create mode 100644 app/eth2wrap/multi.go create mode 100644 app/eth2wrap/multi_test.go create mode 100644 core/fetcher/fetcher_internal_test.go delete mode 100644 core/testdata/TestSSZSerialisation_VersionedBlindedProposal.ssz.golden diff --git a/app/app.go b/app/app.go index f26639d80..b4ee2e72c 100644 --- a/app/app.go +++ b/app/app.go @@ -437,7 +437,7 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config, return err } - fetch, err := fetcher.New(eth2Cl, feeRecipientFunc) + fetch, err := fetcher.New(eth2Cl, feeRecipientFunc, mutableConf.BuilderAPI) if err != nil { return err } diff --git a/app/eth2wrap/eth2wrap.go b/app/eth2wrap/eth2wrap.go index 2560a3289..bdbe73dca 100644 --- a/app/eth2wrap/eth2wrap.go +++ b/app/eth2wrap/eth2wrap.go @@ -20,7 +20,6 @@ import ( "github.com/obolnetwork/charon/app/forkjoin" "github.com/obolnetwork/charon/app/promauto" "github.com/obolnetwork/charon/app/z" - "github.com/obolnetwork/charon/eth2util/eth2exp" ) //go:generate go run genwrap/genwrap.go @@ -80,6 +79,7 @@ func NewMultiHTTP(timeout time.Duration, addresses ...string) (Client, error) { eth2http.WithLogLevel(zeroLogInfo), eth2http.WithAddress(address), eth2http.WithTimeout(timeout), + eth2http.WithAllowDelayedStart(true), ) if err != nil { return nil, wrapError(ctx, err, "new eth2 client", z.Str("address", address)) @@ -98,155 +98,6 @@ func NewMultiHTTP(timeout time.Duration, addresses ...string) (Client, error) { return Instrument(clients...) } -func newMulti(clients []Client) Client { - return multi{ - clients: clients, - selector: newBestSelector(bestPeriod), - } -} - -// multi implements Client by wrapping multiple clients, calling them in parallel -// and returning the first successful response. -// It also adds prometheus metrics and error wrapping. -// It also implements a best client selector. -type multi struct { - clients []Client - selector *bestSelector -} - -func (multi) Name() string { - return "eth2wrap.multi" -} - -func (m multi) Address() string { - address, ok := m.selector.BestAddress() - if !ok { - return m.clients[0].Address() - } - - return address -} - -func (m multi) SetValidatorCache(valCache func(context.Context) (ActiveValidators, error)) { - for _, cl := range m.clients { - cl.SetValidatorCache(valCache) - } -} - -func (m multi) SetForkVersion(forkVersion [4]byte) { - for _, c := range m.clients { - c.SetForkVersion(forkVersion) - } -} - -func (m multi) ActiveValidators(ctx context.Context) (ActiveValidators, error) { - const label = "active_validators" - // No latency since this is a cached endpoint. - - res0, err := provide(ctx, m.clients, - func(ctx context.Context, cl Client) (ActiveValidators, error) { - return cl.ActiveValidators(ctx) - }, - nil, nil, - ) - if err != nil { - incError(label) - err = wrapError(ctx, err, label) - } - - return res0, err -} - -func (m multi) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConfigResponse, error) { - const label = "proposer_config" - defer latency(label)() - - res0, err := provide(ctx, m.clients, - func(ctx context.Context, cl Client) (*eth2exp.ProposerConfigResponse, error) { - return cl.ProposerConfig(ctx) - }, - nil, m.selector, - ) - if err != nil { - incError(label) - err = wrapError(ctx, err, label) - } - - return res0, err -} - -func (m multi) AggregateBeaconCommitteeSelections(ctx context.Context, selections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) { - const label = "aggregate_beacon_committee_selections" - defer latency(label)() - - res0, err := provide(ctx, m.clients, - func(ctx context.Context, cl Client) ([]*eth2exp.BeaconCommitteeSelection, error) { - return cl.AggregateBeaconCommitteeSelections(ctx, selections) - }, - nil, m.selector, - ) - if err != nil { - incError(label) - err = wrapError(ctx, err, label) - } - - return res0, err -} - -func (m multi) AggregateSyncCommitteeSelections(ctx context.Context, selections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) { - const label = "aggregate_sync_committee_selections" - defer latency(label)() - - res, err := provide(ctx, m.clients, - func(ctx context.Context, cl Client) ([]*eth2exp.SyncCommitteeSelection, error) { - return cl.AggregateSyncCommitteeSelections(ctx, selections) - }, - nil, m.selector, - ) - if err != nil { - incError(label) - err = wrapError(ctx, err, label) - } - - return res, err -} - -func (m multi) BlockAttestations(ctx context.Context, stateID string) ([]*eth2p0.Attestation, error) { - const label = "block_attestations" - defer latency(label)() - - res, err := provide(ctx, m.clients, - func(ctx context.Context, cl Client) ([]*eth2p0.Attestation, error) { - return cl.BlockAttestations(ctx, stateID) - }, - nil, m.selector, - ) - if err != nil { - incError(label) - err = wrapError(ctx, err, label) - } - - return res, err -} - -func (m multi) NodePeerCount(ctx context.Context) (int, error) { - const label = "node_peer_count" - defer latency(label)() - - res, err := provide(ctx, m.clients, - func(ctx context.Context, cl Client) (int, error) { - return cl.NodePeerCount(ctx) - }, - nil, m.selector, - ) - if err != nil { - incError(label) - err = wrapError(ctx, err, label) - } - - return res, err -} - // provide calls the work function with each client in parallel, returning the // first successful result or first error. // The bestIdxFunc is called with the index of the client returning a successful response. diff --git a/app/eth2wrap/eth2wrap_gen.go b/app/eth2wrap/eth2wrap_gen.go index 8047959b8..3a4afb10a 100644 --- a/app/eth2wrap/eth2wrap_gen.go +++ b/app/eth2wrap/eth2wrap_gen.go @@ -38,7 +38,6 @@ type Client interface { eth2client.AttesterDutiesProvider eth2client.BeaconBlockRootProvider eth2client.BeaconCommitteeSubscriptionsSubmitter - eth2client.BlindedProposalProvider eth2client.BlindedProposalSubmitter eth2client.DepositContractProvider eth2client.DomainProvider @@ -392,13 +391,13 @@ func (m multi) BeaconBlockRoot(ctx context.Context, opts *api.BeaconBlockRootOpt } // SubmitProposal submits a proposal. -func (m multi) SubmitProposal(ctx context.Context, block *api.VersionedSignedProposal) error { +func (m multi) SubmitProposal(ctx context.Context, opts *api.SubmitProposalOpts) error { const label = "submit_proposal" defer latency(label)() err := submit(ctx, m.clients, func(ctx context.Context, cl Client) error { - return cl.SubmitProposal(ctx, block) + return cl.SubmitProposal(ctx, opts) }, m.selector, ) @@ -431,34 +430,14 @@ func (m multi) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscript return err } -// BlindedProposal fetches a blinded proposed beacon block for signing. -func (m multi) BlindedProposal(ctx context.Context, opts *api.BlindedProposalOpts) (*api.Response[*api.VersionedBlindedProposal], error) { - const label = "blinded_proposal" - defer latency(label)() - - res0, err := provide(ctx, m.clients, - func(ctx context.Context, cl Client) (*api.Response[*api.VersionedBlindedProposal], error) { - return cl.BlindedProposal(ctx, opts) - }, - nil, m.selector, - ) - - if err != nil { - incError(label) - err = wrapError(ctx, err, label) - } - - return res0, err -} - // SubmitBlindedProposal submits a beacon block. -func (m multi) SubmitBlindedProposal(ctx context.Context, block *api.VersionedSignedBlindedProposal) error { +func (m multi) SubmitBlindedProposal(ctx context.Context, opts *api.SubmitBlindedProposalOpts) error { const label = "submit_blinded_proposal" defer latency(label)() err := submit(ctx, m.clients, func(ctx context.Context, cl Client) error { - return cl.SubmitBlindedProposal(ctx, block) + return cl.SubmitBlindedProposal(ctx, opts) }, m.selector, ) @@ -921,13 +900,13 @@ func (l *lazy) BeaconBlockRoot(ctx context.Context, opts *api.BeaconBlockRootOpt } // SubmitProposal submits a proposal. -func (l *lazy) SubmitProposal(ctx context.Context, block *api.VersionedSignedProposal) (err error) { +func (l *lazy) SubmitProposal(ctx context.Context, opts *api.SubmitProposalOpts) (err error) { cl, err := l.getOrCreateClient(ctx) if err != nil { return err } - return cl.SubmitProposal(ctx, block) + return cl.SubmitProposal(ctx, opts) } // SubmitBeaconCommitteeSubscriptions subscribes to beacon committees. @@ -940,24 +919,14 @@ func (l *lazy) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscript return cl.SubmitBeaconCommitteeSubscriptions(ctx, subscriptions) } -// BlindedProposal fetches a blinded proposed beacon block for signing. -func (l *lazy) BlindedProposal(ctx context.Context, opts *api.BlindedProposalOpts) (res0 *api.Response[*api.VersionedBlindedProposal], err error) { - cl, err := l.getOrCreateClient(ctx) - if err != nil { - return res0, err - } - - return cl.BlindedProposal(ctx, opts) -} - // SubmitBlindedProposal submits a beacon block. -func (l *lazy) SubmitBlindedProposal(ctx context.Context, block *api.VersionedSignedBlindedProposal) (err error) { +func (l *lazy) SubmitBlindedProposal(ctx context.Context, opts *api.SubmitBlindedProposalOpts) (err error) { cl, err := l.getOrCreateClient(ctx) if err != nil { return err } - return cl.SubmitBlindedProposal(ctx, block) + return cl.SubmitBlindedProposal(ctx, opts) } // SubmitValidatorRegistrations submits a validator registration. diff --git a/app/eth2wrap/eth2wrap_test.go b/app/eth2wrap/eth2wrap_test.go index 8fd1cc51a..376a3d94b 100644 --- a/app/eth2wrap/eth2wrap_test.go +++ b/app/eth2wrap/eth2wrap_test.go @@ -167,6 +167,7 @@ func TestSyncState(t *testing.T) { func TestErrors(t *testing.T) { ctx := context.Background() + t.Run("network dial error", func(t *testing.T) { cl, err := eth2wrap.NewMultiHTTP(time.Hour, "localhost:22222") require.NoError(t, err) @@ -174,7 +175,7 @@ func TestErrors(t *testing.T) { _, err = cl.SlotsPerEpoch(ctx) 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") + require.ErrorContains(t, err, "beacon api slots_per_epoch: client is not active") }) // Test http server that just hangs until request cancelled @@ -189,7 +190,7 @@ func TestErrors(t *testing.T) { _, err = cl.SlotsPerEpoch(ctx) 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") + require.ErrorContains(t, err, "beacon api slots_per_epoch: client is not active") }) t.Run("caller cancelled", func(t *testing.T) { @@ -226,7 +227,7 @@ func TestErrors(t *testing.T) { bmock.SignedBeaconBlockFunc = func(_ context.Context, blockID string) (*eth2spec.VersionedSignedBeaconBlock, error) { return nil, ð2api.Error{ Method: http.MethodGet, - Endpoint: fmt.Sprintf("/eth/v2/beacon/blocks/%s", blockID), + Endpoint: "/eth/v3/beacon/blocks/" + blockID, StatusCode: http.StatusNotFound, Data: []byte(fmt.Sprintf(`{"code":404,"message":"NOT_FOUND: beacon block at slot %s","stacktraces":[]}`, blockID)), } @@ -373,6 +374,9 @@ func TestOnlyTimeout(t *testing.T) { require.Fail(t, "Expect this only to return after main ctx cancelled") }() + // Allow the above goroutine to block on the .Spec() call. + time.Sleep(10 * time.Millisecond) + // testCtxCancel tests that no concurrent calls block if the user cancels the context. testCtxCancel := func(t *testing.T, timeout time.Duration) { t.Helper() @@ -426,7 +430,6 @@ func TestLazy(t *testing.T) { // Both proxies are disabled, so this should fail. _, err = eth2Cl.NodeSyncing(ctx, nil) require.Error(t, err) - require.Equal(t, "", eth2Cl.Address()) enabled1.Store(true) diff --git a/app/eth2wrap/lazy.go b/app/eth2wrap/lazy.go index cfb6076a4..37dc8a026 100644 --- a/app/eth2wrap/lazy.go +++ b/app/eth2wrap/lazy.go @@ -12,6 +12,18 @@ import ( "github.com/obolnetwork/charon/eth2util/eth2exp" ) +//go:generate mockery --name=Client --output=mocks --outpkg=mocks --case=underscore + +// NewLazyForT creates a new lazy client for testing. +func NewLazyForT(client Client) Client { + return &lazy{ + provider: func(context.Context) (Client, error) { + return client, nil + }, + client: client, + } +} + // newLazy creates a new lazy client. func newLazy(provider func(context.Context) (Client, error)) *lazy { return &lazy{ @@ -107,6 +119,24 @@ func (l *lazy) Address() string { return cl.Address() } +func (l *lazy) IsActive() bool { + cl, ok := l.getClient() + if !ok { + return false + } + + return cl.IsActive() +} + +func (l *lazy) IsSynced() bool { + cl, ok := l.getClient() + if !ok { + return false + } + + return cl.IsSynced() +} + func (l *lazy) ActiveValidators(ctx context.Context) (ActiveValidators, error) { cl, err := l.getOrCreateClient(ctx) if err != nil { diff --git a/app/eth2wrap/lazy_test.go b/app/eth2wrap/lazy_test.go new file mode 100644 index 000000000..f2e1e4005 --- /dev/null +++ b/app/eth2wrap/lazy_test.go @@ -0,0 +1,147 @@ +// Copyright © 2022-2024 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package eth2wrap_test + +import ( + "context" + "testing" + + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/obolnetwork/charon/app/eth2wrap" + "github.com/obolnetwork/charon/app/eth2wrap/mocks" + "github.com/obolnetwork/charon/eth2util/eth2exp" +) + +func TestLazy_Name(t *testing.T) { + client := mocks.NewClient(t) + client.On("Name").Return("test").Once() + + l := eth2wrap.NewLazyForT(client) + + require.Equal(t, "test", l.Name()) +} + +func TestLazy_Address(t *testing.T) { + client := mocks.NewClient(t) + client.On("Address").Return("test").Once() + + l := eth2wrap.NewLazyForT(client) + + require.Equal(t, "test", l.Address()) +} + +func TestLazy_IsActive(t *testing.T) { + client := mocks.NewClient(t) + client.On("IsActive").Return(true).Once() + + l := eth2wrap.NewLazyForT(client) + + require.True(t, l.IsActive()) +} + +func TestLazy_IsSynced(t *testing.T) { + client := mocks.NewClient(t) + client.On("IsSynced").Return(true).Once() + + l := eth2wrap.NewLazyForT(client) + + require.True(t, l.IsSynced()) +} + +func TestLazy_NodePeerCount(t *testing.T) { + client := mocks.NewClient(t) + client.On("NodePeerCount", mock.Anything).Return(5, nil).Once() + + l := eth2wrap.NewLazyForT(client) + + c, err := l.NodePeerCount(context.Background()) + require.NoError(t, err) + require.Equal(t, 5, c) +} + +func TestLazy_BlockAttestations(t *testing.T) { + ctx := context.Background() + atts := make([]*eth2p0.Attestation, 3) + + client := mocks.NewClient(t) + client.On("BlockAttestations", ctx, "state").Return(atts, nil).Once() + + l := eth2wrap.NewLazyForT(client) + + atts2, err := l.BlockAttestations(ctx, "state") + require.NoError(t, err) + require.Equal(t, atts, atts2) +} + +func TestLazy_AggregateSyncCommitteeSelections(t *testing.T) { + ctx := context.Background() + partsel := make([]*eth2exp.SyncCommitteeSelection, 1) + selections := make([]*eth2exp.SyncCommitteeSelection, 3) + + client := mocks.NewClient(t) + client.On("AggregateSyncCommitteeSelections", ctx, partsel).Return(selections, nil).Once() + + l := eth2wrap.NewLazyForT(client) + + selections2, err := l.AggregateSyncCommitteeSelections(ctx, partsel) + require.NoError(t, err) + require.Equal(t, selections, selections2) +} + +func TestLazy_AggregateBeaconCommitteeSelections(t *testing.T) { + ctx := context.Background() + partsel := make([]*eth2exp.BeaconCommitteeSelection, 1) + selections := make([]*eth2exp.BeaconCommitteeSelection, 3) + + client := mocks.NewClient(t) + client.On("AggregateBeaconCommitteeSelections", ctx, partsel).Return(selections, nil).Once() + + l := eth2wrap.NewLazyForT(client) + + selections2, err := l.AggregateBeaconCommitteeSelections(ctx, partsel) + require.NoError(t, err) + require.Equal(t, selections, selections2) +} + +func TestLazy_ProposerConfig(t *testing.T) { + ctx := context.Background() + resp := ð2exp.ProposerConfigResponse{} + + client := mocks.NewClient(t) + client.On("ProposerConfig", ctx).Return(resp, nil).Once() + + l := eth2wrap.NewLazyForT(client) + + resp2, err := l.ProposerConfig(ctx) + require.NoError(t, err) + require.Equal(t, resp, resp2) +} + +func TestLazy_ActiveValidators(t *testing.T) { + ctx := context.Background() + vals := make(eth2wrap.ActiveValidators) + + client := mocks.NewClient(t) + client.On("ActiveValidators", ctx).Return(vals, nil).Once() + + l := eth2wrap.NewLazyForT(client) + + vals2, err := l.ActiveValidators(ctx) + require.NoError(t, err) + require.Equal(t, vals, vals2) +} + +func TestLazy_SetValidatorCache(t *testing.T) { + valCache := func(context.Context) (eth2wrap.ActiveValidators, error) { + return nil, nil + } + + client := mocks.NewClient(t) + client.On("SetValidatorCache", mock.Anything).Once() + + l := eth2wrap.NewLazyForT(client) + l.SetValidatorCache(valCache) +} diff --git a/app/eth2wrap/mocks/client.go b/app/eth2wrap/mocks/client.go new file mode 100644 index 000000000..f3f41a7f9 --- /dev/null +++ b/app/eth2wrap/mocks/client.go @@ -0,0 +1,1157 @@ +// Copyright © 2022-2024 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/attestantio/go-eth2-client/api" + altair "github.com/attestantio/go-eth2-client/spec/altair" + + context "context" + + eth2exp "github.com/obolnetwork/charon/eth2util/eth2exp" + + eth2wrap "github.com/obolnetwork/charon/app/eth2wrap" + + mock "github.com/stretchr/testify/mock" + + phase0 "github.com/attestantio/go-eth2-client/spec/phase0" + + spec "github.com/attestantio/go-eth2-client/spec" + + time "time" + + v1 "github.com/attestantio/go-eth2-client/api/v1" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// ActiveValidators provides a mock function with given fields: _a0 +func (_m *Client) ActiveValidators(_a0 context.Context) (eth2wrap.ActiveValidators, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for ActiveValidators") + } + + var r0 eth2wrap.ActiveValidators + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (eth2wrap.ActiveValidators, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) eth2wrap.ActiveValidators); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(eth2wrap.ActiveValidators) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Address provides a mock function with given fields: +func (_m *Client) Address() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Address") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// AggregateAttestation provides a mock function with given fields: ctx, opts +func (_m *Client) AggregateAttestation(ctx context.Context, opts *api.AggregateAttestationOpts) (*api.Response[*phase0.Attestation], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for AggregateAttestation") + } + + var r0 *api.Response[*phase0.Attestation] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.AggregateAttestationOpts) (*api.Response[*phase0.Attestation], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.AggregateAttestationOpts) *api.Response[*phase0.Attestation]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[*phase0.Attestation]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.AggregateAttestationOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AggregateBeaconCommitteeSelections provides a mock function with given fields: ctx, partialSelections +func (_m *Client) AggregateBeaconCommitteeSelections(ctx context.Context, partialSelections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) { + ret := _m.Called(ctx, partialSelections) + + if len(ret) == 0 { + panic("no return value specified for AggregateBeaconCommitteeSelections") + } + + var r0 []*eth2exp.BeaconCommitteeSelection + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error)); ok { + return rf(ctx, partialSelections) + } + if rf, ok := ret.Get(0).(func(context.Context, []*eth2exp.BeaconCommitteeSelection) []*eth2exp.BeaconCommitteeSelection); ok { + r0 = rf(ctx, partialSelections) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*eth2exp.BeaconCommitteeSelection) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []*eth2exp.BeaconCommitteeSelection) error); ok { + r1 = rf(ctx, partialSelections) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AggregateSyncCommitteeSelections provides a mock function with given fields: ctx, partialSelections +func (_m *Client) AggregateSyncCommitteeSelections(ctx context.Context, partialSelections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) { + ret := _m.Called(ctx, partialSelections) + + if len(ret) == 0 { + panic("no return value specified for AggregateSyncCommitteeSelections") + } + + var r0 []*eth2exp.SyncCommitteeSelection + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error)); ok { + return rf(ctx, partialSelections) + } + if rf, ok := ret.Get(0).(func(context.Context, []*eth2exp.SyncCommitteeSelection) []*eth2exp.SyncCommitteeSelection); ok { + r0 = rf(ctx, partialSelections) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*eth2exp.SyncCommitteeSelection) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []*eth2exp.SyncCommitteeSelection) error); ok { + r1 = rf(ctx, partialSelections) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AttestationData provides a mock function with given fields: ctx, opts +func (_m *Client) AttestationData(ctx context.Context, opts *api.AttestationDataOpts) (*api.Response[*phase0.AttestationData], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for AttestationData") + } + + var r0 *api.Response[*phase0.AttestationData] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.AttestationDataOpts) (*api.Response[*phase0.AttestationData], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.AttestationDataOpts) *api.Response[*phase0.AttestationData]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[*phase0.AttestationData]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.AttestationDataOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AttesterDuties provides a mock function with given fields: ctx, opts +func (_m *Client) AttesterDuties(ctx context.Context, opts *api.AttesterDutiesOpts) (*api.Response[[]*v1.AttesterDuty], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for AttesterDuties") + } + + var r0 *api.Response[[]*v1.AttesterDuty] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.AttesterDutiesOpts) (*api.Response[[]*v1.AttesterDuty], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.AttesterDutiesOpts) *api.Response[[]*v1.AttesterDuty]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[[]*v1.AttesterDuty]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.AttesterDutiesOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BeaconBlockRoot provides a mock function with given fields: ctx, opts +func (_m *Client) BeaconBlockRoot(ctx context.Context, opts *api.BeaconBlockRootOpts) (*api.Response[*phase0.Root], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for BeaconBlockRoot") + } + + var r0 *api.Response[*phase0.Root] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconBlockRootOpts) (*api.Response[*phase0.Root], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconBlockRootOpts) *api.Response[*phase0.Root]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[*phase0.Root]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.BeaconBlockRootOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BlockAttestations provides a mock function with given fields: ctx, stateID +func (_m *Client) BlockAttestations(ctx context.Context, stateID string) ([]*phase0.Attestation, error) { + ret := _m.Called(ctx, stateID) + + if len(ret) == 0 { + panic("no return value specified for BlockAttestations") + } + + var r0 []*phase0.Attestation + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]*phase0.Attestation, error)); ok { + return rf(ctx, stateID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []*phase0.Attestation); ok { + r0 = rf(ctx, stateID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*phase0.Attestation) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, stateID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DepositContract provides a mock function with given fields: ctx, opts +func (_m *Client) DepositContract(ctx context.Context, opts *api.DepositContractOpts) (*api.Response[*v1.DepositContract], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for DepositContract") + } + + var r0 *api.Response[*v1.DepositContract] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.DepositContractOpts) (*api.Response[*v1.DepositContract], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.DepositContractOpts) *api.Response[*v1.DepositContract]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[*v1.DepositContract]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.DepositContractOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Domain provides a mock function with given fields: ctx, domainType, epoch +func (_m *Client) Domain(ctx context.Context, domainType phase0.DomainType, epoch phase0.Epoch) (phase0.Domain, error) { + ret := _m.Called(ctx, domainType, epoch) + + if len(ret) == 0 { + panic("no return value specified for Domain") + } + + var r0 phase0.Domain + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, phase0.DomainType, phase0.Epoch) (phase0.Domain, error)); ok { + return rf(ctx, domainType, epoch) + } + if rf, ok := ret.Get(0).(func(context.Context, phase0.DomainType, phase0.Epoch) phase0.Domain); ok { + r0 = rf(ctx, domainType, epoch) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(phase0.Domain) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, phase0.DomainType, phase0.Epoch) error); ok { + r1 = rf(ctx, domainType, epoch) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Fork provides a mock function with given fields: ctx, opts +func (_m *Client) Fork(ctx context.Context, opts *api.ForkOpts) (*api.Response[*phase0.Fork], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Fork") + } + + var r0 *api.Response[*phase0.Fork] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.ForkOpts) (*api.Response[*phase0.Fork], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.ForkOpts) *api.Response[*phase0.Fork]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[*phase0.Fork]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.ForkOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ForkSchedule provides a mock function with given fields: ctx, opts +func (_m *Client) ForkSchedule(ctx context.Context, opts *api.ForkScheduleOpts) (*api.Response[[]*phase0.Fork], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for ForkSchedule") + } + + var r0 *api.Response[[]*phase0.Fork] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.ForkScheduleOpts) (*api.Response[[]*phase0.Fork], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.ForkScheduleOpts) *api.Response[[]*phase0.Fork]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[[]*phase0.Fork]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.ForkScheduleOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Genesis provides a mock function with given fields: ctx, opts +func (_m *Client) Genesis(ctx context.Context, opts *api.GenesisOpts) (*api.Response[*v1.Genesis], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Genesis") + } + + var r0 *api.Response[*v1.Genesis] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.GenesisOpts) (*api.Response[*v1.Genesis], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.GenesisOpts) *api.Response[*v1.Genesis]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[*v1.Genesis]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.GenesisOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GenesisDomain provides a mock function with given fields: ctx, domainType +func (_m *Client) GenesisDomain(ctx context.Context, domainType phase0.DomainType) (phase0.Domain, error) { + ret := _m.Called(ctx, domainType) + + if len(ret) == 0 { + panic("no return value specified for GenesisDomain") + } + + var r0 phase0.Domain + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, phase0.DomainType) (phase0.Domain, error)); ok { + return rf(ctx, domainType) + } + if rf, ok := ret.Get(0).(func(context.Context, phase0.DomainType) phase0.Domain); ok { + r0 = rf(ctx, domainType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(phase0.Domain) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, phase0.DomainType) error); ok { + r1 = rf(ctx, domainType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GenesisTime provides a mock function with given fields: ctx +func (_m *Client) GenesisTime(ctx context.Context) (time.Time, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GenesisTime") + } + + var r0 time.Time + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (time.Time, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) time.Time); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(time.Time) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IsActive provides a mock function with given fields: +func (_m *Client) IsActive() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for IsActive") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// IsSynced provides a mock function with given fields: +func (_m *Client) IsSynced() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for IsSynced") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Name provides a mock function with given fields: +func (_m *Client) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// NodePeerCount provides a mock function with given fields: ctx +func (_m *Client) NodePeerCount(ctx context.Context) (int, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for NodePeerCount") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (int, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) int); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NodeSyncing provides a mock function with given fields: ctx, opts +func (_m *Client) NodeSyncing(ctx context.Context, opts *api.NodeSyncingOpts) (*api.Response[*v1.SyncState], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for NodeSyncing") + } + + var r0 *api.Response[*v1.SyncState] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.NodeSyncingOpts) (*api.Response[*v1.SyncState], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.NodeSyncingOpts) *api.Response[*v1.SyncState]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[*v1.SyncState]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.NodeSyncingOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NodeVersion provides a mock function with given fields: ctx, opts +func (_m *Client) NodeVersion(ctx context.Context, opts *api.NodeVersionOpts) (*api.Response[string], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for NodeVersion") + } + + var r0 *api.Response[string] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.NodeVersionOpts) (*api.Response[string], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.NodeVersionOpts) *api.Response[string]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[string]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.NodeVersionOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Proposal provides a mock function with given fields: ctx, opts +func (_m *Client) Proposal(ctx context.Context, opts *api.ProposalOpts) (*api.Response[*api.VersionedProposal], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Proposal") + } + + var r0 *api.Response[*api.VersionedProposal] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.ProposalOpts) (*api.Response[*api.VersionedProposal], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.ProposalOpts) *api.Response[*api.VersionedProposal]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[*api.VersionedProposal]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.ProposalOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ProposerConfig provides a mock function with given fields: ctx +func (_m *Client) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConfigResponse, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ProposerConfig") + } + + var r0 *eth2exp.ProposerConfigResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*eth2exp.ProposerConfigResponse, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *eth2exp.ProposerConfigResponse); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*eth2exp.ProposerConfigResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ProposerDuties provides a mock function with given fields: ctx, opts +func (_m *Client) ProposerDuties(ctx context.Context, opts *api.ProposerDutiesOpts) (*api.Response[[]*v1.ProposerDuty], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for ProposerDuties") + } + + var r0 *api.Response[[]*v1.ProposerDuty] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.ProposerDutiesOpts) (*api.Response[[]*v1.ProposerDuty], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.ProposerDutiesOpts) *api.Response[[]*v1.ProposerDuty]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[[]*v1.ProposerDuty]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.ProposerDutiesOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetForkVersion provides a mock function with given fields: forkVersion +func (_m *Client) SetForkVersion(forkVersion [4]byte) { + _m.Called(forkVersion) +} + +// SetValidatorCache provides a mock function with given fields: _a0 +func (_m *Client) SetValidatorCache(_a0 func(context.Context) (eth2wrap.ActiveValidators, error)) { + _m.Called(_a0) +} + +// SignedBeaconBlock provides a mock function with given fields: ctx, opts +func (_m *Client) SignedBeaconBlock(ctx context.Context, opts *api.SignedBeaconBlockOpts) (*api.Response[*spec.VersionedSignedBeaconBlock], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for SignedBeaconBlock") + } + + var r0 *api.Response[*spec.VersionedSignedBeaconBlock] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.SignedBeaconBlockOpts) (*api.Response[*spec.VersionedSignedBeaconBlock], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.SignedBeaconBlockOpts) *api.Response[*spec.VersionedSignedBeaconBlock]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[*spec.VersionedSignedBeaconBlock]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.SignedBeaconBlockOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SlotDuration provides a mock function with given fields: ctx +func (_m *Client) SlotDuration(ctx context.Context) (time.Duration, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for SlotDuration") + } + + var r0 time.Duration + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (time.Duration, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) time.Duration); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(time.Duration) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SlotsPerEpoch provides a mock function with given fields: ctx +func (_m *Client) SlotsPerEpoch(ctx context.Context) (uint64, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for SlotsPerEpoch") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Spec provides a mock function with given fields: ctx, opts +func (_m *Client) Spec(ctx context.Context, opts *api.SpecOpts) (*api.Response[map[string]interface{}], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Spec") + } + + var r0 *api.Response[map[string]interface{}] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.SpecOpts) (*api.Response[map[string]interface{}], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.SpecOpts) *api.Response[map[string]interface{}]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[map[string]interface{}]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.SpecOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SubmitAggregateAttestations provides a mock function with given fields: ctx, aggregateAndProofs +func (_m *Client) SubmitAggregateAttestations(ctx context.Context, aggregateAndProofs []*phase0.SignedAggregateAndProof) error { + ret := _m.Called(ctx, aggregateAndProofs) + + if len(ret) == 0 { + panic("no return value specified for SubmitAggregateAttestations") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []*phase0.SignedAggregateAndProof) error); ok { + r0 = rf(ctx, aggregateAndProofs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubmitAttestations provides a mock function with given fields: ctx, attestations +func (_m *Client) SubmitAttestations(ctx context.Context, attestations []*phase0.Attestation) error { + ret := _m.Called(ctx, attestations) + + if len(ret) == 0 { + panic("no return value specified for SubmitAttestations") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []*phase0.Attestation) error); ok { + r0 = rf(ctx, attestations) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubmitBeaconCommitteeSubscriptions provides a mock function with given fields: ctx, subscriptions +func (_m *Client) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscriptions []*v1.BeaconCommitteeSubscription) error { + ret := _m.Called(ctx, subscriptions) + + if len(ret) == 0 { + panic("no return value specified for SubmitBeaconCommitteeSubscriptions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []*v1.BeaconCommitteeSubscription) error); ok { + r0 = rf(ctx, subscriptions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubmitBlindedProposal provides a mock function with given fields: ctx, opts +func (_m *Client) SubmitBlindedProposal(ctx context.Context, opts *api.SubmitBlindedProposalOpts) error { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for SubmitBlindedProposal") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *api.SubmitBlindedProposalOpts) error); ok { + r0 = rf(ctx, opts) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubmitProposal provides a mock function with given fields: ctx, opts +func (_m *Client) SubmitProposal(ctx context.Context, opts *api.SubmitProposalOpts) error { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for SubmitProposal") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *api.SubmitProposalOpts) error); ok { + r0 = rf(ctx, opts) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubmitProposalPreparations provides a mock function with given fields: ctx, preparations +func (_m *Client) SubmitProposalPreparations(ctx context.Context, preparations []*v1.ProposalPreparation) error { + ret := _m.Called(ctx, preparations) + + if len(ret) == 0 { + panic("no return value specified for SubmitProposalPreparations") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []*v1.ProposalPreparation) error); ok { + r0 = rf(ctx, preparations) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubmitSyncCommitteeContributions provides a mock function with given fields: ctx, contributionAndProofs +func (_m *Client) SubmitSyncCommitteeContributions(ctx context.Context, contributionAndProofs []*altair.SignedContributionAndProof) error { + ret := _m.Called(ctx, contributionAndProofs) + + if len(ret) == 0 { + panic("no return value specified for SubmitSyncCommitteeContributions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []*altair.SignedContributionAndProof) error); ok { + r0 = rf(ctx, contributionAndProofs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubmitSyncCommitteeMessages provides a mock function with given fields: ctx, messages +func (_m *Client) SubmitSyncCommitteeMessages(ctx context.Context, messages []*altair.SyncCommitteeMessage) error { + ret := _m.Called(ctx, messages) + + if len(ret) == 0 { + panic("no return value specified for SubmitSyncCommitteeMessages") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []*altair.SyncCommitteeMessage) error); ok { + r0 = rf(ctx, messages) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubmitSyncCommitteeSubscriptions provides a mock function with given fields: ctx, subscriptions +func (_m *Client) SubmitSyncCommitteeSubscriptions(ctx context.Context, subscriptions []*v1.SyncCommitteeSubscription) error { + ret := _m.Called(ctx, subscriptions) + + if len(ret) == 0 { + panic("no return value specified for SubmitSyncCommitteeSubscriptions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []*v1.SyncCommitteeSubscription) error); ok { + r0 = rf(ctx, subscriptions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubmitValidatorRegistrations provides a mock function with given fields: ctx, registrations +func (_m *Client) SubmitValidatorRegistrations(ctx context.Context, registrations []*api.VersionedSignedValidatorRegistration) error { + ret := _m.Called(ctx, registrations) + + if len(ret) == 0 { + panic("no return value specified for SubmitValidatorRegistrations") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []*api.VersionedSignedValidatorRegistration) error); ok { + r0 = rf(ctx, registrations) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubmitVoluntaryExit provides a mock function with given fields: ctx, voluntaryExit +func (_m *Client) SubmitVoluntaryExit(ctx context.Context, voluntaryExit *phase0.SignedVoluntaryExit) error { + ret := _m.Called(ctx, voluntaryExit) + + if len(ret) == 0 { + panic("no return value specified for SubmitVoluntaryExit") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *phase0.SignedVoluntaryExit) error); ok { + r0 = rf(ctx, voluntaryExit) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SyncCommitteeContribution provides a mock function with given fields: ctx, opts +func (_m *Client) SyncCommitteeContribution(ctx context.Context, opts *api.SyncCommitteeContributionOpts) (*api.Response[*altair.SyncCommitteeContribution], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for SyncCommitteeContribution") + } + + var r0 *api.Response[*altair.SyncCommitteeContribution] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.SyncCommitteeContributionOpts) (*api.Response[*altair.SyncCommitteeContribution], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.SyncCommitteeContributionOpts) *api.Response[*altair.SyncCommitteeContribution]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[*altair.SyncCommitteeContribution]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.SyncCommitteeContributionOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SyncCommitteeDuties provides a mock function with given fields: ctx, opts +func (_m *Client) SyncCommitteeDuties(ctx context.Context, opts *api.SyncCommitteeDutiesOpts) (*api.Response[[]*v1.SyncCommitteeDuty], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for SyncCommitteeDuties") + } + + var r0 *api.Response[[]*v1.SyncCommitteeDuty] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.SyncCommitteeDutiesOpts) (*api.Response[[]*v1.SyncCommitteeDuty], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.SyncCommitteeDutiesOpts) *api.Response[[]*v1.SyncCommitteeDuty]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[[]*v1.SyncCommitteeDuty]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.SyncCommitteeDutiesOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Validators provides a mock function with given fields: ctx, opts +func (_m *Client) Validators(ctx context.Context, opts *api.ValidatorsOpts) (*api.Response[map[phase0.ValidatorIndex]*v1.Validator], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for Validators") + } + + var r0 *api.Response[map[phase0.ValidatorIndex]*v1.Validator] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.ValidatorsOpts) (*api.Response[map[phase0.ValidatorIndex]*v1.Validator], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.ValidatorsOpts) *api.Response[map[phase0.ValidatorIndex]*v1.Validator]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[map[phase0.ValidatorIndex]*v1.Validator]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.ValidatorsOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewClient(t interface { + mock.TestingT + Cleanup(func()) +}) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/app/eth2wrap/multi.go b/app/eth2wrap/multi.go new file mode 100644 index 000000000..795899270 --- /dev/null +++ b/app/eth2wrap/multi.go @@ -0,0 +1,188 @@ +// Copyright © 2022-2024 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package eth2wrap + +import ( + "context" + + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" + + "github.com/obolnetwork/charon/eth2util/eth2exp" +) + +// NewMultiForT creates a new mutil client for testing. +func NewMultiForT(clients []Client) Client { + return &multi{ + clients: clients, + selector: newBestSelector(bestPeriod), + } +} + +func newMulti(clients []Client) Client { + return multi{ + clients: clients, + selector: newBestSelector(bestPeriod), + } +} + +// multi implements Client by wrapping multiple clients, calling them in parallel +// and returning the first successful response. +// It also adds prometheus metrics and error wrapping. +// It also implements a best client selector. +type multi struct { + clients []Client + selector *bestSelector +} + +func (m multi) SetForkVersion(forkVersion [4]byte) { + for _, cl := range m.clients { + cl.SetForkVersion(forkVersion) + } +} + +func (multi) Name() string { + return "eth2wrap.multi" +} + +func (m multi) Address() string { + address, ok := m.selector.BestAddress() + if !ok { + return m.clients[0].Address() + } + + return address +} + +func (m multi) IsActive() bool { + for _, cl := range m.clients { + if cl.IsActive() { + return true + } + } + + return false +} + +func (m multi) IsSynced() bool { + for _, cl := range m.clients { + if cl.IsSynced() { + return true + } + } + + return false +} + +func (m multi) SetValidatorCache(valCache func(context.Context) (ActiveValidators, error)) { + for _, cl := range m.clients { + cl.SetValidatorCache(valCache) + } +} + +func (m multi) ActiveValidators(ctx context.Context) (ActiveValidators, error) { + const label = "active_validators" + // No latency since this is a cached endpoint. + + res0, err := provide(ctx, m.clients, + func(ctx context.Context, cl Client) (ActiveValidators, error) { + return cl.ActiveValidators(ctx) + }, + nil, nil, + ) + if err != nil { + incError(label) + err = wrapError(ctx, err, label) + } + + return res0, err +} + +func (m multi) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConfigResponse, error) { + const label = "proposer_config" + defer latency(label)() + + res0, err := provide(ctx, m.clients, + func(ctx context.Context, cl Client) (*eth2exp.ProposerConfigResponse, error) { + return cl.ProposerConfig(ctx) + }, + nil, m.selector, + ) + if err != nil { + incError(label) + err = wrapError(ctx, err, label) + } + + return res0, err +} + +func (m multi) AggregateBeaconCommitteeSelections(ctx context.Context, selections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) { + const label = "aggregate_beacon_committee_selections" + defer latency(label)() + + res0, err := provide(ctx, m.clients, + func(ctx context.Context, cl Client) ([]*eth2exp.BeaconCommitteeSelection, error) { + return cl.AggregateBeaconCommitteeSelections(ctx, selections) + }, + nil, m.selector, + ) + if err != nil { + incError(label) + err = wrapError(ctx, err, label) + } + + return res0, err +} + +func (m multi) AggregateSyncCommitteeSelections(ctx context.Context, selections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) { + const label = "aggregate_sync_committee_selections" + defer latency(label)() + + res, err := provide(ctx, m.clients, + func(ctx context.Context, cl Client) ([]*eth2exp.SyncCommitteeSelection, error) { + return cl.AggregateSyncCommitteeSelections(ctx, selections) + }, + nil, m.selector, + ) + if err != nil { + incError(label) + err = wrapError(ctx, err, label) + } + + return res, err +} + +func (m multi) BlockAttestations(ctx context.Context, stateID string) ([]*eth2p0.Attestation, error) { + const label = "block_attestations" + defer latency(label)() + + res, err := provide(ctx, m.clients, + func(ctx context.Context, cl Client) ([]*eth2p0.Attestation, error) { + return cl.BlockAttestations(ctx, stateID) + }, + nil, m.selector, + ) + if err != nil { + incError(label) + err = wrapError(ctx, err, label) + } + + return res, err +} + +func (m multi) NodePeerCount(ctx context.Context) (int, error) { + const label = "node_peer_count" + defer latency(label)() + + res, err := provide(ctx, m.clients, + func(ctx context.Context, cl Client) (int, error) { + return cl.NodePeerCount(ctx) + }, + nil, m.selector, + ) + if err != nil { + incError(label) + err = wrapError(ctx, err, label) + } + + return res, err +} diff --git a/app/eth2wrap/multi_test.go b/app/eth2wrap/multi_test.go new file mode 100644 index 000000000..51ab2eb3a --- /dev/null +++ b/app/eth2wrap/multi_test.go @@ -0,0 +1,186 @@ +// Copyright © 2022-2024 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package eth2wrap_test + +import ( + "context" + "errors" + "testing" + + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/obolnetwork/charon/app/eth2wrap" + "github.com/obolnetwork/charon/app/eth2wrap/mocks" + "github.com/obolnetwork/charon/eth2util/eth2exp" +) + +func TestMulti_Name(t *testing.T) { + client := mocks.NewClient(t) + + m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}) + + require.Equal(t, "eth2wrap.multi", m.Name()) +} + +func TestMulti_Address(t *testing.T) { + client := mocks.NewClient(t) + client.On("Address").Return("test").Once() + + m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}) + + require.Equal(t, "test", m.Address()) +} + +func TestMulti_IsActive(t *testing.T) { + client1 := mocks.NewClient(t) + client1.On("IsActive").Return(false).Once() + client2 := mocks.NewClient(t) + client2.On("IsActive").Return(true).Once() + + m := eth2wrap.NewMultiForT([]eth2wrap.Client{client1, client2}) + + require.True(t, m.IsActive()) +} + +func TestMulti_IsSynced(t *testing.T) { + client1 := mocks.NewClient(t) + client1.On("IsSynced").Return(false).Once() + client2 := mocks.NewClient(t) + client2.On("IsSynced").Return(true).Once() + + m := eth2wrap.NewMultiForT([]eth2wrap.Client{client1, client2}) + + require.True(t, m.IsSynced()) +} + +func TestMulti_NodePeerCount(t *testing.T) { + client := mocks.NewClient(t) + client.On("Address").Return("test").Once() + client.On("NodePeerCount", mock.Anything).Return(5, nil).Once() + + m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}) + + c, err := m.NodePeerCount(context.Background()) + require.NoError(t, err) + require.Equal(t, 5, c) + + expectedErr := errors.New("boo") + client.On("NodePeerCount", mock.Anything).Return(0, expectedErr).Once() + _, err = m.NodePeerCount(context.Background()) + require.ErrorIs(t, err, expectedErr) +} + +func TestMulti_BlockAttestations(t *testing.T) { + ctx := context.Background() + atts := make([]*eth2p0.Attestation, 3) + + client := mocks.NewClient(t) + client.On("Address").Return("test").Once() + client.On("BlockAttestations", mock.Anything, "state").Return(atts, nil).Once() + + m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}) + + atts2, err := m.BlockAttestations(ctx, "state") + require.NoError(t, err) + require.Equal(t, atts, atts2) + + expectedErr := errors.New("boo") + client.On("BlockAttestations", mock.Anything, "state").Return(nil, expectedErr).Once() + _, err = m.BlockAttestations(ctx, "state") + require.ErrorIs(t, err, expectedErr) +} + +func TestMulti_AggregateSyncCommitteeSelections(t *testing.T) { + ctx := context.Background() + partsel := make([]*eth2exp.SyncCommitteeSelection, 1) + selections := make([]*eth2exp.SyncCommitteeSelection, 3) + + client := mocks.NewClient(t) + client.On("Address").Return("test").Once() + client.On("AggregateSyncCommitteeSelections", mock.Anything, partsel).Return(selections, nil).Once() + + m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}) + + selections2, err := m.AggregateSyncCommitteeSelections(ctx, partsel) + require.NoError(t, err) + require.Equal(t, selections, selections2) + + expectedErr := errors.New("boo") + client.On("AggregateSyncCommitteeSelections", mock.Anything, partsel).Return(nil, expectedErr).Once() + _, err = m.AggregateSyncCommitteeSelections(ctx, partsel) + require.ErrorIs(t, err, expectedErr) +} + +func TestMulti_AggregateBeaconCommitteeSelections(t *testing.T) { + ctx := context.Background() + partsel := make([]*eth2exp.BeaconCommitteeSelection, 1) + selections := make([]*eth2exp.BeaconCommitteeSelection, 3) + + client := mocks.NewClient(t) + client.On("Address").Return("test").Once() + client.On("AggregateBeaconCommitteeSelections", mock.Anything, partsel).Return(selections, nil).Once() + + m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}) + + selections2, err := m.AggregateBeaconCommitteeSelections(ctx, partsel) + require.NoError(t, err) + require.Equal(t, selections, selections2) + + expectedErr := errors.New("boo") + client.On("AggregateBeaconCommitteeSelections", mock.Anything, partsel).Return(nil, expectedErr).Once() + _, err = m.AggregateBeaconCommitteeSelections(ctx, partsel) + require.ErrorIs(t, err, expectedErr) +} + +func TestMulti_ProposerConfig(t *testing.T) { + ctx := context.Background() + resp := ð2exp.ProposerConfigResponse{} + + client := mocks.NewClient(t) + client.On("Address").Return("test").Once() + client.On("ProposerConfig", mock.Anything).Return(resp, nil).Once() + + m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}) + + resp2, err := m.ProposerConfig(ctx) + require.NoError(t, err) + require.Equal(t, resp, resp2) + + expectedErr := errors.New("boo") + client.On("ProposerConfig", mock.Anything).Return(nil, expectedErr).Once() + _, err = m.ProposerConfig(ctx) + require.ErrorIs(t, err, expectedErr) +} + +func TestMulti_ActiveValidators(t *testing.T) { + ctx := context.Background() + vals := make(eth2wrap.ActiveValidators) + + client := mocks.NewClient(t) + client.On("ActiveValidators", mock.Anything).Return(vals, nil).Once() + + m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}) + + vals2, err := m.ActiveValidators(ctx) + require.NoError(t, err) + require.Equal(t, vals, vals2) + + expectedErr := errors.New("boo") + client.On("ActiveValidators", mock.Anything).Return(nil, expectedErr).Once() + _, err = m.ActiveValidators(ctx) + require.ErrorIs(t, err, expectedErr) +} + +func TestMulti_SetValidatorCache(t *testing.T) { + valCache := func(context.Context) (eth2wrap.ActiveValidators, error) { + return nil, nil + } + + client := mocks.NewClient(t) + client.On("SetValidatorCache", mock.Anything).Once() + + m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}) + m.SetValidatorCache(valCache) +} diff --git a/app/eth2wrap/synthproposer.go b/app/eth2wrap/synthproposer.go index d08137047..e0f266ce0 100644 --- a/app/eth2wrap/synthproposer.go +++ b/app/eth2wrap/synthproposer.go @@ -13,13 +13,9 @@ import ( eth2client "github.com/attestantio/go-eth2-client" eth2api "github.com/attestantio/go-eth2-client/api" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" - eth2bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" - eth2capella "github.com/attestantio/go-eth2-client/api/v1/capella" eth2deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" eth2spec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/bellatrix" - "github.com/attestantio/go-eth2-client/spec/capella" - "github.com/attestantio/go-eth2-client/spec/deneb" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" shuffle "github.com/protolambda/eth2-shuffle" @@ -113,33 +109,6 @@ func (h *synthWrapper) Proposal(ctx context.Context, opts *eth2api.ProposalOpts) return wrapResponse(proposal), nil } -// BlindedProposal returns an unsigned blinded beacon block proposal, possibly marked as synthetic. -func (h *synthWrapper) BlindedProposal(ctx context.Context, opts *eth2api.BlindedProposalOpts) (*eth2api.Response[*eth2api.VersionedBlindedProposal], error) { - vIdx, ok, err := h.synthProposerCache.SyntheticVIdx(ctx, h.Client, opts.Slot) - if err != nil { - return nil, err - } else if !ok { - resp, err := h.Client.BlindedProposal(ctx, opts) - if err != nil { - return nil, errors.Wrap(err, "propose blinded beacon block") - } - - return resp, nil - } - - proposal, err := h.syntheticProposal(ctx, opts.Slot, vIdx) - if err != nil { - return nil, err - } - - synthBlindedProposal, err := blindedProposal(proposal) - if err != nil { - return nil, err - } - - return wrapResponse(synthBlindedProposal), nil -} - // syntheticProposal returns a synthetic unsigned beacon block to propose. func (h *synthWrapper) syntheticProposal(ctx context.Context, slot eth2p0.Slot, vIdx eth2p0.ValidatorIndex) (*eth2api.VersionedProposal, error) { var signedBlock *eth2spec.VersionedSignedBeaconBlock @@ -219,23 +188,23 @@ func fraction(transactions []bellatrix.Transaction) []bellatrix.Transaction { } // SubmitBlindedProposal submits a blinded beacon block proposal or swallows it if marked as synthetic. -func (h *synthWrapper) SubmitBlindedProposal(ctx context.Context, proposal *eth2api.VersionedSignedBlindedProposal) error { - if IsSyntheticBlindedBlock(proposal) { +func (h *synthWrapper) SubmitBlindedProposal(ctx context.Context, opts *eth2api.SubmitBlindedProposalOpts) error { + if IsSyntheticBlindedBlock(opts.Proposal) { log.Debug(ctx, "Synthetic blinded beacon proposal swallowed") return nil } - return h.Client.SubmitBlindedProposal(ctx, proposal) + return h.Client.SubmitBlindedProposal(ctx, opts) } // SubmitProposal submits a beacon block or swallows it if marked as synthetic. -func (h *synthWrapper) SubmitProposal(ctx context.Context, proposal *eth2api.VersionedSignedProposal) error { - if IsSyntheticProposal(proposal) { +func (h *synthWrapper) SubmitProposal(ctx context.Context, opts *eth2api.SubmitProposalOpts) error { + if IsSyntheticProposal(opts.Proposal) { log.Debug(ctx, "Synthetic beacon block swallowed") return nil } - return h.Client.SubmitProposal(ctx, proposal) + return h.Client.SubmitProposal(ctx, opts) } // GetSyntheticGraffiti returns the graffiti used to mark synthetic blocks. @@ -448,134 +417,6 @@ func getStandardHashFn() shuffle.HashFn { return hashFn } -// blindedProposal converts a normal block into a blinded block proposal. -func blindedProposal(proposal *eth2api.VersionedProposal) (*eth2api.VersionedBlindedProposal, error) { - var resp *eth2api.VersionedBlindedProposal - // Blinded blocks are only available from bellatrix. - switch proposal.Version { - case eth2spec.DataVersionBellatrix: - resp = ð2api.VersionedBlindedProposal{ - Version: proposal.Version, - Bellatrix: ð2bellatrix.BlindedBeaconBlock{ - Slot: proposal.Bellatrix.Slot, - ProposerIndex: proposal.Bellatrix.ProposerIndex, - ParentRoot: proposal.Bellatrix.ParentRoot, - StateRoot: proposal.Bellatrix.StateRoot, - Body: ð2bellatrix.BlindedBeaconBlockBody{ - RANDAOReveal: proposal.Bellatrix.Body.RANDAOReveal, - ETH1Data: proposal.Bellatrix.Body.ETH1Data, - Graffiti: proposal.Bellatrix.Body.Graffiti, - ProposerSlashings: proposal.Bellatrix.Body.ProposerSlashings, - AttesterSlashings: proposal.Bellatrix.Body.AttesterSlashings, - Attestations: proposal.Bellatrix.Body.Attestations, - Deposits: proposal.Bellatrix.Body.Deposits, - VoluntaryExits: proposal.Bellatrix.Body.VoluntaryExits, - SyncAggregate: proposal.Bellatrix.Body.SyncAggregate, - ExecutionPayloadHeader: &bellatrix.ExecutionPayloadHeader{ - ParentHash: proposal.Bellatrix.Body.ExecutionPayload.ParentHash, - FeeRecipient: proposal.Bellatrix.Body.ExecutionPayload.FeeRecipient, - StateRoot: proposal.Bellatrix.Body.ExecutionPayload.StateRoot, - ReceiptsRoot: proposal.Bellatrix.Body.ExecutionPayload.ReceiptsRoot, - LogsBloom: proposal.Bellatrix.Body.ExecutionPayload.LogsBloom, - PrevRandao: proposal.Bellatrix.Body.ExecutionPayload.PrevRandao, - BlockNumber: proposal.Bellatrix.Body.ExecutionPayload.BlockNumber, - GasLimit: proposal.Bellatrix.Body.ExecutionPayload.GasLimit, - GasUsed: proposal.Bellatrix.Body.ExecutionPayload.GasUsed, - Timestamp: proposal.Bellatrix.Body.ExecutionPayload.Timestamp, - ExtraData: proposal.Bellatrix.Body.ExecutionPayload.ExtraData, - BaseFeePerGas: proposal.Bellatrix.Body.ExecutionPayload.BaseFeePerGas, - BlockHash: proposal.Bellatrix.Body.ExecutionPayload.BlockHash, - TransactionsRoot: eth2p0.Root{}, // Use empty root. - }, - }, - }, - } - case eth2spec.DataVersionCapella: - resp = ð2api.VersionedBlindedProposal{ - Version: proposal.Version, - Capella: ð2capella.BlindedBeaconBlock{ - Slot: proposal.Capella.Slot, - ProposerIndex: proposal.Capella.ProposerIndex, - ParentRoot: proposal.Capella.ParentRoot, - StateRoot: proposal.Capella.StateRoot, - Body: ð2capella.BlindedBeaconBlockBody{ - RANDAOReveal: proposal.Capella.Body.RANDAOReveal, - ETH1Data: proposal.Capella.Body.ETH1Data, - Graffiti: proposal.Capella.Body.Graffiti, - ProposerSlashings: proposal.Capella.Body.ProposerSlashings, - AttesterSlashings: proposal.Capella.Body.AttesterSlashings, - Attestations: proposal.Capella.Body.Attestations, - Deposits: proposal.Capella.Body.Deposits, - VoluntaryExits: proposal.Capella.Body.VoluntaryExits, - SyncAggregate: proposal.Capella.Body.SyncAggregate, - ExecutionPayloadHeader: &capella.ExecutionPayloadHeader{ - ParentHash: proposal.Capella.Body.ExecutionPayload.ParentHash, - FeeRecipient: proposal.Capella.Body.ExecutionPayload.FeeRecipient, - StateRoot: proposal.Capella.Body.ExecutionPayload.StateRoot, - ReceiptsRoot: proposal.Capella.Body.ExecutionPayload.ReceiptsRoot, - LogsBloom: proposal.Capella.Body.ExecutionPayload.LogsBloom, - PrevRandao: proposal.Capella.Body.ExecutionPayload.PrevRandao, - BlockNumber: proposal.Capella.Body.ExecutionPayload.BlockNumber, - GasLimit: proposal.Capella.Body.ExecutionPayload.GasLimit, - GasUsed: proposal.Capella.Body.ExecutionPayload.GasUsed, - Timestamp: proposal.Capella.Body.ExecutionPayload.Timestamp, - ExtraData: proposal.Capella.Body.ExecutionPayload.ExtraData, - BaseFeePerGas: proposal.Capella.Body.ExecutionPayload.BaseFeePerGas, - BlockHash: proposal.Capella.Body.ExecutionPayload.BlockHash, - TransactionsRoot: eth2p0.Root{}, // Use empty root. - }, - }, - }, - } - case eth2spec.DataVersionDeneb: - resp = ð2api.VersionedBlindedProposal{ - Version: proposal.Version, - Deneb: ð2deneb.BlindedBeaconBlock{ - Slot: proposal.Deneb.Block.Slot, - ProposerIndex: proposal.Deneb.Block.ProposerIndex, - ParentRoot: proposal.Deneb.Block.ParentRoot, - StateRoot: proposal.Deneb.Block.StateRoot, - Body: ð2deneb.BlindedBeaconBlockBody{ - RANDAOReveal: proposal.Deneb.Block.Body.RANDAOReveal, - ETH1Data: proposal.Deneb.Block.Body.ETH1Data, - Graffiti: proposal.Deneb.Block.Body.Graffiti, - ProposerSlashings: proposal.Deneb.Block.Body.ProposerSlashings, - AttesterSlashings: proposal.Deneb.Block.Body.AttesterSlashings, - Attestations: proposal.Deneb.Block.Body.Attestations, - Deposits: proposal.Deneb.Block.Body.Deposits, - VoluntaryExits: proposal.Deneb.Block.Body.VoluntaryExits, - SyncAggregate: proposal.Deneb.Block.Body.SyncAggregate, - ExecutionPayloadHeader: &deneb.ExecutionPayloadHeader{ - ParentHash: proposal.Deneb.Block.Body.ExecutionPayload.ParentHash, - FeeRecipient: proposal.Deneb.Block.Body.ExecutionPayload.FeeRecipient, - StateRoot: proposal.Deneb.Block.Body.ExecutionPayload.StateRoot, - ReceiptsRoot: proposal.Deneb.Block.Body.ExecutionPayload.ReceiptsRoot, - LogsBloom: proposal.Deneb.Block.Body.ExecutionPayload.LogsBloom, - PrevRandao: proposal.Deneb.Block.Body.ExecutionPayload.PrevRandao, - BlockNumber: proposal.Deneb.Block.Body.ExecutionPayload.BlockNumber, - GasLimit: proposal.Deneb.Block.Body.ExecutionPayload.GasLimit, - GasUsed: proposal.Deneb.Block.Body.ExecutionPayload.GasUsed, - Timestamp: proposal.Deneb.Block.Body.ExecutionPayload.Timestamp, - ExtraData: proposal.Deneb.Block.Body.ExecutionPayload.ExtraData, - BaseFeePerGas: proposal.Deneb.Block.Body.ExecutionPayload.BaseFeePerGas, - BlockHash: proposal.Deneb.Block.Body.ExecutionPayload.BlockHash, - TransactionsRoot: eth2p0.Root{}, - WithdrawalsRoot: eth2p0.Root{}, - BlobGasUsed: proposal.Deneb.Block.Body.ExecutionPayload.BlobGasUsed, - ExcessBlobGas: proposal.Deneb.Block.Body.ExecutionPayload.ExcessBlobGas, - }, - BLSToExecutionChanges: proposal.Deneb.Block.Body.BLSToExecutionChanges, - BlobKZGCommitments: proposal.Deneb.Block.Body.BlobKZGCommitments, - }, - }, - } - default: - return nil, errors.New("unsupported blinded proposal version") - } - - return resp, nil -} - // wrapResponse wraps the provided data into an API Response and returns the response. func wrapResponse[T any](data T) *eth2api.Response[T] { return ð2api.Response[T]{Data: data} diff --git a/app/eth2wrap/synthproposer_test.go b/app/eth2wrap/synthproposer_test.go index aeb9a65b0..f7971c89b 100644 --- a/app/eth2wrap/synthproposer_test.go +++ b/app/eth2wrap/synthproposer_test.go @@ -37,8 +37,15 @@ func TestSynthProposer(t *testing.T) { bmock, err := beaconmock.New(beaconmock.WithValidatorSet(set), beaconmock.WithSlotsPerEpoch(slotsPerEpoch)) require.NoError(t, err) - bmock.SubmitProposalFunc = func(ctx context.Context, proposal *eth2api.VersionedSignedProposal) error { - require.Equal(t, realBlockSlot, proposal.Capella.Message.Slot) + bmock.SubmitProposalFunc = func(ctx context.Context, opts *eth2api.SubmitProposalOpts) error { + require.Equal(t, realBlockSlot, opts.Proposal.Capella.Message.Slot) + close(done) + + return nil + } + + bmock.SubmitBlindedProposalFunc = func(ctx context.Context, opts *eth2api.SubmitBlindedProposalOpts) error { + require.Equal(t, realBlockSlot, opts.Proposal.Capella.Message.Slot) close(done) return nil @@ -102,62 +109,59 @@ func TestSynthProposer(t *testing.T) { // Submit blocks for _, duty := range duties { + var bbf uint64 = 100 var graff [32]byte copy(graff[:], "test") opts1 := ð2api.ProposalOpts{ - Slot: duty.Slot, - RandaoReveal: testutil.RandomEth2Signature(), - Graffiti: graff, + Slot: duty.Slot, + RandaoReveal: testutil.RandomEth2Signature(), + Graffiti: graff, + BuilderBoostFactor: &bbf, } resp, err := eth2Cl.Proposal(ctx, opts1) require.NoError(t, err) - block := resp.Data - - if duty.Slot == realBlockSlot { - require.NotContains(t, string(block.Capella.Body.Graffiti[:]), "DO NOT SUBMIT") - require.NotEqual(t, feeRecipient, block.Capella.Body.ExecutionPayload.FeeRecipient) - } else { - require.Contains(t, string(block.Capella.Body.Graffiti[:]), "DO NOT SUBMIT") - require.Equal(t, feeRecipient, block.Capella.Body.ExecutionPayload.FeeRecipient) - - continue - } - require.Equal(t, eth2spec.DataVersionCapella, block.Version) - - signed := testutil.RandomCapellaVersionedSignedProposal() - signed.Capella.Message = block.Capella - err = eth2Cl.SubmitProposal(ctx, signed) - require.NoError(t, err) - } - // Submit blinded blocks - for _, duty := range duties { - var graff [32]byte - copy(graff[:], "test") - opts := ð2api.BlindedProposalOpts{ - Slot: duty.Slot, - RandaoReveal: testutil.RandomEth2Signature(), - Graffiti: graff, - } - resp, err := eth2Cl.BlindedProposal(ctx, opts) - require.NoError(t, err) - block := resp.Data - if duty.Slot == realBlockSlot { - require.NotContains(t, string(block.Capella.Body.Graffiti[:]), "DO NOT SUBMIT") - require.NotEqual(t, feeRecipient, block.Capella.Body.ExecutionPayloadHeader.FeeRecipient) + if resp.Data.Blinded { + block := resp.Data + if duty.Slot == realBlockSlot { + require.NotContains(t, string(block.CapellaBlinded.Body.Graffiti[:]), "DO NOT SUBMIT") + require.NotEqual(t, feeRecipient, block.CapellaBlinded.Body.ExecutionPayloadHeader.FeeRecipient) + } else { + require.Equal(t, feeRecipient, block.CapellaBlinded.Body.ExecutionPayloadHeader.FeeRecipient) + } + require.Equal(t, eth2spec.DataVersionCapella, block.Version) + + signed := ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersionCapella, + Capella: ð2capella.SignedBlindedBeaconBlock{ + Message: block.CapellaBlinded, + Signature: testutil.RandomEth2Signature(), + }, + } + err = eth2Cl.SubmitBlindedProposal(ctx, ð2api.SubmitBlindedProposalOpts{ + Proposal: signed, + }) + require.NoError(t, err) } else { - require.Equal(t, feeRecipient, block.Capella.Body.ExecutionPayloadHeader.FeeRecipient) - } - require.Equal(t, eth2spec.DataVersionCapella, block.Version) - - signed := ð2api.VersionedSignedBlindedProposal{ - Version: eth2spec.DataVersionCapella, - Capella: ð2capella.SignedBlindedBeaconBlock{ - Message: block.Capella, - Signature: testutil.RandomEth2Signature(), - }, + block := resp.Data + + if duty.Slot == realBlockSlot { + require.NotContains(t, string(block.Capella.Body.Graffiti[:]), "DO NOT SUBMIT") + require.NotEqual(t, feeRecipient, block.Capella.Body.ExecutionPayload.FeeRecipient) + } else { + require.Contains(t, string(block.Capella.Body.Graffiti[:]), "DO NOT SUBMIT") + require.Equal(t, feeRecipient, block.Capella.Body.ExecutionPayload.FeeRecipient) + + continue + } + require.Equal(t, eth2spec.DataVersionCapella, block.Version) + + signed := testutil.RandomCapellaVersionedSignedProposal() + signed.Capella.Message = block.Capella + err = eth2Cl.SubmitProposal(ctx, ð2api.SubmitProposalOpts{ + Proposal: signed, + }) } - err = eth2Cl.SubmitBlindedProposal(ctx, signed) require.NoError(t, err) } diff --git a/codecov.yml b/codecov.yml index 846f9e52b..7e4f2dc28 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,4 +10,5 @@ coverage: informational: true ignore: - "**/*_test.go" + - "**/mocks/*.go" - "testutil" diff --git a/core/bcast/bcast.go b/core/bcast/bcast.go index 2b78040ac..ccf65de2b 100644 --- a/core/bcast/bcast.go +++ b/core/bcast/bcast.go @@ -78,7 +78,9 @@ func (b Broadcaster) Broadcast(ctx context.Context, duty core.Duty, set core.Sig return errors.New("invalid proposal") } - err = b.eth2Cl.SubmitProposal(ctx, &block.VersionedSignedProposal) + err = b.eth2Cl.SubmitProposal(ctx, ð2api.SubmitProposalOpts{ + Proposal: &block.VersionedSignedProposal, + }) if err == nil { log.Info(ctx, "Successfully submitted block proposal to beacon node", z.Any("delay", b.delayFunc(duty.Slot)), @@ -89,25 +91,7 @@ func (b Broadcaster) Broadcast(ctx context.Context, duty core.Duty, set core.Sig return err case core.DutyBuilderProposer: - pubkey, aggData, err := setToOne(set) - if err != nil { - return err - } - - block, ok := aggData.(core.VersionedSignedBlindedProposal) - if !ok { - return errors.New("invalid blinded proposal") - } - - err = b.eth2Cl.SubmitBlindedProposal(ctx, &block.VersionedSignedBlindedProposal) - if err == nil { - log.Info(ctx, "Successfully submitted blinded block proposal to beacon node", - z.Any("delay", b.delayFunc(duty.Slot)), - z.Any("pubkey", pubkey), - ) - } - - return err + return core.ErrDeprecatedDutyBuilderProposer case core.DutyBuilderRegistration: slot, err := firstSlotInCurrentEpoch(ctx, b.eth2Cl) diff --git a/core/bcast/bcast_test.go b/core/bcast/bcast_test.go index b49c1fb46..699cf38d7 100644 --- a/core/bcast/bcast_test.go +++ b/core/bcast/bcast_test.go @@ -32,7 +32,7 @@ func TestBroadcast(t *testing.T) { testFuncs := []func(*testing.T, *beaconmock.Mock) test{ attData, // Attestation proposalData, // BeaconBlock - blindedProposalData, // BlindedBeaconBlock + blindedProposalData, // BlindedBlock validatorRegistrationData, // ValidatorRegistration validatorExitData, // ValidatorExit aggregateAttestationData, // AggregateAttestation @@ -72,6 +72,29 @@ func TestBroadcast(t *testing.T) { } } +func TestBroadcastOtherDuties(t *testing.T) { + mock, err := beaconmock.New() + require.NoError(t, err) + + bcaster, err := bcast.New(context.Background(), mock) + require.NoError(t, err) + + err = bcaster.Broadcast(context.Background(), core.Duty{Type: core.DutyBuilderProposer}, nil) + require.ErrorIs(t, err, core.ErrDeprecatedDutyBuilderProposer) + + err = bcaster.Broadcast(context.Background(), core.Duty{Type: core.DutyRandao}, nil) + require.NoError(t, err) + + err = bcaster.Broadcast(context.Background(), core.Duty{Type: core.DutyPrepareAggregator}, nil) + require.NoError(t, err) + + err = bcaster.Broadcast(context.Background(), core.Duty{Type: core.DutyPrepareSyncContribution}, nil) + require.NoError(t, err) + + err = bcaster.Broadcast(context.Background(), core.Duty{Type: core.DutyUnknown}, nil) + require.ErrorContains(t, err, "unsupported duty type") +} + func attData(t *testing.T, mock *beaconmock.Mock) test { t.Helper() @@ -117,8 +140,8 @@ func proposalData(t *testing.T, mock *beaconmock.Mock) test { aggData := core.VersionedSignedProposal{VersionedSignedProposal: proposal1} - mock.SubmitProposalFunc = func(ctx context.Context, proposal2 *eth2api.VersionedSignedProposal) error { - require.Equal(t, proposal1, *proposal2) + mock.SubmitProposalFunc = func(ctx context.Context, opts *eth2api.SubmitProposalOpts) error { + require.Equal(t, proposal1, *opts.Proposal) close(asserted) return nil @@ -138,27 +161,27 @@ func blindedProposalData(t *testing.T, mock *beaconmock.Mock) test { asserted := make(chan struct{}) - proposal1 := eth2api.VersionedSignedBlindedProposal{ - Version: eth2spec.DataVersionBellatrix, - Capella: ð2capella.SignedBlindedBeaconBlock{ + proposal1 := eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionPhase0, + CapellaBlinded: ð2capella.SignedBlindedBeaconBlock{ Message: testutil.RandomCapellaBlindedBeaconBlock(), Signature: testutil.RandomEth2Signature(), }, } - aggData := core.VersionedSignedBlindedProposal{VersionedSignedBlindedProposal: proposal1} + aggData := core.VersionedSignedProposal{VersionedSignedProposal: proposal1} - mock.SubmitBlindedProposalFunc = func(ctx context.Context, proposal2 *eth2api.VersionedSignedBlindedProposal) error { - require.Equal(t, proposal1, *proposal2) + mock.SubmitProposalFunc = func(ctx context.Context, opts *eth2api.SubmitProposalOpts) error { + require.Equal(t, proposal1, *opts.Proposal) close(asserted) return nil } return test{ - name: "Broadcast Blinded Beacon Block Proposal", + name: "Broadcast Blinded Block Proposal", aggData: aggData, - duty: core.DutyBuilderProposer, + duty: core.DutyProposer, bcastCnt: 1, asserted: asserted, } diff --git a/core/deadline_test.go b/core/deadline_test.go index b68662e67..5ba9dc13a 100644 --- a/core/deadline_test.go +++ b/core/deadline_test.go @@ -98,7 +98,6 @@ func setupData(t *testing.T) ([]core.Duty, []core.Duty, []core.Duty, func(core.D nonExpiredDuties := []core.Duty{ core.NewProposerDuty(1), core.NewAttesterDuty(2), - core.NewBuilderProposerDuty(3), } voluntaryExits := []core.Duty{ diff --git a/core/dutydb/memory.go b/core/dutydb/memory.go index eff6d0bcb..cf74dbe9f 100644 --- a/core/dutydb/memory.go +++ b/core/dutydb/memory.go @@ -21,7 +21,6 @@ func NewMemDB(deadliner core.Deadliner) *MemDB { attDuties: make(map[attKey]*eth2p0.AttestationData), attPubKeys: make(map[pkKey]core.PubKey), attKeysBySlot: make(map[uint64][]pkKey), - builderProDuties: make(map[uint64]*eth2api.VersionedBlindedProposal), proDuties: make(map[uint64]*eth2api.VersionedProposal), aggDuties: make(map[aggKey]core.AggregatedAttestation), aggKeysBySlot: make(map[uint64][]aggKey), @@ -43,10 +42,6 @@ type MemDB struct { attKeysBySlot map[uint64][]pkKey attQueries []attQuery - // DutyBuilderProposer - builderProDuties map[uint64]*eth2api.VersionedBlindedProposal - builderProQueries []builderProQuery - // DutyProposer proDuties map[uint64]*eth2api.VersionedProposal proQueries []proQuery @@ -94,17 +89,7 @@ func (db *MemDB) Store(_ context.Context, duty core.Duty, unsignedSet core.Unsig } db.resolveProQueriesUnsafe() case core.DutyBuilderProposer: - // Sanity check max one builder proposer per slot - if len(unsignedSet) > 1 { - return errors.New("unexpected builder proposer data set length", z.Int("n", len(unsignedSet))) - } - for _, unsignedData := range unsignedSet { - err := db.storeBlindedBeaconBlockUnsafe(unsignedData) - if err != nil { - return err - } - } - db.resolveBuilderProQueriesUnsafe() + return core.ErrDeprecatedDutyBuilderProposer case core.DutyAttester: for pubkey, unsignedData := range unsignedSet { err := db.storeAttestationUnsafe(pubkey, unsignedData) @@ -179,31 +164,6 @@ func (db *MemDB) AwaitProposal(ctx context.Context, slot uint64) (*eth2api.Versi } } -// AwaitBlindedProposal implements core.DutyDB, see its godoc. -func (db *MemDB) AwaitBlindedProposal(ctx context.Context, slot uint64) (*eth2api.VersionedBlindedProposal, error) { - cancel := make(chan struct{}) - defer close(cancel) - response := make(chan *eth2api.VersionedBlindedProposal, 1) - - db.mu.Lock() - db.builderProQueries = append(db.builderProQueries, builderProQuery{ - Key: slot, - Response: response, - Cancel: cancel, - }) - db.resolveBuilderProQueriesUnsafe() - db.mu.Unlock() - - select { - case <-db.shutdown: - return nil, errors.New("dutydb shutdown") - case <-ctx.Done(): - return nil, ctx.Err() - case block := <-response: - return block, nil - } -} - // AwaitAttestation implements core.DutyDB, see its godoc. func (db *MemDB) AwaitAttestation(ctx context.Context, slot uint64, commIdx uint64) (*eth2p0.AttestationData, error) { cancel := make(chan struct{}) @@ -489,44 +449,6 @@ func (db *MemDB) storeProposalUnsafe(unsignedData core.UnsignedData) error { return nil } -// storeBlindedBeaconBlockUnsafe stores the unsigned BlindedBeaconBlock. It is unsafe since it assumes the lock is held. -func (db *MemDB) storeBlindedBeaconBlockUnsafe(unsignedData core.UnsignedData) error { - cloned, err := unsignedData.Clone() // Clone before storing. - if err != nil { - return err - } - - block, ok := cloned.(core.VersionedBlindedProposal) - if !ok { - return errors.New("invalid unsigned blinded block") - } - - slot, err := block.Slot() - if err != nil { - return err - } - - if existing, ok := db.builderProDuties[uint64(slot)]; ok { - existingRoot, err := existing.Root() - if err != nil { - return errors.Wrap(err, "blinded block root") - } - - providedRoot, err := block.Root() - if err != nil { - return errors.Wrap(err, "blinded block root") - } - - if existingRoot != providedRoot { - return errors.New("clashing blinded blocks") - } - } else { - db.builderProDuties[uint64(slot)] = &block.VersionedBlindedProposal - } - - return nil -} - // resolveAttQueriesUnsafe resolve any attQuery to a result if found. // It is unsafe since it assume that the lock is held. func (db *MemDB) resolveAttQueriesUnsafe() { @@ -590,27 +512,6 @@ func (db *MemDB) resolveAggQueriesUnsafe() { db.aggQueries = unresolved } -// resolveBuilderProQueriesUnsafe resolve any builderProQuery to a result if found. -// It is unsafe since it assume that the lock is held. -func (db *MemDB) resolveBuilderProQueriesUnsafe() { - var unresolved []builderProQuery - for _, query := range db.builderProQueries { - if cancelled(query.Cancel) { - continue // Drop cancelled queries. - } - - value, ok := db.builderProDuties[query.Key] - if !ok { - unresolved = append(unresolved, query) - continue - } - - query.Response <- value - } - - db.builderProQueries = unresolved -} - // resolveContribQueriesUnsafe resolves any contribQuery to a result if found. // It is unsafe since it assumes that the lock is held. func (db *MemDB) resolveContribQueriesUnsafe() { @@ -638,7 +539,7 @@ func (db *MemDB) deleteDutyUnsafe(duty core.Duty) error { case core.DutyProposer: delete(db.proDuties, duty.Slot) case core.DutyBuilderProposer: - delete(db.builderProDuties, duty.Slot) + return core.ErrDeprecatedDutyBuilderProposer case core.DutyAttester: for _, key := range db.attKeysBySlot[duty.Slot] { delete(db.attPubKeys, key) @@ -709,13 +610,6 @@ type aggQuery struct { Cancel <-chan struct{} } -// builderProQuery is a waiting builderProQuery with a response channel. -type builderProQuery struct { - Key uint64 - Response chan<- *eth2api.VersionedBlindedProposal - Cancel <-chan struct{} -} - // contribQuery is a waiting contribQuery with a response channel. type contribQuery struct { Key contribKey diff --git a/core/dutydb/memory_internal_test.go b/core/dutydb/memory_internal_test.go index 9101cec6a..6b8de0f27 100644 --- a/core/dutydb/memory_internal_test.go +++ b/core/dutydb/memory_internal_test.go @@ -30,9 +30,6 @@ func TestCancelledQueries(t *testing.T) { _, err = db.AwaitProposal(ctx, slot) require.ErrorContains(t, err, "shutdown") - _, err = db.AwaitBlindedProposal(ctx, slot) - require.ErrorContains(t, err, "shutdown") - _, err = db.AwaitSyncContribution(ctx, slot, 0, eth2p0.Root{}) require.ErrorContains(t, err, "shutdown") @@ -41,21 +38,18 @@ func TestCancelledQueries(t *testing.T) { require.NotEmpty(t, db.attQueries) require.NotEmpty(t, db.proQueries) require.NotEmpty(t, db.aggQueries) - require.NotEmpty(t, db.builderProQueries) // Resolve queries db.resolveAggQueriesUnsafe() db.resolveAttQueriesUnsafe() db.resolveContribQueriesUnsafe() db.resolveProQueriesUnsafe() - db.resolveBuilderProQueriesUnsafe() // Ensure all queries are gone. require.Empty(t, db.contribQueries) require.Empty(t, db.attQueries) require.Empty(t, db.proQueries) require.Empty(t, db.aggQueries) - require.Empty(t, db.builderProQueries) } type noopDeadliner struct{} diff --git a/core/dutydb/memory_test.go b/core/dutydb/memory_test.go index 04866b7cd..54ca58465 100644 --- a/core/dutydb/memory_test.go +++ b/core/dutydb/memory_test.go @@ -125,6 +125,30 @@ func TestMemDB(t *testing.T) { require.Equal(t, pubkeysByIdx[vIdxB], pkB) } +func TestMemDBStoreUnsupported(t *testing.T) { + ctx := context.Background() + db := dutydb.NewMemDB(new(testDeadliner)) + + unsupported := []core.DutyType{ + core.DutyUnknown, + core.DutySignature, + core.DutyExit, + core.DutyBuilderRegistration, + core.DutyRandao, + core.DutyPrepareAggregator, + core.DutySyncMessage, + core.DutyPrepareSyncContribution, + core.DutyInfoSync, + } + for _, dutyType := range unsupported { + err := db.Store(ctx, core.Duty{Type: dutyType}, nil) + require.ErrorContains(t, err, "unsupported duty type") + } + + err := db.Store(ctx, core.Duty{Type: core.DutyBuilderProposer}, nil) + require.ErrorIs(t, err, core.ErrDeprecatedDutyBuilderProposer) +} + func TestMemDBProposer(t *testing.T) { ctx := context.Background() db := dutydb.NewMemDB(new(testDeadliner)) @@ -373,134 +397,6 @@ func TestMemDBClashProposer(t *testing.T) { require.ErrorContains(t, err, "clashing blocks") } -func TestMemDBBuilderProposer(t *testing.T) { - ctx := context.Background() - db := dutydb.NewMemDB(new(testDeadliner)) - - const queries = 3 - slots := [queries]uint64{123, 456, 789} - - type response struct { - block *eth2api.VersionedBlindedProposal - } - var awaitResponse [queries]chan response - for i := 0; i < queries; i++ { - awaitResponse[i] = make(chan response) - go func(slot int) { - block, err := db.AwaitBlindedProposal(ctx, slots[slot]) - require.NoError(t, err) - awaitResponse[slot] <- response{block: block} - }(i) - } - - blocks := make([]*eth2api.VersionedBlindedProposal, queries) - pubkeysByIdx := make(map[eth2p0.ValidatorIndex]core.PubKey) - for i := 0; i < queries; i++ { - blocks[i] = ð2api.VersionedBlindedProposal{ - Version: eth2spec.DataVersionBellatrix, - Bellatrix: testutil.RandomBellatrixBlindedBeaconBlock(), - } - blocks[i].Bellatrix.Slot = eth2p0.Slot(slots[i]) - blocks[i].Bellatrix.ProposerIndex = eth2p0.ValidatorIndex(i) - pubkeysByIdx[eth2p0.ValidatorIndex(i)] = testutil.RandomCorePubKey(t) - } - - // Store the Blocks - for i := 0; i < queries; i++ { - unsigned, err := core.NewVersionedBlindedProposal(blocks[i]) - require.NoError(t, err) - - duty := core.Duty{Slot: slots[i], Type: core.DutyBuilderProposer} - err = db.Store(ctx, duty, core.UnsignedDataSet{ - pubkeysByIdx[eth2p0.ValidatorIndex(i)]: unsigned, - }) - require.NoError(t, err) - } - - // Get and assert the proQuery responses - for i := 0; i < queries; i++ { - actualData := <-awaitResponse[i] - require.Equal(t, blocks[i], actualData.block) - } -} - -func TestMemDBClashingBlindedBlocks(t *testing.T) { - ctx := context.Background() - db := dutydb.NewMemDB(new(testDeadliner)) - - const slot = 123 - block1 := ð2api.VersionedBlindedProposal{ - Version: eth2spec.DataVersionBellatrix, - Bellatrix: testutil.RandomBellatrixBlindedBeaconBlock(), - } - block1.Bellatrix.Slot = eth2p0.Slot(slot) - block2 := ð2api.VersionedBlindedProposal{ - Version: eth2spec.DataVersionBellatrix, - Bellatrix: testutil.RandomBellatrixBlindedBeaconBlock(), - } - block2.Bellatrix.Slot = eth2p0.Slot(slot) - pubkey := testutil.RandomCorePubKey(t) - - // Encode the Blocks - unsigned1, err := core.NewVersionedBlindedProposal(block1) - require.NoError(t, err) - - unsigned2, err := core.NewVersionedBlindedProposal(block2) - require.NoError(t, err) - - // Store the Blocks - duty := core.Duty{Slot: slot, Type: core.DutyBuilderProposer} - err = db.Store(ctx, duty, core.UnsignedDataSet{ - pubkey: unsigned1, - }) - require.NoError(t, err) - - err = db.Store(ctx, duty, core.UnsignedDataSet{ - pubkey: unsigned2, - }) - require.ErrorContains(t, err, "clashing blinded blocks") -} - -func TestMemDBClashBuilderProposer(t *testing.T) { - ctx := context.Background() - db := dutydb.NewMemDB(new(testDeadliner)) - - const slot = 123 - - block := ð2api.VersionedBlindedProposal{ - Version: eth2spec.DataVersionBellatrix, - Bellatrix: testutil.RandomBellatrixBlindedBeaconBlock(), - } - block.Bellatrix.Slot = eth2p0.Slot(slot) - pubkey := testutil.RandomCorePubKey(t) - - // Encode the block - unsigned, err := core.NewVersionedBlindedProposal(block) - require.NoError(t, err) - - // Store the Blocks - duty := core.Duty{Slot: slot, Type: core.DutyBuilderProposer} - err = db.Store(ctx, duty, core.UnsignedDataSet{ - pubkey: unsigned, - }) - require.NoError(t, err) - - // Store same block from same validator to test idempotent inserts - err = db.Store(ctx, duty, core.UnsignedDataSet{ - pubkey: unsigned, - }) - require.NoError(t, err) - - // Store a different block for the same slot - block.Bellatrix.ProposerIndex++ - unsignedB, err := core.NewVersionedBlindedProposal(block) - require.NoError(t, err) - err = db.Store(ctx, duty, core.UnsignedDataSet{ - pubkey: unsignedB, - }) - require.ErrorContains(t, err, "clashing blinded blocks") -} - func TestDutyExpiry(t *testing.T) { ctx := context.Background() deadliner := &testDeadliner{ch: make(chan core.Duty, 10)} diff --git a/core/fetcher/fetcher.go b/core/fetcher/fetcher.go index 64a97c944..53806e56c 100644 --- a/core/fetcher/fetcher.go +++ b/core/fetcher/fetcher.go @@ -5,6 +5,7 @@ package fetcher import ( "context" "fmt" + "math" "strings" eth2api "github.com/attestantio/go-eth2-client/api" @@ -21,10 +22,11 @@ import ( ) // New returns a new fetcher instance. -func New(eth2Cl eth2wrap.Client, feeRecipientFunc func(core.PubKey) string) (*Fetcher, error) { +func New(eth2Cl eth2wrap.Client, feeRecipientFunc func(core.PubKey) string, builderEnabled core.BuilderEnabled) (*Fetcher, error) { return &Fetcher{ eth2Cl: eth2Cl, feeRecipientFunc: feeRecipientFunc, + builderEnabled: builderEnabled, }, nil } @@ -35,6 +37,7 @@ type Fetcher struct { subs []func(context.Context, core.Duty, core.UnsignedDataSet) error aggSigDBFunc func(context.Context, core.Duty, core.PubKey) (core.SignedData, error) awaitAttDataFunc func(ctx context.Context, slot, commIdx uint64) (*eth2p0.AttestationData, error) + builderEnabled core.BuilderEnabled } // Subscribe registers a callback for fetched duties. @@ -62,10 +65,7 @@ func (f *Fetcher) Fetch(ctx context.Context, duty core.Duty, defSet core.DutyDef return errors.Wrap(err, "fetch attester data") } case core.DutyBuilderProposer: - unsignedSet, err = f.fetchBuilderProposerData(ctx, duty.Slot, defSet) - if err != nil { - return errors.Wrap(err, "fetch builder proposer data") - } + return core.ErrDeprecatedDutyBuilderProposer case core.DutyAggregator: unsignedSet, err = f.fetchAggregatorData(ctx, duty.Slot, defSet) if err != nil { @@ -251,10 +251,18 @@ func (f *Fetcher) fetchProposerData(ctx context.Context, slot uint64, defSet cor commitSHA, _ := version.GitCommit() copy(graffiti[:], fmt.Sprintf("charon/%v-%s", version.Version, commitSHA)) + var bbf uint64 + if f.builderEnabled(slot) { + // This gives maximum priority to builder blocks: + // https://ethereum.github.io/beacon-APIs/#/Validator/produceBlockV3 + bbf = math.MaxUint64 + } + opts := ð2api.ProposalOpts{ - Slot: eth2p0.Slot(slot), - RandaoReveal: randao, - Graffiti: graffiti, + Slot: eth2p0.Slot(slot), + RandaoReveal: randao, + Graffiti: graffiti, + BuilderBoostFactor: &bbf, } eth2Resp, err := f.eth2Cl.Proposal(ctx, opts) if err != nil { @@ -276,50 +284,6 @@ func (f *Fetcher) fetchProposerData(ctx context.Context, slot uint64, defSet cor return resp, nil } -func (f *Fetcher) fetchBuilderProposerData(ctx context.Context, slot uint64, defSet core.DutyDefinitionSet) (core.UnsignedDataSet, error) { - resp := make(core.UnsignedDataSet) - for pubkey := range defSet { - // Fetch previously aggregated randao reveal from AggSigDB - dutyRandao := core.Duty{ - Slot: slot, - Type: core.DutyRandao, - } - randaoData, err := f.aggSigDBFunc(ctx, dutyRandao, pubkey) - if err != nil { - return nil, err - } - - randao := randaoData.Signature().ToETH2() - - // TODO(dhruv): replace hardcoded graffiti with the one from cluster-lock.json - var graffiti [32]byte - commitSHA, _ := version.GitCommit() - copy(graffiti[:], fmt.Sprintf("charon/%v-%s", version.Version, commitSHA)) - - opts := ð2api.BlindedProposalOpts{ - Slot: eth2p0.Slot(slot), - RandaoReveal: randao, - Graffiti: graffiti, - } - eth2Resp, err := f.eth2Cl.BlindedProposal(ctx, opts) - if err != nil { - return nil, err - } - blindedProposal := eth2Resp.Data - - verifyFeeRecipientBlinded(ctx, blindedProposal, f.feeRecipientFunc(pubkey)) - - coreProposal, err := core.NewVersionedBlindedProposal(blindedProposal) - if err != nil { - return nil, errors.Wrap(err, "new block") - } - - resp[pubkey] = coreProposal - } - - return resp, nil -} - // fetchContributionData fetches the sync committee contribution data. func (f *Fetcher) fetchContributionData(ctx context.Context, slot uint64, defSet core.DutyDefinitionSet) (core.UnsignedDataSet, error) { resp := make(core.UnsignedDataSet) @@ -393,33 +357,23 @@ func verifyFeeRecipient(ctx context.Context, proposal *eth2api.VersionedProposal switch proposal.Version { case eth2spec.DataVersionBellatrix: - actualAddr = fmt.Sprintf("%#x", proposal.Bellatrix.Body.ExecutionPayload.FeeRecipient) - case eth2spec.DataVersionCapella: - actualAddr = fmt.Sprintf("%#x", proposal.Capella.Body.ExecutionPayload.FeeRecipient) - case eth2spec.DataVersionDeneb: - actualAddr = fmt.Sprintf("%#x", proposal.Deneb.Block.Body.ExecutionPayload.FeeRecipient) - default: - return - } - - if actualAddr != "" && !strings.EqualFold(actualAddr, feeRecipientAddress) { - log.Warn(ctx, "Proposal with unexpected fee recipient address", nil, - z.Str("expected", feeRecipientAddress), z.Str("actual", actualAddr)) - } -} - -// verifyFeeRecipientBlinded logs a warning when fee recipient is not correctly populated in the provided blinded beacon block. -func verifyFeeRecipientBlinded(ctx context.Context, proposal *eth2api.VersionedBlindedProposal, feeRecipientAddress string) { - // Note that fee-recipient is not available in forks earlier than bellatrix. - var actualAddr string - - switch proposal.Version { - case eth2spec.DataVersionBellatrix: - actualAddr = fmt.Sprintf("%#x", proposal.Bellatrix.Body.ExecutionPayloadHeader.FeeRecipient) + if proposal.Blinded { + actualAddr = fmt.Sprintf("%#x", proposal.BellatrixBlinded.Body.ExecutionPayloadHeader.FeeRecipient) + } else { + actualAddr = fmt.Sprintf("%#x", proposal.Bellatrix.Body.ExecutionPayload.FeeRecipient) + } case eth2spec.DataVersionCapella: - actualAddr = fmt.Sprintf("%#x", proposal.Capella.Body.ExecutionPayloadHeader.FeeRecipient) + if proposal.Blinded { + actualAddr = fmt.Sprintf("%#x", proposal.CapellaBlinded.Body.ExecutionPayloadHeader.FeeRecipient) + } else { + actualAddr = fmt.Sprintf("%#x", proposal.Capella.Body.ExecutionPayload.FeeRecipient) + } case eth2spec.DataVersionDeneb: - actualAddr = fmt.Sprintf("%#x", proposal.Deneb.Body.ExecutionPayloadHeader.FeeRecipient) + if proposal.Blinded { + actualAddr = fmt.Sprintf("%#x", proposal.DenebBlinded.Body.ExecutionPayloadHeader.FeeRecipient) + } else { + actualAddr = fmt.Sprintf("%#x", proposal.Deneb.Block.Body.ExecutionPayload.FeeRecipient) + } default: return } diff --git a/core/fetcher/fetcher_internal_test.go b/core/fetcher/fetcher_internal_test.go new file mode 100644 index 000000000..c62c9e033 --- /dev/null +++ b/core/fetcher/fetcher_internal_test.go @@ -0,0 +1,84 @@ +// Copyright © 2022-2024 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package fetcher + +import ( + "context" + "testing" + + eth2api "github.com/attestantio/go-eth2-client/api" + eth2spec "github.com/attestantio/go-eth2-client/spec" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + + "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/testutil" +) + +func TestVerifyFeeRecipient(t *testing.T) { + type testCase struct { + name string + proposal eth2api.VersionedProposal + } + + tests := []testCase{ + { + name: "bellatrix", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionBellatrix, + Bellatrix: testutil.RandomBellatrixBeaconBlock(), + }, + }, + { + name: "bellatrix blinded", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionBellatrix, + BellatrixBlinded: testutil.RandomBellatrixBlindedBeaconBlock(), + Blinded: true, + }, + }, + { + name: "capella", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionCapella, + Capella: testutil.RandomCapellaBeaconBlock(), + }, + }, + { + name: "capella blinded", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionCapella, + CapellaBlinded: testutil.RandomCapellaBlindedBeaconBlock(), + Blinded: true, + }, + }, + { + name: "deneb", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionDeneb, + Deneb: testutil.RandomDenebVersionedProposal().Deneb, + }, + }, + { + name: "deneb blinded", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionDeneb, + DenebBlinded: testutil.RandomDenebBlindedBeaconBlock(), + Blinded: true, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var buf zaptest.Buffer + log.InitLogfmtForT(t, &buf) + + verifyFeeRecipient(context.Background(), &test.proposal, "0x0000000000000000000000000000000000000000") + require.Empty(t, buf.String()) + + verifyFeeRecipient(context.Background(), &test.proposal, "0xdead") + require.Contains(t, buf.String(), "Proposal with unexpected fee recipient address") + }) + } +} diff --git a/core/fetcher/fetcher_test.go b/core/fetcher/fetcher_test.go index f3645e928..5e9af7404 100644 --- a/core/fetcher/fetcher_test.go +++ b/core/fetcher/fetcher_test.go @@ -62,9 +62,8 @@ func TestFetchAttester(t *testing.T) { duty := core.NewAttesterDuty(slot) bmock, err := beaconmock.New() require.NoError(t, err) - fetch, err := fetcher.New(bmock, nil) - require.NoError(t, err) + fetch := mustCreateFetcher(t, bmock) fetch.Subscribe(func(ctx context.Context, resDuty core.Duty, resDataSet core.UnsignedDataSet) error { require.Equal(t, duty, resDuty) require.Len(t, resDataSet, 2) @@ -157,9 +156,7 @@ func TestFetchAggregator(t *testing.T) { return nil, errors.New("expected unknown root") } - fetch, err := fetcher.New(bmock, nil) - require.NoError(t, err) - + fetch := mustCreateFetcher(t, bmock) fetch.RegisterAggSigDB(func(ctx context.Context, duty core.Duty, key core.PubKey) (core.SignedData, error) { require.Equal(t, core.NewPrepareAggregatorDuty(slot), duty) @@ -276,10 +273,7 @@ func TestFetchBlocks(t *testing.T) { t.Run("fetch DutyProposer", func(t *testing.T) { duty := core.NewProposerDuty(slot) - fetch, err := fetcher.New(bmock, func(core.PubKey) string { - return feeRecipientAddr - }) - require.NoError(t, err) + fetch := mustCreateFetcherWithAddress(t, bmock, feeRecipientAddr) fetch.RegisterAggSigDB(func(ctx context.Context, duty core.Duty, key core.PubKey) (core.SignedData, error) { return randaoByPubKey[key], nil @@ -293,14 +287,22 @@ func TestFetchBlocks(t *testing.T) { slotA, err := dutyDataA.Slot() require.NoError(t, err) require.EqualValues(t, slot, slotA) - require.Equal(t, feeRecipientAddr, fmt.Sprintf("%#x", dutyDataA.Capella.Body.ExecutionPayload.FeeRecipient)) + if dutyDataA.Blinded { + require.Equal(t, feeRecipientAddr, fmt.Sprintf("%#x", dutyDataA.CapellaBlinded.Body.ExecutionPayloadHeader.FeeRecipient)) + } else { + require.Equal(t, feeRecipientAddr, fmt.Sprintf("%#x", dutyDataA.Capella.Body.ExecutionPayload.FeeRecipient)) + } assertRandao(t, randaoByPubKey[pubkeysByIdx[vIdxA]].Signature().ToETH2(), dutyDataA) dutyDataB := resDataSet[pubkeysByIdx[vIdxB]].(core.VersionedProposal) slotB, err := dutyDataB.Slot() require.NoError(t, err) require.EqualValues(t, slot, slotB) - require.Equal(t, feeRecipientAddr, fmt.Sprintf("%#x", dutyDataB.Capella.Body.ExecutionPayload.FeeRecipient)) + if dutyDataB.Blinded { + require.Equal(t, feeRecipientAddr, fmt.Sprintf("%#x", dutyDataB.CapellaBlinded.Body.ExecutionPayloadHeader.FeeRecipient)) + } else { + require.Equal(t, feeRecipientAddr, fmt.Sprintf("%#x", dutyDataB.Capella.Body.ExecutionPayload.FeeRecipient)) + } assertRandao(t, randaoByPubKey[pubkeysByIdx[vIdxB]].Signature().ToETH2(), dutyDataB) return nil @@ -309,42 +311,6 @@ func TestFetchBlocks(t *testing.T) { err = fetch.Fetch(ctx, duty, defSet) require.NoError(t, err) }) - - t.Run("fetch DutyBuilderProposer", func(t *testing.T) { - duty := core.NewBuilderProposerDuty(slot) - fetch, err := fetcher.New(bmock, func(core.PubKey) string { - return feeRecipientAddr - }) - require.NoError(t, err) - - fetch.RegisterAggSigDB(func(ctx context.Context, duty core.Duty, key core.PubKey) (core.SignedData, error) { - return randaoByPubKey[key], nil - }) - - fetch.Subscribe(func(ctx context.Context, resDuty core.Duty, resDataSet core.UnsignedDataSet) error { - require.Equal(t, duty, resDuty) - require.Len(t, resDataSet, 2) - - dutyDataA := resDataSet[pubkeysByIdx[vIdxA]].(core.VersionedBlindedProposal) - slotA, err := dutyDataA.Slot() - require.NoError(t, err) - require.EqualValues(t, slot, slotA) - require.Equal(t, feeRecipientAddr, fmt.Sprintf("%#x", dutyDataA.Capella.Body.ExecutionPayloadHeader.FeeRecipient)) - assertRandaoBlindedBlock(t, randaoByPubKey[pubkeysByIdx[vIdxA]].Signature().ToETH2(), dutyDataA) - - dutyDataB := resDataSet[pubkeysByIdx[vIdxB]].(core.VersionedBlindedProposal) - slotB, err := dutyDataB.Slot() - require.NoError(t, err) - require.EqualValues(t, slot, slotB) - require.Equal(t, feeRecipientAddr, fmt.Sprintf("%#x", dutyDataB.Capella.Body.ExecutionPayloadHeader.FeeRecipient)) - assertRandaoBlindedBlock(t, randaoByPubKey[pubkeysByIdx[vIdxB]].Signature().ToETH2(), dutyDataB) - - return nil - }) - - err = fetch.Fetch(ctx, duty, defSet) - require.NoError(t, err) - }) } func TestFetchSyncContribution(t *testing.T) { @@ -410,7 +376,6 @@ func TestFetchSyncContribution(t *testing.T) { } t.Run("contribution aggregator", func(t *testing.T) { - // Construct beaconmock. bmock, err := beaconmock.New() require.NoError(t, err) @@ -448,10 +413,7 @@ func TestFetchSyncContribution(t *testing.T) { }, nil } - // Construct fetcher component. - fetch, err := fetcher.New(bmock, nil) - require.NoError(t, err) - + fetch := mustCreateFetcher(t, bmock) fetch.RegisterAggSigDB(func(ctx context.Context, duty core.Duty, key core.PubKey) (core.SignedData, error) { if duty.Type == core.DutyPrepareSyncContribution { require.Equal(t, core.NewPrepareSyncContributionDuty(slot), duty) @@ -505,14 +467,10 @@ func TestFetchSyncContribution(t *testing.T) { }) t.Run("not contribution aggregator", func(t *testing.T) { - // Construct beaconmock. bmock, err := beaconmock.New() require.NoError(t, err) - // Construct fetcher component. - fetch, err := fetcher.New(bmock, nil) - require.NoError(t, err) - + fetch := mustCreateFetcher(t, bmock) fetch.RegisterAggSigDB(func(ctx context.Context, duty core.Duty, key core.PubKey) (core.SignedData, error) { if duty.Type == core.DutyPrepareSyncContribution { require.Equal(t, core.NewPrepareSyncContributionDuty(slot), duty) @@ -535,14 +493,10 @@ func TestFetchSyncContribution(t *testing.T) { }) t.Run("fetch contribution data error", func(t *testing.T) { - // Construct beaconmock. bmock, err := beaconmock.New() require.NoError(t, err) - // Construct fetcher component. - fetch, err := fetcher.New(bmock, nil) - require.NoError(t, err) - + fetch := mustCreateFetcher(t, bmock) fetch.RegisterAggSigDB(func(ctx context.Context, duty core.Duty, key core.PubKey) (core.SignedData, error) { return nil, errors.New("error") }) @@ -554,6 +508,30 @@ func TestFetchSyncContribution(t *testing.T) { }) } +func mustCreateFetcher(t *testing.T, bmock beaconmock.Mock) *fetcher.Fetcher { + t.Helper() + + fetch, err := fetcher.New(bmock, nil, func(uint64) bool { + return true + }) + require.NoError(t, err) + + return fetch +} + +func mustCreateFetcherWithAddress(t *testing.T, bmock beaconmock.Mock, addr string) *fetcher.Fetcher { + t.Helper() + + fetch, err := fetcher.New(bmock, func(core.PubKey) string { + return addr + }, func(uint64) bool { + return true + }) + require.NoError(t, err) + + return fetch +} + func assertRandao(t *testing.T, randao eth2p0.BLSSignature, block core.VersionedProposal) { t.Helper() @@ -563,26 +541,23 @@ func assertRandao(t *testing.T, randao eth2p0.BLSSignature, block core.Versioned case eth2spec.DataVersionAltair: require.EqualValues(t, randao, block.Altair.Body.RANDAOReveal) case eth2spec.DataVersionBellatrix: - require.EqualValues(t, randao, block.Bellatrix.Body.RANDAOReveal) - case eth2spec.DataVersionCapella: - require.EqualValues(t, randao, block.Capella.Body.RANDAOReveal) - case eth2spec.DataVersionDeneb: - require.EqualValues(t, randao, block.Deneb.Block.Body.RANDAOReveal) - default: - require.Fail(t, "invalid block") - } -} - -func assertRandaoBlindedBlock(t *testing.T, randao eth2p0.BLSSignature, block core.VersionedBlindedProposal) { - t.Helper() - - switch block.Version { - case eth2spec.DataVersionBellatrix: - require.EqualValues(t, randao, block.Bellatrix.Body.RANDAOReveal) + if block.Blinded { + require.EqualValues(t, randao, block.BellatrixBlinded.Body.RANDAOReveal) + } else { + require.EqualValues(t, randao, block.Bellatrix.Body.RANDAOReveal) + } case eth2spec.DataVersionCapella: - require.EqualValues(t, randao, block.Capella.Body.RANDAOReveal) + if block.Blinded { + require.EqualValues(t, randao, block.CapellaBlinded.Body.RANDAOReveal) + } else { + require.EqualValues(t, randao, block.Capella.Body.RANDAOReveal) + } case eth2spec.DataVersionDeneb: - require.EqualValues(t, randao, block.Deneb.Body.RANDAOReveal) + if block.Blinded { + require.EqualValues(t, randao, block.DenebBlinded.Body.RANDAOReveal) + } else { + require.EqualValues(t, randao, block.Deneb.Block.Body.RANDAOReveal) + } default: require.Fail(t, "invalid block") } diff --git a/core/interfaces.go b/core/interfaces.go index a07c604f7..786876124 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -47,10 +47,6 @@ type DutyDB interface { // for the slot when available. AwaitProposal(ctx context.Context, slot uint64) (*eth2api.VersionedProposal, error) - // AwaitBlindedProposal blocks and returns the proposed blinded beacon block - // for the slot when available. - AwaitBlindedProposal(ctx context.Context, slot uint64) (*eth2api.VersionedBlindedProposal, error) - // AwaitAttestation blocks and returns the attestation data // for the slot and committee index when available. AwaitAttestation(ctx context.Context, slot, commIdx uint64) (*eth2p0.AttestationData, error) @@ -86,9 +82,6 @@ type ValidatorAPI interface { // RegisterAwaitProposal registers a function to query unsigned beacon block proposals by providing the slot. RegisterAwaitProposal(func(ctx context.Context, slot uint64) (*eth2api.VersionedProposal, error)) - // RegisterAwaitBlindedProposal registers a function to query unsigned blinded beacon block proposals by providing the slot. - RegisterAwaitBlindedProposal(func(ctx context.Context, slot uint64) (*eth2api.VersionedBlindedProposal, error)) - // RegisterAwaitAttestation registers a function to query attestation data. RegisterAwaitAttestation(func(ctx context.Context, slot, commIdx uint64) (*eth2p0.AttestationData, error)) @@ -220,7 +213,6 @@ type wireFuncs struct { ConsensusSubscribe func(func(context.Context, Duty, UnsignedDataSet) error) DutyDBStore func(context.Context, Duty, UnsignedDataSet) error DutyDBAwaitProposal func(ctx context.Context, slot uint64) (*eth2api.VersionedProposal, error) - DutyDBAwaitBlindedProposal func(ctx context.Context, slot uint64) (*eth2api.VersionedBlindedProposal, error) DutyDBAwaitAttestation func(ctx context.Context, slot, commIdx uint64) (*eth2p0.AttestationData, error) DutyDBPubKeyByAttestation func(ctx context.Context, slot, commIdx, valCommIdx uint64) (PubKey, error) DutyDBAwaitAggAttestation func(ctx context.Context, slot uint64, attestationRoot eth2p0.Root) (*eth2p0.Attestation, error) @@ -228,7 +220,6 @@ type wireFuncs struct { VAPIRegisterAwaitAttestation func(func(ctx context.Context, slot, commIdx uint64) (*eth2p0.AttestationData, error)) VAPIRegisterAwaitSyncContribution func(func(ctx context.Context, slot, subcommIdx uint64, beaconBlockRoot eth2p0.Root) (*altair.SyncCommitteeContribution, error)) VAPIRegisterAwaitProposal func(func(ctx context.Context, slot uint64) (*eth2api.VersionedProposal, error)) - VAPIRegisterAwaitBlindedProposal func(func(ctx context.Context, slot uint64) (*eth2api.VersionedBlindedProposal, error)) VAPIRegisterGetDutyDefinition func(func(context.Context, Duty) (DutyDefinitionSet, error)) VAPIRegisterPubKeyByAttestation func(func(ctx context.Context, slot, commIdx, valCommIdx uint64) (PubKey, error)) VAPIRegisterAwaitAggAttestation func(func(ctx context.Context, slot uint64, attestationRoot eth2p0.Root) (*eth2p0.Attestation, error)) @@ -277,12 +268,10 @@ func Wire(sched Scheduler, DutyDBStore: dutyDB.Store, DutyDBAwaitAttestation: dutyDB.AwaitAttestation, DutyDBAwaitProposal: dutyDB.AwaitProposal, - DutyDBAwaitBlindedProposal: dutyDB.AwaitBlindedProposal, DutyDBPubKeyByAttestation: dutyDB.PubKeyByAttestation, DutyDBAwaitAggAttestation: dutyDB.AwaitAggAttestation, DutyDBAwaitSyncContribution: dutyDB.AwaitSyncContribution, VAPIRegisterAwaitProposal: vapi.RegisterAwaitProposal, - VAPIRegisterAwaitBlindedProposal: vapi.RegisterAwaitBlindedProposal, VAPIRegisterAwaitAttestation: vapi.RegisterAwaitAttestation, VAPIRegisterAwaitSyncContribution: vapi.RegisterAwaitSyncContribution, VAPIRegisterGetDutyDefinition: vapi.RegisterGetDutyDefinition, @@ -316,7 +305,6 @@ func Wire(sched Scheduler, w.FetcherRegisterAwaitAttData(w.DutyDBAwaitAttestation) w.ConsensusSubscribe(w.DutyDBStore) w.VAPIRegisterAwaitProposal(w.DutyDBAwaitProposal) - w.VAPIRegisterAwaitBlindedProposal(w.DutyDBAwaitBlindedProposal) w.VAPIRegisterAwaitAttestation(w.DutyDBAwaitAttestation) w.VAPIRegisterAwaitSyncContribution(w.DutyDBAwaitSyncContribution) w.VAPIRegisterGetDutyDefinition(w.SchedulerGetDutyDefinition) diff --git a/core/parsigex/parsigex_test.go b/core/parsigex/parsigex_test.go index 9ade004b6..f1cae407a 100644 --- a/core/parsigex/parsigex_test.go +++ b/core/parsigex/parsigex_test.go @@ -185,7 +185,7 @@ func TestParSigExVerifier(t *testing.T) { data, err := core.NewPartialVersionedSignedBlindedProposal(&blindedBlock.VersionedSignedBlindedProposal, shareIdx) require.NoError(t, err) - require.NoError(t, verifyFunc(ctx, core.NewBuilderProposerDuty(slot), pubkey, data)) + require.NoError(t, verifyFunc(ctx, core.NewProposerDuty(slot), pubkey, data)) }) t.Run("Verify Randao", func(t *testing.T) { diff --git a/core/proto.go b/core/proto.go index 80f010723..e47cc7365 100644 --- a/core/proto.go +++ b/core/proto.go @@ -74,11 +74,7 @@ func ParSignedDataFromProto(typ DutyType, data *pbv1.ParSignedData) (_ ParSigned } signedData = b case DutyBuilderProposer: - var b VersionedSignedBlindedProposal - if err := unmarshal(data.Data, &b); err != nil { - return ParSignedData{}, errors.Wrap(err, "unmarshal blinded proposal") - } - signedData = b + return ParSignedData{}, ErrDeprecatedDutyBuilderProposer case DutyBuilderRegistration: var r VersionedSignedValidatorRegistration if err := unmarshal(data.Data, &r); err != nil { diff --git a/core/proto_test.go b/core/proto_test.go index 7e6f6e229..28b8cec5f 100644 --- a/core/proto_test.go +++ b/core/proto_test.go @@ -51,18 +51,6 @@ func TestParSignedDataSetProto(t *testing.T) { Type: core.DutyProposer, Data: testutil.RandomDenebCoreVersionedSignedProposal(), }, - { - Type: core.DutyBuilderProposer, - Data: testutil.RandomBellatrixVersionedSignedBlindedProposal(), - }, - { - Type: core.DutyBuilderProposer, - Data: testutil.RandomCapellaVersionedSignedBlindedProposal(), - }, - { - Type: core.DutyBuilderProposer, - Data: testutil.RandomDenebVersionedSignedBlindedProposal(), - }, { Type: core.DutyBuilderRegistration, Data: testutil.RandomCoreVersionedSignedValidatorRegistration(t), @@ -132,10 +120,6 @@ func TestUnsignedDataToProto(t *testing.T) { Type: core.DutyProposer, Data: testutil.RandomBellatrixCoreVersionedProposal(), }, - { - Type: core.DutyBuilderProposer, - Data: testutil.RandomBellatrixVersionedBlindedProposal(), - }, { Type: core.DutyAggregator, Data: core.NewAggregatedAttestation(testutil.RandomAttestation()), @@ -200,6 +184,26 @@ func TestParSignedData(t *testing.T) { } } +func TestParSignedDataFromProtoErrors(t *testing.T) { + parSig1 := core.ParSignedData{ + SignedData: core.Attestation{Attestation: *testutil.RandomAttestation()}, + ShareIdx: rand.Intn(100), + } + + // We need valid protobuf message to test this + pb1, err := core.ParSignedDataToProto(parSig1) + require.NoError(t, err) + + _, err = core.ParSignedDataFromProto(core.DutyUnknown, pb1) + require.ErrorContains(t, err, "unsupported duty type") + + _, err = core.ParSignedDataFromProto(core.DutyProposer, pb1) + require.ErrorContains(t, err, "unknown data version") + + _, err = core.ParSignedDataFromProto(core.DutyBuilderProposer, pb1) + require.ErrorIs(t, err, core.ErrDeprecatedDutyBuilderProposer) +} + func TestSetSignature(t *testing.T) { for typ, signedData := range randomSignedData(t) { t.Run(typ.String(), func(t *testing.T) { diff --git a/core/scheduler/scheduler.go b/core/scheduler/scheduler.go index 41cdb4c2d..140d44a22 100644 --- a/core/scheduler/scheduler.go +++ b/core/scheduler/scheduler.go @@ -141,10 +141,8 @@ func (s *Scheduler) emitCoreSlot(ctx context.Context, slot core.Slot) { // GetDutyDefinition returns the definition for a duty or core.ErrNotFound if no definitions exist for a resolved epoch // or another error. func (s *Scheduler) GetDutyDefinition(ctx context.Context, duty core.Duty) (core.DutyDefinitionSet, error) { - if duty.Type == core.DutyBuilderProposer && !s.builderEnabled(duty.Slot) { - return nil, errors.New("builder-api not enabled, but duty builder proposer requested") - } else if duty.Type == core.DutyProposer && s.builderEnabled(duty.Slot) { - return nil, errors.New("builder-api enabled, but duty proposer requested") + if duty.Type == core.DutyBuilderProposer { + return nil, core.ErrDeprecatedDutyBuilderProposer } slotsPerEpoch, err := s.eth2Cl.SlotsPerEpoch(ctx) @@ -386,13 +384,7 @@ func (s *Scheduler) resolveProDuties(ctx context.Context, slot core.Slot, vals v continue } - var duty core.Duty - - if s.builderEnabled(uint64(proDuty.Slot)) { - duty = core.Duty{Slot: uint64(proDuty.Slot), Type: core.DutyBuilderProposer} - } else { - duty = core.Duty{Slot: uint64(proDuty.Slot), Type: core.DutyProposer} - } + duty := core.NewProposerDuty(uint64(proDuty.Slot)) pubkey, ok := vals.PubKeyFromIndex(proDuty.ValidatorIndex) if !ok { diff --git a/core/scheduler/scheduler_test.go b/core/scheduler/scheduler_test.go index 04a45c06e..ad127481f 100644 --- a/core/scheduler/scheduler_test.go +++ b/core/scheduler/scheduler_test.go @@ -322,8 +322,11 @@ func TestScheduler_GetDuty(t *testing.T) { _, err = sched.GetDutyDefinition(ctx, core.NewSyncContributionDuty(slot)) require.ErrorContains(t, err, "epoch not resolved yet") - _, err = sched.GetDutyDefinition(ctx, core.NewBuilderProposerDuty(slot)) - require.ErrorContains(t, err, "builder-api not enabled") + _, err = sched.GetDutyDefinition(ctx, core.Duty{ + Slot: slot, + Type: core.DutyBuilderProposer, + }) + require.ErrorIs(t, err, core.ErrDeprecatedDutyBuilderProposer) eth2Resp, err := eth2Cl.Spec(ctx, ð2api.SpecOpts{}) require.NoError(t, err) diff --git a/core/serialise_test.go b/core/serialise_test.go index 4649a6ad0..1a2db4d5b 100644 --- a/core/serialise_test.go +++ b/core/serialise_test.go @@ -31,7 +31,6 @@ var coreTypeFuncs = []func() any{ func() any { return new(core.AttestationData) }, func() any { return new(core.AggregatedAttestation) }, func() any { return new(core.VersionedProposal) }, - func() any { return new(core.VersionedBlindedProposal) }, func() any { return new(core.SyncContribution) }, } diff --git a/core/sigagg/sigagg_test.go b/core/sigagg/sigagg_test.go index 8b68946bf..a968552f2 100644 --- a/core/sigagg/sigagg_test.go +++ b/core/sigagg/sigagg_test.go @@ -12,7 +12,6 @@ import ( eth2capella "github.com/attestantio/go-eth2-client/api/v1/capella" eth2deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" eth2spec "github.com/attestantio/go-eth2-client/spec" - "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/deneb" @@ -330,26 +329,9 @@ func TestSigAgg_DutyProposer(t *testing.T) { name string proposal *eth2api.VersionedSignedProposal }{ - { - name: "phase0 proposal", - proposal: ð2api.VersionedSignedProposal{ - Version: eth2spec.DataVersionPhase0, - Phase0: ð2p0.SignedBeaconBlock{ - Message: testutil.RandomPhase0BeaconBlock(), - Signature: testutil.RandomEth2Signature(), - }, - }, - }, - { - name: "altair proposal", - proposal: ð2api.VersionedSignedProposal{ - Version: eth2spec.DataVersionAltair, - Altair: &altair.SignedBeaconBlock{ - Message: testutil.RandomAltairBeaconBlock(), - Signature: testutil.RandomEth2Signature(), - }, - }, - }, + // phase0 and altair cannot be tested + // In according with Jim: + // "Proposals are only relevant from bellatrix onwards, where we had execution payloads." { name: "bellatrix proposal", proposal: ð2api.VersionedSignedProposal{ diff --git a/core/signeddata.go b/core/signeddata.go index 1788f2372..9cd127b3c 100644 --- a/core/signeddata.go +++ b/core/signeddata.go @@ -141,17 +141,26 @@ func NewVersionedSignedProposal(proposal *eth2api.VersionedSignedProposal) (Vers return VersionedSignedProposal{}, errors.New("no altair proposal") } case eth2spec.DataVersionBellatrix: - if proposal.Bellatrix == nil { + if proposal.Bellatrix == nil && !proposal.Blinded { return VersionedSignedProposal{}, errors.New("no bellatrix proposal") } + if proposal.BellatrixBlinded == nil && proposal.Blinded { + return VersionedSignedProposal{}, errors.New("no bellatrix blinded proposal") + } case eth2spec.DataVersionCapella: - if proposal.Capella == nil { + if proposal.Capella == nil && !proposal.Blinded { return VersionedSignedProposal{}, errors.New("no capella proposal") } + if proposal.CapellaBlinded == nil && proposal.Blinded { + return VersionedSignedProposal{}, errors.New("no capella blinded proposal") + } case eth2spec.DataVersionDeneb: - if proposal.Deneb == nil { + if proposal.Deneb == nil && !proposal.Blinded { return VersionedSignedProposal{}, errors.New("no deneb proposal") } + if proposal.DenebBlinded == nil && proposal.Blinded { + return VersionedSignedProposal{}, errors.New("no deneb blinded proposal") + } default: return VersionedSignedProposal{}, errors.New("unknown version") } @@ -185,10 +194,22 @@ func (p VersionedSignedProposal) Signature() Signature { case eth2spec.DataVersionAltair: return SigFromETH2(p.Altair.Signature) case eth2spec.DataVersionBellatrix: + if p.Blinded { + return SigFromETH2(p.BellatrixBlinded.Signature) + } + return SigFromETH2(p.Bellatrix.Signature) case eth2spec.DataVersionCapella: + if p.Blinded { + return SigFromETH2(p.CapellaBlinded.Signature) + } + return SigFromETH2(p.Capella.Signature) case eth2spec.DataVersionDeneb: + if p.Blinded { + return SigFromETH2(p.DenebBlinded.Signature) + } + return SigFromETH2(p.Deneb.SignedBlock.Signature) default: panic("unknown version") // Note this is avoided by using `NewVersionedSignedProposal`. @@ -208,11 +229,23 @@ func (p VersionedSignedProposal) SetSignature(sig Signature) (SignedData, error) case eth2spec.DataVersionAltair: resp.Altair.Signature = sig.ToETH2() case eth2spec.DataVersionBellatrix: - resp.Bellatrix.Signature = sig.ToETH2() + if resp.Blinded { + resp.BellatrixBlinded.Signature = sig.ToETH2() + } else { + resp.Bellatrix.Signature = sig.ToETH2() + } case eth2spec.DataVersionCapella: - resp.Capella.Signature = sig.ToETH2() + if resp.Blinded { + resp.CapellaBlinded.Signature = sig.ToETH2() + } else { + resp.Capella.Signature = sig.ToETH2() + } case eth2spec.DataVersionDeneb: - resp.Deneb.SignedBlock.Signature = sig.ToETH2() + if resp.Blinded { + resp.DenebBlinded.Signature = sig.ToETH2() + } else { + resp.Deneb.SignedBlock.Signature = sig.ToETH2() + } default: return nil, errors.New("unknown type") } @@ -228,10 +261,22 @@ func (p VersionedSignedProposal) MessageRoot() ([32]byte, error) { case eth2spec.DataVersionAltair: return p.Altair.Message.HashTreeRoot() case eth2spec.DataVersionBellatrix: + if p.Blinded { + return p.BellatrixBlinded.Message.HashTreeRoot() + } + return p.Bellatrix.Message.HashTreeRoot() case eth2spec.DataVersionCapella: + if p.Blinded { + return p.CapellaBlinded.Message.HashTreeRoot() + } + return p.Capella.Message.HashTreeRoot() case eth2spec.DataVersionDeneb: + if p.Blinded { + return p.DenebBlinded.Message.HashTreeRoot() + } + return p.Deneb.SignedBlock.Message.HashTreeRoot() default: panic("unknown version") // Note this is avoided by using `NewVersionedSignedProposal`. @@ -263,11 +308,23 @@ func (p VersionedSignedProposal) MarshalJSON() ([]byte, error) { case eth2spec.DataVersionAltair: marshaller = p.VersionedSignedProposal.Altair case eth2spec.DataVersionBellatrix: - marshaller = p.VersionedSignedProposal.Bellatrix + if p.Blinded { + marshaller = p.VersionedSignedProposal.BellatrixBlinded + } else { + marshaller = p.VersionedSignedProposal.Bellatrix + } case eth2spec.DataVersionCapella: - marshaller = p.VersionedSignedProposal.Capella + if p.Blinded { + marshaller = p.VersionedSignedProposal.CapellaBlinded + } else { + marshaller = p.VersionedSignedProposal.Capella + } case eth2spec.DataVersionDeneb: - marshaller = p.VersionedSignedProposal.Deneb + if p.Blinded { + marshaller = p.VersionedSignedProposal.DenebBlinded + } else { + marshaller = p.VersionedSignedProposal.Deneb + } default: return nil, errors.New("unknown version") } @@ -285,6 +342,7 @@ func (p VersionedSignedProposal) MarshalJSON() ([]byte, error) { resp, err := json.Marshal(versionedRawBlockJSON{ Version: version, Block: proposal, + Blinded: p.Blinded, }) if err != nil { return nil, errors.Wrap(err, "marshal wrapper") @@ -314,27 +372,53 @@ func (p *VersionedSignedProposal) UnmarshalJSON(input []byte) error { } resp.Altair = block case eth2spec.DataVersionBellatrix: - block := new(bellatrix.SignedBeaconBlock) - if err := json.Unmarshal(raw.Block, &block); err != nil { - return errors.Wrap(err, "unmarshal bellatrix") + if raw.Blinded { + block := new(eth2bellatrix.SignedBlindedBeaconBlock) + if err := json.Unmarshal(raw.Block, &block); err != nil { + return errors.Wrap(err, "unmarshal bellatrix blinded") + } + resp.BellatrixBlinded = block + } else { + block := new(bellatrix.SignedBeaconBlock) + if err := json.Unmarshal(raw.Block, &block); err != nil { + return errors.Wrap(err, "unmarshal bellatrix") + } + resp.Bellatrix = block } - resp.Bellatrix = block case eth2spec.DataVersionCapella: - block := new(capella.SignedBeaconBlock) - if err := json.Unmarshal(raw.Block, &block); err != nil { - return errors.Wrap(err, "unmarshal capella") + if raw.Blinded { + block := new(eth2capella.SignedBlindedBeaconBlock) + if err := json.Unmarshal(raw.Block, &block); err != nil { + return errors.Wrap(err, "unmarshal capella blinded") + } + resp.CapellaBlinded = block + } else { + block := new(capella.SignedBeaconBlock) + if err := json.Unmarshal(raw.Block, &block); err != nil { + return errors.Wrap(err, "unmarshal capella") + } + resp.Capella = block } - resp.Capella = block case eth2spec.DataVersionDeneb: - block := new(eth2deneb.SignedBlockContents) - if err := json.Unmarshal(raw.Block, &block); err != nil { - return errors.Wrap(err, "unmarshal deneb") + if raw.Blinded { + block := new(eth2deneb.SignedBlindedBeaconBlock) + if err := json.Unmarshal(raw.Block, &block); err != nil { + return errors.Wrap(err, "unmarshal deneb blinded") + } + resp.DenebBlinded = block + } else { + block := new(eth2deneb.SignedBlockContents) + if err := json.Unmarshal(raw.Block, &block); err != nil { + return errors.Wrap(err, "unmarshal deneb") + } + resp.Deneb = block } - resp.Deneb = block default: return errors.New("unknown version") } + resp.Blinded = raw.Blinded + p.VersionedSignedProposal = resp return nil @@ -472,6 +556,7 @@ func (p VersionedSignedBlindedProposal) MarshalJSON() ([]byte, error) { resp, err := json.Marshal(versionedRawBlockJSON{ Version: version, Block: block, + Blinded: true, }) if err != nil { return nil, errors.Wrap(err, "marshal wrapper") @@ -485,6 +570,9 @@ func (p *VersionedSignedBlindedProposal) UnmarshalJSON(input []byte) error { if err := json.Unmarshal(input, &raw); err != nil { return errors.Wrap(err, "unmarshal block") } + if !raw.Blinded { + return errors.New("unmarshalled block is not blinded") + } resp := eth2api.VersionedSignedBlindedProposal{Version: raw.Version.ToETH2()} switch resp.Version { @@ -519,6 +607,7 @@ func (p *VersionedSignedBlindedProposal) UnmarshalJSON(input []byte) error { type versionedRawBlockJSON struct { Version eth2util.DataVersion `json:"version"` Block json.RawMessage `json:"block"` + Blinded bool `json:"blinded,omitempty"` } // NewAttestation is a convenience function that returns a new wrapped attestation. diff --git a/core/signeddata_test.go b/core/signeddata_test.go index 0354183e3..62c09a6bd 100644 --- a/core/signeddata_test.go +++ b/core/signeddata_test.go @@ -4,6 +4,7 @@ package core_test import ( "encoding/json" + "fmt" "testing" eth2api "github.com/attestantio/go-eth2-client/api" @@ -13,6 +14,7 @@ import ( eth2spec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/capella" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/stretchr/testify/require" @@ -20,6 +22,9 @@ import ( "github.com/obolnetwork/charon/testutil" ) +// To satisfy linter. +const unmarshalPrefix = "unmarshal " + func TestSignedDataSetSignature(t *testing.T) { tests := []struct { name string @@ -137,3 +142,361 @@ func TestMarshalSubscription(t *testing.T) { require.NoError(t, err) require.Equal(t, selection2, selection) } + +func TestNewPartialSignature(t *testing.T) { + sig := testutil.RandomCoreSignature() + + partialSig := core.NewPartialSignature(sig, 3) + + require.Equal(t, sig, partialSig.Signature()) + require.Equal(t, 3, partialSig.ShareIdx) +} + +func TestSignature(t *testing.T) { + sig1 := testutil.RandomCoreSignature() + + sig2, err := sig1.Clone() + require.NoError(t, err) + require.Equal(t, sig1, sig2) + + _, err = sig1.MessageRoot() + require.ErrorContains(t, err, "signed message root not supported by signature type") + require.Equal(t, sig1, sig1.Signature()) + + blssig1 := sig1.ToETH2() + blssig2 := sig2.Signature().ToETH2() + require.Equal(t, blssig1, blssig2) + + ss, err := sig1.SetSignature(sig2.Signature()) + require.NoError(t, err) + require.Equal(t, sig2, ss) + + js, err := sig1.MarshalJSON() + require.NoError(t, err) + + sig3 := &core.Signature{} + err = sig3.UnmarshalJSON(js) + require.NoError(t, err) + require.Equal(t, sig1, *sig3) +} + +func TestNewVersionedSignedProposal(t *testing.T) { + type testCase struct { + error string + version eth2spec.DataVersion + blinded bool + } + + tests := []testCase{ + { + error: "unknown version", + version: eth2spec.DataVersion(999), + }, + { + error: "no phase0 proposal", + version: eth2spec.DataVersionPhase0, + }, + { + error: "no altair proposal", + version: eth2spec.DataVersionAltair, + }, + { + error: "no bellatrix proposal", + version: eth2spec.DataVersionBellatrix, + }, + { + error: "no capella proposal", + version: eth2spec.DataVersionCapella, + }, + { + error: "no deneb proposal", + version: eth2spec.DataVersionDeneb, + }, + { + error: "no bellatrix blinded proposal", + version: eth2spec.DataVersionBellatrix, + blinded: true, + }, + { + error: "no capella blinded proposal", + version: eth2spec.DataVersionCapella, + blinded: true, + }, + { + error: "no deneb blinded proposal", + version: eth2spec.DataVersionDeneb, + blinded: true, + }, + } + + for _, test := range tests { + t.Run(test.error, func(t *testing.T) { + _, err := core.NewVersionedSignedProposal(ð2api.VersionedSignedProposal{ + Version: test.version, + Blinded: test.blinded, + }) + require.ErrorContains(t, err, test.error) + }) + } + + t.Run("happy path", func(t *testing.T) { + proposal := testutil.RandomBellatrixCoreVersionedSignedProposal() + + p, err := core.NewVersionedSignedProposal(&proposal.VersionedSignedProposal) + require.NoError(t, err) + require.Equal(t, proposal, p) + }) +} + +func TestNewPartialVersionedSignedProposal(t *testing.T) { + proposal := testutil.RandomBellatrixCoreVersionedSignedProposal() + + psd, err := core.NewPartialVersionedSignedProposal(&proposal.VersionedSignedProposal, 3) + + require.NoError(t, err) + require.NotNil(t, psd.SignedData) + require.Equal(t, 3, psd.ShareIdx) +} + +func TestVersionedSignedProposal(t *testing.T) { + type testCase struct { + name string + proposal eth2api.VersionedSignedProposal + } + + tests := []testCase{ + { + name: "phase0", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionPhase0, + Phase0: ð2p0.SignedBeaconBlock{ + Message: testutil.RandomPhase0BeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + { + name: "altair", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionAltair, + Altair: &altair.SignedBeaconBlock{ + Message: testutil.RandomAltairBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + { + name: "bellatrix", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionBellatrix, + Bellatrix: &bellatrix.SignedBeaconBlock{ + Message: testutil.RandomBellatrixBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + { + name: "bellatrix blinded", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionBellatrix, + BellatrixBlinded: ð2bellatrix.SignedBlindedBeaconBlock{ + Message: testutil.RandomBellatrixBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + Blinded: true, + }, + }, + { + name: "capella", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionCapella, + Capella: &capella.SignedBeaconBlock{ + Message: testutil.RandomCapellaBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + { + name: "capella blinded", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionCapella, + CapellaBlinded: ð2capella.SignedBlindedBeaconBlock{ + Message: testutil.RandomCapellaBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + Blinded: true, + }, + }, + { + name: "deneb", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionDeneb, + Deneb: testutil.RandomDenebVersionedSignedProposal().Deneb, + }, + }, + { + name: "deneb blinded", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionDeneb, + DenebBlinded: ð2deneb.SignedBlindedBeaconBlock{ + Message: testutil.RandomDenebBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + Blinded: true, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p, err := core.NewVersionedSignedProposal(&test.proposal) + require.NoError(t, err) + + msgRoot, err := p.MessageRoot() + require.NoError(t, err) + require.NotEmpty(t, msgRoot) + + _, err = p.SetSignature(testutil.RandomCoreSignature()) + require.NoError(t, err) + + clone, err := p.Clone() + require.NoError(t, err) + require.Equal(t, p, clone) + + js, err := p.MarshalJSON() + require.NoError(t, err) + + p2 := &core.VersionedSignedProposal{} + err = p2.UnmarshalJSON(js) + require.NoError(t, err) + require.Equal(t, p, *p2) + + // Malformed data + err = p2.UnmarshalJSON([]byte("malformed")) + require.ErrorContains(t, err, "unmarshal block") + + if test.proposal.Version != eth2spec.DataVersionUnknown { + js := fmt.Sprintf(`{"version":%d,"blinded":%v,"block":123}`, test.proposal.Version-1, test.proposal.Blinded) + err = p2.UnmarshalJSON([]byte(js)) + require.ErrorContains(t, err, unmarshalPrefix+test.proposal.Version.String()) + } + }) + } +} + +func TestNewVersionedSignedBlindedProposal(t *testing.T) { + type testCase struct { + error string + proposal *eth2api.VersionedSignedBlindedProposal + } + + tests := []testCase{ + { + error: "unknown version", + proposal: ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersion(999), + }, + }, + { + error: "no bellatrix block", + proposal: ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersionBellatrix, + }, + }, + { + error: "no capella block", + proposal: ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersionCapella, + }, + }, + { + error: "no deneb block", + proposal: ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersionDeneb, + }, + }, + } + + for _, test := range tests { + t.Run(test.error, func(t *testing.T) { + _, err := core.NewVersionedSignedBlindedProposal(test.proposal) + require.ErrorContains(t, err, test.error) + }) + } + + t.Run("happy path", func(t *testing.T) { + proposal := testutil.RandomBellatrixVersionedSignedBlindedProposal() + + p, err := core.NewVersionedSignedBlindedProposal(&proposal.VersionedSignedBlindedProposal) + require.NoError(t, err) + require.Equal(t, proposal, p) + }) +} + +func TestNewPartialVersionedSignedBlindedProposal(t *testing.T) { + proposal := testutil.RandomBellatrixVersionedSignedBlindedProposal() + + psd, err := core.NewPartialVersionedSignedBlindedProposal(&proposal.VersionedSignedBlindedProposal, 3) + + require.NoError(t, err) + require.NotNil(t, psd.SignedData) + require.Equal(t, 3, psd.ShareIdx) +} + +func TestVersionedSignedBlindedProposal(t *testing.T) { + type testCase struct { + name string + proposal eth2api.VersionedSignedBlindedProposal + } + + tests := []testCase{ + { + name: "bellatrix", + proposal: testutil.RandomBellatrixVersionedSignedBlindedProposal().VersionedSignedBlindedProposal, + }, + { + name: "capella", + proposal: testutil.RandomCapellaVersionedSignedBlindedProposal().VersionedSignedBlindedProposal, + }, + { + name: "deneb", + proposal: testutil.RandomDenebVersionedSignedBlindedProposal().VersionedSignedBlindedProposal, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p, err := core.NewVersionedSignedBlindedProposal(&test.proposal) + require.NoError(t, err) + + msgRoot, err := p.MessageRoot() + require.NoError(t, err) + require.NotEmpty(t, msgRoot) + + _, err = p.SetSignature(testutil.RandomCoreSignature()) + require.NoError(t, err) + + clone, err := p.Clone() + require.NoError(t, err) + require.Equal(t, p, clone) + + js, err := p.MarshalJSON() + require.NoError(t, err) + + p2 := &core.VersionedSignedBlindedProposal{} + err = p2.UnmarshalJSON(js) + require.NoError(t, err) + require.Equal(t, p, *p2) + + // Malformed data + err = p2.UnmarshalJSON([]byte("malformed")) + require.ErrorContains(t, err, "unmarshal block") + + if test.proposal.Version != eth2spec.DataVersionUnknown { + js := fmt.Sprintf(`{"version":%d,"blinded":true,"block":123}`, test.proposal.Version-1) + err = p2.UnmarshalJSON([]byte(js)) + require.ErrorContains(t, err, unmarshalPrefix+test.proposal.Version.String()) + } + }) + } +} diff --git a/core/ssz.go b/core/ssz.go index 8367a51e4..05c6cac8d 100644 --- a/core/ssz.go +++ b/core/ssz.go @@ -46,17 +46,18 @@ func (p VersionedSignedProposal) MarshalSSZTo(buf []byte) ([]byte, error) { return nil, errors.Wrap(err, "invalid version") } - return marshalSSZVersionedTo(buf, version, p.sszValFromVersion) + return marshalSSZVersionedTo(buf, version, p.Blinded, p.sszValFromVersion) } // UnmarshalSSZ ssz unmarshals the VersionedSignedProposal object. func (p *VersionedSignedProposal) UnmarshalSSZ(buf []byte) error { - version, err := unmarshalSSZVersioned(buf, p.sszValFromVersion) + version, blinded, err := unmarshalSSZVersioned(buf, p.sszValFromVersion) if err != nil { return errors.Wrap(err, "unmarshal VersionedSignedProposal") } p.Version = version.ToETH2() + p.Blinded = blinded return nil } @@ -69,7 +70,7 @@ func (p VersionedSignedProposal) SizeSSZ() int { return 0 } - val, err := p.sszValFromVersion(version) + val, err := p.sszValFromVersion(version, p.Blinded) if err != nil { // SSZMarshaller interface doesn't return an error, so we can't either. return 0 @@ -79,7 +80,7 @@ func (p VersionedSignedProposal) SizeSSZ() int { } // sszValFromVersion returns the internal value of the VersionedSignedProposal object for a given version. -func (p *VersionedSignedProposal) sszValFromVersion(version eth2util.DataVersion) (sszType, error) { +func (p *VersionedSignedProposal) sszValFromVersion(version eth2util.DataVersion, blinded bool) (sszType, error) { switch version { case eth2util.DataVersionPhase0: if p.Phase0 == nil { @@ -94,21 +95,42 @@ func (p *VersionedSignedProposal) sszValFromVersion(version eth2util.DataVersion return p.Altair, nil case eth2util.DataVersionBellatrix: - if p.Bellatrix == nil { + if p.Bellatrix == nil && !blinded { p.Bellatrix = new(bellatrix.SignedBeaconBlock) } + if p.BellatrixBlinded == nil && blinded { + p.BellatrixBlinded = new(eth2bellatrix.SignedBlindedBeaconBlock) + } + + if blinded { + return p.BellatrixBlinded, nil + } return p.Bellatrix, nil case eth2util.DataVersionCapella: - if p.Capella == nil { + if p.Capella == nil && !blinded { p.Capella = new(capella.SignedBeaconBlock) } + if p.CapellaBlinded == nil && blinded { + p.CapellaBlinded = new(eth2capella.SignedBlindedBeaconBlock) + } + + if blinded { + return p.CapellaBlinded, nil + } return p.Capella, nil case eth2util.DataVersionDeneb: - if p.Deneb == nil { + if p.Deneb == nil && !blinded { p.Deneb = new(eth2deneb.SignedBlockContents) } + if p.DenebBlinded == nil && blinded { + p.DenebBlinded = new(eth2deneb.SignedBlindedBeaconBlock) + } + + if blinded { + return p.DenebBlinded, nil + } return p.Deneb, nil default: @@ -135,17 +157,18 @@ func (p VersionedProposal) MarshalSSZTo(buf []byte) ([]byte, error) { return nil, errors.Wrap(err, "invalid version") } - return marshalSSZVersionedTo(buf, version, p.sszValFromVersion) + return marshalSSZVersionedTo(buf, version, p.Blinded, p.sszValFromVersion) } // UnmarshalSSZ ssz unmarshalls the VersionedProposal object. func (p *VersionedProposal) UnmarshalSSZ(buf []byte) error { - version, err := unmarshalSSZVersioned(buf, p.sszValFromVersion) + version, blinded, err := unmarshalSSZVersioned(buf, p.sszValFromVersion) if err != nil { return errors.Wrap(err, "unmarshal VersionedProposal") } p.Version = version.ToETH2() + p.Blinded = blinded return nil } @@ -158,7 +181,7 @@ func (p VersionedProposal) SizeSSZ() int { return 0 } - val, err := p.sszValFromVersion(version) + val, err := p.sszValFromVersion(version, p.Blinded) if err != nil { // SSZMarshaller interface doesn't return an error, so we can't either. return 0 @@ -168,7 +191,7 @@ func (p VersionedProposal) SizeSSZ() int { } // sszValFromVersion returns the internal value of the VersionedBeaconBlock object for a given version. -func (p *VersionedProposal) sszValFromVersion(version eth2util.DataVersion) (sszType, error) { +func (p *VersionedProposal) sszValFromVersion(version eth2util.DataVersion, blinded bool) (sszType, error) { switch version { case eth2util.DataVersionPhase0: if p.Phase0 == nil { @@ -183,21 +206,42 @@ func (p *VersionedProposal) sszValFromVersion(version eth2util.DataVersion) (ssz return p.Altair, nil case eth2util.DataVersionBellatrix: - if p.Bellatrix == nil { + if p.Bellatrix == nil && !blinded { p.Bellatrix = new(bellatrix.BeaconBlock) } + if p.BellatrixBlinded == nil && blinded { + p.BellatrixBlinded = new(eth2bellatrix.BlindedBeaconBlock) + } + + if blinded { + return p.BellatrixBlinded, nil + } return p.Bellatrix, nil case eth2util.DataVersionCapella: - if p.Capella == nil { + if p.Capella == nil && !blinded { p.Capella = new(capella.BeaconBlock) } + if p.CapellaBlinded == nil && blinded { + p.CapellaBlinded = new(eth2capella.BlindedBeaconBlock) + } + + if blinded { + return p.CapellaBlinded, nil + } return p.Capella, nil case eth2util.DataVersionDeneb: - if p.Deneb == nil { + if p.Deneb == nil && !blinded { p.Deneb = new(eth2deneb.BlockContents) } + if p.DenebBlinded == nil && blinded { + p.DenebBlinded = new(eth2deneb.BlindedBeaconBlock) + } + + if blinded { + return p.DenebBlinded, nil + } return p.Deneb, nil default: @@ -224,15 +268,18 @@ func (p VersionedSignedBlindedProposal) MarshalSSZTo(buf []byte) ([]byte, error) return nil, errors.Wrap(err, "invalid version") } - return marshalSSZVersionedTo(buf, version, p.sszValFromVersion) + return marshalSSZVersionedTo(buf, version, true, p.sszValFromVersion) } // UnmarshalSSZ ssz unmarshalls the VersionedSignedBlindedProposal object. func (p *VersionedSignedBlindedProposal) UnmarshalSSZ(buf []byte) error { - version, err := unmarshalSSZVersioned(buf, p.sszValFromVersion) + version, blinded, err := unmarshalSSZVersioned(buf, p.sszValFromVersion) if err != nil { return errors.Wrap(err, "unmarshal VersionedSignedBlindedProposal") } + if !blinded { + return errors.New("expected blinded data") + } p.Version = version.ToETH2() @@ -247,7 +294,7 @@ func (p VersionedSignedBlindedProposal) SizeSSZ() int { return 0 } - val, err := p.sszValFromVersion(version) + val, err := p.sszValFromVersion(version, true) if err != nil { // SSZMarshaller interface doesn't return an error, so we can't either. return 0 @@ -257,7 +304,11 @@ func (p VersionedSignedBlindedProposal) SizeSSZ() int { } // sszValFromVersion returns the internal value of the VersionedSignedBlindedBeaconBlock object for a given version. -func (p *VersionedSignedBlindedProposal) sszValFromVersion(version eth2util.DataVersion) (sszType, error) { +func (p *VersionedSignedBlindedProposal) sszValFromVersion(version eth2util.DataVersion, blinded bool) (sszType, error) { + if !blinded { + return nil, errors.New("blinded param must be true") + } + switch version { case eth2util.DataVersionBellatrix: if p.Bellatrix == nil { @@ -282,97 +333,23 @@ func (p *VersionedSignedBlindedProposal) sszValFromVersion(version eth2util.Data } } -// ================== VersionedBlindedProposal =================== - -// MarshalSSZ ssz marshals the VersionedBlindedProposal object. -func (p VersionedBlindedProposal) MarshalSSZ() ([]byte, error) { - resp, err := ssz.MarshalSSZ(p) - if err != nil { - return nil, errors.Wrap(err, "marshal VersionedSignedBlindedBeaconBlock") - } - - return resp, nil -} - -// MarshalSSZTo ssz marshals the VersionedBlindedProposal object to a target array. -func (p VersionedBlindedProposal) MarshalSSZTo(buf []byte) ([]byte, error) { - version, err := eth2util.DataVersionFromETH2(p.Version) - if err != nil { - return nil, errors.Wrap(err, "invalid version") - } - - return marshalSSZVersionedTo(buf, version, p.sszValFromVersion) -} - -// UnmarshalSSZ ssz unmarshals the VersionedBlindedProposal object. -func (p *VersionedBlindedProposal) UnmarshalSSZ(buf []byte) error { - version, err := unmarshalSSZVersioned(buf, p.sszValFromVersion) - if err != nil { - return errors.Wrap(err, "unmarshal VersionedSignedBeaconBlock") - } - - p.Version = version.ToETH2() - - return nil -} - -// SizeSSZ returns the ssz encoded size in bytes for the VersionedBlindedProposal object. -func (p VersionedBlindedProposal) SizeSSZ() int { - version, err := eth2util.DataVersionFromETH2(p.Version) - if err != nil { - // SSZMarshaller interface doesn't return an error, so we can't either. - return 0 - } - - val, err := p.sszValFromVersion(version) - if err != nil { - // SSZMarshaller interface doesn't return an error, so we can't either. - return 0 - } - - return sizeSSZVersioned(val) -} - -// sszValFromVersion returns the internal value of the VersionedBlindedProposal object for a given version. -func (p *VersionedBlindedProposal) sszValFromVersion(version eth2util.DataVersion) (sszType, error) { - switch version { - case eth2util.DataVersionBellatrix: - if p.Bellatrix == nil { - p.Bellatrix = new(eth2bellatrix.BlindedBeaconBlock) - } - - return p.Bellatrix, nil - case eth2util.DataVersionCapella: - if p.Capella == nil { - p.Capella = new(eth2capella.BlindedBeaconBlock) - } - - return p.Capella, nil - case eth2util.DataVersionDeneb: - if p.Deneb == nil { - p.Deneb = new(eth2deneb.BlindedBeaconBlock) - } - - return p.Deneb, nil - default: - return nil, errors.New("invalid version") - } -} - // versionedOffset is the offset of a versioned ssz encoded object. -const versionedOffset = 8 + 4 // version (uint64) + offset (uint32) +const versionedOffset = 8 + 1 + 4 // version (uint64) + blinded (uint8) + offset (uint32) // marshalSSZVersionedTo marshals a versioned object to a target array. -func marshalSSZVersionedTo(dst []byte, version eth2util.DataVersion, valFunc func(eth2util.DataVersion) (sszType, error)) ([]byte, error) { +func marshalSSZVersionedTo(dst []byte, version eth2util.DataVersion, blinded bool, valFunc func(eth2util.DataVersion, bool) (sszType, error)) ([]byte, error) { // Field (0) 'Version' dst = ssz.MarshalUint64(dst, version.ToUint64()) - // Offset (1) 'Value' + // Field (1) 'Blinded' + dst = ssz.MarshalBool(dst, blinded) + + // Offset (2) 'Value' dst = ssz.WriteOffset(dst, versionedOffset) // TODO(corver): Add a constant length data version string field, ensure this is backwards compatible. - val, err := valFunc(version) + val, err := valFunc(version, blinded) if err != nil { return nil, errors.Wrap(err, "sszValFromVersion from version") } @@ -386,35 +363,38 @@ func marshalSSZVersionedTo(dst []byte, version eth2util.DataVersion, valFunc fun } // unmarshalSSZVersioned unmarshals a versioned object. -func unmarshalSSZVersioned(buf []byte, valFunc func(eth2util.DataVersion) (sszType, error)) (eth2util.DataVersion, error) { +func unmarshalSSZVersioned(buf []byte, valFunc func(eth2util.DataVersion, bool) (sszType, error)) (eth2util.DataVersion, bool, error) { if len(buf) < versionedOffset { - return "", errors.Wrap(ssz.ErrSize, "versioned object too short") + return "", false, errors.Wrap(ssz.ErrSize, "versioned object too short") } // Field (0) 'Version' version, err := eth2util.DataVersionFromUint64(ssz.UnmarshallUint64(buf[0:8])) if err != nil { - return "", errors.Wrap(err, "unmarshal sszValFromVersion version") + return "", false, errors.Wrap(err, "unmarshal sszValFromVersion version") } - // Offset (1) 'Value' - o1 := ssz.ReadOffset(buf[8:12]) + // Field (1) 'Blinded' + blinded := ssz.UnmarshalBool(buf[8:9]) + + // Offset (2) 'Value' + o1 := ssz.ReadOffset(buf[9:13]) if versionedOffset > o1 { - return "", errors.Wrap(ssz.ErrOffset, "sszValFromVersion offset", z.Any("version", version)) + return "", false, errors.Wrap(ssz.ErrOffset, "sszValFromVersion offset", z.Any("version", version), z.Bool("blinded", blinded)) } // TODO(corver): Add a constant length data version string field, ensure this is backwards compatible. - val, err := valFunc(version) + val, err := valFunc(version, blinded) if err != nil { - return "", errors.Wrap(err, "sszValFromVersion from version", z.Any("version", version)) + return "", false, errors.Wrap(err, "sszValFromVersion from version", z.Any("version", version), z.Bool("blinded", blinded)) } if err = val.UnmarshalSSZ(buf[o1:]); err != nil { - return "", errors.Wrap(err, "unmarshal sszValFromVersion", z.Any("version", version)) + return "", false, errors.Wrap(err, "unmarshal sszValFromVersion", z.Any("version", version), z.Bool("blinded", blinded)) } - return version, nil + return version, blinded, nil } // sizeSSZVersioned returns the ssz encoded size in bytes for a given versioned object. @@ -423,12 +403,12 @@ func sizeSSZVersioned(value sszType) int { } // VersionedSSZValueForT exposes the value method of a type for testing purposes. -func VersionedSSZValueForT(t *testing.T, value any, version eth2util.DataVersion) sszType { +func VersionedSSZValueForT(t *testing.T, value any, version eth2util.DataVersion, blinded bool) sszType { t.Helper() resp, err := value.(interface { - sszValFromVersion(eth2util.DataVersion) (sszType, error) - }).sszValFromVersion(version) + sszValFromVersion(version eth2util.DataVersion, blinded bool) (sszType, error) + }).sszValFromVersion(version, blinded) require.NoError(t, err) return resp diff --git a/core/ssz_test.go b/core/ssz_test.go index ab12bec76..7a2d73f5d 100644 --- a/core/ssz_test.go +++ b/core/ssz_test.go @@ -10,6 +10,15 @@ import ( "testing" "time" + eth2api "github.com/attestantio/go-eth2-client/api" + eth2bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" + eth2capella "github.com/attestantio/go-eth2-client/api/v1/capella" + eth2deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + eth2spec "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/capella" + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" @@ -61,7 +70,6 @@ func TestSSZ(t *testing.T) { {zero: func() any { return new(core.SignedSyncContributionAndProof) }}, {zero: func() any { return new(core.AggregatedAttestation) }}, {zero: func() any { return new(core.VersionedProposal) }}, - {zero: func() any { return new(core.VersionedBlindedProposal) }}, {zero: func() any { return new(core.SyncContribution) }}, } @@ -107,10 +115,6 @@ func TestMarshalUnsignedProto(t *testing.T) { dutyType: core.DutyProposer, unsignedPtr: func() any { return new(core.VersionedProposal) }, }, - { - dutyType: core.DutyBuilderProposer, - unsignedPtr: func() any { return new(core.VersionedBlindedProposal) }, - }, { dutyType: core.DutySyncContribution, unsignedPtr: func() any { return new(core.SyncContribution) }, @@ -185,10 +189,6 @@ func TestMarshalParSignedProto(t *testing.T) { dutyType: core.DutyProposer, signedPtr: func() any { return new(core.VersionedSignedProposal) }, }, - { - dutyType: core.DutyBuilderProposer, - signedPtr: func() any { return new(core.VersionedSignedBlindedProposal) }, - }, { dutyType: core.DutySyncContribution, signedPtr: func() any { return new(core.SignedSyncContributionAndProof) }, @@ -248,3 +248,194 @@ func TestMarshalParSignedProto(t *testing.T) { t.Logf("%s: ssz (%d) vs json (%d) == %.2f%%", typ, sszSize, jsonSize, 100*float64(sszSize)/float64(jsonSize)) } } + +func TestV3SignedProposalSSZSerialisation(t *testing.T) { + type testCase struct { + name string + proposal eth2api.VersionedSignedProposal + } + + tests := []testCase{ + { + name: "phase0", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionPhase0, + Phase0: ð2p0.SignedBeaconBlock{ + Message: testutil.RandomPhase0BeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + { + name: "altair", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionAltair, + Altair: &altair.SignedBeaconBlock{ + Message: testutil.RandomAltairBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + { + name: "bellatrix", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionBellatrix, + Bellatrix: &bellatrix.SignedBeaconBlock{ + Message: testutil.RandomBellatrixBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + { + name: "bellatrix blinded", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionBellatrix, + BellatrixBlinded: ð2bellatrix.SignedBlindedBeaconBlock{ + Message: testutil.RandomBellatrixBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + Blinded: true, + }, + }, + { + name: "capella", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionCapella, + Capella: &capella.SignedBeaconBlock{ + Message: testutil.RandomCapellaBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + { + name: "capella blinded", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionCapella, + CapellaBlinded: ð2capella.SignedBlindedBeaconBlock{ + Message: testutil.RandomCapellaBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + Blinded: true, + }, + }, + { + name: "deneb", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionDeneb, + Deneb: testutil.RandomDenebVersionedSignedProposal().Deneb, + }, + }, + { + name: "deneb blinded", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionDeneb, + DenebBlinded: ð2deneb.SignedBlindedBeaconBlock{ + Message: testutil.RandomDenebBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + Blinded: true, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p, err := core.NewVersionedSignedProposal(&test.proposal) + require.NoError(t, err) + + b, err := ssz.MarshalSSZ(p) + require.NoError(t, err) + + p2 := new(core.VersionedSignedProposal) + p2.Blinded = p.Blinded + err = p2.UnmarshalSSZ(b) + require.NoError(t, err) + require.Equal(t, p, *p2) + }) + } +} + +func TestV3ProposalSSZSerialisation(t *testing.T) { + type testCase struct { + name string + proposal eth2api.VersionedProposal + } + + tests := []testCase{ + { + name: "phase0", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionPhase0, + Phase0: testutil.RandomPhase0BeaconBlock(), + }, + }, + { + name: "altair", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionAltair, + Altair: testutil.RandomAltairBeaconBlock(), + }, + }, + { + name: "bellatrix", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionBellatrix, + Bellatrix: testutil.RandomBellatrixBeaconBlock(), + }, + }, + { + name: "bellatrix blinded", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionBellatrix, + BellatrixBlinded: testutil.RandomBellatrixBlindedBeaconBlock(), + Blinded: true, + }, + }, + { + name: "capella", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionCapella, + Capella: testutil.RandomCapellaBeaconBlock(), + }, + }, + { + name: "capella blinded", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionCapella, + CapellaBlinded: testutil.RandomCapellaBlindedBeaconBlock(), + Blinded: true, + }, + }, + { + name: "deneb", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionDeneb, + Deneb: testutil.RandomDenebVersionedProposal().Deneb, + }, + }, + { + name: "deneb blinded", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionDeneb, + DenebBlinded: testutil.RandomDenebBlindedBeaconBlock(), + Blinded: true, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p, err := core.NewVersionedProposal(&test.proposal) + require.NoError(t, err) + + b, err := ssz.MarshalSSZ(p) + require.NoError(t, err) + + p2 := new(core.VersionedProposal) + p2.Blinded = p.Blinded + err = p2.UnmarshalSSZ(b) + require.NoError(t, err) + require.Equal(t, p, *p2) + }) + } +} diff --git a/core/testdata/TestJSONSerialisation_VersionedSignedBlindedProposal.json.golden b/core/testdata/TestJSONSerialisation_VersionedSignedBlindedProposal.json.golden index 1b42a05a1..87b68f000 100644 --- a/core/testdata/TestJSONSerialisation_VersionedSignedBlindedProposal.json.golden +++ b/core/testdata/TestJSONSerialisation_VersionedSignedBlindedProposal.json.golden @@ -512,5 +512,6 @@ } }, "signature": "0xd136e45a22f2634dcdce80f368f457d4262844ae50bb6d098aeed272e3192c813e096cf652397f3b3f4b95795d3fd65ab9a164e0040cef446460dc5a51518234d3232eeb2630c1f27612093119ebcfaac8db946a6e4bd6825f27db9f6f6606cd" - } + }, + "blinded": true } \ No newline at end of file diff --git a/core/testdata/TestSSZSerialisation_VersionedBlindedProposal.ssz.golden b/core/testdata/TestSSZSerialisation_VersionedBlindedProposal.ssz.golden deleted file mode 100644 index 6a82b14ddf3bfdfd5a56adf6707e29b60221a6a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7317 zcmb7|Wm6nlvxNtOYZ%-mxVuZx;O?%2y96EFT>}ik-QAra!3mb&?(WXbIp_TYcUN_- zZ@c!hYE^gjh5`I{Q2t6Bgshx|bMOKhxyjB05zR?k+Gv}LVX%o(^WyxHG~=9#Dx7Ad zK8cy)R#S*i23BHzGU-7W8aNB@2P;!c7~B$P{(=Qo4y*Ph80=`4vAI+Kd#7ma??CN^ z1kdd)i8CxB-!~$(i~c(lIuknWTVg3d-Nh#T>T^gb6{37~@JeFN5$na4R5(jA*G4Y)0kGIWQnOqiGGs zLpbu>VBTH3DnL!YEuCwDm{E7Ow9nHC)VixncG>pm2VO zz-v3lBNdR(0YNd7;`6VsMQq;Euha;|SGdvusoHWB=wA`9ZIAqD;p1auIXQI0h?SJQ z*-2Kx`c4fCCPMwPN~v_lPl4@0LXvJ-O=XU*h#g5j_!+UKB^>j(Vq@2x{IEk{L;k-l)n6a})6;k^sO=`fG?C ztT@%}G<@>eWap^*BkT&a%Q;^R#NH0cgY~(RvnY6P9kET$lx@9Fd#7Fjg8Jp#u!T|m z^}#M(l!t23nhS~n=qe<;BqnNJhxzlh*ib)fwi|qxs*fwO{GMU-q7Akv?ubL#3L|BZ z7GR{+-wS{+l3$8e!Y;^UxNT?mh|+siEj*eZH)`0&HXw_M(g3bXM_92=#*LmI`@zN9 zjTFat+A5Rh+K*J;ewTIxvHYF1qFym;vO^NB(we$$uUE3nA38?M;9KCbp7Wk}c2>*G z`G$r{dESgGEm12$*DC>ojJ=pCOp>imAcm(mQg0elJm=;=w{r^|d%6LIht^}qS6g%- zh$_PPj7QVqW!s<>On$O#2bbV2`3fiGAdhtMi{e z?*iB&f3la+(S%|hAVciBH%X{xg!)`&Tud{?9?iFly%cO#!W#cHlvcM^`idgZiZ_jW zKaqjce>76SB!;3sYm@gff3*K7!bEo^#^5W%2iD~|^au_<#Q#P-H|i@Y;k^8Yn%FxQ z=Kk6z@@9&bITz0qN}YQ!GuTKLe@9y3*v)^S9D?e;>7lPJ3Wc-H))(@l9W6D%aNLE^ zMq@rrP=FtcT>T@NvshNMn3)b8gdzcKQc^FiBJq1vFWyD;;(&*J_&f~YGAgsB%GiE} zl-NV4*es}CD7pg_O5g#IXw_0IW2c=|@<1HVCVzOz7aHJBgX$+i@ z(vGwoY+5y-y@G>5ig8kcX*GRgSISI*eMmq_zs3;x7`Xo3qc2GW?u~`eVA66+`rIBN z$QCTKt74H|yO7{ucwR!iVJDoKJA0fJ@6X)HaZC7fK1zv^{6l1aRuTxcDPRHS3kf8O zjB(pSj(Bm5)3v3zLvN6(I1XIwXw9DBM8p_3A=CpwmF<}I56Ha6v-ve5fr<1t@;Zu;!(IjNU% z*cgkljh8*$eK|!FIIAu7LFkMyIPj6nW-|_OdE&@NXhV1HnAA|FRdFc^S(-+Klvn8s z;TFlR*XxDvqMhIzu5F`QPYbJuc&c05Q@dK=NB#tVM>19zH;Q}@kVHPOkS}AJF&HXh z4IvYEjyO@X4Cv^5ck~hD0*9xUW0qr}uWR(W@o@V;x4`X8q4H?(-=ch>51Qo&)W<5swdB0dj-+~C?4*_6b~ndY{Q`FXzbiA z3L#D+;jWFot*P*YV%zNCAxCS%!BONgzu+%A?pk6Q|$%$wgZp z^%f7=1H%UBzij>;od9X*zcci&a{isAJAZXt`zJ0@bK5;cw^#Vw82NG$Nn@sA%kR<| z$Oi0jSqA&Z3up$dZDk@?!=U=o!_d7%B7ibG>q$xq&wBehRilo>D>zT^y>qu1~E=OLu zc8){z4hlD=K7r!OFjQ>(l2S+QHoFQpr0+2>lo9L)QM0SYWNR?_DDzN_xXC;dw6&~( zc{M>y<6=P$*?z{dPU5!LQa1#*_V7x<#hy!ya0&uTk_D5sWQRju&C2>3Mk*gD-P7^& z7yA=0sKGWWYNj7OM8Z=eIic)S)JTZV_oRBt*M&}oOd#d0@WwKF{-MhD?JzOHG}<*C zFOCorp05qLKzDE)+X4RZa)wfCoh}qj&WJwwMbanFv`6!@C6ofksgsfNV9~O-uxX1g z5cKWGo-3ScrhEOt&)@zn$6)s*nl!;(l^&xG)zkMnaO~&X%hgsBOR+(NE+FLgoUQNT zB{C!CYsf&UX8&afpLn8Xl|`nNtqT#Mc!*EQ=W1}Oua)o4jgF}Sp1FL_$sY{(pCxc` zWANqlpl&WjzTOPEvj@4d(|__K_`m%8eK6?FCCU+5klqb`Uysup?_$G5tf!^XGZv zRU+8b$yGZ(*9q=;iE_`+3FIl#^F`OJyr*(!F7a`nI6+tV?G$*y3J42Ps79GtD|eX> zUkE6M1`4i2=Dnp=zD+uj3Pd3{_U@9vii(2q_TvKh#b?HQS#4CPOeU{jBnZQtO9jgd7KT=y=YlAzMoO71<`ye*^{Y3uWcJaj?|4t zeAIO9K`q=#6EW48(jm2VL$+r#qU~Mr7Pgh=ke%C~5CZ+N=I>eSGTydtp>p6p53P92SFd}vBRJ!oUXo?}9Gkn&NooDuKXBCEMvNPkR}}$=_iBYDu_#UK@QMf5i=#SlHG!hGbWk+pAr`6VkC~?Z{|e zH4ovDdMnIZxr+Hr8BzZA0OfCfuK(7`-+t8p0RX6g0RXf4(v(0S+|eoa>G1@KjTt z+v7h3M$A=A@t=*qP3gGVzHo1yIJiy?b3590%*f7zyB)W5Ldp@TfrVQsrMX*7K31LI zB?M^=jY`OW)8BdfY_b?~%fUxhe&~4ad6qD`Tip7TFms@MIWn)`tsL_i_7&#^%vaC? zt;|zIB%q+!t4&08+9wS4Pae)hQ>M(@H1SsP+RDbepo7^g3WBs=@2w-nEsImn5bXZ$ zJu1`ujiO)kp?k~kv%Kq9lCk_KNA8FWV-#t>BGz(aCsdLvce9Zr$GCZGn3PDkdQ;%2 zL&a7bYj{Kmmots$U#{bB*vrG)?nZeo+6uW@>@%~R~rOAN#{h;BYk+Km{a=MC)ecIlJykY3yfgt{lqP~oXNGqQRE}5 z80GE<=^O=PIhVZxBIqcfM=|6CsAj&vPAm%DUOi@`(E$%5ssVI|w_M7j z-K7+XtG1ro=w($`_`KO9&ld@*^s_9u#3MC}sZt`!&nX*b0KqS!5EvswJqdr?fRkD- zPUN3Jv`+DZ?AY{^3r%vmtDy~5FHHI1F*-Z3Y}4C=pN!CFe)k`@?aS4YU7FPV)GYjY zqiV+o91s2leag-q_f3!_UHKESGES`dj?(3_u3#^l`2AZwNjpS_RC6Qi5u>NZ9olpuAJMvy zRe!uRHn^ofz=;*2gB3|q12uO-eDM*GBiHj7nb~qFfT2*lyg3hu3`NHqAm~|-R$$*!hPA`cXY|5GX)RWOj(g9!Aa>gbK=hBea95CNb%|llf6!LYtQqqWI zGS>mqjC8W_!**}$>LaobuB`Nn@Ks~M$PiF^Xyd0KCGkWkaVA>%(+3(&X9-hi54Dg|p#|9pRXW{b=YMWZfZrN0V59k1lTaS8K9= ztlGW_ADSD}_=3MFPNsX+70LNSw>fBXt!i;Av1|d{4mN8W-+zFPh#3;clr8*mHhESlGK&$zQZ#{Yuq zca_)MeNHI)Ba%_fJhG2ox=;59c)b}A`6r#L`qH`ai*A59YC+aF)5dM9nfzD*;4tmt z?(hcu?E4!$Fob5JNKvFQ6KkdYA)G~D$5aEudMhASp#t+JlTf^OtVWUNNIShsGBy(} zXAG&BAqmd)JYMgVgmHBQmi|}ljqHhZ9xozXFrW6L)2fqJi0(M3bV?XWbO1KN)PE-j zDn@g8ze5R!PI5+ze)S#WvF)ulj8+Ksv-#5iMQK=Xq>(K&nK`!ovtAdDvFsx6cJczd zUPGyNLw^YK(Q*}TKT<&5?`*xx17WlX$6)T~4X+ax zXpEmthg}syy6C$(VtyIkOioTSc6d*DP3R(cxr4~set$BZHmS01Sh@-IgW}rgWdk=h z1FMQgRXO_T7HkfhU~y@Qkol9mGhow5wNs!jjcPfnrr?{$eX_-Ug-VMT14=k%7=Z&J z9PNuz(Txgbd6_)oQDp@0)WwKKb3Hx-+Z$C^Cz3i6^5A}o5Tna$t1_8LbIT=?F=4(a z*iTC~Aj>P@a?_iwca6tXT`>AOs!^i0{!!KD-N@=^{&DMXDpknPORlH6*a8WA~D-+GwV?ToEx;$>&U= zDC^9jB?v4*8<$BsOYyhopRlMqoGrfkGCKD1LXAYteaSwQ~!fcToi`mp3bLF(3=+p|wR}%~I1pJ_}3smtt04uTyqd3A;0j5!`Ey<5cj;lxJ53&ftbwPL`rY+GObK4LO!-TO!ptDc zUk8QrYHD=VwcJa?fK-bDJqhd%m@;z)fN$M2Nhyp*B;h2fbR zAYPbfw?KCz|%tXqT+-Sw;aMIX8C+iyBDik3fF_}gX zO%6HiXXPqJbS>aKZCibhA4XW{y%>agl>GX@(!ag^WZ8sT5_)lT&x_tq`+Y(_qfRMNK=tfJn$>DB?2}IZ{Rh2xMAFCqH4xO9RnZQ0SN;K3~D{ z=v%9qtpI1S%!;{%uHJyVZ?oC_Gu%766jx>n6O#egRySI6)U*=S${qHIQD?tgw(%JQ zg%R!URQdb`MC4Mv3`7=m+w#meU{KB_%#BxcRaAmFXFo9?Q?vG6c>odF@Q)jA)i~96@jsygXQBUh>((@~_+z87^=)!r>V8ikRS;eg?vY{k}@#yF|ZFNlAN1PFW{$zv9<%~ug zdL;4!H{q~aRSS0vs{2aeuBxP|1x5i5JeS<*KkGKczm)2a+j{#wI^#=Wu@)fzfka zNqq!_*;Y!M(vJH(_~WWasW+2YC18rC&BJpN|2VSkg_o>~{BVJplo}UdAkJmZqI)fN zuzLc?))_*QVQM(qyr zZy~q=E@LZ){ed*SSqO2%HVx!K#~k^At_kNYs|9OVv+<|rh8F909PP36RGYbO4+Msl zg7$nIX0xr#Of8HXklTbED}iR}qe%E{!o_}WCN$9#${Vu$H8{Sk5}+N4W@~5Vz&x@LZ~?M4c=P>27nEVo0h9h*p>s#=_7f*;uyT*xvCnCfne4cD z=w9Ufa0aU0J#Id%nGh+ZuMibn3I2H4%1%mMTjIC>yoG8lClt~xN62L{Vf*z3C73ue zj^)`}&)o4B@3UrV3-!}b9z^$`!KqYc!8@7ONT>eeLF#wo>tu6m(MxyXL`Hg@D7{N0 za|Vw3DdpFgT7W%sLFXs3MHtV|C9?ZgN65?!Gnuz)$+f)n9gaSXDlJ+b0lpPWbza`f zr%I=X3^{FY1BCe)?ZYZqTviOM(P)T|WNIH5mhKu*I6k&W!~*xo&G*02egOL3?SXd7 ztnGnEa#GkNnr`n7+XTbvRV5MV73E=q@DA?tSG^TYlJ)g9?%zj|rnA}h2WGL*L=n|~ zo``*!_Uw55t0 z+NvkGaga>2Hb%RKBH6U|E4Cm|x=t|d1U>a=U&0Bbug_9c#ojbPZTvc{g&VC; not just a single DV. This allows the workflow to aggregate and batch multiple DVs in some steps, specifically consensus. > Which is critical for clusters with a large number of DVs. +### DutyBuilderProposer deprecation +The new version of Beacon API spec introduced [produceBlockV3](https://ethereum.github.io/beacon-APIs/#/Validator/produceBlockV3) endpoint, +which is now fully supported by Charon (since v1). + +Previously, `DutyProposer` and `DutyBuilderProposer` served full and blinded blocks respectively. +Now, `DutyProposer` serves both full and blinded blocks. To this end, the serialization logic incorporated `Blinded` flag, +which is used across many components to determine the corresponding block type. +The deprecated `DutyBuilderProposer` definition is kept in the codebase for testing period and will be removed in the future. +Every component hitting `DutyBuilderProposer`, must return `ErrDeprecatedDutyBuilderProposer` error. + +The new v3 endpoint specification added a few new parameters and this is how Charon handles them: +* `builder_boost_factor`: Charon overrides VC's value in according with Builder API flag: when the flag is `true`, Charon sets `builder_boost_factor` to `math.MaxUint64`, otherwise to `0`. To guarantee consistency, all Charon nodes in a cluster shall have the same Builder API flag. +* `Eth-Execution-Payload-Value` and `Eth-Consensus-Block-Value` are always set to `1`, since these are required parameters. Charon does not propagate BN's values to VC to avoid potential inconsistencies caused by different BN providers. + +> ℹ️ The change in serialization (both json and ssz) introduced *a breaking change* in the internal protocol. +Therefore, Charon v1.x will not work together with Charon v0.x. See *Version compatibility* section of `README.md` for more details. + ### Scheduler The scheduler is the initiator of a duty in the core workflow. It resolves the which DVs in the cluster are active and @@ -331,10 +348,6 @@ type DutyDB interface { // for the slot when available. It also returns the DV public key. AwaitBeaconBlock(context.Context, slot int) (PubKey, beaconapi.BeaconBlock, error) - // AwaitBlindedBeaconBlock blocks and returns the proposed blinded beacon block - // for the slot when available. It also returns the DV public key. - AwaitBlindedBeaconBlock(context.Context, slot int) (PubKey, beaconapi.BlindedBeaconBlock, error) - // AwaitAttestation blocks and returns the attestation data // for the slot and committee index when available. AwaitAttestation(context.Context, slot int, commIdx int) (*beaconapi.AttestationData, error) @@ -388,20 +401,13 @@ The validator API provides the following beacon-node endpoints relating to dutie - The request arguments are: `slot` and `committee_index` - Query the `DutyDB` `AwaitAttester` with `slot` and `committee_index` - Serve response -- `GET /eth/v2/validator/blocks/{slot}` Produce a new block, without signature. - - The request arguments are: `slot` and `randao_reveal` +- `GET /eth/v3/validator/blocks/{slot}` Produce a new (full or blinded) block, without signature. + - The request arguments are: `slot`, `randao_reveal` and `builder_boost_factor` - Lookup `PubKey` by querying the `Scheduler` `AwaitProposer` with the slot in the request body. - Verify `randao_reveal` signature. - Construct a `DutyRandao` `ParSignedData` and submit it to `ParSigDB` for async aggregation and inclusion in block consensus. - Query the `DutyDB` `AwaitBeaconBlock` with the `slot` - Serve response -- `GET /eth/v1/validator/blinded_blocks/{slot}` Produce a new blinded block, without signature. - - The request arguments are: `slot` and `randao_reveal` - - Lookup `PubKey` by querying the `Scheduler` `AwaitBuilderProposer` with the slot in the request body. - - Verify `randao_reveal` signature. - - Construct a `DutyRandao` `ParSignedData` and submit it to `ParSigDB` for async aggregation and inclusion in block consensus. - - Query the `DutyDB` `AwaitBlindedBeaconBlock` with the `slot` - - Serve response - `POST /eth/v1/beacon/pool/attestations` Submit Attestation objects to node - Construct a `ParSignedData` for each attestation object in request body. - Infer `PubKey` of the request by querying the `DutyDB` `PubKeyByAttestation` with the `slot`, `committee index` and `aggregation bits` provided in the request body. @@ -436,9 +442,6 @@ type ValidatorAPI interface { // RegisterAwaitProposal registers a function to query unsigned beacon block proposals by providing the slot. RegisterAwaitProposal(func(ctx context.Context, slot uint64) (*eth2api.VersionedProposal, error)) - // RegisterAwaitBlindedProposal registers a function to query unsigned blinded beacon block proposals by providing the slot. - RegisterAwaitBlindedProposal(func(ctx context.Context, slot uint64) (*eth2api.VersionedBlindedProposal, error)) - // RegisterAwaitAttestation registers a function to query attestation data. RegisterAwaitAttestation(func(ctx context.Context, slot, commIdx uint64) (*eth2p0.AttestationData, error)) @@ -468,7 +471,7 @@ which authorises charon to request adhoc signatures from a [remote signer instan In the _middleware_ architecture charon cannot initiate signatures itself and has to wait for the VC to submit signatures. -Duties originating in the `scheduler` (`DutyAttester`, `DutyProposer`, `DutyBuilderProposer`) are not significantly affected by this change in architecture. +Duties originating in the `scheduler` (`DutyAttester`, `DutyProposer`) are not significantly affected by this change in architecture. Instead of waiting for the `validatorapi` to submit signatures, these duties directly request signatures from the remote signer instance. The flow is otherwise unaffected. diff --git a/go.mod b/go.mod index 2040da637..c574c5b1b 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/obolnetwork/charon go 1.21 require ( - github.com/attestantio/go-eth2-client v0.19.10 + github.com/attestantio/go-eth2-client v0.21.1 github.com/bufbuild/buf v1.31.0 github.com/coinbase/kryptology v1.5.6-0.20220316191335-269410e1b06b github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 @@ -171,6 +171,7 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/vbatts/tar-split v0.11.5 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 // indirect diff --git a/go.sum b/go.sum index 898ee7d4b..0d261aa31 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,8 @@ github.com/ObolNetwork/kryptology v0.0.0-20231016091344-eed023b6cac8/go.mod h1:q github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= -github.com/attestantio/go-eth2-client v0.19.10 h1:NLs9mcBvZpBTZ3du7Ey2NHQoj8d3UePY7pFBXX6C6qs= -github.com/attestantio/go-eth2-client v0.19.10/go.mod h1:TTz7YF6w4z6ahvxKiHuGPn6DbQn7gH6HPuWm/DEQeGE= +github.com/attestantio/go-eth2-client v0.21.1 h1:yvsMd/azPUbxiJzWZhgqfOJJRNF1zLvAJpcBXTHzyh8= +github.com/attestantio/go-eth2-client v0.21.1/go.mod h1:Tb412NpzhsC0sbtpXS4D51y5se6nDkWAi6amsJrqX9c= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= diff --git a/testutil/beaconmock/beaconmock.go b/testutil/beaconmock/beaconmock.go index 08c64044e..95d7af521 100644 --- a/testutil/beaconmock/beaconmock.go +++ b/testutil/beaconmock/beaconmock.go @@ -98,6 +98,8 @@ func defaultHTTPMock() Mock { Value: fmt.Sprint(genesis.Unix()), }, }, + IsActiveFunc: func() bool { return true }, + IsSyncedFunc: func() bool { return true }, } } @@ -119,18 +121,19 @@ type Mock struct { headProducer *headProducer forkVersion [4]byte + IsActiveFunc func() bool + IsSyncedFunc func() bool ActiveValidatorsFunc func(ctx context.Context) (eth2wrap.ActiveValidators, error) AttestationDataFunc func(context.Context, eth2p0.Slot, eth2p0.CommitteeIndex) (*eth2p0.AttestationData, error) AttesterDutiesFunc func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.AttesterDuty, error) BlockAttestationsFunc func(ctx context.Context, stateID string) ([]*eth2p0.Attestation, error) NodePeerCountFunc func(ctx context.Context) (int, error) ProposalFunc func(ctx context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) - BlindedProposalFunc func(ctx context.Context, opts *eth2api.BlindedProposalOpts) (*eth2api.VersionedBlindedProposal, error) SignedBeaconBlockFunc func(ctx context.Context, blockID string) (*eth2spec.VersionedSignedBeaconBlock, error) ProposerDutiesFunc func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) SubmitAttestationsFunc func(context.Context, []*eth2p0.Attestation) error - SubmitProposalFunc func(context.Context, *eth2api.VersionedSignedProposal) error - SubmitBlindedProposalFunc func(context.Context, *eth2api.VersionedSignedBlindedProposal) error + SubmitProposalFunc func(context.Context, *eth2api.SubmitProposalOpts) error + SubmitBlindedProposalFunc func(context.Context, *eth2api.SubmitBlindedProposalOpts) error SubmitVoluntaryExitFunc func(context.Context, *eth2p0.SignedVoluntaryExit) error ValidatorsByPubKeyFunc func(context.Context, string, []eth2p0.BLSPubKey) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) ValidatorsFunc func(context.Context, *eth2api.ValidatorsOpts) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) @@ -189,16 +192,7 @@ func (m Mock) Proposal(ctx context.Context, opts *eth2api.ProposalOpts) (*eth2ap return wrapResponse(block), nil } -func (m Mock) BlindedProposal(ctx context.Context, opts *eth2api.BlindedProposalOpts) (*eth2api.Response[*eth2api.VersionedBlindedProposal], error) { - block, err := m.BlindedProposalFunc(ctx, opts) - if err != nil { - return nil, err - } - - return wrapResponse(block), nil -} - -func (m Mock) SubmitBlindedProposal(ctx context.Context, block *eth2api.VersionedSignedBlindedProposal) error { +func (m Mock) SubmitBlindedProposal(ctx context.Context, block *eth2api.SubmitBlindedProposalOpts) error { return m.SubmitBlindedProposalFunc(ctx, block) } @@ -220,7 +214,7 @@ func (m Mock) NodeSyncing(ctx context.Context, opts *eth2api.NodeSyncingOpts) (* return wrapResponse(schedule), nil } -func (m Mock) SubmitProposal(ctx context.Context, block *eth2api.VersionedSignedProposal) error { +func (m Mock) SubmitProposal(ctx context.Context, block *eth2api.SubmitProposalOpts) error { return m.SubmitProposalFunc(ctx, block) } @@ -357,6 +351,14 @@ func (m Mock) Address() string { return "http://" + m.httpServer.Addr } +func (m Mock) IsActive() bool { + return m.IsActiveFunc() +} + +func (m Mock) IsSynced() bool { + return m.IsSyncedFunc() +} + func (m Mock) Close() error { m.headProducer.Close() diff --git a/testutil/beaconmock/beaconmock_fuzz.go b/testutil/beaconmock/beaconmock_fuzz.go index 5ce7396e4..9ce99b362 100644 --- a/testutil/beaconmock/beaconmock_fuzz.go +++ b/testutil/beaconmock/beaconmock_fuzz.go @@ -129,13 +129,6 @@ func WithBeaconMockFuzzer() Option { return vals, nil } - mock.BlindedProposalFunc = func(ctx context.Context, opts *eth2api.BlindedProposalOpts) (*eth2api.VersionedBlindedProposal, error) { - var block *eth2api.VersionedBlindedProposal - fuzz.New().Fuzz(&block) - - return block, nil - } - mock.NodePeerCountFunc = func(context.Context) (int, error) { var count int fuzz.New().Fuzz(&count) diff --git a/testutil/beaconmock/options.go b/testutil/beaconmock/options.go index 3e71d93d2..71b3c8a87 100644 --- a/testutil/beaconmock/options.go +++ b/testutil/beaconmock/options.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "math/big" "net/http" "sort" "strings" @@ -497,24 +498,29 @@ func defaultMock(httpMock HTTPMock, httpServer *http.Server, clock clockwork.Clo httpServer: httpServer, headProducer: headProducer, ProposalFunc: func(ctx context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) { - block := ð2api.VersionedProposal{ - Version: eth2spec.DataVersionCapella, - Capella: testutil.RandomCapellaBeaconBlock(), - } - block.Capella.Slot = opts.Slot - block.Capella.Body.RANDAOReveal = opts.RandaoReveal - block.Capella.Body.Graffiti = opts.Graffiti - - return block, nil - }, - BlindedProposalFunc: func(ctx context.Context, opts *eth2api.BlindedProposalOpts) (*eth2api.VersionedBlindedProposal, error) { - block := ð2api.VersionedBlindedProposal{ - Version: eth2spec.DataVersionCapella, - Capella: testutil.RandomCapellaBlindedBeaconBlock(), + var block *eth2api.VersionedProposal + if opts.BuilderBoostFactor == nil || *opts.BuilderBoostFactor == 0 { + block = ð2api.VersionedProposal{ + Version: eth2spec.DataVersionCapella, + Capella: testutil.RandomCapellaBeaconBlock(), + } + block.Capella.Slot = opts.Slot + block.Capella.Body.RANDAOReveal = opts.RandaoReveal + block.Capella.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + } else { + block = ð2api.VersionedProposal{ + Version: eth2spec.DataVersionCapella, + CapellaBlinded: testutil.RandomCapellaBlindedBeaconBlock(), + } + block.CapellaBlinded.Slot = opts.Slot + block.CapellaBlinded.Body.RANDAOReveal = opts.RandaoReveal + block.CapellaBlinded.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + block.Blinded = true } - block.Capella.Slot = opts.Slot - block.Capella.Body.RANDAOReveal = opts.RandaoReveal - block.Capella.Body.Graffiti = opts.Graffiti return block, nil }, @@ -559,10 +565,10 @@ func defaultMock(httpMock HTTPMock, httpServer *http.Server, clock clockwork.Clo SubmitAttestationsFunc: func(context.Context, []*eth2p0.Attestation) error { return nil }, - SubmitProposalFunc: func(context.Context, *eth2api.VersionedSignedProposal) error { + SubmitProposalFunc: func(context.Context, *eth2api.SubmitProposalOpts) error { return nil }, - SubmitBlindedProposalFunc: func(context.Context, *eth2api.VersionedSignedBlindedProposal) error { + SubmitBlindedProposalFunc: func(context.Context, *eth2api.SubmitBlindedProposalOpts) error { return nil }, SubmitVoluntaryExitFunc: func(context.Context, *eth2p0.SignedVoluntaryExit) error { diff --git a/testutil/compose/smoke/smoke_test.go b/testutil/compose/smoke/smoke_test.go index 626f5e6cc..572267d53 100644 --- a/testutil/compose/smoke/smoke_test.go +++ b/testutil/compose/smoke/smoke_test.go @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/require" - "github.com/obolnetwork/charon/app/version" "github.com/obolnetwork/charon/testutil" "github.com/obolnetwork/charon/testutil/compose" ) @@ -86,25 +85,26 @@ func TestSmoke(t *testing.T) { }, Timeout: time.Minute * 2, }, - { - Name: "run_version_matrix_with_dkg", - PrintYML: true, - ConfigFunc: func(conf *compose.Config) { - conf.KeyGen = compose.KeyGenDKG - // NOTE: Add external VCs when supported versions include minimal preset. - conf.VCs = []compose.VCType{compose.VCMock} - }, - DefineTmplFunc: func(data *compose.TmplData) { - // Use oldest supported version for cluster lock - pegImageTag(data.Nodes, 0, last(version.Supported()[1:])+"-rc") - }, - RunTmplFunc: func(data *compose.TmplData) { - // Node 0 is local build - pegImageTag(data.Nodes, 1, nth(version.Supported(), 0)+"-dev") // Node 1 is previous commit on this branch (v0.X-dev/rc) Note this will fail for first commit on new branch version. - pegImageTag(data.Nodes, 2, nth(version.Supported()[1:], 1)+"-rc") - pegImageTag(data.Nodes, 3, nth(version.Supported()[1:], 2)+"-rc") - }, - }, + // TODO: https://github.com/ObolNetwork/charon/issues/3004 + // { + // Name: "run_version_matrix_with_dkg", + // PrintYML: true, + // ConfigFunc: func(conf *compose.Config) { + // conf.KeyGen = compose.KeyGenDKG + // // NOTE: Add external VCs when supported versions include minimal preset. + // conf.VCs = []compose.VCType{compose.VCMock} + // }, + // DefineTmplFunc: func(data *compose.TmplData) { + // // Use oldest supported version for cluster lock + // pegImageTag(data.Nodes, 0, last(version.Supported()[1:])+"-rc") + // }, + // RunTmplFunc: func(data *compose.TmplData) { + // // Node 0 is local build + // pegImageTag(data.Nodes, 1, nth(version.Supported(), 0)+"-dev") // Node 1 is previous commit on this branch (v0.X-dev/rc) Note this will fail for first commit on new branch version. + // pegImageTag(data.Nodes, 2, nth(version.Supported()[1:], 1)+"-rc") + // pegImageTag(data.Nodes, 3, nth(version.Supported()[1:], 2)+"-rc") + // }, + // }, { Name: "teku_versions", ConfigFunc: func(conf *compose.Config) { @@ -214,17 +214,17 @@ func TestSmoke(t *testing.T) { // pegImageTag pegs the charon docker image tag for one of the nodes. // It overrides the default that uses locally built latest version. -func pegImageTag(nodes []compose.TmplNode, index int, imageTag string) { - nodes[index].ImageTag = imageTag - nodes[index].Entrypoint = "/usr/local/bin/charon" // Use contains binary, not locally built latest version. -} - -// last returns the last element of a slice. -func last(s []version.SemVer) string { - return s[len(s)-1].String() -} - -// nth returns the nth element of a slice, wrapping if n > len(s). -func nth(s []version.SemVer, n int) string { - return s[n%len(s)].String() -} +// func pegImageTag(nodes []compose.TmplNode, index int, imageTag string) { +// nodes[index].ImageTag = imageTag +// nodes[index].Entrypoint = "/usr/local/bin/charon" // Use contains binary, not locally built latest version. +// } + +// // last returns the last element of a slice. +// func last(s []version.SemVer) string { +// return s[len(s)-1].String() +// } + +// // nth returns the nth element of a slice, wrapping if n > len(s). +// func nth(s []version.SemVer, n int) string { +// return s[n%len(s)].String() +// } diff --git a/testutil/fuzz.go b/testutil/fuzz.go index d3513f64a..9025758e2 100644 --- a/testutil/fuzz.go +++ b/testutil/fuzz.go @@ -110,7 +110,7 @@ func NewEth2Fuzzer(t *testing.T, seed int64) *fuzz.Fuzzer { version, err := eth2util.DataVersionFromETH2(e.Version) require.NoError(t, err) - val := core.VersionedSSZValueForT(t, e, version) + val := core.VersionedSSZValueForT(t, e, version, true) c.Fuzz(val) // Limit length of blob KZG commitments to 6 @@ -120,20 +120,12 @@ func NewEth2Fuzzer(t *testing.T, seed int64) *fuzz.Fuzzer { e.Deneb.Message.Body.BlobKZGCommitments = e.Deneb.Message.Body.BlobKZGCommitments[:maxBlobCommitments] } }, - func(e *core.VersionedBlindedProposal, c fuzz.Continue) { - e.Version = blindedVersions[(c.Intn(len(blindedVersions)))] - version, err := eth2util.DataVersionFromETH2(e.Version) - require.NoError(t, err) - - val := core.VersionedSSZValueForT(t, e, version) - c.Fuzz(val) - }, func(e *core.VersionedSignedProposal, c fuzz.Continue) { e.Version = allVersions[(c.Intn(len(allVersions)))] version, err := eth2util.DataVersionFromETH2(e.Version) require.NoError(t, err) - val := core.VersionedSSZValueForT(t, e, version) + val := core.VersionedSSZValueForT(t, e, version, false) c.Fuzz(val) // Limit length of KZGProofs and Blobs to 6 @@ -152,7 +144,7 @@ func NewEth2Fuzzer(t *testing.T, seed int64) *fuzz.Fuzzer { version, err := eth2util.DataVersionFromETH2(e.Version) require.NoError(t, err) - val := core.VersionedSSZValueForT(t, e, version) + val := core.VersionedSSZValueForT(t, e, version, false) c.Fuzz(val) // Limit length of KZGProofs and Blobs to 6 diff --git a/testutil/integration/simnet_test.go b/testutil/integration/simnet_test.go index 06e2b1190..676547cb4 100644 --- a/testutil/integration/simnet_test.go +++ b/testutil/integration/simnet_test.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path" + "strconv" "strings" "sync" "testing" @@ -77,23 +78,16 @@ func TestSimnetDuties(t *testing.T) { vcType: vcVmock, }, { - name: "proposer with teku", - scheduledType: core.DutyProposer, - duties: []core.DutyType{core.DutyProposer, core.DutyRandao}, - vcType: vcTeku, - }, - { - name: "builder proposer with mock VCs", + name: "proposer with mock VCs with builder API", scheduledType: core.DutyProposer, - duties: []core.DutyType{core.DutyBuilderRegistration, core.DutyBuilderProposer, core.DutyRandao}, - builderAPI: true, + duties: []core.DutyType{core.DutyBuilderRegistration, core.DutyProposer, core.DutyRandao}, vcType: vcVmock, + builderAPI: true, }, { - name: "builder proposer with teku", + name: "proposer with teku", scheduledType: core.DutyProposer, - duties: []core.DutyType{core.DutyBuilderProposer, core.DutyRandao, core.DutyBuilderRegistration}, - builderAPI: true, + duties: []core.DutyType{core.DutyProposer, core.DutyRandao}, vcType: vcTeku, }, { @@ -410,6 +404,8 @@ var ( "validator-client", "--network=auto", "--log-destination=console", + "--Xblock-v3-enabled=true", + "--validators-external-signer-slashing-protection-enabled=true", "--validators-proposer-default-fee-recipient=0x000000000000000000000000000000000000dead", } tekuExit tekuCmd = []string{ @@ -450,7 +446,7 @@ func startTeku(t *testing.T, args simnetArgs, node int) simnetArgs { tekuArgs = append(tekuArgs, cmd...) tekuArgs = append(tekuArgs, "--validator-keys=/keys:/keys", - fmt.Sprintf("--beacon-node-api-endpoint=http://%s", args.VAPIAddrs[node]), + "--beacon-node-api-endpoint=http://"+args.VAPIAddrs[node], ) if args.TekuRegistration { @@ -466,14 +462,14 @@ func startTeku(t *testing.T, args simnetArgs, node int) simnetArgs { } // Configure docker - name := fmt.Sprint(time.Now().UnixNano()) + name := strconv.FormatInt(time.Now().UnixNano(), 10) dockerArgs := []string{ "run", "--rm", - fmt.Sprintf("--name=%s", name), + "--name=" + name, fmt.Sprintf("--volume=%s:/keys", tempDir), "--user=root", // Root required to read volume files in GitHub actions. - "consensys/teku:23.11.0", + "consensys/teku:24.3.1", } dockerArgs = append(dockerArgs, tekuArgs...) t.Logf("docker args: %v", dockerArgs) diff --git a/testutil/random.go b/testutil/random.go index 60fafcf1d..7636abacb 100644 --- a/testutil/random.go +++ b/testutil/random.go @@ -464,20 +464,22 @@ func RandomCapellaBlindedBeaconBlockBody() *eth2capella.BlindedBeaconBlockBody { } } -func RandomBellatrixVersionedBlindedProposal() core.VersionedBlindedProposal { - return core.VersionedBlindedProposal{ - VersionedBlindedProposal: eth2api.VersionedBlindedProposal{ - Version: eth2spec.DataVersionBellatrix, - Bellatrix: RandomBellatrixBlindedBeaconBlock(), +func RandomBellatrixVersionedBlindedProposal() core.VersionedProposal { + return core.VersionedProposal{ + VersionedProposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionBellatrix, + Blinded: true, + BellatrixBlinded: RandomBellatrixBlindedBeaconBlock(), }, } } -func RandomCapellaVersionedBlindedProposal() core.VersionedBlindedProposal { - return core.VersionedBlindedProposal{ - VersionedBlindedProposal: eth2api.VersionedBlindedProposal{ - Version: eth2spec.DataVersionCapella, - Capella: RandomCapellaBlindedBeaconBlock(), +func RandomCapellaVersionedBlindedProposal() core.VersionedProposal { + return core.VersionedProposal{ + VersionedProposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionCapella, + Blinded: true, + CapellaBlinded: RandomCapellaBlindedBeaconBlock(), }, } } diff --git a/testutil/validatormock/component.go b/testutil/validatormock/component.go index c808318e2..d8a47e227 100644 --- a/testutil/validatormock/component.go +++ b/testutil/validatormock/component.go @@ -272,21 +272,11 @@ func (m *Component) runDuty(ctx context.Context, duty core.Duty) error { return err } case core.DutyProposer: - if m.builderAPI { - return nil - } - if err = ProposeBlock(ctx, eth2Cl, m.signFunc, eth2Slot); err != nil { return err } case core.DutyBuilderProposer: - if !m.builderAPI { - return nil - } - - if err = ProposeBlindedBlock(ctx, eth2Cl, m.signFunc, eth2Slot); err != nil { - return err - } + return core.ErrDeprecatedDutyBuilderProposer case core.DutyBuilderRegistration: if !m.builderAPI { return nil @@ -450,7 +440,6 @@ var dutyStartTimeFuncsByDuty = map[core.DutyType][]dutyStartTimeFunc{ core.DutyAttester: {fraction(1, 3)}, // 1/3 slot duration core.DutyAggregator: {fraction(2, 3)}, // 2/3 slot duration core.DutyProposer: {slotStartTime}, - core.DutyBuilderProposer: {slotStartTime}, core.DutyBuilderRegistration: {startOfCurrentEpoch}, core.DutyPrepareSyncContribution: {slotStartTime}, core.DutySyncMessage: {fraction(1, 3)}, diff --git a/testutil/validatormock/propose.go b/testutil/validatormock/propose.go index 386db6402..8755c5ee5 100644 --- a/testutil/validatormock/propose.go +++ b/testutil/validatormock/propose.go @@ -126,7 +126,35 @@ func ProposeBlock(ctx context.Context, eth2Cl eth2wrap.Client, signFunc SignFunc return err } - // Create signed beacon block proposal. + if block.Blinded { + signedBlock := new(eth2api.VersionedSignedBlindedProposal) + signedBlock.Version = block.Version + switch block.Version { + case eth2spec.DataVersionBellatrix: + signedBlock.Bellatrix = ð2bellatrix.SignedBlindedBeaconBlock{ + Message: block.BellatrixBlinded, + Signature: sig, + } + case eth2spec.DataVersionCapella: + signedBlock.Capella = ð2capella.SignedBlindedBeaconBlock{ + Message: block.CapellaBlinded, + Signature: sig, + } + case eth2spec.DataVersionDeneb: + signedBlock.Deneb = ð2deneb.SignedBlindedBeaconBlock{ + Message: block.DenebBlinded, + Signature: sig, + } + default: + return errors.New("invalid blinded block") + } + + return eth2Cl.SubmitBlindedProposal(ctx, ð2api.SubmitBlindedProposalOpts{ + Proposal: signedBlock, + }) + } + + // Full block signedBlock := new(eth2api.VersionedSignedProposal) signedBlock.Version = block.Version switch block.Version { @@ -163,129 +191,7 @@ func ProposeBlock(ctx context.Context, eth2Cl eth2wrap.Client, signFunc SignFunc return errors.New("invalid block") } - return eth2Cl.SubmitProposal(ctx, signedBlock) -} - -// ProposeBlindedBlock proposes blinded block for the given slot. -func ProposeBlindedBlock(ctx context.Context, eth2Cl eth2wrap.Client, signFunc SignFunc, - slot eth2p0.Slot, -) error { - valMap, err := eth2Cl.ActiveValidators(ctx) - if err != nil { - return err - } - - slotsPerEpoch, err := eth2Cl.SlotsPerEpoch(ctx) - if err != nil { - return err - } - - epoch := eth2p0.Epoch(uint64(slot) / slotsPerEpoch) - - var indexes []eth2p0.ValidatorIndex - for index := range valMap { - indexes = append(indexes, index) - } - - opts := ð2api.ProposerDutiesOpts{ - Epoch: epoch, - Indices: indexes, - } - eth2Resp, err := eth2Cl.ProposerDuties(ctx, opts) - if err != nil { - return err - } - duties := eth2Resp.Data - - var slotProposer *eth2v1.ProposerDuty - for _, duty := range duties { - if duty.Slot == slot { - slotProposer = duty - break - } - } - - if slotProposer == nil { - return nil - } - - var ( - pubkey eth2p0.BLSPubKey - block *eth2api.VersionedBlindedProposal - ) - pubkey = slotProposer.PubKey - - // Create randao reveal to propose block - randaoSigRoot, err := eth2util.SignedEpoch{Epoch: epoch}.HashTreeRoot() - if err != nil { - return err - } - - randaoSigData, err := signing.GetDataRoot(ctx, eth2Cl, signing.DomainRandao, epoch, randaoSigRoot) - if err != nil { - return err - } - - randao, err := signFunc(slotProposer.PubKey, randaoSigData[:]) - if err != nil { - return err - } - - // Get Unsigned beacon block with given randao and slot - proposalOpts := ð2api.BlindedProposalOpts{ - Slot: slot, - RandaoReveal: randao, - } - proposalResp, err := eth2Cl.BlindedProposal(ctx, proposalOpts) - if err != nil { - return errors.Wrap(err, "vmock blinded beacon block proposal") - } - block = proposalResp.Data - - if block == nil { - return errors.New("block not found") - } - - // Sign beacon block - blockSigRoot, err := block.Root() - if err != nil { - return err - } - - blockSigData, err := signing.GetDataRoot(ctx, eth2Cl, signing.DomainBeaconProposer, epoch, blockSigRoot) - if err != nil { - return err - } - - sig, err := signFunc(pubkey, blockSigData[:]) - if err != nil { - return err - } - - // create signed beacon block - signedBlock := new(eth2api.VersionedSignedBlindedProposal) - signedBlock.Version = block.Version - switch block.Version { - case eth2spec.DataVersionBellatrix: - signedBlock.Bellatrix = ð2bellatrix.SignedBlindedBeaconBlock{ - Message: block.Bellatrix, - Signature: sig, - } - case eth2spec.DataVersionCapella: - signedBlock.Capella = ð2capella.SignedBlindedBeaconBlock{ - Message: block.Capella, - Signature: sig, - } - case eth2spec.DataVersionDeneb: - signedBlock.Deneb = ð2deneb.SignedBlindedBeaconBlock{ - Message: block.Deneb, - Signature: sig, - } - default: - return errors.New("invalid block") - } - - return eth2Cl.SubmitBlindedProposal(ctx, signedBlock) + return eth2Cl.SubmitProposal(ctx, ð2api.SubmitProposalOpts{Proposal: signedBlock}) } // RegistrationsFromProposerConfig returns all enabled builder-API registrations from upstream proposer config. diff --git a/testutil/validatormock/propose_test.go b/testutil/validatormock/propose_test.go index 1726b23ef..98d564630 100644 --- a/testutil/validatormock/propose_test.go +++ b/testutil/validatormock/propose_test.go @@ -191,6 +191,8 @@ func TestProposeBlindedBlock(t *testing.T) { testResponse = append(testResponse, []byte(`}`)...) require.NoError(t, err) + w.Header().Set("Eth-Execution-Payload-Blinded", "true") + _, _ = w.Write(testResponse) })) defer mockVAPI.Close() @@ -201,7 +203,7 @@ func TestProposeBlindedBlock(t *testing.T) { } // Call propose block function - err = validatormock.ProposeBlindedBlock(ctx, provider, signFunc, eth2p0.Slot(slotsPerEpoch)) + err = validatormock.ProposeBlock(ctx, provider, signFunc, eth2p0.Slot(slotsPerEpoch)) require.NoError(t, err) } diff --git a/tools.go b/tools.go index d31d9a8be..fd91a42c6 100644 --- a/tools.go +++ b/tools.go @@ -19,6 +19,9 @@ import ( //go:generate echo Installing tools: stringer //go:generate go install golang.org/x/tools/cmd/stringer +//go:generate echo Installing tools: mockery +//go install github.com/vektra/mockery/v2@v2.42.1 + //go:generate echo Installing tools: protobuf //go:generate go install github.com/bufbuild/buf/cmd/buf@latest //go:generate go install github.com/bufbuild/buf/cmd/protoc-gen-buf-breaking@latest