Repository: ObolNetwork/charon

Issue:
Add optional --publish flags to our test commands
# 🎯 Problem to be solved

People are open to giving anonymous and maybe pseudonymous performance data, to get a better understanding of their node's health, and how best to optimise it. 

Right now they fill out forms by hand, translating what their CLI test output says. We then manually translate that into charts and reports. We will need to simplify this to scale it. 

# 🛠️ Proposed solution

We could add an optional `--publish` flag (and `--publish-address`), like the rest of our commands, that pushes to our API. 

- [ ] Approved design doc: *link*
- [ ] Core team consensus on the proposed solution

Describe the solution to be implemented

# 🧪 Tests
- [ ] Tested by new automated unit/integration/smoke tests
- [ ] Manually tested on core team/canary/test clusters
- [ ] Manually tested on local compose simnet



# 👐 Additional acceptance criteria
- [ ] All of the related impact on Launchpad/API team 


# ❌ Out of Scope

In future we could also return a launchpad URI like `https://launchpad.obol.org/operator/<address>/<enr>/` that shows a more fancy test report. 





Discussion:
@LukeHackett12: sample output here would be great 👍🏼 @LukeHackett12: ```
{
  "charon_peers": {
    "category_name": "peers",
    "targets": {
      "peer anxious-pencil enr:-HW4QDwUF...vKDw": [
        {
          "name": "ping",
          "verdict": "Fail",
          "error": "timeout/interrupted"
        }
      ],
      "peer inexpensive-farm enr:-HW4QBHlc...rx6o": [
        {
          "name": "ping",
          "verdict": "Fail",
          "error": "timeout/interrupted"
        }
      ],
      "relay https://0.relay.obol.tech": [
        {
          "name": "pingRelay",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "pingMeasureRelay",
          "verdict": "Poor",
          "measurement": "290.590325ms",
          "error": ""
        }
      ],
      "relay https://1.relay.obol.tech": [
        {
          "name": "pingRelay",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "pingMeasureRelay",
          "verdict": "Poor",
          "measurement": "294.153029ms",
          "error": ""
        }
      ],
      "relay https://2.relay.obol.dev": [
        {
          "name": "pingRelay",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "pingMeasureRelay",
          "verdict": "Avg",
          "measurement": "70.054395ms",
          "error": ""
        }
      ],
      "self": [
        {
          "name": "libp2pTCPPortOpenTest",
          "verdict": "OK",
          "error": ""
        }
      ]
    },
    "execution_time": "5m23.870156326s",
    "score": "C"
  },
  "beacon_node": {
    "category_name": "beacon",
    "targets": {
      "http://172.31.41.191:5052": [
        {
          "name": "ping",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "pingMeasure",
          "verdict": "Good",
          "measurement": "439.69µs",
          "error": ""
        },
        {
          "name": "version",
          "verdict": "OK",
          "measurement": "Lighthouse/v5.3.0-d6ba8c3/x86_64-linux",
          "error": ""
        },
        {
          "name": "isSynced",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "peerCount",
          "verdict": "Good",
          "measurement": "97",
          "error": ""
        },
        {
          "name": "pingLoad",
          "verdict": "Good",
          "measurement": "2.533179ms",
          "error": ""
        },
        {
          "name": "simulate1",
          "verdict": "Good",
          "measurement": "4.768451ms",
          "error": ""
        },
        {
          "name": "simulate10",
          "verdict": "Good",
          "measurement": "22.450994ms",
          "error": ""
        },
        {
          "name": "simulate100",
          "verdict": "Good",
          "measurement": "141.707804ms",
          "error": ""
        },
        {
          "name": "simulate500",
          "verdict": "Poor",
          "measurement": "924.419033ms",
          "error": ""
        },
        {
          "name": "simulate1000",
          "verdict": "Poor",
          "measurement": "5.860375455s",
          "error": ""
        }
      ],
      "http://172.31.46.232:5052": [
        {
          "name": "ping",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "pingMeasure",
          "verdict": "Good",
          "measurement": "20.650376ms",
          "error": ""
        },
        {
          "name": "version",
          "verdict": "OK",
          "measurement": "Lighthouse/v5.3.0-d6ba8c3/x86_64-linux",
          "error": ""
        },
        {
          "name": "isSynced",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "peerCount",
          "verdict": "Good",
          "measurement": "98",
          "error": ""
        },
        {
          "name": "pingLoad",
          "verdict": "Good",
          "measurement": "2.163099ms",
          "error": ""
        },
        {
          "name": "simulate1",
          "verdict": "Good",
          "measurement": "4.227453ms",
          "error": ""
        },
        {
          "name": "simulate10",
          "verdict": "Good",
          "measurement": "17.923884ms",
          "error": ""
        },
        {
          "name": "simulate100",
          "verdict": "Good",
          "measurement": "105.057051ms",
          "error": ""
        },
        {
          "name": "simulate500",
          "verdict": "Poor",
          "measurement": "856.391625ms",
          "error": ""
        },
        {
          "name": "simulate1000",
          "verdict": "Poor",
          "measurement": "5.039197199s",
          "error": ""
        }
      ]
    },
    "execution_time": "32m12.279742595s",
    "score": "C"
  },
  "validator_client": {
    "category_name": "validator",
    "targets": {
      "127.0.0.1:3600": [
        {
          "name": "ping",
          "verdict": "Fail",
          "error": "dial tcp 127.0.0.1:3600: connect: connection refused"
        },
        {
          "name": "pingMeasure",
          "verdict": "Fail",
          "error": "dial tcp 127.0.0.1:3600: connect: connection refused"
        },
        {
          "name": "pingLoad",
          "verdict": "Poor",
          "measurement": "0s",
          "error": ""
        }
      ]
    },
    "execution_time": "5.001680394s",
    "score": "C"
  },
  "mev": {
    "category_name": "mev",
    "targets": {
      "global.titanrelay.xyz": [
        {
          "name": "ping",
          "verdict": "Fail",
          "error": "Get \"global.titanrelay.xyz/eth/v1/builder/status\": unsupported protocol scheme \"\""
        },
        {
          "name": "pingMeasure",
          "verdict": "Fail",
          "error": "unsupported protocol scheme \"\""
        },
        {
          "name": "createBlock",
          "verdict": "Fail",
          "error": "http request rtt: unsupported protocol scheme \"\""
        },
        {
          "name": "createMultipleBlocks",
          "verdict": "Fail",
          "error": "http request rtt: unsupported protocol scheme \"\""
        }
      ],
      "https://0xa7ab...9561@agnostic-relay.net": [
        {
          "name": "ping",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "pingMeasure",
          "verdict": "Poor",
          "measurement": "100.791401ms",
          "error": ""
        },
        {
          "name": "createBlock",
          "verdict": "Good",
          "measurement": "225.253982ms",
          "error": ""
        },
        {
          "name": "createMultipleBlocks",
          "verdict": "Good",
          "measurement": "231.100722ms",
          "error": ""
        }
      ],
      "https://0xac6e...37ae@boost-relay.flashbots.net": [
        {
          "name": "ping",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "pingMeasure",
          "verdict": "Good",
          "measurement": "23.086927ms",
          "error": ""
        },
        {
          "name": "createBlock",
          "verdict": "Good",
          "measurement": "62.468014ms",
          "error": ""
        },
        {
          "name": "createMultipleBlocks",
          "verdict": "Good",
          "measurement": "71.132422ms",
          "error": ""
        }
      ]
    },
    "execution_time": "39.704745313s",
    "score": "A"
  },
  "infra": {
    "category_name": "infra",
    "targets": {
      "local": [
        {
          "name": "diskWriteSpeed",
          "verdict": "Poor",
          "measurement": "129.1621MB/s",
          "error": ""
        },
        {
          "name": "diskWriteIOPS",
          "verdict": "Poor",
          "measurement": "32",
          "error": ""
        },
        {
          "name": "diskReadSpeed",
          "verdict": "Poor",
          "measurement": "126.6416MB/s",
          "error": ""
        },
        {
          "name": "diskReadIOPS",
          "verdict": "Poor",
          "measurement": "32",
          "error": ""
        },
        {
          "name": "availableMemory",
          "verdict": "Avg",
          "measurement": "3228MB",
          "error": ""
        },
        {
          "name": "totalMemory",
          "verdict": "Poor",
          "measurement": "3836MB",
          "error": ""
        },
        {
          "name": "internetLatency",
          "verdict": "Good",
          "measurement": "2.112ms",
          "error": ""
        },
        {
          "name": "internetDownloadSpeed",
          "verdict": "Good",
          "measurement": "3134.84MB/s",
          "error": ""
        },
        {
          "name": "internetUploadSpeed",
          "verdict": "Good",
          "measurement": "412.36MB/s",
          "error": ""
        }
      ]
    },
    "execution_time": "2m40.865715161s",
    "score": "C"
  }
}
```@LukeHackett12: ```
{
  "charon_peers": {
    "execution_time": "0s"
  },
  "beacon_node": {
    "execution_time": "0s"
  },
  "validator_client": {
    "execution_time": "0s"
  },
  "mev": {
    "category_name": "mev",
    "targets": {
      "https://0x8b5d...6b8f@bloxroute.max-profit.blxrbdn.com": [
        {
          "name": "Ping",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "PingMeasure",
          "verdict": "Poor",
          "measurement": "180ms",
          "error": ""
        },
        {
          "name": "CreateBlock",
          "verdict": "Skip",
          "error": ""
        }
      ],
      "https://0x8c4e...677d@titanrelay.xyz": [
        {
          "name": "Ping",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "PingMeasure",
          "verdict": "Good",
          "measurement": "28ms",
          "error": ""
        },
        {
          "name": "CreateBlock",
          "verdict": "Skip",
          "error": ""
        }
      ],
      "https://0xa155...7a62@relay.ultrasound.money": [
        {
          "name": "Ping",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "PingMeasure",
          "verdict": "Good",
          "measurement": "28ms",
          "error": ""
        },
        {
          "name": "CreateBlock",
          "verdict": "Skip",
          "error": ""
        }
      ],
      "https://0xa15b...ab7e@aestus.live": [
        {
          "name": "Ping",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "PingMeasure",
          "verdict": "Good",
          "measurement": "24ms",
          "error": ""
        },
        {
          "name": "CreateBlock",
          "verdict": "Skip",
          "error": ""
        }
      ],
      "https://0xa7ab...9561@agnostic-relay.net": [
        {
          "name": "Ping",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "PingMeasure",
          "verdict": "Avg",
          "measurement": "73ms",
          "error": ""
        },
        {
          "name": "CreateBlock",
          "verdict": "Skip",
          "error": ""
        }
      ],
      "https://0xac6e...37ae@boost-relay.flashbots.net": [
        {
          "name": "Ping",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "PingMeasure",
          "verdict": "Poor",
          "measurement": "279ms",
          "error": ""
        },
        {
          "name": "CreateBlock",
          "verdict": "Skip",
          "error": ""
        }
      ],
      "https://0xb0b0...fe88@bloxroute.regulated.blxrbdn.com": [
        {
          "name": "Ping",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "PingMeasure",
          "verdict": "Poor",
          "measurement": "185ms",
          "error": ""
        },
        {
          "name": "CreateBlock",
          "verdict": "Skip",
          "error": ""
        }
      ],
      "https://0xb3ee...18be@relay.edennetwork.io": [
        {
          "name": "Ping",
          "verdict": "OK",
          "error": ""
        },
        {
          "name": "PingMeasure",
          "verdict": "Poor",
          "measurement": "112ms",
          "error": ""
        },
        {
          "name": "CreateBlock",
          "verdict": "Skip",
          "error": ""
        }
      ]
    },
    "execution_time": "636.089125ms",
    "score": "A"
  },
  "infra": {
    "execution_time": "0s"
  }
}
```

Solution patch:
diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml
index 8fc63428..0528c037 100644
--- a/.github/actions/setup-go/action.yml
+++ b/.github/actions/setup-go/action.yml
@@ -1,9 +1,20 @@
 name: Setup go
 
+outputs:
+  go-version:
+    description: "The Go version that was set up"
+    value: ${{ steps.setup-go.outputs.go-version }}
+
 runs:
   using: "composite"
   steps:
     - name: Setup go
+      id: setup-go
       uses: actions/setup-go@v5
       with:
         go-version: "1.24"
+
+    - name: Verify setup
+      shell: bash
+      run: |
+        echo "Go version: $(go version)"
diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml
new file mode 100644
index 00000000..9bf65df7
--- /dev/null
+++ b/.github/workflows/build-release-binaries.yml
@@ -0,0 +1,83 @@
+name: Build and Release Binaries
+
+on:
+  workflow_run:
+    workflows: ["Publish Release"]
+    types:
+      - completed
+
+jobs:
+  build-binaries:
+    # Only run if the triggering workflow succeeded and it was triggered by a tag
+    if: >
+      github.event.workflow_run.conclusion == 'success' &&
+      startsWith(github.event.workflow_run.head_branch, 'refs/tags/v')
+    runs-on: ubuntu-24.04
+    name: Build Cross-Platform Binaries
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        fetch-depth: 0
+
+    - uses: ./.github/actions/setup-go
+      id: setup-go
+
+    - name: Set version from git tag
+      if: github.ref_type == 'tag'
+      run: |
+        echo "APP_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV
+        echo "RELEASE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV
+        echo "LDFLAGS_VALUE=-X github.com/obolnetwork/charon/app/version.version=${{ github.ref_name }}" >> $GITHUB_ENV
+
+    - name: Build x86_64 binary
+      run: |
+        docker run --rm --platform linux/amd64 \
+          -v $(pwd):/workspace -w /workspace \
+          golang:${{ steps.setup-go.outputs.go-version }}-bookworm \
+          bash -c "apt-get update && apt-get install -y build-essential && \
+          env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC=x86_64-linux-gnu-gcc \
+          go build -tags='netgo osusergo' -ldflags \"${{ env.LDFLAGS_VALUE }} -extldflags '-static'\" \
+          -o dist/charon-${{ env.RELEASE_VERSION }}-linux-amd64"
+
+    - name: Build ARM64 binary
+      run: |
+        docker run --rm --platform linux/arm64 \
+          -v $(pwd):/workspace -w /workspace \
+          golang:${{ steps.setup-go.outputs.go-version }}-bookworm \
+          bash -c "apt-get update && \
+          apt-get install -y build-essential && \
+          env CGO_ENABLED=1 GOOS=linux GOARCH=arm64 \
+          go build -tags='netgo osusergo' -ldflags \"${{ env.LDFLAGS_VALUE }} -extldflags '-static'\" \
+          -o dist/charon-${{ env.RELEASE_VERSION }}-linux-arm64"
+
+    - name: Create release archives
+      run: |
+        cd dist
+        tar czf charon-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz charon-${{ env.RELEASE_VERSION }}-linux-amd64
+        tar czf charon-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz charon-${{ env.RELEASE_VERSION }}-linux-arm64
+        echo "# Linux AMD64 (x86_64)" > checksums.txt
+        sha256sum charon-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz >> checksums.txt
+        echo -e "\n# Linux ARM64 (aarch64)" >> checksums.txt
+        sha256sum charon-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz >> checksums.txt
+
+    - name: Upload artifacts
+      uses: actions/upload-artifact@v4
+      with:
+        name: charon-${{ env.RELEASE_VERSION }}-binaries
+        path: |
+          dist/charon-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz
+          dist/charon-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz
+          dist/checksums.txt
+
+    - name: Attach to GitHub Release
+      if: github.event.workflow_run.conclusion == 'success'
+      uses: softprops/action-gh-release@v2
+      with:
+        files: |
+          dist/charon-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz
+          dist/charon-${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz
+          dist/checksums.txt
+        draft: true
+        tag_name: ${{ github.event.workflow_run.head_branch }}
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/cmd/createcluster.go b/cmd/createcluster.go
index c65548af..842b86cf 100644
--- a/cmd/createcluster.go
+++ b/cmd/createcluster.go
@@ -227,7 +227,7 @@ func runCreateCluster(ctx context.Context, w io.Writer, conf clusterConfig) erro
 	}
 
 	if len(depositAmounts) == 0 {
-		depositAmounts = deposit.DefaultDepositAmounts()
+		depositAmounts = deposit.DefaultDepositAmounts(conf.Compounding)
 	}
 
 	if len(secrets) == 0 {
diff --git a/dkg/dkg.go b/dkg/dkg.go
index 0ac82cca..6d9ebc08 100644
--- a/dkg/dkg.go
+++ b/dkg/dkg.go
@@ -253,7 +253,7 @@ func Run(ctx context.Context, conf Config) (err error) {
 	depositAmounts := def.DepositAmounts
 	if len(depositAmounts) == 0 {
 		if cluster.SupportPartialDeposits(def.Version) {
-			depositAmounts = deposit.DefaultDepositAmounts()
+			depositAmounts = deposit.DefaultDepositAmounts(def.Compounding)
 		} else {
 			depositAmounts = []eth2p0.Gwei{deposit.DefaultDepositAmount}
 		}
diff --git a/eth2util/deposit/deposit.go b/eth2util/deposit/deposit.go
index 57178567..8153aa94 100644
--- a/eth2util/deposit/deposit.go
+++ b/eth2util/deposit/deposit.go
@@ -291,8 +291,14 @@ func DedupAmounts(amounts []eth2p0.Gwei) []eth2p0.Gwei {
 	return result
 }
 
-// DefaultDepositAmounts returns the default deposit amounts: 1ETH and 32ETH.
-func DefaultDepositAmounts() []eth2p0.Gwei {
+// DefaultDepositAmounts returns the default deposit amounts:
+// --compounding=false: [1,32] ETH,
+// --compounding=true: [1,8,32,256] ETH.
+func DefaultDepositAmounts(compounding bool) []eth2p0.Gwei {
+	if compounding {
+		return []eth2p0.Gwei{MinDepositAmount, 8 * OneEthInGwei, 32 * OneEthInGwei, 256 * OneEthInGwei}
+	}
+
 	return []eth2p0.Gwei{MinDepositAmount, DefaultDepositAmount}
 }
 
diff --git a/go.mod b/go.mod
index a25aed2e..faee0296 100644
--- a/go.mod
+++ b/go.mod
@@ -20,7 +20,7 @@ require (
 	github.com/libp2p/go-libp2p v0.41.1
 	github.com/libp2p/go-msgio v0.3.0
 	github.com/multiformats/go-multiaddr v0.15.0
-	github.com/prometheus/client_golang v1.21.1
+	github.com/prometheus/client_golang v1.22.0
 	github.com/prometheus/client_model v0.6.1
 	github.com/protolambda/eth2-shuffle v1.1.0
 	github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15
@@ -41,12 +41,12 @@ require (
 	go.uber.org/automaxprocs v1.6.0
 	go.uber.org/goleak v1.3.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/crypto v0.36.0
-	golang.org/x/sync v0.12.0
-	golang.org/x/term v0.30.0
-	golang.org/x/text v0.23.0
+	golang.org/x/crypto v0.37.0
+	golang.org/x/sync v0.13.0
+	golang.org/x/term v0.31.0
+	golang.org/x/text v0.24.0
 	golang.org/x/time v0.11.0
-	golang.org/x/tools v0.31.0
+	golang.org/x/tools v0.32.0
 	google.golang.org/protobuf v1.36.6
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 )
@@ -282,8 +282,8 @@ require (
 	go.uber.org/zap/exp v0.3.0 // indirect
 	golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
 	golang.org/x/mod v0.24.0 // indirect
-	golang.org/x/net v0.38.0 // indirect
-	golang.org/x/sys v0.31.0 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sys v0.32.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
 	google.golang.org/grpc v1.71.0 // indirect
diff --git a/go.sum b/go.sum
index cae8d6dc..3802442f 100644
--- a/go.sum
+++ b/go.sum
@@ -594,8 +594,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
 github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
-github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
@@ -816,8 +816,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
 golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
 golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
-golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
-golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
@@ -859,8 +859,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
 golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
-golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
-golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -876,8 +876,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
-golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -918,8 +918,8 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
-golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -927,8 +927,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
 golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
-golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
-golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -938,8 +938,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
-golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
@@ -958,8 +958,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
-golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
+golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
+golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/testutil/genchangelog/template.md b/testutil/genchangelog/template.md
index 2860e423..db20f8e9 100644
--- a/testutil/genchangelog/template.md
+++ b/testutil/genchangelog/template.md
@@ -2,7 +2,9 @@
 
 ![Obol Logo](https://obol.tech/obolnetwork.png)
 
-This release introduces general fixes and improvements including progress on great new features.
+<!-- TODO: Add resume of the release in free text -->
+
+Read the rest of the release notes for more:
 
 **Full Changelog**: [{{.RangeText}}]({{.RangeLink}})
 {{range .Categories}}
@@ -11,6 +13,27 @@ This release introduces general fixes and improvements including progress on gre
 - {{.Title}} {{.Label}} ({{range $i, $v := .PRs}}{{if $i}},{{end}}{{$v.Label}}{{end}})
   {{- end}}
 {{end}}
+## Compatibility Matrix
+
+<!-- TODO: Update versions with which this version is compatible -->
+This release of Charon is backwards compatible with Charon v1.0, v1.1, v1.2.
+
+The below matrix details a combination of beacon node (consensus layer) + validator clients and their corresponding versions the DV Labs team have tested with this Charon release. More validator and consensus client will be added to this list as they are supported in our automated testing framework.
+
+**Legend**
+- ✅: All duties succeed in testing
+- 🟡: All duties succeed in testing, except non-penalised aggregation duties
+- 🟠: Duties may fail for this combination
+- 🔴: One or more duties fails consistently
+
+<!-- TODO: Update clients versions, results and remarks -->
+| Validator 👉 Consensus 👇 | Teku v25.3.0 | Lighthouse v7.0.0 | Lodestar v1.28.1 | Nimbus v25.3.1 | Prysm v5.3.1 | Remarks |
+|---------------------------|--------------|-------------------|------------------|----------------|--------------|---------|
+| Teku v25.3.0              |       ✅     |         ✅        |        ✅        |       ✅       |       ✅     |         |
+| Lighthouse v7.0.0         |       ✅     |         ✅        |        ✅        |       ✅       |       ✅     |         |
+| Lodestar v1.28.1          |       ✅     |         ✅        |        ✅        |       ✅       |       ✅     |         |
+| Nimbus v25.3.1            |       ✅     |         ✅        |        ✅        |       ✅       |       ✅     |         |
+| Prysm v5.3.1              |       ✅     |         ✅        |        ✅        |       ✅       |       ✅     |         |
 
 ## What's Changed
 {{range .ExtraPRs}}



Test patch:
diff --git a/app/obolapi/test.go b/app/obolapi/test.go
new file mode 100644
index 00000000..5318a62f
--- /dev/null
+++ b/app/obolapi/test.go
@@ -0,0 +1,33 @@
+// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
+
+package obolapi
+
+import (
+	"context"
+	"net/url"
+
+	"github.com/obolnetwork/charon/app/errors"
+)
+
+const (
+	postTestPath = "/test"
+)
+
+func (c Client) PostTestResult(ctx context.Context, jsonTestResult []byte) error {
+	u, err := url.ParseRequestURI(c.baseURL)
+	if err != nil {
+		return errors.Wrap(err, "bad Obol API url")
+	}
+
+	u.Path = postTestPath
+
+	ctx, cancel := context.WithTimeout(ctx, c.reqTimeout)
+	defer cancel()
+
+	err = httpPost(ctx, u, jsonTestResult, nil)
+	if err != nil {
+		return errors.Wrap(err, "http Obol API POST request")
+	}
+
+	return nil
+}
diff --git a/cmd/createcluster_internal_test.go b/cmd/createcluster_internal_test.go
index d3aad308..ebde67b7 100644
--- a/cmd/createcluster_internal_test.go
+++ b/cmd/createcluster_internal_test.go
@@ -372,7 +372,7 @@ func testCreateCluster(t *testing.T, conf clusterConfig, def cluster.Definition,
 		vals := make(map[string]struct{})
 		amounts := deposit.DedupAmounts(deposit.EthsToGweis(conf.DepositAmounts))
 		if len(amounts) == 0 {
-			amounts = deposit.DefaultDepositAmounts()
+			amounts = deposit.DefaultDepositAmounts(conf.Compounding)
 		}
 		for _, val := range lock.Validators {
 			vals[val.PublicKeyHex()] = struct{}{}
diff --git a/cmd/test.go b/cmd/test.go
index e552fdaf..f575b081 100644
--- a/cmd/test.go
+++ b/cmd/test.go
@@ -20,12 +20,16 @@ import (
 	"time"
 	"unicode/utf8"
 
+	ssz "github.com/ferranbt/fastssz"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 
 	"github.com/obolnetwork/charon/app/errors"
+	"github.com/obolnetwork/charon/app/k1util"
 	"github.com/obolnetwork/charon/app/log"
+	"github.com/obolnetwork/charon/app/obolapi"
 	"github.com/obolnetwork/charon/app/z"
+	"github.com/obolnetwork/charon/eth2util/enr"
 )
 
 var (
@@ -49,10 +53,13 @@ const (
 )
 
 type testConfig struct {
-	OutputJSON string
-	Quiet      bool
-	TestCases  []string
-	Timeout    time.Duration
+	OutputJSON            string
+	Quiet                 bool
+	TestCases             []string
+	Timeout               time.Duration
+	Publish               bool
+	PublishAddr           string
+	PublishPrivateKeyFile string
 }
 
 func newTestCmd(cmds ...*cobra.Command) *cobra.Command {
@@ -72,6 +79,9 @@ func bindTestFlags(cmd *cobra.Command, config *testConfig) {
 	cmd.Flags().StringSliceVar(&config.TestCases, "test-cases", nil, fmt.Sprintf("List of comma separated names of tests to be exeucted. Available tests are: %v", listTestCases(cmd)))
 	cmd.Flags().DurationVar(&config.Timeout, "timeout", time.Hour, "Execution timeout for all tests.")
 	cmd.Flags().BoolVar(&config.Quiet, "quiet", false, "Do not print test results to stdout.")
+	cmd.Flags().BoolVar(&config.Publish, "publish", false, "Publish test result file to obol-api.")
+	cmd.Flags().StringVar(&config.PublishAddr, "publish-address", "https://api.obol.tech/v1", "The URL to publish the test result file to.")
+	cmd.Flags().StringVar(&config.PublishPrivateKeyFile, "publish-private-key-file", ".charon/charon-enr-private-key", "The path to the charon enr private key file, used for signing the publish request.")
 }
 
 func bindTestLogFlags(flags *pflag.FlagSet, config *log.Config) {
@@ -204,6 +214,14 @@ type testCategoryResult struct {
 	Score         categoryScore           `json:"score,omitempty"`
 }
 
+type allCategoriesResult struct {
+	Peers     testCategoryResult `json:"charon_peers,omitempty"`
+	Beacon    testCategoryResult `json:"beacon_node,omitempty"`
+	Validator testCategoryResult `json:"validator_client,omitempty"`
+	MEV       testCategoryResult `json:"mev,omitempty"`
+	Infra     testCategoryResult `json:"infra,omitempty"`
+}
+
 func appendScore(cat []string, score []string) []string {
 	var res []string
 	for i, l := range cat {
@@ -213,12 +231,67 @@ func appendScore(cat []string, score []string) []string {
 	return res
 }
 
-type fileResult struct {
-	Peers     testCategoryResult `json:"charon_peers,omitempty"`
-	Beacon    testCategoryResult `json:"beacon_node,omitempty"`
-	Validator testCategoryResult `json:"validator_client,omitempty"`
-	MEV       testCategoryResult `json:"mev,omitempty"`
-	Infra     testCategoryResult `json:"infra,omitempty"`
+type obolAPIResult struct {
+	ENR  string              `json:"enr,omitempty"`
+	Sig  []byte              `json:"sig,omitempty"`
+	Data allCategoriesResult `json:"data"`
+}
+
+func publishResultToObolAPI(ctx context.Context, data allCategoriesResult, path string, privateKeyFile string) error {
+	p2pPrivKey, err := k1util.Load(privateKeyFile)
+	if err != nil {
+		return err
+	}
+
+	enr, err := enr.New(p2pPrivKey)
+	if err != nil {
+		return err
+	}
+
+	signDataBytes, err := json.Marshal(data)
+	if err != nil {
+		return errors.Wrap(err, "marshal all test categories signing data")
+	}
+
+	hh := ssz.DefaultHasherPool.Get()
+	defer ssz.DefaultHasherPool.Put(hh)
+
+	indx := hh.Index()
+	hh.PutBytes(signDataBytes)
+	hh.Merkleize(indx)
+
+	hash, err := hh.HashRoot()
+	if err != nil {
+		return errors.Wrap(err, "hash root")
+	}
+
+	sig, err := k1util.Sign(p2pPrivKey, hash[:])
+	if err != nil {
+		return errors.Wrap(err, "k1 sign")
+	}
+
+	obolAPI, err := obolapi.New(path)
+	if err != nil {
+		return err
+	}
+
+	res := obolAPIResult{
+		ENR:  enr.String(),
+		Sig:  sig,
+		Data: data,
+	}
+
+	obolAPIJSON, err := json.Marshal(res)
+	if err != nil {
+		return errors.Wrap(err, "marshal Obol API test struct")
+	}
+
+	err = obolAPI.PostTestResult(ctx, obolAPIJSON)
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 func writeResultToFile(res testCategoryResult, path string) error {
@@ -233,9 +306,9 @@ func writeResultToFile(res testCategoryResult, path string) error {
 		return errors.Wrap(err, "get file stat")
 	}
 	// read file contents or default to empty structure
-	var file fileResult
+	var file allCategoriesResult
 	if stat.Size() == 0 {
-		file = fileResult{}
+		file = allCategoriesResult{}
 	} else {
 		err = json.NewDecoder(existingFile).Decode(&file)
 		if err != nil {
diff --git a/cmd/testbeacon.go b/cmd/testbeacon.go
index 0cf9df1c..4d853136 100644
--- a/cmd/testbeacon.go
+++ b/cmd/testbeacon.go
@@ -263,6 +263,13 @@ func runTestBeacon(ctx context.Context, w io.Writer, cfg testBeaconConfig) (res
 		}
 	}
 
+	if cfg.Publish {
+		err = publishResultToObolAPI(ctx, allCategoriesResult{Beacon: res}, cfg.PublishAddr, cfg.PublishPrivateKeyFile)
+		if err != nil {
+			return res, err
+		}
+	}
+
 	return res, nil
 }
 
diff --git a/cmd/testinfra.go b/cmd/testinfra.go
index 06c47b6a..71591712 100644
--- a/cmd/testinfra.go
+++ b/cmd/testinfra.go
@@ -176,6 +176,13 @@ func runTestInfra(ctx context.Context, w io.Writer, cfg testInfraConfig) (res te
 		}
 	}
 
+	if cfg.Publish {
+		err = publishResultToObolAPI(ctx, allCategoriesResult{Infra: res}, cfg.PublishAddr, cfg.PublishPrivateKeyFile)
+		if err != nil {
+			return res, err
+		}
+	}
+
 	return res, nil
 }
 
diff --git a/cmd/testmev.go b/cmd/testmev.go
index ca1a4960..cd7c91cd 100644
--- a/cmd/testmev.go
+++ b/cmd/testmev.go
@@ -158,6 +158,13 @@ func runTestMEV(ctx context.Context, w io.Writer, cfg testMEVConfig) (res testCa
 		}
 	}
 
+	if cfg.Publish {
+		err = publishResultToObolAPI(ctx, allCategoriesResult{MEV: res}, cfg.PublishAddr, cfg.PublishPrivateKeyFile)
+		if err != nil {
+			return res, err
+		}
+	}
+
 	return res, nil
 }
 
diff --git a/cmd/testpeers.go b/cmd/testpeers.go
index 838d00a7..c67b9d43 100644
--- a/cmd/testpeers.go
+++ b/cmd/testpeers.go
@@ -238,6 +238,13 @@ func runTestPeers(ctx context.Context, w io.Writer, conf testPeersConfig) (res t
 		}
 	}
 
+	if conf.Publish {
+		err = publishResultToObolAPI(ctx, allCategoriesResult{Peers: res}, conf.PublishAddr, conf.PublishPrivateKeyFile)
+		if err != nil {
+			return res, err
+		}
+	}
+
 	log.Info(ctx, "Keeping TCP node alive for peers until keep-alive time is reached...")
 	blockAndWait(ctx, conf.KeepAlive)
 
diff --git a/cmd/testpeers_internal_test.go b/cmd/testpeers_internal_test.go
index 7eb376a3..1cfbd61b 100644
--- a/cmd/testpeers_internal_test.go
+++ b/cmd/testpeers_internal_test.go
@@ -11,6 +11,7 @@ import (
 	"io"
 	"maps"
 	"net/http"
+	"net/http/httptest"
 	"os"
 	"slices"
 	"strings"
@@ -267,6 +268,49 @@ func TestPeersTest(t *testing.T) {
 				require.NoError(t, err)
 			},
 		},
+		{
+			name: "publish to Obol API",
+			config: testPeersConfig{
+				testConfig: testConfig{
+					Publish: true,
+					Quiet:   false,
+					Timeout: 3 * time.Second,
+				},
+				ENRs: []string{
+					"enr:-HW4QBHlcyD3fYWUMADiOv4OxODaL5wJG0a7P7d_ltu4VZe1MibZ1N-twFaoaq0BoCtXcY71etxLJGeEZT5p3XCO6GOAgmlkgnY0iXNlY3AyNTZrMaEDI2HRUlVBag__njkOWEEQRLlC9ylIVCrIXOuNBSlrx6o",
+					"enr:-HW4QDwUF804f4WhUjwcp4JJ-PrRH0glQZv8s2cVHlBRPJ3SYcYO-dvJGsKhztffrski5eujJkl8oAc983MZy6-PqF2AgmlkgnY0iXNlY3AyNTZrMaECPEPryjkmUBnQFyjmMw9rl7DVtKL0243nN5iepqsvKDw",
+					"enr:-HW4QPSBgUTag8oZs3zIsgWzlBUrSgT8pgZmFJa7HWwKXUcRLlISa68OJtp-JTzhUXsJ2vSGwKGACn0OTatWdJATxn-AgmlkgnY0iXNlY3AyNTZrMaECA3R_ffXLXCLJsfEwf6xeoAFgWnDIOdq8kS0Yqkhwbr0",
+				},
+				P2P: p2p.Config{
+					TCPAddrs: []string{freeTCPAddr.String()},
+					Relays:   []string{relayAddr},
+				},
+				Log: log.DefaultConfig(),
+			},
+			expected: testCategoryResult{
+				CategoryName: peersTestCategory,
+				Targets: map[string][]testResult{
+					"self": {
+						{Name: "Libp2pTCPPortOpen", Verdict: testVerdictOk, Measurement: "", Suggestion: "", Error: testResultError{}},
+					},
+					fmt.Sprintf("relay %v", relayAddr): {
+						{Name: "PingRelay", Verdict: testVerdictOk, Measurement: "", Suggestion: "", Error: testResultError{}},
+						{Name: "PingMeasureRelay", Verdict: testVerdictGood, Measurement: "", Suggestion: "", Error: testResultError{}},
+					},
+					"peer inexpensive-farm enr:-HW4QBHlc...rx6o": {
+						{Name: "Ping", Verdict: testVerdictFail, Measurement: "", Suggestion: "", Error: errTimeoutInterrupted},
+					},
+					"peer anxious-pencil enr:-HW4QDwUF...vKDw": {
+						{Name: "Ping", Verdict: testVerdictFail, Measurement: "", Suggestion: "", Error: errTimeoutInterrupted},
+					},
+					"peer important-pen enr:-HW4QPSBg...wbr0": {
+						{Name: "Ping", Verdict: testVerdictFail, Measurement: "", Suggestion: "", Error: errTimeoutInterrupted},
+					},
+				},
+				Score: categoryScoreC,
+			},
+			expectedErr: "",
+		},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
@@ -276,6 +320,11 @@ func TestPeersTest(t *testing.T) {
 			_, err := p2p.NewSavedPrivKey(temp)
 			require.NoError(t, err)
 			conf.PrivateKeyFile = p2p.KeyPath(temp)
+			if conf.Publish {
+				var server *httptest.Server
+				server, conf = testPublishToObolAPI(t, test.expected, conf, temp)
+				defer server.Close()
+			}
 			if test.prepare != nil {
 				conf = test.prepare(t, conf)
 			}
@@ -382,7 +431,7 @@ func testWriteFile(t *testing.T, expectedRes testCategoryResult, path string) {
 	t.Helper()
 	file, err := os.ReadFile(path)
 	require.NoError(t, err)
-	var res fileResult
+	var res allCategoriesResult
 	err = json.Unmarshal(file, &res)
 	require.NoError(t, err)
 
@@ -429,6 +478,29 @@ func testWriteFile(t *testing.T, expectedRes testCategoryResult, path string) {
 	}
 }
 
+func testPublishToObolAPI(t *testing.T, expectedRes testCategoryResult, conf testPeersConfig, keyPath string) (*httptest.Server, testPeersConfig) {
+	t.Helper()
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, r.URL.Path, "/test")
+		require.Equal(t, r.Method, http.MethodPost)
+		require.Equal(t, r.Header.Get("Content-Type"), "application/json")
+
+		w.WriteHeader(http.StatusOK)
+		b, err := io.ReadAll(r.Body)
+		require.NoError(t, err)
+		var res obolAPIResult
+		err = json.Unmarshal(b, &res)
+		require.NoError(t, err)
+		require.Equal(t, expectedRes.CategoryName, res.Data.Peers.CategoryName)
+		require.NotEmpty(t, res.ENR)
+		require.NotEmpty(t, res.Sig)
+	}))
+	conf.PublishAddr = server.URL
+	conf.PublishPrivateKeyFile = p2p.KeyPath(keyPath)
+
+	return server, conf
+}
+
 func startPeer(t *testing.T, conf testPeersConfig, peerPrivKey *k1.PrivateKey) enr.Record {
 	t.Helper()
 	ctx := context.Background()
diff --git a/cmd/testvalidator.go b/cmd/testvalidator.go
index fa936522..3f6c583d 100644
--- a/cmd/testvalidator.go
+++ b/cmd/testvalidator.go
@@ -126,6 +126,13 @@ func runTestValidator(ctx context.Context, w io.Writer, cfg testValidatorConfig)
 		}
 	}
 
+	if cfg.Publish {
+		err = publishResultToObolAPI(ctx, allCategoriesResult{Validator: res}, cfg.PublishAddr, cfg.PublishPrivateKeyFile)
+		if err != nil {
+			return res, err
+		}
+	}
+
 	return res, nil
 }
 
diff --git a/dkg/dkg_test.go b/dkg/dkg_test.go
index cc519e22..d1a26882 100644
--- a/dkg/dkg_test.go
+++ b/dkg/dkg_test.go
@@ -418,7 +418,7 @@ func verifyDistValidators(t *testing.T, lock cluster.Lock, def cluster.Definitio
 			if !cluster.SupportPartialDeposits(def.Version) {
 				depositAmounts = []eth2p0.Gwei{deposit.DefaultDepositAmount}
 			} else {
-				depositAmounts = deposit.DefaultDepositAmounts()
+				depositAmounts = deposit.DefaultDepositAmounts(def.Compounding)
 			}
 		}
 		require.Len(t, val.PartialDepositData, len(depositAmounts))
diff --git a/eth2util/deposit/deposit_test.go b/eth2util/deposit/deposit_test.go
index cee12e5a..225e10e6 100644
--- a/eth2util/deposit/deposit_test.go
+++ b/eth2util/deposit/deposit_test.go
@@ -155,12 +155,21 @@ func TestVerifyDepositAmounts(t *testing.T) {
 }
 
 func TestDefaultDepositAmounts(t *testing.T) {
-	amounts := deposit.DefaultDepositAmounts()
+	amounts := deposit.DefaultDepositAmounts(false)
 
 	require.Equal(t, []eth2p0.Gwei{
 		deposit.MinDepositAmount,
 		deposit.DefaultDepositAmount,
 	}, amounts)
+
+	amounts = deposit.DefaultDepositAmounts(true)
+
+	require.Equal(t, []eth2p0.Gwei{
+		deposit.MinDepositAmount,
+		8 * deposit.OneEthInGwei,
+		32 * deposit.OneEthInGwei,
+		256 * deposit.OneEthInGwei,
+	}, amounts)
 }
 
 func TestEthsToGweis(t *testing.T) {
diff --git a/testutil/genchangelog/main.go b/testutil/genchangelog/main.go
index d0656d4b..9413e65c 100644
--- a/testutil/genchangelog/main.go
+++ b/testutil/genchangelog/main.go
@@ -377,6 +377,11 @@ func prFromLog(l log) (pullRequest, bool) {
 		return pullRequest{}, false
 	}
 
+	if strings.Contains(l.Subject, "chore(deps)") {
+		fmt.Printf("Skipping renovate PR (%s): %s\n", l.Commit, l.Subject)
+		return pullRequest{}, false
+	}
+
 	var ok bool
 
 	pr := pullRequest{