From 30947fadc9a1098ef1f04f1b29f26f51723efc81 Mon Sep 17 00:00:00 2001 From: Sam Calder-Mason Date: Tue, 22 Oct 2024 12:27:48 +1000 Subject: [PATCH] feat: Add light_client routes --- pkg/beacon/api/api.go | 70 +++++ .../types/lightclient/beacon_block_header.go | 85 ++++++ .../lightclient/beacon_block_header_test.go | 84 ++++++ pkg/beacon/api/types/lightclient/bootstrap.go | 256 ++++++++++++++++ .../api/types/lightclient/bootstrap_test.go | 88 ++++++ .../api/types/lightclient/finality_update.go | 94 ++++++ .../types/lightclient/finality_update_test.go | 130 +++++++++ pkg/beacon/api/types/lightclient/header.go | 36 +++ .../api/types/lightclient/header_test.go | 80 +++++ .../types/lightclient/optimistic_update.go | 43 +++ .../lightclient/optimsitic_update_test.go | 114 ++++++++ .../api/types/lightclient/sync_aggregate.go | 68 +++++ .../types/lightclient/sync_aggregate_test.go | 80 +++++ .../api/types/lightclient/sync_committee.go | 69 +++++ .../types/lightclient/sync_committee_test.go | 86 ++++++ pkg/beacon/api/types/lightclient/update.go | 105 +++++++ .../api/types/lightclient/update_test.go | 184 ++++++++++++ pkg/beacon/fetch.go | 274 ++++++++++++++++-- 18 files changed, 1927 insertions(+), 19 deletions(-) create mode 100644 pkg/beacon/api/types/lightclient/beacon_block_header.go create mode 100644 pkg/beacon/api/types/lightclient/beacon_block_header_test.go create mode 100644 pkg/beacon/api/types/lightclient/bootstrap.go create mode 100644 pkg/beacon/api/types/lightclient/bootstrap_test.go create mode 100644 pkg/beacon/api/types/lightclient/finality_update.go create mode 100644 pkg/beacon/api/types/lightclient/finality_update_test.go create mode 100644 pkg/beacon/api/types/lightclient/header.go create mode 100644 pkg/beacon/api/types/lightclient/header_test.go create mode 100644 pkg/beacon/api/types/lightclient/optimistic_update.go create mode 100644 pkg/beacon/api/types/lightclient/optimsitic_update_test.go create mode 100644 pkg/beacon/api/types/lightclient/sync_aggregate.go create mode 100644 pkg/beacon/api/types/lightclient/sync_aggregate_test.go create mode 100644 pkg/beacon/api/types/lightclient/sync_committee.go create mode 100644 pkg/beacon/api/types/lightclient/sync_committee_test.go create mode 100644 pkg/beacon/api/types/lightclient/update.go create mode 100644 pkg/beacon/api/types/lightclient/update_test.go diff --git a/pkg/beacon/api/api.go b/pkg/beacon/api/api.go index f6f9e91..8a7d7d0 100644 --- a/pkg/beacon/api/api.go +++ b/pkg/beacon/api/api.go @@ -4,12 +4,14 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" "net/url" "github.com/ethpandaops/beacon/pkg/beacon/api/types" + "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" "github.com/sirupsen/logrus" ) @@ -22,6 +24,10 @@ type ConsensusClient interface { RawDebugBeaconState(ctx context.Context, stateID string, contentType string) ([]byte, error) DepositSnapshot(ctx context.Context) (*types.DepositSnapshot, error) NodeIdentity(ctx context.Context) (*types.Identity, error) + LightClientBootstrap(ctx context.Context, blockRoot string) (*lightclient.Bootstrap, error) + LightClientUpdate(ctx context.Context, startPeriod, count int) (*lightclient.Update, error) + LightClientFinalityUpdate(ctx context.Context) (*lightclient.FinalityUpdate, error) + LightClientOptimisticUpdate(ctx context.Context) (*lightclient.OptimisticUpdate, error) } type consensusClient struct { @@ -250,3 +256,67 @@ func (c *consensusClient) NodeIdentity(ctx context.Context) (*types.Identity, er return &rsp, nil } + +func (c *consensusClient) LightClientBootstrap(ctx context.Context, blockRoot string) (*lightclient.Bootstrap, error) { + data, err := c.get(ctx, fmt.Sprintf("/eth/v1/beacon/light_client/bootstrap/%s", blockRoot)) + if err != nil { + return nil, err + } + + rsp := lightclient.Bootstrap{} + if err := json.Unmarshal(data, &rsp); err != nil { + return nil, err + } + + return &rsp, nil +} + +func (c *consensusClient) LightClientUpdate(ctx context.Context, startPeriod, count int) (*lightclient.Update, error) { + if count == 0 { + return nil, errors.New("count must be greater than 0") + } + + params := url.Values{} + 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 { + return nil, err + } + + rsp := lightclient.Update{} + if err := json.Unmarshal(data, &rsp); err != nil { + return nil, err + } + + return &rsp, nil +} + +func (c *consensusClient) LightClientFinalityUpdate(ctx context.Context) (*lightclient.FinalityUpdate, error) { + data, err := c.get(ctx, "/eth/v1/beacon/light_client/finality_update") + if err != nil { + return nil, err + } + + rsp := lightclient.FinalityUpdate{} + if err := json.Unmarshal(data, &rsp); err != nil { + return nil, err + } + + return &rsp, nil +} + +func (c *consensusClient) LightClientOptimisticUpdate(ctx context.Context) (*lightclient.OptimisticUpdate, error) { + data, err := c.get(ctx, "/eth/v1/beacon/light_client/optimistic_update") + if err != nil { + return nil, err + } + + rsp := lightclient.OptimisticUpdate{} + if err := json.Unmarshal(data, &rsp); err != nil { + return nil, err + } + + return &rsp, nil +} diff --git a/pkg/beacon/api/types/lightclient/beacon_block_header.go b/pkg/beacon/api/types/lightclient/beacon_block_header.go new file mode 100644 index 0000000..e837098 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/beacon_block_header.go @@ -0,0 +1,85 @@ +package lightclient + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// BeaconBlockHeader represents a beacon block header. +type BeaconBlockHeader struct { + Slot phase0.Slot `json:"slot"` + ProposerIndex phase0.ValidatorIndex `json:"proposer_index"` + ParentRoot phase0.Root `json:"parent_root"` + StateRoot phase0.Root `json:"state_root"` + BodyRoot phase0.Root `json:"body_root"` +} + +type beaconBlockHeaderJSON 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"` +} + +func (h *BeaconBlockHeader) ToJSON() beaconBlockHeaderJSON { + return beaconBlockHeaderJSON{ + Slot: fmt.Sprintf("%d", h.Slot), + ProposerIndex: fmt.Sprintf("%d", h.ProposerIndex), + ParentRoot: h.ParentRoot.String(), + StateRoot: h.StateRoot.String(), + BodyRoot: h.BodyRoot.String(), + } +} + +func (h *BeaconBlockHeader) FromJSON(data beaconBlockHeaderJSON) error { + slot, err := strconv.ParseUint(data.Slot, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid slot") + } + h.Slot = phase0.Slot(slot) + + proposerIndex, err := strconv.ParseUint(data.ProposerIndex, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid proposer index") + } + h.ProposerIndex = phase0.ValidatorIndex(proposerIndex) + + parentRoot, err := hex.DecodeString(strings.TrimPrefix(data.ParentRoot, "0x")) + if err != nil { + return errors.Wrap(err, "invalid parent root") + } + h.ParentRoot = phase0.Root(parentRoot) + + stateRoot, err := hex.DecodeString(strings.TrimPrefix(data.StateRoot, "0x")) + if err != nil { + return errors.Wrap(err, "invalid state root") + } + h.StateRoot = phase0.Root(stateRoot) + + bodyRoot, err := hex.DecodeString(strings.TrimPrefix(data.BodyRoot, "0x")) + if err != nil { + return errors.Wrap(err, "invalid body root") + } + h.BodyRoot = phase0.Root(bodyRoot) + + return nil +} + +func (h BeaconBlockHeader) MarshalJSON() ([]byte, error) { + return json.Marshal(h.ToJSON()) +} + +func (h *BeaconBlockHeader) UnmarshalJSON(data []byte) error { + var jsonData beaconBlockHeaderJSON + if err := json.Unmarshal(data, &jsonData); err != nil { + return err + } + return h.FromJSON(jsonData) +} diff --git a/pkg/beacon/api/types/lightclient/beacon_block_header_test.go b/pkg/beacon/api/types/lightclient/beacon_block_header_test.go new file mode 100644 index 0000000..570ab6d --- /dev/null +++ b/pkg/beacon/api/types/lightclient/beacon_block_header_test.go @@ -0,0 +1,84 @@ +package lightclient_test + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBeaconBlockHeaderMarshalUnmarshal(t *testing.T) { + testCases := []struct { + name string + header lightclient.BeaconBlockHeader + }{ + { + name: "Basic BeaconBlockHeader", + header: lightclient.BeaconBlockHeader{ + Slot: 1234, + ProposerIndex: 5678, + ParentRoot: phase0.Root{0x01, 0x02, 0x03}, + StateRoot: phase0.Root{0x04, 0x05, 0x06}, + BodyRoot: phase0.Root{0x07, 0x08, 0x09}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + marshaled, err := json.Marshal(tc.header) + if err != nil { + t.Fatalf("Failed to marshal LightClientHeader: %v", err) + } + + var unmarshaled lightclient.LightClientHeader + err = json.Unmarshal(marshaled, &unmarshaled) + if err != nil { + t.Fatalf("Failed to unmarshal LightClientHeader: %v", err) + } + + if !reflect.DeepEqual(tc.header, unmarshaled) { + t.Errorf("Unmarshaled LightClientHeader does not match original. Got %+v, want %+v", unmarshaled, tc.header) + } + }) + } +} + +func TestBeaconBlockHeaderUnmarshalJSON(t *testing.T) { + expectedSlot := "1" + expectedProposerIndex := "1" + expectedParentRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + expectedStateRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + expectedBodyRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + + jsonStr := `{ + "slot": "` + expectedSlot + `", + "proposer_index": "` + expectedProposerIndex + `", + "parent_root": "` + expectedParentRoot + `", + "state_root": "` + expectedStateRoot + `", + "body_root": "` + expectedBodyRoot + `" + }` + + var header lightclient.BeaconBlockHeader + err := json.Unmarshal([]byte(jsonStr), &header) + require.NoError(t, err) + + assert.Equal(t, expectedSlot, fmt.Sprintf("%d", header.Slot)) + assert.Equal(t, expectedProposerIndex, fmt.Sprintf("%d", header.ProposerIndex)) + assert.Equal(t, expectedParentRoot, header.ParentRoot.String()) + assert.Equal(t, expectedStateRoot, header.StateRoot.String()) + assert.Equal(t, expectedBodyRoot, header.BodyRoot.String()) + + // Test marshalling back to JSON + marshaled, err := json.Marshal(header) + require.NoError(t, err) + + var unmarshaled lightclient.BeaconBlockHeader + err = json.Unmarshal(marshaled, &unmarshaled) + require.NoError(t, err) +} diff --git a/pkg/beacon/api/types/lightclient/bootstrap.go b/pkg/beacon/api/types/lightclient/bootstrap.go new file mode 100644 index 0000000..acbd4b1 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/bootstrap.go @@ -0,0 +1,256 @@ +package lightclient + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// Bootstrap is a light client bootstrap. +type Bootstrap struct { + Header BootstrapHeader `json:"header"` + CurrentSyncCommittee BootstrapCurrentSyncCommittee `json:"current_sync_committee"` + CurrentSyncCommitteeBranch []phase0.Root `json:"current_sync_committee_branch"` +} + +// bootstrapJSON is the JSON representation of a bootstrap +type bootstrapJSON struct { + Header bootstrapHeaderJSON `json:"header"` + CurrentSyncCommittee bootstrapCurrentSyncCommitteeJSON `json:"current_sync_committee"` + 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 + AggregatePubkey phase0.BLSPubKey +} + +// bootstrapCurrentSyncCommitteeJSON is the JSON representation of a bootstrap current sync committee. +type bootstrapCurrentSyncCommitteeJSON struct { + Pubkeys []string `json:"pubkeys"` + AggregatePubkey string `json:"aggregate_pubkey"` +} + +// bootstrapCurrentSyncCommitteeBranchJSON is the JSON representation of a bootstrap current sync committee branch. +type bootstrapCurrentSyncCommitteeBranchJSON []string + +func (b Bootstrap) MarshalJSON() ([]byte, error) { + pubkeys := make([]string, len(b.CurrentSyncCommittee.Pubkeys)) + for i, pubkey := range b.CurrentSyncCommittee.Pubkeys { + pubkeys[i] = pubkey.String() + } + + branch := make([]string, len(b.CurrentSyncCommitteeBranch)) + for i, root := range b.CurrentSyncCommitteeBranch { + branch[i] = root.String() + } + + 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(), + }, + CurrentSyncCommittee: bootstrapCurrentSyncCommitteeJSON{ + Pubkeys: pubkeys, + AggregatePubkey: b.CurrentSyncCommittee.AggregatePubkey.String(), + }, + CurrentSyncCommitteeBranch: branch, + }) +} + +func (b *Bootstrap) UnmarshalJSON(input []byte) error { + var err error + + var jsonData bootstrapJSON + if err = json.Unmarshal(input, &jsonData); err != nil { + 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)) + } + b.Header.BodyRoot = phase0.Root(bodyRoot) + + if len(jsonData.CurrentSyncCommitteeBranch) == 0 { + return errors.New("current sync committee branch is required") + } + + if len(jsonData.CurrentSyncCommittee.Pubkeys) == 0 { + return errors.New("current sync committee pubkeys are required") + } + + pubkeys := make([]phase0.BLSPubKey, len(jsonData.CurrentSyncCommittee.Pubkeys)) + for i, pubkey := range jsonData.CurrentSyncCommittee.Pubkeys { + pubkeyBytes, err := hex.DecodeString(strings.TrimPrefix(pubkey, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid pubkey: %s", pubkey)) + } + + pubkeys[i] = phase0.BLSPubKey(pubkeyBytes) + } + b.CurrentSyncCommittee.Pubkeys = pubkeys + + if jsonData.CurrentSyncCommittee.AggregatePubkey == "" { + return errors.New("current sync committee aggregate pubkey is required") + } + + aggregatePubkeyBytes, err := hex.DecodeString(strings.TrimPrefix(jsonData.CurrentSyncCommittee.AggregatePubkey, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid aggregate pubkey: %s", jsonData.CurrentSyncCommittee.AggregatePubkey)) + } + b.CurrentSyncCommittee.AggregatePubkey = phase0.BLSPubKey(aggregatePubkeyBytes) + + branch := make([]phase0.Root, len(jsonData.CurrentSyncCommitteeBranch)) + for i, root := range jsonData.CurrentSyncCommitteeBranch { + r, err := hex.DecodeString(strings.TrimPrefix(root, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid root: %s", root)) + } + + branch[i] = phase0.Root(r) + } + + b.CurrentSyncCommitteeBranch = branch + + 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 + + var jsonData bootstrapCurrentSyncCommitteeJSON + if err = json.Unmarshal(input, &jsonData); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + b.Pubkeys = make([]phase0.BLSPubKey, len(jsonData.Pubkeys)) + for i, pubkey := range jsonData.Pubkeys { + pubkeyBytes, err := hex.DecodeString(pubkey) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid pubkey: %s", pubkey)) + } + + b.Pubkeys[i] = phase0.BLSPubKey(pubkeyBytes) + } + + aggregatePubkeyBytes, err := hex.DecodeString(jsonData.AggregatePubkey) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid aggregate pubkey: %s", jsonData.AggregatePubkey)) + } + b.AggregatePubkey = phase0.BLSPubKey(aggregatePubkeyBytes) + + return nil +} diff --git a/pkg/beacon/api/types/lightclient/bootstrap_test.go b/pkg/beacon/api/types/lightclient/bootstrap_test.go new file mode 100644 index 0000000..ba899ff --- /dev/null +++ b/pkg/beacon/api/types/lightclient/bootstrap_test.go @@ -0,0 +1,88 @@ +package lightclient_test + +import ( + "encoding/json" + "testing" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" + "github.com/stretchr/testify/require" +) + +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}, + }, + CurrentSyncCommittee: lightclient.BootstrapCurrentSyncCommittee{ + Pubkeys: []phase0.BLSPubKey{{0x04}, {0x05}}, + AggregatePubkey: phase0.BLSPubKey{0x06}, + }, + CurrentSyncCommitteeBranch: []phase0.Root{{0x07}, {0x08}}, + } + + jsonData, err := json.Marshal(bootstrap) + require.NoError(t, err) + + expectedJSON := `{ + "header": { + "slot": "123", + "proposer_index": "456", + "parent_root": "0x0100000000000000000000000000000000000000000000000000000000000000", + "state_root": "0x0200000000000000000000000000000000000000000000000000000000000000", + "body_root": "0x0300000000000000000000000000000000000000000000000000000000000000" + }, + "current_sync_committee": { + "pubkeys": [ + "0x040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "0x050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "aggregate_pubkey": "0x060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + "current_sync_committee_branch": [ + "0x0700000000000000000000000000000000000000000000000000000000000000", + "0x0800000000000000000000000000000000000000000000000000000000000000" + ] + }` + require.JSONEq(t, expectedJSON, string(jsonData)) +} + +func TestBootstrap_UnmarshalJSON(t *testing.T) { + jsonData := []byte(`{ + "header": { + "slot": "123", + "proposer_index": "456", + "parent_root": "0x0100000000000000000000000000000000000000000000000000000000000000", + "state_root": "0x0200000000000000000000000000000000000000000000000000000000000000", + "body_root": "0x0300000000000000000000000000000000000000000000000000000000000000" + }, + "current_sync_committee": { + "pubkeys": [ + "0x040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "0x050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "aggregate_pubkey": "0x060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + "current_sync_committee_branch": [ + "0x0700000000000000000000000000000000000000000000000000000000000000", + "0x0800000000000000000000000000000000000000000000000000000000000000" + ] + }`) + + var bootstrap lightclient.Bootstrap + 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.BLSPubKey{{0x04}, {0x05}}, bootstrap.CurrentSyncCommittee.Pubkeys) + require.Equal(t, phase0.BLSPubKey{0x06}, bootstrap.CurrentSyncCommittee.AggregatePubkey) + require.Equal(t, []phase0.Root{{0x07}, {0x08}}, bootstrap.CurrentSyncCommitteeBranch) +} diff --git a/pkg/beacon/api/types/lightclient/finality_update.go b/pkg/beacon/api/types/lightclient/finality_update.go new file mode 100644 index 0000000..5e4d08d --- /dev/null +++ b/pkg/beacon/api/types/lightclient/finality_update.go @@ -0,0 +1,94 @@ +package lightclient + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// FinalityUpdate represents a finality update for light clients. +type FinalityUpdate struct { + AttestedHeader LightClientHeader + FinalizedHeader LightClientHeader + FinalityBranch []phase0.Root + SyncAggregate SyncAggregate + SignatureSlot phase0.Slot +} + +type finalityUpdateJSON struct { + AttestedHeader lightClientHeaderJSON `json:"attested_header"` + FinalizedHeader lightClientHeaderJSON `json:"finalized_header"` + FinalityBranch []string `json:"finality_branch"` + SyncAggregate syncAggregateJSON `json:"sync_aggregate"` + SignatureSlot string `json:"signature_slot"` +} + +func (f *FinalityUpdate) UnmarshalJSON(data []byte) error { + var jsonData finalityUpdateJSON + if err := json.Unmarshal(data, &jsonData); err != nil { + return err + } + return f.FromJSON(jsonData) +} + +func (f *FinalityUpdate) FromJSON(data finalityUpdateJSON) error { + attestedHeader := LightClientHeader{} + if err := attestedHeader.FromJSON(data.AttestedHeader); err != nil { + return errors.Wrap(err, "failed to unmarshal attested header") + } + f.AttestedHeader = attestedHeader + + finalizedHeader := LightClientHeader{} + if err := finalizedHeader.FromJSON(data.FinalizedHeader); err != nil { + return errors.Wrap(err, "failed to unmarshal finalized header") + } + f.FinalizedHeader = finalizedHeader + + finalityBranch := make([]phase0.Root, len(data.FinalityBranch)) + for i, root := range data.FinalityBranch { + decoded, err := hex.DecodeString(strings.TrimPrefix(root, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to unmarshal finality branch at index %d: %s", i, root)) + } + finalityBranch[i] = phase0.Root(decoded) + } + f.FinalityBranch = finalityBranch + + syncAggregate := SyncAggregate{} + if err := syncAggregate.FromJSON(data.SyncAggregate); err != nil { + return errors.Wrap(err, "failed to unmarshal sync aggregate") + } + f.SyncAggregate = syncAggregate + + signatureSlot, err := strconv.ParseUint(data.SignatureSlot, 10, 64) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to unmarshal signature slot: %s", data.SignatureSlot)) + } + f.SignatureSlot = phase0.Slot(signatureSlot) + + return nil +} + +func (f FinalityUpdate) MarshalJSON() ([]byte, error) { + return json.Marshal(f.ToJSON()) +} + +func (f *FinalityUpdate) ToJSON() finalityUpdateJSON { + finalityBranch := make([]string, len(f.FinalityBranch)) + for i, root := range f.FinalityBranch { + finalityBranch[i] = fmt.Sprintf("%x", root) + } + + return finalityUpdateJSON{ + AttestedHeader: f.AttestedHeader.ToJSON(), + FinalizedHeader: f.FinalizedHeader.ToJSON(), + FinalityBranch: finalityBranch, + SyncAggregate: f.SyncAggregate.ToJSON(), + SignatureSlot: fmt.Sprintf("%d", f.SignatureSlot), + } +} diff --git a/pkg/beacon/api/types/lightclient/finality_update_test.go b/pkg/beacon/api/types/lightclient/finality_update_test.go new file mode 100644 index 0000000..51cc755 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/finality_update_test.go @@ -0,0 +1,130 @@ +package lightclient + +import ( + "encoding/json" + "testing" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/prysmaticlabs/go-bitfield" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFinalityUpdateMarshalUnmarshal(t *testing.T) { + originalUpdate := &FinalityUpdate{ + AttestedHeader: LightClientHeader{ + Beacon: BeaconBlockHeader{ + Slot: 123, + ProposerIndex: 456, + ParentRoot: phase0.Root{0x01}, + StateRoot: phase0.Root{0x02}, + BodyRoot: phase0.Root{0x03}, + }, + }, + FinalizedHeader: LightClientHeader{ + Beacon: BeaconBlockHeader{ + Slot: 789, + ProposerIndex: 101, + ParentRoot: phase0.Root{0x04}, + StateRoot: phase0.Root{0x05}, + BodyRoot: phase0.Root{0x06}, + }, + }, + FinalityBranch: []phase0.Root{{01, 02}, {03, 04}}, + SyncAggregate: SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512{1, 1, 1, 0, 0, 1}, + SyncCommitteeSignature: [96]byte{0x0a}, + }, + SignatureSlot: 1234, + } + + // Marshal to JSON + jsonData, err := json.Marshal(originalUpdate) + require.NoError(t, err) + + // Unmarshal from JSON + var unmarshaledUpdate FinalityUpdate + err = json.Unmarshal(jsonData, &unmarshaledUpdate) + require.NoError(t, err) + + // Compare original and unmarshaled data + assert.Equal(t, originalUpdate.AttestedHeader, unmarshaledUpdate.AttestedHeader) + assert.Equal(t, originalUpdate.FinalizedHeader, unmarshaledUpdate.FinalizedHeader) + assert.Equal(t, originalUpdate.FinalityBranch, unmarshaledUpdate.FinalityBranch) + assert.Equal(t, originalUpdate.SyncAggregate, unmarshaledUpdate.SyncAggregate) + assert.Equal(t, originalUpdate.SignatureSlot, unmarshaledUpdate.SignatureSlot) +} + +func TestFinalityUpdateUnmarshalPhase0(t *testing.T) { + expectedRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + expectedSignature := "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + + jsonData := []byte(` + { + "attested_header": { + "beacon": { + "slot": "1", + "proposer_index": "1", + "parent_root": "` + expectedRoot + `", + "state_root": "` + expectedRoot + `", + "body_root": "` + expectedRoot + `" + } + }, + "finalized_header": { + "beacon": { + "slot": "1", + "proposer_index": "1", + "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "state_root": "` + expectedRoot + `", + "body_root": "` + expectedRoot + `" + } + }, + "finality_branch": [ + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `" + ], + "sync_aggregate": { + "sync_committee_bits": "0x01", + "sync_committee_signature": "` + expectedSignature + `" + }, + "signature_slot": "1" + }`) + + var update FinalityUpdate + err := json.Unmarshal(jsonData, &update) + require.NoError(t, err) + + assert.Equal(t, phase0.Slot(1), update.AttestedHeader.Beacon.Slot) + assert.Equal(t, phase0.ValidatorIndex(1), update.AttestedHeader.Beacon.ProposerIndex) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.ParentRoot.String()) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.StateRoot.String()) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.BodyRoot.String()) + + assert.Equal(t, phase0.Slot(1), update.FinalizedHeader.Beacon.Slot) + assert.Equal(t, phase0.ValidatorIndex(1), update.FinalizedHeader.Beacon.ProposerIndex) + assert.Equal(t, expectedRoot, update.FinalizedHeader.Beacon.ParentRoot.String()) + assert.Equal(t, expectedRoot, update.FinalizedHeader.Beacon.StateRoot.String()) + assert.Equal(t, expectedRoot, update.FinalizedHeader.Beacon.BodyRoot.String()) + + assert.Len(t, update.FinalityBranch, 6) + for _, root := range update.FinalityBranch { + assert.Equal(t, expectedRoot, root.String()) + } + + assert.Equal(t, bitfield.Bitvector512{1}, update.SyncAggregate.SyncCommitteeBits) + assert.Equal(t, expectedSignature, update.SyncAggregate.SyncCommitteeSignature.String()) + + assert.Equal(t, phase0.Slot(1), update.SignatureSlot) + + // Test marshalling back to JSON + marshaled, err := json.Marshal(update) + require.NoError(t, err) + + var unmarshaled FinalityUpdate + err = json.Unmarshal(marshaled, &unmarshaled) + require.NoError(t, err) +} diff --git a/pkg/beacon/api/types/lightclient/header.go b/pkg/beacon/api/types/lightclient/header.go new file mode 100644 index 0000000..987914e --- /dev/null +++ b/pkg/beacon/api/types/lightclient/header.go @@ -0,0 +1,36 @@ +package lightclient + +import ( + "encoding/json" +) + +// LightClientHeader represents a light client header. +type LightClientHeader struct { + Beacon BeaconBlockHeader `json:"beacon"` +} + +type lightClientHeaderJSON struct { + Beacon beaconBlockHeaderJSON `json:"beacon"` +} + +func (h *LightClientHeader) ToJSON() lightClientHeaderJSON { + return lightClientHeaderJSON{ + Beacon: h.Beacon.ToJSON(), + } +} + +func (h *LightClientHeader) FromJSON(data lightClientHeaderJSON) error { + return h.Beacon.FromJSON(data.Beacon) +} + +func (h LightClientHeader) MarshalJSON() ([]byte, error) { + return json.Marshal(h.ToJSON()) +} + +func (h *LightClientHeader) UnmarshalJSON(data []byte) error { + var jsonData lightClientHeaderJSON + if err := json.Unmarshal(data, &jsonData); err != nil { + return err + } + return h.FromJSON(jsonData) +} diff --git a/pkg/beacon/api/types/lightclient/header_test.go b/pkg/beacon/api/types/lightclient/header_test.go new file mode 100644 index 0000000..73f1d69 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/header_test.go @@ -0,0 +1,80 @@ +package lightclient_test + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLightClientHeaderMarshalUnmarshal(t *testing.T) { + testCases := []struct { + name string + header lightclient.LightClientHeader + }{ + { + name: "Basic LightClientHeader", + header: lightclient.LightClientHeader{ + Beacon: lightclient.BeaconBlockHeader{ + Slot: 1234, + ProposerIndex: 5678, + ParentRoot: phase0.Root{0x01, 0x02, 0x03}, + StateRoot: phase0.Root{0x04, 0x05, 0x06}, + BodyRoot: phase0.Root{0x07, 0x08, 0x09}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + marshaled, err := json.Marshal(tc.header) + if err != nil { + t.Fatalf("Failed to marshal LightClientHeader: %v", err) + } + + var unmarshaled lightclient.LightClientHeader + err = json.Unmarshal(marshaled, &unmarshaled) + if err != nil { + t.Fatalf("Failed to unmarshal LightClientHeader: %v", err) + } + + if !reflect.DeepEqual(tc.header, unmarshaled) { + t.Errorf("Unmarshaled LightClientHeader does not match original. Got %+v, want %+v", unmarshaled, tc.header) + } + }) + } +} + +func TestLightClientHeaderUnmarshalJSON(t *testing.T) { + expectedSlot := "1" + expectedProposerIndex := "1" + expectedParentRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + expectedStateRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + expectedBodyRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + + jsonStr := `{ + "beacon": { + "slot": "` + expectedSlot + `", + "proposer_index": "` + expectedProposerIndex + `", + "parent_root": "` + expectedParentRoot + `", + "state_root": "` + expectedStateRoot + `", + "body_root": "` + expectedBodyRoot + `" + } + }` + + var header lightclient.LightClientHeader + err := json.Unmarshal([]byte(jsonStr), &header) + require.NoError(t, err) + + assert.Equal(t, expectedSlot, fmt.Sprintf("%d", header.Beacon.Slot)) + assert.Equal(t, expectedProposerIndex, fmt.Sprintf("%d", header.Beacon.ProposerIndex)) + assert.Equal(t, expectedParentRoot, header.Beacon.ParentRoot.String()) + assert.Equal(t, expectedStateRoot, header.Beacon.StateRoot.String()) + assert.Equal(t, expectedBodyRoot, header.Beacon.BodyRoot.String()) +} diff --git a/pkg/beacon/api/types/lightclient/optimistic_update.go b/pkg/beacon/api/types/lightclient/optimistic_update.go new file mode 100644 index 0000000..2340b6b --- /dev/null +++ b/pkg/beacon/api/types/lightclient/optimistic_update.go @@ -0,0 +1,43 @@ +package lightclient + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +// OptimisticUpdate represents a light client optimistic update. +type OptimisticUpdate struct { + AttestedHeader LightClientHeader `json:"attested_header"` + SyncAggregate SyncAggregate `json:"sync_aggregate"` +} + +// optimisticUpdateJSON is the JSON representation of an optimistic update +type optimisticUpdateJSON struct { + AttestedHeader lightClientHeaderJSON `json:"attested_header"` + SyncAggregate syncAggregateJSON `json:"sync_aggregate"` +} + +func (u OptimisticUpdate) MarshalJSON() ([]byte, error) { + return json.Marshal(&optimisticUpdateJSON{ + AttestedHeader: u.AttestedHeader.ToJSON(), + SyncAggregate: u.SyncAggregate.ToJSON(), + }) +} + +func (u *OptimisticUpdate) UnmarshalJSON(input []byte) error { + var jsonData optimisticUpdateJSON + if err := json.Unmarshal(input, &jsonData); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if err := u.AttestedHeader.FromJSON(jsonData.AttestedHeader); err != nil { + return errors.Wrap(err, "invalid attested header") + } + + if err := u.SyncAggregate.FromJSON(jsonData.SyncAggregate); err != nil { + return errors.Wrap(err, "invalid sync aggregate") + } + + return nil +} diff --git a/pkg/beacon/api/types/lightclient/optimsitic_update_test.go b/pkg/beacon/api/types/lightclient/optimsitic_update_test.go new file mode 100644 index 0000000..c1cf366 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/optimsitic_update_test.go @@ -0,0 +1,114 @@ +package lightclient_test + +import ( + "fmt" + "testing" + + "encoding/json" + "reflect" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" + "github.com/prysmaticlabs/go-bitfield" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOptimisticUpdateMarshalUnmarshal(t *testing.T) { + testCases := []struct { + name string + update lightclient.OptimisticUpdate + }{ + { + name: "Basic Update", + update: lightclient.OptimisticUpdate{ + AttestedHeader: lightclient.LightClientHeader{ + Beacon: lightclient.BeaconBlockHeader{ + Slot: 1234, + ProposerIndex: 5678, + ParentRoot: phase0.Root{0x01}, + StateRoot: phase0.Root{0x02}, + BodyRoot: phase0.Root{0x03}, + }, + }, + SyncAggregate: lightclient.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512{0, 1}, + SyncCommitteeSignature: [96]byte{0x0c}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Marshal + marshaled, err := json.Marshal(tc.update) + if err != nil { + t.Fatalf("Failed to marshal Update: %v", err) + } + + // Unmarshal + var unmarshaled lightclient.OptimisticUpdate + err = json.Unmarshal(marshaled, &unmarshaled) + if err != nil { + t.Fatalf("Failed to unmarshal Update: %v", err) + } + + // Compare + if !reflect.DeepEqual(tc.update, unmarshaled) { + t.Errorf("Unmarshaled Update does not match original. Got %+v, want %+v", unmarshaled, tc.update) + } + }) + } +} + +func TestOptimisticUpdateUnmarshalJSON(t *testing.T) { + expectedRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + expectedSignature := "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + expectedBits := "0x01" + + jsonStr := `{ + "attested_header": { + "beacon": { + "slot": "1", + "proposer_index": "1", + "parent_root": "` + expectedRoot + `", + "state_root": "` + expectedRoot + `", + "body_root": "` + expectedRoot + `" + } + }, + "sync_aggregate": { + "sync_committee_bits": "` + expectedBits + `", + "sync_committee_signature": "` + expectedSignature + `" + } + }` + + var update lightclient.OptimisticUpdate + err := json.Unmarshal([]byte(jsonStr), &update) + require.NoError(t, err) + + // Check all fields manually + assert.Equal(t, phase0.Slot(1), update.AttestedHeader.Beacon.Slot) + assert.Equal(t, phase0.ValidatorIndex(1), update.AttestedHeader.Beacon.ProposerIndex) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.ParentRoot.String()) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.StateRoot.String()) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.BodyRoot.String()) + + assert.Equal(t, expectedBits, fmt.Sprintf("%#x", update.SyncAggregate.SyncCommitteeBits.Bytes())) + + assert.Equal(t, expectedSignature, update.SyncAggregate.SyncCommitteeSignature.String()) + + // Marshal back to JSON + marshaledJSON, err := json.Marshal(update) + require.NoError(t, err) + + // Unmarshal both JSONs to interfaces for comparison + var originalData, remarshaledData interface{} + err = json.Unmarshal([]byte(jsonStr), &originalData) + require.NoError(t, err) + err = json.Unmarshal(marshaledJSON, &remarshaledData) + require.NoError(t, err) + + // Compare the unmarshaled data + assert.Equal(t, originalData, remarshaledData, "Remarshaled JSON does not match the original") +} diff --git a/pkg/beacon/api/types/lightclient/sync_aggregate.go b/pkg/beacon/api/types/lightclient/sync_aggregate.go new file mode 100644 index 0000000..b0af754 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/sync_aggregate.go @@ -0,0 +1,68 @@ +package lightclient + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "github.com/prysmaticlabs/go-bitfield" +) + +// SyncAggregate represents a sync aggregate. +type SyncAggregate struct { + SyncCommitteeBits bitfield.Bitvector512 `json:"sync_committee_bits"` + SyncCommitteeSignature phase0.BLSSignature `json:"sync_committee_signature"` +} + +type syncAggregateJSON struct { + SyncCommitteeBits string `json:"sync_committee_bits"` + SyncCommitteeSignature string `json:"sync_committee_signature"` +} + +func (s *SyncAggregate) ToJSON() syncAggregateJSON { + return syncAggregateJSON{ + SyncCommitteeBits: fmt.Sprintf("%#x", s.SyncCommitteeBits.Bytes()), + SyncCommitteeSignature: fmt.Sprintf("%#x", s.SyncCommitteeSignature), + } +} + +func (s *SyncAggregate) FromJSON(data syncAggregateJSON) error { + if data.SyncCommitteeBits == "" { + return errors.New("sync committee bits are required") + } + + if data.SyncCommitteeSignature == "" { + return errors.New("sync committee signature is required") + } + + bits, err := hex.DecodeString(strings.TrimPrefix(data.SyncCommitteeBits, "0x")) + if err != nil { + return errors.Wrap(err, "invalid sync committee bits") + } + + s.SyncCommitteeBits = bitfield.Bitvector512(bits) + + signature, err := hex.DecodeString(strings.TrimPrefix(data.SyncCommitteeSignature, "0x")) + if err != nil { + return errors.Wrap(err, "invalid sync committee signature") + } + s.SyncCommitteeSignature = phase0.BLSSignature(signature) + + return nil +} + +func (s SyncAggregate) MarshalJSON() ([]byte, error) { + return json.Marshal(s.ToJSON()) +} + +func (s *SyncAggregate) UnmarshalJSON(input []byte) error { + var data syncAggregateJSON + if err := json.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "failed to unmarshal sync aggregate") + } + + return s.FromJSON(data) +} diff --git a/pkg/beacon/api/types/lightclient/sync_aggregate_test.go b/pkg/beacon/api/types/lightclient/sync_aggregate_test.go new file mode 100644 index 0000000..3d42128 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/sync_aggregate_test.go @@ -0,0 +1,80 @@ +package lightclient_test + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" + "github.com/prysmaticlabs/go-bitfield" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSyncAggregateMarshalUnmarshal(t *testing.T) { + testCases := []struct { + name string + syncAggregate lightclient.SyncAggregate + }{ + { + name: "Basic SyncAggregate", + syncAggregate: lightclient.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512{0, 1, 0, 1, 0}, + SyncCommitteeSignature: phase0.BLSSignature{0x03, 0x04}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Marshal + marshaled, err := json.Marshal(tc.syncAggregate) + if err != nil { + t.Fatalf("Failed to marshal SyncAggregate: %v", err) + } + + // Unmarshal + var unmarshaled lightclient.SyncAggregate + err = json.Unmarshal(marshaled, &unmarshaled) + if err != nil { + t.Fatalf("Failed to unmarshal SyncAggregate: %v", err) + } + + // Compare + if !reflect.DeepEqual(tc.syncAggregate, unmarshaled) { + t.Errorf("Unmarshaled SyncAggregate does not match original. Got %+v, want %+v", unmarshaled, tc.syncAggregate) + } + }) + } +} + +func TestSyncAggregateUnmarshalJSON(t *testing.T) { + expectedBits := "0x01" + expectedSignature := "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + jsonStr := ` + { + "sync_committee_bits": "` + expectedBits + `", + "sync_committee_signature": "` + expectedSignature + `" + } + ` + + var syncAggregate lightclient.SyncAggregate + err := json.Unmarshal([]byte(jsonStr), &syncAggregate) + require.NoError(t, err) + + assert.Equal(t, expectedBits, fmt.Sprintf("%#x", syncAggregate.SyncCommitteeBits.Bytes())) + assert.Equal(t, expectedSignature, fmt.Sprintf("%#x", syncAggregate.SyncCommitteeSignature)) + + // Test marshalling back to JSON + marshaled, err := json.Marshal(syncAggregate) + require.NoError(t, err) + + var unmarshaled lightclient.SyncAggregate + err = json.Unmarshal(marshaled, &unmarshaled) + require.NoError(t, err) + + assert.Equal(t, expectedBits, fmt.Sprintf("%#x", unmarshaled.SyncCommitteeBits.Bytes())) + assert.Equal(t, expectedSignature, fmt.Sprintf("%#x", unmarshaled.SyncCommitteeSignature)) +} diff --git a/pkg/beacon/api/types/lightclient/sync_committee.go b/pkg/beacon/api/types/lightclient/sync_committee.go new file mode 100644 index 0000000..3ef9cd8 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/sync_committee.go @@ -0,0 +1,69 @@ +package lightclient + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// SyncCommittee represents a sync committee. +type SyncCommittee struct { + Pubkeys []phase0.BLSPubKey `json:"pubkeys"` + AggregatePubkey phase0.BLSPubKey `json:"aggregate_pubkey"` +} + +// syncCommitteeJSON is the JSON representation of a sync committee. +type syncCommitteeJSON struct { + Pubkeys []string `json:"pubkeys"` + AggregatePubkey string `json:"aggregate_pubkey"` +} + +// ToJSON converts a SyncCommittee to its JSON representation. +func (s *SyncCommittee) ToJSON() syncCommitteeJSON { + pubkeys := make([]string, len(s.Pubkeys)) + for i, pubkey := range s.Pubkeys { + pubkeys[i] = fmt.Sprintf("%#x", pubkey) + } + return syncCommitteeJSON{ + Pubkeys: pubkeys, + AggregatePubkey: fmt.Sprintf("%#x", s.AggregatePubkey), + } +} + +// FromJSON converts a JSON representation of a SyncCommittee to a SyncCommittee. +func (s *SyncCommittee) FromJSON(data syncCommitteeJSON) error { + s.Pubkeys = make([]phase0.BLSPubKey, len(data.Pubkeys)) + for i, pubkey := range data.Pubkeys { + pk, err := hex.DecodeString(strings.TrimPrefix(pubkey, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid pubkey: %s", pubkey)) + } + copy(s.Pubkeys[i][:], pk) + } + + aggregatePubkey, err := hex.DecodeString(strings.TrimPrefix(data.AggregatePubkey, "0x")) + if err != nil { + return errors.Wrap(err, "invalid aggregate pubkey") + } + copy(s.AggregatePubkey[:], aggregatePubkey) + + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (s SyncCommittee) MarshalJSON() ([]byte, error) { + return json.Marshal(s.ToJSON()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (s *SyncCommittee) UnmarshalJSON(data []byte) error { + var jsonData syncCommitteeJSON + if err := json.Unmarshal(data, &jsonData); err != nil { + return err + } + return s.FromJSON(jsonData) +} diff --git a/pkg/beacon/api/types/lightclient/sync_committee_test.go b/pkg/beacon/api/types/lightclient/sync_committee_test.go new file mode 100644 index 0000000..21293c0 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/sync_committee_test.go @@ -0,0 +1,86 @@ +package lightclient + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSyncCommitteeMarshalUnmarshal(t *testing.T) { + testCases := []struct { + name string + syncCommittee SyncCommittee + }{ + { + name: "Basic SyncCommittee", + syncCommittee: SyncCommittee{ + Pubkeys: []phase0.BLSPubKey{ + {0x01, 0x23, 0x45}, + {0x67, 0x89, 0xab}, + }, + AggregatePubkey: phase0.BLSPubKey{0xcd, 0xef, 0x01}, + }, + }, + { + name: "Empty SyncCommittee", + syncCommittee: SyncCommittee{ + Pubkeys: []phase0.BLSPubKey{}, + AggregatePubkey: phase0.BLSPubKey{}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + marshaled, err := json.Marshal(tc.syncCommittee) + if err != nil { + t.Fatalf("Failed to marshal SyncCommittee: %v", err) + } + + var unmarshaled SyncCommittee + err = json.Unmarshal(marshaled, &unmarshaled) + if err != nil { + t.Fatalf("Failed to unmarshal SyncCommittee: %v", err) + } + + if !reflect.DeepEqual(tc.syncCommittee, unmarshaled) { + t.Errorf("Unmarshaled SyncCommittee does not match original. Got %+v, want %+v", unmarshaled, tc.syncCommittee) + } + }) + } +} + +func TestSyncCommitteeUnmarshalJSON(t *testing.T) { + expectedPubkey := "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + + jsonStr := ` + { + "pubkeys": [ + "` + expectedPubkey + `", + "` + expectedPubkey + `" + ], + "aggregate_pubkey": "` + expectedPubkey + `" + } + ` + + var syncCommittee SyncCommittee + err := json.Unmarshal([]byte(jsonStr), &syncCommittee) + require.NoError(t, err) + + assert.Equal(t, expectedPubkey, syncCommittee.AggregatePubkey.String()) + for _, pubkey := range syncCommittee.Pubkeys { + assert.Equal(t, expectedPubkey, pubkey.String()) + } + + // Test marshalling back to JSON + marshaled, err := json.Marshal(syncCommittee) + require.NoError(t, err) + + var unmarshaled SyncCommittee + err = json.Unmarshal(marshaled, &unmarshaled) + require.NoError(t, err) +} diff --git a/pkg/beacon/api/types/lightclient/update.go b/pkg/beacon/api/types/lightclient/update.go new file mode 100644 index 0000000..35b7753 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/update.go @@ -0,0 +1,105 @@ +package lightclient + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// Update represents a light client update. +type Update struct { + AttestedHeader LightClientHeader `json:"attested_header"` + NextSyncCommittee SyncCommittee `json:"next_sync_committee"` + NextSyncCommitteeBranch []phase0.Root `json:"next_sync_committee_branch"` + FinalizedHeader LightClientHeader `json:"finalized_header"` + FinalityBranch []phase0.Root `json:"finality_branch"` + SyncAggregate SyncAggregate `json:"sync_aggregate"` + SignatureSlot phase0.Slot `json:"signature_slot"` +} + +// updateJSON is the JSON representation of an update +type updateJSON struct { + AttestedHeader lightClientHeaderJSON `json:"attested_header"` + NextSyncCommittee syncCommitteeJSON `json:"next_sync_committee"` + NextSyncCommitteeBranch []string `json:"next_sync_committee_branch"` + FinalizedHeader lightClientHeaderJSON `json:"finalized_header"` + FinalityBranch []string `json:"finality_branch"` + SyncAggregate syncAggregateJSON `json:"sync_aggregate"` + SignatureSlot string `json:"signature_slot"` +} + +func (u Update) MarshalJSON() ([]byte, error) { + nextSyncCommitteeBranch := make([]string, len(u.NextSyncCommitteeBranch)) + for i, root := range u.NextSyncCommitteeBranch { + nextSyncCommitteeBranch[i] = root.String() + } + + finalityBranch := make([]string, len(u.FinalityBranch)) + for i, root := range u.FinalityBranch { + finalityBranch[i] = root.String() + } + + return json.Marshal(&updateJSON{ + AttestedHeader: u.AttestedHeader.ToJSON(), + NextSyncCommittee: u.NextSyncCommittee.ToJSON(), + NextSyncCommitteeBranch: nextSyncCommitteeBranch, + FinalizedHeader: u.FinalizedHeader.ToJSON(), + FinalityBranch: finalityBranch, + SyncAggregate: u.SyncAggregate.ToJSON(), + SignatureSlot: fmt.Sprintf("%d", u.SignatureSlot), + }) +} + +func (u *Update) UnmarshalJSON(input []byte) error { + var jsonData updateJSON + if err := json.Unmarshal(input, &jsonData); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if err := u.AttestedHeader.FromJSON(jsonData.AttestedHeader); err != nil { + return errors.Wrap(err, "invalid attested header") + } + + if err := u.NextSyncCommittee.FromJSON(jsonData.NextSyncCommittee); err != nil { + return errors.Wrap(err, "invalid next sync committee") + } + + u.NextSyncCommitteeBranch = make([]phase0.Root, len(jsonData.NextSyncCommitteeBranch)) + for i, root := range jsonData.NextSyncCommitteeBranch { + r, err := hex.DecodeString(strings.TrimPrefix(root, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid next sync committee branch root: %s", root)) + } + u.NextSyncCommitteeBranch[i] = phase0.Root(r) + } + + if err := u.FinalizedHeader.FromJSON(jsonData.FinalizedHeader); err != nil { + return errors.Wrap(err, "invalid finalized header") + } + + u.FinalityBranch = make([]phase0.Root, len(jsonData.FinalityBranch)) + for i, root := range jsonData.FinalityBranch { + r, err := hex.DecodeString(strings.TrimPrefix(root, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid finality branch root: %s", root)) + } + u.FinalityBranch[i] = phase0.Root(r) + } + + if err := u.SyncAggregate.FromJSON(jsonData.SyncAggregate); err != nil { + return errors.Wrap(err, "invalid sync aggregate") + } + + slot, err := strconv.ParseUint(jsonData.SignatureSlot, 10, 64) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid signature slot: %s", jsonData.SignatureSlot)) + } + u.SignatureSlot = phase0.Slot(slot) + + return nil +} diff --git a/pkg/beacon/api/types/lightclient/update_test.go b/pkg/beacon/api/types/lightclient/update_test.go new file mode 100644 index 0000000..9635c59 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/update_test.go @@ -0,0 +1,184 @@ +package lightclient_test + +import ( + "testing" + + "encoding/json" + "reflect" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" + "github.com/prysmaticlabs/go-bitfield" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdateMarshalUnmarshal(t *testing.T) { + testCases := []struct { + name string + update lightclient.Update + }{ + { + name: "Basic Update", + update: lightclient.Update{ + AttestedHeader: lightclient.LightClientHeader{ + Beacon: lightclient.BeaconBlockHeader{ + Slot: 1234, + ProposerIndex: 5678, + ParentRoot: phase0.Root{0x01}, + StateRoot: phase0.Root{0x02}, + BodyRoot: phase0.Root{0x03}, + }, + }, + NextSyncCommittee: lightclient.SyncCommittee{ + Pubkeys: []phase0.BLSPubKey{{0x04}}, + AggregatePubkey: phase0.BLSPubKey{0x05}, + }, + NextSyncCommitteeBranch: []phase0.Root{{0x06}}, + FinalizedHeader: lightclient.LightClientHeader{ + Beacon: lightclient.BeaconBlockHeader{ + Slot: 5678, + ProposerIndex: 1234, + ParentRoot: phase0.Root{0x07}, + StateRoot: phase0.Root{0x08}, + BodyRoot: phase0.Root{0x09}, + }, + }, + FinalityBranch: []phase0.Root{{0x0a}}, + SyncAggregate: lightclient.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512{0, 1}, + SyncCommitteeSignature: [96]byte{0x0c}, + }, + SignatureSlot: 9876, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Marshal + marshaled, err := json.Marshal(tc.update) + if err != nil { + t.Fatalf("Failed to marshal Update: %v", err) + } + + // Unmarshal + var unmarshaled lightclient.Update + err = json.Unmarshal(marshaled, &unmarshaled) + if err != nil { + t.Fatalf("Failed to unmarshal Update: %v", err) + } + + // Compare + if !reflect.DeepEqual(tc.update, unmarshaled) { + t.Errorf("Unmarshaled Update does not match original. Got %+v, want %+v", unmarshaled, tc.update) + } + }) + } +} + +func TestUpdateUnmarshalJSON(t *testing.T) { + expectedRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + expectedSignature := "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + expectedPubkey := "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + + jsonStr := `{ + "attested_header": { + "beacon": { + "slot": "1", + "proposer_index": "1", + "parent_root": "` + expectedRoot + `", + "state_root": "` + expectedRoot + `", + "body_root": "` + expectedRoot + `" + } + }, + "next_sync_committee": { + "pubkeys": [ + "` + expectedPubkey + `", + "` + expectedPubkey + `" + ], + "aggregate_pubkey": "` + expectedPubkey + `" + }, + "next_sync_committee_branch": [ + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `" + ], + "finalized_header": { + "beacon": { + "slot": "1", + "proposer_index": "1", + "parent_root": "` + expectedRoot + `", + "state_root": "` + expectedRoot + `", + "body_root": "` + expectedRoot + `" + } + }, + "finality_branch": [ + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `" + ], + "sync_aggregate": { + "sync_committee_bits": "0x01", + "sync_committee_signature": "` + expectedSignature + `" + }, + "signature_slot": "1" + }` + + var update lightclient.Update + err := json.Unmarshal([]byte(jsonStr), &update) + require.NoError(t, err) + + // Check all fields manually + assert.Equal(t, phase0.Slot(1), update.AttestedHeader.Beacon.Slot) + assert.Equal(t, phase0.ValidatorIndex(1), update.AttestedHeader.Beacon.ProposerIndex) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.ParentRoot.String()) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.StateRoot.String()) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.BodyRoot.String()) + + assert.Len(t, update.NextSyncCommittee.Pubkeys, 2) + for _, pubkey := range update.NextSyncCommittee.Pubkeys { + assert.Equal(t, expectedPubkey, pubkey.String()) + } + assert.Equal(t, expectedPubkey, update.NextSyncCommittee.AggregatePubkey.String()) + + assert.Len(t, update.NextSyncCommitteeBranch, 5) + for _, root := range update.NextSyncCommitteeBranch { + assert.Equal(t, expectedRoot, root.String()) + } + + assert.Equal(t, phase0.Slot(1), update.FinalizedHeader.Beacon.Slot) + assert.Equal(t, phase0.ValidatorIndex(1), update.FinalizedHeader.Beacon.ProposerIndex) + assert.Equal(t, expectedRoot, update.FinalizedHeader.Beacon.ParentRoot.String()) + assert.Equal(t, expectedRoot, update.FinalizedHeader.Beacon.StateRoot.String()) + assert.Equal(t, expectedRoot, update.FinalizedHeader.Beacon.BodyRoot.String()) + + assert.Len(t, update.FinalityBranch, 6) + for _, root := range update.FinalityBranch { + assert.Equal(t, expectedRoot, root.String()) + } + + assert.Equal(t, bitfield.Bitvector512{1}, update.SyncAggregate.SyncCommitteeBits) + assert.Equal(t, expectedSignature, update.SyncAggregate.SyncCommitteeSignature.String()) + + assert.Equal(t, phase0.Slot(1), update.SignatureSlot) + + // Marshal back to JSON + marshaledJSON, err := json.Marshal(update) + require.NoError(t, err) + + // Unmarshal both JSONs to interfaces for comparison + var originalData, remarshaledData interface{} + err = json.Unmarshal([]byte(jsonStr), &originalData) + require.NoError(t, err) + err = json.Unmarshal(marshaledJSON, &remarshaledData) + require.NoError(t, err) + + // Compare the unmarshaled data + assert.Equal(t, originalData, remarshaledData, "Remarshaled JSON does not match the original") +} diff --git a/pkg/beacon/fetch.go b/pkg/beacon/fetch.go index 5e9ea54..0963162 100644 --- a/pkg/beacon/fetch.go +++ b/pkg/beacon/fetch.go @@ -15,13 +15,20 @@ import ( ) func (n *node) FetchSyncStatus(ctx context.Context) (*v1.SyncState, error) { + logCtx := n.log.WithField("method", "FetchSyncStatus") provider, isProvider := n.client.(eth2client.NodeSyncingProvider) if !isProvider { + logCtx.Error("client does not implement eth2client.NodeSyncingProvider") + return nil, errors.New("client does not implement eth2client.NodeSyncingProvider") } + logCtx.Debug("Fetching sync status") + status, err := provider.NodeSyncing(ctx, &api.NodeSyncingOpts{}) if err != nil { + logCtx.WithError(err).Error("failed to fetch sync status") + return nil, err } @@ -29,15 +36,25 @@ func (n *node) FetchSyncStatus(ctx context.Context) (*v1.SyncState, error) { n.publishSyncStatus(ctx, status.Data) + logCtx.WithField("status", status.Data).Debug("Successfully fetched sync status") + return status.Data, nil } func (n *node) FetchPeers(ctx context.Context) (*types.Peers, error) { + logCtx := n.log.WithField("method", "FetchPeers") + + logCtx.Debug("Fetching peers") + peers, err := n.api.NodePeers(ctx) if err != nil { + logCtx.WithError(err).Error("failed to fetch peers") + return nil, err } + logCtx.WithField("peers", len(peers)).Debug("Successfully fetched peers") + n.peers = peers n.publishPeersUpdated(ctx, peers) @@ -46,13 +63,22 @@ func (n *node) FetchPeers(ctx context.Context) (*types.Peers, error) { } func (n *node) FetchNodeVersion(ctx context.Context) (string, error) { + logCtx := n.log.WithField("method", "FetchNodeVersion") + + logCtx.Debug("Fetching node version") + provider, isProvider := n.client.(eth2client.NodeVersionProvider) if !isProvider { - return "", errors.New("client does not implement eth2client.NodeVersionProvider") + err := errors.New("client does not implement eth2client.NodeVersionProvider") + logCtx.WithError(err).Error("client does not implement eth2client.NodeVersionProvider") + + return "", err } rsp, err := provider.NodeVersion(ctx, &api.NodeVersionOpts{}) if err != nil { + logCtx.WithError(err).Error("failed to fetch node version") + return "", err } @@ -60,51 +86,127 @@ func (n *node) FetchNodeVersion(ctx context.Context) (string, error) { n.publishNodeVersionUpdated(ctx, rsp.Data) + logCtx.WithField("version", rsp.Data).Debug("Successfully fetched node version") + return rsp.Data, nil } func (n *node) FetchBlock(ctx context.Context, stateID string) (*spec.VersionedSignedBeaconBlock, error) { - return n.getBlock(ctx, stateID) + logCtx := n.log.WithField("method", "FetchBlock").WithField("state_id", stateID) + + logCtx.Debug("Fetching block") + + block, err := n.getBlock(ctx, stateID) + if err != nil { + logCtx.WithError(err).Error("failed to fetch block") + + return nil, err + } + + logCtx.Debug("Successfully fetched block") + + return block, nil } func (n *node) FetchRawBlock(ctx context.Context, stateID string, contentType string) ([]byte, error) { - return n.api.RawBlock(ctx, stateID, contentType) + logCtx := n.log.WithField("method", "FetchRawBlock").WithField("state_id", stateID) + + logCtx.Debug("Fetching raw block") + + block, err := n.api.RawBlock(ctx, stateID, contentType) + if err != nil { + logCtx.WithError(err).Error("failed to fetch raw block") + + return nil, err + } + + logCtx.Debug("Successfully fetched raw block") + + return block, nil } func (n *node) FetchBlockRoot(ctx context.Context, stateID string) (*phase0.Root, error) { - return n.getBlockRoot(ctx, stateID) + logCtx := n.log.WithField("method", "FetchBlockRoot").WithField("state_id", stateID) + + logCtx.Debug("Fetching block root") + + root, err := n.getBlockRoot(ctx, stateID) + if err != nil { + logCtx.WithError(err).Error("failed to fetch block root") + + return nil, err + } + + logCtx.Debug("Successfully fetched block root") + + return root, nil } func (n *node) FetchBeaconState(ctx context.Context, stateID string) (*spec.VersionedBeaconState, error) { + logCtx := n.log.WithField("method", "FetchBeaconState").WithField("state_id", stateID) + provider, isProvider := n.client.(eth2client.BeaconStateProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.NodeVersionProvider") + err := errors.New("client does not implement eth2client.NodeVersionProvider") + + logCtx.Error(err.Error()) + + return nil, err } + logCtx.Debug("Fetching beacon state") + rsp, err := provider.BeaconState(ctx, &api.BeaconStateOpts{ State: stateID, }) if err != nil { + logCtx.WithError(err).Error("failed to fetch beacon state") + return nil, err } + logCtx.Debug("Successfully fetched beacon state") + return rsp.Data, nil } func (n *node) FetchRawBeaconState(ctx context.Context, stateID string, contentType string) ([]byte, error) { - return n.api.RawDebugBeaconState(ctx, stateID, contentType) + logCtx := n.log.WithField("method", "FetchRawBeaconState").WithField("state_id", stateID) + + logCtx.Debug("Fetching raw beacon state") + + block, err := n.api.RawDebugBeaconState(ctx, stateID, contentType) + if err != nil { + logCtx.WithError(err).Error("failed to fetch raw beacon state") + + return nil, err + } + + logCtx.Debug("Successfully fetched raw beacon state") + + return block, nil } func (n *node) FetchFinality(ctx context.Context, stateID string) (*v1.Finality, error) { + logCtx := n.log.WithField("method", "FetchFinality").WithField("state_id", stateID) + provider, isProvider := n.client.(eth2client.FinalityProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.FinalityProvider") + err := errors.New("client does not implement eth2client.FinalityProvider") + + logCtx.Error(err.Error()) + + return nil, err } + logCtx.Debug("Fetching finality") + rsp, err := provider.Finality(ctx, &api.FinalityOpts{ State: stateID, }) if err != nil { + logCtx.WithError(err).Error("failed to fetch finality") + return nil, err } @@ -129,31 +231,55 @@ func (n *node) FetchFinality(ctx context.Context, stateID string) (*v1.Finality, } } + logCtx.Debug("Successfully fetched finality") + return finality, nil } func (n *node) FetchRawSpec(ctx context.Context) (map[string]any, error) { + logCtx := n.log.WithField("method", "FetchRawSpec") + + logCtx.Debug("Fetching raw spec") + provider, isProvider := n.client.(eth2client.SpecProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.SpecProvider") + err := errors.New("client does not implement eth2client.SpecProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.Spec(ctx, &api.SpecOpts{}) if err != nil { + logCtx.WithError(err).Error("failed to fetch raw spec") + return nil, err } + logCtx.Debug("Successfully fetched raw spec") + return rsp.Data, nil } func (n *node) FetchSpec(ctx context.Context) (*state.Spec, error) { + logCtx := n.log.WithField("method", "FetchSpec") + + logCtx.Debug("Fetching spec") + provider, isProvider := n.client.(eth2client.SpecProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.SpecProvider") + err := errors.New("client does not implement eth2client.SpecProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.Spec(ctx, &api.SpecOpts{}) if err != nil { + logCtx.WithError(err).Error("failed to fetch spec") + return nil, err } @@ -163,85 +289,167 @@ func (n *node) FetchSpec(ctx context.Context) (*state.Spec, error) { n.publishSpecUpdated(ctx, &sp) + logCtx.Debug("Successfully fetched spec") + return &sp, nil } func (n *node) FetchBeaconBlockBlobs(ctx context.Context, blockID string) ([]*deneb.BlobSidecar, error) { + logCtx := n.log.WithField("method", "FetchBeaconBlockBlobs").WithField("block_id", blockID) + + logCtx.Debug("Fetching beacon blobs") + provider, isProvider := n.client.(eth2client.BlobSidecarsProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.BlobSidecarsProvider") + err := errors.New("client does not implement eth2client.BlobSidecarsProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.BlobSidecars(ctx, &api.BlobSidecarsOpts{ Block: blockID, }) if err != nil { + logCtx.WithError(err).Error("failed to fetch beacon blobs") + return nil, err } + logCtx.WithField("blob_count", len(rsp.Data)).Debug("Successfully fetched beacon blobs") + return rsp.Data, nil } func (n *node) FetchProposerDuties(ctx context.Context, epoch phase0.Epoch) ([]*v1.ProposerDuty, error) { - n.log.WithField("epoch", epoch).Debug("Fetching proposer duties") + logCtx := n.log.WithField("method", "FetchProposerDuties").WithField("epoch", epoch) + + logCtx.Debug("Fetching proposer duties") provider, isProvider := n.client.(eth2client.ProposerDutiesProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.ProposerDutiesProvider") + err := errors.New("client does not implement eth2client.ProposerDutiesProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.ProposerDuties(ctx, &api.ProposerDutiesOpts{ Epoch: epoch, }) if err != nil { + logCtx.WithError(err).Error("failed to fetch proposer duties") + return nil, err } + logCtx.Debug("Successfully fetched proposer duties") + return rsp.Data, nil } func (n *node) FetchForkChoice(ctx context.Context) (*v1.ForkChoice, error) { + logCtx := n.log.WithField("method", "FetchForkChoice") + + logCtx.Debug("Fetching fork choice") + provider, isProvider := n.client.(eth2client.ForkChoiceProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.ForkChoiceProvider") + err := errors.New("client does not implement eth2client.ForkChoiceProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.ForkChoice(ctx, &api.ForkChoiceOpts{}) if err != nil { + logCtx.WithError(err).Error("failed to fetch fork choice") + return nil, err } + logCtx.Debug("Successfully fetched fork choice") + return rsp.Data, nil } func (n *node) FetchDepositSnapshot(ctx context.Context) (*types.DepositSnapshot, error) { - return n.api.DepositSnapshot(ctx) + logCtx := n.log.WithField("method", "FetchDepositSnapshot") + + logCtx.Debug("Fetching deposit snapshot") + + snapshot, err := n.api.DepositSnapshot(ctx) + if err != nil { + logCtx.WithError(err).Error("failed to fetch deposit snapshot") + + return nil, err + } + + logCtx.Debug("Successfully fetched deposit snapshot") + + return snapshot, nil } func (n *node) FetchNodeIdentity(ctx context.Context) (*types.Identity, error) { - return n.api.NodeIdentity(ctx) + logCtx := n.log.WithField("method", "FetchNodeIdentity") + + logCtx.Debug("Fetching node identity") + + identity, err := n.api.NodeIdentity(ctx) + if err != nil { + logCtx.WithError(err).Error("failed to fetch node identity") + + return nil, err + } + + logCtx.WithField("identity", identity).Debug("Successfully fetched node identity") + + return identity, nil } func (n *node) FetchBeaconStateRoot(ctx context.Context, state string) (phase0.Root, error) { + logCtx := n.log.WithField("method", "FetchBeaconStateRoot").WithField("state", state) + + logCtx.Debug("Fetching beacon state root") + provider, isProvider := n.client.(eth2client.BeaconStateRootProvider) if !isProvider { - return phase0.Root{}, errors.New("client does not implement eth2client.StateRootProvider") + err := errors.New("client does not implement eth2client.StateRootProvider") + + logCtx.Error(err.Error()) + + return phase0.Root{}, err } rsp, err := provider.BeaconStateRoot(ctx, &api.BeaconStateRootOpts{ State: state, }) if err != nil { + logCtx.WithError(err).Error("failed to fetch beacon state root") + return phase0.Root{}, err } + logCtx.Debug("Successfully fetched beacon state root") + return *rsp.Data, nil } func (n *node) FetchBeaconCommittees(ctx context.Context, state string, epoch *phase0.Epoch) ([]*v1.BeaconCommittee, error) { + logCtx := n.log.WithField("method", "FetchBeaconCommittees").WithField("state", state) + + logCtx.Debug("Fetching beacon committees") + provider, isProvider := n.client.(eth2client.BeaconCommitteesProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.BeaconCommitteesProvider") + err := errors.New("client does not implement eth2client.BeaconCommitteesProvider") + + logCtx.Error(err.Error()) + + return nil, err } opts := &api.BeaconCommitteesOpts{ @@ -254,16 +462,28 @@ func (n *node) FetchBeaconCommittees(ctx context.Context, state string, epoch *p rsp, err := provider.BeaconCommittees(ctx, opts) if err != nil { + logCtx.WithError(err).Error("failed to fetch beacon committees") + return nil, err } + logCtx.WithField("committee_count", len(rsp.Data)).Debug("Successfully fetched beacon committees") + return rsp.Data, nil } func (n *node) FetchAttestationData(ctx context.Context, slot phase0.Slot, committeeIndex phase0.CommitteeIndex) (*phase0.AttestationData, error) { + logCtx := n.log.WithField("method", "FetchAttestationData").WithField("slot", slot).WithField("committee_index", committeeIndex) + + logCtx.Debug("Fetching attestation data") + provider, isProvider := n.client.(eth2client.AttestationDataProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.AttestationDataProvider") + err := errors.New("client does not implement eth2client.AttestationDataProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.AttestationData(ctx, &api.AttestationDataOpts{ @@ -271,22 +491,38 @@ func (n *node) FetchAttestationData(ctx context.Context, slot phase0.Slot, commi CommitteeIndex: committeeIndex, }) if err != nil { + logCtx.WithError(err).Error("failed to fetch attestation data") + return nil, err } + logCtx.Debug("Successfully fetched attestation data") + return rsp.Data, nil } func (n *node) FetchBeaconBlockHeader(ctx context.Context, opts *api.BeaconBlockHeaderOpts) (*v1.BeaconBlockHeader, error) { + logCtx := n.log.WithField("method", "FetchBeaconBlockHeader") + + logCtx.Debug("Fetching beacon block header") + provider, isProvider := n.client.(eth2client.BeaconBlockHeadersProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.BeaconBlockHeadersProvider") + err := errors.New("client does not implement eth2client.BeaconBlockHeadersProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.BeaconBlockHeader(ctx, opts) if err != nil { + logCtx.WithError(err).Error("failed to fetch beacon block header") + return nil, err } + logCtx.Debug("Successfully fetched beacon block header") + return rsp.Data, nil }