Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions spec/altair/syncaggregatorselectiondata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,3 +30,92 @@ 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)
}
160 changes: 160 additions & 0 deletions spec/altair/syncaggregatorselectiondata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// 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
//
// 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"
"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}`),
},
{
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"}`),
},
}
Comment thread
jshufro marked this conversation as resolved.

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))
}
})
}
}