Repository: ObolNetwork/charon

Issue:
`combine` default input should align with  `create cluster` default output. 
# 🎯 Problem to be solved

We changed `charon create cluster` to not put a `./cluster/` folder within `.charon`. So now the default is to create a folder called `cluster`. 

However we never changed the default `charon combine` behaviour, which still expects `.charon/cluster` as the default data-dir. 

# 🛠️ Proposed solution

Change the default `data-dir` for `charon combine` to `./cluster`

- [ ] Core team consensus on the proposed solution


# 🧪 Tests
- [ ] Tested by new automated unit/integration/smoke tests
- [ ] Manually tested on core team/canary/test clusters
- [ ] Manually tested on local compose simnet

<!-- Optional sections

# 👐 Additional acceptance criteria
List any additional acceptance criteria for this issue to be marked as closed.

# ❌ Out of Scope
If there is anything to highlight as out of scope for this issue, please outline it here.

-->


Discussion:
@KaloyanTanev: This one should've been fixed here already?
https://github.com/ObolNetwork/charon/pull/1876@OisinKyne: Doesn't look like it to me? (default combine expects `.charon/cluster`, it looks like nowadays default create outputs to `./`? So i guess the ticket should change to `./` not `./cluster`
```
 docker run obolnetwork/charon:v1.3.0 combine --help
Combines the private key shares from a threshold of operators in a distributed validator cluster into a set of validator private keys that can be imported into a standard Ethereum validator client.

Warning: running the resulting private keys in a validator alongside the original distributed validator cluster *will* result in slashing.

Usage:
  charon combine [flags]

Flags:
      --cluster-dir string              Parent directory containing a number of .charon subdirectories from the required threshold of nodes in the cluster. (default ".charon/cluster")
```

```
 docker run obolnetwork/charon:v1.3.0 create cluster --help
Creates a local charon cluster configuration including validator keys, charon p2p keys, cluster-lock.json and deposit-data.json file(s). See flags for supported features.

Usage:
  charon create cluster [flags]

Flags:
      --cluster-dir string                The target folder to create the cluster in. (default "./")
```

Solution patch:
diff --git a/app/app.go b/app/app.go
index f482f9c3..e174309e 100644
--- a/app/app.go
+++ b/app/app.go
@@ -523,7 +523,20 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config,
 	if err != nil {
 		return err
 	}
-	fetch, err := fetcher.New(eth2Cl, feeRecipientFunc, conf.BuilderAPI, graffitiBuilder)
+
+	forkSchedule, err := eth2wrap.FetchForkConfig(ctx, eth2Cl)
+	if err != nil {
+		return err
+	}
+
+	_, slotsPerEpoch, err := eth2wrap.FetchSlotsConfig(ctx, eth2Cl)
+	if err != nil {
+		return err
+	}
+
+	electraSlot := eth2p0.Slot(uint64(forkSchedule[eth2wrap.Electra].Epoch) * slotsPerEpoch)
+
+	fetch, err := fetcher.New(eth2Cl, feeRecipientFunc, conf.BuilderAPI, graffitiBuilder, electraSlot)
 	if err != nil {
 		return err
 	}
diff --git a/app/eth2wrap/utils.go b/app/eth2wrap/utils.go
index 380101ab..32d50ae8 100644
--- a/app/eth2wrap/utils.go
+++ b/app/eth2wrap/utils.go
@@ -8,18 +8,55 @@ import (
 
 	eth2client "github.com/attestantio/go-eth2-client"
 	"github.com/attestantio/go-eth2-client/api"
+	eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
 
 	"github.com/obolnetwork/charon/app/errors"
 )
 
+type ForkSchedule struct {
+	Version eth2p0.Version
+	Epoch   eth2p0.Epoch
+}
+
+type ForkForkSchedule map[Fork]ForkSchedule
+
+type Fork uint64
+
+const (
+	Altair Fork = iota
+	Bellatrix
+	Capella
+	Deneb
+	Electra
+	Fulu
+)
+
+func (f Fork) String() string {
+	return forkLabels[f]
+}
+
+var forkLabels = map[Fork]string{
+	Altair:    "ALTAIR",
+	Bellatrix: "BELLATRIX",
+	Capella:   "CAPELLA",
+	Deneb:     "DENEB",
+	Electra:   "ELECTRA",
+	Fulu:      "FULU",
+}
+
+var (
+	errFetchNetworkSpec   = errors.New("fetch network spec")
+	errMissingNetworkSpec = errors.New("missing network spec")
+)
+
 func FetchGenesisTime(ctx context.Context, client eth2client.GenesisProvider) (time.Time, error) {
 	genesisTime, err := client.Genesis(ctx, &api.GenesisOpts{})
 	if err != nil {
-		return time.Time{}, errors.Wrap(err, "failed to fetch network spec")
+		return time.Time{}, errFetchNetworkSpec
 	}
 
 	if genesisTime == nil {
-		return time.Time{}, errors.New("missing network spec")
+		return time.Time{}, errMissingNetworkSpec
 	}
 
 	return genesisTime.Data.GenesisTime, nil
@@ -28,11 +65,11 @@ func FetchGenesisTime(ctx context.Context, client eth2client.GenesisProvider) (t
 func FetchSlotsConfig(ctx context.Context, client eth2client.SpecProvider) (slotDuration time.Duration, slotsPerEpoch uint64, err error) {
 	spec, err := client.Spec(ctx, &api.SpecOpts{})
 	if err != nil {
-		return 0, 0, errors.Wrap(err, "failed to fetch network spec")
+		return 0, 0, errFetchNetworkSpec
 	}
 
 	if spec == nil {
-		return 0, 0, errors.New("missing network spec")
+		return 0, 0, errMissingNetworkSpec
 	}
 
 	var ok bool
@@ -48,3 +85,45 @@ func FetchSlotsConfig(ctx context.Context, client eth2client.SpecProvider) (slot
 
 	return slotDuration, slotsPerEpoch, nil
 }
+
+func FetchForkConfig(ctx context.Context, client eth2client.SpecProvider) (fork ForkForkSchedule, err error) {
+	spec, err := client.Spec(ctx, &api.SpecOpts{})
+	if err != nil {
+		return nil, errFetchNetworkSpec
+	}
+
+	if spec == nil {
+		return nil, errMissingNetworkSpec
+	}
+
+	res := ForkForkSchedule{}
+	for k, v := range forkLabels {
+		fs, err := fetchFork(v, spec.Data)
+		if err != nil {
+			return nil, err
+		}
+		res[k] = fs
+	}
+
+	return res, nil
+}
+
+func fetchFork(forkName string, data map[string]any) (ForkSchedule, error) {
+	var ok bool
+	fs := ForkSchedule{}
+	forkVersion := forkName + "_FORK_VERSION"
+	version, ok := data[forkVersion].(eth2p0.Version)
+	if !ok {
+		return fs, errors.New("missing " + forkVersion + " in network spec")
+	}
+	fs.Version = version
+
+	forkEpoch := forkName + "_FORK_EPOCH"
+	epoch, ok := data[forkEpoch].(uint64)
+	if !ok {
+		return fs, errors.New("missing " + forkEpoch + " in network spec")
+	}
+	fs.Epoch = eth2p0.Epoch(epoch)
+
+	return fs, nil
+}
diff --git a/cmd/addvalidators.go b/cmd/addvalidators.go
index 8f891450..20aa1063 100644
--- a/cmd/addvalidators.go
+++ b/cmd/addvalidators.go
@@ -91,7 +91,7 @@ func bindAddValidatorsFlags(cmd *cobra.Command, config *addValidatorsConfig) {
 	cmd.Flags().StringSliceVar(&config.WithdrawalAddrs, "withdrawal-addresses", nil, "Comma separated list of Ethereum addresses to receive the returned stake and accrued rewards for each new validator. Either provide a single withdrawal address or withdrawal addresses for each validator.")
 	cmd.Flags().StringVar(&config.Lockfile, "lock-file", "cluster-lock.json", "The path to the legacy cluster lock file defining distributed validator cluster. If both cluster manifest and cluster lock files are provided, the cluster manifest file takes precedence.")
 	cmd.Flags().StringVar(&config.ManifestFile, "manifest-file", "cluster-manifest.pb", "The path to the cluster manifest file. If both cluster manifest and cluster lock files are provided, the cluster manifest file takes precedence.")
-	cmd.Flags().StringVar(&config.ClusterDir, "cluster-dir", ".charon/cluster", "The path to the charon cluster directory. This directory should contain directories for individual charon nodes, ie, node0, node1 and so on.")
+	cmd.Flags().StringVar(&config.ClusterDir, "cluster-dir", "./", "The path to the charon cluster directory. This directory should contain directories for individual charon nodes, ie, node0, node1 and so on.")
 }
 
 func runAddValidatorsSolo(ctx context.Context, conf addValidatorsConfig) (err error) {
diff --git a/cmd/combine.go b/cmd/combine.go
index e12739a3..4705e0c7 100644
--- a/cmd/combine.go
+++ b/cmd/combine.go
@@ -60,7 +60,7 @@ func newCombineFunc(ctx context.Context, clusterDir, outputDir string, force, no
 }
 
 func bindCombineFlags(flags *pflag.FlagSet, clusterDir, outputDir *string, force *bool, executionEngineAddr *string, config *eth2util.Network) {
-	flags.StringVar(clusterDir, "cluster-dir", ".charon/cluster", `Parent directory containing a number of .charon subdirectories from the required threshold of nodes in the cluster.`)
+	flags.StringVar(clusterDir, "cluster-dir", "./", `Parent directory containing a number of .charon subdirectories from the required threshold of nodes in the cluster.`)
 	flags.StringVar(outputDir, "output-dir", "./validator_keys", "Directory to output the combined private keys to.")
 	flags.BoolVar(force, "force", false, "Overwrites private keys with the same name if present.")
 	flags.StringVar(executionEngineAddr, "execution-client-rpc-endpoint", "", "The address of the execution engine JSON-RPC API.")
diff --git a/core/deadline.go b/core/deadline.go
index 22c20653..05a3c34d 100644
--- a/core/deadline.go
+++ b/core/deadline.go
@@ -19,7 +19,7 @@ import (
 const (
 	// marginFactor defines the fraction of the slot duration to use as a margin.
 	// This is to consider network delays and other factors that may affect the timing.
-	marginFactor = 24
+	marginFactor = 12
 )
 
 // DeadlineFunc is a function that returns the deadline for a duty.
diff --git a/core/dutydb/memory.go b/core/dutydb/memory.go
index 5424ac8d..9f4e9206 100644
--- a/core/dutydb/memory.go
+++ b/core/dutydb/memory.go
@@ -201,8 +201,7 @@ func (db *MemDB) AwaitAggAttestation(ctx context.Context, slot uint64, attestati
 ) (*eth2spec.VersionedAttestation, error) {
 	cancel := make(chan struct{})
 	defer close(cancel)
-	response := make(chan core.AggregatedAttestation, 1)                   // Instance of one so resolving never blocks
-	versionedResponse := make(chan core.VersionedAggregatedAttestation, 1) // Instance of one so resolving never blocks
+	response := make(chan core.VersionedAggregatedAttestation, 1) // Instance of one so resolving never blocks
 
 	db.mu.Lock()
 	db.aggQueries = append(db.aggQueries, aggQuery{
@@ -210,7 +209,7 @@ func (db *MemDB) AwaitAggAttestation(ctx context.Context, slot uint64, attestati
 			Slot: slot,
 			Root: attestationRoot,
 		},
-		Response: versionedResponse,
+		Response: response,
 		Cancel:   cancel,
 	})
 	db.resolveAggQueriesUnsafe()
@@ -221,33 +220,18 @@ func (db *MemDB) AwaitAggAttestation(ctx context.Context, slot uint64, attestati
 		return nil, errors.New("dutydb shutdown")
 	case <-ctx.Done():
 		return nil, ctx.Err()
-	case versionedValue := <-versionedResponse:
-		// Clone before returning.
-		clone, err := versionedValue.Clone()
-		if err != nil {
-			return nil, err
-		}
-		aggAtt, ok := clone.(core.VersionedAggregatedAttestation)
-		if !ok {
-			return nil, errors.New("invalid aggregated attestation")
-		}
-
-		return &aggAtt.VersionedAttestation, nil
 	case value := <-response:
 		// Clone before returning.
 		clone, err := value.Clone()
 		if err != nil {
 			return nil, err
 		}
-		aggAtt, ok := clone.(core.AggregatedAttestation)
+		aggAtt, ok := clone.(core.VersionedAggregatedAttestation)
 		if !ok {
 			return nil, errors.New("invalid aggregated attestation")
 		}
 
-		return &eth2spec.VersionedAttestation{
-			Version: eth2spec.DataVersionDeneb,
-			Deneb:   &aggAtt.Attestation,
-		}, nil
+		return &aggAtt.VersionedAttestation, nil
 	}
 }
 
@@ -344,6 +328,45 @@ func (db *MemDB) storeAttestationUnsafe(pubkey core.PubKey, unsignedData core.Un
 		db.attDuties[aKey] = &attData.Data
 	}
 
+	// TODO(kalo):
+	// Committee index 0 should be the default behaviour post-electra.
+	// However, some VCs are still requesting for attestation data with a committee index.
+	// Because of that on Charon side we are also saving attestation data with a committee index.
+	// VCs that work correctly and ask for the hardcoded committee index of 0 need the logic below in order to function properly.
+	// Once all VCs work correctly and ask for index 0, we can remove the logic below, as we will always receive committee index 0
+	// and write it as such from the logic on top.
+	// https://ethereum.github.io/beacon-APIs/#/Validator/produceAttestationData
+
+	// Store key and value for PubKeyByAttestation
+	pKeyCommIdx0 := pkKey{
+		Slot:    uint64(attData.Data.Slot),
+		CommIdx: 0,
+		ValIdx:  uint64(attData.Duty.ValidatorIndex),
+	}
+
+	if value, ok := db.attPubKeys[pKeyCommIdx0]; ok {
+		if *value != *pubkeyStore {
+			return errors.New("clashing public key", z.Any("pKey", pKeyCommIdx0))
+		}
+	} else {
+		db.attPubKeys[pKeyCommIdx0] = pubkeyStore
+		db.attKeysBySlot[uint64(attData.Duty.Slot)] = append(db.attKeysBySlot[uint64(attData.Duty.Slot)], pKeyCommIdx0)
+	}
+
+	// Store key and value for AwaitAttestation
+	aKeyCommIdx0 := attKey{
+		Slot:    uint64(attData.Data.Slot),
+		CommIdx: 0,
+	}
+
+	if value, ok := db.attDuties[aKeyCommIdx0]; ok {
+		if value.String() != attData.Data.String() {
+			return errors.New("clashing attestation data", z.Any("key", aKeyCommIdx0))
+		}
+	} else {
+		db.attDuties[aKeyCommIdx0] = &attData.Data
+	}
+
 	return nil
 }
 
diff --git a/core/fetcher/fetcher.go b/core/fetcher/fetcher.go
index 248dea39..935d6dec 100644
--- a/core/fetcher/fetcher.go
+++ b/core/fetcher/fetcher.go
@@ -21,12 +21,13 @@ import (
 )
 
 // New returns a new fetcher instance.
-func New(eth2Cl eth2wrap.Client, feeRecipientFunc func(core.PubKey) string, builderEnabled bool, graffitiBuilder *GraffitiBuilder) (*Fetcher, error) {
+func New(eth2Cl eth2wrap.Client, feeRecipientFunc func(core.PubKey) string, builderEnabled bool, graffitiBuilder *GraffitiBuilder, electraSlot eth2p0.Slot) (*Fetcher, error) {
 	return &Fetcher{
 		eth2Cl:           eth2Cl,
 		feeRecipientFunc: feeRecipientFunc,
 		builderEnabled:   builderEnabled,
 		graffitiBuilder:  graffitiBuilder,
+		electraSlot:      electraSlot,
 	}, nil
 }
 
@@ -39,6 +40,7 @@ type Fetcher struct {
 	awaitAttDataFunc func(ctx context.Context, slot, commIdx uint64) (*eth2p0.AttestationData, error)
 	builderEnabled   bool
 	graffitiBuilder  *GraffitiBuilder
+	electraSlot      eth2p0.Slot
 }
 
 // Subscribe registers a callback for fetched duties.
@@ -126,6 +128,19 @@ func (f *Fetcher) fetchAttesterData(ctx context.Context, slot uint64, defSet cor
 
 		commIdx := attDuty.CommitteeIndex
 
+		// TODO(kalo):
+		// Attestation data for Electra is not bound by committee index.
+		// Committee index is still persisted in the request but should be set to 0.
+		// https://ethereum.github.io/beacon-APIs/#/Validator/produceAttestationData
+		// However, some validator clients are still sending attestation_data requests for each committee index.
+		// Because of that, we should continue asking for all + 0 committee indices for the ones that work correctly.
+		// In a future releases, when we know all validator clients ask only for committee index 0,
+		// we can start doing the same and uncomment the code block below.
+
+		// if slot >= uint64(f.electraSlot) {
+		// 	commIdx = 0
+		// }
+
 		eth2AttData, ok := dataByCommIdx[commIdx]
 		if !ok {
 			var err error
diff --git a/testutil/beaconmock/static.json b/testutil/beaconmock/static.json
index 6065fc90..627ebafb 100644
--- a/testutil/beaconmock/static.json
+++ b/testutil/beaconmock/static.json
@@ -129,7 +129,7 @@
       "TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH": "18446744073709551615",
       "MAX_EFFECTIVE_BALANCE": "32000000000",
       "DOMAIN_BEACON_PROPOSER": "0x00000000",
-      "DENEB_FORK_EPOCH": "29696",
+      "DENEB_FORK_EPOCH": "0",
       "DOMAIN_SYNC_COMMITTEE": "0x07000000",
       "PROPOSER_SCORE_BOOST": "40",
       "FULU_FORK_EPOCH": "18446744073709551615",
@@ -151,7 +151,7 @@
       "DOMAIN_DEPOSIT": "0x03000000",
       "DOMAIN_CONTRIBUTION_AND_PROOF": "0x09000000",
       "UPDATE_TIMEOUT": "8192",
-      "ELECTRA_FORK_EPOCH": "115968",
+      "ELECTRA_FORK_EPOCH": "2048",
       "SYNC_COMMITTEE_BRANCH_LENGTH": "5",
       "DEPOSIT_CHAIN_ID": "17000",
       "MAX_BLOB_COMMITMENTS_PER_BLOCK": "4096",



Test patch:
diff --git a/app/eth2wrap/utils_test.go b/app/eth2wrap/utils_test.go
index 142f6e19..4e60b367 100644
--- a/app/eth2wrap/utils_test.go
+++ b/app/eth2wrap/utils_test.go
@@ -3,6 +3,7 @@
 package eth2wrap_test
 
 import (
+	"encoding/hex"
 	"testing"
 	"time"
 
@@ -34,3 +35,35 @@ func TestFetchSlotsConfig(t *testing.T) {
 	require.Equal(t, 12*time.Second, slotDuration)
 	require.EqualValues(t, 16, slotsPerEpoch)
 }
+
+func TestFetchForkConfig(t *testing.T) {
+	eth2Cl, err := beaconmock.New()
+	require.NoError(t, err)
+
+	forkConfig, err := eth2wrap.FetchForkConfig(t.Context(), eth2Cl)
+	require.NoError(t, err)
+
+	aVersion, err := hex.DecodeString("20000910")
+	require.NoError(t, err)
+	bVersion, err := hex.DecodeString("30000910")
+	require.NoError(t, err)
+	cVersion, err := hex.DecodeString("40000910")
+	require.NoError(t, err)
+	dVersion, err := hex.DecodeString("50000910")
+	require.NoError(t, err)
+	eVersion, err := hex.DecodeString("60000910")
+	require.NoError(t, err)
+	fVersion, err := hex.DecodeString("70000910")
+	require.NoError(t, err)
+	ffs := eth2wrap.ForkForkSchedule{
+		eth2wrap.Altair:    eth2wrap.ForkSchedule{Epoch: 0, Version: [4]byte(aVersion)},
+		eth2wrap.Bellatrix: eth2wrap.ForkSchedule{Epoch: 0, Version: [4]byte(bVersion)},
+		eth2wrap.Capella:   eth2wrap.ForkSchedule{Epoch: 0, Version: [4]byte(cVersion)},
+		eth2wrap.Deneb:     eth2wrap.ForkSchedule{Epoch: 0, Version: [4]byte(dVersion)},
+		eth2wrap.Electra:   eth2wrap.ForkSchedule{Epoch: 2048, Version: [4]byte(eVersion)},
+		eth2wrap.Fulu:      eth2wrap.ForkSchedule{Epoch: 18446744073709551615, Version: [4]byte(fVersion)},
+	}
+
+	// Matching beaconmock/static.json
+	require.Equal(t, forkConfig, ffs)
+}
diff --git a/cmd/test.go b/cmd/test.go
index f575b081..54ae6b06 100644
--- a/cmd/test.go
+++ b/cmd/test.go
@@ -549,6 +549,9 @@ func requestRTT(ctx context.Context, url string, method string, body io.Reader,
 	if err != nil {
 		return 0, errors.Wrap(err, "create new request with trace and context")
 	}
+	if body != nil {
+		req.Header.Set("Content-Type", "application/json")
+	}
 
 	resp, err := http.DefaultTransport.RoundTrip(req)
 	if err != nil {
diff --git a/cmd/testinfra.go b/cmd/testinfra.go
index 8293fa1e..febb162c 100644
--- a/cmd/testinfra.go
+++ b/cmd/testinfra.go
@@ -12,6 +12,7 @@ import (
 	"maps"
 	"os"
 	"os/exec"
+	"path/filepath"
 	"runtime"
 	"slices"
 	"strconv"
@@ -124,6 +125,21 @@ func supportedInfraTestCases() map[testCaseName]func(context.Context, *testInfra
 func runTestInfra(ctx context.Context, w io.Writer, cfg testInfraConfig) (res testCategoryResult, err error) {
 	log.Info(ctx, "Starting hardware performance and network connectivity test")
 
+	// Checking if we have permissions to write to DiskIOTestFileDir
+	var testFilePath string
+	if cfg.DiskIOTestFileDir == "" {
+		testFilePath, err = os.UserHomeDir()
+		if err != nil {
+			return res, errors.Wrap(err, "get user home directory")
+		}
+	} else {
+		testFilePath = cfg.DiskIOTestFileDir
+	}
+	if !canWriteToDir(testFilePath) {
+		return res, errors.New("no write permissions to disk IO test file directory: " + testFilePath)
+	}
+	cfg.DiskIOTestFileDir = testFilePath
+
 	testCases := supportedInfraTestCases()
 	queuedTests := filterTests(slices.Collect(maps.Keys(testCases)), cfg.testConfig)
 	if len(queuedTests) == 0 {
@@ -232,28 +248,16 @@ func testInfra(ctx context.Context, queuedTests []testCaseName, allTests map[tes
 func infraDiskWriteSpeedTest(ctx context.Context, conf *testInfraConfig) testResult {
 	testRes := testResult{Name: "DiskWriteSpeed"}
 
-	var err error
-	var testFilePath string
-	if conf.DiskIOTestFileDir == "" {
-		testFilePath, err = os.UserHomeDir()
-		if err != nil {
-			return failedTestResult(testRes, err)
-		}
-	} else {
-		testFilePath = conf.DiskIOTestFileDir
-	}
-
 	log.Info(ctx, "Testing disk write speed...",
 		z.Any("test_file_size_mb", diskOpsMBsTotal),
 		z.Any("jobs", diskOpsNumOfJobs),
-		z.Any("test_file_path", testFilePath))
+		z.Any("test_file_path", conf.DiskIOTestFileDir))
 
-	err = conf.DiskTestTool.CheckAvailability()
-	if err != nil {
+	if err := conf.DiskTestTool.CheckAvailability(); err != nil {
 		return failedTestResult(testRes, err)
 	}
 
-	diskWriteMBs, err := conf.DiskTestTool.WriteSpeed(ctx, testFilePath, conf.DiskIOBlockSizeKb)
+	diskWriteMBs, err := conf.DiskTestTool.WriteSpeed(ctx, conf.DiskIOTestFileDir, conf.DiskIOBlockSizeKb)
 	if err != nil {
 		return failedTestResult(testRes, err)
 	}
@@ -273,28 +277,16 @@ func infraDiskWriteSpeedTest(ctx context.Context, conf *testInfraConfig) testRes
 func infraDiskWriteIOPSTest(ctx context.Context, conf *testInfraConfig) testResult {
 	testRes := testResult{Name: "DiskWriteIOPS"}
 
-	var err error
-	var testFilePath string
-	if conf.DiskIOTestFileDir == "" {
-		testFilePath, err = os.UserHomeDir()
-		if err != nil {
-			return failedTestResult(testRes, err)
-		}
-	} else {
-		testFilePath = conf.DiskIOTestFileDir
-	}
-
 	log.Info(ctx, "Testing disk write IOPS...",
 		z.Any("test_file_size_mb", diskOpsMBsTotal),
 		z.Any("jobs", diskOpsNumOfJobs),
-		z.Any("test_file_path", testFilePath))
+		z.Any("test_file_path", conf.DiskIOTestFileDir))
 
-	err = conf.DiskTestTool.CheckAvailability()
-	if err != nil {
+	if err := conf.DiskTestTool.CheckAvailability(); err != nil {
 		return failedTestResult(testRes, err)
 	}
 
-	diskWriteIOPS, err := conf.DiskTestTool.WriteIOPS(ctx, testFilePath, conf.DiskIOBlockSizeKb)
+	diskWriteIOPS, err := conf.DiskTestTool.WriteIOPS(ctx, conf.DiskIOTestFileDir, conf.DiskIOBlockSizeKb)
 	if err != nil {
 		return failedTestResult(testRes, err)
 	}
@@ -314,28 +306,16 @@ func infraDiskWriteIOPSTest(ctx context.Context, conf *testInfraConfig) testResu
 func infraDiskReadSpeedTest(ctx context.Context, conf *testInfraConfig) testResult {
 	testRes := testResult{Name: "DiskReadSpeed"}
 
-	var err error
-	var testFilePath string
-	if conf.DiskIOTestFileDir == "" {
-		testFilePath, err = os.UserHomeDir()
-		if err != nil {
-			return failedTestResult(testRes, err)
-		}
-	} else {
-		testFilePath = conf.DiskIOTestFileDir
-	}
-
 	log.Info(ctx, "Testing disk read speed...",
 		z.Any("test_file_size_mb", diskOpsMBsTotal),
 		z.Any("jobs", diskOpsNumOfJobs),
-		z.Any("test_file_path", testFilePath))
+		z.Any("test_file_path", conf.DiskIOTestFileDir))
 
-	err = conf.DiskTestTool.CheckAvailability()
-	if err != nil {
+	if err := conf.DiskTestTool.CheckAvailability(); err != nil {
 		return failedTestResult(testRes, err)
 	}
 
-	diskReadMBs, err := conf.DiskTestTool.ReadSpeed(ctx, testFilePath, conf.DiskIOBlockSizeKb)
+	diskReadMBs, err := conf.DiskTestTool.ReadSpeed(ctx, conf.DiskIOTestFileDir, conf.DiskIOBlockSizeKb)
 	if err != nil {
 		return failedTestResult(testRes, err)
 	}
@@ -836,3 +816,16 @@ func fetchOoklaServer(_ context.Context, conf *testInfraConfig) (speedtest.Serve
 
 	return *servers[0], nil
 }
+
+func canWriteToDir(dir string) bool {
+	testFile := filepath.Join(dir, ".perm_test_tmp")
+	f, err := os.Create(testFile)
+	if err != nil {
+		return false
+	}
+
+	f.Close()
+	os.Remove(testFile)
+
+	return true
+}
diff --git a/core/deadline_test.go b/core/deadline_test.go
index f9abd2c3..08e04228 100644
--- a/core/deadline_test.go
+++ b/core/deadline_test.go
@@ -94,7 +94,7 @@ func TestNewDutyDeadlineFunc(t *testing.T) {
 	slotDuration, _, err := eth2wrap.FetchSlotsConfig(t.Context(), bmock)
 	require.NoError(t, err)
 
-	margin := slotDuration / 24
+	margin := slotDuration / 12
 	currentSlot := uint64(time.Since(genesisTime) / slotDuration)
 	now := genesisTime.Add(time.Duration(currentSlot) * slotDuration)
 
diff --git a/core/fetcher/fetcher_test.go b/core/fetcher/fetcher_test.go
index b2871108..dd66b80e 100644
--- a/core/fetcher/fetcher_test.go
+++ b/core/fetcher/fetcher_test.go
@@ -538,7 +538,7 @@ func TestFetchSyncContribution(t *testing.T) {
 func mustCreateFetcher(t *testing.T, bmock beaconmock.Mock) *fetcher.Fetcher {
 	t.Helper()
 
-	fetch, err := fetcher.New(bmock, nil, true, &fetcher.GraffitiBuilder{})
+	fetch, err := fetcher.New(bmock, nil, true, &fetcher.GraffitiBuilder{}, 5)
 	require.NoError(t, err)
 
 	return fetch
@@ -549,7 +549,7 @@ func mustCreateFetcherWithAddressAndGraffiti(t *testing.T, bmock beaconmock.Mock
 
 	fetch, err := fetcher.New(bmock, func(core.PubKey) string {
 		return addr
-	}, true, graffitiBuilder)
+	}, true, graffitiBuilder, 5)
 	require.NoError(t, err)
 
 	return fetch
diff --git a/testutil/beaconmock/beaconmock.go b/testutil/beaconmock/beaconmock.go
index b47f9a6b..7d856a49 100644
--- a/testutil/beaconmock/beaconmock.go
+++ b/testutil/beaconmock/beaconmock.go
@@ -98,6 +98,66 @@ func defaultHTTPMock() Mock {
 				Key:      "SECONDS_PER_SLOT",
 				Value:    "12",
 			},
+			{
+				Endpoint: "/eth/v1/config/spec",
+				Key:      "ALTAIR_FORK_VERSION",
+				Value:    "0x20000910",
+			},
+			{
+				Endpoint: "/eth/v1/config/spec",
+				Key:      "ALTAIR_FORK_EPOCH",
+				Value:    "0",
+			},
+			{
+				Endpoint: "/eth/v1/config/spec",
+				Key:      "BELLATRIX_FORK_VERSION",
+				Value:    "0x30000910",
+			},
+			{
+				Endpoint: "/eth/v1/config/spec",
+				Key:      "BELLATRIX_FORK_EPOCH",
+				Value:    "0",
+			},
+			{
+				Endpoint: "/eth/v1/config/spec",
+				Key:      "CAPELLA_FORK_VERSION",
+				Value:    "0x40000910",
+			},
+			{
+				Endpoint: "/eth/v1/config/spec",
+				Key:      "CAPELLA_FORK_EPOCH",
+				Value:    "0",
+			},
+			{
+				Endpoint: "/eth/v1/config/spec",
+				Key:      "DENEB_FORK_VERSION",
+				Value:    "0x50000910",
+			},
+			{
+				Endpoint: "/eth/v1/config/spec",
+				Key:      "DENEB_FORK_EPOCH",
+				Value:    "0",
+			},
+			{
+				Endpoint: "/eth/v1/config/spec",
+				Key:      "ELECTRA_FORK_VERSION",
+				Value:    "0x60000910",
+			},
+			{
+				Endpoint: "/eth/v1/config/spec",
+				Key:      "ELECTRA_FORK_EPOCH",
+				Value:    "2048",
+			},
+			{
+				Endpoint: "/eth/v1/config/spec",
+				Key:      "FULU_FORK_VERSION",
+				Value:    "0x70000910",
+			},
+			{
+				Endpoint: "/eth/v1/config/spec",
+				Key:      "FULU_FORK_EPOCH",
+				Value:    "18446744073709551615",
+			},
 			{
 				Endpoint: "/eth/v1/beacon/genesis",
 				Key:      "genesis_time",
diff --git a/testutil/beaconmock/server_test.go b/testutil/beaconmock/server_test.go
index 80b421c6..5acf2550 100644
--- a/testutil/beaconmock/server_test.go
+++ b/testutil/beaconmock/server_test.go
@@ -26,7 +26,8 @@ func TestStatic(t *testing.T) {
 	configResp, err := eth2Cl.Spec(ctx, &eth2api.SpecOpts{})
 	require.NoError(t, err)
 	require.Equal(t, uint64(0), configResp.Data["ALTAIR_FORK_EPOCH"])
-	require.Equal(t, uint64(29696), configResp.Data["DENEB_FORK_EPOCH"])
+	require.Equal(t, uint64(0), configResp.Data["DENEB_FORK_EPOCH"])
+	require.Equal(t, uint64(2048), configResp.Data["ELECTRA_FORK_EPOCH"])
 
 	contractResp, err := eth2Cl.DepositContract(ctx, &eth2api.DepositContractOpts{})
 	require.NoError(t, err)