Repository: ObolNetwork/charon
Pull request #3631: *: add test command publish
Author: login: KaloyanTanev
Labels: name: merge when ready
description: Indicates bulldozer bot may merge when all checks pass
Created: 2025-04-07T12:27:18Z | Merged: 2025-04-09T15:06:34Z

Add publish option for test commands.

category: feature
ticket: #3350 



Diff:
diff --git a/app/obolapi/test.go b/app/obolapi/test.go
new file mode 100644
index 0000000000..5318a62fe6
--- /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/test.go b/cmd/test.go
index e552fdaf8d..f575b0814f 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 0cf9df1ce8..4d8531367f 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 06c47b6ac8..7159171266 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 ca1a496091..cd7c91cd0c 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 838d00a780..c67b9d434d 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 7eb376a3d9..1cfbd61b9e 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 fa9365228f..3f6c583dc2 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
 }
 


Reviews:
user: login: DiogoSantoss
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T10:35:07Z
user: login: pinebit
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T10:59:11Z
user: login: pinebit
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-09T10:59:28Z
user: login: KaloyanTanev
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T15:06:22Z

Review comments:
diff_hunk: @@ -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)
path: cmd/test.go
user: login: pinebit
body: Could be just `return obolAPI.PostTestResult(ctx, obolAPIJSON)`
created_at: 2025-04-09T10:59:11Z
author_association: COLLABORATOR
diff_hunk: @@ -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)
path: cmd/test.go
user: login: KaloyanTanev
body: I usually prefer returning nils in longer functions like that one... no big difference, just a bit more clear for the reader IMO and easier to add more functionalities/debug logs later on.
created_at: 2025-04-09T15:06:22Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-07T12:32:30Z
body: ## [Codecov](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork) Report
Attention: Patch coverage is `32.60870%` with `62 lines` in your changes missing coverage. Please review.
> Project coverage is 53.72%. Comparing base [(`c1e8cfc`)](https://app.codecov.io/gh/ObolNetwork/charon/commit/c1e8cfc643dede60b8c02abc88e51e12c2149ebc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork) to head [(`e9d1754`)](https://app.codecov.io/gh/ObolNetwork/charon/commit/e9d175486dd1e16af9982143ce305ab3b938ec5f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork).
> Report is 9 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork) | Patch % | Lines |
|---|---|---|
| [cmd/test.go](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&filepath=cmd%2Ftest.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork#diff-Y21kL3Rlc3QuZ28=) | 53.84% | [16 Missing and 8 partials :warning: ](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork) |
| [app/obolapi/test.go](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&filepath=app%2Fobolapi%2Ftest.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork#diff-YXBwL29ib2xhcGkvdGVzdC5nbw==) | 0.00% | [15 Missing :warning: ](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork) |
| [cmd/testbeacon.go](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&filepath=cmd%2Ftestbeacon.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork#diff-Y21kL3Rlc3RiZWFjb24uZ28=) | 0.00% | [4 Missing and 1 partial :warning: ](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork) |
| [cmd/testinfra.go](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&filepath=cmd%2Ftestinfra.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork#diff-Y21kL3Rlc3RpbmZyYS5nbw==) | 0.00% | [4 Missing and 1 partial :warning: ](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork) |
| [cmd/testmev.go](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&filepath=cmd%2Ftestmev.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork#diff-Y21kL3Rlc3RtZXYuZ28=) | 0.00% | [4 Missing and 1 partial :warning: ](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork) |
| [cmd/testvalidator.go](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&filepath=cmd%2Ftestvalidator.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork#diff-Y21kL3Rlc3R2YWxpZGF0b3IuZ28=) | 0.00% | [4 Missing and 1 partial :warning: ](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork) |
| [cmd/testpeers.go](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&filepath=cmd%2Ftestpeers.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork#diff-Y21kL3Rlc3RwZWVycy5nbw==) | 40.00% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #3631   +/-   ##
=======================================
  Coverage   53.72%   53.72%           
=======================================
  Files         221      222    +1     
  Lines       35084    35174   +90     
=======================================
+ Hits        18849    18898   +49     
- Misses      14152    14184   +32     
- Partials     2083     2092    +9     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ObolNetwork/charon/pull/3631?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ObolNetwork).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-08T16:13:11Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=ObolNetwork_charon&pullRequest=3631) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=ObolNetwork_charon&pullRequest=3631&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=ObolNetwork_charon&pullRequest=3631&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=ObolNetwork_charon&pullRequest=3631&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=ObolNetwork_charon&pullRequest=3631&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=ObolNetwork_charon&pullRequest=3631&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=ObolNetwork_charon&pullRequest=3631)


author_association: NONE