From a7df5f8dc53fa9aace7c6099de2e18540d1ab92c Mon Sep 17 00:00:00 2001 From: dmarzzz Date: Mon, 9 Jun 2025 14:03:58 +0200 Subject: [PATCH 1/6] vibe code the minimal spec --- common/dynssz.go | 165 ++++++++++++++++++++++++ common/dynssz_test.go | 272 ++++++++++++++++++++++++++++++++++++++++ common/types_spec.go | 77 ++++++++++-- go.mod | 2 + go.sum | 4 + services/api/service.go | 34 ++++- 6 files changed, 546 insertions(+), 8 deletions(-) create mode 100644 common/dynssz.go create mode 100644 common/dynssz_test.go diff --git a/common/dynssz.go b/common/dynssz.go new file mode 100644 index 00000000..f8d406a3 --- /dev/null +++ b/common/dynssz.go @@ -0,0 +1,165 @@ +package common + +import ( + "fmt" + "sync" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/flashbots/go-boost-utils/bls" + "github.com/flashbots/go-boost-utils/ssz" + dynamicssz "github.com/pk910/dynamic-ssz" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +var ( + ErrSSZManagerNotInitialized = errors.New("SSZ manager not initialized") + ErrInvalidSpecConfig = errors.New("invalid spec config") +) + +// HashRootObject represents an object that can compute its hash tree root +type HashRootObject interface { + HashTreeRoot() ([32]byte, error) +} + +// SSZManager manages dynamic SSZ operations with a single initialized instance +type SSZManager struct { + mu sync.RWMutex + dynSSZ *dynamicssz.DynSsz + config map[string]interface{} + log *logrus.Entry +} + +// NewSSZManager creates a new SSZ manager +func NewSSZManager(log *logrus.Entry) *SSZManager { + return &SSZManager{ + log: log, + } +} + +// Initialize sets up the dynamic SSZ library with the provided beacon config +func (m *SSZManager) Initialize(beaconConfig map[string]interface{}) error { + m.mu.Lock() + defer m.mu.Unlock() + + if beaconConfig == nil { + return ErrInvalidSpecConfig + } + + m.config = beaconConfig + + // Create dynamic SSZ instance with config - it will handle minimal vs mainnet detection internally + m.dynSSZ = dynamicssz.NewDynSsz(beaconConfig) + + m.log.WithFields(logrus.Fields{ + "configKeys": len(beaconConfig), + }).Info("SSZ manager initialized") + + return nil +} + +// IsInitialized returns whether the SSZ manager has been initialized +func (m *SSZManager) IsInitialized() bool { + m.mu.RLock() + defer m.mu.RUnlock() + return m.dynSSZ != nil +} + +// SignMessage signs a message using the appropriate SSZ encoding +// For now, we use standard SSZ for signing since dynamic SSZ doesn't provide hash tree root functionality +func (m *SSZManager) SignMessage(obj HashRootObject, domain phase0.Domain, secretKey *bls.SecretKey) (phase0.BLSSignature, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + if m.dynSSZ == nil { + return phase0.BLSSignature{}, ErrSSZManagerNotInitialized + } + + // For now, always use standard SSZ for signing until we have hash tree root support in dynamic SSZ + return ssz.SignMessage(obj, domain, secretKey) +} + +// MarshalSSZ marshals an object using the appropriate SSZ encoding +func (m *SSZManager) MarshalSSZ(obj interface{}) ([]byte, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + if m.dynSSZ == nil { + return nil, ErrSSZManagerNotInitialized + } + + // Try dynamic SSZ first - it will automatically fall back to standard SSZ for mainnet/testnet + data, err := m.dynSSZ.MarshalSSZ(obj) + if err == nil { + return data, nil + } + + // If dynamic SSZ fails, fall back to standard SSZ + if sszObj, ok := obj.(interface{ MarshalSSZ() ([]byte, error) }); ok { + return sszObj.MarshalSSZ() + } + + return nil, fmt.Errorf("object does not support SSZ marshaling") +} + +// UnmarshalSSZ unmarshals data using the appropriate SSZ encoding +func (m *SSZManager) UnmarshalSSZ(obj interface{}, data []byte) error { + m.mu.RLock() + defer m.mu.RUnlock() + + if m.dynSSZ == nil { + return ErrSSZManagerNotInitialized + } + + // Try dynamic SSZ first - it will automatically fall back to standard SSZ for mainnet/testnet + err := m.dynSSZ.UnmarshalSSZ(obj, data) + if err == nil { + return nil + } + + // If dynamic SSZ fails, fall back to standard SSZ + if sszObj, ok := obj.(interface{ UnmarshalSSZ([]byte) error }); ok { + return sszObj.UnmarshalSSZ(data) + } + + return fmt.Errorf("object does not support SSZ unmarshaling") +} + +// HashTreeRoot computes hash tree root using the appropriate SSZ encoding +// For now, always uses standard SSZ since dynamic SSZ doesn't support hash tree root +func (m *SSZManager) HashTreeRoot(obj interface{}) (phase0.Root, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + if m.dynSSZ == nil { + return phase0.Root{}, ErrSSZManagerNotInitialized + } + + // For now, always use standard SSZ for hash tree root + if sszObj, ok := obj.(interface{ HashTreeRoot() ([32]byte, error) }); ok { + root, err := sszObj.HashTreeRoot() + if err != nil { + return phase0.Root{}, err + } + return phase0.Root(root), nil + } + + return phase0.Root{}, fmt.Errorf("object does not support HashTreeRoot") +} + +// GetConfig returns a copy of the current beacon config +func (m *SSZManager) GetConfig() map[string]interface{} { + m.mu.RLock() + defer m.mu.RUnlock() + + if m.config == nil { + return nil + } + + // Return a deep copy to prevent modification + configCopy := make(map[string]interface{}) + for k, v := range m.config { + configCopy[k] = v + } + return configCopy +} diff --git a/common/dynssz_test.go b/common/dynssz_test.go new file mode 100644 index 00000000..f85adde2 --- /dev/null +++ b/common/dynssz_test.go @@ -0,0 +1,272 @@ +package common + +import ( + "sync" + "testing" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/flashbots/go-boost-utils/bls" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +// TestStruct implements HashRootObject for testing +type TestStruct struct { + Value uint64 +} + +// HashTreeRoot implements the HashRootObject interface +func (t *TestStruct) HashTreeRoot() ([32]byte, error) { + // Simple mock implementation for testing + var root [32]byte + copy(root[:], []byte("test_root_hash_for_testing_123")) + return root, nil +} + +func TestSSZManager_Initialize(t *testing.T) { + log := logrus.WithField("test", "SSZManager") + + t.Run("Initialize with minimal spec config", func(t *testing.T) { + manager := NewSSZManager(log) + + minimalConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "8", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(minimalConfig) + require.NoError(t, err) + require.True(t, manager.IsInitialized()) + + config := manager.GetConfig() + require.Equal(t, minimalConfig, config) + }) + + t.Run("Initialize with mainnet config", func(t *testing.T) { + manager := NewSSZManager(log) + + mainnetConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "32", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(mainnetConfig) + require.NoError(t, err) + require.True(t, manager.IsInitialized()) + + config := manager.GetConfig() + require.Equal(t, mainnetConfig, config) + }) + + t.Run("Initialize with nil config", func(t *testing.T) { + manager := NewSSZManager(log) + + err := manager.Initialize(nil) + require.Error(t, err) + require.Equal(t, ErrInvalidSpecConfig, err) + require.False(t, manager.IsInitialized()) + }) +} + +func TestSSZManager_SignMessage(t *testing.T) { + log := logrus.WithField("test", "SSZManager") + + // Generate test key + sk, _, err := bls.GenerateNewKeypair() + require.NoError(t, err) + + // Create test domain + domain := phase0.Domain{1, 2, 3, 4} + + // Create test struct instance + testStruct := &TestStruct{Value: 123} + + t.Run("Sign with uninitialized manager", func(t *testing.T) { + manager := NewSSZManager(log) + + // This should return an error since manager is not initialized + _, err := manager.SignMessage(testStruct, domain, sk) + require.Error(t, err) + require.Equal(t, ErrSSZManagerNotInitialized, err) + }) + + t.Run("Sign with initialized manager", func(t *testing.T) { + manager := NewSSZManager(log) + + config := map[string]interface{}{ + "SLOTS_PER_EPOCH": "32", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(config) + require.NoError(t, err) + + // This should work without error since we use standard SSZ for signing + _, err = manager.SignMessage(testStruct, domain, sk) + require.NoError(t, err) + }) +} + +func TestSSZManager_MarshalUnmarshal(t *testing.T) { + log := logrus.WithField("test", "SSZManager") + + t.Run("MarshalSSZ and UnmarshalSSZ with minimal config", func(t *testing.T) { + manager := NewSSZManager(log) + + minimalConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "8", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(minimalConfig) + require.NoError(t, err) + + // Test marshal/unmarshal functionality + type TestData struct { + Value uint64 + } + + original := &TestData{Value: 12345} + + // Marshal should not error (even if it falls back to standard SSZ) + data, err := manager.MarshalSSZ(original) + require.NoError(t, err) + require.NotEmpty(t, data) + + // Unmarshal should work + unmarshaled := &TestData{} + err = manager.UnmarshalSSZ(unmarshaled, data) + require.NoError(t, err) + require.Equal(t, original.Value, unmarshaled.Value) + }) + + t.Run("MarshalSSZ and UnmarshalSSZ with mainnet config", func(t *testing.T) { + manager := NewSSZManager(log) + + mainnetConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "32", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(mainnetConfig) + require.NoError(t, err) + + // Test marshal/unmarshal functionality + type TestData struct { + Value uint64 + } + + original := &TestData{Value: 67890} + + // Marshal should work + data, err := manager.MarshalSSZ(original) + require.NoError(t, err) + require.NotEmpty(t, data) + + // Unmarshal should work + unmarshaled := &TestData{} + err = manager.UnmarshalSSZ(unmarshaled, data) + require.NoError(t, err) + require.Equal(t, original.Value, unmarshaled.Value) + }) +} + +func TestSSZManager_HashTreeRoot(t *testing.T) { + log := logrus.WithField("test", "SSZManager") + + t.Run("HashTreeRoot with minimal config", func(t *testing.T) { + manager := NewSSZManager(log) + + minimalConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "8", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(minimalConfig) + require.NoError(t, err) + + testData := &TestStruct{Value: 12345} + + // HashTreeRoot should work (using standard SSZ) + root, err := manager.HashTreeRoot(testData) + require.NoError(t, err) + require.NotEqual(t, phase0.Root{}, root) + }) + + t.Run("HashTreeRoot with mainnet config", func(t *testing.T) { + manager := NewSSZManager(log) + + mainnetConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "32", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(mainnetConfig) + require.NoError(t, err) + + testData := &TestStruct{Value: 67890} + + // HashTreeRoot should work + root, err := manager.HashTreeRoot(testData) + require.NoError(t, err) + require.NotEqual(t, phase0.Root{}, root) + }) +} + +func TestSSZManager_ConfigCopy(t *testing.T) { + log := logrus.WithField("test", "SSZManager") + manager := NewSSZManager(log) + + originalConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "8", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(originalConfig) + require.NoError(t, err) + + // Get config copy + configCopy := manager.GetConfig() + require.Equal(t, originalConfig, configCopy) + + // Modify the copy - should not affect original + configCopy["SLOTS_PER_EPOCH"] = "16" + + // Original config should be unchanged + configFromManager := manager.GetConfig() + require.Equal(t, "8", configFromManager["SLOTS_PER_EPOCH"]) +} + +func TestSSZManager_ConcurrentAccess(t *testing.T) { + log := logrus.WithField("test", "SSZManager") + manager := NewSSZManager(log) + + config := map[string]interface{}{ + "SLOTS_PER_EPOCH": "8", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(config) + require.NoError(t, err) + + // Test concurrent access to the manager + var wg sync.WaitGroup + numGoroutines := 10 + + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + // Test various operations concurrently + _ = manager.IsInitialized() + _ = manager.GetConfig() + + testStruct := &TestStruct{Value: 123} + _, _ = manager.MarshalSSZ(testStruct) + _, _ = manager.HashTreeRoot(testStruct) + }() + } + + wg.Wait() +} diff --git a/common/types_spec.go b/common/types_spec.go index 5b0b5fe9..9f3c319d 100644 --- a/common/types_spec.go +++ b/common/types_spec.go @@ -42,6 +42,10 @@ type HTTPErrorResp struct { var NilResponse = struct{}{} func BuildGetHeaderResponse(payload *VersionedSubmitBlockRequest, sk *bls.SecretKey, pubkey *phase0.BLSPubKey, domain phase0.Domain) (*builderSpec.VersionedSignedBuilderBid, error) { + return BuildGetHeaderResponseWithSSZ(payload, sk, pubkey, domain, nil) +} + +func BuildGetHeaderResponseWithSSZ(payload *VersionedSubmitBlockRequest, sk *bls.SecretKey, pubkey *phase0.BLSPubKey, domain phase0.Domain, sszManager *SSZManager) (*builderSpec.VersionedSignedBuilderBid, error) { if payload == nil { return nil, ErrMissingRequest } @@ -58,7 +62,7 @@ func BuildGetHeaderResponse(payload *VersionedSubmitBlockRequest, sk *bls.Secret if err != nil { return nil, err } - signedBuilderBid, err := BuilderBlockRequestToSignedBuilderBid(payload, header, sk, pubkey, domain) + signedBuilderBid, err := BuilderBlockRequestToSignedBuilderBid(payload, header, sk, pubkey, domain, sszManager) if err != nil { return nil, err } @@ -72,7 +76,7 @@ func BuildGetHeaderResponse(payload *VersionedSubmitBlockRequest, sk *bls.Secret if err != nil { return nil, err } - signedBuilderBid, err := BuilderBlockRequestToSignedBuilderBid(payload, header, sk, pubkey, domain) + signedBuilderBid, err := BuilderBlockRequestToSignedBuilderBid(payload, header, sk, pubkey, domain, sszManager) if err != nil { return nil, err } @@ -86,7 +90,7 @@ func BuildGetHeaderResponse(payload *VersionedSubmitBlockRequest, sk *bls.Secret if err != nil { return nil, err } - signedBuilderBid, err := BuilderBlockRequestToSignedBuilderBid(payload, header, sk, pubkey, domain) + signedBuilderBid, err := BuilderBlockRequestToSignedBuilderBid(payload, header, sk, pubkey, domain, sszManager) if err != nil { return nil, err } @@ -130,7 +134,7 @@ func BuildGetPayloadResponse(payload *VersionedSubmitBlockRequest) (*builderApi. return nil, ErrEmptyPayload } -func BuilderBlockRequestToSignedBuilderBid(payload *VersionedSubmitBlockRequest, header *builderApi.VersionedExecutionPayloadHeader, sk *bls.SecretKey, pubkey *phase0.BLSPubKey, domain phase0.Domain) (*builderSpec.VersionedSignedBuilderBid, error) { +func BuilderBlockRequestToSignedBuilderBid(payload *VersionedSubmitBlockRequest, header *builderApi.VersionedExecutionPayloadHeader, sk *bls.SecretKey, pubkey *phase0.BLSPubKey, domain phase0.Domain, sszManager *SSZManager) (*builderSpec.VersionedSignedBuilderBid, error) { value, err := payload.Value() if err != nil { return nil, err @@ -144,7 +148,12 @@ func BuilderBlockRequestToSignedBuilderBid(payload *VersionedSubmitBlockRequest, Pubkey: *pubkey, } - sig, err := ssz.SignMessage(&builderBid, domain, sk) + var sig phase0.BLSSignature + if sszManager != nil && sszManager.IsInitialized() { + sig, err = sszManager.SignMessage(&builderBid, domain, sk) + } else { + sig, err = ssz.SignMessage(&builderBid, domain, sk) + } if err != nil { return nil, err } @@ -164,7 +173,12 @@ func BuilderBlockRequestToSignedBuilderBid(payload *VersionedSubmitBlockRequest, Pubkey: *pubkey, } - sig, err := ssz.SignMessage(&builderBid, domain, sk) + var sig phase0.BLSSignature + if sszManager != nil && sszManager.IsInitialized() { + sig, err = sszManager.SignMessage(&builderBid, domain, sk) + } else { + sig, err = ssz.SignMessage(&builderBid, domain, sk) + } if err != nil { return nil, err } @@ -185,7 +199,12 @@ func BuilderBlockRequestToSignedBuilderBid(payload *VersionedSubmitBlockRequest, Pubkey: *pubkey, } - sig, err := ssz.SignMessage(&builderBid, domain, sk) + var sig phase0.BLSSignature + if sszManager != nil && sszManager.IsInitialized() { + sig, err = sszManager.SignMessage(&builderBid, domain, sk) + } else { + sig, err = ssz.SignMessage(&builderBid, domain, sk) + } if err != nil { return nil, err } @@ -402,6 +421,13 @@ func (r *VersionedSubmitBlockRequest) MarshalSSZ() ([]byte, error) { } } +func (r *VersionedSubmitBlockRequest) MarshalSSZWithManager(sszManager *SSZManager) ([]byte, error) { + if sszManager != nil && sszManager.IsInitialized() { + return sszManager.MarshalSSZ(r) + } + return r.MarshalSSZ() +} + func (r *VersionedSubmitBlockRequest) UnmarshalSSZ(input []byte) error { var err error electraRequest := new(builderApiElectra.SubmitBlockRequest) @@ -425,6 +451,13 @@ func (r *VersionedSubmitBlockRequest) UnmarshalSSZ(input []byte) error { return errors.Wrap(err, "failed to unmarshal SubmitBlockRequest SSZ") } +func (r *VersionedSubmitBlockRequest) UnmarshalSSZWithManager(input []byte, sszManager *SSZManager) error { + if sszManager != nil && sszManager.IsInitialized() { + return sszManager.UnmarshalSSZ(r, input) + } + return r.UnmarshalSSZ(input) +} + func (r *VersionedSubmitBlockRequest) HashTreeRoot() (phase0.Root, error) { switch r.Version { case spec.DataVersionCapella: @@ -440,6 +473,36 @@ func (r *VersionedSubmitBlockRequest) HashTreeRoot() (phase0.Root, error) { } } +func (r *VersionedSubmitBlockRequest) HashTreeRootWithManager(sszManager *SSZManager) (phase0.Root, error) { + if sszManager != nil && sszManager.IsInitialized() { + return sszManager.HashTreeRoot(r) + } + + // Use standard SSZ for mainnet/testnet + switch r.Version { //nolint:exhaustive + case spec.DataVersionCapella: + root, err := r.Capella.HashTreeRoot() + if err != nil { + return phase0.Root{}, err + } + return phase0.Root(root), nil + case spec.DataVersionDeneb: + root, err := r.Deneb.HashTreeRoot() + if err != nil { + return phase0.Root{}, err + } + return phase0.Root(root), nil + case spec.DataVersionElectra: + root, err := r.Electra.HashTreeRoot() + if err != nil { + return phase0.Root{}, err + } + return phase0.Root(root), nil + default: + return phase0.Root{}, errors.Wrap(ErrInvalidVersion, fmt.Sprintf("%s is not supported", r.Version)) + } +} + func (r *VersionedSubmitBlockRequest) MarshalJSON() ([]byte, error) { switch r.Version { //nolint:exhaustive case spec.DataVersionCapella: diff --git a/go.mod b/go.mod index 0c41273d..4e1c04bb 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/holiman/uint256 v1.3.2 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 + github.com/pk910/dynamic-ssz v0.0.4 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.21.0 github.com/r3labs/sse/v2 v2.10.0 @@ -70,6 +71,7 @@ require ( go.opentelemetry.io/otel/trace v1.34.0 // indirect golang.org/x/sync v0.13.0 // indirect google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/go.sum b/go.sum index ad108e45..41848390 100644 --- a/go.sum +++ b/go.sum @@ -187,6 +187,8 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pk910/dynamic-ssz v0.0.4 h1:DT29+1055tCEPCaR4V/ez+MOKW7BzBsmjyFvBRqx0ME= +github.com/pk910/dynamic-ssz v0.0.4/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c= 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= @@ -322,6 +324,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/services/api/service.go b/services/api/service.go index 00c6d458..d381c08b 100644 --- a/services/api/service.go +++ b/services/api/service.go @@ -199,6 +199,9 @@ type RelayAPI struct { memcached *datastore.Memcached db database.IDatabaseService + // SSZ manager for dynamic SSZ operations + sszManager *common.SSZManager + headSlot uberatomic.Uint64 genesisInfo *beaconclient.GetGenesisResponse capellaEpoch int64 @@ -304,6 +307,9 @@ func NewRelayAPI(opts RelayAPIOpts) (api *RelayAPI, err error) { memcached: opts.Memcached, db: opts.DB, + // Initialize SSZ manager + sszManager: common.NewSSZManager(opts.Log.WithField("component", "sszManager")), + payloadAttributes: make(map[string]payloadAttributesHelper), proposerDutiesResponse: &[]byte{}, @@ -426,6 +432,32 @@ func (api *RelayAPI) StartServer() (err error) { } log.Infof("genesis info: %d", api.genesisInfo.Data.GenesisTime) + // Get beacon spec configuration and initialize SSZ manager + beaconSpec, err := api.beaconClient.GetSpec() + if err != nil { + log.WithError(err).Warn("failed to get beacon spec config, SSZ manager will use standard SSZ") + } else { + // Convert spec to map[string]interface{} for dynamic SSZ + specConfig := make(map[string]interface{}) + + // Add relevant fields from beacon spec + if beaconSpec.SecondsPerSlot != 0 { + specConfig["SECONDS_PER_SLOT"] = fmt.Sprintf("%d", beaconSpec.SecondsPerSlot) + } + + // Add other configuration fields as needed - you can extend this + // based on what fields are available in GetSpecResponse + + // Try to initialize the SSZ manager with the beacon config + if err := api.sszManager.Initialize(specConfig); err != nil { + log.WithError(err).Warn("failed to initialize SSZ manager with beacon config, will use standard SSZ") + } else { + log.WithFields(logrus.Fields{ + "configKeys": len(specConfig), + }).Info("SSZ manager initialized with beacon config") + } + } + // Get and prepare fork schedule forkSchedule, err := api.beaconClient.GetForkSchedule() if err != nil { @@ -1994,7 +2026,7 @@ type redisUpdateBidOpts struct { func (api *RelayAPI) updateRedisBid(opts redisUpdateBidOpts) (*datastore.SaveBidAndUpdateTopBidResponse, *builderApi.VersionedSubmitBlindedBlockResponse, bool) { // Prepare the response data - getHeaderResponse, err := common.BuildGetHeaderResponse(opts.payload, api.blsSk, api.publicKey, api.opts.EthNetDetails.DomainBuilder) + getHeaderResponse, err := common.BuildGetHeaderResponseWithSSZ(opts.payload, api.blsSk, api.publicKey, api.opts.EthNetDetails.DomainBuilder, api.sszManager) if err != nil { opts.log.WithError(err).Error("could not sign builder bid") api.RespondError(opts.w, http.StatusBadRequest, err.Error()) From 59274c8c2b964c751cc2744efbcf8afbf59b672d Mon Sep 17 00:00:00 2001 From: dmarzzz Date: Mon, 9 Jun 2025 14:17:50 +0200 Subject: [PATCH 2/6] reformat and clean up --- common/dynssz.go | 165 ------------------------------------------ common/dynssz_test.go | 35 --------- common/types_spec.go | 151 +++++++++++++++++++++++++++++++++++++- 3 files changed, 148 insertions(+), 203 deletions(-) delete mode 100644 common/dynssz.go diff --git a/common/dynssz.go b/common/dynssz.go deleted file mode 100644 index f8d406a3..00000000 --- a/common/dynssz.go +++ /dev/null @@ -1,165 +0,0 @@ -package common - -import ( - "fmt" - "sync" - - "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/flashbots/go-boost-utils/bls" - "github.com/flashbots/go-boost-utils/ssz" - dynamicssz "github.com/pk910/dynamic-ssz" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -var ( - ErrSSZManagerNotInitialized = errors.New("SSZ manager not initialized") - ErrInvalidSpecConfig = errors.New("invalid spec config") -) - -// HashRootObject represents an object that can compute its hash tree root -type HashRootObject interface { - HashTreeRoot() ([32]byte, error) -} - -// SSZManager manages dynamic SSZ operations with a single initialized instance -type SSZManager struct { - mu sync.RWMutex - dynSSZ *dynamicssz.DynSsz - config map[string]interface{} - log *logrus.Entry -} - -// NewSSZManager creates a new SSZ manager -func NewSSZManager(log *logrus.Entry) *SSZManager { - return &SSZManager{ - log: log, - } -} - -// Initialize sets up the dynamic SSZ library with the provided beacon config -func (m *SSZManager) Initialize(beaconConfig map[string]interface{}) error { - m.mu.Lock() - defer m.mu.Unlock() - - if beaconConfig == nil { - return ErrInvalidSpecConfig - } - - m.config = beaconConfig - - // Create dynamic SSZ instance with config - it will handle minimal vs mainnet detection internally - m.dynSSZ = dynamicssz.NewDynSsz(beaconConfig) - - m.log.WithFields(logrus.Fields{ - "configKeys": len(beaconConfig), - }).Info("SSZ manager initialized") - - return nil -} - -// IsInitialized returns whether the SSZ manager has been initialized -func (m *SSZManager) IsInitialized() bool { - m.mu.RLock() - defer m.mu.RUnlock() - return m.dynSSZ != nil -} - -// SignMessage signs a message using the appropriate SSZ encoding -// For now, we use standard SSZ for signing since dynamic SSZ doesn't provide hash tree root functionality -func (m *SSZManager) SignMessage(obj HashRootObject, domain phase0.Domain, secretKey *bls.SecretKey) (phase0.BLSSignature, error) { - m.mu.RLock() - defer m.mu.RUnlock() - - if m.dynSSZ == nil { - return phase0.BLSSignature{}, ErrSSZManagerNotInitialized - } - - // For now, always use standard SSZ for signing until we have hash tree root support in dynamic SSZ - return ssz.SignMessage(obj, domain, secretKey) -} - -// MarshalSSZ marshals an object using the appropriate SSZ encoding -func (m *SSZManager) MarshalSSZ(obj interface{}) ([]byte, error) { - m.mu.RLock() - defer m.mu.RUnlock() - - if m.dynSSZ == nil { - return nil, ErrSSZManagerNotInitialized - } - - // Try dynamic SSZ first - it will automatically fall back to standard SSZ for mainnet/testnet - data, err := m.dynSSZ.MarshalSSZ(obj) - if err == nil { - return data, nil - } - - // If dynamic SSZ fails, fall back to standard SSZ - if sszObj, ok := obj.(interface{ MarshalSSZ() ([]byte, error) }); ok { - return sszObj.MarshalSSZ() - } - - return nil, fmt.Errorf("object does not support SSZ marshaling") -} - -// UnmarshalSSZ unmarshals data using the appropriate SSZ encoding -func (m *SSZManager) UnmarshalSSZ(obj interface{}, data []byte) error { - m.mu.RLock() - defer m.mu.RUnlock() - - if m.dynSSZ == nil { - return ErrSSZManagerNotInitialized - } - - // Try dynamic SSZ first - it will automatically fall back to standard SSZ for mainnet/testnet - err := m.dynSSZ.UnmarshalSSZ(obj, data) - if err == nil { - return nil - } - - // If dynamic SSZ fails, fall back to standard SSZ - if sszObj, ok := obj.(interface{ UnmarshalSSZ([]byte) error }); ok { - return sszObj.UnmarshalSSZ(data) - } - - return fmt.Errorf("object does not support SSZ unmarshaling") -} - -// HashTreeRoot computes hash tree root using the appropriate SSZ encoding -// For now, always uses standard SSZ since dynamic SSZ doesn't support hash tree root -func (m *SSZManager) HashTreeRoot(obj interface{}) (phase0.Root, error) { - m.mu.RLock() - defer m.mu.RUnlock() - - if m.dynSSZ == nil { - return phase0.Root{}, ErrSSZManagerNotInitialized - } - - // For now, always use standard SSZ for hash tree root - if sszObj, ok := obj.(interface{ HashTreeRoot() ([32]byte, error) }); ok { - root, err := sszObj.HashTreeRoot() - if err != nil { - return phase0.Root{}, err - } - return phase0.Root(root), nil - } - - return phase0.Root{}, fmt.Errorf("object does not support HashTreeRoot") -} - -// GetConfig returns a copy of the current beacon config -func (m *SSZManager) GetConfig() map[string]interface{} { - m.mu.RLock() - defer m.mu.RUnlock() - - if m.config == nil { - return nil - } - - // Return a deep copy to prevent modification - configCopy := make(map[string]interface{}) - for k, v := range m.config { - configCopy[k] = v - } - return configCopy -} diff --git a/common/dynssz_test.go b/common/dynssz_test.go index f85adde2..e028b845 100644 --- a/common/dynssz_test.go +++ b/common/dynssz_test.go @@ -1,7 +1,6 @@ package common import ( - "sync" "testing" "github.com/attestantio/go-eth2-client/spec/phase0" @@ -236,37 +235,3 @@ func TestSSZManager_ConfigCopy(t *testing.T) { configFromManager := manager.GetConfig() require.Equal(t, "8", configFromManager["SLOTS_PER_EPOCH"]) } - -func TestSSZManager_ConcurrentAccess(t *testing.T) { - log := logrus.WithField("test", "SSZManager") - manager := NewSSZManager(log) - - config := map[string]interface{}{ - "SLOTS_PER_EPOCH": "8", - "SECONDS_PER_SLOT": "12", - } - - err := manager.Initialize(config) - require.NoError(t, err) - - // Test concurrent access to the manager - var wg sync.WaitGroup - numGoroutines := 10 - - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func() { - defer wg.Done() - - // Test various operations concurrently - _ = manager.IsInitialized() - _ = manager.GetConfig() - - testStruct := &TestStruct{Value: 123} - _, _ = manager.MarshalSSZ(testStruct) - _, _ = manager.HashTreeRoot(testStruct) - }() - } - - wg.Wait() -} diff --git a/common/types_spec.go b/common/types_spec.go index 9f3c319d..45654c57 100644 --- a/common/types_spec.go +++ b/common/types_spec.go @@ -3,6 +3,7 @@ package common import ( "bytes" "fmt" + "sync" builderApi "github.com/attestantio/go-builder-client/api" builderApiCapella "github.com/attestantio/go-builder-client/api/capella" @@ -25,13 +26,17 @@ import ( "github.com/flashbots/go-boost-utils/utils" "github.com/goccy/go-json" "github.com/holiman/uint256" + dynamicssz "github.com/pk910/dynamic-ssz" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) var ( - ErrMissingRequest = errors.New("req is nil") - ErrMissingSecretKey = errors.New("secret key is nil") - ErrInvalidVersion = errors.New("invalid version") + ErrMissingRequest = errors.New("req is nil") + ErrMissingSecretKey = errors.New("secret key is nil") + ErrInvalidVersion = errors.New("invalid version") + ErrSSZManagerNotInitialized = errors.New("SSZ manager not initialized") + ErrInvalidSpecConfig = errors.New("invalid spec config") ) type HTTPErrorResp struct { @@ -687,3 +692,143 @@ func (r *VersionedSignedBlindedBeaconBlock) Unmarshal(input []byte, contentType, } return ErrInvalidContentType } + +// SSZ Manager for dynamic SSZ operations + +// HashRootObject represents an object that can compute its hash tree root +type HashRootObject interface { + HashTreeRoot() ([32]byte, error) +} + +// SSZManager manages dynamic SSZ operations with a single initialized instance +type SSZManager struct { + mu sync.RWMutex + dynSSZ *dynamicssz.DynSsz + config map[string]interface{} + log *logrus.Entry +} + +func NewSSZManager(log *logrus.Entry) *SSZManager { + return &SSZManager{ + log: log, + } +} + +func (s *SSZManager) Initialize(beaconConfig map[string]interface{}) error { + s.mu.Lock() + defer s.mu.Unlock() + + if beaconConfig == nil { + return ErrInvalidSpecConfig + } + + s.config = beaconConfig + s.dynSSZ = dynamicssz.NewDynSsz(beaconConfig) + + s.log.WithFields(logrus.Fields{ + "configKeys": len(beaconConfig), + }).Info("SSZ manager initialized") + + return nil +} + +func (s *SSZManager) IsInitialized() bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.dynSSZ != nil +} + +func (s *SSZManager) GetConfig() map[string]interface{} { + s.mu.RLock() + defer s.mu.RUnlock() + + if s.config == nil { + return nil + } + + configCopy := make(map[string]interface{}) + for k, v := range s.config { + configCopy[k] = v + } + return configCopy +} + +// SignMessage signs a message using the appropriate SSZ encoding +func (s *SSZManager) SignMessage(obj HashRootObject, domain phase0.Domain, secretKey *bls.SecretKey) (phase0.BLSSignature, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + if s.dynSSZ == nil { + return phase0.BLSSignature{}, ErrSSZManagerNotInitialized + } + + // Use standard SSZ for signing since dynamic SSZ doesn't provide hash tree root functionality yet + return ssz.SignMessage(obj, domain, secretKey) +} + +// MarshalSSZ marshals an object using the appropriate SSZ encoding +func (s *SSZManager) MarshalSSZ(obj interface{}) ([]byte, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + if s.dynSSZ == nil { + return nil, ErrSSZManagerNotInitialized + } + + // Try dynamic SSZ first - it automatically falls back to standard SSZ for mainnet/testnet + data, err := s.dynSSZ.MarshalSSZ(obj) + if err == nil { + return data, nil + } + + // If dynamic SSZ fails, fall back to standard SSZ + if sszObj, ok := obj.(interface{ MarshalSSZ() ([]byte, error) }); ok { + return sszObj.MarshalSSZ() + } + + return nil, fmt.Errorf("object does not support SSZ marshaling") +} + +// UnmarshalSSZ unmarshals data using the appropriate SSZ encoding +func (s *SSZManager) UnmarshalSSZ(obj interface{}, data []byte) error { + s.mu.RLock() + defer s.mu.RUnlock() + + if s.dynSSZ == nil { + return ErrSSZManagerNotInitialized + } + + // Try dynamic SSZ first - it automatically falls back to standard SSZ for mainnet/testnet + err := s.dynSSZ.UnmarshalSSZ(obj, data) + if err == nil { + return nil + } + + // If dynamic SSZ fails, fall back to standard SSZ + if sszObj, ok := obj.(interface{ UnmarshalSSZ([]byte) error }); ok { + return sszObj.UnmarshalSSZ(data) + } + + return fmt.Errorf("object does not support SSZ unmarshaling") +} + +// HashTreeRoot computes hash tree root using the appropriate SSZ encoding +func (s *SSZManager) HashTreeRoot(obj interface{}) (phase0.Root, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + if s.dynSSZ == nil { + return phase0.Root{}, ErrSSZManagerNotInitialized + } + + // Use standard SSZ for hash tree root since dynamic SSZ doesn't support it yet + if sszObj, ok := obj.(interface{ HashTreeRoot() ([32]byte, error) }); ok { + root, err := sszObj.HashTreeRoot() + if err != nil { + return phase0.Root{}, err + } + return phase0.Root(root), nil + } + + return phase0.Root{}, fmt.Errorf("object does not support HashTreeRoot") +} From a9ae3f771429ce41e827a07862ad00afe337d315 Mon Sep 17 00:00:00 2001 From: dmarzzz Date: Mon, 9 Jun 2025 14:42:07 +0200 Subject: [PATCH 3/6] fix lints --- common/types_spec.go | 23 +++++++++++++---------- services/api/service.go | 4 +--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/common/types_spec.go b/common/types_spec.go index 45654c57..a3540b7c 100644 --- a/common/types_spec.go +++ b/common/types_spec.go @@ -32,11 +32,14 @@ import ( ) var ( - ErrMissingRequest = errors.New("req is nil") - ErrMissingSecretKey = errors.New("secret key is nil") - ErrInvalidVersion = errors.New("invalid version") - ErrSSZManagerNotInitialized = errors.New("SSZ manager not initialized") - ErrInvalidSpecConfig = errors.New("invalid spec config") + ErrMissingRequest = errors.New("req is nil") + ErrMissingSecretKey = errors.New("secret key is nil") + ErrInvalidVersion = errors.New("invalid version") + ErrSSZManagerNotInitialized = errors.New("SSZ manager not initialized") + ErrInvalidSpecConfig = errors.New("invalid spec config") + ErrObjectDoesNotSupportSSZMarshaling = errors.New("object does not support SSZ marshaling") + ErrObjectDoesNotSupportSSZUnmarshaling = errors.New("object does not support SSZ unmarshaling") + ErrObjectDoesNotSupportHashTreeRoot = errors.New("object does not support HashTreeRoot") ) type HTTPErrorResp struct { @@ -504,7 +507,7 @@ func (r *VersionedSubmitBlockRequest) HashTreeRootWithManager(sszManager *SSZMan } return phase0.Root(root), nil default: - return phase0.Root{}, errors.Wrap(ErrInvalidVersion, fmt.Sprintf("%s is not supported", r.Version)) + return phase0.Root{}, ErrObjectDoesNotSupportHashTreeRoot } } @@ -786,7 +789,7 @@ func (s *SSZManager) MarshalSSZ(obj interface{}) ([]byte, error) { return sszObj.MarshalSSZ() } - return nil, fmt.Errorf("object does not support SSZ marshaling") + return nil, ErrObjectDoesNotSupportSSZMarshaling } // UnmarshalSSZ unmarshals data using the appropriate SSZ encoding @@ -805,11 +808,11 @@ func (s *SSZManager) UnmarshalSSZ(obj interface{}, data []byte) error { } // If dynamic SSZ fails, fall back to standard SSZ - if sszObj, ok := obj.(interface{ UnmarshalSSZ([]byte) error }); ok { + if sszObj, ok := obj.(interface{ UnmarshalSSZ(data []byte) error }); ok { return sszObj.UnmarshalSSZ(data) } - return fmt.Errorf("object does not support SSZ unmarshaling") + return ErrObjectDoesNotSupportSSZUnmarshaling } // HashTreeRoot computes hash tree root using the appropriate SSZ encoding @@ -830,5 +833,5 @@ func (s *SSZManager) HashTreeRoot(obj interface{}) (phase0.Root, error) { return phase0.Root(root), nil } - return phase0.Root{}, fmt.Errorf("object does not support HashTreeRoot") + return phase0.Root{}, ErrObjectDoesNotSupportHashTreeRoot } diff --git a/services/api/service.go b/services/api/service.go index d381c08b..038efc16 100644 --- a/services/api/service.go +++ b/services/api/service.go @@ -441,9 +441,7 @@ func (api *RelayAPI) StartServer() (err error) { specConfig := make(map[string]interface{}) // Add relevant fields from beacon spec - if beaconSpec.SecondsPerSlot != 0 { - specConfig["SECONDS_PER_SLOT"] = fmt.Sprintf("%d", beaconSpec.SecondsPerSlot) - } + specConfig["SECONDS_PER_SLOT"] = strconv.FormatUint(beaconSpec.SecondsPerSlot, 10) // Add other configuration fields as needed - you can extend this // based on what fields are available in GetSpecResponse From a961d7fa212c4d940d2d56acbae0ec4903abe420 Mon Sep 17 00:00:00 2001 From: dmarzzz Date: Mon, 9 Jun 2025 14:52:10 +0200 Subject: [PATCH 4/6] move tests --- common/dynssz_test.go | 237 ------------------------------------- common/test_utils.go | 2 +- common/types_spec.go | 6 +- common/types_spec_test.go | 229 +++++++++++++++++++++++++++++++++++ services/api/service.go | 2 +- services/api/types_test.go | 2 +- 6 files changed, 233 insertions(+), 245 deletions(-) delete mode 100644 common/dynssz_test.go diff --git a/common/dynssz_test.go b/common/dynssz_test.go deleted file mode 100644 index e028b845..00000000 --- a/common/dynssz_test.go +++ /dev/null @@ -1,237 +0,0 @@ -package common - -import ( - "testing" - - "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/flashbots/go-boost-utils/bls" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/require" -) - -// TestStruct implements HashRootObject for testing -type TestStruct struct { - Value uint64 -} - -// HashTreeRoot implements the HashRootObject interface -func (t *TestStruct) HashTreeRoot() ([32]byte, error) { - // Simple mock implementation for testing - var root [32]byte - copy(root[:], []byte("test_root_hash_for_testing_123")) - return root, nil -} - -func TestSSZManager_Initialize(t *testing.T) { - log := logrus.WithField("test", "SSZManager") - - t.Run("Initialize with minimal spec config", func(t *testing.T) { - manager := NewSSZManager(log) - - minimalConfig := map[string]interface{}{ - "SLOTS_PER_EPOCH": "8", - "SECONDS_PER_SLOT": "12", - } - - err := manager.Initialize(minimalConfig) - require.NoError(t, err) - require.True(t, manager.IsInitialized()) - - config := manager.GetConfig() - require.Equal(t, minimalConfig, config) - }) - - t.Run("Initialize with mainnet config", func(t *testing.T) { - manager := NewSSZManager(log) - - mainnetConfig := map[string]interface{}{ - "SLOTS_PER_EPOCH": "32", - "SECONDS_PER_SLOT": "12", - } - - err := manager.Initialize(mainnetConfig) - require.NoError(t, err) - require.True(t, manager.IsInitialized()) - - config := manager.GetConfig() - require.Equal(t, mainnetConfig, config) - }) - - t.Run("Initialize with nil config", func(t *testing.T) { - manager := NewSSZManager(log) - - err := manager.Initialize(nil) - require.Error(t, err) - require.Equal(t, ErrInvalidSpecConfig, err) - require.False(t, manager.IsInitialized()) - }) -} - -func TestSSZManager_SignMessage(t *testing.T) { - log := logrus.WithField("test", "SSZManager") - - // Generate test key - sk, _, err := bls.GenerateNewKeypair() - require.NoError(t, err) - - // Create test domain - domain := phase0.Domain{1, 2, 3, 4} - - // Create test struct instance - testStruct := &TestStruct{Value: 123} - - t.Run("Sign with uninitialized manager", func(t *testing.T) { - manager := NewSSZManager(log) - - // This should return an error since manager is not initialized - _, err := manager.SignMessage(testStruct, domain, sk) - require.Error(t, err) - require.Equal(t, ErrSSZManagerNotInitialized, err) - }) - - t.Run("Sign with initialized manager", func(t *testing.T) { - manager := NewSSZManager(log) - - config := map[string]interface{}{ - "SLOTS_PER_EPOCH": "32", - "SECONDS_PER_SLOT": "12", - } - - err := manager.Initialize(config) - require.NoError(t, err) - - // This should work without error since we use standard SSZ for signing - _, err = manager.SignMessage(testStruct, domain, sk) - require.NoError(t, err) - }) -} - -func TestSSZManager_MarshalUnmarshal(t *testing.T) { - log := logrus.WithField("test", "SSZManager") - - t.Run("MarshalSSZ and UnmarshalSSZ with minimal config", func(t *testing.T) { - manager := NewSSZManager(log) - - minimalConfig := map[string]interface{}{ - "SLOTS_PER_EPOCH": "8", - "SECONDS_PER_SLOT": "12", - } - - err := manager.Initialize(minimalConfig) - require.NoError(t, err) - - // Test marshal/unmarshal functionality - type TestData struct { - Value uint64 - } - - original := &TestData{Value: 12345} - - // Marshal should not error (even if it falls back to standard SSZ) - data, err := manager.MarshalSSZ(original) - require.NoError(t, err) - require.NotEmpty(t, data) - - // Unmarshal should work - unmarshaled := &TestData{} - err = manager.UnmarshalSSZ(unmarshaled, data) - require.NoError(t, err) - require.Equal(t, original.Value, unmarshaled.Value) - }) - - t.Run("MarshalSSZ and UnmarshalSSZ with mainnet config", func(t *testing.T) { - manager := NewSSZManager(log) - - mainnetConfig := map[string]interface{}{ - "SLOTS_PER_EPOCH": "32", - "SECONDS_PER_SLOT": "12", - } - - err := manager.Initialize(mainnetConfig) - require.NoError(t, err) - - // Test marshal/unmarshal functionality - type TestData struct { - Value uint64 - } - - original := &TestData{Value: 67890} - - // Marshal should work - data, err := manager.MarshalSSZ(original) - require.NoError(t, err) - require.NotEmpty(t, data) - - // Unmarshal should work - unmarshaled := &TestData{} - err = manager.UnmarshalSSZ(unmarshaled, data) - require.NoError(t, err) - require.Equal(t, original.Value, unmarshaled.Value) - }) -} - -func TestSSZManager_HashTreeRoot(t *testing.T) { - log := logrus.WithField("test", "SSZManager") - - t.Run("HashTreeRoot with minimal config", func(t *testing.T) { - manager := NewSSZManager(log) - - minimalConfig := map[string]interface{}{ - "SLOTS_PER_EPOCH": "8", - "SECONDS_PER_SLOT": "12", - } - - err := manager.Initialize(minimalConfig) - require.NoError(t, err) - - testData := &TestStruct{Value: 12345} - - // HashTreeRoot should work (using standard SSZ) - root, err := manager.HashTreeRoot(testData) - require.NoError(t, err) - require.NotEqual(t, phase0.Root{}, root) - }) - - t.Run("HashTreeRoot with mainnet config", func(t *testing.T) { - manager := NewSSZManager(log) - - mainnetConfig := map[string]interface{}{ - "SLOTS_PER_EPOCH": "32", - "SECONDS_PER_SLOT": "12", - } - - err := manager.Initialize(mainnetConfig) - require.NoError(t, err) - - testData := &TestStruct{Value: 67890} - - // HashTreeRoot should work - root, err := manager.HashTreeRoot(testData) - require.NoError(t, err) - require.NotEqual(t, phase0.Root{}, root) - }) -} - -func TestSSZManager_ConfigCopy(t *testing.T) { - log := logrus.WithField("test", "SSZManager") - manager := NewSSZManager(log) - - originalConfig := map[string]interface{}{ - "SLOTS_PER_EPOCH": "8", - "SECONDS_PER_SLOT": "12", - } - - err := manager.Initialize(originalConfig) - require.NoError(t, err) - - // Get config copy - configCopy := manager.GetConfig() - require.Equal(t, originalConfig, configCopy) - - // Modify the copy - should not affect original - configCopy["SLOTS_PER_EPOCH"] = "16" - - // Original config should be unchanged - configFromManager := manager.GetConfig() - require.Equal(t, "8", configFromManager["SLOTS_PER_EPOCH"]) -} diff --git a/common/test_utils.go b/common/test_utils.go index 777b9f35..b2d97460 100644 --- a/common/test_utils.go +++ b/common/test_utils.go @@ -207,7 +207,7 @@ func CreateTestBlockSubmission(t *testing.T, builderPubkey string, value *uint25 } } - getHeaderResponse, err = BuildGetHeaderResponse(payload, &relaySk, &relayPk, domain) + getHeaderResponse, err = BuildGetHeaderResponse(payload, &relaySk, &relayPk, domain, nil) require.NoError(t, err) getPayloadResponse, err = BuildGetPayloadResponse(payload) diff --git a/common/types_spec.go b/common/types_spec.go index a3540b7c..0892796c 100644 --- a/common/types_spec.go +++ b/common/types_spec.go @@ -49,11 +49,7 @@ type HTTPErrorResp struct { var NilResponse = struct{}{} -func BuildGetHeaderResponse(payload *VersionedSubmitBlockRequest, sk *bls.SecretKey, pubkey *phase0.BLSPubKey, domain phase0.Domain) (*builderSpec.VersionedSignedBuilderBid, error) { - return BuildGetHeaderResponseWithSSZ(payload, sk, pubkey, domain, nil) -} - -func BuildGetHeaderResponseWithSSZ(payload *VersionedSubmitBlockRequest, sk *bls.SecretKey, pubkey *phase0.BLSPubKey, domain phase0.Domain, sszManager *SSZManager) (*builderSpec.VersionedSignedBuilderBid, error) { +func BuildGetHeaderResponse(payload *VersionedSubmitBlockRequest, sk *bls.SecretKey, pubkey *phase0.BLSPubKey, domain phase0.Domain, sszManager *SSZManager) (*builderSpec.VersionedSignedBuilderBid, error) { if payload == nil { return nil, ErrMissingRequest } diff --git a/common/types_spec_test.go b/common/types_spec_test.go index 87615d7f..fd67bd65 100644 --- a/common/types_spec_test.go +++ b/common/types_spec_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/flashbots/go-boost-utils/bls" "github.com/goccy/go-json" "github.com/stretchr/testify/require" ) @@ -135,3 +137,230 @@ func TestBuildGetPayloadResponse(t *testing.T) { }) } } + +// TestStruct implements HashRootObject for testing +type TestStruct struct { + Value uint64 +} + +// HashTreeRoot implements the HashRootObject interface +func (t *TestStruct) HashTreeRoot() ([32]byte, error) { + // Simple mock implementation for testing + var root [32]byte + copy(root[:], []byte("test_root_hash_for_testing_123")) + return root, nil +} + +func TestSSZManager_Initialize(t *testing.T) { + log := TestLog.WithField("test", "SSZManager") + + t.Run("Initialize with minimal spec config", func(t *testing.T) { + manager := NewSSZManager(log) + + minimalConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "8", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(minimalConfig) + require.NoError(t, err) + require.True(t, manager.IsInitialized()) + + config := manager.GetConfig() + require.Equal(t, minimalConfig, config) + }) + + t.Run("Initialize with mainnet config", func(t *testing.T) { + manager := NewSSZManager(log) + + mainnetConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "32", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(mainnetConfig) + require.NoError(t, err) + require.True(t, manager.IsInitialized()) + + config := manager.GetConfig() + require.Equal(t, mainnetConfig, config) + }) + + t.Run("Initialize with nil config", func(t *testing.T) { + manager := NewSSZManager(log) + + err := manager.Initialize(nil) + require.Error(t, err) + require.Equal(t, ErrInvalidSpecConfig, err) + require.False(t, manager.IsInitialized()) + }) +} + +func TestSSZManager_SignMessage(t *testing.T) { + log := TestLog.WithField("test", "SSZManager") + + // Generate test key + sk, _, err := bls.GenerateNewKeypair() + require.NoError(t, err) + + // Create test domain + domain := phase0.Domain{1, 2, 3, 4} + + // Create test struct instance + testStruct := &TestStruct{Value: 123} + + t.Run("Sign with uninitialized manager", func(t *testing.T) { + manager := NewSSZManager(log) + + // This should return an error since manager is not initialized + _, err := manager.SignMessage(testStruct, domain, sk) + require.Error(t, err) + require.Equal(t, ErrSSZManagerNotInitialized, err) + }) + + t.Run("Sign with initialized manager", func(t *testing.T) { + manager := NewSSZManager(log) + + config := map[string]interface{}{ + "SLOTS_PER_EPOCH": "32", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(config) + require.NoError(t, err) + + // This should work without error since we use standard SSZ for signing + _, err = manager.SignMessage(testStruct, domain, sk) + require.NoError(t, err) + }) +} + +func TestSSZManager_MarshalUnmarshal(t *testing.T) { + log := TestLog.WithField("test", "SSZManager") + + t.Run("MarshalSSZ and UnmarshalSSZ with minimal config", func(t *testing.T) { + manager := NewSSZManager(log) + + minimalConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "8", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(minimalConfig) + require.NoError(t, err) + + // Test marshal/unmarshal functionality + type TestData struct { + Value uint64 + } + + original := &TestData{Value: 12345} + + // Marshal should not error (even if it falls back to standard SSZ) + data, err := manager.MarshalSSZ(original) + require.NoError(t, err) + require.NotEmpty(t, data) + + // Unmarshal should work + unmarshaled := &TestData{} + err = manager.UnmarshalSSZ(unmarshaled, data) + require.NoError(t, err) + require.Equal(t, original.Value, unmarshaled.Value) + }) + + t.Run("MarshalSSZ and UnmarshalSSZ with mainnet config", func(t *testing.T) { + manager := NewSSZManager(log) + + mainnetConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "32", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(mainnetConfig) + require.NoError(t, err) + + // Test marshal/unmarshal functionality + type TestData struct { + Value uint64 + } + + original := &TestData{Value: 67890} + + // Marshal should work + data, err := manager.MarshalSSZ(original) + require.NoError(t, err) + require.NotEmpty(t, data) + + // Unmarshal should work + unmarshaled := &TestData{} + err = manager.UnmarshalSSZ(unmarshaled, data) + require.NoError(t, err) + require.Equal(t, original.Value, unmarshaled.Value) + }) +} + +func TestSSZManager_HashTreeRoot(t *testing.T) { + log := TestLog.WithField("test", "SSZManager") + + t.Run("HashTreeRoot with minimal config", func(t *testing.T) { + manager := NewSSZManager(log) + + minimalConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "8", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(minimalConfig) + require.NoError(t, err) + + testData := &TestStruct{Value: 12345} + + // HashTreeRoot should work (using standard SSZ) + root, err := manager.HashTreeRoot(testData) + require.NoError(t, err) + require.NotEqual(t, phase0.Root{}, root) + }) + + t.Run("HashTreeRoot with mainnet config", func(t *testing.T) { + manager := NewSSZManager(log) + + mainnetConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "32", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(mainnetConfig) + require.NoError(t, err) + + testData := &TestStruct{Value: 67890} + + // HashTreeRoot should work + root, err := manager.HashTreeRoot(testData) + require.NoError(t, err) + require.NotEqual(t, phase0.Root{}, root) + }) +} + +func TestSSZManager_ConfigCopy(t *testing.T) { + log := TestLog.WithField("test", "SSZManager") + manager := NewSSZManager(log) + + originalConfig := map[string]interface{}{ + "SLOTS_PER_EPOCH": "8", + "SECONDS_PER_SLOT": "12", + } + + err := manager.Initialize(originalConfig) + require.NoError(t, err) + + // Get config copy + configCopy := manager.GetConfig() + require.Equal(t, originalConfig, configCopy) + + // Modify the copy - should not affect original + configCopy["SLOTS_PER_EPOCH"] = "16" + + // Original config should be unchanged + configFromManager := manager.GetConfig() + require.Equal(t, "8", configFromManager["SLOTS_PER_EPOCH"]) +} diff --git a/services/api/service.go b/services/api/service.go index 038efc16..b4faf007 100644 --- a/services/api/service.go +++ b/services/api/service.go @@ -2024,7 +2024,7 @@ type redisUpdateBidOpts struct { func (api *RelayAPI) updateRedisBid(opts redisUpdateBidOpts) (*datastore.SaveBidAndUpdateTopBidResponse, *builderApi.VersionedSubmitBlindedBlockResponse, bool) { // Prepare the response data - getHeaderResponse, err := common.BuildGetHeaderResponseWithSSZ(opts.payload, api.blsSk, api.publicKey, api.opts.EthNetDetails.DomainBuilder, api.sszManager) + getHeaderResponse, err := common.BuildGetHeaderResponse(opts.payload, api.blsSk, api.publicKey, api.opts.EthNetDetails.DomainBuilder, api.sszManager) if err != nil { opts.log.WithError(err).Error("could not sign builder bid") api.RespondError(opts.w, http.StatusBadRequest, err.Error()) diff --git a/services/api/types_test.go b/services/api/types_test.go index 8c0a028a..5aaed04e 100644 --- a/services/api/types_test.go +++ b/services/api/types_test.go @@ -180,7 +180,7 @@ func TestBuilderBlockRequestToSignedBuilderBid(t *testing.T) { publicKey, err := utils.BlsPublicKeyToPublicKey(pubkey) require.NoError(t, err) - signedBuilderBid, err := common.BuildGetHeaderResponse(tc.reqPayload, sk, &publicKey, ssz.DomainBuilder) + signedBuilderBid, err := common.BuildGetHeaderResponse(tc.reqPayload, sk, &publicKey, ssz.DomainBuilder, nil) require.NoError(t, err) bidValue, err := signedBuilderBid.Value() From b692fb2a8fabb833e5c846c677fcb86aab94dede Mon Sep 17 00:00:00 2001 From: dmarzzz Date: Mon, 9 Jun 2025 15:03:14 +0200 Subject: [PATCH 5/6] parse full spec configs from becaon API response --- beaconclient/mock_beacon_instance.go | 4 ++++ beaconclient/mock_multi_beacon_client.go | 4 ++++ beaconclient/multi_beacon_client.go | 19 +++++++++++++++++++ beaconclient/prod_beacon_instance.go | 9 +++++++++ 4 files changed, 36 insertions(+) diff --git a/beaconclient/mock_beacon_instance.go b/beaconclient/mock_beacon_instance.go index 6d551ed2..54a40d33 100644 --- a/beaconclient/mock_beacon_instance.go +++ b/beaconclient/mock_beacon_instance.go @@ -122,6 +122,10 @@ func (c *MockBeaconInstance) GetSpec() (spec *GetSpecResponse, err error) { return nil, nil } +func (c *MockBeaconInstance) GetSpecRaw() (spec map[string]interface{}, err error) { + return nil, nil +} + func (c *MockBeaconInstance) GetForkSchedule() (spec *GetForkScheduleResponse, err error) { return nil, nil } diff --git a/beaconclient/mock_multi_beacon_client.go b/beaconclient/mock_multi_beacon_client.go index 5fdeb7fe..400fd758 100644 --- a/beaconclient/mock_multi_beacon_client.go +++ b/beaconclient/mock_multi_beacon_client.go @@ -42,6 +42,10 @@ func (*MockMultiBeaconClient) GetSpec() (spec *GetSpecResponse, err error) { return nil, nil } +func (*MockMultiBeaconClient) GetSpecRaw() (spec map[string]interface{}, err error) { + return nil, nil +} + func (*MockMultiBeaconClient) GetForkSchedule() (spec *GetForkScheduleResponse, err error) { resp := &GetForkScheduleResponse{ Data: []struct { diff --git a/beaconclient/multi_beacon_client.go b/beaconclient/multi_beacon_client.go index b3c15952..4421d5e6 100644 --- a/beaconclient/multi_beacon_client.go +++ b/beaconclient/multi_beacon_client.go @@ -41,6 +41,7 @@ type IMultiBeaconClient interface { PublishBlock(block *common.VersionedSignedProposal) (code int, err error) GetGenesis() (*GetGenesisResponse, error) GetSpec() (spec *GetSpecResponse, err error) + GetSpecRaw() (spec map[string]interface{}, err error) GetForkSchedule() (spec *GetForkScheduleResponse, err error) GetRandao(slot uint64) (spec *GetRandaoResponse, err error) GetWithdrawals(slot uint64) (spec *GetWithdrawalsResponse, err error) @@ -59,6 +60,7 @@ type IBeaconInstance interface { PublishBlock(block *common.VersionedSignedProposal, broadcastMode BroadcastMode) (code int, err error) GetGenesis() (*GetGenesisResponse, error) GetSpec() (spec *GetSpecResponse, err error) + GetSpecRaw() (spec map[string]interface{}, err error) GetForkSchedule() (spec *GetForkScheduleResponse, err error) GetRandao(slot uint64) (spec *GetRandaoResponse, err error) GetWithdrawals(slot uint64) (spec *GetWithdrawalsResponse, err error) @@ -349,6 +351,23 @@ func (c *MultiBeaconClient) GetSpec() (spec *GetSpecResponse, err error) { return nil, err } +// GetSpecRaw returns the complete beacon spec response as map[string]interface{} +func (c *MultiBeaconClient) GetSpecRaw() (spec map[string]interface{}, err error) { + clients := c.beaconInstancesByLastResponse() + for _, client := range clients { + log := c.log.WithField("uri", client.GetURI()) + if spec, err = client.GetSpecRaw(); err != nil { + log.WithError(err).Warn("failed to get spec raw") + continue + } + + return spec, nil + } + + c.log.WithError(err).Error("failed to get spec raw on any CL node") + return nil, err +} + // GetForkSchedule - https://ethereum.github.io/beacon-APIs/#/Config/getForkSchedule func (c *MultiBeaconClient) GetForkSchedule() (spec *GetForkScheduleResponse, err error) { clients := c.beaconInstancesByLastResponse() diff --git a/beaconclient/prod_beacon_instance.go b/beaconclient/prod_beacon_instance.go index a69a622f..4d36a825 100644 --- a/beaconclient/prod_beacon_instance.go +++ b/beaconclient/prod_beacon_instance.go @@ -334,6 +334,15 @@ func (c *ProdBeaconInstance) GetSpec() (spec *GetSpecResponse, err error) { return resp, err } +// GetSpecRaw - returns the complete beacon spec configuration as map[string]interface{} +// This captures all fields returned by the beacon API, not just the limited ones in GetSpecResponse +func (c *ProdBeaconInstance) GetSpecRaw() (spec map[string]interface{}, err error) { + uri := c.beaconURI + "/eth/v1/config/spec" + var rawResponse map[string]interface{} + _, err = fetchBeacon(http.MethodGet, uri, nil, &rawResponse, nil, http.Header{}, false) + return rawResponse, err +} + type GetForkScheduleResponse struct { Data []struct { PreviousVersion string `json:"previous_version"` From bda1bda08e7ccdade93eb43b7ff98b64a1c0a2a0 Mon Sep 17 00:00:00 2001 From: dmarzzz Date: Tue, 10 Jun 2025 18:31:27 +0200 Subject: [PATCH 6/6] parse full spec --- beaconclient/mock_beacon_instance.go | 7 +++++-- beaconclient/mock_multi_beacon_client.go | 7 +++++-- beaconclient/multi_beacon_client.go | 20 ++++++++++---------- beaconclient/prod_beacon_instance.go | 11 +++++------ services/api/service.go | 21 ++++++--------------- 5 files changed, 31 insertions(+), 35 deletions(-) diff --git a/beaconclient/mock_beacon_instance.go b/beaconclient/mock_beacon_instance.go index 54a40d33..1fd43a9e 100644 --- a/beaconclient/mock_beacon_instance.go +++ b/beaconclient/mock_beacon_instance.go @@ -122,8 +122,11 @@ func (c *MockBeaconInstance) GetSpec() (spec *GetSpecResponse, err error) { return nil, nil } -func (c *MockBeaconInstance) GetSpecRaw() (spec map[string]interface{}, err error) { - return nil, nil +func (c *MockBeaconInstance) GetSpecRaw() (map[string]interface{}, error) { + return map[string]interface{}{ + "SECONDS_PER_SLOT": "12", + "SLOTS_PER_EPOCH": "32", + }, nil } func (c *MockBeaconInstance) GetForkSchedule() (spec *GetForkScheduleResponse, err error) { diff --git a/beaconclient/mock_multi_beacon_client.go b/beaconclient/mock_multi_beacon_client.go index 400fd758..2089b58c 100644 --- a/beaconclient/mock_multi_beacon_client.go +++ b/beaconclient/mock_multi_beacon_client.go @@ -42,8 +42,11 @@ func (*MockMultiBeaconClient) GetSpec() (spec *GetSpecResponse, err error) { return nil, nil } -func (*MockMultiBeaconClient) GetSpecRaw() (spec map[string]interface{}, err error) { - return nil, nil +func (*MockMultiBeaconClient) GetSpecRaw() (map[string]interface{}, error) { + return map[string]interface{}{ + "SECONDS_PER_SLOT": "12", + "SLOTS_PER_EPOCH": "32", + }, nil } func (*MockMultiBeaconClient) GetForkSchedule() (spec *GetForkScheduleResponse, err error) { diff --git a/beaconclient/multi_beacon_client.go b/beaconclient/multi_beacon_client.go index 4421d5e6..3638b6e0 100644 --- a/beaconclient/multi_beacon_client.go +++ b/beaconclient/multi_beacon_client.go @@ -41,7 +41,7 @@ type IMultiBeaconClient interface { PublishBlock(block *common.VersionedSignedProposal) (code int, err error) GetGenesis() (*GetGenesisResponse, error) GetSpec() (spec *GetSpecResponse, err error) - GetSpecRaw() (spec map[string]interface{}, err error) + GetSpecRaw() (map[string]interface{}, error) GetForkSchedule() (spec *GetForkScheduleResponse, err error) GetRandao(slot uint64) (spec *GetRandaoResponse, err error) GetWithdrawals(slot uint64) (spec *GetWithdrawalsResponse, err error) @@ -60,7 +60,7 @@ type IBeaconInstance interface { PublishBlock(block *common.VersionedSignedProposal, broadcastMode BroadcastMode) (code int, err error) GetGenesis() (*GetGenesisResponse, error) GetSpec() (spec *GetSpecResponse, err error) - GetSpecRaw() (spec map[string]interface{}, err error) + GetSpecRaw() (map[string]interface{}, error) GetForkSchedule() (spec *GetForkScheduleResponse, err error) GetRandao(slot uint64) (spec *GetRandaoResponse, err error) GetWithdrawals(slot uint64) (spec *GetWithdrawalsResponse, err error) @@ -351,21 +351,21 @@ func (c *MultiBeaconClient) GetSpec() (spec *GetSpecResponse, err error) { return nil, err } -// GetSpecRaw returns the complete beacon spec response as map[string]interface{} -func (c *MultiBeaconClient) GetSpecRaw() (spec map[string]interface{}, err error) { +// GetSpecRaw returns the complete beacon spec as raw JSON +func (c *MultiBeaconClient) GetSpecRaw() (map[string]interface{}, error) { clients := c.beaconInstancesByLastResponse() for _, client := range clients { log := c.log.WithField("uri", client.GetURI()) - if spec, err = client.GetSpecRaw(); err != nil { - log.WithError(err).Warn("failed to get spec raw") + if spec, err := client.GetSpecRaw(); err != nil { + log.WithError(err).Warn("failed to get spec as raw JSON") continue + } else { + return spec, nil } - - return spec, nil } - c.log.WithError(err).Error("failed to get spec raw on any CL node") - return nil, err + c.log.Error("failed to get spec as raw JSON on any CL node") + return nil, ErrBeaconNodesUnavailable } // GetForkSchedule - https://ethereum.github.io/beacon-APIs/#/Config/getForkSchedule diff --git a/beaconclient/prod_beacon_instance.go b/beaconclient/prod_beacon_instance.go index 4d36a825..4ac02a8b 100644 --- a/beaconclient/prod_beacon_instance.go +++ b/beaconclient/prod_beacon_instance.go @@ -334,13 +334,12 @@ func (c *ProdBeaconInstance) GetSpec() (spec *GetSpecResponse, err error) { return resp, err } -// GetSpecRaw - returns the complete beacon spec configuration as map[string]interface{} -// This captures all fields returned by the beacon API, not just the limited ones in GetSpecResponse -func (c *ProdBeaconInstance) GetSpecRaw() (spec map[string]interface{}, err error) { +// GetSpecRaw fetches the complete beacon spec configuration as raw JSON +func (c *ProdBeaconInstance) GetSpecRaw() (map[string]interface{}, error) { uri := c.beaconURI + "/eth/v1/config/spec" - var rawResponse map[string]interface{} - _, err = fetchBeacon(http.MethodGet, uri, nil, &rawResponse, nil, http.Header{}, false) - return rawResponse, err + var rawSpec map[string]interface{} + _, err := fetchBeacon(http.MethodGet, uri, nil, &rawSpec, nil, http.Header{}, false) + return rawSpec, err } type GetForkScheduleResponse struct { diff --git a/services/api/service.go b/services/api/service.go index b4faf007..97a7750e 100644 --- a/services/api/service.go +++ b/services/api/service.go @@ -433,26 +433,17 @@ func (api *RelayAPI) StartServer() (err error) { log.Infof("genesis info: %d", api.genesisInfo.Data.GenesisTime) // Get beacon spec configuration and initialize SSZ manager - beaconSpec, err := api.beaconClient.GetSpec() + specConfig, err := api.beaconClient.GetSpecRaw() if err != nil { - log.WithError(err).Warn("failed to get beacon spec config, SSZ manager will use standard SSZ") + api.log.WithError(err).Warn("failed to get beacon spec config, SSZ manager will use standard SSZ") } else { - // Convert spec to map[string]interface{} for dynamic SSZ - specConfig := make(map[string]interface{}) - - // Add relevant fields from beacon spec - specConfig["SECONDS_PER_SLOT"] = strconv.FormatUint(beaconSpec.SecondsPerSlot, 10) - - // Add other configuration fields as needed - you can extend this - // based on what fields are available in GetSpecResponse - - // Try to initialize the SSZ manager with the beacon config + // Try to initialize the SSZ manager with the complete beacon config if err := api.sszManager.Initialize(specConfig); err != nil { - log.WithError(err).Warn("failed to initialize SSZ manager with beacon config, will use standard SSZ") + api.log.WithError(err).Warn("failed to initialize SSZ manager with beacon config, will use standard SSZ") } else { - log.WithFields(logrus.Fields{ + api.log.WithFields(logrus.Fields{ "configKeys": len(specConfig), - }).Info("SSZ manager initialized with beacon config") + }).Info("SSZ manager initialized with complete beacon config") } }