diff --git a/go.mod b/go.mod index 2e75af7d..47d52a51 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,18 @@ module github.com/ethpandaops/buildoor go 1.25.7 require ( - github.com/attestantio/go-builder-client v0.0.0-00010101000000-000000000000 - github.com/attestantio/go-eth2-client v0.27.1 github.com/ethereum/go-ethereum v1.17.3 github.com/ethpandaops/go-eth2-client v0.1.3 github.com/ethpandaops/service-authenticatoor v0.0.1 github.com/glebarez/go-sqlite v1.22.0 + github.com/goccy/go-yaml v1.19.2 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/gorilla/mux v1.8.1 github.com/herumi/bls-eth-go-binary v1.37.0 github.com/holiman/uint256 v1.3.2 github.com/jmoiron/sqlx v1.4.0 github.com/pk910/dynamic-ssz v1.3.2-0.20260505131440-111bcb265c8f + github.com/pkg/errors v0.9.1 github.com/pressly/goose/v3 v3.27.1 github.com/prometheus/client_golang v1.23.2 github.com/rs/zerolog v1.35.1 @@ -60,7 +60,6 @@ require ( github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/goccy/go-yaml v1.19.2 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -83,12 +82,10 @@ require ( github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pk910/hashtree-bindings v0.1.0 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.20.1 // indirect - github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 // indirect github.com/r3labs/sse/v2 v2.10.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect @@ -126,4 +123,4 @@ require ( modernc.org/sqlite v1.49.1 // indirect ) -replace github.com/attestantio/go-builder-client => github.com/bharath-123/go-builder-client v0.0.0-20260614061206-3d327877ba19 +tool github.com/pk910/dynamic-ssz/dynssz-gen diff --git a/go.sum b/go.sum index fa76811f..c191cdad 100644 --- a/go.sum +++ b/go.sum @@ -19,12 +19,8 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= -github.com/attestantio/go-eth2-client v0.27.1 h1:g7bm+gG/p+gfzYdEuxuAepVWYb8EO+2KojV5/Lo2BxM= -github.com/attestantio/go-eth2-client v0.27.1/go.mod h1:fvULSL9WtNskkOB4i+Yyr6BKpNHXvmpGZj9969fCrfY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bharath-123/go-builder-client v0.0.0-20260614061206-3d327877ba19 h1:nlEc90bi1ObfWrGS+3xvFxu44Gxv/gQZIolzdydkfMY= -github.com/bharath-123/go-builder-client v0.0.0-20260614061206-3d327877ba19/go.mod h1:DUKOrPS+kfquqoUh5rbCoZempShC4NiSxKv2VrTlbZs= github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= @@ -227,8 +223,6 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= -github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 h1:lC8kiphgdOBTcbTvo8MwkvpKjO0SlAgjv4xIK5FGJ94= -github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15/go.mod h1:8svFBIKKu31YriBG/pNizo9N0Jr9i5PQ+dFkxWg3x5k= github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= diff --git a/pkg/builderapi/builder_preferences_test.go b/pkg/builderapi/builder_preferences_test.go index 0a0bcaa4..86a7fb6e 100644 --- a/pkg/builderapi/builder_preferences_test.go +++ b/pkg/builderapi/builder_preferences_test.go @@ -8,19 +8,13 @@ import ( "net/http/httptest" "testing" - buildergloas "github.com/attestantio/go-builder-client/api/gloas" - // attphase0 is the attestantio phase0 used by go-builder-client's builder-API - // types. Buildoor uses ethpandaops/go-eth2-client everywhere; this import exists - // ONLY so tests can populate buildergloas struct fields (Slot/Gwei/Signature), - // which are attestantio-typed. Production code converts at the boundary and never - // imports attestantio. Prefer ethpandaops/go-eth2-client outside of this. - attphase0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethpandaops/go-eth2-client/spec/phase0" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" gloasauth "github.com/ethpandaops/buildoor/pkg/builderapi/gloas" + gloastypes "github.com/ethpandaops/buildoor/pkg/builderapi/gloas/types" "github.com/ethpandaops/buildoor/pkg/config" "github.com/ethpandaops/buildoor/pkg/signer" ) @@ -44,9 +38,9 @@ func signBuilderPrefsRequest( ) []byte { t.Helper() - auth := &buildergloas.RequestAuthV1{ + auth := &gloastypes.RequestAuthV1{ Data: []byte(builderURL), - Slot: attphase0.Slot(slot), + Slot: slot, } root, err := auth.HashTreeRoot() require.NoError(t, err) @@ -55,11 +49,11 @@ func signBuilderPrefsRequest( sig, err := s.SignWithDomain(phase0.Root(root), domain) require.NoError(t, err) - req := &buildergloas.BuilderPreferencesRequestV1{ - Preferences: &buildergloas.BuilderPreferencesV1{MaxExecutionPayment: attphase0.Gwei(maxPayment)}, - Auth: &buildergloas.SignedRequestAuthV1{ + req := &gloastypes.BuilderPreferencesRequestV1{ + Preferences: &gloastypes.BuilderPreferencesV1{MaxExecutionPayment: maxPayment}, + Auth: &gloastypes.SignedRequestAuthV1{ Message: auth, - Signature: attphase0.BLSSignature(sig), + Signature: sig, }, } body, err := json.Marshal(req) @@ -140,7 +134,7 @@ func TestSubmitBuilderPreferences_SuccessSSZ(t *testing.T) { // Build the same signed request as the JSON path, but submit it SSZ-encoded. jsonBody := signBuilderPrefsRequest(t, blsSigner, testBuilderURL, 100, 5_000_000_000, gfv) - var prefsReq buildergloas.BuilderPreferencesRequestV1 + var prefsReq gloastypes.BuilderPreferencesRequestV1 require.NoError(t, json.Unmarshal(jsonBody, &prefsReq)) sszBody, err := prefsReq.MarshalSSZ() require.NoError(t, err) diff --git a/pkg/builderapi/encoding.go b/pkg/builderapi/encoding.go index 8cf796a3..d646962d 100644 --- a/pkg/builderapi/encoding.go +++ b/pkg/builderapi/encoding.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - buildergloas "github.com/attestantio/go-builder-client/api/gloas" + gloastypes "github.com/ethpandaops/buildoor/pkg/builderapi/gloas/types" ) // Wire formats accepted on Gloas builder-API request bodies. The builder-spec @@ -33,8 +33,8 @@ func normalizeContentType(ct string) string { // by contentType. The Content-Type must be set explicitly to one of the two // supported media types; an empty or unrecognized type returns // errUnsupportedContentType (which handlers map to 415). -func parseSignedRequestAuth(data []byte, contentType string) (*buildergloas.SignedRequestAuthV1, error) { - var v buildergloas.SignedRequestAuthV1 +func parseSignedRequestAuth(data []byte, contentType string) (*gloastypes.SignedRequestAuthV1, error) { + var v gloastypes.SignedRequestAuthV1 switch normalizeContentType(contentType) { case contentTypeSSZ: if err := v.UnmarshalSSZ(data); err != nil { @@ -53,8 +53,8 @@ func parseSignedRequestAuth(data []byte, contentType string) (*buildergloas.Sign // parseBuilderPreferencesRequest decodes a BuilderPreferencesRequestV1 from JSON // or SSZ selected by contentType, following the same rules as // parseSignedRequestAuth. -func parseBuilderPreferencesRequest(data []byte, contentType string) (*buildergloas.BuilderPreferencesRequestV1, error) { - var v buildergloas.BuilderPreferencesRequestV1 +func parseBuilderPreferencesRequest(data []byte, contentType string) (*gloastypes.BuilderPreferencesRequestV1, error) { + var v gloastypes.BuilderPreferencesRequestV1 switch normalizeContentType(contentType) { case contentTypeSSZ: if err := v.UnmarshalSSZ(data); err != nil { diff --git a/pkg/builderapi/encoding_test.go b/pkg/builderapi/encoding_test.go index f2ef9865..0daf79c5 100644 --- a/pkg/builderapi/encoding_test.go +++ b/pkg/builderapi/encoding_test.go @@ -4,19 +4,18 @@ import ( "encoding/json" "testing" - buildergloas "github.com/attestantio/go-builder-client/api/gloas" - // attphase0 is the attestantio phase0 used by go-builder-client's builder-API - // types; imported here ONLY to populate buildergloas struct fields in tests. - attphase0 "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethpandaops/go-eth2-client/spec/phase0" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + gloastypes "github.com/ethpandaops/buildoor/pkg/builderapi/gloas/types" ) -func sampleSignedRequestAuth() *buildergloas.SignedRequestAuthV1 { - var sig attphase0.BLSSignature +func sampleSignedRequestAuth() *gloastypes.SignedRequestAuthV1 { + var sig phase0.BLSSignature sig[0] = 0xaa - return &buildergloas.SignedRequestAuthV1{ - Message: &buildergloas.RequestAuthV1{Data: []byte("https://builder.example.com"), Slot: 42}, + return &gloastypes.SignedRequestAuthV1{ + Message: &gloastypes.RequestAuthV1{Data: []byte("https://builder.example.com"), Slot: 42}, Signature: sig, } } @@ -38,7 +37,7 @@ func TestParseSignedRequestAuth_JSONAndSSZRoundTrip(t *testing.T) { fromJSONParam, err := parseSignedRequestAuth(jsonBody, "application/json; charset=utf-8") require.NoError(t, err) - for _, got := range []*buildergloas.SignedRequestAuthV1{fromJSON, fromSSZ, fromJSONParam} { + for _, got := range []*gloastypes.SignedRequestAuthV1{fromJSON, fromSSZ, fromJSONParam} { require.NotNil(t, got.Message) assert.Equal(t, orig.Message.Slot, got.Message.Slot) assert.Equal(t, orig.Message.Data, got.Message.Data) @@ -66,8 +65,8 @@ func TestParseSignedRequestAuth_Errors(t *testing.T) { } func TestParseBuilderPreferencesRequest_SSZRoundTrip(t *testing.T) { - orig := &buildergloas.BuilderPreferencesRequestV1{ - Preferences: &buildergloas.BuilderPreferencesV1{MaxExecutionPayment: 5_000_000_000}, + orig := &gloastypes.BuilderPreferencesRequestV1{ + Preferences: &gloastypes.BuilderPreferencesV1{MaxExecutionPayment: 5_000_000_000}, Auth: sampleSignedRequestAuth(), } diff --git a/pkg/builderapi/gloas/auth.go b/pkg/builderapi/gloas/auth.go index 193c227e..8991f7fe 100644 --- a/pkg/builderapi/gloas/auth.go +++ b/pkg/builderapi/gloas/auth.go @@ -6,9 +6,9 @@ import ( "errors" "fmt" - buildergloas "github.com/attestantio/go-builder-client/api/gloas" "github.com/ethpandaops/go-eth2-client/spec/phase0" + gloastypes "github.com/ethpandaops/buildoor/pkg/builderapi/gloas/types" "github.com/ethpandaops/buildoor/pkg/signer" ) @@ -36,7 +36,7 @@ var ( // // Returns nil on success, or one of the package's sentinel errors on failure. func VerifyRequestAuth( - signed *buildergloas.SignedRequestAuthV1, + signed *gloastypes.SignedRequestAuthV1, validatorPubkey phase0.BLSPubKey, genesisForkVersion phase0.Version, ) error { diff --git a/pkg/builderapi/gloas/auth_test.go b/pkg/builderapi/gloas/auth_test.go index bb8fbff6..4971c688 100644 --- a/pkg/builderapi/gloas/auth_test.go +++ b/pkg/builderapi/gloas/auth_test.go @@ -4,17 +4,11 @@ import ( "strings" "testing" - buildergloas "github.com/attestantio/go-builder-client/api/gloas" - // attphase0 is the attestantio phase0 used by go-builder-client's builder-API - // types. Buildoor uses ethpandaops/go-eth2-client everywhere; this import exists - // ONLY so tests can populate buildergloas struct fields (Slot/Signature), which - // are attestantio-typed. Production code converts at the boundary and never - // imports attestantio. Prefer ethpandaops/go-eth2-client outside of this. - attphase0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethpandaops/go-eth2-client/spec/phase0" "github.com/stretchr/testify/require" "github.com/ethpandaops/buildoor/pkg/builderapi/gloas" + gloastypes "github.com/ethpandaops/buildoor/pkg/builderapi/gloas/types" "github.com/ethpandaops/buildoor/pkg/signer" ) @@ -34,12 +28,12 @@ func signRequestAuth( builderURL []byte, slot phase0.Slot, genesisForkVersion phase0.Version, -) *buildergloas.SignedRequestAuthV1 { +) *gloastypes.SignedRequestAuthV1 { t.Helper() - msg := &buildergloas.RequestAuthV1{ + msg := &gloastypes.RequestAuthV1{ Data: builderURL, - Slot: attphase0.Slot(slot), + Slot: slot, } root, err := msg.HashTreeRoot() @@ -50,9 +44,9 @@ func signRequestAuth( sig, err := s.SignWithDomain(root, domain) require.NoError(t, err) - return &buildergloas.SignedRequestAuthV1{ + return &gloastypes.SignedRequestAuthV1{ Message: msg, - Signature: attphase0.BLSSignature(sig), + Signature: sig, } } @@ -67,7 +61,7 @@ func TestRequestAuth_SSZRoundTripAndSign(t *testing.T) { genesisForkVersion := phase0.Version{} - msg := &buildergloas.RequestAuthV1{ + msg := &gloastypes.RequestAuthV1{ Data: []byte(testBuilderURL), Slot: 1234, } @@ -78,7 +72,7 @@ func TestRequestAuth_SSZRoundTripAndSign(t *testing.T) { require.NotEmpty(t, encoded) // Unmarshal from SSZ into a fresh value. - decoded := &buildergloas.RequestAuthV1{} + decoded := &gloastypes.RequestAuthV1{} require.NoError(t, decoded.UnmarshalSSZ(encoded)) // The decoded value must match the original. @@ -97,9 +91,9 @@ func TestRequestAuth_SSZRoundTripAndSign(t *testing.T) { sig, err := validator.SignWithDomain(decodedRoot, domain) require.NoError(t, err) - signed := &buildergloas.SignedRequestAuthV1{ + signed := &gloastypes.SignedRequestAuthV1{ Message: decoded, - Signature: attphase0.BLSSignature(sig), + Signature: sig, } require.NoError(t, gloas.VerifyRequestAuth(signed, validator.PublicKey(), genesisForkVersion)) } @@ -181,9 +175,9 @@ func TestVerifyRequestAuth_NilMessage(t *testing.T) { validator, err := signer.NewBLSSigner(validatorPrivkeyHex) require.NoError(t, err) - signed := &buildergloas.SignedRequestAuthV1{ + signed := &gloastypes.SignedRequestAuthV1{ Message: nil, - Signature: attphase0.BLSSignature{}, + Signature: phase0.BLSSignature{}, } err = gloas.VerifyRequestAuth(signed, validator.PublicKey(), phase0.Version{}) @@ -194,13 +188,13 @@ func TestVerifyRequestAuth_GarbageSignature(t *testing.T) { validator, err := signer.NewBLSSigner(validatorPrivkeyHex) require.NoError(t, err) - signed := &buildergloas.SignedRequestAuthV1{ - Message: &buildergloas.RequestAuthV1{ + signed := &gloastypes.SignedRequestAuthV1{ + Message: &gloastypes.RequestAuthV1{ Data: []byte(testBuilderURL), Slot: 1, }, // All-zero signature is not a valid BLS signature. - Signature: attphase0.BLSSignature{}, + Signature: phase0.BLSSignature{}, } err = gloas.VerifyRequestAuth(signed, validator.PublicKey(), phase0.Version{}) diff --git a/pkg/builderapi/gloas/types/builderpreferencesrequestv1.go b/pkg/builderapi/gloas/types/builderpreferencesrequestv1.go new file mode 100644 index 00000000..2361d48a --- /dev/null +++ b/pkg/builderapi/gloas/types/builderpreferencesrequestv1.go @@ -0,0 +1,39 @@ +// 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 types + +import ( + "fmt" + + "github.com/goccy/go-yaml" +) + +// BuilderPreferencesRequestV1 is the body submitted to a builder via the +// submitBuilderPreferences API. The Auth.Message.Data identifies the +// intended builder so the builder can reject preferences that were not +// destined for it. +type BuilderPreferencesRequestV1 struct { + Preferences *BuilderPreferencesV1 + Auth *SignedRequestAuthV1 +} + +// String returns a string version of the structure. +func (b *BuilderPreferencesRequestV1) String() string { + data, err := yaml.Marshal(b) + if err != nil { + return fmt.Sprintf("ERR: %v", err) + } + + return string(data) +} diff --git a/pkg/builderapi/gloas/types/builderpreferencesrequestv1_json.go b/pkg/builderapi/gloas/types/builderpreferencesrequestv1_json.go new file mode 100644 index 00000000..68c207d7 --- /dev/null +++ b/pkg/builderapi/gloas/types/builderpreferencesrequestv1_json.go @@ -0,0 +1,54 @@ +// 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 types + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +// builderPreferencesRequestV1JSON is the spec representation of the struct. +type builderPreferencesRequestV1JSON struct { + Preferences *BuilderPreferencesV1 `json:"preferences"` + Auth *SignedRequestAuthV1 `json:"auth"` +} + +// MarshalJSON implements json.Marshaler. +func (b *BuilderPreferencesRequestV1) MarshalJSON() ([]byte, error) { + return json.Marshal(&builderPreferencesRequestV1JSON{ + Preferences: b.Preferences, + Auth: b.Auth, + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (b *BuilderPreferencesRequestV1) UnmarshalJSON(input []byte) error { + var data builderPreferencesRequestV1JSON + if err := json.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if data.Preferences == nil { + return errors.New("preferences missing") + } + b.Preferences = data.Preferences + + if data.Auth == nil { + return errors.New("auth missing") + } + b.Auth = data.Auth + + return nil +} diff --git a/pkg/builderapi/gloas/types/builderpreferencesrequestv1_ssz.go b/pkg/builderapi/gloas/types/builderpreferencesrequestv1_ssz.go new file mode 100644 index 00000000..d6a6b12e --- /dev/null +++ b/pkg/builderapi/gloas/types/builderpreferencesrequestv1_ssz.go @@ -0,0 +1,136 @@ +// Code generated by dynamic-ssz. DO NOT EDIT. +// Hash: 73051b06cc63fbd730494da90db480932cf6120fd2b1e8914804a555bdb3a416 +// Version: v1.3.1 (https://github.com/pk910/dynamic-ssz) +package types + +import ( + "encoding/binary" + + dynssz "github.com/pk910/dynamic-ssz" + "github.com/pk910/dynamic-ssz/hasher" + "github.com/pk910/dynamic-ssz/sszutils" +) + +var _ = sszutils.ErrListTooBig + +// MarshalSSZ marshals the *BuilderPreferencesRequestV1 to SSZ-encoded bytes. +func (t *BuilderPreferencesRequestV1) MarshalSSZ() ([]byte, error) { + return dynssz.GetGlobalDynSsz().MarshalSSZ(t) +} + +// MarshalSSZTo marshals the *BuilderPreferencesRequestV1 to SSZ-encoded bytes, appending to the provided buffer. +func (t *BuilderPreferencesRequestV1) MarshalSSZTo(buf []byte) (dst []byte, err error) { + dst = buf + if t == nil { + t = new(BuilderPreferencesRequestV1) + } + dstlen := len(dst) + { // Static Field #0 'Preferences' + t := t.Preferences + if t == nil { + t = new(BuilderPreferencesV1) + } + if dst, err = t.MarshalSSZTo(dst); err != nil { + return nil, sszutils.ErrorWithPath(err, "Preferences") + } + } + // Offset Field #1 'Auth' + dst = append(dst, 0, 0, 0, 0) + { // Dynamic Field #1 'Auth' + binary.LittleEndian.PutUint32(dst[dstlen+8:], uint32(len(dst)-dstlen)) + t := t.Auth + if t == nil { + t = new(SignedRequestAuthV1) + } + if dst, err = t.MarshalSSZTo(dst); err != nil { + return nil, sszutils.ErrorWithPath(err, "Auth") + } + } + return dst, nil +} + +// UnmarshalSSZ unmarshals the *BuilderPreferencesRequestV1 from SSZ-encoded bytes. +func (t *BuilderPreferencesRequestV1) UnmarshalSSZ(buf []byte) (err error) { + buflen := len(buf) + if buflen < 12 { + return sszutils.ErrFixedFieldsEOFFn(buflen, 12) + } + { // Field #0 'Preferences' (static) + buf := buf[0:8] + if t.Preferences == nil { + t.Preferences = new(BuilderPreferencesV1) + } + if err = t.Preferences.UnmarshalSSZ(buf); err != nil { + return sszutils.ErrorWithPath(err, "Preferences") + } + } + // Field #1 'Auth' (offset) + offset1 := int(binary.LittleEndian.Uint32(buf[8:12])) + if offset1 != 12 { + return sszutils.ErrorWithPath(sszutils.ErrFirstOffsetMismatchFn(offset1, 12), "Auth:o") + } + { // Field #1 'Auth' (dynamic) + buf := buf[offset1:] + if t.Auth == nil { + t.Auth = new(SignedRequestAuthV1) + } + if err = t.Auth.UnmarshalSSZ(buf); err != nil { + return sszutils.ErrorWithPath(err, "Auth") + } + } + return nil +} + +// SizeSSZ returns the SSZ encoded size of the *BuilderPreferencesRequestV1. +func (t *BuilderPreferencesRequestV1) SizeSSZ() (size int) { + if t == nil { + t = new(BuilderPreferencesRequestV1) + } + // Field #0 'Preferences' static (8 bytes) + // Field #1 'Auth' offset (4 bytes) + size += 12 + { // Dynamic field #1 'Auth' + size += t.Auth.SizeSSZ() + } + return size +} + +// HashTreeRoot computes the SSZ hash tree root of the *BuilderPreferencesRequestV1. +func (t *BuilderPreferencesRequestV1) HashTreeRoot() (root [32]byte, err error) { + err = hasher.WithDefaultHasher(func(hh sszutils.HashWalker) (err error) { + err = t.HashTreeRootWith(hh) + if err == nil { + root, err = hh.HashRoot() + } + return + }) + return +} + +// HashTreeRootWith computes the SSZ hash tree root of the *BuilderPreferencesRequestV1 using the given hash walker. +func (t *BuilderPreferencesRequestV1) HashTreeRootWith(hh sszutils.HashWalker) error { + if t == nil { + t = new(BuilderPreferencesRequestV1) + } + idx := hh.StartTree(sszutils.TreeTypeNone) + { // Field #0 'Preferences' + t := t.Preferences + if t == nil { + t = new(BuilderPreferencesV1) + } + if err := t.HashTreeRootWith(hh); err != nil { + return sszutils.ErrorWithPath(err, "Preferences") + } + } + { // Field #1 'Auth' + t := t.Auth + if t == nil { + t = new(SignedRequestAuthV1) + } + if err := t.HashTreeRootWith(hh); err != nil { + return sszutils.ErrorWithPath(err, "Auth") + } + } + hh.Merkleize(idx) + return nil +} diff --git a/pkg/builderapi/gloas/types/builderpreferencesrequestv1_yaml.go b/pkg/builderapi/gloas/types/builderpreferencesrequestv1_yaml.go new file mode 100644 index 00000000..4b401904 --- /dev/null +++ b/pkg/builderapi/gloas/types/builderpreferencesrequestv1_yaml.go @@ -0,0 +1,49 @@ +// 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 types + +import ( + "bytes" + "encoding/json" + + "github.com/goccy/go-yaml" + "github.com/pkg/errors" +) + +// MarshalYAML implements yaml.Marshaler. +func (b *BuilderPreferencesRequestV1) MarshalYAML() ([]byte, error) { + yamlBytes, err := yaml.MarshalWithOptions(&builderPreferencesRequestV1JSON{ + Preferences: b.Preferences, + Auth: b.Auth, + }, yaml.Flow(true)) + if err != nil { + return nil, err + } + + return bytes.ReplaceAll(yamlBytes, []byte(`"`), []byte(`'`)), nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (b *BuilderPreferencesRequestV1) UnmarshalYAML(input []byte) error { + var data builderPreferencesRequestV1JSON + if err := yaml.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "failed to unmarshal YAML") + } + marshaled, err := json.Marshal(&data) + if err != nil { + return errors.Wrap(err, "failed to marshal JSON") + } + + return b.UnmarshalJSON(marshaled) +} diff --git a/pkg/builderapi/gloas/types/builderpreferencesv1.go b/pkg/builderapi/gloas/types/builderpreferencesv1.go new file mode 100644 index 00000000..81cc63a4 --- /dev/null +++ b/pkg/builderapi/gloas/types/builderpreferencesv1.go @@ -0,0 +1,40 @@ +// 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 types + +import ( + "fmt" + + "github.com/ethpandaops/go-eth2-client/spec/phase0" + "github.com/goccy/go-yaml" +) + +// BuilderPreferencesV1 communicates a proposer's per-builder preferences to a +// specific builder ahead of the bid request. A value of zero for +// MaxExecutionPayment indicates the proposer does not accept any execution +// layer payments from this builder, requiring the use of the on-chain +// trustless payment mechanism instead. +type BuilderPreferencesV1 struct { + MaxExecutionPayment phase0.Gwei +} + +// String returns a string version of the structure. +func (b *BuilderPreferencesV1) String() string { + data, err := yaml.Marshal(b) + if err != nil { + return fmt.Sprintf("ERR: %v", err) + } + + return string(data) +} diff --git a/pkg/builderapi/gloas/types/builderpreferencesv1_json.go b/pkg/builderapi/gloas/types/builderpreferencesv1_json.go new file mode 100644 index 00000000..bb28cccf --- /dev/null +++ b/pkg/builderapi/gloas/types/builderpreferencesv1_json.go @@ -0,0 +1,54 @@ +// 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 types + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/ethpandaops/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// builderPreferencesV1JSON is the spec representation of the struct. +type builderPreferencesV1JSON struct { + MaxExecutionPayment string `json:"max_execution_payment"` +} + +// MarshalJSON implements json.Marshaler. +func (b *BuilderPreferencesV1) MarshalJSON() ([]byte, error) { + return json.Marshal(&builderPreferencesV1JSON{ + MaxExecutionPayment: fmt.Sprintf("%d", b.MaxExecutionPayment), + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (b *BuilderPreferencesV1) UnmarshalJSON(input []byte) error { + var data builderPreferencesV1JSON + if err := json.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if data.MaxExecutionPayment == "" { + return errors.New("max execution payment missing") + } + maxExecutionPayment, err := strconv.ParseUint(data.MaxExecutionPayment, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid max execution payment") + } + b.MaxExecutionPayment = phase0.Gwei(maxExecutionPayment) + + return nil +} diff --git a/pkg/builderapi/gloas/types/builderpreferencesv1_ssz.go b/pkg/builderapi/gloas/types/builderpreferencesv1_ssz.go new file mode 100644 index 00000000..6dfcc229 --- /dev/null +++ b/pkg/builderapi/gloas/types/builderpreferencesv1_ssz.go @@ -0,0 +1,75 @@ +// Code generated by dynamic-ssz. DO NOT EDIT. +// Hash: b88f4577f809d21495b23ade5b57ee353a8cb5f9310f34f1e02e865c3ed09955 +// Version: v1.3.1 (https://github.com/pk910/dynamic-ssz) +package types + +import ( + "encoding/binary" + + "github.com/ethpandaops/go-eth2-client/spec/phase0" + dynssz "github.com/pk910/dynamic-ssz" + "github.com/pk910/dynamic-ssz/hasher" + "github.com/pk910/dynamic-ssz/sszutils" +) + +var _ = sszutils.ErrListTooBig + +// MarshalSSZ marshals the *BuilderPreferencesV1 to SSZ-encoded bytes. +func (t *BuilderPreferencesV1) MarshalSSZ() ([]byte, error) { + return dynssz.GetGlobalDynSsz().MarshalSSZ(t) +} + +// MarshalSSZTo marshals the *BuilderPreferencesV1 to SSZ-encoded bytes, appending to the provided buffer. +func (t *BuilderPreferencesV1) MarshalSSZTo(buf []byte) (dst []byte, err error) { + dst = buf + if t == nil { + t = new(BuilderPreferencesV1) + } + { // Static Field #0 'MaxExecutionPayment' + dst = binary.LittleEndian.AppendUint64(dst, uint64(t.MaxExecutionPayment)) + } + return dst, nil +} + +// UnmarshalSSZ unmarshals the *BuilderPreferencesV1 from SSZ-encoded bytes. +func (t *BuilderPreferencesV1) UnmarshalSSZ(buf []byte) (err error) { + buflen := len(buf) + if buflen < 8 { + return sszutils.ErrFixedFieldsEOFFn(buflen, 8) + } + { // Field #0 'MaxExecutionPayment' (static) + buf := buf[0:8] + t.MaxExecutionPayment = phase0.Gwei(binary.LittleEndian.Uint64(buf)) + } + return nil +} + +// SizeSSZ returns the SSZ encoded size of the *BuilderPreferencesV1. +func (t *BuilderPreferencesV1) SizeSSZ() (size int) { + return 8 +} + +// HashTreeRoot computes the SSZ hash tree root of the *BuilderPreferencesV1. +func (t *BuilderPreferencesV1) HashTreeRoot() (root [32]byte, err error) { + err = hasher.WithDefaultHasher(func(hh sszutils.HashWalker) (err error) { + err = t.HashTreeRootWith(hh) + if err == nil { + root, err = hh.HashRoot() + } + return + }) + return +} + +// HashTreeRootWith computes the SSZ hash tree root of the *BuilderPreferencesV1 using the given hash walker. +func (t *BuilderPreferencesV1) HashTreeRootWith(hh sszutils.HashWalker) error { + if t == nil { + t = new(BuilderPreferencesV1) + } + idx := hh.StartTree(sszutils.TreeTypeNone) + { // Field #0 'MaxExecutionPayment' + hh.PutUint64(uint64(t.MaxExecutionPayment)) + } + hh.Merkleize(idx) + return nil +} diff --git a/pkg/builderapi/gloas/types/builderpreferencesv1_yaml.go b/pkg/builderapi/gloas/types/builderpreferencesv1_yaml.go new file mode 100644 index 00000000..e134ef89 --- /dev/null +++ b/pkg/builderapi/gloas/types/builderpreferencesv1_yaml.go @@ -0,0 +1,49 @@ +// 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 types + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/pkg/errors" +) + +// MarshalYAML implements yaml.Marshaler. +func (b *BuilderPreferencesV1) MarshalYAML() ([]byte, error) { + yamlBytes, err := yaml.MarshalWithOptions(&builderPreferencesV1JSON{ + MaxExecutionPayment: fmt.Sprintf("%d", b.MaxExecutionPayment), + }, yaml.Flow(true)) + if err != nil { + return nil, err + } + + return bytes.ReplaceAll(yamlBytes, []byte(`"`), []byte(`'`)), nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (b *BuilderPreferencesV1) UnmarshalYAML(input []byte) error { + var data builderPreferencesV1JSON + if err := yaml.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "failed to unmarshal YAML") + } + marshaled, err := json.Marshal(&data) + if err != nil { + return errors.Wrap(err, "failed to marshal JSON") + } + + return b.UnmarshalJSON(marshaled) +} diff --git a/pkg/builderapi/gloas/types/generate.go b/pkg/builderapi/gloas/types/generate.go new file mode 100644 index 00000000..d01517f8 --- /dev/null +++ b/pkg/builderapi/gloas/types/generate.go @@ -0,0 +1,17 @@ +// 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 types + +//go:generate rm -f *_ssz.go +//go:generate go tool dynssz-gen -config generate.yaml diff --git a/pkg/builderapi/gloas/types/generate.yaml b/pkg/builderapi/gloas/types/generate.yaml new file mode 100644 index 00000000..1bf46482 --- /dev/null +++ b/pkg/builderapi/gloas/types/generate.yaml @@ -0,0 +1,13 @@ +package: . +legacy: true +without-dynamic-expressions: true + +types: + - name: RequestAuthV1 + output: requestauthv1_ssz.go + - name: SignedRequestAuthV1 + output: signedrequestauthv1_ssz.go + - name: BuilderPreferencesV1 + output: builderpreferencesv1_ssz.go + - name: BuilderPreferencesRequestV1 + output: builderpreferencesrequestv1_ssz.go diff --git a/pkg/builderapi/gloas/types/requestauthv1.go b/pkg/builderapi/gloas/types/requestauthv1.go new file mode 100644 index 00000000..92a786fe --- /dev/null +++ b/pkg/builderapi/gloas/types/requestauthv1.go @@ -0,0 +1,46 @@ +// 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 types holds the Gloas-fork Builder API request/response types +// (RequestAuth, SignedRequestAuth, BuilderPreferences, BuilderPreferencesRequest). +// They were vendored from go-builder-client so buildoor owns them directly, and +// use buildoor's ethpandaops/go-eth2-client phase0 types. The _ssz.go files are +// generated; see generate.go. +package types + +import ( + "fmt" + + "github.com/ethpandaops/go-eth2-client/spec/phase0" + "github.com/goccy/go-yaml" +) + +// RequestAuthV1 is used by a proposer to authenticate a bid request to a specific +// builder. The proposer signs over a generic data field (set to the builder's +// URL) and the slot to prevent other builders from replaying the request to +// learn the builder's valuation, and to prevent DOS attempts from competing +// parties. +type RequestAuthV1 struct { + Data []byte `ssz-max:"4096"` + Slot phase0.Slot +} + +// String returns a string version of the structure. +func (r *RequestAuthV1) String() string { + data, err := yaml.Marshal(r) + if err != nil { + return fmt.Sprintf("ERR: %v", err) + } + + return string(data) +} diff --git a/pkg/builderapi/gloas/types/requestauthv1_json.go b/pkg/builderapi/gloas/types/requestauthv1_json.go new file mode 100644 index 00000000..6e03ed4b --- /dev/null +++ b/pkg/builderapi/gloas/types/requestauthv1_json.go @@ -0,0 +1,73 @@ +// 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 types + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/ethpandaops/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// MaxDataSize is the maximum number of bytes in the request auth data field. +const MaxDataSize = 4096 + +// requestAuthV1JSON is the spec representation of the struct. +type requestAuthV1JSON struct { + Data string `json:"data"` + Slot string `json:"slot"` +} + +// MarshalJSON implements json.Marshaler. +func (r *RequestAuthV1) MarshalJSON() ([]byte, error) { + return json.Marshal(&requestAuthV1JSON{ + Data: fmt.Sprintf("%#x", r.Data), + Slot: fmt.Sprintf("%d", r.Slot), + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (r *RequestAuthV1) UnmarshalJSON(input []byte) error { + var data requestAuthV1JSON + if err := json.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if data.Data == "" { + return errors.New("data missing") + } + dataBytes, err := hex.DecodeString(strings.TrimPrefix(data.Data, "0x")) + if err != nil { + return errors.Wrap(err, "invalid data") + } + if len(dataBytes) > MaxDataSize { + return errors.New("data too long") + } + r.Data = dataBytes + + if data.Slot == "" { + return errors.New("slot missing") + } + slot, err := strconv.ParseUint(data.Slot, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid slot") + } + r.Slot = phase0.Slot(slot) + + return nil +} diff --git a/pkg/builderapi/gloas/types/requestauthv1_ssz.go b/pkg/builderapi/gloas/types/requestauthv1_ssz.go new file mode 100644 index 00000000..d8b8cb9b --- /dev/null +++ b/pkg/builderapi/gloas/types/requestauthv1_ssz.go @@ -0,0 +1,117 @@ +// Code generated by dynamic-ssz. DO NOT EDIT. +// Hash: 597218dd9b058462ddcddfb47bf27558f66616c2efb1e93c89d8f2eb8d6e3421 +// Version: v1.3.1 (https://github.com/pk910/dynamic-ssz) +package types + +import ( + "encoding/binary" + + "github.com/ethpandaops/go-eth2-client/spec/phase0" + dynssz "github.com/pk910/dynamic-ssz" + "github.com/pk910/dynamic-ssz/hasher" + "github.com/pk910/dynamic-ssz/sszutils" +) + +var _ = sszutils.ErrListTooBig + +// MarshalSSZ marshals the *RequestAuthV1 to SSZ-encoded bytes. +func (t *RequestAuthV1) MarshalSSZ() ([]byte, error) { + return dynssz.GetGlobalDynSsz().MarshalSSZ(t) +} + +// MarshalSSZTo marshals the *RequestAuthV1 to SSZ-encoded bytes, appending to the provided buffer. +func (t *RequestAuthV1) MarshalSSZTo(buf []byte) (dst []byte, err error) { + dst = buf + if t == nil { + t = new(RequestAuthV1) + } + dstlen := len(dst) + // Offset Field #0 'Data' + dst = append(dst, 0, 0, 0, 0) + { // Static Field #1 'Slot' + dst = binary.LittleEndian.AppendUint64(dst, uint64(t.Slot)) + } + { // Dynamic Field #0 'Data' + binary.LittleEndian.PutUint32(dst[dstlen:], uint32(len(dst)-dstlen)) + vlen := len(t.Data) + if vlen > 4096 { + return nil, sszutils.ErrorWithPath(sszutils.ErrListLengthFn(vlen, 4096), "Data") + } + dst = append(dst, t.Data[:]...) + } + return dst, nil +} + +// UnmarshalSSZ unmarshals the *RequestAuthV1 from SSZ-encoded bytes. +func (t *RequestAuthV1) UnmarshalSSZ(buf []byte) (err error) { + buflen := len(buf) + if buflen < 12 { + return sszutils.ErrFixedFieldsEOFFn(buflen, 12) + } + // Field #0 'Data' (offset) + offset0 := int(binary.LittleEndian.Uint32(buf[0:4])) + if offset0 != 12 { + return sszutils.ErrorWithPath(sszutils.ErrFirstOffsetMismatchFn(offset0, 12), "Data:o") + } + { // Field #1 'Slot' (static) + buf := buf[4:12] + t.Slot = phase0.Slot(binary.LittleEndian.Uint64(buf)) + } + { // Field #0 'Data' (dynamic) + buf := buf[offset0:] + if len(buf) > 4096 { + return sszutils.ErrorWithPath(sszutils.ErrListLengthFn(len(buf), 4096), "Data") + } + t.Data = sszutils.ExpandSlice(t.Data, len(buf)) + copy(t.Data[:], buf) + } + return nil +} + +// SizeSSZ returns the SSZ encoded size of the *RequestAuthV1. +func (t *RequestAuthV1) SizeSSZ() (size int) { + if t == nil { + t = new(RequestAuthV1) + } + // Field #0 'Data' offset (4 bytes) + // Field #1 'Slot' static (8 bytes) + size += 12 + { // Dynamic field #0 'Data' + size += len(t.Data) + } + return size +} + +// HashTreeRoot computes the SSZ hash tree root of the *RequestAuthV1. +func (t *RequestAuthV1) HashTreeRoot() (root [32]byte, err error) { + err = hasher.WithDefaultHasher(func(hh sszutils.HashWalker) (err error) { + err = t.HashTreeRootWith(hh) + if err == nil { + root, err = hh.HashRoot() + } + return + }) + return +} + +// HashTreeRootWith computes the SSZ hash tree root of the *RequestAuthV1 using the given hash walker. +func (t *RequestAuthV1) HashTreeRootWith(hh sszutils.HashWalker) error { + if t == nil { + t = new(RequestAuthV1) + } + idx := hh.StartTree(sszutils.TreeTypeNone) + { // Field #0 'Data' + vlen := uint64(len(t.Data)) + if vlen > 4096 { + return sszutils.ErrorWithPath(sszutils.ErrListLengthFn(vlen, 4096), "Data") + } + idx := hh.StartTree(sszutils.TreeTypeBinary) + hh.AppendBytes32(t.Data[:]) + hh.MerkleizeWithMixin(idx, vlen, sszutils.CalculateLimit(4096, vlen, 1)) + } + { // Field #1 'Slot' + hh.PutUint64(uint64(t.Slot)) + } + hh.Merkleize(idx) + return nil +} diff --git a/pkg/builderapi/gloas/types/requestauthv1_yaml.go b/pkg/builderapi/gloas/types/requestauthv1_yaml.go new file mode 100644 index 00000000..16999c48 --- /dev/null +++ b/pkg/builderapi/gloas/types/requestauthv1_yaml.go @@ -0,0 +1,50 @@ +// 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 types + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/pkg/errors" +) + +// MarshalYAML implements yaml.Marshaler. +func (r *RequestAuthV1) MarshalYAML() ([]byte, error) { + yamlBytes, err := yaml.MarshalWithOptions(&requestAuthV1JSON{ + Data: fmt.Sprintf("%#x", r.Data), + Slot: fmt.Sprintf("%d", r.Slot), + }, yaml.Flow(true)) + if err != nil { + return nil, err + } + + return bytes.ReplaceAll(yamlBytes, []byte(`"`), []byte(`'`)), nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (r *RequestAuthV1) UnmarshalYAML(input []byte) error { + var data requestAuthV1JSON + if err := yaml.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "failed to unmarshal YAML") + } + marshaled, err := json.Marshal(&data) + if err != nil { + return errors.Wrap(err, "failed to marshal JSON") + } + + return r.UnmarshalJSON(marshaled) +} diff --git a/pkg/builderapi/gloas/types/signedrequestauthv1.go b/pkg/builderapi/gloas/types/signedrequestauthv1.go new file mode 100644 index 00000000..8ff20dba --- /dev/null +++ b/pkg/builderapi/gloas/types/signedrequestauthv1.go @@ -0,0 +1,40 @@ +// 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 types + +import ( + "fmt" + + "github.com/ethpandaops/go-eth2-client/spec/phase0" + "github.com/goccy/go-yaml" +) + +// SignedRequestAuthV1 wraps a RequestAuthV1 with the proposer's signature over the +// hash tree root of the message. It is sent in the body of getExecutionPayloadBid +// and submitBuilderPreferences requests so that builders can authenticate the +// requesting validator. +type SignedRequestAuthV1 struct { + Message *RequestAuthV1 + Signature phase0.BLSSignature `ssz-size:"96"` +} + +// String returns a string version of the structure. +func (s *SignedRequestAuthV1) String() string { + data, err := yaml.Marshal(s) + if err != nil { + return fmt.Sprintf("ERR: %v", err) + } + + return string(data) +} diff --git a/pkg/builderapi/gloas/types/signedrequestauthv1_json.go b/pkg/builderapi/gloas/types/signedrequestauthv1_json.go new file mode 100644 index 00000000..13defc79 --- /dev/null +++ b/pkg/builderapi/gloas/types/signedrequestauthv1_json.go @@ -0,0 +1,65 @@ +// 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 types + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/ethpandaops/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// signedRequestAuthV1JSON is the spec representation of the struct. +type signedRequestAuthV1JSON struct { + Message *RequestAuthV1 `json:"message"` + Signature string `json:"signature"` +} + +// MarshalJSON implements json.Marshaler. +func (s *SignedRequestAuthV1) MarshalJSON() ([]byte, error) { + return json.Marshal(&signedRequestAuthV1JSON{ + Message: s.Message, + Signature: fmt.Sprintf("%#x", s.Signature), + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (s *SignedRequestAuthV1) UnmarshalJSON(input []byte) error { + var data signedRequestAuthV1JSON + if err := json.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if data.Message == nil { + return errors.New("message missing") + } + s.Message = data.Message + + if data.Signature == "" { + return errors.New("signature missing") + } + signature, err := hex.DecodeString(strings.TrimPrefix(data.Signature, "0x")) + if err != nil { + return errors.Wrap(err, "invalid signature") + } + if len(signature) != phase0.SignatureLength { + return errors.New("incorrect length for signature") + } + copy(s.Signature[:], signature) + + return nil +} diff --git a/pkg/builderapi/gloas/types/signedrequestauthv1_ssz.go b/pkg/builderapi/gloas/types/signedrequestauthv1_ssz.go new file mode 100644 index 00000000..94ac7a5e --- /dev/null +++ b/pkg/builderapi/gloas/types/signedrequestauthv1_ssz.go @@ -0,0 +1,119 @@ +// Code generated by dynamic-ssz. DO NOT EDIT. +// Hash: 90a5f069bf853b5db64738807deada0a571106725fdeb43e4414702f018bc23f +// Version: v1.3.1 (https://github.com/pk910/dynamic-ssz) +package types + +import ( + "encoding/binary" + + dynssz "github.com/pk910/dynamic-ssz" + "github.com/pk910/dynamic-ssz/hasher" + "github.com/pk910/dynamic-ssz/sszutils" +) + +var _ = sszutils.ErrListTooBig + +// MarshalSSZ marshals the *SignedRequestAuthV1 to SSZ-encoded bytes. +func (t *SignedRequestAuthV1) MarshalSSZ() ([]byte, error) { + return dynssz.GetGlobalDynSsz().MarshalSSZ(t) +} + +// MarshalSSZTo marshals the *SignedRequestAuthV1 to SSZ-encoded bytes, appending to the provided buffer. +func (t *SignedRequestAuthV1) MarshalSSZTo(buf []byte) (dst []byte, err error) { + dst = buf + if t == nil { + t = new(SignedRequestAuthV1) + } + dstlen := len(dst) + // Offset Field #0 'Message' + dst = append(dst, 0, 0, 0, 0) + { // Static Field #1 'Signature' + dst = append(dst, t.Signature[:96]...) + } + { // Dynamic Field #0 'Message' + binary.LittleEndian.PutUint32(dst[dstlen:], uint32(len(dst)-dstlen)) + t := t.Message + if t == nil { + t = new(RequestAuthV1) + } + if dst, err = t.MarshalSSZTo(dst); err != nil { + return nil, sszutils.ErrorWithPath(err, "Message") + } + } + return dst, nil +} + +// UnmarshalSSZ unmarshals the *SignedRequestAuthV1 from SSZ-encoded bytes. +func (t *SignedRequestAuthV1) UnmarshalSSZ(buf []byte) (err error) { + buflen := len(buf) + if buflen < 100 { + return sszutils.ErrFixedFieldsEOFFn(buflen, 100) + } + // Field #0 'Message' (offset) + offset0 := int(binary.LittleEndian.Uint32(buf[0:4])) + if offset0 != 100 { + return sszutils.ErrorWithPath(sszutils.ErrFirstOffsetMismatchFn(offset0, 100), "Message:o") + } + { // Field #1 'Signature' (static) + buf := buf[4:100] + copy(t.Signature[:], buf) + } + { // Field #0 'Message' (dynamic) + buf := buf[offset0:] + if t.Message == nil { + t.Message = new(RequestAuthV1) + } + if err = t.Message.UnmarshalSSZ(buf); err != nil { + return sszutils.ErrorWithPath(err, "Message") + } + } + return nil +} + +// SizeSSZ returns the SSZ encoded size of the *SignedRequestAuthV1. +func (t *SignedRequestAuthV1) SizeSSZ() (size int) { + if t == nil { + t = new(SignedRequestAuthV1) + } + // Field #0 'Message' offset (4 bytes) + // Field #1 'Signature' static (96 bytes) + size += 100 + { // Dynamic field #0 'Message' + size += t.Message.SizeSSZ() + } + return size +} + +// HashTreeRoot computes the SSZ hash tree root of the *SignedRequestAuthV1. +func (t *SignedRequestAuthV1) HashTreeRoot() (root [32]byte, err error) { + err = hasher.WithDefaultHasher(func(hh sszutils.HashWalker) (err error) { + err = t.HashTreeRootWith(hh) + if err == nil { + root, err = hh.HashRoot() + } + return + }) + return +} + +// HashTreeRootWith computes the SSZ hash tree root of the *SignedRequestAuthV1 using the given hash walker. +func (t *SignedRequestAuthV1) HashTreeRootWith(hh sszutils.HashWalker) error { + if t == nil { + t = new(SignedRequestAuthV1) + } + idx := hh.StartTree(sszutils.TreeTypeNone) + { // Field #0 'Message' + t := t.Message + if t == nil { + t = new(RequestAuthV1) + } + if err := t.HashTreeRootWith(hh); err != nil { + return sszutils.ErrorWithPath(err, "Message") + } + } + { // Field #1 'Signature' + hh.PutBytes(t.Signature[:96]) + } + hh.Merkleize(idx) + return nil +} diff --git a/pkg/builderapi/gloas/types/signedrequestauthv1_yaml.go b/pkg/builderapi/gloas/types/signedrequestauthv1_yaml.go new file mode 100644 index 00000000..5cd4cdf8 --- /dev/null +++ b/pkg/builderapi/gloas/types/signedrequestauthv1_yaml.go @@ -0,0 +1,50 @@ +// 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 types + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/pkg/errors" +) + +// MarshalYAML implements yaml.Marshaler. +func (s *SignedRequestAuthV1) MarshalYAML() ([]byte, error) { + yamlBytes, err := yaml.MarshalWithOptions(&signedRequestAuthV1JSON{ + Message: s.Message, + Signature: fmt.Sprintf("%#x", s.Signature), + }, yaml.Flow(true)) + if err != nil { + return nil, err + } + + return bytes.ReplaceAll(yamlBytes, []byte(`"`), []byte(`'`)), nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (s *SignedRequestAuthV1) UnmarshalYAML(input []byte) error { + var data signedRequestAuthV1JSON + if err := yaml.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "failed to unmarshal YAML") + } + marshaled, err := json.Marshal(&data) + if err != nil { + return errors.Wrap(err, "failed to marshal JSON") + } + + return s.UnmarshalJSON(marshaled) +}