diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4c7592c --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/ethpandaops/go-lean-types + +go 1.22.4 + +require ( + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..33dd589 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/specs/phase0/block.go b/specs/phase0/block.go new file mode 100644 index 0000000..299cc1e --- /dev/null +++ b/specs/phase0/block.go @@ -0,0 +1,8 @@ +package phase0 + +type Block struct { + Slot Slot `json:"slot"` + Parent Root `json:"parent"` + Votes []Vote `json:"votes" dynssz-max:"VALIDATOR_REGISTRY_LIMIT" ssz-max:"4096"` + StateRoot Root `json:"state_root"` +} diff --git a/specs/phase0/checkpoint.go b/specs/phase0/checkpoint.go new file mode 100644 index 0000000..2c14ffc --- /dev/null +++ b/specs/phase0/checkpoint.go @@ -0,0 +1,6 @@ +package phase0 + +type Checkpoint struct { + Root Root `json:"root"` + Slot Slot `json:"slot"` +} diff --git a/specs/phase0/config.go b/specs/phase0/config.go new file mode 100644 index 0000000..10ac273 --- /dev/null +++ b/specs/phase0/config.go @@ -0,0 +1,5 @@ +package phase0 + +type Config struct { + NumValidators uint64 `json:"num_validators"` +} diff --git a/specs/phase0/consts.go b/specs/phase0/consts.go new file mode 100644 index 0000000..b8f9155 --- /dev/null +++ b/specs/phase0/consts.go @@ -0,0 +1,4 @@ +package phase0 + +// RootLength is the number of bytes in a root. +const RootLength = 32 diff --git a/specs/phase0/root.go b/specs/phase0/root.go new file mode 100644 index 0000000..52d8a5c --- /dev/null +++ b/specs/phase0/root.go @@ -0,0 +1,106 @@ +package phase0 + +import ( + "bytes" + "encoding/hex" + "fmt" + + "github.com/pkg/errors" +) + +// Root is a merkle root. +type Root [RootLength]byte + +var zeroRoot = Root{} + +// IsZero returns true if the root is zero. +func (r Root) IsZero() bool { + return bytes.Equal(r[:], zeroRoot[:]) +} + +// String returns a string version of the structure. +func (r Root) String() string { + return fmt.Sprintf("%#x", r) +} + +// Format formats the root. +func (r Root) Format(state fmt.State, v rune) { + format := string(v) + switch v { + case 's': + fmt.Fprint(state, r.String()) + case 'x', 'X': + if state.Flag('#') { + format = "#" + format + } + fmt.Fprintf(state, "%"+format, r[:]) + default: + fmt.Fprintf(state, "%"+format, r[:]) + } +} + +// UnmarshalJSON implements json.Unmarshaler. +func (r *Root) UnmarshalJSON(input []byte) error { + if len(input) == 0 { + return errors.New("input missing") + } + + if !bytes.HasPrefix(input, []byte{'"', '0', 'x'}) { + return errors.New("invalid prefix") + } + if !bytes.HasSuffix(input, []byte{'"'}) { + return errors.New("invalid suffix") + } + if len(input) != 1+2+RootLength*2+1 { + return errors.New("incorrect length") + } + + length, err := hex.Decode(r[:], input[3:3+RootLength*2]) + if err != nil { + return errors.Wrapf(err, "invalid value %s", string(input[3:3+RootLength*2])) + } + + if length != RootLength { + return errors.New("incorrect length") + } + + return nil +} + +// MarshalJSON implements json.Marshaler. +func (r Root) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%#x"`, r)), nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (r *Root) UnmarshalYAML(input []byte) error { + if len(input) == 0 { + return errors.New("input missing") + } + + if !bytes.HasPrefix(input, []byte{'\'', '0', 'x'}) { + return errors.New("invalid prefix") + } + if !bytes.HasSuffix(input, []byte{'\''}) { + return errors.New("invalid suffix") + } + if len(input) != 1+2+RootLength*2+1 { + return errors.New("incorrect length") + } + + length, err := hex.Decode(r[:], input[3:3+RootLength*2]) + if err != nil { + return errors.Wrapf(err, "invalid value %s", string(input[3:3+RootLength*2])) + } + + if length != RootLength { + return errors.New("incorrect length") + } + + return nil +} + +// MarshalYAML implements yaml.Marshaler. +func (r Root) MarshalYAML() ([]byte, error) { + return []byte(fmt.Sprintf(`'%#x'`, r)), nil +} diff --git a/specs/phase0/root_test.go b/specs/phase0/root_test.go new file mode 100644 index 0000000..e19fd97 --- /dev/null +++ b/specs/phase0/root_test.go @@ -0,0 +1,16 @@ +package phase0_test + +import ( + "testing" + + "github.com/ethpandaops/go-lean-types/specs/phase0" + "github.com/stretchr/testify/require" +) + +func TestZeroRoot(t *testing.T) { + zeroRoot := &phase0.Root{} + require.True(t, zeroRoot.IsZero()) + + nonZeroRoot := &phase0.Root{0x01} + require.False(t, nonZeroRoot.IsZero()) +} diff --git a/specs/phase0/signature.go b/specs/phase0/signature.go new file mode 100644 index 0000000..db66111 --- /dev/null +++ b/specs/phase0/signature.go @@ -0,0 +1,111 @@ +package phase0 + +import ( + "bytes" + "encoding/hex" + "fmt" + + "github.com/pkg/errors" +) + +// Signature is a signature. +type Signature [32]byte + +// SignatureLength is the number of bytes in a signature. +const SignatureLength = 32 + +var ( + emptySignature = Signature{} +) + +// IsZero returns true if the signature is zero. +func (s Signature) IsZero() bool { + return bytes.Equal(s[:], emptySignature[:]) +} + +// String returns a string version of the structure. +func (s Signature) String() string { + return fmt.Sprintf("%#x", s) +} + +// Format formats the signature. +func (s Signature) Format(state fmt.State, v rune) { + format := string(v) + switch v { + case 's': + fmt.Fprint(state, s.String()) + case 'x', 'X': + if state.Flag('#') { + format = "#" + format + } + fmt.Fprintf(state, "%"+format, s[:]) + default: + fmt.Fprintf(state, "%"+format, s[:]) + } +} + +// UnmarshalJSON implements json.Unmarshaler. +func (s *Signature) UnmarshalJSON(input []byte) error { + if len(input) == 0 { + return errors.New("input missing") + } + + if !bytes.HasPrefix(input, []byte{'"', '0', 'x'}) { + return errors.New("invalid prefix") + } + if !bytes.HasSuffix(input, []byte{'"'}) { + return errors.New("invalid suffix") + } + if len(input) != 1+2+SignatureLength*2+1 { + return errors.New("incorrect length") + } + + length, err := hex.Decode(s[:], input[3:3+SignatureLength*2]) + if err != nil { + return errors.Wrapf(err, "invalid value %s", string(input[3:3+SignatureLength*2])) + } + + if length != SignatureLength { + return errors.New("incorrect length") + } + + return nil +} + +// MarshalJSON implements json.Marshaler. +func (s Signature) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%#x"`, s)), nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (s *Signature) UnmarshalYAML(input []byte) error { + if len(input) == 0 { + return errors.New("input missing") + } + + if !bytes.HasPrefix(input, []byte{'\'', '0', 'x'}) { + return errors.New("invalid prefix") + } + if !bytes.HasSuffix(input, []byte{'\''}) { + return errors.New("invalid suffix") + } + if len(input) != 1+2+SignatureLength*2+1 { + return errors.New("incorrect length") + } + + length, err := hex.Decode(s[:], input[3:3+SignatureLength*2]) + if err != nil { + return errors.Wrapf(err, "invalid value %s", string(input[3:3+SignatureLength*2])) + } + + if length != SignatureLength { + return errors.New("incorrect length") + } + + return nil +} + +// MarshalYAML implements yaml.Marshaler. +func (s Signature) MarshalYAML() ([]byte, error) { + return []byte(fmt.Sprintf(`'%#x'`, s)), nil +} diff --git a/specs/phase0/signedvote.go b/specs/phase0/signedvote.go new file mode 100644 index 0000000..2c7030a --- /dev/null +++ b/specs/phase0/signedvote.go @@ -0,0 +1,6 @@ +package phase0 + +type SignedVote struct { + Vote Vote `json:"vote"` + Signature Signature `json:"signature"` +} diff --git a/specs/phase0/slot.go b/specs/phase0/slot.go new file mode 100644 index 0000000..9d17f2b --- /dev/null +++ b/specs/phase0/slot.go @@ -0,0 +1,41 @@ +package phase0 + +import ( + "bytes" + "fmt" + "strconv" + + "github.com/pkg/errors" +) + +// Slot is a slot number. +type Slot uint64 + +// UnmarshalJSON implements json.Unmarshaler. +func (s *Slot) UnmarshalJSON(input []byte) error { + if len(input) == 0 { + return errors.New("input missing") + } + if len(input) < 3 { + return errors.New("input malformed") + } + if !bytes.HasPrefix(input, []byte{'"'}) { + return errors.New("invalid prefix") + } + if !bytes.HasSuffix(input, []byte{'"'}) { + return errors.New("invalid suffix") + } + + val, err := strconv.ParseUint(string(input[1:len(input)-1]), 10, 64) + if err != nil { + return errors.Wrapf(err, "invalid value %s", string(input[1:len(input)-1])) + } + *s = Slot(val) + + return nil +} + +// MarshalJSON implements json.Marshaler. +func (s Slot) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%d"`, s)), nil +} diff --git a/specs/phase0/state.go b/specs/phase0/state.go new file mode 100644 index 0000000..630ca88 --- /dev/null +++ b/specs/phase0/state.go @@ -0,0 +1,18 @@ +package phase0 + +// Bitlist represents a bitlist with a maximum length. +type Bitlist []byte + +// State represents the beacon chain state container. +type State struct { + Config Config `json:"config"` + + LatestJustified Checkpoint `json:"latest_justified"` + LatestFinalized Checkpoint `json:"latest_finalized"` + + HistoricalBlockHashes []Root `json:"historical_block_hashes" dynssz-max:"HISTORICAL_ROOTS_LIMIT" ssz-max:"262144"` + JustifiedSlots []bool `json:"justified_slots" dynssz-max:"HISTORICAL_ROOTS_LIMIT" ssz-max:"262144"` + + JustificationsRoots []Root `json:"justifications_roots" dynssz-max:"HISTORICAL_ROOTS_LIMIT" ssz-max:"262144"` + JustificationsValidators []byte `json:"justifications_validators" dynssz-max:"(HISTORICAL_ROOTS_LIMIT * VALIDATOR_REGISTRY_LIMIT) / 8" ssz-max:"134217728"` +} diff --git a/specs/phase0/vote.go b/specs/phase0/vote.go new file mode 100644 index 0000000..ef209dd --- /dev/null +++ b/specs/phase0/vote.go @@ -0,0 +1,9 @@ +package phase0 + +type Vote struct { + ValidatorId uint64 `json:"validator_id"` + Slot Slot `json:"slot"` + Head Checkpoint `json:"head"` + Target Checkpoint `json:"target"` + Source Checkpoint `json:"source"` +} diff --git a/specs/preset.go b/specs/preset.go new file mode 100644 index 0000000..86f1e13 --- /dev/null +++ b/specs/preset.go @@ -0,0 +1,6 @@ +package specs + +var MainnetPreset = map[string]interface{}{ + "HISTORICAL_ROOTS_LIMIT": 262144, + "VALIDATOR_REGISTRY_LIMIT": 4096, +}