From 4db8d8a6979c1ca9b113f32918b74c0ba125e841 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Fri, 21 Aug 2026 10:26:21 +0200 Subject: [PATCH 01/29] app: fix flaky TestStartChecker (#4654) (cherry picked from commit f0633d4d9cca4a65bf5bc8c7c1cd85c75ce726b5) --- app/monitoringapi_internal_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/monitoringapi_internal_test.go b/app/monitoringapi_internal_test.go index 6e6c0ecb96..208ed47d30 100644 --- a/app/monitoringapi_internal_test.go +++ b/app/monitoringapi_internal_test.go @@ -163,10 +163,13 @@ func TestStartChecker(t *testing.T) { return true }, waitFor, tickInterval) } else { + // Don't advance the clock further: only one vapi call was sent, so the checker + // observing two more epoch rollovers would swap it out and permanently flip + // readiness to errReadyVCNotConnected. The ticks already fired above are enough + // for the checker to evaluate readiness as nil, which is then stable. require.Eventually(t, func() bool { - advanceClock(t, ctx, clock, slotDuration) return readyErrFunc() == nil - }, waitFor, tickInterval) + }, 5*time.Second, tickInterval) } }) } From 2286e13cf175e55b2945d1e24fcdfb0ceeff9150 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:42:52 +0200 Subject: [PATCH 02/29] core/tracker: fix inclusion trim underflow at chain start (#4662) * core/tracker: fix inclusion trim underflow at chain start The inclusion checker's slot arithmetic underflows on fresh chains: before genesis the wall-clock offset is negative, during the first InclCheckLag slots the checked slot goes negative, and until wall slot InclCheckLag+InclMissedLag the trim slot goes negative. The trim underflow makes Trim(huge) delete every pending submission and report each fresh duty as "duty not included on-chain", failing all early proposals on every fresh-genesis network. Skip ticks until a slot is old enough to check, and only trim once a slot can actually be declared missed. * core/tracker: check the genesis slot and deflake the regression test The zero-valued checkedSlot skipped slot 0 as already-checked; start from a MaxUint64 sentinel instead. Synchronize the chain-start regression test on the first inclusion check rather than sleeping, and pin it to the genesis slot so it covers both guards. * core/tracker: pin attestation-inclusion feature off in regression test The test wires only checkBlockFunc; disable the attestation_inclusion feature explicitly so Run cannot take the nil checkBlockAndAttsFunc path regardless of test ordering. (cherry picked from commit ad04571b167c09ce0e89ee0bf8ca97cee545e5ce) --- core/tracker/inclusion.go | 23 ++++++- core/tracker/inclusion_internal_test.go | 80 +++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/core/tracker/inclusion.go b/core/tracker/inclusion.go index 70c9f19609..8d4c77b324 100644 --- a/core/tracker/inclusion.go +++ b/core/tracker/inclusion.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "maps" + "math" "net/http" "slices" "strconv" @@ -589,7 +590,9 @@ func (a *InclusionChecker) Run(ctx context.Context) { } var ( - checkedSlot uint64 + // MaxUint64 sentinel so the first computed slot (0 at chain start) + // is not skipped as already-checked. + checkedSlot = uint64(math.MaxUint64) attesterDuties []*eth2v1.AttesterDuty ) @@ -598,7 +601,15 @@ func (a *InclusionChecker) Run(ctx context.Context) { case <-ctx.Done(): return case <-ticker.C: - slot := uint64(time.Since(a.genesis)/a.slotDuration) - InclCheckLag + // Skip until a slot is old enough to check: the unsigned + // arithmetic below underflows before genesis (negative elapsed + // time) and during the first InclCheckLag slots. + sinceGenesis := time.Since(a.genesis) + if sinceGenesis < a.slotDuration*InclCheckLag { + continue + } + + slot := uint64(sinceGenesis/a.slotDuration) - InclCheckLag if checkedSlot == slot { continue } @@ -656,7 +667,13 @@ func (a *InclusionChecker) Run(ctx context.Context) { } checkedSlot = slot - a.core.Trim(ctx, slot-InclMissedLag) + + // Only trim once a slot is old enough to be declared missed: + // slot-InclMissedLag underflows otherwise, and Trim would then + // report every pending submission as not included on-chain. + if slot >= InclMissedLag { + a.core.Trim(ctx, slot-InclMissedLag) + } } } } diff --git a/core/tracker/inclusion_internal_test.go b/core/tracker/inclusion_internal_test.go index 84911f7e30..df1c6ae591 100644 --- a/core/tracker/inclusion_internal_test.go +++ b/core/tracker/inclusion_internal_test.go @@ -6,7 +6,9 @@ import ( "context" "math/rand" "slices" + "sync" "testing" + "time" "github.com/OffchainLabs/go-bitfield" eth2api "github.com/attestantio/go-eth2-client/api" @@ -579,3 +581,81 @@ func TestInclusion404Handling(t *testing.T) { require.Error(t, err, "checkBlockAndAtts should return an error for non-404 errors") }) } + +// TestRunNoFalseMissesAtChainStart is a regression test for the first slots +// after genesis: the trim-slot arithmetic underflowed, so Trim deleted every +// pending submission and reported fresh duties as "duty not included +// on-chain", and the zero-valued checkedSlot skipped the genesis slot's +// inclusion check entirely. +func TestRunNoFalseMissesAtChainStart(t *testing.T) { + // Run must take the checkBlockFunc path below, not checkBlockAndAttsFunc. + featureset.DisableForT(t, featureset.AttestationInclusion) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + bmock, err := beaconmock.New(ctx) + require.NoError(t, err) + + var ( + mu sync.Mutex + missed []core.Duty + ) + + incl := &inclusionCore{ + missedFunc: func(ctx context.Context, sub submission) { + mu.Lock() + defer mu.Unlock() + + missed = append(missed, sub.Duty) + }, + trackerInclFunc: func(duty core.Duty, key core.PubKey, data core.SignedData, err error) {}, + submissions: make(map[subkey]submission), + beaconCommittees: make(map[eth2p0.Slot][]*eth2v1.BeaconCommittee), + } + + // Pin the checked slot to 0 for the whole test: genesis InclCheckLag + // slots ago with a slot duration far longer than the test. + checked := make(chan uint64, 1) + checker := &InclusionChecker{ + core: incl, + eth2Cl: bmock, + genesis: time.Now().Add(-InclCheckLag * time.Hour), + slotDuration: time.Hour, + checkBlockFunc: func(ctx context.Context, slot uint64, found bool) { + incl.CheckBlock(ctx, slot, found) + + select { + case checked <- slot: + default: + } + }, + } + + // A pending proposal from slot 3: too recent to be declared missed. + block := testutil.RandomDenebVersionedSignedProposal() + coreBlock, err := core.NewVersionedSignedProposal(block) + require.NoError(t, err) + require.NoError(t, incl.Submitted(core.NewProposerDuty(3), "", coreBlock, 0)) + + var wg sync.WaitGroup + + wg.Go(func() { + checker.Run(ctx) + }) + + select { + case slot := <-checked: + require.Zero(t, slot, "the genesis slot must be checked, not skipped as already-checked") + case <-time.After(10 * time.Second): + t.Fatal("timed out waiting for the genesis slot inclusion check") + } + + cancel() + wg.Wait() // Run exits only after the tick completes, including any trim. + + mu.Lock() + defer mu.Unlock() + + require.Empty(t, missed, "fresh submissions must not be reported missed right after genesis") +} From e53f92bc85eb65baa697273aa0a8f69074444472 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:46:27 +0200 Subject: [PATCH 03/29] build(deps): Bump the docker-dependencies group across 2 directories with 1 update (#4661) * build(deps): Bump the docker-dependencies group across 2 directories with 1 update Bumps the docker-dependencies group with 1 update in the / directory: golang. Bumps the docker-dependencies group with 1 update in the /testutil/promrated directory: golang. Updates `golang` from 1.26.6-trixie to 1.27.0-trixie Updates `golang` from 1.26.6-trixie to 1.27.0-trixie Updates `golang` from 1.26.6-alpine to 1.27.0-alpine Updates `golang` from 1.26.6-alpine to 1.27.0-alpine --- updated-dependencies: - dependency-name: golang dependency-version: 1.27.0-alpine dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker-dependencies - dependency-name: golang dependency-version: 1.27.0-trixie dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker-dependencies ... Signed-off-by: dependabot[bot] * bump go to 1.27 * bump cockroachdb/swiss for go1.27 support --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kalo <24719519+KaloyanTanev@users.noreply.github.com> (cherry picked from commit a67667d9dead058f1a55588cf651effdd8587926) --- .github/actions/setup-go/action.yml | 2 +- .github/workflows/golangci-lint.yml | 2 +- .golangci.yml | 3 +- .pre-commit-config.yaml | 2 +- CLAUDE.md | 2 +- app/obolapi/exit_model_test.go | 12 +- cmd/feerecipientfetch_internal_test.go | 72 ++++---- cmd/feerecipientlist_internal_test.go | 170 ++++++++----------- cmd/feerecipientsign_internal_test.go | 90 +++++----- cmd/testbeacon_internal_test.go | 4 +- cmd/testinfra_internal_test.go | 4 +- cmd/testmev_internal_test.go | 4 +- cmd/testvalidator_internal_test.go | 4 +- core/aggsigdb/memory.go | 2 +- core/dutydb/memory_test.go | 14 +- core/tracker/tracker_internal_test.go | 10 +- core/validatorapi/router_internal_test.go | 42 +++-- dkg/dkg.go | 2 +- go.mod | 4 +- go.sum | 4 +- testutil/fuzz.go | 12 +- testutil/integration/infosync_test.go | 4 +- testutil/integration/ping_test.go | 8 +- testutil/obolapimock/deposit.go | 8 +- testutil/random.go | 198 +++++++++------------- 25 files changed, 283 insertions(+), 396 deletions(-) diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml index 373ad72338..cefb86d53d 100644 --- a/.github/actions/setup-go/action.yml +++ b/.github/actions/setup-go/action.yml @@ -13,7 +13,7 @@ runs: uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: check-latest: true - go-version: "1.26" + go-version: "1.27" - name: Verify setup shell: bash diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 2c5be57241..e9ff62534d 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -19,4 +19,4 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: - version: v2.12.2 + version: v2.13.2 diff --git a/.golangci.yml b/.golangci.yml index efac98b42a..4b61e995d7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,6 @@ version: "2" run: - go: "1.26" + go: "1.27" linters: default: all disable: @@ -9,6 +9,7 @@ linters: - cyclop - err113 - exhaustruct + - exhaustruct_v5 - forcetypeassert - funlen - funcorder diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10f97f3692..6796706792 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: rev: v0.0.6 hooks: - id: check-go-version - args: [-v=go1.26] # Only check minor version locally + args: [-v=go1.27] # Only check minor version locally pass_filenames: false additional_dependencies: [packaging] - id: check-licence-header diff --git a/CLAUDE.md b/CLAUDE.md index 60c486d575..5abb111b3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -127,7 +127,7 @@ testutil/ # Test utilities, mocks, golden files ## Code Style and Guidelines ### Go Version -Requires Go 1.26 (enforced by pre-commit hooks) +Requires Go 1.27 (enforced by pre-commit hooks) ### Core Principles (from [docs/goguidelines.md](docs/goguidelines.md)) 1. **Functions over methods**: Prefer stateless functions over stateful objects diff --git a/app/obolapi/exit_model_test.go b/app/obolapi/exit_model_test.go index e2ce32b9e6..c664ca1dda 100644 --- a/app/obolapi/exit_model_test.go +++ b/app/obolapi/exit_model_test.go @@ -16,15 +16,13 @@ import ( func TestPartialExitRequest(t *testing.T) { pr := obolapi.PartialExitRequest{ - UnsignedPartialExitRequest: obolapi.UnsignedPartialExitRequest{ - PartialExits: []obolapi.ExitBlob{ - { - PublicKey: string(testutil.RandomCorePubKey(t)), - SignedExitMessage: *testutil.RandomExit(), - }, + PartialExits: []obolapi.ExitBlob{ + { + PublicKey: string(testutil.RandomCorePubKey(t)), + SignedExitMessage: *testutil.RandomExit(), }, - ShareIdx: 0, }, + ShareIdx: 0, Signature: testutil.RandomSecp256k1Signature(), } diff --git a/cmd/feerecipientfetch_internal_test.go b/cmd/feerecipientfetch_internal_test.go index 5d81d331fc..f8340ecd44 100644 --- a/cmd/feerecipientfetch_internal_test.go +++ b/cmd/feerecipientfetch_internal_test.go @@ -74,16 +74,14 @@ func signConfigForOperator(c feeRecipientTestCluster, opIdx int, validatorPubkey baseDir := filepath.Join(c.root, fmt.Sprintf("op%d", opIdx)) return feerecipientSignConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: []string{validatorPubkey}, - PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), - LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), - PublishAddress: c.srvURL, - PublishTimeout: 10 * time.Second, - }, - ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), - FeeRecipient: feeRecipient, - Timestamp: timestamp, + ValidatorPublicKeys: []string{validatorPubkey}, + PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), + LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), + PublishAddress: c.srvURL, + PublishTimeout: 10 * time.Second, + ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), + FeeRecipient: feeRecipient, + Timestamp: timestamp, } } @@ -102,13 +100,11 @@ func fetchFeeRecipient(ctx context.Context, t *testing.T, c feeRecipientTestClus t.Helper() fetchConfig := feerecipientFetchConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: validatorPubkeys, - LockFilePath: filepath.Join(c.root, "op0", "cluster-lock.json"), - OverridesFilePath: overridesFile, - PublishAddress: c.srvURL, - PublishTimeout: 10 * time.Second, - }, + ValidatorPublicKeys: validatorPubkeys, + LockFilePath: filepath.Join(c.root, "op0", "cluster-lock.json"), + OverridesFilePath: overridesFile, + PublishAddress: c.srvURL, + PublishTimeout: 10 * time.Second, } require.NoError(t, runFeeRecipientFetch(ctx, fetchConfig)) @@ -287,11 +283,9 @@ func TestFeeRecipientFetchSkipsInvalidSignature(t *testing.T) { func TestFeeRecipientFetchInvalidLockFile(t *testing.T) { config := feerecipientFetchConfig{ - feerecipientConfig: feerecipientConfig{ - LockFilePath: "nonexistent-lock.json", - PublishAddress: "http://localhost:0", - PublishTimeout: time.Second, - }, + LockFilePath: "nonexistent-lock.json", + PublishAddress: "http://localhost:0", + PublishTimeout: time.Second, } err := runFeeRecipientFetch(t.Context(), config) @@ -308,11 +302,9 @@ func TestFeeRecipientFetchAPIUnreachable(t *testing.T) { srv.Close() config := feerecipientFetchConfig{ - feerecipientConfig: feerecipientConfig{ - LockFilePath: filepath.Join(c.root, "op0", "cluster-lock.json"), - PublishAddress: srv.URL, - PublishTimeout: time.Second, - }, + LockFilePath: filepath.Join(c.root, "op0", "cluster-lock.json"), + PublishAddress: srv.URL, + PublishTimeout: time.Second, } err := runFeeRecipientFetch(ctx, config) @@ -357,15 +349,13 @@ func TestFeeRecipientFetchNoQuorum(t *testing.T) { baseDir := filepath.Join(root, "op0") signConfig := feerecipientSignConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: []string{validatorPubkey}, - PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), - LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, - ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), - FeeRecipient: newFeeRecipient, + ValidatorPublicKeys: []string{validatorPubkey}, + PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), + LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, + ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), + FeeRecipient: newFeeRecipient, } require.NoError(t, runFeeRecipientSign(ctx, signConfig)) @@ -374,12 +364,10 @@ func TestFeeRecipientFetchNoQuorum(t *testing.T) { overridesFile := filepath.Join(root, "output", "builder_registrations_overrides.json") fetchConfig := feerecipientFetchConfig{ - feerecipientConfig: feerecipientConfig{ - LockFilePath: filepath.Join(root, "op0", "cluster-lock.json"), - OverridesFilePath: overridesFile, - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, + LockFilePath: filepath.Join(root, "op0", "cluster-lock.json"), + OverridesFilePath: overridesFile, + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, } require.NoError(t, runFeeRecipientFetch(ctx, fetchConfig)) diff --git a/cmd/feerecipientlist_internal_test.go b/cmd/feerecipientlist_internal_test.go index 9d91ab8912..81b736504c 100644 --- a/cmd/feerecipientlist_internal_test.go +++ b/cmd/feerecipientlist_internal_test.go @@ -49,12 +49,10 @@ func TestFeeRecipientListValid(t *testing.T) { require.NoError(t, os.WriteFile(lockFile, lockJSON, 0o644)) config := feerecipientListConfig{ - feerecipientConfig: feerecipientConfig{ - LockFilePath: lockFile, - OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), - PublishAddress: "http://127.0.0.1:0", - PublishTimeout: 500 * time.Millisecond, - }, + LockFilePath: lockFile, + OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), + PublishAddress: "http://127.0.0.1:0", + PublishTimeout: 500 * time.Millisecond, } require.NoError(t, runFeeRecipientList(t.Context(), config)) @@ -75,13 +73,11 @@ func TestFeeRecipientListWithFilter(t *testing.T) { require.NoError(t, os.WriteFile(lockFile, lockJSON, 0o644)) config := feerecipientListConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: []string{lock.Validators[0].PublicKeyHex()}, - LockFilePath: lockFile, - OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), - PublishAddress: "http://127.0.0.1:0", - PublishTimeout: 500 * time.Millisecond, - }, + ValidatorPublicKeys: []string{lock.Validators[0].PublicKeyHex()}, + LockFilePath: lockFile, + OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), + PublishAddress: "http://127.0.0.1:0", + PublishTimeout: 500 * time.Millisecond, } require.NoError(t, runFeeRecipientList(t.Context(), config)) @@ -102,11 +98,9 @@ func TestFeeRecipientListInvalidPubkey(t *testing.T) { require.NoError(t, os.WriteFile(lockFile, lockJSON, 0o644)) config := feerecipientListConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: []string{"0x" + strings.Repeat("ab", 48)}, - LockFilePath: lockFile, - OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), - }, + ValidatorPublicKeys: []string{"0x" + strings.Repeat("ab", 48)}, + LockFilePath: lockFile, + OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), } err = runFeeRecipientList(t.Context(), config) @@ -115,10 +109,8 @@ func TestFeeRecipientListInvalidPubkey(t *testing.T) { func TestFeeRecipientListInvalidLockFile(t *testing.T) { config := feerecipientListConfig{ - feerecipientConfig: feerecipientConfig{ - LockFilePath: "nonexistent-lock.json", - OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), - }, + LockFilePath: "nonexistent-lock.json", + OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), } err := runFeeRecipientList(t.Context(), config) @@ -153,12 +145,10 @@ func TestFeeRecipientListWithOverrides(t *testing.T) { require.NoError(t, os.WriteFile(overridesFile, overridesJSON, 0o644)) config := feerecipientListConfig{ - feerecipientConfig: feerecipientConfig{ - LockFilePath: lockFile, - OverridesFilePath: overridesFile, - PublishAddress: "http://127.0.0.1:0", - PublishTimeout: 500 * time.Millisecond, - }, + LockFilePath: lockFile, + OverridesFilePath: overridesFile, + PublishAddress: "http://127.0.0.1:0", + PublishTimeout: 500 * time.Millisecond, } require.NoError(t, runFeeRecipientList(t.Context(), config)) @@ -463,15 +453,13 @@ func TestFeeRecipientListFetchesRemote(t *testing.T) { baseDir := filepath.Join(root, fmt.Sprintf("op%d", opIdx)) signConfig := feerecipientSignConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: []string{validatorPubkey}, - PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), - LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, - ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), - FeeRecipient: newFeeRecipient, + ValidatorPublicKeys: []string{validatorPubkey}, + PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), + LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, + ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), + FeeRecipient: newFeeRecipient, } require.NoError(t, runFeeRecipientSign(ctx, signConfig), "operator %d submit feerecipient sign", opIdx) @@ -480,12 +468,10 @@ func TestFeeRecipientListFetchesRemote(t *testing.T) { overridesFile := filepath.Join(root, "output", "builder_registrations_overrides.json") listConfig := feerecipientListConfig{ - feerecipientConfig: feerecipientConfig{ - LockFilePath: filepath.Join(root, "op0", "cluster-lock.json"), - OverridesFilePath: overridesFile, - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, + LockFilePath: filepath.Join(root, "op0", "cluster-lock.json"), + OverridesFilePath: overridesFile, + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, } require.NoError(t, runFeeRecipientList(ctx, listConfig)) @@ -532,26 +518,22 @@ func TestFeeRecipientListRemoteOnlyTriggersSuggestion(t *testing.T) { for opIdx := range lock.Threshold { baseDir := filepath.Join(root, fmt.Sprintf("op%d", opIdx)) signConfig := feerecipientSignConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: []string{validatorPubkey}, - PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), - LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, - ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), - FeeRecipient: newFeeRecipient, + ValidatorPublicKeys: []string{validatorPubkey}, + PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), + LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, + ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), + FeeRecipient: newFeeRecipient, } require.NoError(t, runFeeRecipientSign(ctx, signConfig)) } listConfig := feerecipientListConfig{ - feerecipientConfig: feerecipientConfig{ - LockFilePath: filepath.Join(root, "op0", "cluster-lock.json"), - OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, + LockFilePath: filepath.Join(root, "op0", "cluster-lock.json"), + OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, } require.NoError(t, runFeeRecipientList(ctx, listConfig)) @@ -602,15 +584,13 @@ func TestFeeRecipientListOverridesMatchRemote(t *testing.T) { for opIdx := range lock.Threshold { baseDir := filepath.Join(root, fmt.Sprintf("op%d", opIdx)) signConfig := feerecipientSignConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: []string{validatorPubkey}, - PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), - LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, - ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), - FeeRecipient: newFeeRecipient, + ValidatorPublicKeys: []string{validatorPubkey}, + PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), + LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, + ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), + FeeRecipient: newFeeRecipient, } require.NoError(t, runFeeRecipientSign(ctx, signConfig)) } @@ -619,22 +599,18 @@ func TestFeeRecipientListOverridesMatchRemote(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Dir(overridesFile), 0o755)) fetchConfig := feerecipientFetchConfig{ - feerecipientConfig: feerecipientConfig{ - LockFilePath: filepath.Join(root, "op0", "cluster-lock.json"), - OverridesFilePath: overridesFile, - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, + LockFilePath: filepath.Join(root, "op0", "cluster-lock.json"), + OverridesFilePath: overridesFile, + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, } require.NoError(t, runFeeRecipientFetch(ctx, fetchConfig)) listConfig := feerecipientListConfig{ - feerecipientConfig: feerecipientConfig{ - LockFilePath: filepath.Join(root, "op0", "cluster-lock.json"), - OverridesFilePath: overridesFile, - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, + LockFilePath: filepath.Join(root, "op0", "cluster-lock.json"), + OverridesFilePath: overridesFile, + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, } require.NoError(t, runFeeRecipientList(ctx, listConfig)) @@ -668,12 +644,10 @@ func TestFeeRecipientListRemoteUnreachable(t *testing.T) { srv.Close() listConfig := feerecipientListConfig{ - feerecipientConfig: feerecipientConfig{ - LockFilePath: lockFile, - OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), - PublishAddress: srv.URL, - PublishTimeout: 500 * time.Millisecond, - }, + LockFilePath: lockFile, + OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), + PublishAddress: srv.URL, + PublishTimeout: 500 * time.Millisecond, } require.NoError(t, runFeeRecipientList(ctx, listConfig)) @@ -726,26 +700,22 @@ func TestFeeRecipientListIncompleteNoQuorum(t *testing.T) { for opIdx := range lock.Threshold - 1 { baseDir := filepath.Join(root, fmt.Sprintf("op%d", opIdx)) signConfig := feerecipientSignConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: []string{validatorPubkey}, - PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), - LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, - ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), - FeeRecipient: newFeeRecipient, + ValidatorPublicKeys: []string{validatorPubkey}, + PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), + LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, + ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), + FeeRecipient: newFeeRecipient, } require.NoError(t, runFeeRecipientSign(ctx, signConfig)) } listConfig := feerecipientListConfig{ - feerecipientConfig: feerecipientConfig{ - LockFilePath: filepath.Join(root, "op0", "cluster-lock.json"), - OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, + LockFilePath: filepath.Join(root, "op0", "cluster-lock.json"), + OverridesFilePath: filepath.Join(t.TempDir(), "nonexistent-overrides.json"), + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, } require.NoError(t, runFeeRecipientList(ctx, listConfig)) diff --git a/cmd/feerecipientsign_internal_test.go b/cmd/feerecipientsign_internal_test.go index cd100fbdd1..fe44b06181 100644 --- a/cmd/feerecipientsign_internal_test.go +++ b/cmd/feerecipientsign_internal_test.go @@ -67,15 +67,13 @@ func TestFeeRecipientSignValid(t *testing.T) { baseDir := filepath.Join(root, fmt.Sprintf("op%d", idx)) signConfig := feerecipientSignConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: []string{lock.Validators[0].PublicKeyHex()}, - PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), - LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, - ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), - FeeRecipient: "0x0000000000000000000000000000000000001234", + ValidatorPublicKeys: []string{lock.Validators[0].PublicKeyHex()}, + PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), + LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, + ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), + FeeRecipient: "0x0000000000000000000000000000000000001234", } require.NoError(t, runFeeRecipientSign(ctx, signConfig), "operator index submit feerecipient sign: %v", idx) @@ -122,16 +120,14 @@ func TestFeeRecipientSignWithTimestamp(t *testing.T) { baseDir := filepath.Join(root, fmt.Sprintf("op%d", opIdx)) signConfig := feerecipientSignConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: []string{validatorPubkey}, - PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), - LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, - ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), - FeeRecipient: newFeeRecipient, - Timestamp: fixedTimestamp, + ValidatorPublicKeys: []string{validatorPubkey}, + PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), + LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, + ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), + FeeRecipient: newFeeRecipient, + Timestamp: fixedTimestamp, } require.NoError(t, runFeeRecipientSign(ctx, signConfig), "operator %d sign with timestamp", opIdx) @@ -140,13 +136,11 @@ func TestFeeRecipientSignWithTimestamp(t *testing.T) { func TestFeeRecipientSignInvalidFeeRecipient(t *testing.T) { config := feerecipientSignConfig{ - feerecipientConfig: feerecipientConfig{ - PrivateKeyPath: "nonexistent", - LockFilePath: "nonexistent", - PublishAddress: "http://localhost:0", - PublishTimeout: time.Second, - }, - FeeRecipient: "not-an-address", + PrivateKeyPath: "nonexistent", + LockFilePath: "nonexistent", + PublishAddress: "http://localhost:0", + PublishTimeout: time.Second, + FeeRecipient: "not-an-address", } err := runFeeRecipientSign(t.Context(), config) @@ -255,13 +249,11 @@ func TestFeeRecipientSignAdoptsInProgressTimestamp(t *testing.T) { func TestFeeRecipientSignInvalidLockFile(t *testing.T) { config := feerecipientSignConfig{ - feerecipientConfig: feerecipientConfig{ - PrivateKeyPath: "nonexistent", - LockFilePath: "nonexistent-lock.json", - PublishAddress: "http://localhost:0", - PublishTimeout: time.Second, - }, - FeeRecipient: "0x0000000000000000000000000000000000001234", + PrivateKeyPath: "nonexistent", + LockFilePath: "nonexistent-lock.json", + PublishAddress: "http://localhost:0", + PublishTimeout: time.Second, + FeeRecipient: "0x0000000000000000000000000000000000001234", } err := runFeeRecipientSign(t.Context(), config) @@ -299,15 +291,13 @@ func TestFeeRecipientSignAPIUnreachable(t *testing.T) { baseDir := filepath.Join(root, "op0") config := feerecipientSignConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: []string{lock.Validators[0].PublicKeyHex()}, - PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), - LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), - PublishAddress: srv.URL, - PublishTimeout: time.Second, - }, - ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), - FeeRecipient: "0x0000000000000000000000000000000000001234", + ValidatorPublicKeys: []string{lock.Validators[0].PublicKeyHex()}, + PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), + LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), + PublishAddress: srv.URL, + PublishTimeout: time.Second, + ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), + FeeRecipient: "0x0000000000000000000000000000000000001234", } err = runFeeRecipientSign(ctx, config) @@ -348,15 +338,13 @@ func TestFeeRecipientSignPubkeyNotInCluster(t *testing.T) { baseDir := filepath.Join(root, "op0") config := feerecipientSignConfig{ - feerecipientConfig: feerecipientConfig{ - ValidatorPublicKeys: []string{"0x" + strings.Repeat("ab", 48)}, - PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), - LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), - PublishAddress: srv.URL, - PublishTimeout: 10 * time.Second, - }, - ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), - FeeRecipient: "0x0000000000000000000000000000000000001234", + ValidatorPublicKeys: []string{"0x" + strings.Repeat("ab", 48)}, + PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), + LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, + ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), + FeeRecipient: "0x0000000000000000000000000000000000001234", } err = runFeeRecipientSign(ctx, config) diff --git a/cmd/testbeacon_internal_test.go b/cmd/testbeacon_internal_test.go index ed448e8c24..f90095f7f2 100644 --- a/cmd/testbeacon_internal_test.go +++ b/cmd/testbeacon_internal_test.go @@ -277,9 +277,7 @@ func TestBeaconTestTimeoutAlwaysProducesResults(t *testing.T) { var buf bytes.Buffer res, err := runTestBeacon(context.Background(), &buf, testBeaconConfig{ - testConfig: testConfig{ - Timeout: time.Nanosecond, - }, + Timeout: time.Nanosecond, Endpoints: endpoints, }) require.NoError(t, err) diff --git a/cmd/testinfra_internal_test.go b/cmd/testinfra_internal_test.go index 30a5ca7bf0..1cecdf1515 100644 --- a/cmd/testinfra_internal_test.go +++ b/cmd/testinfra_internal_test.go @@ -247,9 +247,7 @@ func TestInfraTestTimeoutAlwaysProducesResults(t *testing.T) { var buf bytes.Buffer res, err := runTestInfra(context.Background(), &buf, testInfraConfig{ - testConfig: testConfig{ - Timeout: time.Nanosecond, - }, + Timeout: time.Nanosecond, DiskTestTool: DiskTestToolMock{}, }) require.NoError(t, err) diff --git a/cmd/testmev_internal_test.go b/cmd/testmev_internal_test.go index f4576e4067..cdfcb1f3f1 100644 --- a/cmd/testmev_internal_test.go +++ b/cmd/testmev_internal_test.go @@ -277,9 +277,7 @@ func TestMEVTestTimeoutAlwaysProducesResults(t *testing.T) { var buf bytes.Buffer res, err := runTestMEV(context.Background(), &buf, testMEVConfig{ - testConfig: testConfig{ - Timeout: time.Nanosecond, - }, + Timeout: time.Nanosecond, Endpoints: endpoints, }) require.NoError(t, err) diff --git a/cmd/testvalidator_internal_test.go b/cmd/testvalidator_internal_test.go index 8377217156..d7c249106c 100644 --- a/cmd/testvalidator_internal_test.go +++ b/cmd/testvalidator_internal_test.go @@ -211,9 +211,7 @@ func TestValidatorTestTimeoutAlwaysProducesResults(t *testing.T) { var buf bytes.Buffer res, err := runTestValidator(context.Background(), &buf, testValidatorConfig{ - testConfig: testConfig{ - Timeout: time.Nanosecond, - }, + Timeout: time.Nanosecond, APIAddress: fmt.Sprintf("localhost:%v", testutil.GetFreePort(t)), }) require.NoError(t, err) diff --git a/core/aggsigdb/memory.go b/core/aggsigdb/memory.go index c3ae95f833..cee0be6298 100644 --- a/core/aggsigdb/memory.go +++ b/core/aggsigdb/memory.go @@ -95,7 +95,7 @@ func (db *MemDB) Await(ctx context.Context, duty core.Duty, pubKey core.PubKey, response := make(chan core.SignedData, 1) query := readQuery{ - memDBKey: memDBKey{duty: duty, pubKey: pubKey, subcommIdx: subcommIdx}, + duty: duty, pubKey: pubKey, subcommIdx: subcommIdx, response: response, cancel: cancel, } diff --git a/core/dutydb/memory_test.go b/core/dutydb/memory_test.go index d3c5a29977..07a0bca361 100644 --- a/core/dutydb/memory_test.go +++ b/core/dutydb/memory_test.go @@ -270,14 +270,12 @@ func TestMemDBAggregatorElectraMultiCommittee(t *testing.T) { bits.SetBitAt(uint64(commIdx), true) return core.VersionedAggregatedAttestation{ - VersionedAttestation: eth2spec.VersionedAttestation{ - Version: eth2spec.DataVersionElectra, - Electra: &electra.Attestation{ - AggregationBits: testutil.RandomBitList(64), - Data: attData, - Signature: testutil.RandomEth2Signature(), - CommitteeBits: bits, - }, + Version: eth2spec.DataVersionElectra, + Electra: &electra.Attestation{ + AggregationBits: testutil.RandomBitList(64), + Data: attData, + Signature: testutil.RandomEth2Signature(), + CommitteeBits: bits, }, } } diff --git a/core/tracker/tracker_internal_test.go b/core/tracker/tracker_internal_test.go index 6601b38a5d..dcc7fbb816 100644 --- a/core/tracker/tracker_internal_test.go +++ b/core/tracker/tracker_internal_test.go @@ -1394,18 +1394,14 @@ func TestSubmittedProposals(t *testing.T) { require.NotPanics(t, func() { // Not blinded err = ic.Submitted(core.NewProposerDuty(42), testutil.RandomCorePubKey(t), core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Version: eth2spec.DataVersionDeneb, - }, + Version: eth2spec.DataVersionDeneb, }, 1*time.Millisecond) require.ErrorContains(t, err, "could not determine if proposal was synthetic or not") // Blinded err = ic.Submitted(core.NewProposerDuty(42), testutil.RandomCorePubKey(t), core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Blinded: true, - Version: eth2spec.DataVersionDeneb, - }, + Blinded: true, + Version: eth2spec.DataVersionDeneb, }, 1*time.Millisecond) require.ErrorContains(t, err, "could not determine if proposal was synthetic or not") }) diff --git a/core/validatorapi/router_internal_test.go b/core/validatorapi/router_internal_test.go index 6ba8babfaa..b3e349205d 100644 --- a/core/validatorapi/router_internal_test.go +++ b/core/validatorapi/router_internal_test.go @@ -2461,31 +2461,29 @@ func (h testHandler) newBeaconHandler(t *testing.T) http.Handler { func TestUnmarshalSSZ(t *testing.T) { phase0Proposal := core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Version: eth2spec.DataVersionPhase0, - Phase0: ð2p0.SignedBeaconBlock{ - Message: ð2p0.BeaconBlock{ - Slot: 1, - ProposerIndex: 2, - ParentRoot: eth2p0.Root{0x01}, - StateRoot: eth2p0.Root{0x02}, - Body: ð2p0.BeaconBlockBody{ - RANDAOReveal: eth2p0.BLSSignature{0xAA}, - ETH1Data: ð2p0.ETH1Data{ - DepositRoot: eth2p0.Root{0x03}, - DepositCount: 5, - BlockHash: append([]byte{0x04}, make([]byte, 31)...), - }, - Graffiti: [32]byte{0x05}, - ProposerSlashings: []*eth2p0.ProposerSlashing{}, - AttesterSlashings: []*eth2p0.AttesterSlashing{}, - Attestations: []*eth2p0.Attestation{}, - Deposits: []*eth2p0.Deposit{}, - VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, + Version: eth2spec.DataVersionPhase0, + Phase0: ð2p0.SignedBeaconBlock{ + Message: ð2p0.BeaconBlock{ + Slot: 1, + ProposerIndex: 2, + ParentRoot: eth2p0.Root{0x01}, + StateRoot: eth2p0.Root{0x02}, + Body: ð2p0.BeaconBlockBody{ + RANDAOReveal: eth2p0.BLSSignature{0xAA}, + ETH1Data: ð2p0.ETH1Data{ + DepositRoot: eth2p0.Root{0x03}, + DepositCount: 5, + BlockHash: append([]byte{0x04}, make([]byte, 31)...), }, + Graffiti: [32]byte{0x05}, + ProposerSlashings: []*eth2p0.ProposerSlashing{}, + AttesterSlashings: []*eth2p0.AttesterSlashing{}, + Attestations: []*eth2p0.Attestation{}, + Deposits: []*eth2p0.Deposit{}, + VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, }, - Signature: eth2p0.BLSSignature{0xBB}, }, + Signature: eth2p0.BLSSignature{0xBB}, }, } diff --git a/dkg/dkg.go b/dkg/dkg.go index e1c9fa9d88..b391d56769 100644 --- a/dkg/dkg.go +++ b/dkg/dkg.go @@ -526,7 +526,7 @@ func startSyncProtocol(ctx context.Context, p2pNode host.Host, key *k1.PrivateKe ) (stepSyncFunc func(context.Context) error, shutdownFunc func(context.Context) error, fatalFunc func() error, err error) { // Sign definition hash with charon-enr-private-key // Note: libp2p signing does another hash of the defHash. - hashSig, err := ((*libp2pcrypto.Secp256k1PrivateKey)(key)).Sign(defHash) + hashSig, err := (*libp2pcrypto.Secp256k1PrivateKey)(key).Sign(defHash) if err != nil { return nil, nil, nil, errors.Wrap(err, "sign definition hash") } diff --git a/go.mod b/go.mod index f9044b52c2..bba96ee2c3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/obolnetwork/charon -go 1.26.0 +go 1.27 toolchain go1.27.3 @@ -105,7 +105,7 @@ require ( github.com/cockroachdb/pebble v1.1.5 // indirect github.com/cockroachdb/pebble/v2 v2.1.4 // indirect github.com/cockroachdb/redact v1.1.6 // indirect - github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b // indirect + github.com/cockroachdb/swiss v0.0.0-20260820225851-333444432258 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/consensys/gnark-crypto v0.19.2 // indirect github.com/containerd/errdefs v1.0.0 // indirect diff --git a/go.sum b/go.sum index 0c59df6151..33c94840dd 100644 --- a/go.sum +++ b/go.sum @@ -119,8 +119,8 @@ github.com/cockroachdb/pebble/v2 v2.1.4 h1:j9wPgMDbkErFdAKYFGhsoCcvzcjR+6zrJ4jhK github.com/cockroachdb/pebble/v2 v2.1.4/go.mod h1:Reo1RTniv1UjVTAu/Fv74y5i3kJ5gmVrPhO9UtFiKn8= github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= -github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b h1:VXvSNzmr8hMj8XTuY0PT9Ane9qZGul/p67vGYwl9BFI= -github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg= +github.com/cockroachdb/swiss v0.0.0-20260820225851-333444432258 h1:IJ+uNItEm0qx9FE2AgIc1PMsCUtk8nbSIzhQE1t5GWw= +github.com/cockroachdb/swiss v0.0.0-20260820225851-333444432258/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80= diff --git a/testutil/fuzz.go b/testutil/fuzz.go index 37563f038a..4e02009749 100644 --- a/testutil/fuzz.go +++ b/testutil/fuzz.go @@ -133,9 +133,9 @@ func NewEth2Fuzzer(t *testing.T, seed int64) *fuzz.Fuzzer { }, // Populate one of the versions of these VersionedSignedProposal types. func(e *core.VersionedSignedProposal, c fuzz.Continue) { - e.Version = allVersions[(c.Intn(len(allVersions)))] + e.Version = allVersions[c.Intn(len(allVersions))] if e.Blinded { - e.Version = blindedVersions[(c.Intn(len(blindedVersions)))] + e.Version = blindedVersions[c.Intn(len(blindedVersions))] } version, err := eth2util.DataVersionFromETH2(e.Version) @@ -270,7 +270,7 @@ func NewEth2Fuzzer(t *testing.T, seed int64) *fuzz.Fuzzer { } }, func(e *core.VersionedProposal, c fuzz.Continue) { - e.Version = allVersions[(c.Intn(len(allVersions)))] + e.Version = allVersions[c.Intn(len(allVersions))] version, err := eth2util.DataVersionFromETH2(e.Version) require.NoError(t, err) @@ -353,7 +353,7 @@ func NewEth2Fuzzer(t *testing.T, seed int64) *fuzz.Fuzzer { } }, func(e *core.VersionedAttestation, c fuzz.Continue) { - e.Version = allVersions[(c.Intn(len(allVersions)))] + e.Version = allVersions[c.Intn(len(allVersions))] version, err := eth2util.DataVersionFromETH2(e.Version) require.NoError(t, err) @@ -383,7 +383,7 @@ func NewEth2Fuzzer(t *testing.T, seed int64) *fuzz.Fuzzer { e.CommitteeBits = bits }, func(e *core.VersionedAggregatedAttestation, c fuzz.Continue) { - e.Version = allVersions[(c.Intn(len(allVersions)))] + e.Version = allVersions[c.Intn(len(allVersions))] version, err := eth2util.DataVersionFromETH2(e.Version) require.NoError(t, err) @@ -391,7 +391,7 @@ func NewEth2Fuzzer(t *testing.T, seed int64) *fuzz.Fuzzer { c.Fuzz(val) }, func(e *core.VersionedSignedAggregateAndProof, c fuzz.Continue) { - e.Version = allVersions[(c.Intn(len(allVersions)))] + e.Version = allVersions[c.Intn(len(allVersions))] version, err := eth2util.DataVersionFromETH2(e.Version) require.NoError(t, err) diff --git a/testutil/integration/infosync_test.go b/testutil/integration/infosync_test.go index 1de4efe7f3..0ecb28280d 100644 --- a/testutil/integration/infosync_test.go +++ b/testutil/integration/infosync_test.go @@ -39,8 +39,8 @@ func TestInfoSync(t *testing.T) { lock, p2pKeys, _ := cluster.NewForT(t, 1, n, n, seed, random) asserter := &priorityAsserter{ - asserter: asserter{Timeout: time.Second * 10}, - N: n, + Timeout: time.Second * 10, + N: n, } p2pNodeCallback := testutil.NewP2PNodeCallback(t, priority.Protocols()...) diff --git a/testutil/integration/ping_test.go b/testutil/integration/ping_test.go index 902a1df584..22c10b743a 100644 --- a/testutil/integration/ping_test.go +++ b/testutil/integration/ping_test.go @@ -85,11 +85,9 @@ func pingCluster(t *testing.T, test pingTest) { random := rand.New(rand.NewSource(int64(seed))) lock, p2pKeys, _ := cluster.NewForT(t, 1, n, n, seed, random) asserter := &pingAsserter{ - asserter: asserter{ - Timeout: time.Second * 10, - }, - N: n, - Lock: lock, + Timeout: time.Second * 10, + N: n, + Lock: lock, } var eg errgroup.Group diff --git a/testutil/obolapimock/deposit.go b/testutil/obolapimock/deposit.go index e45389d25a..51d3fce860 100644 --- a/testutil/obolapimock/deposit.go +++ b/testutil/obolapimock/deposit.go @@ -140,11 +140,9 @@ func (ts *testServer) HandleSubmitPartialDeposit(writer http.ResponseWriter, req } ts.partialDeposits[depositData.PublicKey.String()] = depositBlob{ - FullDepositResponse: obolapi.FullDepositResponse{ - PublicKey: depositData.PublicKey.String(), - WithdrawalCredentials: hex.EncodeToString(depositData.WithdrawalCredentials), - Amounts: amounts, - }, + PublicKey: depositData.PublicKey.String(), + WithdrawalCredentials: hex.EncodeToString(depositData.WithdrawalCredentials), + Amounts: amounts, } } diff --git a/testutil/random.go b/testutil/random.go index 1769d72918..b79ad4cec0 100644 --- a/testutil/random.go +++ b/testutil/random.go @@ -144,11 +144,9 @@ func RandomDenebCoreVersionedAttestation() core.VersionedAttestation { vIdx := eth2p0.ValidatorIndex(0) return core.VersionedAttestation{ - VersionedAttestation: eth2spec.VersionedAttestation{ - Version: eth2spec.DataVersionDeneb, - ValidatorIndex: &vIdx, - Deneb: RandomPhase0Attestation(), - }, + Version: eth2spec.DataVersionDeneb, + ValidatorIndex: &vIdx, + Deneb: RandomPhase0Attestation(), } } @@ -156,11 +154,9 @@ func RandomElectraCoreVersionedAttestation() core.VersionedAttestation { vIdx := RandomVIdx() return core.VersionedAttestation{ - VersionedAttestation: eth2spec.VersionedAttestation{ - Version: eth2spec.DataVersionElectra, - ValidatorIndex: &vIdx, - Electra: RandomElectraAttestation(), - }, + Version: eth2spec.DataVersionElectra, + ValidatorIndex: &vIdx, + Electra: RandomElectraAttestation(), } } @@ -168,11 +164,9 @@ func RandomFuluCoreVersionedAttestation() core.VersionedAttestation { vIdx := RandomVIdx() return core.VersionedAttestation{ - VersionedAttestation: eth2spec.VersionedAttestation{ - Version: eth2spec.DataVersionFulu, - ValidatorIndex: &vIdx, - Fulu: RandomElectraAttestation(), - }, + Version: eth2spec.DataVersionFulu, + ValidatorIndex: &vIdx, + Fulu: RandomElectraAttestation(), } } @@ -207,13 +201,11 @@ func RandomAggregateAttestation() *eth2p0.Attestation { func RandomDenebCoreVersionedAggregateAttestation() core.VersionedAggregatedAttestation { return core.VersionedAggregatedAttestation{ - VersionedAttestation: eth2spec.VersionedAttestation{ - Version: eth2spec.DataVersionDeneb, - Deneb: ð2p0.Attestation{ - AggregationBits: RandomBitList(64), - Data: RandomAttestationDataPhase0(), - Signature: RandomEth2Signature(), - }, + Version: eth2spec.DataVersionDeneb, + Deneb: ð2p0.Attestation{ + AggregationBits: RandomBitList(64), + Data: RandomAttestationDataPhase0(), + Signature: RandomEth2Signature(), }, } } @@ -380,90 +372,76 @@ func RandomCapellaExecutionPayload() *capella.ExecutionPayload { func RandomBellatrixCoreVersionedProposal() core.VersionedProposal { return core.VersionedProposal{ - VersionedProposal: eth2api.VersionedProposal{ - Version: eth2spec.DataVersionBellatrix, - Bellatrix: RandomBellatrixBeaconBlock(), - }, + Version: eth2spec.DataVersionBellatrix, + Bellatrix: RandomBellatrixBeaconBlock(), } } func RandomCapellaCoreVersionedProposal() core.VersionedProposal { return core.VersionedProposal{ - VersionedProposal: eth2api.VersionedProposal{ - Version: eth2spec.DataVersionCapella, - Capella: RandomCapellaBeaconBlock(), - }, + Version: eth2spec.DataVersionCapella, + Capella: RandomCapellaBeaconBlock(), } } func RandomBellatrixCoreVersionedSignedProposal() core.VersionedSignedProposal { return core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Version: eth2spec.DataVersionBellatrix, - Bellatrix: &bellatrix.SignedBeaconBlock{ - Message: RandomBellatrixBeaconBlock(), - Signature: RandomEth2Signature(), - }, + Version: eth2spec.DataVersionBellatrix, + Bellatrix: &bellatrix.SignedBeaconBlock{ + Message: RandomBellatrixBeaconBlock(), + Signature: RandomEth2Signature(), }, } } func RandomCapellaCoreVersionedSignedProposal() core.VersionedSignedProposal { return core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Version: eth2spec.DataVersionCapella, - Capella: &capella.SignedBeaconBlock{ - Message: RandomCapellaBeaconBlock(), - Signature: RandomEth2Signature(), - }, + Version: eth2spec.DataVersionCapella, + Capella: &capella.SignedBeaconBlock{ + Message: RandomCapellaBeaconBlock(), + Signature: RandomEth2Signature(), }, } } func RandomDenebCoreVersionedSignedProposal() core.VersionedSignedProposal { return core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Version: eth2spec.DataVersionDeneb, - Deneb: ð2deneb.SignedBlockContents{ - SignedBlock: &deneb.SignedBeaconBlock{ - Message: RandomDenebBeaconBlock(), - Signature: RandomEth2Signature(), - }, - KZGProofs: []deneb.KZGProof{}, - Blobs: []deneb.Blob{}, + Version: eth2spec.DataVersionDeneb, + Deneb: ð2deneb.SignedBlockContents{ + SignedBlock: &deneb.SignedBeaconBlock{ + Message: RandomDenebBeaconBlock(), + Signature: RandomEth2Signature(), }, + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, }, } } func RandomElectraCoreVersionedSignedProposal() core.VersionedSignedProposal { return core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Version: eth2spec.DataVersionElectra, - Electra: ð2electra.SignedBlockContents{ - SignedBlock: &electra.SignedBeaconBlock{ - Message: RandomElectraBeaconBlock(), - Signature: RandomEth2Signature(), - }, - KZGProofs: []deneb.KZGProof{}, - Blobs: []deneb.Blob{}, + Version: eth2spec.DataVersionElectra, + Electra: ð2electra.SignedBlockContents{ + SignedBlock: &electra.SignedBeaconBlock{ + Message: RandomElectraBeaconBlock(), + Signature: RandomEth2Signature(), }, + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, }, } } func RandomFuluCoreVersionedSignedProposal() core.VersionedSignedProposal { return core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Version: eth2spec.DataVersionFulu, - Fulu: ð2fulu.SignedBlockContents{ - SignedBlock: &electra.SignedBeaconBlock{ - Message: RandomElectraBeaconBlock(), - Signature: RandomEth2Signature(), - }, - KZGProofs: []deneb.KZGProof{}, - Blobs: []deneb.Blob{}, + Version: eth2spec.DataVersionFulu, + Fulu: ð2fulu.SignedBlockContents{ + SignedBlock: &electra.SignedBeaconBlock{ + Message: RandomElectraBeaconBlock(), + Signature: RandomEth2Signature(), }, + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, }, } } @@ -673,85 +651,71 @@ func RandomCapellaBlindedBeaconBlockBody() *eth2capella.BlindedBeaconBlockBody { func RandomBellatrixVersionedBlindedProposal() core.VersionedProposal { return core.VersionedProposal{ - VersionedProposal: eth2api.VersionedProposal{ - Version: eth2spec.DataVersionBellatrix, - Blinded: true, - BellatrixBlinded: RandomBellatrixBlindedBeaconBlock(), - }, + Version: eth2spec.DataVersionBellatrix, + Blinded: true, + BellatrixBlinded: RandomBellatrixBlindedBeaconBlock(), } } func RandomCapellaVersionedBlindedProposal() core.VersionedProposal { return core.VersionedProposal{ - VersionedProposal: eth2api.VersionedProposal{ - Version: eth2spec.DataVersionCapella, - Blinded: true, - CapellaBlinded: RandomCapellaBlindedBeaconBlock(), - }, + Version: eth2spec.DataVersionCapella, + Blinded: true, + CapellaBlinded: RandomCapellaBlindedBeaconBlock(), } } func RandomBellatrixVersionedSignedBlindedProposal() core.VersionedSignedProposal { return core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Blinded: true, - Version: eth2spec.DataVersionBellatrix, - BellatrixBlinded: ð2bellatrix.SignedBlindedBeaconBlock{ - Message: RandomBellatrixBlindedBeaconBlock(), - Signature: RandomEth2Signature(), - }, + Blinded: true, + Version: eth2spec.DataVersionBellatrix, + BellatrixBlinded: ð2bellatrix.SignedBlindedBeaconBlock{ + Message: RandomBellatrixBlindedBeaconBlock(), + Signature: RandomEth2Signature(), }, } } func RandomCapellaVersionedSignedBlindedProposal() core.VersionedSignedProposal { return core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Blinded: true, - Version: eth2spec.DataVersionCapella, - CapellaBlinded: ð2capella.SignedBlindedBeaconBlock{ - Message: RandomCapellaBlindedBeaconBlock(), - Signature: RandomEth2Signature(), - }, + Blinded: true, + Version: eth2spec.DataVersionCapella, + CapellaBlinded: ð2capella.SignedBlindedBeaconBlock{ + Message: RandomCapellaBlindedBeaconBlock(), + Signature: RandomEth2Signature(), }, } } func RandomDenebVersionedSignedBlindedProposal() core.VersionedSignedProposal { return core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Blinded: true, - Version: eth2spec.DataVersionDeneb, - DenebBlinded: ð2deneb.SignedBlindedBeaconBlock{ - Message: RandomDenebBlindedBeaconBlock(), - Signature: RandomEth2Signature(), - }, + Blinded: true, + Version: eth2spec.DataVersionDeneb, + DenebBlinded: ð2deneb.SignedBlindedBeaconBlock{ + Message: RandomDenebBlindedBeaconBlock(), + Signature: RandomEth2Signature(), }, } } func RandomElectraVersionedSignedBlindedProposal() core.VersionedSignedProposal { return core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Blinded: true, - Version: eth2spec.DataVersionElectra, - ElectraBlinded: ð2electra.SignedBlindedBeaconBlock{ - Message: RandomElectraBlindedBeaconBlock(), - Signature: RandomEth2Signature(), - }, + Blinded: true, + Version: eth2spec.DataVersionElectra, + ElectraBlinded: ð2electra.SignedBlindedBeaconBlock{ + Message: RandomElectraBlindedBeaconBlock(), + Signature: RandomEth2Signature(), }, } } func RandomFuluVersionedSignedBlindedProposal() core.VersionedSignedProposal { return core.VersionedSignedProposal{ - VersionedSignedProposal: eth2api.VersionedSignedProposal{ - Blinded: true, - Version: eth2spec.DataVersionFulu, - FuluBlinded: ð2electra.SignedBlindedBeaconBlock{ - Message: RandomElectraBlindedBeaconBlock(), - Signature: RandomEth2Signature(), - }, + Blinded: true, + Version: eth2spec.DataVersionFulu, + FuluBlinded: ð2electra.SignedBlindedBeaconBlock{ + Message: RandomElectraBlindedBeaconBlock(), + Signature: RandomEth2Signature(), }, } } @@ -918,10 +882,8 @@ func RandomCoreVersionedSignedValidatorRegistration(t *testing.T) core.Versioned t.Helper() return core.VersionedSignedValidatorRegistration{ - VersionedSignedValidatorRegistration: eth2api.VersionedSignedValidatorRegistration{ - Version: eth2spec.BuilderVersionV1, - V1: RandomSignedValidatorRegistration(t), - }, + Version: eth2spec.BuilderVersionV1, + V1: RandomSignedValidatorRegistration(t), } } From 2f78791c2e6b5e5145c00dc9e3d3b40c6fc18abd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:01:20 +0200 Subject: [PATCH 04/29] build(deps): Bump the go-dependencies group across 1 directory with 4 updates (#4687) Bumps the go-dependencies group with 4 updates in the / directory: [github.com/prometheus/client_model](https://github.com/prometheus/client_model), [github.com/showwin/speedtest-go](https://github.com/showwin/speedtest-go), [golang.org/x/crypto](https://github.com/golang/crypto) and [github.com/vektra/mockery/v2](https://github.com/vektra/mockery). Updates `github.com/prometheus/client_model` from 0.6.2 to 0.6.3 - [Release notes](https://github.com/prometheus/client_model/releases) - [Commits](https://github.com/prometheus/client_model/compare/v0.6.2...v0.6.3) Updates `github.com/showwin/speedtest-go` from 1.8.2 to 1.8.3 - [Release notes](https://github.com/showwin/speedtest-go/releases) - [Changelog](https://github.com/showwin/speedtest-go/blob/master/docs/release.md) - [Commits](https://github.com/showwin/speedtest-go/compare/v1.8.2...v1.8.3) Updates `golang.org/x/crypto` from 0.55.0 to 0.56.0 - [Commits](https://github.com/golang/crypto/compare/v0.55.0...v0.56.0) Updates `github.com/vektra/mockery/v2` from 2.53.6 to 2.53.7 - [Release notes](https://github.com/vektra/mockery/releases) - [Commits](https://github.com/vektra/mockery/compare/v2.53.6...v2.53.7) --- updated-dependencies: - dependency-name: github.com/prometheus/client_model dependency-version: 0.6.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: github.com/showwin/speedtest-go dependency-version: 1.8.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: golang.org/x/crypto dependency-version: 0.56.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: github.com/vektra/mockery/v2 dependency-version: 2.53.7 dependency-type: indirect update-type: version-update:semver-patch dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 4eef0bda69d0f880a8a3525262f67ab7c0e8e134) --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index bba96ee2c3..1a9bc94e59 100644 --- a/go.mod +++ b/go.mod @@ -28,11 +28,11 @@ require ( github.com/libp2p/go-msgio v0.3.0 github.com/multiformats/go-multiaddr v0.16.1 github.com/prometheus/client_golang v1.24.1 - github.com/prometheus/client_model v0.6.2 + github.com/prometheus/client_model v0.6.3 github.com/protolambda/eth2-shuffle v1.1.0 github.com/r3labs/sse/v2 v2.10.0 github.com/rs/zerolog v1.35.1 - github.com/showwin/speedtest-go v1.8.2 + github.com/showwin/speedtest-go v1.8.3 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 diff --git a/go.sum b/go.sum index 33c94840dd..d598e24980 100644 --- a/go.sum +++ b/go.sum @@ -490,8 +490,8 @@ github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= +github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= @@ -533,8 +533,8 @@ github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfv github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/showwin/speedtest-go v1.8.2 h1:vvNQBnk2BFqv96fmjEI+q0bNZ1WIaBkYXYrF6bDsm9A= -github.com/showwin/speedtest-go v1.8.2/go.mod h1:N4bn2lgNNWmfJyC1AIHD4T5wYhU+9D21OfacVhJsxiU= +github.com/showwin/speedtest-go v1.8.3 h1:x988hyZFsa5V1EEwox84niAji632uxBTk8Clsc1itF8= +github.com/showwin/speedtest-go v1.8.3/go.mod h1:N4bn2lgNNWmfJyC1AIHD4T5wYhU+9D21OfacVhJsxiU= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= From caa87bc492196450defd3c2f82bffa409417d51e Mon Sep 17 00:00:00 2001 From: aly <16789036+aly-obol@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:23:34 -0400 Subject: [PATCH 05/29] app/stacksnipe: redact secret flag values from exported command lines (#4688) The stack sniping collector read the full command line of every detected validator client and exported it verbatim, both as the cli_parameters label of the app_validator_stack_params gauge served on the monitoring endpoint, and as a field of a debug log line. Validator client command lines routinely carry secret material, for example keystore passwords, the paths of files holding them, and keymanager bearer tokens. Exporting them unmodified pushes that material into the metrics and log planes, whose readers and retention are usually much broader than the process table it came from. Redact the values of flags whose names look sensitive (auth, jwt, key, passphrase, password, secret, token) before the command line is logged or handed to the metrics callback. Both the "--flag value" and the "--flag=value" forms are handled, and flag names are preserved so the telemetry stays useful. A /proc cmdline is NUL separated, so redaction normally operates per argument; if the whole command line ever arrives as a single blob it is split on whitespace first, so redaction fails safe rather than passing the blob through untouched. Also warn at startup when --proc-directory is set, and note the disclosure tradeoff in the flag's help text, so the behaviour is not a surprise. The feature remains disabled by default. (cherry picked from commit d46731260901138fae78bc4955fd88d308cd50bc) --- app/stacksnipe/stacksnipe.go | 183 ++++++++++++++++++++- app/stacksnipe/stacksnipe_internal_test.go | 134 +++++++++++++++ app/stacksnipe/stacksnipe_test.go | 101 ++++++++++++ cmd/run.go | 2 +- docs/configuration.md | 2 +- 5 files changed, 419 insertions(+), 3 deletions(-) create mode 100644 app/stacksnipe/stacksnipe_internal_test.go diff --git a/app/stacksnipe/stacksnipe.go b/app/stacksnipe/stacksnipe.go index b3a87cb594..7efdb2e464 100644 --- a/app/stacksnipe/stacksnipe.go +++ b/app/stacksnipe/stacksnipe.go @@ -8,6 +8,7 @@ import ( "io/fs" "os" "path/filepath" + "regexp" "strconv" "strings" "time" @@ -38,6 +39,31 @@ var maybeVCs = map[string]struct{}{ "node": {}, } +// sensitiveFlagFragments are the flag name fragments whose values must never leave the process. +// Validator client command lines routinely carry keystore passwords, keymanager bearer tokens and +// the paths of files holding them, none of which belong in a metric label or a log line. +var sensitiveFlagFragments = []string{ + "auth", + "jwt", + "key", + "mnemonic", + "passphrase", + "password", + "secret", + "token", +} + +// redactedValue replaces the value of a sensitive flag in an exported command line. +const redactedValue = "" + +// basicAuthRe matches the userinfo of a URL so a credential embedded in an otherwise innocuous +// flag value (e.g. https://user:pass@host) is redacted while the scheme, user and host stay visible. +var basicAuthRe = regexp.MustCompile(`([a-zA-Z][a-zA-Z0-9+.-]*://[^:@/?#\s]+):[^@/?#\s]+@`) + +// queryParamRe matches a single URL query parameter so sensitive ones (?token=..., &jwt=...) +// can be redacted by name while the rest of the value is preserved. +var queryParamRe = regexp.MustCompile(`([?&])([^=&#\s]+)=([^&#\s]*)`) + // StackComponent is a named process of the Ethereum validator stack running on the machine, // whose CLI parameters (also called cmdline) is read from a /proc-like filesystem. type StackComponent struct { @@ -79,6 +105,10 @@ func (i *Instance) Run(ctx context.Context) { return } + log.Info(ctx, "Stack component sniping enabled: command lines of detected validator clients are exported "+ + "to the monitoring endpoint and debug logs, with the values of secret-shaped flags redacted", + z.Str("proc_directory", i.procPath)) + ticker := time.NewTicker(i.interval) defer ticker.Stop() @@ -135,6 +165,157 @@ func snipe(ctx context.Context, procPath string) ([]StackComponent, error) { return ret, nil } +// isSensitiveFlag reports whether name looks like a flag whose value carries secret material. +func isSensitiveFlag(name string) bool { + name = strings.ToLower(strings.TrimLeft(name, "-")) + + for _, fragment := range sensitiveFlagFragments { + if strings.Contains(name, fragment) { + return true + } + } + + return false +} + +// redactValue scrubs secret material embedded inside a value that a flag name check misses: +// basic-auth credentials in a URL and the values of sensitive query parameters. The rest of the +// value (scheme, host, path, innocuous parameters) is preserved so telemetry stays useful. +func redactValue(value string) string { + value = basicAuthRe.ReplaceAllString(value, "${1}:"+redactedValue+"@") + + value = queryParamRe.ReplaceAllStringFunc(value, func(param string) string { + groups := queryParamRe.FindStringSubmatch(param) + if groups[3] != "" && isSensitiveFlag(groups[2]) { + return groups[1] + groups[2] + "=" + redactedValue + } + + return param + }) + + return value +} + +// splitCmdlineBlob tokenises a whole command line that arrived as a single blob, honouring single +// and double quotes so a quoted value containing spaces stays one argument (and is redacted whole) +// rather than being split into leaking fragments. +func splitCmdlineBlob(blob string) []string { + var ( + tokens []string + cur strings.Builder + quote rune + inTok bool + ) + + flush := func() { + if inTok { + tokens = append(tokens, cur.String()) + cur.Reset() + + inTok = false + } + } + + for _, r := range blob { + switch { + case quote != 0: + if r == quote { + quote = 0 + } else { + cur.WriteRune(r) + } + + inTok = true + case r == '\'' || r == '"': + quote = r + inTok = true + case r == ' ' || r == '\t': + flush() + default: + cur.WriteRune(r) + + inTok = true + } + } + + flush() + + return tokens +} + +// redactCmdline redacts the value of every sensitive flag while keeping flag names and innocuous +// values, so the exported command line stays diagnostically useful. It covers the "--flag value" +// and "--flag=value" forms, secrets embedded in a URL value of an innocuous flag (see redactValue), +// and values beginning with "-". Args are normally the NUL separated /proc elements; a whole command +// line handed over as one blob is tokenised (see splitCmdlineBlob) and its sensitive values redacted +// greedily, so redaction fails safe rather than leaking. +func redactCmdline(args []string) []string { + greedy := false + + if len(args) == 1 && strings.ContainsAny(args[0], " \t") { + args = splitCmdlineBlob(args[0]) + greedy = true + } + + redacted := make([]string, 0, len(args)) + + var ( + redactNext bool // the following token(s) are the value of a sensitive flag + valueEmitted bool // the single marker for that value has already been appended + ) + + for _, arg := range args { + if redactNext { + // A long flag means the sensitive flag took no value; stop redacting and reprocess + // this token as a flag. Anything else is (part of) the value. + if !strings.HasPrefix(arg, "--") { + if !valueEmitted { + redacted = append(redacted, redactedValue) + valueEmitted = true + } + + // A NUL separated cmdline gives one value per flag; only the ambiguous blob + // fallback keeps consuming tokens into the same value. + if !greedy { + redactNext = false + } + + continue + } + + redactNext = false + } + + if !strings.HasPrefix(arg, "-") { + redacted = append(redacted, redactValue(arg)) + continue + } + + name, value, hasValue := strings.Cut(arg, "=") + if !isSensitiveFlag(name) { + if hasValue { + redacted = append(redacted, name+"="+redactValue(value)) + } else { + redacted = append(redacted, arg) + } + + continue + } + + if hasValue { + redacted = append(redacted, name+"="+redactedValue) + continue + } + + redactNext = true + valueEmitted = false + + redacted = append(redacted, arg) + } + + return redacted +} + // walkFunc walks a /proc-like filesystem as invoked by filepath.WalkDir, and sends entries to wb. func walkFunc(ctx context.Context, wb chan<- StackComponent) fs.WalkDirFunc { cmdlineDedup := make(map[string]struct{}) @@ -216,7 +397,7 @@ func walkFunc(ctx context.Context, wb chan<- StackComponent) fs.WalkDirFunc { return nil } - cmdLineStr := strings.Join(cmdLine, " ") + cmdLineStr := strings.Join(redactCmdline(cmdLine), " ") log.Debug(ctx, "Detected stack component", z.Str("name", vcName), z.U64("host_pid", hostPID), z.Str("cmdline", cmdLineStr)) diff --git a/app/stacksnipe/stacksnipe_internal_test.go b/app/stacksnipe/stacksnipe_internal_test.go new file mode 100644 index 0000000000..2a6a456a87 --- /dev/null +++ b/app/stacksnipe/stacksnipe_internal_test.go @@ -0,0 +1,134 @@ +// Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package stacksnipe + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRedactCmdline(t *testing.T) { + tests := []struct { + name string + args []string + want []string + }{ + { + name: "sensitive flag space form", + args: []string{"--keystore-password", "SuperSecretPw123!"}, + want: []string{"--keystore-password", redactedValue}, + }, + { + name: "sensitive flag equals form", + args: []string{"--keymanager-auth-token=eyJhbGciOiJSUzI1NiJ9.secret"}, + want: []string{"--keymanager-auth-token=" + redactedValue}, + }, + { + name: "non-sensitive flags untouched", + args: []string{"--datadir", "/var/lib/lighthouse", "--debug-level", "info"}, + want: []string{"--datadir", "/var/lib/lighthouse", "--debug-level", "info"}, + }, + { + name: "value starting with a dash is still redacted", + args: []string{"--keystore-password", "-hunter2", "--debug-level", "info"}, + want: []string{"--keystore-password", redactedValue, "--debug-level", "info"}, + }, + { + name: "sensitive boolean flag followed by a long flag", + args: []string{"--keymanager", "--datadir", "/var/lib/teku"}, + want: []string{"--keymanager", "--datadir", "/var/lib/teku"}, + }, + { + name: "basic-auth credentials in a URL value of an innocuous flag", + args: []string{"--beacon-nodes", "https://user:s3cret@bn.internal:5052"}, + want: []string{"--beacon-nodes", "https://user:" + redactedValue + "@bn.internal:5052"}, + }, + { + name: "basic-auth credentials in the equals form", + args: []string{"--beacon-nodes=https://user:s3cret@bn.internal"}, + want: []string{"--beacon-nodes=https://user:" + redactedValue + "@bn.internal"}, + }, + { + name: "sensitive query parameter in a URL value", + args: []string{"--metrics-url", "https://push.internal/ingest?token=abc123&interval=5s"}, + want: []string{"--metrics-url", "https://push.internal/ingest?token=" + redactedValue + "&interval=5s"}, + }, + { + name: "innocuous query parameters untouched", + args: []string{"--beacon-nodes", "https://bn.internal?timeout=5s&retries=3"}, + want: []string{"--beacon-nodes", "https://bn.internal?timeout=5s&retries=3"}, + }, + { + name: "plain host:port URL without credentials untouched", + args: []string{"--validators-external-signer-url", "https://signer.internal:9000"}, + want: []string{"--validators-external-signer-url", "https://signer.internal:9000"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, redactCmdline(tt.args)) + }) + } +} + +// TestRedactCmdlineBlob covers the fail-safe fallback for a whole command line that arrives as a +// single blob rather than the NUL separated form a real /proc exposes. +func TestRedactCmdlineBlob(t *testing.T) { + tests := []struct { + name string + blob string + wantContain []string + wantNotContain []string + }{ + { + name: "unquoted multi word value redacts every token, not just the first", + blob: "lighthouse vc --keystore-password My Secret Pw --debug-level info", + wantContain: []string{"--keystore-password " + redactedValue, "--debug-level info"}, + wantNotContain: []string{"My", "Secret", "Pw"}, + }, + { + name: "quoted value with spaces stays one argument and is redacted whole", + blob: `lighthouse vc --keystore-password "alpha beta gamma" --debug-level info`, + wantContain: []string{"--keystore-password " + redactedValue, "--debug-level info"}, + wantNotContain: []string{"alpha", "beta", "gamma"}, + }, + { + name: "single quotes are honoured too", + blob: `teku vc --validators-keystore-password 'p a s s'`, + wantContain: []string{"--validators-keystore-password " + redactedValue}, + wantNotContain: []string{"p a s s", "a s s"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := strings.Join(redactCmdline([]string{tt.blob}), " ") + + for _, want := range tt.wantContain { + require.Contains(t, got, want) + } + + for _, notWant := range tt.wantNotContain { + require.NotContains(t, got, notWant) + } + }) + } +} + +func TestIsSensitiveFlag(t *testing.T) { + sensitive := []string{ + "--keystore-password", "--jwt-secret", "--api-token", "--wallet-passphrase", + "--mnemonic-file", "--auth-token", "--graffiti-key", + } + for _, name := range sensitive { + require.True(t, isSensitiveFlag(name), name) + } + + innocuous := []string{"--datadir", "--debug-level", "--suggested-fee-recipient", "--network"} + for _, name := range innocuous { + require.False(t, isSensitiveFlag(name), name) + } +} diff --git a/app/stacksnipe/stacksnipe_test.go b/app/stacksnipe/stacksnipe_test.go index cc8aecb2d7..c77b17eb24 100644 --- a/app/stacksnipe/stacksnipe_test.go +++ b/app/stacksnipe/stacksnipe_test.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "testing" "time" @@ -103,6 +104,106 @@ func Test_StackSnipe(t *testing.T) { require.NotContains(t, result.cmdlines, extraCmdlines[idx]) } }) + + t.Run("redacts secret flag values", func(t *testing.T) { + baseDir := t.TempDir() + + // A real /proc cmdline is NUL separated, one argument per element. + populateProc(t, baseDir, procEntry{ + pid: 42, + procName: "lighthouse", + cmdline: strings.Join([]string{ + "/usr/bin/lighthouse", "vc", + "--datadir", "/var/lib/lighthouse", + "--validators-keystore-password-file", "/run/secrets/vc-password.txt", + "--keystore-password", "SuperSecretPw123!", + "--suggested-fee-recipient", "0xdeadbeef", + "--debug-level", "info", + }, "\x00"), + }) + + populateProc(t, baseDir, procEntry{ + pid: 43, + procName: "teku", + cmdline: strings.Join([]string{ + "/usr/bin/teku", "validator-client", + "--data-path", "/var/lib/teku", + "--keymanager-auth-token=eyJhbGciOiJSUzI1NiJ9.secret-jwt", + "--validators-external-signer-url", "https://signer.internal:9000", + }, "\x00"), + }) + + var ( + ctx, cancel = context.WithCancel(context.Background()) + resultChan = make(chan snipeResult) + ) + + defer cancel() + + snipe := stacksnipe.NewWithInterval(baseDir, func(names []string, cmdlines []string) { + resultChan <- snipeResult{names: names, cmdlines: cmdlines} + + cancel() + }, 50*time.Millisecond) + + go snipe.Run(ctx) + + result := <-resultChan + require.Len(t, result.cmdlines, 2) + + exported := strings.Join(result.cmdlines, "\n") + + // No secret material survives export, in either the "--flag value" or the "--flag=value" form. + require.NotContains(t, exported, "SuperSecretPw123!") + require.NotContains(t, exported, "/run/secrets/vc-password.txt") + require.NotContains(t, exported, "eyJhbGciOiJSUzI1NiJ9.secret-jwt") + + // The flag names themselves are kept, so the telemetry stays useful. + require.Contains(t, exported, "--validators-keystore-password-file ") + require.Contains(t, exported, "--keystore-password ") + require.Contains(t, exported, "--keymanager-auth-token=") + + // Non-sensitive flags and their values are untouched. + require.Contains(t, exported, "--datadir /var/lib/lighthouse") + require.Contains(t, exported, "--suggested-fee-recipient 0xdeadbeef") + require.Contains(t, exported, "--debug-level info") + require.Contains(t, exported, "--data-path /var/lib/teku") + require.Contains(t, exported, "--validators-external-signer-url https://signer.internal:9000") + }) + + // Redaction must not silently no-op if the whole command line ever arrives as a single + // argument rather than the NUL separated form a real /proc exposes. + t.Run("redacts secret flag values in an unsplit command line", func(t *testing.T) { + baseDir := t.TempDir() + + populateProc(t, baseDir, procEntry{ + pid: 42, + procName: "lighthouse", + cmdline: "/usr/bin/lighthouse vc --keystore-password SuperSecretPw123! --debug-level info", + }) + + var ( + ctx, cancel = context.WithCancel(context.Background()) + resultChan = make(chan snipeResult) + ) + + defer cancel() + + snipe := stacksnipe.NewWithInterval(baseDir, func(names []string, cmdlines []string) { + resultChan <- snipeResult{names: names, cmdlines: cmdlines} + + cancel() + }, 50*time.Millisecond) + + go snipe.Run(ctx) + + result := <-resultChan + require.Len(t, result.cmdlines, 1) + + require.NotContains(t, result.cmdlines[0], "SuperSecretPw123!") + require.Contains(t, result.cmdlines[0], "--keystore-password ") + require.Contains(t, result.cmdlines[0], "--debug-level info") + }) } func populateProc(t *testing.T, base string, entry procEntry) { diff --git a/cmd/run.go b/cmd/run.go index b98d0a24d5..6dea05dbb2 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -97,7 +97,7 @@ func bindRunFlags(cmd *cobra.Command, config *app.Config) { cmd.Flags().Uint64Var(&config.TestnetConfig.ChainID, "testnet-chain-id", 0, "Chain ID of the custom test network.") cmd.Flags().Int64Var(&config.TestnetConfig.GenesisTimestamp, "testnet-genesis-timestamp", 0, "Genesis timestamp of the custom test network.") cmd.Flags().StringVar(&config.TestnetConfig.CapellaHardFork, "testnet-capella-hard-fork", "", "Capella hard fork version of the custom test network.") - cmd.Flags().StringVar(&config.ProcDirectory, "proc-directory", "", "Directory to look into in order to detect other stack components running on the host.") + cmd.Flags().StringVar(&config.ProcDirectory, "proc-directory", "", "Directory to look into in order to detect other stack components running on the host. Enabling this exports the command lines of detected validator clients to the monitoring endpoint and debug logs, with the values of secret-shaped flags redacted.") cmd.Flags().StringVar(&config.ConsensusProtocol, "consensus-protocol", "", "Preferred consensus protocol name for the node. Selected automatically when not specified.") cmd.Flags().StringVar(&config.Nickname, "nickname", "", "Human friendly peer nickname. Maximum 32 characters.") cmd.Flags().StringSliceVar(&config.BeaconNodeHeaders, "beacon-node-headers", nil, "Comma separated list of headers formatted as header=value") diff --git a/docs/configuration.md b/docs/configuration.md index 09cb850501..296bb7d916 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -199,7 +199,7 @@ Flags: --p2p-udp-address strings Comma-separated list of listening UDP addresses (ip and port) for libP2P traffic. Empty default doesn't bind to local port therefore only supports outgoing connections. --private-key-file string The path to the charon enr private key file. (default ".charon/charon-enr-private-key") --private-key-file-lock Enables private key locking to prevent multiple instances using the same key. - --proc-directory string Directory to look into in order to detect other stack components running on the host. + --proc-directory string Directory to look into in order to detect other stack components running on the host. Enabling this exports the command lines of detected validator clients to the monitoring endpoint and debug logs, with the values of secret-shaped flags redacted. --publish-address string The URL of the remote API for background fee recipient fetching. (default "https://api.obol.tech/v1") --publish-timeout duration Timeout for accessing the remote API. (default 5m0s) --simnet-beacon-mock Enables an internal mock beacon node for running a simnet. From 6b6b7510b30e3b2d9dea617b22d0ce852813a100 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:35:50 +0200 Subject: [PATCH 06/29] build(deps): Bump the go-dependencies group with 2 updates (#4690) Bumps the go-dependencies group with 2 updates: [golang.org/x/sync](https://github.com/golang/sync) and [golang.org/x/time](https://github.com/golang/time). Updates `golang.org/x/sync` from 0.22.0 to 0.23.0 - [Commits](https://github.com/golang/sync/compare/v0.22.0...v0.23.0) Updates `golang.org/x/time` from 0.15.0 to 0.16.0 - [Commits](https://github.com/golang/time/compare/v0.15.0...v0.16.0) --- updated-dependencies: - dependency-name: golang.org/x/sync dependency-version: 0.23.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/time dependency-version: 0.16.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 67f4b594a224fb57ae68511bb6301e4fe5f32032) # Conflicts: # go.mod # go.sum --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1a9bc94e59..dfeab771f3 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( golang.org/x/sync v0.23.0 golang.org/x/term v0.46.0 golang.org/x/text v0.42.0 - golang.org/x/time v0.15.0 + golang.org/x/time v0.16.0 golang.org/x/tools v0.49.0 google.golang.org/protobuf v1.36.12 gopkg.in/natefinch/lumberjack.v2 v2.2.1 diff --git a/go.sum b/go.sum index d598e24980..91b005029f 100644 --- a/go.sum +++ b/go.sum @@ -706,8 +706,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= +golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= From fd4a495f5a4559234ff446915c887ed3f3418f9f Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:25:51 +0200 Subject: [PATCH 07/29] dkg/pedersen: buffer channels to not drop early bundles (#4683) * dkg/pedersen: buffer bundle channels to avoid dropping early bundles * test response and justification bundle paths (cherry picked from commit 7464e1327b8adabada1ab3369ffc5451f05e6442) --- dkg/pedersen/board.go | 37 +++-- dkg/pedersen/board_internal_test.go | 238 ++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 9 deletions(-) create mode 100644 dkg/pedersen/board_internal_test.go diff --git a/dkg/pedersen/board.go b/dkg/pedersen/board.go index 0e4ead7e0c..a3e9a18ecb 100644 --- a/dkg/pedersen/board.go +++ b/dkg/pedersen/board.go @@ -72,15 +72,18 @@ var ( // In the future Kyber fork we will address this and fix all logging as well. func NewBoard(ctx context.Context, host host.Host, config *Config, bcastComp *bcast.Component) *Board { board := &Board{ - logCtx: log.WithTopic(ctx, "pedersen"), - host: host, - sender: new(p2p.Sender), - config: config, - bcastComp: bcastComp, - dedup: newBundleDedup(), - dealCh: make(chan kdkg.DealBundle), - responseCh: make(chan kdkg.ResponseBundle), - justificationCh: make(chan kdkg.JustificationBundle), + logCtx: log.WithTopic(ctx, "pedersen"), + host: host, + sender: new(p2p.Sender), + config: config, + bcastComp: bcastComp, + dedup: newBundleDedup(), + // Buffer bundle channels to cluster size: peers ahead in the ceremony send + // bundles before this node's kyber protocol reads them, and each node sends + // at most one bundle of each type per protocol run. + dealCh: make(chan kdkg.DealBundle, config.Nodes()), + responseCh: make(chan kdkg.ResponseBundle, config.Nodes()), + justificationCh: make(chan kdkg.JustificationBundle, config.Nodes()), dealOutCh: make(chan kdkg.DealBundle), responseOutCh: make(chan kdkg.ResponseBundle), justificationOutCh: make(chan kdkg.JustificationBundle), @@ -334,6 +337,8 @@ func (b *Board) handleDealBundleMessage(ctx context.Context, peerID peer.ID, msg select { case b.dealCh <- bundle: case <-ctx.Done(): + // Forget the dropped bundle so a redelivery is not refused as a duplicate. + b.dedup.forget(dealBundleMsg, protoBundle.GetSignature()) log.Error(b.logCtx, "Dropping deal bundle, context done", nil, z.Str("from", peerID.String())) } @@ -362,6 +367,8 @@ func (b *Board) handleResponseBundleMessage(ctx context.Context, peerID peer.ID, select { case b.responseCh <- bundle: case <-ctx.Done(): + // Forget the dropped bundle so a redelivery is not refused as a duplicate. + b.dedup.forget(respBundleMsg, protoBundle.GetSignature()) log.Error(b.logCtx, "Dropping response bundle, context done", nil, z.Str("from", peerID.String())) } @@ -388,6 +395,8 @@ func (b *Board) handleJustificationBundleMessage(ctx context.Context, peerID pee select { case b.justificationCh <- bundle: case <-ctx.Done(): + // Forget the dropped bundle so a redelivery is not refused as a duplicate. + b.dedup.forget(justBundleMsg, protoBundle.GetSignature()) log.Error(b.logCtx, "Dropping justification bundle, context done", nil, z.Str("from", peerID.String())) } @@ -444,6 +453,16 @@ func (d *bundleDedup) isDuplicate(msgType string, signature []byte) bool { return false } +// forget removes a bundle from the seen set, allowing it to be delivered again. +func (d *bundleDedup) forget(msgType string, signature []byte) { + h := sha256.Sum256(append([]byte(msgType), signature...)) + + d.mu.Lock() + defer d.mu.Unlock() + + delete(d.seen, h) +} + // forwardBundles forwards bundles from in to out until ctx is canceled, then closes out. // Kyber FastSync protocol goroutines exit when a board channel closes, so this stops // them without modifying the kyber library. diff --git a/dkg/pedersen/board_internal_test.go b/dkg/pedersen/board_internal_test.go new file mode 100644 index 0000000000..83f80c4269 --- /dev/null +++ b/dkg/pedersen/board_internal_test.go @@ -0,0 +1,238 @@ +// Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package pedersen + +import ( + "context" + "testing" + "time" + + "github.com/drand/kyber" + kdkg "github.com/drand/kyber/share/dkg" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" + + "github.com/obolnetwork/charon/cluster" + "github.com/obolnetwork/charon/testutil" +) + +// bundleKind abstracts delivering a bundle of one type via its p2p handler and +// receiving it from the corresponding incoming board channel. +type bundleKind struct { + name string + deliver func(ctx context.Context, t *testing.T, board *Board, from peer.ID, idx uint32, session, sig []byte) + receive func(t *testing.T, board *Board) (uint32, bool) +} + +func bundleKinds() []bundleKind { + return []bundleKind{ + { + name: "deal", + deliver: func(ctx context.Context, t *testing.T, board *Board, from peer.ID, idx uint32, session, sig []byte) { + t.Helper() + + protoBundle, err := DealBundleToProto(kdkg.DealBundle{ + DealerIndex: idx, + Deals: []kdkg.Deal{ + { + ShareIndex: 1, + EncryptedShare: []byte{1, 2, 3}, + }, + }, + Public: []kyber.Point{RandomPoint(t)}, + SessionID: session, + Signature: sig, + }) + require.NoError(t, err) + + _, _, err = board.handleDealBundleMessage(ctx, from, protoBundle) + require.NoError(t, err) + }, + receive: func(t *testing.T, board *Board) (uint32, bool) { + t.Helper() + + select { + case bundle := <-board.IncomingDeal(): + return bundle.DealerIndex, true + case <-time.After(time.Second): + return 0, false + } + }, + }, + { + name: "response", + deliver: func(ctx context.Context, t *testing.T, board *Board, from peer.ID, idx uint32, session, sig []byte) { + t.Helper() + + protoBundle, err := ResponseBundleToProto(kdkg.ResponseBundle{ + ShareIndex: idx, + Responses: []kdkg.Response{ + { + DealerIndex: 1, + Status: true, + }, + }, + SessionID: session, + Signature: sig, + }) + require.NoError(t, err) + + _, _, err = board.handleResponseBundleMessage(ctx, from, protoBundle) + require.NoError(t, err) + }, + receive: func(t *testing.T, board *Board) (uint32, bool) { + t.Helper() + + select { + case bundle := <-board.IncomingResponse(): + return bundle.ShareIndex, true + case <-time.After(time.Second): + return 0, false + } + }, + }, + { + name: "justification", + deliver: func(ctx context.Context, t *testing.T, board *Board, from peer.ID, idx uint32, session, sig []byte) { + t.Helper() + + protoBundle, err := JustificationBundleToProto(kdkg.JustificationBundle{ + DealerIndex: idx, + Justifications: []kdkg.Justification{ + { + ShareIndex: 1, + Share: RandomScalar(t), + }, + }, + SessionID: session, + Signature: sig, + }) + require.NoError(t, err) + + _, _, err = board.handleJustificationBundleMessage(ctx, from, protoBundle) + require.NoError(t, err) + }, + receive: func(t *testing.T, board *Board) (uint32, bool) { + t.Helper() + + select { + case bundle := <-board.IncomingJustification(): + return bundle.DealerIndex, true + case <-time.After(time.Second): + return 0, false + } + }, + }, + } +} + +// TestBoardBuffersEarlyBundles verifies that bundles arriving before the kyber +// protocol starts reading the incoming board channels are buffered and delivered +// later, instead of being dropped when the p2p request context expires. This +// happens when peers start the DKG a few seconds earlier than this node. +func TestBoardBuffersEarlyBundles(t *testing.T) { + const ( + numNodes = 4 + threshold = 3 + ) + + for _, kind := range bundleKinds() { + t.Run(kind.name, func(t *testing.T) { + board, nodes, session := newTestBoard(t, numNodes, threshold) + + // Deliver bundles from all other peers before anything reads the + // incoming channel. Each handler call gets a short-lived context, + // like an expiring p2p request context. + for i := 1; i < numNodes; i++ { + ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) + kind.deliver(ctx, t, board, nodes[i].NodeHost.ID(), uint32(i), session, []byte{byte(i)}) + cancel() + } + + // All early bundles must be delivered once the protocol starts reading. + received := make(map[uint32]bool) + + for i := 1; i < numNodes; i++ { + idx, ok := kind.receive(t, board) + if !ok { + require.Failf(t, "early bundle dropped", "received %d of %d bundles", len(received), numNodes-1) + } + + received[idx] = true + } + + require.Len(t, received, numNodes-1) + }) + } +} + +// TestBoardAcceptsRedeliveryAfterDrop verifies that a bundle dropped because the +// request context expired is not recorded as a duplicate, so a redelivery of the +// same bundle is accepted. +func TestBoardAcceptsRedeliveryAfterDrop(t *testing.T) { + const ( + numNodes = 4 + threshold = 3 + ) + + for _, kind := range bundleKinds() { + t.Run(kind.name, func(t *testing.T) { + board, nodes, session := newTestBoard(t, numNodes, threshold) + from := nodes[1].NodeHost.ID() + + // Fill the channel buffer plus the forwarder's in-flight slot so the + // next delivery blocks and gets dropped. + total := numNodes + 1 + for i := range total { + ctx, cancel := context.WithTimeout(t.Context(), time.Second) + kind.deliver(ctx, t, board, from, uint32(i), session, []byte{byte(i)}) + cancel() + } + + // This bundle is dropped since nothing is reading yet and the buffer is full. + droppedIdx := uint32(total) + droppedSig := []byte{byte(total)} + + ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) + kind.deliver(ctx, t, board, from, droppedIdx, session, droppedSig) + cancel() + + // Drain the buffered bundles. + for range total { + _, ok := kind.receive(t, board) + require.True(t, ok, "buffered bundle not delivered") + } + + // Redeliver the dropped bundle. It must not be refused as a duplicate. + ctx, cancel = context.WithTimeout(t.Context(), time.Second) + defer cancel() + + kind.deliver(ctx, t, board, from, droppedIdx, session, droppedSig) + + idx, ok := kind.receive(t, board) + require.True(t, ok, "redelivered bundle refused as duplicate") + require.Equal(t, droppedIdx, idx) + }) + } +} + +func newTestBoard(t *testing.T, numNodes, threshold int) (*Board, []*TestNode, []byte) { + t.Helper() + + var ( + peers []peer.ID + peerMap = make(map[peer.ID]cluster.NodeIdx) + session = testutil.RandomArray32() + ) + + nodes := make([]*TestNode, numNodes) + for i := range numNodes { + nodes[i] = NewTestNode(t, i) + peerMap[nodes[i].NodeHost.ID()] = nodes[i].NodeIdx + peers = append(peers, nodes[i].NodeHost.ID()) + } + + nodes[0].InitBoard(t, threshold, peers, peerMap, session[:]) + + return nodes[0].Board, nodes, session[:] +} From ba011872e0ed736f554a45d4e2d7356218ed7276 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:27:15 +0200 Subject: [PATCH 08/29] dkg: retry transient p2p send failures during ceremonies (#4684) * p2p: include peer name in send errors * dkg: retry transient p2p send failures during ceremonies * p2p: address copilot review on send retries * dkg: add frost round 1 send retry test * dkg: apply 60s p2p send timeout to ceremony sends * dkg: increase ceremony send retries to 5 * p2p: bound send retries by the send timeout budget * p2p: bound stream creation by the send timeout budget * p2p: cap each send attempt at its slice of the timeout budget * p2p: bound each attempt's dial by its deadline slice * p2p: give each SendReceive attempt the full timeout, clamp negative retries * p2p: bound SendReceive by its send timeout without slicing the response wait * p2p: make send timeout per-attempt instead of a shared budget (cherry picked from commit c878ff063a3ca0966eb73c5a8d89b76835e7059d) --- dkg/bcast/client.go | 4 +- dkg/bcast/helpers.go | 5 + dkg/bcast/impl_test.go | 153 +++++++++++++++ dkg/exchanger.go | 30 ++- dkg/exchanger_internal_test.go | 62 +++++++ dkg/frostp2p.go | 2 +- dkg/frostp2p_internal_test.go | 86 +++++++++ dkg/pedersen/board.go | 13 +- dkg/pedersen/board_test.go | 58 ++++++ docs/metrics.md | 2 +- p2p/metrics.go | 2 +- p2p/sender.go | 106 +++++++++-- p2p/sender_test.go | 329 ++++++++++++++++++++++++++++++++- testutil/random.go | 57 ++++++ 14 files changed, 885 insertions(+), 24 deletions(-) diff --git a/dkg/bcast/client.go b/dkg/bcast/client.go index 25c410faf8..5f6c9d5048 100644 --- a/dkg/bcast/client.go +++ b/dkg/bcast/client.go @@ -65,7 +65,7 @@ func (c *client) Broadcast(ctx context.Context, msgID string, msg proto.Message) fork, join, cancel := forkjoin.New(ctx, func(ctx context.Context, pID peer.ID) (*pb.BCastSigResponse, error) { sigResp := new(pb.BCastSigResponse) - err := c.sendRecvFunc(ctx, c.p2pNode, pID, sigReq, sigResp, protocolIDSig) + err := c.sendRecvFunc(ctx, c.p2pNode, pID, sigReq, sigResp, protocolIDSig, p2p.WithSendTimeout(sendTimeout), p2p.WithRetries(sendRetries)) return sigResp, err }) @@ -133,7 +133,7 @@ func (c *client) Broadcast(ctx context.Context, msgID string, msg proto.Message) continue // Skip self. } - err := c.sendFunc(ctx, c.p2pNode, protocolIDMsg, pID, bcastMsg, p2p.WithSendTimeout(sendTimeout)) + err := c.sendFunc(ctx, c.p2pNode, protocolIDMsg, pID, bcastMsg, p2p.WithSendTimeout(sendTimeout), p2p.WithRetries(sendRetries)) if err != nil { return errors.Wrap(err, "send message") } diff --git a/dkg/bcast/helpers.go b/dkg/bcast/helpers.go index ac6f4a1228..cdb5a4af82 100644 --- a/dkg/bcast/helpers.go +++ b/dkg/bcast/helpers.go @@ -20,6 +20,11 @@ const ( protocolIDMsg = protocolIDPrefix + "/msg" receiveTimeout = time.Minute // Allow for peers to be out of sync, with some sending messages much earlier and having to wait. sendTimeout = receiveTimeout + 2*time.Second // Allow for server to timeout first. + + // sendRetries is the number of additional p2p send attempts. A DKG ceremony aborts on the + // first failed exchange, so transient failures (e.g. a stalled relay connection) are retried + // instead of failing the whole ceremony. Receivers deduplicate re-delivered messages. + sendRetries = 5 ) // hashFunc is a function that hashes a message ID and a any-wrapped protobuf message. diff --git a/dkg/bcast/impl_test.go b/dkg/bcast/impl_test.go index f50b9d3531..b39016453f 100644 --- a/dkg/bcast/impl_test.go +++ b/dkg/bcast/impl_test.go @@ -5,11 +5,13 @@ package bcast_test import ( "context" "testing" + "time" k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peerstore" + "github.com/libp2p/go-libp2p/core/protocol" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" @@ -150,6 +152,157 @@ func TestBCast(t *testing.T) { assertResults(t, p0Result, peers[0]) } +// TestBCastRetriesTransientSendFailures ensures a reliable-broadcast survives transient +// p2p send failures in each phase instead of aborting the whole ceremony on the first one. +func TestBCastRetriesTransientSendFailures(t *testing.T) { + const ( + n = 3 + msgID = "msgID" + ) + + // The bcast protocol IDs are unexported; keep in sync with helpers.go. + tests := []struct { + name string + flakyProtocol protocol.ID + }{ + {name: "signature request phase", flakyProtocol: "/charon/dkg/bcast/2.0.0/sig"}, + {name: "message send phase", flakyProtocol: "/charon/dkg/bcast/2.0.0/msg"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var ( + ctx = context.Background() + secrets []*k1.PrivateKey + tcpNodes []host.Host + peers []peer.ID + ) + + for range n { + secret, err := k1.GeneratePrivateKey() + require.NoError(t, err) + + secrets = append(secrets, secret) + + tcpNode := testutil.CreateHostWithIdentity(t, testutil.AvailableAddr(t), secret) + tcpNodes = append(tcpNodes, tcpNode) + + peers = append(peers, tcpNode.ID()) + } + + for i := range n { + for j := range n { + tcpNodes[i].Peerstore().AddAddrs(tcpNodes[j].ID(), tcpNodes[j].Addrs(), peerstore.PermanentAddrTTL) + } + } + + // Node 0's first send to each peer in this phase fails transiently. + tcpNodes[0] = testutil.NewFlakyHost(tcpNodes[0], n-1, test.flakyProtocol) + + received := make(chan proto.Message, n) + callback := func(_ context.Context, _ peer.ID, _ string, msg proto.Message) error { + received <- msg + return nil + } + checkMessage := func(_ context.Context, _ peer.ID, msgAny *anypb.Any) error { + var ts timestamppb.Timestamp + if err := msgAny.UnmarshalTo(&ts); err != nil { + return errors.Wrap(err, "anypb error") + } + + return nil + } + + var bcasts []bcast.BroadcastFunc + + for i := range n { + bcastFunc := bcast.New(tcpNodes[i], peers, secrets[i], []byte("session hash")) + bcastFunc.RegisterMessageIDFuncs(msgID, callback, checkMessage) + bcasts = append(bcasts, bcastFunc.Broadcast) + } + + msg := timestamppb.Now() + err := bcasts[0](ctx, msgID, msg) + require.NoError(t, err) + + for range n - 1 { + require.True(t, proto.Equal(msg, <-received)) + } + }) + } +} + +// TestBCastSigRequestAllowsSlowPeer ensures a signature request tolerates a peer that +// responds slower than the default p2p send timeout (7s), since ceremony peers may +// lawfully lag by up to the bcast receive timeout (1min) while catching up. +func TestBCastSigRequestAllowsSlowPeer(t *testing.T) { + if testing.Short() { + t.Skip("skipping slow-peer timeout test in short mode") + } + + const ( + n = 2 + msgID = "msgID" + // slowDelay must exceed the 7s default p2p send timeout to prove the + // bcast send timeout is applied to signature requests. + slowDelay = 8 * time.Second + ) + + var ( + ctx = context.Background() + secrets []*k1.PrivateKey + tcpNodes []host.Host + peers []peer.ID + bcasts []bcast.BroadcastFunc + ) + + for range n { + secret, err := k1.GeneratePrivateKey() + require.NoError(t, err) + + secrets = append(secrets, secret) + + tcpNode := testutil.CreateHostWithIdentity(t, testutil.AvailableAddr(t), secret) + tcpNodes = append(tcpNodes, tcpNode) + + peers = append(peers, tcpNode.ID()) + } + + for i := range n { + for j := range n { + tcpNodes[i].Peerstore().AddAddrs(tcpNodes[j].ID(), tcpNodes[j].Addrs(), peerstore.PermanentAddrTTL) + } + } + + callback := func(context.Context, peer.ID, string, proto.Message) error { + return nil + } + + for i := range n { + delay := time.Duration(0) + if i == 1 { + delay = slowDelay // Node 1 is slow to check (and so sign) node 0's message. + } + + checkMessage := func(_ context.Context, _ peer.ID, msgAny *anypb.Any) error { + time.Sleep(delay) + + var ts timestamppb.Timestamp + if err := msgAny.UnmarshalTo(&ts); err != nil { + return errors.Wrap(err, "anypb error") + } + + return nil + } + + bcastFunc := bcast.New(tcpNodes[i], peers, secrets[i], []byte("session hash")) + bcastFunc.RegisterMessageIDFuncs(msgID, callback, checkMessage) + bcasts = append(bcasts, bcastFunc.Broadcast) + } + + require.NoError(t, bcasts[0](ctx, msgID, timestamppb.Now())) +} + // TestBCastSessionHashMismatch ensures that messages signed in one session // cannot be verified in another, binding broadcasts to the cluster session. func TestBCastSessionHashMismatch(t *testing.T) { diff --git a/dkg/exchanger.go b/dkg/exchanger.go index 94b85bb984..48a44276cb 100644 --- a/dkg/exchanger.go +++ b/dkg/exchanger.go @@ -11,6 +11,8 @@ import ( "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + "google.golang.org/protobuf/proto" "github.com/obolnetwork/charon/app/errors" "github.com/obolnetwork/charon/app/z" @@ -38,6 +40,32 @@ const ( // Do not add new values greater than sigDepositData. ) +const ( + // sendRetries is the number of additional p2p send attempts during DKG ceremonies. + // A DKG aborts on the first failed exchange, so transient failures (e.g. a stalled + // relay connection) are retried instead of failing the whole ceremony. All DKG + // receive paths deduplicate messages, so a retry of an already-delivered message + // is harmless. + sendRetries = 5 + + // sendTimeout is the per-attempt p2p send timeout during DKG ceremonies. Ceremony peers + // may lag behind (e.g. slower operators or relay-routed connections), so sends get much + // more headroom than the 7s p2p default. + sendTimeout = time.Minute +) + +// newRetryingSendFunc returns a p2p.SendFunc wrapping p2p.Send with the given send +// timeout and sendRetries retries. +func newRetryingSendFunc(timeout time.Duration) p2p.SendFunc { + return func(ctx context.Context, p2pNode host.Host, protoID protocol.ID, peerID peer.ID, + msg proto.Message, opts ...p2p.SendRecvOption, + ) error { + opts = append(opts, p2p.WithSendTimeout(timeout), p2p.WithRetries(sendRetries)) + + return p2p.Send(ctx, p2pNode, protoID, peerID, msg, opts...) + } +} + // sigTypeStore is a shorthand for a map of sigType to map of core.PubKey to slice of core.ParSignedData. type sigTypeStore map[sigType]map[core.PubKey][]core.ParSignedData @@ -111,7 +139,7 @@ func newExchanger(p2pNode host.Host, peerIdx int, peers []peer.ID, peerMap map[p ex := &exchanger{ // threshold is len(peers) to wait until we get all the partial sigs from all the peers per DV sigdb: parsigdb.NewMemDB(len(peers), noopDeadliner{}, parsigdb.NewMemDBMetadata(0, time.Now())), // metadata timestamps are used for metrics, irrelevant for DKG - sigex: parsigex.NewParSigEx(p2pNode, p2p.Send, peerIdx, peers, verifyShareIdx, dutyGaterFunc, p2p.WithSendTimeout(timeout), p2p.WithReceiveTimeout(timeout)), + sigex: parsigex.NewParSigEx(p2pNode, newRetryingSendFunc(timeout), peerIdx, peers, verifyShareIdx, dutyGaterFunc, p2p.WithSendTimeout(timeout), p2p.WithReceiveTimeout(timeout)), sigTypes: st, sigData: dataByPubkey{ store: sigTypeStore{}, diff --git a/dkg/exchanger_internal_test.go b/dkg/exchanger_internal_test.go index 86656c5816..09771acc77 100644 --- a/dkg/exchanger_internal_test.go +++ b/dkg/exchanger_internal_test.go @@ -194,6 +194,68 @@ func TestExchanger(t *testing.T) { require.Len(t, actual, len(expectedSigTypes)) } +// TestExchangerRetriesTransientSendFailures ensures the DKG signature exchange survives +// transient p2p send failures instead of aborting the whole ceremony on the first one. +func TestExchangerRetriesTransientSendFailures(t *testing.T) { + ctx := context.Background() + + const nodes = 3 + + pubkey := testutil.RandomCorePubKey(t) + + var ( + peers []peer.ID + hosts []host.Host + hostsInfo []peer.AddrInfo + ) + + for range nodes { + h := testutil.CreateHost(t, testutil.AvailableAddr(t)) + hostsInfo = append(hostsInfo, peer.AddrInfo{ID: h.ID(), Addrs: h.Addrs()}) + peers = append(peers, h.ID()) + hosts = append(hosts, h) + } + + for i := range nodes { + for j := range nodes { + if i == j { + continue + } + + hosts[i].Peerstore().AddAddrs(hostsInfo[j].ID, hostsInfo[j].Addrs, peerstore.PermanentAddrTTL) + } + } + + // Node 0's first send to each peer fails transiently. + hosts[0] = testutil.NewFlakyHost(hosts[0], nodes-1) + + var exchangers []*exchanger + + for i := range nodes { + ex, err := newExchanger(hosts[i], i, peers, positionalPeerMap(peers), []sigType{sigLock}, 8*time.Second) + require.NoError(t, err) + + exchangers = append(exchangers, ex) + } + + errChan := make(chan error, nodes) + + for i := range nodes { + go func(node int) { + set := core.ParSignedDataSet{ + pubkey: core.NewPartialSignature(testutil.RandomCoreSignature(), node+1), + } + + _, err := exchangers[node].exchange(ctx, sigLock, set) + errChan <- err + }(i) + } + + for range nodes { + require.NoError(t, <-errChan) + } +} + // TestExchangerPushPsigsNeverBlocks fires pushPsigs repeatedly with no exchange call draining // results, which must not block. A previous implementation used a shared size-1 channel that // deadlocked once full, especially when pushPsigs ran synchronously on the exchange goroutine. diff --git a/dkg/frostp2p.go b/dkg/frostp2p.go index 896332cede..24bca7de6d 100644 --- a/dkg/frostp2p.go +++ b/dkg/frostp2p.go @@ -305,7 +305,7 @@ func (f *frostP2P) Round1(ctx context.Context, castR1 map[msgKey]frost.Round1Bca return nil, nil, errors.New("bug: unexpected p2p message to self") } - err := p2p.Send(ctx, f.p2pNode, round1P2PID, pID, p2pMsg) + err := p2p.Send(ctx, f.p2pNode, round1P2PID, pID, p2pMsg, p2p.WithSendTimeout(sendTimeout), p2p.WithRetries(sendRetries)) if err != nil { return nil, nil, err } diff --git a/dkg/frostp2p_internal_test.go b/dkg/frostp2p_internal_test.go index 2e72dae8d9..21389f752c 100644 --- a/dkg/frostp2p_internal_test.go +++ b/dkg/frostp2p_internal_test.go @@ -5,17 +5,103 @@ package dkg import ( "context" "testing" + "time" k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/peerstore" "github.com/stretchr/testify/require" "github.com/obolnetwork/charon/cluster" + "github.com/obolnetwork/charon/dkg/bcast" pb "github.com/obolnetwork/charon/dkg/dkgpb/v1" "github.com/obolnetwork/charon/testutil" ) +// TestFrostRound1RetriesTransientSendFailures ensures the frost round-1 p2p transport +// survives transient send failures instead of aborting the whole ceremony on the first one. +func TestFrostRound1RetriesTransientSendFailures(t *testing.T) { + const ( + nodes = 3 + threshold = 2 + numVals = 2 + dkgCtx = "test round 1 retries" + ) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + var ( + secrets []*k1.PrivateKey + hosts []host.Host + peers []peer.ID + peerMap = make(map[peer.ID]cluster.NodeIdx) + ) + + for i := range nodes { + secret, err := k1.GeneratePrivateKey() + require.NoError(t, err) + + secrets = append(secrets, secret) + + h := testutil.CreateHostWithIdentity(t, testutil.AvailableAddr(t), secret) + hosts = append(hosts, h) + peers = append(peers, h.ID()) + peerMap[h.ID()] = cluster.NodeIdx{PeerIdx: i, ShareIdx: i + 1} + } + + for i := range nodes { + for j := range nodes { + if i == j { + continue + } + + hosts[i].Peerstore().AddAddrs(peers[j], hosts[j].Addrs(), peerstore.PermanentAddrTTL) + } + } + + // Node 0's first round-1 p2p send to each peer fails transiently, + // leaving the reliable-broadcast phase untouched. + hosts[0] = testutil.NewFlakyHost(hosts[0], nodes-1, round1P2PID) + + var transports []*frostP2P + + for i := range nodes { + bcastComp := bcast.New(hosts[i], peers, secrets[i], []byte("session hash")) + + tp, err := newFrostP2P(hosts[i], peerMap, bcastComp, threshold, numVals) + require.NoError(t, err) + + transports = append(transports, tp) + } + + errChan := make(chan error, nodes) + + for i := range nodes { + go func(node int) { + validators, err := newFrostParticipants(numVals, nodes, threshold, uint32(node+1), dkgCtx) + if err != nil { + errChan <- err + return + } + + castR1, p2pR1, err := round1(validators) + if err != nil { + errChan <- err + return + } + + _, _, err = transports[node].Round1(ctx, castR1, p2pR1) + errChan <- err + }(i) + } + + for range nodes { + require.NoError(t, <-errChan) + } +} + func TestBcastCallback(t *testing.T) { const ( n = 4 diff --git a/dkg/pedersen/board.go b/dkg/pedersen/board.go index a3e9a18ecb..815ec560da 100644 --- a/dkg/pedersen/board.go +++ b/dkg/pedersen/board.go @@ -8,6 +8,7 @@ import ( "crypto/sha256" "path" "sync" + "time" kdkg "github.com/drand/kyber/share/dkg" "github.com/libp2p/go-libp2p/core/host" @@ -56,6 +57,16 @@ type ValidatorPubKeyShare struct { const ( protocolID = "/charon/dkg/pedersen/1.0.0" + + // sendRetries is the number of additional p2p send attempts. A DKG ceremony cannot + // complete if a bundle is dropped, so transient failures (e.g. a stalled relay + // connection) are retried instead. Receivers deduplicate re-delivered messages. + sendRetries = 5 + + // sendTimeout is the per-attempt p2p send timeout. Ceremony peers may lag behind + // (e.g. slower operators or relay-routed connections), so sends get much more + // headroom than the 7s p2p default. + sendTimeout = time.Minute ) var ( @@ -257,7 +268,7 @@ func (b *Board) broadcastP2P(ctx context.Context, msgID string, msg proto.Messag continue } - if err := b.sender.SendAsync(ctx, b.host, protocol.ID(msgID), peerID, msg); err != nil { + if err := b.sender.SendAsync(ctx, b.host, protocol.ID(msgID), peerID, msg, p2p.WithSendTimeout(sendTimeout), p2p.WithRetries(sendRetries)); err != nil { return errors.Wrap(err, "p2p send", z.Str("msg", msgID), z.Str("to", peerID.String())) } } diff --git a/dkg/pedersen/board_test.go b/dkg/pedersen/board_test.go index 97a8c07fbb..f012d4da59 100644 --- a/dkg/pedersen/board_test.go +++ b/dkg/pedersen/board_test.go @@ -4,6 +4,7 @@ package pedersen_test import ( "testing" + "time" "github.com/drand/kyber" kdkg "github.com/drand/kyber/share/dkg" @@ -15,6 +16,63 @@ import ( "github.com/obolnetwork/charon/testutil" ) +// TestBoardRetriesTransientSendFailures ensures board bundle broadcasts survive transient +// p2p send failures instead of silently dropping the bundle for a peer. +func TestBoardRetriesTransientSendFailures(t *testing.T) { + const ( + numNodes = 3 + threshold = 2 + ) + + var ( + peers []peer.ID + peerMap = make(map[peer.ID]cluster.NodeIdx) + session = testutil.RandomArray32() + ) + + nodes := make([]*pedersen.TestNode, numNodes) + for i := range numNodes { + nodes[i] = pedersen.NewTestNode(t, i) + peerMap[nodes[i].NodeHost.ID()] = nodes[i].NodeIdx + peers = append(peers, nodes[i].NodeHost.ID()) + } + + pedersen.ConnectTestNodes(t, nodes) + + // Node 0's first send to each peer fails transiently. + nodes[0].NodeHost = testutil.NewFlakyHost(nodes[0].NodeHost, numNodes-1) + + for i := range nodes { + nodes[i].InitBoard(t, threshold, peers, peerMap, session[:]) + } + + dealBundle := kdkg.DealBundle{ + DealerIndex: 0, + Deals: []kdkg.Deal{ + { + ShareIndex: 1, + EncryptedShare: []byte{1, 2, 3}, + }, + }, + Public: []kyber.Point{ + pedersen.RandomPoint(t), + }, + SessionID: []byte("sessionID"), + Signature: []byte{13, 14, 15}, + } + + nodes[0].Board.PushDeals(&dealBundle) + + for i := range nodes { + select { + case received := <-nodes[i].Board.IncomingDeal(): + require.Equal(t, []byte{13, 14, 15}, received.Signature) + case <-time.After(10 * time.Second): + require.Fail(t, "timed out waiting for deal bundle", "node %d", i) + } + } +} + func TestBoard(t *testing.T) { const ( numNodes = 4 diff --git a/docs/metrics.md b/docs/metrics.md index b20cfe7c03..488ba70cca 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -111,7 +111,7 @@ when storing metrics from multiple nodes or clusters in one Prometheus instance. | `p2p_reachability_status` | Gauge | Current libp2p reachability status of this node as detected by autonat: unknown(0), public(1) or private(2). | | | `p2p_relay_connection_types` | Gauge | Current number of libp2p connections by relay, type (`direct` or `relay`), and protocol (`tcp`, `quic`). Note that peers may have multiple connections. | `peer, type, protocol` | | `p2p_relay_connections` | Gauge | Connected relays by name | `peer` | -| `p2p_send_duration_seconds` | Histogram | Wall-clock duration of synchronous libp2p Send (one-way) and SendReceive (round-trip) calls, by peer, protocol, and topic. Topic is a sub-protocol label (e.g. qbft_pre_prepare, parsigex_proposer); empty when not set by the caller. | `peer, protocol, topic` | +| `p2p_send_duration_seconds` | Histogram | Wall-clock duration of synchronous libp2p Send (one-way) and SendReceive (round-trip) attempts, by peer, protocol, and topic. Sends with retries enabled observe each attempt separately, excluding backoff. Topic is a sub-protocol label (e.g. qbft_pre_prepare, parsigex_proposer); empty when not set by the caller. | `peer, protocol, topic` | | `relay_p2p_active_connections` | Gauge | Current number of active connections by peer and cluster | `peer, peer_cluster` | | `relay_p2p_connection_total` | Counter | Total number of new connections by peer and cluster | `peer, peer_cluster` | | `relay_p2p_network_receive_bytes_total` | Counter | Total number of network bytes received from the peer and cluster | `peer, peer_cluster` | diff --git a/p2p/metrics.go b/p2p/metrics.go index 6f4a04398c..b7049c36cc 100644 --- a/p2p/metrics.go +++ b/p2p/metrics.go @@ -36,7 +36,7 @@ var ( sendDurations = promauto.NewHistogramVec(prometheus.HistogramOpts{ Namespace: "p2p", Name: "send_duration_seconds", - Help: "Wall-clock duration of synchronous libp2p Send (one-way) and SendReceive (round-trip) calls, by peer, protocol, and topic. Topic is a sub-protocol label (e.g. qbft_pre_prepare, parsigex_proposer); empty when not set by the caller.", + Help: "Wall-clock duration of synchronous libp2p Send (one-way) and SendReceive (round-trip) attempts, by peer, protocol, and topic. Sends with retries enabled observe each attempt separately, excluding backoff. Topic is a sub-protocol label (e.g. qbft_pre_prepare, parsigex_proposer); empty when not set by the caller.", Buckets: prometheus.ExponentialBuckets(0.0001, 2, 18), // 0.1ms .. ~13.1s, covers the ~7s default SendReceive timeout }, []string{"peer", "protocol", "topic"}) diff --git a/p2p/sender.go b/p2p/sender.go index 3006c03fb9..0280ca2e96 100644 --- a/p2p/sender.go +++ b/p2p/sender.go @@ -17,6 +17,7 @@ import ( "google.golang.org/protobuf/proto" "github.com/obolnetwork/charon/app/errors" + "github.com/obolnetwork/charon/app/expbackoff" "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/z" ) @@ -202,6 +203,7 @@ type sendRecvOpts struct { rttCallback func(time.Duration) receiveTimeout time.Duration sendTimeout time.Duration + retries int // Number of additional send attempts after a failure. metricTopic string // Optional sub-protocol label for the send_duration metric. } @@ -213,12 +215,26 @@ func WithReceiveTimeout(timeout time.Duration) func(*sendRecvOpts) { } // WithSendTimeout returns an option for SendReceive that sets a timeout for sending messages. +// The timeout applies per attempt, so with WithRetries the whole call can take up to +// (retries+1) times this timeout plus the inter-attempt backoff. func WithSendTimeout(timeout time.Duration) func(*sendRecvOpts) { return func(opts *sendRecvOpts) { opts.sendTimeout = timeout } } +// WithRetries returns an option that retries a failed send up to the given number of +// additional attempts, each on a fresh stream with the full send timeout, backing off +// briefly in between. Only use it for protocols whose handlers tolerate duplicate delivery, +// since a send that failed on the sender side may still have been delivered (e.g. DKG +// ceremony messages, which are deduplicated by all receivers). Negative values are clamped +// to zero (no retries); left unclamped they would make the retry loop skip the send entirely. +func WithRetries(retries int) func(*sendRecvOpts) { + return func(opts *sendRecvOpts) { + opts.retries = max(retries, 0) + } +} + // WithSendMetricTopic returns an option that adds a topic label to the send_duration metric. // Use a small bounded set of values (e.g. a message-type name) to keep metric cardinality low. func WithSendMetricTopic(topic string) func(*sendRecvOpts) { @@ -288,6 +304,32 @@ func defaultSendRecvOpts(pID protocol.ID) sendRecvOpts { } } +// withRetries calls fn and retries it up to the given number of additional times, +// backing off briefly between attempts. Each attempt is independently timed by fn (via +// the send timeout), so the whole call takes up to (retries+1) attempts plus the backoffs. +// Cancellation of ctx stops retrying and surfaces as the context error (with the last +// attempt error attached as a field), so callers can detect it with errors.Is. +func withRetries(ctx context.Context, retries int, fn func() error) error { + var err error + + for attempt := range retries + 1 { + err = fn() + if err == nil || attempt == retries { + return err + } + + timer := time.NewTimer(expbackoff.Backoff(expbackoff.FastConfig, attempt)) + select { + case <-ctx.Done(): + timer.Stop() + return errors.Wrap(ctx.Err(), "aborting send retries", z.Err(err)) + case <-timer.C: + } + } + + return err // Unreachable: the final iteration (attempt == retries) always returns. +} + // SendReceive sends and receives a libp2p request and response message // pair synchronously and then closes the stream. // The provided response proto will be populated if err is nil. @@ -295,8 +337,6 @@ func defaultSendRecvOpts(pID protocol.ID) sendRecvOpts { func SendReceive(ctx context.Context, p2pNode host.Host, peerID peer.ID, req, resp proto.Message, pID protocol.ID, opts ...SendRecvOption, ) error { - tStart := time.Now() - if !isZeroProto(resp) { return errors.New("bug: response proto must be zero value") } @@ -306,23 +346,44 @@ func SendReceive(ctx context.Context, p2pNode host.Host, peerID peer.ID, opt(&o) } + return withRetries(ctx, o.retries, func() error { + // A failed attempt may have partially populated the response. + proto.Reset(resp) + + return sendReceive(ctx, p2pNode, peerID, req, resp, pID, o) + }) +} + +// sendReceive is a single SendReceive attempt, timed out after the send timeout. +func sendReceive(ctx context.Context, p2pNode host.Host, peerID peer.ID, + req, resp proto.Message, pID protocol.ID, o sendRecvOpts, +) error { + tStart := time.Now() + protoLabel := string(pID) // Updated to the negotiated protocol once NewStream succeeds. defer func() { sendDurations.WithLabelValues(PeerName(peerID), protoLabel, o.metricTopic).Observe(time.Since(tStart).Seconds()) }() + // The send timeout bounds this attempt: the context covers dialing and protocol + // negotiation, the stream deadline covers the write and response read. + deadline := time.Now().Add(o.sendTimeout) + + ctx, cancel := context.WithDeadline(ctx, deadline) + defer cancel() + // Circuit relay connections are transient s, err := p2pNode.NewStream(network.WithAllowLimitedConn(ctx, ""), peerID, o.protocols...) if err != nil { - return errors.Wrap(err, "new stream", z.Any("protocols", o.protocols)) + return errors.Wrap(err, "new stream", z.Any("protocols", o.protocols), z.Str("peer", PeerName(peerID))) } defer s.Close() protoLabel = string(s.Protocol()) - if err := s.SetDeadline(time.Now().Add(o.sendTimeout)); err != nil { - return errors.Wrap(err, "set deadline") + if err := s.SetDeadline(deadline); err != nil { + return errors.Wrap(err, "set deadline", z.Str("peer", PeerName(peerID))) } writeFunc, ok := o.writersByProtocol[s.Protocol()] @@ -341,7 +402,7 @@ func SendReceive(ctx context.Context, p2pNode host.Host, peerID peer.ID, t0 := time.Now() if err = writer.WriteMsg(req); err != nil { - return errors.Wrap(err, "write request", z.Any("protocol", s.Protocol())) + return errors.Wrap(err, "write request", z.Any("protocol", s.Protocol()), z.Str("peer", PeerName(peerID))) } if err := s.CloseWrite(); err != nil { @@ -354,12 +415,12 @@ func SendReceive(ctx context.Context, p2pNode host.Host, peerID peer.ID, if isCanceledStreamErr(err) { log.Debug(ctx, "Closing write of canceled stream", z.Err(err), z.Any("protocol", s.Protocol())) } else { - return errors.Wrap(err, "close write", z.Any("protocol", s.Protocol())) + return errors.Wrap(err, "close write", z.Any("protocol", s.Protocol()), z.Str("peer", PeerName(peerID))) } } if err = reader.ReadMsg(resp); err != nil { - return errors.Wrap(err, "read response", z.Any("protocol", s.Protocol())) + return errors.Wrap(err, "read response", z.Any("protocol", s.Protocol()), z.Str("peer", PeerName(peerID))) } o.rttCallback(time.Since(t0)) @@ -371,13 +432,22 @@ func SendReceive(ctx context.Context, p2pNode host.Host, peerID peer.ID, func Send(ctx context.Context, p2pNode host.Host, protoID protocol.ID, peerID peer.ID, msg proto.Message, opts ...SendRecvOption, ) error { - t0 := time.Now() - o := defaultSendRecvOpts(protoID) for _, opt := range opts { opt(&o) } + return withRetries(ctx, o.retries, func() error { + return send(ctx, p2pNode, protoID, peerID, msg, o) + }) +} + +// send is a single Send attempt, timed out after the send timeout. +func send(ctx context.Context, p2pNode host.Host, protoID protocol.ID, peerID peer.ID, msg proto.Message, + o sendRecvOpts, +) error { + t0 := time.Now() + protoLabel := string(protoID) if len(o.protocols) > 0 { protoLabel = string(o.protocols[0]) @@ -386,17 +456,25 @@ func Send(ctx context.Context, p2pNode host.Host, protoID protocol.ID, peerID pe defer func() { sendDurations.WithLabelValues(PeerName(peerID), protoLabel, o.metricTopic).Observe(time.Since(t0).Seconds()) }() + + // The send timeout bounds this attempt: the context covers dialing and protocol + // negotiation, the stream deadline covers the message write. + deadline := time.Now().Add(o.sendTimeout) + + ctx, cancel := context.WithDeadline(ctx, deadline) + defer cancel() + // Circuit relay connections are transient s, err := p2pNode.NewStream(network.WithAllowLimitedConn(ctx, ""), peerID, o.protocols...) if err != nil { - return errors.Wrap(err, "p2pNode stream") + return errors.Wrap(err, "p2pNode stream", z.Str("peer", PeerName(peerID))) } defer s.Close() protoLabel = string(s.Protocol()) - if err := s.SetDeadline(time.Now().Add(o.sendTimeout)); err != nil { - return errors.Wrap(err, "set deadline") + if err := s.SetDeadline(deadline); err != nil { + return errors.Wrap(err, "set deadline", z.Str("peer", PeerName(peerID))) } writeFunc, ok := o.writersByProtocol[s.Protocol()] @@ -405,7 +483,7 @@ func Send(ctx context.Context, p2pNode host.Host, protoID protocol.ID, peerID pe } if err = writeFunc(s).WriteMsg(msg); err != nil { - return errors.Wrap(err, "write message", z.Any("protocol", s.Protocol())) + return errors.Wrap(err, "write message", z.Any("protocol", s.Protocol()), z.Str("peer", PeerName(peerID))) } return nil diff --git a/p2p/sender_test.go b/p2p/sender_test.go index 21a646357e..dd998729b8 100644 --- a/p2p/sender_test.go +++ b/p2p/sender_test.go @@ -12,13 +12,17 @@ import ( "time" "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peerstore" "github.com/libp2p/go-libp2p/core/protocol" "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" "google.golang.org/protobuf/proto" "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/app/z" pbv1 "github.com/obolnetwork/charon/core/corepb/v1" "github.com/obolnetwork/charon/p2p" "github.com/obolnetwork/charon/testutil" @@ -121,10 +125,9 @@ func TestSendReceiveClosesStreamOnError(t *testing.T) { func TestWithSendTimeout(t *testing.T) { servers := []host.Host{testutil.CreateHost(t, testutil.AvailableAddr(t)), testutil.CreateQUICHost(t, testutil.AvailableUDPAddr(t))} clients := []host.Host{testutil.CreateHost(t, testutil.AvailableAddr(t)), testutil.CreateQUICHost(t, testutil.AvailableUDPAddr(t))} - errors := []string{"deadline reached", "deadline exceeded"} for i := range len(servers) { - client, server, errorStr := clients[i], servers[i], errors[i] + client, server := clients[i], servers[i] client.Peerstore().AddAddrs(server.ID(), server.Addrs(), time.Hour) @@ -138,13 +141,333 @@ func TestWithSendTimeout(t *testing.T) { return nil, false, nil }) + // Depending on how far the tiny budget lets the call get, the deadline error + // surfaces at dialing ("context deadline exceeded"), or on the stream + // ("deadline reached" on TCP, "deadline exceeded" on QUIC). err := p2p.SendReceive(context.Background(), client, server.ID(), new(pbv1.Duty), new(pbv1.Duty), protocolID, p2p.WithSendTimeout(sendTimeout)) require.Error(t, err) - require.ErrorContains(t, err, errorStr) + require.ErrorContains(t, err, "deadline") + } +} + +// errFields extracts the structured fields of an error into a map. +func errFields(t *testing.T, err error) map[string]any { + t.Helper() + + fielder, ok := err.(interface{ Fields() []z.Field }) + require.True(t, ok, "error does not have structured fields") + + enc := zapcore.NewMapObjectEncoder() + + for _, field := range fielder.Fields() { + field(func(zf zap.Field) { + zf.AddTo(enc) + }) + } + + return enc.Fields +} + +func TestSendErrorContainsPeerField(t *testing.T) { + ctx := context.Background() + server := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) + + peerName := p2p.PeerName(server.ID()) + + t.Run("send new stream error", func(t *testing.T) { + // No handler registered on the server, so protocol negotiation fails. + err := p2p.Send(ctx, client, "unknown", server.ID(), &pbv1.Duty{Slot: 1}) + require.Error(t, err) + require.Equal(t, peerName, errFields(t, err)["peer"]) + }) + + t.Run("send receive new stream error", func(t *testing.T) { + err := p2p.SendReceive(ctx, client, server.ID(), new(pbv1.Duty), new(pbv1.Duty), "unknown") + require.Error(t, err) + require.Equal(t, peerName, errFields(t, err)["peer"]) + }) + + t.Run("send write error", func(t *testing.T) { + protocolID := protocol.ID("testprotocol-peerfield") + p2p.RegisterHandler("test", server, protocolID, + func() proto.Message { return new(pbv1.Duty) }, + func(context.Context, peer.ID, proto.Message) (proto.Message, bool, error) { + return nil, false, nil + }) + + // The negative send timeout sets an already-expired stream deadline, failing the write. + err := p2p.Send(ctx, client, protocolID, server.ID(), &pbv1.Duty{Slot: 1}, + p2p.WithSendTimeout(-time.Second)) + require.Error(t, err) + require.Equal(t, peerName, errFields(t, err)["peer"]) + }) +} + +func TestSendRetries(t *testing.T) { + ctx := context.Background() + server := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) + + received := make(chan *pbv1.Duty, 1) + protocolID := protocol.ID("testprotocol-retries") + p2p.RegisterHandler("test", server, protocolID, + func() proto.Message { return new(pbv1.Duty) }, + func(_ context.Context, _ peer.ID, req proto.Message) (proto.Message, bool, error) { + duty, ok := req.(*pbv1.Duty) + require.True(t, ok) + + received <- duty + + return nil, false, nil + }) + + t.Run("no retries by default", func(t *testing.T) { + flaky := testutil.NewFlakyHost(client, 1) + err := p2p.Send(ctx, flaky, protocolID, server.ID(), &pbv1.Duty{Slot: 1}) + require.Error(t, err) + require.Equal(t, 1, flaky.Calls()) + }) + + t.Run("retries transient failures", func(t *testing.T) { + flaky := testutil.NewFlakyHost(client, 2) + err := p2p.Send(ctx, flaky, protocolID, server.ID(), &pbv1.Duty{Slot: 2}, p2p.WithRetries(2)) + require.NoError(t, err) + require.Equal(t, 3, flaky.Calls()) + + select { + case duty := <-received: + require.EqualValues(t, 2, duty.GetSlot()) + case <-time.After(5 * time.Second): + require.Fail(t, "timed out waiting for message") + } + }) + + t.Run("fails when retries exhausted", func(t *testing.T) { + flaky := testutil.NewFlakyHost(client, 3) + err := p2p.Send(ctx, flaky, protocolID, server.ID(), &pbv1.Duty{Slot: 3}, p2p.WithRetries(2)) + require.ErrorContains(t, err, "transient stream failure") + require.Equal(t, 3, flaky.Calls()) + }) + + t.Run("each attempt gets the full send timeout", func(t *testing.T) { + // blocking dials never succeed, so every attempt runs until its own send + // timeout. The send timeout is per attempt, so the total spans all attempts. + const perAttempt = 300 * time.Millisecond + + blocking := blockingHost{Host: client} + t0 := time.Now() + + err := p2p.Send(ctx, blocking, protocolID, server.ID(), &pbv1.Duty{Slot: 5}, + p2p.WithRetries(2), p2p.WithSendTimeout(perAttempt)) + require.ErrorIs(t, err, context.DeadlineExceeded) + // 3 attempts each get the full timeout (not a sliced share of it). + require.GreaterOrEqual(t, time.Since(t0), 3*perAttempt) + }) + + t.Run("cancellation surfaces as context error", func(t *testing.T) { + cancelledCtx, cancel := context.WithCancel(ctx) + cancel() + + flaky := testutil.NewFlakyHost(client, 10) + err := p2p.Send(cancelledCtx, flaky, protocolID, server.ID(), &pbv1.Duty{Slot: 4}, p2p.WithRetries(5)) + require.ErrorIs(t, err, context.Canceled) + require.Equal(t, 1, flaky.Calls()) + }) +} + +// blockingHost wraps a host whose NewStream blocks until the context is done, +// simulating a hung dial or protocol negotiation. +type blockingHost struct { + host.Host +} + +func (h blockingHost) NewStream(ctx context.Context, _ peer.ID, _ ...protocol.ID) (network.Stream, error) { + <-ctx.Done() + return nil, ctx.Err() +} + +func TestSendTimeoutBoundsStreamCreation(t *testing.T) { + ctx := context.Background() + server := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) + + blocking := blockingHost{Host: client} + + tests := []struct { + name string + send func() error + }{ + { + name: "send", + send: func() error { + return p2p.Send(ctx, blocking, "proto", server.ID(), &pbv1.Duty{Slot: 1}, + p2p.WithSendTimeout(100*time.Millisecond), p2p.WithRetries(2)) + }, + }, + { + name: "send receive", + send: func() error { + return p2p.SendReceive(ctx, blocking, server.ID(), new(pbv1.Duty), new(pbv1.Duty), "proto", + p2p.WithSendTimeout(100*time.Millisecond), p2p.WithRetries(2)) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + errChan := make(chan error, 1) + go func() { + errChan <- test.send() + }() + + select { + case err := <-errChan: + require.ErrorIs(t, err, context.DeadlineExceeded) + case <-time.After(2 * time.Second): + require.Fail(t, "send blocked past its timeout budget on stream creation") + } + }) + } +} + +// blockOnceHost wraps a host whose first NewStream blocks until the context is done, +// simulating a hung dial on the first attempt only. +type blockOnceHost struct { + host.Host + + calls atomic.Int32 +} + +func (h *blockOnceHost) NewStream(ctx context.Context, peerID peer.ID, pIDs ...protocol.ID) (network.Stream, error) { + if h.calls.Add(1) == 1 { + <-ctx.Done() + return nil, ctx.Err() + } + + return h.Host.NewStream(ctx, peerID, pIDs...) +} + +// TestSendRetriesAfterStalledDial ensures a hung dial only consumes its slice of the +// total send budget, leaving room for a retry. +func TestSendRetriesAfterStalledDial(t *testing.T) { + ctx := context.Background() + server := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) + + received := make(chan *pbv1.Duty, 1) + protocolID := protocol.ID("testprotocol-stalled-dial") + p2p.RegisterHandler("test", server, protocolID, + func() proto.Message { return new(pbv1.Duty) }, + func(_ context.Context, _ peer.ID, req proto.Message) (proto.Message, bool, error) { + duty, ok := req.(*pbv1.Duty) + require.True(t, ok) + + received <- duty + + return nil, false, nil + }) + + blocking := &blockOnceHost{Host: client} + err := p2p.Send(ctx, blocking, protocolID, server.ID(), &pbv1.Duty{Slot: 7}, + p2p.WithSendTimeout(2*time.Second), p2p.WithRetries(2)) + require.NoError(t, err) + require.GreaterOrEqual(t, blocking.calls.Load(), int32(2)) + + select { + case duty := <-received: + require.EqualValues(t, 7, duty.GetSlot()) + case <-time.After(5 * time.Second): + require.Fail(t, "timed out waiting for message") } } +// TestSendReceiveAllowsSlowResponse ensures each SendReceive attempt gets the full send +// timeout for the response wait, not a slice of it, so a peer that legitimately responds +// slower than sendTimeout/(retries+1) is not aborted. +func TestSendReceiveAllowsSlowResponse(t *testing.T) { + ctx := context.Background() + server := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) + + var calls atomic.Int32 + + protocolID := protocol.ID("testprotocol-slow-response") + p2p.RegisterHandler("test", server, protocolID, + func() proto.Message { return new(pbv1.Duty) }, + func(_ context.Context, _ peer.ID, req proto.Message) (proto.Message, bool, error) { + calls.Add(1) + // Respond slower than a sliced budget (1s/6 ≈ 166ms) would allow, but + // well within the full 1s send timeout. + time.Sleep(400 * time.Millisecond) + + duty, ok := req.(*pbv1.Duty) + require.True(t, ok) + + return &pbv1.Duty{Slot: duty.GetSlot() + 1}, true, nil + }) + + resp := new(pbv1.Duty) + err := p2p.SendReceive(ctx, client, server.ID(), &pbv1.Duty{Slot: 41}, resp, protocolID, + p2p.WithSendTimeout(time.Second), p2p.WithRetries(5)) + require.NoError(t, err) + require.EqualValues(t, 42, resp.GetSlot()) + require.EqualValues(t, 1, calls.Load(), "slow but valid response must succeed on the first attempt") +} + +func TestWithRetriesClampsNegative(t *testing.T) { + ctx := context.Background() + server := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) + + protocolID := protocol.ID("testprotocol-negative-retries") + p2p.RegisterHandler("test", server, protocolID, + func() proto.Message { return new(pbv1.Duty) }, + func(context.Context, peer.ID, proto.Message) (proto.Message, bool, error) { + return nil, false, nil + }) + + // A negative retry count must not panic (it would divide by zero when computing the + // per-attempt slice) and behaves as zero retries. + require.NotPanics(t, func() { + flaky := testutil.NewFlakyHost(client, 1) + err := p2p.Send(ctx, flaky, protocolID, server.ID(), &pbv1.Duty{Slot: 1}, p2p.WithRetries(-1)) + require.Error(t, err) + require.Equal(t, 1, flaky.Calls()) + }) +} + +func TestSendReceiveRetries(t *testing.T) { + ctx := context.Background() + server := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client := testutil.CreateHost(t, testutil.AvailableAddr(t)) + client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) + + protocolID := protocol.ID("testprotocol-sendrecv-retries") + p2p.RegisterHandler("test", server, protocolID, + func() proto.Message { return new(pbv1.Duty) }, + func(_ context.Context, _ peer.ID, req proto.Message) (proto.Message, bool, error) { + duty, ok := req.(*pbv1.Duty) + require.True(t, ok) + + return &pbv1.Duty{Slot: duty.GetSlot() + 1}, true, nil + }) + + flaky := testutil.NewFlakyHost(client, 2) + resp := new(pbv1.Duty) + err := p2p.SendReceive(ctx, flaky, server.ID(), &pbv1.Duty{Slot: 41}, resp, protocolID, p2p.WithRetries(2)) + require.NoError(t, err) + require.Equal(t, 3, flaky.Calls()) + require.EqualValues(t, 42, resp.GetSlot()) +} + func TestSend(t *testing.T) { var ( undelimID = protocol.ID("undelimited") diff --git a/testutil/random.go b/testutil/random.go index b79ad4cec0..a1a911f2c6 100644 --- a/testutil/random.go +++ b/testutil/random.go @@ -4,13 +4,16 @@ package testutil import ( + "context" "crypto/ecdsa" crand "crypto/rand" "fmt" "math" "math/rand" "net" + "slices" "strings" + "sync" "testing" "time" @@ -34,11 +37,15 @@ import ( "github.com/libp2p/go-libp2p" p2pcrypto "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" quic "github.com/libp2p/go-libp2p/p2p/transport/quic" //nolint:revive // Must be imported with alias "github.com/libp2p/go-libp2p/p2p/transport/tcp" "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" + "github.com/obolnetwork/charon/app/errors" "github.com/obolnetwork/charon/core" "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/enr" @@ -1527,6 +1534,56 @@ func CreateHost(t *testing.T, addr *net.TCPAddr, opts ...libp2p.Option) host.Hos return CreateHostWithIdentity(t, addr, pkey, opts...) } +// NewFlakyHost wraps a host, failing the first `failures` outbound NewStream calls +// with a transient error. If protocols are provided, only NewStream calls for those +// protocols fail; other calls pass through. It is used to test p2p send retry behavior. +func NewFlakyHost(h host.Host, failures int, protocols ...protocol.ID) *FlakyHost { + return &FlakyHost{Host: h, failures: failures, protocols: protocols} +} + +// FlakyHost is a host whose first outbound NewStream calls fail with a transient error. +type FlakyHost struct { + host.Host + + protocols []protocol.ID + + mu sync.Mutex + failures int + calls int +} + +func (h *FlakyHost) NewStream(ctx context.Context, peerID peer.ID, pIDs ...protocol.ID) (network.Stream, error) { + match := len(h.protocols) == 0 + for _, pID := range pIDs { + if slices.Contains(h.protocols, pID) { + match = true + } + } + + h.mu.Lock() + h.calls++ + fail := match && h.failures > 0 + + if fail { + h.failures-- + } + h.mu.Unlock() + + if fail { + return nil, errors.New("transient stream failure") + } + + return h.Host.NewStream(ctx, peerID, pIDs...) +} + +// Calls returns the number of NewStream calls made on the host. +func (h *FlakyHost) Calls() int { + h.mu.Lock() + defer h.mu.Unlock() + + return h.calls +} + func CreateHostWithIdentity(t *testing.T, addr *net.TCPAddr, secret *k1.PrivateKey, opts ...libp2p.Option) host.Host { t.Helper() From 7722a0fc543d18376484c0848bc297490a7e76a3 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:37:24 +0200 Subject: [PATCH 09/29] p2p: add message size, read error and concurrency metrics (#4680) * p2p: add message size and concurrency metrics * p2p: update inflight gauge under lock * p2p: cleanup idle inflight keys, observe sent after close write * p2p: consistent relay error handling, concurrency test * p2p: count direct-stream resets as read errors * p2p: observe received size before proto validation * p2p: named struct for inflight key (cherry picked from commit c6a6d42a9d0081d97f50d5f42a38d119c07b41b5) --- docs/metrics.md | 6 + p2p/metrics.go | 110 ++++++++++++++++++ p2p/metrics_internal_test.go | 208 +++++++++++++++++++++++++++++++++++ p2p/receive.go | 23 +++- p2p/sender.go | 12 ++ 5 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 p2p/metrics_internal_test.go diff --git a/docs/metrics.md b/docs/metrics.md index 488ba70cca..d18f0517a0 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -100,6 +100,10 @@ when storing metrics from multiple nodes or clusters in one Prometheus instance. | `core_validatorapi_request_latency_seconds` | Histogram | The validatorapi request latencies in seconds by endpoint | `endpoint` | | `core_validatorapi_request_total` | Counter | The total number of requests per content-type and endpoint | `endpoint, content_type` | | `core_validatorapi_vc_user_agent` | Gauge | Gauge with label set to user agent string of requests made by VC | `user_agent` | +| `p2p_concurrent_requests` | Histogram | Number of concurrently handled inbound messages, observed at each message arrival, by protocol and sending peer. Unlike the sampled inflight_requests gauge, this captures bursts between scrapes. | `protocol, peer` | +| `p2p_handler_duration_seconds` | Histogram | Duration of inbound libp2p message handling from stream accept to handler completion by protocol. Explains inflight_requests: inflight equals message rate times this duration. | `protocol` | +| `p2p_inflight_requests` | Gauge | Current number of inbound libp2p messages being handled (stream accept to handler completion) by protocol and sending peer. | `protocol, peer` | +| `p2p_message_read_errors_total` | Counter | Total number of failures reading a libp2p message by protocol and sending peer. Includes messages exceeding the protocol read limit. | `protocol, peer` | | `p2p_peer_connection_total` | Counter | Total number of libp2p connections per peer. | `peer` | | `p2p_peer_connection_types` | Gauge | Current number of libp2p connections by peer, type (`direct` or `relay`), and protocol (`tcp`, `quic`). Note that peers may have multiple connections. | `peer, type, protocol` | | `p2p_peer_network_receive_bytes_total` | Counter | Total number of network bytes received from the peer by protocol and transport. Transport is based on first active connection (accurate in steady state). | `peer, protocol, transport` | @@ -109,9 +113,11 @@ when storing metrics from multiple nodes or clusters in one Prometheus instance. | `p2p_ping_latency_secs` | Histogram | Ping latencies in seconds per peer | `peer` | | `p2p_ping_success` | Gauge | Whether the last ping was successful (1) or not (0). Can be used as proxy for connected peers | `peer` | | `p2p_reachability_status` | Gauge | Current libp2p reachability status of this node as detected by autonat: unknown(0), public(1) or private(2). | | +| `p2p_received_message_size_bytes` | Histogram | Size in bytes of received libp2p protobuf messages by protocol and sending peer. | `protocol, peer` | | `p2p_relay_connection_types` | Gauge | Current number of libp2p connections by relay, type (`direct` or `relay`), and protocol (`tcp`, `quic`). Note that peers may have multiple connections. | `peer, type, protocol` | | `p2p_relay_connections` | Gauge | Connected relays by name | `peer` | | `p2p_send_duration_seconds` | Histogram | Wall-clock duration of synchronous libp2p Send (one-way) and SendReceive (round-trip) attempts, by peer, protocol, and topic. Sends with retries enabled observe each attempt separately, excluding backoff. Topic is a sub-protocol label (e.g. qbft_pre_prepare, parsigex_proposer); empty when not set by the caller. | `peer, protocol, topic` | +| `p2p_sent_message_size_bytes` | Histogram | Size in bytes of sent libp2p protobuf messages by protocol. Not labelled by peer since duty messages are broadcast identically to all peers. | `protocol` | | `relay_p2p_active_connections` | Gauge | Current number of active connections by peer and cluster | `peer, peer_cluster` | | `relay_p2p_connection_total` | Counter | Total number of new connections by peer and cluster | `peer, peer_cluster` | | `relay_p2p_network_receive_bytes_total` | Counter | Total number of network bytes received from the peer and cluster | `peer, peer_cluster` | diff --git a/p2p/metrics.go b/p2p/metrics.go index b7049c36cc..3d3fab28fa 100644 --- a/p2p/metrics.go +++ b/p2p/metrics.go @@ -3,6 +3,7 @@ package p2p import ( + "sync" "time" "github.com/libp2p/go-libp2p" @@ -12,6 +13,7 @@ import ( "github.com/libp2p/go-libp2p/core/protocol" "github.com/libp2p/go-libp2p/p2p/net/swarm" "github.com/prometheus/client_golang/prometheus" + "google.golang.org/protobuf/proto" "github.com/obolnetwork/charon/app/promauto" ) @@ -99,8 +101,116 @@ var ( Name: "peer_network_sent_bytes_total", Help: "Total number of network bytes sent to the peer by protocol and transport. Transport is based on first active connection (accurate in steady state).", }, []string{"peer", "protocol", "transport"}) + + receivedMsgSizeHist = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "p2p", + Name: "received_message_size_bytes", + Help: "Size in bytes of received libp2p protobuf messages by protocol and sending peer.", + Buckets: messageSizeBuckets, + }, []string{"protocol", "peer"}) + + sentMsgSizeHist = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "p2p", + Name: "sent_message_size_bytes", + Help: "Size in bytes of sent libp2p protobuf messages by protocol. Not labelled by peer since duty messages are broadcast identically to all peers.", + Buckets: messageSizeBuckets, + }, []string{"protocol"}) + + msgReadErrorCounter = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "p2p", + Name: "message_read_errors_total", + Help: "Total number of failures reading a libp2p message by protocol and sending peer. Includes messages exceeding the protocol read limit.", + }, []string{"protocol", "peer"}) + + inflightGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "p2p", + Name: "inflight_requests", + Help: "Current number of inbound libp2p messages being handled (stream accept to handler completion) by protocol and sending peer.", + }, []string{"protocol", "peer"}) + + concurrentRequestsHist = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "p2p", + Name: "concurrent_requests", + Help: "Number of concurrently handled inbound messages, observed at each message arrival, by protocol and sending peer. Unlike the sampled inflight_requests gauge, this captures bursts between scrapes.", + Buckets: []float64{1, 2, 4, 8, 16, 32, 64, 128, 256}, + }, []string{"protocol", "peer"}) + + handlerDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "p2p", + Name: "handler_duration_seconds", + Help: "Duration of inbound libp2p message handling from stream accept to handler completion by protocol. Explains inflight_requests: inflight equals message rate times this duration.", + Buckets: prometheus.ExponentialBuckets(0.001, 2, 15), // 1ms .. ~16s, covers the ~10s default receive timeout. + }, []string{"protocol"}) ) +// inflightKey identifies a per-protocol, per-peer inflight request counter. +type inflightKey struct { + protocol string + peer string +} + +// inflightCounts tracks the number of concurrently handled inbound messages per (protocol, peer). +// It is the source of truth for both inflightGauge and concurrentRequestsHist so they never drift. +var inflightCounts = struct { + sync.Mutex + + counts map[inflightKey]int +}{counts: make(map[inflightKey]int)} + +// observeHandlerStart records the start of inbound message handling and returns a done function +// to be called (deferred) when handling completes. +func observeHandlerStart(pID protocol.ID, peerID peer.ID) func() { + key := inflightKey{protocol: string(pID), peer: PeerName(peerID)} + t0 := time.Now() + + inflightCounts.Lock() + inflightCounts.counts[key]++ + n := inflightCounts.counts[key] + // Update the gauge while holding the lock so concurrent Set calls cannot be reordered. + inflightGauge.WithLabelValues(key.protocol, key.peer).Set(float64(n)) + inflightCounts.Unlock() + + concurrentRequestsHist.WithLabelValues(key.protocol, key.peer).Observe(float64(n)) + + return func() { + inflightCounts.Lock() + inflightCounts.counts[key]-- + + n := inflightCounts.counts[key] + if n <= 0 { + delete(inflightCounts.counts, key) // Don't grow the map with idle keys. + } + + inflightGauge.WithLabelValues(key.protocol, key.peer).Set(float64(n)) + inflightCounts.Unlock() + + handlerDuration.WithLabelValues(key.protocol).Observe(time.Since(t0).Seconds()) + } +} + +// messageSizeBuckets covers charon p2p message sizes from small control messages up to the +// 128MiB default read limit. The 32MiB and 128MiB edges match protocol read limits, so +// messages that (would) exceed a limit are countable by exact bucket subtraction. +var messageSizeBuckets = []float64{ + 256, 1 << 10, 4 << 10, 16 << 10, 64 << 10, 256 << 10, 512 << 10, + 1 << 20, 2 << 20, 4 << 20, 8 << 20, 16 << 20, 32 << 20, 64 << 20, 128 << 20, +} + +// observeReceivedMessage records the size of a successfully read libp2p protobuf message. +func observeReceivedMessage(pID protocol.ID, peerID peer.ID, msg proto.Message) { + receivedMsgSizeHist.WithLabelValues(string(pID), PeerName(peerID)).Observe(float64(proto.Size(msg))) +} + +// observeSentMessage records the size of a successfully written libp2p protobuf message. +func observeSentMessage(pID protocol.ID, msg proto.Message) { + sentMsgSizeHist.WithLabelValues(string(pID)).Observe(float64(proto.Size(msg))) +} + +// incMessageReadError increments the read error counter for the protocol and sending peer. +func incMessageReadError(pID protocol.ID, peerID peer.ID) { + msgReadErrorCounter.WithLabelValues(string(pID), PeerName(peerID)).Inc() +} + func observePing(p peer.ID, d time.Duration) { pingLatencies.WithLabelValues(PeerName(p)).Observe(d.Seconds()) pingSuccess.WithLabelValues(PeerName(p)).Set(1) diff --git a/p2p/metrics_internal_test.go b/p2p/metrics_internal_test.go new file mode 100644 index 0000000000..79a9217c26 --- /dev/null +++ b/p2p/metrics_internal_test.go @@ -0,0 +1,208 @@ +// Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package p2p + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/peerstore" + "github.com/libp2p/go-libp2p/core/protocol" + "github.com/prometheus/client_golang/prometheus" + promtestutil "github.com/prometheus/client_golang/prometheus/testutil" + pb "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + pbv1 "github.com/obolnetwork/charon/core/corepb/v1" + "github.com/obolnetwork/charon/testutil" +) + +// histSample returns the sample count and sum of the histogram for the given label values. +func histSample(t *testing.T, hist *prometheus.HistogramVec, labels ...string) (uint64, float64) { + t.Helper() + + m := new(pb.Metric) + + h, err := hist.GetMetricWithLabelValues(labels...) + require.NoError(t, err) + require.NoError(t, h.(prometheus.Histogram).Write(m)) + + return m.GetHistogram().GetSampleCount(), m.GetHistogram().GetSampleSum() +} + +func TestMessageSizeMetrics(t *testing.T) { + var ( + pID = protocol.ID("test-msg-size") + ctx = context.Background() + client = testutil.CreateHost(t, testutil.AvailableAddr(t)) + server = testutil.CreateHost(t, testutil.AvailableAddr(t)) + ) + + client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) + + RegisterHandler("server", server, pID, + func() proto.Message { return new(pbv1.Duty) }, + func(_ context.Context, _ peer.ID, req proto.Message) (proto.Message, bool, error) { + return req, true, nil + }, + ) + + req := &pbv1.Duty{Slot: 123, Type: 4} + msgSize := float64(proto.Size(req)) + require.Positive(t, msgSize) + + sentCount0, sentSum0 := histSample(t, sentMsgSizeHist, string(pID)) + recvClient0, _ := histSample(t, receivedMsgSizeHist, string(pID), PeerName(server.ID())) + recvServer0, _ := histSample(t, receivedMsgSizeHist, string(pID), PeerName(client.ID())) + + resp := new(pbv1.Duty) + require.NoError(t, SendReceive(ctx, client, server.ID(), req, resp, pID)) + + // Client sent the request and server sent the identical echoed response. + sentCount, sentSum := histSample(t, sentMsgSizeHist, string(pID)) + require.Equal(t, sentCount0+2, sentCount) + require.InDelta(t, sentSum0+2*msgSize, sentSum, 0.1) + + // Server received the request from the client. + recvServer, recvServerSum := histSample(t, receivedMsgSizeHist, string(pID), PeerName(client.ID())) + require.Equal(t, recvServer0+1, recvServer) + require.InDelta(t, msgSize, recvServerSum, 0.1) + + // Client received the response from the server. + recvClient, _ := histSample(t, receivedMsgSizeHist, string(pID), PeerName(server.ID())) + require.Equal(t, recvClient0+1, recvClient) +} + +func TestMessageReadErrorMetric(t *testing.T) { + var ( + pID = protocol.ID("test-read-limit") + ctx = context.Background() + client = testutil.CreateHost(t, testutil.AvailableAddr(t)) + server = testutil.CreateHost(t, testutil.AvailableAddr(t)) + ) + + client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) + + RegisterHandler("server", server, pID, + func() proto.Message { return new(pbv1.ParSigExMsg) }, + func(context.Context, peer.ID, proto.Message) (proto.Message, bool, error) { + require.Fail(t, "handler must not be called for oversized message") + return nil, false, nil + }, + WithReadLimit(16), + ) + + errCounter := msgReadErrorCounter.WithLabelValues(string(pID), PeerName(client.ID())) + errCount0 := promtestutil.ToFloat64(errCounter) + + // Message larger than the 16 byte read limit is rejected server side before the handler. + // Send is one-way so the local write succeeds regardless. + msg := &pbv1.ParSigExMsg{Duty: &pbv1.Duty{Slot: 99, Type: 2}, DataSet: &pbv1.ParSignedDataSet{ + Set: map[string]*pbv1.ParSignedData{"0xdeadbeef": {Data: make([]byte, 1024), Signature: make([]byte, 96)}}, + }} + require.NoError(t, Send(ctx, client, pID, server.ID(), msg)) + + require.Eventually(t, func() bool { + return promtestutil.ToFloat64(errCounter) >= errCount0+1 + }, time.Second*5, time.Millisecond*10) +} + +func TestInflightRequestMetrics(t *testing.T) { + var ( + pID = protocol.ID("test-inflight") + ctx = context.Background() + client = testutil.CreateHost(t, testutil.AvailableAddr(t)) + server = testutil.CreateHost(t, testutil.AvailableAddr(t)) + ) + + client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) + + RegisterHandler("server", server, pID, + func() proto.Message { return new(pbv1.Duty) }, + func(_ context.Context, _ peer.ID, req proto.Message) (proto.Message, bool, error) { + return req, true, nil + }, + ) + + clientName := PeerName(client.ID()) + + resp := new(pbv1.Duty) + require.NoError(t, SendReceive(ctx, client, server.ID(), &pbv1.Duty{Slot: 1}, resp, pID)) + + // One arrival observed in the concurrency histogram with count 1. + concCount, concSum := histSample(t, concurrentRequestsHist, string(pID), clientName) + require.Equal(t, uint64(1), concCount) + require.InDelta(t, 1, concSum, 0.1) + + // Handler duration observed once. + durCount, durSum := histSample(t, handlerDuration, string(pID)) + require.Equal(t, uint64(1), durCount) + require.Positive(t, durSum) + + // In-flight gauge back to zero after completion. + gauge, err := inflightGauge.GetMetricWithLabelValues(string(pID), clientName) + require.NoError(t, err) + require.Zero(t, promtestutil.ToFloat64(gauge)) +} + +func TestConcurrentRequestDepths(t *testing.T) { + var ( + pID = protocol.ID("test-concurrency") + ctx = context.Background() + client = testutil.CreateHost(t, testutil.AvailableAddr(t)) + server = testutil.CreateHost(t, testutil.AvailableAddr(t)) + ) + + client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) + + release := make(chan struct{}) + RegisterHandler("server", server, pID, + func() proto.Message { return new(pbv1.Duty) }, + func(_ context.Context, _ peer.ID, req proto.Message) (proto.Message, bool, error) { + <-release + return req, true, nil + }, + ) + + clientName := PeerName(client.ID()) + + // Issue concurrent requests against a blocked handler. + const n = 3 + + var wg sync.WaitGroup + for range n { + wg.Add(1) + + go func() { + defer wg.Done() + + resp := new(pbv1.Duty) + _ = SendReceive(ctx, client, server.ID(), &pbv1.Duty{Slot: 1}, resp, pID) + }() + } + + gauge, err := inflightGauge.GetMetricWithLabelValues(string(pID), clientName) + require.NoError(t, err) + + // All requests block in the handler, so the gauge reaches n. + require.Eventually(t, func() bool { + return promtestutil.ToFloat64(gauge) == n + }, time.Second*5, time.Millisecond*10) + + close(release) + wg.Wait() + + // Gauge returns to zero once all handlers complete. + require.Eventually(t, func() bool { + return promtestutil.ToFloat64(gauge) == 0 + }, time.Second*5, time.Millisecond*10) + + // The concurrency histogram observed depths 1, 2 and 3 (in some order), so the sum is 6. + concCount, concSum := histSample(t, concurrentRequestsHist, string(pID), clientName) + require.Equal(t, uint64(n), concCount) + require.InDelta(t, 6, concSum, 0.1) +} diff --git a/p2p/receive.go b/p2p/receive.go index c6726757f9..62c31000ff 100644 --- a/p2p/receive.go +++ b/p2p/receive.go @@ -54,6 +54,9 @@ func RegisterHandler(logTopic string, p2pNode host.Host, pID protocol.ID, t0 := time.Now() name := PeerName(s.Conn().RemotePeer()) + handlerDone := observeHandlerStart(s.Protocol(), s.Conn().RemotePeer()) + defer handlerDone() + _ = s.SetReadDeadline(time.Now().Add(o.receiveTimeout)) ctx, cancel := context.WithTimeout(context.Background(), o.receiveTimeout) ctx = log.WithTopic(ctx, logTopic) @@ -91,14 +94,30 @@ func RegisterHandler(logTopic string, p2pNode host.Host, pID protocol.ID, err := readFunc(s).ReadMsg(req) if IsRelayError(err) { + // Resets on relayed (limited) connections are benign circuit recycling, + // but a reset on a direct connection is a genuine read failure. + if !s.Conn().Stat().Limited { + incMessageReadError(s.Protocol(), s.Conn().RemotePeer()) + } + return // Ignore relay errors. } else if netErr := net.Error(nil); errors.As(err, &netErr) && netErr.Timeout() { + incMessageReadError(s.Protocol(), s.Conn().RemotePeer()) log.Error(ctx, "Timeout reading p2p message from peer. This may indicate network latency issues or unresponsive peer", err, z.Any("duration", time.Since(t0))) + return } else if err != nil { + incMessageReadError(s.Protocol(), s.Conn().RemotePeer()) log.Error(ctx, "Failed to read p2p request from peer. Check network connectivity and peer health", err, z.Any("duration", time.Since(t0))) + return - } else if err := protonil.Check(req); err != nil { + } + + // Observe before application-level validation so malformed-but-readable messages + // are still visible in the size histogram. + observeReceivedMessage(s.Protocol(), s.Conn().RemotePeer(), req) + + if err := protonil.Check(req); err != nil { log.Warn(ctx, "LibP2P received invalid proto", err) return } @@ -119,5 +138,7 @@ func RegisterHandler(logTopic string, p2pNode host.Host, pID protocol.ID, log.Error(ctx, "Failed to write p2p response to peer. Connection may have been closed", err) return } + + observeSentMessage(s.Protocol(), resp) }) } diff --git a/p2p/sender.go b/p2p/sender.go index 0280ca2e96..aedf11c952 100644 --- a/p2p/sender.go +++ b/p2p/sender.go @@ -419,10 +419,20 @@ func sendReceive(ctx context.Context, p2pNode host.Host, peerID peer.ID, } } + observeSentMessage(s.Protocol(), req) + if err = reader.ReadMsg(resp); err != nil { + // Resets on relayed (limited) connections are benign circuit recycling, + // but any other failure (including a reset on a direct connection) counts. + if !IsRelayError(err) || !s.Conn().Stat().Limited { + incMessageReadError(s.Protocol(), peerID) + } + return errors.Wrap(err, "read response", z.Any("protocol", s.Protocol()), z.Str("peer", PeerName(peerID))) } + observeReceivedMessage(s.Protocol(), peerID, resp) + o.rttCallback(time.Since(t0)) return nil @@ -486,6 +496,8 @@ func send(ctx context.Context, p2pNode host.Host, protoID protocol.ID, peerID pe return errors.Wrap(err, "write message", z.Any("protocol", s.Protocol()), z.Str("peer", PeerName(peerID))) } + observeSentMessage(s.Protocol(), msg) + return nil } From 18428321d07760e3c0439df8981d7035c25ebfbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:56:13 +0000 Subject: [PATCH 10/29] build(deps): Bump the go-dependencies group across 1 directory with 5 updates (#4698) Bumps the go-dependencies group with 3 updates in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/net](https://github.com/golang/net) and [golang.org/x/tools](https://github.com/golang/tools). Updates `golang.org/x/crypto` from 0.56.0 to 0.57.0 - [Commits](https://github.com/golang/crypto/compare/v0.56.0...v0.57.0) Updates `golang.org/x/net` from 0.58.0 to 0.59.0 - [Commits](https://github.com/golang/net/compare/v0.58.0...v0.59.0) Updates `golang.org/x/term` from 0.45.0 to 0.46.0 - [Commits](https://github.com/golang/term/compare/v0.45.0...v0.46.0) Updates `golang.org/x/text` from 0.41.0 to 0.42.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.41.0...v0.42.0) Updates `golang.org/x/tools` from 0.49.0 to 0.50.0 - [Release notes](https://github.com/golang/tools/releases) - [Commits](https://github.com/golang/tools/compare/v0.49.0...v0.50.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.57.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/net dependency-version: 0.59.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/term dependency-version: 0.46.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/text dependency-version: 0.42.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/tools dependency-version: 0.50.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 6b1e60718c0905441d99c133fe6550917dfe3636) # Conflicts: # go.mod # go.sum --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index dfeab771f3..e0934d1365 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( golang.org/x/term v0.46.0 golang.org/x/text v0.42.0 golang.org/x/time v0.16.0 - golang.org/x/tools v0.49.0 + golang.org/x/tools v0.50.0 google.golang.org/protobuf v1.36.12 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -275,7 +275,7 @@ require ( golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect golang.org/x/mod v0.41.0 // indirect golang.org/x/sys v0.48.0 // indirect - golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 // indirect + golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 // indirect golang.org/x/vuln v1.1.4 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect diff --git a/go.sum b/go.sum index 91b005029f..64939ad8d7 100644 --- a/go.sum +++ b/go.sum @@ -696,8 +696,8 @@ golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 h1:ZUSxONxc981v7AW7QUg+I9WwZzSTTJ019ENBYr5pV/Q= -golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5/go.mod h1:LVehoXe41cL5SCVQilsV7Gg6BNG+Js6P9PhSbYTIUkQ= +golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 h1:F5BWKvW126NXR74uxkxuc1jQHhm/rwm/J3rSiFyuRs4= +golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518/go.mod h1:i+ivNqjDnTF3WTElsdk5g9V5DTSBYgdNo7xTU9SDwYA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= @@ -712,8 +712,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= +golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= From 2378131c65a680aa48b9191a9c279d7134307b02 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Mon, 21 Sep 2026 13:59:07 +0200 Subject: [PATCH 11/29] cmd/exit_sign: warn on validators with pending deposits and no index (#4702) (cherry picked from commit 1928fd020c075466346e485517662472fb65ce4c) --- app/eth2wrap/eth2wrap_gen.go | 33 ++++ app/eth2wrap/genwrap/genwrap.go | 1 + app/eth2wrap/mocks/client.go | 32 ++++ cmd/exit_sign.go | 71 +++++++- cmd/exit_sign_internal_test.go | 260 ++++++++++++++++++++++++++++++ testutil/beaconmock/beaconmock.go | 11 ++ testutil/beaconmock/options.go | 3 + 7 files changed, 410 insertions(+), 1 deletion(-) diff --git a/app/eth2wrap/eth2wrap_gen.go b/app/eth2wrap/eth2wrap_gen.go index ef0415a4d3..1e8bdddf76 100644 --- a/app/eth2wrap/eth2wrap_gen.go +++ b/app/eth2wrap/eth2wrap_gen.go @@ -14,6 +14,7 @@ import ( apiv1 "github.com/attestantio/go-eth2-client/api/v1" "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/electra" "github.com/attestantio/go-eth2-client/spec/phase0" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" ) @@ -58,6 +59,7 @@ type Client interface { eth2client.NodeSyncingProvider eth2client.NodeVersionProvider eth2client.NodeVersionV2Provider + eth2client.PendingDepositProvider eth2client.ProposalPreparationsSubmitter eth2client.ProposalProvider eth2client.ProposalSubmitter @@ -872,6 +874,27 @@ func (m multi) SubmitVoluntaryExit(ctx context.Context, voluntaryExit *phase0.Si return err } +// PendingDeposits provides the pending deposits for a given state. +func (m multi) PendingDeposits(ctx context.Context, opts *api.PendingDepositsOpts) (*api.Response[[]*electra.PendingDeposit], error) { + const label = "pending_deposits" + defer latency(ctx, label, false)() + defer incRequest(label) + + res0, err := provide(ctx, m.clients, m.fallbacks, + func(ctx context.Context, args provideArgs) (*api.Response[[]*electra.PendingDeposit], error) { + return args.client.PendingDeposits(ctx, opts) + }, + nil, m.selector, + ) + + if err != nil { + incError(label) + err = wrapError(ctx, err, label) + } + + return res0, err +} + // Domain provides a domain for a given domain type at a given epoch. // Note this endpoint is cached in go-eth2-client. func (m multi) Domain(ctx context.Context, domainType phase0.DomainType, epoch phase0.Epoch) (phase0.Domain, error) { @@ -1296,6 +1319,16 @@ func (l *lazy) SubmitVoluntaryExit(ctx context.Context, voluntaryExit *phase0.Si return cl.SubmitVoluntaryExit(ctx, voluntaryExit) } +// PendingDeposits provides the pending deposits for a given state. +func (l *lazy) PendingDeposits(ctx context.Context, opts *api.PendingDepositsOpts) (res0 *api.Response[[]*electra.PendingDeposit], err error) { + cl, err := l.getOrCreateClient(ctx) + if err != nil { + return res0, err + } + + return cl.PendingDeposits(ctx, opts) +} + // Domain provides a domain for a given domain type at a given epoch. func (l *lazy) Domain(ctx context.Context, domainType phase0.DomainType, epoch phase0.Epoch) (res0 phase0.Domain, err error) { cl, err := l.getOrCreateClient(ctx) diff --git a/app/eth2wrap/genwrap/genwrap.go b/app/eth2wrap/genwrap/genwrap.go index 5f8e9c7e94..b3433fe649 100644 --- a/app/eth2wrap/genwrap/genwrap.go +++ b/app/eth2wrap/genwrap/genwrap.go @@ -130,6 +130,7 @@ type Client interface { "NodeSyncingProvider": {Latency: true, Log: false}, "NodeVersionProvider": {Latency: false, Log: false}, "NodeVersionV2Provider": {Latency: false, Log: false}, + "PendingDepositProvider": {Latency: true, Log: false}, "ProposerDutiesProvider": {Latency: true, Log: false}, "ProposalPreparationsSubmitter": {Latency: true, Log: true}, "SlotDurationProvider": {Latency: false, Log: false}, diff --git a/app/eth2wrap/mocks/client.go b/app/eth2wrap/mocks/client.go index 7ade54a226..3a5f52b12b 100644 --- a/app/eth2wrap/mocks/client.go +++ b/app/eth2wrap/mocks/client.go @@ -20,6 +20,8 @@ import ( spec "github.com/attestantio/go-eth2-client/spec" + electra "github.com/attestantio/go-eth2-client/spec/electra" + time "time" v1 "github.com/attestantio/go-eth2-client/api/v1" @@ -770,6 +772,36 @@ func (_m *Client) NodeVersionV2(ctx context.Context, opts *api.NodeVersionV2Opts return r0, r1 } +// PendingDeposits provides a mock function with given fields: ctx, opts +func (_m *Client) PendingDeposits(ctx context.Context, opts *api.PendingDepositsOpts) (*api.Response[[]*electra.PendingDeposit], error) { + ret := _m.Called(ctx, opts) + + if len(ret) == 0 { + panic("no return value specified for PendingDeposits") + } + + var r0 *api.Response[[]*electra.PendingDeposit] + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *api.PendingDepositsOpts) (*api.Response[[]*electra.PendingDeposit], error)); ok { + return rf(ctx, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *api.PendingDepositsOpts) *api.Response[[]*electra.PendingDeposit]); ok { + r0 = rf(ctx, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Response[[]*electra.PendingDeposit]) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *api.PendingDepositsOpts) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Proposal provides a mock function with given fields: ctx, opts func (_m *Client) Proposal(ctx context.Context, opts *api.ProposalOpts) (*api.Response[*api.VersionedProposal], error) { ret := _m.Called(ctx, opts) diff --git a/cmd/exit_sign.go b/cmd/exit_sign.go index 582696be02..93c53b120d 100644 --- a/cmd/exit_sign.go +++ b/cmd/exit_sign.go @@ -3,11 +3,14 @@ package cmd import ( + "bytes" "context" "fmt" + "slices" eth2api "github.com/attestantio/go-eth2-client/api" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/electra" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/spf13/cobra" @@ -216,7 +219,7 @@ func signAllValidatorsExits(ctx context.Context, config exitConfig, eth2Cl eth2w valsEth2 = append(valsEth2, eth2PK) } - rawValData, err := queryBeaconForValidator(ctx, eth2Cl, valsEth2, nil, []eth2v1.ValidatorState{eth2v1.ValidatorStatePendingQueued, eth2v1.ValidatorStateActiveOngoing}) + rawValData, err := queryBeaconForValidator(ctx, eth2Cl, valsEth2, nil, []eth2v1.ValidatorState{eth2v1.ValidatorStatePendingInitialized, eth2v1.ValidatorStatePendingQueued, eth2v1.ValidatorStateActiveOngoing}) if err != nil { return nil, errors.Wrap(err, "fetch all validators indices from beacon") } @@ -233,6 +236,8 @@ func signAllValidatorsExits(ctx context.Context, config exitConfig, eth2Cl eth2w activeShares[core.PubKeyFrom48Bytes(val.Validator.PublicKey)] = share } + warnOnPendingDeposits(ctx, eth2Cl, valsEth2, rawValData.Data) + log.Info(ctx, "Signing partial exit message for all active validators", z.Int("active_validators", len(activeShares)), z.Int("inactive_validators", len(shares)-len(activeShares))) var exitBlobs []obolapi.ExitBlob @@ -260,6 +265,70 @@ func signAllValidatorsExits(ctx context.Context, config exitConfig, eth2Cl eth2w return exitBlobs, nil } +// pendingDepositsWithoutIndex returns the cluster validator public keys that have a registered +// deposit in the beacon pending-deposits queue but no validator index yet, so no exit can be +// signed for them. Top-up deposits for already-indexed validators and deposits for validators +// outside the cluster are ignored. The result is deduplicated and sorted deterministically. +func pendingDepositsWithoutIndex(clusterPubkeys, indexedPubkeys map[eth2p0.BLSPubKey]bool, pendingDeposits []*electra.PendingDeposit) []eth2p0.BLSPubKey { + seen := make(map[eth2p0.BLSPubKey]bool) + + var out []eth2p0.BLSPubKey + + for _, pd := range pendingDeposits { + pk := pd.Pubkey + if !clusterPubkeys[pk] || indexedPubkeys[pk] || seen[pk] { + continue + } + + seen[pk] = true + out = append(out, pk) + } + + slices.SortFunc(out, func(a, b eth2p0.BLSPubKey) int { + return bytes.Compare(a[:], b[:]) + }) + + return out +} + +// warnOnPendingDeposits queries the beacon pending-deposits queue and warns about the cluster +// validators that have a registered deposit but no validator index yet, so no exit could be +// signed for them. It is best-effort: any error querying the queue is logged and swallowed so it +// never blocks signing exits for the exitable validators. The affected public keys are returned. +func warnOnPendingDeposits(ctx context.Context, eth2Cl eth2wrap.Client, clusterPubkeys []eth2p0.BLSPubKey, indexed map[eth2p0.ValidatorIndex]*eth2v1.Validator) []eth2p0.BLSPubKey { + resp, err := eth2Cl.PendingDeposits(ctx, ð2api.PendingDepositsOpts{State: "head"}) + if err != nil { + log.Warn(ctx, "Could not check beacon pending-deposits queue for validators without an index", err) + return nil + } + + clusterSet := make(map[eth2p0.BLSPubKey]bool, len(clusterPubkeys)) + for _, pk := range clusterPubkeys { + clusterSet[pk] = true + } + + indexedSet := make(map[eth2p0.BLSPubKey]bool, len(indexed)) + for _, val := range indexed { + indexedSet[val.Validator.PublicKey] = true + } + + warnPubkeys := pendingDepositsWithoutIndex(clusterSet, indexedSet, resp.Data) + if len(warnPubkeys) == 0 { + return nil + } + + pubkeyStrs := make([]string, 0, len(warnPubkeys)) + for _, pk := range warnPubkeys { + pubkeyStrs = append(pubkeyStrs, pk.String()) + } + + log.Warn(ctx, "Some validators have a registered deposit but no validator index yet, so no exit was signed for them; "+ + "they will activate later and cannot be exited now. Re-run 'charon exit sign --all' once they have a validator index.", nil, + z.Int("count", len(warnPubkeys)), z.Any("validators", pubkeyStrs)) + + return warnPubkeys +} + func fetchValidatorBLSPubKey(ctx context.Context, config exitConfig, eth2Cl eth2wrap.Client) (eth2p0.BLSPubKey, error) { if config.ValidatorPubkey != "" { valEth2, err := core.PubKey(config.ValidatorPubkey).ToETH2() diff --git a/cmd/exit_sign_internal_test.go b/cmd/exit_sign_internal_test.go index b84eefdd6c..b4da45a6df 100644 --- a/cmd/exit_sign_internal_test.go +++ b/cmd/exit_sign_internal_test.go @@ -3,24 +3,30 @@ package cmd import ( + "context" "encoding/json" "fmt" "math/rand" "net/http/httptest" "os" "path/filepath" + "slices" "testing" "time" + eth2api "github.com/attestantio/go-eth2-client/api" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/electra" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/stretchr/testify/require" + "github.com/obolnetwork/charon/app/errors" "github.com/obolnetwork/charon/app/k1util" "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/z" "github.com/obolnetwork/charon/cluster" + "github.com/obolnetwork/charon/core" "github.com/obolnetwork/charon/eth2util/keystore" "github.com/obolnetwork/charon/tbls" "github.com/obolnetwork/charon/testutil" @@ -490,3 +496,257 @@ func TestExitSignCLI(t *testing.T) { }) } } + +func Test_pendingDepositsWithoutIndex(t *testing.T) { + pk := func(b byte) eth2p0.BLSPubKey { + var out eth2p0.BLSPubKey + + out[0] = b + + return out + } + + clusterA, clusterB, clusterC := pk(1), pk(2), pk(3) + nonCluster := pk(9) + + deposit := func(p eth2p0.BLSPubKey) *electra.PendingDeposit { + return &electra.PendingDeposit{Pubkey: p} + } + + tests := []struct { + name string + cluster []eth2p0.BLSPubKey + indexed []eth2p0.BLSPubKey + pendingDeposits []*electra.PendingDeposit + want []eth2p0.BLSPubKey + }{ + { + name: "deposit registered but no index yet", + cluster: []eth2p0.BLSPubKey{clusterA, clusterB}, + indexed: nil, + pendingDeposits: []*electra.PendingDeposit{deposit(clusterA)}, + want: []eth2p0.BLSPubKey{clusterA}, + }, + { + name: "top-up deposit for indexed validator is ignored", + cluster: []eth2p0.BLSPubKey{clusterA}, + indexed: []eth2p0.BLSPubKey{clusterA}, + pendingDeposits: []*electra.PendingDeposit{deposit(clusterA)}, + want: nil, + }, + { + name: "deposit for non-cluster validator is ignored", + cluster: []eth2p0.BLSPubKey{clusterA}, + indexed: nil, + pendingDeposits: []*electra.PendingDeposit{deposit(nonCluster)}, + want: nil, + }, + { + name: "empty queue yields nothing", + cluster: []eth2p0.BLSPubKey{clusterA}, + indexed: nil, + pendingDeposits: nil, + want: nil, + }, + { + name: "multiple deposits for same validator deduplicated", + cluster: []eth2p0.BLSPubKey{clusterA}, + indexed: nil, + pendingDeposits: []*electra.PendingDeposit{deposit(clusterA), deposit(clusterA)}, + want: []eth2p0.BLSPubKey{clusterA}, + }, + { + name: "results sorted deterministically", + cluster: []eth2p0.BLSPubKey{clusterA, clusterB, clusterC}, + indexed: nil, + pendingDeposits: []*electra.PendingDeposit{deposit(clusterC), deposit(clusterA), deposit(clusterB)}, + want: []eth2p0.BLSPubKey{clusterA, clusterB, clusterC}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + clusterSet := make(map[eth2p0.BLSPubKey]bool) + for _, p := range test.cluster { + clusterSet[p] = true + } + + indexedSet := make(map[eth2p0.BLSPubKey]bool) + for _, p := range test.indexed { + indexedSet[p] = true + } + + got := pendingDepositsWithoutIndex(clusterSet, indexedSet, test.pendingDeposits) + require.Equal(t, test.want, got) + }) + } +} + +// mixedValidatorSet builds a cluster and a beacon validator set where the validators +// are split across states: active_ongoing, pending_initialized (has an index) and +// entirely absent from the beacon (no index). The absent validators' public keys are +// returned so callers can place them in the pending-deposits queue. +func mixedValidatorSet(t *testing.T) (keystore.ValidatorShares, beaconmock.ValidatorSet, []eth2p0.BLSPubKey) { + t.Helper() + + const ( + valAmt = 3 + operatorAmt = 4 + ) + + random := rand.New(rand.NewSource(int64(0))) + + lock, _, keyShares := cluster.NewForT(t, valAmt, operatorAmt, operatorAmt, 0, random) + + shares := make(keystore.ValidatorShares) + + for i, v := range lock.Validators { + pk, err := core.PubKeyFromBytes(v.PubKey) + require.NoError(t, err) + + shares[pk] = keystore.IndexedKeyShare{Share: keyShares[i][0], Index: i} + } + + validatorSet := beaconmock.ValidatorSet{} + // index 0: active_ongoing, index 1: pending_initialized, index 2: absent (no index). + states := []eth2v1.ValidatorState{eth2v1.ValidatorStateActiveOngoing, eth2v1.ValidatorStatePendingInitialized} + + var noIndex []eth2p0.BLSPubKey + + for idx, v := range lock.Validators { + if idx >= len(states) { + noIndex = append(noIndex, eth2p0.BLSPubKey(v.PubKey)) + continue + } + + validatorSet[eth2p0.ValidatorIndex(idx)] = ð2v1.Validator{ + Index: eth2p0.ValidatorIndex(idx), + Balance: 42, + Status: states[idx], + Validator: ð2p0.Validator{ + PublicKey: eth2p0.BLSPubKey(v.PubKey), + WithdrawalCredentials: testutil.RandomBytes32(), + }, + } + } + + return shares, validatorSet, noIndex +} + +// stateFilteringValidators overrides a beacon mock's ValidatorsFunc to emulate a real beacon +// node: it returns only validators whose status is among the requested states (and, when set, +// whose public key is requested). This lets tests exercise the state filter that the plain +// WithValidatorSet mock ignores. +func stateFilteringValidators(set beaconmock.ValidatorSet) func(context.Context, *eth2api.ValidatorsOpts) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) { + return func(_ context.Context, opts *eth2api.ValidatorsOpts) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) { + resp := make(map[eth2p0.ValidatorIndex]*eth2v1.Validator) + + for idx, val := range set { + if len(opts.ValidatorStates) > 0 && !slices.Contains(opts.ValidatorStates, val.Status) { + continue + } + + if len(opts.PubKeys) > 0 && !slices.Contains(opts.PubKeys, val.Validator.PublicKey) { + continue + } + + resp[idx] = val + } + + return resp, nil + } +} + +func Test_signAllValidatorsExits_signsPendingInitialized(t *testing.T) { + ctx := t.Context() + + shares, validatorSet, noIndex := mixedValidatorSet(t) + + beaconMock, err := beaconmock.New(ctx, beaconmock.WithValidatorSet(validatorSet)) + require.NoError(t, err) + + t.Cleanup(func() { require.NoError(t, beaconMock.Close()) }) + + beaconMock.ValidatorsFunc = stateFilteringValidators(validatorSet) + + // The no-index validator sits in the pending-deposits queue: it triggers the warning but + // must not affect signing of the exitable validators. + beaconMock.PendingDepositsFunc = func(context.Context, *eth2api.PendingDepositsOpts) ([]*electra.PendingDeposit, error) { + return []*electra.PendingDeposit{{Pubkey: noIndex[0]}}, nil + } + + config := exitConfig{ExitEpoch: 194048} + + exitBlobs, err := signAllValidatorsExits(ctx, config, beaconMock, shares) + require.NoError(t, err) + + // active_ongoing and pending_initialized are signed; the no-index validator is not. + require.Len(t, exitBlobs, 2) + + signed := make(map[string]bool) + for _, b := range exitBlobs { + signed[b.PublicKey] = true + } + + require.False(t, signed[noIndex[0].String()], "no-index validator must not be signed") +} + +func Test_pendingDepositWarnings(t *testing.T) { + ctx := t.Context() + + shares, validatorSet, noIndex := mixedValidatorSet(t) + + beaconMock, err := beaconmock.New(ctx, beaconmock.WithValidatorSet(validatorSet)) + require.NoError(t, err) + + t.Cleanup(func() { require.NoError(t, beaconMock.Close()) }) + + activePubkey := validatorSet[0].Validator.PublicKey + + beaconMock.PendingDepositsFunc = func(context.Context, *eth2api.PendingDepositsOpts) ([]*electra.PendingDeposit, error) { + return []*electra.PendingDeposit{ + {Pubkey: noIndex[0]}, // cluster validator with a registered deposit but no index -> warn. + {Pubkey: activePubkey}, // top-up for an already-indexed validator -> ignored. + }, nil + } + + var clusterPubkeys []eth2p0.BLSPubKey + + for pk := range shares { + eth2PK, err := pk.ToETH2() + require.NoError(t, err) + + clusterPubkeys = append(clusterPubkeys, eth2PK) + } + + // index 0 is active_ongoing: its top-up deposit must be excluded from the warning. + indexed := map[eth2p0.ValidatorIndex]*eth2v1.Validator{0: validatorSet[0]} + + got := warnOnPendingDeposits(ctx, beaconMock, clusterPubkeys, indexed) + require.Equal(t, []eth2p0.BLSPubKey{noIndex[0]}, got) +} + +func Test_signAllValidatorsExits_pendingDepositsCheckIsBestEffort(t *testing.T) { + ctx := t.Context() + + shares, validatorSet, _ := mixedValidatorSet(t) + + beaconMock, err := beaconmock.New(ctx, beaconmock.WithValidatorSet(validatorSet)) + require.NoError(t, err) + + t.Cleanup(func() { require.NoError(t, beaconMock.Close()) }) + + beaconMock.ValidatorsFunc = stateFilteringValidators(validatorSet) + + // A beacon node that does not support the pending-deposits queue (e.g. pre-Electra) must not + // block signing exits for the exitable validators. + beaconMock.PendingDepositsFunc = func(context.Context, *eth2api.PendingDepositsOpts) ([]*electra.PendingDeposit, error) { + return nil, errors.New("pending deposits not supported") + } + + config := exitConfig{ExitEpoch: 194048} + + exitBlobs, err := signAllValidatorsExits(ctx, config, beaconMock, shares) + require.NoError(t, err) + require.Len(t, exitBlobs, 2) +} diff --git a/testutil/beaconmock/beaconmock.go b/testutil/beaconmock/beaconmock.go index 79b8b950ce..fe34f25b14 100644 --- a/testutil/beaconmock/beaconmock.go +++ b/testutil/beaconmock/beaconmock.go @@ -33,6 +33,7 @@ import ( eth2v1 "github.com/attestantio/go-eth2-client/api/v1" eth2spec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/electra" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/jonboulle/clockwork" @@ -230,6 +231,7 @@ type Mock struct { NodeVersionFunc func(context.Context, *eth2api.NodeVersionOpts) (*eth2api.Response[string], error) NodeVersionV2Func func(context.Context, *eth2api.NodeVersionV2Opts) (*eth2api.Response[*eth2v1.NodeVersionV2], error) ProxyFunc func(context.Context, *http.Request) (*http.Response, error) + PendingDepositsFunc func(context.Context, *eth2api.PendingDepositsOpts) ([]*electra.PendingDeposit, error) } func (m Mock) UpdateCacheIndices(ctx context.Context, idxs []eth2p0.ValidatorIndex) { @@ -355,6 +357,15 @@ func (m Mock) Validators(ctx context.Context, opts *eth2api.ValidatorsOpts) (*et return wrapResponse(vals), nil } +func (m Mock) PendingDeposits(ctx context.Context, opts *eth2api.PendingDepositsOpts) (*eth2api.Response[[]*electra.PendingDeposit], error) { + deposits, err := m.PendingDepositsFunc(ctx, opts) + if err != nil { + return nil, err + } + + return wrapResponse(deposits), nil +} + func (Mock) SetValidatorCache(func(context.Context) (eth2wrap.ActiveValidators, eth2wrap.CompleteValidators, error)) { // Ignore this, only rely on WithValidator functional option. } diff --git a/testutil/beaconmock/options.go b/testutil/beaconmock/options.go index b6af40334c..a315de79b0 100644 --- a/testutil/beaconmock/options.go +++ b/testutil/beaconmock/options.go @@ -798,6 +798,9 @@ func defaultMock(httpMock HTTPMock, httpServer *http.Server, clock clockwork.Clo return res, nil }, + PendingDepositsFunc: func(context.Context, *eth2api.PendingDepositsOpts) ([]*electra.PendingDeposit, error) { + return []*electra.PendingDeposit{}, nil + }, } } From e44a68c6fc952d080c5338ea5a0cf8f59b06b14c Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:00:09 +0200 Subject: [PATCH 12/29] *: keep //nolint:revive from nolintlint false autofix (#4703) (cherry picked from commit 3f40e6cf86115b5a37668c3df47da4223917642a) --- .golangci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 4b61e995d7..6b0c6b5f84 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -170,6 +170,12 @@ linters: - legacy - std-error-handling rules: + # golangci-lint's nolintlint falsely reports //nolint:revive directives as + # unused and (with fix: true) deletes them, even though revive would fire + # without them. Keep such directives; do not report or auto-remove them. + - linters: + - nolintlint + text: 'is unused for linter "revive"' - linters: - bodyclose - gosec From 5770697bd65c805ddb8470576c66805cc7803f4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:32:01 +0200 Subject: [PATCH 13/29] build(deps): Bump github.com/ethereum/go-ethereum (#4730) Bumps the go-dependencies group with 1 update: [github.com/ethereum/go-ethereum](https://github.com/ethereum/go-ethereum). Updates `github.com/ethereum/go-ethereum` from 1.17.5 to 1.17.6 - [Release notes](https://github.com/ethereum/go-ethereum/releases) - [Commits](https://github.com/ethereum/go-ethereum/compare/v1.17.5...v1.17.6) --- updated-dependencies: - dependency-name: github.com/ethereum/go-ethereum dependency-version: 1.17.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit bcd3f416d0eff770380cc20450a3b147627811ee) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e0934d1365..5ddf3fbf2d 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 github.com/drand/kyber v1.3.2 github.com/drand/kyber-bls12381 v0.3.4 - github.com/ethereum/go-ethereum v1.17.5 + github.com/ethereum/go-ethereum v1.17.6 github.com/ferranbt/fastssz v1.0.0 github.com/fsnotify/fsnotify v1.10.1 github.com/golang/snappy v1.0.1-0.20260716114414-9ae09f520e93 diff --git a/go.sum b/go.sum index 64939ad8d7..fdbcc90c12 100644 --- a/go.sum +++ b/go.sum @@ -171,8 +171,8 @@ github.com/ethereum/c-kzg-4844/v2 v2.1.8 h1:oQ48q/TMe2SKU8qBE3N7e4/HlG3EpJftom6E github.com/ethereum/c-kzg-4844/v2 v2.1.8/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= -github.com/ethereum/go-ethereum v1.17.5 h1:o9BIXs2Q/3cPHVxw49n+Zjn2i6rB9TOXatev46duOC4= -github.com/ethereum/go-ethereum v1.17.5/go.mod h1:vz2YvG7RewA4sFHTgzLyW+WmFG1N4jfk/hgXQVhhn9c= +github.com/ethereum/go-ethereum v1.17.6 h1:27mdzjoN/bjz+rgjjZPGnD6E44W/Nd+vG+FKQFd/heg= +github.com/ethereum/go-ethereum v1.17.6/go.mod h1:nl9wZjMuIjAottU6bq82UihXPbyY0jHHwkYXhnYhmU4= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8= From addfffa8f2898c4d2b481e2933c160a9e44073f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:07:12 +0200 Subject: [PATCH 14/29] build(deps): Bump the go-dependencies group with 7 updates (#4737) Bumps the go-dependencies group with 7 updates: | Package | From | To | | --- | --- | --- | | [github.com/ethereum/go-ethereum](https://github.com/ethereum/go-ethereum) | `1.17.6` | `1.17.7` | | [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) | `1.46.0` | `1.47.0` | | [go.opentelemetry.io/otel/exporters/otlp/otlptrace](https://github.com/open-telemetry/opentelemetry-go) | `1.46.0` | `1.47.0` | | [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://github.com/open-telemetry/opentelemetry-go) | `1.46.0` | `1.47.0` | | [go.opentelemetry.io/otel/exporters/stdout/stdouttrace](https://github.com/open-telemetry/opentelemetry-go) | `1.46.0` | `1.47.0` | | [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) | `1.46.0` | `1.47.0` | | [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) | `1.46.0` | `1.47.0` | Updates `github.com/ethereum/go-ethereum` from 1.17.6 to 1.17.7 - [Release notes](https://github.com/ethereum/go-ethereum/releases) - [Commits](https://github.com/ethereum/go-ethereum/compare/v1.17.6...v1.17.7) Updates `go.opentelemetry.io/otel` from 1.46.0 to 1.47.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.46.0...v1.47.0) Updates `go.opentelemetry.io/otel/exporters/otlp/otlptrace` from 1.46.0 to 1.47.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.46.0...v1.47.0) Updates `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc` from 1.46.0 to 1.47.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.46.0...v1.47.0) Updates `go.opentelemetry.io/otel/exporters/stdout/stdouttrace` from 1.46.0 to 1.47.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.46.0...v1.47.0) Updates `go.opentelemetry.io/otel/sdk` from 1.46.0 to 1.47.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.46.0...v1.47.0) Updates `go.opentelemetry.io/otel/trace` from 1.46.0 to 1.47.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.46.0...v1.47.0) --- updated-dependencies: - dependency-name: github.com/ethereum/go-ethereum dependency-version: 1.17.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: go.opentelemetry.io/otel dependency-version: 1.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace dependency-version: 1.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc dependency-version: 1.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: go.opentelemetry.io/otel/exporters/stdout/stdouttrace dependency-version: 1.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: go.opentelemetry.io/otel/sdk dependency-version: 1.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: go.opentelemetry.io/otel/trace dependency-version: 1.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit ce7f7b20b8d49b37fe3aba82dcf1fdffe9fc3b97) --- go.mod | 23 ++++++++++++----------- go.sum | 54 ++++++++++++++++++++++++++++++------------------------ 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/go.mod b/go.mod index 5ddf3fbf2d..15155f5aeb 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 github.com/drand/kyber v1.3.2 github.com/drand/kyber-bls12381 v0.3.4 - github.com/ethereum/go-ethereum v1.17.6 + github.com/ethereum/go-ethereum v1.17.7 github.com/ferranbt/fastssz v1.0.0 github.com/fsnotify/fsnotify v1.10.1 github.com/golang/snappy v1.0.1-0.20260716114414-9ae09f520e93 @@ -38,12 +38,12 @@ require ( github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.12.1 github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1 - go.opentelemetry.io/otel v1.46.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.46.0 - go.opentelemetry.io/otel/sdk v1.46.0 - go.opentelemetry.io/otel/trace v1.46.0 + go.opentelemetry.io/otel v1.47.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0 + go.opentelemetry.io/otel/sdk v1.47.0 + go.opentelemetry.io/otel/trace v1.47.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.28.0 @@ -265,7 +265,8 @@ require ( go.lsp.dev/uri v0.3.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect - go.opentelemetry.io/otel/metric v1.46.0 // indirect + go.opentelemetry.io/otel/log v1.47.0 // indirect + go.opentelemetry.io/otel/metric v1.47.0 // indirect go.opentelemetry.io/proto/otlp v1.11.0 // indirect go.uber.org/dig v1.19.0 // indirect go.uber.org/fx v1.24.0 // indirect @@ -277,9 +278,9 @@ require ( golang.org/x/sys v0.48.0 // indirect golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 // indirect golang.org/x/vuln v1.1.4 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect - google.golang.org/grpc v1.83.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/grpc v1.83.2 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index fdbcc90c12..0be4cef2cb 100644 --- a/go.sum +++ b/go.sum @@ -171,8 +171,8 @@ github.com/ethereum/c-kzg-4844/v2 v2.1.8 h1:oQ48q/TMe2SKU8qBE3N7e4/HlG3EpJftom6E github.com/ethereum/c-kzg-4844/v2 v2.1.8/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= -github.com/ethereum/go-ethereum v1.17.6 h1:27mdzjoN/bjz+rgjjZPGnD6E44W/Nd+vG+FKQFd/heg= -github.com/ethereum/go-ethereum v1.17.6/go.mod h1:nl9wZjMuIjAottU6bq82UihXPbyY0jHHwkYXhnYhmU4= +github.com/ethereum/go-ethereum v1.17.7 h1:jhoGxw/5aYPYUwEIfzfog0RcsiJuLA6SSqsHdhkx1tA= +github.com/ethereum/go-ethereum v1.17.7/go.mod h1:nl9wZjMuIjAottU6bq82UihXPbyY0jHHwkYXhnYhmU4= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8= @@ -412,6 +412,10 @@ github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/n github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 h1:shk/vn9oCoOTmwcouEdwIeOtOGA/ELRUw/GwvxwfT+0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= @@ -607,23 +611,25 @@ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbE go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04= go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI= -go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= -go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.46.0 h1:KdRxPiAoMptR3vfWzvjjvutTsSiwbC2uG0496rzZNfo= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.46.0/go.mod h1:K/qSA+3G7Eovxi4K09wzrAgkWRnosS0DAOZeEpve7sM= -go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= -go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= -go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= -go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM= -go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= -go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= +go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 h1:UFpxOpYPmMNUtOWhdb+nC1WELIgVf8z9higURrbzYU4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0/go.mod h1:YBGjxe3lt0jtXoEXxGrGhv/jM7oUBUDVK7zhAvNyEjc= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0 h1:N3YQCxjxQ/bMjyc3heladfRm9t9RTksGQH8z4w6yU/0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0/go.mod h1:Mp8HOFqcaUyypCuGv9IhDdTHnJ56lSudSHMd+pVSCEA= +go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= +go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw= +go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY= +go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU= +go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I= +go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0= +go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw= +go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs= -go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= -go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= +go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= +go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= @@ -728,12 +734,12 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI= -google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= -google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= -google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE= +google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From bbbf10a0a694fc69cd4804ad9519991ab1430715 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:42:01 +0200 Subject: [PATCH 15/29] core/scheduler: fix flaky TestSchedulerWait clock rewind (#4726) (cherry picked from commit a81824113b3c6e93c47bc4dfc99373635f4fe3ef) --- core/scheduler/scheduler_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/scheduler/scheduler_test.go b/core/scheduler/scheduler_test.go index 6bb4d92d91..c13fa3b12b 100644 --- a/core/scheduler/scheduler_test.go +++ b/core/scheduler/scheduler_test.go @@ -727,6 +727,13 @@ func (c *testClock) After(d time.Duration) <-chan time.Time { } func (c *testClock) Sleep(d time.Duration) { + // A non-positive duration fires immediately on a real clock and must never move time + // backwards. The slot ticker legitimately calls After with a non-positive duration for the + // current slot, which would otherwise rewind the fake clock and race with the test. + if d <= 0 { + return + } + for c.paused.Load() { runtime.Gosched() } From b780d84bff62273321ef1e4ea5e9b9aa7b7258b4 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:39:35 +0200 Subject: [PATCH 16/29] p2p: label handler metrics from validated stream protocol (#4722) * p2p: label handler metrics from validated stream protocol * p2p: deflake metrics tests racing deferred observations (cherry picked from commit eb506a61959b0ed1853db473a24c201001042624) --- p2p/metrics_internal_test.go | 27 +++++++++++++++++++++------ p2p/receive.go | 26 ++++++++++++++------------ 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/p2p/metrics_internal_test.go b/p2p/metrics_internal_test.go index 79a9217c26..5b99576c9c 100644 --- a/p2p/metrics_internal_test.go +++ b/p2p/metrics_internal_test.go @@ -42,6 +42,11 @@ func TestMessageSizeMetrics(t *testing.T) { server = testutil.CreateHost(t, testutil.AvailableAddr(t)) ) + // Received sizes are labelled by peer name, so both directions must not share one. + for PeerName(client.ID()) == PeerName(server.ID()) { + client = testutil.CreateHost(t, testutil.AvailableAddr(t)) + } + client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) RegisterHandler("server", server, pID, @@ -63,8 +68,13 @@ func TestMessageSizeMetrics(t *testing.T) { require.NoError(t, SendReceive(ctx, client, server.ID(), req, resp, pID)) // Client sent the request and server sent the identical echoed response. - sentCount, sentSum := histSample(t, sentMsgSizeHist, string(pID)) - require.Equal(t, sentCount0+2, sentCount) + // The server observes its send after writing the response, so wait for it. + require.Eventually(t, func() bool { + sentCount, _ := histSample(t, sentMsgSizeHist, string(pID)) + return sentCount == sentCount0+2 + }, time.Second*5, time.Millisecond*10) + + _, sentSum := histSample(t, sentMsgSizeHist, string(pID)) require.InDelta(t, sentSum0+2*msgSize, sentSum, 0.1) // Server received the request from the client. @@ -138,12 +148,17 @@ func TestInflightRequestMetrics(t *testing.T) { require.Equal(t, uint64(1), concCount) require.InDelta(t, 1, concSum, 0.1) - // Handler duration observed once. - durCount, durSum := histSample(t, handlerDuration, string(pID)) - require.Equal(t, uint64(1), durCount) + // Handler duration observed once. It is observed when the handler returns, after + // writing the response, so wait for it. + require.Eventually(t, func() bool { + durCount, _ := histSample(t, handlerDuration, string(pID)) + return durCount == 1 + }, time.Second*5, time.Millisecond*10) + + _, durSum := histSample(t, handlerDuration, string(pID)) require.Positive(t, durSum) - // In-flight gauge back to zero after completion. + // In-flight gauge back to zero after completion (set before the duration is observed). gauge, err := inflightGauge.GetMetricWithLabelValues(string(pID), clientName) require.NoError(t, err) require.Zero(t, promtestutil.ToFloat64(gauge)) diff --git a/p2p/receive.go b/p2p/receive.go index 62c31000ff..8afe6c7864 100644 --- a/p2p/receive.go +++ b/p2p/receive.go @@ -54,15 +54,14 @@ func RegisterHandler(logTopic string, p2pNode host.Host, pID protocol.ID, t0 := time.Now() name := PeerName(s.Conn().RemotePeer()) - handlerDone := observeHandlerStart(s.Protocol(), s.Conn().RemotePeer()) - defer handlerDone() + streamProtocol := s.Protocol() _ = s.SetReadDeadline(time.Now().Add(o.receiveTimeout)) ctx, cancel := context.WithTimeout(context.Background(), o.receiveTimeout) ctx = log.WithTopic(ctx, logTopic) ctx = log.WithCtx(ctx, z.Str("peer", name), - z.Any("protocol", s.Protocol()), + z.Any("protocol", streamProtocol), ) defer cancel() @@ -78,18 +77,21 @@ func RegisterHandler(logTopic string, p2pNode host.Host, pID protocol.ID, } }() - writeFunc, ok := o.writersByProtocol[s.Protocol()] + writeFunc, ok := o.writersByProtocol[streamProtocol] if !ok { - log.Error(ctx, "No writer registered for protocol. This may indicate an unsupported or misconfigured p2p protocol", nil, z.Any("protocol", s.Protocol())) + log.Error(ctx, "No writer registered for protocol. This may indicate an unsupported or misconfigured p2p protocol", nil, z.Any("protocol", streamProtocol)) return } - readFunc, ok := o.readersByProtocol[s.Protocol()] + readFunc, ok := o.readersByProtocol[streamProtocol] if !ok { - log.Error(ctx, "No reader registered for protocol. This may indicate an unsupported or misconfigured p2p protocol", nil, z.Any("protocol", s.Protocol())) + log.Error(ctx, "No reader registered for protocol. This may indicate an unsupported or misconfigured p2p protocol", nil, z.Any("protocol", streamProtocol)) return } + handlerDone := observeHandlerStart(streamProtocol, s.Conn().RemotePeer()) + defer handlerDone() + req := zeroReq() err := readFunc(s).ReadMsg(req) @@ -97,17 +99,17 @@ func RegisterHandler(logTopic string, p2pNode host.Host, pID protocol.ID, // Resets on relayed (limited) connections are benign circuit recycling, // but a reset on a direct connection is a genuine read failure. if !s.Conn().Stat().Limited { - incMessageReadError(s.Protocol(), s.Conn().RemotePeer()) + incMessageReadError(streamProtocol, s.Conn().RemotePeer()) } return // Ignore relay errors. } else if netErr := net.Error(nil); errors.As(err, &netErr) && netErr.Timeout() { - incMessageReadError(s.Protocol(), s.Conn().RemotePeer()) + incMessageReadError(streamProtocol, s.Conn().RemotePeer()) log.Error(ctx, "Timeout reading p2p message from peer. This may indicate network latency issues or unresponsive peer", err, z.Any("duration", time.Since(t0))) return } else if err != nil { - incMessageReadError(s.Protocol(), s.Conn().RemotePeer()) + incMessageReadError(streamProtocol, s.Conn().RemotePeer()) log.Error(ctx, "Failed to read p2p request from peer. Check network connectivity and peer health", err, z.Any("duration", time.Since(t0))) return @@ -115,7 +117,7 @@ func RegisterHandler(logTopic string, p2pNode host.Host, pID protocol.ID, // Observe before application-level validation so malformed-but-readable messages // are still visible in the size histogram. - observeReceivedMessage(s.Protocol(), s.Conn().RemotePeer(), req) + observeReceivedMessage(streamProtocol, s.Conn().RemotePeer(), req) if err := protonil.Check(req); err != nil { log.Warn(ctx, "LibP2P received invalid proto", err) @@ -139,6 +141,6 @@ func RegisterHandler(logTopic string, p2pNode host.Host, pID protocol.ID, return } - observeSentMessage(s.Protocol(), resp) + observeSentMessage(streamProtocol, resp) }) } From 8cf417c1b90f0f0cde8272847a0c0171df3d68bb Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:19:30 +0200 Subject: [PATCH 17/29] p2p: restore message handler panics metric (#4706) --- docs/metrics.md | 1 + p2p/metrics.go | 11 +++++++++++ p2p/receive.go | 1 + 3 files changed, 13 insertions(+) diff --git a/docs/metrics.md b/docs/metrics.md index d18f0517a0..61b4d60d89 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -103,6 +103,7 @@ when storing metrics from multiple nodes or clusters in one Prometheus instance. | `p2p_concurrent_requests` | Histogram | Number of concurrently handled inbound messages, observed at each message arrival, by protocol and sending peer. Unlike the sampled inflight_requests gauge, this captures bursts between scrapes. | `protocol, peer` | | `p2p_handler_duration_seconds` | Histogram | Duration of inbound libp2p message handling from stream accept to handler completion by protocol. Explains inflight_requests: inflight equals message rate times this duration. | `protocol` | | `p2p_inflight_requests` | Gauge | Current number of inbound libp2p messages being handled (stream accept to handler completion) by protocol and sending peer. | `protocol, peer` | +| `p2p_message_handler_panics_total` | Counter | Total number of panics recovered while handling an inbound libp2p message by protocol and sending peer. A non-zero value indicates a handler bug or malformed peer message. | `protocol, peer` | | `p2p_message_read_errors_total` | Counter | Total number of failures reading a libp2p message by protocol and sending peer. Includes messages exceeding the protocol read limit. | `protocol, peer` | | `p2p_peer_connection_total` | Counter | Total number of libp2p connections per peer. | `peer` | | `p2p_peer_connection_types` | Gauge | Current number of libp2p connections by peer, type (`direct` or `relay`), and protocol (`tcp`, `quic`). Note that peers may have multiple connections. | `peer, type, protocol` | diff --git a/p2p/metrics.go b/p2p/metrics.go index 3d3fab28fa..68eddfea8c 100644 --- a/p2p/metrics.go +++ b/p2p/metrics.go @@ -122,6 +122,12 @@ var ( Help: "Total number of failures reading a libp2p message by protocol and sending peer. Includes messages exceeding the protocol read limit.", }, []string{"protocol", "peer"}) + msgHandlerPanicCounter = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "p2p", + Name: "message_handler_panics_total", + Help: "Total number of panics recovered while handling an inbound libp2p message by protocol and sending peer. A non-zero value indicates a handler bug or malformed peer message.", + }, []string{"protocol", "peer"}) + inflightGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "p2p", Name: "inflight_requests", @@ -211,6 +217,11 @@ func incMessageReadError(pID protocol.ID, peerID peer.ID) { msgReadErrorCounter.WithLabelValues(string(pID), PeerName(peerID)).Inc() } +// incMessageHandlerPanic increments the recovered handler panic counter for the protocol and sending peer. +func incMessageHandlerPanic(pID protocol.ID, peerID peer.ID) { + msgHandlerPanicCounter.WithLabelValues(string(pID), PeerName(peerID)).Inc() +} + func observePing(p peer.ID, d time.Duration) { pingLatencies.WithLabelValues(PeerName(p)).Observe(d.Seconds()) pingSuccess.WithLabelValues(PeerName(p)).Set(1) diff --git a/p2p/receive.go b/p2p/receive.go index 8afe6c7864..d72c421f8c 100644 --- a/p2p/receive.go +++ b/p2p/receive.go @@ -70,6 +70,7 @@ func RegisterHandler(logTopic string, p2pNode host.Host, pID protocol.ID, // Recover any panic in this stream handler goroutine. defer func() { if r := recover(); r != nil { + incMessageHandlerPanic(streamProtocol, s.Conn().RemotePeer()) log.Error(ctx, "Recovered from panic handling p2p message; stream dropped and process kept alive", nil, z.Any("recover", r), z.Str("stacktrace", string(debug.Stack())), From e13990281b593f77cd12ebdcfe1c31bd55a824ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:19:30 +0200 Subject: [PATCH 18/29] build(deps): Bump golang.org/x/tools (#4742) Bumps the go-dependencies group with 1 update: [golang.org/x/tools](https://github.com/golang/tools). Updates `golang.org/x/tools` from 0.50.0 to 0.51.0 - [Release notes](https://github.com/golang/tools/releases) - [Commits](https://github.com/golang/tools/compare/v0.50.0...v0.51.0) --- updated-dependencies: - dependency-name: golang.org/x/tools dependency-version: 0.51.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 7c0ff601afaf17b24504b474e339b531b9507922) --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 15155f5aeb..9c2bf7f05f 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( golang.org/x/term v0.46.0 golang.org/x/text v0.42.0 golang.org/x/time v0.16.0 - golang.org/x/tools v0.50.0 + golang.org/x/tools v0.51.0 google.golang.org/protobuf v1.36.12 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -276,7 +276,7 @@ require ( golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect golang.org/x/mod v0.41.0 // indirect golang.org/x/sys v0.48.0 // indirect - golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 // indirect + golang.org/x/telemetry v0.0.0-20260924152758-ed294f943157 // indirect golang.org/x/vuln v1.1.4 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect diff --git a/go.sum b/go.sum index 0be4cef2cb..8452b9fed5 100644 --- a/go.sum +++ b/go.sum @@ -702,8 +702,8 @@ golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 h1:F5BWKvW126NXR74uxkxuc1jQHhm/rwm/J3rSiFyuRs4= -golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518/go.mod h1:i+ivNqjDnTF3WTElsdk5g9V5DTSBYgdNo7xTU9SDwYA= +golang.org/x/telemetry v0.0.0-20260924152758-ed294f943157 h1:DftxASHKl6JolFPqoN+2WrFO53JvkHwRXvFqf8gsivA= +golang.org/x/telemetry v0.0.0-20260924152758-ed294f943157/go.mod h1:i+ivNqjDnTF3WTElsdk5g9V5DTSBYgdNo7xTU9SDwYA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= @@ -718,8 +718,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= -golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= From 037b13d8d3c066f5ffb9d3e3837306bb3bc117e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:47:48 +0200 Subject: [PATCH 19/29] build(deps): Bump the go-dependencies group with 3 updates (#4770) Bumps the go-dependencies group with 3 updates: [github.com/drand/kyber](https://github.com/drand/kyber), [github.com/drand/kyber-bls12381](https://github.com/drand/kyber-bls12381) and [golang.org/x/sync](https://github.com/golang/sync). Updates `github.com/drand/kyber` from 1.3.2 to 1.3.3 - [Release notes](https://github.com/drand/kyber/releases) - [Commits](https://github.com/drand/kyber/compare/v1.3.2...v1.3.3) Updates `github.com/drand/kyber-bls12381` from 0.3.4 to 0.3.5 - [Release notes](https://github.com/drand/kyber-bls12381/releases) - [Commits](https://github.com/drand/kyber-bls12381/compare/v0.3.4...v0.3.5) Updates `golang.org/x/sync` from 0.23.0 to 0.24.0 - [Commits](https://github.com/golang/sync/compare/v0.23.0...v0.24.0) --- updated-dependencies: - dependency-name: github.com/drand/kyber dependency-version: 1.3.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: github.com/drand/kyber-bls12381 dependency-version: 0.3.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: golang.org/x/sync dependency-version: 0.24.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 3c480a127d45534aeaccce1c10a25bd1454cd8c7) # Conflicts: # go.mod --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 9c2bf7f05f..c3808588d5 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( github.com/attestantio/go-eth2-client v0.29.0 github.com/coinbase/kryptology v1.5.6-0.20220316191335-269410e1b06b github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 - github.com/drand/kyber v1.3.2 - github.com/drand/kyber-bls12381 v0.3.4 + github.com/drand/kyber v1.3.3 + github.com/drand/kyber-bls12381 v0.3.5 github.com/ethereum/go-ethereum v1.17.7 github.com/ferranbt/fastssz v1.0.0 github.com/fsnotify/fsnotify v1.10.1 @@ -49,7 +49,7 @@ require ( go.uber.org/zap v1.28.0 golang.org/x/crypto v0.57.0 golang.org/x/net v0.60.0 - golang.org/x/sync v0.23.0 + golang.org/x/sync v0.24.0 golang.org/x/term v0.46.0 golang.org/x/text v0.42.0 golang.org/x/time v0.16.0 @@ -88,7 +88,7 @@ require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.24.4 // indirect + github.com/bits-and-blooms/bitset v1.26.0 // indirect github.com/bufbuild/buf v1.72.0 // indirect github.com/bufbuild/protocompile v0.14.2-0.20260716165721-bb5762d29672 // indirect github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b // indirect @@ -107,7 +107,7 @@ require ( github.com/cockroachdb/redact v1.1.6 // indirect github.com/cockroachdb/swiss v0.0.0-20260820225851-333444432258 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect - github.com/consensys/gnark-crypto v0.19.2 // indirect + github.com/consensys/gnark-crypto v0.22.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect diff --git a/go.sum b/go.sum index 8452b9fed5..bb5c7f9821 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= -github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.26.0 h1:WPvcPZbP1yuKofmjs2SYIFO1EjzIGhCP9ducElLsh5Q= +github.com/bits-and-blooms/bitset v1.26.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= @@ -123,8 +123,8 @@ github.com/cockroachdb/swiss v0.0.0-20260820225851-333444432258 h1:IJ+uNItEm0qx9 github.com/cockroachdb/swiss v0.0.0-20260820225851-333444432258/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= -github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80= -github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= +github.com/consensys/gnark-crypto v0.22.0 h1:RYH7FX1/zkmNrSiABo70HAqew41SRUsoluKundObo3o= +github.com/consensys/gnark-crypto v0.22.0/go.mod h1:HL8XflRG4cvau1PEG3CWBTD2kkzUzdlqZF6BBgy57F8= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -159,10 +159,10 @@ github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/drand/kyber v1.3.2 h1:Cf3NNcb5bV3eODopr3XVHzImjDK40GiObhFUFG93Zeo= -github.com/drand/kyber v1.3.2/go.mod h1:ciDFWoC7ajb89niGJnS4C1Xeo4lSJMmbi+km5w8juAI= -github.com/drand/kyber-bls12381 v0.3.4 h1:rrmYcRcXmtOAvKWVBxRQxi22qNMVcS2Jz7MAebZQJxI= -github.com/drand/kyber-bls12381 v0.3.4/go.mod h1:jh3IGIAQfdLrdNKYz1HWZ3YdfJM0DWlN1TxXkh60utk= +github.com/drand/kyber v1.3.3 h1:iUIg5JWlAr8Qdqkl36J+PbHK0BMdQlLm+P/wV34EDX8= +github.com/drand/kyber v1.3.3/go.mod h1:ptX3XpLo5LH4tX2y297cXSwFOIuZii5O7MkDgG0I7mA= +github.com/drand/kyber-bls12381 v0.3.5 h1:k6il25UDe3jhC0vEbrAV5y6SIZNLmleMmcnPFSPBIQ4= +github.com/drand/kyber-bls12381 v0.3.5/go.mod h1:fU16hOyVzjOob1G1A8TRRVNCSd2Z0VNv43XlXo2gtbU= github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/emicklei/dot v1.8.0 h1:HnD60yAKFAevNeT+TPYr9pb8VB9bqdeSo0nzwIW6IOI= @@ -682,8 +682,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= -golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 9e7656af9275a32d46d127202e861cd84e0bfc55 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:24:41 +0200 Subject: [PATCH 20/29] app/eth2wrap: bind eth2http service lifetime to caller ctx (#4772) * app/eth2wrap: bind eth2http service lifetime to caller ctx * app/retry: retry eth2http not active and not synced errors (cherry picked from commit e76d179763f34a6a3b4ca25c1974391726f6bdb7) --- app/app.go | 6 +- app/eth2wrap/eth2wrap.go | 29 ++- app/eth2wrap/eth2wrap_internal_test.go | 300 +++++++++++++++++++++++++ app/eth2wrap/eth2wrap_test.go | 18 +- app/retry/retry.go | 7 + app/retry/retry_test.go | 23 ++ cmd/exit.go | 2 +- core/scheduler/scheduler_test.go | 2 +- 8 files changed, 361 insertions(+), 26 deletions(-) create mode 100644 app/eth2wrap/eth2wrap_internal_test.go diff --git a/app/app.go b/app/app.go index 724dce736a..9034c4c4b3 100644 --- a/app/app.go +++ b/app/app.go @@ -957,7 +957,7 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager, lo return nil, nil, err } - fb := eth2wrap.NewSimnetFallbacks(bnTimeout, [4]byte(forkVersion), beaconNodeHeaders, conf.FallbackBeaconNodeAddrs) + fb := eth2wrap.NewSimnetFallbacks(ctx, bnTimeout, [4]byte(forkVersion), beaconNodeHeaders, conf.FallbackBeaconNodeAddrs) wrap, err := eth2wrap.Instrument([]eth2wrap.Client{bmock}, fb) if err != nil { @@ -1000,7 +1000,7 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager, lo return nil, nil, err } - fb := eth2wrap.NewSimnetFallbacks(bnTimeout, [4]byte(forkVersion), beaconNodeHeaders, conf.FallbackBeaconNodeAddrs) + fb := eth2wrap.NewSimnetFallbacks(ctx, bnTimeout, [4]byte(forkVersion), beaconNodeHeaders, conf.FallbackBeaconNodeAddrs) wrap, err := eth2wrap.Instrument([]eth2wrap.Client{bmock}, fb) if err != nil { @@ -1046,7 +1046,7 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager, lo // configureEth2Client configures a beacon node client with the provided settings. func configureEth2Client(ctx context.Context, forkVersion []byte, fallbackAddrs []string, addrs []string, headers map[string]string, timeout time.Duration, syntheticBlockProposals bool) (eth2wrap.Client, error) { - eth2Cl, err := eth2wrap.NewMultiHTTP(timeout, [4]byte(forkVersion), headers, addrs, fallbackAddrs) + eth2Cl, err := eth2wrap.NewMultiHTTP(ctx, timeout, [4]byte(forkVersion), headers, addrs, fallbackAddrs) if err != nil { return nil, errors.Wrap(err, "new eth2 http client") } diff --git a/app/eth2wrap/eth2wrap.go b/app/eth2wrap/eth2wrap.go index 8568d9cb30..b1c8af7617 100644 --- a/app/eth2wrap/eth2wrap.go +++ b/app/eth2wrap/eth2wrap.go @@ -92,46 +92,51 @@ func WithSyntheticDuties(cl Client) Client { } // NewMultiHTTP returns a new instrumented multi eth2 http client. -func NewMultiHTTP(timeout time.Duration, forkVersion [4]byte, headers map[string]string, addrs []string, fallbackAddrs []string) (Client, error) { +// The ctx bounds the lifetime of the underlying eth2http services' background routines. +func NewMultiHTTP(ctx context.Context, timeout time.Duration, forkVersion [4]byte, headers map[string]string, addrs []string, fallbackAddrs []string) (Client, error) { return Instrument( - newClients(timeout, forkVersion, headers, addrs), - newClients(timeout, forkVersion, headers, fallbackAddrs), + newClients(ctx, timeout, forkVersion, headers, addrs), + newClients(ctx, timeout, forkVersion, headers, fallbackAddrs), ) } // NewSimnetFallbacks returns a slice of Client initialized with the provided settings. Used in Simnet setting. -func NewSimnetFallbacks(timeout time.Duration, forkVersion [4]byte, headers map[string]string, addresses []string) []Client { +func NewSimnetFallbacks(ctx context.Context, timeout time.Duration, forkVersion [4]byte, headers map[string]string, addresses []string) []Client { var clients []Client for _, address := range addresses { - clients = append(clients, newBeaconClient(timeout, forkVersion, headers, address)) + clients = append(clients, newBeaconClient(ctx, timeout, forkVersion, headers, address)) } return clients } // newClients returns a slice of Client initialized with the provided settings. -func newClients(timeout time.Duration, forkVersion [4]byte, headers map[string]string, addresses []string) []Client { +func newClients(ctx context.Context, timeout time.Duration, forkVersion [4]byte, headers map[string]string, addresses []string) []Client { var clients []Client for _, address := range addresses { - clients = append(clients, newBeaconClient(timeout, forkVersion, headers, address)) + clients = append(clients, newBeaconClient(ctx, timeout, forkVersion, headers, address)) } return clients } -// newBeaconClient returns a Client with the provided settings. -func newBeaconClient(timeout time.Duration, forkVersion [4]byte, headers map[string]string, address string) Client { - parameters := []eth2http.Parameter{ +// newBeaconClient returns a Client with the provided settings, extraParams override them. +func newBeaconClient(svcCtx context.Context, timeout time.Duration, forkVersion [4]byte, headers map[string]string, address string, + extraParams ...eth2http.Parameter, +) Client { + parameters := append([]eth2http.Parameter{ eth2http.WithLogLevel(zeroLogInfo), eth2http.WithAddress(address), eth2http.WithTimeout(timeout), eth2http.WithAllowDelayedStart(true), eth2http.WithEnforceJSON(featureset.Enabled(featureset.JSONRequests)), eth2http.WithExtraHeaders(headers), - } + }, extraParams...) cl := newLazy(func(ctx context.Context) (Client, error) { - eth2Svc, err := eth2http.New(ctx, parameters...) + // eth2http.New ties its background routines (static value cache clearing, connection + // state pings) to the ctx it is given, so use svcCtx, not the triggering request's ctx. + eth2Svc, err := eth2http.New(svcCtx, parameters...) if err != nil { return nil, wrapError(ctx, err, "new eth2 client", z.Str("address", address)) } diff --git a/app/eth2wrap/eth2wrap_internal_test.go b/app/eth2wrap/eth2wrap_internal_test.go new file mode 100644 index 0000000000..5029f243cf --- /dev/null +++ b/app/eth2wrap/eth2wrap_internal_test.go @@ -0,0 +1,300 @@ +// Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package eth2wrap + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "sync" + "sync/atomic" + "testing" + "testing/synctest" + "time" + + eth2api "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" + eth2http "github.com/attestantio/go-eth2-client/http" + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/stretchr/testify/require" + + "github.com/obolnetwork/charon/app/retry" +) + +// TestNodeVersionV2Refresh covers a beacon node that reports its execution client only after its +// version was first fetched, e.g. when they start together. The execution client must show up once +// the eth2http service clears its static values cache, without restarting. +func TestNodeVersionV2Refresh(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + var withEE atomic.Bool + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/eth/v1/node/version": + _, _ = w.Write([]byte(`{"data":{"version":"Lighthouse/v8.0.0"}}`)) + case "/eth/v1/node/syncing": + _, _ = w.Write([]byte(`{"data":{"head_slot":"100","sync_distance":"0","is_syncing":false,"is_optimistic":false,"el_offline":false}}`)) + case "/eth/v2/node/version": + ee := "" + if withEE.Load() { + ee = `,"execution_client":{"code":"GE","name":"Geth","version":"v1.16.0","commit":"12345678"}` + } + + _, _ = fmt.Fprintf(w, `{"data":{"beacon_node":{"code":"LH","name":"Lighthouse","version":"v8.0.0","commit":"abcdef01"}%s}}`, ee) + default: + w.WriteHeader(http.StatusNotFound) + } + }) + + // Serve in-memory, since time in the bubble only advances while no goroutine waits on the network. + httpCl := &http.Client{Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, r) + + return rec.Result(), nil + })} + + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + cl, err := Instrument([]Client{ + newBeaconClient(ctx, time.Second, [4]byte{}, nil, "http://beacon-node", eth2http.WithHTTPClient(httpCl)), + }, nil) + require.NoError(t, err) + + executionClient := func() *apiv1.ClientVersion { + resp, err := cl.NodeVersionV2(ctx, ð2api.NodeVersionV2Opts{}) + require.NoError(t, err) + + return resp.Data.ExecutionClient + } + + require.Nil(t, executionClient()) + + withEE.Store(true) + require.Nil(t, executionClient(), "expected cached version") + + // The eth2http service clears its static values every 5 minutes. + time.Sleep(5 * time.Minute) + synctest.Wait() + + ee := executionClient() + require.NotNil(t, ee, "execution client didn't refresh") + require.Equal(t, "Geth", ee.Name) + }) +} + +type roundTripperFunc func(*http.Request) (*http.Response, error) + +func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { + return f(r) +} + +// TestConnectionStateRecovery covers duty requests after the eth2http service's periodic connection +// state ping found a beacon node slow or syncing. The service then fails requests without querying +// the node until a request's own ping succeeds, and concurrent requests fail while that ping is in +// flight, so duties rely on retries to recover. +func TestConnectionStateRecovery(t *testing.T) { + t.Parallel() + + const ( + timeout = 500 * time.Millisecond + deadline = 2 * time.Second + requests = 20 + ) + + slowPing := func(bn *fakeBeaconNode) { bn.syncingDelay.Store(int64(2 * timeout)) } + syncing := func(bn *fakeBeaconNode) { bn.syncing.Store(true) } + healthy := func(bn *fakeBeaconNode) { + bn.syncingDelay.Store(0) + bn.syncing.Store(false) + } + unchanged := func(*fakeBeaconNode) {} + + tests := []struct { + name string + // nodes is the number of beacon nodes, the first one is pinged. + nodes int + // beforePing and afterPing modify the first node around the ping. + beforePing func(*fakeBeaconNode) + afterPing func(*fakeBeaconNode) + // expectSuccess is whether the requests succeed within the deadline. + expectSuccess bool + }{ + { + name: "ping timed out, node recovered", + nodes: 1, + beforePing: slowPing, + afterPing: healthy, + expectSuccess: true, + }, + { + name: "ping found node syncing, node synced", + nodes: 1, + beforePing: syncing, + afterPing: healthy, + expectSuccess: true, + }, + { + name: "ping keeps timing out", + nodes: 1, + beforePing: slowPing, + afterPing: unchanged, + expectSuccess: false, + }, + { + name: "ping timed out, other node healthy", + nodes: 2, + beforePing: slowPing, + afterPing: unchanged, + expectSuccess: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + var ( + bns []*fakeBeaconNode + clients []Client + ) + + for range test.nodes { + bn := newFakeBeaconNode(t) + bns = append(bns, bn) + clients = append(clients, newBeaconClient(t.Context(), timeout, [4]byte{}, nil, bn.URL)) + } + + // Create the lazy clients individually, since a multi client stops at the first response. + for _, c := range clients { + _, err := c.AttestationData(t.Context(), ð2api.AttestationDataOpts{Slot: 1}) + require.NoError(t, err) + } + + cl, err := Instrument(clients, nil) + require.NoError(t, err) + + pinged := bns[0] + svc := eth2Service(t, clients[0]) + + test.beforePing(pinged) + svc.CheckConnectionState(t.Context()) + require.False(t, svc.IsSynced()) + test.afterPing(pinged) + + pinged.attDataHits.Store(0) + + retryer := retry.New(func(deadline time.Time) (time.Time, bool) { + return deadline, true + }) + + var ( + wg sync.WaitGroup + successes atomic.Int64 + ) + + for i := range requests { + wg.Go(func() { + retryer.DoAsync(t.Context(), time.Now().Add(deadline), "test", "attestation_data", func(ctx context.Context) error { + _, err := cl.AttestationData(ctx, ð2api.AttestationDataOpts{Slot: eth2p0.Slot(10 + i)}) + if err != nil { + return err + } + + successes.Add(1) + + return nil + }) + }) + } + + wg.Wait() + + if test.expectSuccess { + require.EqualValues(t, requests, successes.Load()) + } else { + require.Zero(t, successes.Load()) + require.Zero(t, pinged.attDataHits.Load(), "service queried node it considers inactive") + } + }) + } +} + +// fakeBeaconNode is a beacon node serving node version and attestation data, with a controllable +// node syncing endpoint. Like real beacon nodes, it doesn't serve attestation data while syncing. +type fakeBeaconNode struct { + URL string + syncingDelay atomic.Int64 // time.Duration + syncing atomic.Bool + attDataHits atomic.Int64 +} + +func newFakeBeaconNode(t *testing.T) *fakeBeaconNode { + t.Helper() + + bn := new(fakeBeaconNode) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/eth/v1/node/version": + _, _ = w.Write([]byte(`{"data":{"version":"Lighthouse/v8.0.0"}}`)) + case "/eth/v1/node/syncing": + select { + case <-time.After(time.Duration(bn.syncingDelay.Load())): + case <-r.Context().Done(): + return + } + + if bn.syncing.Load() { + _, _ = w.Write([]byte(`{"data":{"head_slot":"100","sync_distance":"50","is_syncing":true,"is_optimistic":false,"el_offline":false}}`)) + } else { + _, _ = w.Write([]byte(`{"data":{"head_slot":"100","sync_distance":"0","is_syncing":false,"is_optimistic":false,"el_offline":false}}`)) + } + case "/eth/v1/config/spec": + _, _ = w.Write([]byte(`{"data":{"SLOTS_PER_EPOCH":"32","ELECTRA_FORK_EPOCH":"0"}}`)) + case "/eth/v1/validator/attestation_data": + bn.attDataHits.Add(1) + + if bn.syncing.Load() { + w.WriteHeader(http.StatusServiceUnavailable) + _, _ = w.Write([]byte(`{"code":503,"message":"Beacon node is currently syncing"}`)) + + return + } + + root := "0x" + strings.Repeat("00", 32) + _, _ = fmt.Fprintf(w, `{"data":{"slot":"%s","index":"0","beacon_block_root":"%s","source":{"epoch":"1","root":"%s"},"target":{"epoch":"2","root":"%s"}}}`, + r.URL.Query().Get("slot"), root, root, root) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + t.Cleanup(srv.Close) + + bn.URL = srv.URL + + return bn +} + +// eth2Service returns the eth2http service of a created lazy beacon client. +func eth2Service(t *testing.T, cl Client) *eth2http.Service { + t.Helper() + + l, ok := cl.(*lazy) + require.True(t, ok) + + inner, ok := l.getClient() + require.True(t, ok) + + adapter, ok := inner.(*httpAdapter) + require.True(t, ok) + + return adapter.Service +} diff --git a/app/eth2wrap/eth2wrap_test.go b/app/eth2wrap/eth2wrap_test.go index 900b13af62..76436e6be2 100644 --- a/app/eth2wrap/eth2wrap_test.go +++ b/app/eth2wrap/eth2wrap_test.go @@ -283,7 +283,7 @@ func TestErrors(t *testing.T) { ctx := context.Background() t.Run("network dial error", func(t *testing.T) { - cl, err := eth2wrap.NewMultiHTTP(time.Hour, [4]byte{}, nil, []string{"localhost:22222"}, nil) + cl, err := eth2wrap.NewMultiHTTP(t.Context(), time.Hour, [4]byte{}, nil, []string{"localhost:22222"}, nil) require.NoError(t, err) _, err = cl.NodePeerCount(ctx, ð2api.NodePeerCountOpts{}) @@ -298,7 +298,7 @@ func TestErrors(t *testing.T) { })) t.Run("http timeout", func(t *testing.T) { - cl, err := eth2wrap.NewMultiHTTP(time.Millisecond, [4]byte{}, nil, []string{srv.URL}, nil) + cl, err := eth2wrap.NewMultiHTTP(t.Context(), time.Millisecond, [4]byte{}, nil, []string{srv.URL}, nil) require.NoError(t, err) _, err = cl.NodePeerCount(ctx, ð2api.NodePeerCountOpts{}) @@ -311,7 +311,7 @@ func TestErrors(t *testing.T) { ctx, cancel := context.WithCancel(ctx) cancel() - cl, err := eth2wrap.NewMultiHTTP(time.Millisecond, [4]byte{}, nil, []string{srv.URL}, nil) + cl, err := eth2wrap.NewMultiHTTP(t.Context(), time.Millisecond, [4]byte{}, nil, []string{srv.URL}, nil) require.NoError(t, err) _, err = cl.NodePeerCount(ctx, ð2api.NodePeerCountOpts{}) @@ -367,7 +367,7 @@ func TestCtxCancel(t *testing.T) { bmock, err := beaconmock.New(t.Context()) require.NoError(t, err) - eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte{}, nil, []string{bmock.Address()}, nil) + eth2Cl, err := eth2wrap.NewMultiHTTP(t.Context(), time.Second, [4]byte{}, nil, []string{bmock.Address()}, nil) require.NoError(t, err) cancel() // Cancel context before calling method. @@ -394,7 +394,7 @@ func TestOneError(t *testing.T) { bmock.Address(), // Valid } - eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte{}, nil, addresses, nil) + eth2Cl, err := eth2wrap.NewMultiHTTP(t.Context(), time.Second, [4]byte{}, nil, addresses, nil) require.NoError(t, err) eth2Resp, err := eth2Cl.Spec(ctx, ð2api.SpecOpts{}) @@ -425,7 +425,7 @@ func TestOneTimeout(t *testing.T) { bmock.Address(), // Valid } - eth2Cl, err := eth2wrap.NewMultiHTTP(time.Minute, [4]byte{}, nil, addresses, nil) + eth2Cl, err := eth2wrap.NewMultiHTTP(t.Context(), time.Minute, [4]byte{}, nil, addresses, nil) require.NoError(t, err) eth2Resp, err := eth2Cl.Spec(ctx, ð2api.SpecOpts{}) @@ -448,7 +448,7 @@ func TestOnlyTimeout(t *testing.T) { defer srv.Close() defer cancel() // Cancel the context before stopping the server. - eth2Cl, err := eth2wrap.NewMultiHTTP(time.Minute, [4]byte{}, nil, []string{srv.URL}, nil) + eth2Cl, err := eth2wrap.NewMultiHTTP(t.Context(), time.Minute, [4]byte{}, nil, []string{srv.URL}, nil) require.NoError(t, err) // Start goroutine that is blocking trying to create the client. @@ -519,7 +519,7 @@ func TestLazy(t *testing.T) { httputil.NewSingleHostReverseProxy(target).ServeHTTP(w, r) })) - eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte{}, nil, []string{srv1.URL, srv2.URL}, nil) + eth2Cl, err := eth2wrap.NewMultiHTTP(t.Context(), time.Second, [4]byte{}, nil, []string{srv1.URL, srv2.URL}, nil) require.NoError(t, err) // Both proxies are disabled, so this should fail. @@ -598,7 +598,7 @@ func TestLazyDomain(t *testing.T) { forkVersionHex, err := hex.DecodeString(test.in) require.NoError(t, err) - eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second, [4]byte(forkVersionHex), nil, []string{srv.URL}, nil) + eth2Cl, err := eth2wrap.NewMultiHTTP(t.Context(), time.Second, [4]byte(forkVersionHex), nil, []string{srv.URL}, nil) require.NoError(t, err) voluntaryExitDomain := eth2p0.DomainType{0x04, 0x00, 0x00, 0x00} diff --git a/app/retry/retry.go b/app/retry/retry.go index 2788f55c94..91330a3355 100644 --- a/app/retry/retry.go +++ b/app/retry/retry.go @@ -15,6 +15,7 @@ import ( "testing" "time" + eth2client "github.com/attestantio/go-eth2-client" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" @@ -276,5 +277,11 @@ func isTemporaryBeaconErr(err error) bool { return true } + // The eth2http service fails requests while its connection state is stale or being + // refreshed, which it does on the next request. + if errors.Is(err, eth2client.ErrNotActive) || errors.Is(err, eth2client.ErrNotSynced) { + return true + } + return false } diff --git a/app/retry/retry_test.go b/app/retry/retry_test.go index f94679efaf..1ed92c2ab8 100644 --- a/app/retry/retry_test.go +++ b/app/retry/retry_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + eth2client "github.com/attestantio/go-eth2-client" "github.com/stretchr/testify/require" "github.com/obolnetwork/charon/app/errors" @@ -39,6 +40,28 @@ func TestRetryer(t *testing.T) { }, ExpectBackoffs: 1, }, + { + Name: "one retry on beacon node not active", + Func: func(ctx context.Context, attempt int) error { + if attempt == 0 { + return errors.Wrap(eth2client.ErrNotActive, "beacon api attestation_data") + } + + return nil + }, + ExpectBackoffs: 1, + }, + { + Name: "one retry on beacon node not synced", + Func: func(ctx context.Context, attempt int) error { + if attempt == 0 { + return errors.Wrap(eth2client.ErrNotSynced, "beacon api attestation_data") + } + + return nil + }, + ExpectBackoffs: 1, + }, { Name: "not retryable error", Func: func(ctx context.Context, attempt int) error { diff --git a/cmd/exit.go b/cmd/exit.go index e0e7bf4b81..428b9334e3 100644 --- a/cmd/exit.go +++ b/cmd/exit.go @@ -211,7 +211,7 @@ func bindExitFlags(cmd *cobra.Command, config *exitConfig, flags []exitCLIFlag) } func eth2Client(ctx context.Context, fallbackAddresses []string, headers map[string]string, u []string, timeout time.Duration, forkVersion [4]byte) (eth2wrap.Client, error) { - cl, err := eth2wrap.NewMultiHTTP(timeout, forkVersion, headers, u, fallbackAddresses) + cl, err := eth2wrap.NewMultiHTTP(ctx, timeout, forkVersion, headers, u, fallbackAddresses) if err != nil { return nil, err } diff --git a/core/scheduler/scheduler_test.go b/core/scheduler/scheduler_test.go index c13fa3b12b..57249921b0 100644 --- a/core/scheduler/scheduler_test.go +++ b/core/scheduler/scheduler_test.go @@ -55,7 +55,7 @@ func TestIntegration(t *testing.T) { t.Fatal("BEACON_URL env var not set") } - eth2Cl, err := eth2wrap.NewMultiHTTP(time.Second*2, [4]byte{}, map[string]string{}, []string{beaconURL}, []string{}) + eth2Cl, err := eth2wrap.NewMultiHTTP(t.Context(), time.Second*2, [4]byte{}, map[string]string{}, []string{beaconURL}, []string{}) require.NoError(t, err) // Builder registrations for mainnet validators From 59d893a8490fd2f74e9c7e13ebe98e883a2acd46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:50:04 +0200 Subject: [PATCH 21/29] build(deps): Bump the go-dependencies group across 1 directory with 7 updates (#4778) Bumps the go-dependencies group with 7 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/ethereum/go-ethereum](https://github.com/ethereum/go-ethereum) | `1.17.7` | `1.17.8` | | [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) | `1.24.1` | `1.25.0` | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.57.0` | `0.58.0` | | [golang.org/x/net](https://github.com/golang/net) | `0.60.0` | `0.61.0` | | [golang.org/x/term](https://github.com/golang/term) | `0.46.0` | `0.47.0` | | [golang.org/x/text](https://github.com/golang/text) | `0.42.0` | `0.43.0` | | [golang.org/x/tools](https://github.com/golang/tools) | `0.51.0` | `0.52.0` | Updates `github.com/ethereum/go-ethereum` from 1.17.7 to 1.17.8 - [Release notes](https://github.com/ethereum/go-ethereum/releases) - [Commits](https://github.com/ethereum/go-ethereum/compare/v1.17.7...v1.17.8) Updates `github.com/prometheus/client_golang` from 1.24.1 to 1.25.0 - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/v1.25.0/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.24.1...v1.25.0) Updates `golang.org/x/crypto` from 0.57.0 to 0.58.0 - [Commits](https://github.com/golang/crypto/compare/v0.57.0...v0.58.0) Updates `golang.org/x/net` from 0.60.0 to 0.61.0 - [Commits](https://github.com/golang/net/compare/v0.60.0...v0.61.0) Updates `golang.org/x/term` from 0.46.0 to 0.47.0 - [Commits](https://github.com/golang/term/compare/v0.46.0...v0.47.0) Updates `golang.org/x/text` from 0.42.0 to 0.43.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.42.0...v0.43.0) Updates `golang.org/x/tools` from 0.51.0 to 0.52.0 - [Release notes](https://github.com/golang/tools/releases) - [Commits](https://github.com/golang/tools/compare/v0.51.0...v0.52.0) --- updated-dependencies: - dependency-name: github.com/ethereum/go-ethereum dependency-version: 1.17.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: github.com/prometheus/client_golang dependency-version: 1.25.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/crypto dependency-version: 0.58.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/net dependency-version: 0.61.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/term dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/text dependency-version: 0.43.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/tools dependency-version: 0.52.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 6704e6ab0ae509f8c01ee6b0b950df7b67f57e05) --- go.mod | 32 ++++++++++++++--------------- go.sum | 64 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/go.mod b/go.mod index c3808588d5..f8fe7dcf86 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 github.com/drand/kyber v1.3.3 github.com/drand/kyber-bls12381 v0.3.5 - github.com/ethereum/go-ethereum v1.17.7 + github.com/ethereum/go-ethereum v1.17.8 github.com/ferranbt/fastssz v1.0.0 github.com/fsnotify/fsnotify v1.10.1 github.com/golang/snappy v1.0.1-0.20260716114414-9ae09f520e93 @@ -27,7 +27,7 @@ require ( github.com/libp2p/go-libp2p v0.48.0 github.com/libp2p/go-msgio v0.3.0 github.com/multiformats/go-multiaddr v0.16.1 - github.com/prometheus/client_golang v1.24.1 + github.com/prometheus/client_golang v1.25.0 github.com/prometheus/client_model v0.6.3 github.com/protolambda/eth2-shuffle v1.1.0 github.com/r3labs/sse/v2 v2.10.0 @@ -47,13 +47,13 @@ require ( go.uber.org/automaxprocs v1.6.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.28.0 - golang.org/x/crypto v0.57.0 - golang.org/x/net v0.60.0 + golang.org/x/crypto v0.58.0 + golang.org/x/net v0.61.0 golang.org/x/sync v0.24.0 - golang.org/x/term v0.46.0 - golang.org/x/text v0.42.0 + golang.org/x/term v0.47.0 + golang.org/x/text v0.43.0 golang.org/x/time v0.16.0 - golang.org/x/tools v0.51.0 + golang.org/x/tools v0.52.0 google.golang.org/protobuf v1.36.12 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -162,7 +162,7 @@ require ( github.com/jdx/go-netrc v1.0.0 // indirect github.com/jinzhu/copier v0.4.0 // indirect github.com/kilic/bls12-381 v0.1.0 // indirect - github.com/klauspost/compress v1.19.1 // indirect + github.com/klauspost/compress v1.20.1 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/koron/go-ssdp v0.0.6 // indirect @@ -210,7 +210,7 @@ require ( github.com/peterh/liner v1.2.2 // indirect github.com/petermattis/goid v0.0.0-20260716134002-a9b348f0a2b9 // indirect github.com/pion/datachannel v1.5.10 // indirect - github.com/pion/dtls/v3 v3.1.4 // indirect + github.com/pion/dtls/v3 v3.1.5 // indirect github.com/pion/ice/v4 v4.0.10 // indirect github.com/pion/interceptor v0.1.40 // indirect github.com/pion/logging v0.2.4 // indirect @@ -221,16 +221,16 @@ require ( github.com/pion/sctp v1.8.39 // indirect github.com/pion/sdp/v3 v3.0.18 // indirect github.com/pion/srtp/v3 v3.0.6 // indirect - github.com/pion/stun/v3 v3.1.5 // indirect + github.com/pion/stun/v3 v3.1.7 // indirect github.com/pion/transport/v3 v3.0.7 // indirect - github.com/pion/transport/v4 v4.0.2 // indirect + github.com/pion/transport/v4 v4.1.0 // indirect github.com/pion/turn/v4 v4.0.2 // indirect github.com/pion/webrtc/v4 v4.1.2 // indirect github.com/pk910/dynamic-ssz v1.3.2 // indirect github.com/pk910/hashtree-bindings v0.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/common v0.70.1 // indirect - github.com/prometheus/procfs v0.21.1 // indirect + github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/procfs v0.22.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.60.0 // indirect github.com/quic-go/webtransport-go v0.11.1 // indirect @@ -274,9 +274,9 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect - golang.org/x/mod v0.41.0 // indirect - golang.org/x/sys v0.48.0 // indirect - golang.org/x/telemetry v0.0.0-20260924152758-ed294f943157 // indirect + golang.org/x/mod v0.42.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/telemetry v0.0.0-20261009151841-97e8ea77aa2a // indirect golang.org/x/vuln v1.1.4 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect diff --git a/go.sum b/go.sum index bb5c7f9821..bb9d1a867b 100644 --- a/go.sum +++ b/go.sum @@ -171,8 +171,8 @@ github.com/ethereum/c-kzg-4844/v2 v2.1.8 h1:oQ48q/TMe2SKU8qBE3N7e4/HlG3EpJftom6E github.com/ethereum/c-kzg-4844/v2 v2.1.8/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= -github.com/ethereum/go-ethereum v1.17.7 h1:jhoGxw/5aYPYUwEIfzfog0RcsiJuLA6SSqsHdhkx1tA= -github.com/ethereum/go-ethereum v1.17.7/go.mod h1:nl9wZjMuIjAottU6bq82UihXPbyY0jHHwkYXhnYhmU4= +github.com/ethereum/go-ethereum v1.17.8 h1:iKuXrT38t1PaU6aBw6MjBJhGoCl/+Ir0vf5aqQhYT38= +github.com/ethereum/go-ethereum v1.17.8/go.mod h1:Iui379BD7ryQSFeIDn3sh9Od2I1uheqJD2/6N0m4xm0= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8= @@ -310,8 +310,8 @@ github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4 github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= -github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= @@ -448,8 +448,8 @@ github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= -github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= -github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc= +github.com/pion/dtls/v3 v3.1.5 h1:9xJtVsHwMYeSjPp5Hh1FTis4DchnQWtnOa5o+6ygqfc= +github.com/pion/dtls/v3 v3.1.5/go.mod h1:gz1K4jg6c+fq86oQMH4pilpCEOEPwmEr2jY+VcF/mkU= github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= @@ -470,12 +470,12 @@ github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= -github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8= -github.com/pion/stun/v3 v3.1.5/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= +github.com/pion/stun/v3 v3.1.7 h1:uRXMTlGLf89WgItGNyZ6aR5jMTX0NBbybXADpQCzn+E= +github.com/pion/stun/v3 v3.1.7/go.mod h1:Nq77RW4aRrSNrltf2ksUJLjxWeipj4lnlgdsYIxC8g8= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= -github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= +github.com/pion/transport/v4 v4.1.0 h1:8S+nF2reM2cJuqC6g78OVy2BBgmbdns+acx3jA97BvQ= +github.com/pion/transport/v4 v4.1.0/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= @@ -492,14 +492,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= -github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= +github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKqbYtNOXuiK0= +github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= -github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= -github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= -github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= -github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= +github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= +github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= +github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4= github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9/go.mod h1:SKZx6stCn03JN3BOWTwvVIO2ajMkb/zQdTceXYhKw/4= github.com/protolambda/eth2-shuffle v1.1.0 h1:gixIBI84IeugTwwHXm8vej1bSSEhueBCSryA4lAKRLU= @@ -657,14 +657,14 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= +golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g= golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= -golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -675,8 +675,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= -golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -700,26 +700,26 @@ golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/telemetry v0.0.0-20260924152758-ed294f943157 h1:DftxASHKl6JolFPqoN+2WrFO53JvkHwRXvFqf8gsivA= -golang.org/x/telemetry v0.0.0-20260924152758-ed294f943157/go.mod h1:i+ivNqjDnTF3WTElsdk5g9V5DTSBYgdNo7xTU9SDwYA= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/telemetry v0.0.0-20261009151841-97e8ea77aa2a h1:wnveMBKHwx8/nIb8WYT5P9vtefdXOvIWxzycCkc5n6Q= +golang.org/x/telemetry v0.0.0-20261009151841-97e8ea77aa2a/go.mod h1:i+ivNqjDnTF3WTElsdk5g9V5DTSBYgdNo7xTU9SDwYA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= -golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= -golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= +golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= From 87cdbc54820ed2b8cef31652a84de2bd60e7096c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:06:55 +0200 Subject: [PATCH 22/29] build(deps): Bump the go-dependencies group across 1 directory with 2 updates (#4790) * build(deps): Bump the go-dependencies group across 1 directory with 2 updates Bumps the go-dependencies group with 2 updates in the / directory: [github.com/bufbuild/buf](https://github.com/bufbuild/buf) and [github.com/vektra/mockery/v2](https://github.com/vektra/mockery). Updates `github.com/bufbuild/buf` from 1.72.0 to 1.74.0 - [Release notes](https://github.com/bufbuild/buf/releases) - [Changelog](https://github.com/bufbuild/buf/blob/main/CHANGELOG.md) - [Commits](https://github.com/bufbuild/buf/compare/v1.72.0...v1.74.0) Updates `github.com/vektra/mockery/v2` from 2.53.7 to 2.53.8 - [Release notes](https://github.com/vektra/mockery/releases) - [Commits](https://github.com/vektra/mockery/compare/v2.53.7...v2.53.8) --- updated-dependencies: - dependency-name: github.com/bufbuild/buf dependency-version: 1.74.0 dependency-type: indirect update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: github.com/vektra/mockery/v2 dependency-version: 2.53.8 dependency-type: indirect update-type: version-update:semver-patch dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] * *: bump go-libp2p to v0.50.0 for quic-go v0.63 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kalo <24719519+KaloyanTanev@users.noreply.github.com> (cherry picked from commit dd58d17107b42e64eed50d007d0bb34c1382425a) # Conflicts: # go.mod # go.sum --- go.mod | 106 +++++++++++++-------------- go.sum | 224 ++++++++++++++++++++++++++++----------------------------- 2 files changed, 165 insertions(+), 165 deletions(-) diff --git a/go.mod b/go.mod index f8fe7dcf86..d9cefca0f9 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/ipfs/go-log/v2 v2.9.2 github.com/jonboulle/clockwork v0.5.0 github.com/jsternberg/zap-logfmt v1.3.0 - github.com/libp2p/go-libp2p v0.48.0 + github.com/libp2p/go-libp2p v0.50.0 github.com/libp2p/go-msgio v0.3.0 github.com/multiformats/go-multiaddr v0.16.1 github.com/prometheus/client_golang v1.25.0 @@ -59,27 +59,28 @@ require ( ) require ( - buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260626152828-968bf0468096.1 // indirect - buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 // indirect - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 // indirect - buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260713175918-10d915f5b43b.1 // indirect - buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260713175918-10d915f5b43b.1 // indirect - buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 // indirect - buf.build/go/app v0.2.1-0.20260626143626-be153867abea // indirect - buf.build/go/bufplugin v0.10.0 // indirect + buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20261002170247-538130002972.2 // indirect + buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.12-20250109164928-1da0de137947.2 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect + buf.build/gen/go/bufbuild/registry/connectrpc/go/v2 v2.0.0-20261009230124-4d217860dade.1 // indirect + buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.12-20261009230124-4d217860dade.2 // indirect + buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.12-20241007202033-cf42259fcbfc.2 // indirect + buf.build/go/app v0.2.1-0.20260928213032-9a091f2dc726 // indirect + buf.build/go/bufplugin v0.11.1 // indirect buf.build/go/bufprivateusage v0.1.0 // indirect buf.build/go/interrupt v1.1.0 // indirect - buf.build/go/protovalidate v1.2.0 // indirect + buf.build/go/protovalidate v1.4.0 // indirect buf.build/go/protoyaml v0.7.0 // indirect - buf.build/go/spdx v0.2.0 // indirect + buf.build/go/spdx v0.3.0 // indirect buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect - cel.dev/expr v0.25.2 // indirect - connectrpc.com/connect v1.20.0 // indirect - connectrpc.com/otelconnect v0.9.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + connectrpc.com/connect/v2 v2.0.1 // indirect + connectrpc.com/otelconnect v0.11.0 // indirect filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 // indirect - filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b // indirect + filippo.io/keygen v1.0.0 // indirect github.com/DataDog/zstd v1.5.7 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/go-winio v0.6.3 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/RaduBerinde/axisds v0.1.0 // indirect github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54 // indirect @@ -89,8 +90,8 @@ require ( github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.26.0 // indirect - github.com/bufbuild/buf v1.72.0 // indirect - github.com/bufbuild/protocompile v0.14.2-0.20260716165721-bb5762d29672 // indirect + github.com/bufbuild/buf v1.74.0 // indirect + github.com/bufbuild/protocompile v0.14.2-0.20261013160345-b79bfe2f85df // indirect github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b // indirect github.com/casbin/govaluate v1.10.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect @@ -117,15 +118,16 @@ require ( github.com/dchest/siphash v1.2.3 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v29.6.1+incompatible // indirect - github.com/docker/docker-credential-helpers v0.9.8 // indirect - github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/cli v29.9.0+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.10 // indirect + github.com/docker/go-connections v0.8.2 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/dunglas/httpsfv v1.1.0 // indirect + github.com/dunglas/httpsfv v1.1.1 // indirect github.com/emicklei/dot v1.8.0 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.8 // indirect github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/felixge/httpsnoop v1.1.0 // indirect + github.com/filecoin-project/go-clock v0.1.0 // indirect github.com/fjl/jsonw v0.1.0 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect @@ -135,11 +137,10 @@ require ( github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/goccy/go-yaml v1.17.0 // indirect - github.com/gofrs/flock v0.13.0 // indirect + github.com/gofrs/flock v0.13.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/google/cel-go v0.29.2 // indirect - github.com/google/go-containerregistry v0.21.7 // indirect + github.com/google/go-containerregistry v0.22.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grafana/pyroscope-go v1.2.7 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect @@ -156,30 +157,29 @@ require ( github.com/influxdata/influxdb-client-go/v2 v2.14.0 // indirect github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c // indirect github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect - github.com/ipfs/go-cid v0.5.0 // indirect + github.com/ipfs/go-cid v0.6.2 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/jdx/go-netrc v1.0.0 // indirect github.com/jinzhu/copier v0.4.0 // indirect github.com/kilic/bls12-381 v0.1.0 // indirect - github.com/klauspost/compress v1.20.1 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/klauspost/pgzip v1.2.6 // indirect - github.com/koron/go-ssdp v0.0.6 // indirect + github.com/klauspost/compress v1.20.3 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/klauspost/pgzip v1.2.7 // indirect + github.com/koron/go-ssdp v0.9.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect - github.com/libp2p/go-flow-metrics v0.2.0 // indirect + github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-netroute v0.4.0 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect - github.com/libp2p/go-yamux/v5 v5.0.1 // indirect + github.com/libp2p/go-yamux/v5 v5.1.0 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect - github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-colorable v0.1.16 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-runewidth v0.0.28 // indirect - github.com/miekg/dns v1.1.66 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882 // indirect @@ -188,18 +188,18 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/pointerstructure v1.2.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/moby/api v1.55.0 // indirect - github.com/moby/moby/client v0.5.0 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect + github.com/moby/moby/api v1.56.1 // indirect + github.com/moby/moby/client v0.6.2 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect + github.com/multiformats/go-multiaddr-dns v0.6.0 // indirect github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect - github.com/multiformats/go-multibase v0.2.0 // indirect - github.com/multiformats/go-multicodec v0.9.1 // indirect + github.com/multiformats/go-multibase v0.3.0 // indirect + github.com/multiformats/go-multicodec v0.10.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-multistream v0.6.1 // indirect - github.com/multiformats/go-varint v0.0.7 // indirect + github.com/multiformats/go-varint v0.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oapi-codegen/runtime v1.6.0 // indirect github.com/onsi/gomega v1.36.2 // indirect @@ -208,7 +208,7 @@ require ( github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/peterh/liner v1.2.2 // indirect - github.com/petermattis/goid v0.0.0-20260716134002-a9b348f0a2b9 // indirect + github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v3 v3.1.5 // indirect github.com/pion/ice/v4 v4.0.10 // indirect @@ -232,8 +232,8 @@ require ( github.com/prometheus/common v0.72.0 // indirect github.com/prometheus/procfs v0.22.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.60.0 // indirect - github.com/quic-go/webtransport-go v0.11.1 // indirect + github.com/quic-go/quic-go v0.63.0 // indirect + github.com/quic-go/webtransport-go v0.13.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.11.1 // indirect @@ -242,7 +242,7 @@ require ( github.com/segmentio/asm v1.2.1 // indirect github.com/segmentio/encoding v0.5.4 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/sirupsen/logrus v1.9.4 // indirect + github.com/sirupsen/logrus v1.10.2 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect @@ -251,11 +251,11 @@ require ( github.com/supranational/blst v0.3.16 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/tetratelabs/wazero v1.12.0 // indirect - github.com/tidwall/btree v1.8.1 // indirect + github.com/tidwall/btree v1.8.2 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect github.com/urfave/cli/v2 v2.27.6 // indirect - github.com/vektra/mockery/v2 v2.53.6 // indirect + github.com/vektra/mockery/v2 v2.53.8 // indirect github.com/wlynxg/anet v0.0.5 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect @@ -264,29 +264,29 @@ require ( go.lsp.dev/protocol v0.12.0 // indirect go.lsp.dev/uri v0.3.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 // indirect go.opentelemetry.io/otel/log v1.47.0 // indirect go.opentelemetry.io/otel/metric v1.47.0 // indirect go.opentelemetry.io/proto/otlp v1.11.0 // indirect go.uber.org/dig v1.19.0 // indirect go.uber.org/fx v1.24.0 // indirect - go.uber.org/mock v0.5.2 // indirect + go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect + golang.org/x/exp v0.0.0-20261011100332-faecfb24eb49 // indirect golang.org/x/mod v0.42.0 // indirect golang.org/x/sys v0.49.0 // indirect golang.org/x/telemetry v0.0.0-20261009151841-97e8ea77aa2a // indirect golang.org/x/vuln v1.1.4 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8 // indirect google.golang.org/grpc v1.83.2 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect mvdan.cc/xurls/v2 v2.6.0 // indirect - pluginrpc.com/pluginrpc v0.5.0 // indirect + pluginrpc.com/pluginrpc v0.6.0 // indirect ) // We're replacing kryptology with our own fork, which fixes dependencies' security vulnerabilities. diff --git a/go.sum b/go.sum index bb9d1a867b..7bfd573912 100644 --- a/go.sum +++ b/go.sum @@ -1,47 +1,49 @@ -buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260626152828-968bf0468096.1 h1:bHi5/cwz7IPRhFcrRA4iytTjDI3WwFkv1m5K7y+YOTc= -buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20260626152828-968bf0468096.1/go.mod h1:1Znr6gmYBhbxWUPRrrVnSLXQsz8bvFVw1HHJq2bI3VQ= -buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 h1:HwzzCRS4ZrEm1++rzSDxHnO0DOjiT1b8I/24e8a4exY= -buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1/go.mod h1:8PRKXhgNes29Tjrnv8KdZzg3I1QceOkzibW1QK7EXv0= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 h1:fXh8CsdNpjRr8R5vFdqtIxPt/Lno2IIJlYOdZBIZn0w= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= -buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260713175918-10d915f5b43b.1 h1:c4w6fQ6wAhZLgVI+HZozlWX9I2aVPN34vrl/8cuovVc= -buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260713175918-10d915f5b43b.1/go.mod h1:mbcuFKpnCQsF8H2fBs1lN1dQVAiASPQKwFgTkXqYscI= -buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260713175918-10d915f5b43b.1 h1:qJUlFKLFdb3OFCpmA+r5AQbysgBD7WLRapE9lOs08r8= -buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260713175918-10d915f5b43b.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40= -buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 h1:iGPvEJltOXUMANWf0zajcRcbiOXLD90ZwPUFvbcuv6Q= -buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1/go.mod h1:nWVKKRA29zdt4uvkjka3i/y4mkrswyWwiu0TbdX0zts= -buf.build/go/app v0.2.1-0.20260626143626-be153867abea h1:krHaIyUJSTnurb0fbYQtsXkZWiWQ/ydOgfm61TPSODc= -buf.build/go/app v0.2.1-0.20260626143626-be153867abea/go.mod h1:V32mBaPWsfq6REAeZvvs/rQl7ZCl9Dn7eW1BBrmH0GQ= -buf.build/go/bufplugin v0.10.0 h1:vZBX0mq9as5UIBug8U+/DkGRaHNlM/HVOw59O8fvOIU= -buf.build/go/bufplugin v0.10.0/go.mod h1:ax7obVurKDH1I2nR4pFTS+TE6K3kZhTmwDCN2YgdV8I= +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20261002170247-538130002972.2 h1:nNNsSGs3l9F8q4NfHaIa8RdGfucw957dlFuNf9Z3H/w= +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20261002170247-538130002972.2/go.mod h1:Owcuj2phVyvThVYCD74iSV8ki0dAXw/uxkdcdZScdCc= +buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.12-20250109164928-1da0de137947.2 h1:tE4A2RVukQOSd4K+6ndK+8s65FbjiBt2k5CiddXxCCo= +buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.12-20250109164928-1da0de137947.2/go.mod h1:Gjxz0o7qPHokNsmlX8D+/aKd29NRLRr9CCUkPeMAzJ0= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/gen/go/bufbuild/registry/connectrpc/go/v2 v2.0.0-20261009230124-4d217860dade.1 h1:9utgoyULVklOqVzOG+mnd+ZQcOiWPKVP5KmKPXZaVYw= +buf.build/gen/go/bufbuild/registry/connectrpc/go/v2 v2.0.0-20261009230124-4d217860dade.1/go.mod h1:U3WcHUGfS/L67G7R0PmcaHA7MfviJJqDqhHXiCWeOEY= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.12-20261009230124-4d217860dade.2 h1:Bqu3+Hwxoh8gvRvGMtkz/Kbq4U+cOvyk37jLzE4PkC0= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.12-20261009230124-4d217860dade.2/go.mod h1:v7IaNcBKy5G3G8gpG2m92P0R3HK2qtMGclWzf+1Aut4= +buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.12-20241007202033-cf42259fcbfc.2 h1:9xw8g8Q6zHLC7CO/jtpl2UK0kQticxIG+mI6N86f2/M= +buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.12-20241007202033-cf42259fcbfc.2/go.mod h1:+0bw+k8FY5MU3tojFh33HeA5xMfGWjAUQpr+maW5Z3o= +buf.build/go/app v0.2.1-0.20260928213032-9a091f2dc726 h1:Xx8K9w2tN9TfQSq1g6g4vKuBbG3ZlOR2bb9A+PbgBt4= +buf.build/go/app v0.2.1-0.20260928213032-9a091f2dc726/go.mod h1:KuUkGoXxZnxNVGN/AB7s4UCUG2/1xtk8NjxqdSCSSyM= +buf.build/go/bufplugin v0.11.1 h1:fegT6xiiGVwyNiLlxBz07ochKTX/DAtIKe/oPdrqrCs= +buf.build/go/bufplugin v0.11.1/go.mod h1:0xoAhb/CPqXwS/FUup9vwIRBYxDcXYXeOhYXHca5uuc= buf.build/go/bufprivateusage v0.1.0 h1:SzCoCcmzS3zyXHEXHeSQhGI7OTkgtljoknLzsUz9Gg4= buf.build/go/bufprivateusage v0.1.0/go.mod h1:GlCCJ3VVF7EqqU0CoRmo1FzAwwaKymEWSr+ty69xU5w= buf.build/go/interrupt v1.1.0 h1:olBuhgv9Sav4/9pkSLoxgiOsZDgM5VhRhvRpn3DL0lE= buf.build/go/interrupt v1.1.0/go.mod h1:ql56nXPG1oHlvZa6efNC7SKAQ/tUjS6z0mhJl0gyeRM= -buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0= -buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= buf.build/go/protoyaml v0.7.0 h1:z4oVoFicbpPefhT7WAykxUdfp0yEQlhMQ2mCZOY5V38= buf.build/go/protoyaml v0.7.0/go.mod h1:+a0cavd0uMvirb87xdu2ZMMmjlIQoiH/N2Ich5MGSQ0= -buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw= -buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8= +buf.build/go/spdx v0.3.0 h1:vVxy1W0x1n3sqhZmJ7VZ/UtzNwK3xm0QowGGtwric4Y= +buf.build/go/spdx v0.3.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8= buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 h1:njYKSWoLiq2i5O7y2bPPU2Yzp7iAU0Wk9KJ2OoAhNiU= buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5/go.mod h1:DQmodNT9EHX94WzUaWiZK+/4EaFa/xZTc1gzfCxZVXU= -cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= -cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= -connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= -connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= -connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc= -connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs= -connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA= -connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +connectrpc.com/connect/v2 v2.0.1 h1:WTAycxe87UqxSM5oxS30d1dalZ+C5xz8Ec8O6zVYkHQ= +connectrpc.com/connect/v2 v2.0.1/go.mod h1:N32OLrstcPUzkykFynnefFro3+xzSdOrZlK2jHmS/xo= +connectrpc.com/grpcreflect/v2 v2.0.0 h1:Ah7U5kSd/kslH5KFNS8jh/e3u6UYVoX7lwkjcIfSq6A= +connectrpc.com/grpcreflect/v2 v2.0.0/go.mod h1:V7Hj+ibxuZH3667Ng2zKKvxJQJIri20iG50vzG7i8GM= +connectrpc.com/otelconnect v0.11.0 h1:n1HjvoZLzsGJ5nb2PGnFH2msXPQLf4j3EWzLDylhdWQ= +connectrpc.com/otelconnect v0.11.0/go.mod h1:xE0w5pyAk519Nub9zP63x9oOIBwcKofv5IYKQAhzQF4= filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= -filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b h1:REI1FbdW71yO56Are4XAxD+OS/e+BQsB3gE4mZRQEXY= -filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= +filippo.io/keygen v1.0.0 h1:u0/Fhxlgz3uPv+XxhfgTq3BJt5VesIPM5ue/OuG7qjQ= +filippo.io/keygen v1.0.0/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/go-winio v0.6.3 h1:M/52tsoyTfwaxajLnXZm7PqkbMGRH8iSWP+8fcg9MQ4= +github.com/Microsoft/go-winio v0.6.3/go.mod h1:ma0QpxizD4fyyzJ1C7MIblymTD2Nxz5Mho/3CepwIZg= github.com/ObolNetwork/go-eth2-client v0.28.1-obol h1:Xith6vfi0R7m/zl+vy0EWJbtFwUMTD0dmGnABi9rzkY= github.com/ObolNetwork/go-eth2-client v0.28.1-obol/go.mod h1:1MBvz1DVe6jZduHJDJuJ/1S4LzYmDmvx+r5alo4pyrM= github.com/ObolNetwork/kryptology v0.1.0 h1:AhoG4My70+xMhEJSpVaJay/t+T/vIUNHQYLjsDJHulI= @@ -79,10 +81,10 @@ github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6 github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= -github.com/bufbuild/buf v1.72.0 h1:VMmGFtCLrxyS2wkpghExmhhiqJDdmc8DcwAvsGJGJ94= -github.com/bufbuild/buf v1.72.0/go.mod h1:bhtIlPDo3q/PDw4yaTdx2+jxkc33Aq+ygFIi6Diz2yU= -github.com/bufbuild/protocompile v0.14.2-0.20260716165721-bb5762d29672 h1:6xykiXQPoF/hhOjAhwiHQ0kgcAvrDZw0Tjl1VQkpu5c= -github.com/bufbuild/protocompile v0.14.2-0.20260716165721-bb5762d29672/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E= +github.com/bufbuild/buf v1.74.0 h1:zk0IvEFZpUBWKUh9Hyt6vKtIu7uoSjgU8le9sysJYhI= +github.com/bufbuild/buf v1.74.0/go.mod h1:ovhzeVGqUqy4vuumyWvsUbAq0bqdJHptYKRktjiIqJA= +github.com/bufbuild/protocompile v0.14.2-0.20261013160345-b79bfe2f85df h1:DRUi+xCmK/geLBx7WYsdWRVsg7OmyZPytgEA9koxI0c= +github.com/bufbuild/protocompile v0.14.2-0.20261013160345-b79bfe2f85df/go.mod h1:bX3ObJfML+aki7PJevkWLSlOfEZ8EnNsfCz0hiYkhiI= github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b h1:b7wvo9ZhjLzCp7tGbOUMvgtYTnd33zGSAmMxcdxMnhQ= github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ= github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw= @@ -151,20 +153,20 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3h github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v29.6.1+incompatible h1:oO7F4nn3Ovr/5TlfTUWFbMwBSS/B7Xs6Epv26gBrUP8= -github.com/docker/cli v29.6.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q= -github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= -github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= -github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/cli v29.9.0+incompatible h1:Cw9x50ioSGgGwXJ4eO+hIBry/w3LLbyF8GPAEKz1fnE= +github.com/docker/cli v29.9.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker-credential-helpers v0.9.10 h1:LCWwv7UmS7fQVmdKDWeMuureOeY5kkbC5kZfkzxkS6k= +github.com/docker/docker-credential-helpers v0.9.10/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= +github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc= +github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/drand/kyber v1.3.3 h1:iUIg5JWlAr8Qdqkl36J+PbHK0BMdQlLm+P/wV34EDX8= github.com/drand/kyber v1.3.3/go.mod h1:ptX3XpLo5LH4tX2y297cXSwFOIuZii5O7MkDgG0I7mA= github.com/drand/kyber-bls12381 v0.3.5 h1:k6il25UDe3jhC0vEbrAV5y6SIZNLmleMmcnPFSPBIQ4= github.com/drand/kyber-bls12381 v0.3.5/go.mod h1:fU16hOyVzjOob1G1A8TRRVNCSd2Z0VNv43XlXo2gtbU= -github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= -github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= +github.com/dunglas/httpsfv v1.1.1 h1:HoSs101zIE9I23DlqlmljJ/OIi7ILwrH347pXhRZdxI= +github.com/dunglas/httpsfv v1.1.1/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/emicklei/dot v1.8.0 h1:HnD60yAKFAevNeT+TPYr9pb8VB9bqdeSo0nzwIW6IOI= github.com/emicklei/dot v1.8.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/ethereum/c-kzg-4844/v2 v2.1.8 h1:oQ48q/TMe2SKU8qBE3N7e4/HlG3EpJftom6EsPQgJ58= @@ -177,6 +179,8 @@ github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeO github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8= github.com/ferranbt/fastssz v1.0.0/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= +github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= github.com/fjl/jsonw v0.1.0 h1:V3MyR79fjLpn/+bMgvegdGUIhoJOzjmqWcKDgcOmY1I= github.com/fjl/jsonw v0.1.0/go.mod h1:2KMLevM6FXEJnfhtk7naXu9vZdVfOma1GlnGdPRlumU= github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= @@ -208,8 +212,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-yaml v1.17.0 h1:JJhayi67p5LeTLh9UJYFhayPIOGDZjAqQNoEzHhYvik= github.com/goccy/go-yaml v1.17.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= -github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo= +github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= @@ -226,8 +230,6 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.1-0.20260716114414-9ae09f520e93 h1:GpQQr4L8jsBtJSURCDqQboOdgpVMU6vR9REjc8nR4Qc= github.com/golang/snappy v1.0.1-0.20260716114414-9ae09f520e93/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= -github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU= github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -236,8 +238,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= -github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= +github.com/google/go-containerregistry v0.22.1 h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84= +github.com/google/go-containerregistry v0.22.1/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= @@ -287,8 +289,8 @@ github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSH github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU= github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= -github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= +github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= +github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= github.com/ipfs/go-log/v2 v2.9.2 h1:O/5BB0elpkRILvT24rCJ5976wWd7u0nJ436T3rdYdc4= github.com/ipfs/go-log/v2 v2.9.2/go.mod h1:RziRwwXWhndlk8L75RnEe0zeAYaq2heKtEMc3jqUov0= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= @@ -297,8 +299,8 @@ github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABo github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ= github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8= -github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88= -github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM= +github.com/jhump/protoreflect/v2 v2.0.0 h1:dBg0utH2witLqXfczkVVrHFrFQc8SN1rvaKkOR5ALDg= +github.com/jhump/protoreflect/v2 v2.0.0/go.mod h1:Sz8TB0WtuTKOF4eYj2iZovXSSU3tqZ5Ii8Oeg9k9vKY= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= @@ -310,14 +312,14 @@ github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4 github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= -github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= -github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= -github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= +github.com/klauspost/compress v1.20.3 h1:zWNoFqq/lSuYQRim3dUXlXeA0IqwuZGicdG5ZTfY5j4= +github.com/klauspost/compress v1.20.3/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/klauspost/pgzip v1.2.7 h1:02QB3Ttao6zOWDnSsv3bIvjN24bX0eGjWniQ8vuBfkA= +github.com/klauspost/pgzip v1.2.7/go.mod h1:g7E6NrOKHOzah4QwK6Ue1tNCJs8IDiNOfjiXTr85U2E= +github.com/koron/go-ssdp v0.9.1 h1:zvxbAAuJftJIZ8Jh8mda+LI7V92hYZf/sKprmOxpxwA= +github.com/koron/go-ssdp v0.9.1/go.mod h1:C43c047jWkDaeg9YuZlSh/QGqOieuWV6dbhWi/jcaLk= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -331,10 +333,10 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= -github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= -github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= -github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo= -github.com/libp2p/go-libp2p v0.48.0/go.mod h1:Q1fBZNdmC2Hf82husCTfkKJVfHm2we5zk+NWmOGEmWk= +github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= +github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= +github.com/libp2p/go-libp2p v0.50.0 h1:A0tBP6mr6GV7l5ip2Q04a/IxPqVYYaCiif5s8JuXkN4= +github.com/libp2p/go-libp2p v0.50.0/go.mod h1:RjqB+dxCWNZ33yw0yeK5Pu157oNxKuaTZY6vLed4t8w= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= @@ -345,21 +347,19 @@ github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= -github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= -github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= -github.com/marcopolo/simnet v0.0.4 h1:50Kx4hS9kFGSRIbrt9xUS3NJX33EyPqHVmpXvaKLqrY= -github.com/marcopolo/simnet v0.0.4/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= +github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE= +github.com/libp2p/go-yamux/v5 v5.1.0/go.mod h1:tgIQ07ObtRR/I0IWsFOyQIL9/dR5UXgc2s8xKmNZv1o= +github.com/marcopolo/simnet v0.0.7 h1:DpH8BMGsF9+1w13L8rvCaAhb6nYJdY+dIXncDrssvUs= +github.com/marcopolo/simnet v0.0.7/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= -github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= -github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.16 h1:+2YjeAe7xP1OQbZW1jERPWWCDLlNqFNCvjiqGiAt84A= +github.com/mattn/go-colorable v0.1.16/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU= github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= -github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= -github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= @@ -381,13 +381,13 @@ github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQh github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= -github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= -github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= -github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= +github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c= +github.com/moby/moby/api v1.56.1/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= +github.com/moby/moby/client v0.6.2 h1:ekSPlMccrZGcw6Scb8T9D39qE0mB+XupU/yS0zNw9N4= +github.com/moby/moby/client v0.6.2/go.mod h1:DGcnO7qmPJrqobx/ggQgQ/AUk+2wgVdQv7uxB0/mEsc= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= @@ -395,21 +395,21 @@ github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= -github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= -github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= +github.com/multiformats/go-multiaddr-dns v0.6.0 h1:yKIW08WJHSPJ8bDAT2O/5fypCaUu9Bjl8r/1eJ4XAW8= +github.com/multiformats/go-multiaddr-dns v0.6.0/go.mod h1:dwIQwdORZfnNQCeS7xLXyn+7626oRmMsVP30Uronhf0= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= -github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= -github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= -github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo= -github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo= +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= -github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= -github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= @@ -442,8 +442,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= -github.com/petermattis/goid v0.0.0-20260716134002-a9b348f0a2b9 h1:UyKlK0Ke63afxhHrgJAk8KlCt+kP9KYBRUsWG6lK2WM= -github.com/petermattis/goid v0.0.0-20260716134002-a9b348f0a2b9/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b h1:OzNsuVdSWGwXvWKTtChx9ve89k4cFJ6NxYM4Aw4/f+E= +github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= @@ -510,10 +510,10 @@ github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4 github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= -github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= -github.com/quic-go/webtransport-go v0.11.1 h1:rrFQMO+7/52ZDJ04fsrjIaWqn6q1z1MYo9iVFq6JtbA= -github.com/quic-go/webtransport-go v0.11.1/go.mod h1:SHgEzUFVyj+9WUSuGB1P6Zd351Pww2leWV3SwlTovkA= +github.com/quic-go/quic-go v0.63.0 h1:LIFGHI4PFUhhw2dDD1ARHdCff143ffMHwZtbnbuJ78A= +github.com/quic-go/quic-go v0.63.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w= +github.com/quic-go/webtransport-go v0.13.0 h1:RJLrTUHlTj8jJaQlQJUy0z0Mf7u1fVM0I6L1b9pe2M0= +github.com/quic-go/webtransport-go v0.13.0/go.mod h1:K83X9YHbAqgSLO6ikS6BXCMdWOvqh9JTHALulvb2JVk= github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -539,8 +539,8 @@ github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKl github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/showwin/speedtest-go v1.8.3 h1:x988hyZFsa5V1EEwox84niAji632uxBTk8Clsc1itF8= github.com/showwin/speedtest-go v1.8.3/go.mod h1:N4bn2lgNNWmfJyC1AIHD4T5wYhU+9D21OfacVhJsxiU= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= +github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -572,16 +572,16 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= -github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA= -github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A= +github.com/tidwall/btree v1.8.2 h1:mTY5iM4UTh2e80TG7RHDQQmnAY3xGNQA/DbU0olkhkA= +github.com/tidwall/btree v1.8.2/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= -github.com/vektra/mockery/v2 v2.53.6 h1:qfUB6saauu652ZlMF/mEdlj7B/A0fw2XR0XBACBrf7Y= -github.com/vektra/mockery/v2 v2.53.6/go.mod h1:fjxC+mskIZqf67+z34pHxRRyyZnPnWNA36Cirf01Pkg= +github.com/vektra/mockery/v2 v2.53.8 h1:wSyeOK1BfE+wwls7VKy2uKQoVVA8mPVBJvnm2RfZLic= +github.com/vektra/mockery/v2 v2.53.8/go.mod h1:MUc05xv+h/jtny5E3RrYgVwGmCuD+P2T/CPzlzsjyV0= github.com/wealdtech/go-eth2-types/v2 v2.8.2 h1:b5aXlNBLKgjAg/Fft9VvGlqAUCQMP5LzYhlHRrr4yPg= github.com/wealdtech/go-eth2-types/v2 v2.8.2/go.mod h1:IAz9Lz1NVTaHabQa+4zjk2QDKMv8LVYo0n46M9o/TXw= github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1 h1:9j7bpwjT9wmwBb54ZkBhTm1uNIlFFcCJXefd/YskZPw= @@ -608,8 +608,8 @@ go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo= go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 h1:LxwW/9ctSCv+QkE/cLR7M91ZIkXNMqJtEMi1vCw9U8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0/go.mod h1:tOsftB4SslBwwErVEPaenU2RpThXWPIU8DoJHEC4dyw= go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI= go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU= @@ -640,8 +640,8 @@ go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= @@ -659,8 +659,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= -golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g= -golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= +golang.org/x/exp v0.0.0-20261011100332-faecfb24eb49 h1:pQo5TwmxKJvI8k39uYS2Gqkk8OXWxX/TMSis2Twp1AQ= +golang.org/x/exp v0.0.0-20261011100332-faecfb24eb49/go.mod h1:DBoxwWhjaW8DJJw/YeM4zpeOKMC1FMXSblsYy+1mFSw= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= @@ -734,10 +734,10 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE= -google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8 h1:cFbkNigDInw+wVTXc+CyhUsoHGJCw0BR6mlNoRVfre0= +google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8/go.mod h1:QK6p7IgD7IZEZJBhhSUCib5X2vMOzlrGCGUw2XyVDh0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8 h1:0SXBqli5dnuCKMjI/X3FM226zEaEa0GUcSCJtN1vL2o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -776,5 +776,5 @@ mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= -pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo= -pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o= +pluginrpc.com/pluginrpc v0.6.0 h1:Dx5r3y9Nbbt02NSm+bKHvs+WgXM34lalD5grkjd8sno= +pluginrpc.com/pluginrpc v0.6.0/go.mod h1:9FPMdIy8nmkALE5eSe2ipE42G83WpZnA2+I7M7ADijY= From 5a85ca0ea872ab94bf66bab33f0bb54f39e45c06 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:37:44 +0100 Subject: [PATCH 23/29] core/tracker: debug log sync selection proof inconsistency (#4801) (cherry picked from commit 2b534ebb719d168414ca3bf49ae75563c661f6b2) --- core/tracker/tracker.go | 6 +++--- core/tracker/tracker_internal_test.go | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/core/tracker/tracker.go b/core/tracker/tracker.go index b4058e97f9..7b4140dcd5 100644 --- a/core/tracker/tracker.go +++ b/core/tracker/tracker.go @@ -934,9 +934,9 @@ func reportParSigs(ctx context.Context, duty core.Duty, parsigMsgs parsigsByMsg) entriesJSON = json.RawMessage(fmt.Sprintf("%q", err.Error())) } - // DutySyncMessage is handled earlier via reportSyncMessageCohorts. Sync contribution - // inconsistency is expected (per-validator, per-subcommittee) so log at debug. - if duty.Type == core.DutySyncContribution { + // DutySyncMessage is handled earlier via reportSyncMessageCohorts. Sync contribution and + // selection proof inconsistency is expected (per-validator, per-subcommittee) so log at debug. + if duty.Type == core.DutySyncContribution || duty.Type == core.DutyPrepareSyncContribution { log.Debug(ctx, "Inconsistent sync committee partial signed data", z.Any("pubkey", pubkey), z.Any("duty", duty), diff --git a/core/tracker/tracker_internal_test.go b/core/tracker/tracker_internal_test.go index dcc7fbb816..05f68cbd35 100644 --- a/core/tracker/tracker_internal_test.go +++ b/core/tracker/tracker_internal_test.go @@ -18,8 +18,10 @@ import ( eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" promtestutil "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" "github.com/obolnetwork/charon/app/errors" + "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/core" "github.com/obolnetwork/charon/p2p" "github.com/obolnetwork/charon/testutil" @@ -1614,3 +1616,19 @@ func TestReportSyncMessageCohorts(t *testing.T) { require.InDelta(t, before, promtestutil.ToFloat64(cohortRank.WithLabelValues(attLabel, "0", "0")), 0) }) } + +// TestReportParSigsSyncSubcommittees verifies that a validator's sync contributions and selection proofs +// for different subcommittees aren't warned about as inconsistent, since their message roots differ. +func TestReportParSigsSyncSubcommittees(t *testing.T) { + for _, duty := range []core.Duty{core.NewSyncContributionDuty(123), core.NewPrepareSyncContributionDuty(123)} { + t.Run(duty.Type.String(), func(t *testing.T) { + var buf zaptest.Buffer + log.InitConsoleForT(t, &buf) + + reportParSigs(context.Background(), duty, parsigsByMsg{"pk": {rootFrom(0x0a): syncSigs(1, 2), rootFrom(0x0b): syncSigs(3)}}) + + require.Contains(t, buf.String(), "Inconsistent sync committee partial signed data") + require.NotContains(t, buf.String(), "Inconsistent partial signed data") + }) + } +} From 5211622fd81cb29685f22c4a75f850bb473e0631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn=20Kyne?= <4981644+OisinKyne@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:17:48 +0000 Subject: [PATCH 24/29] app/obolapi: send a User-Agent and type 404s on POST (#4808) * app/obolapi: send a User-Agent and type 404s on POST * app/obolapi: make WithUserAgent override the default --------- Co-authored-by: kalo <24719519+KaloyanTanev@users.noreply.github.com> (cherry picked from commit ae46490bfad10d332f5ef7ffaaebd030b362ad47) --- app/obolapi/api.go | 44 +++++++++++-- app/obolapi/api_internal_test.go | 109 ++++++++++++++++++++++++++++++- app/obolapi/deposit.go | 4 +- app/obolapi/exit.go | 6 +- app/obolapi/feerecipient.go | 9 +-- app/obolapi/publish.go | 6 +- app/obolapi/test.go | 2 +- 7 files changed, 161 insertions(+), 19 deletions(-) diff --git a/app/obolapi/api.go b/app/obolapi/api.go index 529612ab7a..e5b055d5c0 100644 --- a/app/obolapi/api.go +++ b/app/obolapi/api.go @@ -13,6 +13,7 @@ import ( "github.com/obolnetwork/charon/app/errors" "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/app/version" "github.com/obolnetwork/charon/app/z" "github.com/obolnetwork/charon/cluster" ) @@ -25,6 +26,11 @@ const ( defaultTimeout = 10 * time.Second ) +// ErrLockNotFound is returned when the Obol API responds 404 Not Found to a POST request. For the +// lock hash scoped endpoints (partial exits, deposits and fee recipients) it means the cluster lock +// isn't published to that Obol API, so the data can't be stored until it is. +var ErrLockNotFound = errors.NewSentinel("cluster lock not published to Obol API") + // New returns a new Client. func New(urlStr string, options ...func(*Client)) (Client, error) { _, err := url.ParseRequestURI(urlStr) // check that urlStr is valid @@ -35,7 +41,8 @@ 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, + baseURL: urlStr, + userAgent: charonUserAgent(), } for _, opt := range options { @@ -49,6 +56,7 @@ func New(urlStr string, options ...func(*Client)) (Client, error) { type Client struct { baseURL string // Base obol-api URL reqTimeout time.Duration // Timeout to use for HTTP requests + userAgent string // User-Agent header sent with every request } // WithTimeout sets the HTTP request timeout for all Client calls to the provided value. @@ -58,6 +66,23 @@ func WithTimeout(timeout time.Duration) func(*Client) { } } +// WithUserAgent sets the User-Agent header of all Client calls, overriding the charon default. +// An empty user agent keeps the default. +func WithUserAgent(userAgent string) func(*Client) { + return func(client *Client) { + if userAgent == "" { + return + } + + client.userAgent = userAgent + } +} + +// charonUserAgent returns the default User-Agent header value, e.g. "charon/v1.12.0". +func charonUserAgent() string { + return "charon/" + version.Version.String() +} + // url returns a *url.URL from the baseURL stored in c. // Will panic if somehow c.baseURL got corrupted, and it's not a valid URL anymore. func (c Client) url() *url.URL { @@ -83,13 +108,14 @@ func launchpadURLPath(lock cluster.Lock) string { return fmt.Sprintf(launchpadReturnPathFmt, lock.LockHash) } -func httpPost(ctx context.Context, url *url.URL, body []byte, headers map[string]string) error { +func (c Client) httpPost(ctx context.Context, url *url.URL, body []byte, headers map[string]string) error { req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewReader(body)) if err != nil { return errors.Wrap(err, "create POST request") } req.Header.Add("Content-Type", "application/json") + req.Header.Set("User-Agent", c.userAgent) for key, val := range headers { req.Header.Set(key, val) @@ -107,6 +133,10 @@ func httpPost(ctx context.Context, url *url.URL, body []byte, headers map[string return errors.Wrap(err, "read POST response", z.Int("status", res.StatusCode)) } + if res.StatusCode == http.StatusNotFound { + return errors.Wrap(ErrLockNotFound, "http POST failed", z.Int("status", res.StatusCode), z.Str("body", string(data))) + } + if res.StatusCode != http.StatusConflict { return errors.New("http POST failed", z.Int("status", res.StatusCode), z.Str("body", string(data))) } @@ -117,12 +147,14 @@ func httpPost(ctx context.Context, url *url.URL, body []byte, headers map[string return nil } -func httpGet(ctx context.Context, url *url.URL, headers map[string]string) (io.ReadCloser, error) { +func (c Client) httpGet(ctx context.Context, url *url.URL, headers map[string]string) (io.ReadCloser, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) if err != nil { return nil, errors.Wrap(err, "create GET request") } + req.Header.Set("User-Agent", c.userAgent) + for key, val := range headers { req.Header.Set(key, val) } @@ -133,6 +165,8 @@ func httpGet(ctx context.Context, url *url.URL, headers map[string]string) (io.R } if res.StatusCode/100 != 2 { + defer res.Body.Close() + if res.StatusCode == http.StatusNotFound { return nil, ErrNoValue } @@ -148,12 +182,14 @@ func httpGet(ctx context.Context, url *url.URL, headers map[string]string) (io.R return res.Body, nil } -func httpDelete(ctx context.Context, url *url.URL, headers map[string]string) error { +func (c Client) httpDelete(ctx context.Context, url *url.URL, headers map[string]string) error { req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url.String(), nil) if err != nil { return errors.Wrap(err, "create DELETE request") } + req.Header.Set("User-Agent", c.userAgent) + for key, val := range headers { req.Header.Set(key, val) } diff --git a/app/obolapi/api_internal_test.go b/app/obolapi/api_internal_test.go index fbb1ed0817..fe6a451107 100644 --- a/app/obolapi/api_internal_test.go +++ b/app/obolapi/api_internal_test.go @@ -11,7 +11,11 @@ import ( "testing" "time" + k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/stretchr/testify/require" + + "github.com/obolnetwork/charon/app/version" + "github.com/obolnetwork/charon/app/z" ) func TestWithTimeout(t *testing.T) { @@ -35,6 +39,7 @@ func TestHttpPost(t *testing.T) { server *httptest.Server endpoint string expectedError string + expectedIs error }{ { name: "default scenario", @@ -89,6 +94,19 @@ func TestHttpPost(t *testing.T) { })), expectedError: "POST failed", }, + { + name: "status code 404 Not Found", + body: nil, + headers: nil, + endpoint: "/post-request", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, err := w.Write([]byte(`{"message":"cluster lock is not found"}`)) + require.NoError(t, err) + })), + expectedError: "POST failed", + expectedIs: ErrLockNotFound, + }, { name: "status code 409 Conflict (duplicate ignored)", body: nil, @@ -111,10 +129,20 @@ func TestHttpPost(t *testing.T) { testServerURL, err := url.ParseRequestURI(test.server.URL) require.NoError(t, err) - err = httpPost(context.Background(), testServerURL.JoinPath(test.endpoint), test.body, test.headers) + cl, err := New(test.server.URL) + require.NoError(t, err) + + err = cl.httpPost(context.Background(), testServerURL.JoinPath(test.endpoint), test.body, test.headers) if test.expectedError != "" { require.Error(t, err) require.ErrorContains(t, err, test.expectedError) + + if test.expectedIs != nil { + require.ErrorIs(t, err, test.expectedIs) + require.True(t, z.ContainsField(err, z.Int("status", http.StatusNotFound))) + } else { + require.NotErrorIs(t, err, ErrLockNotFound) + } } else { require.NoError(t, err) } @@ -179,7 +207,10 @@ func TestHttpGet(t *testing.T) { testServerURL, err := url.ParseRequestURI(test.server.URL) require.NoError(t, err) - respBody, err := httpGet(context.Background(), testServerURL.JoinPath(test.endpoint), test.headers) + cl, err := New(test.server.URL) + require.NoError(t, err) + + respBody, err := cl.httpGet(context.Background(), testServerURL.JoinPath(test.endpoint), test.headers) if test.expectedError != "" { require.Error(t, err) require.ErrorContains(t, err, test.expectedError) @@ -195,3 +226,77 @@ func TestHttpGet(t *testing.T) { }) } } + +func TestUserAgent(t *testing.T) { + charonUA := "charon/" + version.Version.String() + + tests := []struct { + name string + options []func(*Client) + want string + }{ + { + name: "default", + want: charonUA, + }, + { + name: "custom user agent", + options: []func(*Client){WithUserAgent("lido-dv-exit/v0.7.0")}, + want: "lido-dv-exit/v0.7.0", + }, + { + name: "empty user agent", + options: []func(*Client){WithUserAgent("")}, + want: charonUA, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var got []string + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got = append(got, r.Header.Get("User-Agent")) + + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + cl, err := New(srv.URL, test.options...) + require.NoError(t, err) + + u, err := url.ParseRequestURI(srv.URL) + require.NoError(t, err) + + ctx := context.Background() + + require.NoError(t, cl.httpPost(ctx, u, nil, nil)) + + body, err := cl.httpGet(ctx, u, nil) + require.NoError(t, err) + require.NoError(t, body.Close()) + + require.NoError(t, cl.httpDelete(ctx, u, nil)) + + require.Equal(t, []string{test.want, test.want, test.want}, got) + }) + } +} + +func TestPostPartialExitsLockNotFound(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"statusCode":404,"message":"cluster lock is not found"}`)) + })) + defer srv.Close() + + cl, err := New(srv.URL) + require.NoError(t, err) + + key, err := k1.GeneratePrivateKey() + require.NoError(t, err) + + err = cl.PostPartialExits(context.Background(), make([]byte, 32), 1, key) + require.ErrorIs(t, err, ErrLockNotFound) + require.NotErrorIs(t, err, ErrNoValue) +} diff --git a/app/obolapi/deposit.go b/app/obolapi/deposit.go index 8cc94dbcaa..52201f20ce 100644 --- a/app/obolapi/deposit.go +++ b/app/obolapi/deposit.go @@ -68,7 +68,7 @@ func (c Client) PostPartialDeposits(ctx context.Context, lockHash []byte, shareI ctx, cancel := context.WithTimeout(ctx, c.reqTimeout) defer cancel() - err = httpPost(ctx, u, data, nil) + err = c.httpPost(ctx, u, data, nil) if err != nil { return errors.Wrap(err, "http Obol API POST request") } @@ -103,7 +103,7 @@ func (c Client) GetFullDeposit(ctx context.Context, valPubkey string, lockHash [ ctx, cancel := context.WithTimeout(ctx, c.reqTimeout) defer cancel() - respBody, err := httpGet(ctx, u, map[string]string{}) + respBody, err := c.httpGet(ctx, u, map[string]string{}) if err != nil { return []eth2p0.DepositData{}, errors.Wrap(err, "http Obol API GET request") } diff --git a/app/obolapi/exit.go b/app/obolapi/exit.go index af8be2e4e9..2203bc061e 100644 --- a/app/obolapi/exit.go +++ b/app/obolapi/exit.go @@ -126,7 +126,7 @@ func (c Client) PostPartialExits(ctx context.Context, lockHash []byte, shareInde ctx, cancel := context.WithTimeout(ctx, c.reqTimeout) defer cancel() - err = httpPost(ctx, u, data, nil) + err = c.httpPost(ctx, u, data, nil) if err != nil { return errors.Wrap(err, "http Obol API POST request") } @@ -175,7 +175,7 @@ func (c Client) GetFullExit(ctx context.Context, valPubkey string, lockHash []by return ExitBlob{}, errors.Wrap(err, "k1 sign") } - respBody, err := httpGet(ctx, u, map[string]string{"Authorization": bearerString(lockHashSignature)}) + respBody, err := c.httpGet(ctx, u, map[string]string{"Authorization": bearerString(lockHashSignature)}) if err != nil { return ExitBlob{}, errors.Wrap(err, "http Obol API GET request") } @@ -320,7 +320,7 @@ func (c Client) DeletePartialExit(ctx context.Context, valPubkey string, lockHas return errors.Wrap(err, "k1 sign") } - err = httpDelete(ctx, u, map[string]string{"Authorization": bearerString(lockHashSignature)}) + err = c.httpDelete(ctx, u, map[string]string{"Authorization": bearerString(lockHashSignature)}) if err != nil { return errors.Wrap(err, "http Obol API DELETE request") } diff --git a/app/obolapi/feerecipient.go b/app/obolapi/feerecipient.go index 3f6cc47291..f93b4ca051 100644 --- a/app/obolapi/feerecipient.go +++ b/app/obolapi/feerecipient.go @@ -22,7 +22,7 @@ const ( fetchFeeRecipientTmpl = "/fee_recipient/" + lockHashPath errNoPartialsRegistrations = "no partial registrations found" - errLockNotFound = "lock not found" + lockNotFoundBody = "lock not found" ) // submitPartialFeeRecipientURL returns the partial fee recipient Obol API URL for a given lock hash. @@ -65,7 +65,7 @@ func (c Client) PostPartialFeeRecipients(ctx context.Context, lockHash []byte, s ctx, cancel := context.WithTimeout(ctx, c.reqTimeout) defer cancel() - err = httpPost(ctx, u, data, nil) + err = c.httpPost(ctx, u, data, nil) if err != nil { return errors.Wrap(err, "http Obol API POST request") } @@ -101,6 +101,7 @@ func (c Client) PostFeeRecipientsFetch(ctx context.Context, lockHash []byte, pub } httpReq.Header.Add("Content-Type", "application/json") + httpReq.Header.Set("User-Agent", c.userAgent) httpResp, err := new(http.Client).Do(httpReq) if err != nil { @@ -119,8 +120,8 @@ func (c Client) PostFeeRecipientsFetch(ctx context.Context, lockHash []byte, pub return FeeRecipientFetchResponse{}, nil } - if strings.Contains(string(body), errLockNotFound) { - return FeeRecipientFetchResponse{}, errors.New("cluster is unknown to the API, publish the lock file first") + if strings.Contains(string(body), lockNotFoundBody) { + return FeeRecipientFetchResponse{}, errors.Wrap(ErrLockNotFound, "cluster is unknown to the API, publish the lock file first") } } diff --git a/app/obolapi/publish.go b/app/obolapi/publish.go index 00503a5a87..12795dbb75 100644 --- a/app/obolapi/publish.go +++ b/app/obolapi/publish.go @@ -39,7 +39,7 @@ func (c Client) PublishLock(ctx context.Context, lock cluster.Lock) error { ctx, cancel := context.WithTimeout(ctx, c.reqTimeout) defer cancel() - err = httpPost(ctx, addr, b, nil) + err = c.httpPost(ctx, addr, b, nil) if err != nil { return err } @@ -65,7 +65,7 @@ func (c Client) PublishDefinition(ctx context.Context, def cluster.Definition, s ctx, cancel := context.WithTimeout(ctx, c.reqTimeout) defer cancel() - return httpPost(ctx, addr, b, headers) + return c.httpPost(ctx, addr, b, headers) } type RequestSignTermsAndConditions struct { @@ -99,5 +99,5 @@ func (c Client) SignTermsAndConditions(ctx context.Context, userAddr string, for ctx, cancel := context.WithTimeout(ctx, c.reqTimeout) defer cancel() - return httpPost(ctx, addr, r, headers) + return c.httpPost(ctx, addr, r, headers) } diff --git a/app/obolapi/test.go b/app/obolapi/test.go index 1927e08761..62c82743ea 100644 --- a/app/obolapi/test.go +++ b/app/obolapi/test.go @@ -24,7 +24,7 @@ func (c Client) PostTestResult(ctx context.Context, jsonTestResult []byte) error ctx, cancel := context.WithTimeout(ctx, c.reqTimeout) defer cancel() - err = httpPost(ctx, u, jsonTestResult, nil) + err = c.httpPost(ctx, u, jsonTestResult, nil) if err != nil { return errors.Wrap(err, "http Obol API POST request") } From 1dc8075e61e9a962bbe52dcb047ffbdfc8798839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn=20Kyne?= <4981644+OisinKyne@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:22:24 +0000 Subject: [PATCH 25/29] cmd: convert deposit sign withdrawal addresses to creds (#4804) * cmd: convert deposit sign withdrawal addresses to credentials charon deposit sign hex-decoded --withdrawal-addresses straight into DepositMessage.WithdrawalCredentials. A 20 byte address (as documented) was zero-padded by SSZ hashing, so operators signed and the API stored malformed deposits. Addresses are now converted to 0x01 (or 0x02 for compounding clusters) withdrawal credentials. Mixed case addresses must match their EIP-55 checksum. Full 32 byte credentials are still accepted if the prefix matches the cluster's compounding setting. deposit fetch rejects credentials from the API that are not 32 bytes. * eth2util/deposit: reuse withdrawalCredsFromAddr for 32 byte creds * cmd: test deposit sign zero address and compounding creds --------- Co-authored-by: kalo <24719519+KaloyanTanev@users.noreply.github.com> (cherry picked from commit e7d8587b57ada9c79ad58bfde4ff76a4908e145a) --- app/obolapi/deposit.go | 6 +++ app/obolapi/deposit_test.go | 14 ++++++ cmd/depositfetch_internal_test.go | 41 +++++++++++++++- cmd/depositsign.go | 23 +++++---- cmd/depositsign_internal_test.go | 81 ++++++++++++++++++++++++++++++- eth2util/deposit/deposit.go | 49 +++++++++++++++++++ eth2util/deposit/deposit_test.go | 46 ++++++++++++++++++ testutil/obolapimock/deposit.go | 5 ++ 8 files changed, 253 insertions(+), 12 deletions(-) diff --git a/app/obolapi/deposit.go b/app/obolapi/deposit.go index 52201f20ce..dc6a04716a 100644 --- a/app/obolapi/deposit.go +++ b/app/obolapi/deposit.go @@ -120,6 +120,12 @@ func (c Client) GetFullDeposit(ctx context.Context, valPubkey string, lockHash [ return []eth2p0.DepositData{}, errors.Wrap(err, "withdrawal credentials to bytes") } + // SSZ hashing zero-pads short credentials, so a malformed value would otherwise verify. + if len(withdrawalCredentialsBytes) != 32 { + return []eth2p0.DepositData{}, errors.New("withdrawal credentials must be 32 bytes", + z.Str("withdrawal_credentials", dr.WithdrawalCredentials)) + } + // Build a hex-encoded public-share -> 1-based share-index lookup so we can resolve the correct index. partialPubKeyToIdx := make(map[string]int, len(partialPubKeys)) for i, parPubKey := range partialPubKeys { diff --git a/app/obolapi/deposit_test.go b/app/obolapi/deposit_test.go index d61faa72be..d9abe76f76 100644 --- a/app/obolapi/deposit_test.go +++ b/app/obolapi/deposit_test.go @@ -243,3 +243,17 @@ func TestGetFullDeposit_NotEnoughSignatures(t *testing.T) { _, err = cl.GetFullDeposit(t.Context(), f.lock.Validators[0].PublicKeyHex(), f.lock.LockHash, f.lock.Threshold, f.lock.Validators[0].PubShares, f.network) require.ErrorContains(t, err, "not enough partial signatures to meet threshold") } + +func TestGetFullDeposit_ShortWithdrawalCredentials(t *testing.T) { + f := newDepositFixture(t) + // A bare 20 byte address, as older charon deposit sign versions submitted. + f.response.WithdrawalCredentials = "0x000000000000000000000000000000000000dead" + + srv := serveCannedDeposit(t, f.response) + + cl, err := obolapi.New(srv.URL) + require.NoError(t, err) + + _, err = cl.GetFullDeposit(t.Context(), f.lock.Validators[0].PublicKeyHex(), f.lock.LockHash, f.lock.Threshold, f.lock.Validators[0].PubShares, f.network) + require.ErrorContains(t, err, "withdrawal credentials must be 32 bytes") +} diff --git a/cmd/depositfetch_internal_test.go b/cmd/depositfetch_internal_test.go index 66c93a5804..0d44a0bf9c 100644 --- a/cmd/depositfetch_internal_test.go +++ b/cmd/depositfetch_internal_test.go @@ -3,6 +3,7 @@ package cmd import ( + "encoding/hex" "encoding/json" "fmt" "math/rand" @@ -16,11 +17,22 @@ import ( "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/z" "github.com/obolnetwork/charon/cluster" + "github.com/obolnetwork/charon/eth2util/deposit" "github.com/obolnetwork/charon/tbls" "github.com/obolnetwork/charon/testutil/obolapimock" ) func TestDepositFetchValid(t *testing.T) { + for _, compounding := range []bool{false, true} { + t.Run(fmt.Sprintf("compounding=%v", compounding), func(t *testing.T) { + testDepositFetchValid(t, compounding) + }) + } +} + +func testDepositFetchValid(t *testing.T, compounding bool) { + t.Helper() + ctx := t.Context() ctx = log.WithCtx(ctx, z.Str("test_case", t.Name())) @@ -36,8 +48,15 @@ func TestDepositFetchValid(t *testing.T) { operatorAmt, 0, random, + func(d *cluster.Definition) { d.Compounding = compounding }, ) + // Withdrawal credentials prefix, 0x02 for compounding clusters. + prefix := "01" + if compounding { + prefix = "02" + } + root := t.TempDir() operatorShares := make([][]tbls.PrivateKey, operatorAmt) @@ -74,8 +93,9 @@ func TestDepositFetchValid(t *testing.T) { } signConfig := depositSignConfig{ - depositConfig: config, - WithdrawalAddresses: []string{"0x0100000000000000000000000000000000000000000000000000000000001234", "0x0100000000000000000000000000000000000000000000000000000000001235"}, + depositConfig: config, + // One plain (checksummed) address and one full withdrawal credential. + WithdrawalAddresses: []string{"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", "0x" + prefix + "00000000000000000000000000000000000000000000000000000000001235"}, DepositAmounts: []uint{32, 1}, } @@ -100,6 +120,23 @@ func TestDepositFetchValid(t *testing.T) { err = runDepositFetch(ctx, fetchConfig) require.NoError(t, err) + + depositDatas, err := deposit.ReadDepositDataFiles(fetchConfig.DepositDataDir) + require.NoError(t, err) + require.Len(t, depositDatas, 2) + + expectedCreds := map[string]string{ + lock.Validators[0].PublicKeyHex(): prefix + "00000000000000000000005aaeb6053f3e94c9b9a09f33669435e7ef1beaed", + lock.Validators[1].PublicKeyHex(): prefix + "00000000000000000000000000000000000000000000000000000000001235", + } + + for _, dds := range depositDatas { + require.Len(t, dds, 2) + + for _, dd := range dds { + require.Equal(t, expectedCreds[dd.PublicKey.String()], hex.EncodeToString(dd.WithdrawalCredentials)) + } + } } func TestDepositFetchCLI(t *testing.T) { diff --git a/cmd/depositsign.go b/cmd/depositsign.go index e1a9a7916e..e3c7867ed6 100644 --- a/cmd/depositsign.go +++ b/cmd/depositsign.go @@ -59,7 +59,7 @@ func newDepositSignCmd(runFunc func(context.Context, depositSignConfig) error) * } func bindDepositSignFlags(cmd *cobra.Command, config *depositSignConfig) { - cmd.Flags().StringSliceVar(&config.WithdrawalAddresses, "withdrawal-addresses", []string{}, "[REQUIRED] Withdrawal addresses for which the new deposits will be signed. Either a single address for all specified validator-public-keys or one address per key should be specified.") + cmd.Flags().StringSliceVar(&config.WithdrawalAddresses, "withdrawal-addresses", []string{}, "[REQUIRED] Withdrawal addresses for which the new deposits will be signed, converted to 0x01 or 0x02 (compounding clusters) withdrawal credentials. Full 32 byte withdrawal credentials are also accepted. Either a single address for all specified validator-public-keys or one address per key should be specified.") cmd.Flags().UintSliceVar(&config.DepositAmounts, "deposit-amounts", []uint{32}, "Comma separated list of partial deposit amounts (integers) in ETH.") cmd.Flags().BoolVar(&config.AllowIncompleteKeystores, "allow-incomplete-keystores", false, "Allow the validator keys directory to contain key shares for only a subset of the cluster's validators. The command operates on the validators whose key shares are present.") } @@ -111,24 +111,29 @@ func runDepositSign(ctx context.Context, config depositSignConfig) error { pubkeys = append(pubkeys, blsPK) } + network, err := eth2util.ForkVersionToNetwork(cl.ForkVersion) + if err != nil { + return err + } + withdrawalAddrs := [][]byte{} for _, wAddr := range config.WithdrawalAddresses { - withdrawalAddr, err := hex.DecodeString(strings.TrimPrefix(wAddr, "0x")) + creds, err := deposit.WithdrawalCredsFromAddrOrCreds(wAddr, cl.Compounding) if err != nil { - return errors.Wrap(err, "decode withdrawal address", z.Str("withdrawal_address", wAddr)) + return err + } + + // We cannot allow a zero withdrawal address on mainnet or gnosis. + if isMainOrGnosis(network) && [20]byte(creds[12:]) == [20]byte{} { + return errors.New("zero withdrawal address forbidden on this network", z.Str("network", network)) } - withdrawalAddrs = append(withdrawalAddrs, withdrawalAddr) + withdrawalAddrs = append(withdrawalAddrs, creds[:]) } depositDatas := []eth2p0.DepositData{} - network, err := eth2util.ForkVersionToNetwork(cl.ForkVersion) - if err != nil { - return err - } - for i, pubkey := range pubkeys { for _, amount := range config.DepositAmounts { if !cl.Compounding && (amount < 1 || amount > 32) { diff --git a/cmd/depositsign_internal_test.go b/cmd/depositsign_internal_test.go index fc821b1640..c23230fc21 100644 --- a/cmd/depositsign_internal_test.go +++ b/cmd/depositsign_internal_test.go @@ -8,6 +8,7 @@ import ( "math/rand" "net/http/httptest" "path/filepath" + "strings" "testing" "time" @@ -16,6 +17,7 @@ import ( "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/z" "github.com/obolnetwork/charon/cluster" + "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/tbls" "github.com/obolnetwork/charon/testutil/obolapimock" ) @@ -75,13 +77,90 @@ func TestDepositSignValid(t *testing.T) { signConfig := depositSignConfig{ depositConfig: config, - WithdrawalAddresses: []string{"0x0100000000000000000000000000000000000000000000000000000000001234"}, + WithdrawalAddresses: []string{"0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"}, DepositAmounts: []uint{32}, } require.NoError(t, runDepositSign(ctx, signConfig), "operator index submit deposit sign: %v", idx) } +func TestDepositSignWithdrawalAddresses(t *testing.T) { + const zeroAddr = "0x0000000000000000000000000000000000000000" + + tests := []struct { + name string + mainnet bool + compounding bool + addr string + expectedErr string + }{ + {name: "mainnet zero address", mainnet: true, addr: zeroAddr, expectedErr: "zero withdrawal address forbidden"}, + {name: "mainnet zero credentials", mainnet: true, addr: "0x01" + strings.Repeat("00", 31), expectedErr: "zero withdrawal address forbidden"}, + {name: "mainnet address", mainnet: true, addr: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"}, + {name: "testnet zero address", addr: zeroAddr}, + {name: "compounding address", compounding: true, addr: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"}, + {name: "compounding 0x02 credentials", compounding: true, addr: "0x02" + strings.Repeat("00", 11) + "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"}, + {name: "compounding 0x01 credentials", compounding: true, addr: "0x01" + strings.Repeat("00", 11) + "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed", expectedErr: "prefix does not match"}, + {name: "bad checksum", addr: "0x5AAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", expectedErr: "does not match its EIP-55 checksum"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := log.WithCtx(t.Context(), z.Str("test_case", t.Name())) + + const operatorAmt = 4 + + lock, enrs, keyShares := cluster.NewForT(t, 1, operatorAmt, operatorAmt, 0, rand.New(rand.NewSource(0)), func(d *cluster.Definition) { + d.Compounding = test.compounding + if test.mainnet { + forkVersion, err := eth2util.NetworkToForkVersionBytes(eth2util.Mainnet.Name) + require.NoError(t, err) + + d.ForkVersion = forkVersion + } + }) + + operatorShares := make([][]tbls.PrivateKey, operatorAmt) + for opIdx := range operatorAmt { + operatorShares[opIdx] = append(operatorShares[opIdx], keyShares[0][opIdx]) + } + + lockJSON, err := json.Marshal(lock) + require.NoError(t, err) + + root := t.TempDir() + writeAllLockData(t, root, enrs, operatorShares, lockJSON) + + handler, addLockFiles := obolapimock.MockServer(false, nil) + + srv := httptest.NewServer(handler) + defer srv.Close() + + addLockFiles(lock) + + baseDir := filepath.Join(root, "op0") + signConfig := depositSignConfig{ + ValidatorPublicKeys: []string{lock.Validators[0].PublicKeyHex()}, + PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"), + ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"), + LockFilePath: filepath.Join(baseDir, "cluster-lock.json"), + PublishAddress: srv.URL, + PublishTimeout: 10 * time.Second, + WithdrawalAddresses: []string{test.addr}, + DepositAmounts: []uint{32}, + } + + err = runDepositSign(ctx, signConfig) + if test.expectedErr != "" { + require.ErrorContains(t, err, test.expectedErr) + return + } + + require.NoError(t, err) + }) + } +} + func TestDepositSignCLI(t *testing.T) { tests := []struct { name string diff --git a/eth2util/deposit/deposit.go b/eth2util/deposit/deposit.go index 0745541130..a6210e3db0 100644 --- a/eth2util/deposit/deposit.go +++ b/eth2util/deposit/deposit.go @@ -210,6 +210,55 @@ func GetMessageSigningRoot(msg eth2p0.DepositMessage, network string) ([32]byte, return resp, nil } +// WithdrawalCredsFromAddrOrCreds returns withdrawal credentials for either a 0x-prefixed 20 byte +// execution address or 0x-prefixed 32 byte 0x01/0x02 withdrawal credentials. +// Mixed case addresses must match their EIP-55 checksum, all lower or all upper case addresses are accepted. +// The credentials prefix must match the compounding flag. +func WithdrawalCredsFromAddrOrCreds(input string, compounding bool) ([32]byte, error) { + hexPart, ok := strings.CutPrefix(input, "0x") + if !ok { + return [32]byte{}, errors.New("withdrawal address or credentials must be 0x-prefixed", z.Str("input", input)) + } + + switch len(hexPart) { + case 20 * 2: + if hexPart != strings.ToLower(hexPart) && hexPart != strings.ToUpper(hexPart) { + checksummed, err := eth2util.ChecksumAddress(input) + if err != nil { + return [32]byte{}, errors.Wrap(err, "invalid withdrawal address", z.Str("addr", input)) + } else if checksummed != input { + return [32]byte{}, errors.New("withdrawal address does not match its EIP-55 checksum; use all-lowercase or the checksummed form", + z.Str("addr", input), z.Str("checksummed", checksummed)) + } + } + + return withdrawalCredsFromAddr("0x"+strings.ToLower(hexPart), compounding) + case 32 * 2: + b, err := hex.DecodeString(hexPart) + if err != nil { + return [32]byte{}, errors.Wrap(err, "decode withdrawal credentials", z.Str("creds", input)) + } + + if slices.ContainsFunc(b[1:12], func(b byte) bool { return b != 0 }) { + return [32]byte{}, errors.New("withdrawal credentials bytes 1 to 11 must be zero", z.Str("creds", input)) + } + + creds, err := withdrawalCredsFromAddr("0x"+hex.EncodeToString(b[12:]), compounding) + if err != nil { + return [32]byte{}, err + } + + if b[0] != creds[0] { + return [32]byte{}, errors.New("withdrawal credentials prefix does not match cluster compounding setting", + z.Str("creds", input), z.Bool("compounding", compounding)) + } + + return creds, nil + default: + return [32]byte{}, errors.New("withdrawal address must be 20 bytes or withdrawal credentials 32 bytes", z.Str("input", input)) + } +} + // withdrawalCredsFromAddr returns the Withdrawal Credentials. func withdrawalCredsFromAddr(addr string, compounding bool) ([32]byte, error) { // Check for validity of address. diff --git a/eth2util/deposit/deposit_test.go b/eth2util/deposit/deposit_test.go index 567637435c..b54ef1a203 100644 --- a/eth2util/deposit/deposit_test.go +++ b/eth2util/deposit/deposit_test.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "path" + "strings" "testing" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" @@ -473,3 +474,48 @@ func TestDedupAmounts(t *testing.T) { require.Equal(t, []eth2p0.Gwei{0, 100, 300, 500}, amounts) } + +func TestWithdrawalCredsFromAddrOrCreds(t *testing.T) { + const ( + checksummed = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" + lower = "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed" + ) + + tests := []struct { + name string + input string + compounding bool + expected string + expectedErr string + }{ + {name: "checksummed address", input: checksummed, expected: "010000000000000000000000" + lower}, + {name: "lowercase address", input: "0x" + lower, expected: "010000000000000000000000" + lower}, + {name: "uppercase address", input: "0x" + strings.ToUpper(lower), expected: "010000000000000000000000" + lower}, + {name: "compounding address", input: checksummed, compounding: true, expected: "020000000000000000000000" + lower}, + {name: "bad checksum", input: "0x5AAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", expectedErr: "does not match its EIP-55 checksum"}, + {name: "0x01 credentials", input: "0x010000000000000000000000" + lower, expected: "010000000000000000000000" + lower}, + {name: "0x02 credentials", input: "0x020000000000000000000000" + strings.ToUpper(lower), compounding: true, expected: "020000000000000000000000" + lower}, + {name: "0x01 credentials compounding", input: "0x010000000000000000000000" + lower, compounding: true, expectedErr: "prefix does not match"}, + {name: "0x02 credentials not compounding", input: "0x020000000000000000000000" + lower, expectedErr: "prefix does not match"}, + {name: "0x00 credentials", input: "0x000000000000000000000000" + lower, expectedErr: "prefix does not match"}, + {name: "bad padding", input: "0x010000000000000000000001" + lower, expectedErr: "bytes 1 to 11 must be zero"}, + {name: "no 0x prefix", input: lower, expectedErr: "must be 0x-prefixed"}, + {name: "19 bytes", input: "0x" + lower[2:], expectedErr: "must be 20 bytes"}, + {name: "33 bytes", input: "0x00010000000000000000000000" + lower, expectedErr: "must be 20 bytes"}, + {name: "not hex", input: "0x" + strings.Repeat("z", 40), expectedErr: "invalid withdrawal address"}, + {name: "credentials not hex", input: "0x" + strings.Repeat("z", 64), expectedErr: "decode withdrawal credentials"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + creds, err := deposit.WithdrawalCredsFromAddrOrCreds(test.input, test.compounding) + if test.expectedErr != "" { + require.ErrorContains(t, err, test.expectedErr) + return + } + + require.NoError(t, err) + require.Equal(t, test.expected, hex.EncodeToString(creds[:])) + }) + } +} diff --git a/testutil/obolapimock/deposit.go b/testutil/obolapimock/deposit.go index 51d3fce860..057ea3dc46 100644 --- a/testutil/obolapimock/deposit.go +++ b/testutil/obolapimock/deposit.go @@ -78,6 +78,11 @@ func (ts *testServer) HandleSubmitPartialDeposit(writer http.ResponseWriter, req } for _, depositData := range data.PartialDepositData { + if len(depositData.WithdrawalCredentials) != 32 { + writeErr(writer, http.StatusBadRequest, "invalid withdrawal credentials length") + return + } + signedDepositsRoot, err := deposit.GetMessageSigningRoot(eth2p0.DepositMessage{ PublicKey: depositData.PublicKey, WithdrawalCredentials: depositData.WithdrawalCredentials, From f258ea1e74a4213a754ba2dfb7c0782c8ac863c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn=20Kyne?= <4981644+OisinKyne@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:17:06 +0000 Subject: [PATCH 26/29] cmd: validate addresses consistently across create and edit (#4805) Validates fee recipient and withdrawal addresses the same way in `create cluster`, `create dkg` and `edit add-validators`, and stores them in EIP-55 checksummed form. Before this, `create cluster` without a definition file skipped the withdrawal address check entirely. A zero address on mainnet or gnosis is an error in `create dkg` and a warning elsewhere (where it's legitimate, e.g. splitting the keys of validators that are already active). The "all-lowercase or all-uppercase, otherwise it must match EIP-55" rule now lives in one place, `eth2util.ValidateChecksumAddress`. It's used by these commands, by `feerecipient sign` (which wrongly rejected all-uppercase addresses) and by `deposit.WithdrawalCredsFromAddrOrCreds` (#4804). category: bug ticket: none (cherry picked from commit fd8c6fa5af5ce0e9127ea436e01ce73305ab592a) --- cmd/create.go | 114 +++++++++++++++- cmd/create_internal_test.go | 132 ++++++++++++++++++ cmd/createcluster.go | 167 +++++++++++------------ cmd/createcluster_internal_test.go | 172 +++++++++++++++++++++--- cmd/createdkg.go | 31 +---- cmd/createdkg_internal_test.go | 131 ++++++++++++++---- cmd/edit_addvalidators.go | 11 ++ cmd/edit_addvalidators_internal_test.go | 63 +++++++++ cmd/exit.go | 4 +- cmd/exit_sign.go | 35 ++++- cmd/exit_sign_internal_test.go | 16 +++ cmd/feerecipientsign.go | 16 +-- cmd/feerecipientsign_internal_test.go | 4 + eth2util/deposit/deposit.go | 10 +- eth2util/helpers.go | 18 +++ eth2util/helpers_test.go | 32 +++++ 16 files changed, 771 insertions(+), 185 deletions(-) create mode 100644 cmd/create_internal_test.go diff --git a/cmd/create.go b/cmd/create.go index bde7aafe28..7e5c83fbc6 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -2,7 +2,20 @@ package cmd -import "github.com/spf13/cobra" +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/obolnetwork/charon/app/errors" + "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/app/z" + "github.com/obolnetwork/charon/eth2util" +) + +// zeroAddress is not owned by any user, is often associated with token burn & mint/genesis events and used as a generic null address. +// See https://etherscan.io/address/0x0000000000000000000000000000000000000000. +const zeroAddress = "0x0000000000000000000000000000000000000000" func newCreateCmd(cmds ...*cobra.Command) *cobra.Command { root := &cobra.Command{ @@ -15,3 +28,102 @@ func newCreateCmd(cmds ...*cobra.Command) *cobra.Command { return root } + +// validateNetworkConfig returns an error if the network configuration is invalid in the given cluster configuration. +func validateNetworkConfig(network string, testnet eth2util.Network) error { + // Check if custom testnet configuration is provided first (takes precedence over default network). + if testnet.IsNonZero() { + // Add testnet config to supported networks. + eth2util.AddTestNetwork(testnet) + + return nil + } + + if network != "" { + if eth2util.ValidNetwork(network) { + return nil + } + + return errors.New("invalid network specified", z.Str("network", network)) + } + + return errors.New("missing --network flag or testnet config flags") +} + +// isMainOrGnosis returns true if the network is either mainnet or gnosis. +func isMainOrGnosis(network string) bool { + return network == eth2util.Mainnet.Name || network == eth2util.Gnosis.Name +} + +// validateAddresses checks if we have sufficient addresses. It also fills addresses slices if only one is provided. +// +//nolint:revive // Not really confusing here as they are passed as arguments as well. +func validateAddresses(numVals int, feeRecipientAddrs []string, withdrawalAddrs []string) ([]string, []string, error) { + if len(feeRecipientAddrs) != numVals && len(feeRecipientAddrs) != 1 { + return nil, nil, errors.New("mismatching --num-validators and --fee-recipient-addresses", + z.Int("num_validators", numVals), z.Int("addresses", len(feeRecipientAddrs))) + } + + if len(withdrawalAddrs) != numVals && len(withdrawalAddrs) != 1 { + return nil, nil, errors.New("mismatching --num-validators and --withdrawal-addresses", + z.Int("num_validators", numVals), z.Int("addresses", len(withdrawalAddrs))) + } + + if len(feeRecipientAddrs) == 1 { + for i := 1; i < numVals; i++ { + feeRecipientAddrs = append(feeRecipientAddrs, feeRecipientAddrs[0]) + } + } + + if len(withdrawalAddrs) == 1 { + for i := 1; i < numVals; i++ { + withdrawalAddrs = append(withdrawalAddrs, withdrawalAddrs[0]) + } + } + + return feeRecipientAddrs, withdrawalAddrs, nil +} + +// checksumValidatorAddrs validates and checksums both fee recipient and withdrawal addresses. +// A zero fee recipient only warns; a zero withdrawal address on mainnet or gnosis is an error +// if rejectZeroWithdrawal is set, otherwise a warning. +func checksumValidatorAddrs(ctx context.Context, feeRecipientAddrs, withdrawalAddrs []string, network string, rejectZeroWithdrawal bool) (checksummedFeeRecipients []string, checksummedWithdrawals []string, err error) { + checksummedFeeRecipients, err = checksumAddrs(ctx, feeRecipientAddrs, "fee recipient", network, false) + if err != nil { + return nil, nil, err + } + + checksummedWithdrawals, err = checksumAddrs(ctx, withdrawalAddrs, "withdrawal", network, rejectZeroWithdrawal) + if err != nil { + return nil, nil, err + } + + return checksummedFeeRecipients, checksummedWithdrawals, nil +} + +// checksumAddrs validates the provided addresses (see eth2util.ValidateChecksumAddress) of the given kind +// (e.g. "withdrawal" or "fee recipient") and returns their EIP-55 checksummed forms. +// The zero address on mainnet or gnosis is an error if rejectZero is set, otherwise a warning: +// it is legitimate when the generated deposit data is never used, e.g. split existing keys of +// validators that are already active. +func checksumAddrs(ctx context.Context, addrs []string, kind string, network string, rejectZero bool) ([]string, error) { + resp := make([]string, 0, len(addrs)) + for _, addr := range addrs { + checksummed, err := eth2util.ValidateChecksumAddress(addr) + if err != nil { + return nil, errors.Wrap(err, "invalid "+kind+" address", z.Str("addr", addr)) + } + + if isMainOrGnosis(network) && checksummed == zeroAddress { + if rejectZero { + return nil, errors.New("zero "+kind+" address forbidden on this network", z.Str("network", network)) + } + + log.Warn(ctx, "Zero "+kind+" address on "+network+": only continue if the generated deposit data and registrations will never be used", nil, z.Str("network", network)) + } + + resp = append(resp, checksummed) + } + + return resp, nil +} diff --git a/cmd/create_internal_test.go b/cmd/create_internal_test.go new file mode 100644 index 0000000000..b5961a173e --- /dev/null +++ b/cmd/create_internal_test.go @@ -0,0 +1,132 @@ +// Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package cmd + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/obolnetwork/charon/eth2util" +) + +func TestChecksumAddrs(t *testing.T) { + // EIP-55 test vector address. + const checksummed = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" + + tests := []struct { + name string + addrs []string + network string + expected []string + expectedErr string + }{ + { + name: "checksummed", + addrs: []string{checksummed}, + network: eth2util.Mainnet.Name, + expected: []string{checksummed}, + }, + { + name: "lowercase normalised", + addrs: []string{strings.ToLower(checksummed)}, + network: eth2util.Mainnet.Name, + expected: []string{checksummed}, + }, + { + name: "uppercase normalised", + addrs: []string{"0x" + strings.ToUpper(checksummed[2:])}, + network: eth2util.Gnosis.Name, + expected: []string{checksummed}, + }, + { + name: "mixed case bad checksum", + addrs: []string{"0x5AAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"}, + network: eth2util.Hoodi.Name, + expectedErr: "does not match its EIP-55 checksum", + }, + { + name: "too short", + addrs: []string{"0xBAD000BAD000BAD"}, + network: eth2util.Hoodi.Name, + expectedErr: "invalid withdrawal address", + }, + { + name: "no 0x prefix", + addrs: []string{checksummed[2:] + "00"}, + network: eth2util.Hoodi.Name, + expectedErr: "invalid withdrawal address", + }, + { + name: "non hex", + addrs: []string{"0x" + strings.Repeat("zz", 20)}, + network: eth2util.Hoodi.Name, + expectedErr: "invalid withdrawal address", + }, + { + name: "zero address mainnet", + addrs: []string{zeroAddress}, + network: eth2util.Mainnet.Name, + expectedErr: "zero withdrawal address forbidden on this network", + }, + { + name: "zero address gnosis", + addrs: []string{checksummed, zeroAddress}, + network: eth2util.Gnosis.Name, + expectedErr: "zero withdrawal address forbidden on this network", + }, + { + name: "zero address testnet", + addrs: []string{zeroAddress}, + network: eth2util.Hoodi.Name, + expected: []string{zeroAddress}, + }, + { + name: "zero address custom testnet", + addrs: []string{zeroAddress}, + network: "", + expected: []string{zeroAddress}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := checksumAddrs(t.Context(), test.addrs, "withdrawal", test.network, true) + if test.expectedErr != "" { + require.ErrorContains(t, err, test.expectedErr) + return + } + + require.NoError(t, err) + require.Equal(t, test.expected, got) + }) + } +} + +func TestChecksumValidatorAddrs(t *testing.T) { + const checksummed = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" + + ctx := t.Context() + + fee, wd, err := checksumValidatorAddrs(ctx, []string{strings.ToLower(checksummed)}, []string{strings.ToLower(checksummed)}, eth2util.Mainnet.Name, true) + require.NoError(t, err) + require.Equal(t, []string{checksummed}, fee) + require.Equal(t, []string{checksummed}, wd) + + // A zero fee recipient only warns. + fee, _, err = checksumValidatorAddrs(ctx, []string{zeroAddress}, []string{checksummed}, eth2util.Mainnet.Name, true) + require.NoError(t, err) + require.Equal(t, []string{zeroAddress}, fee) + + // A zero withdrawal address is rejected only when asked to. + _, _, err = checksumValidatorAddrs(ctx, []string{checksummed}, []string{zeroAddress}, eth2util.Mainnet.Name, true) + require.ErrorContains(t, err, "zero withdrawal address forbidden on this network") + + _, wd, err = checksumValidatorAddrs(ctx, []string{checksummed}, []string{zeroAddress}, eth2util.Gnosis.Name, false) + require.NoError(t, err) + require.Equal(t, []string{zeroAddress}, wd) + + _, _, err = checksumValidatorAddrs(ctx, []string{checksummed}, []string{"0x5AAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"}, eth2util.Mainnet.Name, true) + require.ErrorContains(t, err, "invalid withdrawal address") +} diff --git a/cmd/createcluster.go b/cmd/createcluster.go index 856fefc95a..20eb036633 100644 --- a/cmd/createcluster.go +++ b/cmd/createcluster.go @@ -49,9 +49,6 @@ import ( ) const ( - // zeroAddress is not owned by any user, is often associated with token burn & mint/genesis events and used as a generic null address. - // See https://etherscan.io/address/0x0000000000000000000000000000000000000000. - zeroAddress = "0x0000000000000000000000000000000000000000" defaultNetwork = "mainnet" minNodes = 3 minThreshold = 2 @@ -191,6 +188,22 @@ func runCreateCluster(ctx context.Context, w io.Writer, conf clusterConfig) erro return err } + var network string + if conf.DefFile != "" { + network, err = eth2util.ForkVersionToNetwork(def.ForkVersion) + } else { + _, network, err = networkFromConfig(conf) + } + + if err != nil { + return err + } + + // Reject insecure keys as soon as the network is known, before any keys are read or generated. + if err := validateInsecureKeysFlag(ctx, conf.InsecureKeys, network); err != nil { + return err + } + var secrets []tbls.PrivateKey // If we're splitting keys, read them from SplitKeysDir and set conf.NumDVs to the amount of @@ -228,12 +241,7 @@ func runCreateCluster(ctx context.Context, w io.Writer, conf clusterConfig) erro } // Validate the provided definition. - err = validateDef(ctx, conf.InsecureKeys, conf.KeymanagerAddrs, def, eth1Cl) - if err != nil { - return err - } - - network, err := eth2util.ForkVersionToNetwork(def.ForkVersion) + err = validateDef(ctx, conf.KeymanagerAddrs, def, network, eth1Cl) if err != nil { return err } @@ -299,11 +307,6 @@ func runCreateCluster(ctx context.Context, w io.Writer, conf clusterConfig) erro } } - network, err := eth2util.ForkVersionToNetwork(def.ForkVersion) - if err != nil { - return err - } - depositDatas, err := createDepositDatas(def.WithdrawalAddresses(), network, secrets, depositAmounts, def.Compounding) if err != nil { return err @@ -913,17 +916,15 @@ func newDefFromConfig(ctx context.Context, conf clusterConfig) (cluster.Definiti return cluster.Definition{}, err } - var forkVersion string - if conf.testnetConfig.IsNonZero() { - // Custom testnet config takes precedence (already registered in validateCreateConfig via validateNetworkConfig). - forkVersion = conf.testnetConfig.GenesisForkVersionHex - } else if conf.Network != "" { - forkVersion, err = eth2util.NetworkToForkVersion(conf.Network) - if err != nil { - return cluster.Definition{}, err - } - } else { - return cluster.Definition{}, errors.New("network not specified, missing --network or --testnet-fork-version") + forkVersion, network, err := networkFromConfig(conf) + if err != nil { + return cluster.Definition{}, err + } + + // Validate and normalise addresses to their EIP-55 checksummed form before writing them into the definition. + feeRecipientAddrs, withdrawalAddrs, err = checksumValidatorAddrs(ctx, feeRecipientAddrs, withdrawalAddrs, network, false) + if err != nil { + return cluster.Definition{}, err } var ops []cluster.Operator @@ -945,6 +946,30 @@ func newDefFromConfig(ctx context.Context, conf clusterConfig) (cluster.Definiti return def, nil } +// networkFromConfig returns the fork version and network name from the network flags. +func networkFromConfig(conf clusterConfig) (forkVersion string, network string, err error) { + if conf.testnetConfig.IsNonZero() { + // Custom testnet config takes precedence (already registered in validateCreateConfig via validateNetworkConfig). + forkVersion = conf.testnetConfig.GenesisForkVersionHex + } else { + forkVersion, err = eth2util.NetworkToForkVersion(conf.Network) + if err != nil { + return "", "", err + } + } + + forkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(forkVersion, "0x")) + if err != nil { + return "", "", errors.Wrap(err, "decode fork version", z.Str("fork_version", forkVersion)) + } + + // Unknown (unregistered custom testnet) fork versions resolve to an empty network name, which is + // never mainnet or gnosis, so the mainnet-only checks are still applied correctly. + network, _ = eth2util.ForkVersionToNetwork(forkVersionBytes) + + return forkVersion, network, nil +} + // newPeer returns a new peer ENR, generating a p2pkey in node directory. func newPeer(clusterDir string, peerIdx int) (enr.Record, *k1.PrivateKey, error) { dir := nodeDir(clusterDir, peerIdx) @@ -1001,7 +1026,8 @@ func nodeDir(clusterDir string, i int) string { } // validateDef returns an error if the provided cluster definition is invalid. -func validateDef(ctx context.Context, insecureKeys bool, keymanagerAddrs []string, def cluster.Definition, eth1Cl eth1wrap.EthClientRunner) error { +// The network is the one resolved from the definition's fork version. +func validateDef(ctx context.Context, keymanagerAddrs []string, def cluster.Definition, network string, eth1Cl eth1wrap.EthClientRunner) error { if def.NumValidators == 0 { return errors.New("--num-validators not set, specify at least one") } @@ -1020,26 +1046,17 @@ func validateDef(ctx context.Context, insecureKeys bool, keymanagerAddrs []strin } } - network, err := eth2util.ForkVersionToNetwork(def.ForkVersion) - if err != nil { - return err - } - - if insecureKeys && isMainOrGnosis(network) { - return errors.New("insecure keys not supported on mainnet") - } else if insecureKeys { - log.Warn(ctx, "Insecure keystores configured. ONLY DO THIS DURING TESTING", nil) - } - if def.Name == "" { return errors.New("name not provided") } - if err = def.VerifyHashes(); err != nil { + err := def.VerifyHashes() + if err != nil { return err } - if err = def.VerifySignatures(eth1Cl); err != nil { + err = def.VerifySignatures(eth1Cl) + if err != nil { return err } @@ -1051,7 +1068,27 @@ func validateDef(ctx context.Context, insecureKeys bool, keymanagerAddrs []strin return errors.New("unsupported consensus protocol", z.Str("protocol", def.ConsensusProtocol)) } - return validateWithdrawalAddrs(def.WithdrawalAddresses(), network) + // Addresses in an existing definition are covered by its hashes and signatures, so they are + // only validated here, not normalised. + _, _, err = checksumValidatorAddrs(ctx, def.FeeRecipientAddresses(), def.WithdrawalAddresses(), network, true) + + return err +} + +// validateInsecureKeysFlag returns an error if insecure keys are requested on mainnet or gnosis, +// otherwise it logs a warning when insecure keys are requested. +func validateInsecureKeysFlag(ctx context.Context, insecureKeys bool, network string) error { + if !insecureKeys { + return nil + } + + if isMainOrGnosis(network) { + return errors.New("insecure keys not supported on mainnet") + } + + log.Warn(ctx, "Insecure keystores configured. ONLY DO THIS DURING TESTING", nil) + + return nil } // aggSign returns a bls aggregate signatures of the message signed by all the shares. @@ -1188,35 +1225,6 @@ func hasDistinctAddrs(addrs []string) bool { return false } -// validateAddresses checks if we have sufficient addresses. It also fills addresses slices if only one is provided. -// -//nolint:revive // Not really confusing here as they are passed as arguments as well. -func validateAddresses(numVals int, feeRecipientAddrs []string, withdrawalAddrs []string) ([]string, []string, error) { - if len(feeRecipientAddrs) != numVals && len(feeRecipientAddrs) != 1 { - return nil, nil, errors.New("mismatching --num-validators and --fee-recipient-addresses", - z.Int("num_validators", numVals), z.Int("addresses", len(feeRecipientAddrs))) - } - - if len(withdrawalAddrs) != numVals && len(withdrawalAddrs) != 1 { - return nil, nil, errors.New("mismatching --num-validators and --withdrawal-addresses", - z.Int("num_validators", numVals), z.Int("addresses", len(withdrawalAddrs))) - } - - if len(feeRecipientAddrs) == 1 { - for i := 1; i < numVals; i++ { - feeRecipientAddrs = append(feeRecipientAddrs, feeRecipientAddrs[0]) - } - } - - if len(withdrawalAddrs) == 1 { - for i := 1; i < numVals; i++ { - withdrawalAddrs = append(withdrawalAddrs, withdrawalAddrs[0]) - } - } - - return feeRecipientAddrs, withdrawalAddrs, nil -} - func builderRegistrationFromETH2(reg core.VersionedSignedValidatorRegistration) (cluster.BuilderRegistration, error) { feeRecipient, err := reg.FeeRecipient() if err != nil { @@ -1248,24 +1256,3 @@ func builderRegistrationFromETH2(reg core.VersionedSignedValidatorRegistration) Signature: reg.Signature(), }, nil } - -// validateNetworkConfig returns an error if the network configuration is invalid in the given cluster configuration. -func validateNetworkConfig(network string, testnet eth2util.Network) error { - // Check if custom testnet configuration is provided first (takes precedence over default network). - if testnet.IsNonZero() { - // Add testnet config to supported networks. - eth2util.AddTestNetwork(testnet) - - return nil - } - - if network != "" { - if eth2util.ValidNetwork(network) { - return nil - } - - return errors.New("invalid network specified", z.Str("network", network)) - } - - return errors.New("missing --network flag or testnet config flags") -} diff --git a/cmd/createcluster_internal_test.go b/cmd/createcluster_internal_test.go index 47dc8dc54f..ebb8aded6f 100644 --- a/cmd/createcluster_internal_test.go +++ b/cmd/createcluster_internal_test.go @@ -472,13 +472,13 @@ func TestValidateDef(t *testing.T) { def.ForkVersion = gnosis - err = validateDef(ctx, false, conf.KeymanagerAddrs, def, nil) + err = validateDef(ctx, conf.KeymanagerAddrs, def, eth2util.Gnosis.Name, nil) require.Error(t, err, "zero address") }) t.Run("fork versions", func(t *testing.T) { def := definition - err = validateDef(ctx, false, conf.KeymanagerAddrs, def, nil) + err = validateDef(ctx, conf.KeymanagerAddrs, def, eth2util.Goerli.Name, nil) require.NoError(t, err) mainnet, err := hex.DecodeString(strings.TrimPrefix(eth2util.Mainnet.GenesisForkVersionHex, "0x")) @@ -486,7 +486,7 @@ func TestValidateDef(t *testing.T) { def.ForkVersion = mainnet - err = validateDef(ctx, conf.InsecureKeys, conf.KeymanagerAddrs, def, nil) + err = validateDef(ctx, conf.KeymanagerAddrs, def, eth2util.Mainnet.Name, nil) require.Error(t, err, "zero address") }) @@ -494,41 +494,35 @@ func TestValidateDef(t *testing.T) { conf := conf conf.KeymanagerAddrs = []string{"127.0.0.1:1234"} - err = validateDef(ctx, true, conf.KeymanagerAddrs, definition, nil) + err = validateDef(ctx, conf.KeymanagerAddrs, definition, eth2util.Goerli.Name, nil) require.Error(t, err) }) - t.Run("insecure keys", func(t *testing.T) { - conf := conf - err = validateDef(ctx, true, conf.KeymanagerAddrs, definition, nil) // Validate with insecure keys set to true - require.NoError(t, err) - }) - t.Run("insufficient number of nodes", func(t *testing.T) { def := definition def.Operators = nil - err = validateDef(ctx, conf.InsecureKeys, conf.KeymanagerAddrs, def, nil) + err = validateDef(ctx, conf.KeymanagerAddrs, def, eth2util.Goerli.Name, nil) require.ErrorContains(t, err, "insufficient number of nodes") }) t.Run("name not provided", func(t *testing.T) { def := definition def.Name = "" - err = validateDef(ctx, conf.InsecureKeys, conf.KeymanagerAddrs, def, nil) + err = validateDef(ctx, conf.KeymanagerAddrs, def, eth2util.Goerli.Name, nil) require.ErrorContains(t, err, "name not provided") }) t.Run("zero validators provided", func(t *testing.T) { def := definition def.NumValidators = 0 - err = validateDef(ctx, conf.InsecureKeys, conf.KeymanagerAddrs, def, nil) + err = validateDef(ctx, conf.KeymanagerAddrs, def, eth2util.Goerli.Name, nil) require.ErrorContains(t, err, "--num-validators not set, specify at least one") }) t.Run("invalid hash", func(t *testing.T) { def := remoteDef def.NumValidators = 3 - err = validateDef(ctx, conf.InsecureKeys, conf.KeymanagerAddrs, def, nil) + err = validateDef(ctx, conf.KeymanagerAddrs, def, eth2util.Sepolia.Name, nil) require.ErrorContains(t, err, "invalid config hash") }) @@ -537,18 +531,160 @@ func TestValidateDef(t *testing.T) { def.NumValidators = 3 def, err = def.SetDefinitionHashes() require.NoError(t, err) - err = validateDef(ctx, conf.InsecureKeys, conf.KeymanagerAddrs, def, nil) + err = validateDef(ctx, conf.KeymanagerAddrs, def, eth2util.Sepolia.Name, nil) require.ErrorContains(t, err, "invalid creator config signature") }) t.Run("unsupported consensus protocol", func(t *testing.T) { def := definition def.ConsensusProtocol = "unreal" - err = validateDef(ctx, false, conf.KeymanagerAddrs, def, nil) + err = validateDef(ctx, conf.KeymanagerAddrs, def, eth2util.Goerli.Name, nil) require.Error(t, err, "unsupported consensus protocol") }) } +// TestNewDefFromConfigValidation verifies that creating a cluster from flags (no --definition-file) +// applies the same address checks as a definition file. +func TestNewDefFromConfigValidation(t *testing.T) { + ctx := t.Context() + badChecksum := "0xFB6916095ca1df60bB79Ce92cE3Ea74c37c5d359" + + tests := []struct { + name string + network string + feeRecipient string + withdrawal string + expectedErr string + }{ + { + name: "mainnet valid", + network: eth2util.Mainnet.Name, + feeRecipient: validEthAddr, + withdrawal: validEthAddr, + }, + { + name: "mainnet lowercase normalised", + network: eth2util.Mainnet.Name, + feeRecipient: strings.ToLower(validEthAddr), + withdrawal: strings.ToLower(validEthAddr), + }, + { + name: "testnet zero addresses", + network: eth2util.Hoodi.Name, + feeRecipient: zeroAddress, + withdrawal: zeroAddress, + }, + { + // Zero addresses only warn: split existing keys of active validators never use deposit data. + name: "mainnet zero withdrawal", + network: eth2util.Mainnet.Name, + feeRecipient: validEthAddr, + withdrawal: zeroAddress, + }, + { + name: "mainnet zero fee recipient", + network: eth2util.Mainnet.Name, + feeRecipient: zeroAddress, + withdrawal: validEthAddr, + }, + { + name: "bad withdrawal checksum", + network: eth2util.Hoodi.Name, + feeRecipient: validEthAddr, + withdrawal: badChecksum, + expectedErr: "invalid withdrawal address", + }, + { + name: "malformed fee recipient", + network: eth2util.Hoodi.Name, + feeRecipient: "0xabc", + withdrawal: validEthAddr, + expectedErr: "invalid fee recipient address", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + conf := clusterConfig{ + Name: "test", + NumNodes: 4, + NumDVs: 2, + Threshold: 3, + TargetGasLimit: 30000000, + Network: test.network, + FeeRecipientAddrs: []string{test.feeRecipient}, + WithdrawalAddrs: []string{test.withdrawal}, + } + + def, err := newDefFromConfig(ctx, conf) + if test.expectedErr != "" { + require.ErrorContains(t, err, test.expectedErr) + return + } + + require.NoError(t, err) + + wantFee, err := eth2util.ChecksumAddress(test.feeRecipient) + require.NoError(t, err) + wantWd, err := eth2util.ChecksumAddress(test.withdrawal) + require.NoError(t, err) + + require.Equal(t, []string{wantFee, wantFee}, def.FeeRecipientAddresses()) + require.Equal(t, []string{wantWd, wantWd}, def.WithdrawalAddresses()) + }) + } +} + +// TestCreateClusterInsecureKeys verifies insecure keys are rejected on mainnet and gnosis before any keys are read. +func TestCreateClusterInsecureKeys(t *testing.T) { + mainnetForkVersion, err := eth2util.NetworkToForkVersionBytes(eth2util.Mainnet.Name) + require.NoError(t, err) + + lock, _, _ := cluster.NewForT(t, 1, 3, 4, 0, rand.New(rand.NewSource(0)), func(d *cluster.Definition) { + d.ForkVersion = mainnetForkVersion + }) + + defBytes, err := json.Marshal(lock.Definition) + require.NoError(t, err) + + mainnetDefFile := filepath.Join(t.TempDir(), "cluster-definition.json") + require.NoError(t, os.WriteFile(mainnetDefFile, defBytes, 0o644)) + + tests := []struct { + name string + network string + defFile string + }{ + {name: "mainnet flag", network: eth2util.Mainnet.Name}, + {name: "gnosis flag", network: eth2util.Gnosis.Name}, + // The definition file's network takes precedence over the network flag. + {name: "mainnet definition file", network: eth2util.Hoodi.Name, defFile: mainnetDefFile}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + conf := clusterConfig{ + Name: t.Name(), + ClusterDir: t.TempDir(), + DefFile: test.defFile, + NumNodes: 4, + Threshold: 3, + Network: test.network, + FeeRecipientAddrs: []string{validEthAddr}, + WithdrawalAddrs: []string{validEthAddr}, + TargetGasLimit: 30000000, + InsecureKeys: true, + // A missing keys dir proves the check runs before any keys are read. + SplitKeys: true, + SplitKeysDir: filepath.Join(t.TempDir(), "missing"), + } + + err := runCreateCluster(t.Context(), io.Discard, conf) + require.ErrorContains(t, err, "insecure keys not supported") + }) + } +} + func TestSplitKeys(t *testing.T) { tests := []struct { name string @@ -733,7 +869,7 @@ func TestTargetGasLimit(t *testing.T) { NumNodes: 4, Threshold: 3, NumDVs: 1, - Network: defaultNetwork, + Network: eth2util.Hoodi.Name, WithdrawalAddrs: []string{zeroAddress}, FeeRecipientAddrs: []string{zeroAddress}, InsecureKeys: true, @@ -749,7 +885,7 @@ func TestTargetGasLimit(t *testing.T) { NumNodes: 4, Threshold: 3, NumDVs: 1, - Network: defaultNetwork, + Network: eth2util.Hoodi.Name, WithdrawalAddrs: []string{zeroAddress}, FeeRecipientAddrs: []string{zeroAddress}, InsecureKeys: true, diff --git a/cmd/createdkg.go b/cmd/createdkg.go index 5d5d650a6c..eb11fb600d 100644 --- a/cmd/createdkg.go +++ b/cmd/createdkg.go @@ -142,7 +142,8 @@ func runCreateDKG(ctx context.Context, conf createDKGConfig) (err error) { return err } - if err = validateWithdrawalAddrs(conf.WithdrawalAddrs, conf.Network); err != nil { + conf.FeeRecipientAddrs, conf.WithdrawalAddrs, err = checksumValidatorAddrs(ctx, conf.FeeRecipientAddrs, conf.WithdrawalAddrs, conf.Network, true) + if err != nil { return err } @@ -193,13 +194,11 @@ func runCreateDKG(ctx context.Context, conf createDKGConfig) (err error) { if conf.testnetConfig.IsNonZero() { // Custom testnet config takes precedence (already registered in validateDKGConfig via validateNetworkConfig). forkVersion = conf.testnetConfig.GenesisForkVersionHex - } else if conf.Network != "" { + } else { forkVersion, err = eth2util.NetworkToForkVersion(conf.Network) if err != nil { return err } - } else { - return errors.New("network not specified, missing --network or --testnet-fork-version") } var privKey *k1.PrivateKey @@ -276,25 +275,6 @@ func runCreateDKG(ctx context.Context, conf createDKGConfig) (err error) { return nil } -// validateWithdrawalAddrs returns an error if any of the provided withdrawal addresses is invalid. -func validateWithdrawalAddrs(addrs []string, network string) error { - for _, addr := range addrs { - checksumAddr, err := eth2util.ChecksumAddress(addr) - if err != nil { - return errors.Wrap(err, "invalid withdrawal address", z.Str("addr", addr)) - } else if checksumAddr != addr { - return errors.New("invalid checksummed address", z.Str("addr", addr)) - } - - // We cannot allow a zero withdrawal address on mainnet or gnosis. - if isMainOrGnosis(network) && addr == zeroAddress { - return errors.New("zero address forbidden on this network", z.Str("network", network)) - } - } - - return nil -} - // validateDKGConfig returns an error if any of the provided config parameter is invalid. func validateDKGConfig(numOperators int, network string, testnet eth2util.Network, depositAmounts []int, consensusProtocol string, compounding bool) error { // Don't allow cluster size to be less than 3. @@ -321,11 +301,6 @@ func validateDKGConfig(numOperators int, network string, testnet eth2util.Networ return nil } -// isMainOrGnosis returns true if the network is either mainnet or gnosis. -func isMainOrGnosis(network string) bool { - return network == eth2util.Mainnet.Name || network == eth2util.Gnosis.Name -} - func generateLaunchpadLink(configHash []byte, network string) string { var networkLink string diff --git a/cmd/createdkg_internal_test.go b/cmd/createdkg_internal_test.go index 85675de39d..5fcc4dec13 100644 --- a/cmd/createdkg_internal_test.go +++ b/cmd/createdkg_internal_test.go @@ -4,6 +4,7 @@ package cmd import ( "context" + "encoding/json" "os" "path" "strings" @@ -11,6 +12,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/obolnetwork/charon/cluster" "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/testutil" ) @@ -121,6 +123,110 @@ func TestCreateDkgInvalid(t *testing.T) { } } +func TestCreateDkgAddresses(t *testing.T) { + enrs := []string{ + "enr:-JG4QFI0llFYxSoTAHm24OrbgoVx77dL6Ehl1Ydys39JYoWcBhiHrRhtGXDTaygWNsEWFb1cL7a1Bk0klIdaNuXplKWGAYGv0Gt7gmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQL6bcis0tFXnbqG4KuywxT5BLhtmijPFApKCDJNl3mXFYN0Y3CCDhqDdWRwgg4u", + "enr:-JG4QPnqHa7FU3PBqGxpV5L0hjJrTUqv8Wl6_UTHt-rELeICWjvCfcVfwmax8xI_eJ0ntI3ly9fgxAsmABud6-yBQiuGAYGv0iYPgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMLLCMZ5Oqi_sdnBfdyhmysZMfFm78PgF7Y9jitTJPSroN0Y3CCPoODdWRwgj6E", + "enr:-JG4QDKNYm_JK-w6NuRcUFKvJAlq2L4CwkECelzyCVrMWji4YnVRn8AqQEL5fTQotPL2MKxiKNmn2k6XEINtq-6O3Z2GAYGvzr_LgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQKlO7fSaBa3h48CdM-qb_Xb2_hSrJOy6nNjR0mapAqMboN0Y3CCDhqDdWRwgg4u", + "enr:-JG4QKu734_MXQklKrNHe9beXIsIV5bqv58OOmsjWmp6CF5vJSHNinYReykn7-IIkc5-YsoF8Hva1Q3pl7_gUj5P9cOGAYGv0jBLgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMM3AvPhXGCUIzBl9VFOw7VQ6_m8dGifVfJ1YXrvZsaZoN0Y3CCDhqDdWRwgg4u", + } + + badChecksum := "0xFB6916095ca1df60bB79Ce92cE3Ea74c37c5d359" + + tests := []struct { + name string + network string + feeRecipient string + withdrawal string + expectedErr string + }{ + { + name: "checksummed", + network: eth2util.Mainnet.Name, + feeRecipient: validEthAddr, + withdrawal: validEthAddr, + }, + { + name: "lowercase normalised", + network: eth2util.Mainnet.Name, + feeRecipient: strings.ToLower(validEthAddr), + withdrawal: strings.ToLower(validEthAddr), + }, + { + name: "invalid fee recipient checksum", + network: eth2util.Mainnet.Name, + feeRecipient: badChecksum, + withdrawal: validEthAddr, + expectedErr: "invalid fee recipient address", + }, + { + name: "malformed fee recipient", + network: eth2util.Hoodi.Name, + feeRecipient: "0x1234", + withdrawal: validEthAddr, + expectedErr: "invalid fee recipient address", + }, + { + // A zero fee recipient only warns. + name: "zero fee recipient on mainnet", + network: eth2util.Mainnet.Name, + feeRecipient: zeroAddress, + withdrawal: validEthAddr, + }, + { + name: "zero fee recipient on testnet", + network: eth2util.Hoodi.Name, + feeRecipient: zeroAddress, + withdrawal: validEthAddr, + }, + { + name: "zero withdrawal on gnosis", + network: eth2util.Gnosis.Name, + feeRecipient: validEthAddr, + withdrawal: zeroAddress, + expectedErr: "zero withdrawal address forbidden on this network", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + dir := t.TempDir() + conf := createDKGConfig{ + OutputDir: dir, + NumValidators: 1, + FeeRecipientAddrs: []string{test.feeRecipient}, + WithdrawalAddrs: []string{test.withdrawal}, + Network: test.network, + DKGAlgo: "default", + TargetGasLimit: 30000000, + OperatorENRs: enrs, + } + + err := runCreateDKG(context.Background(), conf) + if test.expectedErr != "" { + require.ErrorContains(t, err, test.expectedErr) + return + } + + require.NoError(t, err) + + b, err := os.ReadFile(path.Join(dir, "cluster-definition.json")) + require.NoError(t, err) + + var def cluster.Definition + require.NoError(t, json.Unmarshal(b, &def)) + + wantFee, err := eth2util.ChecksumAddress(test.feeRecipient) + require.NoError(t, err) + wantWd, err := eth2util.ChecksumAddress(test.withdrawal) + require.NoError(t, err) + + require.Equal(t, []string{wantFee}, def.FeeRecipientAddresses()) + require.Equal(t, []string{wantWd}, def.WithdrawalAddresses()) + }) + } +} + func TestRequireOperatorENRFlag(t *testing.T) { tests := []struct { name string @@ -170,31 +276,6 @@ func TestExistingClusterDefinition(t *testing.T) { require.EqualError(t, cmd.Execute(), "existing cluster-definition.json found. Try again after deleting it") } -func TestValidateWithdrawalAddr(t *testing.T) { - t.Run("ok", func(t *testing.T) { - addrs := []string{validEthAddr} - err := validateWithdrawalAddrs(addrs, eth2util.Goerli.Name) - require.NoError(t, err) - }) - - t.Run("invalid network", func(t *testing.T) { - err := validateWithdrawalAddrs([]string{zeroAddress}, eth2util.Mainnet.Name) - require.ErrorContains(t, err, "zero address forbidden on this network") - }) - - t.Run("invalid withdrawal address", func(t *testing.T) { - addrs := []string{"0xBAD000BAD000BAD"} - err := validateWithdrawalAddrs(addrs, eth2util.Gnosis.Name) - require.ErrorContains(t, err, "invalid withdrawal address") - }) - - t.Run("invalid checksum", func(t *testing.T) { - addrs := []string{"0x000BAD0000000BAD0000000BAD0000000BAD0000"} - err := validateWithdrawalAddrs(addrs, eth2util.Gnosis.Name) - require.ErrorContains(t, err, "invalid checksummed address") - }) -} - func TestValidateDKGConfig(t *testing.T) { t.Run("insufficient ENRs", func(t *testing.T) { numOperators := 2 diff --git a/cmd/edit_addvalidators.go b/cmd/edit_addvalidators.go index bb52667045..145dd49045 100644 --- a/cmd/edit_addvalidators.go +++ b/cmd/edit_addvalidators.go @@ -17,6 +17,7 @@ import ( "github.com/obolnetwork/charon/app/z" "github.com/obolnetwork/charon/cluster" "github.com/obolnetwork/charon/dkg" + "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/deposit" "github.com/obolnetwork/charon/eth2util/keystore" "github.com/obolnetwork/charon/tbls" @@ -103,6 +104,16 @@ func runAddValidators(ctx context.Context, conf addValidatorsConfig) error { return err } + // Unknown (custom testnet) fork versions resolve to an empty network, which is never mainnet or gnosis. + network, _ := eth2util.ForkVersionToNetwork(lock.ForkVersion) + + // Validate and normalise new addresses to their EIP-55 checksummed form, so that all operators + // produce the same cluster lock regardless of the address case they provided. + conf.FeeRecipientAddrs, conf.WithdrawalAddrs, err = checksumValidatorAddrs(ctx, conf.FeeRecipientAddrs, conf.WithdrawalAddrs, network, false) + if err != nil { + return err + } + // Loading the existing cluster keystore. var secrets []tbls.PrivateKey diff --git a/cmd/edit_addvalidators_internal_test.go b/cmd/edit_addvalidators_internal_test.go index 1bf11ee426..96ac377629 100644 --- a/cmd/edit_addvalidators_internal_test.go +++ b/cmd/edit_addvalidators_internal_test.go @@ -437,3 +437,66 @@ func mustLoadTestLockFile(t *testing.T, path string) cluster.Lock { return lock } + +func TestRunAddValidatorsAddressValidation(t *testing.T) { + conf := clusterConfig{ + ClusterDir: t.TempDir(), + Name: "test_cluster", + NumNodes: 4, + Threshold: 3, + NumDVs: 1, + Network: eth2util.Mainnet.Name, + TargetGasLimit: 36000000, + FeeRecipientAddrs: []string{validEthAddr}, + WithdrawalAddrs: []string{validEthAddr}, + } + + var buf bytes.Buffer + + require.NoError(t, runCreateCluster(t.Context(), &buf, conf)) + + ddir := nodeDir(conf.ClusterDir, 0) + badChecksum := "0xFB6916095ca1df60bB79Ce92cE3Ea74c37c5d359" + + tests := []struct { + name string + feeRecipient string + withdrawal string + expectedErr string + }{ + // Zero addresses only warn here (see TestChecksumValidatorAddrs), so they would proceed to a DKG. + { + name: "bad withdrawal checksum", + feeRecipient: validEthAddr, + withdrawal: badChecksum, + expectedErr: "invalid withdrawal address", + }, + { + name: "malformed fee recipient", + feeRecipient: "0x1234", + withdrawal: validEthAddr, + expectedErr: "invalid fee recipient address", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + addConf := addValidatorsConfig{ + PrivateKeyPath: path.Join(ddir, enrPrivateKeyFile), + LockFilePath: path.Join(ddir, clusterLockFile), + ValidatorKeysDir: path.Join(ddir, validatorKeysSubDir), + OutputDir: nodeDir(t.TempDir(), 0), + NumValidators: 1, + WithdrawalAddrs: []string{test.withdrawal}, + FeeRecipientAddrs: []string{test.feeRecipient}, + DKG: dkg.Config{ + Log: log.DefaultConfig(), + Timeout: time.Second, + }, + } + + err := runAddValidators(t.Context(), addConf) + require.ErrorContains(t, err, test.expectedErr) + }) + } +} diff --git a/cmd/exit.go b/cmd/exit.go index 428b9334e3..b40a846d72 100644 --- a/cmd/exit.go +++ b/cmd/exit.go @@ -167,7 +167,7 @@ func bindExitFlags(cmd *cobra.Command, config *exitConfig, flags []exitCLIFlag) case validatorKeysDir: cmd.Flags().StringVar(&config.ValidatorKeysDir, validatorKeysDir.String(), ".charon/validator_keys", maybeRequired("Path to the directory containing the validator private key share files and passwords.")) case validatorPubkey: - cmd.Flags().StringVar(&config.ValidatorPubkey, validatorPubkey.String(), "", maybeRequired("Public key of the validator to exit, must be present in the cluster lock manifest. If --validator-index is also provided, validator liveliness won't be checked on the beacon chain.")) + cmd.Flags().StringVar(&config.ValidatorPubkey, validatorPubkey.String(), "", maybeRequired("Public key of the validator to exit, must be present in the cluster lock manifest. If --validator-index is also provided, validator liveliness won't be checked on the beacon chain, but the index is cross-checked against the public key if the beacon node knows it.")) case exitEpoch: cmd.Flags().Uint64Var(&config.ExitEpoch, exitEpoch.String(), 194048, maybeRequired("Exit epoch at which the validator will exit, must be the same across all the partial exits.")) case exitFromFile: @@ -181,7 +181,7 @@ func bindExitFlags(cmd *cobra.Command, config *exitConfig, flags []exitCLIFlag) case publishTimeout: cmd.Flags().DurationVar(&config.PublishTimeout, publishTimeout.String(), 5*time.Minute, "Timeout for publishing a signed exit to the publish-address API.") case validatorIndex: - cmd.Flags().Uint64Var(&config.ValidatorIndex, validatorIndex.String(), 0, "Validator index of the validator to exit, the associated public key must be present in the cluster lock manifest. If --validator-public-key is also provided, validator existence won't be checked on the beacon chain.") + cmd.Flags().Uint64Var(&config.ValidatorIndex, validatorIndex.String(), 0, "Validator index of the validator to exit, the associated public key must be present in the cluster lock manifest. If --validator-public-key is also provided, validator existence won't be checked on the beacon chain, but the index is cross-checked against the public key if the beacon node knows it.") case all: cmd.Flags().BoolVar(&config.All, all.String(), false, "Exit all currently active validators in the cluster.") case testnetName: diff --git a/cmd/exit_sign.go b/cmd/exit_sign.go index 93c53b120d..99fc2ee868 100644 --- a/cmd/exit_sign.go +++ b/cmd/exit_sign.go @@ -151,7 +151,7 @@ func runSignPartialExit(ctx context.Context, config exitConfig) error { } if config.SkipBeaconNodeCheck { - log.Info(ctx, "Both public key and index are specified, beacon node won't be checked for validator existence/liveness") + log.Info(ctx, "Both public key and index are specified, beacon node won't be checked for validator liveness, only that the index matches the public key") } var exitBlobs []obolapi.ExitBlob @@ -192,6 +192,12 @@ func signSingleValidatorExit(ctx context.Context, config exitConfig, eth2Cl eth2 return nil, errors.Wrap(err, "fetch validator index") } + if config.SkipBeaconNodeCheck { + if err := crossCheckValidatorIndex(ctx, eth2Cl, valEth2, valIndex); err != nil { + return nil, err + } + } + log.Info(ctx, "Signing partial exit message for validator", z.Str("validator_public_key", valEth2.String()), z.U64("validator_index", uint64(valIndex))) exitMsg, err := signExit(ctx, eth2Cl, valIndex, ourShare.Share, eth2p0.Epoch(config.ExitEpoch)) @@ -353,6 +359,33 @@ func fetchValidatorBLSPubKey(ctx context.Context, config exitConfig, eth2Cl eth2 return eth2p0.BLSPubKey{}, errors.New("validator index not found in beacon node response", z.Str("beacon_address", eth2Cl.Address()), z.U64("validator_index", config.ValidatorIndex), z.Any("raw_response", rawValData)) } +// crossCheckValidatorIndex verifies, when both --validator-public-key and --validator-index are provided, +// that the beacon node reports the given index as belonging to the given public key. A mismatch is an +// error. If the beacon node cannot be queried or does not know the index yet, a warning is logged and +// signing proceeds, preserving the existing behaviour of not requiring the validator to be on chain. +func crossCheckValidatorIndex(ctx context.Context, eth2Cl eth2wrap.Client, pubkey eth2p0.BLSPubKey, index eth2p0.ValidatorIndex) error { + rawValData, err := queryBeaconForValidator(ctx, eth2Cl, nil, []eth2p0.ValidatorIndex{index}, nil) + if err != nil { + log.Warn(ctx, "Could not cross-check validator index against public key on beacon node, proceeding", err) + return nil + } + + val, ok := rawValData.Data[index] + if !ok { + log.Warn(ctx, "Validator index not found on beacon node, could not cross-check it against the public key, proceeding", nil) + return nil + } + + if val.Validator.PublicKey != pubkey { + return errors.New("validator index belongs to a different public key on the beacon node", + z.U64("validator_index", uint64(index)), + z.Str("validator_public_key", pubkey.String()), + z.Str("beacon_public_key", val.Validator.PublicKey.String())) + } + + return nil +} + func fetchValidatorIndex(ctx context.Context, config exitConfig, eth2Cl eth2wrap.Client) (eth2p0.ValidatorIndex, error) { if config.ValidatorIndexPresent { return eth2p0.ValidatorIndex(config.ValidatorIndex), nil diff --git a/cmd/exit_sign_internal_test.go b/cmd/exit_sign_internal_test.go index b4da45a6df..79528eb2c8 100644 --- a/cmd/exit_sign_internal_test.go +++ b/cmd/exit_sign_internal_test.go @@ -120,6 +120,22 @@ func Test_runSubmitPartialExit(t *testing.T) { ) }) + t.Run("main flow with skipBeaconNodeCheck mode with index of another validator", func(t *testing.T) { + runSubmitPartialExitFlowTest( + t, + true, + true, + "", + 1, // Index 1 belongs to lock.Validators[1], not the default lock.Validators[0] pubkey. + "validator index belongs to a different public key on the beacon node", + false, + ) + }) + + t.Run("main flow with skipBeaconNodeCheck mode with index unknown to beacon node", func(t *testing.T) { + runSubmitPartialExitFlowTest(t, true, true, "", 9999, "", false) + }) + t.Run("main flow with pubkey", func(t *testing.T) { runSubmitPartialExitFlowTest(t, false, false, "", 0, "", false) }) diff --git a/cmd/feerecipientsign.go b/cmd/feerecipientsign.go index e769e48bac..5d6f478661 100644 --- a/cmd/feerecipientsign.go +++ b/cmd/feerecipientsign.go @@ -366,26 +366,18 @@ func filterPubkeysByStatus( return pubkeysToSign, nil } -// validateFeeRecipient checks that the fee recipient address is well-formed, is not the zero -// (burn) address, and, when supplied in mixed case, matches its EIP-55 checksum. +// validateFeeRecipient checks that the fee recipient address is well-formed (see eth2util.ValidateChecksumAddress) +// and is not the zero (burn) address. func validateFeeRecipient(address string) error { - checksummed, err := eth2util.ChecksumAddress(address) + checksummed, err := eth2util.ValidateChecksumAddress(address) if err != nil { return errors.Wrap(err, "invalid fee recipient address", z.Str("fee_recipient", address)) } - if checksummed == "0x0000000000000000000000000000000000000000" { + if checksummed == zeroAddress { return errors.New("fee recipient address must not be the zero address", z.Str("fee_recipient", address)) } - hexPart := strings.TrimPrefix(address, "0x") - if hexPart != strings.ToLower(hexPart) && address != checksummed { - return errors.New("fee recipient address does not match its EIP-55 checksum; use all-lowercase or the checksummed form", - z.Str("fee_recipient", address), - z.Str("checksummed", checksummed), - ) - } - return nil } diff --git a/cmd/feerecipientsign_internal_test.go b/cmd/feerecipientsign_internal_test.go index fe44b06181..2a36135e77 100644 --- a/cmd/feerecipientsign_internal_test.go +++ b/cmd/feerecipientsign_internal_test.go @@ -164,6 +164,10 @@ func TestValidateFeeRecipient(t *testing.T) { name: "valid checksummed", address: checksummed, }, + { + name: "valid uppercase", + address: "0x" + strings.ToUpper(checksummed[2:]), + }, { name: "invalid checksum", address: "0x5AAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", diff --git a/eth2util/deposit/deposit.go b/eth2util/deposit/deposit.go index a6210e3db0..340551e65a 100644 --- a/eth2util/deposit/deposit.go +++ b/eth2util/deposit/deposit.go @@ -222,14 +222,8 @@ func WithdrawalCredsFromAddrOrCreds(input string, compounding bool) ([32]byte, e switch len(hexPart) { case 20 * 2: - if hexPart != strings.ToLower(hexPart) && hexPart != strings.ToUpper(hexPart) { - checksummed, err := eth2util.ChecksumAddress(input) - if err != nil { - return [32]byte{}, errors.Wrap(err, "invalid withdrawal address", z.Str("addr", input)) - } else if checksummed != input { - return [32]byte{}, errors.New("withdrawal address does not match its EIP-55 checksum; use all-lowercase or the checksummed form", - z.Str("addr", input), z.Str("checksummed", checksummed)) - } + if _, err := eth2util.ValidateChecksumAddress(input); err != nil { + return [32]byte{}, errors.Wrap(err, "invalid withdrawal address") } return withdrawalCredsFromAddr("0x"+strings.ToLower(hexPart), compounding) diff --git a/eth2util/helpers.go b/eth2util/helpers.go index 8f8694f486..5dc91e83d6 100644 --- a/eth2util/helpers.go +++ b/eth2util/helpers.go @@ -87,6 +87,24 @@ func ChecksumAddress(address string) (string, error) { return checksumAddressBytes(b), nil } +// ValidateChecksumAddress validates an Ethereum address and returns its EIP-55 checksummed form. +// The address must be "0x" followed by 40 hex characters. All-lowercase and all-uppercase hex +// is accepted (no checksum); mixed case must match the EIP-55 checksum exactly. +func ValidateChecksumAddress(addr string) (string, error) { + checksummed, err := ChecksumAddress(addr) + if err != nil { + return "", err + } + + hexPart := strings.TrimPrefix(addr, "0x") + if hexPart != strings.ToLower(hexPart) && hexPart != strings.ToUpper(hexPart) && addr != checksummed { + return "", errors.New("address does not match its EIP-55 checksum; use all-lowercase or the checksummed form", + z.Str("addr", addr), z.Str("checksummed", checksummed)) + } + + return checksummed, nil +} + // checksumAddressBytes returns an EIP55-compliant 0xhex representation of the binary ethereum address. func checksumAddressBytes(addressBytes []byte) string { hexAddr := hex.EncodeToString(addressBytes) diff --git a/eth2util/helpers_test.go b/eth2util/helpers_test.go index b94f34ca3b..65cc602a53 100644 --- a/eth2util/helpers_test.go +++ b/eth2util/helpers_test.go @@ -14,6 +14,38 @@ import ( "github.com/obolnetwork/charon/eth2util" ) +func TestValidateChecksumAddress(t *testing.T) { + const checksummed = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed" + + tests := []struct { + name string + addr string + expectedErr string + }{ + {name: "checksummed", addr: checksummed}, + {name: "lowercase", addr: strings.ToLower(checksummed)}, + {name: "uppercase", addr: "0x" + strings.ToUpper(checksummed[2:])}, + {name: "bad checksum", addr: "0x5AAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", expectedErr: "does not match its EIP-55 checksum"}, + {name: "uppercase 0X prefix", addr: "0X" + checksummed[2:], expectedErr: "invalid ethereum address"}, + {name: "no 0x prefix", addr: checksummed[2:], expectedErr: "invalid ethereum address"}, + {name: "too short", addr: checksummed[:40], expectedErr: "invalid ethereum address"}, + {name: "not hex", addr: "0x" + strings.Repeat("z", 40), expectedErr: "invalid ethereum hex address"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + resp, err := eth2util.ValidateChecksumAddress(test.addr) + if test.expectedErr != "" { + require.ErrorContains(t, err, test.expectedErr) + return + } + + require.NoError(t, err) + require.Equal(t, checksummed, resp) + }) + } +} + func TestChecksummedAddress(t *testing.T) { // Test examples from https://eips.ethereum.org/EIPS/eip-55. addrs := []string{ From fce50549e363a0d98a9acb162f5bc8c4478962a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 13:18:19 -0500 Subject: [PATCH 27/29] build(deps): Bump the go-dependencies group across 1 directory with 4 updates (#4817) Partial backport: only golang.org/x/net v0.62.0. go-builder-client, go-eth2-client and go-ethereum v1.17.10 (requires go-eth2-client v0.30.1 and dynamic-ssz v1.4.0) are gloas-related and left out. (cherry picked from commit beea8ba7bb854246ed5ecaa5046149d01e131695) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d9cefca0f9..fc91d3ac45 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( go.uber.org/goleak v1.3.0 go.uber.org/zap v1.28.0 golang.org/x/crypto v0.58.0 - golang.org/x/net v0.61.0 + golang.org/x/net v0.62.0 golang.org/x/sync v0.24.0 golang.org/x/term v0.47.0 golang.org/x/text v0.43.0 diff --git a/go.sum b/go.sum index 7bfd573912..3769e6b5b7 100644 --- a/go.sum +++ b/go.sum @@ -675,8 +675,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= -golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From c645a2dc58b4dcc6ff8b7814d8f1d1939dfe0fc7 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:03:25 +0200 Subject: [PATCH 28/29] *: bump golangci-lint to v2.14.0 (#4741) (cherry picked from commit 589fc22f55472562176d13c15869198e235c44fb) --- .github/workflows/golangci-lint.yml | 2 +- .golangci.yml | 1 + .pre-commit/run_linter.sh | 2 +- app/log/config.go | 6 +++--- core/ssz.go | 4 ++-- core/validatorapi/validatorapi.go | 7 +++++-- p2p/metrics_internal_test.go | 9 +++------ 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index e9ff62534d..bfc7bf8573 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -19,4 +19,4 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: - version: v2.13.2 + version: v2.14.0 diff --git a/.golangci.yml b/.golangci.yml index 6b0c6b5f84..d59cec7ad4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,6 +19,7 @@ linters: - godot - godox - gomoddirectives + - gomodguard - inamedparam - interfacebloat - intrange diff --git a/.pre-commit/run_linter.sh b/.pre-commit/run_linter.sh index 5cf19fb235..e27c2d36ea 100755 --- a/.pre-commit/run_linter.sh +++ b/.pre-commit/run_linter.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="2.12.2" +VERSION="2.14.0" if ! command -v golangci-lint &>/dev/null; then echo "golangci-lint could not be found" diff --git a/app/log/config.go b/app/log/config.go index 2f7ddbfc71..478ae5ff20 100644 --- a/app/log/config.go +++ b/app/log/config.go @@ -501,13 +501,13 @@ func formatZapStack(zapStack string) string { if strings.HasPrefix(line, "\t") { const sep = "charon/" // Note that this only works if source built in a folder named 'charon'. - i := strings.LastIndex(line, sep) - if i < 0 { + _, after, ok := strings.CutLast(line, sep) + if !ok { // Skip non-charon lines continue } - resp = append(resp, "\t"+line[i+len(sep):]+" "+prevFunc) + resp = append(resp, "\t"+after+" "+prevFunc) prevFunc = "" continue diff --git a/core/ssz.go b/core/ssz.go index 4087cb8275..090997a63a 100644 --- a/core/ssz.go +++ b/core/ssz.go @@ -835,7 +835,7 @@ func sizeSSZValIdxVersioned(value sszType) int { } // VersionedBlindedSSZValueForT exposes the value method of a type for testing purposes. -func VersionedBlindedSSZValueForT(t *testing.T, value any, version eth2util.DataVersion, blinded bool) sszType { +func VersionedBlindedSSZValueForT(t *testing.T, value any, version eth2util.DataVersion, blinded bool) any { t.Helper() resp, err := value.(interface { @@ -847,7 +847,7 @@ func VersionedBlindedSSZValueForT(t *testing.T, value any, version eth2util.Data } // VersionedSSZValueForT exposes the value method of a type for testing purposes. -func VersionedSSZValueForT(t *testing.T, value any, version eth2util.DataVersion) sszType { +func VersionedSSZValueForT(t *testing.T, value any, version eth2util.DataVersion) any { t.Helper() resp, err := value.(interface { diff --git a/core/validatorapi/validatorapi.go b/core/validatorapi/validatorapi.go index 5c4cb52005..b1595ab9ff 100644 --- a/core/validatorapi/validatorapi.go +++ b/core/validatorapi/validatorapi.go @@ -644,7 +644,7 @@ func (c Component) SubmitBlindedProposal(ctx context.Context, opts *eth2api.Subm return errors.Wrap(err, "could not fetch block definition from dutydb") } - if err := propDataMatchesDuty(ð2api.SubmitProposalOpts{ + blindedOpts := ð2api.SubmitProposalOpts{ Common: opts.Common, Proposal: ð2api.VersionedSignedProposal{ Version: opts.Proposal.Version, @@ -656,7 +656,10 @@ func (c Component) SubmitBlindedProposal(ctx context.Context, opts *eth2api.Subm FuluBlinded: opts.Proposal.Fulu, }, BroadcastValidation: opts.BroadcastValidation, - }, prop); err != nil { + } + + err = propDataMatchesDuty(blindedOpts, prop) + if err != nil { return errors.Wrap(err, "consensus proposal and VC-submitted one do not match") } diff --git a/p2p/metrics_internal_test.go b/p2p/metrics_internal_test.go index 5b99576c9c..42e0b0288c 100644 --- a/p2p/metrics_internal_test.go +++ b/p2p/metrics_internal_test.go @@ -175,6 +175,7 @@ func TestConcurrentRequestDepths(t *testing.T) { client.Peerstore().AddAddrs(server.ID(), server.Addrs(), peerstore.PermanentAddrTTL) release := make(chan struct{}) + RegisterHandler("server", server, pID, func() proto.Message { return new(pbv1.Duty) }, func(_ context.Context, _ peer.ID, req proto.Message) (proto.Message, bool, error) { @@ -190,14 +191,10 @@ func TestConcurrentRequestDepths(t *testing.T) { var wg sync.WaitGroup for range n { - wg.Add(1) - - go func() { - defer wg.Done() - + wg.Go(func() { resp := new(pbv1.Duty) _ = SendReceive(ctx, client, server.ID(), &pbv1.Duty{Slot: 1}, resp, pID) - }() + }) } gauge, err := inflightGauge.GetMetricWithLabelValues(string(pID), clientName) From 0d7f1ba5319d747a190238bcc318f8a59ae2b751 Mon Sep 17 00:00:00 2001 From: kalo <24719519+KaloyanTanev@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:34:22 +0200 Subject: [PATCH 29/29] app/log: silence revive package-naming in redact.go --- app/log/redact.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app/log/redact.go b/app/log/redact.go index 12a5beabf2..05860d500d 100644 --- a/app/log/redact.go +++ b/app/log/redact.go @@ -1,5 +1,6 @@ // Copyright © 2022-2026 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 +//nolint:revive,nolintlint // somehow the nolintlint linter catches revive as unnecessary, while it is package log import (