Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ gloas:
- default controller.max-attestation-delay, controller.attestation-aggregation-delay, controller.max-sync-committee-message-delay and controller.sync-committee-aggregation-delay to 0; the hardcoded defaults made the spec-derived deadlines unreachable
- add the payload timeliness committee duty: fetch PTC duties for the epoch, group each slot's validators into one job scheduled inside the gloas payload timing window, and vote on whether the slot's execution payload was revealed on time
- submit versioned payload attestations through the immediate, multinode and null submitters, batching signatures for validators that share a slot
- obtain payload attestation data from a dedicated multiclient, configurable with strategies.payloadattestationdata.beacon-node-addresses
- update go-eth2-client to a gloas pseudo-version
- satisfy the attgo struct field order and comment capitalisation rules across services and strategies

Expand Down
71 changes: 71 additions & 0 deletions clients_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import (
"encoding/binary"
nethttp "net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"

bitfield "github.com/OffchainLabs/go-bitfield"
client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
apiv1gloas "github.com/attestantio/go-eth2-client/api/v1/gloas"
mockconsensusclient "github.com/attestantio/go-eth2-client/mock"
"github.com/attestantio/go-eth2-client/spec"
Expand All @@ -32,9 +34,12 @@ import (
"github.com/attestantio/go-eth2-client/spec/gloas"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/vouch/services/metrics/null"
"github.com/attestantio/vouch/services/payloadattester"
"github.com/attestantio/vouch/testutil"
dynssz "github.com/pk910/dynamic-ssz"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)

func TestFetchClientCustomSpecSupport(t *testing.T) {
Expand Down Expand Up @@ -101,6 +106,72 @@ func TestFetchClientCustomSpecSupport(t *testing.T) {
require.Equal(t, block.Slot, response.Data.Gloas.Slot)
}

func TestPayloadAttesterUsesConfiguredPayloadAttestationDataProviders(t *testing.T) {
ctx := context.Background()
var configuredRequests atomic.Int64
var globalRequests atomic.Int64
newServer := func(requests *atomic.Int64) *httptest.Server {
return httptest.NewServer(nethttp.HandlerFunc(func(w nethttp.ResponseWriter, r *nethttp.Request) {
switch r.URL.Path {
case "/eth/v1/node/version":
_, _ = w.Write([]byte(`{"data":{"version":"test"}}`))
case "/eth/v1/node/syncing":
_, _ = w.Write([]byte(`{"data":{"is_syncing":false,"is_optimistic":false,"el_offline":false,"head_slot":"12","sync_distance":"0"}}`))
case "/eth/v1/validator/payload_attestation_data":
if r.URL.Query().Get("slot") != "12" {
t.Errorf("unexpected slot %q", r.URL.Query().Get("slot"))
}
requests.Add(1)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Eth-Consensus-Version", "gloas")
_, _ = w.Write([]byte(`{"version":"gloas","data":{"beacon_block_root":"0x0100000000000000000000000000000000000000000000000000000000000000","slot":"12","payload_present":true,"blob_data_available":true}}`))
default:
t.Errorf("unexpected request %s", r.URL.Path)
w.WriteHeader(nethttp.StatusNotFound)
}
}))
}
configuredServer := newServer(&configuredRequests)
defer configuredServer.Close()
globalServer := newServer(&globalRequests)
defer globalServer.Close()

viper.Set("timeout", time.Second)
viper.Set("beacon-node-addresses", []string{globalServer.URL})
viper.Set("strategies.payloadattestationdata.beacon-node-addresses", []string{configuredServer.URL})
t.Cleanup(func() {
viper.Reset()
knownClientsMu.Lock()
delete(knownClients, configuredServer.URL)
delete(knownClients, globalServer.URL)
delete(knownClients, "multi:"+configuredServer.URL)
knownClientsMu.Unlock()
})
service, err := startPayloadAttester(ctx, null.New(), &payloadAttestationDataSigner{}, &payloadAttestationMessagesSubmitter{})
require.NoError(t, err)
accounts, err := testutil.CreateTestWalletAndAccounts([]phase0.ValidatorIndex{1}, "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866")
require.NoError(t, err)
duty := payloadattester.NewDuty(&apiv1.PTCDuty{Slot: 12, ValidatorIndex: 1})
duty.SetAccount(1, accounts[1])

_, err = service.Attest(ctx, duty)
require.NoError(t, err)
require.Equal(t, int64(1), configuredRequests.Load())
require.Zero(t, globalRequests.Load())
}

type payloadAttestationDataSigner struct{}

func (*payloadAttestationDataSigner) SignPayloadAttestationData(_ context.Context, accounts []e2wtypes.Account, _ *gloas.PayloadAttestationData) ([]phase0.BLSSignature, error) {
return make([]phase0.BLSSignature, len(accounts)), nil
}

type payloadAttestationMessagesSubmitter struct{}

func (*payloadAttestationMessagesSubmitter) SubmitPayloadAttestationMessages(_ context.Context, _ *api.SubmitPayloadAttestationMessagesOpts) error {
return nil
}

func TestSimpleProposalProviderRejectsZeroFeeRecipient(t *testing.T) {
ctx := context.Background()
const address = "http://proposal.test"
Expand Down
6 changes: 6 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ strategies:
deadline: '1s'
# bid-gap is the gap between receiving a response from a relay and querying it again.
bid-gap: '100ms'
# The payloadattestationdata strategy obtains payload attestation data for payload timeliness committee duties.
# It uses a single multiclient; there is no style to select.
payloadattestationdata:
# beacon-node-addresses are the addresses from which to receive payload attestation data, falling back to the
# top-level beacon-node-addresses if not set.
beacon-node-addresses: ['localhost:4000', 'localhost:5051', 'localhost:5052']
# The signedbeaconblock strategy obtains the signed beacon blocks from multiple beacon nodes.
signedbeaconblock:
# style can be 'first'. If not defined, the 'first' style will be used.
Expand Down
24 changes: 16 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ func startServices(ctx context.Context,
return nil, nil, errors.Wrap(err, "failed to select submitter")
}

payloadAttester, err := startPayloadAttester(ctx, monitor, eth2Client, signerSvc, submitter)
payloadAttester, err := startPayloadAttester(ctx, monitor, signerSvc, submitter)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to start payload attester")
}
Expand Down Expand Up @@ -1679,25 +1679,33 @@ func selectSubmitterStrategy(ctx context.Context, monitor metrics.Service, eth2C
return submitter, nil
}

// startPayloadAttester starts the payload attester when the consensus client, signer and submitter
// all provide their side of the payload attestation flow. Anything missing leaves the service
// disabled rather than making startup fail. Whether the network is at Gloas is not decided here:
// startPayloadAttester starts the payload attester when the signer and submitter both provide
// their side of the payload attestation flow. A missing signer or submitter side leaves the
// service disabled rather than making startup fail; a misconfigured or unreachable set of
// payload attestation data addresses does fail startup, as it would otherwise silently drop
// every payload attestation duty. Whether the network is at Gloas is not decided here:
// the controller schedules no payload attestation duty before GLOAS_FORK_EPOCH.
func startPayloadAttester(ctx context.Context,
monitor metrics.Service,
eth2Client eth2client.Service,
signerSvc signer.Service,
submitterStrategy submitter.Service,
) (payloadattester.Service, error) {
payloadAttestationDataProvider, ok := eth2Client.(eth2client.PayloadAttestationDataProvider)
payloadAttestationDataSigner, ok := signerSvc.(signer.PayloadAttestationDataSigner)
if !ok {
return nil, nil
}
payloadAttestationDataSigner, ok := signerSvc.(signer.PayloadAttestationDataSigner)
payloadAttestationMessagesSubmitter, ok := submitterStrategy.(submitter.PayloadAttestationMessagesSubmitter)
if !ok {
return nil, nil
}
payloadAttestationMessagesSubmitter, ok := submitterStrategy.(submitter.PayloadAttestationMessagesSubmitter)
payloadAttestationDataClient, err := fetchMultiClient(ctx, monitor,
"payloadattestationdata",
util.BeaconNodeAddressesForPayloadAttestationData(),
)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch clients for payload attestation data")
}
payloadAttestationDataProvider, ok := payloadAttestationDataClient.(eth2client.PayloadAttestationDataProvider)
if !ok {
return nil, nil
}
Expand Down
9 changes: 8 additions & 1 deletion util/config.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2024 Attestant Limited.
// Copyright © 2024 - 2026 Attestant Limited.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
Expand Down Expand Up @@ -76,6 +76,13 @@ func BeaconNodeAddressesForProposing() []string {
)
}

// BeaconNodeAddressesForPayloadAttestationData obtains the beacon node addresses used for
// payload attestation data from the configuration.
// This follows the hierarchical address configuration, and removes duplicates.
func BeaconNodeAddressesForPayloadAttestationData() []string {
return uniqueSortedAddresses(BeaconNodeAddresses("strategies.payloadattestationdata"))
}

// BeaconNodeAddressesForAttestationData obtains the beacon node addresses used for
// attestation data from the configuration.
// This takes into account the used styles in strategies, and removes duplicates.
Expand Down
30 changes: 30 additions & 0 deletions util/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,36 @@ func TestBeaconNodeAddressesPerStrategy(t *testing.T) {
envPrefix: "VOUCH_BEACONNODEADDRESSFORATTESTING",
handler: util.BeaconNodeAddressesForAttestationData,
},
{
name: "PayloadAttestationDataFallsBackToTopLevelAddresses",
env: map[string]string{
"BEACON_NODE_ADDRESSES": "1 2",
},
expected: []string{"1", "2"},
envPrefix: "VOUCH_BEACONNODEADDRESSFORPAYLOADATTESTATIONDATA",
handler: util.BeaconNodeAddressesForPayloadAttestationData,
},
{
name: "PayloadAttestationDataUsesStrategiesAddresses",
env: map[string]string{
"BEACON_NODE_ADDRESSES": "1 2",
"STRATEGIES_BEACON_NODE_ADDRESSES": "3 4",
},
expected: []string{"3", "4"},
envPrefix: "VOUCH_BEACONNODEADDRESSFORPAYLOADATTESTATIONDATA",
handler: util.BeaconNodeAddressesForPayloadAttestationData,
},
{
name: "PayloadAttestationDataOverridesStrategiesAddresses",
env: map[string]string{
"BEACON_NODE_ADDRESSES": "1 2",
"STRATEGIES_BEACON_NODE_ADDRESSES": "3 4",
"STRATEGIES_PAYLOADATTESTATIONDATA_BEACON_NODE_ADDRESSES": "5 6",
},
expected: []string{"5", "6"},
envPrefix: "VOUCH_BEACONNODEADDRESSFORPAYLOADATTESTATIONDATA",
handler: util.BeaconNodeAddressesForPayloadAttestationData,
},
}

// SignedBeaconBlock and BeaconBlockHeader only support "first" style,
Expand Down
Loading