Repository: ObolNetwork/charon

Issue:
`charon --split-existing-keys` support multiple formats
# 🎯 Problem to be solved
Currently for `split-existing-keys` Charon reads only [EIP-2335](https://eips.ethereum.org/EIPS/eip-2335) formatted keystores. However, some VCs do not use EIP-2335. This requires some manual work from users to parse their existing keystore files to EIP-2335.

# 🛠️ Proposed solution
We can add optinoal flag `split-keys-vc-type`, where users can specify one of the 6 VCs we support. Then we parse with the file format the VC uses. If not specified, we default to the current EIP-2335 format.

# 🧪 Tests
- [ ] Tested by new automated unit tests
- [ ] Manually tested with all 6 VCs

# ❌ Out of Scope
Parsing old file formats that are no longer supported by latest versions of the VCs. We cannot go back in time and check for each VC what file format they have supported in the past. We assume they staid more or less consistent.


Discussion:

Solution patch:
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index a2d53e4f..539739a0 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -19,4 +19,4 @@ jobs:
       - name: golangci-lint
         uses: golangci/golangci-lint-action@v8
         with:
-          version: v2.2.1
+          version: v2.3.1
diff --git a/.pre-commit/run_linter.sh b/.pre-commit/run_linter.sh
index f611f7a8..2e3628ed 100755
--- a/.pre-commit/run_linter.sh
+++ b/.pre-commit/run_linter.sh
@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-VERSION="2.2.2"
+VERSION="v2.3.1"
 
 if ! command -v golangci-lint &>/dev/null; then
     echo "golangci-lint could not be found"
diff --git a/app/app.go b/app/app.go
index aa0c9d94..f242f6df 100644
--- a/app/app.go
+++ b/app/app.go
@@ -911,7 +911,7 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager, cl
 	if conf.SimnetBMockFuzz {
 		log.Info(ctx, "Beaconmock fuzz configured!")
 
-		bmock, err := beaconmock.New(beaconmock.WithBeaconMockFuzzer(), beaconmock.WithForkVersion([4]byte(forkVersion)))
+		bmock, err := beaconmock.New(ctx, beaconmock.WithBeaconMockFuzzer(), beaconmock.WithForkVersion([4]byte(forkVersion)))
 		if err != nil {
 			return nil, nil, err
 		}
@@ -954,7 +954,7 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager, cl
 
 		opts = append(opts, conf.TestConfig.SimnetBMockOpts...)
 
-		bmock, err := beaconmock.New(opts...)
+		bmock, err := beaconmock.New(ctx, opts...)
 		if err != nil {
 			return nil, nil, err
 		}
diff --git a/app/eth1wrap/mocks/erc1271.go b/app/eth1wrap/mocks/erc1271.go
index 9587b7be..e57cca03 100644
--- a/app/eth1wrap/mocks/erc1271.go
+++ b/app/eth1wrap/mocks/erc1271.go
@@ -1,11 +1,11 @@
 // Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
 
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.53.3. DO NOT EDIT.
 
 package mocks
 
 import (
-	bind "github.com/ethereum/go-ethereum/accounts/abi/bind"
+	bind "github.com/ethereum/go-ethereum/accounts/abi/bind/v2"
 
 	mock "github.com/stretchr/testify/mock"
 )
diff --git a/app/eth1wrap/mocks/eth_client.go b/app/eth1wrap/mocks/eth_client.go
index 46efdd00..e575b62f 100644
--- a/app/eth1wrap/mocks/eth_client.go
+++ b/app/eth1wrap/mocks/eth_client.go
@@ -1,6 +1,6 @@
 // Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
 
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.53.3. DO NOT EDIT.
 
 package mocks
 
@@ -292,7 +292,7 @@ func (_m *EthClient) ChainID(ctx context.Context) (*big.Int, error) {
 	return r0, r1
 }
 
-// Client provides a mock function with given fields:
+// Client provides a mock function with no fields
 func (_m *EthClient) Client() *rpc.Client {
 	ret := _m.Called()
 
@@ -312,7 +312,7 @@ func (_m *EthClient) Client() *rpc.Client {
 	return r0
 }
 
-// Close provides a mock function with given fields:
+// Close provides a mock function with no fields
 func (_m *EthClient) Close() {
 	_m.Called()
 }
diff --git a/app/eth1wrap/mocks/eth_client_runner.go b/app/eth1wrap/mocks/eth_client_runner.go
index f87d15fc..31de82ba 100644
--- a/app/eth1wrap/mocks/eth_client_runner.go
+++ b/app/eth1wrap/mocks/eth_client_runner.go
@@ -1,6 +1,6 @@
 // Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
 
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.53.3. DO NOT EDIT.
 
 package mocks
 
diff --git a/app/eth2wrap/eth2wrap.go b/app/eth2wrap/eth2wrap.go
index c4b2d1db..4cecb0d3 100644
--- a/app/eth2wrap/eth2wrap.go
+++ b/app/eth2wrap/eth2wrap.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"net"
 	"net/url"
+	"strings"
 	"sync"
 	"time"
 
@@ -163,13 +164,7 @@ func provide[O any](ctx context.Context, clients []Client, fallbacks []Client,
 
 	zero := func() O { var z O; return z }()
 
-	runForkJoin := func(clients []Client, isFallback bool) (O, error) {
-		if isFallback {
-			usingFallbackGauge.Set(1)
-		} else {
-			usingFallbackGauge.Set(0)
-		}
-
+	runForkJoin := func(clients []Client) (O, error) {
 		fork, join, cancel := forkjoin.New(ctx, work,
 			forkjoin.WithoutFailFast(),
 			forkjoin.WithWorkers(len(clients)),
@@ -208,13 +203,29 @@ func provide[O any](ctx context.Context, clients []Client, fallbacks []Client,
 
 		return nokResp.Output, nokResp.Err
 	}
+	output, err := runForkJoin(clients)
 
-	output, err := runForkJoin(clients, false)
-	if err == nil || ctx.Err() != nil || len(fallbacks) == 0 {
-		return output, err
+	// Call fallback nodes when request to beacon node timeout or if it's syncing
+	if err != nil && len(fallbacks) != 0 && (isTimeoutError(err) || isSyncingError(err)) {
+		usingFallbackGauge.Set(1)
+		return runForkJoin(fallbacks)
 	}
 
-	return runForkJoin(fallbacks, true)
+	usingFallbackGauge.Set(0)
+
+	return output, err
+}
+
+// isTimeoutError returns true if error message contains known strings for timeout related issues.
+func isTimeoutError(err error) bool {
+	msg := err.Error()
+	return strings.Contains(msg, "http request timeout") || strings.Contains(msg, "client is not active") || strings.Contains(msg, "context deadline exceeded")
+}
+
+// isSyncingError returns true if error message contains the word "syncing".
+func isSyncingError(err error) bool {
+	msg := err.Error()
+	return strings.Contains(msg, "syncing")
 }
 
 type empty struct{}
diff --git a/app/eth2wrap/eth2wrap_gen.go b/app/eth2wrap/eth2wrap_gen.go
index a6fb97b1..09ebd85a 100644
--- a/app/eth2wrap/eth2wrap_gen.go
+++ b/app/eth2wrap/eth2wrap_gen.go
@@ -14,19 +14,11 @@ import (
 	"github.com/attestantio/go-eth2-client/spec"
 	"github.com/attestantio/go-eth2-client/spec/altair"
 	"github.com/attestantio/go-eth2-client/spec/phase0"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 )
 
 // Client defines all go-eth2-client interfaces used in charon.
 type Client interface {
 	eth2client.Service
-	eth2exp.BeaconCommitteeSelectionAggregator
-	eth2exp.SyncCommitteeSelectionAggregator
-	eth2exp.ProposerConfigProvider
-	BlockAttestationsProvider
-	BlockProvider
-	BeaconStateCommitteesProvider
-	NodePeerCountProvider
 
 	CachedValidatorsProvider
 	SetValidatorCache(func(context.Context) (ActiveValidators, CompleteValidators, error))
@@ -38,14 +30,18 @@ type Client interface {
 	eth2client.AttestationDataProvider
 	eth2client.AttestationsSubmitter
 	eth2client.AttesterDutiesProvider
+	eth2client.BeaconBlockAttestationsProvider
 	eth2client.BeaconBlockRootProvider
+	eth2client.BeaconCommitteeSelectionsProvider
 	eth2client.BeaconCommitteeSubscriptionsSubmitter
+	eth2client.BeaconCommitteesProvider
 	eth2client.BlindedProposalSubmitter
 	eth2client.DepositContractProvider
 	eth2client.DomainProvider
 	eth2client.ForkProvider
 	eth2client.ForkScheduleProvider
 	eth2client.GenesisProvider
+	eth2client.NodePeerCountProvider
 	eth2client.NodeSyncingProvider
 	eth2client.NodeVersionProvider
 	eth2client.ProposalPreparationsSubmitter
@@ -60,6 +56,7 @@ type Client interface {
 	eth2client.SyncCommitteeContributionsSubmitter
 	eth2client.SyncCommitteeDutiesProvider
 	eth2client.SyncCommitteeMessagesSubmitter
+	eth2client.SyncCommitteeSelectionsProvider
 	eth2client.SyncCommitteeSubscriptionsSubmitter
 	eth2client.ValidatorRegistrationsSubmitter
 	eth2client.ValidatorsProvider
@@ -135,6 +132,27 @@ func (m multi) SignedBeaconBlock(ctx context.Context, opts *api.SignedBeaconBloc
 	return res0, err
 }
 
+// BeaconCommittees fetches all beacon committees for the given options.
+func (m multi) BeaconCommittees(ctx context.Context, opts *api.BeaconCommitteesOpts) (*api.Response[[]*apiv1.BeaconCommittee], error) {
+	const label = "beacon_committees"
+	defer latency(ctx, label, false)()
+	defer incRequest(label)
+
+	res0, err := provide(ctx, m.clients, m.fallbacks,
+		func(ctx context.Context, args provideArgs) (*api.Response[[]*apiv1.BeaconCommittee], error) {
+			return args.client.BeaconCommittees(ctx, opts)
+		},
+		nil, m.selector,
+	)
+
+	if err != nil {
+		incError(label)
+		err = wrapError(ctx, err, label)
+	}
+
+	return res0, err
+}
+
 // AggregateAttestation fetches the aggregate attestation for the given options.
 func (m multi) AggregateAttestation(ctx context.Context, opts *api.AggregateAttestationOpts) (*api.Response[*spec.VersionedAttestation], error) {
 	const label = "aggregate_attestation"
@@ -368,6 +386,27 @@ func (m multi) SubmitSyncCommitteeContributions(ctx context.Context, contributio
 	return err
 }
 
+// SyncCommitteeSelections submits sync committee selections.
+func (m multi) SyncCommitteeSelections(ctx context.Context, opts *api.SyncCommitteeSelectionsOpts) (*api.Response[[]*apiv1.SyncCommitteeSelection], error) {
+	const label = "sync_committee_selections"
+	defer latency(ctx, label, false)()
+	defer incRequest(label)
+
+	res0, err := provide(ctx, m.clients, m.fallbacks,
+		func(ctx context.Context, args provideArgs) (*api.Response[[]*apiv1.SyncCommitteeSelection], error) {
+			return args.client.SyncCommitteeSelections(ctx, opts)
+		},
+		nil, m.selector,
+	)
+
+	if err != nil {
+		incError(label)
+		err = wrapError(ctx, err, label)
+	}
+
+	return res0, err
+}
+
 // Proposal fetches a proposal for signing.
 func (m multi) Proposal(ctx context.Context, opts *api.ProposalOpts) (*api.Response[*api.VersionedProposal], error) {
 	const label = "proposal"
@@ -411,6 +450,27 @@ func (m multi) BeaconBlockRoot(ctx context.Context, opts *api.BeaconBlockRootOpt
 	return res0, err
 }
 
+// BeaconBlockAttestations fetches a block's attestations given a set of options.
+func (m multi) BeaconBlockAttestations(ctx context.Context, opts *api.BeaconBlockAttestationsOpts) (*api.Response[[]*spec.VersionedAttestation], error) {
+	const label = "beacon_block_attestations"
+	defer latency(ctx, label, false)()
+	defer incRequest(label)
+
+	res0, err := provide(ctx, m.clients, m.fallbacks,
+		func(ctx context.Context, args provideArgs) (*api.Response[[]*spec.VersionedAttestation], error) {
+			return args.client.BeaconBlockAttestations(ctx, opts)
+		},
+		nil, m.selector,
+	)
+
+	if err != nil {
+		incError(label)
+		err = wrapError(ctx, err, label)
+	}
+
+	return res0, err
+}
+
 // SubmitProposal submits a proposal.
 func (m multi) SubmitProposal(ctx context.Context, opts *api.SubmitProposalOpts) error {
 	const label = "submit_proposal"
@@ -453,6 +513,27 @@ func (m multi) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscript
 	return err
 }
 
+// BeaconCommitteeSelections submits beacon committee selections.
+func (m multi) BeaconCommitteeSelections(ctx context.Context, opts *api.BeaconCommitteeSelectionsOpts) (*api.Response[[]*apiv1.BeaconCommitteeSelection], error) {
+	const label = "beacon_committee_selections"
+	defer latency(ctx, label, false)()
+	defer incRequest(label)
+
+	res0, err := provide(ctx, m.clients, m.fallbacks,
+		func(ctx context.Context, args provideArgs) (*api.Response[[]*apiv1.BeaconCommitteeSelection], error) {
+			return args.client.BeaconCommitteeSelections(ctx, opts)
+		},
+		nil, m.selector,
+	)
+
+	if err != nil {
+		incError(label)
+		err = wrapError(ctx, err, label)
+	}
+
+	return res0, err
+}
+
 // SubmitBlindedProposal submits a beacon block.
 func (m multi) SubmitBlindedProposal(ctx context.Context, opts *api.SubmitBlindedProposalOpts) error {
 	const label = "submit_blinded_proposal"
@@ -559,6 +640,28 @@ func (m multi) Genesis(ctx context.Context, opts *api.GenesisOpts) (*api.Respons
 	return res0, err
 }
 
+// NodePeerCount provides the peer count of the node.
+// Note this endpoint is cached in go-eth2-client.
+func (m multi) NodePeerCount(ctx context.Context, opts *api.NodePeerCountOpts) (*api.Response[*apiv1.PeerCount], error) {
+	const label = "node_peer_count"
+
+	defer incRequest(label)
+
+	res0, err := provide(ctx, m.clients, m.fallbacks,
+		func(ctx context.Context, args provideArgs) (*api.Response[*apiv1.PeerCount], error) {
+			return args.client.NodePeerCount(ctx, opts)
+		},
+		nil, m.selector,
+	)
+
+	if err != nil {
+		incError(label)
+		err = wrapError(ctx, err, label)
+	}
+
+	return res0, err
+}
+
 // NodeSyncing provides the state of the node's synchronization with the chain.
 func (m multi) NodeSyncing(ctx context.Context, opts *api.NodeSyncingOpts) (*api.Response[*apiv1.SyncState], error) {
 	const label = "node_syncing"
@@ -790,6 +893,16 @@ func (l *lazy) SignedBeaconBlock(ctx context.Context, opts *api.SignedBeaconBloc
 	return cl.SignedBeaconBlock(ctx, opts)
 }
 
+// BeaconCommittees fetches all beacon committees for the given options.
+func (l *lazy) BeaconCommittees(ctx context.Context, opts *api.BeaconCommitteesOpts) (res0 *api.Response[[]*apiv1.BeaconCommittee], err error) {
+	cl, err := l.getOrCreateClient(ctx)
+	if err != nil {
+		return res0, err
+	}
+
+	return cl.BeaconCommittees(ctx, opts)
+}
+
 // AggregateAttestation fetches the aggregate attestation for the given options.
 func (l *lazy) AggregateAttestation(ctx context.Context, opts *api.AggregateAttestationOpts) (res0 *api.Response[*spec.VersionedAttestation], err error) {
 	cl, err := l.getOrCreateClient(ctx)
@@ -901,6 +1014,16 @@ func (l *lazy) SubmitSyncCommitteeContributions(ctx context.Context, contributio
 	return cl.SubmitSyncCommitteeContributions(ctx, contributionAndProofs)
 }
 
+// SyncCommitteeSelections submits sync committee selections.
+func (l *lazy) SyncCommitteeSelections(ctx context.Context, opts *api.SyncCommitteeSelectionsOpts) (res0 *api.Response[[]*apiv1.SyncCommitteeSelection], err error) {
+	cl, err := l.getOrCreateClient(ctx)
+	if err != nil {
+		return res0, err
+	}
+
+	return cl.SyncCommitteeSelections(ctx, opts)
+}
+
 // Proposal fetches a proposal for signing.
 func (l *lazy) Proposal(ctx context.Context, opts *api.ProposalOpts) (res0 *api.Response[*api.VersionedProposal], err error) {
 	cl, err := l.getOrCreateClient(ctx)
@@ -921,6 +1044,16 @@ func (l *lazy) BeaconBlockRoot(ctx context.Context, opts *api.BeaconBlockRootOpt
 	return cl.BeaconBlockRoot(ctx, opts)
 }
 
+// BeaconBlockAttestations fetches a block's attestations given a set of options.
+func (l *lazy) BeaconBlockAttestations(ctx context.Context, opts *api.BeaconBlockAttestationsOpts) (res0 *api.Response[[]*spec.VersionedAttestation], err error) {
+	cl, err := l.getOrCreateClient(ctx)
+	if err != nil {
+		return res0, err
+	}
+
+	return cl.BeaconBlockAttestations(ctx, opts)
+}
+
 // SubmitProposal submits a proposal.
 func (l *lazy) SubmitProposal(ctx context.Context, opts *api.SubmitProposalOpts) (err error) {
 	cl, err := l.getOrCreateClient(ctx)
@@ -941,6 +1074,16 @@ func (l *lazy) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscript
 	return cl.SubmitBeaconCommitteeSubscriptions(ctx, subscriptions)
 }
 
+// BeaconCommitteeSelections submits beacon committee selections.
+func (l *lazy) BeaconCommitteeSelections(ctx context.Context, opts *api.BeaconCommitteeSelectionsOpts) (res0 *api.Response[[]*apiv1.BeaconCommitteeSelection], err error) {
+	cl, err := l.getOrCreateClient(ctx)
+	if err != nil {
+		return res0, err
+	}
+
+	return cl.BeaconCommitteeSelections(ctx, opts)
+}
+
 // SubmitBlindedProposal submits a beacon block.
 func (l *lazy) SubmitBlindedProposal(ctx context.Context, opts *api.SubmitBlindedProposalOpts) (err error) {
 	cl, err := l.getOrCreateClient(ctx)
@@ -991,6 +1134,16 @@ func (l *lazy) Genesis(ctx context.Context, opts *api.GenesisOpts) (res0 *api.Re
 	return cl.Genesis(ctx, opts)
 }
 
+// NodePeerCount provides the peer count of the node.
+func (l *lazy) NodePeerCount(ctx context.Context, opts *api.NodePeerCountOpts) (res0 *api.Response[*apiv1.PeerCount], err error) {
+	cl, err := l.getOrCreateClient(ctx)
+	if err != nil {
+		return res0, err
+	}
+
+	return cl.NodePeerCount(ctx, opts)
+}
+
 // NodeSyncing provides the state of the node's synchronization with the chain.
 func (l *lazy) NodeSyncing(ctx context.Context, opts *api.NodeSyncingOpts) (res0 *api.Response[*apiv1.SyncState], err error) {
 	cl, err := l.getOrCreateClient(ctx)
diff --git a/app/eth2wrap/genwrap/genwrap.go b/app/eth2wrap/genwrap/genwrap.go
index cf8025ea..53f03376 100644
--- a/app/eth2wrap/genwrap/genwrap.go
+++ b/app/eth2wrap/genwrap/genwrap.go
@@ -47,13 +47,6 @@ import (
 // Client defines all go-eth2-client interfaces used in charon.
 type Client interface {
     eth2client.Service
-    eth2exp.BeaconCommitteeSelectionAggregator
-    eth2exp.SyncCommitteeSelectionAggregator
-    eth2exp.ProposerConfigProvider
-    BlockAttestationsProvider
-		BlockProvider
-    BeaconStateCommitteesProvider
-    NodePeerCountProvider
 
     CachedValidatorsProvider
     SetValidatorCache(func(context.Context) (ActiveValidators, CompleteValidators, error))
@@ -113,6 +106,9 @@ type Client interface {
 		"BeaconBlockRootProvider":               {Latency: false, Log: false},
 		"ProposalSubmitter":                     {Latency: true, Log: false},
 		"BeaconCommitteeSubscriptionsSubmitter": {Latency: true, Log: false},
+		"BeaconBlockAttestationsProvider":       {Latency: true, Log: false},
+		"BeaconCommitteeSelectionsProvider":     {Latency: true, Log: false},
+		"BeaconCommitteesProvider":              {Latency: true, Log: false},
 		"BlindedProposalProvider":               {Latency: true, Log: false},
 		"BlindedProposalSubmitter":              {Latency: true, Log: false},
 		"DepositContractProvider":               {Latency: false, Log: false},
@@ -120,6 +116,7 @@ type Client interface {
 		"ForkProvider":                          {Latency: true, Log: false},
 		"ForkScheduleProvider":                  {Latency: true, Log: false},
 		"GenesisProvider":                       {Latency: false, Log: false},
+		"NodePeerCountProvider":                 {Latency: false, Log: false},
 		"NodeSyncingProvider":                   {Latency: true, Log: false},
 		"NodeVersionProvider":                   {Latency: false, Log: false},
 		"ProposerDutiesProvider":                {Latency: true, Log: false},
@@ -133,6 +130,7 @@ type Client interface {
 		"SyncCommitteeContributionsSubmitter":   {Latency: true, Log: false},
 		"SyncCommitteeMessagesSubmitter":        {Latency: true, Log: false},
 		"SyncCommitteeSubscriptionsSubmitter":   {Latency: true, Log: false},
+		"SyncCommitteeSelectionsProvider":       {Latency: true, Log: false},
 		"ValidatorsProvider":                    {Latency: true, Log: true},
 		"ValidatorRegistrationsSubmitter":       {Latency: true, Log: false},
 		"VoluntaryExitSubmitter":                {Latency: true, Log: false},
diff --git a/app/eth2wrap/httpwrap.go b/app/eth2wrap/httpwrap.go
index 938d1b64..f6be47eb 100644
--- a/app/eth2wrap/httpwrap.go
+++ b/app/eth2wrap/httpwrap.go
@@ -3,15 +3,8 @@
 package eth2wrap
 
 import (
-	"bytes"
 	"context"
 	"encoding/hex"
-	"encoding/json"
-	"fmt"
-	"io"
-	"net/http"
-	"net/url"
-	"strconv"
 	"sync"
 	"testing"
 	"time"
@@ -19,46 +12,12 @@ import (
 	"github.com/attestantio/go-eth2-client/api"
 	apiv1 "github.com/attestantio/go-eth2-client/api/v1"
 	eth2http "github.com/attestantio/go-eth2-client/http"
-	"github.com/attestantio/go-eth2-client/spec"
-	eth2e "github.com/attestantio/go-eth2-client/spec/electra"
 	eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
 
 	"github.com/obolnetwork/charon/app/errors"
-	"github.com/obolnetwork/charon/app/z"
 	"github.com/obolnetwork/charon/eth2util"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
-	"github.com/obolnetwork/charon/eth2util/statecomm"
 )
 
-// BlockAttestationsProvider is the interface for providing attestations included in blocks.
-// It is a standard beacon API endpoint not implemented by eth2client.
-// See https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockAttestations.
-type BlockAttestationsProvider interface {
-	BlockAttestations(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error)
-}
-
-// BlockProvider is the interface for providing block details.
-// It is a standard beacon API endpoint not implemented by eth2client.
-// See https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockV2.
-type BlockProvider interface {
-	Block(ctx context.Context, stateID string) (*spec.VersionedSignedBeaconBlock, error)
-}
-
-// BeaconStateCommitteesProvider is the interface for providing committees for given slot.
-// It is a standard beacon API endpoint not implemented by eth2client.
-// See https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochCommittees.
-type BeaconStateCommitteesProvider interface {
-	BeaconStateCommittees(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error)
-}
-
-// NodePeerCountProvider is the interface for providing node peer count.
-// It is a standard beacon API endpoint not implemented by eth2client.
-// See https://ethereum.github.io/beacon-APIs/#/Node/getPeerCount.
-type NodePeerCountProvider interface {
-	// NodePeerCount provides peer count of the beacon node.
-	NodePeerCount(ctx context.Context) (int, error)
-}
-
 // NewHTTPAdapterForT returns a http adapter for testing non-eth2service methods as it is nil.
 func NewHTTPAdapterForT(_ *testing.T, address string, headers map[string]string, timeout time.Duration) Client {
 	return newHTTPAdapter(nil, address, headers, timeout)
@@ -157,237 +116,6 @@ func (h *httpAdapter) Validators(ctx context.Context, opts *api.ValidatorsOpts)
 	return h.Service.Validators(reqCtx, opts)
 }
 
-// AggregateBeaconCommitteeSelections implements eth2exp.BeaconCommitteeSelectionAggregator.
-func (h *httpAdapter) AggregateBeaconCommitteeSelections(ctx context.Context, selections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) {
-	reqBody, err := json.Marshal(selections)
-	if err != nil {
-		return nil, errors.Wrap(err, "marshal submit beacon committee selections")
-	}
-
-	respBody, err := httpPost(ctx, h.address, "/eth/v1/validator/beacon_committee_selections", bytes.NewReader(reqBody), h.headers, h.timeout)
-	if err != nil {
-		return nil, errors.Wrap(err, "submit beacon committee selections")
-	}
-
-	var resp submitBeaconCommitteeSelectionsJSON
-	if err := json.Unmarshal(respBody, &resp); err != nil {
-		return nil, errors.Wrap(err, "failed to parse beacon committee selections response")
-	}
-
-	return resp.Data, nil
-}
-
-// AggregateSyncCommitteeSelections implements eth2exp.SyncCommitteeSelectionAggregator.
-func (h *httpAdapter) AggregateSyncCommitteeSelections(ctx context.Context, selections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) {
-	reqBody, err := json.Marshal(selections)
-	if err != nil {
-		return nil, errors.Wrap(err, "marshal sync committee selections")
-	}
-
-	respBody, err := httpPost(ctx, h.address, "/eth/v1/validator/sync_committee_selections", bytes.NewReader(reqBody), h.headers, h.timeout)
-	if err != nil {
-		return nil, errors.Wrap(err, "submit sync committee selections")
-	}
-
-	var resp submitSyncCommitteeSelectionsJSON
-	if err := json.Unmarshal(respBody, &resp); err != nil {
-		return nil, errors.Wrap(err, "failed to parse sync committee selections response")
-	}
-
-	return resp.Data, nil
-}
-
-// BlockAttestations returns the attestations included in the requested block.
-// See https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockAttestationsV2.
-func (h *httpAdapter) BlockAttestations(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error) {
-	path := fmt.Sprintf("/eth/v2/beacon/blocks/%s/attestations", stateID)
-
-	ctx, cancel := context.WithTimeout(ctx, h.timeout)
-	defer cancel()
-
-	resp, err := httpGetRaw(ctx, h.address, path, h.headers, nil)
-	if err != nil {
-		return nil, errors.Wrap(err, "request block attestations")
-	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode == http.StatusNotFound {
-		return nil, nil // No block for slot, so no attestations.
-	} else if resp.StatusCode != http.StatusOK {
-		return nil, errors.New("request block attestations failed", z.Int("status", resp.StatusCode))
-	}
-
-	respBody, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return nil, errors.Wrap(err, "request block attestations body")
-	}
-
-	version, err := fetchConsensusVersion(resp)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to get consensus version")
-	}
-
-	res := []*spec.VersionedAttestation{}
-	switch version {
-	case spec.DataVersionPhase0:
-		var respAttestation p0AttestationsJSON
-		if err := json.Unmarshal(respBody, &respAttestation); err != nil {
-			return nil, errors.Wrap(err, "failed to parse block attestations response")
-		}
-
-		for _, a := range respAttestation.Data {
-			res = append(res, &spec.VersionedAttestation{Version: version, Phase0: a})
-		}
-	case spec.DataVersionAltair:
-		var respAttestation p0AttestationsJSON
-		if err := json.Unmarshal(respBody, &respAttestation); err != nil {
-			return nil, errors.Wrap(err, "failed to parse block attestations response")
-		}
-
-		for _, a := range respAttestation.Data {
-			res = append(res, &spec.VersionedAttestation{Version: version, Altair: a})
-		}
-	case spec.DataVersionBellatrix:
-		var respAttestation p0AttestationsJSON
-		if err := json.Unmarshal(respBody, &respAttestation); err != nil {
-			return nil, errors.Wrap(err, "failed to parse block attestations response")
-		}
-
-		for _, a := range respAttestation.Data {
-			res = append(res, &spec.VersionedAttestation{Version: version, Bellatrix: a})
-		}
-	case spec.DataVersionCapella:
-		var respAttestation p0AttestationsJSON
-		if err := json.Unmarshal(respBody, &respAttestation); err != nil {
-			return nil, errors.Wrap(err, "failed to parse block attestations response")
-		}
-
-		for _, a := range respAttestation.Data {
-			res = append(res, &spec.VersionedAttestation{Version: version, Capella: a})
-		}
-	case spec.DataVersionDeneb:
-		var respAttestation p0AttestationsJSON
-		if err := json.Unmarshal(respBody, &respAttestation); err != nil {
-			return nil, errors.Wrap(err, "failed to parse block attestations response")
-		}
-
-		for _, a := range respAttestation.Data {
-			res = append(res, &spec.VersionedAttestation{Version: version, Deneb: a})
-		}
-	case spec.DataVersionElectra:
-		var respAttestation electraAttestationsJSON
-		if err := json.Unmarshal(respBody, &respAttestation); err != nil {
-			return nil, errors.Wrap(err, "failed to parse block attestations response")
-		}
-
-		for _, a := range respAttestation.Data {
-			res = append(res, &spec.VersionedAttestation{Version: version, Electra: a})
-		}
-	case spec.DataVersionUnknown:
-		return nil, errors.New("attestations data version unknown")
-	}
-
-	return res, nil
-}
-
-// Block returns the block details.
-// See https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockV2.
-func (h *httpAdapter) Block(ctx context.Context, stateID string) (*spec.VersionedSignedBeaconBlock, error) {
-	path := "/eth/v2/beacon/blocks/" + stateID
-
-	ctx, cancel := context.WithTimeout(ctx, h.timeout)
-	defer cancel()
-
-	resp, err := httpGetRaw(ctx, h.address, path, h.headers, nil)
-	if err != nil {
-		return nil, errors.Wrap(err, "request block")
-	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode == http.StatusNotFound {
-		return nil, nil //nolint:nilnil // No block for slot.
-	} else if resp.StatusCode != http.StatusOK {
-		return nil, errors.New("request block failed", z.Int("status", resp.StatusCode))
-	}
-
-	respBody, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return nil, errors.Wrap(err, "request block attestations body")
-	}
-
-	res := spec.VersionedSignedBeaconBlock{}
-	if err := json.Unmarshal(respBody, &res); err != nil {
-		return nil, errors.Wrap(err, "failed to parse block response")
-	}
-
-	return &res, nil
-}
-
-// BeaconStateCommittees returns the attestations included in the requested block.
-// See https://ethereum.github.io/beacon-APIs/#/Beacon/getStateValidators.
-func (h *httpAdapter) BeaconStateCommittees(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error) {
-	r := strconv.FormatUint(slot, 10)
-	path := fmt.Sprintf("/eth/v1/beacon/states/%v/committees", r)
-	queryParams := map[string]string{
-		"slot": strconv.FormatUint(slot, 10),
-	}
-
-	respBody, statusCode, err := httpGet(ctx, h.address, path, h.headers, queryParams, h.timeout)
-	if err != nil {
-		return nil, errors.Wrap(err, "request state committees for slot", z.Int("status", statusCode), z.U64("slot", slot))
-	}
-
-	if statusCode != http.StatusOK {
-		return nil, errors.New("request state committees for slot failed", z.Int("status", statusCode), z.U64("slot", slot))
-	}
-
-	var res statecomm.StateCommitteesResponse
-
-	err = json.Unmarshal(respBody, &res)
-	if err != nil {
-		return nil, errors.Wrap(err, "unmarshal state committees", z.Int("status", statusCode), z.U64("slot", slot))
-	}
-
-	return res.Data, nil
-}
-
-// ProposerConfig implements eth2exp.ProposerConfigProvider.
-func (h *httpAdapter) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConfigResponse, error) {
-	respBody, statusCode, err := httpGet(ctx, h.address, "/proposer_config", h.headers, nil, h.timeout)
-	if err != nil {
-		return nil, errors.Wrap(err, "submit sync committee selections")
-	} else if statusCode != http.StatusOK {
-		return nil, errors.New("request proposer config failed", z.Int("status", statusCode), z.Str("body", string(respBody)))
-	}
-
-	var resp eth2exp.ProposerConfigResponse
-	if err := json.Unmarshal(respBody, &resp); err != nil {
-		return nil, errors.Wrap(err, "failed to parse sync committee selections response")
-	}
-
-	return &resp, nil
-}
-
-// NodePeerCount provides the peer count of the beacon node.
-// See https://ethereum.github.io/beacon-APIs/#/Node/getPeerCount.
-func (h *httpAdapter) NodePeerCount(ctx context.Context) (int, error) {
-	const path = "/eth/v1/node/peer_count"
-
-	respBody, statusCode, err := httpGet(ctx, h.address, path, h.headers, nil, h.timeout)
-	if err != nil {
-		return 0, errors.Wrap(err, "request beacon node peer count")
-	} else if statusCode != http.StatusOK {
-		return 0, errors.New("request beacon node peer count failed", z.Int("status", statusCode), z.Str("body", string(respBody)))
-	}
-
-	var resp peerCountJSON
-	if err := json.Unmarshal(respBody, &resp); err != nil {
-		return 0, errors.Wrap(err, "failed to parse beacon node peer count response")
-	}
-
-	return resp.Data.Connected, nil
-}
-
 // Domain returns the signing domain for a given domain type.
 // After EIP-7044, the VOLUNTARY_EXIT domain must always return a domain relative to the Capella hardfork.
 // This method returns just that for that domain type, otherwise follows the standard go-eth2-client flow.
@@ -403,158 +131,3 @@ func (h *httpAdapter) Domain(ctx context.Context, domainType eth2p0.DomainType,
 
 	return h.Service.Domain(ctx, domainType, epoch)
 }
-
-type submitBeaconCommitteeSelectionsJSON struct {
-	Data []*eth2exp.BeaconCommitteeSelection `json:"data"`
-}
-
-type submitSyncCommitteeSelectionsJSON struct {
-	Data []*eth2exp.SyncCommitteeSelection `json:"data"`
-}
-
-type p0AttestationsJSON struct {
-	Data []*eth2p0.Attestation `json:"data"`
-}
-
-type electraAttestationsJSON struct {
-	Data []*eth2e.Attestation `json:"data"`
-}
-
-type peerCountJSON struct {
-	Data struct {
-		Connected int `json:"connected,string"`
-	} `json:"data"`
-}
-
-type responseMetadata struct {
-	Version spec.DataVersion `json:"version"`
-}
-
-func httpPost(ctx context.Context, base string, endpoint string, body io.Reader, headers map[string]string, timeout time.Duration) ([]byte, error) {
-	ctx, cancel := context.WithTimeout(ctx, timeout)
-	defer cancel()
-
-	addr, err := url.JoinPath(base, endpoint)
-	if err != nil {
-		return nil, errors.Wrap(err, "invalid address")
-	}
-
-	url, err := url.ParseRequestURI(addr)
-	if err != nil {
-		return nil, errors.Wrap(err, "invalid endpoint")
-	}
-
-	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), body)
-	if err != nil {
-		return nil, errors.Wrap(err, "new POST request with ctx")
-	}
-
-	for k, v := range headers {
-		req.Header.Add(k, v)
-	}
-
-	res, err := new(http.Client).Do(req)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to call POST endpoint")
-	}
-	defer res.Body.Close()
-
-	data, err := io.ReadAll(res.Body)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to read POST response")
-	}
-
-	if res.StatusCode/100 != 2 {
-		return nil, errors.New("post failed", z.Int("status", res.StatusCode), z.Str("body", string(data)))
-	}
-
-	return data, nil
-}
-
-// httpGetRaw performs a GET request and returns the raw http response or an error.
-func httpGetRaw(ctx context.Context, base string, endpoint string, headers map[string]string, queryParams map[string]string) (*http.Response, error) {
-	addr, err := url.JoinPath(base, endpoint)
-	if err != nil {
-		return nil, errors.Wrap(err, "invalid address")
-	}
-
-	u, err := url.ParseRequestURI(addr)
-	if err != nil {
-		return nil, errors.Wrap(err, "invalid endpoint")
-	}
-
-	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
-	if err != nil {
-		return nil, errors.Wrap(err, "new GET request with ctx")
-	}
-
-	for k, v := range headers {
-		req.Header.Add(k, v)
-	}
-
-	q := req.URL.Query()
-	for key, val := range queryParams {
-		q.Add(key, val)
-	}
-
-	req.URL.RawQuery = q.Encode()
-
-	res, err := new(http.Client).Do(req)
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to call GET endpoint")
-	}
-
-	return res, nil
-}
-
-// httpGet performs a GET request and returns the body and status code or an error.
-func httpGet(ctx context.Context, base string, endpoint string, headers map[string]string, queryParams map[string]string, timeout time.Duration) ([]byte, int, error) {
-	ctx, cancel := context.WithTimeout(ctx, timeout)
-	defer cancel()
-
-	res, err := httpGetRaw(ctx, base, endpoint, headers, queryParams)
-	if err != nil {
-		return nil, 0, errors.Wrap(err, "failed to read GET response")
-	}
-	defer res.Body.Close()
-
-	data, err := io.ReadAll(res.Body)
-	if err != nil {
-		return nil, res.StatusCode, errors.Wrap(err, "failed to read GET response body")
-	}
-
-	return data, res.StatusCode, nil
-}
-
-// fetchConsensusVersion attempts to extract the consensus version from the beacon node http response.
-func fetchConsensusVersion(resp *http.Response) (spec.DataVersion, error) {
-	respConsensusVersions, exists := resp.Header["Eth-Consensus-Version"]
-	if !exists {
-		// No consensus version supplied in response; obtain it from the body if possible.
-		var metadata responseMetadata
-
-		body, err := io.ReadAll(resp.Body)
-		if err != nil {
-			return spec.DataVersionUnknown, errors.Wrap(err, "read resp body")
-		}
-
-		if err := json.Unmarshal(body, &metadata); err != nil {
-			return spec.DataVersionUnknown, errors.Wrap(err, "no consensus version header and failed to parse response")
-		}
-
-		return metadata.Version, nil
-	}
-
-	if len(respConsensusVersions) != 1 {
-		return spec.DataVersionUnknown, errors.New("malformed consensus version", z.Int("entries", len(respConsensusVersions)))
-	}
-
-	var dataVersion spec.DataVersion
-
-	err := dataVersion.UnmarshalJSON([]byte(fmt.Sprintf("%q", respConsensusVersions[0])))
-	if err != nil {
-		return spec.DataVersionUnknown, errors.Wrap(err, "unmarshal consensus version header to data version")
-	}
-
-	return dataVersion, nil
-}
diff --git a/app/eth2wrap/lazy.go b/app/eth2wrap/lazy.go
index 51a436d5..08ea2633 100644
--- a/app/eth2wrap/lazy.go
+++ b/app/eth2wrap/lazy.go
@@ -6,11 +6,6 @@ import (
 	"context"
 	"sync"
 	"time"
-
-	"github.com/attestantio/go-eth2-client/spec"
-
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
-	"github.com/obolnetwork/charon/eth2util/statecomm"
 )
 
 //go:generate mockery --name=Client --output=mocks --outpkg=mocks --case=underscore
@@ -166,66 +161,3 @@ func (l *lazy) SetValidatorCache(valCache func(context.Context) (ActiveValidator
 		cl.SetValidatorCache(valCache)
 	}
 }
-
-func (l *lazy) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConfigResponse, error) {
-	cl, err := l.getOrCreateClient(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	return cl.ProposerConfig(ctx)
-}
-
-func (l *lazy) AggregateBeaconCommitteeSelections(ctx context.Context, partialSelections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) {
-	cl, err := l.getOrCreateClient(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	return cl.AggregateBeaconCommitteeSelections(ctx, partialSelections)
-}
-
-func (l *lazy) AggregateSyncCommitteeSelections(ctx context.Context, partialSelections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) {
-	cl, err := l.getOrCreateClient(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	return cl.AggregateSyncCommitteeSelections(ctx, partialSelections)
-}
-
-func (l *lazy) BlockAttestations(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error) {
-	cl, err := l.getOrCreateClient(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	return cl.BlockAttestations(ctx, stateID)
-}
-
-func (l *lazy) Block(ctx context.Context, stateID string) (*spec.VersionedSignedBeaconBlock, error) {
-	cl, err := l.getOrCreateClient(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	return cl.Block(ctx, stateID)
-}
-
-func (l *lazy) BeaconStateCommittees(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error) {
-	cl, err := l.getOrCreateClient(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	return cl.BeaconStateCommittees(ctx, slot)
-}
-
-func (l *lazy) NodePeerCount(ctx context.Context) (int, error) {
-	cl, err := l.getOrCreateClient(ctx)
-	if err != nil {
-		return 0, err
-	}
-
-	return cl.NodePeerCount(ctx)
-}
diff --git a/app/eth2wrap/mocks/client.go b/app/eth2wrap/mocks/client.go
index 839f509a..f7ab2197 100644
--- a/app/eth2wrap/mocks/client.go
+++ b/app/eth2wrap/mocks/client.go
@@ -10,8 +10,6 @@ import (
 
 	context "context"
 
-	eth2exp "github.com/obolnetwork/charon/eth2util/eth2exp"
-
 	eth2wrap "github.com/obolnetwork/charon/app/eth2wrap"
 
 	mock "github.com/stretchr/testify/mock"
@@ -20,8 +18,6 @@ import (
 
 	spec "github.com/attestantio/go-eth2-client/spec"
 
-	statecomm "github.com/obolnetwork/charon/eth2util/statecomm"
-
 	time "time"
 
 	v1 "github.com/attestantio/go-eth2-client/api/v1"
@@ -110,66 +106,6 @@ func (_m *Client) AggregateAttestation(ctx context.Context, opts *api.AggregateA
 	return r0, r1
 }
 
-// AggregateBeaconCommitteeSelections provides a mock function with given fields: ctx, partialSelections
-func (_m *Client) AggregateBeaconCommitteeSelections(ctx context.Context, partialSelections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) {
-	ret := _m.Called(ctx, partialSelections)
-
-	if len(ret) == 0 {
-		panic("no return value specified for AggregateBeaconCommitteeSelections")
-	}
-
-	var r0 []*eth2exp.BeaconCommitteeSelection
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error)); ok {
-		return rf(ctx, partialSelections)
-	}
-	if rf, ok := ret.Get(0).(func(context.Context, []*eth2exp.BeaconCommitteeSelection) []*eth2exp.BeaconCommitteeSelection); ok {
-		r0 = rf(ctx, partialSelections)
-	} else {
-		if ret.Get(0) != nil {
-			r0 = ret.Get(0).([]*eth2exp.BeaconCommitteeSelection)
-		}
-	}
-
-	if rf, ok := ret.Get(1).(func(context.Context, []*eth2exp.BeaconCommitteeSelection) error); ok {
-		r1 = rf(ctx, partialSelections)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
-// AggregateSyncCommitteeSelections provides a mock function with given fields: ctx, partialSelections
-func (_m *Client) AggregateSyncCommitteeSelections(ctx context.Context, partialSelections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) {
-	ret := _m.Called(ctx, partialSelections)
-
-	if len(ret) == 0 {
-		panic("no return value specified for AggregateSyncCommitteeSelections")
-	}
-
-	var r0 []*eth2exp.SyncCommitteeSelection
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error)); ok {
-		return rf(ctx, partialSelections)
-	}
-	if rf, ok := ret.Get(0).(func(context.Context, []*eth2exp.SyncCommitteeSelection) []*eth2exp.SyncCommitteeSelection); ok {
-		r0 = rf(ctx, partialSelections)
-	} else {
-		if ret.Get(0) != nil {
-			r0 = ret.Get(0).([]*eth2exp.SyncCommitteeSelection)
-		}
-	}
-
-	if rf, ok := ret.Get(1).(func(context.Context, []*eth2exp.SyncCommitteeSelection) error); ok {
-		r1 = rf(ctx, partialSelections)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
 // AttestationData provides a mock function with given fields: ctx, opts
 func (_m *Client) AttestationData(ctx context.Context, opts *api.AttestationDataOpts) (*api.Response[*phase0.AttestationData], error) {
 	ret := _m.Called(ctx, opts)
@@ -230,28 +166,28 @@ func (_m *Client) AttesterDuties(ctx context.Context, opts *api.AttesterDutiesOp
 	return r0, r1
 }
 
-// BeaconBlockRoot provides a mock function with given fields: ctx, opts
-func (_m *Client) BeaconBlockRoot(ctx context.Context, opts *api.BeaconBlockRootOpts) (*api.Response[*phase0.Root], error) {
+// BeaconBlockAttestations provides a mock function with given fields: ctx, opts
+func (_m *Client) BeaconBlockAttestations(ctx context.Context, opts *api.BeaconBlockAttestationsOpts) (*api.Response[[]*spec.VersionedAttestation], error) {
 	ret := _m.Called(ctx, opts)
 
 	if len(ret) == 0 {
-		panic("no return value specified for BeaconBlockRoot")
+		panic("no return value specified for BeaconBlockAttestations")
 	}
 
-	var r0 *api.Response[*phase0.Root]
+	var r0 *api.Response[[]*spec.VersionedAttestation]
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconBlockRootOpts) (*api.Response[*phase0.Root], error)); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconBlockAttestationsOpts) (*api.Response[[]*spec.VersionedAttestation], error)); ok {
 		return rf(ctx, opts)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconBlockRootOpts) *api.Response[*phase0.Root]); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconBlockAttestationsOpts) *api.Response[[]*spec.VersionedAttestation]); ok {
 		r0 = rf(ctx, opts)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*api.Response[*phase0.Root])
+			r0 = ret.Get(0).(*api.Response[[]*spec.VersionedAttestation])
 		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *api.BeaconBlockRootOpts) error); ok {
+	if rf, ok := ret.Get(1).(func(context.Context, *api.BeaconBlockAttestationsOpts) error); ok {
 		r1 = rf(ctx, opts)
 	} else {
 		r1 = ret.Error(1)
@@ -260,29 +196,29 @@ func (_m *Client) BeaconBlockRoot(ctx context.Context, opts *api.BeaconBlockRoot
 	return r0, r1
 }
 
-// BeaconStateCommittees provides a mock function with given fields: ctx, slot
-func (_m *Client) BeaconStateCommittees(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error) {
-	ret := _m.Called(ctx, slot)
+// BeaconBlockRoot provides a mock function with given fields: ctx, opts
+func (_m *Client) BeaconBlockRoot(ctx context.Context, opts *api.BeaconBlockRootOpts) (*api.Response[*phase0.Root], error) {
+	ret := _m.Called(ctx, opts)
 
 	if len(ret) == 0 {
-		panic("no return value specified for BeaconStateCommittees")
+		panic("no return value specified for BeaconBlockRoot")
 	}
 
-	var r0 []*statecomm.StateCommittee
+	var r0 *api.Response[*phase0.Root]
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, uint64) ([]*statecomm.StateCommittee, error)); ok {
-		return rf(ctx, slot)
+	if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconBlockRootOpts) (*api.Response[*phase0.Root], error)); ok {
+		return rf(ctx, opts)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, uint64) []*statecomm.StateCommittee); ok {
-		r0 = rf(ctx, slot)
+	if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconBlockRootOpts) *api.Response[*phase0.Root]); ok {
+		r0 = rf(ctx, opts)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).([]*statecomm.StateCommittee)
+			r0 = ret.Get(0).(*api.Response[*phase0.Root])
 		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, uint64) error); ok {
-		r1 = rf(ctx, slot)
+	if rf, ok := ret.Get(1).(func(context.Context, *api.BeaconBlockRootOpts) error); ok {
+		r1 = rf(ctx, opts)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -290,29 +226,29 @@ func (_m *Client) BeaconStateCommittees(ctx context.Context, slot uint64) ([]*st
 	return r0, r1
 }
 
-// Block provides a mock function with given fields: ctx, stateID
-func (_m *Client) Block(ctx context.Context, stateID string) (*spec.VersionedSignedBeaconBlock, error) {
-	ret := _m.Called(ctx, stateID)
+// BeaconCommitteeSelections provides a mock function with given fields: ctx, opts
+func (_m *Client) BeaconCommitteeSelections(ctx context.Context, opts *api.BeaconCommitteeSelectionsOpts) (*api.Response[[]*v1.BeaconCommitteeSelection], error) {
+	ret := _m.Called(ctx, opts)
 
 	if len(ret) == 0 {
-		panic("no return value specified for Block")
+		panic("no return value specified for BeaconCommitteeSelections")
 	}
 
-	var r0 *spec.VersionedSignedBeaconBlock
+	var r0 *api.Response[[]*v1.BeaconCommitteeSelection]
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, string) (*spec.VersionedSignedBeaconBlock, error)); ok {
-		return rf(ctx, stateID)
+	if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconCommitteeSelectionsOpts) (*api.Response[[]*v1.BeaconCommitteeSelection], error)); ok {
+		return rf(ctx, opts)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, string) *spec.VersionedSignedBeaconBlock); ok {
-		r0 = rf(ctx, stateID)
+	if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconCommitteeSelectionsOpts) *api.Response[[]*v1.BeaconCommitteeSelection]); ok {
+		r0 = rf(ctx, opts)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*spec.VersionedSignedBeaconBlock)
+			r0 = ret.Get(0).(*api.Response[[]*v1.BeaconCommitteeSelection])
 		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
-		r1 = rf(ctx, stateID)
+	if rf, ok := ret.Get(1).(func(context.Context, *api.BeaconCommitteeSelectionsOpts) error); ok {
+		r1 = rf(ctx, opts)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -320,29 +256,29 @@ func (_m *Client) Block(ctx context.Context, stateID string) (*spec.VersionedSig
 	return r0, r1
 }
 
-// BlockAttestations provides a mock function with given fields: ctx, stateID
-func (_m *Client) BlockAttestations(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error) {
-	ret := _m.Called(ctx, stateID)
+// BeaconCommittees provides a mock function with given fields: ctx, opts
+func (_m *Client) BeaconCommittees(ctx context.Context, opts *api.BeaconCommitteesOpts) (*api.Response[[]*v1.BeaconCommittee], error) {
+	ret := _m.Called(ctx, opts)
 
 	if len(ret) == 0 {
-		panic("no return value specified for BlockAttestations")
+		panic("no return value specified for BeaconCommittees")
 	}
 
-	var r0 []*spec.VersionedAttestation
+	var r0 *api.Response[[]*v1.BeaconCommittee]
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, string) ([]*spec.VersionedAttestation, error)); ok {
-		return rf(ctx, stateID)
+	if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconCommitteesOpts) (*api.Response[[]*v1.BeaconCommittee], error)); ok {
+		return rf(ctx, opts)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, string) []*spec.VersionedAttestation); ok {
-		r0 = rf(ctx, stateID)
+	if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconCommitteesOpts) *api.Response[[]*v1.BeaconCommittee]); ok {
+		r0 = rf(ctx, opts)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).([]*spec.VersionedAttestation)
+			r0 = ret.Get(0).(*api.Response[[]*v1.BeaconCommittee])
 		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
-		r1 = rf(ctx, stateID)
+	if rf, ok := ret.Get(1).(func(context.Context, *api.BeaconCommitteesOpts) error); ok {
+		r1 = rf(ctx, opts)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -614,27 +550,29 @@ func (_m *Client) Name() string {
 	return r0
 }
 
-// NodePeerCount provides a mock function with given fields: ctx
-func (_m *Client) NodePeerCount(ctx context.Context) (int, error) {
-	ret := _m.Called(ctx)
+// NodePeerCount provides a mock function with given fields: ctx, opts
+func (_m *Client) NodePeerCount(ctx context.Context, opts *api.NodePeerCountOpts) (*api.Response[*v1.PeerCount], error) {
+	ret := _m.Called(ctx, opts)
 
 	if len(ret) == 0 {
 		panic("no return value specified for NodePeerCount")
 	}
 
-	var r0 int
+	var r0 *api.Response[*v1.PeerCount]
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context) (int, error)); ok {
-		return rf(ctx)
+	if rf, ok := ret.Get(0).(func(context.Context, *api.NodePeerCountOpts) (*api.Response[*v1.PeerCount], error)); ok {
+		return rf(ctx, opts)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context) int); ok {
-		r0 = rf(ctx)
+	if rf, ok := ret.Get(0).(func(context.Context, *api.NodePeerCountOpts) *api.Response[*v1.PeerCount]); ok {
+		r0 = rf(ctx, opts)
 	} else {
-		r0 = ret.Get(0).(int)
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*api.Response[*v1.PeerCount])
+		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context) error); ok {
-		r1 = rf(ctx)
+	if rf, ok := ret.Get(1).(func(context.Context, *api.NodePeerCountOpts) error); ok {
+		r1 = rf(ctx, opts)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -732,36 +670,6 @@ func (_m *Client) Proposal(ctx context.Context, opts *api.ProposalOpts) (*api.Re
 	return r0, r1
 }
 
-// ProposerConfig provides a mock function with given fields: ctx
-func (_m *Client) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConfigResponse, error) {
-	ret := _m.Called(ctx)
-
-	if len(ret) == 0 {
-		panic("no return value specified for ProposerConfig")
-	}
-
-	var r0 *eth2exp.ProposerConfigResponse
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context) (*eth2exp.ProposerConfigResponse, error)); ok {
-		return rf(ctx)
-	}
-	if rf, ok := ret.Get(0).(func(context.Context) *eth2exp.ProposerConfigResponse); ok {
-		r0 = rf(ctx)
-	} else {
-		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*eth2exp.ProposerConfigResponse)
-		}
-	}
-
-	if rf, ok := ret.Get(1).(func(context.Context) error); ok {
-		r1 = rf(ctx)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
 // ProposerDuties provides a mock function with given fields: ctx, opts
 func (_m *Client) ProposerDuties(ctx context.Context, opts *api.ProposerDutiesOpts) (*api.Response[[]*v1.ProposerDuty], error) {
 	ret := _m.Called(ctx, opts)
@@ -1176,6 +1084,36 @@ func (_m *Client) SyncCommitteeDuties(ctx context.Context, opts *api.SyncCommitt
 	return r0, r1
 }
 
+// SyncCommitteeSelections provides a mock function with given fields: ctx, opts
+func (_m *Client) SyncCommitteeSelections(ctx context.Context, opts *api.SyncCommitteeSelectionsOpts) (*api.Response[[]*v1.SyncCommitteeSelection], error) {
+	ret := _m.Called(ctx, opts)
+
+	if len(ret) == 0 {
+		panic("no return value specified for SyncCommitteeSelections")
+	}
+
+	var r0 *api.Response[[]*v1.SyncCommitteeSelection]
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *api.SyncCommitteeSelectionsOpts) (*api.Response[[]*v1.SyncCommitteeSelection], error)); ok {
+		return rf(ctx, opts)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *api.SyncCommitteeSelectionsOpts) *api.Response[[]*v1.SyncCommitteeSelection]); ok {
+		r0 = rf(ctx, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*api.Response[[]*v1.SyncCommitteeSelection])
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *api.SyncCommitteeSelectionsOpts) error); ok {
+		r1 = rf(ctx, opts)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
 // Validators provides a mock function with given fields: ctx, opts
 func (_m *Client) Validators(ctx context.Context, opts *api.ValidatorsOpts) (*api.Response[map[phase0.ValidatorIndex]*v1.Validator], error) {
 	ret := _m.Called(ctx, opts)
diff --git a/app/eth2wrap/multi.go b/app/eth2wrap/multi.go
index b3868297..bc159e0f 100644
--- a/app/eth2wrap/multi.go
+++ b/app/eth2wrap/multi.go
@@ -4,11 +4,6 @@ package eth2wrap
 
 import (
 	"context"
-
-	"github.com/attestantio/go-eth2-client/spec"
-
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
-	"github.com/obolnetwork/charon/eth2util/statecomm"
 )
 
 // NewMultiForT creates a new mutil client for testing.
@@ -124,137 +119,3 @@ func (m multi) CompleteValidators(ctx context.Context) (CompleteValidators, erro
 
 	return res0, err
 }
-
-func (m multi) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConfigResponse, error) {
-	const label = "proposer_config"
-	defer latency(ctx, label, false)()
-	defer incRequest(label)
-
-	res0, err := provide(ctx, m.clients, m.fallbacks,
-		func(ctx context.Context, args provideArgs) (*eth2exp.ProposerConfigResponse, error) {
-			return args.client.ProposerConfig(ctx)
-		},
-		nil, m.selector,
-	)
-	if err != nil {
-		incError(label)
-		err = wrapError(ctx, err, label)
-	}
-
-	return res0, err
-}
-
-func (m multi) AggregateBeaconCommitteeSelections(ctx context.Context, selections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) {
-	const label = "aggregate_beacon_committee_selections"
-	defer latency(ctx, label, false)()
-	defer incRequest(label)
-
-	res0, err := provide(ctx, m.clients, m.fallbacks,
-		func(ctx context.Context, args provideArgs) ([]*eth2exp.BeaconCommitteeSelection, error) {
-			return args.client.AggregateBeaconCommitteeSelections(ctx, selections)
-		},
-		nil, m.selector,
-	)
-	if err != nil {
-		incError(label)
-		err = wrapError(ctx, err, label)
-	}
-
-	return res0, err
-}
-
-func (m multi) AggregateSyncCommitteeSelections(ctx context.Context, selections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) {
-	const label = "aggregate_sync_committee_selections"
-	defer latency(ctx, label, false)()
-	defer incRequest(label)
-
-	res, err := provide(ctx, m.clients, m.fallbacks,
-		func(ctx context.Context, args provideArgs) ([]*eth2exp.SyncCommitteeSelection, error) {
-			return args.client.AggregateSyncCommitteeSelections(ctx, selections)
-		},
-
-		nil, m.selector,
-	)
-	if err != nil {
-		incError(label)
-		err = wrapError(ctx, err, label)
-	}
-
-	return res, err
-}
-
-func (m multi) BlockAttestations(ctx context.Context, stateID string) ([]*spec.VersionedAttestation, error) {
-	const label = "block_attestations_v2"
-	defer latency(ctx, label, false)()
-	defer incRequest(label)
-
-	res, err := provide(ctx, m.clients, m.fallbacks,
-		func(ctx context.Context, args provideArgs) ([]*spec.VersionedAttestation, error) {
-			return args.client.BlockAttestations(ctx, stateID)
-		},
-		nil, m.selector,
-	)
-	if err != nil {
-		incError(label)
-		err = wrapError(ctx, err, label)
-	}
-
-	return res, err
-}
-
-func (m multi) Block(ctx context.Context, stateID string) (*spec.VersionedSignedBeaconBlock, error) {
-	const label = "block_v2"
-	defer latency(ctx, label, false)()
-	defer incRequest(label)
-
-	res, err := provide(ctx, m.clients, m.fallbacks,
-		func(ctx context.Context, args provideArgs) (*spec.VersionedSignedBeaconBlock, error) {
-			return args.client.Block(ctx, stateID)
-		},
-		nil, m.selector,
-	)
-	if err != nil {
-		incError(label)
-		err = wrapError(ctx, err, label)
-	}
-
-	return res, err
-}
-
-func (m multi) BeaconStateCommittees(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error) {
-	const label = "beacon_state_committees"
-	defer latency(ctx, label, false)()
-	defer incRequest(label)
-
-	res, err := provide(ctx, m.clients, m.fallbacks,
-		func(ctx context.Context, args provideArgs) ([]*statecomm.StateCommittee, error) {
-			return args.client.BeaconStateCommittees(ctx, slot)
-		},
-		nil, m.selector,
-	)
-	if err != nil {
-		incError(label)
-		err = wrapError(ctx, err, label)
-	}
-
-	return res, err
-}
-
-func (m multi) NodePeerCount(ctx context.Context) (int, error) {
-	const label = "node_peer_count"
-	defer latency(ctx, label, false)()
-	defer incRequest(label)
-
-	res, err := provide(ctx, m.clients, m.fallbacks,
-		func(ctx context.Context, args provideArgs) (int, error) {
-			return args.client.NodePeerCount(ctx)
-		},
-		nil, m.selector,
-	)
-	if err != nil {
-		incError(label)
-		err = wrapError(ctx, err, label)
-	}
-
-	return res, err
-}
diff --git a/app/eth2wrap/version.go b/app/eth2wrap/version.go
index 4c4de6ff..f93f4a97 100644
--- a/app/eth2wrap/version.go
+++ b/app/eth2wrap/version.go
@@ -81,6 +81,7 @@ func checkBeaconNodeVersionStatus(bnVersion string) (beaconNodeVersionStatus Bea
 func CheckBeaconNodeVersion(ctx context.Context, bnVersion string) {
 	status, currentVersion, minVersion := checkBeaconNodeVersionStatus(bnVersion)
 
+	//nolint:revive // enforce-switch-style: the list is exhaustive and there is no need for default
 	switch status {
 	case VersionFormatError:
 		log.Warn(ctx, "Failed to parse beacon node version string due to unexpected format",
diff --git a/app/genssz/genssz.go b/app/genssz/genssz.go
index 15e5e30b..8a1684c5 100644
--- a/app/genssz/genssz.go
+++ b/app/genssz/genssz.go
@@ -40,7 +40,7 @@ var pkgFlag = flag.String("package", "", "The fully qualified go package to gene
 func main() {
 	ctx := context.Background()
 
-	pkg, err := getPkg()
+	pkg, err := getPkg(ctx)
 	if err != nil {
 		log.Error(context.Background(), "Failed to get module", err)
 		os.Exit(1)
@@ -188,12 +188,12 @@ func writeTemplate(types []Type, filename string, pkg string) error {
 	return nil
 }
 
-func getPkg() (string, error) {
+func getPkg(ctx context.Context) (string, error) {
 	if *pkgFlag != "" {
 		return *pkgFlag, nil
 	}
 
-	out, err := exec.Command("go", "list", ".").Output()
+	out, err := exec.CommandContext(ctx, "go", "list", ".").Output()
 	if err != nil {
 		return "", errors.Wrap(err, "get package")
 	}
diff --git a/app/log/config.go b/app/log/config.go
index 15d77f24..5a008bbc 100644
--- a/app/log/config.go
+++ b/app/log/config.go
@@ -133,9 +133,9 @@ func (c Config) InferColor() (bool, error) {
 		return true, nil
 	case colorAuto, "":
 		return term.IsTerminal(int(os.Stderr.Fd())), nil
+	default:
+		return false, errors.New("invalid --log-color value", z.Str("value", c.Color))
 	}
-
-	return false, errors.New("invalid --log-color value", z.Str("value", c.Color))
 }
 
 // DefaultConfig returns the default logging config.
diff --git a/app/monitoringapi.go b/app/monitoringapi.go
index de3dbc78..cc33e24a 100644
--- a/app/monitoringapi.go
+++ b/app/monitoringapi.go
@@ -154,7 +154,7 @@ func startReadyChecker(ctx context.Context, p2pNode host.Host, eth2Cl eth2wrap.C
 		ticker := clock.NewTicker(slotDuration)
 		peerCountTicker := clock.NewTicker(1 * time.Minute)
 
-		var bnPeerCount *int // Beacon node peer count value which is queried every minute
+		var bnPeerCount *uint64 // Beacon node peer count value which is queried every minute
 
 		currVAPICount := 0
 		prevVAPICount := 1 // Assume connected.
@@ -167,18 +167,18 @@ func startReadyChecker(ctx context.Context, p2pNode host.Host, eth2Cl eth2wrap.C
 
 		// beaconNodePeerCount queries beacon node peer count and sets the peer count gauge if err is nil.
 		beaconNodePeerCount := func() {
-			peerCount, err := eth2Cl.NodePeerCount(ctx)
+			peerCount, err := eth2Cl.NodePeerCount(ctx, &eth2api.NodePeerCountOpts{})
 			if err != nil {
 				log.Warn(ctx, "Failed to get beacon node peer count", err)
 				return
 			}
 
 			if bnPeerCount == nil {
-				bnPeerCount = new(int)
+				bnPeerCount = new(uint64)
 			}
 
-			bnPeerCount = &peerCount
-			beaconNodePeerCountGauge.Set(float64(peerCount))
+			bnPeerCount = &peerCount.Data.Connected
+			beaconNodePeerCountGauge.Set(float64(peerCount.Data.Connected))
 		}
 
 		for {
diff --git a/app/version/version.go b/app/version/version.go
index 22adcb36..cc0d7063 100644
--- a/app/version/version.go
+++ b/app/version/version.go
@@ -55,6 +55,7 @@ func GitCommit() (hash string, timestamp string) {
 			hash = s.Value[:hashLen]
 		case "vcs.time":
 			timestamp = s.Value
+		default:
 		}
 	}
 
diff --git a/cmd/createcluster.go b/cmd/createcluster.go
index 25ba3675..97fb9b52 100644
--- a/cmd/createcluster.go
+++ b/cmd/createcluster.go
@@ -202,6 +202,8 @@ func runCreateCluster(ctx context.Context, w io.Writer, conf clusterConfig) erro
 			return err
 		}
 
+		log.Debug(ctx, "Read keys from split-keys-dir", z.Int("count", len(secrets)))
+
 		// Needed if --split-existing-keys is called without a definition file.
 		conf.NumDVs = len(secrets)
 	}
@@ -618,15 +620,20 @@ func getKeys(splitKeysDir string, useSequencedKeys bool) ([]tbls.PrivateKey, err
 		return nil, errors.New("--split-keys-dir required when splitting keys")
 	}
 
-	files, err := keystore.LoadFilesUnordered(splitKeysDir)
-	if err != nil {
-		return nil, err
-	}
-
 	if useSequencedKeys {
+		files, err := keystore.LoadFilesUnordered(splitKeysDir)
+		if err != nil {
+			return nil, err
+		}
+
 		return files.SequencedKeys()
 	}
 
+	files, err := keystore.LoadFilesRecursively(splitKeysDir)
+	if err != nil {
+		return nil, err
+	}
+
 	return files.Keys(), nil
 }
 
diff --git a/cmd/exit.go b/cmd/exit.go
index 02f2f17e..c0e01206 100644
--- a/cmd/exit.go
+++ b/cmd/exit.go
@@ -146,6 +146,7 @@ func bindExitFlags(cmd *cobra.Command, config *exitConfig, flags []exitCLIFlag)
 			return s
 		}
 
+		//nolint:revive // enforce-switch-style: the list is exhaustive and there is no need for default
 		switch flag {
 		case publishAddress:
 			cmd.Flags().StringVar(&config.PublishAddress, publishAddress.String(), "https://api.obol.tech/v1", maybeRequired("The URL of the remote API."))
diff --git a/cmd/relay/relay.go b/cmd/relay/relay.go
index 6160f2e0..f41f147a 100644
--- a/cmd/relay/relay.go
+++ b/cmd/relay/relay.go
@@ -166,7 +166,7 @@ func newENRHandler(ctx context.Context, p2pNode host.Host, p2pKey *k1.PrivateKey
 		}
 
 		resolveExtHost := func() {
-			ip, err := net.LookupIP(config.ExternalHost)
+			ip, err := net.DefaultResolver.LookupIPAddr(ctx, config.ExternalHost)
 			if err != nil {
 				log.Warn(ctx, "Failed to resolve external host", err, z.Str("host", config.ExternalHost))
 				return
@@ -174,7 +174,7 @@ func newENRHandler(ctx context.Context, p2pNode host.Host, p2pKey *k1.PrivateKey
 
 			extHostMu.Lock()
 
-			extHostIP = ip[0]
+			extHostIP = ip[0].IP
 
 			extHostMu.Unlock()
 		}
diff --git a/core/consensus/mocks/debugger.go b/core/consensus/mocks/debugger.go
index a1711c08..02c99235 100644
--- a/core/consensus/mocks/debugger.go
+++ b/core/consensus/mocks/debugger.go
@@ -1,6 +1,6 @@
 // Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
 
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.53.3. DO NOT EDIT.
 
 package mocks
 
diff --git a/core/fetcher/fetcher.go b/core/fetcher/fetcher.go
index 52f72536..502d34a3 100644
--- a/core/fetcher/fetcher.go
+++ b/core/fetcher/fetcher.go
@@ -324,7 +324,7 @@ func (f *Fetcher) fetchContributionData(ctx context.Context, slot uint64, defSet
 			return core.UnsignedDataSet{}, errors.New("invalid sync committee selection")
 		}
 
-		subcommIdx := uint64(selection.SubcommitteeIndex)
+		subcommIdx := selection.SubcommitteeIndex
 
 		// Check if the validator is an aggregator for the sync committee.
 		ok, err = eth2exp.IsSyncCommAggregator(ctx, f.eth2Cl, selection.SelectionProof)
diff --git a/core/infosync/infosync.go b/core/infosync/infosync.go
index 769fc903..3b418e73 100644
--- a/core/infosync/infosync.go
+++ b/core/infosync/infosync.go
@@ -61,6 +61,7 @@ func New(prioritiser *priority.Component, versions []version.SemVer, protocols [
 					res.protocols = append(res.protocols, protocol.ID(prio))
 				case topicProposal:
 					res.proposals = append(res.proposals, core.ProposalType(prio))
+				default:
 				}
 			}
 		}
diff --git a/core/mocks/consensus.go b/core/mocks/consensus.go
index 738bef7b..424c90a8 100644
--- a/core/mocks/consensus.go
+++ b/core/mocks/consensus.go
@@ -1,6 +1,6 @@
 // Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
 
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.53.3. DO NOT EDIT.
 
 package mocks
 
@@ -54,7 +54,7 @@ func (_m *Consensus) Propose(_a0 context.Context, _a1 core.Duty, _a2 core.Unsign
 	return r0
 }
 
-// ProtocolID provides a mock function with given fields:
+// ProtocolID provides a mock function with no fields
 func (_m *Consensus) ProtocolID() protocol.ID {
 	ret := _m.Called()
 
diff --git a/core/mocks/deadliner.go b/core/mocks/deadliner.go
index 9a548e14..28923d91 100644
--- a/core/mocks/deadliner.go
+++ b/core/mocks/deadliner.go
@@ -1,6 +1,6 @@
 // Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
 
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.53.3. DO NOT EDIT.
 
 package mocks
 
@@ -32,7 +32,7 @@ func (_m *Deadliner) Add(duty core.Duty) bool {
 	return r0
 }
 
-// C provides a mock function with given fields:
+// C provides a mock function with no fields
 func (_m *Deadliner) C() <-chan core.Duty {
 	ret := _m.Called()
 
diff --git a/core/signeddata.go b/core/signeddata.go
index 99967ebc..9454297f 100644
--- a/core/signeddata.go
+++ b/core/signeddata.go
@@ -26,7 +26,6 @@ import (
 	"github.com/obolnetwork/charon/app/log"
 	"github.com/obolnetwork/charon/app/z"
 	"github.com/obolnetwork/charon/eth2util"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 	"github.com/obolnetwork/charon/eth2util/signing"
 )
 
@@ -1163,14 +1162,14 @@ func (s SignedRandao) clone() (SignedRandao, error) {
 }
 
 // NewBeaconCommitteeSelection is a convenience function which returns new signed BeaconCommitteeSelection.
-func NewBeaconCommitteeSelection(selection *eth2exp.BeaconCommitteeSelection) BeaconCommitteeSelection {
+func NewBeaconCommitteeSelection(selection *eth2v1.BeaconCommitteeSelection) BeaconCommitteeSelection {
 	return BeaconCommitteeSelection{
 		BeaconCommitteeSelection: *selection,
 	}
 }
 
 // NewPartialSignedBeaconCommitteeSelection is a convenience function which returns new partially signed BeaconCommitteeSelection.
-func NewPartialSignedBeaconCommitteeSelection(selection *eth2exp.BeaconCommitteeSelection, shareIdx int) ParSignedData {
+func NewPartialSignedBeaconCommitteeSelection(selection *eth2v1.BeaconCommitteeSelection, shareIdx int) ParSignedData {
 	return ParSignedData{
 		SignedData: NewBeaconCommitteeSelection(selection),
 		ShareIdx:   shareIdx,
@@ -1179,7 +1178,7 @@ func NewPartialSignedBeaconCommitteeSelection(selection *eth2exp.BeaconCommittee
 
 // BeaconCommitteeSelection wraps a BeaconCommitteeSelection which implements SignedData.
 type BeaconCommitteeSelection struct {
-	eth2exp.BeaconCommitteeSelection
+	eth2v1.BeaconCommitteeSelection
 }
 
 func (s BeaconCommitteeSelection) MessageRoot() ([32]byte, error) {
@@ -1225,14 +1224,14 @@ func (s *BeaconCommitteeSelection) UnmarshalJSON(input []byte) error {
 }
 
 // NewSyncCommitteeSelection is a convenience function which returns new signed SyncCommitteeSelection.
-func NewSyncCommitteeSelection(selection *eth2exp.SyncCommitteeSelection) SyncCommitteeSelection {
+func NewSyncCommitteeSelection(selection *eth2v1.SyncCommitteeSelection) SyncCommitteeSelection {
 	return SyncCommitteeSelection{
 		SyncCommitteeSelection: *selection,
 	}
 }
 
 // NewPartialSignedSyncCommitteeSelection is a convenience function which returns new partially signed SyncCommitteeSelection.
-func NewPartialSignedSyncCommitteeSelection(selection *eth2exp.SyncCommitteeSelection, shareIdx int) ParSignedData {
+func NewPartialSignedSyncCommitteeSelection(selection *eth2v1.SyncCommitteeSelection, shareIdx int) ParSignedData {
 	return ParSignedData{
 		SignedData: NewSyncCommitteeSelection(selection),
 		ShareIdx:   shareIdx,
@@ -1241,7 +1240,7 @@ func NewPartialSignedSyncCommitteeSelection(selection *eth2exp.SyncCommitteeSele
 
 // SyncCommitteeSelection wraps an eth2exp.SyncCommitteeSelection and implements SignedData.
 type SyncCommitteeSelection struct {
-	eth2exp.SyncCommitteeSelection
+	eth2v1.SyncCommitteeSelection
 }
 
 // MessageRoot returns the signing roots for the provided SyncCommitteeSelection.
@@ -1249,7 +1248,7 @@ type SyncCommitteeSelection struct {
 func (s SyncCommitteeSelection) MessageRoot() ([32]byte, error) {
 	data := altair.SyncAggregatorSelectionData{
 		Slot:              s.Slot,
-		SubcommitteeIndex: uint64(s.SubcommitteeIndex),
+		SubcommitteeIndex: s.SubcommitteeIndex,
 	}
 
 	return data.HashTreeRoot()
diff --git a/core/tracker/inclusion.go b/core/tracker/inclusion.go
index 131c3e5b..ed1bf1bc 100644
--- a/core/tracker/inclusion.go
+++ b/core/tracker/inclusion.go
@@ -23,7 +23,6 @@ import (
 	"github.com/obolnetwork/charon/app/log"
 	"github.com/obolnetwork/charon/app/z"
 	"github.com/obolnetwork/charon/core"
-	"github.com/obolnetwork/charon/eth2util/statecomm"
 )
 
 const (
@@ -59,7 +58,7 @@ type block struct {
 	Slot                   uint64
 	AttDuties              []*eth2v1.AttesterDuty
 	AttestationsByDataRoot map[eth2p0.Root]*eth2spec.VersionedAttestation
-	BeaconCommitees        []*statecomm.StateCommittee
+	BeaconCommitees        []*eth2v1.BeaconCommittee
 }
 
 // attCommittee is a versioned attestation with its aggregation bits mapped to the respective beacon committee
@@ -74,9 +73,9 @@ type trackerInclFunc func(core.Duty, core.PubKey, core.SignedData, error)
 // inclusionCore tracks the inclusion of submitted duties.
 // It has a simplified API to allow for easy testing.
 type inclusionCore struct {
-	mu              sync.Mutex
-	submissions     map[subkey]submission
-	stateCommittees map[eth2p0.Slot][]*statecomm.StateCommittee
+	mu               sync.Mutex
+	submissions      map[subkey]submission
+	beaconCommittees map[eth2p0.Slot][]*eth2v1.BeaconCommittee
 
 	trackerInclFunc trackerInclFunc
 	missedFunc      func(context.Context, submission)
@@ -232,12 +231,12 @@ func (i *inclusionCore) Trim(ctx context.Context, slot uint64) {
 	}
 
 	// Trim state committees
-	for key := range i.stateCommittees {
+	for key := range i.beaconCommittees {
 		if uint64(key) > slot {
 			continue
 		}
 
-		delete(i.stateCommittees, key)
+		delete(i.beaconCommittees, key)
 	}
 }
 
@@ -347,7 +346,7 @@ func (i *inclusionCore) CheckBlockAndAtts(ctx context.Context, block block) {
 
 	// Delete
 	if block.Slot >= InclMissedLag {
-		delete(i.stateCommittees, eth2p0.Slot(block.Slot-InclMissedLag))
+		delete(i.beaconCommittees, eth2p0.Slot(block.Slot-InclMissedLag))
 	}
 }
 
@@ -537,11 +536,11 @@ func NewInclusion(ctx context.Context, eth2Cl eth2wrap.Client, trackerInclFunc t
 	}
 
 	inclCore := &inclusionCore{
-		attIncludedFunc: reportAttInclusion,
-		missedFunc:      reportMissed,
-		trackerInclFunc: trackerInclFunc,
-		submissions:     make(map[subkey]submission),
-		stateCommittees: make(map[eth2p0.Slot][]*statecomm.StateCommittee),
+		attIncludedFunc:  reportAttInclusion,
+		missedFunc:       reportMissed,
+		trackerInclFunc:  trackerInclFunc,
+		submissions:      make(map[subkey]submission),
+		beaconCommittees: make(map[eth2p0.Slot][]*eth2v1.BeaconCommittee),
 	}
 
 	return &InclusionChecker{
@@ -660,11 +659,13 @@ func (a *InclusionChecker) checkBlock(ctx context.Context, slot uint64, attDutie
 		return a.checkBlockAndAtts(ctx, slot, attDuties)
 	}
 
-	block, err := a.eth2Cl.Block(ctx, strconv.FormatUint(slot, 10))
+	eth2Resp, err := a.eth2Cl.SignedBeaconBlock(ctx, &eth2api.SignedBeaconBlockOpts{Block: strconv.FormatUint(slot, 10)})
 	if err != nil {
 		return err
 	}
 
+	block := eth2Resp.Data
+
 	var found bool
 	if block != nil {
 		found = true
@@ -678,15 +679,17 @@ func (a *InclusionChecker) checkBlock(ctx context.Context, slot uint64, attDutie
 }
 
 func (a *InclusionChecker) checkBlockAndAtts(ctx context.Context, slot uint64, attDuties []*eth2v1.AttesterDuty) error {
-	atts, err := a.eth2Cl.BlockAttestations(ctx, strconv.FormatUint(slot, 10))
+	attsResp, err := a.eth2Cl.BeaconBlockAttestations(ctx, &eth2api.BeaconBlockAttestationsOpts{Block: strconv.FormatUint(slot, 10)})
 	if err != nil {
 		return err
-	} else if len(atts) == 0 {
+	} else if len(attsResp.Data) == 0 {
 		return nil // No block for this slot
 	}
 
+	atts := attsResp.Data
+
 	var (
-		committeesForState []*statecomm.StateCommittee
+		committeesForState []*eth2v1.BeaconCommittee
 		checkedSlots       []eth2p0.Slot
 	)
 
@@ -701,18 +704,20 @@ func (a *InclusionChecker) checkBlockAndAtts(ctx context.Context, slot uint64, a
 			continue
 		}
 
-		stateComms, ok := a.core.stateCommittees[attestationData.Slot]
+		beaconComms, ok := a.core.beaconCommittees[attestationData.Slot]
 		if ok {
-			committeesForState = append(committeesForState, stateComms...)
+			committeesForState = append(committeesForState, beaconComms...)
 		} else {
 			// Get the beacon committee for the above mentioned slot.
-			fetchedCommitteesForState, err := a.eth2Cl.BeaconStateCommittees(ctx, uint64(attestationData.Slot))
+			eth2Resp, err := a.eth2Cl.BeaconCommittees(ctx, &eth2api.BeaconCommitteesOpts{State: fmt.Sprintf("%v", attestationData.Slot)})
 			if err != nil {
 				return err
 			}
 
+			fetchedCommitteesForState := eth2Resp.Data
+
 			committeesForState = append(committeesForState, fetchedCommitteesForState...)
-			a.core.stateCommittees[attestationData.Slot] = fetchedCommitteesForState
+			a.core.beaconCommittees[attestationData.Slot] = fetchedCommitteesForState
 		}
 
 		checkedSlots = append(checkedSlots, attestationData.Slot)
@@ -897,7 +902,7 @@ func setAttestationAggregationBits(att eth2spec.VersionedAttestation, bits bitfi
 	}
 }
 
-func conjugateAggregationBits(att *attCommittee, attsMap map[eth2p0.Root]*attCommittee, root eth2p0.Root, committeesForState []*statecomm.StateCommittee) (map[eth2p0.CommitteeIndex]bitfield.Bitlist, error) {
+func conjugateAggregationBits(att *attCommittee, attsMap map[eth2p0.Root]*attCommittee, root eth2p0.Root, committeesForState []*eth2v1.BeaconCommittee) (map[eth2p0.CommitteeIndex]bitfield.Bitlist, error) {
 	switch att.Attestation.Version {
 	case eth2spec.DataVersionPhase0:
 		if att.Attestation.Phase0 == nil {
@@ -966,7 +971,7 @@ func conjugateAggregationBitsPhase0(att *attCommittee, attsMap map[eth2p0.Root]*
 	return nil
 }
 
-func conjugateAggregationBitsElectra(att *attCommittee, attsMap map[eth2p0.Root]*attCommittee, root eth2p0.Root, committeesForState []*statecomm.StateCommittee) (map[eth2p0.CommitteeIndex]bitfield.Bitlist, error) {
+func conjugateAggregationBitsElectra(att *attCommittee, attsMap map[eth2p0.Root]*attCommittee, root eth2p0.Root, committeesForState []*eth2v1.BeaconCommittee) (map[eth2p0.CommitteeIndex]bitfield.Bitlist, error) {
 	fullAttestationAggregationBits, err := att.Attestation.AggregationBits()
 	if err != nil {
 		return nil, err
diff --git a/core/validatorapi/eth2types.go b/core/validatorapi/eth2types.go
index a070ffe9..d4291d64 100644
--- a/core/validatorapi/eth2types.go
+++ b/core/validatorapi/eth2types.go
@@ -13,7 +13,6 @@ import (
 	eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
 
 	"github.com/obolnetwork/charon/app/errors"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 )
 
 // errorResponse an error response from the beacon-node api.
@@ -99,12 +98,12 @@ type aggregateAttestationV2Response struct {
 	Data    any    `json:"data"`
 }
 
-type aggregateBeaconCommitteeSelectionsJSON struct {
-	Data []*eth2exp.BeaconCommitteeSelection `json:"data"`
+type beaconCommitteeSelectionsJSON struct {
+	Data []*eth2v1.BeaconCommitteeSelection `json:"data"`
 }
 
-type aggregateSyncCommitteeSelectionsJSON struct {
-	Data []*eth2exp.SyncCommitteeSelection `json:"data"`
+type syncCommitteeSelectionsJSON struct {
+	Data []*eth2v1.SyncCommitteeSelection `json:"data"`
 }
 
 // root wraps eth2p0 root adding proper json marshalling.
diff --git a/core/validatorapi/mocks/handler.go b/core/validatorapi/mocks/handler.go
index 87b8c026..6d0b7f14 100644
--- a/core/validatorapi/mocks/handler.go
+++ b/core/validatorapi/mocks/handler.go
@@ -10,8 +10,6 @@ import (
 
 	context "context"
 
-	eth2exp "github.com/obolnetwork/charon/eth2util/eth2exp"
-
 	mock "github.com/stretchr/testify/mock"
 
 	phase0 "github.com/attestantio/go-eth2-client/spec/phase0"
@@ -56,66 +54,6 @@ func (_m *Handler) AggregateAttestation(ctx context.Context, opts *api.Aggregate
 	return r0, r1
 }
 
-// AggregateBeaconCommitteeSelections provides a mock function with given fields: ctx, partialSelections
-func (_m *Handler) AggregateBeaconCommitteeSelections(ctx context.Context, partialSelections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) {
-	ret := _m.Called(ctx, partialSelections)
-
-	if len(ret) == 0 {
-		panic("no return value specified for AggregateBeaconCommitteeSelections")
-	}
-
-	var r0 []*eth2exp.BeaconCommitteeSelection
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error)); ok {
-		return rf(ctx, partialSelections)
-	}
-	if rf, ok := ret.Get(0).(func(context.Context, []*eth2exp.BeaconCommitteeSelection) []*eth2exp.BeaconCommitteeSelection); ok {
-		r0 = rf(ctx, partialSelections)
-	} else {
-		if ret.Get(0) != nil {
-			r0 = ret.Get(0).([]*eth2exp.BeaconCommitteeSelection)
-		}
-	}
-
-	if rf, ok := ret.Get(1).(func(context.Context, []*eth2exp.BeaconCommitteeSelection) error); ok {
-		r1 = rf(ctx, partialSelections)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
-// AggregateSyncCommitteeSelections provides a mock function with given fields: ctx, partialSelections
-func (_m *Handler) AggregateSyncCommitteeSelections(ctx context.Context, partialSelections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) {
-	ret := _m.Called(ctx, partialSelections)
-
-	if len(ret) == 0 {
-		panic("no return value specified for AggregateSyncCommitteeSelections")
-	}
-
-	var r0 []*eth2exp.SyncCommitteeSelection
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error)); ok {
-		return rf(ctx, partialSelections)
-	}
-	if rf, ok := ret.Get(0).(func(context.Context, []*eth2exp.SyncCommitteeSelection) []*eth2exp.SyncCommitteeSelection); ok {
-		r0 = rf(ctx, partialSelections)
-	} else {
-		if ret.Get(0) != nil {
-			r0 = ret.Get(0).([]*eth2exp.SyncCommitteeSelection)
-		}
-	}
-
-	if rf, ok := ret.Get(1).(func(context.Context, []*eth2exp.SyncCommitteeSelection) error); ok {
-		r1 = rf(ctx, partialSelections)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
 // AttestationData provides a mock function with given fields: ctx, opts
 func (_m *Handler) AttestationData(ctx context.Context, opts *api.AttestationDataOpts) (*api.Response[*phase0.AttestationData], error) {
 	ret := _m.Called(ctx, opts)
@@ -176,28 +114,28 @@ func (_m *Handler) AttesterDuties(ctx context.Context, opts *api.AttesterDutiesO
 	return r0, r1
 }
 
-// NodeVersion provides a mock function with given fields: ctx, opts
-func (_m *Handler) NodeVersion(ctx context.Context, opts *api.NodeVersionOpts) (*api.Response[string], error) {
+// BeaconCommitteeSelections provides a mock function with given fields: ctx, opts
+func (_m *Handler) BeaconCommitteeSelections(ctx context.Context, opts *api.BeaconCommitteeSelectionsOpts) (*api.Response[[]*v1.BeaconCommitteeSelection], error) {
 	ret := _m.Called(ctx, opts)
 
 	if len(ret) == 0 {
-		panic("no return value specified for NodeVersion")
+		panic("no return value specified for BeaconCommitteeSelections")
 	}
 
-	var r0 *api.Response[string]
+	var r0 *api.Response[[]*v1.BeaconCommitteeSelection]
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *api.NodeVersionOpts) (*api.Response[string], error)); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconCommitteeSelectionsOpts) (*api.Response[[]*v1.BeaconCommitteeSelection], error)); ok {
 		return rf(ctx, opts)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *api.NodeVersionOpts) *api.Response[string]); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, *api.BeaconCommitteeSelectionsOpts) *api.Response[[]*v1.BeaconCommitteeSelection]); ok {
 		r0 = rf(ctx, opts)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*api.Response[string])
+			r0 = ret.Get(0).(*api.Response[[]*v1.BeaconCommitteeSelection])
 		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *api.NodeVersionOpts) error); ok {
+	if rf, ok := ret.Get(1).(func(context.Context, *api.BeaconCommitteeSelectionsOpts) error); ok {
 		r1 = rf(ctx, opts)
 	} else {
 		r1 = ret.Error(1)
@@ -206,28 +144,28 @@ func (_m *Handler) NodeVersion(ctx context.Context, opts *api.NodeVersionOpts) (
 	return r0, r1
 }
 
-// Proposal provides a mock function with given fields: ctx, opts
-func (_m *Handler) Proposal(ctx context.Context, opts *api.ProposalOpts) (*api.Response[*api.VersionedProposal], error) {
+// NodeVersion provides a mock function with given fields: ctx, opts
+func (_m *Handler) NodeVersion(ctx context.Context, opts *api.NodeVersionOpts) (*api.Response[string], error) {
 	ret := _m.Called(ctx, opts)
 
 	if len(ret) == 0 {
-		panic("no return value specified for Proposal")
+		panic("no return value specified for NodeVersion")
 	}
 
-	var r0 *api.Response[*api.VersionedProposal]
+	var r0 *api.Response[string]
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *api.ProposalOpts) (*api.Response[*api.VersionedProposal], error)); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, *api.NodeVersionOpts) (*api.Response[string], error)); ok {
 		return rf(ctx, opts)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *api.ProposalOpts) *api.Response[*api.VersionedProposal]); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, *api.NodeVersionOpts) *api.Response[string]); ok {
 		r0 = rf(ctx, opts)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*api.Response[*api.VersionedProposal])
+			r0 = ret.Get(0).(*api.Response[string])
 		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *api.ProposalOpts) error); ok {
+	if rf, ok := ret.Get(1).(func(context.Context, *api.NodeVersionOpts) error); ok {
 		r1 = rf(ctx, opts)
 	} else {
 		r1 = ret.Error(1)
@@ -236,29 +174,29 @@ func (_m *Handler) Proposal(ctx context.Context, opts *api.ProposalOpts) (*api.R
 	return r0, r1
 }
 
-// ProposerConfig provides a mock function with given fields: ctx
-func (_m *Handler) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConfigResponse, error) {
-	ret := _m.Called(ctx)
+// Proposal provides a mock function with given fields: ctx, opts
+func (_m *Handler) Proposal(ctx context.Context, opts *api.ProposalOpts) (*api.Response[*api.VersionedProposal], error) {
+	ret := _m.Called(ctx, opts)
 
 	if len(ret) == 0 {
-		panic("no return value specified for ProposerConfig")
+		panic("no return value specified for Proposal")
 	}
 
-	var r0 *eth2exp.ProposerConfigResponse
+	var r0 *api.Response[*api.VersionedProposal]
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context) (*eth2exp.ProposerConfigResponse, error)); ok {
-		return rf(ctx)
+	if rf, ok := ret.Get(0).(func(context.Context, *api.ProposalOpts) (*api.Response[*api.VersionedProposal], error)); ok {
+		return rf(ctx, opts)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context) *eth2exp.ProposerConfigResponse); ok {
-		r0 = rf(ctx)
+	if rf, ok := ret.Get(0).(func(context.Context, *api.ProposalOpts) *api.Response[*api.VersionedProposal]); ok {
+		r0 = rf(ctx, opts)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*eth2exp.ProposerConfigResponse)
+			r0 = ret.Get(0).(*api.Response[*api.VersionedProposal])
 		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context) error); ok {
-		r1 = rf(ctx)
+	if rf, ok := ret.Get(1).(func(context.Context, *api.ProposalOpts) error); ok {
+		r1 = rf(ctx, opts)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -500,6 +438,36 @@ func (_m *Handler) SyncCommitteeDuties(ctx context.Context, opts *api.SyncCommit
 	return r0, r1
 }
 
+// SyncCommitteeSelections provides a mock function with given fields: ctx, opts
+func (_m *Handler) SyncCommitteeSelections(ctx context.Context, opts *api.SyncCommitteeSelectionsOpts) (*api.Response[[]*v1.SyncCommitteeSelection], error) {
+	ret := _m.Called(ctx, opts)
+
+	if len(ret) == 0 {
+		panic("no return value specified for SyncCommitteeSelections")
+	}
+
+	var r0 *api.Response[[]*v1.SyncCommitteeSelection]
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *api.SyncCommitteeSelectionsOpts) (*api.Response[[]*v1.SyncCommitteeSelection], error)); ok {
+		return rf(ctx, opts)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *api.SyncCommitteeSelectionsOpts) *api.Response[[]*v1.SyncCommitteeSelection]); ok {
+		r0 = rf(ctx, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*api.Response[[]*v1.SyncCommitteeSelection])
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *api.SyncCommitteeSelectionsOpts) error); ok {
+		r1 = rf(ctx, opts)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
 // Validators provides a mock function with given fields: ctx, opts
 func (_m *Handler) Validators(ctx context.Context, opts *api.ValidatorsOpts) (*api.Response[map[phase0.ValidatorIndex]*v1.Validator], error) {
 	ret := _m.Called(ctx, opts)
diff --git a/core/validatorapi/router.go b/core/validatorapi/router.go
index 3331e04b..8a2110cc 100644
--- a/core/validatorapi/router.go
+++ b/core/validatorapi/router.go
@@ -43,7 +43,6 @@ import (
 	"github.com/obolnetwork/charon/app/log"
 	"github.com/obolnetwork/charon/app/z"
 	"github.com/obolnetwork/charon/core"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 	"github.com/obolnetwork/charon/tbls/tblsconv"
 )
 
@@ -71,7 +70,7 @@ type Handler interface {
 	eth2client.AttesterDutiesProvider
 	eth2client.ProposalProvider
 	eth2client.ProposalSubmitter
-	eth2exp.BeaconCommitteeSelectionAggregator
+	eth2client.BeaconCommitteeSelectionsProvider
 	eth2client.BlindedProposalSubmitter
 	eth2client.NodeVersionProvider
 	eth2client.ProposerDutiesProvider
@@ -79,11 +78,10 @@ type Handler interface {
 	eth2client.SyncCommitteeContributionsSubmitter
 	eth2client.SyncCommitteeDutiesProvider
 	eth2client.SyncCommitteeMessagesSubmitter
-	eth2exp.SyncCommitteeSelectionAggregator
+	eth2client.SyncCommitteeSelectionsProvider
 	eth2client.ValidatorsProvider
 	eth2client.ValidatorRegistrationsSubmitter
 	eth2client.VoluntaryExitSubmitter
-	eth2exp.ProposerConfigProvider
 	// Above sorted alphabetically.
 }
 
@@ -221,21 +219,21 @@ func NewRouter(ctx context.Context, h Handler, eth2Cl eth2wrap.Client, builderEn
 		{
 			Name:      "teku_proposer_config",
 			Path:      "/teku_proposer_config",
-			Handler:   proposerConfig(h),
+			Handler:   respond404("/teku_proposer_config"),
 			Methods:   []string{http.MethodGet},
 			Encodings: []contentType{contentTypeJSON},
 		},
 		{
 			Name:      "proposer_config",
 			Path:      "/proposer_config",
-			Handler:   proposerConfig(h),
+			Handler:   respond404("/proposer_config"),
 			Methods:   []string{http.MethodGet},
 			Encodings: []contentType{contentTypeJSON},
 		},
 		{
 			Name:      "aggregate_beacon_committee_selections",
 			Path:      "/eth/v1/validator/beacon_committee_selections",
-			Handler:   aggregateBeaconCommitteeSelections(h),
+			Handler:   beaconCommitteeSelections(h),
 			Methods:   []string{http.MethodPost},
 			Encodings: []contentType{contentTypeJSON},
 		},
@@ -298,7 +296,7 @@ func NewRouter(ctx context.Context, h Handler, eth2Cl eth2wrap.Client, builderEn
 		{
 			Name:      "aggregate_sync_committee_selections",
 			Path:      "/eth/v1/validator/sync_committee_selections",
-			Handler:   aggregateSyncCommitteeSelections(h),
+			Handler:   syncCommitteeSelections(h),
 			Methods:   []string{http.MethodPost},
 			Encodings: []contentType{contentTypeJSON},
 		},
@@ -1176,37 +1174,37 @@ func submitValidatorRegistrations(r eth2client.ValidatorRegistrationsSubmitter)
 	}
 }
 
-// aggregateBeaconCommitteeSelections receives partial beacon committee selections and returns aggregated selections.
-func aggregateBeaconCommitteeSelections(a eth2exp.BeaconCommitteeSelectionAggregator) handlerFunc {
+// beaconCommitteeSelections receives partial beacon committee selections and returns aggregated selections.
+func beaconCommitteeSelections(a eth2client.BeaconCommitteeSelectionsProvider) handlerFunc {
 	return func(ctx context.Context, _ map[string]string, _ http.Header, _ url.Values, typ contentType, body []byte) (res any, headers http.Header, err error) {
-		var selections []*eth2exp.BeaconCommitteeSelection
+		var selections []*eth2v1.BeaconCommitteeSelection
 		if err := unmarshal(typ, body, &selections); err != nil {
 			return nil, nil, errors.Wrap(err, "unmarshal beacon committee selections")
 		}
 
-		resp, err := a.AggregateBeaconCommitteeSelections(ctx, selections)
+		eth2Resp, err := a.BeaconCommitteeSelections(ctx, &eth2api.BeaconCommitteeSelectionsOpts{Selections: selections})
 		if err != nil {
-			return nil, nil, err
+			return nil, nil, errors.Wrap(err, "beacon committee selections")
 		}
 
-		return aggregateBeaconCommitteeSelectionsJSON{Data: resp}, nil, nil
+		return beaconCommitteeSelectionsJSON{Data: eth2Resp.Data}, nil, nil
 	}
 }
 
-// aggregateSyncCommitteeSelections receives partial sync committee selections and returns aggregated selections.
-func aggregateSyncCommitteeSelections(a eth2exp.SyncCommitteeSelectionAggregator) handlerFunc {
+// syncCommitteeSelections receives partial sync committee selections and returns aggregated selections.
+func syncCommitteeSelections(a eth2client.SyncCommitteeSelectionsProvider) handlerFunc {
 	return func(ctx context.Context, _ map[string]string, _ http.Header, _ url.Values, typ contentType, body []byte) (res any, headers http.Header, err error) {
-		var selections []*eth2exp.SyncCommitteeSelection
+		var selections []*eth2v1.SyncCommitteeSelection
 		if err := unmarshal(typ, body, &selections); err != nil {
 			return nil, nil, errors.Wrap(err, "unmarshal sync committee selections")
 		}
 
-		resp, err := a.AggregateSyncCommitteeSelections(ctx, selections)
+		eth2Resp, err := a.SyncCommitteeSelections(ctx, &eth2api.SyncCommitteeSelectionsOpts{Selections: selections})
 		if err != nil {
-			return nil, nil, err
+			return nil, nil, errors.Wrap(err, "sync committee selections")
 		}
 
-		return aggregateSyncCommitteeSelectionsJSON{Data: resp}, nil, nil
+		return syncCommitteeSelectionsJSON{Data: eth2Resp.Data}, nil, nil
 	}
 }
 
@@ -1222,17 +1220,6 @@ func submitExit(p eth2client.VoluntaryExitSubmitter) handlerFunc {
 	}
 }
 
-func proposerConfig(p eth2exp.ProposerConfigProvider) handlerFunc {
-	return func(ctx context.Context, _ map[string]string, _ http.Header, _ url.Values, _ contentType, _ []byte) (any, http.Header, error) {
-		resp, err := p.ProposerConfig(ctx)
-		if err != nil {
-			return nil, nil, errors.Wrap(err, "proposer config")
-		}
-
-		return resp, nil, nil
-	}
-}
-
 func aggregateAttestation(p eth2client.AggregateAttestationProvider) handlerFunc {
 	return func(ctx context.Context, _ map[string]string, _ http.Header, query url.Values, _ contentType, _ []byte) (any, http.Header, error) {
 		slot, err := uintQuery(query, "slot")
@@ -1637,9 +1624,9 @@ func unmarshal(typ contentType, body []byte, v any) error {
 		}
 
 		return nil
+	default:
+		return errors.New("bug: invalid content type")
 	}
-
-	return errors.New("bug: invalid content type")
 }
 
 // uintParam returns a uint path parameter.
diff --git a/core/validatorapi/validatorapi.go b/core/validatorapi/validatorapi.go
index 38243c35..e146c0ff 100644
--- a/core/validatorapi/validatorapi.go
+++ b/core/validatorapi/validatorapi.go
@@ -8,7 +8,6 @@ import (
 	"maps"
 	"math/big"
 	"runtime"
-	"strconv"
 	"testing"
 	"time"
 
@@ -27,7 +26,6 @@ import (
 	"github.com/obolnetwork/charon/app/z"
 	"github.com/obolnetwork/charon/core"
 	"github.com/obolnetwork/charon/eth2util"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 	"github.com/obolnetwork/charon/eth2util/signing"
 	"github.com/obolnetwork/charon/tbls"
 	"github.com/obolnetwork/charon/tbls/tblsconv"
@@ -783,8 +781,8 @@ func (c Component) SubmitVoluntaryExit(ctx context.Context, exit *eth2p0.SignedV
 	return nil
 }
 
-// AggregateBeaconCommitteeSelections returns aggregate beacon committee selection proofs.
-func (c Component) AggregateBeaconCommitteeSelections(ctx context.Context, selections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) {
+// BeaconCommitteeSelections returns aggregate beacon committee selection proofs.
+func (c Component) BeaconCommitteeSelections(ctx context.Context, opts *eth2api.BeaconCommitteeSelectionsOpts) (*eth2api.Response[[]*eth2v1.BeaconCommitteeSelection], error) {
 	vals, err := c.eth2Cl.ActiveValidators(ctx)
 	if err != nil {
 		return nil, err
@@ -792,7 +790,7 @@ func (c Component) AggregateBeaconCommitteeSelections(ctx context.Context, selec
 
 	psigsBySlot := make(map[eth2p0.Slot]core.ParSignedDataSet)
 
-	for _, selection := range selections {
+	for _, selection := range opts.Selections {
 		eth2Pubkey, ok := vals[selection.ValidatorIndex]
 		if !ok {
 			return nil, errors.New("validator not found", z.Any("provided", selection.ValidatorIndex), z.Any("expected", vals.Indices()))
@@ -829,7 +827,27 @@ func (c Component) AggregateBeaconCommitteeSelections(ctx context.Context, selec
 		}
 	}
 
-	return c.getAggregateBeaconCommSelection(ctx, psigsBySlot)
+	var resp []*eth2v1.BeaconCommitteeSelection
+
+	for slot, data := range psigsBySlot {
+		duty := core.NewPrepareAggregatorDuty(uint64(slot))
+		for pk := range data {
+			// Query aggregated subscription from aggsigdb for each duty and public key (this is blocking).
+			s, err := c.awaitAggSigDBFunc(ctx, duty, pk)
+			if err != nil {
+				return nil, err
+			}
+
+			sub, ok := s.(core.BeaconCommitteeSelection)
+			if !ok {
+				return nil, errors.New("invalid beacon committee selection")
+			}
+
+			resp = append(resp, &sub.BeaconCommitteeSelection)
+		}
+	}
+
+	return wrapResponse(resp), nil
 }
 
 // AggregateAttestation returns the aggregate attestation for the given attestation root.
@@ -1037,8 +1055,8 @@ func (c Component) SubmitSyncCommitteeContributions(ctx context.Context, contrib
 	return nil
 }
 
-// AggregateSyncCommitteeSelections returns aggregate sync committee selection proofs.
-func (c Component) AggregateSyncCommitteeSelections(ctx context.Context, partialSelections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) {
+// SyncCommitteeSelections returns aggregate sync committee selection proofs.
+func (c Component) SyncCommitteeSelections(ctx context.Context, opts *eth2api.SyncCommitteeSelectionsOpts) (*eth2api.Response[[]*eth2v1.SyncCommitteeSelection], error) {
 	vals, err := c.eth2Cl.ActiveValidators(ctx)
 	if err != nil {
 		return nil, err
@@ -1046,7 +1064,7 @@ func (c Component) AggregateSyncCommitteeSelections(ctx context.Context, partial
 
 	psigsBySlot := make(map[eth2p0.Slot]core.ParSignedDataSet)
 
-	for _, selection := range partialSelections {
+	for _, selection := range opts.Selections {
 		eth2Pubkey, ok := vals[selection.ValidatorIndex]
 		if !ok {
 			return nil, errors.New("validator not found")
@@ -1083,7 +1101,27 @@ func (c Component) AggregateSyncCommitteeSelections(ctx context.Context, partial
 		}
 	}
 
-	return c.getAggregateSyncCommSelection(ctx, psigsBySlot)
+	var resp []*eth2v1.SyncCommitteeSelection
+
+	for slot, data := range psigsBySlot {
+		duty := core.NewPrepareSyncContributionDuty(uint64(slot))
+		for pk := range data {
+			// Query aggregated sync committee selection from aggsigdb for each duty and public key (this is blocking).
+			s, err := c.awaitAggSigDBFunc(ctx, duty, pk)
+			if err != nil {
+				return nil, err
+			}
+
+			sub, ok := s.(core.SyncCommitteeSelection)
+			if !ok {
+				return nil, errors.New("invalid sync committee selection")
+			}
+
+			resp = append(resp, &sub.SyncCommitteeSelection)
+		}
+	}
+
+	return wrapResponse(resp), nil
 }
 
 // ProposerDuties obtains proposer duties for the given options.
@@ -1316,110 +1354,6 @@ func (c Component) verifyPartialSig(ctx context.Context, parSig core.ParSignedDa
 	return core.VerifyEth2SignedData(ctx, c.eth2Cl, eth2Signed, pubshare)
 }
 
-func (c Component) getAggregateBeaconCommSelection(ctx context.Context, psigsBySlot map[eth2p0.Slot]core.ParSignedDataSet) ([]*eth2exp.BeaconCommitteeSelection, error) {
-	var resp []*eth2exp.BeaconCommitteeSelection
-
-	for slot, data := range psigsBySlot {
-		duty := core.NewPrepareAggregatorDuty(uint64(slot))
-		for pk := range data {
-			// Query aggregated subscription from aggsigdb for each duty and public key (this is blocking).
-			s, err := c.awaitAggSigDBFunc(ctx, duty, pk)
-			if err != nil {
-				return nil, err
-			}
-
-			sub, ok := s.(core.BeaconCommitteeSelection)
-			if !ok {
-				return nil, errors.New("invalid beacon committee selection")
-			}
-
-			resp = append(resp, &sub.BeaconCommitteeSelection)
-		}
-	}
-
-	return resp, nil
-}
-
-func (c Component) getAggregateSyncCommSelection(ctx context.Context, psigsBySlot map[eth2p0.Slot]core.ParSignedDataSet) ([]*eth2exp.SyncCommitteeSelection, error) {
-	var resp []*eth2exp.SyncCommitteeSelection
-
-	for slot, data := range psigsBySlot {
-		duty := core.NewPrepareSyncContributionDuty(uint64(slot))
-		for pk := range data {
-			// Query aggregated sync committee selection from aggsigdb for each duty and public key (this is blocking).
-			s, err := c.awaitAggSigDBFunc(ctx, duty, pk)
-			if err != nil {
-				return nil, err
-			}
-
-			sub, ok := s.(core.SyncCommitteeSelection)
-			if !ok {
-				return nil, errors.New("invalid sync committee selection")
-			}
-
-			resp = append(resp, &sub.SyncCommitteeSelection)
-		}
-	}
-
-	return resp, nil
-}
-
-// ProposerConfig returns the proposer configuration for all validators.
-func (c Component) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConfigResponse, error) {
-	var targetGasLimit uint
-	if c.targetGasLimit == 0 {
-		log.Warn(ctx, "", errors.New("custom target gas limit not supported, setting to default", z.Uint("default_gas_limit", defaultGasLimit)))
-		targetGasLimit = defaultGasLimit
-	} else {
-		targetGasLimit = c.targetGasLimit
-	}
-
-	resp := eth2exp.ProposerConfigResponse{
-		Proposers: make(map[eth2p0.BLSPubKey]eth2exp.ProposerConfig),
-		Default: eth2exp.ProposerConfig{ // Default doesn't make sense, disable for now.
-			FeeRecipient: zeroAddress,
-			Builder: eth2exp.Builder{
-				Enabled:  false,
-				GasLimit: targetGasLimit,
-			},
-		},
-	}
-
-	genesisTime, err := eth2wrap.FetchGenesisTime(ctx, c.eth2Cl)
-	if err != nil {
-		return nil, err
-	}
-
-	slotDuration, _, err := eth2wrap.FetchSlotsConfig(ctx, c.eth2Cl)
-	if err != nil {
-		return nil, err
-	}
-
-	timestamp := genesisTime
-	timestamp = timestamp.Add(slotDuration) // Use slot 1 for timestamp to override pre-generated registrations.
-
-	for pubkey, pubshare := range c.sharesByKey {
-		eth2Share, err := pubshare.ToETH2()
-		if err != nil {
-			return nil, err
-		}
-
-		resp.Proposers[eth2Share] = eth2exp.ProposerConfig{
-			FeeRecipient: c.feeRecipientFunc(pubkey),
-			Builder: eth2exp.Builder{
-				Enabled:  c.builderEnabled,
-				GasLimit: targetGasLimit,
-				Overrides: map[string]string{
-					"timestamp":  strconv.FormatInt(timestamp.Unix(), 10),
-					"public_key": string(pubkey),
-				},
-			},
-		}
-	}
-
-	return &resp, nil
-}
-
 // wrapResponse wraps the provided data into an API Response and returns the response.
 func wrapResponse[T any](data T) *eth2api.Response[T] {
 	return &eth2api.Response[T]{Data: data}
diff --git a/eth2util/enr/enr.go b/eth2util/enr/enr.go
index 29735154..23f7b234 100644
--- a/eth2util/enr/enr.go
+++ b/eth2util/enr/enr.go
@@ -86,6 +86,7 @@ func Parse(enrStr string) (Record, error) {
 			if string(val) != valID {
 				return Record{}, errors.New("non-v4 identity scheme not supported")
 			}
+		default:
 		}
 	}
 
diff --git a/eth2util/eth2exp/attagg.go b/eth2util/eth2exp/attagg.go
deleted file mode 100644
index 1724ded3..00000000
--- a/eth2util/eth2exp/attagg.go
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
-
-package eth2exp
-
-import (
-	"context"
-	"encoding/hex"
-	"encoding/json"
-	"fmt"
-	"strconv"
-	"strings"
-
-	eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
-
-	"github.com/obolnetwork/charon/app/errors"
-)
-
-// BeaconCommitteeSelectionAggregator is the interface for aggregating beacon committee selection proofs in a DVT cluster.
-// NOTE: It should be removed once it is supported by go-eth2-client.
-type BeaconCommitteeSelectionAggregator interface {
-	// AggregateBeaconCommitteeSelections returns DVT aggregated beacon committee selection proofs.
-	// This would call a new BN API endpoint: POST /eth/v1/validator/beacon_committee_selections
-	AggregateBeaconCommitteeSelections(ctx context.Context, partialSelections []*BeaconCommitteeSelection) ([]*BeaconCommitteeSelection, error)
-}
-
-// BeaconCommitteeSelection is the data required for a beacon committee subscription.
-type BeaconCommitteeSelection struct {
-	// ValidatorIdex is the index of the validator making the aggregate request.
-	ValidatorIndex eth2p0.ValidatorIndex
-	// Slot is the slot for which the validator is possibly aggregating.
-	Slot eth2p0.Slot
-	// SelectionProof is the slot signature required to calculate whether the validator is an aggregator.
-	SelectionProof eth2p0.BLSSignature
-}
-
-// beaconCommitteeSelectionJSON is the spec representation of the struct.
-type beaconCommitteeSelectionJSON struct {
-	ValidatorIndex string `json:"validator_index"`
-	Slot           string `json:"slot"`
-	SelectionProof string `json:"selection_proof"`
-}
-
-// MarshalJSON implements json.Marshaler.
-func (b *BeaconCommitteeSelection) MarshalJSON() ([]byte, error) {
-	resp, err := json.Marshal(&beaconCommitteeSelectionJSON{
-		ValidatorIndex: fmt.Sprintf("%d", b.ValidatorIndex),
-		Slot:           fmt.Sprintf("%d", b.Slot),
-		SelectionProof: fmt.Sprintf("%#x", b.SelectionProof),
-	})
-	if err != nil {
-		return nil, errors.Wrap(err, "marshal beacon committee subscription")
-	}
-
-	return resp, nil
-}
-
-// UnmarshalJSON implements json.Unmarshaler.
-func (b *BeaconCommitteeSelection) UnmarshalJSON(input []byte) error {
-	var err error
-
-	var beaconCommitteeSelectionJSON beaconCommitteeSelectionJSON
-	if err = json.Unmarshal(input, &beaconCommitteeSelectionJSON); err != nil {
-		return errors.Wrap(err, "invalid JSON")
-	}
-
-	if beaconCommitteeSelectionJSON.ValidatorIndex == "" {
-		return errors.New("validator index missing")
-	}
-
-	validatorIndex, err := strconv.ParseUint(beaconCommitteeSelectionJSON.ValidatorIndex, 10, 64)
-	if err != nil {
-		return errors.Wrap(err, "invalid value for validator index")
-	}
-
-	b.ValidatorIndex = eth2p0.ValidatorIndex(validatorIndex)
-
-	if beaconCommitteeSelectionJSON.Slot == "" {
-		return errors.New("slot missing")
-	}
-
-	slot, err := strconv.ParseUint(beaconCommitteeSelectionJSON.Slot, 10, 64)
-	if err != nil {
-		return errors.Wrap(err, "invalid value for slot")
-	}
-
-	b.Slot = eth2p0.Slot(slot)
-
-	if beaconCommitteeSelectionJSON.SelectionProof == "" {
-		return errors.New("selection proof missing")
-	}
-
-	signature, err := hex.DecodeString(strings.TrimPrefix(beaconCommitteeSelectionJSON.SelectionProof, "0x"))
-	if err != nil {
-		return errors.Wrap(err, "invalid value for signature")
-	}
-
-	if len(signature) != eth2p0.SignatureLength {
-		return errors.New("incorrect length for signature")
-	}
-
-	copy(b.SelectionProof[:], signature)
-
-	return nil
-}
-
-// String returns a string version of the structure.
-func (b *BeaconCommitteeSelection) String() (string, error) {
-	data, err := json.Marshal(b)
-	if err != nil {
-		return "", errors.Wrap(err, "marshal BeaconCommitteeSelection")
-	}
-
-	return string(data), nil
-}
diff --git a/eth2util/eth2exp/proposeconf.go b/eth2util/eth2exp/proposeconf.go
deleted file mode 100644
index a77983e8..00000000
--- a/eth2util/eth2exp/proposeconf.go
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
-
-package eth2exp
-
-import (
-	"context"
-	"encoding/hex"
-	"encoding/json"
-	"fmt"
-	"strconv"
-	"strings"
-	"time"
-
-	eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
-
-	"github.com/obolnetwork/charon/app/errors"
-)
-
-const (
-	builderOverrideTimestamp = "timestamp"
-	builderOverridePublicKey = "public_key"
-)
-
-// ProposerConfigProvider is the interface for providing proposer configuration.
-type ProposerConfigProvider interface {
-	ProposerConfig(ctx context.Context) (*ProposerConfigResponse, error)
-}
-
-// ProposerConfigResponse is the response from the proposer config endpoint.
-type ProposerConfigResponse struct {
-	Proposers map[eth2p0.BLSPubKey]ProposerConfig
-	Default   ProposerConfig
-}
-
-func (p ProposerConfigResponse) MarshalJSON() ([]byte, error) {
-	resp := &proposerConfigResponseJSON{
-		Proposers: make(map[string]ProposerConfig),
-		Default:   p.Default,
-	}
-	for k, v := range p.Proposers {
-		resp.Proposers[fmt.Sprintf("%#x", k)] = v
-	}
-
-	b, err := json.Marshal(resp)
-	if err != nil {
-		return nil, errors.Wrap(err, "marshal proposer config response")
-	}
-
-	return b, nil
-}
-
-func (p *ProposerConfigResponse) UnmarshalJSON(input []byte) error {
-	resp := new(proposerConfigResponseJSON)
-	if err := json.Unmarshal(input, resp); err != nil {
-		return errors.Wrap(err, "unmarshal proposer config response")
-	}
-
-	p.Proposers = make(map[eth2p0.BLSPubKey]ProposerConfig)
-
-	for k, v := range resp.Proposers {
-		pkBytes, err := hex.DecodeString(strings.TrimPrefix(k, "0x"))
-		if err != nil {
-			return errors.Wrap(err, "decode proposer public key")
-		}
-
-		if len(pkBytes) != 48 {
-			return errors.New("invalid proposer public key")
-		}
-
-		p.Proposers[eth2p0.BLSPubKey(pkBytes)] = v
-	}
-
-	p.Default = resp.Default
-
-	return nil
-}
-
-// ProposerConfig is the configuration for a proposer.
-type ProposerConfig struct {
-	FeeRecipient string  `json:"fee_recipient"`
-	Builder      Builder `json:"builder"`
-}
-
-// Builder is the build-API configuration for a proposer.
-type Builder struct {
-	Enabled   bool              `json:"enabled"`
-	GasLimit  uint              `json:"gas_limit"`
-	Overrides map[string]string `json:"registration_overrides,omitempty"`
-}
-
-// TimestampOverride returns the timestamp override and true if it exists.
-func (b Builder) TimestampOverride() (time.Time, bool, error) {
-	ts, ok := b.Overrides[builderOverrideTimestamp]
-	if !ok {
-		return time.Time{}, false, nil
-	}
-
-	tsUnix, err := strconv.ParseInt(ts, 10, 64)
-	if err != nil {
-		return time.Time{}, false, errors.Wrap(err, "parse timestamp override")
-	}
-
-	return time.Unix(tsUnix, 0), true, nil
-}
-
-// PublicKeyOverride returns the public key override and true if it exists.
-func (b Builder) PublicKeyOverride() (eth2p0.BLSPubKey, bool, error) {
-	pk, ok := b.Overrides[builderOverridePublicKey]
-	if !ok {
-		return eth2p0.BLSPubKey{}, false, nil
-	}
-
-	pkBytes, err := hex.DecodeString(strings.TrimPrefix(pk, "0x"))
-	if err != nil {
-		return eth2p0.BLSPubKey{}, false, errors.Wrap(err, "parse public key override")
-	} else if len(pkBytes) != 48 {
-		return eth2p0.BLSPubKey{}, false, errors.New("invalid public key override")
-	}
-
-	return eth2p0.BLSPubKey(pkBytes), true, nil
-}
-
-type proposerConfigResponseJSON struct {
-	Proposers map[string]ProposerConfig `json:"proposer_config"`
-	Default   ProposerConfig            `json:"default_config"`
-}
diff --git a/eth2util/eth2exp/synccomm_agg.go b/eth2util/eth2exp/synccomm_agg.go
deleted file mode 100644
index f4c09836..00000000
--- a/eth2util/eth2exp/synccomm_agg.go
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
-
-package eth2exp
-
-import (
-	"context"
-	"encoding/hex"
-	"encoding/json"
-	"fmt"
-	"strconv"
-	"strings"
-
-	eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
-
-	"github.com/obolnetwork/charon/app/errors"
-	"github.com/obolnetwork/charon/app/z"
-)
-
-// SyncCommitteeSelectionAggregator is the interface for aggregating sync committee selection proofs.
-type SyncCommitteeSelectionAggregator interface {
-	// AggregateSyncCommitteeSelections returns the threshold aggregated sync committee selection proofs.
-	// This would call a new BN API endpoint: POST /eth/v1/validator/sync_committee_selections
-	AggregateSyncCommitteeSelections(ctx context.Context, partialSelections []*SyncCommitteeSelection) ([]*SyncCommitteeSelection, error)
-}
-
-// SyncCommitteeSelection is the data required for a sync committee subscription.
-type SyncCommitteeSelection struct {
-	// ValidatorIndex is the index of the validator making the selection request.
-	ValidatorIndex eth2p0.ValidatorIndex
-	// Slot is the slot for which the selection request is made.
-	Slot eth2p0.Slot
-	// SubcommitteeIndex is the subcommittee to which the validator is assigned.
-	SubcommitteeIndex eth2p0.CommitteeIndex
-	// SelectionProof is a partial or an aggregated signature.
-	SelectionProof eth2p0.BLSSignature
-}
-
-// syncCommitteeSelectionJSON is the spec representation of the struct.
-type syncCommitteeSelectionJSON struct {
-	ValidatorIndex    string `json:"validator_index"`
-	Slot              string `json:"slot"`
-	SubcommitteeIndex string `json:"subcommittee_index"`
-	SelectionProof    string `json:"selection_proof"`
-}
-
-// MarshalJSON implements json.Marshaler.
-func (s *SyncCommitteeSelection) MarshalJSON() ([]byte, error) {
-	resp, err := json.Marshal(&syncCommitteeSelectionJSON{
-		ValidatorIndex:    fmt.Sprintf("%d", s.ValidatorIndex),
-		Slot:              fmt.Sprintf("%d", s.Slot),
-		SubcommitteeIndex: fmt.Sprintf("%d", s.SubcommitteeIndex),
-		SelectionProof:    fmt.Sprintf("%#x", s.SelectionProof),
-	})
-	if err != nil {
-		return nil, errors.Wrap(err, "marshal sync committee selection")
-	}
-
-	return resp, nil
-}
-
-// UnmarshalJSON implements json.Unmarshaler.
-func (s *SyncCommitteeSelection) UnmarshalJSON(input []byte) error {
-	var err error
-
-	var syncCommitteeSelectionJSON syncCommitteeSelectionJSON
-	if err = json.Unmarshal(input, &syncCommitteeSelectionJSON); err != nil {
-		return errors.Wrap(err, "invalid JSON")
-	}
-
-	// verify and unmarshal ValidatorIndex.
-	if syncCommitteeSelectionJSON.ValidatorIndex == "" {
-		return errors.New("validator index missing")
-	}
-
-	validatorIndex, err := strconv.ParseUint(syncCommitteeSelectionJSON.ValidatorIndex, 10, 64)
-	if err != nil {
-		return errors.Wrap(err, "invalid value for validator index", z.Str("vIdx", syncCommitteeSelectionJSON.ValidatorIndex))
-	}
-
-	s.ValidatorIndex = eth2p0.ValidatorIndex(validatorIndex)
-
-	// verify and unmarshal Slot.
-	if syncCommitteeSelectionJSON.Slot == "" {
-		return errors.New("slot missing")
-	}
-
-	slot, err := strconv.ParseUint(syncCommitteeSelectionJSON.Slot, 10, 64)
-	if err != nil {
-		return errors.Wrap(err, "invalid value for slot", z.Str("slot", syncCommitteeSelectionJSON.Slot))
-	}
-
-	s.Slot = eth2p0.Slot(slot)
-
-	// verify and unmarshal SubcommitteeIndex.
-	if syncCommitteeSelectionJSON.SubcommitteeIndex == "" {
-		return errors.New("subcommittee index missing")
-	}
-
-	subcommIdx, err := strconv.ParseUint(syncCommitteeSelectionJSON.SubcommitteeIndex, 10, 64)
-	if err != nil {
-		return errors.Wrap(err, "invalid value for subcommittee index", z.Str("subcommIdx", syncCommitteeSelectionJSON.SubcommitteeIndex))
-	}
-
-	s.SubcommitteeIndex = eth2p0.CommitteeIndex(subcommIdx)
-
-	// verify and unmarshal SelectionProof.
-	if syncCommitteeSelectionJSON.SelectionProof == "" {
-		return errors.New("selection proof missing")
-	}
-
-	signature, err := hex.DecodeString(strings.TrimPrefix(syncCommitteeSelectionJSON.SelectionProof, "0x"))
-	if err != nil {
-		return errors.Wrap(err, "invalid value for signature", z.Str("sig", syncCommitteeSelectionJSON.SelectionProof))
-	}
-
-	if len(signature) != eth2p0.SignatureLength {
-		return errors.New("invalid signature length")
-	}
-
-	copy(s.SelectionProof[:], signature)
-
-	return nil
-}
-
-// String returns a string version of SyncCommitteeSelection.
-func (s *SyncCommitteeSelection) String() (string, error) {
-	data, err := json.Marshal(s)
-	if err != nil {
-		return "", errors.Wrap(err, "marshal SyncCommitteeSelection")
-	}
-
-	return string(data), nil
-}
diff --git a/eth2util/keystore/load.go b/eth2util/keystore/load.go
index d81a1de8..996c7a00 100644
--- a/eth2util/keystore/load.go
+++ b/eth2util/keystore/load.go
@@ -10,6 +10,8 @@ import (
 	"path/filepath"
 	"regexp"
 	"strconv"
+	"strings"
+	"sync/atomic"
 
 	"github.com/obolnetwork/charon/app/errors"
 	"github.com/obolnetwork/charon/app/forkjoin"
@@ -127,6 +129,118 @@ func LoadFilesUnordered(dir string) (KeyFiles, error) {
 	return joinResults.Flatten()
 }
 
+// LoadFilesRecursively works like LoadFilesUnordered but recursively searches for keystore files in the given directory.
+// It tries matching the found password files to decrypted keystore files.
+func LoadFilesRecursively(dir string) (KeyFiles, error) {
+	var (
+		jsonFiles []string
+		txtFiles  []string
+	)
+
+	// Step 1: Walk the directory to find all .json and .txt files.
+	err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+		if err == nil && !info.IsDir() {
+			if strings.HasSuffix(info.Name(), ".json") {
+				jsonFiles = append(jsonFiles, path)
+			} else if strings.HasSuffix(info.Name(), ".txt") {
+				txtFiles = append(txtFiles, path)
+			}
+		}
+
+		return err
+	})
+	if err != nil {
+		return nil, errors.Wrap(err, "walk directory", z.Str("dir", dir))
+	}
+
+	// Step 2: Decode the keystore files.
+	keyStoresMap := make(map[string]Keystore)
+	validFiles := []string{}
+
+	for _, filepath := range jsonFiles {
+		b, err := os.ReadFile(filepath)
+		if err != nil {
+			return nil, errors.Wrap(err, "read file", z.Str("filepath", filepath))
+		}
+
+		var store Keystore
+		if err := json.Unmarshal(b, &store); err != nil {
+			continue
+		}
+
+		keyStoresMap[filepath] = store
+		validFiles = append(validFiles, filepath)
+	}
+
+	// Step 3: Load all passwords from .txt files.
+	passwordsMap := make(map[string]string)
+
+	for _, filepath := range txtFiles {
+		b, err := os.ReadFile(filepath)
+		if err != nil {
+			return nil, errors.Wrap(err, "read file", z.Str("filepath", filepath))
+		}
+
+		passwordsMap[filepath] = string(b)
+	}
+
+	var keyFileIndex atomic.Int32
+
+	workFunc := func(_ context.Context, filepath string) (KeyFile, error) {
+		store, ok := keyStoresMap[filepath]
+		if !ok {
+			return KeyFile{}, errors.New("keystore not found", z.Str("filepath", filepath))
+		}
+
+		var (
+			secret tbls.PrivateKey
+			err    error
+		)
+
+		// First try the password file that matches the keystore file.
+		passwordFile := strings.Replace(filepath, ".json", ".txt", 1)
+
+		password, ok := passwordsMap[passwordFile]
+		if ok {
+			secret, err = decrypt(store, password)
+		}
+
+		if !ok || err != nil {
+			// Try other passwords
+			for _, otherPassword := range passwordsMap {
+				secret, err = decrypt(store, otherPassword)
+				if err == nil {
+					break
+				}
+			}
+
+			if err != nil {
+				return KeyFile{}, errors.Wrap(err, "keystore decryption", z.Str("filepath", filepath))
+			}
+		}
+
+		// Using atomic, because this worker is executed concurrently.
+		index := keyFileIndex.Add(1)
+
+		return KeyFile{
+			PrivateKey: secret,
+			Filename:   filepath,
+			FileIndex:  int(index),
+		}, nil
+	}
+
+	// Step 4: Final join with decryption.
+	joinResults, cancel := forkjoin.NewWithInputs(
+		context.Background(),
+		workFunc,
+		validFiles,
+		forkjoin.WithWorkers(loadStoreWorkers),
+	)
+	defer cancel()
+
+	return joinResults.Flatten()
+}
+
 var extractor = regexp.MustCompile(`keystore-(?:insecure-)?([0-9]+).json`)
 
 // extractFileIndex extracts the index from a keystore file name or returns -1
diff --git a/eth2util/statecomm/statecomm.go b/eth2util/statecomm/statecomm.go
deleted file mode 100644
index f1e6f3f8..00000000
--- a/eth2util/statecomm/statecomm.go
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
-
-package statecomm
-
-import (
-	"encoding/json"
-	"fmt"
-	"strconv"
-
-	eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
-
-	"github.com/obolnetwork/charon/app/errors"
-)
-
-// StateCommitteesResponse is simplified response structure for fetching the beacon node committees for a given state.
-// This is the response from the BN API endpoint: /eth/v1/beacon/states/{state_id}/validators.
-type StateCommitteesResponse struct {
-	Data []*StateCommittee `json:"data"`
-}
-
-// StateCommittee is the Data field from the BN API endpoint /eth/v1/beacon/states/{state_id}/validators response.
-type StateCommittee struct {
-	Index      eth2p0.CommitteeIndex   `json:"index"`
-	Slot       eth2p0.Slot             `json:"slot"`
-	Validators []eth2p0.ValidatorIndex `json:"validators"`
-}
-
-// beaconCommitteeSelectionJSON is the spec representation of the struct.
-type stateCommitteeJSON struct {
-	Index      string   `json:"index"`
-	Slot       string   `json:"slot"`
-	Validators []string `json:"validators"`
-}
-
-// MarshalJSON implements json.Marshaler.
-func (b *StateCommittee) MarshalJSON() ([]byte, error) {
-	var validators []string
-	for _, v := range b.Validators {
-		validators = append(validators, fmt.Sprintf("%d", v))
-	}
-
-	resp, err := json.Marshal(&stateCommitteeJSON{
-		Index:      fmt.Sprintf("%d", b.Index),
-		Slot:       fmt.Sprintf("%d", b.Slot),
-		Validators: validators,
-	})
-	if err != nil {
-		return nil, errors.Wrap(err, "marshal state committee subscription")
-	}
-
-	return resp, nil
-}
-
-// UnmarshalJSON implements json.Unmarshaler.
-func (b *StateCommittee) UnmarshalJSON(input []byte) error {
-	var err error
-
-	var stateCommitteeJSON stateCommitteeJSON
-	if err = json.Unmarshal(input, &stateCommitteeJSON); err != nil {
-		return errors.Wrap(err, "invalid JSON")
-	}
-
-	if stateCommitteeJSON.Index == "" {
-		return errors.New("index missing")
-	}
-
-	index, err := strconv.ParseUint(stateCommitteeJSON.Index, 10, 64)
-	if err != nil {
-		return errors.Wrap(err, "invalid value for index")
-	}
-
-	b.Index = eth2p0.CommitteeIndex(index)
-
-	if stateCommitteeJSON.Slot == "" {
-		return errors.New("slot missing")
-	}
-
-	slot, err := strconv.ParseUint(stateCommitteeJSON.Slot, 10, 64)
-	if err != nil {
-		return errors.Wrap(err, "invalid value for slot")
-	}
-
-	b.Slot = eth2p0.Slot(slot)
-
-	var validators []eth2p0.ValidatorIndex
-
-	for _, v := range stateCommitteeJSON.Validators {
-		validator, err := strconv.ParseUint(v, 10, 64)
-		if err != nil {
-			return errors.Wrap(err, "invalid value for validator")
-		}
-
-		validators = append(validators, eth2p0.ValidatorIndex(validator))
-	}
-
-	b.Validators = validators
-
-	return nil
-}
-
-// String returns a string version of the structure.
-func (b *StateCommittee) String() (string, error) {
-	data, err := json.Marshal(b)
-	if err != nil {
-		return "", errors.Wrap(err, "marshal StateCommittee")
-	}
-
-	return string(data), nil
-}
diff --git a/go.mod b/go.mod
index b8724cd7..0beed2f1 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/attestantio/go-eth2-client v0.26.0
 	github.com/coinbase/kryptology v1.5.6-0.20220316191335-269410e1b06b
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
-	github.com/ethereum/go-ethereum v1.16.1
+	github.com/ethereum/go-ethereum v1.16.2
 	github.com/ferranbt/fastssz v0.1.4
 	github.com/golang/snappy v1.0.0
 	github.com/google/gofuzz v1.2.0
@@ -293,9 +293,8 @@ require (
 // We're replacing kryptology with our own fork, which fixes dependencies' security vulnerabilities.
 replace github.com/coinbase/kryptology => github.com/ObolNetwork/kryptology v0.1.0
 
-// We're replacing go-eth2-client with a branch off our fork, at version v0.21.10.
-// This is needed to ensure Gnosis compatibility.
-// replace github.com/attestantio/go-eth2-client => github.com/ObolNetwork/go-eth2-client v0.21.11-0.20240822135044-f0a5b21e02c6
+// We're replacing go-eth2-client with a branch off our fork. The branch is kept up to date with the latest attestantio versions.
+replace github.com/attestantio/go-eth2-client => github.com/ObolNetwork/go-eth2-client v0.26.0-obol.2 //nolint
 
 tool (
 	github.com/bufbuild/buf/cmd/buf
diff --git a/go.sum b/go.sum
index 9ca05bdf..c7180666 100644
--- a/go.sum
+++ b/go.sum
@@ -38,6 +38,8 @@ github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
 github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/ObolNetwork/go-eth2-client v0.26.0-obol.2 h1:IwvyBXUPwMFizl9TZeQaG1TwB67ScS1LdXgUrHwD//o=
+github.com/ObolNetwork/go-eth2-client v0.26.0-obol.2/go.mod h1:fvULSL9WtNskkOB4i+Yyr6BKpNHXvmpGZj9969fCrfY=
 github.com/ObolNetwork/kryptology v0.1.0 h1:AhoG4My70+xMhEJSpVaJay/t+T/vIUNHQYLjsDJHulI=
 github.com/ObolNetwork/kryptology v0.1.0/go.mod h1:/Wl7Js2f676GyXZDTaojf/O+l0fxFPWudbyjdFhkpSA=
 github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
@@ -52,8 +54,6 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7D
 github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
 github.com/attestantio/go-builder-client v0.6.4 h1:D0gBdQO4vJLhoFLxdR2TbGdfsWBQZ5YDZBNdU2Ap3uk=
 github.com/attestantio/go-builder-client v0.6.4/go.mod h1:f8wi3HzuPxfJoi2PirpJK3yZhte4SavDgKJbRrKoB1Q=
-github.com/attestantio/go-eth2-client v0.26.0 h1:oDWKvIUJfvr1EBi/w9L6mawYZHOCymjHkml7fZplT20=
-github.com/attestantio/go-eth2-client v0.26.0/go.mod h1:fvULSL9WtNskkOB4i+Yyr6BKpNHXvmpGZj9969fCrfY=
 github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
 github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -172,8 +172,8 @@ github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfU
 github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
 github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w=
 github.com/ethereum/c-kzg-4844/v2 v2.1.0/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E=
-github.com/ethereum/go-ethereum v1.16.1 h1:7684NfKCb1+IChudzdKyZJ12l1Tq4ybPZOITiCDXqCk=
-github.com/ethereum/go-ethereum v1.16.1/go.mod h1:ngYIvmMAYdo4sGW9cGzLvSsPGhDOOzL0jK5S5iXpj0g=
+github.com/ethereum/go-ethereum v1.16.2 h1:VDHqj86DaQiMpnMgc7l0rwZTg0FRmlz74yupSG5SnzI=
+github.com/ethereum/go-ethereum v1.16.2/go.mod h1:X5CIOyo8SuK1Q5GnaEizQVLHT/DfsiGWuNeVdQcEMNA=
 github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
 github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
 github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 00000000..61109bdd
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,50 @@
+# Charon Scripts
+
+This directory contains helper scripts for managing and merging Charon cluster and node configurations.
+
+## `node_merge.sh`
+
+This script merges node-level configuration and key files from a source node folder into a destination node folder.
+It is useful for combining validator keys and configuration for single pair of nodes, ensuring all keys are present in the destination.
+
+### Usage
+
+```bash
+./node_merge.sh <dst_node_folder> <src_node_folder>
+```
+
+- *<dst_node_folder>*: Path to the destination node's top-level folder (e.g., `cluster1/node0`).
+- *<src_node_folder>*: Path to the source node's top-level folder (e.g., `cluster2/node0`).
+
+The script will:
+- Copy all `keystore-N.json` and `keystore-N.txt` pairs from the source to the destination.
+- Renumber files sequentially in the destination folder, sorted numerically.
+- Preserve other files in the destination folder.
+
+## `cluster_merge.sh`
+
+This script merges two clusters by running `node_merge.sh` for each `nodeX` subfolder found in the source cluster.
+It helps expand an existing cluster with new validators, given the same operators set and cluster configuration.
+
+### Usage
+
+```bash
+./cluster_merge.sh <dst_cluster_folder> <src_cluster_folder>
+```
+
+- *<dst_cluster_folder>*: Path to the destination cluster folder (e.g., `cluster1`).
+- *<src_cluster_folder>*: Path to the source cluster folder (e.g., `cluster2`).
+
+The script will execute `node_merge.sh` for each `nodeX` subfolder found in the source cluster.
+
+## Requirements
+
+Both scripts require **bash** (standard on Linux/macOS) and **jq** (version 1.5+).
+Install via `sudo apt-get install jq` (Debian/Ubuntu) or `brew install jq` (macOS).
+
+## Important Warnings
+
+- Always back up your `cluster-lock.json`, node folders, and `validator_keys` folders before use.
+- Make sure the source and destination folders have correct permissions and are accessible by the script.
+- The scripts do not update the integrity hash. You must use `CHARON_NO_VERIFY=true` or `--no-verify=true` with `charon run`.
+- Shut down the source cluster or node before running the merged cluster.
diff --git a/scripts/cluster_merge.sh b/scripts/cluster_merge.sh
new file mode 100755
index 00000000..d8b2ebac
--- /dev/null
+++ b/scripts/cluster_merge.sh
@@ -0,0 +1,91 @@
+#!/bin/bash
+
+set -euo pipefail
+
+#
+# This script orchestrates the merging of multiple DV node configurations
+# from a source cluster into a destination cluster. It calls node_merge.sh
+# for each corresponding nodeX subfolder found.
+# For single node merges, consider using node_merge.sh directly.
+#
+# Please make backups before running this script.
+# Please read the README.md instructions carefully.
+#
+
+# --- Function to display script usage ---
+function usage() {
+    echo "Usage: $0 dst_cluster_folder src_cluster_folder"
+    echo ""
+    echo "  dst_cluster_folder: Path to the destination cluster folder"
+    echo "                      (e.g., ~/dst_cluster)"
+    echo "  src_cluster_folder: Path to the source cluster folder"
+    echo "                      (e.g., ~/src_cluster)"
+    exit 1
+}
+
+# --- Main script starts here ---
+
+# Check for correct number of arguments
+if [ "$#" -ne 2 ]; then
+    usage
+fi
+
+DST_CLUSTER_ROOT="$1"
+SRC_CLUSTER_ROOT="$2"
+
+# Get the directory where this script is located
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
+NODE_MERGE_SCRIPT="$SCRIPT_DIR/node_merge.sh"
+
+# Check if node_merge.sh exists and is executable
+if [ ! -f "$NODE_MERGE_SCRIPT" ]; then
+    echo "Error: '$NODE_MERGE_SCRIPT' not found. Please ensure 'node_merge.sh' is in the same directory as 'cluster_merge.sh'."
+    exit 1
+fi
+if [ ! -x "$NODE_MERGE_SCRIPT" ]; then
+    echo "Error: '$NODE_MERGE_SCRIPT' is not executable. Please run 'chmod +x $NODE_MERGE_SCRIPT'."
+    exit 1
+fi
+
+echo "--- Starting cluster merge from '$SRC_CLUSTER_ROOT' to '$DST_CLUSTER_ROOT' ---"
+echo ""
+
+# Find all nodeX folders in the source cluster
+# Using find with -maxdepth 1 to only look in the immediate subdirectories
+# and regex to match 'node' followed by one or more digits.
+# Sorting numerically to ensure consistent processing order (node0, node1, node2, ..., node10)
+NODE_FOLDERS=$(find "$SRC_CLUSTER_ROOT" -maxdepth 1 -type d -name 'node[0-9]*' | sort -V)
+
+if [ -z "$NODE_FOLDERS" ]; then
+    echo "No 'nodeX' subfolders found in the source cluster '$SRC_CLUSTER_ROOT'."
+    echo "Cluster merge finished (nothing to merge)."
+    exit 0
+fi
+
+for SRC_NODE_FOLDER_PATH in $NODE_FOLDERS; do
+    NODE_NAME=$(basename "$SRC_NODE_FOLDER_PATH") # e.g., node0, node1
+    DST_NODE_FOLDER_PATH="$DST_CLUSTER_ROOT/$NODE_NAME"
+
+    echo "Attempting to merge node '$NODE_NAME':"
+    echo "  Source: $SRC_NODE_FOLDER_PATH"
+    echo "  Destination: $DST_NODE_FOLDER_PATH"
+    echo ""
+
+    # Check if the corresponding destination node folder exists
+    if [ ! -d "$DST_NODE_FOLDER_PATH" ]; then
+        echo "Error: Destination node folder '$DST_NODE_FOLDER_PATH' not found. Script will be terminated."
+        exit 1
+    fi
+
+    # Call node_merge.sh for the current node pair
+    "$NODE_MERGE_SCRIPT" "$DST_NODE_FOLDER_PATH" "$SRC_NODE_FOLDER_PATH"
+    NODE_MERGE_EXIT_CODE=$?
+
+    if [ $NODE_MERGE_EXIT_CODE -ne 0 ]; then
+        echo "Error: node_merge.sh failed for '$NODE_NAME'. See above output for details."
+        exit 1
+    fi
+    echo ""
+done
+
+echo "--- Cluster merge process complete ---"
diff --git a/scripts/node_merge.sh b/scripts/node_merge.sh
new file mode 100755
index 00000000..a59fde24
--- /dev/null
+++ b/scripts/node_merge.sh
@@ -0,0 +1,218 @@
+#!/bin/bash
+
+set -euo pipefail
+
+#
+# This script merges cluster-lock.json and key files for one node of a cluster.
+# To merge full cluster, consider running cluster_merge.sh.
+#
+# Please make backups before running this script.
+# Please read the README.md instructions carefully.
+#
+
+# --- Configuration ---
+CLUSTER_LOCK_FILENAME="cluster-lock.json"
+VALIDATOR_KEYS_DIRNAME="validator_keys"
+
+# --- Function to display script usage ---
+function usage() {
+    echo "Usage: $0 dst_node_folder src_node_folder"
+    echo ""
+    echo "  dst_node_folder: Path to the destination node's folder"
+    echo "                   (e.g., path/to/cluster1/node0)"
+    echo "  src_node_folder: Path to the source node's folder"
+    echo "                   (e.g., path/to/cluster2/node0)"
+    exit 1
+}
+
+# Check for correct number of arguments
+if [ "$#" -ne 2 ]; then
+    usage
+fi
+
+DST_ROOT_FOLDER="$1"
+SRC_ROOT_FOLDER="$2"
+
+DST_CLUSTER_LOCK_FILE="$DST_ROOT_FOLDER/$CLUSTER_LOCK_FILENAME"
+SRC_CLUSTER_LOCK_FILE="$SRC_ROOT_FOLDER/$CLUSTER_LOCK_FILENAME"
+
+DST_KEYS_FOLDER="$DST_ROOT_FOLDER/$VALIDATOR_KEYS_DIRNAME"
+SRC_KEYS_FOLDER="$SRC_ROOT_FOLDER/$VALIDATOR_KEYS_DIRNAME"
+
+# --- Part 1: Merge cluster-lock.json ---
+
+# Check if required JSON files exist
+if [ ! -f "$DST_CLUSTER_LOCK_FILE" ]; then
+    echo "Error: Destination file '$DST_CLUSTER_LOCK_FILE' not found."
+    exit 1
+fi
+if [ ! -f "$SRC_CLUSTER_LOCK_FILE" ]; then
+    echo "Error: Source file '$SRC_CLUSTER_LOCK_FILE' not found."
+    exit 1
+fi
+
+# Check file permissions for cluster-lock.json
+if [ ! -r "$SRC_CLUSTER_LOCK_FILE" ]; then
+    echo "Error: Source file '$SRC_CLUSTER_LOCK_FILE' is not readable. Please check permissions."
+    exit 1
+fi
+if [ ! -r "$DST_CLUSTER_LOCK_FILE" ]; then
+    echo "Error: Destination file '$DST_CLUSTER_LOCK_FILE' is not readable. Please check permissions."
+    exit 1
+fi
+if [ ! -w "$DST_CLUSTER_LOCK_FILE" ]; then
+    echo "Error: Destination file '$DST_CLUSTER_LOCK_FILE' is not writable. Please check permissions."
+    exit 1
+fi
+
+# Check if jq is installed
+if ! command -v jq &>/dev/null; then
+    echo "Error: 'jq' is not installed. Please install it to use this script."
+    echo "  On Debian/Ubuntu: sudo apt-get install jq"
+    echo "  On macOS (Homebrew): brew install jq"
+    exit 1
+fi
+
+echo "Merging '$CLUSTER_LOCK_FILENAME' from '$SRC_CLUSTER_LOCK_FILE' into '$DST_CLUSTER_LOCK_FILE'..."
+
+# Use jq to perform the merge
+# We will stream the destination JSON into jq's stdin, and load the source JSON
+# as a separate argument using --slurpfile.
+# Since --slurpfile reads the file into an array, we'll access the first element ([0])
+# of the $src_data variable.
+MERGED_JSON=$(
+    jq \
+        --slurpfile src_data "$SRC_CLUSTER_LOCK_FILE" \
+        '
+    .cluster_definition.num_validators = (.cluster_definition.num_validators + ($src_data[0].cluster_definition.num_validators)) |
+    .cluster_definition.validators = (.cluster_definition.validators + $src_data[0].cluster_definition.validators) |
+    .distributed_validators = (.distributed_validators + $src_data[0].distributed_validators)
+    ' "$DST_CLUSTER_LOCK_FILE"
+)
+
+# Write the merged JSON back to the destination file
+# It's safer to write to a temporary file first and then move it,
+# to prevent data loss if the write operation is interrupted.
+TEMP_DST_JSON_FILE=$(mktemp "${DST_CLUSTER_LOCK_FILE}.$(date +%s%N).tmp")
+echo "$MERGED_JSON" >"$TEMP_DST_JSON_FILE"
+
+# Check if the temporary file was written successfully
+if [ $? -ne 0 ]; then
+    echo "Error: Failed to write to temporary file '$TEMP_DST_JSON_FILE'."
+    rm -f "$TEMP_DST_JSON_FILE" # Clean up temp file
+    exit 1
+fi
+
+# Move the temporary file to overwrite the original destination file
+mv "$TEMP_DST_JSON_FILE" "$DST_CLUSTER_LOCK_FILE"
+
+# Check if mv command was successful
+if [ $? -ne 0 ]; then
+    echo "Error: Failed to move temporary file to '$DST_CLUSTER_LOCK_FILE'. Original file might be intact."
+    exit 1
+fi
+
+# Get the count of validators from the source file for the summary message
+SRC_VALIDATOR_COUNT_CLUSTER_LOCK=$(jq '.cluster_definition.num_validators' "$SRC_CLUSTER_LOCK_FILE")
+
+echo "---"
+echo "Merged $SRC_VALIDATOR_COUNT_CLUSTER_LOCK validators from '$SRC_CLUSTER_LOCK_FILE' into '$DST_CLUSTER_LOCK_FILE'."
+echo "---"
+
+# --- Part 2: Merge validator_keys folder ---
+
+# Check if source validator_keys folder exists
+if [ ! -d "$SRC_KEYS_FOLDER" ]; then
+    echo "Error: Source folder '$SRC_KEYS_FOLDER' not found."
+    echo "Skipping validator_keys merge as source folder does not exist."
+else
+    # Create destination validator_keys folder if it doesn't exist
+    mkdir -p "$DST_KEYS_FOLDER"
+
+    # Find the next available key index in the destination folder
+    LAST_INDEX_DST=-1
+    if compgen -G "${DST_KEYS_FOLDER}/keystore-*.json" >/dev/null; then
+        for file in "$DST_KEYS_FOLDER"/keystore-*.json; do
+            filename=$(basename "$file")
+            # Extract the number after "keystore-" and before ".json"
+            if [[ "$filename" =~ keystore-([0-9]+)\.json ]]; then
+                current_index="${BASH_REMATCH[1]}"
+                if ((current_index > LAST_INDEX_DST)); then
+                    LAST_INDEX_DST="$current_index"
+                fi
+            fi
+        done
+    fi
+
+    NEXT_INDEX=$((LAST_INDEX_DST + 1))
+    MERGED_KEY_FILES_COUNT=0
+
+    echo "Merging keys from '$SRC_KEYS_FOLDER' into '$DST_KEYS_FOLDER'..."
+
+    # Process JSON files first to get the base index for each key
+    # Then find and copy the corresponding TXT file with the same new index
+    while IFS= read -r src_json_file; do
+        if [ -f "$src_json_file" ]; then
+            json_filename=$(basename "$src_json_file")
+
+            # Extract the original index of the JSON file
+            if [[ "$json_filename" =~ keystore-([0-9]+)\.json ]]; then
+                original_index="${BASH_REMATCH[1]}"
+
+                # Define the new filenames for both JSON and TXT
+                new_json_filename="keystore-${NEXT_INDEX}.json"
+                new_txt_filename="keystore-${NEXT_INDEX}.txt"
+
+                # Check read permission for source JSON file
+                if [ ! -r "$src_json_file" ]; then
+                    echo "Error: Source JSON key file '$src_json_file' is not readable. Skipping."
+                    continue # Skip to the next file
+                fi
+
+                # Copy the JSON file
+                cp "$src_json_file" "$DST_KEYS_FOLDER/$new_json_filename"
+                if [ $? -ne 0 ]; then
+                    echo "Error: Failed to copy '$src_json_file' to '$DST_KEYS_FOLDER/$new_json_filename'. Skipping this key pair."
+                    # Do not increment NEXT_INDEX or MERGED_KEY_FILES_COUNT as copy failed
+                    continue
+                fi
+                echo "Copied '$json_filename' as '$new_json_filename'"
+
+                # Check for and copy the corresponding TXT file
+                src_txt_file="${SRC_KEYS_FOLDER}/keystore-${original_index}.txt"
+                if [ -f "$src_txt_file" ]; then
+                    if [ ! -r "$src_txt_file" ]; then
+                        echo "Error: Corresponding TXT file '$src_txt_file' is not readable. Skipping TXT file for this key."
+                    else
+                        cp "$src_txt_file" "$DST_KEYS_FOLDER/$new_txt_filename"
+                        if [ $? -ne 0 ]; then
+                            echo "Error: Failed to copy '$src_txt_file' to '$DST_KEYS_FOLDER/$new_txt_filename'. This key might be partially merged."
+                        else
+                            echo "Copied 'keystore-${original_index}.txt' as '$new_txt_filename'"
+                            MERGED_KEY_FILES_COUNT=$((MERGED_KEY_FILES_COUNT + 1)) # Increment count only if both files are processed successfully
+                        fi
+                    fi
+                else
+                    echo "Error: Corresponding TXT file 'keystore-${original_index}.txt' not found for '$json_filename'. Script will be terminated."
+                    exit 1
+                fi
+
+                # Increment the index for the next *key pair*
+                NEXT_INDEX=$((NEXT_INDEX + 1))
+            else
+                echo "Error: Could not parse index from '$json_filename'. Script will be terminated."
+                exit 1
+            fi
+        fi
+    done < <(find "$SRC_KEYS_FOLDER" -maxdepth 1 -name 'keystore-*.json' | sort -V)
+
+    echo "---"
+    echo "Merged $MERGED_KEY_FILES_COUNT keys from '$SRC_KEYS_FOLDER' to '$DST_KEYS_FOLDER'."
+fi
+
+echo ""
+echo "WARNING: This script does not update the integrity hash of the merged cluster-lock.json file."
+echo "         Please add CHARON_NO_VERIFY=true or --no-verify=true to 'charon run' command when starting your node."
+echo "WARNING: Make sure to shutdown the other cluster before using the merged cluster-lock.json."
+echo "         Failing to do so may result in slashing!"
+echo ""
diff --git a/testutil/compose/static/lodestar/Dockerfile b/testutil/compose/static/lodestar/Dockerfile
index f21b49a4..7f04927b 100644
--- a/testutil/compose/static/lodestar/Dockerfile
+++ b/testutil/compose/static/lodestar/Dockerfile
@@ -1,4 +1,4 @@
-FROM chainsafe/lodestar:v1.32.0
+FROM chainsafe/lodestar:v1.33.0
 
 RUN apt-get update && apt-get install -y curl jq wget
 



Test patch:
diff --git a/app/disk_internal_test.go b/app/disk_internal_test.go
index 003097b7..549b63fd 100644
--- a/app/disk_internal_test.go
+++ b/app/disk_internal_test.go
@@ -42,6 +42,7 @@ func TestCalculateTrackerDelay(t *testing.T) {
 			genesis := now.Add(-test.slotDuration * currentSlot)
 
 			bmock, err := beaconmock.New(
+				t.Context(),
 				beaconmock.WithSlotDuration(test.slotDuration),
 				beaconmock.WithGenesisTime(genesis),
 			)
@@ -73,7 +74,7 @@ func TestSetFeeRecipient(t *testing.T) {
 			}
 		}
 
-		bmock, err := beaconmock.New(beaconmock.WithValidatorSet(clone))
+		bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(clone))
 		require.NoError(t, err)
 
 		// Only expect preparations for active validators.
diff --git a/app/eth2wrap/eth2wrap_test.go b/app/eth2wrap/eth2wrap_test.go
index 641c7a1e..0ff91c2e 100644
--- a/app/eth2wrap/eth2wrap_test.go
+++ b/app/eth2wrap/eth2wrap_test.go
@@ -5,7 +5,6 @@ package eth2wrap_test
 import (
 	"context"
 	"encoding/hex"
-	"encoding/json"
 	"fmt"
 	"net"
 	"net/http"
@@ -19,7 +18,6 @@ import (
 	eth2api "github.com/attestantio/go-eth2-client/api"
 	eth2v1 "github.com/attestantio/go-eth2-client/api/v1"
 	eth2spec "github.com/attestantio/go-eth2-client/spec"
-	eth2e "github.com/attestantio/go-eth2-client/spec/electra"
 	eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -37,42 +35,42 @@ func TestMulti(t *testing.T) {
 
 	tests := []struct {
 		name   string
-		handle func(cl1Resp, cl2Resp chan int, ctxCancel context.CancelFunc)
+		handle func(cl1Resp, cl2Resp chan *eth2v1.PeerCount, ctxCancel context.CancelFunc)
 		expErr error
-		expRes int
+		expRes *eth2api.Response[*eth2v1.PeerCount]
 	}{
 		{
 			name: "cl1 only",
-			handle: func(cl1Resp, _ chan int, _ context.CancelFunc) {
-				cl1Resp <- 99
+			handle: func(cl1Resp, _ chan *eth2v1.PeerCount, _ context.CancelFunc) {
+				cl1Resp <- &eth2v1.PeerCount{Connected: 99}
 			},
-			expRes: 99,
+			expRes: &eth2api.Response[*eth2v1.PeerCount]{Data: &eth2v1.PeerCount{Connected: 99}},
 		},
 		{
 			name: "cl2 only",
-			handle: func(_, cl2Resp chan int, _ context.CancelFunc) {
-				cl2Resp <- 99
+			handle: func(_, cl2Resp chan *eth2v1.PeerCount, _ context.CancelFunc) {
+				cl2Resp <- &eth2v1.PeerCount{Connected: 99}
 			},
-			expRes: 99,
+			expRes: &eth2api.Response[*eth2v1.PeerCount]{Data: &eth2v1.PeerCount{Connected: 99}},
 		},
 		{
 			name: "ctx cancel",
-			handle: func(_, _ chan int, ctxCancel context.CancelFunc) {
+			handle: func(_, _ chan *eth2v1.PeerCount, ctxCancel context.CancelFunc) {
 				ctxCancel()
 			},
 			expErr: context.Canceled,
 		},
 		{
 			name: "cl1 error, cl2 ok",
-			handle: func(cl1, cl2 chan int, _ context.CancelFunc) {
+			handle: func(cl1, cl2 chan *eth2v1.PeerCount, _ context.CancelFunc) {
 				close(cl1)
-				cl2 <- 99
+				cl2 <- &eth2v1.PeerCount{Connected: 99}
 			},
-			expRes: 99,
+			expRes: &eth2api.Response[*eth2v1.PeerCount]{Data: &eth2v1.PeerCount{Connected: 99}},
 		},
 		{
 			name: "all error",
-			handle: func(cl1, cl2 chan int, _ context.CancelFunc) {
+			handle: func(cl1, cl2 chan *eth2v1.PeerCount, _ context.CancelFunc) {
 				close(cl1)
 				close(cl2)
 			},
@@ -80,7 +78,7 @@ func TestMulti(t *testing.T) {
 		},
 		{
 			name: "cl1 error, ctx cancel",
-			handle: func(cl1, _ chan int, cancel context.CancelFunc) {
+			handle: func(cl1, _ chan *eth2v1.PeerCount, cancel context.CancelFunc) {
 				close(cl1)
 				cancel()
 			},
@@ -88,13 +86,13 @@ func TestMulti(t *testing.T) {
 		},
 		{
 			name: "cl2 before cl1",
-			handle: func(cl1, cl2 chan int, cancel context.CancelFunc) {
-				cl2 <- 99
+			handle: func(cl1, cl2 chan *eth2v1.PeerCount, cancel context.CancelFunc) {
+				cl2 <- &eth2v1.PeerCount{Connected: 99}
 
 				time.Sleep(time.Millisecond)
-				cl1 <- 98 // This might flap?
+				cl1 <- &eth2v1.PeerCount{Connected: 98} // This might flap?
 			},
-			expRes: 99,
+			expRes: &eth2api.Response[*eth2v1.PeerCount]{Data: &eth2v1.PeerCount{Connected: 99}},
 		},
 	}
 	for _, test := range tests {
@@ -102,33 +100,33 @@ func TestMulti(t *testing.T) {
 			ctx, cancel := context.WithCancel(context.Background())
 			defer cancel()
 
-			cl1, err := beaconmock.New()
+			cl1, err := beaconmock.New(t.Context())
 			require.NoError(t, err)
-			cl2, err := beaconmock.New()
+			cl2, err := beaconmock.New(t.Context())
 			require.NoError(t, err)
 
-			cl1Resp := make(chan int)
-			cl2Resp := make(chan int)
+			cl1Resp := make(chan *eth2v1.PeerCount)
+			cl2Resp := make(chan *eth2v1.PeerCount)
 
-			cl1.NodePeerCountFunc = func(ctx context.Context) (int, error) {
+			cl1.NodePeerCountFunc = func(ctx context.Context, opts *eth2api.NodePeerCountOpts) (*eth2v1.PeerCount, error) {
 				select {
 				case <-ctx.Done():
-					return 0, ctx.Err()
+					return &eth2v1.PeerCount{Connected: 0}, ctx.Err()
 				case resp, ok := <-cl1Resp:
 					if !ok {
-						return 0, closedErr
+						return &eth2v1.PeerCount{Connected: 0}, closedErr
 					}
 
 					return resp, nil
 				}
 			}
-			cl2.NodePeerCountFunc = func(ctx context.Context) (int, error) {
+			cl2.NodePeerCountFunc = func(ctx context.Context, opts *eth2api.NodePeerCountOpts) (*eth2v1.PeerCount, error) {
 				select {
 				case <-ctx.Done():
-					return 0, ctx.Err()
+					return &eth2v1.PeerCount{Connected: 0}, ctx.Err()
 				case resp, ok := <-cl2Resp:
 					if !ok {
-						return 0, closedErr
+						return &eth2v1.PeerCount{Connected: 0}, closedErr
 					}
 
 					return resp, nil
@@ -140,7 +138,7 @@ func TestMulti(t *testing.T) {
 
 			go test.handle(cl1Resp, cl2Resp, cancel)
 
-			resp, err := eth2Cl.NodePeerCount(ctx)
+			resp, err := eth2Cl.NodePeerCount(ctx, &eth2api.NodePeerCountOpts{})
 			require.ErrorIs(t, err, test.expErr)
 			require.Equal(t, test.expRes, resp)
 		})
@@ -148,8 +146,8 @@ func TestMulti(t *testing.T) {
 }
 
 func TestFallback(t *testing.T) {
-	returnValue := 42
-	closedErr := errors.New("error")
+	returnValue := &eth2v1.PeerCount{Connected: 42}
+	closedErr := errors.New("context deadline exceeded")
 
 	tests := []struct {
 		name         string
@@ -194,10 +192,10 @@ func TestFallback(t *testing.T) {
 					allPrimariesFail = false
 				}
 
-				cl, err := beaconmock.New()
+				cl, err := beaconmock.New(t.Context())
 				require.NoError(t, err)
 
-				cl.NodePeerCountFunc = func(context.Context) (int, error) {
+				cl.NodePeerCountFunc = func(context.Context, *eth2api.NodePeerCountOpts) (*eth2v1.PeerCount, error) {
 					calledMu.Lock()
 
 					primaryCalled[i] = true
@@ -212,10 +210,10 @@ func TestFallback(t *testing.T) {
 			// Create fallback client
 			fallbackClients := make([]eth2wrap.Client, len(tt.fallbackErrs))
 			for i, fallbackErr := range tt.fallbackErrs {
-				cl, err := beaconmock.New()
+				cl, err := beaconmock.New(t.Context())
 				require.NoError(t, err)
 
-				cl.NodePeerCountFunc = func(context.Context) (int, error) {
+				cl.NodePeerCountFunc = func(context.Context, *eth2api.NodePeerCountOpts) (*eth2v1.PeerCount, error) {
 					calledMu.Lock()
 
 					fallbackCalled[i] = true
@@ -229,9 +227,9 @@ func TestFallback(t *testing.T) {
 
 			eth2Cl, err := eth2wrap.Instrument(primaryClients, fallbackClients)
 			require.NoError(t, err)
-			res, err := eth2Cl.NodePeerCount(t.Context())
+			res, err := eth2Cl.NodePeerCount(t.Context(), &eth2api.NodePeerCountOpts{})
 			require.NoError(t, err)
-			require.Equal(t, returnValue, res)
+			require.Equal(t, returnValue, res.Data)
 
 			calledMu.Lock()
 			defer calledMu.Unlock()
@@ -263,9 +261,9 @@ func TestFallback(t *testing.T) {
 }
 
 func TestSyncState(t *testing.T) {
-	cl1, err := beaconmock.New()
+	cl1, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
-	cl2, err := beaconmock.New()
+	cl2, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	cl1.NodeSyncingFunc = func(ctx context.Context, opts *eth2api.NodeSyncingOpts) (*eth2v1.SyncState, error) {
@@ -290,10 +288,10 @@ func TestErrors(t *testing.T) {
 		cl, err := eth2wrap.NewMultiHTTP(time.Hour, [4]byte{}, nil, []string{"localhost:22222"}, nil)
 		require.NoError(t, err)
 
-		_, err = cl.NodePeerCount(ctx)
+		_, err = cl.NodePeerCount(ctx, &eth2api.NodePeerCountOpts{})
 		log.Error(ctx, "See this error log for fields", err)
 		require.Error(t, err)
-		require.ErrorContains(t, err, "beacon api node_peer_count: network operation error")
+		require.ErrorContains(t, err, "beacon api node_peer_count: client is not active")
 	})
 
 	// Test http server that just hangs until request cancelled
@@ -305,10 +303,10 @@ func TestErrors(t *testing.T) {
 		cl, err := eth2wrap.NewMultiHTTP(time.Millisecond, [4]byte{}, nil, []string{srv.URL}, nil)
 		require.NoError(t, err)
 
-		_, err = cl.NodePeerCount(ctx)
+		_, err = cl.NodePeerCount(ctx, &eth2api.NodePeerCountOpts{})
 		log.Error(ctx, "See this error log for fields", err)
 		require.Error(t, err)
-		require.ErrorContains(t, err, "beacon api node_peer_count: http request timeout")
+		require.ErrorContains(t, err, "beacon api node_peer_count: client is not active")
 	})
 
 	t.Run("caller cancelled", func(t *testing.T) {
@@ -318,14 +316,14 @@ func TestErrors(t *testing.T) {
 		cl, err := eth2wrap.NewMultiHTTP(time.Millisecond, [4]byte{}, nil, []string{srv.URL}, nil)
 		require.NoError(t, err)
 
-		_, err = cl.NodePeerCount(ctx)
+		_, err = cl.NodePeerCount(ctx, &eth2api.NodePeerCountOpts{})
 		log.Error(ctx, "See this error log for fields", err)
 		require.Error(t, err)
 		require.ErrorContains(t, err, "beacon api node_peer_count: context canceled")
 	})
 
 	t.Run("zero net op error", func(t *testing.T) {
-		bmock, err := beaconmock.New()
+		bmock, err := beaconmock.New(t.Context())
 		require.NoError(t, err)
 
 		bmock.GenesisFunc = func(context.Context, *eth2api.GenesisOpts) (*eth2v1.Genesis, error) {
@@ -343,7 +341,7 @@ func TestErrors(t *testing.T) {
 	})
 
 	t.Run("eth2api error", func(t *testing.T) {
-		bmock, err := beaconmock.New()
+		bmock, err := beaconmock.New(t.Context())
 		require.NoError(t, err)
 
 		bmock.SignedBeaconBlockFunc = func(_ context.Context, blockID string) (*eth2spec.VersionedSignedBeaconBlock, error) {
@@ -369,7 +367,7 @@ func TestCtxCancel(t *testing.T) {
 	for range 10 {
 		ctx, cancel := context.WithCancel(context.Background())
 
-		bmock, err := beaconmock.New()
+		bmock, err := beaconmock.New(t.Context())
 		require.NoError(t, err)
 		eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte{}, nil, []string{bmock.Address()}, nil)
 		require.NoError(t, err)
@@ -381,65 +379,6 @@ func TestCtxCancel(t *testing.T) {
 	}
 }
 
-func TestBlockAttestations(t *testing.T) {
-	electraAtt1 := testutil.RandomElectraAttestation()
-	electraAtt2 := testutil.RandomElectraAttestation()
-
-	tests := []struct {
-		version          string
-		attestations     []*eth2spec.VersionedAttestation
-		serverJSONStruct any
-		expErr           string
-	}{
-		{
-			version: "electra",
-			attestations: []*eth2spec.VersionedAttestation{
-				{Version: eth2spec.DataVersionElectra, Electra: electraAtt1},
-				{Version: eth2spec.DataVersionElectra, Electra: electraAtt2},
-			},
-			serverJSONStruct: struct{ Data []*eth2e.Attestation }{Data: []*eth2e.Attestation{electraAtt1, electraAtt2}},
-			expErr:           "",
-		},
-	}
-	for _, test := range tests {
-		t.Run(test.version, func(t *testing.T) {
-			statusCode := http.StatusOK
-			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-				require.Equal(t, http.MethodGet, r.Method)
-				require.Equal(t, "/eth/v2/beacon/blocks/head/attestations", r.URL.Path)
-
-				b, err := json.Marshal(test.serverJSONStruct)
-				require.NoError(t, err)
-
-				w.Header().Add("Eth-Consensus-Version", test.version)
-				w.WriteHeader(statusCode)
-				_, _ = w.Write(b)
-			}))
-
-			cl := eth2wrap.NewHTTPAdapterForT(t, srv.URL, nil, time.Hour)
-
-			resp, err := cl.BlockAttestations(context.Background(), "head")
-			if test.expErr != "" {
-				require.ErrorContains(t, err, test.expErr)
-			} else {
-				require.NoError(t, err)
-			}
-
-			require.Equal(t, test.attestations, resp)
-
-			statusCode = http.StatusNotFound
-			resp, err = cl.BlockAttestations(context.Background(), "head")
-			require.NoError(t, err)
-			require.Empty(t, resp)
-
-			statusCode = http.StatusBadRequest
-			resp, err = cl.BlockAttestations(context.Background(), "head")
-			require.Error(t, err)
-			require.Empty(t, resp)
-		})
-	}
-}
-
 // TestOneError tests the case where one of the servers returns errors.
 func TestOneError(t *testing.T) {
 	// Start an erroring server.
@@ -449,7 +388,7 @@ func TestOneError(t *testing.T) {
 	defer srv.Close()
 
 	ctx := context.Background()
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	addresses := []string{
@@ -480,7 +419,7 @@ func TestOneTimeout(t *testing.T) {
 	defer srv.Close()
 	defer cancel() // Cancel the context before stopping the server.
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	addresses := []string{
@@ -557,7 +496,7 @@ func TestOnlyTimeout(t *testing.T) {
 func TestLazy(t *testing.T) {
 	ctx := context.Background()
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	target := testutil.MustParseURL(t, bmock.Address())
@@ -650,7 +589,7 @@ func TestLazyDomain(t *testing.T) {
 		t.Run(test.name, func(t *testing.T) {
 			ctx := context.Background()
 
-			bmock, err := beaconmock.New()
+			bmock, err := beaconmock.New(t.Context())
 			require.NoError(t, err)
 
 			target := testutil.MustParseURL(t, bmock.Address())
diff --git a/app/eth2wrap/lazy_test.go b/app/eth2wrap/lazy_test.go
index 9f8f6f75..212af598 100644
--- a/app/eth2wrap/lazy_test.go
+++ b/app/eth2wrap/lazy_test.go
@@ -6,13 +6,11 @@ import (
 	"context"
 	"testing"
 
-	"github.com/attestantio/go-eth2-client/spec"
 	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
 
 	"github.com/obolnetwork/charon/app/eth2wrap"
 	"github.com/obolnetwork/charon/app/eth2wrap/mocks"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 )
 
 func TestLazy_Name(t *testing.T) {
@@ -51,75 +49,6 @@ func TestLazy_IsSynced(t *testing.T) {
 	require.True(t, l.IsSynced())
 }
 
-func TestLazy_NodePeerCount(t *testing.T) {
-	client := mocks.NewClient(t)
-	client.On("NodePeerCount", mock.Anything).Return(5, nil).Once()
-
-	l := eth2wrap.NewLazyForT(client)
-
-	c, err := l.NodePeerCount(context.Background())
-	require.NoError(t, err)
-	require.Equal(t, 5, c)
-}
-
-func TestLazy_BlockAttestations(t *testing.T) {
-	ctx := context.Background()
-	atts := make([]*spec.VersionedAttestation, 3)
-
-	client := mocks.NewClient(t)
-	client.On("BlockAttestations", ctx, "state").Return(atts, nil).Once()
-
-	l := eth2wrap.NewLazyForT(client)
-
-	atts2, err := l.BlockAttestations(ctx, "state")
-	require.NoError(t, err)
-	require.Equal(t, atts, atts2)
-}
-
-func TestLazy_AggregateSyncCommitteeSelections(t *testing.T) {
-	ctx := context.Background()
-	partsel := make([]*eth2exp.SyncCommitteeSelection, 1)
-	selections := make([]*eth2exp.SyncCommitteeSelection, 3)
-
-	client := mocks.NewClient(t)
-	client.On("AggregateSyncCommitteeSelections", ctx, partsel).Return(selections, nil).Once()
-
-	l := eth2wrap.NewLazyForT(client)
-
-	selections2, err := l.AggregateSyncCommitteeSelections(ctx, partsel)
-	require.NoError(t, err)
-	require.Equal(t, selections, selections2)
-}
-
-func TestLazy_AggregateBeaconCommitteeSelections(t *testing.T) {
-	ctx := context.Background()
-	partsel := make([]*eth2exp.BeaconCommitteeSelection, 1)
-	selections := make([]*eth2exp.BeaconCommitteeSelection, 3)
-
-	client := mocks.NewClient(t)
-	client.On("AggregateBeaconCommitteeSelections", ctx, partsel).Return(selections, nil).Once()
-
-	l := eth2wrap.NewLazyForT(client)
-
-	selections2, err := l.AggregateBeaconCommitteeSelections(ctx, partsel)
-	require.NoError(t, err)
-	require.Equal(t, selections, selections2)
-}
-
-func TestLazy_ProposerConfig(t *testing.T) {
-	ctx := context.Background()
-	resp := &eth2exp.ProposerConfigResponse{}
-
-	client := mocks.NewClient(t)
-	client.On("ProposerConfig", ctx).Return(resp, nil).Once()
-
-	l := eth2wrap.NewLazyForT(client)
-
-	resp2, err := l.ProposerConfig(ctx)
-	require.NoError(t, err)
-	require.Equal(t, resp, resp2)
-}
-
 func TestLazy_ActiveValidators(t *testing.T) {
 	ctx := context.Background()
 	vals := make(eth2wrap.ActiveValidators)
diff --git a/app/eth2wrap/multi_test.go b/app/eth2wrap/multi_test.go
index 61017ae8..5ba1d736 100644
--- a/app/eth2wrap/multi_test.go
+++ b/app/eth2wrap/multi_test.go
@@ -7,13 +7,11 @@ import (
 	"errors"
 	"testing"
 
-	"github.com/attestantio/go-eth2-client/spec"
 	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
 
 	"github.com/obolnetwork/charon/app/eth2wrap"
 	"github.com/obolnetwork/charon/app/eth2wrap/mocks"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 )
 
 func TestMulti_Name(t *testing.T) {
@@ -57,108 +55,6 @@ func TestMulti_IsSynced(t *testing.T) {
 	require.True(t, m.IsSynced())
 }
 
-func TestMulti_NodePeerCount(t *testing.T) {
-	client := mocks.NewClient(t)
-	client.On("Address").Return("test").Once()
-	client.On("NodePeerCount", mock.Anything).Return(5, nil).Once()
-
-	m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}, nil)
-
-	c, err := m.NodePeerCount(context.Background())
-	require.NoError(t, err)
-	require.Equal(t, 5, c)
-
-	expectedErr := errors.New("boo")
-	client.On("NodePeerCount", mock.Anything).Return(0, expectedErr).Once()
-
-	_, err = m.NodePeerCount(context.Background())
-	require.ErrorIs(t, err, expectedErr)
-}
-
-func TestMulti_BlockAttestations(t *testing.T) {
-	ctx := context.Background()
-	atts := make([]*spec.VersionedAttestation, 3)
-
-	client := mocks.NewClient(t)
-	client.On("Address").Return("test").Once()
-	client.On("BlockAttestations", mock.Anything, "state").Return(atts, nil).Once()
-
-	m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}, nil)
-
-	atts2, err := m.BlockAttestations(ctx, "state")
-	require.NoError(t, err)
-	require.Equal(t, atts, atts2)
-
-	expectedErr := errors.New("boo")
-	client.On("BlockAttestations", mock.Anything, "state").Return(nil, expectedErr).Once()
-
-	_, err = m.BlockAttestations(ctx, "state")
-	require.ErrorIs(t, err, expectedErr)
-}
-
-func TestMulti_AggregateSyncCommitteeSelections(t *testing.T) {
-	ctx := context.Background()
-	partsel := make([]*eth2exp.SyncCommitteeSelection, 1)
-	selections := make([]*eth2exp.SyncCommitteeSelection, 3)
-
-	client := mocks.NewClient(t)
-	client.On("Address").Return("test").Once()
-	client.On("AggregateSyncCommitteeSelections", mock.Anything, partsel).Return(selections, nil).Once()
-
-	m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}, nil)
-
-	selections2, err := m.AggregateSyncCommitteeSelections(ctx, partsel)
-	require.NoError(t, err)
-	require.Equal(t, selections, selections2)
-
-	expectedErr := errors.New("boo")
-	client.On("AggregateSyncCommitteeSelections", mock.Anything, partsel).Return(nil, expectedErr).Once()
-	_, err = m.AggregateSyncCommitteeSelections(ctx, partsel)
-	require.ErrorIs(t, err, expectedErr)
-}
-
-func TestMulti_AggregateBeaconCommitteeSelections(t *testing.T) {
-	ctx := context.Background()
-	partsel := make([]*eth2exp.BeaconCommitteeSelection, 1)
-	selections := make([]*eth2exp.BeaconCommitteeSelection, 3)
-
-	client := mocks.NewClient(t)
-	client.On("Address").Return("test").Once()
-	client.On("AggregateBeaconCommitteeSelections", mock.Anything, partsel).Return(selections, nil).Once()
-
-	m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}, nil)
-
-	selections2, err := m.AggregateBeaconCommitteeSelections(ctx, partsel)
-	require.NoError(t, err)
-	require.Equal(t, selections, selections2)
-
-	expectedErr := errors.New("boo")
-	client.On("AggregateBeaconCommitteeSelections", mock.Anything, partsel).Return(nil, expectedErr).Once()
-	_, err = m.AggregateBeaconCommitteeSelections(ctx, partsel)
-	require.ErrorIs(t, err, expectedErr)
-}
-
-func TestMulti_ProposerConfig(t *testing.T) {
-	ctx := context.Background()
-	resp := &eth2exp.ProposerConfigResponse{}
-
-	client := mocks.NewClient(t)
-	client.On("Address").Return("test").Once()
-	client.On("ProposerConfig", mock.Anything).Return(resp, nil).Once()
-
-	m := eth2wrap.NewMultiForT([]eth2wrap.Client{client}, nil)
-
-	resp2, err := m.ProposerConfig(ctx)
-	require.NoError(t, err)
-	require.Equal(t, resp, resp2)
-
-	expectedErr := errors.New("boo")
-	client.On("ProposerConfig", mock.Anything).Return(nil, expectedErr).Once()
-
-	_, err = m.ProposerConfig(ctx)
-	require.ErrorIs(t, err, expectedErr)
-}
-
 func TestMulti_ActiveValidators(t *testing.T) {
 	ctx := context.Background()
 	vals := make(eth2wrap.ActiveValidators)
diff --git a/app/eth2wrap/synthproposer_test.go b/app/eth2wrap/synthproposer_test.go
index 587107a7..8ab77c69 100644
--- a/app/eth2wrap/synthproposer_test.go
+++ b/app/eth2wrap/synthproposer_test.go
@@ -177,7 +177,7 @@ func TestSynthProposer(t *testing.T) {
 				activeVals                 = 0
 			)
 
-			bmock, err := beaconmock.New(beaconmock.WithValidatorSet(set), beaconmock.WithSlotsPerEpoch(slotsPerEpoch))
+			bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(set), beaconmock.WithSlotsPerEpoch(slotsPerEpoch))
 			require.NoError(t, err)
 
 			bmock.SubmitProposalFunc = func(ctx context.Context, opts *eth2api.SubmitProposalOpts) error {
@@ -332,7 +332,7 @@ func TestSynthProposerBlockNotFound(t *testing.T) {
 		timesCalled   int
 	)
 
-	bmock, err := beaconmock.New(beaconmock.WithValidatorSet(set), beaconmock.WithSlotsPerEpoch(slotsPerEpoch))
+	bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(set), beaconmock.WithSlotsPerEpoch(slotsPerEpoch))
 	require.NoError(t, err)
 
 	bmock.ProposerDutiesFunc = func(ctx context.Context, e eth2p0.Epoch, indices []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) {
diff --git a/app/eth2wrap/utils_test.go b/app/eth2wrap/utils_test.go
index f2e5c97a..26dad117 100644
--- a/app/eth2wrap/utils_test.go
+++ b/app/eth2wrap/utils_test.go
@@ -14,7 +14,7 @@ import (
 )
 
 func TestFetchGenesisTime(t *testing.T) {
-	eth2Cl, err := beaconmock.New()
+	eth2Cl, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	genesisTime, err := eth2wrap.FetchGenesisTime(t.Context(), eth2Cl)
@@ -25,7 +25,7 @@ func TestFetchGenesisTime(t *testing.T) {
 }
 
 func TestFetchSlotsConfig(t *testing.T) {
-	eth2Cl, err := beaconmock.New()
+	eth2Cl, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	slotDuration, slotsPerEpoch, err := eth2wrap.FetchSlotsConfig(t.Context(), eth2Cl)
@@ -37,7 +37,7 @@ func TestFetchSlotsConfig(t *testing.T) {
 }
 
 func TestFetchForkConfig(t *testing.T) {
-	eth2Cl, err := beaconmock.New()
+	eth2Cl, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	forkConfig, err := eth2wrap.FetchForkConfig(t.Context(), eth2Cl)
diff --git a/app/eth2wrap/valcache_test.go b/app/eth2wrap/valcache_test.go
index 17e9e6a9..e669cf35 100644
--- a/app/eth2wrap/valcache_test.go
+++ b/app/eth2wrap/valcache_test.go
@@ -43,7 +43,7 @@ func TestValidatorCache(t *testing.T) {
 	}
 
 	// Create a mock client.
-	eth2Cl, err := beaconmock.New()
+	eth2Cl, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Configure it to return the set of validators if queried.
@@ -97,7 +97,7 @@ func TestValidatorCache(t *testing.T) {
 func TestGetBySlot(t *testing.T) {
 	t.Run("successful fetch", func(t *testing.T) {
 		// Create a mock client.
-		eth2Cl, err := beaconmock.New()
+		eth2Cl, err := beaconmock.New(t.Context())
 		require.NoError(t, err)
 
 		// Create two validators pubkeys
@@ -194,7 +194,7 @@ func TestGetBySlot(t *testing.T) {
 
 	t.Run("fallback to head state", func(t *testing.T) {
 		// Create a mock client.
-		eth2Cl, err := beaconmock.New()
+		eth2Cl, err := beaconmock.New(t.Context())
 		require.NoError(t, err)
 
 		// Create two validators pubkeys
diff --git a/app/monitoringapi_internal_test.go b/app/monitoringapi_internal_test.go
index 9a03ceed..a8a695ed 100644
--- a/app/monitoringapi_internal_test.go
+++ b/app/monitoringapi_internal_test.go
@@ -103,7 +103,7 @@ func TestStartChecker(t *testing.T) {
 			slotDuration := 12 * time.Second
 			slotsPerEpoch := 32
 
-			bmock, err := beaconmock.New(beaconmock.WithSlotDuration(slotDuration), beaconmock.WithSlotsPerEpoch(slotsPerEpoch))
+			bmock, err := beaconmock.New(t.Context(), beaconmock.WithSlotDuration(slotDuration), beaconmock.WithSlotsPerEpoch(slotsPerEpoch))
 			require.NoError(t, err)
 
 			bmock.NodeSyncingFunc = func(ctx context.Context, opts *eth2api.NodeSyncingOpts) (*eth2v1.SyncState, error) {
@@ -117,8 +117,8 @@ func TestStartChecker(t *testing.T) {
 			}
 
 			if tt.zeroBNPeers {
-				bmock.NodePeerCountFunc = func(ctx context.Context) (int, error) {
-					return 0, nil
+				bmock.NodePeerCountFunc = func(ctx context.Context, _ *eth2api.NodePeerCountOpts) (*eth2v1.PeerCount, error) {
+					return &eth2v1.PeerCount{Connected: 0}, nil
 				}
 			}
 
diff --git a/app/obolapi/exit_test.go b/app/obolapi/exit_test.go
index 3b39519b..2d49551a 100644
--- a/app/obolapi/exit_test.go
+++ b/app/obolapi/exit_test.go
@@ -29,7 +29,7 @@ const exitEpoch = eth2p0.Epoch(194048)
 func TestAPIFlow(t *testing.T) {
 	kn := 4
 
-	beaconMock, err := beaconmock.New()
+	beaconMock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	defer func() {
@@ -121,7 +121,7 @@ func TestAPIFlow(t *testing.T) {
 func TestAPIFlowMissingSig(t *testing.T) {
 	kn := 4
 
-	beaconMock, err := beaconmock.New()
+	beaconMock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	defer func() {
diff --git a/app/retry/retry_test.go b/app/retry/retry_test.go
index 2763b64e..44f7ba9d 100644
--- a/app/retry/retry_test.go
+++ b/app/retry/retry_test.go
@@ -105,7 +105,7 @@ func TestRetryer(t *testing.T) {
 
 func TestShutdown(t *testing.T) {
 	ctx := context.Background()
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	deadlineFunc, err := core.NewDutyDeadlineFunc(ctx, bmock)
diff --git a/app/sse/listener_internal_test.go b/app/sse/listener_internal_test.go
index 63555c9f..551c54aa 100644
--- a/app/sse/listener_internal_test.go
+++ b/app/sse/listener_internal_test.go
@@ -105,7 +105,7 @@ func TestHandleEvents(t *testing.T) {
 }
 
 func TestStartListener(t *testing.T) {
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	_, err = StartListener(t.Context(), bmock, []string{bmock.Address()}, []string{})
diff --git a/cmd/exit_broadcast_internal_test.go b/cmd/exit_broadcast_internal_test.go
index 0cda5d7f..5de9e7af 100644
--- a/cmd/exit_broadcast_internal_test.go
+++ b/cmd/exit_broadcast_internal_test.go
@@ -110,6 +110,7 @@ func testRunBcastFullExitCmdFlow(t *testing.T, fromFile bool, all bool) {
 	}
 
 	beaconMock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithValidatorSet(validatorSet),
 		beaconmock.WithEndpoint("/eth/v1/beacon/pool/voluntary_exits", ""),
 	)
@@ -310,7 +311,7 @@ func Test_runBcastFullExitCmd_Config(t *testing.T) {
 			bnURL := badStr
 
 			if !test.badBeaconNodeEndpoints {
-				beaconMock, err := beaconmock.New()
+				beaconMock, err := beaconmock.New(t.Context())
 				require.NoError(t, err)
 
 				defer func() {
@@ -490,6 +491,7 @@ func TestExitBcastFullExitNotActivated(t *testing.T) {
 	}
 
 	beaconMock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithValidatorSet(validatorSet),
 		beaconmock.WithEndpoint("/eth/v1/beacon/pool/voluntary_exits", ""),
 	)
diff --git a/cmd/exit_delete_internal_test.go b/cmd/exit_delete_internal_test.go
index f771b23e..198b7a30 100644
--- a/cmd/exit_delete_internal_test.go
+++ b/cmd/exit_delete_internal_test.go
@@ -82,6 +82,7 @@ func testRunDeleteExitFullFlow(t *testing.T, all bool) {
 	}
 
 	beaconMock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithValidatorSet(validatorSet),
 	)
 	require.NoError(t, err)
diff --git a/cmd/exit_fetch_internal_test.go b/cmd/exit_fetch_internal_test.go
index b2d7ebda..ee1a9c7b 100644
--- a/cmd/exit_fetch_internal_test.go
+++ b/cmd/exit_fetch_internal_test.go
@@ -84,6 +84,7 @@ func testRunFetchExitFullFlow(t *testing.T, all bool) {
 	}
 
 	beaconMock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithValidatorSet(validatorSet),
 	)
 	require.NoError(t, err)
@@ -297,6 +298,7 @@ func TestFetchExitFullFlowNotActivated(t *testing.T) {
 	}
 
 	beaconMock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithValidatorSet(validatorSet),
 	)
 	require.NoError(t, err)
diff --git a/cmd/exit_list_internal_test.go b/cmd/exit_list_internal_test.go
index ca274779..492b2073 100644
--- a/cmd/exit_list_internal_test.go
+++ b/cmd/exit_list_internal_test.go
@@ -67,7 +67,7 @@ func Test_runListActiveVals(t *testing.T) {
 		}
 	}
 
-	beaconMock, err := beaconmock.New(beaconmock.WithValidatorSet(validatorSet))
+	beaconMock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(validatorSet))
 	require.NoError(t, err)
 
 	defer func() {
@@ -135,7 +135,7 @@ func Test_listActiveVals(t *testing.T) {
 			}
 		}
 
-		beaconMock, err := beaconmock.New(beaconmock.WithValidatorSet(validatorSet))
+		beaconMock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(validatorSet))
 		require.NoError(t, err)
 
 		defer func() {
@@ -178,7 +178,7 @@ func Test_listActiveVals(t *testing.T) {
 			}
 		}
 
-		beaconMock, err := beaconmock.New(beaconmock.WithValidatorSet(validatorSet))
+		beaconMock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(validatorSet))
 		require.NoError(t, err)
 
 		defer func() {
diff --git a/cmd/exit_sign_internal_test.go b/cmd/exit_sign_internal_test.go
index b9f4c997..0b690c70 100644
--- a/cmd/exit_sign_internal_test.go
+++ b/cmd/exit_sign_internal_test.go
@@ -184,7 +184,7 @@ func runSubmitPartialExitFlowTest(t *testing.T, useValIdx bool, skipBeaconNodeCh
 		}
 	}
 
-	beaconMock, err := beaconmock.New(beaconmock.WithValidatorSet(validatorSet))
+	beaconMock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(validatorSet))
 	require.NoError(t, err)
 
 	defer func() {
@@ -355,7 +355,7 @@ func Test_runSubmitPartialExit_Config(t *testing.T) {
 			bnURL := badStr
 
 			if !test.badBeaconNodeEndpoints {
-				beaconMock, err := beaconmock.New()
+				beaconMock, err := beaconmock.New(t.Context())
 				require.NoError(t, err)
 
 				defer func() {
diff --git a/cmd/test.go b/cmd/test.go
index 1b8f187f..08338039 100644
--- a/cmd/test.go
+++ b/cmd/test.go
@@ -344,6 +344,8 @@ func writeResultToFile(res testCategoryResult, path string) error {
 		file.MEV = res
 	case infraTestCategory:
 		file.Infra = res
+	default:
+		return errors.New("unknown category", z.Str("category", res.CategoryName))
 	}
 
 	// write data to temp file
@@ -394,6 +396,7 @@ func writeResultToWriter(res testCategoryResult, w io.Writer) error {
 		lines = append(lines, categoryDefaultASCII()...)
 	}
 
+	//nolint:revive // enforce-switch-style: the list is exhaustive and there is no need for default
 	switch res.Score {
 	case categoryScoreA:
 		lines = appendScore(lines, scoreAASCII())
@@ -488,6 +491,7 @@ func calculateScore(results []testResult) categoryScore {
 	avg := 0
 
 	for _, t := range results {
+		//nolint:revive // enforce-switch-style: the list is exhaustive and there is no need for default
 		switch t.Verdict {
 		case testVerdictPoor:
 			return categoryScoreC
diff --git a/cmd/testinfra.go b/cmd/testinfra.go
index bad73c4f..3721430d 100644
--- a/cmd/testinfra.go
+++ b/cmd/testinfra.go
@@ -782,6 +782,7 @@ func availableMemoryMacos(ctx context.Context) (int64, error) {
 			}
 
 			pagesSpeculative = bytes
+		default:
 		}
 	}
 
diff --git a/cmd/testpeers.go b/cmd/testpeers.go
index 39b4ebbf..8ed075f1 100644
--- a/cmd/testpeers.go
+++ b/cmd/testpeers.go
@@ -771,6 +771,8 @@ func fetchENRs(ctx context.Context, conf testPeersConfig) ([]string, error) {
 		if err != nil {
 			return nil, err
 		}
+	default:
+		return nil, errors.New("no enrs, definition file or lock file specified")
 	}
 
 	return enrs, nil
diff --git a/core/bcast/bcast_test.go b/core/bcast/bcast_test.go
index dfe93a57..b5c12e19 100644
--- a/core/bcast/bcast_test.go
+++ b/core/bcast/bcast_test.go
@@ -42,7 +42,7 @@ func TestBroadcast(t *testing.T) {
 	}
 
 	for _, testFunc := range testFuncs {
-		mock, err := beaconmock.New()
+		mock, err := beaconmock.New(t.Context())
 		require.NoError(t, err)
 
 		test := testFunc(t, &mock)
@@ -74,7 +74,7 @@ func TestBroadcast(t *testing.T) {
 }
 
 func TestBroadcastOtherDuties(t *testing.T) {
-	mock, err := beaconmock.New()
+	mock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	bcaster, err := bcast.New(context.Background(), mock)
diff --git a/core/deadline_test.go b/core/deadline_test.go
index eb7efd51..5431a1e4 100644
--- a/core/deadline_test.go
+++ b/core/deadline_test.go
@@ -88,7 +88,7 @@ func TestDeadliner(t *testing.T) {
 }
 
 func TestNewDutyDeadlineFunc(t *testing.T) {
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	genesisTime, err := eth2wrap.FetchGenesisTime(t.Context(), bmock)
diff --git a/core/eth2signeddata_test.go b/core/eth2signeddata_test.go
index d9343981..2edb965a 100644
--- a/core/eth2signeddata_test.go
+++ b/core/eth2signeddata_test.go
@@ -82,7 +82,7 @@ func TestVerifyEth2SignedData(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			bmock, err := beaconmock.New()
+			bmock, err := beaconmock.New(t.Context())
 			require.NoError(t, err)
 
 			epoch, err := test.data.Epoch(context.Background(), bmock)
diff --git a/core/fetcher/fetcher_test.go b/core/fetcher/fetcher_test.go
index 5c4ae39c..34a82a68 100644
--- a/core/fetcher/fetcher_test.go
+++ b/core/fetcher/fetcher_test.go
@@ -22,7 +22,6 @@ import (
 	"github.com/obolnetwork/charon/app/eth2wrap/mocks"
 	"github.com/obolnetwork/charon/core"
 	"github.com/obolnetwork/charon/core/fetcher"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 	"github.com/obolnetwork/charon/testutil"
 	"github.com/obolnetwork/charon/testutil/beaconmock"
 )
@@ -63,7 +62,7 @@ func TestFetchAttester(t *testing.T) {
 		pubkeysByIdx[vIdxB]: core.NewAttesterDefinition(&dutyB),
 	}
 	duty := core.NewAttesterDuty(slot)
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	fetch := mustCreateFetcher(t, bmock)
@@ -139,7 +138,7 @@ func TestFetchAggregator(t *testing.T) {
 		pubkeysByIdx[vIdxB]: testutil.RandomCoreBeaconCommitteeSelection(),
 	}
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	var aggAttCallCount int
@@ -296,7 +295,7 @@ func TestFetchBlocks(t *testing.T) {
 	)
 	require.NoError(t, err)
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	t.Run("fetch DutyProposer", func(t *testing.T) {
@@ -385,14 +384,14 @@ func TestFetchSyncContribution(t *testing.T) {
 		sigB = "99e60f20dde4d4872b048d703f1943071c20213d504012e7e520c229da87661803b9f139b9a0c5be31de3cef6821c080125aed38ebaf51ba9a2e9d21d7fbf2903577983109d097a8599610a92c0305408d97c1fd4b0b2d1743fb4eedf5443f99"
 	)
 	// Construct sync committee selections.
-	selectionA := &eth2exp.SyncCommitteeSelection{
+	selectionA := &eth2v1.SyncCommitteeSelection{
 		ValidatorIndex:    vIdxA,
 		Slot:              slot,
 		SubcommitteeIndex: subCommIdxA,
 		SelectionProof:    blsSigFromHex(t, sigA),
 	}
 
-	selectionB := &eth2exp.SyncCommitteeSelection{
+	selectionB := &eth2v1.SyncCommitteeSelection{
 		ValidatorIndex:    vIdxB,
 		Slot:              slot,
 		SubcommitteeIndex: subCommIdxB,
@@ -414,7 +413,7 @@ func TestFetchSyncContribution(t *testing.T) {
 	}
 
 	t.Run("contribution aggregator", func(t *testing.T) {
-		bmock, err := beaconmock.New()
+		bmock, err := beaconmock.New(t.Context())
 		require.NoError(t, err)
 
 		bmock.SyncCommitteeContributionFunc = func(ctx context.Context, resSlot eth2p0.Slot, subcommitteeIndex uint64, beaconBlockRoot eth2p0.Root) (*altair.SyncCommitteeContribution, error) {
@@ -439,7 +438,7 @@ func TestFetchSyncContribution(t *testing.T) {
 				s, ok := selection.(core.SyncCommitteeSelection)
 				require.True(t, ok)
 
-				if s.SubcommitteeIndex == eth2p0.CommitteeIndex(subcommitteeIndex) {
+				if s.SubcommitteeIndex == subcommitteeIndex {
 					signedSelection = s
 				}
 			}
@@ -489,12 +488,12 @@ func TestFetchSyncContribution(t *testing.T) {
 				}
 
 				require.Equal(t, eth2p0.Slot(slot), selection.Slot)
-				require.Equal(t, eth2p0.CommitteeIndex(contribution.SubcommitteeIndex), selection.SubcommitteeIndex)
+				require.Equal(t, contribution.SubcommitteeIndex, selection.SubcommitteeIndex)
 
 				if selection.ValidatorIndex == eth2p0.ValidatorIndex(vIdxA) {
-					require.Equal(t, eth2p0.CommitteeIndex(subCommIdxA), selection.SubcommitteeIndex)
+					require.Equal(t, uint64(subCommIdxA), selection.SubcommitteeIndex)
 				} else if selection.ValidatorIndex == eth2p0.ValidatorIndex(vIdxB) {
-					require.Equal(t, eth2p0.CommitteeIndex(subCommIdxB), selection.SubcommitteeIndex)
+					require.Equal(t, uint64(subCommIdxB), selection.SubcommitteeIndex)
 				}
 
 				message, ok := syncMsgsByPubkey[pubkey].(core.SignedSyncMessage)
@@ -510,7 +509,7 @@ func TestFetchSyncContribution(t *testing.T) {
 	})
 
 	t.Run("not contribution aggregator", func(t *testing.T) {
-		bmock, err := beaconmock.New()
+		bmock, err := beaconmock.New(t.Context())
 		require.NoError(t, err)
 
 		fetch := mustCreateFetcher(t, bmock)
@@ -521,7 +520,7 @@ func TestFetchSyncContribution(t *testing.T) {
 				// Signature corresponding to a non-aggregator. Refer https://github.com/prysmaticlabs/prysm/blob/39a7988e9edbed5b517229b4d66c2a8aab7c7b4d/beacon-chain/sync/validate_sync_contribution_proof_test.go#L336
 				sig := "b9251a82040d4620b8c5665f328ee6c2eaa02d31d71d153f4abba31a7922a981e541e85283f0ced387d26e86aef9386d18c6982b9b5f8759882fe7f25a328180d86e146994ef19d28bc1432baf29751dec12b5f3d65dbbe224d72cf900c6831a"
 				resp := blsSigFromHex(t, sig)
-				selection := &eth2exp.SyncCommitteeSelection{
+				selection := &eth2v1.SyncCommitteeSelection{
 					SelectionProof: resp,
 				}
 
@@ -536,7 +535,7 @@ func TestFetchSyncContribution(t *testing.T) {
 	})
 
 	t.Run("fetch contribution data error", func(t *testing.T) {
-		bmock, err := beaconmock.New()
+		bmock, err := beaconmock.New(t.Context())
 		require.NoError(t, err)
 
 		fetch := mustCreateFetcher(t, bmock)
diff --git a/core/gater_test.go b/core/gater_test.go
index 88a0b87b..a597c44f 100644
--- a/core/gater_test.go
+++ b/core/gater_test.go
@@ -20,6 +20,7 @@ func TestDutyGater(t *testing.T) {
 	// Allow slots 0-3.
 	slotDuration := time.Second
 	bmock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithGenesisTime(now),
 		beaconmock.WithSlotDuration(slotDuration),
 		beaconmock.WithSlotsPerEpoch(2),
diff --git a/core/parsigdb/memory_internal_test.go b/core/parsigdb/memory_internal_test.go
index 97dc7d6c..1e4c0a8b 100644
--- a/core/parsigdb/memory_internal_test.go
+++ b/core/parsigdb/memory_internal_test.go
@@ -6,13 +6,13 @@ import (
 	"context"
 	"testing"
 
+	eth2v1 "github.com/attestantio/go-eth2-client/api/v1"
 	"github.com/attestantio/go-eth2-client/spec/altair"
 	eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
 	"github.com/stretchr/testify/require"
 
 	"github.com/obolnetwork/charon/cluster"
 	"github.com/obolnetwork/charon/core"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 	"github.com/obolnetwork/charon/testutil"
 )
 
@@ -73,7 +73,7 @@ func TestGetThresholdMatching(t *testing.T) {
 				},
 				"Selection": func(i int) core.ParSignedData {
 					// Message is constant
-					msg := &eth2exp.BeaconCommitteeSelection{
+					msg := &eth2v1.BeaconCommitteeSelection{
 						ValidatorIndex: valIdx,
 						Slot:           eth2p0.Slot(test.input[i]), // Vary slot based on input
 						SelectionProof: testutil.RandomEth2Signature(),
diff --git a/core/parsigex/parsigex_test.go b/core/parsigex/parsigex_test.go
index 5feb7a7e..034966f6 100644
--- a/core/parsigex/parsigex_test.go
+++ b/core/parsigex/parsigex_test.go
@@ -127,7 +127,7 @@ func TestParSigExVerifier(t *testing.T) {
 		shareIdx = 1
 	)
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	slotsPerEpoch, err := bmock.SlotsPerEpoch(ctx)
@@ -314,7 +314,7 @@ func TestParSigExVerifier(t *testing.T) {
 
 		data := &altair.SyncAggregatorSelectionData{
 			Slot:              selection.Slot,
-			SubcommitteeIndex: uint64(selection.SubcommitteeIndex),
+			SubcommitteeIndex: selection.SubcommitteeIndex,
 		}
 		sigRoot, err := data.HashTreeRoot()
 		require.NoError(t, err)
diff --git a/core/scheduler/scheduler_internal_test.go b/core/scheduler/scheduler_internal_test.go
index 496a006c..c47c174a 100644
--- a/core/scheduler/scheduler_internal_test.go
+++ b/core/scheduler/scheduler_internal_test.go
@@ -27,6 +27,7 @@ func setupScheduler(t *testing.T) (*Scheduler, validators) {
 
 	// Configure beacon mock.
 	eth2Cl, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithValidatorSet(valSet),
 		beaconmock.WithGenesisTime(t0),
 		beaconmock.WithDeterministicAttesterDuties(0),
diff --git a/core/scheduler/scheduler_test.go b/core/scheduler/scheduler_test.go
index 98c258a0..f02bb3e0 100644
--- a/core/scheduler/scheduler_test.go
+++ b/core/scheduler/scheduler_test.go
@@ -120,7 +120,7 @@ func TestSchedulerWait(t *testing.T) {
 			var t0 time.Time
 
 			clock := newTestClock(t0)
-			eth2Cl, err := beaconmock.New()
+			eth2Cl, err := beaconmock.New(t.Context())
 			require.NoError(t, err)
 
 			eth2Cl.GenesisFunc = func(context.Context, *eth2api.GenesisOpts) (*eth2v1.Genesis, error) {
@@ -196,6 +196,7 @@ func TestSchedulerDuties(t *testing.T) {
 
 			valSet := beaconmock.ValidatorSetA
 			eth2Cl, err := beaconmock.New(
+				t.Context(),
 				beaconmock.WithValidatorSet(valSet),
 				beaconmock.WithGenesisTime(t0),
 				beaconmock.WithDeterministicAttesterDuties(test.Factor),
@@ -309,6 +310,7 @@ func TestScheduler_GetDuty(t *testing.T) {
 
 	// Configure beacon mock.
 	eth2Cl, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithValidatorSet(valSet),
 		beaconmock.WithGenesisTime(t0),
 		beaconmock.WithDeterministicAttesterDuties(0),
@@ -402,6 +404,7 @@ func TestNoActive(t *testing.T) {
 
 	// Configure beacon mock.
 	eth2Cl, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithGenesisTime(t0),
 		beaconmock.WithSlotDuration(slotDuration),
 		beaconmock.WithSlotsPerEpoch(1),
@@ -432,6 +435,7 @@ func TestHandleChainReorgEvent(t *testing.T) {
 
 	// Configure beacon mock.
 	eth2Cl, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithValidatorSet(valSet),
 		beaconmock.WithGenesisTime(t0),
 		beaconmock.WithDeterministicAttesterDuties(1),
diff --git a/core/sigagg/sigagg_test.go b/core/sigagg/sigagg_test.go
index b0d34ec5..2cdb2b0f 100644
--- a/core/sigagg/sigagg_test.go
+++ b/core/sigagg/sigagg_test.go
@@ -35,7 +35,7 @@ func TestSigAgg(t *testing.T) {
 		peers     = 4
 	)
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	t.Run("invalid threshold", func(t *testing.T) {
@@ -82,7 +82,7 @@ func TestSigAgg_DutyAttester(t *testing.T) {
 	msgRoot, err := att.MessageRoot()
 	require.NoError(t, err)
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	epoch, err := att.Epoch(ctx, bmock)
@@ -171,7 +171,7 @@ func TestSigAgg_DutyRandao(t *testing.T) {
 		epoch     = 123
 	)
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	randao := core.NewSignedRandao(epoch, eth2p0.BLSSignature{})
@@ -248,7 +248,7 @@ func TestSigAgg_DutyExit(t *testing.T) {
 		epoch     = 123
 	)
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Generate private shares
@@ -330,7 +330,7 @@ func TestSigAgg_DutyProposer(t *testing.T) {
 		peers     = 4
 	)
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Generate private shares
@@ -469,7 +469,7 @@ func TestSigAgg_DutyBuilderProposer(t *testing.T) {
 		peers     = 4
 	)
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Generate private shares
@@ -592,7 +592,7 @@ func TestSigAgg_DutyBuilderRegistration(t *testing.T) {
 		epoch     = 123
 	)
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Generate private shares
diff --git a/core/tracker/inclusion_internal_test.go b/core/tracker/inclusion_internal_test.go
index 68d08ac8..0bec600c 100644
--- a/core/tracker/inclusion_internal_test.go
+++ b/core/tracker/inclusion_internal_test.go
@@ -8,6 +8,8 @@ import (
 	"slices"
 	"testing"
 
+	eth2api "github.com/attestantio/go-eth2-client/api"
+	eth2v1 "github.com/attestantio/go-eth2-client/api/v1"
 	eth2spec "github.com/attestantio/go-eth2-client/spec"
 	"github.com/attestantio/go-eth2-client/spec/electra"
 	eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
@@ -17,7 +19,6 @@ import (
 	"github.com/obolnetwork/charon/app/eth2wrap"
 	"github.com/obolnetwork/charon/app/featureset"
 	"github.com/obolnetwork/charon/core"
-	"github.com/obolnetwork/charon/eth2util/statecomm"
 	"github.com/obolnetwork/charon/testutil"
 	"github.com/obolnetwork/charon/testutil/beaconmock"
 )
@@ -28,10 +29,10 @@ func TestDuplicateAttData(t *testing.T) {
 	featureset.EnableForT(t, featureset.AttestationInclusion)
 
 	tests := []struct {
-		name                      string
-		attData                   *eth2p0.AttestationData
-		attestationsFunc          func(*eth2p0.AttestationData, bitfield.Bitlist, bitfield.Bitlist, bitfield.Bitlist) []*eth2spec.VersionedAttestation
-		beaconStateCommitteesFunc func(*eth2p0.AttestationData) []*statecomm.StateCommittee
+		name                 string
+		attData              *eth2p0.AttestationData
+		attestationsFunc     func(*eth2p0.AttestationData, bitfield.Bitlist, bitfield.Bitlist, bitfield.Bitlist) []*eth2spec.VersionedAttestation
+		beaconCommitteesFunc func(*eth2p0.AttestationData) []*eth2v1.BeaconCommittee
 	}{
 		{
 			name:    "phase0",
@@ -43,8 +44,8 @@ func TestDuplicateAttData(t *testing.T) {
 					{Version: eth2spec.DataVersionPhase0, Phase0: &eth2p0.Attestation{AggregationBits: aggBits3, Data: attData}},
 				}
 			},
-			beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee {
-				return []*statecomm.StateCommittee{
+			beaconCommitteesFunc: func(attData *eth2p0.AttestationData) []*eth2v1.BeaconCommittee {
+				return []*eth2v1.BeaconCommittee{
 					{Index: attData.Index, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1}},
 				}
 			},
@@ -59,8 +60,8 @@ func TestDuplicateAttData(t *testing.T) {
 					{Version: eth2spec.DataVersionAltair, Altair: &eth2p0.Attestation{AggregationBits: aggBits3, Data: attData}},
 				}
 			},
-			beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee {
-				return []*statecomm.StateCommittee{
+			beaconCommitteesFunc: func(attData *eth2p0.AttestationData) []*eth2v1.BeaconCommittee {
+				return []*eth2v1.BeaconCommittee{
 					{Index: attData.Index, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1}},
 				}
 			},
@@ -75,8 +76,8 @@ func TestDuplicateAttData(t *testing.T) {
 					{Version: eth2spec.DataVersionBellatrix, Bellatrix: &eth2p0.Attestation{AggregationBits: aggBits3, Data: attData}},
 				}
 			},
-			beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee {
-				return []*statecomm.StateCommittee{
+			beaconCommitteesFunc: func(attData *eth2p0.AttestationData) []*eth2v1.BeaconCommittee {
+				return []*eth2v1.BeaconCommittee{
 					{Index: attData.Index, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1}},
 				}
 			},
@@ -91,8 +92,8 @@ func TestDuplicateAttData(t *testing.T) {
 					{Version: eth2spec.DataVersionCapella, Capella: &eth2p0.Attestation{AggregationBits: aggBits3, Data: attData}},
 				}
 			},
-			beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee {
-				return []*statecomm.StateCommittee{
+			beaconCommitteesFunc: func(attData *eth2p0.AttestationData) []*eth2v1.BeaconCommittee {
+				return []*eth2v1.BeaconCommittee{
 					{Index: attData.Index, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1}},
 				}
 			},
@@ -107,8 +108,8 @@ func TestDuplicateAttData(t *testing.T) {
 					{Version: eth2spec.DataVersionDeneb, Deneb: &eth2p0.Attestation{AggregationBits: aggBits3, Data: attData}},
 				}
 			},
-			beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee {
-				return []*statecomm.StateCommittee{
+			beaconCommitteesFunc: func(attData *eth2p0.AttestationData) []*eth2v1.BeaconCommittee {
+				return []*eth2v1.BeaconCommittee{
 					{Index: attData.Index, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1}},
 				}
 			},
@@ -130,8 +131,8 @@ func TestDuplicateAttData(t *testing.T) {
 					{Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: aggBits3, Data: attData, CommitteeBits: twoComm}},
 				}
 			},
-			beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee {
-				return []*statecomm.StateCommittee{
+			beaconCommitteesFunc: func(attData *eth2p0.AttestationData) []*eth2v1.BeaconCommittee {
+				return []*eth2v1.BeaconCommittee{
 					{Index: 0, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1, 2}},
 					{Index: 1, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1, 2}},
 					{Index: 2, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1, 2}},
@@ -154,8 +155,8 @@ func TestDuplicateAttData(t *testing.T) {
 					{Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: aggBits2, Data: attData, CommitteeBits: oneComm}},
 				}
 			},
-			beaconStateCommitteesFunc: func(attData *eth2p0.AttestationData) []*statecomm.StateCommittee {
-				return []*statecomm.StateCommittee{
+			beaconCommitteesFunc: func(attData *eth2p0.AttestationData) []*eth2v1.BeaconCommittee {
+				return []*eth2v1.BeaconCommittee{
 					{Index: 0, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1, 2}},
 					{Index: 1, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1, 2}},
 					{Index: 2, Slot: attData.Slot, Validators: []eth2p0.ValidatorIndex{0, 1, 2}},
@@ -166,7 +167,7 @@ func TestDuplicateAttData(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			bmock, err := beaconmock.New()
+			bmock, err := beaconmock.New(t.Context())
 			require.NoError(t, err)
 
 			// Mock 3 attestations, with same data but different aggregation bits.
@@ -175,12 +176,12 @@ func TestDuplicateAttData(t *testing.T) {
 			aggBits2 := testutil.RandomBitList(8)
 			aggBits3 := testutil.RandomBitList(8)
 
-			bmock.BlockAttestationsFunc = func(_ context.Context, _ string) ([]*eth2spec.VersionedAttestation, error) {
+			bmock.BeaconBlockAttestationsFunc = func(_ context.Context, _ *eth2api.BeaconBlockAttestationsOpts) ([]*eth2spec.VersionedAttestation, error) {
 				return test.attestationsFunc(attData, aggBits1, aggBits2, aggBits3), nil
 			}
 
-			bmock.BeaconStateCommitteesFunc = func(_ context.Context, slot uint64) ([]*statecomm.StateCommittee, error) {
-				return test.beaconStateCommitteesFunc(attData), nil
+			bmock.BeaconCommitteesFunc = func(_ context.Context, opts *eth2api.BeaconCommitteesOpts) ([]*eth2v1.BeaconCommittee, error) {
+				return test.beaconCommitteesFunc(attData), nil
 			}
 
 			noopTrackerInclFunc := func(duty core.Duty, key core.PubKey, data core.SignedData, err error) {}
diff --git a/core/validatorapi/router_internal_test.go b/core/validatorapi/router_internal_test.go
index 744161ef..524419e3 100644
--- a/core/validatorapi/router_internal_test.go
+++ b/core/validatorapi/router_internal_test.go
@@ -40,7 +40,6 @@ import (
 
 	"github.com/obolnetwork/charon/app/errors"
 	"github.com/obolnetwork/charon/app/eth2wrap"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 	"github.com/obolnetwork/charon/testutil"
 )
 
@@ -1392,23 +1391,23 @@ func TestRouter(t *testing.T) {
 	})
 
 	t.Run("aggregate sync committee selections", func(t *testing.T) {
-		selections := []*eth2exp.SyncCommitteeSelection{testutil.RandomSyncCommitteeSelection(), testutil.RandomSyncCommitteeSelection()}
+		selections := []*eth2v1.SyncCommitteeSelection{testutil.RandomSyncCommitteeSelection(), testutil.RandomSyncCommitteeSelection()}
 
 		handler := testHandler{
-			AggregateSyncCommitteeSelectionsFunc: func(ctx context.Context, partialSelections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) {
+			SyncCommitteeSelectionsFunc: func(ctx context.Context, opts *eth2api.SyncCommitteeSelectionsOpts) (*eth2api.Response[[]*eth2v1.SyncCommitteeSelection], error) {
 				for i := range selections {
-					require.Equal(t, selections[i], partialSelections[i])
+					require.Equal(t, selections[i], opts.Selections[i])
 				}
 
-				return partialSelections, nil
+				return wrapResponse(opts.Selections), nil
 			},
 		}
 
 		callback := func(ctx context.Context, cl *eth2http.Service) {
 			eth2Cl := eth2wrap.AdaptEth2HTTP(cl, nil, time.Second)
-			actual, err := eth2Cl.AggregateSyncCommitteeSelections(ctx, selections)
+			actual, err := eth2Cl.SyncCommitteeSelections(ctx, &eth2api.SyncCommitteeSelectionsOpts{Selections: selections})
 			require.NoError(t, err)
-			require.Equal(t, selections, actual)
+			require.Equal(t, selections, actual.Data)
 		}
 
 		testRouter(t, handler, callback)
@@ -1447,8 +1446,8 @@ func TestBeaconCommitteeSelections(t *testing.T) {
 	)
 
 	handler := testHandler{
-		AggregateBeaconCommitteeSelectionsFunc: func(ctx context.Context, selections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) {
-			return selections, nil
+		BeaconCommitteeSelectionsFunc: func(ctx context.Context, opts *eth2api.BeaconCommitteeSelectionsOpts) (*eth2api.Response[[]*eth2v1.BeaconCommitteeSelection], error) {
+			return wrapResponse(opts.Selections), nil
 		},
 	}
 
@@ -1469,7 +1468,7 @@ func TestBeaconCommitteeSelections(t *testing.T) {
 	)
 	require.NoError(t, err)
 
-	selections := []*eth2exp.BeaconCommitteeSelection{
+	selections := []*eth2v1.BeaconCommitteeSelection{
 		{
 			Slot:           slotA,
 			ValidatorIndex: vIdxA,
@@ -1488,9 +1487,9 @@ func TestBeaconCommitteeSelections(t *testing.T) {
 	}
 
 	eth2Cl := eth2wrap.AdaptEth2HTTP(eth2Svc.(*eth2http.Service), nil, time.Second)
-	actual, err := eth2Cl.AggregateBeaconCommitteeSelections(ctx, selections)
+	actual, err := eth2Cl.BeaconCommitteeSelections(ctx, &eth2api.BeaconCommitteeSelectionsOpts{Selections: selections})
 	require.NoError(t, err)
-	require.Equal(t, selections, actual)
+	require.Equal(t, selections, actual.Data)
 }
 
 func TestSubmitAggregateAttestations(t *testing.T) {
@@ -1990,26 +1989,26 @@ type testHandler struct {
 	Handler
 	eth2client.BeaconStateProvider
 
-	ProxyHandler                           http.HandlerFunc
-	AggregateSyncCommitteeSelectionsFunc   func(ctx context.Context, partialSelections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error)
-	AttestationDataFunc                    func(ctx context.Context, opts *eth2api.AttestationDataOpts) (*eth2api.Response[*eth2p0.AttestationData], error)
-	AttesterDutiesFunc                     func(ctx context.Context, opts *eth2api.AttesterDutiesOpts) (*eth2api.Response[[]*eth2v1.AttesterDuty], error)
-	SubmitAttestationsFunc                 func(ctx context.Context, opts *eth2api.SubmitAttestationsOpts) error
-	ProposalFunc                           func(ctx context.Context, opts *eth2api.ProposalOpts) (*eth2api.Response[*eth2api.VersionedProposal], error)
-	SubmitProposalFunc                     func(ctx context.Context, proposal *eth2api.SubmitProposalOpts) error
-	SubmitBlindedProposalFunc              func(ctx context.Context, proposal *eth2api.SubmitBlindedProposalOpts) error
-	ProposerDutiesFunc                     func(ctx context.Context, opts *eth2api.ProposerDutiesOpts) (*eth2api.Response[[]*eth2v1.ProposerDuty], error)
-	NodeVersionFunc                        func(ctx context.Context, opts *eth2api.NodeVersionOpts) (*eth2api.Response[string], error)
-	ValidatorsFunc                         func(ctx context.Context, opts *eth2api.ValidatorsOpts) (*eth2api.Response[map[eth2p0.ValidatorIndex]*eth2v1.Validator], error)
-	BeaconStateFunc                        func(ctx context.Context, stateId string) (*eth2spec.VersionedBeaconState, error)
-	ValidatorsByPubKeyFunc                 func(ctx context.Context, stateID string, pubkeys []eth2p0.BLSPubKey) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error)
-	SubmitVoluntaryExitFunc                func(ctx context.Context, exit *eth2p0.SignedVoluntaryExit) error
-	SubmitValidatorRegistrationsFunc       func(ctx context.Context, registrations []*eth2api.VersionedSignedValidatorRegistration) error
-	AggregateBeaconCommitteeSelectionsFunc func(ctx context.Context, selections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error)
-	SubmitAggregateAttestationsFunc        func(ctx context.Context, opts *eth2api.SubmitAggregateAttestationsOpts) error
-	SubmitSyncCommitteeMessagesFunc        func(ctx context.Context, messages []*altair.SyncCommitteeMessage) error
-	SyncCommitteeDutiesFunc                func(ctx context.Context, opts *eth2api.SyncCommitteeDutiesOpts) (*eth2api.Response[[]*eth2v1.SyncCommitteeDuty], error)
-	SyncCommitteeContributionFunc          func(ctx context.Context, opts *eth2api.SyncCommitteeContributionOpts) (*eth2api.Response[*altair.SyncCommitteeContribution], error)
+	ProxyHandler                     http.HandlerFunc
+	SyncCommitteeSelectionsFunc      func(ctx context.Context, opts *eth2api.SyncCommitteeSelectionsOpts) (*eth2api.Response[[]*eth2v1.SyncCommitteeSelection], error)
+	AttestationDataFunc              func(ctx context.Context, opts *eth2api.AttestationDataOpts) (*eth2api.Response[*eth2p0.AttestationData], error)
+	AttesterDutiesFunc               func(ctx context.Context, opts *eth2api.AttesterDutiesOpts) (*eth2api.Response[[]*eth2v1.AttesterDuty], error)
+	SubmitAttestationsFunc           func(ctx context.Context, opts *eth2api.SubmitAttestationsOpts) error
+	ProposalFunc                     func(ctx context.Context, opts *eth2api.ProposalOpts) (*eth2api.Response[*eth2api.VersionedProposal], error)
+	SubmitProposalFunc               func(ctx context.Context, proposal *eth2api.SubmitProposalOpts) error
+	SubmitBlindedProposalFunc        func(ctx context.Context, proposal *eth2api.SubmitBlindedProposalOpts) error
+	ProposerDutiesFunc               func(ctx context.Context, opts *eth2api.ProposerDutiesOpts) (*eth2api.Response[[]*eth2v1.ProposerDuty], error)
+	NodeVersionFunc                  func(ctx context.Context, opts *eth2api.NodeVersionOpts) (*eth2api.Response[string], error)
+	ValidatorsFunc                   func(ctx context.Context, opts *eth2api.ValidatorsOpts) (*eth2api.Response[map[eth2p0.ValidatorIndex]*eth2v1.Validator], error)
+	BeaconStateFunc                  func(ctx context.Context, stateId string) (*eth2spec.VersionedBeaconState, error)
+	ValidatorsByPubKeyFunc           func(ctx context.Context, stateID string, pubkeys []eth2p0.BLSPubKey) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error)
+	SubmitVoluntaryExitFunc          func(ctx context.Context, exit *eth2p0.SignedVoluntaryExit) error
+	SubmitValidatorRegistrationsFunc func(ctx context.Context, registrations []*eth2api.VersionedSignedValidatorRegistration) error
+	BeaconCommitteeSelectionsFunc    func(ctx context.Context, opts *eth2api.BeaconCommitteeSelectionsOpts) (*eth2api.Response[[]*eth2v1.BeaconCommitteeSelection], error)
+	SubmitAggregateAttestationsFunc  func(ctx context.Context, opts *eth2api.SubmitAggregateAttestationsOpts) error
+	SubmitSyncCommitteeMessagesFunc  func(ctx context.Context, messages []*altair.SyncCommitteeMessage) error
+	SyncCommitteeDutiesFunc          func(ctx context.Context, opts *eth2api.SyncCommitteeDutiesOpts) (*eth2api.Response[[]*eth2v1.SyncCommitteeDuty], error)
+	SyncCommitteeContributionFunc    func(ctx context.Context, opts *eth2api.SyncCommitteeContributionOpts) (*eth2api.Response[*altair.SyncCommitteeContribution], error)
 }
 
 func (h testHandler) AttestationData(ctx context.Context, opts *eth2api.AttestationDataOpts) (*eth2api.Response[*eth2p0.AttestationData], error) {
@@ -2068,8 +2067,8 @@ func (h testHandler) SubmitValidatorRegistrations(ctx context.Context, registrat
 	return h.SubmitValidatorRegistrationsFunc(ctx, registrations)
 }
 
-func (h testHandler) AggregateBeaconCommitteeSelections(ctx context.Context, selections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) {
-	return h.AggregateBeaconCommitteeSelectionsFunc(ctx, selections)
+func (h testHandler) BeaconCommitteeSelections(ctx context.Context, opts *eth2api.BeaconCommitteeSelectionsOpts) (*eth2api.Response[[]*eth2v1.BeaconCommitteeSelection], error) {
+	return h.BeaconCommitteeSelectionsFunc(ctx, opts)
 }
 
 func (h testHandler) SubmitAggregateAttestations(ctx context.Context, opts *eth2api.SubmitAggregateAttestationsOpts) error {
@@ -2088,8 +2087,8 @@ func (h testHandler) SyncCommitteeContribution(ctx context.Context, opts *eth2ap
 	return h.SyncCommitteeContributionFunc(ctx, opts)
 }
 
-func (h testHandler) AggregateSyncCommitteeSelections(ctx context.Context, partialSelections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) {
-	return h.AggregateSyncCommitteeSelectionsFunc(ctx, partialSelections)
+func (h testHandler) SyncCommitteeSelections(ctx context.Context, opts *eth2api.SyncCommitteeSelectionsOpts) (*eth2api.Response[[]*eth2v1.SyncCommitteeSelection], error) {
+	return h.SyncCommitteeSelectionsFunc(ctx, opts)
 }
 
 // newBeaconHandler returns a mock beacon node handler. It registers a few mock handlers required by the
diff --git a/core/validatorapi/validatorapi_test.go b/core/validatorapi/validatorapi_test.go
index fa268a29..4b7bcd86 100644
--- a/core/validatorapi/validatorapi_test.go
+++ b/core/validatorapi/validatorapi_test.go
@@ -7,7 +7,6 @@ import (
 	"fmt"
 	"maps"
 	"sort"
-	"strconv"
 	"sync"
 	"testing"
 	"time"
@@ -33,7 +32,6 @@ import (
 	"github.com/obolnetwork/charon/core"
 	"github.com/obolnetwork/charon/core/validatorapi"
 	"github.com/obolnetwork/charon/eth2util"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 	"github.com/obolnetwork/charon/eth2util/signing"
 	"github.com/obolnetwork/charon/tbls"
 	"github.com/obolnetwork/charon/tbls/tblsconv"
@@ -145,7 +143,7 @@ func TestComponent_ValidSubmitAttestations(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			ctx := t.Context()
-			eth2Cl, err := beaconmock.New()
+			eth2Cl, err := beaconmock.New(t.Context())
 			require.NoError(t, err)
 
 			vPKA := testutil.RandomCorePubKey(t)
@@ -217,7 +215,7 @@ func TestComponent_ValidSubmitAttestations(t *testing.T) {
 
 func TestComponent_InvalidSubmitAttestations(t *testing.T) {
 	ctx := context.Background()
-	eth2Cl, err := beaconmock.New()
+	eth2Cl, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	const (
@@ -287,6 +285,7 @@ func TestSubmitAttestations_Verify(t *testing.T) {
 
 	// Configure beacon mock
 	bmock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithValidatorSet(beaconmock.ValidatorSet{vIdx: validator}),
 		beaconmock.WithDeterministicAttesterDuties(0), // All duties in first slot of epoch.
 	)
@@ -349,6 +348,7 @@ func TestSignAndVerify(t *testing.T) {
         	"epoch": "0"
       	}]}`
 	bmock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithEndpoint("/eth/v1/config/fork_schedule", forkSchedule),
 		beaconmock.WithGenesisValidatorsRoot([32]byte{0x01, 0x02}))
 	require.NoError(t, err)
@@ -472,7 +472,7 @@ func padTo(b []byte, size int) []byte {
 
 func TestComponent_Proposal(t *testing.T) {
 	ctx := context.Background()
-	eth2Cl, err := beaconmock.New()
+	eth2Cl, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	const (
@@ -564,7 +564,7 @@ func TestComponent_SubmitProposalsWithWrongVCData(t *testing.T) {
 	allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls
 
 	// Configure beacon mock
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Construct the validator api component
@@ -781,7 +781,7 @@ func TestComponent_SubmitProposal(t *testing.T) {
 			allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls
 
 			// Configure beacon mock
-			bmock, err := beaconmock.New()
+			bmock, err := beaconmock.New(t.Context())
 			require.NoError(t, err)
 
 			// Construct the validator api component
@@ -862,7 +862,7 @@ func TestComponent_SubmitProposal(t *testing.T) {
 // 	allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls
 
 // 	// Configure beacon mock
-// 	bmock, err := beaconmock.New()
+// 	bmock, err := beaconmock.New(t.Context())
 // 	require.NoError(t, err)
 
 // Construct the validator api component
@@ -960,7 +960,7 @@ func TestComponent_SubmitProposalInvalidSignature(t *testing.T) {
 	allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls
 
 	// Configure beacon mock
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Construct the validator api component
@@ -1031,7 +1031,7 @@ func TestComponent_SubmitProposalInvalidBlock(t *testing.T) {
 	allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{pubkey: {shareIdx: tblsPubkey}} // Maps self to self since not tbls
 
 	// Configure beacon mock
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Construct the validator api component
@@ -1262,7 +1262,7 @@ func TestComponent_SubmitBlindedProposal(t *testing.T) {
 			allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls
 
 			// Configure beacon mock
-			bmock, err := beaconmock.New()
+			bmock, err := beaconmock.New(t.Context())
 			require.NoError(t, err)
 
 			// Construct the validator api component
@@ -1340,7 +1340,7 @@ func TestComponent_SubmitBlindedProposalInvalidSignature(t *testing.T) {
 	allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls
 
 	// Configure beacon mock
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Construct the validator api component
@@ -1414,7 +1414,7 @@ func TestComponent_SubmitBlindedProposalInvalidBlock(t *testing.T) {
 	allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{pubkey: {shareIdx: *(*tbls.PublicKey)(pkb)}} // Maps self to self since not tbls
 
 	// Configure beacon mock
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Construct the validator api component
@@ -1545,7 +1545,7 @@ func TestComponent_SubmitVoluntaryExit(t *testing.T) {
 	require.NoError(t, err)
 
 	// Configure beacon mock
-	bmock, err := beaconmock.New(beaconmock.WithValidatorSet(beaconmock.ValidatorSetA))
+	bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(beaconmock.ValidatorSetA))
 	require.NoError(t, err)
 
 	// Construct the validator api component
@@ -1617,7 +1617,7 @@ func TestComponent_SubmitVoluntaryExitInvalidSignature(t *testing.T) {
 	require.NoError(t, err)
 
 	// Configure beacon mock
-	bmock, err := beaconmock.New(beaconmock.WithValidatorSet(beaconmock.ValidatorSetA))
+	bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(beaconmock.ValidatorSetA))
 	require.NoError(t, err)
 
 	// Construct the validator api component
@@ -1662,7 +1662,7 @@ func TestComponent_Duties(t *testing.T) {
 
 	allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubshare}}
 	// Configure beacon mock
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	t.Run("proposer_duties", func(t *testing.T) {
@@ -1765,7 +1765,7 @@ func TestComponent_SubmitValidatorRegistration(t *testing.T) {
 	allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls
 
 	// Configure beacon mock
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Construct the validator api component
@@ -1848,7 +1848,7 @@ func TestComponent_SubmitValidatorRegistrationInvalidSignature(t *testing.T) {
 	allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls
 
 	// Configure beacon mock
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Construct the validator api component
@@ -1884,93 +1884,19 @@ func TestComponent_SubmitValidatorRegistrationInvalidSignature(t *testing.T) {
 	require.ErrorContains(t, err, "signature not verified")
 }
 
-func TestComponent_TekuProposerConfig(t *testing.T) {
-	ctx := context.Background()
-
-	const (
-		zeroAddr     = "0x0000000000000000000000000000000000000000"
-		feeRecipient = "0x123456"
-		shareIdx     = 1
-	)
-	// Create keys (just use normal keys, not split tbls)
-	secret, err := tbls.GenerateSecretKey()
-	require.NoError(t, err)
-
-	pubkey, err := tbls.SecretToPublicKey(secret)
-	require.NoError(t, err)
-
-	// Convert pubkey
-	corePubKey, err := core.PubKeyFromBytes(pubkey[:])
-	require.NoError(t, err)
-
-	allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls
-
-	// Configure beacon mock
-	bmock, err := beaconmock.New()
-	require.NoError(t, err)
-
-	// Construct the validator api component
-	vapi, err := validatorapi.NewComponent(bmock, allPubSharesByKey, shareIdx, func(core.PubKey) string {
-		return feeRecipient
-	}, true, 30000000, nil)
-	require.NoError(t, err)
-
-	resp, err := vapi.ProposerConfig(ctx)
-	require.NoError(t, err)
-
-	pk, err := core.PubKeyFromBytes(pubkey[:])
-	require.NoError(t, err)
-
-	genesis, err := bmock.Genesis(ctx, &eth2api.GenesisOpts{})
-	require.NoError(t, err)
-
-	genesisTime := genesis.Data.GenesisTime
-	respSpec, err := bmock.Spec(ctx, &eth2api.SpecOpts{})
-	require.NoError(t, err)
-
-	slotDuration, ok := respSpec.Data["SECONDS_PER_SLOT"].(time.Duration)
-	require.True(t, ok)
-
-	eth2pk, err := pk.ToETH2()
-	require.NoError(t, err)
-
-	require.Equal(t, &eth2exp.ProposerConfigResponse{
-		Proposers: map[eth2p0.BLSPubKey]eth2exp.ProposerConfig{
-			eth2pk: {
-				FeeRecipient: feeRecipient,
-				Builder: eth2exp.Builder{
-					Enabled:  true,
-					GasLimit: 30000000,
-					Overrides: map[string]string{
-						"timestamp":  strconv.FormatInt(genesisTime.Add(slotDuration).Unix(), 10),
-						"public_key": string(pk),
-					},
-				},
-			},
-		},
-		Default: eth2exp.ProposerConfig{
-			FeeRecipient: zeroAddr,
-			Builder: eth2exp.Builder{
-				Enabled:  false,
-				GasLimit: 30000000,
-			},
-		},
-	}, resp)
-}
-
 func TestComponent_AggregateBeaconCommitteeSelections(t *testing.T) {
 	ctx := context.Background()
 
 	const slot = 99
 
 	valSet := beaconmock.ValidatorSetA
-	eth2Cl, err := beaconmock.New(beaconmock.WithValidatorSet(valSet))
+	eth2Cl, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(valSet))
 	require.NoError(t, err)
 
 	vapi, err := validatorapi.NewComponentInsecure(t, eth2Cl, 0)
 	require.NoError(t, err)
 
-	selections := []*eth2exp.BeaconCommitteeSelection{
+	selections := []*eth2v1.BeaconCommitteeSelection{
 		{
 			ValidatorIndex: valSet[1].Index,
 			Slot:           slot,
@@ -2003,9 +1929,11 @@ func TestComponent_AggregateBeaconCommitteeSelections(t *testing.T) {
 		return nil, errors.New("unknown public key")
 	})
 
-	actual, err := vapi.AggregateBeaconCommitteeSelections(ctx, selections)
+	eth2Resp, err := vapi.BeaconCommitteeSelections(ctx, &eth2api.BeaconCommitteeSelectionsOpts{Selections: selections})
 	require.NoError(t, err)
 
+	actual := eth2Resp.Data
+
 	// Sort by VIdx before comparing
 	sort.Slice(actual, func(i, j int) bool {
 		return actual[i].ValidatorIndex < actual[j].ValidatorIndex
@@ -2035,7 +1963,7 @@ func TestComponent_SubmitAggregateAttestations(t *testing.T) {
 
 	pubkey := beaconmock.ValidatorSetA[vIdx].Validator.PublicKey
 
-	bmock, err := beaconmock.New(beaconmock.WithValidatorSet(beaconmock.ValidatorSetA))
+	bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(beaconmock.ValidatorSetA))
 	require.NoError(t, err)
 
 	vapi, err := validatorapi.NewComponentInsecure(t, bmock, 0)
@@ -2069,7 +1997,7 @@ func TestComponent_SubmitSyncCommitteeMessages(t *testing.T) {
 
 	msg.ValidatorIndex = vIdx
 
-	bmock, err := beaconmock.New(beaconmock.WithValidatorSet(beaconmock.ValidatorSetA))
+	bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(beaconmock.ValidatorSetA))
 	require.NoError(t, err)
 
 	vapi, err := validatorapi.NewComponentInsecure(t, bmock, 0)
@@ -2107,7 +2035,7 @@ func TestComponent_SubmitSyncCommitteeContributions(t *testing.T) {
 
 	contrib.Message.AggregatorIndex = vIdx
 
-	bmock, err := beaconmock.New(beaconmock.WithValidatorSet(beaconmock.ValidatorSetA))
+	bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(beaconmock.ValidatorSetA))
 	require.NoError(t, err)
 
 	vapi, err := validatorapi.NewComponentInsecure(t, bmock, 0)
@@ -2139,7 +2067,7 @@ func TestComponent_SubmitSyncCommitteeContributionsVerify(t *testing.T) {
 		ctx        = context.Background()
 		val        = testutil.RandomValidator(t)
 		slot       = eth2p0.Slot(50)
-		subcommIdx = eth2p0.CommitteeIndex(1)
+		subcommIdx = uint64(1)
 	)
 
 	// Create keys (just use normal keys, not split tbls).
@@ -2156,7 +2084,7 @@ func TestComponent_SubmitSyncCommitteeContributionsVerify(t *testing.T) {
 
 	val.Validator.PublicKey = eth2p0.BLSPubKey(pubkey)
 
-	bmock, err := beaconmock.New(beaconmock.WithValidatorSet(beaconmock.ValidatorSet{val.Index: val}))
+	bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(beaconmock.ValidatorSet{val.Index: val}))
 	require.NoError(t, err)
 
 	// Create contribution and proof.
@@ -2165,7 +2093,7 @@ func TestComponent_SubmitSyncCommitteeContributionsVerify(t *testing.T) {
 		Contribution:    testutil.RandomSyncCommitteeContribution(),
 	}
 	contribAndProof.Contribution.Slot = slot
-	contribAndProof.Contribution.SubcommitteeIndex = uint64(subcommIdx)
+	contribAndProof.Contribution.SubcommitteeIndex = subcommIdx
 	contribAndProof.SelectionProof = syncCommSelectionProof(t, bmock, secret, slot, subcommIdx)
 
 	signedContribAndProof := &altair.SignedContributionAndProof{
@@ -2211,7 +2139,7 @@ func TestComponent_ValidatorCache(t *testing.T) {
 		valByPubkey[val.Validator.PublicKey] = val
 	}
 
-	bmock, err := beaconmock.New(beaconmock.WithValidatorSet(baseValSet))
+	bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(baseValSet))
 	require.NoError(t, err)
 
 	bmock.CachedValidatorsFunc = func(ctx context.Context) (eth2wrap.ActiveValidators, eth2wrap.CompleteValidators, error) {
@@ -2326,7 +2254,7 @@ func TestComponent_GetAllValidators(t *testing.T) {
 		}
 	}
 
-	bmock, err := beaconmock.New(beaconmock.WithValidatorSet(validatorSet))
+	bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(validatorSet))
 	require.NoError(t, err)
 
 	// Construct validatorapi component.
@@ -2365,7 +2293,7 @@ func TestComponent_GetClusterValidatorsWithError(t *testing.T) {
 		indices = append(indices, vidx)
 	}
 
-	bmock, err := beaconmock.New(beaconmock.WithValidatorSet(validatorSet))
+	bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(validatorSet))
 	require.NoError(t, err)
 
 	// Construct validatorapi component.
@@ -2426,13 +2354,13 @@ func TestComponent_AggregateSyncCommitteeSelectionsVerify(t *testing.T) {
 	selection2.Slot = slot
 
 	// Construct beaconmock.
-	bmock, err := beaconmock.New(beaconmock.WithValidatorSet(valSet))
+	bmock, err := beaconmock.New(t.Context(), beaconmock.WithValidatorSet(valSet))
 	require.NoError(t, err)
 
 	selection1.SelectionProof = syncCommSelectionProof(t, bmock, secret1, slot, selection1.SubcommitteeIndex)
 	selection2.SelectionProof = syncCommSelectionProof(t, bmock, secret2, slot, selection2.SubcommitteeIndex)
 
-	selections := []*eth2exp.SyncCommitteeSelection{selection1, selection2}
+	selections := []*eth2v1.SyncCommitteeSelection{selection1, selection2}
 
 	// Populate all pubshares map.
 	corePubKey1, err := core.PubKeyFromBytes(pubkey1[:])
@@ -2485,9 +2413,11 @@ func TestComponent_AggregateSyncCommitteeSelectionsVerify(t *testing.T) {
 		return nil
 	})
 
-	got, err := vapi.AggregateSyncCommitteeSelections(ctx, selections)
+	eth2Resp, err := vapi.SyncCommitteeSelections(ctx, &eth2api.SyncCommitteeSelectionsOpts{Selections: selections})
 	require.NoError(t, err)
 
+	got := eth2Resp.Data
+
 	// Sort by VIdx before comparing.
 	sort.Slice(got, func(i, j int) bool {
 		return got[i].ValidatorIndex < got[j].ValidatorIndex
@@ -2498,7 +2428,7 @@ func TestComponent_AggregateSyncCommitteeSelectionsVerify(t *testing.T) {
 
 // syncCommSelectionProof returns the selection_proof corresponding to the provided altair.ContributionAndProof.
 // Refer get_sync_committee_selection_proof from https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/validator.md#aggregation-selection.
-func syncCommSelectionProof(t *testing.T, eth2Cl eth2wrap.Client, secret tbls.PrivateKey, slot eth2p0.Slot, subcommIdx eth2p0.CommitteeIndex) eth2p0.BLSSignature {
+func syncCommSelectionProof(t *testing.T, eth2Cl eth2wrap.Client, secret tbls.PrivateKey, slot eth2p0.Slot, subcommIdx uint64) eth2p0.BLSSignature {
 	t.Helper()
 
 	epoch, err := eth2util.EpochFromSlot(context.Background(), eth2Cl, slot)
@@ -2506,7 +2436,7 @@ func syncCommSelectionProof(t *testing.T, eth2Cl eth2wrap.Client, secret tbls.Pr
 
 	data := altair.SyncAggregatorSelectionData{
 		Slot:              slot,
-		SubcommitteeIndex: uint64(subcommIdx),
+		SubcommitteeIndex: subcommIdx,
 	}
 
 	sigRoot, err := data.HashTreeRoot()
@@ -2623,7 +2553,7 @@ func TestSlotFromTimestamp(t *testing.T) {
 			require.NoError(t, err)
 
 			ctx := context.Background()
-			eth2Cl, err := beaconmock.New(beaconmock.WithGenesisTime(genesis))
+			eth2Cl, err := beaconmock.New(t.Context(), beaconmock.WithGenesisTime(genesis))
 			require.NoError(t, err)
 
 			got, err := validatorapi.SlotFromTimestamp(ctx, eth2Cl, tt.timestamp)
diff --git a/dkg/dkg_test.go b/dkg/dkg_test.go
index 55c3093b..1d5f1fdc 100644
--- a/dkg/dkg_test.go
+++ b/dkg/dkg_test.go
@@ -57,13 +57,7 @@ const (
 )
 
 func isAnyVersion(version string, list ...string) bool {
-	for _, v := range list {
-		if version == v {
-			return true
-		}
-	}
-
-	return false
+	return slices.Contains(list, version)
 }
 
 func TestDKG(t *testing.T) {
diff --git a/eth2util/eth2exp/attagg_test.go b/eth2util/eth2exp/attagg_test.go
index e2d4960b..748c1686 100644
--- a/eth2util/eth2exp/attagg_test.go
+++ b/eth2util/eth2exp/attagg_test.go
@@ -18,7 +18,7 @@ import (
 func TestIsAttAggregator(t *testing.T) {
 	ctx := context.Background()
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// https://github.com/prysmaticlabs/prysm/blob/8627fe72e80009ae162430140bcfff6f209d7a32/beacon-chain/core/helpers/attestation_test.go#L28
@@ -47,7 +47,7 @@ func TestIsAttAggregator(t *testing.T) {
 func TestIsSyncCommAggregator(t *testing.T) {
 	ctx := context.Background()
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// The non-aggregator tests (isAgg: false) are taken from https://github.com/prysmaticlabs/prysm/blob/39a7988e9edbed5b517229b4d66c2a8aab7c7b4d/beacon-chain/sync/validate_sync_contribution_proof_test.go#L336.
diff --git a/eth2util/keystore/keystore_test.go b/eth2util/keystore/keystore_test.go
index 3cb45967..e7d37c37 100644
--- a/eth2util/keystore/keystore_test.go
+++ b/eth2util/keystore/keystore_test.go
@@ -288,6 +288,55 @@ func TestSequencedKeys(t *testing.T) {
 	}
 }
 
+func TestLoadFilesRecursively(t *testing.T) {
+	dir := t.TempDir()
+
+	// Create a nested directory structure with keystore files.
+	nestedDir := filepath.Join(dir, "nested")
+	require.NoError(t, os.Mkdir(nestedDir, 0o755))
+
+	// Store keys in the root & nested directories.
+	pk1 := storeNewKeyForT(t, filepath.Join(dir, "keystore-alpha.json"))
+	pk2 := storeNewKeyForT(t, filepath.Join(nestedDir, "keystore-bravo.json"))
+
+	keyFiles, err := keystore.LoadFilesRecursively(dir)
+	require.NoError(t, err)
+
+	require.Len(t, keyFiles, 2)
+
+	// Check if both keys are loaded correctly.
+	for i := 0; i < 2; i++ {
+		isPk1 := bytes.Equal(keyFiles[i].PrivateKey[:], pk1[:])
+		isPk2 := bytes.Equal(keyFiles[i].PrivateKey[:], pk2[:])
+		require.True(t, isPk1 || isPk2, "Loaded key does not match expected keys")
+	}
+
+	require.NotEqual(t, keyFiles[0].PrivateKey, keyFiles[1].PrivateKey)
+	require.NotEqual(t, keyFiles[0].FileIndex, keyFiles[1].FileIndex)
+
+	t.Run("shuffle password files", func(t *testing.T) {
+		alphaPassword, err := os.ReadFile(filepath.Join(dir, "keystore-alpha.txt"))
+		require.NoError(t, err)
+		bravoPassword, err := os.ReadFile(filepath.Join(nestedDir, "keystore-bravo.txt"))
+		require.NoError(t, err)
+
+		err = os.Remove(filepath.Join(dir, "keystore-alpha.txt"))
+		require.NoError(t, err)
+		err = os.Remove(filepath.Join(nestedDir, "keystore-bravo.txt"))
+		require.NoError(t, err)
+
+		err = os.WriteFile(filepath.Join(dir, "keystore-alpha.txt"), bravoPassword, 0o444)
+		require.NoError(t, err)
+		err = os.WriteFile(filepath.Join(nestedDir, "keystore-bravo.txt"), alphaPassword, 0o444)
+		require.NoError(t, err)
+
+		keyFiles, err := keystore.LoadFilesRecursively(dir)
+		require.NoError(t, err)
+
+		require.Len(t, keyFiles, 2)
+	})
+}
+
 // storeNewKeyForT generates a new key and stores it in the given target filename
 // it also stores the corresponding txt password next to it.
 // It also returns the generated key.
diff --git a/eth2util/registration/registration_test.go b/eth2util/registration/registration_test.go
index 4d4f2776..6fa54d11 100644
--- a/eth2util/registration/registration_test.go
+++ b/eth2util/registration/registration_test.go
@@ -136,7 +136,7 @@ func TestVerifySignedRegistration(t *testing.T) {
 	})
 	require.NoError(t, err)
 
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	epoch, err := signedRegistration.Epoch(ctx, bmock)
diff --git a/eth2util/signing/signing_test.go b/eth2util/signing/signing_test.go
index 62a6e306..01a499f8 100644
--- a/eth2util/signing/signing_test.go
+++ b/eth2util/signing/signing_test.go
@@ -22,7 +22,7 @@ import (
 )
 
 func TestVerifyRegistrationReference(t *testing.T) {
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	// Test data obtained from teku.
@@ -89,7 +89,7 @@ func TestConstantApplicationBuilder(t *testing.T) {
 	getDomain := func(t *testing.T, i int) eth2p0.Domain {
 		t.Helper()
 
-		eth2Cl, err := beaconmock.New()
+		eth2Cl, err := beaconmock.New(t.Context())
 		require.NoError(t, err)
 
 		eth2Cl.ForkScheduleFunc = func(ctx context.Context, opts *eth2api.ForkScheduleOpts) ([]*eth2p0.Fork, error) {
diff --git a/testutil/beaconmock/beaconmock.go b/testutil/beaconmock/beaconmock.go
index 3ebebe63..645240f5 100644
--- a/testutil/beaconmock/beaconmock.go
+++ b/testutil/beaconmock/beaconmock.go
@@ -38,8 +38,6 @@ import (
 
 	"github.com/obolnetwork/charon/app/errors"
 	"github.com/obolnetwork/charon/app/eth2wrap"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
-	"github.com/obolnetwork/charon/eth2util/statecomm"
 )
 
 // Interface assertions.
@@ -49,7 +47,7 @@ var (
 )
 
 // New returns a new beacon client mock configured with the default and provided options.
-func New(opts ...Option) (Mock, error) {
+func New(ctx context.Context, opts ...Option) (Mock, error) {
 	// Configure http mock first.
 	temp := defaultHTTPMock()
 	for _, opt := range opts {
@@ -58,7 +56,7 @@ func New(opts ...Option) (Mock, error) {
 
 	headProducer := newHeadProducer()
 
-	httpMock, httpServer, err := newHTTPMock(headProducer.Handlers(), temp.overrides...)
+	httpMock, httpServer, err := newHTTPMock(ctx, headProducer.Handlers(), temp.overrides...)
 	if err != nil {
 		return Mock{}, err
 	}
@@ -193,10 +191,10 @@ type Mock struct {
 	CachedValidatorsFunc                   func(ctx context.Context) (eth2wrap.ActiveValidators, eth2wrap.CompleteValidators, error)
 	AttestationDataFunc                    func(context.Context, eth2p0.Slot, eth2p0.CommitteeIndex) (*eth2p0.AttestationData, error)
 	AttesterDutiesFunc                     func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.AttesterDuty, error)
-	BlockAttestationsFunc                  func(ctx context.Context, stateID string) ([]*eth2spec.VersionedAttestation, error)
 	BlockFunc                              func(ctx context.Context, stateID string) (*eth2spec.VersionedSignedBeaconBlock, error)
-	BeaconStateCommitteesFunc              func(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error)
-	NodePeerCountFunc                      func(ctx context.Context) (int, error)
+	BeaconBlockAttestationsFunc            func(context.Context, *eth2api.BeaconBlockAttestationsOpts) ([]*eth2spec.VersionedAttestation, error)
+	BeaconCommitteesFunc                   func(ctx context.Context, opts *eth2api.BeaconCommitteesOpts) ([]*eth2v1.BeaconCommittee, error)
+	NodePeerCountFunc                      func(ctx context.Context, opts *eth2api.NodePeerCountOpts) (*eth2v1.PeerCount, error)
 	ProposalFunc                           func(ctx context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error)
 	SignedBeaconBlockFunc                  func(ctx context.Context, blockID string) (*eth2spec.VersionedSignedBeaconBlock, error)
 	ProposerDutiesFunc                     func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error)
@@ -210,8 +208,8 @@ type Mock struct {
 	NodeSyncingFunc                        func(context.Context, *eth2api.NodeSyncingOpts) (*eth2v1.SyncState, error)
 	SubmitValidatorRegistrationsFunc       func(context.Context, []*eth2api.VersionedSignedValidatorRegistration) error
 	SlotsPerEpochFunc                      func(context.Context) (uint64, error)
-	AggregateBeaconCommitteeSelectionsFunc func(context.Context, []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error)
-	AggregateSyncCommitteeSelectionsFunc   func(context.Context, []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error)
+	BeaconCommitteeSelectionsFunc          func(context.Context, *eth2api.BeaconCommitteeSelectionsOpts) ([]*eth2v1.BeaconCommitteeSelection, error)
+	SyncCommitteeSelectionsFunc            func(context.Context, *eth2api.SyncCommitteeSelectionsOpts) ([]*eth2v1.SyncCommitteeSelection, error)
 	SubmitBeaconCommitteeSubscriptionsFunc func(ctx context.Context, subscriptions []*eth2v1.BeaconCommitteeSubscription) error
 	AggregateAttestationFunc               func(ctx context.Context, slot eth2p0.Slot, attestationDataRoot eth2p0.Root) (*eth2spec.VersionedAttestation, error)
 	SubmitAggregateAttestationsFunc        func(ctx context.Context, aggregateAndProofs *eth2api.SubmitAggregateAttestationsOpts) error
@@ -222,7 +220,6 @@ type Mock struct {
 	SubmitSyncCommitteeSubscriptionsFunc   func(ctx context.Context, subscriptions []*eth2v1.SyncCommitteeSubscription) error
 	SubmitProposalPreparationsFunc         func(ctx context.Context, preparations []*eth2v1.ProposalPreparation) error
 	ForkScheduleFunc                       func(context.Context, *eth2api.ForkScheduleOpts) ([]*eth2p0.Fork, error)
-	ProposerConfigFunc                     func(context.Context) (*eth2exp.ProposerConfigResponse, error)
 	NodeVersionFunc                        func(context.Context, *eth2api.NodeVersionOpts) (*eth2api.Response[string], error)
 }
 
@@ -356,20 +353,49 @@ func (m Mock) Genesis(ctx context.Context, opts *eth2api.GenesisOpts) (*eth2api.
 	return wrapResponse(genesis), nil
 }
 
-func (m Mock) BlockAttestations(ctx context.Context, stateID string) ([]*eth2spec.VersionedAttestation, error) {
-	return m.BlockAttestationsFunc(ctx, stateID)
+func (m Mock) BeaconBlockAttestations(ctx context.Context, opts *eth2api.BeaconBlockAttestationsOpts) (*eth2api.Response[[]*eth2spec.VersionedAttestation], error) {
+	atts, err := m.BeaconBlockAttestationsFunc(ctx, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return wrapResponse(atts), nil
+}
+
+func (m Mock) BeaconCommitteeSelections(ctx context.Context, opts *eth2api.BeaconCommitteeSelectionsOpts) (*eth2api.Response[[]*eth2v1.BeaconCommitteeSelection], error) {
+	selections, err := m.BeaconCommitteeSelectionsFunc(ctx, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return wrapResponse(selections), nil
 }
 
-func (m Mock) Block(ctx context.Context, stateID string) (*eth2spec.VersionedSignedBeaconBlock, error) {
-	return m.BlockFunc(ctx, stateID)
+func (m Mock) BeaconCommittees(ctx context.Context, opts *eth2api.BeaconCommitteesOpts) (*eth2api.Response[[]*eth2v1.BeaconCommittee], error) {
+	beaconCommittees, err := m.BeaconCommitteesFunc(ctx, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return wrapResponse(beaconCommittees), nil
 }
 
-func (m Mock) BeaconStateCommittees(ctx context.Context, slot uint64) ([]*statecomm.StateCommittee, error) {
-	return m.BeaconStateCommitteesFunc(ctx, slot)
+func (m Mock) SyncCommitteeSelections(ctx context.Context, opts *eth2api.SyncCommitteeSelectionsOpts) (*eth2api.Response[[]*eth2v1.SyncCommitteeSelection], error) {
+	selections, err := m.SyncCommitteeSelectionsFunc(ctx, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return wrapResponse(selections), nil
 }
 
-func (m Mock) NodePeerCount(ctx context.Context) (int, error) {
-	return m.NodePeerCountFunc(ctx)
+func (m Mock) NodePeerCount(ctx context.Context, opts *eth2api.NodePeerCountOpts) (*eth2api.Response[*eth2v1.PeerCount], error) {
+	peerCount, err := m.NodePeerCountFunc(ctx, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return wrapResponse(peerCount), nil
 }
 
 func (m Mock) SubmitAttestations(ctx context.Context, opts *eth2api.SubmitAttestationsOpts) error {
@@ -388,14 +414,6 @@ func (m Mock) SubmitValidatorRegistrations(ctx context.Context, registrations []
 	return m.SubmitValidatorRegistrationsFunc(ctx, registrations)
 }
 
-func (m Mock) AggregateBeaconCommitteeSelections(ctx context.Context, selections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) {
-	return m.AggregateBeaconCommitteeSelectionsFunc(ctx, selections)
-}
-
-func (m Mock) AggregateSyncCommitteeSelections(ctx context.Context, selections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) {
-	return m.AggregateSyncCommitteeSelectionsFunc(ctx, selections)
-}
-
 func (m Mock) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscriptions []*eth2v1.BeaconCommitteeSubscription) error {
 	return m.SubmitBeaconCommitteeSubscriptionsFunc(ctx, subscriptions)
 }
@@ -424,10 +442,6 @@ func (m Mock) SlotsPerEpoch(ctx context.Context) (uint64, error) {
 	return m.SlotsPerEpochFunc(ctx)
 }
 
-func (m Mock) ProposerConfig(ctx context.Context) (*eth2exp.ProposerConfigResponse, error) {
-	return m.ProposerConfigFunc(ctx)
-}
-
 func (m Mock) NodeVersion(ctx context.Context, opts *eth2api.NodeVersionOpts) (*eth2api.Response[string], error) {
 	return m.NodeVersionFunc(ctx, opts)
 }
diff --git a/testutil/beaconmock/beaconmock_fuzz.go b/testutil/beaconmock/beaconmock_fuzz.go
index db3fad20..cb4e53eb 100644
--- a/testutil/beaconmock/beaconmock_fuzz.go
+++ b/testutil/beaconmock/beaconmock_fuzz.go
@@ -132,11 +132,11 @@ func WithBeaconMockFuzzer() Option {
 			return vals, nil
 		}
 
-		mock.NodePeerCountFunc = func(context.Context) (int, error) {
-			var count int
+		mock.NodePeerCountFunc = func(context.Context, *eth2api.NodePeerCountOpts) (*eth2v1.PeerCount, error) {
+			var count uint64
 			fuzz.New().Fuzz(&count)
 
-			return count, nil
+			return &eth2v1.PeerCount{Connected: count}, nil
 		}
 
 		mock.ValidatorsByPubKeyFunc = func(_ context.Context, _ string, pubkeys []eth2p0.BLSPubKey) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) {
@@ -179,7 +179,7 @@ func WithBeaconMockFuzzer() Option {
 			return contribution, nil
 		}
 
-		mock.BlockAttestationsFunc = func(context.Context, string) ([]*eth2spec.VersionedAttestation, error) {
+		mock.BeaconBlockAttestationsFunc = func(context.Context, *eth2api.BeaconBlockAttestationsOpts) ([]*eth2spec.VersionedAttestation, error) {
 			var atts []*eth2spec.VersionedAttestation
 			fuzz.New().Fuzz(&atts)
 
diff --git a/testutil/beaconmock/beaconmock_test.go b/testutil/beaconmock/beaconmock_test.go
index ff539ee7..8e1168ca 100644
--- a/testutil/beaconmock/beaconmock_test.go
+++ b/testutil/beaconmock/beaconmock_test.go
@@ -18,6 +18,7 @@ import (
 
 func TestDeterministicAttesterDuties(t *testing.T) {
 	bmock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithValidatorSet(beaconmock.ValidatorSetA),
 		beaconmock.WithDeterministicAttesterDuties(1),
 	)
@@ -34,6 +35,7 @@ func TestDeterministicAttesterDuties(t *testing.T) {
 
 func TestDeterministicProposerDuties(t *testing.T) {
 	bmock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithValidatorSet(beaconmock.ValidatorSetA),
 		beaconmock.WithDeterministicProposerDuties(1),
 	)
@@ -49,7 +51,7 @@ func TestDeterministicProposerDuties(t *testing.T) {
 }
 
 func TestAttestationStore(t *testing.T) {
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	ctx := context.Background()
diff --git a/testutil/beaconmock/headproducer_internal_test.go b/testutil/beaconmock/headproducer_internal_test.go
index 2ba55d41..e2773a31 100644
--- a/testutil/beaconmock/headproducer_internal_test.go
+++ b/testutil/beaconmock/headproducer_internal_test.go
@@ -19,7 +19,7 @@ import (
 )
 
 func TestHeadProducer(t *testing.T) {
-	bmock, err := New()
+	bmock, err := New(t.Context())
 	require.NoError(t, err)
 
 	defer bmock.Close()
diff --git a/testutil/beaconmock/options.go b/testutil/beaconmock/options.go
index d74edd24..ba85804c 100644
--- a/testutil/beaconmock/options.go
+++ b/testutil/beaconmock/options.go
@@ -28,7 +28,6 @@ import (
 	"github.com/obolnetwork/charon/app/eth2wrap"
 	"github.com/obolnetwork/charon/app/log"
 	"github.com/obolnetwork/charon/core"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 	"github.com/obolnetwork/charon/testutil"
 )
 
@@ -555,14 +554,14 @@ func defaultMock(httpMock HTTPMock, httpServer *http.Server, clock clockwork.Clo
 		AttesterDutiesFunc: func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.AttesterDuty, error) {
 			return []*eth2v1.AttesterDuty{}, nil
 		},
-		BlockAttestationsFunc: func(context.Context, string) ([]*eth2spec.VersionedAttestation, error) {
+		BeaconBlockAttestationsFunc: func(context.Context, *eth2api.BeaconBlockAttestationsOpts) ([]*eth2spec.VersionedAttestation, error) {
 			return []*eth2spec.VersionedAttestation{}, nil
 		},
 		BlockFunc: func(context.Context, string) (*eth2spec.VersionedSignedBeaconBlock, error) {
 			return &eth2spec.VersionedSignedBeaconBlock{}, nil
 		},
-		NodePeerCountFunc: func(context.Context) (int, error) {
-			return 80, nil
+		NodePeerCountFunc: func(context.Context, *eth2api.NodePeerCountOpts) (*eth2v1.PeerCount, error) {
+			return &eth2v1.PeerCount{Connected: 80}, nil
 		},
 		AttestationDataFunc: func(ctx context.Context, slot eth2p0.Slot, index eth2p0.CommitteeIndex) (*eth2p0.AttestationData, error) {
 			return attStore.NewAttestationData(ctx, slot, index)
@@ -626,8 +625,8 @@ func defaultMock(httpMock HTTPMock, httpServer *http.Server, clock clockwork.Clo
 		SubmitValidatorRegistrationsFunc: func(context.Context, []*eth2api.VersionedSignedValidatorRegistration) error {
 			return nil
 		},
-		AggregateBeaconCommitteeSelectionsFunc: func(_ context.Context, selections []*eth2exp.BeaconCommitteeSelection) ([]*eth2exp.BeaconCommitteeSelection, error) {
-			return selections, nil
+		BeaconCommitteeSelectionsFunc: func(_ context.Context, opts *eth2api.BeaconCommitteeSelectionsOpts) ([]*eth2v1.BeaconCommitteeSelection, error) {
+			return opts.Selections, nil
 		},
 		SubmitAggregateAttestationsFunc: func(context.Context, *eth2api.SubmitAggregateAttestationsOpts) error {
 			return nil
@@ -650,8 +649,8 @@ func defaultMock(httpMock HTTPMock, httpServer *http.Server, clock clockwork.Clo
 		SyncCommitteeDutiesFunc: func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.SyncCommitteeDuty, error) {
 			return []*eth2v1.SyncCommitteeDuty{}, nil
 		},
-		AggregateSyncCommitteeSelectionsFunc: func(_ context.Context, selections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) {
-			return selections, nil
+		SyncCommitteeSelectionsFunc: func(_ context.Context, opts *eth2api.SyncCommitteeSelectionsOpts) ([]*eth2v1.SyncCommitteeSelection, error) {
+			return opts.Selections, nil
 		},
 		SubmitSyncCommitteeMessagesFunc: func(context.Context, []*altair.SyncCommitteeMessage) error {
 			return nil
@@ -682,9 +681,6 @@ func defaultMock(httpMock HTTPMock, httpServer *http.Server, clock clockwork.Clo
 
 			return eth2Resp.Data, nil
 		},
-		ProposerConfigFunc: func(context.Context) (*eth2exp.ProposerConfigResponse, error) {
-			return nil, nil
-		},
 		NodeVersionFunc: func(_ context.Context, _ *eth2api.NodeVersionOpts) (*eth2api.Response[string], error) {
 			return &eth2api.Response[string]{Data: "charon/static_beacon_mock"}, nil
 		},
diff --git a/testutil/beaconmock/server.go b/testutil/beaconmock/server.go
index bb5b4b91..a7a67f55 100644
--- a/testutil/beaconmock/server.go
+++ b/testutil/beaconmock/server.go
@@ -174,8 +174,10 @@ func newHTTPServer(addr string, optionalHandlers map[string]http.HandlerFunc, ov
 }
 
 // newHTTPMock starts and returns a static beacon mock http server and client.
-func newHTTPMock(optionalHandlers map[string]http.HandlerFunc, overrides ...staticOverride) (HTTPMock, *http.Server, error) {
-	l, err := net.Listen("tcp", "localhost:0")
+func newHTTPMock(ctx context.Context, optionalHandlers map[string]http.HandlerFunc, overrides ...staticOverride) (HTTPMock, *http.Server, error) {
+	lc := net.ListenConfig{}
+
+	l, err := lc.Listen(ctx, "tcp", "localhost:0")
 	if err != nil {
 		return nil, nil, errors.Wrap(err, "listen")
 	}
diff --git a/testutil/beaconmock/server_test.go b/testutil/beaconmock/server_test.go
index 3c5a686a..8ddc5587 100644
--- a/testutil/beaconmock/server_test.go
+++ b/testutil/beaconmock/server_test.go
@@ -16,7 +16,7 @@ import (
 func TestStatic(t *testing.T) {
 	ctx := context.Background()
 
-	eth2Cl, err := beaconmock.New()
+	eth2Cl, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	genesisResp, err := eth2Cl.Genesis(ctx, &eth2api.GenesisOpts{})
@@ -50,7 +50,7 @@ func TestGenesisTimeOverride(t *testing.T) {
 	ctx := context.Background()
 
 	t0 := time.Now().Truncate(time.Second)
-	eth2Cl, err := beaconmock.New(beaconmock.WithGenesisTime(t0))
+	eth2Cl, err := beaconmock.New(t.Context(), beaconmock.WithGenesisTime(t0))
 	require.NoError(t, err)
 
 	genesisResp, err := eth2Cl.Genesis(ctx, &eth2api.GenesisOpts{})
@@ -62,7 +62,7 @@ func TestSlotsPerEpochOverride(t *testing.T) {
 	ctx := context.Background()
 
 	expect := 5
-	eth2Cl, err := beaconmock.New(beaconmock.WithSlotsPerEpoch(expect))
+	eth2Cl, err := beaconmock.New(t.Context(), beaconmock.WithSlotsPerEpoch(expect))
 	require.NoError(t, err)
 
 	actual, err := eth2Cl.SlotsPerEpoch(ctx)
@@ -78,7 +78,7 @@ func TestSlotsDurationOverride(t *testing.T) {
 	ctx := context.Background()
 
 	expect := time.Second
-	eth2Cl, err := beaconmock.New(beaconmock.WithSlotDuration(expect))
+	eth2Cl, err := beaconmock.New(t.Context(), beaconmock.WithSlotDuration(expect))
 	require.NoError(t, err)
 
 	eth2Resp, err := eth2Cl.Spec(ctx, &eth2api.SpecOpts{})
@@ -104,6 +104,7 @@ func TestEndpointOverride(t *testing.T) {
         	"epoch": "0"
       	}]}`
 	bmock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithEndpoint("/eth/v1/config/fork_schedule", forkSchedule),
 	)
 	require.NoError(t, err)
@@ -117,7 +118,7 @@ func TestEndpointOverride(t *testing.T) {
 
 func TestDefaultOverrides(t *testing.T) {
 	ctx := context.Background()
-	bmock, err := beaconmock.New()
+	bmock, err := beaconmock.New(t.Context())
 	require.NoError(t, err)
 
 	resp, err := bmock.Spec(ctx, &eth2api.SpecOpts{})
diff --git a/testutil/genchangelog/main.go b/testutil/genchangelog/main.go
index 08b6a05c..8ed04810 100644
--- a/testutil/genchangelog/main.go
+++ b/testutil/genchangelog/main.go
@@ -112,6 +112,8 @@ type tplPR struct {
 }
 
 func main() {
+	ctx := context.Background()
+
 	flag.Parse()
 
 	token, ok := os.LookupEnv("GITHUB_TOKEN")
@@ -121,7 +123,7 @@ func main() {
 		fmt.Println("GITHUB_TOKEN env var not set, using unauthenticated API")
 	}
 
-	err := run(*rangeFlag, *outputFlag, token)
+	err := run(ctx, *rangeFlag, *outputFlag, token)
 	if err != nil {
 		applog.Error(context.Background(), "Run error", err)
 		os.Exit(1)
@@ -129,9 +131,9 @@ func main() {
 }
 
 // run runs the command.
-func run(gitRange string, output string, token string) error {
+func run(ctx context.Context, gitRange string, output string, token string) error {
 	if gitRange == "" {
-		tags, err := getLatestTags(2)
+		tags, err := getLatestTags(ctx, 2)
 		if err != nil {
 			return err
 		}
@@ -140,7 +142,7 @@ func run(gitRange string, output string, token string) error {
 		fmt.Printf("Flag --range empty, defaulting to %s\n", gitRange)
 	}
 
-	prs, err := parsePRs(gitRange)
+	prs, err := parsePRs(ctx, gitRange)
 	if err != nil {
 		return err
 	}
@@ -340,11 +342,11 @@ func selectCategory(current, candidate string) string {
 }
 
 // parsePRs returns parsed PRs by query the git logs for the provided range.
-func parsePRs(gitRange string) ([]pullRequest, error) {
+func parsePRs(ctx context.Context, gitRange string) ([]pullRequest, error) {
 	// Custom json encoding of git log output.
 	const format = `--pretty=format:{¬commit¬: ¬%h¬,¬body¬: ¬%b¬,¬subject¬: ¬%s¬,¬author¬: ¬%aE¬}†`
 
-	b, err := exec.Command("git", "log", format, gitRange).CombinedOutput()
+	b, err := exec.CommandContext(ctx, "git", "log", format, gitRange).CombinedOutput()
 	if err != nil {
 		return nil, errors.Wrap(err, "git log")
 	}
@@ -460,13 +462,13 @@ func getFirstMatch(r *regexp.Regexp, s string) (string, bool) {
 }
 
 // getLatestTags returns the latest N git tags.
-func getLatestTags(n int) ([]string, error) {
-	out, err := exec.Command("git", "fetch", "--tags", "-f").CombinedOutput()
+func getLatestTags(ctx context.Context, n int) ([]string, error) {
+	out, err := exec.CommandContext(ctx, "git", "fetch", "--tags", "-f").CombinedOutput()
 	if err != nil {
 		return nil, errors.Wrap(err, "git fetch", z.Str("out", string(out)))
 	}
 
-	out, err = exec.Command("git", "tag", "--sort=v:refname").CombinedOutput()
+	out, err = exec.CommandContext(ctx, "git", "tag", "--sort=v:refname").CombinedOutput()
 	if err != nil {
 		return nil, errors.Wrap(err, "git tag", z.Str("out", string(out)))
 	}
diff --git a/testutil/genchangelog/main_internal_test.go b/testutil/genchangelog/main_internal_test.go
index d003a62b..2de650b6 100644
--- a/testutil/genchangelog/main_internal_test.go
+++ b/testutil/genchangelog/main_internal_test.go
@@ -107,7 +107,7 @@ func TestLatestTags(t *testing.T) {
 		return
 	}
 
-	tags, err := getLatestTags(2)
+	tags, err := getLatestTags(t.Context(), 2)
 	require.NoError(t, err)
 	require.Len(t, tags, 2)
 	t.Log(tags)
@@ -122,7 +122,7 @@ func TestParsePRs(t *testing.T) {
 	}
 
 	gitRange := "v0.12.0..v0.13.0"
-	prs, err := parsePRs(gitRange)
+	prs, err := parsePRs(t.Context(), gitRange)
 	require.NoError(t, err)
 
 	data, err := tplDataFromPRs(prs, gitRange, func(i int) (string, string, error) {
diff --git a/testutil/random.go b/testutil/random.go
index 91e1bbb5..07b778c7 100644
--- a/testutil/random.go
+++ b/testutil/random.go
@@ -41,7 +41,6 @@ import (
 	"github.com/obolnetwork/charon/core"
 	"github.com/obolnetwork/charon/eth2util"
 	"github.com/obolnetwork/charon/eth2util/enr"
-	"github.com/obolnetwork/charon/eth2util/eth2exp"
 	"github.com/obolnetwork/charon/tbls"
 )
 
@@ -848,8 +847,8 @@ func RandomVersionedSignedValidatorRegistration(t *testing.T) *eth2api.Versioned
 	}
 }
 
-func RandomBeaconCommitteeSelection() *eth2exp.BeaconCommitteeSelection {
-	return &eth2exp.BeaconCommitteeSelection{
+func RandomBeaconCommitteeSelection() *eth2v1.BeaconCommitteeSelection {
+	return &eth2v1.BeaconCommitteeSelection{
 		ValidatorIndex: RandomVIdx(),
 		Slot:           RandomSlot(),
 		SelectionProof: RandomEth2Signature(),
@@ -945,11 +944,11 @@ func RandomSyncCommittee(t *testing.T) *altair.SyncCommittee {
 	}
 }
 
-func RandomSyncCommitteeSelection() *eth2exp.SyncCommitteeSelection {
-	return &eth2exp.SyncCommitteeSelection{
+func RandomSyncCommitteeSelection() *eth2v1.SyncCommitteeSelection {
+	return &eth2v1.SyncCommitteeSelection{
 		ValidatorIndex:    RandomVIdx(),
 		Slot:              RandomSlot(),
-		SubcommitteeIndex: RandomCommIdx(),
+		SubcommitteeIndex: uint64(rand.Intn(4)),
 		SelectionProof:    RandomEth2Signature(),
 	}
 }
@@ -1398,7 +1397,8 @@ func SkipIfBindErr(t *testing.T, err error) {
 func AvailableAddr(t *testing.T) *net.TCPAddr {
 	t.Helper()
 
-	l, err := net.Listen("tcp", "localhost:0")
+	lc := net.ListenConfig{}
+	l, err := lc.Listen(t.Context(), "tcp", "localhost:0")
 	require.NoError(t, err)
 
 	defer l.Close()
@@ -1436,7 +1436,8 @@ func AvailableUDPAddr(t *testing.T) *net.UDPAddr {
 func AvailableMultiAddr(t *testing.T) multiaddr.Multiaddr {
 	t.Helper()
 
-	l, err := net.Listen("tcp", "localhost:0")
+	lc := net.ListenConfig{}
+	l, err := lc.Listen(t.Context(), "tcp", "localhost:0")
 	require.NoError(t, err)
 
 	defer l.Close()
diff --git a/testutil/validatormock/attest.go b/testutil/validatormock/attest.go
index 2106579d..7c10287b 100644
--- a/testutil/validatormock/attest.go
+++ b/testutil/validatormock/attest.go
@@ -25,7 +25,7 @@ import (
 // Type aliases for concise function signatures.
 type (
 	attDuties     []*eth2v1.AttesterDuty
-	attSelections []*eth2exp.BeaconCommitteeSelection
+	attSelections []*eth2v1.BeaconCommitteeSelection
 	attDatas      []*eth2p0.AttestationData
 )
 
@@ -250,7 +250,7 @@ func prepareAggregators(ctx context.Context, eth2Cl eth2wrap.Client, signFunc Si
 	}
 
 	var (
-		partials    []*eth2exp.BeaconCommitteeSelection
+		partials    []*eth2v1.BeaconCommitteeSelection
 		commLengths = make(map[eth2p0.ValidatorIndex]uint64)
 	)
 	for _, duty := range duties {
@@ -266,18 +266,20 @@ func prepareAggregators(ctx context.Context, eth2Cl eth2wrap.Client, signFunc Si
 
 		commLengths[duty.ValidatorIndex] = duty.CommitteeLength
 
-		partials = append(partials, &eth2exp.BeaconCommitteeSelection{
+		partials = append(partials, &eth2v1.BeaconCommitteeSelection{
 			ValidatorIndex: duty.ValidatorIndex,
 			Slot:           duty.Slot,
 			SelectionProof: slotSig,
 		})
 	}
 
-	aggregateSelections, err := eth2Cl.AggregateBeaconCommitteeSelections(ctx, partials)
+	eth2Resp, err := eth2Cl.BeaconCommitteeSelections(ctx, &eth2api.BeaconCommitteeSelectionsOpts{Selections: partials})
 	if err != nil {
 		return nil, err
 	}
 
+	aggregateSelections := eth2Resp.Data
+
 	var selections attSelections
 
 	for _, selection := range aggregateSelections {
diff --git a/testutil/validatormock/component.go b/testutil/validatormock/component.go
index 1b87b39e..eb7433fd 100644
--- a/testutil/validatormock/component.go
+++ b/testutil/validatormock/component.go
@@ -278,22 +278,6 @@ func (m *Component) runDuty(ctx context.Context, duty core.Duty) error {
 		}
 	case core.DutyBuilderProposer:
 		return core.ErrDeprecatedDutyBuilderProposer
-	case core.DutyBuilderRegistration:
-		if !m.builderAPI {
-			return nil
-		}
-
-		regs, err := RegistrationsFromProposerConfig(ctx, eth2Cl)
-		if err != nil {
-			return err
-		}
-
-		for pubshare, reg := range regs {
-			err = Register(ctx, eth2Cl, m.signFunc, reg, pubshare)
-			if err != nil {
-				return err
-			}
-		}
 	case core.DutyPrepareSyncContribution:
 		if syncComm == nil {
 			return errors.New("syncomm is nil", z.Str("duty", duty.String()))
diff --git a/testutil/validatormock/propose.go b/testutil/validatormock/propose.go
index 3df43dc6..72e808f0 100644
--- a/testutil/validatormock/propose.go
+++ b/testutil/validatormock/propose.go
@@ -5,8 +5,6 @@ package validatormock
 
 import (
 	"context"
-	"encoding/hex"
-	"strings"
 
 	eth2api "github.com/attestantio/go-eth2-client/api"
 	eth2v1 "github.com/attestantio/go-eth2-client/api/v1"
@@ -219,59 +217,6 @@ func ProposeBlock(ctx context.Context, eth2Cl eth2wrap.Client, signFunc SignFunc
 	return eth2Cl.SubmitProposal(ctx, &eth2api.SubmitProposalOpts{Proposal: signedBlock})
 }
 
-// RegistrationsFromProposerConfig returns all enabled builder-API registrations from upstream proposer config.
-func RegistrationsFromProposerConfig(ctx context.Context, eth2Cl eth2wrap.Client) (map[eth2p0.BLSPubKey]*eth2api.VersionedValidatorRegistration, error) {
-	confs, err := eth2Cl.ProposerConfig(ctx)
-	if err != nil {
-		return nil, errors.Wrap(err, "get proposer config")
-	}
-
-	resp := make(map[eth2p0.BLSPubKey]*eth2api.VersionedValidatorRegistration)
-
-	for pubshare, conf := range confs.Proposers {
-		if !conf.Builder.Enabled {
-			continue
-		}
-
-		ts, ok, err := conf.Builder.TimestampOverride()
-		if err != nil {
-			return nil, errors.Wrap(err, "get builder timestamp override")
-		} else if !ok {
-			return nil, errors.Wrap(err, "no builder timestamp override")
-		}
-
-		pubkey, ok, err := conf.Builder.PublicKeyOverride()
-		if err != nil {
-			return nil, errors.Wrap(err, "get builder public key override")
-		} else if !ok {
-			return nil, errors.Wrap(err, "no builder public key override")
-		}
-
-		if conf.Builder.GasLimit == 0 {
-			return nil, errors.New("invalid builder gas limit")
-		}
-
-		feeRecipientBytes, err := hex.DecodeString(strings.TrimPrefix(conf.FeeRecipient, "0x"))
-		if err != nil {
-			return nil, errors.Wrap(err, "decode fee recipient")
-		} else if len(feeRecipientBytes) != 20 {
-			return nil, errors.New("invalid fee recipient")
-		}
-
-		resp[pubshare] = &eth2api.VersionedValidatorRegistration{
-			Version: eth2spec.BuilderVersionV1,
-			V1: &eth2v1.ValidatorRegistration{
-				FeeRecipient: bellatrix.ExecutionAddress(feeRecipientBytes),
-				GasLimit:     uint64(conf.Builder.GasLimit),
-				Timestamp:    ts,
-				Pubkey:       pubkey,
-			},
-		}
-	}
-
-	return resp, nil
-}
-
 // Register signs and submits the validator builder registration to the validator API.
 func Register(ctx context.Context, eth2Cl eth2wrap.Client, signFunc SignFunc,
 	registration *eth2api.VersionedValidatorRegistration, pubshare eth2p0.BLSPubKey,
diff --git a/testutil/validatormock/propose_test.go b/testutil/validatormock/propose_test.go
index b1ea57bf..03cfcc06 100644
--- a/testutil/validatormock/propose_test.go
+++ b/testutil/validatormock/propose_test.go
@@ -52,6 +52,7 @@ func TestAttest(t *testing.T) {
 			// Configure beacon mock
 			valSet := beaconmock.ValidatorSetA
 			beaconMock, err := beaconmock.New(
+				t.Context(),
 				beaconmock.WithClock(clock),
 				beaconmock.WithValidatorSet(valSet),
 				beaconmock.WithDeterministicAttesterDuties(test.DutyFactor),
@@ -279,6 +280,7 @@ func TestProposeBlock(t *testing.T) {
 			// Configure beacon mock
 			valSet := beaconmock.ValidatorSetA
 			beaconMock, err := beaconmock.New(
+				t.Context(),
 				beaconmock.WithValidatorSet(valSet),
 				beaconmock.WithDeterministicProposerDuties(0),
 			)
@@ -328,6 +330,7 @@ func TestProposeBlindedBlock(t *testing.T) {
 	// Configure beacon mock
 	valSet := beaconmock.ValidatorSetA
 	beaconMock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithValidatorSet(valSet),
 		beaconmock.WithDeterministicProposerDuties(0),
 	)
diff --git a/testutil/validatormock/synccomm.go b/testutil/validatormock/synccomm.go
index 6394269f..f123dd5d 100644
--- a/testutil/validatormock/synccomm.go
+++ b/testutil/validatormock/synccomm.go
@@ -23,7 +23,7 @@ import (
 
 type (
 	syncDuties     []*eth2v1.SyncCommitteeDuty
-	syncSelections []*eth2exp.SyncCommitteeSelection
+	syncSelections []*eth2v1.SyncCommitteeSelection
 )
 
 func NewSyncCommMember(eth2Cl eth2wrap.Client, epoch eth2p0.Epoch, signFunc SignFunc, pubkeys []eth2p0.BLSPubKey) *SyncCommMember {
@@ -314,7 +314,7 @@ func prepareSyncSelections(ctx context.Context, eth2Cl eth2wrap.Client, signFunc
 		return nil, err
 	}
 
-	var partials []*eth2exp.SyncCommitteeSelection
+	var partials []*eth2v1.SyncCommitteeSelection
 
 	for _, duty := range duties {
 		subcommIdxs, err := getSubcommittees(ctx, eth2Cl, duty)
@@ -343,20 +343,22 @@ func prepareSyncSelections(ctx context.Context, eth2Cl eth2wrap.Client, signFunc
 				return nil, err
 			}
 
-			partials = append(partials, &eth2exp.SyncCommitteeSelection{
+			partials = append(partials, &eth2v1.SyncCommitteeSelection{
 				ValidatorIndex:    duty.ValidatorIndex,
 				Slot:              slot,
-				SubcommitteeIndex: subcommIdx,
+				SubcommitteeIndex: uint64(subcommIdx),
 				SelectionProof:    sig,
 			})
 		}
 	}
 
-	aggregateSelections, err := eth2Cl.AggregateSyncCommitteeSelections(ctx, partials)
+	eth2Resp, err := eth2Cl.SyncCommitteeSelections(ctx, &eth2api.SyncCommitteeSelectionsOpts{Selections: partials})
 	if err != nil {
 		return nil, err
 	}
 
+	aggregateSelections := eth2Resp.Data
+
 	var selections syncSelections
 
 	for _, selection := range aggregateSelections {
@@ -466,7 +468,7 @@ func aggContributions(ctx context.Context, eth2Cl eth2wrap.Client, signFunc Sign
 		// Query BN to get sync committee contribution.
 		opts := &eth2api.SyncCommitteeContributionOpts{
 			Slot:              selection.Slot,
-			SubcommitteeIndex: uint64(selection.SubcommitteeIndex),
+			SubcommitteeIndex: selection.SubcommitteeIndex,
 			BeaconBlockRoot:   blockRoot,
 		}
 
diff --git a/testutil/validatormock/synccomm_internal_test.go b/testutil/validatormock/synccomm_internal_test.go
index 4b9c41e4..f4a110e2 100644
--- a/testutil/validatormock/synccomm_internal_test.go
+++ b/testutil/validatormock/synccomm_internal_test.go
@@ -17,6 +17,7 @@ import (
 func TestGetSubcommittees(t *testing.T) {
 	ctx := context.Background()
 	bmock, err := beaconmock.New(
+		t.Context(),
 		beaconmock.WithSyncCommitteeSize(512),
 		beaconmock.WithSyncCommitteeSubnetCount(4),
 	)