diff --git a/pkg/beacon/api/api.go b/pkg/beacon/api/api.go index 21c856b..61f97f9 100644 --- a/pkg/beacon/api/api.go +++ b/pkg/beacon/api/api.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "net/url" + "strings" "github.com/ethpandaops/beacon/pkg/beacon/api/types" "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" @@ -52,6 +53,8 @@ type BeaconAPIResponse struct { Version string `json:"version"` } +type BeaconAPIResponses[T any] []BeaconAPIResponse + //nolint:unused // this is used in the future func (c *consensusClient) post(ctx context.Context, path string, body map[string]interface{}) (*BeaconAPIResponse, error) { jsonData, err := json.Marshal(body) @@ -94,12 +97,18 @@ func (c *consensusClient) post(ctx context.Context, path string, body map[string } //nolint:unparam // ctx will probably be used in the future -func (c *consensusClient) get(ctx context.Context, path string) (*BeaconAPIResponse, error) { +func (c *consensusClient) get(ctx context.Context, path string, contentType string, rspType any) error { + if contentType == "" { + contentType = "application/json" + } + req, err := http.NewRequestWithContext(ctx, "GET", c.url+path, nil) if err != nil { - return nil, err + return err } + req.Header.Set("Accept", contentType) + // Set headers from c.headers for k, v := range c.headers { req.Header.Set(k, v) @@ -107,32 +116,40 @@ func (c *consensusClient) get(ctx context.Context, path string) (*BeaconAPIRespo rsp, err := c.client.Do(req) if err != nil { - return nil, err + return err } defer rsp.Body.Close() if rsp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("status code: %d", rsp.StatusCode) + return fmt.Errorf("status code: %d", rsp.StatusCode) + } + + // Parse the content type header to handle parameters like charset + contentTypeHeader := rsp.Header.Get("Content-Type") + if contentTypeHeader != "" { + if !strings.Contains(contentTypeHeader, contentType) { + return fmt.Errorf("unexpected content type: wanted (%s): got (%s)", contentType, contentTypeHeader) + } } data, err := io.ReadAll(rsp.Body) if err != nil { - return nil, err + return err } - resp := new(BeaconAPIResponse) - if err := json.Unmarshal(data, resp); err != nil { - return nil, err + if err := json.Unmarshal(data, rspType); err != nil { + return err } - return resp, nil + return nil } func (c *consensusClient) getRaw(ctx context.Context, path string, contentType string) ([]byte, error) { if contentType == "" { contentType = "application/json" } + u, err := url.Parse(c.url + path) if err != nil { return nil, err @@ -166,8 +183,8 @@ func (c *consensusClient) getRaw(ctx context.Context, path string, contentType s // NodePeers returns the list of peers connected to the node. func (c *consensusClient) NodePeers(ctx context.Context) (types.Peers, error) { - data, err := c.get(ctx, "/eth/v1/node/peers") - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, "/eth/v1/node/peers", ContentTypeJSON, data); err != nil { return nil, err } @@ -181,8 +198,8 @@ func (c *consensusClient) NodePeers(ctx context.Context) (types.Peers, error) { // NodePeer returns the peer with the given peer ID. func (c *consensusClient) NodePeer(ctx context.Context, peerID string) (types.Peer, error) { - data, err := c.get(ctx, fmt.Sprintf("/eth/v1/node/peers/%s", peerID)) - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, fmt.Sprintf("/eth/v1/node/peers/%s", peerID), ContentTypeJSON, data); err != nil { return types.Peer{}, err } @@ -196,8 +213,8 @@ func (c *consensusClient) NodePeer(ctx context.Context, peerID string) (types.Pe // NodePeerCount returns the number of peers connected to the node. func (c *consensusClient) NodePeerCount(ctx context.Context) (types.PeerCount, error) { - data, err := c.get(ctx, "/eth/v1/node/peer_count") - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, "/eth/v1/node/peer_count", ContentTypeJSON, data); err != nil { return types.PeerCount{}, err } @@ -231,8 +248,8 @@ func (c *consensusClient) RawBlock(ctx context.Context, stateID string, contentT // DepositSnapshot returns the deposit snapshot in the requested format. func (c *consensusClient) DepositSnapshot(ctx context.Context) (*types.DepositSnapshot, error) { - data, err := c.get(ctx, "/eth/v1/beacon/deposit_snapshot") - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, "/eth/v1/beacon/deposit_snapshot", ContentTypeJSON, data); err != nil { return nil, err } @@ -245,8 +262,8 @@ func (c *consensusClient) DepositSnapshot(ctx context.Context) (*types.DepositSn } func (c *consensusClient) NodeIdentity(ctx context.Context) (*types.Identity, error) { - data, err := c.get(ctx, "/eth/v1/node/identity") - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, "/eth/v1/node/identity", ContentTypeJSON, data); err != nil { return nil, err } @@ -259,8 +276,8 @@ func (c *consensusClient) NodeIdentity(ctx context.Context) (*types.Identity, er } func (c *consensusClient) LightClientBootstrap(ctx context.Context, blockRoot string) (*LightClientBootstrapResponse, error) { - data, err := c.get(ctx, fmt.Sprintf("/eth/v1/beacon/light_client/bootstrap/%s", blockRoot)) - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, fmt.Sprintf("/eth/v1/beacon/light_client/bootstrap/%s", blockRoot), ContentTypeJSON, data); err != nil { return nil, err } @@ -272,7 +289,7 @@ func (c *consensusClient) LightClientBootstrap(ctx context.Context, blockRoot st }, }, } - if err := json.Unmarshal(data.Data, &rsp.Data); err != nil { + if err := json.Unmarshal(data.Data, &rsp.Response.Data); err != nil { return nil, err } @@ -288,29 +305,36 @@ func (c *consensusClient) LightClientUpdates(ctx context.Context, startPeriod, c params.Add("start_period", fmt.Sprintf("%d", startPeriod)) params.Add("count", fmt.Sprintf("%d", count)) - data, err := c.get(ctx, "/eth/v1/beacon/light_client/updates?"+params.Encode()) - if err != nil { + data := new(BeaconAPIResponses[*lightclient.Updates]) + if err := c.get(ctx, "/eth/v1/beacon/light_client/updates?"+params.Encode(), ContentTypeJSON, data); err != nil { return nil, err } rsp := LightClientUpdatesResponse{ Response: Response[*lightclient.Updates]{ - Data: &lightclient.Updates{}, - Metadata: map[string]any{ - "version": data.Version, - }, + Data: &lightclient.Updates{}, + Metadata: map[string]any{}, }, } - if err := json.Unmarshal(data.Data, &rsp.Data); err != nil { - return nil, err + + updates := make(lightclient.Updates, 0) + for _, resp := range *data { + update := lightclient.Update{} + if err := json.Unmarshal(resp.Data, &update); err != nil { + return nil, err + } + + updates = append(updates, &update) } + rsp.Response.Data = &updates + return &rsp, nil } func (c *consensusClient) LightClientFinalityUpdate(ctx context.Context) (*LightClientFinalityUpdateResponse, error) { - data, err := c.get(ctx, "/eth/v1/beacon/light_client/finality_update") - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, "/eth/v1/beacon/light_client/finality_update", ContentTypeJSON, data); err != nil { return nil, err } @@ -330,8 +354,8 @@ func (c *consensusClient) LightClientFinalityUpdate(ctx context.Context) (*Light } func (c *consensusClient) LightClientOptimisticUpdate(ctx context.Context) (*LightClientOptimisticUpdateResponse, error) { - data, err := c.get(ctx, "/eth/v1/beacon/light_client/optimistic_update") - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, "/eth/v1/beacon/light_client/optimistic_update", ContentTypeJSON, data); err != nil { return nil, err } diff --git a/pkg/beacon/api/content_type.go b/pkg/beacon/api/content_type.go new file mode 100644 index 0000000..c025650 --- /dev/null +++ b/pkg/beacon/api/content_type.go @@ -0,0 +1,6 @@ +package api + +const ( + ContentTypeJSON = "application/json" + ContentTypeSSZ = "application/octet-stream" +) diff --git a/pkg/beacon/api/types/lightclient/bootstrap.go b/pkg/beacon/api/types/lightclient/bootstrap.go index acbd4b1..a65b7d4 100644 --- a/pkg/beacon/api/types/lightclient/bootstrap.go +++ b/pkg/beacon/api/types/lightclient/bootstrap.go @@ -4,7 +4,6 @@ import ( "encoding/hex" "encoding/json" "fmt" - "strconv" "strings" "github.com/attestantio/go-eth2-client/spec/phase0" @@ -25,24 +24,6 @@ type bootstrapJSON struct { CurrentSyncCommitteeBranch bootstrapCurrentSyncCommitteeBranchJSON `json:"current_sync_committee_branch"` } -// BootstrapHeader is the header of a light client bootstrap. -type BootstrapHeader struct { - Slot phase0.Slot - ProposerIndex phase0.ValidatorIndex - ParentRoot phase0.Root - StateRoot phase0.Root - BodyRoot phase0.Root -} - -// bootstrapHeaderJSON is the JSON representation of a bootstrap header. -type bootstrapHeaderJSON struct { - Slot string `json:"slot"` - ProposerIndex string `json:"proposer_index"` - ParentRoot string `json:"parent_root"` - StateRoot string `json:"state_root"` - BodyRoot string `json:"body_root"` -} - // BootstrapCurrentSyncCommittee is the current sync committee of a light client bootstrap. type BootstrapCurrentSyncCommittee struct { Pubkeys []phase0.BLSPubKey @@ -70,13 +51,7 @@ func (b Bootstrap) MarshalJSON() ([]byte, error) { } return json.Marshal(&bootstrapJSON{ - Header: bootstrapHeaderJSON{ - Slot: fmt.Sprintf("%d", b.Header.Slot), - ProposerIndex: fmt.Sprintf("%d", b.Header.ProposerIndex), - ParentRoot: b.Header.ParentRoot.String(), - StateRoot: b.Header.StateRoot.String(), - BodyRoot: b.Header.BodyRoot.String(), - }, + Header: b.Header.ToJSON(), CurrentSyncCommittee: bootstrapCurrentSyncCommitteeJSON{ Pubkeys: pubkeys, AggregatePubkey: b.CurrentSyncCommittee.AggregatePubkey.String(), @@ -93,55 +68,9 @@ func (b *Bootstrap) UnmarshalJSON(input []byte) error { return errors.Wrap(err, "invalid JSON") } - if jsonData.Header.Slot == "" { - return errors.New("slot is required") - } - - slot, err := strconv.ParseUint(jsonData.Header.Slot, 10, 64) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid slot: %s", jsonData.Header.Slot)) - } - b.Header.Slot = phase0.Slot(slot) - - if jsonData.Header.ProposerIndex == "" { - return errors.New("proposer index is required") - } - - proposerIndex, err := strconv.ParseUint(jsonData.Header.ProposerIndex, 10, 64) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid proposer index: %s", jsonData.Header.ProposerIndex)) - } - b.Header.ProposerIndex = phase0.ValidatorIndex(proposerIndex) - - if jsonData.Header.ParentRoot == "" { - return errors.New("parent root is required") - } - - parentRoot, err := hex.DecodeString(strings.TrimPrefix(jsonData.Header.ParentRoot, "0x")) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid parent root: %s", jsonData.Header.ParentRoot)) - } - b.Header.ParentRoot = phase0.Root(parentRoot) - - if jsonData.Header.StateRoot == "" { - return errors.New("state root is required") - } - - stateRoot, err := hex.DecodeString(strings.TrimPrefix(jsonData.Header.StateRoot, "0x")) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid state root: %s", jsonData.Header.StateRoot)) - } - b.Header.StateRoot = phase0.Root(stateRoot) - - if jsonData.Header.BodyRoot == "" { - return errors.New("body root is required") - } - - bodyRoot, err := hex.DecodeString(strings.TrimPrefix(jsonData.Header.BodyRoot, "0x")) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid body root: %s", jsonData.Header.BodyRoot)) + if err = b.Header.Beacon.FromJSON(jsonData.Header.Beacon); err != nil { + return errors.Wrap(err, "invalid header") } - b.Header.BodyRoot = phase0.Root(bodyRoot) if len(jsonData.CurrentSyncCommitteeBranch) == 0 { return errors.New("current sync committee branch is required") @@ -187,47 +116,6 @@ func (b *Bootstrap) UnmarshalJSON(input []byte) error { return nil } -func (b *BootstrapHeader) UnmarshalJSON(input []byte) error { - var err error - - var jsonData bootstrapHeaderJSON - if err = json.Unmarshal(input, &jsonData); err != nil { - return errors.Wrap(err, "invalid JSON") - } - - slot, err := strconv.ParseUint(jsonData.Slot, 10, 64) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid slot: %s", jsonData.Slot)) - } - b.Slot = phase0.Slot(slot) - - proposerIndex, err := strconv.ParseUint(jsonData.ProposerIndex, 10, 64) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid proposer index: %s", jsonData.ProposerIndex)) - } - b.ProposerIndex = phase0.ValidatorIndex(proposerIndex) - - parentRoot, err := hex.DecodeString(jsonData.ParentRoot) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid parent root: %s", jsonData.ParentRoot)) - } - b.ParentRoot = phase0.Root(parentRoot) - - stateRoot, err := hex.DecodeString(jsonData.StateRoot) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid state root: %s", jsonData.StateRoot)) - } - b.StateRoot = phase0.Root(stateRoot) - - bodyRoot, err := hex.DecodeString(jsonData.BodyRoot) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid body root: %s", jsonData.BodyRoot)) - } - b.BodyRoot = phase0.Root(bodyRoot) - - return nil -} - func (b *BootstrapCurrentSyncCommittee) UnmarshalJSON(input []byte) error { var err error diff --git a/pkg/beacon/api/types/lightclient/bootstrap_header.go b/pkg/beacon/api/types/lightclient/bootstrap_header.go new file mode 100644 index 0000000..24905e5 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/bootstrap_header.go @@ -0,0 +1,42 @@ +package lightclient + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +// BootstrapHeader is the header of a light client bootstrap. +type BootstrapHeader struct { + Beacon BeaconBlockHeader `json:"beacon"` +} + +// bootstrapHeaderJSON is the JSON representation of a bootstrap header. +type bootstrapHeaderJSON struct { + Beacon beaconBlockHeaderJSON `json:"beacon"` +} + +func (h *BootstrapHeader) ToJSON() bootstrapHeaderJSON { + return bootstrapHeaderJSON{ + Beacon: h.Beacon.ToJSON(), + } +} + +func (h *BootstrapHeader) FromJSON(input bootstrapHeaderJSON) error { + return h.Beacon.FromJSON(input.Beacon) +} + +func (h BootstrapHeader) MarshalJSON() ([]byte, error) { + return json.Marshal(h.ToJSON()) +} + +func (b *BootstrapHeader) UnmarshalJSON(input []byte) error { + var err error + + var jsonData bootstrapHeaderJSON + if err = json.Unmarshal(input, &jsonData); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + return b.FromJSON(jsonData) +} diff --git a/pkg/beacon/api/types/lightclient/bootstrap_test.go b/pkg/beacon/api/types/lightclient/bootstrap_test.go index ba899ff..2195a1a 100644 --- a/pkg/beacon/api/types/lightclient/bootstrap_test.go +++ b/pkg/beacon/api/types/lightclient/bootstrap_test.go @@ -12,11 +12,13 @@ import ( func TestBootstrap_MarshalJSON(t *testing.T) { bootstrap := &lightclient.Bootstrap{ Header: lightclient.BootstrapHeader{ - Slot: 123, - ProposerIndex: 456, - ParentRoot: phase0.Root{0x01}, - StateRoot: phase0.Root{0x02}, - BodyRoot: phase0.Root{0x03}, + Beacon: lightclient.BeaconBlockHeader{ + Slot: 123, + ProposerIndex: 456, + ParentRoot: phase0.Root{0x01}, + StateRoot: phase0.Root{0x02}, + BodyRoot: phase0.Root{0x03}, + }, }, CurrentSyncCommittee: lightclient.BootstrapCurrentSyncCommittee{ Pubkeys: []phase0.BLSPubKey{{0x04}, {0x05}}, @@ -30,11 +32,13 @@ func TestBootstrap_MarshalJSON(t *testing.T) { expectedJSON := `{ "header": { - "slot": "123", - "proposer_index": "456", - "parent_root": "0x0100000000000000000000000000000000000000000000000000000000000000", - "state_root": "0x0200000000000000000000000000000000000000000000000000000000000000", - "body_root": "0x0300000000000000000000000000000000000000000000000000000000000000" + "beacon": { + "slot": "123", + "proposer_index": "456", + "parent_root": "0x0100000000000000000000000000000000000000000000000000000000000000", + "state_root": "0x0200000000000000000000000000000000000000000000000000000000000000", + "body_root": "0x0300000000000000000000000000000000000000000000000000000000000000" + } }, "current_sync_committee": { "pubkeys": [ @@ -54,11 +58,13 @@ func TestBootstrap_MarshalJSON(t *testing.T) { func TestBootstrap_UnmarshalJSON(t *testing.T) { jsonData := []byte(`{ "header": { - "slot": "123", - "proposer_index": "456", - "parent_root": "0x0100000000000000000000000000000000000000000000000000000000000000", - "state_root": "0x0200000000000000000000000000000000000000000000000000000000000000", - "body_root": "0x0300000000000000000000000000000000000000000000000000000000000000" + "beacon": { + "slot": "123", + "proposer_index": "456", + "parent_root": "0x0100000000000000000000000000000000000000000000000000000000000000", + "state_root": "0x0200000000000000000000000000000000000000000000000000000000000000", + "body_root": "0x0300000000000000000000000000000000000000000000000000000000000000" + } }, "current_sync_committee": { "pubkeys": [ @@ -77,11 +83,11 @@ func TestBootstrap_UnmarshalJSON(t *testing.T) { err := json.Unmarshal(jsonData, &bootstrap) require.NoError(t, err) - require.Equal(t, phase0.Slot(123), bootstrap.Header.Slot) - require.Equal(t, phase0.ValidatorIndex(456), bootstrap.Header.ProposerIndex) - require.Equal(t, phase0.Root{0x01}, bootstrap.Header.ParentRoot) - require.Equal(t, phase0.Root{0x02}, bootstrap.Header.StateRoot) - require.Equal(t, phase0.Root{0x03}, bootstrap.Header.BodyRoot) + require.Equal(t, phase0.Slot(123), bootstrap.Header.Beacon.Slot) + require.Equal(t, phase0.ValidatorIndex(456), bootstrap.Header.Beacon.ProposerIndex) + require.Equal(t, phase0.Root{0x01}, bootstrap.Header.Beacon.ParentRoot) + require.Equal(t, phase0.Root{0x02}, bootstrap.Header.Beacon.StateRoot) + require.Equal(t, phase0.Root{0x03}, bootstrap.Header.Beacon.BodyRoot) require.Equal(t, []phase0.BLSPubKey{{0x04}, {0x05}}, bootstrap.CurrentSyncCommittee.Pubkeys) require.Equal(t, phase0.BLSPubKey{0x06}, bootstrap.CurrentSyncCommittee.AggregatePubkey) require.Equal(t, []phase0.Root{{0x07}, {0x08}}, bootstrap.CurrentSyncCommitteeBranch)