diff --git a/beaconclient/mock_beacon_instance.go b/beaconclient/mock_beacon_instance.go index 6d551ed2..1fd43a9e 100644 --- a/beaconclient/mock_beacon_instance.go +++ b/beaconclient/mock_beacon_instance.go @@ -122,6 +122,13 @@ func (c *MockBeaconInstance) GetSpec() (spec *GetSpecResponse, 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) { return nil, nil } diff --git a/beaconclient/mock_multi_beacon_client.go b/beaconclient/mock_multi_beacon_client.go index 5fdeb7fe..2089b58c 100644 --- a/beaconclient/mock_multi_beacon_client.go +++ b/beaconclient/mock_multi_beacon_client.go @@ -42,6 +42,13 @@ func (*MockMultiBeaconClient) GetSpec() (spec *GetSpecResponse, 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) { resp := &GetForkScheduleResponse{ Data: []struct { diff --git a/beaconclient/multi_beacon_client.go b/beaconclient/multi_beacon_client.go index b3c15952..3638b6e0 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() (map[string]interface{}, 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() (map[string]interface{}, 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 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 as raw JSON") + continue + } else { + return spec, nil + } + } + + 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 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..4ac02a8b 100644 --- a/beaconclient/prod_beacon_instance.go +++ b/beaconclient/prod_beacon_instance.go @@ -334,6 +334,14 @@ func (c *ProdBeaconInstance) GetSpec() (spec *GetSpecResponse, err error) { return resp, err } +// 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 rawSpec map[string]interface{} + _, err := fetchBeacon(http.MethodGet, uri, nil, &rawSpec, nil, http.Header{}, false) + return rawSpec, err +} + type GetForkScheduleResponse struct { Data []struct { PreviousVersion string `json:"previous_version"` 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 5b0b5fe9..0892796c 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,20 @@ 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") + 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 { @@ -41,7 +49,7 @@ type HTTPErrorResp struct { var NilResponse = struct{}{} -func BuildGetHeaderResponse(payload *VersionedSubmitBlockRequest, sk *bls.SecretKey, pubkey *phase0.BLSPubKey, domain phase0.Domain) (*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 } @@ -58,7 +66,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 +80,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 +94,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 +138,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 +152,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 +177,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 +203,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 +425,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 +455,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 +477,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{}, ErrObjectDoesNotSupportHashTreeRoot + } +} + func (r *VersionedSubmitBlockRequest) MarshalJSON() ([]byte, error) { switch r.Version { //nolint:exhaustive case spec.DataVersionCapella: @@ -624,3 +691,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, ErrObjectDoesNotSupportSSZMarshaling +} + +// 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(data []byte) error }); ok { + return sszObj.UnmarshalSSZ(data) + } + + return ErrObjectDoesNotSupportSSZUnmarshaling +} + +// 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{}, ErrObjectDoesNotSupportHashTreeRoot +} 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/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..97a7750e 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,21 @@ func (api *RelayAPI) StartServer() (err error) { } log.Infof("genesis info: %d", api.genesisInfo.Data.GenesisTime) + // Get beacon spec configuration and initialize SSZ manager + specConfig, err := api.beaconClient.GetSpecRaw() + if err != nil { + api.log.WithError(err).Warn("failed to get beacon spec config, SSZ manager will use standard SSZ") + } else { + // Try to initialize the SSZ manager with the complete beacon config + if err := api.sszManager.Initialize(specConfig); err != nil { + api.log.WithError(err).Warn("failed to initialize SSZ manager with beacon config, will use standard SSZ") + } else { + api.log.WithFields(logrus.Fields{ + "configKeys": len(specConfig), + }).Info("SSZ manager initialized with complete beacon config") + } + } + // Get and prepare fork schedule forkSchedule, err := api.beaconClient.GetForkSchedule() if err != nil { @@ -1994,7 +2015,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.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()