Skip to content

Commit

Permalink
*: electra versioned attestations (#3473)
Browse files Browse the repository at this point in the history
* Comment out gnosis fix

* SubmitAttestation to versioned attestation

* Rework attestations, add BlockAttestationsV2 endpoint, update signed blocks with electra version

* go mod tidy

* Add more random attestation util functions

* Fix duty attester test signatures

* Fix raw attestation json tag

* Fix error messages
  • Loading branch information
KaloyanTanev authored Jan 22, 2025
1 parent cc5f229 commit eab13c1
Show file tree
Hide file tree
Showing 52 changed files with 3,406 additions and 1,655 deletions.
15 changes: 7 additions & 8 deletions app/eth2wrap/eth2wrap_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 8 additions & 7 deletions app/eth2wrap/eth2wrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,33 +262,34 @@ func TestCtxCancel(t *testing.T) {
}

func TestBlockAttestations(t *testing.T) {
atts := []*eth2p0.Attestation{
testutil.RandomAttestation(),
testutil.RandomAttestation(),
atts := []*eth2spec.VersionedAttestation{
testutil.RandomDenebVersionedAttestation(),
testutil.RandomDenebVersionedAttestation(),
}

statusCode := http.StatusOK
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "/eth/v1/beacon/blocks/head/attestations", r.URL.Path)
require.Equal(t, "/eth/v2/beacon/blocks/head/attestations", r.URL.Path)
b, err := json.Marshal(struct {
Data []*eth2p0.Attestation
}{
Data: atts,
Data: []*eth2p0.Attestation{atts[0].Deneb, atts[1].Deneb},
})
require.NoError(t, err)

w.Header().Add("Eth-Consensus-Version", "deneb")
w.WriteHeader(statusCode)
_, _ = w.Write(b)
}))

cl := eth2wrap.NewHTTPAdapterForT(t, srv.URL, time.Hour)
resp, err := cl.BlockAttestations(context.Background(), "head")
resp, err := cl.BlockAttestationsV2(context.Background(), "head")
require.NoError(t, err)
require.Equal(t, atts, resp)

statusCode = http.StatusNotFound
resp, err = cl.BlockAttestations(context.Background(), "head")
resp, err = cl.BlockAttestationsV2(context.Background(), "head")
require.NoError(t, err)
require.Empty(t, resp)
}
Expand Down
152 changes: 143 additions & 9 deletions app/eth2wrap/httpwrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
eth2http "github.com/attestantio/go-eth2-client/http"
"github.com/attestantio/go-eth2-client/spec"
eth2e "github.com/attestantio/go-eth2-client/spec/electra"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"

"github.com/obolnetwork/charon/app/errors"
Expand All @@ -30,7 +32,9 @@ import (
// It is a standard beacon API endpoint not implemented by eth2client.
// See https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockAttestations.
type BlockAttestationsProvider interface {
// Deprecated: use BlockAttestationsV2(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error)
BlockAttestations(ctx context.Context, stateID string) ([]*eth2p0.Attestation, error)
BlockAttestationsV2(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error)
}

// NodePeerCountProvider is the interface for providing node peer count.
Expand Down Expand Up @@ -175,6 +179,7 @@ func (h *httpAdapter) AggregateSyncCommitteeSelections(ctx context.Context, sele
return resp.Data, nil
}

// Deprecated: use BlockAttestationsV2(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error)
// BlockAttestations returns the attestations included in the requested block.
// See https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockAttestations.
func (h *httpAdapter) BlockAttestations(ctx context.Context, stateID string) ([]*eth2p0.Attestation, error) {
Expand All @@ -188,14 +193,97 @@ func (h *httpAdapter) BlockAttestations(ctx context.Context, stateID string) ([]
return nil, errors.New("request block attestations failed", z.Int("status", statusCode), z.Str("body", string(respBody)))
}

var resp attestationsJSON
var resp p0AttestationsJSON
if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, errors.Wrap(err, "failed to parse block attestations response")
}

return resp.Data, nil
}

// BlockAttestationsV2 returns the attestations included in the requested block.
// See https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockAttestationsV2.
func (h *httpAdapter) BlockAttestationsV2(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error) {
path := fmt.Sprintf("/eth/v2/beacon/blocks/%s/attestations", stateID)
resp, err := httpGetRaw(ctx, h.address, path, h.timeout)
if err != nil {
return nil, errors.Wrap(err, "request block attestations")
}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "request block attestations body")
}

if resp.StatusCode == http.StatusNotFound {
return nil, nil // No block for slot, so no attestations.
} else if resp.StatusCode != http.StatusOK {
return nil, errors.New("request block attestations failed", z.Int("status", resp.StatusCode), z.Str("body", string(respBody)))
}

version, err := fetchConsensusVersion(resp)
if err != nil {
return nil, errors.Wrap(err, "failed to get consensus version")
}

res := []*spec.VersionedAttestation{}
switch version {
case spec.DataVersionPhase0:
var respAttestation p0AttestationsJSON
if err := json.Unmarshal(respBody, &respAttestation); err != nil {
return nil, errors.Wrap(err, "failed to parse block attestations response")
}
for _, a := range respAttestation.Data {
res = append(res, &spec.VersionedAttestation{Version: version, Phase0: a})
}
case spec.DataVersionAltair:
var respAttestation p0AttestationsJSON
if err := json.Unmarshal(respBody, &respAttestation); err != nil {
return nil, errors.Wrap(err, "failed to parse block attestations response")
}
for _, a := range respAttestation.Data {
res = append(res, &spec.VersionedAttestation{Version: version, Altair: a})
}
case spec.DataVersionBellatrix:
var respAttestation p0AttestationsJSON
if err := json.Unmarshal(respBody, &respAttestation); err != nil {
return nil, errors.Wrap(err, "failed to parse block attestations response")
}
for _, a := range respAttestation.Data {
res = append(res, &spec.VersionedAttestation{Version: version, Bellatrix: a})
}
case spec.DataVersionCapella:
var respAttestation p0AttestationsJSON
if err := json.Unmarshal(respBody, &respAttestation); err != nil {
return nil, errors.Wrap(err, "failed to parse block attestations response")
}
for _, a := range respAttestation.Data {
res = append(res, &spec.VersionedAttestation{Version: version, Capella: a})
}
case spec.DataVersionDeneb:
var respAttestation p0AttestationsJSON
if err := json.Unmarshal(respBody, &respAttestation); err != nil {
return nil, errors.Wrap(err, "failed to parse block attestations response")
}
for _, a := range respAttestation.Data {
res = append(res, &spec.VersionedAttestation{Version: version, Deneb: a})
}
case spec.DataVersionElectra:
var respAttestation electraAttestationsJSON
if err := json.Unmarshal(respBody, &respAttestation); err != nil {
return nil, errors.Wrap(err, "failed to parse block attestations response")
}
for _, a := range respAttestation.Data {
res = append(res, &spec.VersionedAttestation{Version: version, Electra: a})
}
case spec.DataVersionUnknown:
return nil, errors.New("attestations data version unknown")
}

return res, nil
}

// ProposerConfig implements eth2exp.ProposerConfigProvider.
func (h *httpAdapter) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConfigResponse, error) {
respBody, statusCode, err := httpGet(ctx, h.address, "/proposer_config", h.timeout)
Expand Down Expand Up @@ -256,16 +344,24 @@ type submitSyncCommitteeSelectionsJSON struct {
Data []*eth2exp.SyncCommitteeSelection `json:"data"`
}

type attestationsJSON struct {
type p0AttestationsJSON struct {
Data []*eth2p0.Attestation `json:"data"`
}

type electraAttestationsJSON struct {
Data []*eth2e.Attestation `json:"data"`
}

type peerCountJSON struct {
Data struct {
Connected int `json:"connected,string"`
} `json:"data"`
}

type responseMetadata struct {
Version spec.DataVersion `json:"version"`
}

func httpPost(ctx context.Context, base string, endpoint string, body io.Reader, timeout time.Duration) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
Expand Down Expand Up @@ -303,36 +399,74 @@ func httpPost(ctx context.Context, base string, endpoint string, body io.Reader,
return data, nil
}

// httpGet performs a GET request and returns the body and status code or an error.
func httpGet(ctx context.Context, base string, endpoint string, timeout time.Duration) ([]byte, int, error) {
// httpGetRaw performs a GET request and returns the raw http response or an error.
func httpGetRaw(ctx context.Context, base string, endpoint string, timeout time.Duration) (*http.Response, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

addr, err := url.JoinPath(base, endpoint)
if err != nil {
return nil, 0, errors.Wrap(err, "invalid address")
return nil, errors.Wrap(err, "invalid address")
}

u, err := url.ParseRequestURI(addr)
if err != nil {
return nil, 0, errors.Wrap(err, "invalid endpoint")
return nil, errors.Wrap(err, "invalid endpoint")
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, 0, errors.Wrap(err, "new GET request with ctx")
return nil, errors.Wrap(err, "new GET request with ctx")
}

res, err := new(http.Client).Do(req)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to call GET endpoint")
return nil, errors.Wrap(err, "failed to call GET endpoint")
}

return res, nil
}

// httpGet performs a GET request and returns the body and status code or an error.
func httpGet(ctx context.Context, base string, endpoint string, timeout time.Duration) ([]byte, int, error) {
res, err := httpGetRaw(ctx, base, endpoint, timeout)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to read GET response")
}
defer res.Body.Close()

data, err := io.ReadAll(res.Body)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to read GET response")
return nil, 0, errors.Wrap(err, "failed to read GET response body")
}

return data, res.StatusCode, nil
}

// fetchConsensusVersion attempts to extract the consensus version from the beacon node http response.
func fetchConsensusVersion(resp *http.Response) (spec.DataVersion, error) {
respConsensusVersions, exists := resp.Header["Eth-Consensus-Version"]
if !exists {
// No consensus version supplied in response; obtain it from the body if possible.
var metadata responseMetadata
body, err := io.ReadAll(resp.Body)
if err != nil {
return spec.DataVersionUnknown, errors.Wrap(err, "read resp body")
}
if err := json.Unmarshal(body, &metadata); err != nil {
return spec.DataVersionUnknown, errors.Wrap(err, "no consensus version header and failed to parse response")
}

return metadata.Version, nil
}
if len(respConsensusVersions) != 1 {
return spec.DataVersionUnknown, errors.New("malformed consensus version", z.Int("entries", len(respConsensusVersions)))
}
var dataVersion spec.DataVersion
err := dataVersion.UnmarshalJSON([]byte(fmt.Sprintf("%q", respConsensusVersions[0])))
if err != nil {
return spec.DataVersionUnknown, errors.Wrap(err, "unmarshal consensus version header to data version")
}

return dataVersion, nil
}
11 changes: 11 additions & 0 deletions app/eth2wrap/lazy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"sync"
"time"

"github.com/attestantio/go-eth2-client/spec"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"

"github.com/obolnetwork/charon/eth2util/eth2exp"
Expand Down Expand Up @@ -192,6 +193,7 @@ func (l *lazy) AggregateSyncCommitteeSelections(ctx context.Context, partialSele
return cl.AggregateSyncCommitteeSelections(ctx, partialSelections)
}

// Deprecated: use BlockAttestationsV2(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error)
func (l *lazy) BlockAttestations(ctx context.Context, stateID string) ([]*eth2p0.Attestation, error) {
cl, err := l.getOrCreateClient(ctx)
if err != nil {
Expand All @@ -201,6 +203,15 @@ func (l *lazy) BlockAttestations(ctx context.Context, stateID string) ([]*eth2p0
return cl.BlockAttestations(ctx, stateID)
}

func (l *lazy) BlockAttestationsV2(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error) {
cl, err := l.getOrCreateClient(ctx)
if err != nil {
return nil, err
}

return cl.BlockAttestationsV2(ctx, stateID)
}

func (l *lazy) NodePeerCount(ctx context.Context) (int, error) {
cl, err := l.getOrCreateClient(ctx)
if err != nil {
Expand Down
Loading

0 comments on commit eab13c1

Please sign in to comment.