From 490d07a8e0c258f4528d3039109696679d79787d Mon Sep 17 00:00:00 2001 From: Jim McDonald Date: Mon, 12 Aug 2024 17:45:11 +0100 Subject: [PATCH] Add events. --- CHANGELOG.md | 3 + api/v1/blockgossipevent.go | 86 +++++++++++++++++++++++ api/v1/blockgossipevent_test.go | 101 ++++++++++++++++++++++++++ api/v1/event.go | 43 ++++++++---- http/events.go | 121 +++++++++++++++++--------------- 5 files changed, 283 insertions(+), 71 deletions(-) create mode 100644 api/v1/blockgossipevent.go create mode 100644 api/v1/blockgossipevent_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 51bb6fde3..d41170828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +dev: + - add attester_slashing, block_gossip, bls_to_execution_change and proposer_slashing events + 0.21.10: - better validator state when balance not supplied diff --git a/api/v1/blockgossipevent.go b/api/v1/blockgossipevent.go new file mode 100644 index 000000000..1b38ae7af --- /dev/null +++ b/api/v1/blockgossipevent.go @@ -0,0 +1,86 @@ +// Copyright © 2024 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// BlockGossipEvent is the data for the block gossip event. +type BlockGossipEvent struct { + Slot phase0.Slot + Block phase0.Root +} + +// blockGossipEventJSON is the spec representation of the struct. +type blockGossipEventJSON struct { + Slot string `json:"slot"` + Block string `json:"block"` +} + +// MarshalJSON implements json.Marshaler. +func (e *BlockGossipEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(&blockGossipEventJSON{ + Slot: fmt.Sprintf("%d", e.Slot), + Block: fmt.Sprintf("%#x", e.Block), + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (e *BlockGossipEvent) UnmarshalJSON(input []byte) error { + var err error + + var data blockGossipEventJSON + if err = json.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "invalid JSON") + } + if data.Slot == "" { + return errors.New("slot missing") + } + slot, err := strconv.ParseUint(data.Slot, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for slot") + } + e.Slot = phase0.Slot(slot) + if data.Block == "" { + return errors.New("block missing") + } + block, err := hex.DecodeString(strings.TrimPrefix(data.Block, "0x")) + if err != nil { + return errors.Wrap(err, "invalid value for block") + } + if len(block) != rootLength { + return fmt.Errorf("incorrect length %d for block", len(block)) + } + copy(e.Block[:], block) + + return nil +} + +// String returns a string version of the structure. +func (e *BlockGossipEvent) String() string { + data, err := json.Marshal(e) + if err != nil { + return fmt.Sprintf("ERR: %v", err) + } + + return string(data) +} diff --git a/api/v1/blockgossipevent_test.go b/api/v1/blockgossipevent_test.go new file mode 100644 index 000000000..b73fae73a --- /dev/null +++ b/api/v1/blockgossipevent_test.go @@ -0,0 +1,101 @@ +// Copyright © 2024 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_test + +import ( + "encoding/json" + "testing" + + api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/stretchr/testify/assert" + require "github.com/stretchr/testify/require" +) + +func TestBlockGossipEventJSON(t *testing.T) { + tests := []struct { + name string + input []byte + err string + }{ + { + name: "Empty", + err: "unexpected end of JSON input", + }, + { + name: "JSONBad", + input: []byte("[]"), + err: "invalid JSON: json: cannot unmarshal array into Go value of type v1.blockGossipEventJSON", + }, + { + name: "SlotMissing", + input: []byte(`{"block":"0x99e3f24aab3dd084045a0c927a33b8463eb5c7b17eeadfecdcf4e4badf7b6028"}`), + err: "slot missing", + }, + { + name: "SlotWrongType", + input: []byte(`{"slot":true,"block":"0x99e3f24aab3dd084045a0c927a33b8463eb5c7b17eeadfecdcf4e4badf7b6028"}`), + err: "invalid JSON: json: cannot unmarshal bool into Go struct field blockGossipEventJSON.slot of type string", + }, + { + name: "SlotInvalid", + input: []byte(`{"slot":"-1","block":"0x99e3f24aab3dd084045a0c927a33b8463eb5c7b17eeadfecdcf4e4badf7b6028"}`), + err: "invalid value for slot: strconv.ParseUint: parsing \"-1\": invalid syntax", + }, + { + name: "BlockMissing", + input: []byte(`{"slot":"525277"}`), + err: "block missing", + }, + { + name: "BlockWrongType", + input: []byte(`{"slot":"525277","block":true}`), + err: "invalid JSON: json: cannot unmarshal bool into Go struct field blockGossipEventJSON.block of type string", + }, + { + name: "BlockInvalid", + input: []byte(`{"slot":"525277","block":"invalid"}`), + err: "invalid value for block: encoding/hex: invalid byte: U+0069 'i'", + }, + { + name: "BlockShort", + input: []byte(`{"slot":"525277","block":"0xe3f24aab3dd084045a0c927a33b8463eb5c7b17eeadfecdcf4e4badf7b6028"}`), + err: "incorrect length 31 for block", + }, + { + name: "BlockLong", + input: []byte(`{"slot":"525277","block":"0x9999e3f24aab3dd084045a0c927a33b8463eb5c7b17eeadfecdcf4e4badf7b6028"}`), + err: "incorrect length 33 for block", + }, + { + name: "Good", + input: []byte(`{"slot":"525277","block":"0x99e3f24aab3dd084045a0c927a33b8463eb5c7b17eeadfecdcf4e4badf7b6028"}`), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var res api.BlockGossipEvent + err := json.Unmarshal(test.input, &res) + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + rt, err := json.Marshal(&res) + require.NoError(t, err) + assert.Equal(t, string(test.input), string(rt)) + assert.Equal(t, string(rt), res.String()) + } + }) + } +} diff --git a/api/v1/event.go b/api/v1/event.go index 8a991d399..977cdb493 100644 --- a/api/v1/event.go +++ b/api/v1/event.go @@ -18,6 +18,7 @@ import ( "fmt" "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" ) @@ -32,15 +33,19 @@ type Event struct { // SupportedEventTopics is a map of supported event topics. var SupportedEventTopics = map[string]bool{ - "attestation": true, - "block": true, - "chain_reorg": true, - "finalized_checkpoint": true, - "head": true, - "voluntary_exit": true, - "contribution_and_proof": true, - "payload_attributes": true, - "blob_sidecar": true, + "attestation": true, + "attester_slashing": true, + "blob_sidecar": true, + "block": true, + "block_gossip": true, + "bls_to_execution_change": true, + "chain_reorg": true, + "contribution_and_proof": true, + "finalized_checkpoint": true, + "head": true, + "payload_attributes": true, + "proposer_slashing": true, + "voluntary_exit": true, } // eventJSON is the spec representation of the struct. @@ -86,22 +91,30 @@ func (e *Event) UnmarshalJSON(input []byte) error { switch eventJSON.Topic { case "attestation": e.Data = &phase0.Attestation{} + case "attester_slashing": + e.Data = &phase0.AttesterSlashing{} + case "blob_sidecar": + e.Data = &BlobSidecarEvent{} case "block": e.Data = &BlockEvent{} + case "block_gossip": + e.Data = &BlockGossipEvent{} + case "bls_to_execution_change": + e.Data = &capella.SignedBLSToExecutionChange{} case "chain_reorg": e.Data = &ChainReorgEvent{} + case "contribution_and_proof": + e.Data = &altair.SignedContributionAndProof{} case "finalized_checkpoint": e.Data = &FinalizedCheckpointEvent{} case "head": e.Data = &HeadEvent{} - case "voluntary_exit": - e.Data = &phase0.SignedVoluntaryExit{} - case "contribution_and_proof": - e.Data = &altair.SignedContributionAndProof{} case "payload_attributes": e.Data = &PayloadAttributesEvent{} - case "blob_sidecar": - e.Data = &BlobSidecarEvent{} + case "proposer_slashing": + e.Data = &phase0.ProposerSlashing{} + case "voluntary_exit": + e.Data = &phase0.SignedVoluntaryExit{} default: return fmt.Errorf("unsupported event topic %s", eventJSON.Topic) } diff --git a/http/events.go b/http/events.go index 589859f50..cb3f189df 100644 --- a/http/events.go +++ b/http/events.go @@ -117,114 +117,123 @@ func (*Service) handleEvent(ctx context.Context, msg *sse.Event, handler consens Topic: string(msg.Event), } switch string(msg.Event) { - case "head": - headEvent := &api.HeadEvent{} - err := json.Unmarshal(msg.Data, headEvent) + case "attestation": + data := &phase0.Attestation{} + err := json.Unmarshal(msg.Data, data) if err != nil { - log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse head event") + log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse attestation") return } - event.Data = headEvent - case "block": - blockEvent := &api.BlockEvent{} - err := json.Unmarshal(msg.Data, blockEvent) + event.Data = data + case "attester_slashing": + data := &phase0.AttesterSlashing{} + err := json.Unmarshal(msg.Data, data) if err != nil { - log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse block event") + log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse attester slashing event") return } - event.Data = blockEvent - case "attestation": - attestation := &phase0.Attestation{} - err := json.Unmarshal(msg.Data, attestation) + event.Data = data + case "blob_sidecar": + data := &api.BlobSidecarEvent{} + err := json.Unmarshal(msg.Data, data) if err != nil { - log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse attestation") + log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse blob sidecar event") return } - event.Data = attestation - case "voluntary_exit": - voluntaryExit := &phase0.SignedVoluntaryExit{} - err := json.Unmarshal(msg.Data, voluntaryExit) + event.Data = data + case "block": + data := &api.BlockEvent{} + err := json.Unmarshal(msg.Data, data) if err != nil { - log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse voluntary exit") + log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse block event") return } - event.Data = voluntaryExit - case "finalized_checkpoint": - finalizedCheckpointEvent := &api.FinalizedCheckpointEvent{} - err := json.Unmarshal(msg.Data, finalizedCheckpointEvent) + event.Data = data + case "block_gossip": + data := &api.BlockGossipEvent{} + err := json.Unmarshal(msg.Data, data) if err != nil { - log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse finalized checkpoint event") + log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse block gossip event") + + return + } + event.Data = data + case "bls_to_execution_change": + data := &capella.SignedBLSToExecutionChange{} + err := json.Unmarshal(msg.Data, data) + if err != nil { + log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse bls to execution change event") return } - event.Data = finalizedCheckpointEvent + event.Data = data case "chain_reorg": - chainReorgEvent := &api.ChainReorgEvent{} - err := json.Unmarshal(msg.Data, chainReorgEvent) + data := &api.ChainReorgEvent{} + err := json.Unmarshal(msg.Data, data) if err != nil { log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse chain reorg event") return } - event.Data = chainReorgEvent + event.Data = data case "contribution_and_proof": - contributionAndProofEvent := &altair.SignedContributionAndProof{} - err := json.Unmarshal(msg.Data, contributionAndProofEvent) + data := &altair.SignedContributionAndProof{} + err := json.Unmarshal(msg.Data, data) if err != nil { log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse contribution and proof event") return } - event.Data = contributionAndProofEvent - case "payload_attributes": - payloadAttributesEvent := &api.PayloadAttributesEvent{} - err := json.Unmarshal(msg.Data, payloadAttributesEvent) + event.Data = data + case "finalized_checkpoint": + data := &api.FinalizedCheckpointEvent{} + err := json.Unmarshal(msg.Data, data) if err != nil { - log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse payload attributes event") + log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse finalized checkpoint event") return } - event.Data = payloadAttributesEvent - case "proposer_slashing": - proposerSlashingEvent := &phase0.ProposerSlashing{} - err := json.Unmarshal(msg.Data, proposerSlashingEvent) + event.Data = data + case "head": + data := &api.HeadEvent{} + err := json.Unmarshal(msg.Data, data) if err != nil { - log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse proposer slashing event") + log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse head event") return } - event.Data = proposerSlashingEvent - case "attester_slashing": - attesterSlashingEvent := &phase0.AttesterSlashing{} - err := json.Unmarshal(msg.Data, attesterSlashingEvent) + event.Data = data + case "payload_attributes": + data := &api.PayloadAttributesEvent{} + err := json.Unmarshal(msg.Data, data) if err != nil { - log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse attester slashing event") + log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse payload attributes event") return } - event.Data = attesterSlashingEvent - case "bls_to_execution_change": - blsToExecutionChangeEvent := &capella.BLSToExecutionChange{} - err := json.Unmarshal(msg.Data, blsToExecutionChangeEvent) + event.Data = data + case "proposer_slashing": + data := &phase0.ProposerSlashing{} + err := json.Unmarshal(msg.Data, data) if err != nil { - log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse bls to execution change event") + log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse proposer slashing event") return } - event.Data = blsToExecutionChangeEvent - case "blob_sidecar": - blobSidecar := &api.BlobSidecarEvent{} - err := json.Unmarshal(msg.Data, blobSidecar) + event.Data = data + case "voluntary_exit": + data := &phase0.SignedVoluntaryExit{} + err := json.Unmarshal(msg.Data, data) if err != nil { - log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse blob sidecar event") + log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse voluntary exit") return } - event.Data = blobSidecar + event.Data = data case "": // Used as keepalive. Ignore. return