From fa3a2c64e16904ea4d0f5813cd9ea8c0cee6719a Mon Sep 17 00:00:00 2001 From: Jacob Shufro Date: Sun, 15 Mar 2026 16:20:00 -0400 Subject: [PATCH 1/4] Add json and yaml serialization for syncaggregatorselectiondata --- spec/altair/syncaggregatorselectiondata.go | 93 ++++++++++++++ .../syncaggregatorselectiondata_test.go | 121 ++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 spec/altair/syncaggregatorselectiondata_test.go diff --git a/spec/altair/syncaggregatorselectiondata.go b/spec/altair/syncaggregatorselectiondata.go index a0a8f5a6d..439695316 100644 --- a/spec/altair/syncaggregatorselectiondata.go +++ b/spec/altair/syncaggregatorselectiondata.go @@ -14,7 +14,14 @@ package altair import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/goccy/go-yaml" + "github.com/pkg/errors" ) // SyncAggregatorSelectionData is an internal struct for @@ -23,3 +30,89 @@ type SyncAggregatorSelectionData struct { Slot phase0.Slot SubcommitteeIndex uint64 } + +// syncAggregatorSelectionDataJSON is the spec representation of the struct. +type syncAggregatorSelectionDataJSON struct { + Slot string `json:"slot"` + SubcommitteeIndex string `json:"subcommittee_index"` +} + +// syncAggregatorSelectionDataYAML is the spec representation of the struct. +type syncAggregatorSelectionDataYAML struct { + Slot uint64 `yaml:"slot"` + SubcommitteeIndex uint64 `yaml:"subcommittee_index"` +} + +// MarshalJSON implements json.Marshaler. +func (s *SyncAggregatorSelectionData) MarshalJSON() ([]byte, error) { + return json.Marshal(&syncAggregatorSelectionDataJSON{ + Slot: fmt.Sprintf("%d", s.Slot), + SubcommitteeIndex: fmt.Sprintf("%d", s.SubcommitteeIndex), + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (s *SyncAggregatorSelectionData) UnmarshalJSON(input []byte) error { + var syncAggregatorSelectionDataJSON syncAggregatorSelectionDataJSON + if err := json.Unmarshal(input, &syncAggregatorSelectionDataJSON); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + return s.unpack(&syncAggregatorSelectionDataJSON) +} + +func (s *SyncAggregatorSelectionData) unpack(syncAggregatorSelectionDataJSON *syncAggregatorSelectionDataJSON) error { + if syncAggregatorSelectionDataJSON.Slot == "" { + return errors.New("slot missing") + } + + slot, err := strconv.ParseUint(syncAggregatorSelectionDataJSON.Slot, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for slot") + } + + s.Slot = phase0.Slot(slot) + + if syncAggregatorSelectionDataJSON.SubcommitteeIndex == "" { + return errors.New("subcommittee index missing") + } + + subcommitteeIndex, err := strconv.ParseUint(syncAggregatorSelectionDataJSON.SubcommitteeIndex, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for subcommittee index") + } + + s.SubcommitteeIndex = subcommitteeIndex + + return nil +} + +// MarshalYAML implements yaml.Marshaler. +func (s *SyncAggregatorSelectionData) MarshalYAML() ([]byte, error) { + yamlBytes, err := yaml.MarshalWithOptions(&syncAggregatorSelectionDataYAML{ + Slot: uint64(s.Slot), + SubcommitteeIndex: s.SubcommitteeIndex, + }, yaml.Flow(true)) + if err != nil { + return nil, err + } + return bytes.ReplaceAll(yamlBytes, []byte(`"`), []byte(`'`)), nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (s *SyncAggregatorSelectionData) UnmarshalYAML(input []byte) error { + var syncAggregatorSelectionDataJSON syncAggregatorSelectionDataJSON + if err := yaml.Unmarshal(input, &syncAggregatorSelectionDataJSON); err != nil { + return err + } + return s.unpack(&syncAggregatorSelectionDataJSON) +} + +// String returns a string version of the structure. +func (s *SyncAggregatorSelectionData) String() string { + data, err := yaml.Marshal(s) + if err != nil { + return fmt.Sprintf("ERR: %v", err) + } + return string(data) +} diff --git a/spec/altair/syncaggregatorselectiondata_test.go b/spec/altair/syncaggregatorselectiondata_test.go new file mode 100644 index 000000000..2fc578a99 --- /dev/null +++ b/spec/altair/syncaggregatorselectiondata_test.go @@ -0,0 +1,121 @@ +// Copyright © 2021 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 altair_test + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/goccy/go-yaml" + "github.com/stretchr/testify/assert" + require "github.com/stretchr/testify/require" +) + +func TestSyncAggregatorSelectionDataJSON(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 altair.syncAggregatorSelectionDataJSON", + }, + { + name: "SlotMissing", + input: []byte(`{"subcommittee_index":"3"}`), + err: "slot missing", + }, + { + name: "SlotWrongType", + input: []byte(`{"slot":true,"subcommittee_index":"3"}`), + err: "invalid JSON: json: cannot unmarshal bool into Go struct field syncAggregatorSelectionDataJSON.slot of type string", + }, + { + name: "SlotInvalid", + input: []byte(`{"slot":"-1","subcommittee_index":"3"}`), + err: "invalid value for slot: strconv.ParseUint: parsing \"-1\": invalid syntax", + }, + { + name: "SubcommitteeIndexMissing", + input: []byte(`{"slot":"1"}`), + err: "subcommittee index missing", + }, + { + name: "SubcommitteeIndexWrongType", + input: []byte(`{"slot":"1","subcommittee_index":true}`), + err: "invalid JSON: json: cannot unmarshal bool into Go struct field syncAggregatorSelectionDataJSON.subcommittee_index of type string", + }, + { + name: "SubcommitteeIndexInvalid", + input: []byte(`{"slot":"1","subcommittee_index":"-1"}`), + err: "invalid value for subcommittee index: strconv.ParseUint: parsing \"-1\": invalid syntax", + }, + { + name: "Valid", + input: []byte(`{"slot":"1","subcommittee_index":"3"}`), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var res altair.SyncAggregatorSelectionData + 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)) + } + }) + } +} + +func TestSyncAggregatorSelectionDataYAML(t *testing.T) { + tests := []struct { + name string + input []byte + err string + }{ + { + name: "Good", + input: []byte(`{slot: 1, subcommittee_index: 3}`), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var res altair.SyncAggregatorSelectionData + err := yaml.Unmarshal(test.input, &res) + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + rt, err := yaml.Marshal(&res) + require.NoError(t, err) + assert.Equal(t, string(rt), res.String()) + rt = bytes.TrimSuffix(rt, []byte("\n")) + assert.Equal(t, string(test.input), string(rt)) + } + }) + } +} From eca0a2d960fb8fdd4c650c79aa7f52db128d7411 Mon Sep 17 00:00:00 2001 From: Jacob Shufro <116244+jshufro@users.noreply.github.com> Date: Sun, 19 Apr 2026 14:07:19 -0400 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Miguel Tenorio <46824157+AntiD2ta@users.noreply.github.com> --- spec/altair/syncaggregatorselectiondata_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/altair/syncaggregatorselectiondata_test.go b/spec/altair/syncaggregatorselectiondata_test.go index 2fc578a99..0d725e3a0 100644 --- a/spec/altair/syncaggregatorselectiondata_test.go +++ b/spec/altair/syncaggregatorselectiondata_test.go @@ -1,4 +1,4 @@ -// Copyright © 2021 Attestant Limited. +// Copyright © 2026 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 @@ -21,7 +21,7 @@ import ( "github.com/attestantio/go-eth2-client/spec/altair" "github.com/goccy/go-yaml" "github.com/stretchr/testify/assert" - require "github.com/stretchr/testify/require" + "github.com/stretchr/testify/require" ) func TestSyncAggregatorSelectionDataJSON(t *testing.T) { From ca25fc1d9880adff777be891f8f7f980f253ca29 Mon Sep 17 00:00:00 2001 From: Jacob Shufro Date: Sun, 19 Apr 2026 14:11:04 -0400 Subject: [PATCH 3/4] Resolve CI errors in spec/altair/syncaggregatorselectiondata.go --- spec/altair/syncaggregatorselectiondata.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/altair/syncaggregatorselectiondata.go b/spec/altair/syncaggregatorselectiondata.go index 439695316..8e719cb62 100644 --- a/spec/altair/syncaggregatorselectiondata.go +++ b/spec/altair/syncaggregatorselectiondata.go @@ -96,6 +96,7 @@ func (s *SyncAggregatorSelectionData) MarshalYAML() ([]byte, error) { if err != nil { return nil, err } + return bytes.ReplaceAll(yamlBytes, []byte(`"`), []byte(`'`)), nil } @@ -105,6 +106,7 @@ func (s *SyncAggregatorSelectionData) UnmarshalYAML(input []byte) error { if err := yaml.Unmarshal(input, &syncAggregatorSelectionDataJSON); err != nil { return err } + return s.unpack(&syncAggregatorSelectionDataJSON) } @@ -114,5 +116,6 @@ func (s *SyncAggregatorSelectionData) String() string { if err != nil { return fmt.Sprintf("ERR: %v", err) } + return string(data) } From e709b40a9f530f8621224b4023ee0cc99e3b0d2d Mon Sep 17 00:00:00 2001 From: Jacob Shufro Date: Sun, 19 Apr 2026 14:29:03 -0400 Subject: [PATCH 4/4] Add more test cases to spec/altair/syncaggregatorselectiondata_test.go --- .../syncaggregatorselectiondata_test.go | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/spec/altair/syncaggregatorselectiondata_test.go b/spec/altair/syncaggregatorselectiondata_test.go index 0d725e3a0..e0249d7b3 100644 --- a/spec/altair/syncaggregatorselectiondata_test.go +++ b/spec/altair/syncaggregatorselectiondata_test.go @@ -100,6 +100,45 @@ func TestSyncAggregatorSelectionDataYAML(t *testing.T) { name: "Good", input: []byte(`{slot: 1, subcommittee_index: 3}`), }, + { + name: "YAMLBad", + input: []byte("[]"), + err: "[1:1] sequence was used where mapping is expected\n> 1 | []\n ^\n", + }, + { + name: "SlotMissing", + input: []byte(`{"subcommittee_index":"3"}`), + err: "slot missing", + }, + { + name: "SlotWrongType", + input: []byte(`{"slot":true,"subcommittee_index":"3"}`), + err: "invalid value for slot: strconv.ParseUint: parsing \"true\": invalid syntax", + }, + { + name: "SlotInvalid", + input: []byte(`{"slot":"-1","subcommittee_index":"3"}`), + err: "invalid value for slot: strconv.ParseUint: parsing \"-1\": invalid syntax", + }, + { + name: "SubcommitteeIndexMissing", + input: []byte(`{"slot":"1"}`), + err: "subcommittee index missing", + }, + { + name: "SubcommitteeIndexWrongType", + input: []byte(`{"slot":"1","subcommittee_index":true}`), + err: "invalid JSON: json: cannot unmarshal bool into Go struct field syncAggregatorSelectionDataJSON.subcommittee_index of type string", + }, + { + name: "SubcommitteeIndexInvalid", + input: []byte(`{"slot":"1","subcommittee_index":"-1"}`), + err: "invalid value for subcommittee index: strconv.ParseUint: parsing \"-1\": invalid syntax", + }, + { + name: "Valid", + input: []byte(`{"slot":"1","subcommittee_index":"3"}`), + }, } for _, test := range tests {