Repository: ObolNetwork/charon

Issue:
Add `Date-Milliseconds` and X-Timeout-Ms` to `charon alpha test mev`
# 🎯 Problem to be solved

Our [MEV test](https://docs.obol.org/learn/charon/charon-cli-reference#test-mev) command doesn't include expected headers, lets add them. 

# 🛠️ Proposed solution

Lets add the two headers as per the spec https://ethereum.github.io/builder-specs/?urls.primaryName=dev#/Builder/getHeader


# 👐 Additional acceptance criteria

We could test that they are being respected by giving a short timeout and seeing if we get an answer (e.g. 200ms)

# ❌ Out of Scope






Discussion:
@KaloyanTanev: They are not mandatory by spec, but doesn't hurt to add them indeed.

Solution patch:
diff --git a/.github/workflows/build-push-deploy.yml b/.github/workflows/build-push-deploy.yml
index 8d9bf02e..7d54013e 100644
--- a/.github/workflows/build-push-deploy.yml
+++ b/.github/workflows/build-push-deploy.yml
@@ -17,7 +17,7 @@ jobs:
     - name: Set up QEMU
       id: qemu
       uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
-    - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
+    - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
       with:
         driver-opts: "image=moby/buildkit:v0.10.5" # avoid unknown/unknown arch in ghcr
 
@@ -27,7 +27,7 @@ jobs:
 
     - name: Define docker image meta data tags
       id: meta
-      uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
+      uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
       with:
         images: |
           obolnetwork/charon
@@ -53,14 +53,14 @@ jobs:
           type=ref,event=tag
 
     - name: Login to Github container registry
-      uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
+      uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
       with:
         registry: ghcr.io
         username: ${{ github.actor }}
         password: ${{ secrets.GITHUB_TOKEN }}
 
     - name: Login to Dockerhub container registry
-      uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
+      uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
       with:
         username: obolnetwork
         password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -69,7 +69,7 @@ jobs:
       if: github.ref_type == 'tag'
       run: echo 'GO_BUILD_FLAG=-ldflags=-X github.com/obolnetwork/charon/app/version.version=${{ github.ref_name }}' >> $GITHUB_ENV
 
-    - uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
+    - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
       with:
         context: .
         platforms: linux/amd64,linux/arm64
diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml
index 9acc0271..27d816a8 100644
--- a/.github/workflows/build-release-binaries.yml
+++ b/.github/workflows/build-release-binaries.yml
@@ -96,7 +96,7 @@ jobs:
     - name: Attach to GitHub Release
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      uses: softprops/action-gh-release@d5382d3e6f2fa7bd53cb749d33091853d4985daf # v2.3.0
+      uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
       with:
         files: |
           dist/charon-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz
diff --git a/.github/workflows/compose-tests.yml b/.github/workflows/compose-tests.yml
index e019d0b5..cd815042 100644
--- a/.github/workflows/compose-tests.yml
+++ b/.github/workflows/compose-tests.yml
@@ -17,11 +17,11 @@ jobs:
       - name: Set up QEMU
         id: qemu
         uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
-      - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
+      - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
         with:
           driver-opts: "image=moby/buildkit:v0.10.5"
       - uses: ./.github/actions/setup-go
-      - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
+      - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
         with:
           path: |
             ~/go/pkg/mod
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index 6161e131..98c3c689 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -15,7 +15,7 @@ jobs:
     steps:
       - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
       - uses: ./.github/actions/setup-go
-      - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
+      - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
         with:
           path: |
             ~/go/pkg/mod
diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml
index 500072c4..d1268edc 100644
--- a/.github/workflows/nightly-tests.yml
+++ b/.github/workflows/nightly-tests.yml
@@ -10,7 +10,7 @@ jobs:
     steps:
       - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
       - uses: ./.github/actions/setup-go
-      - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
+      - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
         with:
           path: |
             ~/go/pkg/mod
@@ -27,11 +27,11 @@ jobs:
       - name: Set up QEMU
         id: qemu
         uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
-      - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
+      - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
         with:
           driver-opts: "image=moby/buildkit:v0.10.5" # avoid unknown/unknown arch in ghcr
       - uses: ./.github/actions/setup-go
-      - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
+      - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
         with:
           path: |
             ~/go/pkg/mod
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5b03fe28..38a5ca56 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,7 +25,7 @@ jobs:
         run: go run testutil/genchangelog/main.go
 
       - name: Create GitHub release draft
-        uses: softprops/action-gh-release@d5382d3e6f2fa7bd53cb749d33091853d4985daf # v2.3.0
+        uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
         with:
           draft: true
           files: cli-reference.txt
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index f46d9466..c9c881ac 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -15,7 +15,7 @@ jobs:
     steps:
       - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
       - uses: ./.github/actions/setup-go
-      - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
+      - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
         with:
           path: |
             ~/go/pkg/mod
diff --git a/Dockerfile b/Dockerfile
index 79f17d87..cf97107c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # Container for building Go binary.
-FROM golang:1.25.3-bookworm AS builder
+FROM golang:1.25.4-bookworm AS builder
 # Install dependencies
 RUN apt-get update && apt-get install -y --no-install-recommends build-essential git
 
diff --git a/go.mod b/go.mod
index 09ea7a7f..596a65d3 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/gorilla/mux v1.8.1
 	github.com/herumi/bls-eth-go-binary v1.36.4
 	github.com/holiman/uint256 v1.3.2
-	github.com/ipfs/go-log/v2 v2.8.2
+	github.com/ipfs/go-log/v2 v2.9.0
 	github.com/jonboulle/clockwork v0.5.0
 	github.com/jsternberg/zap-logfmt v1.3.0
 	github.com/libp2p/go-libp2p v0.43.0
diff --git a/go.sum b/go.sum
index e366f438..4b2eccd9 100644
--- a/go.sum
+++ b/go.sum
@@ -300,8 +300,8 @@ github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmne
 github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
 github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
 github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
-github.com/ipfs/go-log/v2 v2.8.2 h1:nVG4nNHUwwI/sTs9Bi5iE8sXFQwXs3AjkkuWhg7+Y2I=
-github.com/ipfs/go-log/v2 v2.8.2/go.mod h1:UhIYAwMV7Nb4ZmihUxfIRM2Istw/y9cAk3xaK+4Zs2c=
+github.com/ipfs/go-log/v2 v2.9.0 h1:l4b06AwVXwldIzbVPZy5z7sKp9lHFTX0KWfTBCtHaOk=
+github.com/ipfs/go-log/v2 v2.9.0/go.mod h1:UhIYAwMV7Nb4ZmihUxfIRM2Istw/y9cAk3xaK+4Zs2c=
 github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
 github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
 github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=



Test patch:
diff --git a/cmd/testmev.go b/cmd/testmev.go
index 2e189aab..d6206727 100644
--- a/cmd/testmev.go
+++ b/cmd/testmev.go
@@ -41,6 +41,7 @@ type testMEVConfig struct {
 	BeaconNodeEndpoint string
 	LoadTest           bool
 	NumberOfPayloads   uint
+	XTimeoutMs         uint
 }
 
 type testCaseMEV func(context.Context, *testMEVConfig, string) testResult
@@ -97,6 +98,7 @@ func bindTestMEVFlags(cmd *cobra.Command, config *testMEVConfig, flagsPrefix str
 	cmd.Flags().StringVar(&config.BeaconNodeEndpoint, flagsPrefix+"beacon-node-endpoint", "", "[REQUIRED] Beacon node endpoint URL used for block creation test.")
 	cmd.Flags().BoolVar(&config.LoadTest, flagsPrefix+"load-test", false, "Enable load test.")
 	cmd.Flags().UintVar(&config.NumberOfPayloads, flagsPrefix+"number-of-payloads", 1, "Increases the accuracy of the load test by asking for multiple payloads. Increases test duration.")
+	cmd.Flags().UintVar(&config.XTimeoutMs, flagsPrefix+"x-timeout-ms", 1000, "X-Timeout-Ms header flag for each request in milliseconds, used by MEVs to compute maximum delay for reply.")
 	mustMarkFlagRequired(cmd, flagsPrefix+"endpoints")
 }
 
@@ -342,12 +344,16 @@ func mevCreateBlockTest(ctx context.Context, conf *testMEVConfig, target string)
 
 	allBlocksRTT := []time.Duration{}
 
-	log.Info(ctx, "Starting attempts for block creation", z.Any("mev_relay", target), z.Any("blocks", conf.NumberOfPayloads))
+	headers := map[string]string{
+		"X-Timeout-Ms": strconv.FormatUint(uint64(conf.XTimeoutMs), 10),
+	}
+
+	log.Info(ctx, "Starting attempts for block creation", z.Any("mev_relay", target), z.Any("blocks", conf.NumberOfPayloads), z.Uint("x-timeout-ms", conf.XTimeoutMs))
 
 	for ctx.Err() == nil {
 		startIteration := time.Now()
 
-		rtt, err := createMEVBlock(ctx, conf, target, nextSlot, latestBlock, proposerDuties)
+		rtt, err := createMEVBlock(ctx, conf, target, headers, nextSlot, latestBlock, proposerDuties)
 		if err != nil {
 			return failedTestResult(testRes, err)
 		}
@@ -382,7 +388,7 @@ func mevCreateBlockTest(ctx context.Context, conf *testMEVConfig, target string)
 
 	averageRTT := totalRTT / time.Duration(len(allBlocksRTT))
 
-	testRes = evaluateRTT(averageRTT, testRes, thresholdMEVBlockAvg, thresholdMEVBlockPoor)
+	testRes = evaluateRTT(averageRTT, testRes, time.Duration(conf.XTimeoutMs*9/10)*time.Millisecond, time.Duration(conf.XTimeoutMs)*time.Millisecond)
 
 	return testRes
 }
@@ -413,7 +419,7 @@ func formatMEVRelayName(urlString string) string {
 	return splitScheme[0] + "://" + hashShort + "@" + hashSplit[1]
 }
 
-func getBlockHeader(ctx context.Context, target string, nextSlot int64, blockHash string, validatorPubKey string) (builderspec.VersionedSignedBuilderBid, time.Duration, error) {
+func getBlockHeader(ctx context.Context, target string, headers map[string]string, nextSlot int64, blockHash string, validatorPubKey string) (builderspec.VersionedSignedBuilderBid, time.Duration, error) {
 	var (
 		start     time.Time
 		firstByte time.Duration
@@ -435,6 +441,11 @@ func getBlockHeader(ctx context.Context, target string, nextSlot int64, blockHas
 		return builderspec.VersionedSignedBuilderBid{}, 0, errors.Wrap(err, "http request")
 	}
 
+	for k, v := range headers {
+		req.Header.Set(k, v)
+	}
+	req.Header.Set("Date-Milliseconds", strconv.FormatInt(time.Now().UnixMilli(), 10))
+
 	resp, err := http.DefaultTransport.RoundTrip(req)
 	if err != nil {
 		return builderspec.VersionedSignedBuilderBid{}, 0, errors.Wrap(err, "http request rtt")
@@ -463,7 +474,7 @@ func getBlockHeader(ctx context.Context, target string, nextSlot int64, blockHas
 	return builderBid, rttGetHeader, nil
 }
 
-func createMEVBlock(ctx context.Context, conf *testMEVConfig, target string, nextSlot int64, latestBlock BeaconBlockMessage, proposerDuties []ProposerDutiesData) (time.Duration, error) {
+func createMEVBlock(ctx context.Context, conf *testMEVConfig, target string, headers map[string]string, nextSlot int64, latestBlock BeaconBlockMessage, proposerDuties []ProposerDutiesData) (time.Duration, error) {
 	var (
 		rttGetHeader time.Duration
 		builderBid   builderspec.VersionedSignedBuilderBid
@@ -487,7 +498,7 @@ func createMEVBlock(ctx context.Context, conf *testMEVConfig, target string, nex
 			}
 		}
 
-		builderBid, rttGetHeader, err = getBlockHeader(ctx, target, nextSlot, latestBlock.Body.ExecutionPayload.BlockHash, validatorPubKey)
+		builderBid, rttGetHeader, err = getBlockHeader(ctx, target, headers, nextSlot, latestBlock.Body.ExecutionPayload.BlockHash, validatorPubKey)
 		if err != nil {
 			// the current proposer was not registered with the builder, wait for next block
 			if errors.Is(err, errStatusCodeNot200) {