Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

*: electra versioned attestations #3473

Merged
merged 8 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
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 @@
"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 @@
// 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 @@
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 @@
return nil, errors.New("request block attestations failed", z.Int("status", statusCode), z.Str("body", string(respBody)))
}

var resp attestationsJSON
var resp p0AttestationsJSON

Check warning on line 196 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L196

Added line #L196 was not covered by tests
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")
}

Check warning on line 211 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L210-L211

Added lines #L210 - L211 were not covered by tests
defer resp.Body.Close()

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

Check warning on line 217 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L216-L217

Added lines #L216 - L217 were not covered by tests

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)))
}

Check warning on line 223 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L222-L223

Added lines #L222 - L223 were not covered by tests

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

Check warning on line 228 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L227-L228

Added lines #L227 - L228 were not covered by tests

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})
}

Check warning on line 263 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L232-L263

Added lines #L232 - L263 were not covered by tests
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")
}

Check warning on line 268 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L267-L268

Added lines #L267 - L268 were not covered by tests
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")

Check warning on line 281 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L272-L281

Added lines #L272 - L281 were not covered by tests
}

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 @@
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 @@
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")

Check warning on line 409 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L409

Added line #L409 was not covered by tests
}

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

Check warning on line 414 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L414

Added line #L414 was not covered by tests
}

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")

Check warning on line 419 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L419

Added line #L419 was not covered by tests
}

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")
}

Check warning on line 425 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L424-L425

Added lines #L424 - L425 were not covered by tests

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")

Check warning on line 434 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L431-L434

Added lines #L431 - L434 were not covered by tests
}
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")

Check warning on line 440 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L440

Added line #L440 was not covered by tests
}

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")
}

Check warning on line 458 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L450-L458

Added lines #L450 - L458 were not covered by tests

return metadata.Version, nil

Check warning on line 460 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L460

Added line #L460 was not covered by tests
}
if len(respConsensusVersions) != 1 {
return spec.DataVersionUnknown, errors.New("malformed consensus version", z.Int("entries", len(respConsensusVersions)))
}

Check warning on line 464 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L463-L464

Added lines #L463 - L464 were not covered by tests
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")
}

Check warning on line 469 in app/eth2wrap/httpwrap.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/httpwrap.go#L468-L469

Added lines #L468 - L469 were not covered by tests

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 @@
"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 @@
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 @@
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
}

Check warning on line 210 in app/eth2wrap/lazy.go

View check run for this annotation

Codecov / codecov/patch

app/eth2wrap/lazy.go#L209-L210

Added lines #L209 - L210 were not covered by tests

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
Loading