diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d63f33..11ba2eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +dev: + - support single_attestation event + - support change to attestation event; this event now emits a spec.VersionedAttestation + 0.24.0: - support electra - the most notable change is that a number of functions now use spec.VersionedAttestation in place of phase0.Attestation diff --git a/api/v1/event.go b/api/v1/event.go index 977cdb49..15a43a6e 100644 --- a/api/v1/event.go +++ b/api/v1/event.go @@ -1,4 +1,4 @@ -// Copyright © 2020, 2021 Attestant Limited. +// Copyright © 2020 - 2025 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 @@ -17,8 +17,10 @@ import ( "encoding/json" "fmt" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/electra" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" ) @@ -45,6 +47,7 @@ var SupportedEventTopics = map[string]bool{ "head": true, "payload_attributes": true, "proposer_slashing": true, + "single_attestation": true, "voluntary_exit": true, } @@ -90,7 +93,7 @@ func (e *Event) UnmarshalJSON(input []byte) error { } switch eventJSON.Topic { case "attestation": - e.Data = &phase0.Attestation{} + e.Data = &spec.VersionedAttestation{} case "attester_slashing": e.Data = &phase0.AttesterSlashing{} case "blob_sidecar": @@ -113,6 +116,8 @@ func (e *Event) UnmarshalJSON(input []byte) error { e.Data = &PayloadAttributesEvent{} case "proposer_slashing": e.Data = &phase0.ProposerSlashing{} + case "single_attestation": + e.Data = &electra.SingleAttestation{} case "voluntary_exit": e.Data = &phase0.SignedVoluntaryExit{} default: diff --git a/api/v1/event_test.go b/api/v1/event_test.go index 4035cedc..c225e1e4 100644 --- a/api/v1/event_test.go +++ b/api/v1/event_test.go @@ -1,4 +1,4 @@ -// Copyright © 2020 Attestant Limited. +// Copyright © 2020, 2025 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 @@ -18,8 +18,8 @@ import ( "testing" api "github.com/attestantio/go-eth2-client/api/v1" - require "github.com/stretchr/testify/require" "github.com/stretchr/testify/assert" + require "github.com/stretchr/testify/require" ) func TestEvent(t *testing.T) { @@ -58,9 +58,17 @@ func TestEvent(t *testing.T) { err: "data missing", }, { - name: "GoodAttestation", + name: "GoodPhase0Attestation", input: []byte(`{"topic":"attestation","data":{"aggregation_bits":"0x010203","data":{"beacon_block_root":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f","index":"1","slot":"100","source":{"epoch":"1","root":"0x202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"},"target":{"epoch":"2","root":"0x404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"}},"signature":"0x606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf"}}`), }, + { + name: "GoodElectraAttestation", + input: []byte(`{"topic":"attestation","data":{"aggregation_bits":"0xf77ffffffdfbfffffffdbfffffe5fff71f","data":{"slot":"98106","index":"0","beacon_block_root":"0xf8df02ed08b9adcb88a22cb22cd2a6074b184128ae6a240e3172109fdfacaa7b","source":{"epoch":"3064","root":"0x19ffd95e92753046cf63b4298f859e1fb1271a160ef0139ea1eb9f06d45d3b93"},"target":{"epoch":"3065","root":"0xffac9506e2262991ed19b1804bec9f7a1c4c4e61eb37444e6c8826bb362716d6"}},"signature":"0xb4f12c02e0f1a5db07999ceb8c1a4ccd41a3cb46ca15abe1c145337f1287360c49d5780fb7b44dfebeb96f3898824605008c9d458bdd2413358da3edf1b181d4e98edfe90d5fd016ac8f6aebc6646b2da83ab98722a7b4ee5264506bf6ae08e9","committee_bits":"0x0040000000000000"}}`), + }, + { + name: "GoodSingleAttestation", + input: []byte(`{"topic":"single_attestation","data":{"committee_index":"11","attester_index":"23784","data":{"slot":"98122","index":"0","beacon_block_root":"0x497033a5af8e64b748c554524e1e269da3c3af71515cf31f2d7bf9bab256a03c","source":{"epoch":"3065","root":"0xffac9506e2262991ed19b1804bec9f7a1c4c4e61eb37444e6c8826bb362716d6"},"target":{"epoch":"3066","root":"0x5982836668f92d786ef82f8841011a0888be5583bd09ab73760563855789d49b"}},"signature":"0xa448d3dc9520d4cb8e70094108169893a94ef7d074151ba333169ea92e2586da6f6efa622722743725c8012707f78efa02d99dd8ee094fa5bf5ca2b24066096ab0bc7671d4037521cbe69411871dd614e60d2c0eed8d2a0b2e4b77602b39d50e"}}`), + }, { name: "GoodBlock", input: []byte(`{"topic":"block","data":{"block":"0xbe36e714a6114cf718e35dafc4ac530ce8f01e4a9a360e78098eb129772dcc39","slot":"1"}}`), @@ -97,8 +105,8 @@ func TestEvent(t *testing.T) { 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()) + assert.JSONEq(t, string(test.input), string(rt)) + assert.JSONEq(t, string(rt), res.String()) } }) } diff --git a/http/events.go b/http/events.go index b0418c97..f4fd6621 100644 --- a/http/events.go +++ b/http/events.go @@ -26,8 +26,10 @@ import ( consensusclient "github.com/attestantio/go-eth2-client" api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/electra" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/r3labs/sse/v2" "github.com/rs/zerolog" @@ -115,7 +117,7 @@ func (*Service) handleEvent(ctx context.Context, msg *sse.Event, handler consens } switch string(msg.Event) { case "attestation": - data := &phase0.Attestation{} + data := &spec.VersionedAttestation{} err := json.Unmarshal(msg.Data, data) if err != nil { log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse attestation") @@ -222,6 +224,15 @@ func (*Service) handleEvent(ctx context.Context, msg *sse.Event, handler consens return } event.Data = data + case "single_attestation": + data := &electra.SingleAttestation{} + err := json.Unmarshal(msg.Data, data) + if err != nil { + log.Error().Err(err).RawJSON("data", msg.Data).Msg("Failed to parse single attestation event") + + return + } + event.Data = data case "voluntary_exit": data := &phase0.SignedVoluntaryExit{} err := json.Unmarshal(msg.Data, data) diff --git a/spec/versionedattestation_json.go b/spec/versionedattestation_json.go new file mode 100644 index 00000000..c60c3230 --- /dev/null +++ b/spec/versionedattestation_json.go @@ -0,0 +1,48 @@ +// Copyright © 2025 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 spec + +import ( + "encoding/json" + + "github.com/attestantio/go-eth2-client/spec/electra" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// attestationIdentificationJSON contains fields that allow us to identify the attestation variant. +type attestationIdentificationJSON struct { + CommitteeBits *string `json:"committee_bits"` +} + +// UnmarshalJSON implements json.Unmarshaler. +func (v *VersionedAttestation) UnmarshalJSON(input []byte) error { + var id attestationIdentificationJSON + if err := json.Unmarshal(input, &id); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + switch { + case id.CommitteeBits != nil: + v.Version = DataVersionElectra + v.Electra = &electra.Attestation{} + + return v.Electra.UnmarshalJSON(input) + default: + v.Version = DataVersionPhase0 + v.Phase0 = &phase0.Attestation{} + + return v.Phase0.UnmarshalJSON(input) + } +}