From 6e0955441c7755a592c2e7297b0f2fdf5706f823 Mon Sep 17 00:00:00 2001 From: Andrew Davis <1709934+Savid@users.noreply.github.com> Date: Wed, 4 Oct 2023 12:58:58 +1000 Subject: [PATCH] add blob_sidecar event --- api/v1/blobsidecarevent.go | 96 ++++++++++++++++++++ api/v1/blobsidecarevent_test.go | 153 ++++++++++++++++++++++++++++++++ api/v1/event.go | 3 + 3 files changed, 252 insertions(+) create mode 100644 api/v1/blobsidecarevent.go create mode 100644 api/v1/blobsidecarevent_test.go diff --git a/api/v1/blobsidecarevent.go b/api/v1/blobsidecarevent.go new file mode 100644 index 00000000..ef566a7d --- /dev/null +++ b/api/v1/blobsidecarevent.go @@ -0,0 +1,96 @@ +package v1 + +import ( + "encoding/json" + "fmt" + + "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// BlobSidecarEvent is the data for the blob sidecar event. +type BlobSidecarEvent struct { + BlockRoot phase0.Root + Slot phase0.Slot + Index deneb.BlobIndex + KzgCommitment deneb.KzgCommitment + VersionedHash deneb.VersionedHash +} + +// blobSidecarEventJSON is the spec representation of the struct. +type blobSidecarEventJSON struct { + BlockRoot string `json:"block_root"` + Slot string `json:"slot"` + Index string `json:"index"` + KzgCommitment string `json:"kzg_commitment"` + VersionedHash string `json:"versioned_hash"` +} + +// MarshalJSON implements json.Marshaler. +func (e *BlobSidecarEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(&blobSidecarEventJSON{ + BlockRoot: fmt.Sprintf("%#x", e.BlockRoot), + Slot: fmt.Sprintf("%d", e.Slot), + Index: fmt.Sprintf("%d", e.Index), + KzgCommitment: fmt.Sprintf("%#x", e.KzgCommitment), + VersionedHash: fmt.Sprintf("%#x", e.VersionedHash), + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (e *BlobSidecarEvent) UnmarshalJSON(input []byte) error { + var err error + + var blobSidecarEventJSON blobSidecarEventJSON + if err = json.Unmarshal(input, &blobSidecarEventJSON); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if blobSidecarEventJSON.BlockRoot == "" { + return errors.New("block_root missing") + } + err = e.BlockRoot.UnmarshalJSON([]byte(fmt.Sprintf(`"%s"`, blobSidecarEventJSON.BlockRoot))) + if err != nil { + return errors.Wrap(err, "invalid value for block_root") + } + if blobSidecarEventJSON.Slot == "" { + return errors.New("slot missing") + } + err = e.Slot.UnmarshalJSON([]byte(fmt.Sprintf(`"%s"`, blobSidecarEventJSON.Slot))) + if err != nil { + return errors.Wrap(err, "invalid value for slot") + } + if blobSidecarEventJSON.Index == "" { + return errors.New("index missing") + } + err = e.Index.UnmarshalJSON([]byte(fmt.Sprintf(`"%s"`, blobSidecarEventJSON.Index))) + if err != nil { + return errors.Wrap(err, "invalid value for index") + } + if blobSidecarEventJSON.KzgCommitment == "" { + return errors.New("kzg_commitment missing") + } + err = e.KzgCommitment.UnmarshalJSON([]byte(fmt.Sprintf(`"%s"`, blobSidecarEventJSON.KzgCommitment))) + if err != nil { + return errors.Wrap(err, "invalid value for kzg_commitment") + } + if blobSidecarEventJSON.VersionedHash == "" { + return errors.New("versioned_hash missing") + } + err = e.VersionedHash.UnmarshalJSON([]byte(fmt.Sprintf(`"%s"`, blobSidecarEventJSON.VersionedHash))) + if err != nil { + return errors.Wrap(err, "invalid value for versioned_hash") + } + + return nil +} + +// String returns a string version of the structure. +func (e *BlobSidecarEvent) String() string { + data, err := json.Marshal(e) + if err != nil { + return fmt.Sprintf("ERR: %v", err) + } + return string(data) +} diff --git a/api/v1/blobsidecarevent_test.go b/api/v1/blobsidecarevent_test.go new file mode 100644 index 00000000..dc412ea1 --- /dev/null +++ b/api/v1/blobsidecarevent_test.go @@ -0,0 +1,153 @@ +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 TestBlobSidecarEventJSON(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.blobSidecarEventJSON", + }, + { + name: "BlockRootMissing", + input: []byte(`{"slot":"1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "block_root missing", + }, + { + name: "BlockRootWrongType", + input: []byte(`{"block_root": true, "slot":"1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid JSON: json: cannot unmarshal bool into Go struct field blobSidecarEventJSON.block_root of type string", + }, + { + name: "BlockRootInvalid", + input: []byte(`{"block_root":"0xinvalide9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid value for block_root: invalid value invalide9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2: encoding/hex: invalid byte: U+0069 'i'", + }, + { + name: "BlockRootShort", + input: []byte(`{"block_root":"0x","slot":"1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid value for block_root: incorrect length", + }, + { + name: "BlockRootLong", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2a","slot":"1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid value for block_root: incorrect length", + }, + { + name: "SlotMissing", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "slot missing", + }, + { + name: "SlotWrongType", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":true,"index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid JSON: json: cannot unmarshal bool into Go struct field blobSidecarEventJSON.slot of type string", + }, + { + name: "SlotInvalid", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"-1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid value for slot: invalid value -1: strconv.ParseUint: parsing \"-1\": invalid syntax", + }, + { + name: "IndexMissing", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "index missing", + }, + { + name: "IndexWrongType", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":true,"kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid JSON: json: cannot unmarshal bool into Go struct field blobSidecarEventJSON.index of type string", + }, + { + name: "IndexInvalid", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"-1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid value for index: invalid value -1: strconv.ParseUint: parsing \"-1\": invalid syntax", + }, + { + name: "KZGCommitmentMissing", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"1","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "kzg_commitment missing", + }, + { + name: "KZGCommitmentWrongType", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"1","kzg_commitment":true,"versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid JSON: json: cannot unmarshal bool into Go struct field blobSidecarEventJSON.kzg_commitment of type string", + }, + { + name: "KZGCommitmentInvalid", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"1","kzg_commitment":"0xinvalidfb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid value for kzg_commitment: invalid value invalidfb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2: encoding/hex: invalid byte: U+0069 'i'", + }, + { + name: "KZGCommitmentShort", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"1","kzg_commitment":"0x","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid value for kzg_commitment: incorrect length", + }, + { + name: "KZGCommitmentLong", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2a","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid value for kzg_commitment: incorrect length", + }, + { + name: "VersionedHashMissing", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2"}`), + err: "versioned_hash missing", + }, + { + name: "VersionedHashWrongType", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":true}`), + err: "invalid JSON: json: cannot unmarshal bool into Go struct field blobSidecarEventJSON.versioned_hash of type string", + }, + { + name: "VersionedHashInvalid", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xinvalide9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + err: "invalid value for versioned_hash: invalid value invalide9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2: encoding/hex: invalid byte: U+0069 'i'", + }, + { + name: "VersionedHashShort", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0x"}`), + err: "invalid value for versioned_hash: incorrect length", + }, + { + name: "VersionedHashLong", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2a"}`), + err: "invalid value for versioned_hash: incorrect length", + }, + { + name: "Good", + input: []byte(`{"block_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","slot":"1","index":"1","kzg_commitment":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2","versioned_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}`), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var res api.BlobSidecarEvent + 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 6adc2dc5..06724def 100644 --- a/api/v1/event.go +++ b/api/v1/event.go @@ -40,6 +40,7 @@ var SupportedEventTopics = map[string]bool{ "voluntary_exit": true, "contribution_and_proof": true, "payload_attributes": true, + "blob_sidecar": true, } // eventJSON is the spec representation of the struct. @@ -99,6 +100,8 @@ func (e *Event) UnmarshalJSON(input []byte) error { e.Data = &altair.SignedContributionAndProof{} case "payload_attributes": e.Data = &PayloadAttributesEvent{} + case "blob_sidecar": + e.Data = &BlobSidecarEvent{} default: return fmt.Errorf("unsupported event topic %s", eventJSON.Topic) }