Repository: ObolNetwork/charon

Issue:
`charon create dkg` can return a launchpad invitation to a DKG
# 🎯 Problem to be solved

`charon create dkg` creates a local cluster-definition with operator-enrs only. There is no easy CLI way to create a cluster invitation for someone else. 

# 🛠️ Proposed solution

Add a `--publish` (and `--publish-address`) flag to `charon create dkg`, which if present, will prepare and post a cluster-definition.json to our POST /dv API endpoint, before outputting a text prompt saying something like:

```
Cluster Invitation Prepared:

Direct the Node Operators to: https://launchpad.obol.org/invite/<cluster-defintion> to review the cluster configuration and begin the distributed key generation ceremony. 
```

Complexity to be aware of:
- Right now this command hard requires operator enrs. If we instead want to offer addresses rather than ENRs, we probably need to tweak the params. Also, to get past the API validation, the `creator` field of a definition I think needs to be populated. We can probably use the --charon-private-key if present to sign with? or we can generate a temp one if none is present? its not generally that crucial an address right now in our product, so temp might be okay. 
- Whatever `creator` address we decide to use, we likely have to first call the /signTermsandConditions api endpoint or the request to POST a defintion to /dv will be rejected. 

- [ ] 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

<!-- 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:

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/app/obolapi/api.go b/app/obolapi/api.go
index 7ad1f560..b8ce27fb 100644
--- a/app/obolapi/api.go
+++ b/app/obolapi/api.go
@@ -20,7 +20,7 @@ const (
 	// launchpadReturnPathFmt is the URL path format string at which one can find details for a given cluster lock hash.
 	launchpadReturnPathFmt = "/lock/0x%X/launchpad"
 
-	// defaultTimeout is the default HTTP request timeout if not specified
+	// defaultTimeout is the default HTTP request timeout if not specified.
 	defaultTimeout = 10 * time.Second
 )
 
@@ -33,7 +33,6 @@ func New(urlStr string, options ...func(*Client)) (Client, error) {
 
 	// always set a default timeout, even if no options are provided
 	options = append([]func(*Client){WithTimeout(defaultTimeout)}, options...)
-
 	cl := Client{
 		baseURL: urlStr,
 	}
@@ -69,28 +68,6 @@ func (c Client) url() *url.URL {
 	return baseURL
 }
 
-// PublishLock posts the lockfile to obol-api.
-// It respects the timeout specified in the Client instance.
-func (c Client) PublishLock(ctx context.Context, lock cluster.Lock) error {
-	addr := c.url()
-	addr.Path = "lock"
-
-	b, err := lock.MarshalJSON()
-	if err != nil {
-		return errors.Wrap(err, "marshal lock")
-	}
-
-	ctx, cancel := context.WithTimeout(ctx, c.reqTimeout)
-	defer cancel()
-
-	err = httpPost(ctx, addr, b, nil)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
 // LaunchpadURLForLock returns the Launchpad cluster dashboard page for a given lock, on the given
 // Obol API client.
 func (c Client) LaunchpadURLForLock(lock cluster.Lock) string {
diff --git a/app/obolapi/publish.go b/app/obolapi/publish.go
new file mode 100644
index 00000000..8c56e7eb
--- /dev/null
+++ b/app/obolapi/publish.go
@@ -0,0 +1,102 @@
+// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1
+
+package obolapi
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/obolnetwork/charon/app/errors"
+	"github.com/obolnetwork/charon/cluster"
+)
+
+const (
+	// publishLockPath is the path to publish a cluster lockfile to obol-api.
+	publishLockPath = "lock"
+
+	// publishDefinitionPath is the path to publish a cluster definition to obol-api.
+	publishDefinitionPath = "/definition"
+
+	// termsAndConditionsPath is the path to sign Obol's Terms and Conditions.
+	termsAndConditionsPath = "/termsAndConditions"
+
+	// termsAndConditionsHash is the hash of the terms and conditions that the user must sign.
+	termsAndConditionsHash = "0xd33721644e8f3afab1495a74abe3523cec12d48b8da6cb760972492ca3f1a273"
+)
+
+// PublishLock posts the lockfile to obol-api.
+// It respects the timeout specified in the Client instance.
+func (c Client) PublishLock(ctx context.Context, lock cluster.Lock) error {
+	addr := c.url()
+	addr.Path = publishLockPath
+
+	b, err := lock.MarshalJSON()
+	if err != nil {
+		return errors.Wrap(err, "marshal lock")
+	}
+
+	ctx, cancel := context.WithTimeout(ctx, c.reqTimeout)
+	defer cancel()
+
+	err = httpPost(ctx, addr, b, nil)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// PublishDefinition posts the cluster definition to obol-api.
+// It requires the cluster creator to previously sign Obol's Terms and Conditions.
+func (c Client) PublishDefinition(ctx context.Context, def cluster.Definition, sig []byte) error {
+	addr := c.url()
+	addr.Path += publishDefinitionPath
+
+	b, err := def.MarshalJSON()
+	if err != nil {
+		return errors.Wrap(err, "marshal definition")
+	}
+
+	headers := map[string]string{
+		"authorization": bearerString(sig),
+	}
+
+	ctx, cancel := context.WithTimeout(ctx, c.reqTimeout)
+	defer cancel()
+
+	return httpPost(ctx, addr, b, headers)
+}
+
+type RequestSignTermsAndConditions struct {
+	Address                string `json:"address"`
+	Version                int    `json:"version"`
+	TermsAndConditionsHash string `json:"terms_and_conditions_hash"`
+	ForkVersion            string `json:"fork_version"`
+}
+
+// SignTermsAndConditions submits the user's signature of Obol's Terms and Conditions to obol-api.
+func (c Client) SignTermsAndConditions(ctx context.Context, userAddr string, forkVersion []byte, sig []byte) error {
+	addr := c.url()
+	addr.Path += termsAndConditionsPath
+
+	req := RequestSignTermsAndConditions{
+		Address:                userAddr,
+		Version:                1,
+		TermsAndConditionsHash: termsAndConditionsHash,
+		ForkVersion:            fmt.Sprintf("0x%x", forkVersion),
+	}
+
+	r, err := json.Marshal(req)
+	if err != nil {
+		return errors.Wrap(err, "marshal sign terms and Conditions")
+	}
+
+	headers := map[string]string{
+		"authorization": bearerString(sig),
+	}
+
+	ctx, cancel := context.WithTimeout(ctx, c.reqTimeout)
+	defer cancel()
+	return httpPost(ctx, addr, r, headers)
+}
diff --git a/cluster/eip712sigs.go b/cluster/eip712sigs.go
index 9542b1a5..19d0b159 100644
--- a/cluster/eip712sigs.go
+++ b/cluster/eip712sigs.go
@@ -11,47 +11,94 @@ import (
 	"github.com/obolnetwork/charon/eth2util/eip712"
 )
 
-// eip712Type defines the EIP712 (https://eips.ethereum.org/EIPS/eip-712) Primary type and the Message field and value.
+// eip712TypeField defines the fields, values and types of a EIP712 message
+type eip712TypeField struct {
+	Field     string
+	Type      eip712.Primitive
+	ValueFunc func(Definition, Operator) any
+}
+
+// eip712Type defines the EIP712 (https://eips.ethereum.org/EIPS/eip-712) Primary type and the Message fields.
 type eip712Type struct {
 	PrimaryType string
-	Field       string
-	ValueFunc   func(Definition, Operator) string
+	Fields      []eip712TypeField
 }
 
 var (
 	// eip712CreatorConfigHash defines the EIP712 structure of the legacy config signature for v1.4 and later.
 	eip712CreatorConfigHash = eip712Type{
 		PrimaryType: "CreatorConfigHash",
-		Field:       "creator_config_hash",
-		ValueFunc: func(definition Definition, _ Operator) string {
-			return to0xHex(definition.ConfigHash)
+		Fields: []eip712TypeField{
+			{
+				Field: "creator_config_hash",
+				Type:  eip712.PrimitiveString,
+				ValueFunc: func(definition Definition, _ Operator) any {
+					return to0xHex(definition.ConfigHash)
+				},
+			},
 		},
 	}
 
 	// eip712OperatorConfigHash defines the EIP712 structure of the operator config signature for v1.4 and later.
 	eip712OperatorConfigHash = eip712Type{
 		PrimaryType: "OperatorConfigHash",
-		Field:       "operator_config_hash",
-		ValueFunc: func(definition Definition, _ Operator) string {
-			return to0xHex(definition.ConfigHash)
+		Fields: []eip712TypeField{
+			{
+				Field: "operator_config_hash",
+				Type:  eip712.PrimitiveString,
+				ValueFunc: func(definition Definition, _ Operator) any {
+					return to0xHex(definition.ConfigHash)
+				},
+			},
 		},
 	}
 
 	// eip712V1x3ConfigHash defines the EIP712 structure of the legacy config signature for v1.3 and before.
 	eip712V1x3ConfigHash = eip712Type{
 		PrimaryType: "ConfigHash",
-		Field:       "config_hash",
-		ValueFunc: func(definition Definition, _ Operator) string {
-			return to0xHex(definition.ConfigHash)
+		Fields: []eip712TypeField{
+			{
+				Field: "config_hash",
+				Type:  eip712.PrimitiveString,
+				ValueFunc: func(definition Definition, _ Operator) any {
+					return to0xHex(definition.ConfigHash)
+				},
+			},
 		},
 	}
 
 	// eip712ENR defines the EIP712 structure of the enr signature.
 	eip712ENR = eip712Type{
 		PrimaryType: "ENR",
-		Field:       "enr",
-		ValueFunc: func(_ Definition, operator Operator) string {
-			return operator.ENR
+		Fields: []eip712TypeField{
+			{
+				Field: "enr",
+				Type:  eip712.PrimitiveString,
+				ValueFunc: func(_ Definition, operator Operator) any {
+					return operator.ENR
+				},
+			},
+		},
+	}
+
+	// eip712TermsAndConditions defines the EIP712 structure of the terms and conditions signature.
+	eip712TermsAndConditions = eip712Type{
+		PrimaryType: "TermsAndConditions",
+		Fields: []eip712TypeField{
+			{
+				Field: "terms_and_conditions_hash",
+				Type:  eip712.PrimitiveString,
+				ValueFunc: func(_ Definition, _ Operator) any {
+					return "0xd33721644e8f3afab1495a74abe3523cec12d48b8da6cb760972492ca3f1a273"
+				},
+			},
+			{
+				Field: "version",
+				Type:  eip712.PrimitiveUint256,
+				ValueFunc: func(_ Definition, _ Operator) any {
+					return uint64(1)
+				},
+			},
 		},
 	}
 )
@@ -84,16 +131,17 @@ func digestEIP712(typ eip712Type, def Definition, operator Operator) ([]byte, er
 		},
 		Type: eip712.Type{
 			Name: typ.PrimaryType,
-			Fields: []eip712.Field{
-				{
-					Name:  typ.Field,
-					Type:  eip712.PrimitiveString,
-					Value: typ.ValueFunc(def, operator),
-				},
-			},
 		},
 	}
 
+	for _, field := range typ.Fields {
+		data.Type.Fields = append(data.Type.Fields, eip712.Field{
+			Name:  field.Field,
+			Type:  field.Type,
+			Value: field.ValueFunc(def, operator),
+		})
+	}
+
 	digest, err := eip712.HashTypedData(data)
 	if err != nil {
 		return nil, errors.Wrap(err, "hash EIP712")
@@ -116,3 +164,13 @@ func signEIP712(secret *k1.PrivateKey, typ eip712Type, def Definition, operator
 
 	return sig, nil
 }
+
+// SignTermsAndConditions returns the EIP712 signature for Obol's Terms and Conditions
+func SignTermsAndConditions(secret *k1.PrivateKey, def Definition) ([]byte, error) {
+	return signEIP712(secret, eip712TermsAndConditions, def, Operator{})
+}
+
+// SignClusterDefinitionHash returns the EIP712 signature for cluster configuration hash
+func SignClusterDefinitionHash(secret *k1.PrivateKey, def Definition) ([]byte, error) {
+	return signEIP712(secret, eip712CreatorConfigHash, def, Operator{})
+}
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/cmd/createdkg.go b/cmd/createdkg.go
index 9deed095..52cc0daa 100644
--- a/cmd/createdkg.go
+++ b/cmd/createdkg.go
@@ -6,14 +6,18 @@ import (
 	"context"
 	crand "crypto/rand"
 	"encoding/json"
+	"fmt"
 	"os"
 	"path"
+	"time"
 
+	k1 "github.com/decred/dcrd/dcrec/secp256k1/v4"
 	"github.com/spf13/cobra"
 
 	"github.com/obolnetwork/charon/app/errors"
 	"github.com/obolnetwork/charon/app/eth1wrap"
 	"github.com/obolnetwork/charon/app/log"
+	"github.com/obolnetwork/charon/app/obolapi"
 	"github.com/obolnetwork/charon/app/version"
 	"github.com/obolnetwork/charon/app/z"
 	"github.com/obolnetwork/charon/cluster"
@@ -38,6 +42,9 @@ type createDKGConfig struct {
 	TargetGasLimit      uint
 	Compounding         bool
 	ExecutionEngineAddr string
+	Publish             bool
+	PublishAddress      string
+	OperatorsAddresses  []string
 }
 
 func newCreateDKGCmd(runFunc func(context.Context, createDKGConfig) error) *cobra.Command {
@@ -68,6 +75,16 @@ func newCreateDKGCmd(runFunc func(context.Context, createDKGConfig) error) *cobr
 			}
 		}
 
+		if config.Publish {
+			mustMarkFlagRequired(cmd, "operator-addresses")
+		} else {
+			mustMarkFlagRequired(cmd, "operator-enrs")
+		}
+
+		if len(config.OperatorENRs) != 0 && len(config.OperatorsAddresses) != 0 {
+			return errors.New("cannot provide both --operator-enrs and --operator-addresses")
+		}
+
 		return nil
 	})
 
@@ -75,8 +92,6 @@ func newCreateDKGCmd(runFunc func(context.Context, createDKGConfig) error) *cobr
 }
 
 func bindCreateDKGFlags(cmd *cobra.Command, config *createDKGConfig) {
-	const operatorENRs = "operator-enrs"
-
 	cmd.Flags().StringVar(&config.Name, "name", "", "Optional cosmetic cluster name")
 	cmd.Flags().StringVar(&config.OutputDir, "output-dir", ".charon", "The folder to write the output cluster-definition.json file to.")
 	cmd.Flags().IntVar(&config.NumValidators, "num-validators", 1, "The number of distributed validators the cluster will manage (32ETH+ staked for each).")
@@ -86,13 +101,14 @@ func bindCreateDKGFlags(cmd *cobra.Command, config *createDKGConfig) {
 	cmd.Flags().StringVar(&config.Network, "network", defaultNetwork, "Ethereum network to create validators for. Options: mainnet, goerli, sepolia, hoodi, holesky, gnosis, chiado.")
 	cmd.Flags().StringVar(&config.DKGAlgo, "dkg-algorithm", "default", "DKG algorithm to use; default, frost")
 	cmd.Flags().IntSliceVar(&config.DepositAmounts, "deposit-amounts", nil, "List of partial deposit amounts (integers) in ETH. Values must sum up to at least 32ETH.")
-	cmd.Flags().StringSliceVar(&config.OperatorENRs, operatorENRs, nil, "[REQUIRED] Comma-separated list of each operator's Charon ENR address.")
+	cmd.Flags().StringSliceVar(&config.OperatorENRs, "operator-enrs", nil, "Comma-separated list of each operator's Charon ENR address.")
 	cmd.Flags().StringVar(&config.ConsensusProtocol, "consensus-protocol", "", "Preferred consensus protocol name for the cluster. Selected automatically when not specified.")
 	cmd.Flags().UintVar(&config.TargetGasLimit, "target-gas-limit", 36000000, "Preferred target gas limit for transactions.")
 	cmd.Flags().BoolVar(&config.Compounding, "compounding", false, "Enable compounding rewards for validators by using 0x02 withdrawal credentials.")
 	cmd.Flags().StringVar(&config.ExecutionEngineAddr, "execution-client-rpc-endpoint", "", "The address of the execution engine JSON-RPC API.")
-
-	mustMarkFlagRequired(cmd, operatorENRs)
+	cmd.Flags().BoolVar(&config.Publish, "publish", false, "Creates an invitation to the DKG ceremony on the DV Launchpad. Terms and conditions apply.")
+	cmd.Flags().StringVar(&config.PublishAddress, "publish-address", "https://api.obol.tech/v1", "The URL to publish the cluster to.")
+	cmd.Flags().StringSliceVar(&config.OperatorsAddresses, "operator-addresses", nil, "Comma-separated list of each operator's Ethereum address.")
 }
 
 func mustMarkFlagRequired(cmd *cobra.Command, flag string) {
@@ -107,7 +123,14 @@ func runCreateDKG(ctx context.Context, conf createDKGConfig) (err error) {
 		conf.Network = eth2util.Goerli.Name
 	}
 
-	if err = validateDKGConfig(len(conf.OperatorENRs), conf.Network, conf.DepositAmounts, conf.ConsensusProtocol, conf.Compounding); err != nil {
+	var operatorsLen int
+	if len(conf.OperatorENRs) > 0 {
+		operatorsLen = len(conf.OperatorENRs)
+	} else {
+		operatorsLen = len(conf.OperatorsAddresses)
+	}
+
+	if err = validateDKGConfig(operatorsLen, conf.Network, conf.DepositAmounts, conf.ConsensusProtocol, conf.Compounding); err != nil {
 		return err
 	}
 
@@ -136,8 +159,22 @@ func runCreateDKG(ctx context.Context, conf createDKGConfig) (err error) {
 			ENR: opENR,
 		})
 	}
+	for i, opAddr := range conf.OperatorsAddresses {
+		checksumAddr, err := eth2util.ChecksumAddress(opAddr)
+		if err != nil {
+			return errors.Wrap(err, "invalid operator address", z.Int("operator", i))
+		}
+		operators = append(operators, cluster.Operator{
+			Address: checksumAddr,
+		})
+	}
 
-	safeThreshold := cluster.Threshold(len(conf.OperatorENRs))
+	var safeThreshold int
+	if len(conf.OperatorENRs) == 0 {
+		safeThreshold = cluster.Threshold(len(conf.OperatorsAddresses))
+	} else {
+		safeThreshold = cluster.Threshold(len(conf.OperatorENRs))
+	}
 	if conf.Threshold == 0 {
 		conf.Threshold = safeThreshold
 	} else {
@@ -149,27 +186,55 @@ func runCreateDKG(ctx context.Context, conf createDKGConfig) (err error) {
 		return err
 	}
 
+	var privKey *k1.PrivateKey
+	creator := cluster.Creator{}
+
+	// Populate creator field
+	if conf.Publish {
+		// Temporary creator address
+		privKey, err = k1.GeneratePrivateKey()
+		if err != nil {
+			return errors.Wrap(err, "generate private key")
+		}
+		creator = cluster.Creator{
+			Address: eth2util.PublicKeyToAddress(privKey.PubKey()),
+		}
+	}
+
 	var opts []func(*cluster.Definition)
 	opts = append(opts, cluster.WithDKGAlgorithm(conf.DKGAlgo))
 	def, err := cluster.NewDefinition(
 		conf.Name, conf.NumValidators, conf.Threshold,
 		conf.FeeRecipientAddrs, conf.WithdrawalAddrs,
-		forkVersion, cluster.Creator{}, operators, conf.DepositAmounts,
+		forkVersion, creator, operators, conf.DepositAmounts,
 		conf.ConsensusProtocol, conf.TargetGasLimit, conf.Compounding,
 		crand.Reader, opts...)
 	if err != nil {
 		return err
 	}
-
 	if err := def.VerifyHashes(); err != nil {
 		return err
 	}
 
+	// Generate creator signature after hashes have been populated
+	if conf.Publish {
+		def.Creator.ConfigSignature, err = cluster.SignClusterDefinitionHash(privKey, def)
+		if err != nil {
+			return errors.Wrap(err, "sign cluster definition")
+		}
+	}
+
 	eth1Cl := eth1wrap.NewDefaultEthClientRunner(conf.ExecutionEngineAddr)
 	go eth1Cl.Run(ctx)
 
-	if err := def.VerifySignatures(eth1Cl); err != nil {
-		return err
+	if !conf.Publish {
+		if err := def.VerifySignatures(eth1Cl); err != nil {
+			return err
+		}
+	}
+
+	if conf.Publish {
+		return publishPartialDefinition(ctx, conf, privKey, def)
 	}
 
 	b, err := json.MarshalIndent(def, "", " ")
@@ -237,3 +302,32 @@ func validateDKGConfig(numOperators int, network string, depositAmounts []int, c
 func isMainOrGnosis(network string) bool {
 	return network == eth2util.Mainnet.Name || network == eth2util.Gnosis.Name
 }
+
+func publishPartialDefinition(ctx context.Context, conf createDKGConfig, privKey *k1.PrivateKey, def cluster.Definition) error {
+	apiClient, err := obolapi.New(conf.PublishAddress, obolapi.WithTimeout(10*time.Second))
+	if err != nil {
+		return errors.Wrap(err, "create Obol API client")
+	}
+
+	sig, err := cluster.SignTermsAndConditions(privKey, def)
+	if err != nil {
+		return errors.Wrap(err, "sign terms")
+	}
+
+	err = apiClient.SignTermsAndConditions(ctx, def.Creator.Address, def.ForkVersion, sig)
+	if err != nil {
+		return errors.Wrap(err, "submit sign terms")
+	}
+
+	log.Info(ctx, "Creator successfully signed Obol's terms and conditions")
+
+	err = apiClient.PublishDefinition(ctx, def, def.Creator.ConfigSignature)
+	if err != nil {
+		return errors.Wrap(err, "publish cluster definition")
+	}
+
+	log.Info(ctx, "Cluster Invitation Prepared")
+	log.Info(ctx, "Direct the Node Operators to: https://launchpad.obol.org/dv#"+fmt.Sprintf("%#x", def.ConfigHash)+" to review the cluster configuration and begin the distributed key generation ceremony.")
+
+	return nil
+}
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/eth2util/eip712/eip712.go b/eth2util/eip712/eip712.go
index 7336173e..f3d93ed9 100644
--- a/eth2util/eip712/eip712.go
+++ b/eth2util/eip712/eip712.go
@@ -8,6 +8,7 @@ import (
 	"bytes"
 	"encoding/binary"
 	"fmt"
+	"slices"
 
 	"golang.org/x/crypto/sha3"
 
@@ -18,8 +19,9 @@ import (
 type Primitive string
 
 const (
-	PrimitiveString  Primitive = "string"
-	PrimitiveUint256 Primitive = "uint256"
+	PrimitiveString           Primitive = "string"
+	PrimitiveUint256          Primitive = "uint256"
+	TermsAndConditionTypeName string    = "TermsAndConditions"
 )
 
 // TypedData represents a dynamically typed EIP-712 message.
@@ -64,7 +66,16 @@ func domainToType(domain Domain) Type {
 
 // HashTypedData returns the hash of the typed data.
 func HashTypedData(data TypedData) ([]byte, error) {
-	domainHash, err := hashData(domainToType(data.Domain))
+	domainType := domainToType(data.Domain)
+
+	// TODO(diogo): temporary hack until api is updated then remove
+	// Currently, api doesn't have chainId field in the domain for
+	// eip712 termsAndConditions message. This will change in the future.
+	if data.Type.Name == TermsAndConditionTypeName {
+		domainType.Fields = slices.Delete(domainType.Fields, 2, 3)
+	}
+
+	domainHash, err := hashData(domainType)
 	if err != nil {
 		return nil, err
 	}
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/api_test.go b/app/obolapi/api_test.go
index f28c0c9b..c71e5afa 100644
--- a/app/obolapi/api_test.go
+++ b/app/obolapi/api_test.go
@@ -98,3 +98,116 @@ func TestLaunchpadDashURL(t *testing.T) {
 		)
 	})
 }
+
+func TestSignTermsAndConditions(t *testing.T) {
+	t.Run("successful request", func(t *testing.T) {
+		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			require.Equal(t, "/termsAndConditions", r.URL.Path)
+			require.Equal(t, "application/json", r.Header.Get("Content-Type"))
+			require.Contains(t, r.Header.Get("Authorization"), "Bearer 0x5678")
+
+			data, err := io.ReadAll(r.Body)
+			require.NoError(t, err)
+			defer r.Body.Close()
+
+			var req obolapi.RequestSignTermsAndConditions
+			require.NoError(t, json.Unmarshal(data, &req))
+			require.Equal(t, 1, req.Version)
+			require.Equal(t, "user-address", req.Address)
+			require.Equal(t, "0xd33721644e8f3afab1495a74abe3523cec12d48b8da6cb760972492ca3f1a273", req.TermsAndConditionsHash)
+			require.Equal(t, "0x1234", req.ForkVersion)
+
+			w.WriteHeader(http.StatusOK)
+		}))
+		defer srv.Close()
+
+		cl, err := obolapi.New(srv.URL)
+		require.NoError(t, err)
+
+		err = cl.SignTermsAndConditions(t.Context(), "user-address", []byte{0x12, 0x34}, []byte{0x56, 0x78})
+		require.NoError(t, err)
+	})
+
+	t.Run("marshal error", func(t *testing.T) {
+		cl, err := obolapi.New("http://example.com")
+		require.NoError(t, err)
+
+		err = cl.SignTermsAndConditions(t.Context(), "user-address", nil, nil)
+		require.Error(t, err)
+	})
+
+	t.Run("server error response", func(t *testing.T) {
+		// Mock server to simulate a server error
+		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.WriteHeader(http.StatusInternalServerError)
+		}))
+		defer srv.Close()
+
+		// Create the client
+		cl, err := obolapi.New(srv.URL)
+		require.NoError(t, err)
+
+		// Call the function
+		err = cl.SignTermsAndConditions(t.Context(), "user-address", []byte{0x12, 0x34}, []byte{0x56, 0x78})
+		require.Error(t, err)
+	})
+}
+
+func TestPublishDefinition(t *testing.T) {
+	t.Run("successful request", func(t *testing.T) {
+		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			require.Equal(t, "/definition", r.URL.Path)
+			require.Equal(t, "application/json", r.Header.Get("Content-Type"))
+			require.Contains(t, r.Header.Get("Authorization"), "Bearer 0x5678")
+
+			data, err := io.ReadAll(r.Body)
+			require.NoError(t, err)
+			defer r.Body.Close()
+
+			var req cluster.Definition
+			require.NoError(t, json.Unmarshal(data, &req))
+			require.Equal(t, "v1.10.0", req.Version)
+
+			w.WriteHeader(http.StatusOK)
+		}))
+		defer srv.Close()
+
+		cl, err := obolapi.New(srv.URL)
+		require.NoError(t, err)
+
+		def := cluster.Definition{
+			Version: "v1.10.0",
+		}
+
+		err = cl.PublishDefinition(t.Context(), def, []byte{0x56, 0x78})
+		require.NoError(t, err)
+	})
+
+	t.Run("marshal error", func(t *testing.T) {
+		cl, err := obolapi.New("http://example.com")
+		require.NoError(t, err)
+
+		// Call the function with an invalid definition to force a marshal error
+		var def cluster.Definition
+		err = cl.PublishDefinition(t.Context(), def, nil)
+		require.Error(t, err)
+		require.Contains(t, err.Error(), "marshal definition")
+	})
+
+	t.Run("server error response", func(t *testing.T) {
+		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.WriteHeader(http.StatusInternalServerError)
+		}))
+		defer srv.Close()
+
+		cl, err := obolapi.New(srv.URL)
+		require.NoError(t, err)
+
+		def := cluster.Definition{
+			Version: "v1.10.0",
+		}
+
+		err = cl.PublishDefinition(t.Context(), def, []byte{0x56, 0x78})
+		require.Error(t, err)
+	})
+}
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{