diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..685c70c6b6 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,30 @@ +name: 'Label and close stale PRs' +on: + schedule: + # Runs at 1:30 AM every day + - cron: '30 1 * * *' + workflow_dispatch: # Allows manual triggering + +permissions: + actions: write + contents: read + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9.1.0 + with: + days-before-stale: 60 + days-before-close: 30 + stale-pr-message: 'This pull request has been marked as stale due to 60 days of inactivity. It will be closed in 30 days if there are no updates. Please comment if you would like to keep it open.' + close-pr-message: 'Closing this pull request as it has been inactive for 30 days after being marked stale. You are welcome to reopen it if you wish to continue.' + stale-issue-message: 'This issue has been marked as stale due to 60 days of inactivity. It will be closed in 30 days if there are no updates. Please comment if you would like to keep it open.' + close-issue-message: 'Closing this issue as it has been inactive for 30 days after being marked stale. You are welcome to reopen it if you wish to continue.' + stale-issue-label: 'stale' + stale-pr-label: 'stale' + operations-per-run: 100 + # Enable statistics in the logs + enable-statistics: true \ No newline at end of file diff --git a/audits/SSV_SIGNER_FINAL_REPORT.pdf b/audits/SSV_SIGNER_FINAL_REPORT.pdf new file mode 100644 index 0000000000..9dfbe94d53 Binary files /dev/null and b/audits/SSV_SIGNER_FINAL_REPORT.pdf differ diff --git a/eth/eventsyncer/event_syncer_test.go b/eth/eventsyncer/event_syncer_test.go index b5e74e64ce..f1312c70a9 100644 --- a/eth/eventsyncer/event_syncer_test.go +++ b/eth/eventsyncer/event_syncer_test.go @@ -55,11 +55,6 @@ func TestEventSyncer(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), testTimeout) defer cancel() - blockStream := make(chan []*ethtypes.Block) - defer close(blockStream) - done := make(chan struct{}) - defer close(done) - // Create sim instance with a delay between block execution sim := simTestBackend(testAddr) diff --git a/eth/executionclient/execution_client.go b/eth/executionclient/execution_client.go index 08006f55ed..8053366e77 100644 --- a/eth/executionclient/execution_client.go +++ b/eth/executionclient/execution_client.go @@ -29,7 +29,6 @@ type Provider interface { FetchHistoricalLogs(ctx context.Context, fromBlock uint64) (logs <-chan BlockLogs, errors <-chan error, err error) StreamLogs(ctx context.Context, fromBlock uint64) <-chan BlockLogs Filterer() (*contract.ContractFilterer, error) - BlockByNumber(ctx context.Context, number *big.Int) (*ethtypes.Block, error) HeaderByNumber(ctx context.Context, blockNumber *big.Int) (*ethtypes.Header, error) ChainID(ctx context.Context) (*big.Int, error) Healthy(ctx context.Context) error @@ -395,18 +394,6 @@ func (ec *ExecutionClient) healthy(ctx context.Context) error { return nil } -func (ec *ExecutionClient) BlockByNumber(ctx context.Context, blockNumber *big.Int) (*ethtypes.Block, error) { - b, err := ec.client.BlockByNumber(ctx, blockNumber) - if err != nil { - ec.logger.Error(elResponseErrMsg, - zap.String("method", "eth_getBlockByNumber"), - zap.Error(err)) - return nil, err - } - - return b, nil -} - func (ec *ExecutionClient) HeaderByNumber(ctx context.Context, blockNumber *big.Int) (*ethtypes.Header, error) { h, err := ec.client.HeaderByNumber(ctx, blockNumber) if err != nil { diff --git a/eth/executionclient/execution_client_test.go b/eth/executionclient/execution_client_test.go index 8b147b03db..cc6b4b8038 100644 --- a/eth/executionclient/execution_client_test.go +++ b/eth/executionclient/execution_client_test.go @@ -1080,62 +1080,6 @@ func TestSubscribeFilterLogs(t *testing.T) { }) } -// TestBlockByNumber tests the BlockByNumber method of the client. -func TestBlockByNumber(t *testing.T) { - logger := zaptest.NewLogger(t) - - t.Run("successfully gets block by number", func(t *testing.T) { - env := setupTestEnv(t, 1*time.Second) - - // Deploy the contract - _, err := env.deployCallableContract() - require.NoError(t, err) - - // Create a client and connect to the simulator - err = env.createClient(WithLogger(logger)) - require.NoError(t, err) - - // Create some blocks - for i := 0; i < 5; i++ { - env.sim.Commit() - } - - // Test the BlockByNumber method with specific block number - block, err := env.client.BlockByNumber(env.ctx, big.NewInt(2)) - require.NoError(t, err) - require.NotNil(t, block) - require.Equal(t, uint64(2), block.NumberU64()) - - // Test the BlockByNumber method with nil (latest block) - latestBlock, err := env.client.BlockByNumber(env.ctx, nil) - require.NoError(t, err) - require.NotNil(t, latestBlock) - require.Equal(t, uint64(6), latestBlock.NumberU64()) // Genesis + 1 from deploy + 5 from loop - }) - - t.Run("error when BlockByNumber fails", func(t *testing.T) { - env := setupTestEnv(t, 1*time.Second) - _, err := env.deployCallableContract() - require.NoError(t, err) - - // Create a client - connection should succeed initially - err = env.createClient( - WithLogger(logger), - WithConnectionTimeout(100*time.Millisecond), - ) - require.NoError(t, err) // Connection is established initially - - // Create a context with a very short timeout to ensure BlockByNumber fails - timeoutCtx, cancel := context.WithTimeout(env.ctx, 1*time.Nanosecond) - defer cancel() - - // BlockByNumber should fail because of the short timeout - block, err := env.client.BlockByNumber(timeoutCtx, big.NewInt(1)) - require.Error(t, err) - require.Nil(t, block) - }) -} - // TestHeaderByNumber tests the HeaderByNumber method of the client. func TestHeaderByNumber(t *testing.T) { logger := zaptest.NewLogger(t) diff --git a/eth/executionclient/mocks.go b/eth/executionclient/mocks.go index 77de25158a..a5d8a2c81e 100644 --- a/eth/executionclient/mocks.go +++ b/eth/executionclient/mocks.go @@ -44,21 +44,6 @@ func (m *MockProvider) EXPECT() *MockProviderMockRecorder { return m.recorder } -// BlockByNumber mocks base method. -func (m *MockProvider) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BlockByNumber", ctx, number) - ret0, _ := ret[0].(*types.Block) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// BlockByNumber indicates an expected call of BlockByNumber. -func (mr *MockProviderMockRecorder) BlockByNumber(ctx, number any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockByNumber", reflect.TypeOf((*MockProvider)(nil).BlockByNumber), ctx, number) -} - // ChainID mocks base method. func (m *MockProvider) ChainID(ctx context.Context) (*big.Int, error) { m.ctrl.T.Helper() @@ -216,21 +201,6 @@ func (m *MockSingleClientProvider) EXPECT() *MockSingleClientProviderMockRecorde return m.recorder } -// BlockByNumber mocks base method. -func (m *MockSingleClientProvider) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BlockByNumber", ctx, number) - ret0, _ := ret[0].(*types.Block) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// BlockByNumber indicates an expected call of BlockByNumber. -func (mr *MockSingleClientProviderMockRecorder) BlockByNumber(ctx, number any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockByNumber", reflect.TypeOf((*MockSingleClientProvider)(nil).BlockByNumber), ctx, number) -} - // ChainID mocks base method. func (m *MockSingleClientProvider) ChainID(ctx context.Context) (*big.Int, error) { m.ctrl.T.Helper() diff --git a/eth/executionclient/multi_client.go b/eth/executionclient/multi_client.go index c15be18947..45f219fa2c 100644 --- a/eth/executionclient/multi_client.go +++ b/eth/executionclient/multi_client.go @@ -335,19 +335,6 @@ func (mc *MultiClient) Healthy(ctx context.Context) error { return fmt.Errorf("no healthy clients: %w", err) } -// BlockByNumber retrieves a block by its number. -func (mc *MultiClient) BlockByNumber(ctx context.Context, blockNumber *big.Int) (*ethtypes.Block, error) { - f := func(client SingleClientProvider) (any, error) { - return client.BlockByNumber(ctx, blockNumber) - } - res, err := mc.call(contextWithMethod(ctx, "BlockByNumber"), f, len(mc.clients)) - if err != nil { - return nil, err - } - - return res.(*ethtypes.Block), nil -} - // HeaderByNumber retrieves a block header by its number. func (mc *MultiClient) HeaderByNumber(ctx context.Context, blockNumber *big.Int) (*ethtypes.Header, error) { f := func(client SingleClientProvider) (any, error) { diff --git a/eth/executionclient/multi_client_test.go b/eth/executionclient/multi_client_test.go index f47359ba40..98f8a33762 100644 --- a/eth/executionclient/multi_client_test.go +++ b/eth/executionclient/multi_client_test.go @@ -919,57 +919,6 @@ func TestMultiClient_Healthy_AllClientsUnhealthy(t *testing.T) { require.Contains(t, err.Error(), "client2 unhealthy") } -func TestMultiClient_BlockByNumber(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockClient := NewMockSingleClientProvider(ctrl) - - mockClient. - EXPECT(). - BlockByNumber(gomock.Any(), big.NewInt(1234)). - Return(ðtypes.Block{}, nil). - Times(1) - - mc := &MultiClient{ - nodeAddrs: []string{"mock1"}, - clients: []SingleClientProvider{mockClient}, - clientsMu: make([]sync.Mutex, 1), - logger: zap.NewNop(), - closed: make(chan struct{}), - } - - blk, err := mc.BlockByNumber(t.Context(), big.NewInt(1234)) - require.NoError(t, err) - require.NotNil(t, blk) -} - -func TestMultiClient_BlockByNumber_Error(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockClient := NewMockSingleClientProvider(ctrl) - - mockClient. - EXPECT(). - BlockByNumber(gomock.Any(), big.NewInt(1234)). - Return((*ethtypes.Block)(nil), fmt.Errorf("block not found")). - Times(1) - - mc := &MultiClient{ - nodeAddrs: []string{"mock1"}, - clients: []SingleClientProvider{mockClient}, - clientsMu: make([]sync.Mutex, 1), - logger: zap.NewNop(), - closed: make(chan struct{}), - } - - blk, err := mc.BlockByNumber(t.Context(), big.NewInt(1234)) - require.Error(t, err) - require.Nil(t, blk) - require.Contains(t, err.Error(), "block not found") -} - func TestMultiClient_HeaderByNumber(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -1307,9 +1256,9 @@ func TestMultiClient_Call_Concurrency(t *testing.T) { mockClient. EXPECT(). - BlockByNumber(gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, num *big.Int) (*ethtypes.Block, error) { - return ðtypes.Block{}, nil + HeaderByNumber(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, num *big.Int) (*ethtypes.Header, error) { + return ðtypes.Header{}, nil }). Times(10) @@ -1327,7 +1276,7 @@ func TestMultiClient_Call_Concurrency(t *testing.T) { for i := 0; i < 10; i++ { go func() { defer wg.Done() - _, err := mc.BlockByNumber(t.Context(), big.NewInt(1234)) + _, err := mc.HeaderByNumber(t.Context(), big.NewInt(1234)) require.NoError(t, err) }() } diff --git a/networkconfig/hoodi.go b/networkconfig/hoodi.go index b31c010f58..a7d605db09 100644 --- a/networkconfig/hoodi.go +++ b/networkconfig/hoodi.go @@ -20,5 +20,5 @@ var HoodiSSV = &SSVConfig{ "enr:-Ja4QIKlyNFuFtTOnVoavqwmpgSJXfhSmhpdSDOUhf5-FBr7bBxQRvG6VrpUvlkr8MtpNNuMAkM33AseduSaOhd9IeWGAZWjRbnvgmlkgnY0gmlwhCNVVTCJc2VjcDI1NmsxoQNTTyiJPoZh502xOZpHSHAfR-94NaXLvi5J4CNHMh2tjoNzc3YBg3RjcIITioN1ZHCCD6I", }, TotalEthereumValidators: 1107955, // active_validators from https://hoodi.beaconcha.in/index/data on Apr 18, 2025 - GasLimit36Epoch: 0, + GasLimit36Epoch: 29000, // Jul-24-2025 09:30:00 AM UTC } diff --git a/networkconfig/mainnet.go b/networkconfig/mainnet.go index 92cdd4b7a3..48e30d9809 100644 --- a/networkconfig/mainnet.go +++ b/networkconfig/mainnet.go @@ -1,10 +1,8 @@ package networkconfig import ( - "math" "math/big" - "github.com/attestantio/go-eth2-client/spec/phase0" ethcommon "github.com/ethereum/go-ethereum/common" spectypes "github.com/ssvlabs/ssv-spec/types" @@ -22,7 +20,7 @@ var MainnetSSV = &SSVConfig{ "enr:-Ja4QAbDe5XANqJUDyJU1GmtS01qqMwDYx9JNZgymjBb55fMaha80E2HznRYoUGy6NFVSvs1u1cFqSM0MgJI-h1QKLeGAZKaTo7LgmlkgnY0gmlwhDQrfraJc2VjcDI1NmsxoQNEj0Pgq9-VxfeX83LPDOUPyWiTVzdI-DnfMdO1n468u4Nzc3YBg3RjcIITioN1ZHCCD6I", // 0NEinfra bootnode - "enr:-Li4QDwrOuhEq5gBJBzFUPkezoYiy56SXZUwkSD7bxYo8RAhPnHyS0de0nOQrzl-cL47RY9Jg8k6Y_MgaUd9a5baYXeGAYnfZE76h2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhDaTS0mJc2VjcDI1NmsxoQMZzUHaN3eClRgF9NAqRNc-ilGpJDDJxdenfo4j-zWKKYN0Y3CCE4iDdWRwgg-g", + "enr:-Ja4QFpDBTMnLOykFvZsV8LnBibOQnhAgsN2SkOXApEcRbxyJNkHO9go3gonVUIREbUa0gHvzYdgzZ3U4ezmCyJrkI6GAZfeUw2QgmlkgnY0gmlwhId9yoSJc2VjcDI1NmsxoQMZzUHaN3eClRgF9NAqRNc-ilGpJDDJxdenfo4j-zWKKYNzc3YBg3RjcIITiIN1ZHCCD6A", // Eridian (eridianalpha.com) "enr:-Li4QIzHQ2H82twhvsu8EePZ6CA1gl0_B0WWsKaT07245TkHUqXay-MXEgObJB7BxMFl8TylFxfnKNxQyGTXh-2nAlOGAYuraxUEh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBKCzUSJc2VjcDI1NmsxoQNKskkQ6-mBdBWr_ORJfyHai5uD0vL6Fuw90X0sPwmRsoN0Y3CCE4iDdWRwgg-g", @@ -31,6 +29,5 @@ var MainnetSSV = &SSVConfig{ "enr:-Li4QH7FwJcL8gJj0zHAITXqghMkG-A5bfWh2-3Q7vosy9D1BS8HZk-1ITuhK_rfzG3v_UtBDI6uNJZWpdcWfrQFCxKGAYnQ1DRCh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLb3g2Jc2VjcDI1NmsxoQKeSDcZWSaY9FC723E9yYX1Li18bswhLNlxBZdLfgOKp4N0Y3CCE4mDdWRwgg-h", }, TotalEthereumValidators: 1064860, // active_validators from https://mainnet.beaconcha.in/index/data on Apr 18, 2025 - // TODO - set proper value for mainnet - GasLimit36Epoch: phase0.Epoch(math.MaxUint64), + GasLimit36Epoch: 385150, // Aug-09-2025 06:40:23 AM UTC } diff --git a/operator/duties/scheduler.go b/operator/duties/scheduler.go index 0f263bc1b1..331e3935a2 100644 --- a/operator/duties/scheduler.go +++ b/operator/duties/scheduler.go @@ -59,7 +59,7 @@ type BeaconNode interface { } type ExecutionClient interface { - BlockByNumber(ctx context.Context, blockNumber *big.Int) (*ethtypes.Block, error) + HeaderByNumber(ctx context.Context, blockNumber *big.Int) (*ethtypes.Header, error) } // ValidatorProvider represents the component that controls validators via the scheduler diff --git a/operator/duties/scheduler_mock.go b/operator/duties/scheduler_mock.go index 0d4d0b8140..e813a65974 100644 --- a/operator/duties/scheduler_mock.go +++ b/operator/duties/scheduler_mock.go @@ -254,19 +254,19 @@ func (m *MockExecutionClient) EXPECT() *MockExecutionClientMockRecorder { return m.recorder } -// BlockByNumber mocks base method. -func (m *MockExecutionClient) BlockByNumber(ctx context.Context, blockNumber *big.Int) (*types.Block, error) { +// HeaderByNumber mocks base method. +func (m *MockExecutionClient) HeaderByNumber(ctx context.Context, blockNumber *big.Int) (*types.Header, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BlockByNumber", ctx, blockNumber) - ret0, _ := ret[0].(*types.Block) + ret := m.ctrl.Call(m, "HeaderByNumber", ctx, blockNumber) + ret0, _ := ret[0].(*types.Header) ret1, _ := ret[1].(error) return ret0, ret1 } -// BlockByNumber indicates an expected call of BlockByNumber. -func (mr *MockExecutionClientMockRecorder) BlockByNumber(ctx, blockNumber any) *gomock.Call { +// HeaderByNumber indicates an expected call of HeaderByNumber. +func (mr *MockExecutionClientMockRecorder) HeaderByNumber(ctx, blockNumber any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockByNumber", reflect.TypeOf((*MockExecutionClient)(nil).BlockByNumber), ctx, blockNumber) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeaderByNumber", reflect.TypeOf((*MockExecutionClient)(nil).HeaderByNumber), ctx, blockNumber) } // MockValidatorProvider is a mock of ValidatorProvider interface. diff --git a/operator/duties/voluntary_exit.go b/operator/duties/voluntary_exit.go index 541d413166..8010a820b8 100644 --- a/operator/duties/voluntary_exit.go +++ b/operator/duties/voluntary_exit.go @@ -144,12 +144,12 @@ func (h *VoluntaryExitHandler) blockSlot(ctx context.Context, blockNumber uint64 return blockSlot, nil } - block, err := h.executionClient.BlockByNumber(ctx, new(big.Int).SetUint64(blockNumber)) + header, err := h.executionClient.HeaderByNumber(ctx, new(big.Int).SetUint64(blockNumber)) if err != nil { return 0, err } - blockSlot = h.beaconConfig.EstimatedSlotAtTime(time.Unix(int64(block.Time()), 0)) // #nosec G115 + blockSlot = h.beaconConfig.EstimatedSlotAtTime(time.Unix(int64(header.Time), 0)) // #nosec G115 h.blockSlots[blockNumber] = blockSlot for k, v := range h.blockSlots { diff --git a/operator/duties/voluntary_exit_test.go b/operator/duties/voluntary_exit_test.go index a10ab172ef..d17378fa89 100644 --- a/operator/duties/voluntary_exit_test.go +++ b/operator/duties/voluntary_exit_test.go @@ -9,7 +9,6 @@ import ( "github.com/attestantio/go-eth2-client/spec/phase0" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/trie" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -143,13 +142,12 @@ func TestVoluntaryExitHandler_HandleDuties(t *testing.T) { } func create1to1BlockSlotMapping(scheduler *Scheduler) *atomic.Uint64 { - var blockByNumberCalls atomic.Uint64 + var headerByNumberCalls atomic.Uint64 - scheduler.executionClient.(*MockExecutionClient).EXPECT().BlockByNumber(gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, blockNumber *big.Int) (*ethtypes.Block, error) { - blockByNumberCalls.Add(1) - expectedBlock := ethtypes.NewBlock(ðtypes.Header{Time: blockNumber.Uint64()}, nil, nil, trie.NewStackTrie(nil)) - return expectedBlock, nil + scheduler.executionClient.(*MockExecutionClient).EXPECT().HeaderByNumber(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, blockNumber *big.Int) (*ethtypes.Header, error) { + headerByNumberCalls.Add(1) + return ðtypes.Header{Time: blockNumber.Uint64()}, nil }, ).AnyTimes() scheduler.beaconConfig.(*networkconfig.MockBeacon).EXPECT().EstimatedSlotAtTime(gomock.Any()).DoAndReturn( @@ -158,17 +156,17 @@ func create1to1BlockSlotMapping(scheduler *Scheduler) *atomic.Uint64 { }, ).AnyTimes() - return &blockByNumberCalls + return &headerByNumberCalls } func assert1to1BlockSlotMapping(t *testing.T, scheduler *Scheduler) { const blockNumber = 123 - block, err := scheduler.executionClient.BlockByNumber(context.TODO(), new(big.Int).SetUint64(blockNumber)) + header, err := scheduler.executionClient.HeaderByNumber(context.TODO(), new(big.Int).SetUint64(blockNumber)) require.NoError(t, err) - require.NotNil(t, block) + require.NotNil(t, header) - slot := scheduler.beaconConfig.EstimatedSlotAtTime(time.Unix(int64(block.Time()), 0)) + slot := scheduler.beaconConfig.EstimatedSlotAtTime(time.Unix(int64(header.Time), 0)) require.EqualValues(t, blockNumber, slot) } diff --git a/protocol/v2/ssv/runner/validator_registration.go b/protocol/v2/ssv/runner/validator_registration.go index d49290c2db..dd77f1c748 100644 --- a/protocol/v2/ssv/runner/validator_registration.go +++ b/protocol/v2/ssv/runner/validator_registration.go @@ -12,13 +12,12 @@ import ( "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "github.com/pkg/errors" + specqbft "github.com/ssvlabs/ssv-spec/qbft" + spectypes "github.com/ssvlabs/ssv-spec/types" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" - specqbft "github.com/ssvlabs/ssv-spec/qbft" - spectypes "github.com/ssvlabs/ssv-spec/types" - "github.com/ssvlabs/ssv/ssvsigner/ekm" "github.com/ssvlabs/ssv/logging/fields" @@ -28,6 +27,11 @@ import ( ssvtypes "github.com/ssvlabs/ssv/protocol/v2/types" ) +const ( + DefaultGasLimit = uint64(36_000_000) + DefaultGasLimitOld = uint64(30_000_000) +) + type ValidatorRegistrationRunner struct { BaseRunner *BaseRunner @@ -258,9 +262,20 @@ func (r *ValidatorRegistrationRunner) calculateValidatorRegistration(slot phase0 epoch := r.BaseRunner.NetworkConfig.EstimatedEpochAtSlot(slot) + // Set the default GasLimit value if it hasn't been specified already, use 36 or 30 depending + // on the current epoch as compared to when this transition is supposed to happen. + gasLimit := r.gasLimit + if gasLimit == 0 { + defaultGasLimit := DefaultGasLimit + if r.BaseRunner.NetworkConfig.EstimatedCurrentEpoch() < r.BaseRunner.NetworkConfig.GetGasLimit36Epoch() { + defaultGasLimit = DefaultGasLimitOld + } + gasLimit = defaultGasLimit + } + return &v1.ValidatorRegistration{ FeeRecipient: share.FeeRecipientAddress, - GasLimit: r.gasLimit, + GasLimit: gasLimit, Timestamp: r.BaseRunner.NetworkConfig.EpochStartTime(epoch), Pubkey: pk, }, nil diff --git a/protocol/v2/ssv/testing/runner.go b/protocol/v2/ssv/testing/runner.go index 1887cc02ed..6c3f6a5066 100644 --- a/protocol/v2/ssv/testing/runner.go +++ b/protocol/v2/ssv/testing/runner.go @@ -5,11 +5,10 @@ import ( "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" - "go.uber.org/zap" - specqbft "github.com/ssvlabs/ssv-spec/qbft" spectypes "github.com/ssvlabs/ssv-spec/types" spectestingutils "github.com/ssvlabs/ssv-spec/types/testingutils" + "go.uber.org/zap" "github.com/ssvlabs/ssv/ssvsigner/ekm" @@ -182,7 +181,7 @@ var ConstructBaseRunner = func( net, km, opSigner, - validator.DefaultGasLimitOld, + runner.DefaultGasLimitOld, ) case spectypes.RoleVoluntaryExit: r, err = runner.NewVoluntaryExitRunner( @@ -434,7 +433,7 @@ var ConstructBaseRunnerWithShareMap = func( net, km, opSigner, - validator.DefaultGasLimitOld, + runner.DefaultGasLimitOld, ) case spectypes.RoleVoluntaryExit: r, err = runner.NewVoluntaryExitRunner( diff --git a/protocol/v2/ssv/validator/opts.go b/protocol/v2/ssv/validator/opts.go index 41a03f5f5e..fe32766a28 100644 --- a/protocol/v2/ssv/validator/opts.go +++ b/protocol/v2/ssv/validator/opts.go @@ -20,9 +20,6 @@ import ( const ( DefaultQueueSize = 32 - - DefaultGasLimit = uint64(36_000_000) - DefaultGasLimitOld = uint64(30_000_000) ) // Options represents validator-specific options. @@ -93,16 +90,6 @@ func NewCommonOptions( result.QueueSize = max(result.QueueSize, historySyncBatchSize*2) } - // Set the default GasLimit value if it hasn't been specified already, use 36 or 30 depending - // on the current epoch as compared to when this transition is supposed to happen. - if result.GasLimit == 0 { - defaultGasLimit := DefaultGasLimit - if result.NetworkConfig.EstimatedCurrentEpoch() < result.NetworkConfig.GetGasLimit36Epoch() { - defaultGasLimit = DefaultGasLimitOld - } - result.GasLimit = defaultGasLimit - } - return result } diff --git a/scripts/spec-alignment/differ.config.yaml b/scripts/spec-alignment/differ.config.yaml index 7870eeff94..07070995b8 100644 --- a/scripts/spec-alignment/differ.config.yaml +++ b/scripts/spec-alignment/differ.config.yaml @@ -1,4 +1,4 @@ -ApprovedChanges: ["34e8b7999dfb14b5","56ceb03cd44ff702","ccc48c575f7ab4eb","262a19d18869cedd","e710070bfb15d3ab","1c0da45940bfa76f","881fc8fbb6a4edb1","31bc6100f3d4db08","c7ce0261a1b6014f","c4c4caa5d0938b85","60f1d510640499ac","91073f39440ab37f","d308bd7c553ccdcf","bdaf172971637cbe","396137c9cb425893","d4fef6512374c1f5","1bbcbe4e8194370d","59b2375130aef5df","1bd46741a98043b0","417e005d18c1f7d4","14dd874e7df81b37","50cea598241b0bc9","3ddc92a39c324d69","b0934079dcd986cc","e27b9c1ea1c64357","fe62097e8814c106","74a928f5dcb2fdd9","cbbfdb5e68cdac80","6577bf381d78bae2","39ea06bfd1477d2d","7e2550bab51f22b2","87ebd29bd49fc52f","ef39dd5223e0d080","aebb8e4348b6d667","6146023d4d5708a2","fe14e7f0503ea188","fb4cac598a68c592","257c7eb81d6eb245","62547d1b32ce44e3","5be5c4ce7d3212da","960a9c64cd4ec93c","df3771e2589008f9","c8f122c9fb83793e","2c6054db7088bcef","5714652b88e2d44f","7a53b3b037c56325","11587bf00d05a4e2","601862214795ec47","b523eed171e5b6bf","5797f56a84b7ebef","81a60407a3a0ba80","38faed802f80914b","bcb381e843ace421","1067c9a5622c3508","4b58762c0b433442","d293ec1bc61bb707","3e88c3b49d093605","4890ff80c88cc41d","9b70726386e38170","8bbf7eba3fa0cf7e","8cc38593ebe049b6","ef7b63d9848dc185","7f816e2f33da3186","2314d7fa0b387cff","267886c6b07733d7","f9480b2e48319466","98c46ae55d1d4e39","edfd442b4d725fbb","122f053573538a32","d720d714a20833e1","a81c092c985de728","143c7821cd65bcac","9e53c73ee60b1cc2","9d265e99dd31d4f5","70d29c9e7a4ef797","f2972b2906912d3a","c411b0f9f069c032","a037c260bd97c7d2","96631f77414215f5","389037ae98d33f26","b3e7d9382b320d70","3426003abbd5ebf0","22b9026cb0221ce0","e07da5b414afe6a6","50e62cbde34516ee","57c1885b43dd8d19","e8a49856a5edd893","9273478f6feccd62","1cfd3a3660a62879","859884a25fccea80","8651bdc20c1946f3","5a44c960cc4178dd","108b9575f7c1d4bc","5499865be87b7e00","159536003eeddac8","a20ea51df8b7d65b","9133b167cc5a601d","a748ad5c74850749","16ebe47404323cc1","48bfe5cf1e578b47","4366899a2cb05197","730c3e5e59393b7d","5b44a4b425ecc397","df5debc50ec8babc","92a41554b2910bb8","c36c680554dde59f","f265acf7423fa9b1","447feaa5cdc1a010","8ac6baea1b755066","2bc023c7e062f24b","274336ec1127e6c0","ddf404c2905d3b26","8c43f241aeac6d75","832fc9a9a2b71d00","8850900b5d9bcc65","cc22f28953b787ea","3bad6ae11596a574","8f84422a240d889c","82b392ba39c6c594","7975821460ebe1e7","173c505e12aabb8f","47ee0d148148a56f","6707ecfefa5fec21","d5a7389d730464f1","8e4ec8debe331b36","875393173d59b0f2","943be3ce709a99d3","4e22a08543b079b","f66dbe09241b5a4","fd6796ea6d131c3","634abeefa604ee2","e939b25394581c4","6ccbcb02e457e66","e1d82f0619360a5","5a7ad98296703f6","f4ce01b385c68a2","90aac1feca751b0","678c366df2dd8302","902874f1439b6cbe","6fc8a4fe6e10380e","9a904d77eccac8c4","3c68f86d18282872","4cbea5ccb3ba96f9","ad53997a7af4a476","43ca8640bc136b04","b7a9dc3af09265f2","9e33f2c164150c03","8c8fdd2a8066a8a7","78ac98e0a065deb2","3cd8818a0517abfb","80b8aa081b93ec8b","e781436db8f6a346","21b8868ce00f6261","c4f7f025e7a437ff","1656e3e33948c0d3","a2aab6b86cefe217","97df26235b1eabee","a5202339e0e86850","261d0458cc8b956f","a1efe4ea2b85117","3b53ec55fa55f1df","e92e2c9af55c0a91","db32c6c8ef7b0cf6","1f578f6799e515c1","4fb205310012cc98","9031993dbbed4646","12dd40a163e41548","547bf66f36d25667","1961f69f1787dc5d","3e1e15e5707ad393","1a0e96bf6922218d","63091d22982d70a9","cbafb1c3b188dbd9", "5d954981612100e0","5eb948ff56039ff1","56276561b0c3d933","2ba698695e4fcb6","12dd40a163e41548","e933dbb89bc159ff","56276561b0c3d933","adfc83ab2b7529e2","e397d10800b51844","510f812787530ce5","79d524c526c3202e","615382c9e22d5772","862816d5dd5f9650","76c7a4b27d6dd1ab", "a89f217d08ceef40", "9698d9b7425e5e21", "c217ff38383363c8", "8ce8a758b93cfd3f", "181c96f033f99b2b", "d59992d5de732920", "b0de1d86ee844b43", "32e5196e8623d4e0", "40db73ae9746149f", "93192e1f7dd7f8ab", "3d9d307db16b7a1f","9b18244fb96341a4","7b6444cbaf195b71","2428357c5baa54e3","2f42d05f686ceac5","c5af65609c08034e"] +ApprovedChanges: ["34e8b7999dfb14b5","56ceb03cd44ff702","ccc48c575f7ab4eb","262a19d18869cedd","e710070bfb15d3ab","1c0da45940bfa76f","881fc8fbb6a4edb1","31bc6100f3d4db08","c7ce0261a1b6014f","c4c4caa5d0938b85","60f1d510640499ac","91073f39440ab37f","d308bd7c553ccdcf","bdaf172971637cbe","396137c9cb425893","d4fef6512374c1f5","1bbcbe4e8194370d","59b2375130aef5df","1bd46741a98043b0","417e005d18c1f7d4","14dd874e7df81b37","50cea598241b0bc9","3ddc92a39c324d69","b0934079dcd986cc","e27b9c1ea1c64357","fe62097e8814c106","74a928f5dcb2fdd9","cbbfdb5e68cdac80","6577bf381d78bae2","39ea06bfd1477d2d","7e2550bab51f22b2","87ebd29bd49fc52f","ef39dd5223e0d080","aebb8e4348b6d667","6146023d4d5708a2","fe14e7f0503ea188","fb4cac598a68c592","257c7eb81d6eb245","62547d1b32ce44e3","5be5c4ce7d3212da","960a9c64cd4ec93c","df3771e2589008f9","c8f122c9fb83793e","2c6054db7088bcef","5714652b88e2d44f","7a53b3b037c56325","11587bf00d05a4e2","601862214795ec47","b523eed171e5b6bf","5797f56a84b7ebef","81a60407a3a0ba80","38faed802f80914b","bcb381e843ace421","1067c9a5622c3508","4b58762c0b433442","d293ec1bc61bb707","3e88c3b49d093605","4890ff80c88cc41d","9b70726386e38170","8bbf7eba3fa0cf7e","8cc38593ebe049b6","ef7b63d9848dc185","7f816e2f33da3186","2314d7fa0b387cff","267886c6b07733d7","f9480b2e48319466","98c46ae55d1d4e39","edfd442b4d725fbb","122f053573538a32","d720d714a20833e1","a81c092c985de728","143c7821cd65bcac","9e53c73ee60b1cc2","9d265e99dd31d4f5","70d29c9e7a4ef797","f2972b2906912d3a","c411b0f9f069c032","a037c260bd97c7d2","96631f77414215f5","389037ae98d33f26","b3e7d9382b320d70","3426003abbd5ebf0","22b9026cb0221ce0","e07da5b414afe6a6","50e62cbde34516ee","57c1885b43dd8d19","e8a49856a5edd893","9273478f6feccd62","1cfd3a3660a62879","859884a25fccea80","8651bdc20c1946f3","5a44c960cc4178dd","108b9575f7c1d4bc","5499865be87b7e00","159536003eeddac8","a20ea51df8b7d65b","9133b167cc5a601d","a748ad5c74850749","16ebe47404323cc1","48bfe5cf1e578b47","4366899a2cb05197","730c3e5e59393b7d","5b44a4b425ecc397","df5debc50ec8babc","92a41554b2910bb8","c36c680554dde59f","f265acf7423fa9b1","447feaa5cdc1a010","8ac6baea1b755066","2bc023c7e062f24b","274336ec1127e6c0","ddf404c2905d3b26","8c43f241aeac6d75","832fc9a9a2b71d00","8850900b5d9bcc65","cc22f28953b787ea","3bad6ae11596a574","8f84422a240d889c","82b392ba39c6c594","7975821460ebe1e7","173c505e12aabb8f","47ee0d148148a56f","6707ecfefa5fec21","d5a7389d730464f1","8e4ec8debe331b36","875393173d59b0f2","943be3ce709a99d3","4e22a08543b079b","f66dbe09241b5a4","fd6796ea6d131c3","634abeefa604ee2","e939b25394581c4","6ccbcb02e457e66","e1d82f0619360a5","5a7ad98296703f6","f4ce01b385c68a2","90aac1feca751b0","678c366df2dd8302","902874f1439b6cbe","6fc8a4fe6e10380e","9a904d77eccac8c4","3c68f86d18282872","4cbea5ccb3ba96f9","ad53997a7af4a476","43ca8640bc136b04","b7a9dc3af09265f2","9e33f2c164150c03","8c8fdd2a8066a8a7","78ac98e0a065deb2","3cd8818a0517abfb","80b8aa081b93ec8b","e781436db8f6a346","21b8868ce00f6261","c4f7f025e7a437ff","1656e3e33948c0d3","a2aab6b86cefe217","97df26235b1eabee","a5202339e0e86850","261d0458cc8b956f","a1efe4ea2b85117","3b53ec55fa55f1df","e92e2c9af55c0a91","db32c6c8ef7b0cf6","1f578f6799e515c1","4fb205310012cc98","9031993dbbed4646","12dd40a163e41548","547bf66f36d25667","1961f69f1787dc5d","3e1e15e5707ad393","1a0e96bf6922218d","63091d22982d70a9","cbafb1c3b188dbd9", "5d954981612100e0","5eb948ff56039ff1","56276561b0c3d933","2ba698695e4fcb6","12dd40a163e41548","e933dbb89bc159ff","56276561b0c3d933","adfc83ab2b7529e2","e397d10800b51844","510f812787530ce5","79d524c526c3202e","615382c9e22d5772","862816d5dd5f9650","76c7a4b27d6dd1ab", "a89f217d08ceef40", "9698d9b7425e5e21", "c217ff38383363c8", "8ce8a758b93cfd3f", "181c96f033f99b2b", "d59992d5de732920", "b0de1d86ee844b43", "32e5196e8623d4e0", "40db73ae9746149f", "93192e1f7dd7f8ab", "3d9d307db16b7a1f","9b18244fb96341a4","7b6444cbaf195b71","2428357c5baa54e3","2f42d05f686ceac5","c5af65609c08034e","48621d3cb9e3daf3"] IgnoredIdentifiers: - logger @@ -32,7 +32,7 @@ Comparisons: - ./qbft Hints: BaseCommitValidation: baseCommitValidation - + - Packages: Left: - ./protocol/v2/types diff --git a/ssvsigner/README.md b/ssvsigner/README.md index 6544e713d7..efd63b7f41 100644 --- a/ssvsigner/README.md +++ b/ssvsigner/README.md @@ -402,6 +402,11 @@ database, cleaning keys in Web3Signer is required to ensure all shares are prope to implement a robust backup and recovery strategy for their databases (PostgreSQL for Web3Signer, or the node's local database for local signing setups). Failure to maintain database backups can lead to significant financial loss. Operators are responsible for their own database management and protection. +5. **Local File Read Protection**: We recommend restricting ssv-signer's permissions at the OS level to read files only + from its own directory to avoid possible attacks if the service is compromised. +6. **SSRF Protection**: We recommend restricting ssv-signer's access to networks at the infrastructure level, allowing + only access to the Web3Signer service endpoint. Consider restricting access to all local and private addresses + to prevent attacks on the infrastructure. ## Performance Considerations diff --git a/ssvsigner/e2e/testenv/containers.go b/ssvsigner/e2e/testenv/containers.go index 27d32cc446..55396a3c8b 100644 --- a/ssvsigner/e2e/testenv/containers.go +++ b/ssvsigner/e2e/testenv/containers.go @@ -2,6 +2,7 @@ package testenv import ( "context" + "crypto/tls" "database/sql" "fmt" "os" @@ -156,9 +157,9 @@ func (env *TestEnvironment) startWeb3Signer() error { Cmd: []string{ "--http-listen-host=0.0.0.0", "--http-host-allowlist=*", - "--tls-keystore-file=/certs/localhost.p12", - "--tls-keystore-password-file=/certs/localhost_password.txt", - "--tls-allow-any-client=true", + "--tls-keystore-file=/certs/web3signer.p12", + "--tls-keystore-password-file=/certs/web3signer_password.txt", + "--tls-known-clients-file=/certs/web3signer_known_clients.txt", "eth2", "--network=mainnet", "--slashing-protection-enabled=true", @@ -192,7 +193,7 @@ func (env *TestEnvironment) startWeb3Signer() error { env.web3SignerURL = fmt.Sprintf("https://%s:%s", host, mappedPort.Port()) - tlsConfig, err := createTrustedTLSConfig(env.web3SignerCertPath) + tlsConfig, err := createMutualTLSConfig(env.web3SignerCertPath, env.e2eClientCertPath) if err != nil { return fmt.Errorf("failed to create TLS config: %w", err) } @@ -203,7 +204,7 @@ func (env *TestEnvironment) startWeb3Signer() error { // web3SignerWaitStrategy returns the wait strategy for Web3Signer func (env *TestEnvironment) web3SignerWaitStrategy() (wait.Strategy, error) { - tlsConfig, err := createTrustedTLSConfig(env.web3SignerCertPath) + tlsConfig, err := createMutualTLSConfig(env.web3SignerCertPath, env.e2eClientCertPath) if err != nil { return nil, fmt.Errorf("failed to create TLS config for wait strategy: %w", err) } @@ -226,7 +227,12 @@ func (env *TestEnvironment) startSSVSigner() error { return fmt.Errorf("certificate directory does not exist: %s", env.certDir) } - waitStrategy, err := env.ssvSignerWaitStrategy() + tlsConfig, err := createMutualTLSConfig(env.ssvSignerCertPath, env.e2eClientCertPath) + if err != nil { + return fmt.Errorf("failed to create TLS config for SSV-Signer client: %w", err) + } + + waitStrategy, err := env.ssvSignerWaitStrategy(tlsConfig) if err != nil { return fmt.Errorf("failed to create wait strategy: %w", err) } @@ -253,9 +259,9 @@ func (env *TestEnvironment) startSSVSigner() error { "KNOWN_CLIENTS_FILE": "/certs/known_clients.txt", // Client TLS for Web3Signer - "WEB3SIGNER_KEYSTORE_FILE": "/certs/localhost.p12", - "WEB3SIGNER_KEYSTORE_PASSWORD_FILE": "/certs/localhost_password.txt", - "WEB3SIGNER_SERVER_CERT_FILE": "/certs/localhost.crt", + "WEB3SIGNER_KEYSTORE_FILE": "/certs/ssv-signer.p12", + "WEB3SIGNER_KEYSTORE_PASSWORD_FILE": "/certs/ssv-signer_password.txt", + "WEB3SIGNER_SERVER_CERT_FILE": "/certs/web3signer.crt", }, HostConfigModifier: func(hostConfig *container.HostConfig) { hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{ @@ -287,23 +293,13 @@ func (env *TestEnvironment) startSSVSigner() error { } env.ssvSignerURL = fmt.Sprintf("https://%s:%s", host, mappedPort.Port()) - - tlsConfig, err := createTrustedTLSConfig(env.ssvSignerCertPath) - if err != nil { - return fmt.Errorf("failed to create TLS config for SSV-Signer client: %w", err) - } env.ssvSignerClient = ssvsigner.NewClient(env.ssvSignerURL, ssvsigner.WithTLSConfig(tlsConfig)) return nil } // ssvSignerWaitStrategy returns the wait strategy for SSV-Signer -func (env *TestEnvironment) ssvSignerWaitStrategy() (wait.Strategy, error) { - tlsConfig, err := createTrustedTLSConfig(env.ssvSignerCertPath) - if err != nil { - return nil, fmt.Errorf("failed to create TLS config for wait strategy: %w", err) - } - +func (env *TestEnvironment) ssvSignerWaitStrategy(tlsConfig *tls.Config) (wait.Strategy, error) { return wait.ForHTTP(ssvsigner.PathOperatorIdentity). WithPort("8080/tcp"). WithTLS(true, tlsConfig). diff --git a/ssvsigner/e2e/testenv/environment.go b/ssvsigner/e2e/testenv/environment.go index dd3613f7ae..00bee39d4f 100644 --- a/ssvsigner/e2e/testenv/environment.go +++ b/ssvsigner/e2e/testenv/environment.go @@ -61,9 +61,8 @@ type TestEnvironment struct { // TLS certificates certDir string web3SignerCertPath string - web3SignerKeyPath string ssvSignerCertPath string - ssvSignerKeyPath string + e2eClientCertPath string // Operator key for validator encryption operatorKey keys.OperatorPrivateKey diff --git a/ssvsigner/e2e/testenv/tls.go b/ssvsigner/e2e/testenv/tls.go index 61045f9aee..b43ecdbeca 100644 --- a/ssvsigner/e2e/testenv/tls.go +++ b/ssvsigner/e2e/testenv/tls.go @@ -3,9 +3,11 @@ package testenv import ( "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "encoding/hex" "encoding/pem" "fmt" "math/big" @@ -33,7 +35,6 @@ func generateSelfSignedCert(commonName string, dnsNames []string) ([]byte, []byt return nil, nil, fmt.Errorf("failed to generate private key: %w", err) } - // Generate unique serial number based on current time serialNumber := big.NewInt(time.Now().UnixNano()) template := x509.Certificate{ @@ -98,34 +99,35 @@ func (env *TestEnvironment) setupTLSCertificates() error { return fmt.Errorf("failed to get current working directory: %w", err) } env.certDir = filepath.Join(cwd, ".tmp", "e2e-certs-"+randomSuffix()) - if err := os.MkdirAll(env.certDir, 0750); err != nil { + if err := os.MkdirAll(env.certDir, dirMode); err != nil { return fmt.Errorf("failed to create cert directory: %w", err) } type certConfig struct { name string dnsNames []string certPath *string - keyPath *string - needP12 bool } certConfigs := []certConfig{ { - name: "localhost", - dnsNames: []string{"localhost", "web3signer"}, + name: "web3signer", + dnsNames: []string{"web3signer"}, certPath: &env.web3SignerCertPath, - keyPath: &env.web3SignerKeyPath, - needP12: true, }, { name: "ssv-signer", - dnsNames: []string{"localhost", "ssv-signer"}, + dnsNames: []string{"ssv-signer"}, certPath: &env.ssvSignerCertPath, - keyPath: &env.ssvSignerKeyPath, - needP12: true, + }, + { + name: "e2e-client", + dnsNames: []string{"e2e-client"}, + certPath: &env.e2eClientCertPath, }, } + fingerprints := make(map[string]string) + for _, config := range certConfigs { cert, key, err := generateSelfSignedCert(config.name, config.dnsNames) if err != nil { @@ -133,41 +135,74 @@ func (env *TestEnvironment) setupTLSCertificates() error { } *config.certPath = filepath.Join(env.certDir, config.name+".crt") - *config.keyPath = filepath.Join(env.certDir, config.name+".key") + keyPath := filepath.Join(env.certDir, config.name+".key") if err := os.WriteFile(*config.certPath, cert, fileMode); err != nil { return fmt.Errorf("failed to write %s certificate: %w", config.name, err) } - if err := os.WriteFile(*config.keyPath, key, fileMode); err != nil { + if err := os.WriteFile(keyPath, key, fileMode); err != nil { return fmt.Errorf("failed to write %s key: %w", config.name, err) } - if config.needP12 { - p12Path := filepath.Join(env.certDir, config.name+".p12") - if err := generatePKCS12FromPEM(*config.certPath, *config.keyPath, p12Path, testPassword); err != nil { - return fmt.Errorf("failed to generate PKCS12 keystore for %s: %w", config.name, err) - } + p12Path := filepath.Join(env.certDir, config.name+".p12") + if err := generatePKCS12FromPEM(*config.certPath, keyPath, p12Path, testPassword); err != nil { + return fmt.Errorf("failed to generate PKCS12 keystore for %s: %w", config.name, err) + } + + // Calculate fingerprint for all certificates + certPEM, rest := pem.Decode(cert) + if certPEM == nil { + return fmt.Errorf("failed to decode %s certificate PEM", config.name) } + if certPEM.Type != "CERTIFICATE" { + return fmt.Errorf("expected CERTIFICATE type but got %s for %s", certPEM.Type, config.name) + } + if len(rest) > 0 { + return fmt.Errorf("certificate %s contains extra data after the PEM block", config.name) + } + + parsedCert, err := x509.ParseCertificate(certPEM.Bytes) + if err != nil { + return fmt.Errorf("failed to parse %s certificate: %w", config.name, err) + } + + fingerprint := sha256.Sum256(parsedCert.Raw) + fingerprintHex := hex.EncodeToString(fingerprint[:]) + fingerprints[config.name] = fingerprintHex + } + + ssvSignerKnownClientsPath := filepath.Join(env.certDir, "known_clients.txt") + ssvSignerKnownClientsContent := "# Known client certificates for SSV-Signer E2E tests\n" + + "# Format: \n" + + fmt.Sprintf("e2e-client %s\n", fingerprints["e2e-client"]) + + if err := os.WriteFile(ssvSignerKnownClientsPath, []byte(ssvSignerKnownClientsContent), fileMode); err != nil { + return fmt.Errorf("failed to write SSV-Signer known_clients.txt: %w", err) } - // Create known_clients.txt file for server TLS client authentication - // For E2E tests, we create an empty file since no clients will connect with certificates - knownClientsPath := filepath.Join(env.certDir, "known_clients.txt") - knownClientsContent := "# Known client certificates for SSV-Signer E2E tests\n" + + web3SignerKnownClientsPath := filepath.Join(env.certDir, "web3signer_known_clients.txt") + web3SignerKnownClientsContent := "# Known client certificates for Web3Signer E2E tests\n" + "# Format: \n" + - "# Empty for E2E tests - no client certificate authentication required\n" + fmt.Sprintf("ssv-signer %s\n", fingerprints["ssv-signer"]) + + fmt.Sprintf("e2e-client %s\n", fingerprints["e2e-client"]) - if err := os.WriteFile(knownClientsPath, []byte(knownClientsContent), fileMode); err != nil { - return fmt.Errorf("failed to write known_clients.txt: %w", err) + if err := os.WriteFile(web3SignerKnownClientsPath, []byte(web3SignerKnownClientsContent), fileMode); err != nil { + return fmt.Errorf("failed to write Web3Signer known_clients.txt: %w", err) } return nil } -// createTrustedTLSConfig creates a TLS config that trusts our self-signed certificates -func createTrustedTLSConfig(certPath string) (*tls.Config, error) { +// createMutualTLSConfig creates a mutual TLS config for secure connections +func createMutualTLSConfig(serverCertPath, clientCertPath string) (*tls.Config, error) { + clientP12Path := strings.TrimSuffix(clientCertPath, ".crt") + ".p12" + clientPasswordFile := strings.TrimSuffix(clientCertPath, ".crt") + "_password.txt" + tlsConf := &ssvtls.Config{ - ClientServerCertFile: certPath, + ClientServerCertFile: serverCertPath, + ClientKeystoreFile: clientP12Path, + ClientKeystorePasswordFile: clientPasswordFile, } + return tlsConf.LoadClientTLSConfig() } diff --git a/ssvsigner/ekm/remote_key_manager.go b/ssvsigner/ekm/remote_key_manager.go index ccb7270695..ca108cce40 100644 --- a/ssvsigner/ekm/remote_key_manager.go +++ b/ssvsigner/ekm/remote_key_manager.go @@ -96,7 +96,7 @@ func (km *RemoteKeyManager) AddShare( encryptedPrivKey []byte, pubKey phase0.BLSPubKey, ) error { - if err := km.slashingProtector.BumpSlashingProtectionTxn(txn, pubKey); err != nil { + if err := km.BumpSlashingProtection(txn, pubKey); err != nil { return fmt.Errorf("could not bump slashing protection: %w", err) } @@ -169,11 +169,11 @@ func (km *RemoteKeyManager) RemoveShare(ctx context.Context, txn basedb.Txn, pub } } - if err := km.slashingProtector.RemoveHighestAttestationTxn(txn, pubKey); err != nil { + if err := km.removeHighestAttestation(txn, pubKey); err != nil { return fmt.Errorf("could not remove highest attestation: %w", err) } - if err := km.slashingProtector.RemoveHighestProposalTxn(txn, pubKey); err != nil { + if err := km.removeHighestProposal(txn, pubKey); err != nil { return fmt.Errorf("could not remove highest proposal: %w", err) } @@ -181,10 +181,18 @@ func (km *RemoteKeyManager) RemoveShare(ctx context.Context, txn basedb.Txn, pub } func (km *RemoteKeyManager) IsAttestationSlashable(pubKey phase0.BLSPubKey, attData *phase0.AttestationData) error { + attLock := km.lock(pubKey, lockAttestation) + attLock.Lock() + defer attLock.Unlock() + return km.slashingProtector.IsAttestationSlashable(pubKey, attData) } func (km *RemoteKeyManager) IsBeaconBlockSlashable(pubKey phase0.BLSPubKey, slot phase0.Slot) error { + propLock := km.lock(pubKey, lockProposal) + propLock.Lock() + defer propLock.Unlock() + return km.slashingProtector.IsBeaconBlockSlashable(pubKey, slot) } @@ -200,6 +208,22 @@ func (km *RemoteKeyManager) BumpSlashingProtection(txn basedb.Txn, pubKey phase0 return km.slashingProtector.BumpSlashingProtectionTxn(txn, pubKey) } +func (km *RemoteKeyManager) removeHighestAttestation(txn basedb.Txn, pubKey phase0.BLSPubKey) error { + attLock := km.lock(pubKey, lockAttestation) + attLock.Lock() + defer attLock.Unlock() + + return km.slashingProtector.RemoveHighestAttestationTxn(txn, pubKey) +} + +func (km *RemoteKeyManager) removeHighestProposal(txn basedb.Txn, pubKey phase0.BLSPubKey) error { + propLock := km.lock(pubKey, lockProposal) + propLock.Lock() + defer propLock.Unlock() + + return km.slashingProtector.RemoveHighestProposalTxn(txn, pubKey) +} + // SignBeaconObject checks slashing conditions locally for attestation and beacon block, // then constructs a SignRequest for the remote signerClient. If slashable, returns an error immediately. // Otherwise, forwards to the remote service. It returns signature as well as the computed signing root. diff --git a/ssvsigner/tls/tls.go b/ssvsigner/tls/tls.go index 9792a6cb07..f3f0217635 100644 --- a/ssvsigner/tls/tls.go +++ b/ssvsigner/tls/tls.go @@ -106,13 +106,17 @@ func (c *Config) LoadServerTLSConfig() (*tls.Config, error) { return nil, err } - // For Case 3: Load client fingerprints if provided + // For Case 3: Load client fingerprints var trustedFingerprints map[string]string if c.ServerKnownClientsFile != "" { trustedFingerprints, err = loadFingerprintsFile(c.ServerKnownClientsFile) if err != nil { return nil, fmt.Errorf("load known clients: %w", err) } + + if len(trustedFingerprints) == 0 { + return nil, fmt.Errorf("no client fingerprints found; mutual TLS required") + } } return createServerTLSConfig(certificate, trustedFingerprints) diff --git a/ssvsigner/tls/tls_test.go b/ssvsigner/tls/tls_test.go index 6d69983750..f3a3c42023 100644 --- a/ssvsigner/tls/tls_test.go +++ b/ssvsigner/tls/tls_test.go @@ -1,8 +1,15 @@ package tls import ( + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "net" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -226,3 +233,158 @@ func TestServerTLSConfigWithCompleteConfig(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "read keystore file") } + +func makeCert(raw []byte, subjectCN string, dnsNames []string, ipAddresses []net.IP) *x509.Certificate { + return &x509.Certificate{ + Raw: raw, + Subject: pkixName(subjectCN), + DNSNames: dnsNames, + IPAddresses: ipAddresses, + } +} + +func pkixName(cn string) pkix.Name { + return pkix.Name{CommonName: cn} +} + +func TestVerifyServerCertificate(t *testing.T) { + certRaw := []byte("dummy-cert") + fingerprint := sha256.Sum256(certRaw) + fingerprintHex := hex.EncodeToString(fingerprint[:]) + formattedFingerprint := formatFingerprint(fingerprintHex) + + tests := []struct { + name string + state tls.ConnectionState + trustedFingerprints map[string]string + wantErr bool + errContains string + }{ + { + name: "no certificates provided", + state: tls.ConnectionState{ + PeerCertificates: nil, + }, + trustedFingerprints: map[string]string{}, + wantErr: true, + errContains: "no server certificate provided", + }, + { + name: "matching ServerName", + state: tls.ConnectionState{ + ServerName: "example.com", + PeerCertificates: []*x509.Certificate{makeCert(certRaw, "", nil, nil)}, + }, + trustedFingerprints: map[string]string{ + "example.com": formattedFingerprint, + }, + wantErr: false, + }, + { + name: "matching CommonName", + state: tls.ConnectionState{ + ServerName: "", + PeerCertificates: []*x509.Certificate{ + makeCert(certRaw, "myhost", nil, nil), + }, + }, + trustedFingerprints: map[string]string{ + "myhost": formattedFingerprint, + }, + wantErr: false, + }, + { + name: "matching DNS name", + state: tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{ + makeCert(certRaw, "", []string{"alt.example.com"}, nil), + }, + }, + trustedFingerprints: map[string]string{ + "alt.example.com": formattedFingerprint, + }, + wantErr: false, + }, + { + name: "matching IP address", + state: tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{ + makeCert(certRaw, "", nil, []net.IP{net.ParseIP("127.0.0.1")}), + }, + }, + trustedFingerprints: map[string]string{ + "127.0.0.1": formattedFingerprint, + }, + wantErr: false, + }, + { + name: "mismatch in all fields", + state: tls.ConnectionState{ + ServerName: "example.com", + PeerCertificates: []*x509.Certificate{ + makeCert(certRaw, "wrong-cn", []string{"wrong-dns"}, []net.IP{net.ParseIP("192.168.0.1")}), + }, + }, + trustedFingerprints: map[string]string{ + "other.com": formattedFingerprint, + }, + wantErr: true, + errContains: "server certificate fingerprint not trusted", + }, + { + name: "one match out of many possible names", + state: tls.ConnectionState{ + ServerName: "good.com", + PeerCertificates: []*x509.Certificate{ + makeCert(certRaw, "ignored.cn", []string{"bad.com", "another.com"}, []net.IP{net.ParseIP("10.0.0.1")}), + }, + }, + trustedFingerprints: map[string]string{ + "good.com": formattedFingerprint, + }, + wantErr: false, + }, + { + name: "normalized fingerprint match (with colons)", + state: tls.ConnectionState{ + ServerName: "normalize.com", + PeerCertificates: []*x509.Certificate{ + makeCert(certRaw, "", nil, nil), + }, + }, + trustedFingerprints: map[string]string{ + "normalize.com": formatFingerprint(fingerprintHex), + }, + wantErr: false, + }, + { + name: "case-insensitive fingerprint match", + state: tls.ConnectionState{ + ServerName: "casetest.com", + PeerCertificates: []*x509.Certificate{ + makeCert(certRaw, "", nil, nil), + }, + }, + trustedFingerprints: map[string]string{ + "casetest.com": strings.ToUpper(fingerprintHex), + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := verifyServerCertificate(tt.state, tt.trustedFingerprints) + if tt.wantErr { + require.Error(t, err) + if tt.errContains != "" { + assert.Contains(t, err.Error(), tt.errContains) + } + } else { + require.NoError(t, err) + } + }) + } +}